@colyseus/schema 5.0.11 → 5.0.13
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 +25 -9
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -2
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/changeTree/parentChain.d.ts +9 -0
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +449 -233
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +449 -233
- package/build/index.mjs +448 -232
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/ArraySchema.d.ts +17 -0
- 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 +46 -26
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +97 -21
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +102 -12
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/changeTree/parentChain.ts +29 -0
- 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/ArraySchema.ts +40 -1
- 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
|
|
@@ -34,6 +34,7 @@ import type { DecodeOperation } from "../decoder/DecodeOperation.js";
|
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
36
|
addParent as _addParent, removeParent as _removeParent,
|
|
37
|
+
setParentIndex as _setParentIndex,
|
|
37
38
|
findParent as _findParent, hasParent as _hasParent,
|
|
38
39
|
getAllParents as _getAllParents,
|
|
39
40
|
} from "./changeTree/parentChain.js";
|
|
@@ -97,18 +98,19 @@ export interface ChangeTreeNode {
|
|
|
97
98
|
changeTree: ChangeTree;
|
|
98
99
|
next?: ChangeTreeNode;
|
|
99
100
|
prev?: ChangeTreeNode;
|
|
100
|
-
position: number; //
|
|
101
|
+
position: number; // strictly increasing along the list — O(1) order test
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// Linked list for change trees
|
|
104
105
|
export interface ChangeTreeList {
|
|
105
106
|
next?: ChangeTreeNode;
|
|
106
107
|
tail?: ChangeTreeNode;
|
|
108
|
+
nextPosition: number; // monotonic per drain cycle (resets when list empties)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// Linked list helper functions
|
|
110
112
|
export function createChangeTreeList(): ChangeTreeList {
|
|
111
|
-
return { next: undefined, tail: undefined };
|
|
113
|
+
return { next: undefined, tail: undefined, nextPosition: 0 };
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
export interface ParentChain {
|
|
@@ -117,10 +119,10 @@ export interface ParentChain {
|
|
|
117
119
|
next?: ParentChain;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
// Flags bitfield. *_UNRELIABLE /
|
|
122
|
+
// Flags bitfield. *_UNRELIABLE / _PATCH_ONLY / _STATIC mirror the parent
|
|
121
123
|
// field's annotation — inherited at setParent/setRoot time.
|
|
122
124
|
export const IS_FILTERED = 1, IS_VISIBILITY_SHARED = 2, IS_NEW = 4;
|
|
123
|
-
export const IS_UNRELIABLE = 8,
|
|
125
|
+
export const IS_UNRELIABLE = 8, IS_PATCH_ONLY = 16, IS_FULL_STATE_ONLY = 32;
|
|
124
126
|
// Collection tree attached to a parent field annotated `.stream()` —
|
|
125
127
|
// drives the encoder's priority/broadcast pass. Set in inheritedFlags
|
|
126
128
|
// so both `t.stream(X)` (via StreamSchema's `$isStream` brand) and
|
|
@@ -147,7 +149,7 @@ export const NEEDS_RESTAGE = 128;
|
|
|
147
149
|
* reconsidered if a safe semantics (e.g. reliable ADD + unreliable
|
|
148
150
|
* field mutations only) is designed later.
|
|
149
151
|
*/
|
|
150
|
-
export const INHERITABLE_FLAGS =
|
|
152
|
+
export const INHERITABLE_FLAGS = IS_PATCH_ONLY | IS_FULL_STATE_ONLY;
|
|
151
153
|
|
|
152
154
|
export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
153
155
|
ref: T;
|
|
@@ -170,8 +172,8 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
170
172
|
metadata: Metadata;
|
|
171
173
|
|
|
172
174
|
/**
|
|
173
|
-
* Per-class cache of encoder fn / filter fn / isSchema /
|
|
174
|
-
*
|
|
175
|
+
* Per-class cache of encoder fn / filter fn / isSchema / metadata /
|
|
176
|
+
* per-field arrays, looked up once at construction. The encode loop reads
|
|
175
177
|
* `tree.encDescriptor` and never touches `ref.constructor` again. See
|
|
176
178
|
* EncodeDescriptor.ts.
|
|
177
179
|
*/
|
|
@@ -257,10 +259,10 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
257
259
|
set isNew(v: boolean) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
258
260
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
259
261
|
set isUnreliable(v: boolean) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
260
|
-
get
|
|
261
|
-
set
|
|
262
|
-
get
|
|
263
|
-
set
|
|
262
|
+
get isPatchOnly() { return (this.flags & IS_PATCH_ONLY) !== 0; }
|
|
263
|
+
set isPatchOnly(v: boolean) { this.flags = v ? (this.flags | IS_PATCH_ONLY) : (this.flags & ~IS_PATCH_ONLY); }
|
|
264
|
+
get isFullStateOnly() { return (this.flags & IS_FULL_STATE_ONLY) !== 0; }
|
|
265
|
+
set isFullStateOnly(v: boolean) { this.flags = v ? (this.flags | IS_FULL_STATE_ONLY) : (this.flags & ~IS_FULL_STATE_ONLY); }
|
|
264
266
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
265
267
|
set isStreamCollection(v: boolean) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
266
268
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -270,7 +272,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
270
272
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
271
273
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
272
274
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
273
|
-
// pattern as `
|
|
275
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
274
276
|
get hasFilteredFields(): boolean {
|
|
275
277
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
276
278
|
}
|
|
@@ -296,7 +298,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
296
298
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
297
299
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
298
300
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
299
|
-
// (
|
|
301
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
300
302
|
const desc = this.encDescriptor;
|
|
301
303
|
if (!desc.hasAnyUnreliable) return false;
|
|
302
304
|
if (index < 32) return (desc.unreliableBitmask & (1 << index)) !== 0;
|
|
@@ -305,12 +307,12 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
305
307
|
|
|
306
308
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
307
309
|
// by the tracker (the value still lives on the instance).
|
|
308
|
-
|
|
309
|
-
if (this.
|
|
310
|
+
isFieldFullStateOnly(index: number): boolean {
|
|
311
|
+
if (this.isFullStateOnly) return true;
|
|
310
312
|
const desc = this.encDescriptor;
|
|
311
|
-
if (!desc.
|
|
312
|
-
if (index < 32) return (desc.
|
|
313
|
-
return Metadata.
|
|
313
|
+
if (!desc.hasAnyFullStateOnly) return false;
|
|
314
|
+
if (index < 32) return (desc.fullStateOnlyBitmask & (1 << index)) !== 0;
|
|
315
|
+
return Metadata.hasFullStateOnlyAtIndex(this.metadata, index);
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
@@ -565,7 +567,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
565
567
|
this.unreliableRecorder?.reset();
|
|
566
568
|
|
|
567
569
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
568
|
-
// (FILTERED/
|
|
570
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
569
571
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
570
572
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
571
573
|
this._fullSyncGen = 0;
|
|
@@ -602,7 +604,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
602
604
|
if (this._isSchema) throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
603
605
|
const src = this.collDirty!;
|
|
604
606
|
const dst = new Map<number, OPERATION>();
|
|
605
|
-
const track = !this.paused && !this.
|
|
607
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
606
608
|
if (track) {
|
|
607
609
|
for (let i = 0; i < count; i++) dst.set(i, OPERATION.ADD);
|
|
608
610
|
}
|
|
@@ -625,7 +627,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
625
627
|
}
|
|
626
628
|
|
|
627
629
|
operation(op: OPERATION) {
|
|
628
|
-
if (this.paused || this.
|
|
630
|
+
if (this.paused || this.isFullStateOnly) return;
|
|
629
631
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
630
632
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
631
633
|
//
|
|
@@ -655,10 +657,22 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
655
657
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
656
658
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
657
659
|
* reliable footgun for ref-type fields can't reach this code path.
|
|
660
|
+
*
|
|
661
|
+
* `!isNew` holds an `@unreliable` field on the RELIABLE channel until this
|
|
662
|
+
* tree's own ADD has shipped there. A decoder can only apply a field write
|
|
663
|
+
* to a ref it already knows, so a value emitted before the ADD is dropped —
|
|
664
|
+
* permanently, if the field is never written again. `isNew` clears in
|
|
665
|
+
* `endEncode()`, i.e. after a reliable pass, and recording reliably is
|
|
666
|
+
* itself what enqueues the tree for that pass; the state is self-clearing
|
|
667
|
+
* and no tree can be stranded on the wrong channel. Mirrors `encodeAll`,
|
|
668
|
+
* which has always seeded these fields for late joiners.
|
|
669
|
+
*
|
|
670
|
+
* Ordering matters: `isFieldUnreliable` short-circuits on the class-level
|
|
671
|
+
* `hasAnyUnreliable`, so schemas without the modifier never read `flags`.
|
|
658
672
|
*/
|
|
659
673
|
private _routeAndRecord(index: number, op: OPERATION, raw: boolean): void {
|
|
660
|
-
if (this.paused || this.
|
|
661
|
-
if (this.isFieldUnreliable(index)) {
|
|
674
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return;
|
|
675
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
662
676
|
const r = this.ensureUnreliableRecorder();
|
|
663
677
|
if (raw) r.recordRaw(index, op);
|
|
664
678
|
else r.record(index, op);
|
|
@@ -722,9 +736,11 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
722
736
|
return;
|
|
723
737
|
}
|
|
724
738
|
|
|
725
|
-
if (this.paused || this.
|
|
739
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return this.getValue(index);
|
|
726
740
|
|
|
727
|
-
|
|
741
|
+
// Same pre-ADD hold as `_routeAndRecord` — a DELETE naming a ref the
|
|
742
|
+
// decoder hasn't seen is dropped just like a field write.
|
|
743
|
+
const unreliable = this.isFieldUnreliable(index) && !this.isNew;
|
|
728
744
|
if (unreliable) this.ensureUnreliableRecorder().recordDelete(index, operation ?? OPERATION.DELETE);
|
|
729
745
|
else this.recordDelete(index, operation ?? OPERATION.DELETE);
|
|
730
746
|
|
|
@@ -789,6 +805,9 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
789
805
|
|
|
790
806
|
addParent(parent: Ref, index: number): void { _addParent(this, parent, index); }
|
|
791
807
|
|
|
808
|
+
/** Re-point an existing parent's cached index after the parent reindexed. */
|
|
809
|
+
setParentIndex(parent: Ref, index: number): void { _setParentIndex(this, parent, index); }
|
|
810
|
+
|
|
792
811
|
/** @returns true if parent was found and removed */
|
|
793
812
|
removeParent(parent: Ref = this.parent): boolean { return _removeParent(this, parent); }
|
|
794
813
|
|
|
@@ -841,6 +860,7 @@ export class UntrackedChangeTree {
|
|
|
841
860
|
operation(): void {}
|
|
842
861
|
setParent(): void {}
|
|
843
862
|
addParent(): void {}
|
|
863
|
+
setParentIndex(): void {}
|
|
844
864
|
removeParent(): boolean { return false; }
|
|
845
865
|
getChange(): number { return 0; }
|
|
846
866
|
discard(): void {}
|
|
@@ -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
|
|
@@ -131,6 +140,7 @@ function _fullSyncWalk(ctx: EncodeCtx, changeTree: ChangeTree): void {
|
|
|
131
140
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
132
141
|
ctx.isSchema = desc.isSchema;
|
|
133
142
|
ctx.filterBitmask = desc.filterBitmask;
|
|
143
|
+
ctx.tags = desc.tags;
|
|
134
144
|
ctx.structSwitchEmitted = false;
|
|
135
145
|
ctx.shouldEmitSwitch = (ctx.hasView || ctx.it.offset > ctx.initialOffset || changeTree !== ctx.rootChangeTree);
|
|
136
146
|
|
|
@@ -174,10 +184,13 @@ function encodeChangeCb(ctx: EncodeCtx, fieldIndex: number, op: OPERATION): void
|
|
|
174
184
|
|
|
175
185
|
// Per-field filter decision (same rule as ChangeTree.change()):
|
|
176
186
|
// a field is filtered iff the tree inherits isFiltered OR the field
|
|
177
|
-
// itself carries a @view tag.
|
|
178
|
-
//
|
|
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.
|
|
179
190
|
const fieldFiltered = ctx.isSchema
|
|
180
|
-
? (ctx.treeIsFiltered || (
|
|
191
|
+
? (ctx.treeIsFiltered || (fieldIndex < 32
|
|
192
|
+
? (ctx.filterBitmask & (1 << fieldIndex)) !== 0
|
|
193
|
+
: ctx.tags[fieldIndex] !== undefined))
|
|
181
194
|
: ctx.treeIsFiltered;
|
|
182
195
|
if (fieldFiltered !== ctx.emitFiltered) return;
|
|
183
196
|
|
|
@@ -239,7 +252,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
239
252
|
ref: undefined, encoder: undefined!, filter: undefined, metadata: undefined,
|
|
240
253
|
view: undefined, isEncodeAll: false, hasView: false,
|
|
241
254
|
treeIsFiltered: false, isSchema: false, emitFiltered: false,
|
|
242
|
-
filterBitmask: 0,
|
|
255
|
+
filterBitmask: 0, tags: undefined!,
|
|
243
256
|
structSwitchEmitted: false, isRootTree: false, shouldEmitSwitch: false,
|
|
244
257
|
gen: 0, initialOffset: 0, rootChangeTree: undefined!,
|
|
245
258
|
};
|
|
@@ -320,6 +333,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
320
333
|
ctx.treeIsFiltered = changeTree.isFiltered;
|
|
321
334
|
ctx.isSchema = desc.isSchema;
|
|
322
335
|
ctx.filterBitmask = desc.filterBitmask;
|
|
336
|
+
ctx.tags = desc.tags;
|
|
323
337
|
ctx.structSwitchEmitted = false;
|
|
324
338
|
ctx.isRootTree = (changeTree === rootChangeTree);
|
|
325
339
|
// Root's struct switch is skipped at the very start of the shared
|
|
@@ -639,7 +653,7 @@ export class Encoder<T extends Schema = any> {
|
|
|
639
653
|
// Emit each element's full state — forEachLive walks populated
|
|
640
654
|
// fields structurally, mirroring encodeAllView's bootstrap.
|
|
641
655
|
// Covers both static elements (dirty state was reset by
|
|
642
|
-
// inheritedFlags'
|
|
656
|
+
// inheritedFlags' becameFullStateOnly branch) and non-static (still
|
|
643
657
|
// has dirty state but the main loop skipped them because
|
|
644
658
|
// they're filtered).
|
|
645
659
|
for (const element of emittedElements) {
|
|
@@ -718,24 +732,86 @@ export class Encoder<T extends Schema = any> {
|
|
|
718
732
|
// `t.stream(X).priority(fn)` or the decorator form) and seeded
|
|
719
733
|
// into `_stream.priority` when the stream was attached. Users
|
|
720
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;
|
|
721
740
|
const priority = st.priority;
|
|
741
|
+
const max = st.maxPerTick;
|
|
722
742
|
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
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.
|
|
726
755
|
const positions: number[] = [];
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (priority !== undefined) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
)
|
|
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
|
+
}
|
|
735
810
|
}
|
|
736
811
|
|
|
737
|
-
const
|
|
738
|
-
|
|
812
|
+
for (const pos of stale) pending.delete(pos);
|
|
813
|
+
|
|
814
|
+
const count = positions.length;
|
|
739
815
|
|
|
740
816
|
let sent: Set<number> | undefined = st.sentByView.get(viewId);
|
|
741
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
|
|