@colyseus/schema 5.0.3 → 5.0.5
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/build/codegen/cli.cjs +23 -0
- package/build/codegen/cli.cjs.map +1 -1
- package/build/decoder/strategy/Callbacks.d.ts +6 -6
- package/build/encoder/StateView.d.ts +17 -0
- package/build/index.cjs +184 -37
- package/build/index.cjs.map +1 -1
- package/build/index.js +184 -37
- package/build/index.mjs +184 -37
- package/build/index.mjs.map +1 -1
- package/build/input/index.cjs +28 -4
- package/build/input/index.cjs.map +1 -1
- package/build/input/index.mjs +28 -4
- package/build/input/index.mjs.map +1 -1
- package/build/types/builder.d.ts +83 -29
- package/package.json +6 -5
- package/src/annotations.ts +48 -18
- package/src/codegen/languages/csharp.ts +24 -0
- package/src/decoder/strategy/Callbacks.ts +16 -14
- package/src/encoder/Encoder.ts +13 -2
- package/src/encoder/StateView.ts +63 -2
- package/src/encoder/changeTree/inheritedFlags.ts +16 -2
- package/src/types/builder.ts +82 -20
|
@@ -36,12 +36,12 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
36
36
|
/**
|
|
37
37
|
* Listen to property changes on a nested instance.
|
|
38
38
|
*/
|
|
39
|
-
listen<TInstance
|
|
39
|
+
listen<TInstance, K extends PublicPropNames<TInstance>>(instance: TInstance, property: K, handler: PropertyChangeCallback<TInstance[K]>, immediate?: boolean): () => void;
|
|
40
40
|
protected listenInstance<TInstance extends IRef>(instance: TInstance, propertyName: string, handler: PropertyChangeCallback<any>, immediate?: boolean): () => void;
|
|
41
41
|
/**
|
|
42
42
|
* Listen to any property change on an instance.
|
|
43
43
|
*/
|
|
44
|
-
onChange<TInstance extends
|
|
44
|
+
onChange<TInstance extends object>(instance: TInstance, handler: InstanceChangeCallback): () => void;
|
|
45
45
|
/**
|
|
46
46
|
* Listen to item changes in a collection on root state.
|
|
47
47
|
*/
|
|
@@ -49,7 +49,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
49
49
|
/**
|
|
50
50
|
* Listen to item changes in a nested collection.
|
|
51
51
|
*/
|
|
52
|
-
onChange<TInstance extends
|
|
52
|
+
onChange<TInstance extends object, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>): () => void;
|
|
53
53
|
/**
|
|
54
54
|
* Listen to items added to a collection on root state.
|
|
55
55
|
*/
|
|
@@ -57,7 +57,7 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
57
57
|
/**
|
|
58
58
|
* Listen to items added to a nested collection.
|
|
59
59
|
*/
|
|
60
|
-
onAdd<TInstance
|
|
60
|
+
onAdd<TInstance, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>, immediate?: boolean): () => void;
|
|
61
61
|
/**
|
|
62
62
|
* Listen to items removed from a collection on root state.
|
|
63
63
|
*/
|
|
@@ -65,12 +65,12 @@ export declare class StateCallbackStrategy<TState extends IRef> {
|
|
|
65
65
|
/**
|
|
66
66
|
* Listen to items removed from a nested collection.
|
|
67
67
|
*/
|
|
68
|
-
onRemove<TInstance
|
|
68
|
+
onRemove<TInstance, K extends CollectionPropNames<TInstance>>(instance: TInstance, property: K, handler: ValueKeyCallback<CollectionValueType<TInstance, K>, CollectionKeyType<TInstance, K>>): () => void;
|
|
69
69
|
/**
|
|
70
70
|
* Bind properties from a Schema instance to a target object.
|
|
71
71
|
* Changes will be automatically reflected on the target object.
|
|
72
72
|
*/
|
|
73
|
-
bindTo<TInstance
|
|
73
|
+
bindTo<TInstance, TTarget>(from: TInstance, to: TTarget, properties?: string[], immediate?: boolean): () => void;
|
|
74
74
|
protected triggerChanges(allChanges: DataChange[]): void;
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -107,6 +107,23 @@ export declare class StateView {
|
|
|
107
107
|
*/
|
|
108
108
|
private _markSubtreeVisible;
|
|
109
109
|
protected addParentOf(childChangeTree: ChangeTree, tag: number): void;
|
|
110
|
+
/**
|
|
111
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
112
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
113
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
114
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
115
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
116
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
117
|
+
* `view.add`, this preserves the global invariant that
|
|
118
|
+
* `view.changes` iteration order is topological.
|
|
119
|
+
*
|
|
120
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
121
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
122
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
123
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
124
|
+
* upheld by every prior caller).
|
|
125
|
+
*/
|
|
126
|
+
private _touchAncestorsOf;
|
|
110
127
|
remove(obj: Ref, tag?: number): this;
|
|
111
128
|
remove(obj: Ref, tag?: number, _isClear?: boolean): this;
|
|
112
129
|
has(obj: Ref): boolean;
|
package/build/index.cjs
CHANGED
|
@@ -2110,11 +2110,24 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2110
2110
|
const refType = Metadata.isValidInstance(tree.ref)
|
|
2111
2111
|
? tree.ref.constructor
|
|
2112
2112
|
: tree.ref[$childType];
|
|
2113
|
+
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
2114
|
+
// parent regardless of whether the parent is a collection. The
|
|
2115
|
+
// `parentIsCollection` constraint that used to live here blocked
|
|
2116
|
+
// nested-Schema-field-of-@view-tagged-Schema from sharing visibility,
|
|
2117
|
+
// forcing users to wrap the child in an ArraySchema as a workaround.
|
|
2118
|
+
//
|
|
2119
|
+
// #226 (4.0.25): items inside a non-default-tag `@view(N)` collection
|
|
2120
|
+
// also inherit visibility from the parent collection, so items
|
|
2121
|
+
// pushed/set after `view.add(state, N)` show up automatically.
|
|
2122
|
+
// Default-tag `@view()` collections keep per-item gating —
|
|
2123
|
+
// `view.add(item)` is still required to opt each one in.
|
|
2124
|
+
// The `parentMetadata[parentIndex].tag` access is safe inside the
|
|
2125
|
+
// `fieldHasViewTag` short-circuit (the metadata entry and its `tag`
|
|
2126
|
+
// are guaranteed to exist when that flag is set).
|
|
2113
2127
|
tree.isVisibilitySharedWithParent = (parentChangeTree.isFiltered
|
|
2114
2128
|
&& typeof refType !== "string"
|
|
2115
|
-
&& !fieldHasViewTag
|
|
2116
2129
|
&& !fieldHasStream
|
|
2117
|
-
&& parentIsCollection);
|
|
2130
|
+
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
|
|
2118
2131
|
}
|
|
2119
2132
|
}
|
|
2120
2133
|
|
|
@@ -5447,8 +5460,10 @@ registerType("stream", { constructor: StreamSchema });
|
|
|
5447
5460
|
*/
|
|
5448
5461
|
class FieldBuilder {
|
|
5449
5462
|
[$builder] = true;
|
|
5450
|
-
// Internal configuration.
|
|
5451
|
-
//
|
|
5463
|
+
// Internal configuration. Declared `private` (soft-private): hidden from
|
|
5464
|
+
// editor autocomplete and from normal external `.field` access, but still
|
|
5465
|
+
// reachable at runtime via element access (e.g. `builder['_noSync']`) for
|
|
5466
|
+
// internal tooling/tests. Not meant to be mutated by end users.
|
|
5452
5467
|
_type;
|
|
5453
5468
|
_default = undefined;
|
|
5454
5469
|
_hasDefault = false;
|
|
@@ -5461,6 +5476,7 @@ class FieldBuilder {
|
|
|
5461
5476
|
_static = false;
|
|
5462
5477
|
_stream = false;
|
|
5463
5478
|
_optional = false;
|
|
5479
|
+
_noSync = false;
|
|
5464
5480
|
_streamPriority = undefined;
|
|
5465
5481
|
constructor(type) {
|
|
5466
5482
|
this._type = type;
|
|
@@ -5511,6 +5527,30 @@ class FieldBuilder {
|
|
|
5511
5527
|
this._static = true;
|
|
5512
5528
|
return this;
|
|
5513
5529
|
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Mark this field as **local-only** — it is typed and initialized on the
|
|
5532
|
+
* instance (so `.default()` and the inferred instance type still apply),
|
|
5533
|
+
* but is never registered for synchronization: it never enters change
|
|
5534
|
+
* tracking, never goes over the wire, and decoders never receive it.
|
|
5535
|
+
*
|
|
5536
|
+
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
5537
|
+
* want on the class for typing convenience without paying any sync cost.
|
|
5538
|
+
*
|
|
5539
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`, `.owned()`,
|
|
5540
|
+
* `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
|
|
5541
|
+
* them throws at `schema()` time.
|
|
5542
|
+
*
|
|
5543
|
+
* ```ts
|
|
5544
|
+
* const Player = schema({
|
|
5545
|
+
* hp: t.uint8().default(100), // synchronized
|
|
5546
|
+
* lastInputTick: t.number().noSync(), // local-only
|
|
5547
|
+
* }, 'Player');
|
|
5548
|
+
* ```
|
|
5549
|
+
*/
|
|
5550
|
+
noSync() {
|
|
5551
|
+
this._noSync = true;
|
|
5552
|
+
return this;
|
|
5553
|
+
}
|
|
5514
5554
|
/**
|
|
5515
5555
|
* Opt a collection field into priority-batched streaming delivery —
|
|
5516
5556
|
* ADDs drain at most `maxPerTick` per tick per view (or per broadcast
|
|
@@ -5565,6 +5605,11 @@ class FieldBuilder {
|
|
|
5565
5605
|
this._optional = true;
|
|
5566
5606
|
return this;
|
|
5567
5607
|
}
|
|
5608
|
+
/**
|
|
5609
|
+
* @internal — snapshot of the builder's configuration consumed by
|
|
5610
|
+
* `schema()`. `private` keeps it out of autocomplete; internal callers
|
|
5611
|
+
* reach it via element access (`builder['toDefinition']()`).
|
|
5612
|
+
*/
|
|
5568
5613
|
toDefinition() {
|
|
5569
5614
|
return {
|
|
5570
5615
|
type: this._type,
|
|
@@ -5579,6 +5624,7 @@ class FieldBuilder {
|
|
|
5579
5624
|
static: this._static,
|
|
5580
5625
|
stream: this._stream,
|
|
5581
5626
|
optional: this._optional,
|
|
5627
|
+
noSync: this._noSync,
|
|
5582
5628
|
streamPriority: this._streamPriority,
|
|
5583
5629
|
};
|
|
5584
5630
|
}
|
|
@@ -5586,15 +5632,13 @@ class FieldBuilder {
|
|
|
5586
5632
|
function isBuilder(value) {
|
|
5587
5633
|
return value != null && value[$builder] === true;
|
|
5588
5634
|
}
|
|
5589
|
-
// ---------------------------------------------------------------------------
|
|
5590
|
-
// Factory helpers
|
|
5591
|
-
// ---------------------------------------------------------------------------
|
|
5592
5635
|
function primitive(name) {
|
|
5593
|
-
return () => new FieldBuilder(name);
|
|
5636
|
+
return (() => new FieldBuilder(name));
|
|
5594
5637
|
}
|
|
5595
5638
|
function resolveChild(child) {
|
|
5596
5639
|
if (isBuilder(child)) {
|
|
5597
|
-
|
|
5640
|
+
// `_type` is private; element access bypasses the visibility check.
|
|
5641
|
+
return child['_type'];
|
|
5598
5642
|
}
|
|
5599
5643
|
return child;
|
|
5600
5644
|
}
|
|
@@ -5604,7 +5648,7 @@ const setFactory = ((child) => new FieldBuilder({ set: resolveChild(child) }));
|
|
|
5604
5648
|
const collectionFactory = ((child) => new FieldBuilder({ collection: resolveChild(child) }));
|
|
5605
5649
|
const streamFactory = ((child) => {
|
|
5606
5650
|
const b = new FieldBuilder({ stream: resolveChild(child) });
|
|
5607
|
-
b
|
|
5651
|
+
b['_stream'] = true; // element access bypasses `private`
|
|
5608
5652
|
return b;
|
|
5609
5653
|
});
|
|
5610
5654
|
const refFactory = ((ctor) => new FieldBuilder(ctor));
|
|
@@ -6033,6 +6077,36 @@ function deprecated(throws = true) {
|
|
|
6033
6077
|
});
|
|
6034
6078
|
};
|
|
6035
6079
|
}
|
|
6080
|
+
/**
|
|
6081
|
+
* Produce the auto-instantiated construction default for a builder type
|
|
6082
|
+
* (empty collection or zero-arg Schema ref), or `undefined` when the type
|
|
6083
|
+
* has no auto-default. Shared by synced and `.noSync()` field handling.
|
|
6084
|
+
*/
|
|
6085
|
+
function autoInstantiateDefault(rawType) {
|
|
6086
|
+
if (rawType && typeof rawType === "object") {
|
|
6087
|
+
if (rawType.array !== undefined) {
|
|
6088
|
+
return new ArraySchema();
|
|
6089
|
+
}
|
|
6090
|
+
if (rawType.map !== undefined) {
|
|
6091
|
+
return new MapSchema();
|
|
6092
|
+
}
|
|
6093
|
+
if (rawType.set !== undefined) {
|
|
6094
|
+
return new SetSchema();
|
|
6095
|
+
}
|
|
6096
|
+
if (rawType.collection !== undefined) {
|
|
6097
|
+
return new CollectionSchema();
|
|
6098
|
+
}
|
|
6099
|
+
if (rawType.stream !== undefined) {
|
|
6100
|
+
return new StreamSchema();
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6104
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6105
|
+
return new rawType();
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
return undefined;
|
|
6109
|
+
}
|
|
6036
6110
|
/**
|
|
6037
6111
|
* Define a Schema class declaratively.
|
|
6038
6112
|
*
|
|
@@ -6068,7 +6142,28 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6068
6142
|
for (const fieldName in fieldsAndMethods) {
|
|
6069
6143
|
const value = fieldsAndMethods[fieldName];
|
|
6070
6144
|
if (isBuilder(value)) {
|
|
6071
|
-
const def = value
|
|
6145
|
+
const def = value['toDefinition'](); // private; element access bypasses visibility
|
|
6146
|
+
if (def.noSync) {
|
|
6147
|
+
// Local-only field: skip metadata registration entirely so it is
|
|
6148
|
+
// never encoded/decoded, but still seed its construction default
|
|
6149
|
+
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6150
|
+
if (def.view !== undefined || def.owned || def.unreliable ||
|
|
6151
|
+
def.transient || def.static || def.stream) {
|
|
6152
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6153
|
+
`together with a sync-only modifier (.view/.owned/.unreliable/.transient/.static/.stream). ` +
|
|
6154
|
+
`A local-only field cannot be synchronized.`);
|
|
6155
|
+
}
|
|
6156
|
+
if (def.hasDefault) {
|
|
6157
|
+
defaultValues[fieldName] = def.default;
|
|
6158
|
+
}
|
|
6159
|
+
else if (!def.optional) {
|
|
6160
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6161
|
+
if (autoDefault !== undefined) {
|
|
6162
|
+
defaultValues[fieldName] = autoDefault;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
continue;
|
|
6166
|
+
}
|
|
6072
6167
|
fields[fieldName] = getNormalizedType(def.type);
|
|
6073
6168
|
if (def.view !== undefined) {
|
|
6074
6169
|
viewTagFields[fieldName] = def.view;
|
|
@@ -6103,28 +6198,9 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6103
6198
|
else if (!def.optional) {
|
|
6104
6199
|
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
6105
6200
|
// `.optional()` opts out — field starts as undefined.
|
|
6106
|
-
const
|
|
6107
|
-
if (
|
|
6108
|
-
|
|
6109
|
-
defaultValues[fieldName] = new ArraySchema();
|
|
6110
|
-
}
|
|
6111
|
-
else if (rawType.map !== undefined) {
|
|
6112
|
-
defaultValues[fieldName] = new MapSchema();
|
|
6113
|
-
}
|
|
6114
|
-
else if (rawType.set !== undefined) {
|
|
6115
|
-
defaultValues[fieldName] = new SetSchema();
|
|
6116
|
-
}
|
|
6117
|
-
else if (rawType.collection !== undefined) {
|
|
6118
|
-
defaultValues[fieldName] = new CollectionSchema();
|
|
6119
|
-
}
|
|
6120
|
-
else if (rawType.stream !== undefined) {
|
|
6121
|
-
defaultValues[fieldName] = new StreamSchema();
|
|
6122
|
-
}
|
|
6123
|
-
}
|
|
6124
|
-
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6125
|
-
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6126
|
-
defaultValues[fieldName] = new rawType();
|
|
6127
|
-
}
|
|
6201
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6202
|
+
if (autoDefault !== undefined) {
|
|
6203
|
+
defaultValues[fieldName] = autoDefault;
|
|
6128
6204
|
}
|
|
6129
6205
|
}
|
|
6130
6206
|
}
|
|
@@ -7363,8 +7439,19 @@ class Encoder {
|
|
|
7363
7439
|
// selected element is passed to `view.add()` which populates
|
|
7364
7440
|
// view.changes with the stream-link ADD + element-field ADDs.
|
|
7365
7441
|
this._emitStreamPriority(view);
|
|
7366
|
-
//
|
|
7367
|
-
|
|
7442
|
+
//
|
|
7443
|
+
// `view.changes` Map insertion order IS topological order:
|
|
7444
|
+
// - `view.add` walks the parent chain to root via `addParentOf`
|
|
7445
|
+
// (depth-first ancestor-first), inserting every ancestor's
|
|
7446
|
+
// entry before the descendant's.
|
|
7447
|
+
// - `view.remove` calls `_touchAncestorsOf` before its own
|
|
7448
|
+
// write to insert any missing ancestors at the front of the
|
|
7449
|
+
// chain — empty entries that get skipped by the size==0
|
|
7450
|
+
// check below but establish Map position.
|
|
7451
|
+
// No per-encode topo sort needed.
|
|
7452
|
+
//
|
|
7453
|
+
for (const refId of view.changes.keys()) {
|
|
7454
|
+
const changes = view.changes.get(refId);
|
|
7368
7455
|
const changeTree = this.root.changeTrees[refId];
|
|
7369
7456
|
if (changeTree === undefined) {
|
|
7370
7457
|
// detached instance, remove from view and skip.
|
|
@@ -8766,6 +8853,7 @@ const Callbacks = {
|
|
|
8766
8853
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
8767
8854
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
8768
8855
|
}
|
|
8856
|
+
throw new Error('Invalid room or decoder');
|
|
8769
8857
|
},
|
|
8770
8858
|
getRawChanges(decoder, callback) {
|
|
8771
8859
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -9221,12 +9309,27 @@ class StateView {
|
|
|
9221
9309
|
if (!this.isVisible(changeTree)) {
|
|
9222
9310
|
// view must have all "changeTree" parent tree
|
|
9223
9311
|
this.markVisible(changeTree);
|
|
9224
|
-
//
|
|
9312
|
+
// Recurse all the way to the root regardless of whether the
|
|
9313
|
+
// parent is filtered. Walking the full chain is what makes
|
|
9314
|
+
// `view.changes` topologically ordered by construction — any
|
|
9315
|
+
// filtered ancestor up the chain is touched here, before the
|
|
9316
|
+
// descendant's entry. The actual entry-write below is gated
|
|
9317
|
+
// on `hasFilteredFields` so non-filtered ancestors don't
|
|
9318
|
+
// emit redundant wire bytes (the decoder already knows them
|
|
9319
|
+
// via the shared encode pass). Marking them visible is
|
|
9320
|
+
// still useful: it makes this short-circuit fire on the next
|
|
9321
|
+
// `view.add` instead of re-walking the chain.
|
|
9225
9322
|
const parentChangeTree = changeTree.parent?.[$changes];
|
|
9226
|
-
if (parentChangeTree
|
|
9323
|
+
if (parentChangeTree) {
|
|
9227
9324
|
this.addParentOf(changeTree, tag);
|
|
9228
9325
|
}
|
|
9229
9326
|
}
|
|
9327
|
+
// Skip the entry-write for non-filtered ancestors: their refIds
|
|
9328
|
+
// are already known to the decoder through the shared pass, and
|
|
9329
|
+
// an extra ADD on a non-filtered field's index would only emit
|
|
9330
|
+
// bytes for a no-op (`value === previousValue` on the decoder).
|
|
9331
|
+
if (!changeTree.hasFilteredFields)
|
|
9332
|
+
return;
|
|
9230
9333
|
// add parent's tag properties
|
|
9231
9334
|
if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
|
|
9232
9335
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
@@ -9238,6 +9341,45 @@ class StateView {
|
|
|
9238
9341
|
changes.set(parentIndex, exports.OPERATION.ADD);
|
|
9239
9342
|
}
|
|
9240
9343
|
}
|
|
9344
|
+
/**
|
|
9345
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
9346
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
9347
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
9348
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
9349
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
9350
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
9351
|
+
* `view.add`, this preserves the global invariant that
|
|
9352
|
+
* `view.changes` iteration order is topological.
|
|
9353
|
+
*
|
|
9354
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
9355
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
9356
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
9357
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
9358
|
+
* upheld by every prior caller).
|
|
9359
|
+
*/
|
|
9360
|
+
_touchAncestorsOf(tree) {
|
|
9361
|
+
let cursor = tree.parent?.[$changes];
|
|
9362
|
+
if (cursor === undefined)
|
|
9363
|
+
return;
|
|
9364
|
+
// Collect the missing prefix of the chain, deepest-first. Only
|
|
9365
|
+
// FILTERED ancestors need entries — non-filtered ones never
|
|
9366
|
+
// appear in `view.changes` (mirrors the addParentOf gate), so
|
|
9367
|
+
// they don't need a Map slot reserved either.
|
|
9368
|
+
const stack = [];
|
|
9369
|
+
while (cursor !== undefined) {
|
|
9370
|
+
if (cursor.hasFilteredFields) {
|
|
9371
|
+
const refId = cursor.ref[$refId];
|
|
9372
|
+
if (this.changes.has(refId))
|
|
9373
|
+
break;
|
|
9374
|
+
stack.push(cursor);
|
|
9375
|
+
}
|
|
9376
|
+
cursor = cursor.parent?.[$changes];
|
|
9377
|
+
}
|
|
9378
|
+
// Insert root-first so Map order is topological.
|
|
9379
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
9380
|
+
this.changes.set(stack[i].ref[$refId], new Map());
|
|
9381
|
+
}
|
|
9382
|
+
}
|
|
9241
9383
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
9242
9384
|
const changeTree = obj[$changes];
|
|
9243
9385
|
if (!changeTree) {
|
|
@@ -9298,6 +9440,11 @@ class StateView {
|
|
|
9298
9440
|
const ref = changeTree.ref;
|
|
9299
9441
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
9300
9442
|
const refId = ref[$refId];
|
|
9443
|
+
// Pre-insert any missing ancestors into view.changes so the Map's
|
|
9444
|
+
// iteration order stays topological — the entries we're about to
|
|
9445
|
+
// write (either on this obj, or on its parent collection below)
|
|
9446
|
+
// must come AFTER every ancestor in the chain on the wire.
|
|
9447
|
+
this._touchAncestorsOf(changeTree);
|
|
9301
9448
|
let changes = this.changes.get(refId);
|
|
9302
9449
|
if (changes === undefined) {
|
|
9303
9450
|
changes = new Map();
|