@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,10 +1,17 @@
1
1
  import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
2
- import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
2
+ import { ChangeTree, installUntrackedChangeTree, type IRef } from "../../encoder/ChangeTree.js";
3
3
  import { OPERATION } from "../../encoding/spec.js";
4
4
  import { registerType } from "../registry.js";
5
5
  import { Collection } from "../HelperTypes.js";
6
- import { decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
7
- import { encodeKeyValueOperation } from "../../encoder/EncodeOperation.js";
6
+ import { CollectionKind, decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
7
+ import { encodeIndexedEntry } from "../../encoder/EncodeOperation.js";
8
+ import {
9
+ createStreamableState,
10
+ streamDropView,
11
+ streamRouteAdd,
12
+ streamRouteRemove,
13
+ type StreamableState,
14
+ } from "../../encoder/streaming.js";
8
15
  import type { StateView } from "../../encoder/StateView.js";
9
16
  import type { Schema } from "../../Schema.js";
10
17
 
@@ -16,14 +23,40 @@ export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
16
23
 
17
24
  protected [$childType]: string | typeof Schema;
18
25
 
26
+ /** The user-visible data, keyed directly by the wire-protocol index. */
19
27
  protected $items: Map<number, V> = new Map<number, V>();
20
- protected $indexes: Map<number, number> = new Map<number, number>();
28
+
29
+ /** Snapshots of values that were deleted this tick (for filter visibility). */
21
30
  protected deletedItems: { [field: string]: V } = {};
22
31
 
32
+ /** Monotonic counter for assigning indexes to newly-added items. */
23
33
  protected $refId: number = 0;
24
34
 
25
- static [$encoder] = encodeKeyValueOperation;
35
+ /**
36
+ * Streamable state — lazily allocated when the field is opted into
37
+ * streaming via `t.collection(X).stream()`. See MapSchema for the
38
+ * same pattern / rationale.
39
+ */
40
+ _stream?: StreamableState;
41
+
42
+ get maxPerTick(): number {
43
+ return this._stream?.maxPerTick ?? 32;
44
+ }
45
+ set maxPerTick(n: number) {
46
+ (this._stream ??= createStreamableState()).maxPerTick = n;
47
+ }
48
+
49
+ get priority(): ((view: any, element: V) => number) | undefined {
50
+ return this._stream?.priority as ((view: any, element: V) => number) | undefined;
51
+ }
52
+ set priority(fn: ((view: any, element: V) => number) | undefined) {
53
+ (this._stream ??= createStreamableState()).priority = fn;
54
+ }
55
+
56
+ static [$encoder] = encodeIndexedEntry;
26
57
  static [$decoder] = decodeKeyValueOperation;
58
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
59
+ static readonly COLLECTION_KIND = CollectionKind.Collection;
27
60
 
28
61
  /**
29
62
  * Determine if a property must be filtered.
@@ -47,36 +80,54 @@ export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
47
80
  }
48
81
 
49
82
  constructor (initialValues?: Array<V>) {
50
- this[$changes] = new ChangeTree(this);
51
- this[$changes].indexes = {};
83
+ // $changes must be non-enumerable — see Schema.initialize.
84
+ Object.defineProperty(this, $changes, {
85
+ value: new ChangeTree(this),
86
+ enumerable: false,
87
+ writable: true,
88
+ });
89
+ this[$childType] = undefined as any;
52
90
 
53
91
  if (initialValues) {
54
92
  initialValues.forEach((v) => this.add(v));
55
93
  }
94
+ }
56
95
 
57
- Object.defineProperty(this, $childType, {
58
- value: undefined,
59
- enumerable: false,
60
- writable: true,
61
- configurable: true,
62
- });
96
+ /**
97
+ * Decoder-side factory. Skips the tracking `ChangeTree` allocation;
98
+ * `Object.create` also bypasses the class-field initializers, so we
99
+ * replicate the minimum slot init here. Must stay in sync with the
100
+ * class-field declarations above.
101
+ */
102
+ static initializeForDecoder<V = any>(): CollectionSchema<V> {
103
+ const self: any = Object.create(CollectionSchema.prototype);
104
+ self.$items = new Map<number, V>();
105
+ self.deletedItems = {};
106
+ self.$refId = 0;
107
+ self[$childType] = undefined;
108
+ installUntrackedChangeTree(self);
109
+ return self;
63
110
  }
64
111
 
65
112
  add(value: V) {
66
- // set "index" for reference.
113
+ // assign the next wire-protocol index
67
114
  const index = this.$refId++;
68
115
 
116
+ const changeTree = this[$changes];
69
117
  const isRef = (value[$changes]) !== undefined;
70
118
  if (isRef) {
71
- value[$changes].setParent(this, this[$changes].root, index);
119
+ value[$changes].setParent(this, changeTree.root, index);
72
120
  }
73
121
 
74
- this[$changes].indexes[index] = index;
75
-
76
- this.$indexes.set(index, index);
77
122
  this.$items.set(index, value);
78
123
 
79
- this[$changes].change(index);
124
+ if (changeTree.isStreamCollection) {
125
+ if (changeTree.root !== undefined) {
126
+ streamRouteAdd(this, changeTree.root, index);
127
+ }
128
+ } else {
129
+ changeTree.change(index);
130
+ }
80
131
 
81
132
  return index;
82
133
  }
@@ -108,8 +159,21 @@ export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
108
159
  return false;
109
160
  }
110
161
 
111
- this.deletedItems[index] = this[$changes].delete(index);
112
- this.$indexes.delete(index);
162
+ const changeTree = this[$changes];
163
+ if (changeTree.isStreamCollection) {
164
+ const root = changeTree.root;
165
+ const previousValue = this.$items.get(index);
166
+ if (root !== undefined) {
167
+ streamRouteRemove(this, root, (this as any)[$refId], index);
168
+ }
169
+ if ((previousValue as any)?.[$changes] !== undefined) {
170
+ root?.remove((previousValue as any)[$changes]);
171
+ }
172
+ this.deletedItems[index] = previousValue as V;
173
+ return this.$items.delete(index);
174
+ }
175
+
176
+ this.deletedItems[index] = changeTree.delete(index);
113
177
 
114
178
  return this.$items.delete(index);
115
179
  }
