@colyseus/schema 4.0.20 → 5.0.1

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 +56 -2
  3. package/build/Reflection.d.ts +28 -34
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +64 -17
  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 +5258 -1549
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5258 -1549
  30. package/build/index.mjs +5249 -1549
  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 +7453 -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 +7450 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +67 -9
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +192 -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 +259 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +365 -252
  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 +121 -24
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +331 -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 +93 -6
  96. package/src/utils.ts +4 -6
@@ -1,12 +1,12 @@
1
- import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd, $refId } from "../symbols.js";
1
+ import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd, $proxyTarget, $refId } from "../symbols.js";
2
2
  import type { Schema } from "../../Schema.js";
3
- import { type IRef, ChangeTree, setOperationAtIndex } from "../../encoder/ChangeTree.js";
3
+ import { type IRef, ChangeTree, installUntrackedChangeTree } from "../../encoder/ChangeTree.js";
4
4
  import { OPERATION } from "../../encoding/spec.js";
5
5
  import { registerType } from "../registry.js";
6
6
  import { Collection } from "../HelperTypes.js";
7
7
 
8
8
  import { encodeArray } from "../../encoder/EncodeOperation.js";
9
- import { decodeArray } from "../../decoder/DecodeOperation.js";
9
+ import { CollectionKind, decodeArray } from "../../decoder/DecodeOperation.js";
10
10
  import type { StateView } from "../../encoder/StateView.js";
11
11
  import { assertInstanceType } from "../../encoding/assert.js";
12
12
 
@@ -18,20 +18,113 @@ const DEFAULT_SORT = (a: any, b: any) => {
18
18
  else return 0
19
19
  }
20
20
 
