Skip to content

Commit 7b9ba63

Browse files
committed
skip structure on 'refId not found'. output warning will full refIds for debugging.
1 parent c1498c4 commit 7b9ba63

File tree

10 files changed

+123
-53
lines changed

10 files changed

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

src/Schema.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { StateView } from './encoder/StateView';
99

1010
import { encodeSchemaOperation } from './encoder/EncodeOperation';
1111
import { decodeSchemaOperation } from './decoder/DecodeOperation';
12+
13+
import type { Decoder } from './decoder/Decoder';
1214
import type { Metadata } from './Metadata';
1315
import { getIndent } from './utils';
1416

@@ -183,25 +185,30 @@ export class Schema {
183185
* @param showContents display JSON contents of the instance
184186
* @returns
185187
*/
186-
static debugRefIds(ref: Ref, showContents: boolean = false, level: number = 0) {
188+
static debugRefIds(ref: Ref, showContents: boolean = false, level: number = 0, decoder?: Decoder) {
187189
const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
188-
189190
const changeTree: ChangeTree = ref[$changes];
190-
const refId = changeTree.refId;
191191

192-
// log reference count if > 1
193-
const refCount = (changeTree.root?.refCount?.[refId] > 1)
194-
? ` [×${changeTree.root.refCount[refId]}]`
192+
const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
193+
const root = (decoder) ? decoder.root : changeTree.root;
194+
195+
// log reference count if > 1
196+
const refCount = (root?.refCount?.[refId] > 1)
197+
? ` [×${root.refCount[refId]}]`
195198
: '';
196199

197200
let output = `${getIndent(level)}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
198201

199202
changeTree.forEachChild((childChangeTree) =>
200-
output += this.debugRefIds(childChangeTree.ref, showContents, level + 1));
203+
output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder));
201204

202205
return output;
203206
}
204207

208+
static debugRefIdsDecoder(decoder: Decoder) {
209+
return this.debugRefIds(decoder.state, false, 0, decoder);
210+
}
211+
205212
/**
206213
* Return a string representation of the changes on a Schema instance.
207214
* The list of changes is cleared after each encode.

src/decoder/Decoder.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,23 @@ export class Decoder<T extends Schema = any> {
5858
if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
5959
it.offset++;
6060

61-
this.currentRefId = decode.number(bytes, it);
62-
const nextRef = $root.refs.get(this.currentRefId) as Schema;
61+
const nextRefId = decode.number(bytes, it);
62+
const nextRef = $root.refs.get(nextRefId);
6363

6464
//
6565
// Trying to access a reference that haven't been decoded yet.
6666
//
67-
if (!nextRef) { throw new Error(`"refId" not found: ${this.currentRefId}`); }
67+
if (!nextRef) {
68+
console.error(`"refId" not found: ${nextRefId}`, { previousRef: this, previousRefId: this.currentRefId });
69+
console.warn("Please report this to the developers. All refIds =>");
70+
console.warn(Schema.debugRefIdsDecoder(this));
71+
this.skipCurrentStructure(bytes, it, totalBytes);
72+
}
6873
ref[$onDecodeEnd]?.()
69-
ref = nextRef;
7074

75+
this.currentRefId = nextRefId;
76+
77+
ref = nextRef;
7178
decoder = ref.constructor[$decoder];
7279

7380
continue;
@@ -77,22 +84,7 @@ export class Decoder<T extends Schema = any> {
7784

7885
if (result === DEFINITION_MISMATCH) {
7986
console.warn("@colyseus/schema: definition mismatch");
80-
81-
//
82-
// keep skipping next bytes until reaches a known structure
83-
// by local decoder.
84-
//
85-
const nextIterator: Iterator = { offset: it.offset };
86-
while (it.offset < totalBytes) {
87-
if (bytes[it.offset] === SWITCH_TO_STRUCTURE) {
88-
nextIterator.offset = it.offset + 1;
89-
if ($root.refs.has(decode.number(bytes, nextIterator))) {
90-
break;
91-
}
92-
}
93-
94-
it.offset++;
95-
}
87+
this.skipCurrentStructure(bytes, it, totalBytes);
9688
continue;
9789
}
9890
}
@@ -109,6 +101,23 @@ export class Decoder<T extends Schema = any> {
109101
return allChanges;
110102
}
111103

104+
skipCurrentStructure(bytes: Buffer, it: Iterator, totalBytes: number) {
105+
//
106+
// keep skipping next bytes until reaches a known structure
107+
// by local decoder.
108+
//
109+
const nextIterator: Iterator = { offset: it.offset };
110+
while (it.offset < totalBytes) {
111+
if (bytes[it.offset] === SWITCH_TO_STRUCTURE) {
112+
nextIterator.offset = it.offset + 1;
113+
if (this.root.refs.has(decode.number(bytes, nextIterator))) {
114+
break;
115+
}
116+
}
117+
it.offset++;
118+
}
119+
}
120+
112121
getInstanceType(bytes: Buffer, it: Iterator, defaultType: typeof Schema): typeof Schema {
113122
let type: typeof Schema;
114123

src/decoder/ReferenceTracker.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class ReferenceTracker {
2626
public refs = new Map<number, Ref>();
2727
public refIds = new WeakMap<Ref, number>();
2828

29-
public refCounts: { [refId: number]: number; } = {};
29+
public refCount: { [refId: number]: number; } = {};
3030
public deletedRefs = new Set<number>();
3131

3232
public callbacks: { [refId: number]: SchemaCallbacks } = {};
@@ -42,7 +42,7 @@ export class ReferenceTracker {
4242
this.refIds.set(ref, refId);
4343

4444
if (incrementCount) {
45-
this.refCounts[refId] = (this.refCounts[refId] || 0) + 1;
45+
this.refCount[refId] = (this.refCount[refId] || 0) + 1;
4646
}
4747

4848
if (this.deletedRefs.has(refId)) {
@@ -52,7 +52,7 @@ export class ReferenceTracker {
5252

5353
// for decoding
5454
removeRef(refId: number) {
55-
const refCount = this.refCounts[refId];
55+
const refCount = this.refCount[refId];
5656

5757
if (refCount === undefined) {
5858
try {
@@ -73,7 +73,7 @@ export class ReferenceTracker {
7373
return;
7474
}
7575

76-
if ((this.refCounts[refId] = refCount - 1) <= 0) {
76+
if ((this.refCount[refId] = refCount - 1) <= 0) {
7777
this.deletedRefs.add(refId);
7878
}
7979
}
@@ -82,7 +82,7 @@ export class ReferenceTracker {
8282
this.refs.clear();
8383
this.deletedRefs.clear();
8484
this.callbacks = {};
85-
this.refCounts = {};
85+
this.refCount = {};
8686
}
8787

8888
// for decoding
@@ -91,7 +91,7 @@ export class ReferenceTracker {
9191
//
9292
// Skip active references.
9393
//
94-
if (this.refCounts[refId] > 0) { return; }
94+
if (this.refCount[refId] > 0) { return; }
9595

9696
const ref = this.refs.get(refId);
9797

@@ -121,7 +121,7 @@ export class ReferenceTracker {
121121
}
122122

123123
this.refs.delete(refId); // remove ref
124-
delete this.refCounts[refId]; // remove ref count
124+
delete this.refCount[refId]; // remove ref count
125125
delete this.callbacks[refId]; // remove callbacks
126126
});
127127

test/ArraySchema.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,7 +2170,7 @@ describe("ArraySchema Tests", () => {
21702170

21712171
assert.strictEqual(6, onRemoveCount);
21722172

2173-
const refCounts = getDecoder(decodedState).root.refCounts;
2173+
const refCounts = getDecoder(decodedState).root.refCount;
21742174
assert.deepStrictEqual(refCounts, {
21752175
0: 1,
21762176
2: 1
@@ -2233,7 +2233,7 @@ describe("ArraySchema Tests", () => {
22332233
assert.strictEqual(state.cards.length, decodedState.cards.length);
22342234

22352235
const $root = getDecoder(decodedState).root;
2236-
const refCounts = $root.refCounts;
2236+
const refCounts = $root.refCount;
22372237

22382238
assert.strictEqual($root.refs.size, 7, "should have 7 refs");
22392239
assert.strictEqual(refCounts[0], 1, JSON.stringify($root.refs.get(0).toJSON()));
@@ -2326,7 +2326,7 @@ describe("ArraySchema Tests", () => {
23262326

23272327
console.log(Schema.debugRefIds(state));
23282328

2329-
assert.deepStrictEqual(decoder.root.refCounts, {
2329+
assert.deepStrictEqual(decoder.root.refCount, {
23302330
0: 1,
23312331
1: 1,
23322332
2: 1,
@@ -2358,7 +2358,7 @@ describe("ArraySchema Tests", () => {
23582358

23592359
decodedState.decode(state.encode());
23602360

2361-
const refCounts = getDecoder(decodedState).root.refCounts;
2361+
const refCounts = getDecoder(decodedState).root.refCount;
23622362
assert.deepStrictEqual(refCounts, {
23632363
0: 1,
23642364
1: 1,
@@ -2411,7 +2411,7 @@ describe("ArraySchema Tests", () => {
24112411

24122412
assertDeepStrictEqualEncodeAll(state);
24132413

2414-
const refCounts = getDecoder(decodedState).root.refCounts;
2414+
const refCounts = getDecoder(decodedState).root.refCount;
24152415
assert.deepStrictEqual(refCounts, {
24162416
0: 1,
24172417
1: 1,

test/ChangeTree.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ describe("ChangeTree", () => {
217217
assert.strictEqual(1, encoder.root.refCount[entity2[$changes].refId]);
218218
assert.strictEqual(0, encoder.root.refCount[entity1[$changes].refId]);
219219

220-
assert.strictEqual(1, decoder.root.refCounts[entity2[$changes].refId]);
221-
assert.strictEqual(undefined, decoder.root.refCounts[entity1[$changes].refId]);
220+
assert.strictEqual(1, decoder.root.refCount[entity2[$changes].refId]);
221+
assert.strictEqual(undefined, decoder.root.refCount[entity1[$changes].refId]);
222222

223223
assertDeepStrictEqualEncodeAll(state);
224224
})
@@ -248,8 +248,8 @@ describe("ChangeTree", () => {
248248
assert.strictEqual(1, encoder.root.refCount[entity2[$changes].refId]);
249249
assert.strictEqual(0, encoder.root.refCount[entity1[$changes].refId]);
250250

251-
assert.strictEqual(1, decoder.root.refCounts[entity2[$changes].refId]);
252-
assert.strictEqual(undefined, decoder.root.refCounts[entity1[$changes].refId]);
251+
assert.strictEqual(1, decoder.root.refCount[entity2[$changes].refId]);
252+
assert.strictEqual(undefined, decoder.root.refCount[entity1[$changes].refId]);
253253

254254
assertDeepStrictEqualEncodeAll(state);
255255

@@ -281,8 +281,8 @@ describe("ChangeTree", () => {
281281
assert.strictEqual(1, encoder.root.refCount[entity2[$changes].refId]);
282282
assert.strictEqual(0, encoder.root.refCount[entity1[$changes].refId]);
283283

284-
assert.strictEqual(1, decoder.root.refCounts[entity2[$changes].refId]);
285-
assert.strictEqual(undefined, decoder.root.refCounts[entity1[$changes].refId]);
284+
assert.strictEqual(1, decoder.root.refCount[entity2[$changes].refId]);
285+
assert.strictEqual(undefined, decoder.root.refCount[entity1[$changes].refId]);
286286

287287
assertDeepStrictEqualEncodeAll(state);
288288
});

test/InstanceSharing.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ describe("Instance sharing", () => {
104104
decodedState.decode(state.encode());
105105

106106
assert.strictEqual(Object.keys(encoder.root.refCount).length, 5);
107-
for (let refId in decoder.root.refCounts) {
108-
assert.strictEqual(decoder.root.refCounts[refId], encoder.root.refCount[refId]);
107+
for (let refId in decoder.root.refCount) {
108+
assert.strictEqual(decoder.root.refCount[refId], encoder.root.refCount[refId]);
109109
}
110110
assertRefIdCounts(state, decodedState);
111111

@@ -284,7 +284,7 @@ describe("Instance sharing", () => {
284284
assert.strictEqual(numRefs, decoder.root.refs.size, "should've dropped reference to previous ArraySchema");
285285
assert.strictEqual(
286286
true,
287-
Object.values(decoder.root.refCounts).every(refCount => refCount > 0),
287+
Object.values(decoder.root.refCount).every(refCount => refCount > 0),
288288
"all refCount's should have a valid number."
289289
);
290290

@@ -365,7 +365,7 @@ describe("Instance sharing", () => {
365365
item.x = 999;
366366

367367
decodedState.decode(state.encode());
368-
assert.strictEqual(1, decoder.root.refCounts[item[$changes].refId]);
368+
assert.strictEqual(1, decoder.root.refCount[item[$changes].refId]);
369369

370370
assert.strictEqual(999, decodedState.player.item.x);
371371

test/Schema.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,42 @@ describe("Type: Schema", () => {
381381
const decodedState = createInstanceFromReflection(state);
382382
decodedState.decode(state.encode());
383383
assert.strictEqual(state.timestamp, decodedState.timestamp);
384-
})
384+
});
385+
386+
it("xxxxxx", () => {
387+
class Player extends Schema {
388+
@type("number") health: number = 100;
389+
}
390+
391+
class GameState extends Schema {
392+
@type({ map: Player }) players = new MapSchema<Player>();
393+
@type("number") round: number = 1;
394+
}
395+
396+
function generateId() {
397+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
398+
}
399+
400+
const state = new GameState();
401+
const encoder = getEncoder(state);
402+
403+
function simulateJoin(id = generateId()) {
404+
const player = new Player();
405+
state.players.set(id, player);
406+
407+
const decoded = createInstanceFromReflection(state);
408+
decoded.decode(state.encodeAll());
409+
410+
decoded.decode(state.encode());
411+
state.players.delete(id);
412+
}
413+
414+
for (let i = 0; i < 3; i++) {
415+
simulateJoin();
416+
}
417+
418+
});
419+
385420
});
386421

387422
describe("detecting changes", () => {

test/Schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function assertRefIdCounts(source: Schema, target: Schema) {
5252
for (const refId in encoder.root.refCount) {
5353
const ref = encoder.root.changeTrees[refId]?.ref;
5454
const encoderRefCount = encoder.root.refCount[refId];
55-
const decoderRefCount = decoder.root.refCounts[refId] ?? 0;
55+
const decoderRefCount = decoder.root.refCount[refId] ?? 0;
5656
assert.strictEqual(encoderRefCount, decoderRefCount, `refCount mismatch for '${ref?.constructor.name}' (refId: ${refId}) => (Encoder count: ${encoderRefCount}, Decoder count: ${decoderRefCount})
5757
\n${Schema.debugRefIds(source)}`);
5858
}

test/Utils.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from "assert";
22

3-
import { State, Player, getEncoder } from "./Schema";
3+
import { State, Player, getEncoder, createInstanceFromReflection, getDecoder } from "./Schema";
44
import { MapSchema, dumpChanges, ArraySchema, Schema } from "../src";
55

66
describe("Utils Test", () => {
@@ -70,4 +70,23 @@ describe("Utils Test", () => {
7070

7171
});
7272

73+
describe("debugRefIds", () => {
74+
75+
it("should be able to debug Decoder refIds", () => {
76+
const state = new State();
77+
state.arrayOfPlayers = new ArraySchema<Player>();
78+
state.arrayOfPlayers.push(new Player("One", 1, 1));
79+
state.arrayOfPlayers.push(new Player("Two", 2, 2));
80+
81+
const decoded = createInstanceFromReflection(state);
82+
decoded.decode(state.encode());
83+
84+
const extractRefIds = (debugRefIds: string) =>
85+
Array.from(debugRefIds.matchAll(/\(refId: ([0-9]+)\)/g)).map(entry => entry[0]);
86+
87+
assert.deepStrictEqual(extractRefIds(Schema.debugRefIds(state)), extractRefIds(Schema.debugRefIdsDecoder(getDecoder(decoded))))
88+
});
89+
90+
});
91+
7392
});

0 commit comments

Comments
 (0)