@@ -118,17 +182,13 @@ export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
118
182
  const changeTree = this[$changes];
119
183
 
120
184
  // discard previous operations.
121
- changeTree.discard(true);
122
- changeTree.indexes = {};
185
+ changeTree.discard();
123
186
 
124
187
  // remove children references
125
188
  changeTree.forEachChild((childChangeTree, _) => {
126
189
  changeTree.root?.remove(childChangeTree);
127
190
  });
128
191
 
129
- // clear previous indexes
130
- this.$indexes.clear();
131
-
132
192
  // clear items
133
193
  this.$items.clear();
134
194
 
@@ -151,31 +211,51 @@ export class CollectionSchema<V=any> implements Collection<K, V>, IRef {
151
211
  return this.$items.size;
152
212
  }
153
213
 
214
+ // ────── Change tracking control (same API as Schema) ──────
215
+ pauseTracking(): void { this[$changes].pause(); }
216
+ resumeTracking(): void { this[$changes].resume(); }
217
+ untracked<T>(fn: () => T): T { return this[$changes].untracked(fn); }
218
+ get isTrackingPaused(): boolean { return this[$changes].paused; }
219
+
154
220
  /** Iterator */
155
221
  [Symbol.iterator](): IterableIterator<V> {
156
222
  return this.$items.values();
157
223
  }
158
224
 