21
+ /**
22
+ * Module-level Proxy handler shared by every `ArraySchema` instance. Hoisted
23
+ * out of the ctor so per-instance Proxy setup stops allocating ~6 arrow
24
+ * closures (the `__name` wrappers around those closures dominated one slice
25
+ * of the decoder profile). The handlers reference the target via the trap's
26
+ * `obj` arg — they don't need a captured `this`. Both `new ArraySchema()`
27
+ * and `ArraySchema.initializeForDecoder()` plug into it.
28
+ */
29
+ const ARRAY_PROXY_HANDLER: ProxyHandler<any> = {
30
+ get: (obj, prop) => {
31
+ if (
32
+ typeof (prop) !== "symbol" &&
33
+ // FIXME: d8 accuses this as low performance
34
+ !isNaN(prop as any) // https://stackoverflow.com/a/175787/892698
35
+ ) {
36
+ return obj.items[prop as unknown as number];
37
+ }
38
+ return Reflect.get(obj, prop);
39
+ },
40
+
41
+ set: (obj, key, setValue) => {
42
+ if (typeof (key) !== "symbol" && !isNaN(key as any)) {
43
+ if (setValue === undefined || setValue === null) {
44
+ obj.$deleteAt(key as unknown as number);
45
+
46
+ } else {
47
+ if (setValue[$changes]) {
48
+ assertInstanceType(setValue, obj[$childType] as typeof Schema, obj, key);
49
+
50
+ const previousValue = obj.items[key as unknown as number];
51
+
52
+ if (!obj.isMovingItems) {
53
+ obj.$changeAt(Number(key), setValue);
54
+
55
+ } else {
56
+ if (previousValue !== undefined) {
57
+ if (setValue[$changes].isNew) {
58
+ obj[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
59
+
60
+ } else {
61
+ if ((obj[$changes].getChange(Number(key)) & OPERATION.DELETE) === OPERATION.DELETE) {
62
+ obj[$changes].indexedOperation(Number(key), OPERATION.DELETE_AND_MOVE);
63
+
64
+ } else {
65
+ obj[$changes].indexedOperation(Number(key), OPERATION.MOVE);
66
+ }
67
+ }
68
+
69
+ } else if (setValue[$changes].isNew) {
70
+ obj[$changes].indexedOperation(Number(key), OPERATION.ADD);
71
+ }
72
+
73
+ setValue[$changes].setParent(obj, obj[$changes].root, key);
74
+ }
75
+
76
+ if (previousValue !== undefined) {
77
+ // remove root reference from previous value
78
+ previousValue[$changes].root?.remove(previousValue[$changes]);
79
+ }
80
+
81
+ } else {
82
+ obj.$changeAt(Number(key), setValue);
83
+ }
84
+
85
+ obj.items[key as unknown as number] = setValue;
86
+ obj.tmpItems[key as unknown as number] = setValue;
87
+ }
88
+
89
+ return true;
90
+ }
91
+ return Reflect.set(obj, key, setValue);
92
+ },
93
+
94
+ deleteProperty: (obj, prop) => {
95
+ if (typeof (prop) === "number") {
96
+ obj.$deleteAt(prop);
97
+ } else {
98
+ delete obj[prop as unknown as number];
99
+ }
100
+ return true;
101
+ },
102
+
103
+ has: (obj, key) => {
104
+ if (typeof (key) !== "symbol" && !isNaN(Number(key))) {
105
+ return Reflect.has(obj.items, key);
106
+ }
107
+ return Reflect.has(obj, key);
108
+ },
109
+ };
110
+
21
111
  export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IRef {
22
112
  [n: number]: V;
23
113
  [$changes]: ChangeTree;
24
114
  [$refId]?: number;
115
+ [$proxyTarget]: this;
25
116
 
26
117
  protected [$childType]: string | typeof Schema;
27
118
 
28
119
  protected items: V[] = [];
29
120
  protected tmpItems: V[] = [];
30
- protected deletedIndexes: {[index: number]: boolean} = {};
121
+ protected deletedIndexes: boolean[] = [];
31
122
  protected isMovingItems = false;
32
123
 
33
124
  static [$encoder] = encodeArray;
34
125
  static [$decoder] = decodeArray;
126
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
127
+ static readonly COLLECTION_KIND = CollectionKind.Array;
35
128
 
36
129
  /**
37
130
  * Determine if a property must be filtered.
@@ -65,99 +158,12 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
65
158
  }
66
159
 
67
160
  constructor (...items: V[]) {
68
- Object.defineProperty(this, $childType, {
69
- value: undefined,
70
- enumerable: false,
71
- writable: true,
72
- configurable: true,
73
- });
74
-
75
- const proxy = new Proxy(this, {
76
- get: (obj, prop) => {
77
- if (
78
- typeof (prop) !== "symbol" &&
79
- // FIXME: d8 accuses this as low performance
80
- !isNaN(prop as any) // https://stackoverflow.com/a/175787/892698
81
- ) {
82
- return this.items[prop as unknown as number];
83
-
84
- } else {
85
- return Reflect.get(obj, prop);
86
- }
87
- },
88
-
89
- set: (obj, key, setValue) => {
90
- if (typeof (key) !== "symbol" && !isNaN(key as any)) {
91
- if (setValue === undefined || setValue === null) {
92
- obj.$deleteAt(key as unknown as number);
93
-
94
- } else {
95
- if (setValue[$changes]) {
96
- assertInstanceType(setValue, obj[$childType] as typeof Schema, obj, key);
97
-
98
- const previousValue = obj.items[key as unknown as number];
99
-
100
- if (!obj.isMovingItems) {
101
- obj.$changeAt(Number(key), setValue);
102
-
103
- } else {
104
- if (previousValue !== undefined) {
105
- if (setValue[$changes].isNew) {
106
- obj[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
107
-
108
- } else {
109
- if ((obj[$changes].getChange(Number(key)) & OPERATION.DELETE) === OPERATION.DELETE) {
110
- obj[$changes].indexedOperation(Number(key), OPERATION.DELETE_AND_MOVE);
111
-
112
- } else {
113
- obj[$changes].indexedOperation(Number(key), OPERATION.MOVE);
114
- }
115
- }
116
-
117
- } else if (setValue[$changes].isNew) {
118
- obj[$changes].indexedOperation(Number(key), OPERATION.ADD);
119
- }
120
-
121
- setValue[$changes].setParent(this, obj[$changes].root, key);
122
- }
123
-
124
- if (previousValue !== undefined) {
125
- // remove root reference from previous value
126
- previousValue[$changes].root?.remove(previousValue[$changes]);
127
- }
128
-
129
- } else {
130
- obj.$changeAt(Number(key), setValue);
131
- }
132
-
133
- obj.items[key as unknown as number] = setValue;
134
- obj.tmpItems[key as unknown as number] = setValue;
135
- }
136
-
137
- return true;
138
- } else {
139
- return Reflect.set(obj, key, setValue);
140
- }
141
- },
142
-
143
- deleteProperty: (obj, prop) => {
144
- if (typeof (prop) === "number") {
145
- obj.$deleteAt(prop);
146
-
147
- } else {
148
- delete obj[prop as unknown as number];
149
- }
161
+ this[$childType] = undefined as any;
162
+ // Self-reference so methods called via the Proxy can recover the
163
+ // underlying instance and access fields directly. See $proxyTarget.
164
+ this[$proxyTarget] = this;
150
165
 
151
- return true;
152
- },
153
-
154
- has: (obj, key) => {
155
- if (typeof (key) !== "symbol" && !isNaN(Number(key))) {
156
- return Reflect.has(this.items, key);
157
- }
158
- return Reflect.has(obj, key)
159
- }
160
- });
166
+ const proxy = new Proxy(this, ARRAY_PROXY_HANDLER);
161
167
 
162
168
  Object.defineProperty(this, $changes, {
163
169
  value: new ChangeTree(proxy),
@@ -172,6 +178,30 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
172
178
  return proxy;
173
179
  }
174
180
 
181
+ /**
182
+ * Decoder-side factory. Skips the `ChangeTree` allocation and
183
+ * replicates the class-field initializers by hand (since `Object.create`
184
+ * bypasses them). Must stay in sync with the class-field declarations
185
+ * and the constructor body above.
186
+ *
187
+ * Pass the Proxy to `installUntrackedChangeTree` as the public identity
188
+ * so children set their parent to the Proxy, not the raw target.
189
+ */
190
+ static initializeForDecoder<V = any>(): ArraySchema<V> {
191
+ const self: any = Object.create(ArraySchema.prototype);
192
+ self.items = [];
193
+ // `tmpItems` / `deletedIndexes` are encoder-only (consulted by the
194
+ // staged-snapshot path in `$getByIndex`, `$onEncodeEnd`, etc.). The
195
+ // decoder reads from `items` directly and never maintains them.
196
+ self.isMovingItems = false;
197
+ self[$childType] = undefined;
198
+ self[$proxyTarget] = self;
199
+
200
+ const proxy = new Proxy(self, ARRAY_PROXY_HANDLER);
201
+ installUntrackedChangeTree(self, proxy);
202
+ return proxy;
203
+ }
204
+
175
205
  set length (newLength: number) {
176
206
  if (newLength === 0) {
177
207
  this.clear();
@@ -186,10 +216,22 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
186
216
  return this.items.length;
187
217
  }
188
218
 
189
- push(...values: V[]) {
190
- let length = this.tmpItems.length;
219
+ // ────── Change tracking control (same API as Schema) ──────
220
+ pauseTracking(): void { this[$changes].pause(); }
221
+ resumeTracking(): void { this[$changes].resume(); }
222
+ untracked<T>(fn: () => T): T { return this[$changes].untracked(fn); }
223
+ get isTrackingPaused(): boolean { return this[$changes].paused; }
191
224
 
192
- const changeTree = this[$changes];
225
+ push(...values: V[]) {
226
+ // `this` is the Proxy when called from user code. Grab the underlying
227
+ // instance once so the body's field reads (items, tmpItems, $changes,
228
+ // $childType) skip the Proxy.get trap on every iteration.
229
+ const self = this[$proxyTarget];
230
+ const items = self.items;
231
+ const tmpItems = self.tmpItems;
232
+ const changeTree = self[$changes];
233
+ const childType = self[$childType];
234
+ let length = tmpItems.length;
193
235
 
194
236
  for (let i = 0, l = values.length; i < l; i++, length++) {
195
237
  const value = values[i];
@@ -198,19 +240,21 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
198
240
  // skip null values
199
241
  return;
200
242
 
201
- } else if (typeof (value) === "object" && this[$childType]) {
202
- assertInstanceType(value as any, this[$childType] as typeof Schema, this, i);
243
+ } else if (typeof (value) === "object" && childType) {
244
+ assertInstanceType(value as any, childType as typeof Schema, self, i);
203
245
  // TODO: move value[$changes]?.setParent() to this block.
204
246
  }
205
247
 
206
- changeTree.indexedOperation(length, OPERATION.ADD, this.items.length);
248
+ changeTree.indexedOperation(length, OPERATION.ADD);
207
249
 
208
- this.items.push(value);
209
- this.tmpItems.push(value);
250
+ items.push(value);
251
+ tmpItems.push(value);
210
252
 
211
253
  //
212
254
  // set value's parent after the value is set
213
255
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
256
+ // Pass `this` (the Proxy) as parent — the Proxy is the public
257
+ // identity of the array; ChangeTree.parentRef compares by identity.
214
258
  //
215
259
  value[$changes]?.setParent(this, changeTree.root, length);
216
260
  }
@@ -222,12 +266,15 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
222
266
  * Removes the last element from an array and returns it.
223
267
  */
224
268
  pop(): V | undefined {
269
+ // Unwrap Proxy once — see push() for rationale.
270
+ const self = this[$proxyTarget];
271
+ const tmpItems = self.tmpItems;
272
+ const deletedIndexes = self.deletedIndexes;
225
273
  let index: number = -1;
226
274
 
227
275
  // find last non-undefined index
228
- for (let i = this.tmpItems.length - 1; i >= 0; i--) {
229
- // if (this.tmpItems[i] !== undefined) {
230
- if (this.deletedIndexes[i] !== true) {
276
+ for (let i = tmpItems.length - 1; i >= 0; i--) {
277
+ if (deletedIndexes[i] !== true) {
231
278
  index = i;
232
279
  break;
233
280
  }
@@ -237,11 +284,10 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
237
284
  return undefined;
238
285
  }
239
286
 
240
- this[$changes].delete(index, undefined, this.items.length - 1);
241
-
242
- this.deletedIndexes[index] = true;
287
+ self[$changes].delete(index);
288
+ deletedIndexes[index] = true;
243
289
 
244
- return this.items.pop();
290
+ return self.items.pop();
245
291
  }
246
292
 
247
293
  at(index: number) {
@@ -303,24 +349,25 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
303
349
  }
304
350
 
305
351
  clear() {
352
+ const self = this[$proxyTarget];
306
353
  // skip if already clear
307
- if (this.items.length === 0) {
354
+ if (self.items.length === 0) {
308
355
  return;
309
356
  }
310
357
 
311
358
  // discard previous operations.
312
- const changeTree = this[$changes];
359
+ const changeTree = self[$changes];
313
360
 
314
361
  // remove children references
315
362
  changeTree.forEachChild((childChangeTree, _) => {
316
363
  changeTree.root?.remove(childChangeTree);
317
364
  });
318
365
 
319
- changeTree.discard(true);
366
+ changeTree.discard();
320
367
  changeTree.operation(OPERATION.CLEAR);
321
368
 
322
- this.items.length = 0;
323
- this.tmpItems.length = 0;
369
+ self.items.length = 0;
370
+ self.tmpItems.length = 0;
324
371
  }
325
372
 
326
373
  /**
@@ -345,9 +392,10 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
345
392
  */
346
393
  // @ts-ignore
347
394
  reverse(): ArraySchema<V> {
348
- this[$changes].operation(OPERATION.REVERSE);
349
- this.items.reverse();
350
- this.tmpItems.reverse();
395
+ const self = this[$proxyTarget];
396
+ self[$changes].operation(OPERATION.REVERSE);
397
+ self.items.reverse();
398
+ self.tmpItems.reverse();
351
399
  return this;
352
400
  }
353
401
 
@@ -355,19 +403,17 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
355
403
  * Removes the first element from an array and returns it.
356
404
  */
357
405
  shift(): V | undefined {
358
- if (this.items.length === 0) { return undefined; }
406
+ const self = this[$proxyTarget];
407
+ const items = self.items;
408
+ if (items.length === 0) { return undefined; }
359
409
 
360
- const changeTree = this[$changes];
361
-
362
- const index = this.tmpItems.findIndex(item => item === this.items[0]);
363
- const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
364
-
365
- changeTree.delete(index, OPERATION.DELETE, allChangesIndex);
366
- changeTree.shiftAllChangeIndexes(-1, allChangesIndex);
367
-
368
- this.deletedIndexes[index] = true;
410
+ const changeTree = self[$changes];
411
+ const first = items[0];
412
+ const index = self.tmpItems.findIndex(item => item === first);
413
+ changeTree.delete(index, OPERATION.DELETE);
414
+ self.deletedIndexes[index] = true;
369
415
 
370
- return this.items.shift();
416
+ return items.shift();
371
417
  }
372
418
 
373
419
  /**
@@ -391,17 +437,18 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
391
437
  * ```
392
438
  */
393
439
  sort(compareFn: (a: V, b: V) => number = DEFAULT_SORT): this {
394
- this.isMovingItems = true;
440
+ const self = this[$proxyTarget];
441
+ self.isMovingItems = true;
395
442
 
396
- const changeTree = this[$changes];
397
- const sortedItems = this.items.sort(compareFn);
443
+ const changeTree = self[$changes];
444
+ const sortedItems = self.items.sort(compareFn);
398
445
 
399
446
  // wouldn't OPERATION.MOVE make more sense here?
400
447
  sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
401
448
 
402
- this.tmpItems.sort(compareFn);
449
+ self.tmpItems.sort(compareFn);
403
450
 
404
- this.isMovingItems = false;
451
+ self.isMovingItems = false;
405
452
  return this;
406
453
  }
407
454
 
@@ -416,16 +463,20 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
416
463
  deleteCount?: number,
417
464
  ...insertItems: V[]
418
465
  ): V[] {
419
- const changeTree = this[$changes];
420
-
421
- const itemsLength = this.items.length;
422
- const tmpItemsLength = this.tmpItems.length;
466
+ const self = this[$proxyTarget];
467
+ const changeTree = self[$changes];
468
+ const items = self.items;
469
+ const tmpItems = self.tmpItems;
470
+ const deletedIndexes = self.deletedIndexes;
471
+
472
+ const itemsLength = items.length;
473
+ const tmpItemsLength = tmpItems.length;
423
474
  const insertCount = insertItems.length;
424
475
 
425
476
  // build up-to-date list of indexes, excluding removed values.
426
477
  const indexes: number[] = [];
427
478
  for (let i = 0; i < tmpItemsLength; i++) {
428
- if (this.deletedIndexes[i] !== true) {
479
+ if (deletedIndexes[i] !== true) {
429
480
  indexes.push(i);
430
481
  }
431
482
  }
@@ -442,7 +493,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
442
493
  for (let i = start; i < start + deleteCount; i++) {
443
494
  const index = indexes[i];
444
495
  changeTree.delete(index, OPERATION.DELETE);
445
- this.deletedIndexes[index] = true;
496
+ deletedIndexes[index] = true;
446
497
  }
447
498
 
448
499
  } else {
@@ -462,36 +513,19 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
462
513
 
463
514
  changeTree.indexedOperation(
464
515
  addIndex,
465
- (this.deletedIndexes[addIndex])
516
+ (deletedIndexes[addIndex])
466
517
  ? OPERATION.DELETE_AND_ADD
467
518
  : OPERATION.ADD
468
519
  );
469
520
 
470
- // set value's parent/root
521
+ // set value's parent/root — use `this` (Proxy) as parent.
471
522
  insertItems[i][$changes]?.setParent(this, changeTree.root, addIndex);
472
523
  }
473
524
  }
474
525
 
475
- //
476
- // delete exceeding indexes from "allChanges"
477
- // (prevent .encodeAll() from encoding non-existing items)
478
- //
479
- if (deleteCount > insertCount) {
480
- changeTree.shiftAllChangeIndexes(-(deleteCount - insertCount), indexes[start + insertCount]);
481
- // debugChangeSet("AFTER SHIFT indexes", changeTree.allChanges);
482
- }
483
-
484
- //
485
- // FIXME: this code block is duplicated on ChangeTree
486
- //
487
- if (changeTree.filteredChanges !== undefined) {
488
- changeTree.root?.enqueueChangeTree(changeTree, 'filteredChanges');
526
+ changeTree.root?.enqueueChangeTree(changeTree);
489
527
 
490
- } else {
491
- changeTree.root?.enqueueChangeTree(changeTree, 'changes');
492
- }
493
-
494
- return this.items.splice(start, deleteCount, ...insertItems);
528
+ return items.splice(start, deleteCount, ...insertItems);
495
529
  }
496
530
 
497
531
  /**
@@ -499,28 +533,20 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
499
533
  * @param items Elements to insert at the start of the Array.
500
534
  */
501
535
  unshift(...items: V[]): number {
502
- const changeTree = this[$changes];
536
+ const self = this[$proxyTarget];
537
+ const changeTree = self[$changes];
503
538
 
504
- // shift indexes
539
+ // Existing items shift up — `shiftChangeIndexes` handles their
540
+ // relocation bookkeeping. The prepended `items` are genuinely new
541
+ // (no prior existence to MOVE), so each records an ADD.
505
542
  changeTree.shiftChangeIndexes(items.length);
506
-
507
- // new index
508
- if (changeTree.isFiltered) {
509
- setOperationAtIndex(changeTree.filteredChanges, this.items.length);
510
- // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
511
- } else {
512
- setOperationAtIndex(changeTree.allChanges, this.items.length);
513
- // changeTree.allChanges[this.items.length] = OPERATION.ADD;
514
- }
515
-
516
- // FIXME: should we use OPERATION.MOVE here instead?
517
543
  items.forEach((_, index) => {
518
544
  changeTree.change(index, OPERATION.ADD)
519
545
  });
520
546
 
521
- this.tmpItems.unshift(...items);
547
+ self.tmpItems.unshift(...items);
522
548
 
523
- return this.items.unshift(...items);
549
+ return self.items.unshift(...items);
524
550
  }
525
551
 
526
552
  /**
@@ -648,13 +674,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
648
674
  * length+end.
649
675
  */
650
676
  fill(value: V, start?: number, end?: number): this {
651
- //
652
- // TODO
653
- //
654
677
  throw new Error("ArraySchema#fill() not implemented");
655
- // this.$items.fill(value, start, end);
656
-
657
- return this;
658
678
  }
659
679
 
660
680
  /**
@@ -667,11 +687,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
667
687
  * @param end If not specified, length of the this object is used as its default value.
668
688
  */
669
689
  copyWithin(target: number, start: number, end?: number): this {
670
- //
671
- // TODO
672
- //
673
690
  throw new Error("ArraySchema#copyWithin() not implemented");
674
- return this;
675
691
  }
676
692
 
677
693
  /**
@@ -824,14 +840,13 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
824
840
  return this;
825
841
  }
826
842
 
843
+ /**
844
+ * Encoder-only. Reads the staged-snapshot (`tmpItems`) so the encoder can
845
+ * resolve a wire-index even after the user has mutated `items` mid-tick.
846
+ * The decoder reads `items[index]` directly — see `decodeArray` and
847
+ * `$deleteByIndex` below.
848
+ */
827
849
  [$getByIndex](index: number, isEncodeAll: boolean = false): any {
828
- //
829
- // TODO: avoid unecessary `this.tmpItems` check during decoding.
830
- //
831
- // ENCODING uses `this.tmpItems` (or `this.items` if `isEncodeAll` is true)
832
- // DECODING uses `this.items`
833
- //
834
-
835
850
  return (isEncodeAll)
836
851
  ? this.items[index]
837
852
  : this.deletedIndexes[index]
@@ -841,17 +856,15 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
841
856
 
842
857
  [$deleteByIndex](index: number): void {
843
858
  this.items[index] = undefined;
844
- this.tmpItems[index] = undefined; // TODO: do not try to get "tmpItems" at decoding time.
845
859
  }
846
860
 
847
861
  protected [$onEncodeEnd]() {
848
862
  this.tmpItems = this.items.slice();
849
- this.deletedIndexes = {};
863
+ this.deletedIndexes.length = 0;
850
864
  }
851
865
 
852
866
  protected [$onDecodeEnd]() {
853
867
  this.items = this.items.filter((item) => item !== undefined);
854
- this.tmpItems = this.items.slice(); // TODO: do no use "tmpItems" at decoding time.
855
868
  }
856
869
 
857
870
  toArray() {