@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.
Files changed (58) hide show
  1. package/build/Metadata.d.ts +20 -12
  2. package/build/annotations.d.ts +23 -10
  3. package/build/codegen/cli.cjs +615 -204
  4. package/build/codegen/cli.cjs.map +1 -1
  5. package/build/codegen/languages/dart.d.ts +20 -0
  6. package/build/codegen/types.d.ts +20 -0
  7. package/build/decoder/Resync.d.ts +3 -3
  8. package/build/encoder/ChangeTree.d.ts +22 -10
  9. package/build/encoder/EncodeDescriptor.d.ts +11 -12
  10. package/build/encoder/StateView.d.ts +26 -8
  11. package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
  12. package/build/encoder/streaming.d.ts +7 -0
  13. package/build/index.cjs +393 -287
  14. package/build/index.cjs.map +1 -1
  15. package/build/index.d.ts +1 -1
  16. package/build/index.js +393 -287
  17. package/build/index.mjs +392 -286
  18. package/build/index.mjs.map +1 -1
  19. package/build/types/builder.d.ts +31 -22
  20. package/build/types/custom/StreamSchema.d.ts +1 -1
  21. package/build/types/symbols.d.ts +4 -10
  22. package/package.json +1 -1
  23. package/src/Metadata.ts +58 -31
  24. package/src/annotations.ts +56 -32
  25. package/src/codegen/api.ts +2 -1
  26. package/src/codegen/languages/c.ts +21 -3
  27. package/src/codegen/languages/csharp.ts +7 -1
  28. package/src/codegen/languages/dart.ts +274 -0
  29. package/src/codegen/languages/haxe.ts +7 -1
  30. package/src/codegen/languages/lua.ts +16 -4
  31. package/src/codegen/languages/ts.ts +5 -0
  32. package/src/codegen/parser.ts +97 -3
  33. package/src/codegen/types.ts +24 -0
  34. package/src/decoder/Resync.ts +8 -8
  35. package/src/encoder/ChangeRecorder.ts +1 -1
  36. package/src/encoder/ChangeTree.ts +41 -28
  37. package/src/encoder/EncodeDescriptor.ts +17 -38
  38. package/src/encoder/EncodeOperation.ts +3 -1
  39. package/src/encoder/Encoder.ts +100 -37
  40. package/src/encoder/Root.ts +18 -20
  41. package/src/encoder/StateView.ts +118 -47
  42. package/src/encoder/changeTree/inheritedFlags.ts +10 -10
  43. package/src/encoder/changeTree/liveIteration.ts +9 -9
  44. package/src/encoder/streaming.ts +8 -0
  45. package/src/encoding/spec.ts +1 -1
  46. package/src/index.ts +2 -2
  47. package/src/types/builder.ts +35 -31
  48. package/src/types/custom/StreamSchema.ts +1 -1
  49. package/src/types/symbols.ts +4 -11
  50. package/src/bench_bloat.ts +0 -173
  51. package/src/bench_churn.ts +0 -121
  52. package/src/bench_decode.ts +0 -221
  53. package/src/bench_decode_mem.ts +0 -165
  54. package/src/bench_encode.ts +0 -108
  55. package/src/bench_init.ts +0 -150
  56. package/src/bench_static.ts +0 -109
  57. package/src/bench_stream.ts +0 -295
  58. package/src/bench_view_cmp.ts +0 -142
@@ -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("StateView#add(), invalid object:", obj);
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
- } else if (
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
- this.markVisible(changeTree);
375
-
376
- // add to iterable list (only the explicitly added items)
377
- if (this.iterable && checkIncludeParent) {
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("StateView#remove(), invalid object:", obj);
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 marked invisible to this view — any future
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
- * Idempotent on re-subscribe. Subscribing to an already-subscribed
803
- * collection is a no-op.
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(collection: Ref): this {
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("StateView#subscribe(), invalid collection:", collection);
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("StateView#unsubscribe(), invalid collection:", collection);
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 / transient / static inheritance helpers for
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
- $staticFieldIndexes, $streamFieldIndexes,
11
- $transientFieldIndexes, $viewFieldIndexes,
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, IS_STATIC, IS_TRANSIENT,
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.isStatic) return;
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 / transient / static classification from
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 transient/static annotation checks into
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?.[$transientFieldIndexes]?.includes(parentIndex) ? IS_TRANSIENT : 0)
117
- | (parentMetadata?.[$staticFieldIndexes]?.includes(parentIndex) ? IS_STATIC : 0);
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 & IS_STATIC) {
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-transient indexes on a tree, emitting
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
- * Transient fields (`@transient`) are skipped — they're delivered only on
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 @transient inherit the skip (`tree.isTransient`).
8
+ * field is @patchOnly inherit the skip (`tree.isPatchOnly`).
9
9
  */
10
- import { $childType, $numFields, $transientFieldIndexes } from "../../types/symbols.js";
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 @transient from parent field: skip entirely.
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.isTransient) return;
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
- // (@transient skips below matter to the resync sweep — see
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 transientIndexes = metadata[$transientFieldIndexes];
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 (transientIndexes && transientIndexes.includes(i)) continue;
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
  }
@@ -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
  }
@@ -1,4 +1,4 @@
1
- export const SWITCH_TO_STRUCTURE = 255; // (decoding collides with DELETE_AND_ADD + fieldIndex = 63)
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
@@ -50,9 +50,9 @@ export {
50
50
  type,
51
51
  deprecated,
52
52
  defineTypes,
53
- owned,
54
53
  unreliable,
55
- transient,
54
+ patchOnly,
55
+ fullStateOnly,
56
56
  view,
57
57
  schema,
58
58
  entity,
@@ -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
- transient?: boolean;
24
+ patchOnly?: boolean;
26
25
  deprecated?: boolean;
27
26
  deprecatedThrows?: boolean;
28
- static?: boolean;
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 _transient = false;
78
+ private _patchOnly = false;
81
79
  private _deprecated = false;
82
80
  private _deprecatedThrows = true;
83
- private _static = false;
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 `.transient()`.
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
- * Mark this field as transientNOT persisted to full-sync snapshots
140
- * (`encodeAll` / `encodeAllView`). Late-joining clients see the field
141
- * only after its next mutation is emitted on a tick patch. Orthogonal
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
- transient(): this {
145
- this._transient = true;
142
+ patchOnly(): this {
143
+ this._patchOnly = true;
146
144
  return this;
147
145
  }
148
146
 
149
147
  /**
150
- * Mark this field as static.
151
- * - Primitive / Schema fields: synchronized once, skips change tracking.
152
- * - Stream fields (`t.stream(X).static()`): child elements are frozen
153
- * after add post-add field mutations on elements become no-ops.
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
- static(): this {
156
- this._static = true;
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()`, `.owned()`,
170
- * `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
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
- * -dist2(view.anchor, enemy)
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
- transient: this._transient,
260
+ patchOnly: this._patchOnly,
257
261
  deprecated: this._deprecated,
258
262
  deprecatedThrows: this._deprecatedThrows,
259
- static: this._static,
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 `.static()` on the field builder to suppress
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 {
@@ -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, and filterBitmask into one object stashed on
130
- * the constructor. Replaces 5 separate per-tree property chases /
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 $transientFieldIndexes = "~__transientFieldIndexes";
141
- export const $staticFieldIndexes = "~__staticFieldIndexes";
133
+ export const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
134
+ export const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
142
135
  export const $streamFieldIndexes = "~__streamFieldIndexes";
143
136
  export const $streamPriorities = "~__streamPriorities";