@colyseus/schema 5.0.10 → 5.0.12
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/build/Metadata.d.ts +20 -12
- package/build/annotations.d.ts +23 -10
- package/build/codegen/cli.cjs +615 -204
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/dart.d.ts +20 -0
- package/build/codegen/types.d.ts +20 -0
- package/build/decoder/Resync.d.ts +3 -3
- package/build/encoder/ChangeTree.d.ts +22 -10
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +393 -287
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +393 -287
- package/build/index.mjs +392 -286
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/StreamSchema.d.ts +1 -1
- package/build/types/symbols.d.ts +4 -10
- package/package.json +1 -1
- package/src/Metadata.ts +58 -31
- package/src/annotations.ts +56 -32
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/c.ts +21 -3
- package/src/codegen/languages/csharp.ts +7 -1
- package/src/codegen/languages/dart.ts +274 -0
- package/src/codegen/languages/haxe.ts +7 -1
- package/src/codegen/languages/lua.ts +16 -4
- package/src/codegen/languages/ts.ts +5 -0
- package/src/codegen/parser.ts +97 -3
- package/src/codegen/types.ts +24 -0
- package/src/decoder/Resync.ts +8 -8
- package/src/encoder/ChangeRecorder.ts +1 -1
- package/src/encoder/ChangeTree.ts +41 -28
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +100 -37
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +118 -47
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/streaming.ts +8 -0
- package/src/encoding/spec.ts +1 -1
- package/src/index.ts +2 -2
- package/src/types/builder.ts +35 -31
- package/src/types/custom/StreamSchema.ts +1 -1
- package/src/types/symbols.ts +4 -11
- package/src/bench_bloat.ts +0 -173
- package/src/bench_churn.ts +0 -121
- package/src/bench_decode.ts +0 -221
- package/src/bench_decode_mem.ts +0 -165
- package/src/bench_encode.ts +0 -108
- package/src/bench_init.ts +0 -150
- package/src/bench_static.ts +0 -109
- package/src/bench_stream.ts +0 -295
- package/src/bench_view_cmp.ts +0 -142
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - parentChain.ts addParent / removeParent / find / has / getAll
|
|
10
10
|
* - liveIteration.ts forEachLive
|
|
11
|
-
* - inheritedFlags.ts filter / unreliable /
|
|
11
|
+
* - inheritedFlags.ts filter / unreliable / patchOnly / static inheritance
|
|
12
12
|
* - treeAttachment.ts setRoot / setParent / forEachChild(+WithCtx)
|
|
13
13
|
*
|
|
14
14
|
* Public surface on ChangeTree is unchanged — methods are thin pass-throughs
|
|
@@ -97,18 +97,19 @@ export interface ChangeTreeNode {
|
|
|
97
97
|
changeTree: ChangeTree;
|
|
98
98
|
next?: ChangeTreeNode;
|
|
99
99
|
prev?: ChangeTreeNode;
|
|
100
|
-
position: number; //
|
|
100
|
+
position: number; // strictly increasing along the list — O(1) order test
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Linked list for change trees
|
|
104
104
|
export interface ChangeTreeList {
|
|
105
105
|
next?: ChangeTreeNode;
|
|
106
106
|
tail?: ChangeTreeNode;
|
|
107
|
+
nextPosition: number; // monotonic per drain cycle (resets when list empties)
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
// Linked list helper functions
|
|
110
111
|
export function createChangeTreeList(): ChangeTreeList {
|
|
111
|
-
return { next: undefined, tail: undefined };
|
|
112
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
export interface ParentChain {
|
|
@@ -117,10 +118,10 @@ export interface ParentChain {
|
|
|
117
118
|
next?: ParentChain;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
121
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
121
122
|
// field's annotation — inherited at setParent/setRoot time.
|
|
122
123
|
export const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
123
|
-
export const IS_UNRELIABLE = 8,
|
|
124
|
+
export const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
124
125
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
125
126
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
126
127
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -147,7 +148,7 @@ export const NEEDS_RESTAGE = 128;
|
|
|
147
148
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
148
149
|
* field mutations only) is designed later.
|
|
149
150
|
*/
|
|
150
|
-
export const INHERITABLE_FLAGS =
|
|
151
|
+
export const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
151
152
|
|
|
152
153
|
export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
153
154
|
ref: T;
|
|
@@ -170,8 +171,8 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
170
171
|
metadata: Metadata;
|
|
171
172
|
|
|
172
173
|
/**
|
|
173
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
174
|
-
*
|
|
174
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
175
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
175
176
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
176
177
|
* EncodeDescriptor.ts.
|
|
177
178
|
*/
|
|
@@ -232,7 +233,6 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
232
233
|
// per-view WeakSet lookups with direct bitwise ops.
|
|
233
234
|
// Lazy: undefined until the tree participates in any view.
|
|
234
235
|
visibleViews?: number[];
|
|
235
|
-
invisibleViews?: number[];
|
|
236
236
|
|
|
237
237
|
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
238
238
|
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
@@ -258,10 +258,10 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
258
258
|
set isNew(v: boolean) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
259
259
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
260
260
|
set isUnreliable(v: boolean) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
261
|
-
get
|
|
262
|
-
set
|
|
263
|
-
get
|
|
264
|
-
set
|
|
261
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
262
|
+
set isPatchOnly(v: boolean) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
263
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
264
|
+
set isFullStateOnly(v: boolean) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
265
265
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
266
266
|
set isStreamCollection(v: boolean) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
267
267
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -271,7 +271,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
271
271
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
272
272
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
273
273
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
274
|
-
// pattern as `
|
|
274
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
275
275
|
get hasFilteredFields(): boolean {
|
|
276
276
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
277
277
|
}
|
|
@@ -297,7 +297,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
297
297
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
298
298
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
299
299
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
300
|
-
// (
|
|
300
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
301
301
|
const desc = this.encDescriptor;
|
|
302
302
|
if (!desc.hasAnyUnreliable) return false;
|
|
303
303
|
if (index < 32) return (desc.unreliableBitmask & (1 << index)) !== 0;
|
|
@@ -306,12 +306,12 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
306
306
|
|
|
307
307
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
308
308
|
// by the tracker (the value still lives on the instance).
|
|
309
|
-
|
|
310
|
-
if (this.
|
|
309
|
+
isFieldFullStateOnly(index: number): boolean {
|
|
310
|
+
if (this.isFullStateOnly) return true;
|
|
311
311
|
const desc = this.encDescriptor;
|
|
312
|
-
if (!desc.
|
|
313
|
-
if (index < 32) return (desc.
|
|
314
|
-
return Metadata.
|
|
312
|
+
if (!desc.hasAnyFullStateOnly) return false;
|
|
313
|
+
if (index < 32) return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
314
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
@@ -566,7 +566,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
566
566
|
this.unreliableRecorder?.reset();
|
|
567
567
|
|
|
568
568
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
569
|
-
// (FILTERED/
|
|
569
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
570
570
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
571
571
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
572
572
|
this._fullSyncGen = 0;
|
|
@@ -586,7 +586,6 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
586
586
|
// per-view visibility lives on the tree (NOT keyed by refId), so a
|
|
587
587
|
// recycled tree must not inherit its previous life's view membership.
|
|
588
588
|
this.visibleViews = undefined;
|
|
589
|
-
this.invisibleViews = undefined;
|
|
590
589
|
this.tagViews = undefined;
|
|
591
590
|
this.subscribedViews = undefined;
|
|
592
591
|
}
|
|
@@ -604,7 +603,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
604
603
|
if (this._isSchema) throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
605
604
|
const src = this.collDirty!;
|
|
606
605
|
const dst = new Map<number, OPERATION>();
|
|
607
|
-
const track = !this.paused && !this.
|
|
606
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
608
607
|
if (track) {
|
|
609
608
|
for (let i = 0; i < count; i++) dst.set(i, OPERATION.ADD);
|
|
610
609
|
}
|
|
@@ -627,7 +626,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
627
626
|
}
|
|
628
627
|
|
|
629
628
|
operation(op: OPERATION) {
|
|
630
|
-
if (this.paused || this.
|
|
629
|
+
if (this.paused || this.isFullStateOnly) return;
|
|
631
630
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
632
631
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
633
632
|
//
|
|
@@ -657,10 +656,22 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
657
656
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
658
657
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
659
658
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
659
|
+
*
|
|
660
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
661
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
662
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
663
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
664
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
665
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
666
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
667
|
+
* which has always seeded these fields for late joiners.
|
|
668
|
+
*
|
|
669
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
670
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
660
671
|
*/
|
|
661
672
|
private _routeAndRecord(index: number, op: OPERATION, raw: boolean): void {
|
|
662
|
-
if (this.paused || this.
|
|
663
|
-
if (this.isFieldUnreliable(index)) {
|
|
673
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return;
|
|
674
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
664
675
|
const r = this.ensureUnreliableRecorder();
|
|
665
676
|
if (raw) r.recordRaw(index, op);
|
|
666
677
|
else r.record(index, op);
|
|
@@ -724,9 +735,11 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
724
735
|
return;
|
|
725
736
|
}
|
|
726
737
|
|
|
727
|
-
if (this.paused || this.
|
|
738
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return this.getValue(index);
|
|
728
739
|
|
|
729
|
-
|
|
740
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
741
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
742
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
730
743
|
if (unreliable) this.ensureUnreliableRecorder().recordDelete(index, operation ?? OPERATION.DELETE);
|
|
731
744
|
else this.recordDelete(index, operation ?? OPERATION.DELETE);
|
|
732
745
|
|
|
@@ -10,14 +10,13 @@
|
|
|
10
10
|
* ctor[$filter]
|
|
11
11
|
* ctor[Symbol.metadata]
|
|
12
12
|
* Metadata.isValidInstance(ref)
|
|
13
|
-
* getFilterBitmask(metadata)
|
|
14
13
|
*
|
|
15
14
|
* Lives in its own file to break the Encoder.ts ↔ ChangeTree.ts import
|
|
16
15
|
* cycle (ChangeTree caches descriptors at construction; Encoder reads them
|
|
17
16
|
* during encode).
|
|
18
17
|
*/
|
|
19
18
|
import { Metadata } from "../Metadata.js";
|
|
20
|
-
import { $encodeDescriptor, $encoder, $encoders, $filter, $
|
|
19
|
+
import { $encodeDescriptor, $encoder, $encoders, $filter, $numFields, $fullStateOnlyFieldIndexes, $streamFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "../types/symbols.js";
|
|
21
20
|
import type { StateView } from "./StateView.js";
|
|
22
21
|
import type { EncodeOperation } from "./EncodeOperation.js";
|
|
23
22
|
|
|
@@ -28,35 +27,34 @@ export interface EncodeDescriptor {
|
|
|
28
27
|
isSchema: boolean;
|
|
29
28
|
/**
|
|
30
29
|
* Bit i set iff field i has a @view tag. 0 for collection trees.
|
|
31
|
-
* Lets `encodeChangeCb` do a single bitwise op instead of a
|
|
32
|
-
*
|
|
30
|
+
* Lets `encodeChangeCb` do a single bitwise op instead of a per-field
|
|
31
|
+
* metadata[i]?.tag chase. Fields 0–31 only, like the bitmasks below —
|
|
32
|
+
* `encodeChangeCb` reads `tags` past that.
|
|
33
33
|
*/
|
|
34
34
|
filterBitmask: number;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Class-level "any field has the flag" booleans + per-field bitmasks.
|
|
38
|
-
* Hot path: per-mutation `_routeAndRecord` calls `
|
|
38
|
+
* Hot path: per-mutation `_routeAndRecord` calls `isFieldFullStateOnly` and
|
|
39
39
|
* `isFieldUnreliable`. The common case is "no static/unreliable fields
|
|
40
40
|
* anywhere on this class" (booleans short-circuit before the symbol-keyed
|
|
41
41
|
* metadata lookup); the secondary common case is "this class has some
|
|
42
42
|
* such fields and we need to know if THIS field is one" — the bitmask
|
|
43
43
|
* answers in one bitwise op instead of an `Array.includes` linear scan.
|
|
44
44
|
*
|
|
45
|
-
* Bitmasks cover fields 0–31 only
|
|
46
|
-
*
|
|
47
|
-
* filter-bitmask path.
|
|
45
|
+
* Bitmasks cover fields 0–31 only — shift counts wrap at 32. Fields ≥32
|
|
46
|
+
* fall back to `Metadata.hasXAtIndex`.
|
|
48
47
|
*/
|
|
49
|
-
|
|
48
|
+
hasAnyFullStateOnly: boolean;
|
|
50
49
|
hasAnyUnreliable: boolean;
|
|
51
50
|
hasAnyStream: boolean;
|
|
52
51
|
/**
|
|
53
|
-
* Class-level "any field carries a `@view` tag"
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* decide whether a parent tree must be included in a view's bootstrap.
|
|
52
|
+
* Class-level "any field carries a `@view` tag". Read by
|
|
53
|
+
* `ChangeTree.hasFilteredFields` to decide whether a parent tree must
|
|
54
|
+
* be included in a view's bootstrap.
|
|
57
55
|
*/
|
|
58
56
|
hasAnyView: boolean;
|
|
59
|
-
|
|
57
|
+
fullStateOnlyBitmask: number;
|
|
60
58
|
unreliableBitmask: number;
|
|
61
59
|
/**
|
|
62
60
|
* Bit i set iff field i holds a `t.stream(...)` collection. Hot encode
|
|
@@ -84,29 +82,10 @@ export interface EncodeDescriptor {
|
|
|
84
82
|
encoders: (((bytes: Uint8Array, value: any, it: any) => void) | undefined)[];
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
function computeFilterBitmask(metadata: any): number {
|
|
88
|
-
if (metadata === undefined) return 0;
|
|
89
|
-
let bm: number | undefined = metadata[$filterBitmask];
|
|
90
|
-
if (bm !== undefined) return bm;
|
|
91
|
-
bm = 0;
|
|
92
|
-
const tagged = metadata[$viewFieldIndexes];
|
|
93
|
-
if (tagged !== undefined) {
|
|
94
|
-
for (let i = 0, len = tagged.length; i < len; i++) bm |= (1 << tagged[i]);
|
|
95
|
-
}
|
|
96
|
-
// Non-enumerable so `for (const k in metadata)` iteration in TypeContext
|
|
97
|
-
// and elsewhere doesn't mistake this cache for a real field index.
|
|
98
|
-
Object.defineProperty(metadata, $filterBitmask, {
|
|
99
|
-
value: bm,
|
|
100
|
-
enumerable: false,
|
|
101
|
-
writable: true,
|
|
102
|
-
configurable: true,
|
|
103
|
-
});
|
|
104
|
-
return bm;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
85
|
/**
|
|
108
86
|
* Bitmask of field indexes 0–31 in `indexes`. For fields ≥32 callers must
|
|
109
|
-
* fall back to the array lookup
|
|
87
|
+
* fall back to the array lookup — shift counts wrap at 32, so an unguarded
|
|
88
|
+
* `1 << 40` would set bit 8 and misclassify field 8.
|
|
110
89
|
*/
|
|
111
90
|
function indexesToBitmask(indexes: number[] | undefined): number {
|
|
112
91
|
if (indexes === undefined) return 0;
|
|
@@ -190,12 +169,12 @@ export function getEncodeDescriptor(ref: any): EncodeDescriptor {
|
|
|
190
169
|
filter,
|
|
191
170
|
metadata,
|
|
192
171
|
isSchema,
|
|
193
|
-
filterBitmask: isSchema ?
|
|
194
|
-
|
|
172
|
+
filterBitmask: isSchema ? indexesToBitmask(metadata?.[$viewFieldIndexes]) : 0,
|
|
173
|
+
hasAnyFullStateOnly: (metadata?.[$fullStateOnlyFieldIndexes]?.length ?? 0) > 0,
|
|
195
174
|
hasAnyUnreliable: (metadata?.[$unreliableFieldIndexes]?.length ?? 0) > 0,
|
|
196
175
|
hasAnyStream: (metadata?.[$streamFieldIndexes]?.length ?? 0) > 0,
|
|
197
176
|
hasAnyView,
|
|
198
|
-
|
|
177
|
+
fullStateOnlyBitmask: indexesToBitmask(metadata?.[$fullStateOnlyFieldIndexes]),
|
|
199
178
|
unreliableBitmask: indexesToBitmask(metadata?.[$unreliableFieldIndexes]),
|
|
200
179
|
streamBitmask: indexesToBitmask(metadata?.[$streamFieldIndexes]),
|
|
201
180
|
names: arrays.names,
|
|
@@ -75,7 +75,9 @@ export const encodeSchemaOperation: EncodeOperation = function <T extends Schema
|
|
|
75
75
|
_: any,
|
|
76
76
|
__: any,
|
|
77
77
|
) {
|
|
78
|
-
// "compress" field index + operation
|
|
78
|
+
// "compress" field index + operation. Can't collide with
|
|
79
|
+
// SWITCH_TO_STRUCTURE (255): that needs `DELETE_AND_ADD | 63`, and
|
|
80
|
+
// `Metadata.MAX_FIELDS` keeps index 63 unassignable.
|
|
79
81
|
bytes[it.offset++] = (index | operation) & 255;
|
|
80
82
|
|
|
81
83
|
// Do not encode value for DELETE operations
|
package/src/encoder/Encoder.ts
CHANGED
|
@@ -43,12 +43,21 @@ interface EncodeCtx {
|
|
|
43
43
|
emitFiltered: boolean;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Bitmask: bit i set iff field i has a @view tag. Lets the
|
|
47
|
-
* filter check be a single bitwise op instead of a
|
|
48
|
-
* Always 0 for collection trees.
|
|
46
|
+
* Bitmask: bit i set iff field i has a @view tag, for i < 32. Lets the
|
|
47
|
+
* per-field filter check be a single bitwise op instead of a
|
|
48
|
+
* metadata[i]?.tag chase. Always 0 for collection trees.
|
|
49
49
|
*/
|
|
50
50
|
filterBitmask: number;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Per-field @view tags of the current tree (`undefined` where untagged),
|
|
54
|
+
* covering the fields the bitmask can't reach. Empty for collections.
|
|
55
|
+
* Read through the ctx rather than `changeTree.encDescriptor`: a
|
|
56
|
+
* multi-hop chain here costs ~2% on full-sync even though only Schemas
|
|
57
|
+
* with 32+ fields ever evaluate it.
|
|
58
|
+
*/
|
|
59
|
+
tags: (number | undefined)[];
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Current walk's visit stamp. `_fullSyncWalk` compares it against each
|
|
54
63
|
* tree's `_fullSyncGen` on entry: match means "already visited by
|
|
@@ -119,16 +128,7 @@ function _fullSyncWalk(ctx: EncodeCtx, changeTree: ChangeTree): void {
|
|
|
119
128
|
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
120
129
|
// nothing itself but we still recurse so descendants (possibly added to
|
|
121
130
|
// the view explicitly) are reachable.
|
|
122
|
-
|
|
123
|
-
if (ctx.hasView) {
|
|
124
|
-
const view = ctx.view!;
|
|
125
|
-
if (!view.isChangeTreeVisible(changeTree)) {
|
|
126
|
-
view.markInvisible(changeTree);
|
|
127
|
-
visibleHere = false;
|
|
128
|
-
} else {
|
|
129
|
-
view.unmarkInvisible(changeTree);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
131
|
+
const visibleHere = !ctx.hasView || ctx.view!.isChangeTreeVisible(changeTree);
|
|
132
132
|
|
|
133
133
|
if (visibleHere) {
|
|
134
134
|
const desc = changeTree.encDescriptor;
|
|
@@ -140,6 +140,7 @@ function _fullSyncWalk(ctx: EncodeCtx, changeTree: ChangeTree): void {
|
|
|
140
140
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
141
141
|
ctx.isSchema = desc.isSchema;
|
|
142
142
|
ctx.filterBitmask = desc.filterBitmask;
|
|
143
|
+
ctx.tags = desc.tags;
|
|
143
144
|
ctx.structSwitchEmitted = false;
|
|
144
145
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
145
146
|
|
|
@@ -183,10 +184,13 @@ function encodeChangeCb(ctx: EncodeCtx, fieldIndex: number, op: OPERATION): void
|
|
|
183
184
|
|
|
184
185
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
185
186
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
186
|
-
// itself carries a @view tag.
|
|
187
|
-
//
|
|
187
|
+
// itself carries a @view tag. The bitmask only spans 0–31 — `1 << 40`
|
|
188
|
+
// wraps onto bit 8 — so fields past it read their tag directly. Reaching
|
|
189
|
+
// that arm needs a Schema with more than 32 fields.
|
|
188
190
|
const fieldFiltered = ctx.isSchema
|
|
189
|
-
? (ctx.treeIsFiltered || (
|
|
191
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
192
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
193
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
190
194
|
: ctx.treeIsFiltered;
|
|
191
195
|
if (fieldFiltered !== ctx.emitFiltered) return;
|
|
192
196
|
|
|
@@ -248,7 +252,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
248
252
|
ref: undefined, encoder: undefined!, filter: undefined, metadata: undefined,
|
|
249
253
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
250
254
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
251
|
-
filterBitmask: 0,
|
|
255
|
+
filterBitmask: 0, tags: undefined!,
|
|
252
256
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
253
257
|
gen: 0, initialOffset: 0, rootChangeTree: undefined!,
|
|
254
258
|
};
|
|
@@ -313,12 +317,8 @@ export class Encoder<T extends Schema = any> {
|
|
|
313
317
|
while (current = current.next) {
|
|
314
318
|
const changeTree = (current as ChangeTreeNode).changeTree;
|
|
315
319
|
|
|
316
|
-
if (hasView) {
|
|
317
|
-
|
|
318
|
-
view.markInvisible(changeTree);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
view.unmarkInvisible(changeTree);
|
|
320
|
+
if (hasView && !view.isChangeTreeVisible(changeTree)) {
|
|
321
|
+
continue;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
@@ -333,6 +333,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
333
333
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
334
334
|
ctx.isSchema = desc.isSchema;
|
|
335
335
|
ctx.filterBitmask = desc.filterBitmask;
|
|
336
|
+
ctx.tags = desc.tags;
|
|
336
337
|
ctx.structSwitchEmitted = false;
|
|
337
338
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
338
339
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -652,7 +653,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
652
653
|
// Emit each element's full state — forEachLive walks populated
|
|
653
654
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
654
655
|
// Covers both static elements (dirty state was reset by
|
|
655
|
-
// inheritedFlags'
|
|
656
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
656
657
|
// has dirty state but the main loop skipped them because
|
|
657
658
|
// they're filtered).
|
|
658
659
|
for (const element of emittedElements) {
|
|
@@ -731,24 +732,86 @@ export class Encoder<T extends Schema = any> {
|
|
|
731
732
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
732
733
|
// into `_stream.priority` when the stream was attached. Users
|
|
733
734
|
// can also override per-instance by assigning to the setter.
|
|
735
|
+
// A per-view callback (registered by `subscribe(coll, fn)`)
|
|
736
|
+
// wins over the declaration-scope one: it closes over the
|
|
737
|
+
// client's own entity, so it needs no view-carried anchor.
|
|
738
|
+
const perView = st.priorityByView?.get(viewId);
|
|
739
|
+
const usePerView = perView !== undefined;
|
|
734
740
|
const priority = st.priority;
|
|
741
|
+
const max = st.maxPerTick;
|
|
735
742
|
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
743
|
+
// Select the `max` highest-priority candidates.
|
|
744
|
+
//
|
|
745
|
+
// A comparator-based sort invokes the callback twice per
|
|
746
|
+
// comparison, each with its own `$getByIndex` lookup — ~2·n·log n
|
|
747
|
+
// of each to pick `max` entries (38k calls to select 8 out of a
|
|
748
|
+
// 2000-entry backlog). Scoring every candidate once and keeping a
|
|
749
|
+
// bounded top-`max` window costs n invocations instead, and sizes
|
|
750
|
+
// the scratch by `max` rather than by the backlog.
|
|
751
|
+
//
|
|
752
|
+
// Ties keep the earlier position (both comparisons below are
|
|
753
|
+
// strict), so equal-priority entries still drain in insertion
|
|
754
|
+
// order.
|
|
739
755
|
const positions: number[] = [];
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (priority !== undefined) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
)
|
|
756
|
+
const stale: number[] = [];
|
|
757
|
+
|
|
758
|
+
if (usePerView || priority !== undefined) {
|
|
759
|
+
const bestPos: number[] = [];
|
|
760
|
+
const bestScore: number[] = [];
|
|
761
|
+
let filled = 0;
|
|
762
|
+
|
|
763
|
+
for (const pos of pending) {
|
|
764
|
+
// Symbol-keyed accessor so Map/Set/Stream all route
|
|
765
|
+
// through the same lookup regardless of $items layout.
|
|
766
|
+
const element = s[$getByIndex](pos);
|
|
767
|
+
if (element === undefined) {
|
|
768
|
+
// Removed after being queued — drop it below without
|
|
769
|
+
// spending budget on it.
|
|
770
|
+
stale.push(pos);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const score = usePerView
|
|
775
|
+
? perView!(element)
|
|
776
|
+
: priority!(view, element);
|
|
777
|
+
|
|
778
|
+
// Window not yet full: always insert.
|
|
779
|
+
if (filled < max) {
|
|
780
|
+
let j = filled++;
|
|
781
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
782
|
+
bestScore[j] = bestScore[j - 1];
|
|
783
|
+
bestPos[j] = bestPos[j - 1];
|
|
784
|
+
j--;
|
|
785
|
+
}
|
|
786
|
+
bestScore[j] = score;
|
|
787
|
+
bestPos[j] = pos;
|
|
788
|
+
|
|
789
|
+
// Otherwise only a strictly better score displaces the tail.
|
|
790
|
+
} else if (score > bestScore[max - 1]) {
|
|
791
|
+
let j = max - 1;
|
|
792
|
+
while (j > 0 && bestScore[j - 1] < score) {
|
|
793
|
+
bestScore[j] = bestScore[j - 1];
|
|
794
|
+
bestPos[j] = bestPos[j - 1];
|
|
795
|
+
j--;
|
|
796
|
+
}
|
|
797
|
+
bestScore[j] = score;
|
|
798
|
+
bestPos[j] = pos;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
for (let i = 0; i < filled; i++) positions.push(bestPos[i]);
|
|
803
|
+
|
|
804
|
+
} else {
|
|
805
|
+
// FIFO — take the head of the backlog, no scoring needed.
|
|
806
|
+
for (const pos of pending) {
|
|
807
|
+
if (positions.length >= max) break;
|
|
808
|
+
positions.push(pos);
|
|
809
|
+
}
|
|
748
810
|
}
|
|
749
811
|
|
|
750
|
-
const
|
|
751
|
-
|
|
812
|
+
for (const pos of stale) pending.delete(pos);
|
|
813
|
+
|
|
814
|
+
const count = positions.length;
|
|
752
815
|
|
|
753
816
|
let sent: Set<number> | undefined = st.sentByView.get(viewId);
|
|
754
817
|
if (sent === undefined) {
|
package/src/encoder/Root.ts
CHANGED
|
@@ -162,7 +162,7 @@ export class Root {
|
|
|
162
162
|
const previousRefCount = this.refCount[refId];
|
|
163
163
|
if (previousRefCount === 0 || changeTree.needsRestage) {
|
|
164
164
|
//
|
|
165
|
-
// Re-stage every currently-populated non-
|
|
165
|
+
// Re-stage every currently-populated non-patchOnly index as a
|
|
166
166
|
// fresh ADD in the matching dirty bucket so the next encode
|
|
167
167
|
// re-emits it on the correct channel. Two triggers:
|
|
168
168
|
// - refCount 0: a previously-removed tree re-added under the
|
|
@@ -270,13 +270,9 @@ export class Root {
|
|
|
270
270
|
const parentNode = parent[$changes][nodeField];
|
|
271
271
|
if (!parentNode || parentNode === node) return;
|
|
272
272
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (cursor === node) return; // already after parent
|
|
277
|
-
cursor = cursor.next;
|
|
278
|
-
}
|
|
279
|
-
// If we reach here, node is before parent — need to move
|
|
273
|
+
// Positions are strictly increasing along the list, so this is an
|
|
274
|
+
// exact O(1) "is child already after parent" test — no queue scan.
|
|
275
|
+
if (node.position > parentNode.position) return;
|
|
280
276
|
|
|
281
277
|
// Remove node from current position
|
|
282
278
|
if (node.prev) {
|
|
@@ -291,17 +287,18 @@ export class Root {
|
|
|
291
287
|
changeSet.tail = node.prev;
|
|
292
288
|
}
|
|
293
289
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
290
|
+
// Re-append at the tail: after `parentNode` AND after every other
|
|
291
|
+
// queued parent of a multi-referenced instance — relinking next to
|
|
292
|
+
// the *primary* parent could jump the child ahead of a 2nd/3rd
|
|
293
|
+
// parent whose ADD the decoder must see first. Tail placement gets
|
|
294
|
+
// a fresh max position, keeping the invariant append-only.
|
|
295
|
+
// (`recursivelyMoveNextToParent` visits pre-order, so a moved
|
|
296
|
+
// subtree re-serializes parent-first behind it.)
|
|
297
|
+
node.prev = changeSet.tail;
|
|
298
|
+
node.next = undefined;
|
|
299
|
+
changeSet.tail!.next = node; // parentNode remains in the list — never empty here
|
|
300
|
+
changeSet.tail = node;
|
|
301
|
+
node.position = changeSet.nextPosition++;
|
|
305
302
|
}
|
|
306
303
|
|
|
307
304
|
public enqueueChangeTree(
|
|
@@ -328,11 +325,11 @@ export class Root {
|
|
|
328
325
|
node.changeTree = changeTree;
|
|
329
326
|
node.next = undefined;
|
|
330
327
|
node.prev = undefined;
|
|
331
|
-
node.position = 0;
|
|
332
328
|
} else {
|
|
333
329
|
node = { changeTree, next: undefined, prev: undefined, position: 0 };
|
|
334
330
|
}
|
|
335
331
|
if (!list.next) {
|
|
332
|
+
list.nextPosition = 0; // list drained — restart sequence (stays SMI)
|
|
336
333
|
list.next = node;
|
|
337
334
|
list.tail = node;
|
|
338
335
|
} else {
|
|
@@ -340,6 +337,7 @@ export class Root {
|
|
|
340
337
|
list.tail!.next = node;
|
|
341
338
|
list.tail = node;
|
|
342
339
|
}
|
|
340
|
+
node.position = list.nextPosition++;
|
|
343
341
|
return node;
|
|
344
342
|
}
|
|
345
343
|
|