@colyseus/schema 4.0.19 → 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 (95) 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 -44
  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 +8 -2
  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/parser.ts +83 -61
  62. package/src/decoder/DecodeOperation.ts +168 -63
  63. package/src/decoder/Decoder.ts +20 -10
  64. package/src/decoder/ReferenceTracker.ts +4 -0
  65. package/src/decoder/strategy/Callbacks.ts +30 -26
  66. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  67. package/src/encoder/ChangeRecorder.ts +276 -0
  68. package/src/encoder/ChangeTree.ts +674 -519
  69. package/src/encoder/EncodeDescriptor.ts +213 -0
  70. package/src/encoder/EncodeOperation.ts +107 -65
  71. package/src/encoder/Encoder.ts +630 -119
  72. package/src/encoder/MapJournal.ts +124 -0
  73. package/src/encoder/RefIdAllocator.ts +68 -0
  74. package/src/encoder/Root.ts +247 -120
  75. package/src/encoder/StateView.ts +592 -121
  76. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  77. package/src/encoder/changeTree/liveIteration.ts +74 -0
  78. package/src/encoder/changeTree/parentChain.ts +131 -0
  79. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  80. package/src/encoder/streaming.ts +232 -0
  81. package/src/encoder/subscriptions.ts +71 -0
  82. package/src/index.ts +15 -3
  83. package/src/input/InputDecoder.ts +57 -0
  84. package/src/input/InputEncoder.ts +303 -0
  85. package/src/input/index.ts +3 -0
  86. package/src/types/HelperTypes.ts +21 -9
  87. package/src/types/TypeContext.ts +14 -2
  88. package/src/types/builder.ts +285 -0
  89. package/src/types/custom/ArraySchema.ts +210 -197
  90. package/src/types/custom/CollectionSchema.ts +115 -35
  91. package/src/types/custom/MapSchema.ts +162 -58
  92. package/src/types/custom/SetSchema.ts +128 -39
  93. package/src/types/custom/StreamSchema.ts +310 -0
  94. package/src/types/symbols.ts +54 -6
  95. package/src/utils.ts +4 -6
@@ -2,9 +2,16 @@ import { OPERATION } from "../../encoding/spec.js";
2
2
  import { registerType } from "../registry.js";
3
3
  import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd, $refId } from "../symbols.js";
4
4
  import { Collection } from "../HelperTypes.js";