159
- protected setIndex(index: number, key: number) {
160
- this.$indexes.set(index, key);
225
+ // ────────────────────────────────────────────────────────────────────
226
+ // Decoder-side index hooks. CollectionSchema's "key" IS the wire index,
227
+ // so these are identity operations. Kept for protocol symmetry with
228
+ // MapSchema (decoder calls them polymorphically).
229
+ // ────────────────────────────────────────────────────────────────────
230
+
231
+ protected setIndex(_index: number, _key: number) {
232
+ // no-op: indexes are identity
161
233
  }
162
234
 
163
- protected getIndex(index: number) {
164
- return this.$indexes.get(index);
235
+ protected getIndex(index: number): number {
236
+ return index;
165
237
  }
166
238
 
167
239
  [$getByIndex](index: number): any {
168
- return this.$items.get(this.$indexes.get(index));
240
+ return this.$items.get(index);
169
241
  }
170
242
 
171
243
  [$deleteByIndex](index: number): void {
172
- const key = this.$indexes.get(index);
173
- this.$items.delete(key);
174
- this.$indexes.delete(index);
244
+ this.$items.delete(index);
175
245
  }
176
246
 
177
247
  protected [$onEncodeEnd]() {
178
- this.deletedItems = {};
248
+ for (const key in this.deletedItems) { delete this.deletedItems[key]; }
249
+ }
250
+
251
+ // ─── Streamable interface (Encoder priority / broadcast pass) ──────
252
+
253
+ _dropView(viewId: number): void {
254
+ streamDropView(this, viewId);
255
+ }
256
+
257
+ _unregister(): void {
258
+ // no-op — `Root.unregisterStream` handles the Set removal.
179
259
  }
180
260
 
181
261
  toArray() {
@@ -1,10 +1,18 @@
1
- import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $numFields, $refId } from "../symbols.js";
2
- import { ChangeTree, IRef } from "../../encoder/ChangeTree.js";
1
+ import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $refId } from "../symbols.js";
2
+ import { ChangeTree, installUntrackedChangeTree, IRef } from "../../encoder/ChangeTree.js";
3
3
  import { OPERATION } from "../../encoding/spec.js";
4
4
  import { registerType } from "../registry.js";
5
5
  import { Collection } from "../HelperTypes.js";
6
- import { decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
7
- import { encodeKeyValueOperation } from "../../encoder/EncodeOperation.js";
6
+ import { CollectionKind, decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
7
+ import { encodeMapEntry } from "../../encoder/EncodeOperation.js";
8
+ import { MapJournal } from "../../encoder/MapJournal.js";
9
+ import {
10
+ createStreamableState,
11
+ streamDropView,
12
+ streamRouteAdd,
13
+ streamRouteRemove,
14
+ type StreamableState,
15
+ } from "../../encoder/streaming.js";
8
16
  import type { StateView } from "../../encoder/StateView.js";
9
17
  import type { Schema } from "../../Schema.js";
10
18
  import { assertInstanceType } from "../../encoding/assert.js";
@@ -17,11 +25,59 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
17
25
  protected [$childType]: string | typeof Schema;
18
26
 
19
27
  protected $items: Map<K, V> = new Map<K, V>();
20
- protected $indexes: Map<number, K> = new Map<number, K>();
21
- protected deletedItems: { [index: string]: V } = {};
22
28
 
23
- static [$encoder] = encodeKeyValueOperation;
29
+ /**
30
+ * Wire-protocol identity + change-tracking metadata for this map.
31
+ *
32
+ * Owns: index↔key mapping, monotonic index counter, snapshots of removed
33
+ * values for filter visibility checks. Replaces what used to live as three
34
+ * separate fields on this class ($indexes, _collectionIndexes, deletedItems).
35
+ */
36
+ protected journal: MapJournal<K> = new MapJournal<K>();
37
+
38
+ /**
39
+ * Streamable state — lazily allocated by `inheritedFlags` (or the
40
+ * `maxPerTick` setter) when streaming actually activates. `undefined`
41
+ * on every non-streaming MapSchema so the common case pays zero
42
+ * Map/Set allocation. Single slot → hidden-class shape stays stable
43
+ * across streaming and non-streaming instances.
44
+ */
45
+ _stream?: StreamableState;
46
+
47
+ /** Max ADD ops emitted per tick per view. Ignored outside streaming mode. */
48
+ get maxPerTick(): number {
49
+ return this._stream?.maxPerTick ?? 32;
50
+ }
51
+ set maxPerTick(n: number) {
52
+ (this._stream ??= createStreamableState()).maxPerTick = n;
53
+ }
54
+
55
+ /**
56
+ * Per-view priority callback for `.stream()` maps. Initialized from the
57
+ * schema declaration (`t.map(X).stream().priority(fn)` or `@type({ map,
58
+ * priority })`); assigning here overrides for this instance. Only fires
59
+ * during `encodeView` — broadcast mode drains FIFO.
60
+ */
61
+ get priority(): ((view: any, element: V) => number) | undefined {
62
+ return this._stream?.priority as ((view: any, element: V) => number) | undefined;
63
+ }
64
+ set priority(fn: ((view: any, element: V) => number) | undefined) {
65
+ (this._stream ??= createStreamableState()).priority = fn;
66
+ }
67
+
68
+ /** Backwards-compat alias for `journal.keyByIndex`. */
69
+ get $indexes(): Map<number, K> { return this.journal.keyByIndex; }
70
+
71
+ /**
72
+ * Backwards-compat alias for `journal.indexByKey`. Plain object so
73
+ * polymorphic call sites like `ref._collectionIndexes?.[key]` keep working.
74
+ */
75
+ get _collectionIndexes(): { [key: string]: number } { return this.journal.indexByKey; }
76
+
77
+ static [$encoder] = encodeMapEntry;
24
78
  static [$decoder] = decodeKeyValueOperation;
79
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
80
+ static readonly COLLECTION_KIND = CollectionKind.Map;
25
81
 
26
82
  /**
27
83
  * Determine if a property must be filtered.
@@ -33,11 +89,9 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
33
89
  * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
34
90
  */
35
91
  static [$filter] (ref: MapSchema, index: number, view: StateView) {
36
- return (
37
- !view ||
38
- typeof (ref[$childType]) === "string" ||
39
- view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
40
- );
92
+ if (!view || typeof (ref[$childType]) === "string") return true;
93
+ const value = ref[$getByIndex](index) ?? ref.journal.snapshotAt(index);
94
+ return view.isChangeTreeVisible(value[$changes]);
41
95
  }
42
96
 
43
97
  static is(type: any) {
@@ -45,14 +99,15 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
45
99
  }
46
100
 
47
101
  constructor (initialValues?: Map<K, V> | Record<K, V>) {
48
- const changeTree = new ChangeTree(this);
49
- changeTree.indexes = {};
50
-
102
+ // $changes MUST be non-enumerable — see Schema.initialize comment.
103
+ // ChangeTree has circular refs (root→changeTrees→…) and would send
104
+ // `assert.deepStrictEqual` into exponential recursion.
51
105
  Object.defineProperty(this, $changes, {
52
- value: changeTree,
106
+ value: new ChangeTree(this),
53
107
  enumerable: false,
54
108
  writable: true,
55
109
  });
110
+ this[$childType] = undefined as any;
56
111
 
57
112
  if (initialValues) {
58
113
  if (
@@ -67,13 +122,21 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
67
122
  }
68
123
  }
69
124
  }
125
+ }
70
126
 
71
- Object.defineProperty(this, $childType, {
72
- value: undefined,
73
- enumerable: false,
74
- writable: true,
75
- configurable: true,
76
- });
127
+ /**
128
+ * Decoder-side factory. Skips the tracking `ChangeTree` allocation;
129
+ * `Object.create` also bypasses the class-field initializers, so we
130
+ * replicate the minimum slot init here. Must stay in sync with the
131
+ * class-field declarations above and with the constructor body.
132
+ */
133
+ static initializeForDecoder<V = any, K extends string = string>(): MapSchema<V, K> {
134
+ const self: any = Object.create(MapSchema.prototype);
135
+ self.$items = new Map<K, V>();
136
+ self.journal = new MapJournal<K>();
137
+ self[$childType] = undefined;
138
+ installUntrackedChangeTree(self);
139
+ return self;
77
140
  }
78
141
 
79
142
  /** Iterator */
@@ -96,13 +159,13 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
96
159
 
97
160
  const changeTree = this[$changes];
98
161
  const isRef = (value[$changes]) !== undefined;
162
+ const journal = this.journal;
99
163
 
100
- let index: number;
164
+ let index = journal.indexOf(key);
101
165
  let operation: OPERATION;
102
166
 
103
- // IS REPLACE?
104
- if (typeof(changeTree.indexes[key]) !== "undefined") {
105
- index = changeTree.indexes[key];
167
+ if (index !== undefined) {
168
+ // REPLACE branch
106
169
  operation = OPERATION.REPLACE;
107
170
 
108
171
  const previousValue = this.$items.get(key);
@@ -120,22 +183,31 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
120
183
  }
121
184
  }
122
185
 
123
- if (this.deletedItems[index]) {
124
- delete this.deletedItems[index];
186
+ // Re-setting after a delete: discard the snapshot.
187
+ if (journal.snapshotAt(index) !== undefined) {
188
+ journal.forgetSnapshot(index);
125
189
  }
126
190
 
127
191
  } else {
128
- index = changeTree.indexes[$numFields] ?? 0;
192
+ // ADD branch
193
+ index = journal.assign(key);
129
194
  operation = OPERATION.ADD;
130
-
131
- this.$indexes.set(index, key);
132
- changeTree.indexes[key] = index;
133
- changeTree.indexes[$numFields] = index + 1;
134
195
  }
135
196
 
136
197
  this.$items.set(key, value);
137
198
 
138
- changeTree.change(index, operation);
199
+ // Streaming-mode ADD: route the new entry into per-view or broadcast
200
+ // pending instead of recording on the tree. The encoder's priority /
201
+ // broadcast pass will drain up to `maxPerTick` per tick. REPLACE
202
+ // and DELETE_AND_ADD fall through to the normal recorder path — the
203
+ // old value is already being emitted, so the swap just mutates.
204
+ if (operation === OPERATION.ADD && changeTree.isStreamCollection) {
205
+ if (changeTree.root !== undefined) {
206
+ streamRouteAdd(this, changeTree.root, index);
207
+ }
208
+ } else {
209
+ changeTree.change(index, operation);
210
+ }
139
211
 
140
212
  //
141
213
  // set value's parent after the value is set
@@ -157,9 +229,36 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
157
229
  return false;
158
230
  }
159
231
 
160
- const index = this[$changes].indexes[key];
232
+ const index = this.journal.indexOf(key)!;
233
+ const previousValue = this.$items.get(key)!;
234
+ const changeTree = this[$changes];
161
235
 
162
- this.deletedItems[index] = this[$changes].delete(index);
236
+ // Streaming-mode: silent-drop if the entry never made it out to any
237
+ // client (still in pending). Otherwise force DELETE on the channels
238
+ // where it was already sent — bypasses the normal recorder so the
239
+ // emission path stays symmetric with StreamSchema.
240
+ if (changeTree.isStreamCollection) {
241
+ const root = changeTree.root;
242
+ let neverSent = false;
243
+ if (root !== undefined) {
244
+ neverSent = streamRouteRemove(this, root, this[$refId], index);
245
+ }
246
+ if ((previousValue as any)?.[$changes] !== undefined) {
247
+ root?.remove(previousValue[$changes]);
248
+ }
249
+ this.$items.delete(key);
250
+ // Only snapshot if we actually need a DELETE op (already-sent):
251
+ // filter visibility checks look up the snapshot until the next
252
+ // encode end. Never-sent entries can skip the snapshot work.
253
+ if (!neverSent) this.journal.snapshot(index, previousValue);
254
+ return true;
255
+ }
256
+
257
+ // Snapshot the deleted value (used by [$filter] for visibility checks
258
+ // until $onEncodeEnd cleans it up).
259
+ this.journal.snapshot(index, previousValue);
260
+
261
+ changeTree.delete(index);
163
262
 
164
263
  return this.$items.delete(key);
165
264
  }
