@colyseus/schema 4.0.20 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +55 -2
  3. package/build/Reflection.d.ts +24 -30
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +56 -13
  6. package/build/codegen/cli.cjs +84 -67
  7. package/build/codegen/cli.cjs.map +1 -1
  8. package/build/decoder/DecodeOperation.d.ts +48 -5
  9. package/build/decoder/Decoder.d.ts +2 -2
  10. package/build/decoder/strategy/Callbacks.d.ts +1 -1
  11. package/build/encoder/ChangeRecorder.d.ts +107 -0
  12. package/build/encoder/ChangeTree.d.ts +218 -69
  13. package/build/encoder/EncodeDescriptor.d.ts +63 -0
  14. package/build/encoder/EncodeOperation.d.ts +25 -2
  15. package/build/encoder/Encoder.d.ts +59 -3
  16. package/build/encoder/MapJournal.d.ts +62 -0
  17. package/build/encoder/RefIdAllocator.d.ts +35 -0
  18. package/build/encoder/Root.d.ts +94 -13
  19. package/build/encoder/StateView.d.ts +116 -8
  20. package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
  21. package/build/encoder/changeTree/liveIteration.d.ts +3 -0
  22. package/build/encoder/changeTree/parentChain.d.ts +24 -0
  23. package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
  24. package/build/encoder/streaming.d.ts +73 -0
  25. package/build/encoder/subscriptions.d.ts +25 -0
  26. package/build/index.cjs +5202 -1552
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5202 -1552
  30. package/build/index.mjs +5193 -1552
  31. package/build/index.mjs.map +1 -1
  32. package/build/input/InputDecoder.d.ts +32 -0
  33. package/build/input/InputEncoder.d.ts +117 -0
  34. package/build/input/index.cjs +7429 -0
  35. package/build/input/index.cjs.map +1 -0
  36. package/build/input/index.d.ts +3 -0
  37. package/build/input/index.mjs +7426 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +22 -8
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +162 -0
  42. package/build/types/custom/ArraySchema.d.ts +25 -4
  43. package/build/types/custom/CollectionSchema.d.ts +30 -2
  44. package/build/types/custom/MapSchema.d.ts +52 -3
  45. package/build/types/custom/SetSchema.d.ts +32 -2
  46. package/build/types/custom/StreamSchema.d.ts +114 -0
  47. package/build/types/symbols.d.ts +48 -5
  48. package/package.json +9 -3
  49. package/src/Metadata.ts +258 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +308 -236
  53. package/src/bench_bloat.ts +173 -0
  54. package/src/bench_decode.ts +221 -0
  55. package/src/bench_decode_mem.ts +165 -0
  56. package/src/bench_encode.ts +108 -0
  57. package/src/bench_init.ts +150 -0
  58. package/src/bench_static.ts +109 -0
  59. package/src/bench_stream.ts +295 -0
  60. package/src/bench_view_cmp.ts +142 -0
  61. package/src/codegen/languages/csharp.ts +0 -24
  62. package/src/codegen/parser.ts +83 -61
  63. package/src/decoder/DecodeOperation.ts +168 -63
  64. package/src/decoder/Decoder.ts +20 -10
  65. package/src/decoder/ReferenceTracker.ts +4 -0
  66. package/src/decoder/strategy/Callbacks.ts +30 -26
  67. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  68. package/src/encoder/ChangeRecorder.ts +276 -0
  69. package/src/encoder/ChangeTree.ts +674 -519
  70. package/src/encoder/EncodeDescriptor.ts +213 -0
  71. package/src/encoder/EncodeOperation.ts +107 -65
  72. package/src/encoder/Encoder.ts +630 -119
  73. package/src/encoder/MapJournal.ts +124 -0
  74. package/src/encoder/RefIdAllocator.ts +68 -0
  75. package/src/encoder/Root.ts +247 -120
  76. package/src/encoder/StateView.ts +592 -121
  77. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  78. package/src/encoder/changeTree/liveIteration.ts +74 -0
  79. package/src/encoder/changeTree/parentChain.ts +131 -0
  80. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  81. package/src/encoder/streaming.ts +232 -0
  82. package/src/encoder/subscriptions.ts +71 -0
  83. package/src/index.ts +15 -3
  84. package/src/input/InputDecoder.ts +57 -0
  85. package/src/input/InputEncoder.ts +303 -0
  86. package/src/input/index.ts +3 -0
  87. package/src/types/HelperTypes.ts +21 -9
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +285 -0
  90. package/src/types/custom/ArraySchema.ts +210 -197
  91. package/src/types/custom/CollectionSchema.ts +115 -35
  92. package/src/types/custom/MapSchema.ts +162 -58
  93. package/src/types/custom/SetSchema.ts +128 -39
  94. package/src/types/custom/StreamSchema.ts +310 -0
  95. package/src/types/symbols.ts +54 -6
  96. package/src/utils.ts +4 -6
