@colyseus/schema 5.0.11 → 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 -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/streaming.d.ts +7 -0
- package/build/index.cjs +374 -232
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +374 -232
- package/build/index.mjs +373 -231
- 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 -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/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
|
*/
|
|
@@ -257,10 +258,10 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
257
258
|
set isNew(v: boolean) { this.flags = v ? (this.flags | IS_NEW) : (this.flags & ~IS_NEW); }
|
|
258
259
|
get isUnreliable() { return (this.flags & IS_UNRELIABLE) !== 0; }
|
|
259
260
|
set isUnreliable(v: boolean) { this.flags = v ? (this.flags | IS_UNRELIABLE) : (this.flags & ~IS_UNRELIABLE); }
|
|
260
|
-
get
|
|
261
|
-
set
|
|
262
|
-
get
|
|
263
|
-
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); }
|
|
264
265
|
get isStreamCollection() { return (this.flags & IS_STREAM_COLLECTION) !== 0; }
|
|
265
266
|
set isStreamCollection(v: boolean) { this.flags = v ? (this.flags | IS_STREAM_COLLECTION) : (this.flags & ~IS_STREAM_COLLECTION); }
|
|
266
267
|
get needsRestage() { return (this.flags & NEEDS_RESTAGE) !== 0; }
|
|
@@ -270,7 +271,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
270
271
|
// @view-tagged fields. StateView.addParentOf uses this to decide whether
|
|
271
272
|
// a parent must be included in a view's bootstrap. Reads the class-level
|
|
272
273
|
// "any viewed field" flag that `EncodeDescriptor` precomputes — same
|
|
273
|
-
// pattern as `
|
|
274
|
+
// pattern as `hasAnyFullStateOnly` / `hasAnyUnreliable` / `hasAnyStream`.
|
|
274
275
|
get hasFilteredFields(): boolean {
|
|
275
276
|
return this.isFiltered || this.encDescriptor.hasAnyView;
|
|
276
277
|
}
|
|
@@ -296,7 +297,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
296
297
|
// metadata lookup. For schemas that DO have unreliable fields, the
|
|
297
298
|
// bitmask answers fields 0-31 in one bitwise op (no Array.includes
|
|
298
299
|
// linear scan). Fields ≥32 always fall back to the metadata lookup
|
|
299
|
-
// (
|
|
300
|
+
// (shift counts wrap at 32, so the bitmask only covers the low 32).
|
|
300
301
|
const desc = this.encDescriptor;
|
|
301
302
|
if (!desc.hasAnyUnreliable) return false;
|
|
302
303
|
if (index < 32) return (desc.unreliableBitmask & (1 << index)) !== 0;
|
|
@@ -305,12 +306,12 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
305
306
|
|
|
306
307
|
// @static fields sync once via full-sync; post-init mutations are ignored
|
|
307
308
|
// by the tracker (the value still lives on the instance).
|
|
308
|
-
|
|
309
|
-
if (this.
|
|
309
|
+
isFieldFullStateOnly(index: number): boolean {
|
|
310
|
+
if (this.isFullStateOnly) return true;
|
|
310
311
|
const desc = this.encDescriptor;
|
|
311
|
-
if (!desc.
|
|
312
|
-
if (index < 32) return (desc.
|
|
313
|
-
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);
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
// `t.stream(...)` collection fields — encoded via per-view priority/budget
|
|
@@ -565,7 +566,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
565
566
|
this.unreliableRecorder?.reset();
|
|
566
567
|
|
|
567
568
|
// back to a freshly-constructed tree: IS_NEW, no inherited flags
|
|
568
|
-
// (FILTERED/
|
|
569
|
+
// (FILTERED/PATCH_ONLY/STATIC/STREAM are re-derived on the next setParent).
|
|
569
570
|
// NEEDS_RESTAGE makes the next Root.add re-stage retained field values.
|
|
570
571
|
this.flags = IS_NEW | NEEDS_RESTAGE;
|
|
571
572
|
this._fullSyncGen = 0;
|
|
@@ -602,7 +603,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
602
603
|
if (this._isSchema) throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
603
604
|
const src = this.collDirty!;
|
|
604
605
|
const dst = new Map<number, OPERATION>();
|
|
605
|
-
const track = !this.paused && !this.
|
|
606
|
+
const track = !this.paused && !this.isFullStateOnly;
|
|
606
607
|
if (track) {
|
|
607
608
|
for (let i = 0; i < count; i++) dst.set(i, OPERATION.ADD);
|
|
608
609
|
}
|
|
@@ -625,7 +626,7 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
625
626
|
}
|
|
626
627
|
|
|
627
628
|
operation(op: OPERATION) {
|
|
628
|
-
if (this.paused || this.
|
|
629
|
+
if (this.paused || this.isFullStateOnly) return;
|
|
629
630
|
// Pure ops (CLEAR/REVERSE) only emit from collection trees — the
|
|
630
631
|
// recorder here is always a CollectionChangeRecorder by construction.
|
|
631
632
|
//
|
|
@@ -655,10 +656,22 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
655
656
|
* fields (see annotations.ts), so the per-field unreliable flag here
|
|
656
657
|
* always means "primitive value updates" — the structural-ADD-routes-
|
|
657
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`.
|
|
658
671
|
*/
|
|
659
672
|
private _routeAndRecord(index: number, op: OPERATION, raw: boolean): void {
|
|
660
|
-
if (this.paused || this.
|
|
661
|
-
if (this.isFieldUnreliable(index)) {
|
|
673
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return;
|
|
674
|
+
if (this.isFieldUnreliable(index) && !this.isNew) {
|
|
662
675
|
const r = this.ensureUnreliableRecorder();
|
|
663
676
|
if (raw) r.recordRaw(index, op);
|
|
664
677
|
else r.record(index, op);
|
|
@@ -722,9 +735,11 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
722
735
|
return;
|
|
723
736
|
}
|
|
724
737
|
|
|
725
|
-
if (this.paused || this.
|
|
738
|
+
if (this.paused || this.isFieldFullStateOnly(index)) return this.getValue(index);
|
|
726
739
|
|
|
727
|
-
|
|
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;
|
|
728
743
|
if (unreliable) this.ensureUnreliableRecorder().recordDelete(index, operation ?? OPERATION.DELETE);
|
|
729
744
|
else this.recordDelete(index, operation ?? OPERATION.DELETE);
|
|
730
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
|
|
@@ -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
|
|