@@ -168,16 +267,15 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
168
267
  const changeTree = this[$changes];
169
268
 
170
269
  // discard previous operations.
171
- changeTree.discard(true);
172
- changeTree.indexes = {};
270
+ changeTree.discard();
173
271
 
174
272
  // remove children references
175
273
  changeTree.forEachChild((childChangeTree, _) => {
176
274
  changeTree.root?.remove(childChangeTree);
177
275
  });
178
276
 
179
- // clear previous indexes
180
- this.$indexes.clear();
277
+ // reset journal (clears all index/key state and snapshots)
278
+ this.journal.reset();
181
279
 
182
280
  // clear items
183
281
  this.$items.clear();
@@ -209,39 +307,45 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
209
307
  return this.$items.size;
210
308
  }
211
309
 
310
+ // ────── Change tracking control (same API as Schema) ──────
311
+ pauseTracking(): void { this[$changes].pause(); }
312
+ resumeTracking(): void { this[$changes].resume(); }
313
+ untracked<T>(fn: () => T): T { return this[$changes].untracked(fn); }
314
+ get isTrackingPaused(): boolean { return this[$changes].paused; }
315
+
212
316
  protected setIndex(index: number, key: K) {
213
- this.$indexes.set(index, key);
317
+ this.journal.setIndex(index, key);
214
318
  }
