@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.
- package/README.md +2 -0
- package/build/Metadata.d.ts +55 -2
- package/build/Reflection.d.ts +24 -30
- package/build/Schema.d.ts +70 -9
- package/build/annotations.d.ts +56 -13
- package/build/codegen/cli.cjs +84 -67
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/DecodeOperation.d.ts +48 -5
- package/build/decoder/Decoder.d.ts +2 -2
- package/build/decoder/strategy/Callbacks.d.ts +1 -1
- package/build/encoder/ChangeRecorder.d.ts +107 -0
- package/build/encoder/ChangeTree.d.ts +218 -69
- package/build/encoder/EncodeDescriptor.d.ts +63 -0
- package/build/encoder/EncodeOperation.d.ts +25 -2
- package/build/encoder/Encoder.d.ts +59 -3
- package/build/encoder/MapJournal.d.ts +62 -0
- package/build/encoder/RefIdAllocator.d.ts +35 -0
- package/build/encoder/Root.d.ts +94 -13
- package/build/encoder/StateView.d.ts +116 -8
- package/build/encoder/changeTree/inheritedFlags.d.ts +34 -0
- package/build/encoder/changeTree/liveIteration.d.ts +3 -0
- package/build/encoder/changeTree/parentChain.d.ts +24 -0
- package/build/encoder/changeTree/treeAttachment.d.ts +13 -0
- package/build/encoder/streaming.d.ts +73 -0
- package/build/encoder/subscriptions.d.ts +25 -0
- package/build/index.cjs +5202 -1552
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +5202 -1552
- package/build/index.mjs +5193 -1552
- package/build/index.mjs.map +1 -1
- package/build/input/InputDecoder.d.ts +32 -0
- package/build/input/InputEncoder.d.ts +117 -0
- package/build/input/index.cjs +7429 -0
- package/build/input/index.cjs.map +1 -0
- package/build/input/index.d.ts +3 -0
- package/build/input/index.mjs +7426 -0
- package/build/input/index.mjs.map +1 -0
- package/build/types/HelperTypes.d.ts +22 -8
- package/build/types/TypeContext.d.ts +9 -0
- package/build/types/builder.d.ts +162 -0
- package/build/types/custom/ArraySchema.d.ts +25 -4
- package/build/types/custom/CollectionSchema.d.ts +30 -2
- package/build/types/custom/MapSchema.d.ts +52 -3
- package/build/types/custom/SetSchema.d.ts +32 -2
- package/build/types/custom/StreamSchema.d.ts +114 -0
- package/build/types/symbols.d.ts +48 -5
- package/package.json +9 -3
- package/src/Metadata.ts +258 -31
- package/src/Reflection.ts +15 -13
- package/src/Schema.ts +176 -134
- package/src/annotations.ts +308 -236
- package/src/bench_bloat.ts +173 -0
- package/src/bench_decode.ts +221 -0
- package/src/bench_decode_mem.ts +165 -0
- package/src/bench_encode.ts +108 -0
- package/src/bench_init.ts +150 -0
- package/src/bench_static.ts +109 -0
- package/src/bench_stream.ts +295 -0
- package/src/bench_view_cmp.ts +142 -0
- package/src/codegen/languages/csharp.ts +0 -24
- package/src/codegen/parser.ts +83 -61
- package/src/decoder/DecodeOperation.ts +168 -63
- package/src/decoder/Decoder.ts +20 -10
- package/src/decoder/ReferenceTracker.ts +4 -0
- package/src/decoder/strategy/Callbacks.ts +30 -26
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +16 -13
- package/src/encoder/ChangeRecorder.ts +276 -0
- package/src/encoder/ChangeTree.ts +674 -519
- package/src/encoder/EncodeDescriptor.ts +213 -0
- package/src/encoder/EncodeOperation.ts +107 -65
- package/src/encoder/Encoder.ts +630 -119
- package/src/encoder/MapJournal.ts +124 -0
- package/src/encoder/RefIdAllocator.ts +68 -0
- package/src/encoder/Root.ts +247 -120
- package/src/encoder/StateView.ts +592 -121
- package/src/encoder/changeTree/inheritedFlags.ts +217 -0
- package/src/encoder/changeTree/liveIteration.ts +74 -0
- package/src/encoder/changeTree/parentChain.ts +131 -0
- package/src/encoder/changeTree/treeAttachment.ts +171 -0
- package/src/encoder/streaming.ts +232 -0
- package/src/encoder/subscriptions.ts +71 -0
- package/src/index.ts +15 -3
- package/src/input/InputDecoder.ts +57 -0
- package/src/input/InputEncoder.ts +303 -0
- package/src/input/index.ts +3 -0
- package/src/types/HelperTypes.ts +21 -9
- package/src/types/TypeContext.ts +14 -2
- package/src/types/builder.ts +285 -0
- package/src/types/custom/ArraySchema.ts +210 -197
- package/src/types/custom/CollectionSchema.ts +115 -35
- package/src/types/custom/MapSchema.ts +162 -58
- package/src/types/custom/SetSchema.ts +128 -39
- package/src/types/custom/StreamSchema.ts +310 -0
- package/src/types/symbols.ts +54 -6
- 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
|
+
}
|