@colyseus/schema 4.0.20 → 5.0.0

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 +55 -2
  3. package/build/Reflection.d.ts +24 -30
  4. package/build/Schema.d.ts +70 -9
  5. package/build/annotations.d.ts +56 -13
  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 +5202 -1552
  27. package/build/index.cjs.map +1 -1
  28. package/build/index.d.ts +7 -3
  29. package/build/index.js +5202 -1552
  30. package/build/index.mjs +5193 -1552
  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 +7429 -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 +7426 -0
  38. package/build/input/index.mjs.map +1 -0
  39. package/build/types/HelperTypes.d.ts +22 -8
  40. package/build/types/TypeContext.d.ts +9 -0
  41. package/build/types/builder.d.ts +162 -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 +258 -31
  50. package/src/Reflection.ts +15 -13
  51. package/src/Schema.ts +176 -134
  52. package/src/annotations.ts +308 -236
  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 +21 -9
  88. package/src/types/TypeContext.ts +14 -2
  89. package/src/types/builder.ts +285 -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 +54 -6
  96. package/src/utils.ts +4 -6
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Filter / unreliable / transient / static inheritance helpers for
3
+ * ChangeTree. Called by setRoot / setParent to derive child flags from
4
+ * the parent field's annotation + the parent tree's own state.
5
+ */
6
+ import { Metadata } from "../../Metadata.js";
7
+ import {
8
+ $changes, $childType,
9
+ $staticFieldIndexes, $streamFieldIndexes,
10
+ $transientFieldIndexes, $viewFieldIndexes,
11
+ // $unreliableFieldIndexes — tree-level unreliable currently disabled
12
+ // (see INHERITABLE_FLAGS comment in ChangeTree.ts). Per-field unreliable
13
+ // routing on primitive fields still uses it via `isFieldUnreliable()`.
14
+ } from "../../types/symbols.js";
15
+ import type { Schema } from "../../Schema.js";
16
+ import {
17
+ INHERITABLE_FLAGS, IS_STATIC, IS_TRANSIENT,
18
+ // IS_UNRELIABLE — tree-level unreliable currently disabled; see
19
+ // INHERITABLE_FLAGS comment in ChangeTree.ts.
20
+ type ChangeTree, type Ref,
21
+ } from "../ChangeTree.js";
22
+ import type { ICollectionChangeRecorder } from "../ChangeRecorder.js";
23
+ import type { Streamable } from "../Root.js";
24
+ import { ensureStreamState } from "../streaming.js";
25
+
26
+ /**
27
+ * Reconcile queue membership + inherited flags for a tree that just had
28
+ * its root/parent assigned. See `_checkInheritedFlags` for the flag
29
+ * inheritance logic.
30
+ */
31
+ export function checkIsFiltered(
32
+ tree: ChangeTree,
33
+ parent: Ref,
34
+ parentIndex: number,
35
+ _isNewChangeTree: boolean,
36
+ ): void {
37
+ checkInheritedFlags(tree, parent, parentIndex);
38
+
39
+ // Static trees never track per-tick changes — skip the queue entirely.
40
+ // Full-sync reaches them via structural walk (forEachChild).
41
+ if (tree.isStatic) return;
42
+
43
+ // Mutations that happened before setRoot (e.g. class-field initializers)
44
+ // recorded into the appropriate recorder but couldn't enqueue yet.
45
+ // Reconcile both queues now.
46
+ if (tree.has()) {
47
+ tree.root?.enqueueChangeTree(tree);
48
+ }
49
+ if (tree.unreliableRecorder?.has()) {
50
+ tree.root?.enqueueUnreliable(tree);
51
+ }
52
+ // Fresh tree with nothing recorded: still enqueue into its primary
53
+ // queue so the tree is reachable for its first mutation cycle.
54
+ //
55
+ // Tree-level unreliable is disabled (see INHERITABLE_FLAGS) so the
56
+ // unreliable branch is unreachable today. Kept as a comment for
57
+ // re-enablement.
58
+ if (!tree.has() && !(tree.unreliableRecorder?.has())) {
59
+ // if (tree.isUnreliable) {
60
+ // tree.root?.enqueueUnreliable(tree);
61
+ // } else {
62
+ tree.root?.enqueueChangeTree(tree);
63
+ // }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Inherit filter / unreliable / transient / static classification from
69
+ * the parent field's annotation. Collections (MapSchema / ArraySchema /
70
+ * etc.) inherit these from the Schema field that holds them.
71
+ *
72
+ * The common case — fresh tree attached to a parent field that carries
73
+ * none of the inheritable annotations — produces no flag change, no
74
+ * queue update, and no `parentFiltered` hit. Two small structural
75
+ * choices keep that case cheap without any precomputed descriptor
76
+ * bitmask:
77
+ *
78
+ * 1) Flag inheritance is a single bitwise OR onto `tree.flags`. The
79
+ * three per-annotation reads pack into `fieldBits`, the parent's
80
+ * inherited bits come from `parentChangeTree.flags` directly; one
81
+ * read-modify-write replaces three getter/setter cycles, and the
82
+ * bit diff against `beforeFlags` gives us the "just became static /
83
+ * unreliable" signal for the side-effect branches.
84
+ *
85
+ * 2) The `parentFiltered` string-key lookup is gated on
86
+ * `types.hasParentFilteredEntries`, which is only flipped true when
87
+ * `registerFilteredByParent` actually records an entry — i.e. when
88
+ * some @view-tagged field reaches this (child, parent, index)
89
+ * triple through the ancestry walk. Schemas with @view tags only on
90
+ * sibling fields (not along any attachment chain) skip the string
91
+ * concat + hash lookup entirely.
92
+ */
93
+ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex: number): void {
94
+ if (!parent) { return; }
95
+
96
+ // Walk up a collection level so `parent` lands on the Schema that
97
+ // owns the field at `parentIndex`. Field annotations live on Schema
98
+ // metadata; collections have none.
99
+ let parentChangeTree: ChangeTree = parent[$changes];
100
+ const parentIsCollection = !Metadata.isValidInstance(parent);
101
+ if (parentIsCollection) {
102
+ parent = parentChangeTree.parent;
103
+ parentIndex = parentChangeTree.parentIndex;
104
+ }
105
+
106
+ const parentMetadata: any = (parent as any)?.constructor?.[Symbol.metadata];
107
+
108
+ // Flag inheritance — pack the transient/static annotation checks into
109
+ // flag bits alongside the parent's own transitive flags, then OR onto
110
+ // `tree.flags` in one write. The bit diff tells us which flag just
111
+ // went from 0→1, cheaper than the prior `becameX = !tree.isX && (...)`
112
+ // pairs. IS_UNRELIABLE is omitted from both sides — tree-level
113
+ // unreliable is disabled (see INHERITABLE_FLAGS in ChangeTree.ts).
114
+ const fieldBits =
115
+ (parentMetadata?.[$transientFieldIndexes]?.includes(parentIndex) ? IS_TRANSIENT : 0)
116
+ | (parentMetadata?.[$staticFieldIndexes]?.includes(parentIndex) ? IS_STATIC : 0);
117
+ const inheritedBits = (parentChangeTree.flags & INHERITABLE_FLAGS) | fieldBits;
118
+ const beforeFlags = tree.flags;
119
+ tree.flags = beforeFlags | inheritedBits;
120
+ const gainedBits = inheritedBits & ~beforeFlags;
121
+
122
+ // If this tree just became static via inheritance, discard any entries
123
+ // that may have been recorded before the parent was assigned (e.g.
124
+ // `new Config().assign({...})` populates the recorder before the
125
+ // Config instance is attached). Static trees ship state via structural
126
+ // walk only; per-tick dirty entries would leak post-first-sync.
127
+ if (gainedBits & IS_STATIC) {
128
+ tree.reset();
129
+ tree.unreliableRecorder?.reset();
130
+ }
131
+ // Tree-level unreliable promotion is disabled — no tree can gain
132
+ // IS_UNRELIABLE via inheritance under the current decoration-time
133
+ // rejection (`Metadata.setUnreliable` on ref-type fields throws). The
134
+ // promotion block used to migrate reliable-recorder entries populated
135
+ // before attach (`new Item().assign({...})` then push into an
136
+ // unreliable collection) over to the unreliable recorder. Kept here
137
+ // as a comment for re-enablement if a safe tree-level unreliable
138
+ // semantics is designed later.
139
+ //
140
+ // else if ((gainedBits & IS_UNRELIABLE) && tree.has()) {
141
+ // const dst = tree.ensureUnreliableRecorder() as ICollectionChangeRecorder;
142
+ // tree.forEach((index, op) => {
143
+ // if (index < 0) dst.recordPure(op);
144
+ // else dst.record(index, op);
145
+ // });
146
+ // tree.reset();
147
+ // }
148
+
149
+ // Filter inheritance — only when the type context has any @view or
150
+ // @stream fields registered anywhere.
151
+ const types = tree.root?.types;
152
+ if (!types?.hasFilters) return;
153
+
154
+ const fieldHasViewTag = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex) ?? false;
155
+ // Stream fields are always view-scoped: the stream itself and its
156
+ // child elements must behave as filtered trees. Elements must NOT
157
+ // share visibility with the parent stream — `encodeView`'s priority
158
+ // pass is the only way elements become visible to a view.
159
+ const fieldHasStream = parentMetadata?.[$streamFieldIndexes]?.includes(parentIndex) ?? false;
160
+
161
+ // Skip the `parentFiltered` string-key lookup when no class has
162
+ // actually registered filter inheritance via ancestry. The lookup
163
+ // cannot hit in that state, so the string concat + hash lookup would
164
+ // be wasted work every attach.
165
+ let parentFiltered = false;
166
+ const parentConstructor = (parent as any)?.constructor as typeof Schema | undefined;
167
+ if (types.hasParentFilteredEntries && parentConstructor !== undefined) {
168
+ const refType = Metadata.isValidInstance(tree.ref)
169
+ ? tree.ref.constructor
170
+ : (tree.ref as any)[$childType];
171
+ const key = `${types.getTypeId(refType as typeof Schema)}-${types.schemas.get(parentConstructor)}-${parentIndex}`;
172
+ parentFiltered = types.parentFiltered[key] ?? false;
173
+ }
174
+
175
+ const newFiltered = parentChangeTree.isFiltered || parentFiltered || fieldHasViewTag || fieldHasStream;
176
+ tree.isFiltered = newFiltered;
177
+
178
+ // Flag collection trees attached to a `.stream()` field so the encoder
179
+ // routes their emission through the priority/broadcast pass. Applies
180
+ // when the tree IS the collection (not the collection's parent
181
+ // structure walk above). `parentIsCollection` was true at entry iff
182
+ // `tree.ref` is a child-of-collection (e.g. stream element) — we only
183
+ // set the flag on the collection itself, not its elements.
184
+ if (fieldHasStream && !parentIsCollection) {
185
+ tree.isStreamCollection = true;
186
+ // Allocate the lazy `_stream` slot once, here — so downstream
187
+ // helpers (`streamRouteAdd`, `_emitStreamPriority`, …) never need
188
+ // a null-check. `_stream` was always declared on the class at
189
+ // `undefined`, so this is a value write, not a shape transition.
190
+ const state = ensureStreamState(tree.ref as unknown as Streamable);
191
+ // Seed the priority callback from the schema declaration (builder's
192
+ // `.priority(fn)` or decorator's `{ stream: X, priority: fn }`).
193
+ // Instance-level overrides via `stream.priority = ...` win — only
194
+ // assign if the instance slot hasn't already been set.
195
+ if (state.priority === undefined) {
196
+ const declared = Metadata.getStreamPriority(parentMetadata, parentIndex);
197
+ if (declared !== undefined) state.priority = declared;
198
+ }
199
+ // Auto-register with `root.streamTrees` so the encoder's priority /
200
+ // broadcast pass picks it up. Covers both `StreamSchema` and any
201
+ // `.stream()`-opted collection (e.g. `MapSchema.stream()`).
202
+ tree.root?.registerStream(tree.ref as any);
203
+ }
204
+
205
+ if (newFiltered) {
206
+ const refType = Metadata.isValidInstance(tree.ref)
207
+ ? tree.ref.constructor
208
+ : (tree.ref as any)[$childType];
209
+ tree.isVisibilitySharedWithParent = (
210
+ parentChangeTree.isFiltered
211
+ && typeof refType !== "string"
212
+ && !fieldHasViewTag
213
+ && !fieldHasStream
214
+ && parentIsCollection
215
+ );
216
+ }
217
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Walk all currently-populated non-transient indexes on a tree, emitting
3
+ * each index once. Used by Root.add (re-stage), Encoder.encodeAll, and
4
+ * StateView.add to derive full-sync output from the live structure.
5
+ *
6
+ * Transient fields (`@transient`) are skipped — they're delivered only on
7
+ * tick patches and not persisted to snapshots. Collections whose parent
8
+ * field is @transient inherit the skip (`tree.isTransient`).
9
+ */
10
+ import { $childType, $numFields, $transientFieldIndexes } from "../../types/symbols.js";
11
+ import type { ChangeTree } from "../ChangeTree.js";
12
+
13
+ // Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
14
+ // keeps the no-ctx path closure-free and shares one walker implementation.
15
+ const _invokeNoCtx = (cb: (index: number) => void, index: number) => cb(index);
16
+
17
+ export function forEachLive(tree: ChangeTree, callback: (index: number) => void): void {
18
+ forEachLiveWithCtx(tree, callback, _invokeNoCtx);
19
+ }
20
+
21
+ export function forEachLiveWithCtx<C>(
22
+ tree: ChangeTree,
23
+ ctx: C,
24
+ cb: (ctx: C, index: number) => void,
25
+ ): void {
26
+ // `refTarget` skips the ArraySchema Proxy on every `.items` / `.$items`
27
+ // / `[$childType]` read below. Same reference as `ref` for non-proxied
28
+ // types. See `ChangeTree.refTarget` doc.
29
+ const ref = tree.refTarget as any;
30
+
31
+ if (ref[$childType] !== undefined) {
32
+ // Collection inheriting @transient from parent field: skip entirely.
33
+ if (tree.isTransient) return;
34
+
35
+ // Collection types: dispatch by shape.
36
+ if (Array.isArray(ref.items)) {
37
+ // ArraySchema
38
+ const items = ref.items as any[];
39
+ for (let i = 0, len = items.length; i < len; i++) {
40
+ if (items[i] !== undefined) cb(ctx, i);
41
+ }
42
+ } else if (ref.journal !== undefined) {
43
+ // MapSchema
44
+ for (const [index, key] of ref.journal.keyByIndex as Map<number, any>) {
45
+ if (ref.$items.has(key)) cb(ctx, index);
46
+ }
47
+ } else if (ref.$items !== undefined) {
48
+ // SetSchema / CollectionSchema (key === wire index)
49
+ for (const index of (ref.$items as Map<number, any>).keys()) {
50
+ cb(ctx, index);
51
+ }
52
+ }
53
+ } else {
54
+ // Schema: walk declared fields. `null` is treated as absent —
55
+ // the setter records a DELETE when a field is set to null or
56
+ // undefined, so it should not appear in full-sync output.
57
+ //
58
+ // Read names from the per-class descriptor's parallel array —
59
+ // saves the `metadata[i]` (per-field obj) + `.name` chain on
60
+ // every iteration of the full-sync DFS.
61
+ const metadata = tree.metadata;
62
+ if (!metadata) return;
63
+ const numFields = (metadata[$numFields] ?? -1) as number;
64
+ const transientIndexes = metadata[$transientFieldIndexes];
65
+ const names = tree.encDescriptor.names;
66
+ for (let i = 0; i <= numFields; i++) {
67
+ const name = names[i];
68
+ if (name === undefined) continue;
69
+ if (transientIndexes && transientIndexes.includes(i)) continue;
70
+ const value = ref[name];
71
+ if (value !== undefined && value !== null) cb(ctx, i);
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Parent-chain helpers for ChangeTree. A tree can have multiple parents
3
+ * (rare — instance sharing between Schema/Collection containers). The
4
+ * primary parent is stored inline on the tree (`parentRef` / `_parentIndex`);
5
+ * additional parents live in the `extraParents` linked list.
6
+ */
7
+ import { $changes } from "../../types/symbols.js";
8
+ import type { ChangeTree, ParentChain, Ref } from "../ChangeTree.js";
9
+
10
+ /**
11
+ * Add a parent to the chain. If `parent` already exists anywhere in the
12
+ * chain, update the primary parent's index instead (matches legacy
13
+ * behavior).
14
+ */
15
+ export function addParent(tree: ChangeTree, parent: Ref, index: number): void {
16
+ // Check if this parent already exists anywhere in the chain
17
+ if (tree.parentRef) {
18
+ if (tree.parentRef[$changes] === parent[$changes]) {
19
+ // Primary parent matches — update index
20
+ tree._parentIndex = index;
21
+ return;
22
+ }
23
+
24
+ // Check extra parents for duplicate
25
+ if (hasParent(tree, (p, _) => p[$changes] === parent[$changes])) {
26
+ // Match old behavior: update primary parent's index
27
+ tree._parentIndex = index;
28
+ return;
29
+ }
30
+ }
31
+
32
+ if (tree.parentRef === undefined) {
33
+ // First parent — store inline
34
+ tree.parentRef = parent;
35
+ tree._parentIndex = index;
36
+ } else {
37
+ // Push current inline parent to extraParents, set new as primary
38
+ tree.extraParents = {
39
+ ref: tree.parentRef,
40
+ index: tree._parentIndex,
41
+ next: tree.extraParents
42
+ };
43
+ tree.parentRef = parent;
44
+ tree._parentIndex = index;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Remove a parent from the chain.
50
+ * @returns true if parent was found and removed (Root.remove relies on this).
51
+ */
52
+ export function removeParent(tree: ChangeTree, parent: Ref): boolean {
53
+ //
54
+ // FIXME: it is required to check against `$changes` here because
55
+ // ArraySchema is instance of Proxy
56
+ //
57
+ if (tree.parentRef && tree.parentRef[$changes] === parent[$changes]) {
58
+ // Removing inline parent — promote first extra parent if exists
59
+ if (tree.extraParents) {
60
+ tree.parentRef = tree.extraParents.ref;
61
+ tree._parentIndex = tree.extraParents.index;
62
+ tree.extraParents = tree.extraParents.next;
63
+ } else {
64
+ tree.parentRef = undefined;
65
+ tree._parentIndex = undefined;
66
+ }
67
+ return true;
68
+ }
69
+
70
+ // Search extra parents
71
+ let current = tree.extraParents;
72
+ let previous = null;
73
+ while (current) {
74
+ if (current.ref[$changes] === parent[$changes]) {
75
+ if (previous) {
76
+ previous.next = current.next;
77
+ } else {
78
+ tree.extraParents = current.next;
79
+ }
80
+ return true;
81
+ }
82
+ previous = current;
83
+ current = current.next;
84
+ }
85
+ return tree.parentRef === undefined;
86
+ }
87
+
88
+ /**
89
+ * Find the first parent in the chain matching `predicate`.
90
+ */
91
+ export function findParent(
92
+ tree: ChangeTree,
93
+ predicate: (parent: Ref, index: number) => boolean,
94
+ ): ParentChain | undefined {
95
+ // Check inline parent first
96
+ if (tree.parentRef && predicate(tree.parentRef, tree._parentIndex)) {
97
+ return { ref: tree.parentRef, index: tree._parentIndex };
98
+ }
99
+
100
+ let current = tree.extraParents;
101
+ while (current) {
102
+ if (predicate(current.ref, current.index)) {
103
+ return current;
104
+ }
105
+ current = current.next;
106
+ }
107
+ return undefined;
108
+ }
109
+
110
+ export function hasParent(
111
+ tree: ChangeTree,
112
+ predicate: (parent: Ref, index: number) => boolean,
113
+ ): boolean {
114
+ return findParent(tree, predicate) !== undefined;
115
+ }
116
+
117
+ /**
118
+ * Return all parents as an array (debug/test helper).
119
+ */
120
+ export function getAllParents(tree: ChangeTree): Array<{ ref: Ref, index: number }> {
121
+ const parents: Array<{ ref: Ref, index: number }> = [];
122
+ if (tree.parentRef) {
123
+ parents.push({ ref: tree.parentRef, index: tree._parentIndex });
124
+ }
125
+ let current = tree.extraParents;
126
+ while (current) {
127
+ parents.push({ ref: current.ref, index: current.index });
128
+ current = current.next;
129
+ }
130
+ return parents;
131
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Tree-attachment helpers: setRoot / setParent + child-iteration recursion.
3
+ * Hot path: every new Schema/Collection instance attached to the root
4
+ * goes through here, which is why the recursive walk uses a hoisted
5
+ * callback + ctx-pool instead of per-call closures.
6
+ */
7
+ import type { MapSchema } from "../../types/custom/MapSchema.js";
8
+ import { $changes, $childType, $refTypeFieldIndexes } from "../../types/symbols.js";
9
+ import { Root } from "../Root.js";
10
+ import type { ChangeTree, Ref } from "../ChangeTree.js";
11
+ import { checkIsFiltered } from "./inheritedFlags.js";
12
+ import { propagateNewChildToSubscribers } from "../subscriptions.js";
13
+
14
+ export function setRoot(tree: ChangeTree, root: Root): void {
15
+ tree.root = root;
16
+
17
+ const isNewChangeTree = root.add(tree);
18
+
19
+ checkIsFiltered(tree, tree.parent, tree.parentIndex, isNewChangeTree);
20
+
21
+ // Recursively set root on child structures (closure-free hot path).
22
+ if (isNewChangeTree) {
23
+ forEachChildWithCtx(tree, root, _setRootChildCb);
24
+ }
25
+ }
26
+
27
+ export function setParent(
28
+ tree: ChangeTree,
29
+ parent: Ref,
30
+ root?: Root,
31
+ parentIndex?: number,
32
+ ): void {
33
+ tree.addParent(parent, parentIndex);
34
+
35
+ // avoid setting parents with empty `root`
36
+ if (!root) { return; }
37
+
38
+ const isNewChangeTree = root.add(tree);
39
+
40
+ // skip if parent is already set
41
+ if (root !== tree.root) {
42
+ tree.root = root;
43
+ checkIsFiltered(tree, parent, parentIndex, isNewChangeTree);
44
+ }
45
+
46
+ // Persistent-subscription propagation — when this new child is being
47
+ // attached to a collection that has one or more subscribed views,
48
+ // force-ship (or enqueue, for streams) the new child to each of them.
49
+ // Gated by `parent` being a collection (not a Schema) and the parent
50
+ // tree having a non-empty `subscribedViews` bitmap; both common-case
51
+ // short circuits are cheap.
52
+ const parentTree = parent?.[$changes];
53
+ if (
54
+ parentTree !== undefined &&
55
+ parentTree.subscribedViews !== undefined &&
56
+ // Collection check: `$childType` on the ref identifies Array/Map/
57
+ // Set/Collection/Stream. Schema-field parents don't have it.
58
+ (parent as any)[$childType] !== undefined
59
+ ) {
60
+ propagateNewChildToSubscribers(parentTree, parentIndex!, tree.ref, root);
61
+ }
62
+
63
+ // assign same parent on child structures (closure-free hot path).
64
+ // setParent recurses, so each depth gets its own ctx from a pool
65
+ // that grows to the recursion depth (typically tree height = 3-5).
66
+ if (isNewChangeTree) {
67
+ let ctx = _setParentCtxPool[_setParentDepth];
68
+ if (ctx === undefined) {
69
+ ctx = { parentRef: undefined!, root: undefined! };
70
+ _setParentCtxPool[_setParentDepth] = ctx;
71
+ }
72
+ ctx.parentRef = tree.ref;
73
+ ctx.root = root;
74
+ _setParentDepth++;
75
+ forEachChildWithCtx(tree, ctx, _setParentChildCb);
76
+ _setParentDepth--;
77
+ }
78
+ }
79
+
80
+ export function forEachChild(
81
+ tree: ChangeTree,
82
+ callback: (change: ChangeTree, at: any) => void,
83
+ ): void {
84
+ //
85
+ // assign same parent on child structures
86
+ //
87
+ if ((tree.ref as any)[$childType]) {
88
+ if (typeof ((tree.ref as any)[$childType]) !== "string") {
89
+ // MapSchema / ArraySchema, etc.
90
+ for (const [key, value] of (tree.ref as MapSchema).entries()) {
91
+ if (!value) { continue; } // sparse arrays can have undefined values
92
+ callback(value[$changes], (tree.ref as any)._collectionIndexes?.[key] ?? key);
93
+ };
94
+ }
95
+
96
+ } else {
97
+ const names = tree.encDescriptor.names;
98
+ for (const index of tree.metadata?.[$refTypeFieldIndexes] ?? []) {
99
+ const value = tree.ref[names[index] as keyof Ref];
100
+ if (!value) { continue; }
101
+ callback(value[$changes], index);
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Closure-free variant of {@link forEachChild}. Hot setRoot / setParent
108
+ * recursion calls this once per new Schema instance attached to the
109
+ * tree — the per-call closure was the #1 JS hotspot in profile-baseline.
110
+ * Pass an explicit `ctx` so callers can hoist the callback to module
111
+ * scope and avoid the allocation.
112
+ */
113
+ export function forEachChildWithCtx<C>(
114
+ tree: ChangeTree,
115
+ ctx: C,
116
+ callback: (ctx: C, change: ChangeTree, at: any) => void,
117
+ ): void {
118
+ // `refTarget` is the raw backing instance — identical to `ref` for all
119
+ // non-Proxy types (Schema / Map / Set / Collection / Stream), and the
120
+ // un-wrapped `$proxyTarget` for ArraySchema. Reading through it here
121
+ // skips the ArraySchema Proxy `get` trap on every `$childType`,
122
+ // `_collectionIndexes`, `.entries()`, `.items` lookup below — hot during
123
+ // the encodeAll DFS walk which touches every ArraySchema in the tree.
124
+ const ref = tree.refTarget as any;
125
+ if (ref[$childType]) {
126
+ if (typeof ref[$childType] !== "string") {
127
+ const collectionIndexes = ref._collectionIndexes;
128
+ for (const [key, value] of (ref as MapSchema).entries()) {
129
+ if (!value) { continue; }
130
+ callback(ctx, value[$changes], collectionIndexes?.[key] ?? key);
131
+ }
132
+ }
133
+ } else {
134
+ const metadata = tree.metadata;
135
+ const indexes = metadata?.[$refTypeFieldIndexes];
136
+ if (!indexes) return;
137
+ const names = tree.encDescriptor.names;
138
+ for (let i = 0, len = indexes.length; i < len; i++) {
139
+ const index = indexes[i];
140
+ const value = ref[names[index]];
141
+ if (!value) { continue; }
142
+ callback(ctx, value[$changes], index);
143
+ }
144
+ }
145
+ }
146
+
147
+ // Hoisted callbacks used by setRoot / setParent to avoid per-call
148
+ // closure allocation in the recursive attach path.
149
+
150
+ function _setRootChildCb(root: Root, child: ChangeTree, _index: any): void {
151
+ if (child.root !== root) {
152
+ child.setRoot(root);
153
+ } else {
154
+ root.add(child); // increment refCount
155
+ }
156
+ }
157
+
158
+ interface SetParentCtx { parentRef: Ref; root: Root; }
159
+ // Pool of ctx objects, indexed by setParent recursion depth. Grows to
160
+ // max depth seen (typically tree height = 3-5 in bench), then stays put.
161
+ const _setParentCtxPool: SetParentCtx[] = [];
162
+ let _setParentDepth = 0;
163
+
164
+ function _setParentChildCb(ctx: SetParentCtx, child: ChangeTree, index: any): void {
165
+ if (child.root === ctx.root) {
166
+ ctx.root.add(child);
167
+ ctx.root.moveNextToParent(child);
168
+ return;
169
+ }
170
+ child.setParent(ctx.parentRef, ctx.root, index);
171
+ }