215
319
 
216
320
  protected getIndex(index: number) {
217
- return this.$indexes.get(index);
321
+ return this.journal.keyOf(index);
218
322
  }
219
323
 
220
324
  [$getByIndex](index: number): V | undefined {
221
- return this.$items.get(this.$indexes.get(index));
325
+ const key = this.journal.keyOf(index);
326
+ return key !== undefined ? this.$items.get(key) : undefined;
222
327
  }
223
328
 
224
329
  [$deleteByIndex](index: number): void {
225
- const key = this.$indexes.get(index);
226
- this.$items.delete(key);
227
- this.$indexes.delete(index);
330
+ const key = this.journal.keyOf(index);
331
+ if (key !== undefined) {
332
+ this.$items.delete(key);
333
+ this.journal.keyByIndex.delete(index);
334
+ }
228
335
  }
229
336
 
230
337
  protected [$onEncodeEnd]() {
231
- const changeTree = this[$changes];
338
+ this.journal.cleanupAfterEncode();
339
+ }
232
340
 
233
- // - cleanup changeTree.indexes
234
- // - cleanup $indexes
235
- for (const indexStr in this.deletedItems) {
236
- const index = parseInt(indexStr);
237
- const key = this.$indexes.get(index);
238
- // TODO: refactor this.
239
- // it shouldn't be necessary to keep track of indexes both on changeTree and on $indexes
240
- delete changeTree.indexes[key];
241
- this.$indexes.delete(index);
242
- }
341
+ // ─── Streamable interface (Encoder priority / broadcast pass) ──────
342
+
343
+ _dropView(viewId: number): void {
344
+ streamDropView(this, viewId);
345
+ }
243
346
 
244
- this.deletedItems = {};
347
+ _unregister(): void {
348
+ // no-op — `Root.unregisterStream` handles the Set removal.
245
349
  }
246
350
 
247
351
  toJSON() {