@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
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
|
|
|
@@ -51,6 +55,24 @@ const _disposeRegistry = new FinalizationRegistry<{ root: Root; id: number; slot
|
|
|
51
55
|
},
|
|
52
56
|
);
|
|
53
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
|
+
|
|
54
76
|
export class StateView {
|
|
55
77
|
/**
|
|
56
78
|
* Iterable list of items that are visible to this view
|
|
@@ -282,13 +304,16 @@ export class StateView {
|
|
|
282
304
|
|
|
283
305
|
private _add(obj: Ref, tag: number, checkIncludeParent: boolean, _skipStreamRouting: boolean) {
|
|
284
306
|
const changeTree: ChangeTree = obj?.[$changes];
|
|
285
|
-
const parentChangeTree = changeTree.parent;
|
|
286
|
-
|
|
287
307
|
if (!changeTree) {
|
|
288
|
-
console.warn(
|
|
308
|
+
console.warn(
|
|
309
|
+
`StateView#add(): expected a Schema instance or collection, received ${describeArg(obj)}`,
|
|
310
|
+
);
|
|
289
311
|
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const parentChangeTree = changeTree.parent;
|
|
290
315
|
|
|
291
|
-
|
|
316
|
+
if (
|
|
292
317
|
!parentChangeTree &&
|
|
293
318
|
obj[$refId] !== 0 // allow root object
|
|
294
319
|
) {
|
|
@@ -603,9 +628,11 @@ export class StateView {
|
|
|
603
628
|
remove(obj: Ref, tag?: number): this; // hide _isClear parameter from public API
|
|
604
629
|
remove(obj: Ref, tag?: number, _isClear?: boolean): this;
|
|
605
630
|
remove(obj: Ref, tag: number = DEFAULT_VIEW_TAG, _isClear: boolean = false): this {
|
|
606
|
-
const changeTree: ChangeTree = obj[$changes];
|
|
631
|
+
const changeTree: ChangeTree = obj?.[$changes];
|
|
607
632
|
if (!changeTree) {
|
|
608
|
-
console.warn(
|
|
633
|
+
console.warn(
|
|
634
|
+
`StateView#remove(): expected a Schema instance or collection, received ${describeArg(obj)}`,
|
|
635
|
+
);
|
|
609
636
|
return this;
|
|
610
637
|
}
|
|
611
638
|
|
|
@@ -780,18 +807,79 @@ export class StateView {
|
|
|
780
807
|
* enqueued into `_pendingByView` so the priority pass drains them
|
|
781
808
|
* respecting `maxPerTick`.
|
|
782
809
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
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.
|
|
785
831
|
*/
|
|
786
|
-
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 {
|
|
787
838
|
const tree: ChangeTree = collection?.[$changes];
|
|
788
839
|
if (!tree) {
|
|
789
|
-
console.warn(
|
|
840
|
+
console.warn(
|
|
841
|
+
`StateView#subscribe(): expected a Schema collection, received ${describeArg(collection)}`,
|
|
842
|
+
);
|
|
790
843
|
return this;
|
|
791
844
|
}
|
|
792
845
|
if (this._root === undefined && tree.root !== undefined) {
|
|
793
846
|
this._bindRoot(tree.root);
|
|
794
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
|
+
|
|
795
883
|
if (this.isSubscribed(tree)) return this;
|
|
796
884
|
|
|
797
885
|
// Mark collection visible so its own ADD/DELETE ops emit in the
|
|
@@ -839,7 +927,9 @@ export class StateView {
|
|
|
839
927
|
unsubscribe(collection: Ref): this {
|
|
840
928
|
const tree: ChangeTree = collection?.[$changes];
|
|
841
929
|
if (!tree) {
|
|
842
|
-
console.warn(
|
|
930
|
+
console.warn(
|
|
931
|
+
`StateView#unsubscribe(): expected a Schema collection, received ${describeArg(collection)}`,
|
|
932
|
+
);
|
|
843
933
|
return this;
|
|
844
934
|
}
|
|
845
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
|
}
|
|
@@ -45,6 +45,35 @@ export function addParent(tree: ChangeTree, parent: Ref, index: number): void {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Move `parent`'s existing chain entry to `index`, skipping the attachment
|
|
50
|
+
* work `addParent` does. `parent` must already be a parent of `tree`.
|
|
51
|
+
*
|
|
52
|
+
* Called by collections whose wire slots shift (ArraySchema): StateView
|
|
53
|
+
* addresses per-view ADD/DELETE by that index, so it has to follow the
|
|
54
|
+
* element it names.
|
|
55
|
+
*/
|
|
56
|
+
export function setParentIndex(tree: ChangeTree, parent: Ref, index: number): void {
|
|
57
|
+
if (tree.extraParents === undefined) {
|
|
58
|
+
tree._parentIndex = index; // sole parent, so it is `parent`
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Shared instance — move only the entry `parent` owns. Matching goes
|
|
62
|
+
// through `$changes` because ArraySchema arrives proxied (see removeParent
|
|
63
|
+
// below), and `extraParents` only ever fills by demoting `parentRef`, so
|
|
64
|
+
// the inline parent is set here.
|
|
65
|
+
if (tree.parentRef[$changes] === parent[$changes]) {
|
|
66
|
+
tree._parentIndex = index;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
70
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
71
|
+
entry.index = index;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/**
|
|
49
78
|
* Remove a parent from the chain.
|
|
50
79
|
* @returns true if parent was found and removed (Root.remove relies on this).
|
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,
|
|
@@ -331,6 +331,35 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
331
331
|
return tmpItems.length + (index - live);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Re-point children at their wire slot. `ChangeTree._parentIndex` caches
|
|
336
|
+
* the slot a child holds in `tmpItems`, and StateView addresses per-view
|
|
337
|
+
* ADD/DELETE with it — so a reorder that leaves it behind aims those ops
|
|
338
|
+
* at whichever element inherited the slot (issue #231).
|
|
339
|
+
*
|
|
340
|
+
* The filter check is a correctness boundary, not a tunable: StateView is
|
|
341
|
+
* the only reader and reaches the index only through a filtered array
|
|
342
|
+
* (`addParentOf` bails on `hasFilteredFields`, `remove` on the child's
|
|
343
|
+
* `isFiltered`). Everything else stops at the flag read instead of walking
|
|
344
|
+
* its children every tick.
|
|
345
|
+
*
|
|
346
|
+
* Callers name the lowest slot that moved as `from`. Compaction cannot, so
|
|
347
|
+
* it hands over the pre-compaction layout as `staged` and the unchanged
|
|
348
|
+
* prefix is skipped instead. Either way tail churn walks nothing.
|
|
349
|
+
*/
|
|
350
|
+
protected $reindexChildren(from: number, staged?: V[]) {
|
|
351
|
+
if (!this[$changes].hasFilteredFields) { return; } // nothing will read the cache
|
|
352
|
+
if (typeof this[$childType] === "string") { return; } // primitives have no child tree
|
|
353
|
+
const tmpItems = this.tmpItems;
|
|
354
|
+
const length = tmpItems.length;
|
|
355
|
+
if (staged !== undefined) {
|
|
356
|
+
while (from < length && tmpItems[from] === staged[from]) { from++; }
|
|
357
|
+
}
|
|
358
|
+
for (let i = from; i < length; i++) {
|
|
359
|
+
tmpItems[i]?.[$changes]?.setParentIndex(this, i);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
334
363
|
// encoding only. Returns the wire index the change was recorded at
|
|
335
364
|
// (undefined when nothing was recorded).
|
|
336
365
|
protected $changeAt(index: number, value: V): number | undefined {
|
|
@@ -460,6 +489,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
460
489
|
self[$changes].operation(OPERATION.REVERSE);
|
|
461
490
|
self.items.reverse();
|
|
462
491
|
self.tmpItems.reverse();
|
|
492
|
+
self.$reindexChildren(0);
|
|
463
493
|
return this;
|
|
464
494
|
}
|
|
465
495
|
|
|
@@ -515,6 +545,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
515
545
|
sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
|
|
516
546
|
|
|
517
547
|
self.tmpItems.sort(compareFn);
|
|
548
|
+
self.$reindexChildren(0);
|
|
518
549
|
|
|
519
550
|
self.isMovingItems = false;
|
|
520
551
|
return this;
|
|
@@ -620,6 +651,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
620
651
|
}
|
|
621
652
|
|
|
622
653
|
self.tmpItems.unshift(...items);
|
|
654
|
+
self.$reindexChildren(items.length); // survivors only — the loop above placed the new items
|
|
623
655
|
|
|
624
656
|
return self.items.unshift(...items);
|
|
625
657
|
}
|
|
@@ -938,8 +970,15 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
938
970
|
|
|
939
971
|
protected [$onEncodeEnd]() {
|
|
940
972
|
const self = this[$proxyTarget] ?? this;
|
|
973
|
+
const staged = self.tmpItems;
|
|
941
974
|
self.tmpItems = self.items.slice();
|
|
942
|
-
|
|
975
|
+
|
|
976
|
+
if (self.deletedIndexes.length > 0) {
|
|
977
|
+
// compaction just closed the staged holes — everything above the
|
|
978
|
+
// lowest one slid down a slot
|
|
979
|
+
self.$reindexChildren(0, staged);
|
|
980
|
+
self.deletedIndexes.length = 0;
|
|
981
|
+
}
|
|
943
982
|
}
|
|
944
983
|
|
|
945
984
|
protected [$onDecodeEnd]() {
|
|
@@ -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";
|