@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
package/src/encoder/StateView.ts
CHANGED
|
@@ -4,7 +4,11 @@ import { DEFAULT_VIEW_TAG } from "../annotations.js";
|
|
|
4
4
|
import { OPERATION } from "../encoding/spec.js";
|
|
5
5
|
import { Metadata } from "../Metadata.js";
|
|
6
6
|
import { spliceOne } from "../types/utils.js";
|
|
7
|
-
import { streamDequeueForView, streamEnqueueForView } from "./streaming.js";
|
|
7
|
+
import { ensureStreamState, streamDequeueForView, streamEnqueueForView } from "./streaming.js";
|
|
8
|
+
import type { StreamSchema } from "../types/custom/StreamSchema.js";
|
|
9
|
+
import type { MapSchema } from "../types/custom/MapSchema.js";
|
|
10
|
+
import type { SetSchema } from "../types/custom/SetSchema.js";
|
|
11
|
+
import type { CollectionSchema } from "../types/custom/CollectionSchema.js";
|
|
8
12
|
import type { Schema } from "../Schema.js";
|
|
9
13
|
import type { Root, Streamable } from "./Root.js";
|
|
10
14
|
|
|
@@ -28,8 +32,6 @@ function _clearViewBitFromAllTrees(root: Root, slot: number, bit: number): void
|
|
|
28
32
|
const tree = trees[refId];
|
|
29
33
|
const v = tree.visibleViews;
|
|
30
34
|
if (v !== undefined && slot < v.length) v[slot] &= clearMask;
|
|
31
|
-
const i = tree.invisibleViews;
|
|
32
|
-
if (i !== undefined && slot < i.length) i[slot] &= clearMask;
|
|
33
35
|
const s = tree.subscribedViews;
|
|
34
36
|
if (s !== undefined && slot < s.length) s[slot] &= clearMask;
|
|
35
37
|
const t = tree.tagViews;
|
|
@@ -53,6 +55,24 @@ const _disposeRegistry = new FinalizationRegistry<{ root: Root; id: number; slot
|
|
|
53
55
|
},
|
|
54
56
|
);
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Compact description of a rejected argument, for warning messages.
|
|
60
|
+
* Passing the value itself to `console.warn` is not an option — a
|
|
61
|
+
* populated collection inspects into dozens of lines of encoder
|
|
62
|
+
* internals and buries the message that matters.
|
|
63
|
+
*/
|
|
64
|
+
function describeArg(value: any): string {
|
|
65
|
+
if (value === undefined) { return "undefined"; }
|
|
66
|
+
if (value === null) { return "null"; }
|
|
67
|
+
const type = typeof value;
|
|
68
|
+
if (type === "string") {
|
|
69
|
+
return JSON.stringify(value.length > 30 ? `${value.slice(0, 30)}…` : value);
|
|
70
|
+
}
|
|
71
|
+
if (type !== "object" && type !== "function") { return `${type} ${String(value)}`; }
|
|
72
|
+
if (Array.isArray(value)) { return `Array(${value.length})`; }
|
|
73
|
+
return value.constructor?.name ?? "Object";
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
export class StateView {
|
|
57
77
|
/**
|
|
58
78
|
* Iterable list of items that are visible to this view
|
|
@@ -192,32 +212,6 @@ export class StateView {
|
|
|
192
212
|
if (slot < arr.length) arr[slot] &= ~this._bit;
|
|
193
213
|
}
|
|
194
214
|
|
|
195
|
-
/** True iff this view has previously marked `tree` as invisible. */
|
|
196
|
-
public isInvisible(tree: ChangeTree): boolean {
|
|
197
|
-
const arr = tree.invisibleViews;
|
|
198
|
-
const slot = this._slot;
|
|
199
|
-
return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
203
|
-
public markInvisible(tree: ChangeTree): void {
|
|
204
|
-
const slot = this._slot;
|
|
205
|
-
let arr = tree.invisibleViews;
|
|
206
|
-
if (arr === undefined) {
|
|
207
|
-
arr = tree.invisibleViews = [];
|
|
208
|
-
}
|
|
209
|
-
while (arr.length <= slot) arr.push(0);
|
|
210
|
-
arr[slot] |= this._bit;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Clear invisible bit. */
|
|
214
|
-
public unmarkInvisible(tree: ChangeTree): void {
|
|
215
|
-
const arr = tree.invisibleViews;
|
|
216
|
-
if (arr === undefined) return;
|
|
217
|
-
const slot = this._slot;
|
|
218
|
-
if (slot < arr.length) arr[slot] &= ~this._bit;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
215
|
// ──────────────────────────────────────────────────────────────────
|
|
222
216
|
// Per-tag, per-view bitmap. Replaces the legacy
|
|
223
217
|
// `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
|
|
@@ -310,13 +304,16 @@ export class StateView {
|
|
|
310
304
|
|
|
311
305
|
private _add(obj: Ref, tag: number, checkIncludeParent: boolean, _skipStreamRouting: boolean) {
|
|
312
306
|
const changeTree: ChangeTree = obj?.[$changes];
|
|
313
|
-
const parentChangeTree = changeTree.parent;
|
|
314
|
-
|
|
315
307
|
if (!changeTree) {
|
|
316
|
-
console.warn(
|
|
308
|
+
console.warn(
|
|
309
|
+
`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`,
|
|
310
|
+
);
|
|
317
311
|
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const parentChangeTree = changeTree.parent;
|
|
318
315
|
|
|
319
|
-
|
|
316
|
+
if (
|
|
320
317
|
!parentChangeTree &&
|
|
321
318
|
obj[$refId] !== 0 // allow root object
|
|
322
319
|
) {
|
|
@@ -371,13 +368,20 @@ export class StateView {
|
|
|
371
368
|
// subclasses yield a real Metadata object.
|
|
372
369
|
const metadata: Metadata = (obj.constructor as typeof Schema)[Symbol.metadata];
|
|
373
370
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
|
|
371
|
+
// Add to iterable list (only the explicitly added items), deduping
|
|
372
|
+
// re-adds of an already-visible instance. isVisible must be read
|
|
373
|
+
// BEFORE markVisible; indexOf runs only on the re-add path.
|
|
374
|
+
// NOTE: dedup applies to `items` only — a re-add still re-queues the
|
|
375
|
+
// full snapshot on purpose (shared-view bootstrap re-add: a
|
|
376
|
+
// late-attached client may not have consumed earlier drains).
|
|
377
|
+
// Callers wanting cheap idempotence can guard with `view.has(obj)`.
|
|
378
|
+
if (this.iterable && checkIncludeParent
|
|
379
|
+
&& (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
|
|
378
380
|
this.items.push(obj);
|
|
379
381
|
}
|
|
380
382
|
|
|
383
|
+
this.markVisible(changeTree);
|
|
384
|
+
|
|
381
385
|
// add parent ChangeTree's
|
|
382
386
|
// - if it was invisible to this view
|
|
383
387
|
// - if it were previously filtered out
|
|
@@ -468,7 +472,6 @@ export class StateView {
|
|
|
468
472
|
|
|
469
473
|
} else if (!changeTree.isNew || isChildAdded) {
|
|
470
474
|
// new structures will be added as part of .encode() call, no need to force it to .encodeView()
|
|
471
|
-
const isInvisible = this.isInvisible(changeTree);
|
|
472
475
|
|
|
473
476
|
// Full-sync snapshot: walk the live ref structurally instead of
|
|
474
477
|
// iterating a cumulative recorder bucket. Every populated index
|
|
@@ -476,11 +479,14 @@ export class StateView {
|
|
|
476
479
|
// at encode time). Per-field tags come from the descriptor's
|
|
477
480
|
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
478
481
|
// object hop.
|
|
482
|
+
//
|
|
483
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
484
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
485
|
+
// so anything added leaks straight to the wire.
|
|
479
486
|
const tags = changeTree.encDescriptor.tags;
|
|
480
487
|
changeTree.forEachLive((index) => {
|
|
481
488
|
const tagAtIndex = tags[index];
|
|
482
489
|
if (
|
|
483
|
-
isInvisible || // if "invisible", include all
|
|
484
490
|
tagAtIndex === undefined || // "all change" with no tag
|
|
485
491
|
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
486
492
|
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
@@ -622,9 +628,11 @@ export class StateView {
|
|
|
622
628
|
remove(obj: Ref, tag?: number): this; // hide _isClear parameter from public API
|
|
623
629
|
remove(obj: Ref, tag?: number, _isClear?: boolean): this;
|
|
624
630
|
remove(obj: Ref, tag: number = DEFAULT_VIEW_TAG, _isClear: boolean = false): this {
|
|
625
|
-
const changeTree: ChangeTree = obj[$changes];
|
|
631
|
+
const changeTree: ChangeTree = obj?.[$changes];
|
|
626
632
|
if (!changeTree) {
|
|
627
|
-
console.warn(
|
|
633
|
+
console.warn(
|
|
634
|
+
`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`,
|
|
635
|
+
);
|
|
628
636
|
return this;
|
|
629
637
|
}
|
|
630
638
|
|
|
@@ -652,7 +660,7 @@ export class StateView {
|
|
|
652
660
|
|
|
653
661
|
// ── Streamable-collection unsubscribe (the stream itself) ─────
|
|
654
662
|
// Flush DELETE for every sent position and drop pending. After
|
|
655
|
-
// this, the stream is
|
|
663
|
+
// this, the stream is no longer visible to this view — any future
|
|
656
664
|
// `stream.add()` would still seed broadcast pending (if no views)
|
|
657
665
|
// but would NOT re-seed per-view pending (user must re-subscribe).
|
|
658
666
|
if (changeTree.isStreamCollection) {
|
|
@@ -799,18 +807,79 @@ export class StateView {
|
|
|
799
807
|
* enqueued into `_pendingByView` so the priority pass drains them
|
|
800
808
|
* respecting `maxPerTick`.
|
|
801
809
|
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
810
|
+
* On a streaming collection, pass a `priority` callback to order THIS
|
|
811
|
+
* client's backlog. It receives only the element, so whatever the
|
|
812
|
+
* client sorts by is captured in the closure — nothing is attached to
|
|
813
|
+
* the view, and both the element and the captured entity stay typed:
|
|
814
|
+
*
|
|
815
|
+
* ```ts
|
|
816
|
+
* onJoin(client) {
|
|
817
|
+
* const player = this.state.players.get(client.sessionId);
|
|
818
|
+
* client.view.subscribe(this.state.enemies, (enemy) =>
|
|
819
|
+
* -((enemy.x - player.x) ** 2 + (enemy.y - player.y) ** 2));
|
|
820
|
+
* }
|
|
821
|
+
* ```
|
|
822
|
+
*
|
|
823
|
+
* A per-view callback overrides the collection's declaration-scope
|
|
824
|
+
* `.priority()` for this client only.
|
|
825
|
+
*
|
|
826
|
+
* Idempotent on re-subscribe: subscribing to an already-subscribed
|
|
827
|
+
* collection is a no-op, EXCEPT that a supplied `priority` always
|
|
828
|
+
* replaces the previous one — re-subscribe to retarget the ordering.
|
|
829
|
+
* Omitting the argument leaves any existing callback in place; pass
|
|
830
|
+
* `null` to drop it and fall back to the declaration-scope callback.
|
|
804
831
|
*/
|
|
805
|
-
subscribe(
|
|
832
|
+
subscribe<V>(
|
|
833
|
+
collection: StreamSchema<V> | MapSchema<V, any> | SetSchema<V> | CollectionSchema<V>,
|
|
834
|
+
priority?: ((element: V) => number) | null,
|
|
835
|
+
): this;
|
|
836
|
+
subscribe(collection: Ref): this;
|
|
837
|
+
subscribe(collection: Ref, priority?: ((element: any) => number) | null): this {
|
|
806
838
|
const tree: ChangeTree = collection?.[$changes];
|
|
807
839
|
if (!tree) {
|
|
808
|
-
console.warn(
|
|
840
|
+
console.warn(
|
|
841
|
+
`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`,
|
|
842
|
+
);
|
|
809
843
|
return this;
|
|
810
844
|
}
|
|
811
845
|
if (this._root === undefined && tree.root !== undefined) {
|
|
812
846
|
this._bindRoot(tree.root);
|
|
813
847
|
}
|
|
848
|
+
|
|
849
|
+
if (priority !== undefined) {
|
|
850
|
+
if (!tree.isStreamCollection) {
|
|
851
|
+
// Name the field rather than dumping the collection — a
|
|
852
|
+
// populated MapSchema inspects into dozens of lines of
|
|
853
|
+
// internals and buries the message.
|
|
854
|
+
const kind = (collection as any)?.constructor?.name ?? "collection";
|
|
855
|
+
const parent: any = tree.parent;
|
|
856
|
+
if (parent === undefined) {
|
|
857
|
+
console.warn(
|
|
858
|
+
`StateView#subscribe(): \`priority\` ignored — this ${kind} is not ` +
|
|
859
|
+
`attached to a state yet, so it cannot be identified as a stream. ` +
|
|
860
|
+
`Subscribe after assigning it to the state.`,
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
const field = parent?.constructor?.[Symbol.metadata]?.[tree.parentIndex]?.name;
|
|
864
|
+
const where = field ? `${parent.constructor.name}#${field}` : kind;
|
|
865
|
+
console.warn(
|
|
866
|
+
`StateView#subscribe(): \`priority\` ignored — ${where} is a ${kind}, ` +
|
|
867
|
+
`not a streaming collection. Declare the field with .stream() ` +
|
|
868
|
+
`(e.g. t.map(X).stream()) or use t.stream(X) to enable priority batching.`,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
// Set before the idempotency return below, so re-subscribing
|
|
873
|
+
// is the documented way to retarget this view's ordering.
|
|
874
|
+
const st = ensureStreamState(collection as unknown as Streamable);
|
|
875
|
+
if (priority === null) {
|
|
876
|
+
st.priorityByView?.delete(this.id);
|
|
877
|
+
} else {
|
|
878
|
+
(st.priorityByView ??= new Map()).set(this.id, priority);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
814
883
|
if (this.isSubscribed(tree)) return this;
|
|
815
884
|
|
|
816
885
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -858,7 +927,9 @@ export class StateView {
|
|
|
858
927
|
unsubscribe(collection: Ref): this {
|
|
859
928
|
const tree: ChangeTree = collection?.[$changes];
|
|
860
929
|
if (!tree) {
|
|
861
|
-
console.warn(
|
|
930
|
+
console.warn(
|
|
931
|
+
`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`,
|
|
932
|
+
);
|
|
862
933
|
return this;
|
|
863
934
|
}
|
|
864
935
|
if (!this.isSubscribed(tree)) return this;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Filter / unreliable /
|
|
2
|
+
* Filter / unreliable / patchOnly / static inheritance helpers for
|
|
3
3
|
* ChangeTree. Called by setRoot / setParent to derive child flags from
|
|
4
4
|
* the parent field's annotation + the parent tree's own state.
|
|
5
5
|
*/
|
|
@@ -7,15 +7,15 @@ import { Metadata } from "../../Metadata.js";
|
|
|
7
7
|
import { DEFAULT_VIEW_TAG } from "../../annotations.js";
|
|
8
8
|
import {
|
|
9
9
|
$changes, $childType,
|
|
10
|
-
$
|
|
11
|
-
$
|
|
10
|
+
$fullStateOnlyFieldIndexes, $streamFieldIndexes,
|
|
11
|
+
$patchOnlyFieldIndexes, $viewFieldIndexes,
|
|
12
12
|
// $unreliableFieldIndexes — tree-level unreliable currently disabled
|
|
13
13
|
// (see INHERITABLE_FLAGS comment in ChangeTree.ts). Per-field unreliable
|
|
14
14
|
// routing on primitive fields still uses it via `isFieldUnreliable()`.
|
|
15
15
|
} from "../../types/symbols.js";
|
|
16
16
|
import type { Schema } from "../../Schema.js";
|
|
17
17
|
import {
|
|
18
|
-
INHERITABLE_FLAGS,
|
|
18
|
+
INHERITABLE_FLAGS, IS_FULL_STATE_ONLY, IS_PATCH_ONLY,
|
|
19
19
|
// IS_UNRELIABLE — tree-level unreliable currently disabled; see
|
|
20
20
|
// INHERITABLE_FLAGS comment in ChangeTree.ts.
|
|
21
21
|
type ChangeTree, type Ref,
|
|
@@ -39,7 +39,7 @@ export function checkIsFiltered(
|
|
|
39
39
|
|
|
40
40
|
// Static trees never track per-tick changes — skip the queue entirely.
|
|
41
41
|
// Full-sync reaches them via structural walk (forEachChild).
|
|
42
|
-
if (tree.
|
|
42
|
+
if (tree.isFullStateOnly) return;
|
|
43
43
|
|
|
44
44
|
// Mutations that happened before setRoot (e.g. class-field initializers)
|
|
45
45
|
// recorded into the appropriate recorder but couldn't enqueue yet.
|
|
@@ -66,7 +66,7 @@ export function checkIsFiltered(
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Inherit filter / unreliable /
|
|
69
|
+
* Inherit filter / unreliable / patchOnly / static classification from
|
|
70
70
|
* the parent field's annotation. Collections (MapSchema / ArraySchema /
|
|
71
71
|
* etc.) inherit these from the Schema field that holds them.
|
|
72
72
|
*
|
|
@@ -106,15 +106,15 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
106
106
|
|
|
107
107
|
const parentMetadata: any = (parent as any)?.constructor?.[Symbol.metadata];
|
|
108
108
|
|
|
109
|
-
// Flag inheritance — pack the
|
|
109
|
+
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
110
110
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
111
111
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
112
112
|
// went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
|
|
113
113
|
// pairs. IS_UNRELIABLE is omitted from both sides — tree-level
|
|
114
114
|
// unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
|
|
115
115
|
const fieldBits =
|
|
116
|
-
(parentMetadata?.[$
|
|
117
|
-
| (parentMetadata?.[$
|
|
116
|
+
(parentMetadata?.[$patchOnlyFieldIndexes]?.includes(parentIndex) ? IS_PATCH_ONLY : 0)
|
|
117
|
+
| (parentMetadata?.[$fullStateOnlyFieldIndexes]?.includes(parentIndex) ? IS_FULL_STATE_ONLY : 0);
|
|
118
118
|
const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
|
|
119
119
|
const beforeFlags = tree.flags;
|
|
120
120
|
tree.flags = beforeFlags | inheritedBits;
|
|
@@ -125,7 +125,7 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
125
125
|
// `new Config().assign({...})` populates the recorder before the
|
|
126
126
|
// Config instance is attached). Static trees ship state via structural
|
|
127
127
|
// walk only; per-tick dirty entries would leak post-first-sync.
|
|
128
|
-
if (gainedBits &
|
|
128
|
+
if (gainedBits & IS_FULL_STATE_ONLY) {
|
|
129
129
|
tree.reset();
|
|
130
130
|
tree.unreliableRecorder?.reset();
|
|
131
131
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Walk all currently-populated non-
|
|
2
|
+
* Walk all currently-populated non-patchOnly indexes on a tree, emitting
|
|
3
3
|
* each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
|
|
4
4
|
* StateView.add to derive full-sync output from the live structure.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
7
7
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
8
|
-
* field is @
|
|
8
|
+
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
9
9
|
*/
|
|
10
|
-
import { $childType, $numFields, $
|
|
10
|
+
import { $childType, $numFields, $patchOnlyFieldIndexes } from "../../types/symbols.js";
|
|
11
11
|
import type { ChangeTree } from "../ChangeTree.js";
|
|
12
12
|
|
|
13
13
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
@@ -29,10 +29,10 @@ export function forEachLiveWithCtx<C>(
|
|
|
29
29
|
const ref = tree.refTarget as any;
|
|
30
30
|
|
|
31
31
|
if (ref[$childType] !== undefined) {
|
|
32
|
-
// Collection inheriting @
|
|
32
|
+
// Collection inheriting @patchOnly from parent field: skip entirely.
|
|
33
33
|
// The resync sweep (decoder/Resync.ts) relies on this: a collection
|
|
34
34
|
// absent from full-sync output is never pruned client-side.
|
|
35
|
-
if (tree.
|
|
35
|
+
if (tree.isPatchOnly) return;
|
|
36
36
|
|
|
37
37
|
// Collection types: dispatch by shape.
|
|
38
38
|
if (Array.isArray(ref.items)) {
|
|
@@ -56,7 +56,7 @@ export function forEachLiveWithCtx<C>(
|
|
|
56
56
|
// Schema: walk declared fields. `null` is treated as absent —
|
|
57
57
|
// the setter records a DELETE when a field is set to null or
|
|
58
58
|
// undefined, so it should not appear in full-sync output.
|
|
59
|
-
// (@
|
|
59
|
+
// (@patchOnly skips below matter to the resync sweep — see
|
|
60
60
|
// decoder/Resync.ts: absent-from-payload means never pruned.)
|
|
61
61
|
//
|
|
62
62
|
// Read names from the per-class descriptor's parallel array —
|
|
@@ -65,12 +65,12 @@ export function forEachLiveWithCtx<C>(
|
|
|
65
65
|
const metadata = tree.metadata;
|
|
66
66
|
if (!metadata) return;
|
|
67
67
|
const numFields = (metadata[$numFields] ?? -1) as number;
|
|
68
|
-
const
|
|
68
|
+
const patchOnlyIndexes = metadata[$patchOnlyFieldIndexes];
|
|
69
69
|
const names = tree.encDescriptor.names;
|
|
70
70
|
for (let i = 0; i <= numFields; i++) {
|
|
71
71
|
const name = names[i];
|
|
72
72
|
if (name === undefined) continue;
|
|
73
|
-
if (
|
|
73
|
+
if (patchOnlyIndexes && patchOnlyIndexes.includes(i)) continue;
|
|
74
74
|
const value = ref[name];
|
|
75
75
|
if (value !== undefined && value !== null) cb(ctx, i);
|
|
76
76
|
}
|
package/src/encoder/streaming.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface StreamableState {
|
|
|
53
53
|
* Instance-level override: assign to `stream.priority`.
|
|
54
54
|
*/
|
|
55
55
|
priority?: (view: any, element: any) => number;
|
|
56
|
+
/**
|
|
57
|
+
* Per-view priority registered by `StateView.subscribe(collection, fn)`.
|
|
58
|
+
* Takes precedence over the declaration-scope `priority` for that view.
|
|
59
|
+
* Receives only the element — the client's own entity is captured in
|
|
60
|
+
* the closure, so nothing has to be attached to the view.
|
|
61
|
+
*/
|
|
62
|
+
priorityByView?: Map<number, (element: any) => number>;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export function createStreamableState(): StreamableState {
|
|
@@ -229,4 +236,5 @@ export function streamDropView(s: Streamable, viewId: number): void {
|
|
|
229
236
|
if (st === undefined) return;
|
|
230
237
|
st.pendingByView.delete(viewId);
|
|
231
238
|
st.sentByView.delete(viewId);
|
|
239
|
+
st.priorityByView?.delete(viewId);
|
|
232
240
|
}
|
package/src/encoding/spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SWITCH_TO_STRUCTURE = 255; //
|
|
1
|
+
export const SWITCH_TO_STRUCTURE = 255; // same byte as `DELETE_AND_ADD | 63`, which is why field index 63 is unassignable (Metadata.MAX_FIELDS)
|
|
2
2
|
export const TYPE_ID = 213;
|
|
3
3
|
|
|
4
4
|
/**
|
package/src/index.ts
CHANGED
package/src/types/builder.ts
CHANGED
|
@@ -20,12 +20,11 @@ export interface BuilderDefinition {
|
|
|
20
20
|
default?: any;
|
|
21
21
|
hasDefault: boolean;
|
|
22
22
|
view?: number; // tag value; undefined = no view
|
|
23
|
-
owned?: boolean;
|
|
24
23
|
unreliable?: boolean;
|
|
25
|
-
|
|
24
|
+
patchOnly?: boolean;
|
|
26
25
|
deprecated?: boolean;
|
|
27
26
|
deprecatedThrows?: boolean;
|
|
28
|
-
|
|
27
|
+
fullStateOnly?: boolean;
|
|
29
28
|
stream?: boolean;
|
|
30
29
|
optional?: boolean;
|
|
31
30
|
/** Local-only field: typed + initialized, but never registered for sync. */
|
|
@@ -75,12 +74,11 @@ export class FieldBuilder<
|
|
|
75
74
|
private _default: any = undefined;
|
|
76
75
|
private _hasDefault = false;
|
|
77
76
|
private _view: number | undefined = undefined;
|
|
78
|
-
private _owned = false;
|
|
79
77
|
private _unreliable = false;
|
|
80
|
-
private
|
|
78
|
+
private _patchOnly = false;
|
|
81
79
|
private _deprecated = false;
|
|
82
80
|
private _deprecatedThrows = true;
|
|
83
|
-
private
|
|
81
|
+
private _fullStateOnly = false;
|
|
84
82
|
private _stream = false;
|
|
85
83
|
private _optional = false;
|
|
86
84
|
private _noSync = false;
|
|
@@ -119,16 +117,16 @@ export class FieldBuilder<
|
|
|
119
117
|
return this;
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
/** Mark this field as owned (encoder-side ownership filtering). */
|
|
123
|
-
owned(): this {
|
|
124
|
-
this._owned = true;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
120
|
/**
|
|
129
121
|
* Mark this field as unreliable — tick patches emit it on the unreliable
|
|
130
122
|
* transport channel. Still persisted to full-sync snapshots unless also
|
|
131
|
-
* tagged with `.
|
|
123
|
+
* tagged with `.patchOnly()`. Primitive fields only.
|
|
124
|
+
*
|
|
125
|
+
* The field's FIRST value still travels the reliable channel, as part of
|
|
126
|
+
* the owning instance's ADD; only later mutations become unreliable. A
|
|
127
|
+
* decoder cannot apply a write to a ref it has not been told about, so a
|
|
128
|
+
* value emitted ahead of that ADD would be dropped — and lost for good if
|
|
129
|
+
* the field is never written again.
|
|
132
130
|
*/
|
|
133
131
|
unreliable(): this {
|
|
134
132
|
this._unreliable = true;
|
|
@@ -136,24 +134,28 @@ export class FieldBuilder<
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
/**
|
|
139
|
-
*
|
|
140
|
-
* (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
141
|
-
* only after its next mutation is emitted on a
|
|
142
|
-
* to `.unreliable()`.
|
|
137
|
+
* Deliver this field on tick patches ONLY — it is never written to a
|
|
138
|
+
* full-state sync (`encodeAll` / `encodeAllView`). Late-joining clients
|
|
139
|
+
* see the field only after its next mutation is emitted on a patch.
|
|
140
|
+
* The mirror of `.fullStateOnly()`, and orthogonal to `.unreliable()`.
|
|
143
141
|
*/
|
|
144
|
-
|
|
145
|
-
this.
|
|
142
|
+
patchOnly(): this {
|
|
143
|
+
this._patchOnly = true;
|
|
146
144
|
return this;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
148
|
+
* Deliver this field in the full state sync ONLY (`encodeAll` /
|
|
149
|
+
* `encodeAllView`) — it never enters a tick patch. A client receives it
|
|
150
|
+
* on join (and again on a resync); writes after that are not tracked.
|
|
151
|
+
* The mirror of `.patchOnly()`.
|
|
152
|
+
*
|
|
153
|
+
* The field itself is NOT frozen — it stays mutable server-side, only
|
|
154
|
+
* its propagation stops. On a stream field (`t.stream(X).fullStateOnly()`)
|
|
155
|
+
* the same rule applies per element: post-add mutations are no-ops.
|
|
154
156
|
*/
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
+
fullStateOnly(): this {
|
|
158
|
+
this._fullStateOnly = true;
|
|
157
159
|
return this;
|
|
158
160
|
}
|
|
159
161
|
|
|
@@ -166,8 +168,8 @@ export class FieldBuilder<
|
|
|
166
168
|
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
167
169
|
* want on the class for typing convenience without paying any sync cost.
|
|
168
170
|
*
|
|
169
|
-
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
170
|
-
* `.unreliable()`, `.
|
|
171
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`,
|
|
172
|
+
* `.unreliable()`, `.patchOnly()`, `.fullStateOnly()`, `.stream()`) — combining
|
|
171
173
|
* them throws at `schema()` time.
|
|
172
174
|
*
|
|
173
175
|
* ```ts
|
|
@@ -211,9 +213,12 @@ export class FieldBuilder<
|
|
|
211
213
|
* higher return values emit first. Does nothing in broadcast mode
|
|
212
214
|
* (shared `encode()` drains FIFO). Only meaningful on stream fields.
|
|
213
215
|
*
|
|
216
|
+
* `StateView` carries no position of its own — attach whatever the
|
|
217
|
+
* callback needs to sort by (`view` is loosely typed for this).
|
|
218
|
+
*
|
|
214
219
|
* ```ts
|
|
215
220
|
* t.stream(Enemy).priority((view, enemy) =>
|
|
216
|
-
* -
|
|
221
|
+
* -((enemy.x - view.x) ** 2 + (enemy.y - view.y) ** 2)
|
|
217
222
|
* )
|
|
218
223
|
* ```
|
|
219
224
|
*/
|
|
@@ -251,12 +256,11 @@ export class FieldBuilder<
|
|
|
251
256
|
default: this._default,
|
|
252
257
|
hasDefault: this._hasDefault,
|
|
253
258
|
view: this._view,
|
|
254
|
-
owned: this._owned,
|
|
255
259
|
unreliable: this._unreliable,
|
|
256
|
-
|
|
260
|
+
patchOnly: this._patchOnly,
|
|
257
261
|
deprecated: this._deprecated,
|
|
258
262
|
deprecatedThrows: this._deprecatedThrows,
|
|
259
|
-
|
|
263
|
+
fullStateOnly: this._fullStateOnly,
|
|
260
264
|
stream: this._stream,
|
|
261
265
|
optional: this._optional,
|
|
262
266
|
noSync: this._noSync,
|
|
@@ -34,7 +34,7 @@ import type { Schema } from "../../Schema.js";
|
|
|
34
34
|
* per-client and drained in priority order (callback on StateView) up to
|
|
35
35
|
* `maxPerTick` per encode pass. Field mutations on already-sent elements
|
|
36
36
|
* propagate through the normal reliable channel without consuming the
|
|
37
|
-
* per-tick budget. Chain `.
|
|
37
|
+
* per-tick budget. Chain `.fullStateOnly()` on the field builder to suppress
|
|
38
38
|
* post-add mutation tracking entirely.
|
|
39
39
|
*/
|
|
40
40
|
export class StreamSchema<V = any> implements IRef {
|
package/src/types/symbols.ts
CHANGED
|
@@ -117,17 +117,10 @@ export const $builder = "~builder";
|
|
|
117
117
|
*/
|
|
118
118
|
export const $descriptors = "~descriptors";
|
|
119
119
|
|
|
120
|
-
/**
|
|
121
|
-
* Per-class bitmask: bit i set iff field i carries a @view tag.
|
|
122
|
-
* Lazily computed from $viewFieldIndexes on first encode pass.
|
|
123
|
-
* Skips the per-field metadata[i].tag property chase in the hot encode loop.
|
|
124
|
-
*/
|
|
125
|
-
export const $filterBitmask = "~__filterBitmask";
|
|
126
|
-
|
|
127
120
|
/**
|
|
128
121
|
* Cached per-class encode descriptor: bundles encoder fn, filter fn,
|
|
129
|
-
* metadata, isSchema flag
|
|
130
|
-
* the constructor. Replaces
|
|
122
|
+
* metadata, isSchema flag and the per-field arrays into one object stashed
|
|
123
|
+
* on the constructor. Replaces several separate per-tree property chases /
|
|
131
124
|
* function calls in the encode loop with a single property load.
|
|
132
125
|
*/
|
|
133
126
|
export const $encodeDescriptor = "~__encodeDescriptor";
|
|
@@ -137,7 +130,7 @@ export const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
|
|
|
137
130
|
export const $viewFieldIndexes = "~__viewFieldIndexes";
|
|
138
131
|
export const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
|
|
139
132
|
export const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
|
|
140
|
-
export const $
|
|
141
|
-
export const $
|
|
133
|
+
export const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
|
|
134
|
+
export const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
|
|
142
135
|
export const $streamFieldIndexes = "~__streamFieldIndexes";
|
|
143
136
|
export const $streamPriorities = "~__streamPriorities";
|