5
- import { ChangeTree, type IRef } from "../../encoder/ChangeTree.js";
6
- import { encodeKeyValueOperation } from "../../encoder/EncodeOperation.js";
7
- import { decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
5
+ import { ChangeTree, installUntrackedChangeTree, type IRef } from "../../encoder/ChangeTree.js";
6
+ import { encodeIndexedEntry } from "../../encoder/EncodeOperation.js";
7
+ import { CollectionKind, decodeKeyValueOperation } from "../../decoder/DecodeOperation.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
 
@@ -14,14 +21,42 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
14
21
 
15
22
  protected [$childType]: string | typeof Schema;
16
23
 
24
+ /** The user-visible data, keyed directly by the wire-protocol index. */
17
25
  protected $items: Map<number, V> = new Map<number, V>();
18
- protected $indexes: Map<number, number> = new Map<number, number>();
26
+
27
+ /** Snapshots of values that were deleted this tick (for filter visibility). */
19
28
  protected deletedItems: { [field: string]: V } = {};
20
29
 
30
+ /** Monotonic counter for assigning indexes to newly-added items. */
21
31
  protected $refId: number = 0;
22
32
 
23
- static [$encoder] = encodeKeyValueOperation;
33
+ /**
34
+ * Streamable state — lazily allocated when the field is opted into
35
+ * streaming via `t.set(X).stream()`. See MapSchema for the same
36
+ * pattern / rationale.
37
+ */
38
+ _stream?: StreamableState;
39
+
40
+ /** Max ADD ops emitted per tick per view. Ignored outside streaming mode. */
41
+ get maxPerTick(): number {
42
+ return this._stream?.maxPerTick ?? 32;
43
+ }
44
+ set maxPerTick(n: number) {
45
+ (this._stream ??= createStreamableState()).maxPerTick = n;
46
+ }
47
+
48
+ /** Per-view priority callback — see StreamSchema / MapSchema. */
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;
24
57
  static [$decoder] = decodeKeyValueOperation;
58
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
59
+ static readonly COLLECTION_KIND = CollectionKind.Set;
25
60
 
26
61
  /**
27
62
  * Determine if a property must be filtered.
@@ -36,7 +71,7 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
36
71
  return (
37
72
  !view ||
38
73
  typeof (ref[$childType]) === "string" ||
39
- view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
74
+ view.isVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
40
75
  );
41
76
  }
42
77
 
@@ -45,40 +80,60 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
45
80
  }
46
81
 
47
82
  constructor (initialValues?: Array<V>) {
48
- this[$changes] = new ChangeTree(this);
49
- this[$changes].indexes = {};
83
+ // $changes must be non-enumerable to avoid deepStrictEqual recursing
84
+ // into ChangeTree's circular refs.
85
+ Object.defineProperty(this, $changes, {
86
+ value: new ChangeTree(this),
87
+ enumerable: false,
88
+ writable: true,
89
+ });
90
+ this[$childType] = undefined as any;
50
91
 
51
92
  if (initialValues) {
52
93
  initialValues.forEach((v) => this.add(v));
53
94
  }
95
+ }
54
96
 
55
- Object.defineProperty(this, $childType, {
56
- value: undefined,
57
- enumerable: false,
58
- writable: true,
59
- configurable: true,
60
- });
97
+ /**
98
+ * Decoder-side factory. Skips the tracking `ChangeTree` allocation;
99
+ * `Object.create` also bypasses the class-field initializers, so we
100
+ * replicate the minimum slot init here. Must stay in sync with the
101
+ * class-field declarations above.
102
+ */
103
+ static initializeForDecoder<V = any>(): SetSchema<V> {
104
+ const self: any = Object.create(SetSchema.prototype);
105
+ self.$items = new Map<number, V>();
106
+ self.deletedItems = {};
107
+ self.$refId = 0;
108
+ self[$childType] = undefined;
109
+ installUntrackedChangeTree(self);
110
+ return self;
61
111
  }
62
112
 
63
113
  add(value: V) {
64
114
  // immediatelly return false if value already added.
65
115
  if (this.has(value)) { return false; }
66
116
 
67
- // set "index" for reference.
117
+ // assign the next wire-protocol index
68
118
  const index = this.$refId++;
69
119
 
120
+ const changeTree = this[$changes];
70
121
  if ((value[$changes]) !== undefined) {
71
- value[$changes].setParent(this, this[$changes].root, index);
122
+ value[$changes].setParent(this, changeTree.root, index);
72
123
  }
73
124
 
74
- const operation = this[$changes].indexes[index]?.op ?? OPERATION.ADD;
75
-
76
- this[$changes].indexes[index] = index;
77
-
78
- this.$indexes.set(index, index);
79
125
  this.$items.set(index, value);
80
126
 
81
- this[$changes].change(index, operation);
127
+ // Streaming-mode ADD: route into per-view pending or broadcast
128
+ // pending instead of the tree's recorder. See MapSchema.set for
129
+ // the same branch / rationale.
130
+ if (changeTree.isStreamCollection) {
131
+ if (changeTree.root !== undefined) {
132
+ streamRouteAdd(this, changeTree.root, index);
133
+ }
134
+ } else {
135
+ changeTree.change(index, OPERATION.ADD);
136
+ }
82
137
  return index;
83
138
  }
84
139
 
@@ -104,8 +159,26 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
104
159
  return false;
105
160
  }
106
161
 
107
- this.deletedItems[index] = this[$changes].delete(index);
108
- this.$indexes.delete(index);
162
+ const changeTree = this[$changes];
163
+
164
+ // Streaming-mode: route through stream's pending/sent bookkeeping
165
+ // — silent drop if never sent to any view, force DELETE for views
166
+ // that already received it. Mirror of MapSchema.delete's streaming
167
+ // branch.
168
+ if (changeTree.isStreamCollection) {
169
+ const root = changeTree.root;
170
+ const previousValue = this.$items.get(index);
171
+ if (root !== undefined) {
172
+ streamRouteRemove(this, root, (this as any)[$refId], index);
173
+ }
174
+ if ((previousValue as any)?.[$changes] !== undefined) {
175
+ root?.remove((previousValue as any)[$changes]);
176
+ }
177
+ this.deletedItems[index] = previousValue as V;
178
+ return this.$items.delete(index);
179
+ }
180
+
181
+ this.deletedItems[index] = changeTree.delete(index);
109
182
 
