Skip to content

Commit 1503d8a

Browse files
committed
fix ChangeTree encoding order when sharing references to an instance.
1 parent 937bc7c commit 1503d8a

File tree

6 files changed

+100
-16
lines changed

6 files changed

+100
-16
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": "3.0.10",
3+
"version": "3.0.11",
44
"description": "Binary state serializer with delta encoding for games",
55
"bin": {
66
"schema-codegen": "./bin/schema-codegen",

src/encoder/ChangeTree.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function deleteOperationAtIndex(changeSet: ChangeSet, index: number) {
6262
delete changeSet.indexes[index];
6363
}
6464

65-
function enqueueChangeTree(
65+
export function enqueueChangeTree(
6666
root: Root,
6767
changeTree: ChangeTree,
6868
changeSet: 'changes' | 'filteredChanges' | 'allFilteredChanges',
@@ -384,6 +384,8 @@ export class ChangeTree<T extends Ref=any> {
384384
this.root?.remove(previousValue[$changes]);
385385
}
386386

387+
deleteOperationAtIndex(this.allChanges, allChangesIndex);
388+
387389
//
388390
// FIXME: this is looking a ugly and repeated
389391
//
@@ -392,7 +394,6 @@ export class ChangeTree<T extends Ref=any> {
392394
enqueueChangeTree(this.root, this, 'filteredChanges');
393395

394396
} else {
395-
deleteOperationAtIndex(this.allChanges, allChangesIndex);
396397
enqueueChangeTree(this.root, this, 'changes');
397398
}
398399
}

src/encoder/Root.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OPERATION } from "../encoding/spec";
22
import { TypeContext } from "../types/TypeContext";
33
import { spliceOne } from "../types/utils";
4-
import { ChangeTree, setOperationAtIndex } from "./ChangeTree";
4+
import { ChangeTree, enqueueChangeTree, setOperationAtIndex } from "./ChangeTree";
55

66
export class Root {
77
protected nextUniqueId: number = 0;
@@ -71,6 +71,23 @@ export class Root {
7171

7272
} else {
7373
this.refCount[changeTree.refId] = refCount;
74+
75+
//
76+
// When losing a reference to an instance, it is best to move the
77+
// ChangeTree to the end of the encoding queue.
78+
//
79+
// This way, at decoding time, the instance that contains the
80+
// ChangeTree will be available before the ChangeTree itself. If the
81+
// containing instance is not available, the Decoder will throw
82+
// "refId not found" error.
83+
//
84+
if (changeTree.filteredChanges !== undefined) {
85+
this.removeChangeFromChangeSet("filteredChanges", changeTree);
86+
enqueueChangeTree(this, changeTree, "filteredChanges");
87+
} else {
88+
this.removeChangeFromChangeSet("changes", changeTree);
89+
enqueueChangeTree(this, changeTree, "changes");
90+
}
7491
}
7592

7693
changeTree.forEachChild((child, _) => this.remove(child));
@@ -80,9 +97,8 @@ export class Root {
8097

8198
removeChangeFromChangeSet(changeSetName: "allChanges" | "changes" | "filteredChanges" | "allFilteredChanges", changeTree: ChangeTree) {
8299
const changeSet = this[changeSetName];
83-
const index = changeSet.indexOf(changeTree);
84-
if (index !== -1) {
85-
spliceOne(changeSet, index);
100+
if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
101+
changeTree[changeSetName].queueRootIndex = -1;
86102
// changeSet[index] = undefined;
87103
}
88104
}

test/InstanceSharing.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ describe("Instance sharing", () => {
460460

461461
});
462462

463-
xit("replacing collection of items while keeping a reference to an item", () => {
463+
it("replacing collection of items while keeping a reference to an item", () => {
464464
class Song extends Schema {
465465
@type("string") url: string;
466466
}
@@ -480,14 +480,10 @@ describe("Instance sharing", () => {
480480
const state = new State();
481481
const decodedState = new State();
482482

483-
console.log(">> encode()")
484483
decodedState.decode(state.encode());
485484

486485
state.buckets.set(sessionId, new Player());
487-
488-
console.log(">> encode()")
489486
decodedState.decode(state.encode());
490-
console.log(Schema.debugRefIds(state));
491487

492488
const newSong = new Song().assign({ url: "song2" });
493489
state.buckets.get(sessionId).queue.push(newSong);
@@ -498,8 +494,6 @@ describe("Instance sharing", () => {
498494
state.playing = state.buckets.get(sessionId).queue.shift();
499495
state.queue = new ArraySchema<Song>();
500496

501-
console.log(">> encode()")
502-
console.log(Schema.debugRefIds(state));
503497
decodedState.decode(state.encode());
504498
assert.deepStrictEqual(state.toJSON(), decodedState.toJSON());
505499
});

test/Schema.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function encodeMultiple<T extends Schema>(encoder: Encoder<T>, state: T,
134134
return encodedViews;
135135
}
136136

137-
export function assertEncodeAllMultiple<T extends Schema>(encoder: Encoder<T>, state: T, referenceClients: Array<{ state: Schema, view: StateView }>) {
137+
export function encodeAllMultiple<T extends Schema>(encoder: Encoder<T>, state: T, referenceClients: Array<{ state: Schema, view: StateView }>) {
138138
const clients = referenceClients.map((client) => createClientWithView(state, client.view));
139139

140140
const it = { offset: 0 };
@@ -157,6 +157,12 @@ export function assertEncodeAllMultiple<T extends Schema>(encoder: Encoder<T>, s
157157
return encoded;
158158
});
159159

160+
return { clients, encodedViews };
161+
}
162+
163+
export function assertEncodeAllMultiple<T extends Schema>(encoder: Encoder<T>, state: T, referenceClients: Array<{ state: Schema, view: StateView }>) {
164+
const { clients, encodedViews } = encodeAllMultiple(encoder, state, referenceClients);
165+
160166
referenceClients.forEach((referenceClient, i) => {
161167
assert.deepStrictEqual(referenceClient.state.toJSON(), clients[i].state.toJSON(), `client${i + 1} state mismatch`);
162168
});

test/StateView.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from "assert";
22
import { Schema, type, view, ArraySchema, MapSchema, StateView, Encoder, ChangeTree, $changes, OPERATION } from "../src";
3-
import { createClientWithView, encodeMultiple, assertEncodeAllMultiple, getDecoder, getEncoder, createInstanceFromReflection, encodeAllForView } from "./Schema";
3+
import { createClientWithView, encodeMultiple, assertEncodeAllMultiple, getDecoder, getEncoder, createInstanceFromReflection, encodeAllForView, encodeAllMultiple } from "./Schema";
44
import { getDecoderStateCallbacks } from "../src/decoder/strategy/StateCallbacks";
55
import { nanoid } from "nanoid";
66

@@ -841,6 +841,73 @@ describe("StateView", () => {
841841

842842
assertEncodeAllMultiple(encoder, state, [client1, client2])
843843
});
844+
845+
it("replacing collection of items while keeping a reference to an item", () => {
846+
class Song extends Schema {
847+
@type("string") url: string;
848+
}
849+
850+
class Player extends Schema {
851+
@type([Song]) queue = new ArraySchema<Song>();
852+
}
853+
854+
class State extends Schema {
855+
@type(Song) playing: Song = new Song();
856+
@view() @type([Song]) queue = new ArraySchema<Song>();
857+
@type({ map: Player }) buckets = new MapSchema<Player>();
858+
}
859+
860+
const sessionId = "session1";
861+
862+
const state = new State();
863+
const encoder = getEncoder(state);
864+
865+
const client1 = createClientWithView(state);
866+
867+
encodeMultiple(encoder, state, [client1]);
868+
869+
state.buckets.set(sessionId, new Player());
870+
871+
encodeMultiple(encoder, state, [client1]);
872+
console.log(Schema.debugRefIds(state));
873+
874+
const newSong = new Song().assign({ url: "song2" });
875+
state.buckets.get(sessionId).queue.push(newSong);
876+
877+
state.queue = new ArraySchema<Song>();
878+
state.queue.push(newSong);
879+
880+
state.playing = state.buckets.get(sessionId).queue.shift();
881+
state.queue = new ArraySchema<Song>();
882+
883+
encodeMultiple(encoder, state, [client1]);
884+
885+
assert.doesNotThrow(() =>
886+
encodeAllMultiple(encoder, state, [client1]));
887+
});
888+
889+
it("setting a non-view field to undefined should not interfere on encoding", () => {
890+
class Song extends Schema {
891+
@type("string") url: string;
892+
}
893+
894+
class State extends Schema {
895+
@type(Song) playing: Song = new Song();
896+
@view() @type([Song]) queue = new ArraySchema<Song>();
897+
}
898+
899+
const state = new State();
900+
const encoder = getEncoder(state);
901+
902+
const client1 = createClientWithView(state);
903+
encodeMultiple(encoder, state, [client1]);
904+
905+
state.playing = undefined;
906+
encodeMultiple(encoder, state, [client1]);
907+
908+
assert.doesNotThrow(() =>
909+
encodeAllMultiple(encoder, state, [client1]));
910+
});
844911
});
845912

846913
describe("checkIsFiltered", () => {

0 commit comments

Comments
 (0)