@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.
- package/README.md +2 -0
- package/build/Metadata.d.ts +55 -2
- package/build/Reflection.d.ts +24 -30
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +56 -13
- package/build/codegen/cli.cjs +84 -67
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/DecodeOperation.d.ts +48 -5
- package/build/decoder/Decoder.d.ts +2 -2
- package/build/decoder/strategy/Callbacks.d.ts +1 -1
- package/build/encoder/ChangeRecorder.d.ts +107 -0
- package/build/encoder/ChangeTree.d.ts +218 -69
- package/build/encoder/EncodeDescriptor.d.ts +63 -0
- package/build/encoder/EncodeOperation.d.ts +25 -2
- package/build/encoder/Encoder.d.ts +59 -3
- package/build/encoder/MapJournal.d.ts +62 -0
- package/build/encoder/RefIdAllocator.d.ts +35 -0
- package/build/encoder/Root.d.ts +94 -13
- package/build/encoder/StateView.d.ts +116 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
- package/build/encoder/changeTree/liveIteration.d.ts +3 -0
- package/build/encoder/changeTree/parentChain.d.ts +24 -0
- package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
- package/build/encoder/streaming.d.ts +73 -0
- package/build/encoder/subscriptions.d.ts +25 -0
- package/build/index.cjs +5202 -1552
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5202 -1552
- package/build/index.mjs +5193 -1552
- package/build/index.mjs.map +1 -1
- package/build/input/InputDecoder.d.ts +32 -0
- package/build/input/InputEncoder.d.ts +117 -0
- package/build/input/index.cjs +7429 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7426 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +22 -8
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +162 -0
- package/build/types/custom/ArraySchema.d.ts +25 -4
- package/build/types/custom/CollectionSchema.d.ts +30 -2
- package/build/types/custom/MapSchema.d.ts +52 -3
- package/build/types/custom/SetSchema.d.ts +32 -2
- package/build/types/custom/StreamSchema.d.ts +114 -0
- package/build/types/symbols.d.ts +48 -5
- package/package.json +9 -3
- package/src/Metadata.ts +258 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +308 -236
- package/src/bench_bloat.ts +173 -0
- package/src/bench_decode.ts +221 -0
- package/src/bench_decode_mem.ts +165 -0
- package/src/bench_encode.ts +108 -0
- package/src/bench_init.ts +150 -0
- package/src/bench_static.ts +109 -0
- package/src/bench_stream.ts +295 -0
- package/src/bench_view_cmp.ts +142 -0
- package/src/codegen/languages/csharp.ts +0 -24
- package/src/codegen/parser.ts +83 -61
- package/src/decoder/DecodeOperation.ts +168 -63
- package/src/decoder/Decoder.ts +20 -10
- package/src/decoder/ReferenceTracker.ts +4 -0
- package/src/decoder/strategy/Callbacks.ts +30 -26
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
- package/src/encoder/ChangeRecorder.ts +276 -0
- package/src/encoder/ChangeTree.ts +674 -519
- package/src/encoder/EncodeDescriptor.ts +213 -0
- package/src/encoder/EncodeOperation.ts +107 -65
- package/src/encoder/Encoder.ts +630 -119
- package/src/encoder/MapJournal.ts +124 -0
- package/src/encoder/RefIdAllocator.ts +68 -0
- package/src/encoder/Root.ts +247 -120
- package/src/encoder/StateView.ts +592 -121
- package/src/encoder/changeTree/inheritedFlags.ts +217 -0
- package/src/encoder/changeTree/liveIteration.ts +74 -0
- package/src/encoder/changeTree/parentChain.ts +131 -0
- package/src/encoder/changeTree/treeAttachment.ts +171 -0
- package/src/encoder/streaming.ts +232 -0
- package/src/encoder/subscriptions.ts +71 -0
- package/src/index.ts +15 -3
- package/src/input/InputDecoder.ts +57 -0
- package/src/input/InputEncoder.ts +303 -0
- package/src/input/index.ts +3 -0
- package/src/types/HelperTypes.ts +21 -9
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +285 -0
- package/src/types/custom/ArraySchema.ts +210 -197
- package/src/types/custom/CollectionSchema.ts +115 -35
- package/src/types/custom/MapSchema.ts +162 -58
- package/src/types/custom/SetSchema.ts +128 -39
- package/src/types/custom/StreamSchema.ts +310 -0
- package/src/types/symbols.ts +54 -6
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
this
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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(
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
235
|
+
protected getIndex(index: number): number {
|
|
236
|
+
return index;
|
|
165
237
|
}
|
|
166
238
|
|
|
167
239
|
[$getByIndex](index: number): any {
|
|
168
|
-
return this.$items.get(
|
|
240
|
+
return this.$items.get(index);
|
|
169
241
|
}
|
|
170
242
|
|
|
171
243
|
[$deleteByIndex](index: number): void {
|
|
172
|
-
|
|
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, $
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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:
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
164
|
+
let index = journal.indexOf(key);
|
|
101
165
|
let operation: OPERATION;
|
|
102
166
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
232
|
+
const index = this.journal.indexOf(key)!;
|
|
233
|
+
const previousValue = this.$items.get(key)!;
|
|
234
|
+
const changeTree = this[$changes];
|
|
161
235
|
|
|
162
|
-
|
|
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(
|
|
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
|
-
//
|
|
180
|
-
this
|
|
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
|
|
317
|
+
this.journal.setIndex(index, key);
|
|
214
318
|
}
|
|
215
319
|
|
|
216
320
|
protected getIndex(index: number) {
|
|
217
|
-
return this
|
|
321
|
+
return this.journal.keyOf(index);
|
|
218
322
|
}
|
|
219
323
|
|
|
220
324
|
[$getByIndex](index: number): V | undefined {
|
|
221
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
338
|
+
this.journal.cleanupAfterEncode();
|
|
339
|
+
}
|
|
232
340
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
347
|
+
_unregister(): void {
|
|
348
|
+
// no-op — `Root.unregisterStream` handles the Set removal.
|
|
245
349
|
}
|
|
246
350
|
|
|
247
351
|
toJSON() {
|