@colyseus/schema 5.0.14 → 5.0.19
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 +7 -2
- package/build/Metadata.d.ts +10 -1
- package/build/codegen/api.d.ts +2 -0
- package/build/codegen/cli.cjs +322 -31
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/parser.d.ts +6 -1
- package/build/codegen/resolve.d.ts +25 -0
- package/build/codegen/types.d.ts +2 -0
- package/build/encoder/ChangeTree.d.ts +40 -12
- package/build/encoder/Encoder.d.ts +1 -1
- package/build/encoder/Root.d.ts +9 -0
- package/build/encoder/StateView.d.ts +38 -1
- package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
- package/build/encoder/changeTree/liveIteration.d.ts +8 -0
- package/build/encoder/changeTree/parentChain.d.ts +30 -8
- package/build/encoder/streaming.d.ts +1 -1
- package/build/index.cjs +3232 -2830
- package/build/index.cjs.map +1 -1
- package/build/index.js +3228 -2826
- package/build/index.mjs +3232 -2830
- package/build/index.mjs.map +1 -1
- package/build/types/TypeContext.d.ts +0 -17
- package/build/types/builder.d.ts +1 -5
- package/build/types/symbols.d.ts +1 -0
- package/package.json +1 -1
- package/src/Metadata.ts +59 -77
- package/src/Reflection.ts +9 -5
- package/src/annotations.ts +19 -13
- package/src/codegen/api.ts +3 -1
- package/src/codegen/cli.ts +5 -2
- package/src/codegen/parser.ts +69 -31
- package/src/codegen/resolve.ts +322 -0
- package/src/codegen/types.ts +4 -1
- package/src/decoder/DecodeOperation.ts +13 -2
- package/src/encoder/ChangeTree.ts +76 -25
- package/src/encoder/EncodeOperation.ts +10 -1
- package/src/encoder/Encoder.ts +52 -2
- package/src/encoder/Root.ts +28 -8
- package/src/encoder/StateView.ts +150 -66
- package/src/encoder/changeTree/inheritedFlags.ts +164 -45
- package/src/encoder/changeTree/liveIteration.ts +24 -3
- package/src/encoder/changeTree/parentChain.ts +72 -15
- package/src/encoder/streaming.ts +2 -1
- package/src/types/TypeContext.ts +5 -52
- package/src/types/builder.ts +14 -10
- package/src/types/custom/ArraySchema.ts +57 -14
- package/src/types/symbols.ts +3 -0
|
@@ -13,16 +13,17 @@ import {
|
|
|
13
13
|
// (see INHERITABLE_FLAGS comment in ChangeTree.ts). Per-field unreliable
|
|
14
14
|
// routing on primitive fields still uses it via `isFieldUnreliable()`.
|
|
15
15
|
} from "../../types/symbols.js";
|
|
16
|
-
import type { Schema } from "../../Schema.js";
|
|
17
16
|
import {
|
|
18
|
-
INHERITABLE_FLAGS, IS_FULL_STATE_ONLY, IS_PATCH_ONLY,
|
|
17
|
+
INHERITABLE_FLAGS, IS_FULL_STATE_ONLY, IS_PATCH_ONLY, PENDING_FILTER_REFRESH,
|
|
19
18
|
// IS_UNRELIABLE — tree-level unreliable currently disabled; see
|
|
20
19
|
// INHERITABLE_FLAGS comment in ChangeTree.ts.
|
|
21
20
|
type ChangeTree, type Ref,
|
|
22
21
|
} from "../ChangeTree.js";
|
|
23
22
|
import type { ICollectionChangeRecorder } from "../ChangeRecorder.js";
|
|
24
|
-
import type { Streamable } from "../Root.js";
|
|
23
|
+
import type { Root, Streamable } from "../Root.js";
|
|
25
24
|
import { ensureStreamState } from "../streaming.js";
|
|
25
|
+
import { restageLiveCb } from "./liveIteration.js";
|
|
26
|
+
import { isEdgeLive } from "./parentChain.js";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Reconcile queue membership + inherited flags for a tree that just had
|
|
@@ -71,25 +72,13 @@ export function checkIsFiltered(
|
|
|
71
72
|
* etc.) inherit these from the Schema field that holds them.
|
|
72
73
|
*
|
|
73
74
|
* The common case — fresh tree attached to a parent field that carries
|
|
74
|
-
* none of the inheritable annotations — produces no flag change
|
|
75
|
-
* queue update
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* inherited bits come from `parentChangeTree.flags` directly; one
|
|
82
|
-
* read-modify-write replaces three getter/setter cycles, and the
|
|
83
|
-
* bit diff against `beforeFlags` gives us the "just became static /
|
|
84
|
-
* unreliable" signal for the side-effect branches.
|
|
85
|
-
*
|
|
86
|
-
* 2) The `parentFiltered` string-key lookup is gated on
|
|
87
|
-
* `types.hasParentFilteredEntries`, which is only flipped true when
|
|
88
|
-
* `registerFilteredByParent` actually records an entry — i.e. when
|
|
89
|
-
* some @view-tagged field reaches this (child, parent, index)
|
|
90
|
-
* triple through the ancestry walk. Schemas with @view tags only on
|
|
91
|
-
* sibling fields (not along any attachment chain) skip the string
|
|
92
|
-
* concat + hash lookup entirely.
|
|
75
|
+
* none of the inheritable annotations — produces no flag change and no
|
|
76
|
+
* queue update. Flag inheritance is a single bitwise OR onto
|
|
77
|
+
* `tree.flags`: the per-annotation reads pack into `fieldBits`, the
|
|
78
|
+
* parent's inherited bits come from `parentChangeTree.flags` directly,
|
|
79
|
+
* and one read-modify-write replaces three getter/setter cycles. The bit
|
|
80
|
+
* diff against `beforeFlags` gives the "just became static / unreliable"
|
|
81
|
+
* signal for the side-effect branches.
|
|
93
82
|
*/
|
|
94
83
|
export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex: number): void {
|
|
95
84
|
if (!parent) { return; }
|
|
@@ -97,15 +86,17 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
97
86
|
// Walk up a collection level so `parent` lands on the Schema that
|
|
98
87
|
// owns the field at `parentIndex`. Field annotations live on Schema
|
|
99
88
|
// metadata; collections have none.
|
|
100
|
-
|
|
101
|
-
const parentIsCollection = !
|
|
89
|
+
const parentChangeTree: ChangeTree = parent[$changes];
|
|
90
|
+
const parentIsCollection = !parentChangeTree._isSchema;
|
|
91
|
+
let parentMetadata: any;
|
|
102
92
|
if (parentIsCollection) {
|
|
103
93
|
parent = parentChangeTree.parent;
|
|
104
94
|
parentIndex = parentChangeTree.parentIndex;
|
|
95
|
+
parentMetadata = parent?.[$changes].metadata;
|
|
96
|
+
} else {
|
|
97
|
+
parentMetadata = parentChangeTree.metadata;
|
|
105
98
|
}
|
|
106
99
|
|
|
107
|
-
const parentMetadata: any = (parent as any)?.constructor?.[Symbol.metadata];
|
|
108
|
-
|
|
109
100
|
// Flag inheritance — pack the patchOnly/static annotation checks into
|
|
110
101
|
// flag bits alongside the parent's own transitive flags, then OR onto
|
|
111
102
|
// `tree.flags` in one write. The bit diff tells us which flag just
|
|
@@ -159,21 +150,12 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
159
150
|
// pass is the only way elements become visible to a view.
|
|
160
151
|
const fieldHasStream = parentMetadata?.[$streamFieldIndexes]?.includes(parentIndex) ?? false;
|
|
161
152
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
if (types.hasParentFilteredEntries && parentConstructor !== undefined) {
|
|
169
|
-
const refType = Metadata.isValidInstance(tree.ref)
|
|
170
|
-
? tree.ref.constructor
|
|
171
|
-
: (tree.ref as any)[$childType];
|
|
172
|
-
const key = `${types.getTypeId(refType as typeof Schema)}-${types.schemas.get(parentConstructor)}-${parentIndex}`;
|
|
173
|
-
parentFiltered = types.parentFiltered[key] ?? false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const newFiltered = parentChangeTree.isFiltered || parentFiltered || fieldHasViewTag || fieldHasStream;
|
|
153
|
+
// Filtering is a property of the *attachment*, never of the child class:
|
|
154
|
+
// the same Schema class may sit under a @view field here and under a
|
|
155
|
+
// public field there (#204). `parentChangeTree.isFiltered` carries the
|
|
156
|
+
// ancestry — `setRoot` derives it parent-first before recursing — so the
|
|
157
|
+
// field annotation only has to answer for this one edge.
|
|
158
|
+
const newFiltered = parentChangeTree.isFiltered || fieldHasViewTag || fieldHasStream;
|
|
177
159
|
tree.isFiltered = newFiltered;
|
|
178
160
|
|
|
179
161
|
// Flag collection trees attached to a `.stream()` field so the encoder
|
|
@@ -204,9 +186,7 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
204
186
|
}
|
|
205
187
|
|
|
206
188
|
if (newFiltered) {
|
|
207
|
-
const
|
|
208
|
-
? tree.ref.constructor
|
|
209
|
-
: (tree.ref as any)[$childType];
|
|
189
|
+
const sharesEligible = _sharesEligible(tree);
|
|
210
190
|
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
211
191
|
// parent regardless of whether the parent is a collection. The
|
|
212
192
|
// `parentIsCollection` constraint that used to live here blocked
|
|
@@ -223,9 +203,148 @@ export function checkInheritedFlags(tree: ChangeTree, parent: Ref, parentIndex:
|
|
|
223
203
|
// are guaranteed to exist when that flag is set).
|
|
224
204
|
tree.isVisibilitySharedWithParent = (
|
|
225
205
|
parentChangeTree.isFiltered
|
|
226
|
-
&&
|
|
206
|
+
&& sharesEligible
|
|
227
207
|
&& !fieldHasStream
|
|
228
208
|
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG))
|
|
229
209
|
);
|
|
230
210
|
}
|
|
231
211
|
}
|
|
212
|
+
|
|
213
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
214
|
+
// Per-edge filter refresh — instance sharing across a @view boundary.
|
|
215
|
+
//
|
|
216
|
+
// `checkIsFiltered` classifies a tree from the edge it was FIRST attached
|
|
217
|
+
// through. A shared instance has N parent edges with different visibility,
|
|
218
|
+
// and the wire emits field data per-refId per-channel — so the tree-level
|
|
219
|
+
// invariant is:
|
|
220
|
+
//
|
|
221
|
+
// isFiltered ⇔ no fully-public root path reaches this tree
|
|
222
|
+
//
|
|
223
|
+
// Rather than reconciling eagerly at every attach/detach (whose ordering
|
|
224
|
+
// against the container's own storage mutation is fragile), edge events
|
|
225
|
+
// call `Root.enqueueFilterRefresh` and the encoder re-derives the flags at
|
|
226
|
+
// the top of the next encode — after every container mutation of the tick
|
|
227
|
+
// has settled — via `drainFilterRefresh`. `isFiltered` is only CONSUMED at
|
|
228
|
+
// encode time (recording is channel-agnostic), so the deferral is safe for
|
|
229
|
+
// wire routing; only same-tick StateView bootstrap reads see the stale
|
|
230
|
+
// flags, which at worst emits redundant (deduped) entries.
|
|
231
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Drain `root.pendingFilterRefresh`. Called by the encoder before any
|
|
235
|
+
* emission (per-tick channels and full-sync).
|
|
236
|
+
*/
|
|
237
|
+
export function drainFilterRefresh(root: Root): void {
|
|
238
|
+
const list = root.pendingFilterRefresh;
|
|
239
|
+
for (let i = 0; i < list.length; i++) {
|
|
240
|
+
const tree = list[i];
|
|
241
|
+
// Already settled as another entry's parent, or detached/recycled
|
|
242
|
+
// since it was queued.
|
|
243
|
+
if ((tree.flags & PENDING_FILTER_REFRESH) === 0) continue;
|
|
244
|
+
refreshFilterState(tree);
|
|
245
|
+
}
|
|
246
|
+
list.length = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Primitive-element collections never share visibility downward. One
|
|
251
|
+
* predicate for both derivations (`checkInheritedFlags` and
|
|
252
|
+
* `refreshFilterState`) — the InstanceSharing invariant test pins them
|
|
253
|
+
* together. `_isSchema` short-circuits the `$childType` probe for Schema
|
|
254
|
+
* trees (whose `$childType` is undefined and would pass anyway).
|
|
255
|
+
*/
|
|
256
|
+
function _sharesEligible(tree: ChangeTree): boolean {
|
|
257
|
+
return tree._isSchema || typeof (tree.refTarget as any)[$childType] !== "string";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Re-derive `isFiltered` (AND over live edges) and
|
|
262
|
+
* `isVisibilitySharedWithParent` (OR over live edges) from the parent
|
|
263
|
+
* chain. On a filtered→public flip, live state is re-staged — it may have
|
|
264
|
+
* already drained to view channels only, and clients that hold it decode
|
|
265
|
+
* the duplicate ADDs as no-ops (StateView bootstrap re-adds rely on the
|
|
266
|
+
* same property). The public→filtered flip needs no re-stage: the public
|
|
267
|
+
* container's DELETE already ships on the shared channel.
|
|
268
|
+
*
|
|
269
|
+
* A flip cascades into children so classifications inherited through this
|
|
270
|
+
* tree follow it; re-derivation is idempotent and a child that does not
|
|
271
|
+
* flip does not recurse, so the walk terminates on cyclic instance graphs.
|
|
272
|
+
*/
|
|
273
|
+
function refreshFilterState(tree: ChangeTree): void {
|
|
274
|
+
tree.flags &= ~PENDING_FILTER_REFRESH;
|
|
275
|
+
const root = tree.root;
|
|
276
|
+
if (root === undefined || tree.parentRef === undefined) return;
|
|
277
|
+
|
|
278
|
+
const sharesEligible = _sharesEligible(tree);
|
|
279
|
+
|
|
280
|
+
let bits = _edgeBits(tree, tree.parentRef, tree._parentIndex, sharesEligible);
|
|
281
|
+
// Saturated means no further edge can change the outcome.
|
|
282
|
+
for (let e = tree.extraParents; e !== undefined && bits !== EDGE_SATURATED; e = e.next) {
|
|
283
|
+
bits |= _edgeBits(tree, e.ref, e.index, sharesEligible);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// No live edge resolved (mid-detach churn) — keep the current
|
|
287
|
+
// classification rather than guess.
|
|
288
|
+
if (bits === 0) return;
|
|
289
|
+
|
|
290
|
+
tree.isVisibilitySharedWithParent = (bits & EDGE_SHARES) !== 0;
|
|
291
|
+
|
|
292
|
+
const newFiltered = (bits & EDGE_PUBLIC) === 0;
|
|
293
|
+
if (newFiltered === tree.isFiltered) return;
|
|
294
|
+
tree.isFiltered = newFiltered;
|
|
295
|
+
|
|
296
|
+
// Became public: clients that only ever had the view channel never saw
|
|
297
|
+
// this state. Static trees ship via structural walk instead.
|
|
298
|
+
if (!newFiltered && !tree.isFullStateOnly) {
|
|
299
|
+
tree.forEachLiveWithCtx(tree, restageLiveCb);
|
|
300
|
+
if (tree.has()) root.enqueueChangeTree(tree);
|
|
301
|
+
if (tree.unreliableRecorder?.has()) root.enqueueUnreliable(tree);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
tree.forEachChildWithCtx(tree, _cascadeRefreshCb);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const EDGE_LIVE = 1, EDGE_PUBLIC = 2, EDGE_SHARES = 4;
|
|
308
|
+
const EDGE_SATURATED = EDGE_LIVE | EDGE_PUBLIC | EDGE_SHARES;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Classify one parent edge: is it live, does it make the tree publicly
|
|
312
|
+
* reachable, does view visibility flow through it.
|
|
313
|
+
*/
|
|
314
|
+
function _edgeBits(tree: ChangeTree, parentRef: Ref, index: number, sharesEligible: boolean): number {
|
|
315
|
+
const parentTree: ChangeTree = parentRef[$changes];
|
|
316
|
+
if (parentTree.root !== tree.root || !isEdgeLive(tree, parentTree, index)) return 0;
|
|
317
|
+
|
|
318
|
+
// A queued parent must settle first — this edge reads its `isFiltered`.
|
|
319
|
+
// The flag-clear on entry terminates cycles, and a cascade re-entering
|
|
320
|
+
// `tree` is idempotent (same edges, same result — the outer pass then
|
|
321
|
+
// sees "no change").
|
|
322
|
+
if (parentTree.flags & PENDING_FILTER_REFRESH) refreshFilterState(parentTree);
|
|
323
|
+
|
|
324
|
+
let bits = EDGE_LIVE;
|
|
325
|
+
if (parentTree._isSchema) {
|
|
326
|
+
// A @view/stream-marked field stays filtered even under a public
|
|
327
|
+
// parent, and never shares visibility downward.
|
|
328
|
+
const marked = parentTree.encDescriptor.tags[index] !== undefined
|
|
329
|
+
|| parentTree.isFieldStream(index);
|
|
330
|
+
if (!marked) {
|
|
331
|
+
if (!parentTree.isFiltered) bits |= EDGE_PUBLIC;
|
|
332
|
+
else if (sharesEligible) bits |= EDGE_SHARES;
|
|
333
|
+
}
|
|
334
|
+
} else if (!parentTree.isFiltered) {
|
|
335
|
+
// Collection edge: the collection's own classification already
|
|
336
|
+
// folds in the field that holds it.
|
|
337
|
+
bits |= EDGE_PUBLIC;
|
|
338
|
+
} else if (sharesEligible && !parentTree.isStreamCollection) {
|
|
339
|
+
// #226: default-tag @view() collections keep per-item gating;
|
|
340
|
+
// untagged and non-default-tag @view(N) ones share.
|
|
341
|
+
const gp = parentTree.parent?.[$changes];
|
|
342
|
+
const tag = gp?._isSchema ? gp.encDescriptor.tags[parentTree.parentIndex] : undefined;
|
|
343
|
+
if (tag !== DEFAULT_VIEW_TAG) bits |= EDGE_SHARES;
|
|
344
|
+
}
|
|
345
|
+
return bits;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const _cascadeRefreshCb = (_parentTree: ChangeTree, child: ChangeTree, _index: any): void => {
|
|
349
|
+
refreshFilterState(child);
|
|
350
|
+
};
|
|
@@ -6,10 +6,31 @@
|
|
|
6
6
|
* Patch-only fields (`@patchOnly`) are skipped — they're delivered only on
|
|
7
7
|
* tick patches and not persisted to snapshots. Collections whose parent
|
|
8
8
|
* field is @patchOnly inherit the skip (`tree.isPatchOnly`).
|
|
9
|
+
*
|
|
10
|
+
* `@deprecated()` fields are skipped too: the decorator swaps the field's
|
|
11
|
+
* prototype accessor for a throwing getter, so `ref[name]` below would blow
|
|
12
|
+
* up full sync for the whole state. Both skips ride one decoration-time
|
|
13
|
+
* list (`$fullSyncSkipIndexes`) so the walk pays a single metadata lookup.
|
|
9
14
|
*/
|
|
10
|
-
import {
|
|
15
|
+
import { OPERATION } from "../../encoding/spec.js";
|
|
16
|
+
import { $childType, $numFields, $fullSyncSkipIndexes } from "../../types/symbols.js";
|
|
11
17
|
import type { ChangeTree } from "../ChangeTree.js";
|
|
12
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Re-stage one live index as a fresh ADD on its channel. Shared by
|
|
21
|
+
* `Root.add` (refCount-0 / NEEDS_RESTAGE re-adds) and
|
|
22
|
+
* `inheritedFlags.refreshFilterState` (filtered→public flip) via
|
|
23
|
+
* `forEachLiveWithCtx(tree, restageLiveCb)` — one home for the
|
|
24
|
+
* unreliable-routing rule.
|
|
25
|
+
*/
|
|
26
|
+
export const restageLiveCb = (tree: ChangeTree, fieldIndex: number): void => {
|
|
27
|
+
if (tree.isFieldUnreliable(fieldIndex)) {
|
|
28
|
+
tree.ensureUnreliableRecorder().record(fieldIndex, OPERATION.ADD);
|
|
29
|
+
} else {
|
|
30
|
+
tree.record(fieldIndex, OPERATION.ADD);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
13
34
|
// Adapter that lets `forEachLive(cb)` delegate to `forEachLiveWithCtx(cb, _invokeNoCtx)` —
|
|
14
35
|
// keeps the no-ctx path closure-free and shares one walker implementation.
|
|
15
36
|
const _invokeNoCtx = (cb: (index: number) => void, index: number) => cb(index);
|
|
@@ -65,12 +86,12 @@ export function forEachLiveWithCtx<C>(
|
|
|
65
86
|
const metadata = tree.metadata;
|
|
66
87
|
if (!metadata) return;
|
|
67
88
|
const numFields = (metadata[$numFields] ?? -1) as number;
|
|
68
|
-
const
|
|
89
|
+
const skipIndexes = metadata[$fullSyncSkipIndexes] as number[] | undefined;
|
|
69
90
|
const names = tree.encDescriptor.names;
|
|
70
91
|
for (let i = 0; i <= numFields; i++) {
|
|
71
92
|
const name = names[i];
|
|
72
93
|
if (name === undefined) continue;
|
|
73
|
-
if (
|
|
94
|
+
if (skipIndexes && skipIndexes.includes(i)) continue;
|
|
74
95
|
const value = ref[name];
|
|
75
96
|
if (value !== undefined && value !== null) cb(ctx, i);
|
|
76
97
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* additional parents live in the `extraParents` linked list.
|
|
6
6
|
*/
|
|
7
7
|
import { $changes } from "../../types/symbols.js";
|
|
8
|
-
import type { ChangeTree,
|
|
8
|
+
import type { ChangeTree, ParentEntry, Ref } from "../ChangeTree.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Add a parent to the chain. If `parent` already exists anywhere in the
|
|
@@ -115,39 +115,69 @@ export function removeParent(tree: ChangeTree, parent: Ref): boolean {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* First parent matching `predicate`, as a detached `ParentEntry`. Never returns
|
|
119
|
+
* a live `ParentChain` node — the inline parent has no node to return in the
|
|
120
|
+
* first place, so handing out the real node for the `extraParents` case only
|
|
121
|
+
* would make writes land or vanish depending on which parent matched. Use
|
|
122
|
+
* `setParentIndex` to move an index and `indexInParent` to read one.
|
|
119
123
|
*/
|
|
120
124
|
export function findParent(
|
|
121
125
|
tree: ChangeTree,
|
|
122
126
|
predicate: (parent: Ref, index: number) => boolean,
|
|
123
|
-
):
|
|
124
|
-
|
|
125
|
-
if (tree.parentRef && predicate(tree.parentRef, tree._parentIndex)) {
|
|
127
|
+
): ParentEntry | undefined {
|
|
128
|
+
if (tree.parentRef !== undefined && predicate(tree.parentRef, tree._parentIndex)) {
|
|
126
129
|
return { ref: tree.parentRef, index: tree._parentIndex };
|
|
127
130
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (predicate(current.ref, current.index)) {
|
|
132
|
-
return current;
|
|
131
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
132
|
+
if (predicate(entry.ref, entry.index)) {
|
|
133
|
+
return { ref: entry.ref, index: entry.index };
|
|
133
134
|
}
|
|
134
|
-
current = current.next;
|
|
135
135
|
}
|
|
136
136
|
return undefined;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** Walks in place — `addParent` calls this per shared-instance attach. */
|
|
139
140
|
export function hasParent(
|
|
140
141
|
tree: ChangeTree,
|
|
141
142
|
predicate: (parent: Ref, index: number) => boolean,
|
|
142
143
|
): boolean {
|
|
143
|
-
|
|
144
|
+
if (tree.parentRef !== undefined && predicate(tree.parentRef, tree._parentIndex)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
148
|
+
if (predicate(entry.ref, entry.index)) { return true; }
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/**
|
|
147
|
-
*
|
|
154
|
+
* Wire index `tree` holds inside `parent`, or undefined when `parent` is
|
|
155
|
+
* nowhere in the chain. Allocation-free variant of `findParent` for the
|
|
156
|
+
* encodeView drain, which resolves identity-keyed view entries per emission.
|
|
157
|
+
*
|
|
158
|
+
* A child detached from `parent` this tick usually still resolves: Root.remove
|
|
159
|
+
* leaves the child's own parent link dangling, and the staged snapshot keeps
|
|
160
|
+
* the child in `tmpItems` (so reindexes keep the index current) until
|
|
161
|
+
* `$onEncodeEnd` — which runs after the drain.
|
|
148
162
|
*/
|
|
149
|
-
export function
|
|
150
|
-
|
|
163
|
+
export function indexInParent(tree: ChangeTree, parent: Ref): number | undefined {
|
|
164
|
+
// `$changes` comparison — ArraySchema parents arrive proxied.
|
|
165
|
+
if (tree.parentRef && tree.parentRef[$changes] === parent[$changes]) {
|
|
166
|
+
return tree._parentIndex;
|
|
167
|
+
}
|
|
168
|
+
for (let entry = tree.extraParents; entry !== undefined; entry = entry.next) {
|
|
169
|
+
if (entry.ref[$changes] === parent[$changes]) {
|
|
170
|
+
return entry.index;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Return all parents as detached entries (debug/test helper).
|
|
178
|
+
*/
|
|
179
|
+
export function getAllParents(tree: ChangeTree): ParentEntry[] {
|
|
180
|
+
const parents: ParentEntry[] = [];
|
|
151
181
|
if (tree.parentRef) {
|
|
152
182
|
parents.push({ ref: tree.parentRef, index: tree._parentIndex });
|
|
153
183
|
}
|
|
@@ -158,3 +188,30 @@ export function getAllParents(tree: ChangeTree): Array<{ ref: Ref, index: number
|
|
|
158
188
|
}
|
|
159
189
|
return parents;
|
|
160
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* True iff `parent` currently holds `tree`. Detached edges linger in the
|
|
194
|
+
* parent chain (load-bearing for same-tick view drains — see
|
|
195
|
+
* `indexInParent` above), so the chain alone cannot answer which edges
|
|
196
|
+
* are live. ArraySchema is probed by scanning `items`: the recorded slot
|
|
197
|
+
* can go stale after reorders, and `items` — unlike `$getByIndex`'s staged
|
|
198
|
+
* view — reflects the tick's completed mutations.
|
|
199
|
+
*/
|
|
200
|
+
export function isEdgeLive(tree: ChangeTree, parentTree: ChangeTree, index: number): boolean {
|
|
201
|
+
const target = parentTree.refTarget as any;
|
|
202
|
+
if (parentTree.isArray) {
|
|
203
|
+
// Read `items` directly, not `$getByIndex` — the latter serves the
|
|
204
|
+
// staged (tmpItems) view, which can still hold a same-tick removal.
|
|
205
|
+
const items = target.items;
|
|
206
|
+
const at = items[index];
|
|
207
|
+
if (at !== undefined && at[$changes] === tree) return true;
|
|
208
|
+
// Recorded slot goes stale after reorders — scan before declaring dead.
|
|
209
|
+
for (let i = 0, len = items.length; i < len; i++) {
|
|
210
|
+
const v = items[i];
|
|
211
|
+
if (v !== undefined && v[$changes] === tree) return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const at = parentTree.getValue(index);
|
|
216
|
+
return at !== undefined && at[$changes] === tree;
|
|
217
|
+
}
|
package/src/encoder/streaming.ts
CHANGED
|
@@ -204,7 +204,8 @@ export function streamDequeueForView(
|
|
|
204
204
|
viewId: number,
|
|
205
205
|
refId: number,
|
|
206
206
|
index: number,
|
|
207
|
-
|
|
207
|
+
// widened key: StateView.changes carries identity-keyed array entries too
|
|
208
|
+
viewChanges: Map<number, Map<number | object, number>>,
|
|
208
209
|
): boolean {
|
|
209
210
|
const st = s._stream;
|
|
210
211
|
if (st === undefined) return false;
|
package/src/types/TypeContext.ts
CHANGED
|
@@ -8,16 +8,6 @@ export class TypeContext {
|
|
|
8
8
|
schemas = new Map<typeof Schema, number>();
|
|
9
9
|
|
|
10
10
|
hasFilters: boolean = false;
|
|
11
|
-
parentFiltered: {[typeIdAndParentIndex: string]: boolean} = {};
|
|
12
|
-
/**
|
|
13
|
-
* True iff `parentFiltered` has at least one entry. Flipped on by
|
|
14
|
-
* `registerFilteredByParent` and read in `checkInheritedFlags` as a
|
|
15
|
-
* cheap gate to skip the string-keyed `parentFiltered[key]` lookup
|
|
16
|
-
* when no class has registered filter inheritance via ancestry — the
|
|
17
|
-
* common case when @view tags exist only on sibling fields, not
|
|
18
|
-
* along any attachment chain.
|
|
19
|
-
*/
|
|
20
|
-
hasParentFilteredEntries: boolean = false;
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* For inheritance support
|
|
@@ -84,17 +74,13 @@ export class TypeContext {
|
|
|
84
74
|
return this.schemas.get(klass);
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
private discoverTypes(klass: typeof Schema
|
|
88
|
-
if (parentHasViewTag) {
|
|
89
|
-
this.registerFilteredByParent(klass, parentType, parentIndex);
|
|
90
|
-
}
|
|
91
|
-
|
|
77
|
+
private discoverTypes(klass: typeof Schema) {
|
|
92
78
|
// skip if already registered
|
|
93
79
|
if (!this.add(klass)) { return; }
|
|
94
80
|
|
|
95
81
|
// add classes inherited from this base class
|
|
96
82
|
TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
|
|
97
|
-
this.discoverTypes(child
|
|
83
|
+
this.discoverTypes(child);
|
|
98
84
|
});
|
|
99
85
|
|
|
100
86
|
// add parent classes
|
|
@@ -120,7 +106,6 @@ export class TypeContext {
|
|
|
120
106
|
const index = fieldIndex as any as number;
|
|
121
107
|
|
|
122
108
|
const fieldType = metadata[index].type;
|
|
123
|
-
const fieldHasViewTag = (metadata[index].tag !== undefined);
|
|
124
109
|
|
|
125
110
|
if (typeof (fieldType) === "string") {
|
|
126
111
|
continue;
|
|
@@ -133,7 +118,7 @@ export class TypeContext {
|
|
|
133
118
|
}
|
|
134
119
|
|
|
135
120
|
if (typeof (fieldType) === "function") {
|
|
136
|
-
this.discoverTypes(fieldType as typeof Schema
|
|
121
|
+
this.discoverTypes(fieldType as typeof Schema);
|
|
137
122
|
|
|
138
123
|
} else {
|
|
139
124
|
const type = Object.values(fieldType)[0];
|
|
@@ -143,47 +128,15 @@ export class TypeContext {
|
|
|
143
128
|
continue;
|
|
144
129
|
}
|
|
145
130
|
|
|
146
|
-
this.discoverTypes(type as typeof Schema
|
|
131
|
+
this.discoverTypes(type as typeof Schema);
|
|
147
132
|
}
|
|
148
133
|
}
|
|
149
134
|
}
|
|
150
135
|
|
|
151
|
-
/**
|
|
152
|
-
* Keep track of which classes have filters applied.
|
|
153
|
-
* Format: `${typeid}-${parentTypeid}-${parentIndex}`
|
|
154
|
-
*/
|
|
155
|
-
private registerFilteredByParent(schema: typeof Schema, parentType?: typeof Schema, parentIndex?: number) {
|
|
156
|
-
const typeid = this.schemas.get(schema) ?? this.schemas.size;
|
|
157
|
-
|
|
158
|
-
let key = `${typeid}`;
|
|
159
|
-
if (parentType) { key += `-${this.schemas.get(parentType)}`; }
|
|
160
|
-
|
|
161
|
-
key += `-${parentIndex}`;
|
|
162
|
-
this.parentFiltered[key] = true;
|
|
163
|
-
this.hasParentFilteredEntries = true;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
136
|
debug() {
|
|
167
|
-
let parentFiltered = "";
|
|
168
|
-
|
|
169
|
-
for (const key in this.parentFiltered) {
|
|
170
|
-
const keys: number[] = key.split("-").map(Number);
|
|
171
|
-
const fieldIndex = keys.pop();
|
|
172
|
-
|
|
173
|
-
parentFiltered += `\n\t\t`;
|
|
174
|
-
parentFiltered += `${key}: ${keys.reverse().map((id, i) => {
|
|
175
|
-
const klass = this.types[id];
|
|
176
|
-
const metadata: Metadata = klass[Symbol.metadata];
|
|
177
|
-
let txt = klass.name;
|
|
178
|
-
if (i === 0) { txt += `[${metadata[fieldIndex].name}]`; }
|
|
179
|
-
return `${txt}`;
|
|
180
|
-
}).join(" -> ")}`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
137
|
return `TypeContext ->\n` +
|
|
184
138
|
`\tSchema types: ${this.schemas.size}\n` +
|
|
185
|
-
`\thasFilters: ${this.hasFilters}
|
|
186
|
-
`\tparentFiltered:${parentFiltered}`;
|
|
139
|
+
`\thasFilters: ${this.hasFilters}`;
|
|
187
140
|
}
|
|
188
141
|
|
|
189
142
|
}
|
package/src/types/builder.ts
CHANGED
|
@@ -307,16 +307,24 @@ function primitive<TBase>(name: RawPrimitiveType): PrimitiveFactory<TBase> {
|
|
|
307
307
|
return (() => new FieldBuilder<TBase>(name)) as PrimitiveFactory<TBase>;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
//
|
|
310
|
+
// Collection element: a Schema class or a primitive type NAME (`"string"`).
|
|
311
311
|
export type ChildType =
|
|
312
312
|
| RawPrimitiveType
|
|
313
|
-
| Constructor<Schema
|
|
314
|
-
| FieldBuilder<any>;
|
|
313
|
+
| Constructor<Schema>;
|
|
315
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Guard against `t.array(t.string())`. A builder child looks like it should
|
|
317
|
+
* work — and its bare `_type` would — but every modifier on it (`.view()`,
|
|
318
|
+
* `.default()`, quantize options) would be silently dropped, since modifiers
|
|
319
|
+
* describe the FIELD, not the elements. Fail loudly instead.
|
|
320
|
+
*/
|
|
316
321
|
function resolveChild(child: ChildType): DefinitionType {
|
|
317
322
|
if (isBuilder(child)) {
|
|
318
|
-
|
|
319
|
-
|
|
323
|
+
const inner = child['_type']; // private; element access bypasses the check
|
|
324
|
+
const hint = (typeof inner === "string")
|
|
325
|
+
? `use the type name instead: t.array("${inner}")`
|
|
326
|
+
: `collections accept a Schema class or a primitive type name ("string", "number", …)`;
|
|
327
|
+
throw new Error(`t.array/map/set/collection(): a t.* builder is not a valid element type — ${hint}.`);
|
|
320
328
|
}
|
|
321
329
|
return child as DefinitionType;
|
|
322
330
|
}
|
|
@@ -326,28 +334,24 @@ function resolveChild(child: ChildType): DefinitionType {
|
|
|
326
334
|
// ---------------------------------------------------------------------------
|
|
327
335
|
|
|
328
336
|
// Overloaded factories for collections. Implementation lives in a single function;
|
|
329
|
-
// overloads narrow the return type for Schema/primitive
|
|
337
|
+
// overloads narrow the return type for Schema/primitive children.
|
|
330
338
|
// All collection factories tag `HasDefault = true` because schema() auto-
|
|
331
339
|
// instantiates an empty collection when no explicit default is given.
|
|
332
340
|
interface ArrayFactory {
|
|
333
341
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<ArraySchema<InstanceType<C>>, true, false>;
|
|
334
342
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<ArraySchema<InferValueType<P>>, true, false>;
|
|
335
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<ArraySchema<V>, true, false>;
|
|
336
343
|
}
|
|
337
344
|
interface MapFactory {
|
|
338
345
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<MapSchema<InstanceType<C>>, true, false>;
|
|
339
346
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<MapSchema<InferValueType<P>>, true, false>;
|
|
340
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<MapSchema<V>, true, false>;
|
|
341
347
|
}
|
|
342
348
|
interface SetFactory {
|
|
343
349
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<SetSchema<InstanceType<C>>, true, false>;
|
|
344
350
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<SetSchema<InferValueType<P>>, true, false>;
|
|
345
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<SetSchema<V>, true, false>;
|
|
346
351
|
}
|
|
347
352
|
interface CollectionFactory {
|
|
348
353
|
<C extends Constructor<Schema>>(child: C): FieldBuilder<CollectionSchema<InstanceType<C>>, true, false>;
|
|
349
354
|
<P extends RawPrimitiveType>(child: P): FieldBuilder<CollectionSchema<InferValueType<P>>, true, false>;
|
|
350
|
-
<V>(child: FieldBuilder<V>): FieldBuilder<CollectionSchema<V>, true, false>;
|
|
351
355
|
}
|
|
352
356
|
// t.stream(Entity) — priority-batched collection of Schema instances.
|
|
353
357
|
// Element type is restricted to Schema subclasses (no primitives) because
|