@colyseus/schema 5.0.13 → 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/Reflection.d.ts +36 -34
- 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 +3237 -2834
- package/build/index.cjs.map +1 -1
- package/build/index.js +3233 -2830
- package/build/index.mjs +3237 -2834
- package/build/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +3 -5
- package/build/types/TypeContext.d.ts +0 -17
- package/build/types/builder.d.ts +8 -11
- package/build/types/symbols.d.ts +1 -0
- package/package.json +2 -2
- 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/HelperTypes.ts +16 -13
- package/src/types/TypeContext.ts +5 -52
- package/src/types/builder.ts +21 -16
- 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/HelperTypes.ts
CHANGED
|
@@ -87,10 +87,12 @@ export type InferValueType<T> =
|
|
|
87
87
|
|
|
88
88
|
: never;
|
|
89
89
|
|
|
90
|
-
// Keys whose
|
|
90
|
+
// Keys whose builder carries the `.optional()` brand. Reads the brand rather
|
|
91
|
+
// than `undefined extends V`: the latter is true for EVERY V when the consumer
|
|
92
|
+
// compiles with `strictNullChecks: false`, flipping all fields optional.
|
|
91
93
|
type OptionalBuilderKeys<T> = {
|
|
92
|
-
[K in keyof T]: T[K] extends FieldBuilder<infer
|
|
93
|
-
? (
|
|
94
|
+
[K in keyof T]: T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean>
|
|
95
|
+
? (O extends true ? K : never)
|
|
94
96
|
: never
|
|
95
97
|
}[keyof T];
|
|
96
98
|
|
|
@@ -136,14 +138,16 @@ type ToJSONField<X> =
|
|
|
136
138
|
: X extends Schema ? ToJSON<X>
|
|
137
139
|
: X;
|
|
138
140
|
|
|
139
|
-
// Keys whose value
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}[keyof T];
|
|
141
|
+
// Keys whose value admits `undefined` — runtime `toJSON()` omits those, so
|
|
142
|
+
// they surface as `?:` on the JSON shape. Under `strictNullChecks: false`
|
|
143
|
+
// (`undefined extends {}` detects it) `undefined extends T[K]` is true for
|
|
144
|
+
// every key, so only the `?` modifier can signal optionality there.
|
|
144
145
|
type ToJSONOptionalKeys<T> = {
|
|
145
|
-
[K in keyof T]-?: undefined extends
|
|
146
|
+
[K in keyof T]-?: undefined extends {}
|
|
147
|
+
? ({} extends Pick<T, K> ? K : never)
|
|
148
|
+
: (undefined extends T[K] ? K : never)
|
|
146
149
|
}[keyof T];
|
|
150
|
+
type ToJSONRequiredKeys<T> = Exclude<keyof T, ToJSONOptionalKeys<T>>;
|
|
147
151
|
|
|
148
152
|
export type ToJSON<T> = NonFunctionProps<
|
|
149
153
|
& { [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]> }
|
|
@@ -226,10 +230,9 @@ type FieldValue<F> =
|
|
|
226
230
|
|
|
227
231
|
// Classify each key of a fields map as "required" / "optional" / "none"
|
|
228
232
|
// (methods). Both `HasDefault = true` and the explicit `.optional()` brand
|
|
229
|
-
// `IsOptional = true` mark the field omittable at construction.
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// contravariant + covariant positions.
|
|
233
|
+
// `IsOptional = true` mark the field omittable at construction. Reading the
|
|
234
|
+
// brands (never `undefined extends V`) keeps this correct for consumers on
|
|
235
|
+
// `strictNullChecks: false`, where `undefined extends V` is true for every V.
|
|
233
236
|
type KeyClass<T, K extends keyof T> =
|
|
234
237
|
T[K] extends FieldBuilder<unknown, infer D extends boolean, infer O extends boolean>
|
|
235
238
|
? (D extends true
|
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
|
}
|