@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.
- 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 -44
- 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 +8 -2
- 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/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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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(
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
248
|
+
protected getIndex(index: number): number {
|
|
249
|
+
return index;
|
|
169
250
|
}
|
|
170
251
|
|
|
171
252
|
[$getByIndex](index: number): any {
|
|
172
|
-
return this.$items.get(
|
|
253
|
+
return this.$items.get(index);
|
|
173
254
|
}
|
|
174
255
|
|
|
175
256
|
[$deleteByIndex](index: number): void {
|
|
176
|
-
|
|
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 });
|
package/src/types/symbols.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const $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 =
|
|
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 =
|
|
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
|
-
|
|
42
|
-
|
|
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[
|
|
46
|
-
}
|
|
43
|
+
dump.ops[opName as keyof ChangeDump['ops']]++;
|
|
44
|
+
});
|
|
47
45
|
current = current.next;
|
|
48
46
|
}
|
|
49
47
|
|