110
183
  return this.$items.delete(index);
111
184
  }
@@ -114,11 +187,7 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
114
187
  const changeTree = this[$changes];
115
188
 
116
189
  // discard previous operations.
117
- changeTree.discard(true);
118
- changeTree.indexes = {};
119
-
120
- // clear previous indexes
121
- this.$indexes.clear();
190
+ changeTree.discard();
122
191
 
123
192
  // clear items
124
193
  this.$items.clear();
@@ -155,31 +224,51 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
155
224
  return this.$items.size;
156
225
  }
157
226
 
227
+ // ────── Change tracking control (same API as Schema) ──────
228
+ pauseTracking(): void { this[$changes].pause(); }
229
+ resumeTracking(): void { this[$changes].resume(); }
230
+ untracked<T>(fn: () => T): T { return this[$changes].untracked(fn); }
231
+ get isTrackingPaused(): boolean { return this[$changes].paused; }
232
+
158
233
  /** Iterator */
159
234
  [Symbol.iterator](): IterableIterator<V> {
160
235
  return this.$items.values();
161
236
  }
162
237
 
163
- protected setIndex(index: number, key: number) {
164
- this.$indexes.set(index, key);
238
+ // ────────────────────────────────────────────────────────────────────
239
+ // Decoder-side index hooks. SetSchema's "key" IS the wire index, so
240
+ // these are identity operations. Kept for protocol symmetry with
241
+ // MapSchema (decoder calls them polymorphically).
242
+ // ────────────────────────────────────────────────────────────────────
243
+
244
+ protected setIndex(_index: number, _key: number) {
245
+ // no-op: indexes are identity
165
246
  }
166
247
 
167
- protected getIndex(index: number) {
168
- return this.$indexes.get(index);
248
+ protected getIndex(index: number): number {
249
+ return index;
169
250
  }
170
251
 
171
252
  [$getByIndex](index: number): any {
172
- return this.$items.get(this.$indexes.get(index));
253
+ return this.$items.get(index);
173
254
  }
174
255
 
175
256
  [$deleteByIndex](index: number): void {
176
- const key = this.$indexes.get(index);
177
- this.$items.delete(key);
178
- this.$indexes.delete(index);
257
+ this.$items.delete(index);
179
258
  }
180
259
 
181
260
  protected [$onEncodeEnd]() {
182
- this.deletedItems = {};
261
+ for (const key in this.deletedItems) { delete this.deletedItems[key]; }
262
+ }
263
+
264
+ // ─── Streamable interface (Encoder priority / broadcast pass) ──────
265
+
266
+ _dropView(viewId: number): void {
267
+ streamDropView(this, viewId);
268
+ }
269
+
270
+ _unregister(): void {
271
+ // no-op — `Root.unregisterStream` handles the Set removal.
183
272
  }
184
273
 
185
274
  toArray() {
@@ -227,4 +316,4 @@ export class SetSchema<V=any> implements Collection<number, V>, IRef {
227
316
 
228
317
  }
229
318
 
230
- registerType("set", { constructor: SetSchema });
319
+ registerType("set", { constructor: SetSchema });
@@ -0,0 +1,310 @@
1
+ import { OPERATION } from "../../encoding/spec.js";
2
+ import { registerType } from "../registry.js";
3
+ import {
4
+ $changes,
5
+ $childType,
6
+ $decoder,
7
+ $deleteByIndex,
8
+ $encoder,
9
+ $filter,
10
+ $getByIndex,
11
+ $onEncodeEnd,
12
+ $refId,
13
+ } from "../symbols.js";
14
+ import { ChangeTree, installUntrackedChangeTree, type IRef } from "../../encoder/ChangeTree.js";
15
+ import { encodeIndexedEntry } from "../../encoder/EncodeOperation.js";
16
+ import { CollectionKind, decodeKeyValueOperation } from "../../decoder/DecodeOperation.js";
17
+ import {
18
+ createStreamableState,
19
+ streamDropView,
20
+ streamRouteAdd,
21
+ streamRouteClear,
22
+ streamRouteRemove,
23
+ type StreamableState,
24
+ } from "../../encoder/streaming.js";
25
+ import type { StateView } from "../../encoder/StateView.js";
26
+ import type { Schema } from "../../Schema.js";
27
+
28
+ /**
29
+ * `t.stream(Entity)` — priority-batched collection of Schema instances.
30
+ *
31
+ * Designed for ECS-style use cases where many entities spawn/despawn each
32
+ * tick and the full set won't fit in one encode budget. Adds are queued
33
+ * per-client and drained in priority order (callback on StateView) up to
34
+ * `maxPerTick` per encode pass. Field mutations on already-sent elements
35
+ * propagate through the normal reliable channel without consuming the
36
+ * per-tick budget. Chain `.static()` on the field builder to suppress
37
+ * post-add mutation tracking entirely.
38
+ */
39
+ export class StreamSchema<V = any> implements IRef {
40
+ [$changes]: ChangeTree;
41
+ [$refId]?: number;
42
+
43
+ protected [$childType]: string | typeof Schema;
44
+
45
+ /**
46
+ * Wire-keyed storage: `position → element`. Position is a monotonic
47
+ * counter assigned by `add()` — stable identity even when elements
48
+ * are removed, so pending/sent view state can keep using the same
49
+ * keys across ticks. Map (not Array) so `$items.keys()` / `.values()`
50
+ * skip removed positions without a sparse-slot check.
51
+ */
52
+ protected $items: Map<number, V> = new Map();
53
+
54
+ /** Monotonic position counter. Incremented on every `add()`. */
55
+ protected $nextPosition: number = 0;
56
+
57
+ /** Reverse lookup for O(1) `remove(el)`. */
58
+ protected _itemIndex: Map<V, number> = new Map();
59
+
60
+ /**
61
+ * Streamable state — holds per-view and broadcast bookkeeping. Lazily
62
+ * allocated when the stream is attached to a Root (or when the user
63
+ * touches `maxPerTick`). `undefined` on detached streams so
64
+ * construction is cheap.
65
+ */
66
+ _stream?: StreamableState;
67
+
68
+ /** Max element ADDs emitted per encode tick (per view, or broadcast). */
69
+ get maxPerTick(): number {
70
+ return this._stream?.maxPerTick ?? 32;
71
+ }
72
+ set maxPerTick(n: number) {
73
+ (this._stream ??= createStreamableState()).maxPerTick = n;
74
+ }
75
+
76
+ /**
77
+ * Per-view priority callback. Initialized from the schema declaration
78
+ * (`.priority(fn)` or `@type({ stream, priority })`); assigning here
79
+ * overrides the class-level default for this instance. Only fires
80
+ * during `encodeView` — broadcast mode drains FIFO.
81
+ */
82
+ get priority(): ((view: any, element: V) => number) | undefined {
83
+ return this._stream?.priority as ((view: any, element: V) => number) | undefined;
84
+ }
85
+ set priority(fn: ((view: any, element: V) => number) | undefined) {
86
+ (this._stream ??= createStreamableState()).priority = fn;
87
+ }
88
+
89
+ /**
90
+ * Brand used by Root / StateView to detect stream trees without
91
+ * importing this class (avoids circular deps). The `isStreamCollection`
92
+ * ChangeTree flag (set via `inheritedFlags`) is the preferred runtime
93
+ * check — this brand is kept for back-compat.
94
+ */
95
+ static readonly $isStream: true = true;
96
+
97
+ static [$encoder] = encodeIndexedEntry;
98
+ static [$decoder] = decodeKeyValueOperation;
99
+ /** Integer tag read by `decodeKeyValueOperation` — see `CollectionKind`. */
100
+ static readonly COLLECTION_KIND = CollectionKind.Stream;
101
+
102
+ /**
103
+ * Element-level visibility. Identical to SetSchema's filter: stream
104
+ * elements are always per-view, the filter just defers to the view's
105
+ * per-tree visibility bitmap.
106
+ */
107
+ static [$filter](ref: StreamSchema, index: number, view: StateView) {
108
+ if (!view) return true;
109
+ const value = (ref as any)[$getByIndex](index);
110
+ if (value === undefined) return false;
111
+ return view.isVisible(value[$changes]);
112
+ }
113
+
114
+ static is(type: any): boolean {
115
+ return type && type['stream'] !== undefined;
116
+ }
117
+
118
+ constructor() {
119
+ Object.defineProperty(this, $changes, {
120
+ value: new ChangeTree(this),
121
+ enumerable: false,
122
+ writable: true,
123
+ });
124
+ this[$childType] = undefined;
125
+ // `isFiltered` / `isStreamCollection` are set via `inheritedFlags`
126
+ // when this stream is attached to a parent field — no constructor-
127
+ // time init needed (the stream tree is inert until assignment).
128
+ }
129
+
130
+ /**
131
+ * Decoder-side factory. Skips the tracking `ChangeTree` allocation;
132
+ * `Object.create` also bypasses the class-field initializers, so we
133
+ * replicate the minimum slot init here. Must stay in sync with the
134
+ * class-field declarations above.
135
+ */
136
+ static initializeForDecoder<V = any>(): StreamSchema<V> {
137
+ const self: any = Object.create(StreamSchema.prototype);
138
+ self.$items = new Map<number, V>();
139
+ self.$nextPosition = 0;
140
+ self._itemIndex = new Map();
141
+ self[$childType] = undefined;
142
+ installUntrackedChangeTree(self);
143
+ return self;
144
+ }
145
+
146
+ /**
147
+ * Append an element to the stream. Returns the assigned position,
148
+ * or -1 if the element was already in the stream.
149
+ */
150
+ add(value: V): number {
151
+ if (this._itemIndex.has(value)) return -1;
152
+
153
+ const position = this.$nextPosition++;
154
+ this.$items.set(position, value);
155
+ this._itemIndex.set(value, position);
156
+
157
+ const tree = this[$changes];
158
+ const root = tree.root;
159
+
160
+ // Attach element as a child — assigns $refId and wires the parent
161
+ // chain so the element's own ChangeTree participates in encoding.
162
+ if (value[$changes] !== undefined) {
163
+ value[$changes].setParent(this, root, position);
164
+ }
165
+
166
+ if (root !== undefined) streamRouteAdd(this, root, position);
167
+ return position;
168
+ }
169
+
170
+ /**
171
+ * Remove an element by reference. If the element was pending (never sent
172
+ * to a view), the pending entry is dropped silently. If already sent,
173
+ * a DELETE op is forced on next `encodeView` for that view.
174
+ */
175
+ remove(value: V): boolean {
176
+ const position = this._itemIndex.get(value);
177
+ if (position === undefined) return false;
178
+
179
+ this._itemIndex.delete(value);
180
+ this.$items.delete(position);
181
+
182
+ const root = this[$changes].root;
183
+ if (root !== undefined) {
184
+ streamRouteRemove(this, root, (this as any)[$refId], position);
185
+ if (value[$changes] !== undefined) {
186
+ root.remove((value as any)[$changes]);
187
+ }
188
+ }
189
+
190
+ return true;
191
+ }
192
+
193
+ has(value: V): boolean {
194
+ return this._itemIndex.has(value);
195
+ }
196
+
197
+ /** Remove every element; queue DELETE wire ops for already-sent items. */
198
+ clear(): void {
199
+ const root = this[$changes].root;
200
+ if (root !== undefined) {
201
+ streamRouteClear(this, root, (this as any)[$refId]);
202
+ for (const el of this.$items.values()) {
203
+ if (el[$changes] !== undefined) {
204
+ root.remove((el as any)[$changes]);
205
+ }
206
+ }
207
+ }
208
+ this.$items.clear();
209
+ this._itemIndex.clear();
210
+ }
211
+
212
+ forEach(callback: (value: V, index: number, collection: StreamSchema<V>) => void): void {
213
+ for (const [index, value] of this.$items) callback(value, index, this);
214
+ }
215
+
216
+ values(): IterableIterator<V> {
217
+ return this.$items.values();
218
+ }
219
+
220
+ /**
221
+ * Iterate `[position, value]` pairs in insertion order. Used by
222
+ * `setParent` recursion when the stream is reassigned to a new parent.
223
+ */
224
+ entries(): IterableIterator<[number, V]> {
225
+ return this.$items.entries();
226
+ }
227
+
228
+ [Symbol.iterator](): IterableIterator<V> {
229
+ return this.$items.values();
230
+ }
231
+
232
+ /** Live element count. */
233
+ get size(): number {
234
+ return this.$items.size;
235
+ }
236
+
237
+ /** Alias for `size`. */
238
+ get length(): number {
239
+ return this.$items.size;
240
+ }
241
+
242
+ // ────────────────────────────────────────────────────────────────────
243
+ // Decoder / encoder plumbing — same shape as SetSchema so
244
+ // {encode,decode}KeyValueOperation can route uniformly. StreamSchema
245
+ // keys are identity (wire index === position), so `setIndex`/`getIndex`
246
+ // are no-ops / identity like SetSchema.
247
+ // ────────────────────────────────────────────────────────────────────
248
+
249
+ protected setIndex(_index: number, _key: number): void {
250
+ // no-op: indexes are identity
251
+ }
252
+
253
+ protected getIndex(index: number): number {
254
+ return index;
255
+ }
256
+
257
+ [$getByIndex](index: number): V {
258
+ return this.$items.get(index) as V;
259
+ }
260
+
261
+ [$deleteByIndex](index: number): void {
262
+ const value = this.$items.get(index);
263
+ if (value !== undefined) {
264
+ this._itemIndex.delete(value);
265
+ this.$items.delete(index);
266
+ }
267
+ }
268
+
269
+ protected [$onEncodeEnd](): void {
270
+ // No per-tick cleanup: pending/sent state spans encode ticks by design.
271
+ }
272
+
273
+ toArray(): V[] {
274
+ return Array.from(this.$items.values());
275
+ }
276
+
277
+ toJSON(): any[] {
278
+ const out: any[] = [];
279
+ this.forEach((v: any) => {
280
+ out.push(typeof v?.toJSON === "function" ? v.toJSON() : v);
281
+ });
282
+ return out;
283
+ }
284
+
285
+ clone(isDecoding?: boolean): StreamSchema<V> {
286
+ if (isDecoding) {
287
+ const cloned = Object.assign(new StreamSchema<V>(), this);
288
+ return cloned;
289
+ }
290
+ const cloned = new StreamSchema<V>();
291
+ cloned.maxPerTick = this.maxPerTick;
292
+ this.forEach((v: any) => {
293
+ cloned.add(typeof v?.clone === "function" ? v.clone() : v);
294
+ });
295
+ return cloned;
296
+ }
297
+
298
+ // ─── Streamable interface (Encoder priority / broadcast pass) ──────
299
+
300
+ _dropView(viewId: number): void {
301
+ streamDropView(this, viewId);
302
+ }
303
+
304
+ /** Called by Root.remove when the stream's refcount hits zero. */
305
+ _unregister(): void {
306
+ // no-op — `Root.unregisterStream` handles the Set removal.
307
+ }
308
+ }
309
+
310
+ registerType("stream", { constructor: StreamSchema });
@@ -1,4 +1,4 @@
1
- export const $refId = "~refId";
1
+ export const $refId: unique symbol = Symbol("$refId");
2
2
  export const $track = "~track";
3
3
  export const $encoder = "~encoder";
4
4
  export const $decoder = "~decoder";
@@ -9,15 +9,26 @@ export const $getByIndex = "~getByIndex";
9
9
  export const $deleteByIndex = "~deleteByIndex";
10
10
 
11
11
  /**
12
- * Used to hold ChangeTree instances whitin the structures
12
+ * Used to hold ChangeTree instances whitin the structures.
13
+ *
14
+ * Real JS Symbol — see the `$values` comment for rationale.
13
15
  */
14
- export const $changes = '~changes';
16
+ export const $changes: unique symbol = Symbol("$changes");
15
17
 
16
18
  /**
17
19
  * Used to keep track of the type of the child elements of a collection
18
- * (MapSchema, ArraySchema, etc.)
20
+ * (MapSchema, ArraySchema, etc.). Real Symbol — same rationale as $values.
19
21
  */
20
- export const $childType = '~childType';
22
+ export const $childType: unique symbol = Symbol("$childType");
23
+
24
+ /**
25
+ * Self-reference an instance sets on `this` so its own methods can recover
26
+ * the underlying object even when `this` is a Proxy wrapper. Used by
27
+ * ArraySchema (whose public API is a Proxy) to grab the underlying instance
28
+ * once at the top of hot methods and then access fields directly without
29
+ * paying the Proxy.get cost on every read.
30
+ */
31
+ export const $proxyTarget: unique symbol = Symbol("$proxyTarget");
21
32
 
22
33
  /**
23
34
  * Optional "discard" method for custom types (ArraySchema)
@@ -30,11 +41,48 @@ export const $onEncodeEnd = '~onEncodeEnd';
30
41
  */
31
42
  export const $onDecodeEnd = "~onDecodeEnd";
32
43
 
44
+ /**
45
+ * Per-instance dense array holding field values by index.
46
+ * Replaces per-field _fieldName shadow properties.
47
+ *
48
+ * Real JS Symbol (not "~"-prefixed string) so plain assignment is safe —
49
+ * symbols are non-enumerable to Object.keys / JSON.stringify / for-in,
50
+ * which means we can drop Object.defineProperty(...{ enumerable: false })
51
+ * and avoid the slow-path / dictionary-mode hazards that come with it.
52
+ */
53
+ export const $values: unique symbol = Symbol("$values");
54
+
55
+ /**
56
+ * Brand for FieldBuilder instances so schema() can detect them.
57
+ */
58
+ export const $builder = "~builder";
59
+
33
60
  /**
34
61
  * Metadata
35
62
  */
36
63
  export const $descriptors = "~descriptors";
64
+
65
+ /**
66
+ * Per-class bitmask: bit i set iff field i carries a @view tag.
67
+ * Lazily computed from $viewFieldIndexes on first encode pass.
68
+ * Skips the per-field metadata[i].tag property chase in the hot encode loop.
69
+ */
70
+ export const $filterBitmask = "~__filterBitmask";
71
+
72
+ /**
73
+ * Cached per-class encode descriptor: bundles encoder fn, filter fn,
74
+ * metadata, isSchema flag, and filterBitmask into one object stashed on
75
+ * the constructor. Replaces 5 separate per-tree property chases /
76
+ * function calls in the encode loop with a single property load.
77
+ */
78
+ export const $encodeDescriptor = "~__encodeDescriptor";
79
+ export const $encoders = "~encoders";
37
80
  export const $numFields = "~__numFields";
38
81
  export const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
39
82
  export const $viewFieldIndexes = "~__viewFieldIndexes";
40
- export const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
83
+ export const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
84
+ export const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
85
+ export const $transientFieldIndexes = "~__transientFieldIndexes";
86
+ export const $staticFieldIndexes = "~__staticFieldIndexes";
87
+ export const $streamFieldIndexes = "~__streamFieldIndexes";
88
+ export const $streamPriorities = "~__streamPriorities";
package/src/utils.ts CHANGED
@@ -35,15 +35,13 @@ export function dumpChanges(schema: Schema) {
35
35
  continue;
36
36
  }
37
37
 
38
- const changes = changeTree.indexedOperations;
39
-
40
38
  dump.refs.push(`refId#${changeTree.ref[$refId]}`);
41
- for (const index in changes) {
42
- const op = changes[index];
39
+ changeTree.forEach((index, op) => {
40
+ if (index < 0 || !op) return;
43
41
  const opName = OPERATION[op];
44
42
  if (!dump.ops[opName as keyof ChangeDump['ops']]) { dump.ops[opName as keyof ChangeDump['ops']] = 0; }
45
- dump.ops[OPERATION[op] as keyof ChangeDump['ops']]++;
46
- }
43
+ dump.ops[opName as keyof ChangeDump['ops']]++;
44
+ });
47
45
  current = current.next;
48
46
  }
49
47