Skip to content

Commit 1c7c532

Browse files
committed
callbacks: fix registering callbacks when collection becomes available. colyseus/colyseus#915
1 parent ebaaec1 commit 1c7c532

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@colyseus/schema",
3-
"version": "4.0.12",
3+
"version": "4.0.13",
44
"description": "Binary state serializer with delta encoding for games",
55
"type": "module",
66
"bin": {

src/decoder/strategy/Callbacks.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,19 @@ export class StateCallbackStrategy<TState extends IRef> {
8686

8787
// Collection not available yet. Listen for its availability before attaching the handler.
8888
if (!collection || collection[$refId] === undefined) {
89-
removeHandler = this.addCallback(
89+
let removePropertyCallback: () => void;
90+
removePropertyCallback = this.addCallback(
9091
instance[$refId],
9192
propertyName,
9293
(value: TReturn, _: TReturn) => {
9394
if (value !== null && value !== undefined) {
95+
// Remove the property listener now that collection is available
96+
removePropertyCallback();
9497
removeHandler = this.addCallback(value[$refId], operation, handler);
9598
}
9699
}
97100
);
101+
removeHandler = removePropertyCallback;
98102
return removeOnAdd;
99103

100104
} else {

test/callbacks/Callbacks.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,62 @@ describe("Callbacks (new API)", () => {
238238
assert.deepStrictEqual(addedIndexes, [0, 1]);
239239
assert.deepStrictEqual(addedAmounts, [10, 20]);
240240
});
241+
242+
it("should not register onAdd callback twice when collection becomes available", () => {
243+
class Player extends Schema {
244+
@type("string") name: string;
245+
}
246+
247+
class State extends Schema {
248+
@type({ map: Player }) players: MapSchema<Player>;
249+
}
250+
251+
const state = new State();
252+
// Initially no players collection (undefined)
253+
254+
const decodedState = createInstanceFromReflection(state);
255+
const decoder = getDecoder(decodedState);
256+
const callbacks = Callbacks.get(decoder);
257+
258+
let addCount = 0;
259+
const addedNames: string[] = [];
260+
261+
// Register onAdd while collection is NOT available
262+
callbacks.onAdd(
263+
"players",
264+
(player, sessionId) => {
265+
addCount++;
266+
addedNames.push(player.name);
267+
}
268+
);
269+
270+
// First decode - no collection yet
271+
decodedState.decode(state.encode());
272+
assert.strictEqual(addCount, 0);
273+
274+
// Now set the collection with a player
275+
state.players = new MapSchema<Player>();
276+
state.players.set("one", new Player().assign({ name: "Alice" }));
277+
decodedState.decode(state.encodeAll());
278+
decodedState.decode(state.encode());
279+
280+
// Should be called exactly once for Alice
281+
assert.strictEqual(addCount, 1, `Expected addCount to be 1, but was ${addCount}. Names: ${addedNames.join(", ")}`);
282+
283+
// Add another player to the same collection
284+
state.players.set("two", new Player().assign({ name: "Bob" }));
285+
decodedState.decode(state.encode());
286+
287+
// Should be called once for Bob (total 2)
288+
assert.strictEqual(addCount, 2, `Expected addCount to be 2, but was ${addCount}. Names: ${addedNames.join(", ")}`);
289+
290+
// Add another player to the same collection
291+
state.players.set("three", new Player().assign({ name: "Charlie" }));
292+
decodedState.decode(state.encode());
293+
294+
// Should be called once for Charlie (total 3)
295+
assert.strictEqual(addCount, 3, `Expected addCount to be 3, but was ${addCount}. Names: ${addedNames.join(", ")}`);
296+
});
241297
});
242298

243299
describe("onChange", () => {

0 commit comments

Comments
 (0)