@colyseus/schema 4.0.20 → 5.0.1

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 (96) hide show
  1. package/README.md +2 -0
  2. package/build/Metadata.d.ts +56 -2
  3. package/build/Reflection.d.ts +28 -34
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +64 -17
  6. package/build/codegen/cli.cjs +84 -67
  7. package/build/codegen/cli.cjs.map +1 -1
  8. package/build/decoder/DecodeOperation.d.ts +48 -5
  9. package/build/decoder/Decoder.d.ts +2 -2
  10. package/build/decoder/strategy/Callbacks.d.ts +1 -1
  11. package/build/encoder/ChangeRecorder.d.ts +107 -0
  12. package/build/encoder/ChangeTree.d.ts +218 -69
  13. package/build/encoder/EncodeDescriptor.d.ts +63 -0
  14. package/build/encoder/EncodeOperation.d.ts +25 -2
  15. package/build/encoder/Encoder.d.ts +59 -3
  16. package/build/encoder/MapJournal.d.ts +62 -0
  17. package/build/encoder/RefIdAllocator.d.ts +35 -0
  18. package/build/encoder/Root.d.ts +94 -13
  19. package/build/encoder/StateView.d.ts +116 -8
  20. package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
  21. package/build/encoder/changeTree/liveIteration.d.ts +3 -0
  22. package/build/encoder/changeTree/parentChain.d.ts +24 -0
  23. package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
  24. package/build/encoder/streaming.d.ts +73 -0
  25. package/build/encoder/subscriptions.d.ts +25 -0
  26. package/build/index.cjs +5258 -1549
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5258 -1549
  30. package/build/index.mjs +5249 -1549
  31. package/build/index.mjs.map +1 -1
  32. package/build/input/InputDecoder.d.ts +32 -0
  33. package/build/input/InputEncoder.d.ts +117 -0
  34. package/build/input/index.cjs +7453 -0
  35. package/build/input/index.cjs.map +1 -0
  36. package/build/input/index.d.ts +3 -0
  37. package/build/input/index.mjs +7450 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +67 -9
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +192 -0
  42. package/build/types/custom/ArraySchema.d.ts +25 -4
  43. package/build/types/custom/CollectionSchema.d.ts +30 -2
  44. package/build/types/custom/MapSchema.d.ts +52 -3
  45. package/build/types/custom/SetSchema.d.ts +32 -2
  46. package/build/types/custom/StreamSchema.d.ts +114 -0
  47. package/build/types/symbols.d.ts +48 -5
  48. package/package.json +9 -3
  49. package/src/Metadata.ts +259 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +365 -252
  53. package/src/bench_bloat.ts +173 -0
  54. package/src/bench_decode.ts +221 -0
  55. package/src/bench_decode_mem.ts +165 -0
  56. package/src/bench_encode.ts +108 -0
  57. package/src/bench_init.ts +150 -0
  58. package/src/bench_static.ts +109 -0
  59. package/src/bench_stream.ts +295 -0
  60. package/src/bench_view_cmp.ts +142 -0
  61. package/src/codegen/languages/csharp.ts +0 -24
  62. package/src/codegen/parser.ts +83 -61
  63. package/src/decoder/DecodeOperation.ts +168 -63
  64. package/src/decoder/Decoder.ts +20 -10
  65. package/src/decoder/ReferenceTracker.ts +4 -0
  66. package/src/decoder/strategy/Callbacks.ts +30 -26
  67. package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
  68. package/src/encoder/ChangeRecorder.ts +276 -0
  69. package/src/encoder/ChangeTree.ts +674 -519
  70. package/src/encoder/EncodeDescriptor.ts +213 -0
  71. package/src/encoder/EncodeOperation.ts +107 -65
  72. package/src/encoder/Encoder.ts +630 -119
  73. package/src/encoder/MapJournal.ts +124 -0
  74. package/src/encoder/RefIdAllocator.ts +68 -0
  75. package/src/encoder/Root.ts +247 -120
  76. package/src/encoder/StateView.ts +592 -121
  77. package/src/encoder/changeTree/inheritedFlags.ts +217 -0
  78. package/src/encoder/changeTree/liveIteration.ts +74 -0
  79. package/src/encoder/changeTree/parentChain.ts +131 -0
  80. package/src/encoder/changeTree/treeAttachment.ts +171 -0
  81. package/src/encoder/streaming.ts +232 -0
  82. package/src/encoder/subscriptions.ts +71 -0
  83. package/src/index.ts +15 -3
  84. package/src/input/InputDecoder.ts +57 -0
  85. package/src/input/InputEncoder.ts +303 -0
  86. package/src/input/index.ts +3 -0
  87. package/src/types/HelperTypes.ts +121 -24
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +331 -0
  90. package/src/types/custom/ArraySchema.ts +210 -197
  91. package/src/types/custom/CollectionSchema.ts +115 -35
  92. package/src/types/custom/MapSchema.ts +162 -58
  93. package/src/types/custom/SetSchema.ts +128 -39
  94. package/src/types/custom/StreamSchema.ts +310 -0
  95. package/src/types/symbols.ts +93 -6
  96. package/src/utils.ts +4 -6
