@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
package/src/encoder/Encoder.ts
CHANGED
|
@@ -9,11 +9,12 @@ import type { Iterator } from "../encoding/decode.js";
|
|
|
9
9
|
import { OPERATION, SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec.js';
|
|
10
10
|
import { Root } from "./Root.js";
|
|
11
11
|
|
|
12
|
-
import type
|
|
12
|
+
import { ARRAY_SNAPSHOT, type StateView } from "./StateView.js";
|
|
13
13
|
import type { ChangeTree, ChangeTreeList, ChangeTreeNode } from "./ChangeTree.js";
|
|
14
14
|
import type { EncodeOperation } from "./EncodeOperation.js";
|
|
15
15
|
import { forEachLiveWithCtx as _forEachLiveWithCtx } from "./changeTree/liveIteration.js";
|
|
16
16
|
import { forEachChildWithCtx as _forEachChildWithCtx } from "./changeTree/treeAttachment.js";
|
|
17
|
+
import { drainFilterRefresh } from "./changeTree/inheritedFlags.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Reusable context passed to the recorder's forEachWithCtx to iterate changes
|
|
@@ -296,6 +297,10 @@ export class Encoder<T extends Schema = any> {
|
|
|
296
297
|
initialOffset: number,
|
|
297
298
|
unreliable: boolean,
|
|
298
299
|
): Uint8Array {
|
|
300
|
+
// Settle any pending per-edge filter re-derivations before routing
|
|
301
|
+
// fields to channels (see inheritedFlags.drainFilterRefresh).
|
|
302
|
+
if (this.root.pendingFilterRefresh.length > 0) drainFilterRefresh(this.root);
|
|
303
|
+
|
|
299
304
|
const hasView = (view !== undefined);
|
|
300
305
|
const rootChangeTree = this.state[$changes];
|
|
301
306
|
|
|
@@ -379,6 +384,9 @@ export class Encoder<T extends Schema = any> {
|
|
|
379
384
|
view?: StateView,
|
|
380
385
|
initialOffset: number = it.offset
|
|
381
386
|
): Uint8Array {
|
|
387
|
+
// Full-sync splits fields by the same isFiltered classification.
|
|
388
|
+
if (this.root.pendingFilterRefresh.length > 0) drainFilterRefresh(this.root);
|
|
389
|
+
|
|
382
390
|
const hasView = (view !== undefined);
|
|
383
391
|
const rootChangeTree = this.state[$changes];
|
|
384
392
|
|
|
@@ -521,7 +529,49 @@ export class Encoder<T extends Schema = any> {
|
|
|
521
529
|
|
|
522
530
|
// Iterate entries directly — the inner Map gives us the (index, op)
|
|
523
531
|
// pair without an intermediate keys array or Number() parse.
|
|
524
|
-
for (const [
|
|
532
|
+
for (const [key, op] of changes) {
|
|
533
|
+
// Element-binding entries under a collection parent are keyed
|
|
534
|
+
// by the child's ChangeTree — resolve its CURRENT wire slot
|
|
535
|
+
// here (a slot captured at view.add() time goes stale when
|
|
536
|
+
// the array reindexes later in the same tick).
|
|
537
|
+
let index: number;
|
|
538
|
+
if (key === ARRAY_SNAPSHOT) {
|
|
539
|
+
// Whole-array snapshot: emit an ADD per live element at
|
|
540
|
+
// its CURRENT slot. Structural walk at drain time — a
|
|
541
|
+
// reindex after view.add() cannot go stale, and staged
|
|
542
|
+
// holes (recorder DELETEs) are skipped by construction.
|
|
543
|
+
const tmpItems = refTarget.tmpItems;
|
|
544
|
+
const deletedIndexes = refTarget.deletedIndexes;
|
|
545
|
+
for (let slot = 0; slot < tmpItems.length; slot++) {
|
|
546
|
+
if (tmpItems[slot] === undefined || deletedIndexes[slot] === true) { continue; }
|
|
547
|
+
encoder(this, bytes, changeTree, slot, OPERATION.ADD, it, false, true, metadata);
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (typeof key === "number") {
|
|
552
|
+
index = key;
|
|
553
|
+
} else {
|
|
554
|
+
const resolved = key.indexInParent(ref);
|
|
555
|
+
if (resolved === undefined) { continue; } // detached and re-parented elsewhere
|
|
556
|
+
index = resolved;
|
|
557
|
+
|
|
558
|
+
// SAME PATCH view.add + state-removal: the element is
|
|
559
|
+
// leaving the array (recorder DELETE at its slot), so the
|
|
560
|
+
// binding is moot — and `$getByIndex` on a staged hole
|
|
561
|
+
// reads a DIFFERENT element (compacted `items`), which
|
|
562
|
+
// would ship a mismatched value payload. Cancel the
|
|
563
|
+
// binding AND the child's own pending entry (its refId
|
|
564
|
+
// was never introduced to this client). Topological
|
|
565
|
+
// drain order guarantees the child entry hasn't been
|
|
566
|
+
// visited yet. A recorder ADD at the slot needs no such
|
|
567
|
+
// guard — both channels emit ADD_BY_REFID and the
|
|
568
|
+
// decoder dedups by identity.
|
|
569
|
+
if (op === OPERATION.ADD && changeTree.getChange(index) === OPERATION.DELETE) {
|
|
570
|
+
view.changes.delete(key.ref[$refId]);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
525
575
|
// workaround when using view.add() on item that has been deleted from state
|
|
526
576
|
// (see test "adding to view item that has been removed from state")
|
|
527
577
|
const value = refTarget[$getByIndex](index);
|
package/src/encoder/Root.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OPERATION } from "../encoding/spec.js";
|
|
2
2
|
import { TypeContext } from "../types/TypeContext.js";
|
|
3
|
-
import { ChangeTree, ChangeTreeList, createChangeTreeList, type ChangeTreeNode } from "./ChangeTree.js";
|
|
3
|
+
import { ChangeTree, ChangeTreeList, createChangeTreeList, PENDING_FILTER_REFRESH, type ChangeTreeNode } from "./ChangeTree.js";
|
|
4
|
+
import { restageLiveCb } from "./changeTree/liveIteration.js";
|
|
4
5
|
import { $changes, $refId } from "../types/symbols.js";
|
|
5
6
|
import type { StateView } from "./StateView.js";
|
|
6
7
|
import type { StreamSchema } from "../types/custom/StreamSchema.js";
|
|
@@ -53,6 +54,22 @@ export class Root {
|
|
|
53
54
|
*/
|
|
54
55
|
unreliableChanges: ChangeTreeList = createChangeTreeList();
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Trees whose parent-edge set changed this tick (instance sharing
|
|
59
|
+
* gained or lost an edge). The encoder drains this before emission —
|
|
60
|
+
* `inheritedFlags.drainFilterRefresh` re-derives each tree's filter
|
|
61
|
+
* state against the then-settled containers. Only populated when the
|
|
62
|
+
* TypeContext has any @view/@stream field.
|
|
63
|
+
*/
|
|
64
|
+
public pendingFilterRefresh: ChangeTree[] = [];
|
|
65
|
+
|
|
66
|
+
public enqueueFilterRefresh(tree: ChangeTree): void {
|
|
67
|
+
if (!this.types.hasFilters) return;
|
|
68
|
+
if (tree.flags & PENDING_FILTER_REFRESH) return;
|
|
69
|
+
tree.flags |= PENDING_FILTER_REFRESH;
|
|
70
|
+
this.pendingFilterRefresh.push(tree);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
/**
|
|
57
74
|
* Free-list of ChangeTreeNode objects. Both queues share this pool —
|
|
58
75
|
* a node carries no queue affinity, only `{ changeTree, prev, next, position }`.
|
|
@@ -172,17 +189,16 @@ export class Root {
|
|
|
172
189
|
// values would otherwise never be encoded).
|
|
173
190
|
//
|
|
174
191
|
changeTree.needsRestage = false;
|
|
175
|
-
changeTree.
|
|
176
|
-
if (changeTree.isFieldUnreliable(fieldIndex)) {
|
|
177
|
-
changeTree.ensureUnreliableRecorder().record(fieldIndex, OPERATION.ADD);
|
|
178
|
-
} else {
|
|
179
|
-
changeTree.record(fieldIndex, OPERATION.ADD);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
192
|
+
changeTree.forEachLiveWithCtx(changeTree, restageLiveCb);
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
this.refCount[refId] = (previousRefCount || 0) + 1;
|
|
185
196
|
|
|
197
|
+
// Gained a 2nd+ parent edge (instance sharing / re-assignment) —
|
|
198
|
+
// re-derive filter state before the next encode. Chokepoint for
|
|
199
|
+
// every attach path; mirrors the edge-loss enqueue in `remove()`.
|
|
200
|
+
if (previousRefCount > 0) this.enqueueFilterRefresh(changeTree);
|
|
201
|
+
|
|
186
202
|
return isNewChangeTree;
|
|
187
203
|
}
|
|
188
204
|
|
|
@@ -229,6 +245,10 @@ export class Root {
|
|
|
229
245
|
} else {
|
|
230
246
|
this.refCount[refId] = refCount;
|
|
231
247
|
|
|
248
|
+
// Lost one of several parent edges — the surviving edge set may
|
|
249
|
+
// no longer include a public path (or may have gained one).
|
|
250
|
+
this.enqueueFilterRefresh(changeTree);
|
|
251
|
+
|
|
232
252
|
//
|
|
233
253
|
// When losing a reference to an instance, it is best to move the
|
|
234
254
|
// ChangeTree next to its parent in the encoding queue.
|
package/src/encoder/StateView.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChangeTree, Ref } from "./ChangeTree.js";
|
|
2
|
-
import { $changes, $fieldIndexesByViewTag, $refId, $viewFieldIndexes } from "../types/symbols.js";
|
|
2
|
+
import { $changes, $childType, $fieldIndexesByViewTag, $refId, $viewFieldIndexes } from "../types/symbols.js";
|
|
3
3
|
import { DEFAULT_VIEW_TAG } from "../annotations.js";
|
|
4
4
|
import { OPERATION } from "../encoding/spec.js";
|
|
5
5
|
import { Metadata } from "../Metadata.js";
|
|
@@ -61,6 +61,16 @@ const _disposeRegistry = new FinalizationRegistry<{ root: Root; id: number; slot
|
|
|
61
61
|
* populated collection inspects into dozens of lines of encoder
|
|
62
62
|
* internals and buries the message that matters.
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* Sentinel inner-map key: "snapshot every live element of this ref-typed
|
|
66
|
+
* ArraySchema". Written by `_add`'s bulk path instead of one entry per
|
|
67
|
+
* element; `encodeView` expands it structurally at drain time, so the
|
|
68
|
+
* emitted slots reflect any reindex that happened after `view.add()` —
|
|
69
|
+
* and a whole-array snapshot costs one Map insert instead of N.
|
|
70
|
+
* Real slots are never negative, so -1 cannot collide.
|
|
71
|
+
*/
|
|
72
|
+
export const ARRAY_SNAPSHOT = -1;
|
|
73
|
+
|
|
64
74
|
function describeArg(value: any): string {
|
|
65
75
|
if (value === undefined) { return "undefined"; }
|
|
66
76
|
if (value === null) { return "null"; }
|
|
@@ -107,8 +117,16 @@ export class StateView {
|
|
|
107
117
|
* Inner storage is a Map so the encode loop in `encodeView` can iterate
|
|
108
118
|
* directly with numeric keys — the legacy `{[index]: OPERATION}` shape
|
|
109
119
|
* forced an `Object.keys(...)` allocation + `Number(key)` parse per ref.
|
|
120
|
+
*
|
|
121
|
+
* Inner keys are numbers (Schema field indexes, MapSchema journal
|
|
122
|
+
* indexes, Set/Collection indexes, stream positions — all stable within
|
|
123
|
+
* a tick), EXCEPT element bindings under a ref-typed ArraySchema parent,
|
|
124
|
+
* which are keyed by the child's ChangeTree. An array wire slot captured
|
|
125
|
+
* at `view.add()` time goes stale if the array reindexes (unshift /
|
|
126
|
+
* reverse / move) later in the same tick — identity keys let
|
|
127
|
+
* `encodeView` resolve the CURRENT slot at drain time instead.
|
|
110
128
|
*/
|
|
111
|
-
changes = new Map<number, Map<number, OPERATION>>();
|
|
129
|
+
changes = new Map<number, Map<number | ChangeTree, OPERATION>>();
|
|
112
130
|
|
|
113
131
|
constructor(public iterable: boolean = false) {
|
|
114
132
|
if (iterable) {
|
|
@@ -368,15 +386,18 @@ export class StateView {
|
|
|
368
386
|
// subclasses yield a real Metadata object.
|
|
369
387
|
const metadata: Metadata = (obj.constructor as typeof Schema)[Symbol.metadata];
|
|
370
388
|
|
|
389
|
+
const wasVisible = this.isVisible(changeTree);
|
|
390
|
+
|
|
371
391
|
// Add to iterable list (only the explicitly added items), deduping
|
|
372
|
-
// re-adds of an already-visible instance
|
|
373
|
-
//
|
|
374
|
-
// NOTE: dedup applies to `items` only — a re-add still
|
|
375
|
-
// full snapshot on purpose (shared-view bootstrap
|
|
376
|
-
// late-attached client may not have consumed earlier
|
|
377
|
-
// Callers wanting cheap idempotence can guard with
|
|
392
|
+
// re-adds of an already-visible instance; indexOf runs only on the
|
|
393
|
+
// re-add path.
|
|
394
|
+
// NOTE: dedup applies to `items` only — a default-tag re-add still
|
|
395
|
+
// re-queues the full snapshot on purpose (shared-view bootstrap
|
|
396
|
+
// re-add: a late-attached client may not have consumed earlier
|
|
397
|
+
// drains). Callers wanting cheap idempotence can guard with
|
|
398
|
+
// `view.has(obj)`.
|
|
378
399
|
if (this.iterable && checkIncludeParent
|
|
379
|
-
&& (!
|
|
400
|
+
&& (!wasVisible || this.items.indexOf(obj) === -1)) {
|
|
380
401
|
this.items.push(obj);
|
|
381
402
|
}
|
|
382
403
|
|
|
@@ -469,32 +490,51 @@ export class StateView {
|
|
|
469
490
|
});
|
|
470
491
|
}
|
|
471
492
|
}
|
|
493
|
+
}
|
|
472
494
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
tagAtIndex === undefined || // "all change" with no tag
|
|
491
|
-
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
492
|
-
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
493
|
-
) {
|
|
494
|
-
changes.set(index, OPERATION.ADD);
|
|
495
|
+
// Full-sync snapshot of a non-new tree (fresh ones ship via .encode()).
|
|
496
|
+
// Also runs for custom tags when bootstrapping the tree for this view
|
|
497
|
+
// (!wasVisible) — the per-field filter admits untagged fields, and
|
|
498
|
+
// collections behind tagged fields have no `byTag`: without the
|
|
499
|
+
// snapshot their elements are never introduced ("refId" not found).
|
|
500
|
+
// A tagged add on an already-visible tree stays incremental (byTag
|
|
501
|
+
// only); default-tag re-adds re-snapshot on purpose (see `items`
|
|
502
|
+
// dedup note above).
|
|
503
|
+
if ((tag === DEFAULT_VIEW_TAG || !wasVisible) && (!changeTree.isNew || isChildAdded)) {
|
|
504
|
+
if (changeTree.isArray && typeof (changeTree.refTarget as any)[$childType] !== "string") {
|
|
505
|
+
// Ref-typed ArraySchema (the only proxied collection): one
|
|
506
|
+
// sentinel entry — encodeView snapshots the live elements at
|
|
507
|
+
// drain time, so the slots survive a same-tick reindex (see
|
|
508
|
+
// `changes` field docs) and the write stays O(1).
|
|
509
|
+
if ((changeTree.refTarget as any).items.length > 0) {
|
|
510
|
+
changes.set(ARRAY_SNAPSHOT, OPERATION.ADD);
|
|
495
511
|
isChildAdded = true;
|
|
496
512
|
}
|
|
497
|
-
|
|
513
|
+
|
|
514
|
+
} else {
|
|
515
|
+
// Full-sync snapshot: walk the live ref structurally instead of
|
|
516
|
+
// iterating a cumulative recorder bucket. Every populated index
|
|
517
|
+
// is emitted as ADD (matching the op-coercion previously done
|
|
518
|
+
// at encode time). Per-field tags come from the descriptor's
|
|
519
|
+
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
520
|
+
// object hop.
|
|
521
|
+
//
|
|
522
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
523
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
524
|
+
// so anything added leaks straight to the wire.
|
|
525
|
+
const tags = changeTree.encDescriptor.tags;
|
|
526
|
+
changeTree.forEachLive((index) => {
|
|
527
|
+
const tagAtIndex = tags[index];
|
|
528
|
+
if (
|
|
529
|
+
tagAtIndex === undefined || // "all change" with no tag
|
|
530
|
+
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
531
|
+
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
532
|
+
) {
|
|
533
|
+
changes.set(index, OPERATION.ADD);
|
|
534
|
+
isChildAdded = true;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
498
538
|
}
|
|
499
539
|
|
|
500
540
|
return isChildAdded;
|
|
@@ -575,13 +615,21 @@ export class StateView {
|
|
|
575
615
|
if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
|
|
576
616
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
577
617
|
if (changes === undefined) {
|
|
578
|
-
changes = new Map<number, OPERATION>();
|
|
618
|
+
changes = new Map<number | ChangeTree, OPERATION>();
|
|
579
619
|
this.changes.set(changeTree.ref[$refId], changes);
|
|
580
620
|
}
|
|
581
621
|
|
|
582
622
|
this.addTag(changeTree, tag);
|
|
583
623
|
|
|
584
|
-
|
|
624
|
+
// ArraySchema parents: key by the child's identity, not the wire
|
|
625
|
+
// slot it holds right now — a same-tick unshift()/reverse()/move()
|
|
626
|
+
// would shift the slot before encodeView drains this entry. Other
|
|
627
|
+
// parents keep numeric keys (Schema fields, MapSchema journal
|
|
628
|
+
// indexes and Set/Collection indexes are stable within a tick).
|
|
629
|
+
changes.set(
|
|
630
|
+
changeTree.isArray ? childChangeTree : parentIndex,
|
|
631
|
+
OPERATION.ADD,
|
|
632
|
+
);
|
|
585
633
|
}
|
|
586
634
|
}
|
|
587
635
|
|
|
@@ -641,8 +689,8 @@ export class StateView {
|
|
|
641
689
|
// out of the stream's per-view state. If it never made it to the
|
|
642
690
|
// wire (still in pending), silent drop; if already sent, queue
|
|
643
691
|
// DELETE via `view.changes` for the next encodeView drain.
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
692
|
+
const parentTree = changeTree.parent?.[$changes];
|
|
693
|
+
if (parentTree?.isStreamCollection) {
|
|
646
694
|
this.unmarkVisible(changeTree);
|
|
647
695
|
if (this.iterable && !_isClear) {
|
|
648
696
|
spliceOne(this.items, this.items.indexOf(obj));
|
|
@@ -718,59 +766,46 @@ export class StateView {
|
|
|
718
766
|
// parent is collection (Map/Array)
|
|
719
767
|
const parent = changeTree.parent;
|
|
720
768
|
if (parent && !Metadata.isValidInstance(parent) && changeTree.isFiltered) {
|
|
769
|
+
// ArraySchema parents use identity keys (see `changes` field
|
|
770
|
+
// docs); Map parents keep the (stable) journal index.
|
|
771
|
+
const key = parentTree!.isArray
|
|
772
|
+
? changeTree
|
|
773
|
+
: changeTree.parentIndex;
|
|
721
774
|
const parentRefId = parent[$refId];
|
|
722
775
|
let changes = this.changes.get(parentRefId);
|
|
723
776
|
if (changes === undefined) {
|
|
724
|
-
changes = new Map<number, OPERATION>();
|
|
777
|
+
changes = new Map<number | ChangeTree, OPERATION>();
|
|
725
778
|
this.changes.set(parentRefId, changes);
|
|
726
779
|
|
|
727
|
-
} else if (changes.get(
|
|
780
|
+
} else if (changes.get(key) === OPERATION.ADD) {
|
|
728
781
|
//
|
|
729
782
|
// SAME PATCH ADD + REMOVE:
|
|
730
|
-
//
|
|
783
|
+
// cancel the structure's pending ops and its descendants' —
|
|
784
|
+
// their introduction never reaches this client.
|
|
731
785
|
//
|
|
732
|
-
this.
|
|
786
|
+
this._dropPendingEntries(changeTree);
|
|
733
787
|
}
|
|
734
788
|
|
|
735
789
|
// DELETE / DELETE BY REF ID
|
|
736
|
-
changes.set(
|
|
790
|
+
changes.set(key, OPERATION.DELETE);
|
|
737
791
|
|
|
738
792
|
// Remove child schema from visible set
|
|
739
793
|
this._recursiveDeleteVisibleChangeTree(changeTree);
|
|
740
794
|
|
|
741
795
|
} else {
|
|
742
796
|
// delete all "tagged" properties.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
changes.set(index, OPERATION.DELETE);
|
|
746
|
-
|
|
747
|
-
// Remove child structures of @view() fields from visible set.
|
|
748
|
-
// (They were added during view.add() via forEachChild)
|
|
749
|
-
const value = changeTree.ref[names[index] as keyof Ref];
|
|
750
|
-
if (value?.[$changes]) {
|
|
751
|
-
this.unmarkVisible(value[$changes]);
|
|
752
|
-
this._recursiveDeleteVisibleChangeTree(value[$changes]);
|
|
753
|
-
}
|
|
754
|
-
});
|
|
797
|
+
metadata?.[$viewFieldIndexes]?.forEach((index) =>
|
|
798
|
+
this._removeViewField(changeTree, changes, index));
|
|
755
799
|
}
|
|
756
800
|
|
|
757
801
|
} else {
|
|
758
802
|
// delete only tagged properties. `$fieldIndexesByViewTag` is
|
|
759
803
|
// keyed per-bit, so a combined tag iterates each set bit.
|
|
760
|
-
const names = changeTree.encDescriptor.names;
|
|
761
804
|
const byTag = metadata?.[$fieldIndexesByViewTag];
|
|
762
805
|
if (byTag !== undefined) {
|
|
763
806
|
for (let bits = tag; bits > 0; bits &= bits - 1) {
|
|
764
|
-
byTag[bits & -bits]?.forEach((index) =>
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
// Remove child structures from visible set
|
|
768
|
-
const value = changeTree.ref[names[index] as keyof Ref];
|
|
769
|
-
if (value?.[$changes]) {
|
|
770
|
-
this.unmarkVisible(value[$changes]);
|
|
771
|
-
this._recursiveDeleteVisibleChangeTree(value[$changes]);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
807
|
+
byTag[bits & -bits]?.forEach((index) =>
|
|
808
|
+
this._removeViewField(changeTree, changes, index));
|
|
774
809
|
}
|
|
775
810
|
}
|
|
776
811
|
}
|
|
@@ -956,14 +991,16 @@ export class StateView {
|
|
|
956
991
|
} else {
|
|
957
992
|
// Non-streams: queue DELETE for every current child and
|
|
958
993
|
// unmark their visibility so subsequent mutations stop
|
|
959
|
-
// reaching this view.
|
|
994
|
+
// reaching this view. ArraySchema children are keyed by identity
|
|
995
|
+
// (see `changes` field docs); others by their stable index.
|
|
996
|
+
const isArray = tree.isArray;
|
|
960
997
|
let changes = this.changes.get(collectionRefId);
|
|
961
998
|
tree.forEachChild((childTree, index) => {
|
|
962
999
|
if (changes === undefined) {
|
|
963
1000
|
changes = new Map();
|
|
964
1001
|
this.changes.set(collectionRefId, changes);
|
|
965
1002
|
}
|
|
966
|
-
changes.set(index, OPERATION.DELETE);
|
|
1003
|
+
changes.set(isArray ? childTree : index, OPERATION.DELETE);
|
|
967
1004
|
this.unmarkVisible(childTree);
|
|
968
1005
|
});
|
|
969
1006
|
}
|
|
@@ -999,9 +1036,24 @@ export class StateView {
|
|
|
999
1036
|
// `markVisible` memoizes so the branch fires at most once per
|
|
1000
1037
|
// (tree, view) pair.
|
|
1001
1038
|
if (!isVisible && changeTree.isVisibilitySharedWithParent){
|
|
1039
|
+
// Primary grant is intentionally unguarded — pre-existing
|
|
1040
|
+
// semantics; the extras walk below is stricter on purpose.
|
|
1002
1041
|
if (this.isVisible(changeTree.parent[$changes])) {
|
|
1003
1042
|
this.markVisible(changeTree);
|
|
1004
1043
|
isVisible = true;
|
|
1044
|
+
} else {
|
|
1045
|
+
// Shared instance: the sharing parent may sit anywhere in the
|
|
1046
|
+
// chain — addParent promotes the LAST container to primary.
|
|
1047
|
+
// Only filtered parents can grant (public ones never share
|
|
1048
|
+
// visibility downward).
|
|
1049
|
+
for (let e = changeTree.extraParents; e !== undefined; e = e.next) {
|
|
1050
|
+
const parentTree = e.ref[$changes];
|
|
1051
|
+
if (parentTree.isFiltered && this.isVisible(parentTree)) {
|
|
1052
|
+
this.markVisible(changeTree);
|
|
1053
|
+
isVisible = true;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1005
1057
|
}
|
|
1006
1058
|
}
|
|
1007
1059
|
|
|
@@ -1014,4 +1066,36 @@ export class StateView {
|
|
|
1014
1066
|
this._recursiveDeleteVisibleChangeTree(childChangeTree);
|
|
1015
1067
|
});
|
|
1016
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Drop the pending `view.changes` entries of `tree` and every descendant.
|
|
1072
|
+
* Called when a same-patch pending ADD is cancelled: the subtree's
|
|
1073
|
+
* introduction never reaches this client, so its entries would emit
|
|
1074
|
+
* refIds the decoder cannot resolve ("refId" not found).
|
|
1075
|
+
*/
|
|
1076
|
+
private _dropPendingEntries(tree: ChangeTree): void {
|
|
1077
|
+
this.changes.delete(tree.ref[$refId]);
|
|
1078
|
+
tree.forEachChild((child) => this._dropPendingEntries(child));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Queue DELETE for a @view field on `changes` and hide the field
|
|
1083
|
+
* value's subtree from this view. When the field's ADD is still
|
|
1084
|
+
* pending (same-patch add + remove), the value's introduction never
|
|
1085
|
+
* ships — its pending subtree entries are dropped along with it.
|
|
1086
|
+
*/
|
|
1087
|
+
private _removeViewField(changeTree: ChangeTree, changes: Map<number | ChangeTree, OPERATION>, index: number): void {
|
|
1088
|
+
const wasPendingAdd = changes.get(index) === OPERATION.ADD;
|
|
1089
|
+
changes.set(index, OPERATION.DELETE);
|
|
1090
|
+
|
|
1091
|
+
const value = changeTree.ref[changeTree.encDescriptor.names[index] as keyof Ref];
|
|
1092
|
+
const valueTree: ChangeTree = value?.[$changes];
|
|
1093
|
+
if (valueTree) {
|
|
1094
|
+
this.unmarkVisible(valueTree);
|
|
1095
|
+
this._recursiveDeleteVisibleChangeTree(valueTree);
|
|
1096
|
+
if (wasPendingAdd) {
|
|
1097
|
+
this._dropPendingEntries(valueTree);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1017
1101
|
}
|