package/src/Schema.ts CHANGED
@@ -3,8 +3,8 @@ import { DEFAULT_VIEW_TAG, type DefinitionType } from "./annotations.js";
3
3
 
4
4
  import { AssignableProps, NonFunctionPropNames, ToJSON } from './types/HelperTypes.js';
5
5
 
6
- import { ChangeSet, ChangeSetName, ChangeTree, IRef, Ref } from './encoder/ChangeTree.js';
7
- import { $changes, $decoder, $deleteByIndex, $descriptors, $encoder, $filter, $getByIndex, $refId, $track } from './types/symbols.js';
6
+ import { ChangeTree, installUntrackedChangeTree, IRef, Ref } from './encoder/ChangeTree.js';
7
+ import { $changes, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $numFields, $refId, $track, $values } from './types/symbols.js';
8
8
  import { StateView } from './encoder/StateView.js';
9
9
 
10
10
  import { encodeSchemaOperation } from './encoder/EncodeOperation.js';
@@ -23,28 +23,71 @@ export class Schema<C = any> implements IRef {
23
23
  static [$decoder] = decodeSchemaOperation;
24
24
 
25
25
  [$refId]?: number;
26
+ [$values]: any[];
26
27
 
27
28
  /**
28
- * Assign the property descriptors required to track changes on this instance.
29
- * @param instance
29
+ * Initialize change tracking on this instance.
30
+ * Field accessor descriptors (getter/setter) live on the prototype,
31
+ * installed once at class-definition time. Per-instance work is limited
32
+ * to allocating a ChangeTree and a values array.
30
33
  */
31
34
  static initialize(instance: any) {
35
+ // $changes MUST be non-enumerable: tests use assert.deepStrictEqual on
36
+ // Schema instances (e.g. arrayOfPlayers.toArray()), which walks
37
+ // enumerable own Symbol properties. ChangeTree has circular refs
38
+ // (root → changeTrees → other ChangeTrees), so a visible $changes
39
+ // would send deepStrictEqual into exponential recursion. Plain
40
+ // assignment of a Symbol key would be enumerable: true — hence we
41
+ // keep defineProperty here.
32
42
  Object.defineProperty(instance, $changes, {
33
43
  value: new ChangeTree(instance),
34
44
  enumerable: false,
35
45
  writable: true
36
46
  });
47
+ instance[$values] = [];
48
+ }
37
49
 
38
- Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
50
+ /**
51
+ * Decoder-side factory. Skips the user subclass ctor entirely —
52
+ * decoder-built instances are passive mirrors of server state, so any
53
+ * field initializer / ctor body work would be overwritten by the
54
+ * decoded ADDs immediately after. Assignment order matches
55
+ * {@link Schema.initialize} so V8 assigns the same hidden class
56
+ * ($changes, then $values), keeping decode-path ICs monomorphic even
57
+ * when tracked and untracked instances coexist.
58
+ *
59
+ * The `this:` constraint pins the return type to the concrete subclass
60
+ * when called as `Player.initializeForDecoder()`, not the base Schema.
61
+ */
62
+ static initializeForDecoder<T extends Schema = Schema>(this: { prototype: T } & typeof Schema): T {
63
+ const inst: any = Object.create(this.prototype);
64
+ installUntrackedChangeTree(inst);
65
+ inst[$values] = [];
66
+ return inst;
39
67
  }
40
68
 
69
+ /**
70
+ * Check whether `type` describes a Schema *class* (a subclass
71
+ * constructor carrying `Symbol.metadata`, as installed by `@type`).
72
+ * Returns false for primitive type strings like `"number"`, descriptor
73
+ * objects like `{ map: Player }`, and Schema *instances*.
74
+ *
75
+ * For the instance-level check — "is this value a Schema instance?" —
76
+ * see {@link Schema.isSchema}.
77
+ */
41
78
  static is(type: DefinitionType) {
42
79
  return typeof((type as typeof Schema)[Symbol.metadata]) === "object";
43
80
  }
44
81
 
45
82
  /**
46
- * Check if a value is an instance of Schema.
47
- * This method uses duck-typing to avoid issues with multiple @colyseus/schema versions.
83
+ * Check if a value is an *instance* of Schema. Uses duck-typing on
84
+ * `.assign` to work across multiple `@colyseus/schema` versions that
85
+ * may be loaded in the same process (e.g. bundled server types vs.
86
+ * client types in a p2p setup).
87
+ *
88
+ * For the class-level check — "is this type a Schema subclass?" —
89
+ * see {@link Schema.is}.
90
+ *
48
91
  * @param obj Value to check
49
92
  * @returns true if the value is a Schema instance
50
93
  */
@@ -53,7 +96,11 @@ export class Schema<C = any> implements IRef {
53
96
  }
54
97
 
55
98
  /**
56
- * Track property changes
99
+ * Track property changes. Exposed as an override point so downstream
100
+ * tools (debuggers, transparent proxies, custom instrumentation) can
101
+ * intercept per-field writes. Hot-path code in `annotations.ts` calls
102
+ * `(this.constructor as typeof Schema)[$track](...)` rather than
103
+ * `changeTree.change(...)` directly so any subclass override wins.
57
104
  */
58
105
  static [$track] (changeTree: ChangeTree, index: number, operation: OPERATION = OPERATION.ADD) {
59
106
  changeTree.change(index, operation);
@@ -86,24 +133,15 @@ export class Schema<C = any> implements IRef {
86
133
 
87
134
  } else {
88
135
  // view pass: custom tag
89
- const tags = view.tags?.get(ref[$changes]);
90
- return tags && tags.has(tag);
136
+ return view.hasTagOnTree(ref[$changes], tag);
91
137
  }
92
138
  }
93
139
 
94
140
  // allow inherited classes to have a constructor
95
141
  constructor(arg?: C) {
96
- //
97
- // inline
98
- // Schema.initialize(this);
99
- //
100
142
  Schema.initialize(this);
101
-
102
- //
103
- // Assign initial values
104
- //
105
143
  if (arg) {
106
- Object.assign(this, arg);
144
+ Schema.assignProps(this, arg);
107
145
  }
108
146
  }
109
147
 
@@ -113,10 +151,36 @@ export class Schema<C = any> implements IRef {
113
151
  * @returns
114
152
  */
115
153
  public assign<T extends Partial<this>>(props: AssignableProps<T>,): this {
116
- Object.assign(this, props);
154
+ Schema.assignProps(this, props);
117
155
  return this;
118
156
  }
119
157
 
158
+ /**
159
+ * Metadata-driven property assignment.
160
+ * Reads tracked fields via property access (works with prototype accessors),
161
+ * then copies any remaining own properties for non-tracked fields.
162
+ */
163
+ protected static assignProps(target: any, source: any) {
164
+ const metadata: Metadata = target.constructor[Symbol.metadata];
165
+ if (metadata && metadata[$numFields] !== undefined) {
166
+ for (let i = 0; i <= metadata[$numFields]; i++) {
167
+ const field = metadata[i];
168
+ if (!field) { continue; }
169
+ const value = source[field.name];
170
+ if (value !== undefined) {
171
+ target[field.name] = value;
172
+ }
173
+ }
174
+ }
175
+ // Copy non-tracked own properties (e.g. `notSynched: true`).
176
+ const keys = Object.keys(source);
177
+ for (let i = 0; i < keys.length; i++) {
178
+ const key = keys[i];
179
+ if (metadata && metadata[key] !== undefined) { continue; }
180
+ target[key] = source[key];
181
+ }
182
+ }
183
+
120
184
  /**
121
185
  * Restore the instance from JSON data.
122
186
  * @param jsonData JSON data to restore the instance from
@@ -194,6 +258,50 @@ export class Schema<C = any> implements IRef {
194
258
  );
195
259
  }
196
260
 
261
+ // ────────────────────────────────────────────────────────────────────
262
+ // Change-tracking control API
263
+ //
264
+ // By default, every mutation to a @type() property is automatically
265
+ // recorded as a change. These methods let you opt out for bulk-load
266
+ // scenarios or custom batching.
267
+ //
268
+ // @example
269
+ // // Bulk-load without emitting changes:
270
+ // player.untracked(() => {
271
+ // player.hp = 100;
272
+ // player.name = "alice";
273
+ // });
274
+ //
275
+ // // Pause / resume pattern:
276
+ // player.pauseTracking();
277
+ // player.hp = 100; // not tracked
278
+ // player.resumeTracking();
279
+ // player.hp = 50; // tracked
280
+ // ────────────────────────────────────────────────────────────────────
281
+
282
+ /** Stop recording mutations until resumeTracking() is called. */
283
+ public pauseTracking(): void {
284
+ this[$changes].pause();
285
+ }
286
+
287
+ /** Re-enable automatic change tracking. */
288
+ public resumeTracking(): void {
289
+ this[$changes].resume();
290
+ }
291
+
292
+ /**
293
+ * Run `fn` with change tracking paused, then resume.
294
+ * Returns the function's return value. Safe to nest.
295
+ */
296
+ public untracked<T>(fn: () => T): T {
297
+ return this[$changes].untracked(fn);
298
+ }
299
+
300
+ /** True while tracking is paused. */
301
+ public get isTrackingPaused(): boolean {
302
+ return this[$changes].paused;
303
+ }
304
+
197
305
  clone (): this {
198
306
  // Create instance without calling custom constructor
199
307
  const cloned = Object.create(this.constructor.prototype);
@@ -291,15 +399,45 @@ export class Schema<C = any> implements IRef {
291
399
  return output;
292
400
  }
293
401
 
294
- static debugRefIdEncodingOrder<T extends Ref>(ref: T, changeSet: ChangeSetName = 'allChanges') {
295
- let encodeOrder: number[] = [];
296
- let current = ref[$changes].root[changeSet].next;
297
- while (current) {
298
- if (current.changeTree) {
299
- encodeOrder.push(current.changeTree.ref[$refId]);
402
+ /**
403
+ * @param changeSet
404
+ * - "changes": iterate the current-tick dirty queue (per-tick encode order)
405
+ * - "allChanges" / "allFilteredChanges" (legacy): structurally walk the
406
+ * tree in DFS preorder (matches the order in which full-sync emits
407
+ * trees). The two legacy modes differ by which side of the filter
408
+ * split they include.
409
+ */
410
+ static debugRefIdEncodingOrder<T extends Ref>(
411
+ ref: T,
412
+ changeSet: "changes" | "allChanges" | "allFilteredChanges" = 'allChanges'
413
+ ) {
414
+ const encodeOrder: number[] = [];
415
+ const rootChangeTree = ref[$changes];
416
+
417
+ if (changeSet === "changes") {
418
+ let current = rootChangeTree.root.changes?.next;
419
+ while (current) {
420
+ if (current.changeTree) {
421
+ encodeOrder.push(current.changeTree.ref[$refId]);
422
+ }
423
+ current = current.next;
300
424
  }
301
- current = current.next;
425
+ return encodeOrder;
302
426
  }
427
+
428
+ // Full-sync modes: DFS preorder from root, filtered by tree's
429
+ // filter-status to match the unfiltered / filtered split.
430
+ const wantFiltered = (changeSet === "allFilteredChanges");
431
+ const visited = new Set<ChangeTree>();
432
+ const walk = (changeTree: ChangeTree) => {
433
+ if (visited.has(changeTree)) return;
434
+ visited.add(changeTree);
435
+ if (changeTree.isFiltered === wantFiltered) {
436
+ encodeOrder.push(changeTree.ref[$refId]);
437
+ }
438
+ changeTree.forEachChild((child, _) => walk(child));
439
+ };
440
+ walk(rootChangeTree);
303
441
  return encodeOrder;
304
442
  }
305
443
 
@@ -317,118 +455,22 @@ export class Schema<C = any> implements IRef {
317
455
  */
318
456
  static debugChanges<T extends Ref>(instance: T, isEncodeAll: boolean = false) {
319
457
  const changeTree: ChangeTree = instance[$changes];
458
+ const label = isEncodeAll ? "allChanges" : "changes";
459
+ let output = `${instance.constructor.name} (${instance[$refId]}) -> .${label}:\n`;
320
460
 
321
- const changeSet = (isEncodeAll) ? changeTree.allChanges : changeTree.changes;
322
- const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
323
-
324
- let output = `${instance.constructor.name} (${instance[$refId]}) -> .${changeSetName}:\n`;
325
-
326
- function dumpChangeSet(changeSet: ChangeSet) {
327
- changeSet.operations
328
- .filter(op => op)
329
- .forEach((index) => {
330
- const operation = changeTree.indexedOperations[index];
331
- output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`
332
- });
333
- }
334
-
335
- dumpChangeSet(changeSet);
336
-
337
- // display filtered changes
338
- if (
339
- !isEncodeAll &&
340
- changeTree.filteredChanges &&
341
- (changeTree.filteredChanges.operations).filter(op => op).length > 0
342
- ) {
343
- output += `${instance.constructor.name} (${instance[$refId]}) -> .filteredChanges:\n`;
344
- dumpChangeSet(changeTree.filteredChanges);
345
- }
346
-
347
- // display filtered changes
348
- if (
349
- isEncodeAll &&
350
- changeTree.allFilteredChanges &&
351
- (changeTree.allFilteredChanges.operations).filter(op => op).length > 0
352
- ) {
353
- output += `${instance.constructor.name} (${instance[$refId]}) -> .allFilteredChanges:\n`;
354
- dumpChangeSet(changeTree.allFilteredChanges);
355
- }
356
-
357
- return output;
358
- }
359
-
360
- static debugChangesDeep<T extends Schema>(ref: T, changeSetName: "changes" | "allChanges" | "allFilteredChanges" | "filteredChanges" = "changes") {
361
- let output = "";
362
-
363
- const rootChangeTree: ChangeTree = ref[$changes];
364
- const root = rootChangeTree.root;
365
- const changeTrees: Map<ChangeTree, ChangeTree[]> = new Map();
366
-
367
- const instanceRefIds = [];
368
- let totalOperations = 0;
369
-
370
- // TODO: FIXME: this method is not working as expected
371
- for (const [refId, changes] of Object.entries(root[changeSetName])) {
372
- const changeTree = root.changeTrees[refId as any as number];
373
- if (!changeTree) { continue; }
374
-
375
- let includeChangeTree = false;
376
- let parentChangeTrees: ChangeTree[] = [];
377
- let parentChangeTree = changeTree.parent?.[$changes];
378
-
379
- if (changeTree === rootChangeTree) {
380
- includeChangeTree = true;
381
-
382
- } else {
383
- while (parentChangeTree !== undefined) {
384
- parentChangeTrees.push(parentChangeTree);
385
- if (parentChangeTree.ref === ref) {
386
- includeChangeTree = true;
387
- break;
388
- }
389
- parentChangeTree = parentChangeTree.parent?.[$changes];
390
- }
391
- }
392
-
393
- if (includeChangeTree) {
394
- instanceRefIds.push(changeTree.ref[$refId]);
395
- totalOperations += Object.keys(changes).length;
396
- changeTrees.set(changeTree, parentChangeTrees.reverse());
397
- }
398
- }
399
-
400
- output += "---\n"
401
- output += `root refId: ${rootChangeTree.ref[$refId]}\n`;
402
- output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
403
- output += `Total changes: ${totalOperations}\n`;
404
- output += "---\n"
405
-
406
- // based on root.changes, display a tree of changes that has the "ref" instance as parent
407
- const visitedParents = new WeakSet<ChangeTree>();
408
- for (const [changeTree, parentChangeTrees] of changeTrees.entries()) {
409
- parentChangeTrees.forEach((parentChangeTree, level) => {
410
- if (!visitedParents.has(parentChangeTree)) {
411
- output += `${getIndent(level)}${parentChangeTree.ref.constructor.name} (refId: ${parentChangeTree.ref[$refId]})\n`;
412
- visitedParents.add(parentChangeTree);
413
- }
461
+ if (isEncodeAll) {
462
+ changeTree.forEachLive((index) => {
463
+ output += `- [${index}]: ADD (${JSON.stringify(changeTree.getValue(Number(index), true))})\n`;
464
+ });
465
+ } else {
466
+ changeTree.forEach((index, op) => {
467
+ if (index < 0 || !op) return;
468
+ output += `- [${index}]: ${OPERATION[op]} (${JSON.stringify(changeTree.getValue(Number(index), false))})\n`;
414
469
  });
415
-
416
- const changes = changeTree.indexedOperations;
417
- const level = parentChangeTrees.length;
418
- const indent = getIndent(level);
419
-
420
- const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
421
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.ref[$refId]}) - changes: ${Object.keys(changes).length}\n`;
422
-
423
- for (const index in changes) {
424
- const operation = changes[index];
425
- output += `${getIndent(level + 1)}${OPERATION[operation]}: ${index}\n`;
426
- }
427
470
  }
428
471
 
429
- return `${output}`;
472
+ return output;
430
473
  }
431
474
 
432
-
433
475
  }
434
476