@@ -3,20 +3,76 @@ import { TypeContext } from "../types/TypeContext.js";
3
3
  import type { Iterator } from "../encoding/decode.js";
4
4
  import { Root } from "./Root.js";
5
5
  import type { StateView } from "./StateView.js";
6
- import type { ChangeSetName } from "./ChangeTree.js";
7
6
  export declare class Encoder<T extends Schema = any> {
8
7
  static BUFFER_SIZE: number;
9
8
  sharedBuffer: Uint8Array;
10
9
  context: TypeContext;
11
10
  state: T;
12
11
  root: Root;
13
- constructor(state: T);
12
+ constructor(state: T, root?: Root);
14
13
  protected setState(state: T): void;
15
- encode(it?: Iterator, view?: StateView, buffer?: Uint8Array, changeSetName?: ChangeSetName, isEncodeAll?: boolean, initialOffset?: number): Uint8Array;
14
+ private _encodeCtx;
15
+ /**
16
+ * Monotonic counter bumped at the start of every `encodeFullSync`
17
+ * call. The new value is copied to `ctx.gen` and stamped into every
18
+ * tree the walk touches (`tree._fullSyncGen = ctx.gen`); subsequent
19
+ * revisits of the same tree detect the equality and return early.
20
+ */
21
+ private _fullSyncGen;
22
+ encode(it?: Iterator, view?: StateView, buffer?: Uint8Array, initialOffset?: number): Uint8Array;
23
+ /**
24
+ * Per-tick encode of the UNRELIABLE channel. Walks `root.unreliableChanges`
25
+ * and emits each tree's `unreliableRecorder`. Safe to call at a different
26
+ * cadence than `encode()` (e.g. 60Hz vs 20Hz) — the two channels are
27
+ * fully independent.
28
+ */
29
+ encodeUnreliable(it?: Iterator, view?: StateView, buffer?: Uint8Array, initialOffset?: number): Uint8Array;
30
+ private _encodeChannel;
31
+ /**
32
+ * Structural DFS walker for full-sync (encodeAll / encodeAllView).
33
+ * Visits each ChangeTree in DFS preorder starting from the state root,
34
+ * emitting ADD operations for every currently-populated index via
35
+ * {@link ChangeTree.forEachLive}.
36
+ */
37
+ private encodeFullSync;
38
+ private _resizeBuffer;
16
39
  encodeAll(it?: Iterator, buffer?: Uint8Array): Uint8Array<ArrayBufferLike>;
17
40
  encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
18
41
  encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
42
+ /**
43
+ * Per-view unreliable encode. Walks `root.unreliableChanges` and emits
44
+ * only filtered fields visible to this view. Unlike `encodeView`, this
45
+ * doesn't emit `view.changes` entries — those are used only for
46
+ * reliable view bootstrap (membership ADDs) and are consumed by
47
+ * `encodeView` on the reliable channel.
48
+ */
49
+ encodeUnreliableView(view: StateView, sharedOffset: number, it: Iterator, bytes?: Uint8Array): Uint8Array<ArrayBufferLike>;
50
+ /**
51
+ * Broadcast-mode counterpart to `_emitStreamPriority`. Runs when NO
52
+ * StateViews are registered — streams fall back to broadcast mode
53
+ * where up to `maxPerTick` pending ADDs per stream emit to ALL clients
54
+ * each shared tick. DELETEs always flush (no cap).
55
+ *
56
+ * Emits directly to the shared-encode buffer: stream & element trees
57
+ * are `isFiltered=true` so the main loop would otherwise skip them.
58
+ * Runs AFTER the main loop so state / parent refs are already encoded
59
+ * — stream ADD ops reference element refIds, which must be decodable.
60
+ */
61
+ private _emitStreamBroadcast;
62
+ /**
63
+ * Walk every registered stream, pick up to `maxPerTick` positions from
64
+ * this view's pending backlog (priority-sorted when the view supplies a
65
+ * `streamPriority` callback), and hand each element to `view.add()`.
66
+ * `view.add()` seeds `view.changes` so the subsequent drain emits both
67
+ * the stream-link (position → refId) and the element's field data.
68
+ *
69
+ * Designed to run at the very top of `encodeView`, BEFORE the
70
+ * view.changes drain loop.
71
+ */
72
+ private _emitStreamPriority;
19
73
  discardChanges(): void;
74
+ discardUnreliableChanges(): void;
20
75
  tryEncodeTypeId(bytes: Uint8Array, baseType: typeof Schema, targetType: typeof Schema, it: Iterator): void;
21
76
  get hasChanges(): boolean;
77
+ get hasUnreliableChanges(): boolean;
22
78
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * MapJournal — owns the change-tracking and wire-protocol identity for a MapSchema.
3
+ *
4
+ * Replaces three parallel structures that previously lived on MapSchema:
5
+ * - `$indexes: Map<number, K>` → `keyByIndex`
6
+ * - `_collectionIndexes: { [key]: number }` (+ counter) → `indexByKey` + `nextIndex`
7
+ * - `deletedItems: { [index]: V }` → `snapshots`
8
+ *
9
+ * The journal is the single source of truth for:
10
+ * - assigning wire-protocol indexes to keys (server side)
11
+ * - looking up keys from wire indexes (server + client)
12
+ * - holding snapshots of removed values (for view-filter visibility checks)
13
+ *
14
+ * The journal does NOT track per-index operation types or maintain enqueue
15
+ * order — those remain on `ChangeTree` for now. A future iteration may pull
16
+ * them in too, but this version is intentionally scoped to the data-model
17
+ * cleanup so we can validate the abstraction before going deeper.
18
+ */
19
+ export declare class MapJournal<K = any> {
20
+ /** index → key (was MapSchema.$indexes). Used by encoder and decoder. */
21
+ keyByIndex: Map<number, K>;
22
+ /**
23
+ * key → index (was MapSchema._collectionIndexes — forward direction).
24
+ * Server-only. Plain object so MapSchema can expose it via a getter
25
+ * for backwards-compatible `_collectionIndexes?.[key]` access from
26
+ * ChangeTree.forEachChild and similar polymorphic call sites.
27
+ */
28
+ indexByKey: {
29
+ [key: string]: number;
30
+ };
31
+ /** Monotonic counter for assigning new indexes. Server-only. */
32
+ private nextIndex;
33
+ /**
34
+ * Snapshot of values at the moment they were deleted. Lazy — only
35
+ * allocated on first delete, since most maps are pure-grow and never
36
+ * touch this. Used by `MapSchema[$filter]` to check view visibility
37
+ * of a value that's already been removed from `$items` but whose
38
+ * DELETE op is still in the encode queue.
39
+ */
40
+ snapshots?: Map<number, any>;
41
+ /** Get the index assigned to a key, or undefined if never assigned. */
42
+ indexOf(key: K): number | undefined;
43
+ /** Assign and return a new wire index for an unseen key. */
44
+ assign(key: K): number;
45
+ /** Stash a value at the moment it's deleted (for filter visibility checks). */
46
+ snapshot(index: number, value: any): void;
47
+ /** Discard a snapshot — called when a deleted slot is being re-set. */
48
+ forgetSnapshot(index: number): void;
49
+ /** Look up a snapshot. Returns undefined if no DELETE is pending for this index. */
50
+ snapshotAt(index: number): any;
51
+ /** Decoder calls this when it sees an ADD/DELETE_AND_ADD on the wire. */
52
+ setIndex(index: number, key: K): void;
53
+ /** Reverse lookup: wire index → key. */
54
+ keyOf(index: number): K | undefined;
55
+ /**
56
+ * Called from MapSchema's $onEncodeEnd hook.
57
+ * Cleans up index/key mappings for entries that were deleted in this tick.
58
+ */
59
+ cleanupAfterEncode(): void;
60
+ /** Reset everything (called on .clear()). */
61
+ reset(): void;
62
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Allocates monotonically-increasing refIds with a reuse pool.
3
+ *
4
+ * `acquire()` pops from the free pool when available, otherwise bumps a
5
+ * counter. `release()` queues a refId for reuse; the id doesn't become
6
+ * acquirable until `flushReleases()` runs — the one-tick defer is what
7
+ * lets the encoder guarantee a DELETE for the old instance reaches the
8
+ * wire before the refId is handed to a new one.
9
+ *
10
+ * `reclaim()` handles "resurrection": a ref that was released but whose
11
+ * JS instance is still alive can be re-added to the tree, in which case
12
+ * the encoder must pull the refId back out of the pool before it's
13
+ * handed to an unrelated instance.
14
+ */
15
+ export declare class RefIdAllocator {
16
+ protected nextUniqueId: number;
17
+ private _free;
18
+ private _pending;
19
+ private _pooled;
20
+ constructor(startRefId?: number);
21
+ acquire(): number;
22
+ release(refId: number): void;
23
+ isPooled(refId: number): boolean;
24
+ /**
25
+ * Remove a refId from the pool. Called when a ref whose refId was
26
+ * released is being resurrected. O(n) scan of the relevant array,
27
+ * but resurrection is rare.
28
+ */
29
+ reclaim(refId: number): void;
30
+ /**
31
+ * Promote this tick's releases into the acquirable set. Called from
32
+ * `Encoder.discardChanges()` — never mid-encode.
33
+ */
34
+ flushReleases(): void;
35
+ }
@@ -1,28 +1,109 @@
1
1
  import { TypeContext } from "../types/TypeContext.js";
2
- import { ChangeTree, ChangeTreeList, ChangeSetName, type ChangeTreeNode } from "./ChangeTree.js";
2
+ import { ChangeTree, ChangeTreeList, type ChangeTreeNode } from "./ChangeTree.js";
3
+ import { $changes, $refId } from "../types/symbols.js";
4
+ import { RefIdAllocator } from "./RefIdAllocator.js";
5
+ import type { StateView } from "./StateView.js";
6
+ import type { StreamableState } from "./streaming.js";
7
+ /**
8
+ * Minimal shape the encoder needs from a streamable collection. Both
9
+ * `StreamSchema` and `.stream()`-decorated `MapSchema`/`SetSchema` etc.
10
+ * satisfy this via a single lazily-allocated `_stream` slot — the
11
+ * per-view / broadcast bookkeeping lives on that object, not directly
12
+ * on the collection, so non-streaming instances pay zero Map/Set
13
+ * allocation cost.
14
+ */
15
+ export interface Streamable {
16
+ [$refId]?: number;
17
+ [$changes]: ChangeTree;
18
+ _stream?: StreamableState;
19
+ _dropView(viewId: number): void;
20
+ _unregister(): void;
21
+ }
3
22
  export declare class Root {
4
23
  types: TypeContext;
5
- protected nextUniqueId: number;
24
+ /**
25
+ * Allocates and recycles refIds. See `RefIdAllocator` for the reuse
26
+ * pool semantics (one-tick defer + resurrection).
27
+ */
28
+ readonly refIds: RefIdAllocator;
6
29
  refCount: {
7
30
  [id: number]: number;
8
31
  };
9
32
  changeTrees: {
10
33
  [refId: number]: ChangeTree;
11
34
  };
12
- allChanges: ChangeTreeList;
13
- allFilteredChanges: ChangeTreeList;
35
+ /**
36
+ * Queue of all ChangeTrees with reliable dirty state. Per-tick encode()
37
+ * walks this queue; per-view encodeView() walks it too (filtering at
38
+ * emission time via tree.isFiltered + per-field @view tag).
39
+ */
14
40
  changes: ChangeTreeList;
15
- filteredChanges: ChangeTreeList;
16
- constructor(types: TypeContext);
17
- getNextUniqueId(): number;
41
+ /**
42
+ * Queue of all ChangeTrees with unreliable dirty state. Walked by
43
+ * `Encoder.encodeUnreliable` / `encodeUnreliableView`. A tree may live
44
+ * in both queues when the Schema has both reliable and unreliable
45
+ * fields dirty at the same time.
46
+ */
47
+ unreliableChanges: ChangeTreeList;
48
+ /**
49
+ * Free-list of ChangeTreeNode objects. Both queues share this pool —
50
+ * a node carries no queue affinity, only `{ changeTree, prev, next, position }`.
51
+ * Reusing nodes turns ~1,250 per-tick allocations (in bench) into 0.
52
+ */
53
+ private _nodePool;
54
+ /**
55
+ * View ID allocator for StateView visibility bitmaps on ChangeTree.
56
+ * Each new StateView claims the lowest free ID; releaseViewId() puts
57
+ * the ID back. Avoids unbounded bitmap growth across long-running rooms
58
+ * with view churn (clients joining/leaving).
59
+ */
60
+ private _nextViewId;
61
+ private _freeViewIds;
62
+ /** Allocate a fresh view ID (lowest available). */
63
+ acquireViewId(): number;
64
+ /** Return a view ID to the freelist for reuse. */
65
+ releaseViewId(id: number): void;
66
+ /**
67
+ * Currently-bound StateViews, keyed by view ID and held via `WeakRef`
68
+ * so the FinalizationRegistry backstop in StateView still works when
69
+ * the user forgets `dispose()`. Callers must iterate via
70
+ * `forEachActiveView`, which prunes dead entries.
71
+ */
72
+ activeViews: Map<number, WeakRef<StateView>>;
73
+ /**
74
+ * Streamable collections attached under this Root — `StreamSchema`
75
+ * plus any collection opted into streaming via `.stream()` on the
76
+ * builder. Encoder.encodeView / broadcast pass iterates this set to
77
+ * dispatch per-view / per-tick budget gates.
78
+ */
79
+ streamTrees: Set<Streamable>;
80
+ registerView(view: StateView): void;
81
+ unregisterView(view: StateView): void;
82
+ /**
83
+ * Iterate all live StateViews bound to this Root. Prunes entries
84
+ * whose underlying view has been garbage collected without an
85
+ * explicit `dispose()`.
86
+ */
87
+ forEachActiveView(cb: (view: StateView) => void): void;
88
+ registerStream(stream: Streamable): void;
89
+ unregisterStream(stream: Streamable): void;
90
+ constructor(types: TypeContext, startRefId?: number);
18
91
  add(changeTree: ChangeTree): boolean;
19
92
  remove(changeTree: ChangeTree): number;
20
93
  recursivelyMoveNextToParent(changeTree: ChangeTree): void;
21
94
  moveNextToParent(changeTree: ChangeTree): void;
22
- moveNextToParentInChangeTreeList(changeSetName: ChangeSetName, changeTree: ChangeTree): void;
23
- enqueueChangeTree(changeTree: ChangeTree, changeSet: 'changes' | 'filteredChanges' | 'allFilteredChanges' | 'allChanges', queueRootNode?: ChangeTreeNode): void;
24
- protected addToChangeTreeList(list: ChangeTreeList, changeTree: ChangeTree): ChangeTreeNode;
25
- protected updatePositionsAfterRemoval(list: ChangeTreeList, removedPosition: number): void;
26
- protected updatePositionsAfterMove(list: ChangeTreeList, node: ChangeTreeNode, newPosition: number): void;
27
- removeChangeFromChangeSet(changeSetName: ChangeSetName, changeTree: ChangeTree): boolean;
95
+ private _moveNextToParentInList;
96
+ enqueueChangeTree(changeTree: ChangeTree, existingNode?: ChangeTreeNode): void;
97
+ enqueueUnreliable(changeTree: ChangeTree, existingNode?: ChangeTreeNode): void;
98
+ private _appendToList;
99
+ /**
100
+ * Release a detached node back to the free-list. Caller must have
101
+ * already unlinked it from any list and cleared the changeTree's
102
+ * pointer to it. Clears `changeTree`/`prev`/`next` so the pool
103
+ * doesn't retain references through the GC root.
104
+ */
105
+ releaseNode(node: ChangeTreeNode): void;
106
+ removeFromQueue(changeTree: ChangeTree): boolean;
107
+ removeFromUnreliableQueue(changeTree: ChangeTree): boolean;
108
+ private _removeNode;
28
109
  }
@@ -1,4 +1,5 @@
1
- import { ChangeTree, IndexedOperations, Ref } from "./ChangeTree.js";
1
+ import { ChangeTree, Ref } from "./ChangeTree.js";
2
+ import { OPERATION } from "../encoding/spec.js";
2
3
  export declare function createView(iterable?: boolean): StateView;
3
4
  export declare class StateView {
4
5
  iterable: boolean;
@@ -8,26 +9,133 @@ export declare class StateView {
8
9
  */
9
10
  items: Ref[];
10
11
  /**
11
- * List of ChangeTree's that are visible to this view
12
+ * Unique ID assigned by the Root that owns this view's encoder. Used
13
+ * to address per-StateView visibility bits stored on each ChangeTree.
14
+ * Lazily allocated on first `add()` because the StateView itself
15
+ * doesn't know its Root until then.
12
16
  */
13
- visible: WeakSet<ChangeTree>;
17
+ id: number;
18
+ private _root?;
19
+ /** Cached `id >> 5` and `1 << (id & 31)` for the hot encode-loop check. */
20
+ private _slot;
21
+ private _bit;
14
22
  /**
15
- * List of ChangeTree's that are invisible to this view
23
+ * Per-tree custom-tag membership lives on each ChangeTree's `tagViews`
24
+ * map (keyed by tag, value is a per-view bitmap). The StateView only
25
+ * needs its slot/bit pair to read/write it. Replaces the legacy
26
+ * `tags: WeakMap<ChangeTree, Set<number>>` allocation per (view, tree).
16
27
  */
17
- invisible: WeakSet<ChangeTree>;
18
- tags?: WeakMap<ChangeTree, Set<number>>;
19
28
  /**
20
29
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
21
- * (This is used to force encoding a property, even if it was not changed)
30
+ * (Used to force encoding a property even if it was not changed.)
31
+ *
32
+ * Inner storage is a Map so the encode loop in `encodeView` can iterate
33
+ * directly with numeric keys — the legacy `{[index]: OPERATION}` shape
34
+ * forced an `Object.keys(...)` allocation + `Number(key)` parse per ref.
22
35
  */
23
- changes: Map<number, IndexedOperations>;
36
+ changes: Map<number, Map<number, OPERATION>>;
24
37
  constructor(iterable?: boolean);
38
+ /**
39
+ * Lazily bind this view to a Root and acquire a view ID. Called on
40
+ * the first add() because StateView is constructed before its target
41
+ * Root is known.
42
+ */
43
+ private _bindRoot;
44
+ /**
45
+ * Release this view's ID back to the Root for reuse, AND clear all
46
+ * visibility bits this view set on any ChangeTree. The clear is
47
+ * essential — without it, a future view that acquires this same ID
48
+ * would inherit our visibility state and see things it shouldn't
49
+ * (privacy bug). Documented in StateViewInternals.test.ts.
50
+ *
51
+ * Optional API but strongly recommended on client-leave; otherwise
52
+ * the FinalizationRegistry backstop runs at GC (non-deterministic).
53
+ */
54
+ dispose(): void;
55
+ /** True iff this view can see `tree`. */
56
+ isVisible(tree: ChangeTree): boolean;
57
+ /** Mark `tree` as visible to this view. */
58
+ markVisible(tree: ChangeTree): void;
59
+ /** Clear visibility bit. */
60
+ unmarkVisible(tree: ChangeTree): void;
61
+ /** True iff this view is subscribed to `tree`. */
62
+ isSubscribed(tree: ChangeTree): boolean;
63
+ /** Set the subscription bit on `tree`. */
64
+ private _setSubscribed;
65
+ /** Clear the subscription bit on `tree`. */
66
+ private _clearSubscribed;
67
+ /** True iff this view has previously marked `tree` as invisible. */
68
+ isInvisible(tree: ChangeTree): boolean;
69
+ /** Mark `tree` as invisible to this view (used by encode loop). */
70
+ markInvisible(tree: ChangeTree): void;
71
+ /** Clear invisible bit. */
72
+ unmarkInvisible(tree: ChangeTree): void;
73
+ /** True iff this view has `tag` associated with `tree`. */
74
+ hasTagOnTree(tree: ChangeTree, tag: number): boolean;
75
+ /** Mark `tree` as carrying `tag` for this view. */
76
+ addTag(tree: ChangeTree, tag: number): void;
77
+ /** Clear this view's `tag` bit on `tree`. */
78
+ removeTag(tree: ChangeTree, tag: number): void;
79
+ /** Clear ALL tag bits this view holds on `tree` (used when the per-tag isn't known). */
80
+ removeAllTagsOnTree(tree: ChangeTree): void;
25
81
  add(obj: Ref, tag?: number, checkIncludeParent?: boolean): boolean;
82
+ /**
83
+ * Internal: force-ship an object through `view.changes` without
84
+ * applying stream-element routing. Called by `Encoder._emitStreamPriority`
85
+ * when it's draining `_pendingByView` — the element is already out of
86
+ * pending at that point, so re-routing back into pending would be a
87
+ * loop. User code should always call `add()`.
88
+ */
89
+ _addImmediate(obj: Ref, tag?: number): void;
90
+ private _add;
91
+ /**
92
+ * Walk an isNew subtree marking each descendant visible. Counterpart
93
+ * to the `_add()` fast path: skips `view.changes` allocations because
94
+ * the shared encode pass emits the whole fresh subtree structurally
95
+ * — the view pass just needs visibility bits to let those emissions
96
+ * through the per-tree filter.
97
+ *
98
+ * Preserves the `@view()`-tag filter from `_add`'s forEachChild: a
99
+ * Schema descendant behind a non-matching field tag is skipped so
100
+ * tagged fields don't leak into a default-tag view. Collections have
101
+ * no per-field tags (`encDescriptor.tags` is empty), so the filter
102
+ * is a no-op for collection children.
103
+ *
104
+ * If a descendant has `isNew=false` (rare: a detached sub-collection
105
+ * was re-attached to a fresh parent), fall back to the full `_add`
106
+ * path for that branch so its cumulative state is emitted correctly.
107
+ */
108
+ private _markSubtreeVisible;
26
109
  protected addParentOf(childChangeTree: ChangeTree, tag: number): void;
27
110
  remove(obj: Ref, tag?: number): this;
28
111
  remove(obj: Ref, tag?: number, _isClear?: boolean): this;
29
112
  has(obj: Ref): boolean;
30
113
  hasTag(ob: Ref, tag?: number): boolean;
114
+ /**
115
+ * Persistent subscription to a collection's contents. Unlike `add()`,
116
+ * which is a one-shot bootstrap, `subscribe()` enrolls this view in
117
+ * future content changes — every subsequent push / set / add to the
118
+ * collection automatically flows to this view, and every removal
119
+ * queues a DELETE op. Works on every collection type:
120
+ *
121
+ * - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`:
122
+ * new children are force-shipped immediately (equivalent to
123
+ * `view.add(child)` per item).
124
+ * - `StreamSchema` (or `.stream()` maps/sets): new positions are
125
+ * enqueued into `_pendingByView` so the priority pass drains them
126
+ * respecting `maxPerTick`.
127
+ *
128
+ * Idempotent on re-subscribe. Subscribing to an already-subscribed
129
+ * collection is a no-op.
130
+ */
131
+ subscribe(collection: Ref): this;
132
+ /**
133
+ * End a persistent subscription. Queues DELETE for every already-sent
134
+ * child and clears any pending. After this call, future content
135
+ * changes on the collection no longer auto-flow to this view (though
136
+ * direct `view.add(element)` calls still work for per-entity use).
137
+ */
138
+ unsubscribe(collection: Ref): this;
31
139
  clear(): void;
32
140
  isChangeTreeVisible(changeTree: ChangeTree): boolean;
33
141
  protected _recursiveDeleteVisibleChangeTree(changeTree: ChangeTree): void;
@@ -0,0 +1,34 @@
1
+ import { type ChangeTree, type Ref } from "../ChangeTree.js";
2
+ /**
3
+ * Reconcile queue membership + inherited flags for a tree that just had
4
+ * its root/parent assigned. See `_checkInheritedFlags` for the flag
5
+ * inheritance logic.
6
+ */
7
+ export declare function checkIsFiltered(tree: ChangeTree, parent: Ref, parentIndex: number, _isNewChangeTree: boolean): void;
8
+ /**
9
+ * Inherit filter / unreliable / transient / static classification from
10
+ * the parent field's annotation. Collections (MapSchema / ArraySchema /
11
+ * etc.) inherit these from the Schema field that holds them.
12
+ *
13
+ * The common case — fresh tree attached to a parent field that carries
14
+ * none of the inheritable annotations — produces no flag change, no
15
+ * queue update, and no `parentFiltered` hit. Two small structural
16
+ * choices keep that case cheap without any precomputed descriptor
17
+ * bitmask:
18
+ *
19
+ * 1) Flag inheritance is a single bitwise OR onto `tree.flags`. The
20
+ * three per-annotation reads pack into `fieldBits`, the parent's
21
+ * inherited bits come from `parentChangeTree.flags` directly; one
22
+ * read-modify-write replaces three getter/setter cycles, and the
23
+ * bit diff against `beforeFlags` gives us the "just became static /
24
+ * unreliable" signal for the side-effect branches.
25
+ *
26
+ * 2) The `parentFiltered` string-key lookup is gated on
27
+ * `types.hasParentFilteredEntries`, which is only flipped true when
28
+ * `registerFilteredByParent` actually records an entry — i.e. when
29
+ * some @view-tagged field reaches this (child, parent, index)
30
+ * triple through the ancestry walk. Schemas with @view tags only on
31
+ * sibling fields (not along any attachment chain) skip the string
32
+ * concat + hash lookup entirely.
33
+ */
34
+ export declare function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex: number): void;
@@ -0,0 +1,3 @@
1
+ import type { ChangeTree } from "../ChangeTree.js";
2
+ export declare function forEachLive(tree: ChangeTree, callback: (index: number) => void): void;
3
+ export declare function forEachLiveWithCtx<C>(tree: ChangeTree, ctx: C, cb: (ctx: C, index: number) => void): void;
@@ -0,0 +1,24 @@
1
+ import type { ChangeTree, ParentChain, Ref } from "../ChangeTree.js";
2
+ /**
3
+ * Add a parent to the chain. If `parent` already exists anywhere in the
4
+ * chain, update the primary parent's index instead (matches legacy
5
+ * behavior).
6
+ */
7
+ export declare function addParent(tree: ChangeTree, parent: Ref, index: number): void;
8
+ /**
9
+ * Remove a parent from the chain.
10
+ * @returns true if parent was found and removed (Root.remove relies on this).
11
+ */
12
+ export declare function removeParent(tree: ChangeTree, parent: Ref): boolean;
13
+ /**
14
+ * Find the first parent in the chain matching `predicate`.
15
+ */
16
+ export declare function findParent(tree: ChangeTree, predicate: (parent: Ref, index: number) => boolean): ParentChain | undefined;
17
+ export declare function hasParent(tree: ChangeTree, predicate: (parent: Ref, index: number) => boolean): boolean;
18
+ /**
19
+ * Return all parents as an array (debug/test helper).
20
+ */
21
+ export declare function getAllParents(tree: ChangeTree): Array<{
22
+ ref: Ref;
23
+ index: number;
24
+ }>;
@@ -0,0 +1,13 @@
1
+ import { Root } from "../Root.js";
2
+ import type { ChangeTree, Ref } from "../ChangeTree.js";
3
+ export declare function setRoot(tree: ChangeTree, root: Root): void;
4
+ export declare function setParent(tree: ChangeTree, parent: Ref, root?: Root, parentIndex?: number): void;
5
+ export declare function forEachChild(tree: ChangeTree, callback: (change: ChangeTree, at: any) => void): void;
6
+ /**
7
+ * Closure-free variant of {@link forEachChild}. Hot setRoot / setParent
8
+ * recursion calls this once per new Schema instance attached to the
9
+ * tree — the per-call closure was the #1 JS hotspot in profile-baseline.
10
+ * Pass an explicit `ctx` so callers can hoist the callback to module
11
+ * scope and avoid the allocation.
12
+ */
13
+ export declare function forEachChildWithCtx<C>(tree: ChangeTree, ctx: C, callback: (ctx: C, change: ChangeTree, at: any) => void): void;
@@ -0,0 +1,73 @@
1
+ import type { Root, Streamable } from "./Root.js";
2
+ /**
3
+ * Thrown (from both the `FieldBuilder` chainable and the decorator's
4
+ * `addField` auto-flag) when a user attempts to stream an ArraySchema.
5
+ * Centralized so the two callsites emit the same diagnostic.
6
+ */
7
+ export declare const ARRAY_STREAM_NOT_SUPPORTED: string;
8
+ /**
9
+ * Per-instance bookkeeping for a streamable collection. Lazily allocated
10
+ * by `ensureStreamState` when the collection's ChangeTree picks up the
11
+ * `isStreamCollection` flag (or when the user touches `maxPerTick`).
12
+ */
13
+ export interface StreamableState {
14
+ /** Per-view ADD backlog: wire-indexes not yet sent to that view. */
15
+ pendingByView: Map<number, Set<number>>;
16
+ /** Per-view SENT set — decides whether `remove()` emits a DELETE. */
17
+ sentByView: Map<number, Set<number>>;
18
+ /** Broadcast-mode ADD backlog (no active views). */
19
+ broadcastPending: Set<number>;
20
+ /** Broadcast-mode SENT set. */
21
+ sentBroadcast: Set<number>;
22
+ /** Broadcast-mode DELETE queue — flushes next shared tick. */
23
+ broadcastDeletes: Set<number>;
24
+ /** Max ADD ops emitted per tick per view (or per shared tick). */
25
+ maxPerTick: number;
26
+ /**
27
+ * Priority callback seeded from the schema declaration. Receives the
28
+ * client's StateView and the candidate element; higher return values
29
+ * emit first. Broadcast `encode()` ignores this and drains FIFO.
30
+ * Instance-level override: assign to `stream.priority`.
31
+ */
32
+ priority?: (view: any, element: any) => number;
33
+ }
34
+ export declare function createStreamableState(): StreamableState;
35
+ /** Allocate `_stream` on first use (idempotent). Returns the state. */
36
+ export declare function ensureStreamState(s: Streamable): StreamableState;
37
+ /**
38
+ * Route an ADD into the pending backlogs.
39
+ * - No active views: push into broadcast pending (shared encode drains up
40
+ * to `maxPerTick` per tick).
41
+ * - With views: push into per-view pending for every currently-bound view.
42
+ */
43
+ export declare function streamRouteAdd(s: Streamable, root: Root, index: number): void;
44
+ /**
45
+ * Route a REMOVE: silent-drop if never sent, force DELETE if already sent.
46
+ * Returns `true` iff no wire op reached any channel (caller can skip
47
+ * follow-on work like snapshotting the deleted value).
48
+ */
49
+ export declare function streamRouteRemove(s: Streamable, root: Root, refId: number, index: number): boolean;
50
+ /**
51
+ * Queue DELETE ops for every already-sent entry on all channels and
52
+ * reset pending. Caller is responsible for actually clearing its own
53
+ * storage and releasing any element refs it owns.
54
+ */
55
+ export declare function streamRouteClear(s: Streamable, root: Root, refId: number): void;
56
+ /**
57
+ * Push a single position into `_pendingByView[viewId]` — the building
58
+ * block for `StateView.add(element)` when the element lives under a
59
+ * streamable collection. Idempotent for already-pending positions.
60
+ */
61
+ export declare function streamEnqueueForView(s: Streamable, viewId: number, index: number): void;
62
+ /**
63
+ * Unsubscribe a single position from a view. Returns true iff the
64
+ * element had already been sent and a DELETE op was queued on
65
+ * `view.changes`; false if it was only pending (silent drop) or not
66
+ * present at all.
67
+ */
68
+ export declare function streamDequeueForView(s: Streamable, viewId: number, refId: number, index: number, viewChanges: Map<number, Map<number, number>>): boolean;
69
+ /**
70
+ * Drop all per-view state for a disposing/GC'd StateView. Keeps memory
71
+ * bounded in long-running rooms with client churn.
72
+ */
73
+ export declare function streamDropView(s: Streamable, viewId: number): void;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Per-view collection subscriptions — `view.subscribe(collection)` opts
3
+ * a view into ALL future content changes of a collection, not just a
4
+ * one-shot snapshot. Covers every collection type:
5
+ *
6
+ * - `ArraySchema` / `MapSchema` / `SetSchema` / `CollectionSchema`: new
7
+ * children are force-shipped immediately via `view._addImmediate(child)`.
8
+ * Subsequent field mutations on those children emit via the normal
9
+ * view pass (the children are now visible).
10
+ * - `StreamSchema` (or a `.stream()` map/set): new positions are
11
+ * enqueued into `_pendingByView` so the encoder's priority pass
12
+ * drains them respecting `maxPerTick`.
13
+ *
14
+ * The propagation hook is in `changeTree/treeAttachment.ts setParent`
15
+ * — every new child attachment to a collection checks the parent tree's
16
+ * `subscribedViews` bitmap and fans out to subscribed views.
17
+ */
18
+ import type { ChangeTree, Ref } from "./ChangeTree.js";
19
+ import type { Root } from "./Root.js";
20
+ /**
21
+ * Walk the `subscribedViews` bitmap of `parentTree` and propagate a new
22
+ * child attachment to every subscribed view. Streams route through the
23
+ * priority/pending queue; all other collections force-ship immediately.
24
+ */
25
+ export declare function propagateNewChildToSubscribers(parentTree: ChangeTree, childIndex: number, childRef: Ref, root: Root): void;