@colyseus/schema 5.0.2 → 5.0.4
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/Metadata.d.ts +7 -0
- package/build/Reflection.d.ts +16 -0
- 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 +236 -44
- package/build/index.cjs.map +1 -1
- package/build/index.js +236 -44
- package/build/index.mjs +236 -44
- package/build/index.mjs.map +1 -1
- package/build/input/index.cjs +56 -15
- package/build/input/index.cjs.map +1 -1
- package/build/input/index.mjs +56 -15
- package/build/input/index.mjs.map +1 -1
- package/build/types/builder.d.ts +43 -14
- package/package.json +6 -5
- package/src/Metadata.ts +44 -22
- package/src/Reflection.ts +46 -1
- 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 +55 -18
package/build/index.js
CHANGED
|
@@ -1218,6 +1218,33 @@
|
|
|
1218
1218
|
getStreamPriority(metadata, index) {
|
|
1219
1219
|
return metadata?.[$streamPriorities]?.[index];
|
|
1220
1220
|
},
|
|
1221
|
+
/**
|
|
1222
|
+
* Install a single field with full encoder wiring: accessor descriptor
|
|
1223
|
+
* on the prototype + `metadata[$encoders]` slot for primitives. Shared
|
|
1224
|
+
* between `Metadata.setFields` (build path) and
|
|
1225
|
+
* `Reflection.makeEncodable` (Reflection upgrade path).
|
|
1226
|
+
*/
|
|
1227
|
+
defineField(target, metadata, fieldIndex, fieldName, type) {
|
|
1228
|
+
const normalized = getNormalizedType(type);
|
|
1229
|
+
const { complexTypeKlass, childType } = resolveFieldType(normalized);
|
|
1230
|
+
Metadata.addField(metadata, fieldIndex, fieldName, normalized, getPropertyDescriptor(fieldName, fieldIndex, childType, complexTypeKlass));
|
|
1231
|
+
// Install accessor descriptor on the prototype (once per class field).
|
|
1232
|
+
if (metadata[$descriptors][fieldName]) {
|
|
1233
|
+
Object.defineProperty(target.prototype, fieldName, metadata[$descriptors][fieldName]);
|
|
1234
|
+
}
|
|
1235
|
+
// Pre-compute encoder function for primitive types.
|
|
1236
|
+
if (typeof normalized === "string") {
|
|
1237
|
+
if (!metadata[$encoders]) {
|
|
1238
|
+
Object.defineProperty(metadata, $encoders, {
|
|
1239
|
+
value: [],
|
|
1240
|
+
enumerable: false,
|
|
1241
|
+
configurable: true,
|
|
1242
|
+
writable: true,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
metadata[$encoders][fieldIndex] = encode[normalized];
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1221
1248
|
setFields(target, fields) {
|
|
1222
1249
|
// for inheritance support
|
|
1223
1250
|
const constructor = target.prototype.constructor;
|
|
@@ -1255,17 +1282,7 @@
|
|
|
1255
1282
|
});
|
|
1256
1283
|
}
|
|
1257
1284
|
for (const field in fields) {
|
|
1258
|
-
|
|
1259
|
-
const { complexTypeKlass, childType } = resolveFieldType(type);
|
|
1260
|
-
Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(field, fieldIndex, childType, complexTypeKlass));
|
|
1261
|
-
// Install accessor descriptor on the prototype (once per class field).
|
|
1262
|
-
if (metadata[$descriptors][field]) {
|
|
1263
|
-
Object.defineProperty(target.prototype, field, metadata[$descriptors][field]);
|
|
1264
|
-
}
|
|
1265
|
-
// Pre-compute encoder function for primitive types.
|
|
1266
|
-
if (typeof type === "string") {
|
|
1267
|
-
metadata[$encoders][fieldIndex] = encode[type];
|
|
1268
|
-
}
|
|
1285
|
+
Metadata.defineField(constructor, metadata, fieldIndex, field, fields[field]);
|
|
1269
1286
|
fieldIndex++;
|
|
1270
1287
|
}
|
|
1271
1288
|
return target;
|
|
@@ -2097,11 +2114,24 @@
|
|
|
2097
2114
|
const refType = Metadata.isValidInstance(tree.ref)
|
|
2098
2115
|
? tree.ref.constructor
|
|
2099
2116
|
: tree.ref[$childType];
|
|
2117
|
+
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
2118
|
+
// parent regardless of whether the parent is a collection. The
|
|
2119
|
+
// `parentIsCollection` constraint that used to live here blocked
|
|
2120
|
+
// nested-Schema-field-of-@view-tagged-Schema from sharing visibility,
|
|
2121
|
+
// forcing users to wrap the child in an ArraySchema as a workaround.
|
|
2122
|
+
//
|
|
2123
|
+
// #226 (4.0.25): items inside a non-default-tag `@view(N)` collection
|
|
2124
|
+
// also inherit visibility from the parent collection, so items
|
|
2125
|
+
// pushed/set after `view.add(state, N)` show up automatically.
|
|
2126
|
+
// Default-tag `@view()` collections keep per-item gating —
|
|
2127
|
+
// `view.add(item)` is still required to opt each one in.
|
|
2128
|
+
// The `parentMetadata[parentIndex].tag` access is safe inside the
|
|
2129
|
+
// `fieldHasViewTag` short-circuit (the metadata entry and its `tag`
|
|
2130
|
+
// are guaranteed to exist when that flag is set).
|
|
2100
2131
|
tree.isVisibilitySharedWithParent = (parentChangeTree.isFiltered
|
|
2101
2132
|
&& typeof refType !== "string"
|
|
2102
|
-
&& !fieldHasViewTag
|
|
2103
2133
|
&& !fieldHasStream
|
|
2104
|
-
&& parentIsCollection);
|
|
2134
|
+
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
|
|
2105
2135
|
}
|
|
2106
2136
|
}
|
|
2107
2137
|
|
|
@@ -5434,8 +5464,10 @@
|
|
|
5434
5464
|
*/
|
|
5435
5465
|
class FieldBuilder {
|
|
5436
5466
|
[$builder] = true;
|
|
5437
|
-
// Internal configuration.
|
|
5438
|
-
//
|
|
5467
|
+
// Internal configuration. Declared `private` (soft-private): hidden from
|
|
5468
|
+
// editor autocomplete and from normal external `.field` access, but still
|
|
5469
|
+
// reachable at runtime via element access (e.g. `builder['_noSync']`) for
|
|
5470
|
+
// internal tooling/tests. Not meant to be mutated by end users.
|
|
5439
5471
|
_type;
|
|
5440
5472
|
_default = undefined;
|
|
5441
5473
|
_hasDefault = false;
|
|
@@ -5448,6 +5480,7 @@
|
|
|
5448
5480
|
_static = false;
|
|
5449
5481
|
_stream = false;
|
|
5450
5482
|
_optional = false;
|
|
5483
|
+
_noSync = false;
|
|
5451
5484
|
_streamPriority = undefined;
|
|
5452
5485
|
constructor(type) {
|
|
5453
5486
|
this._type = type;
|
|
@@ -5498,6 +5531,30 @@
|
|
|
5498
5531
|
this._static = true;
|
|
5499
5532
|
return this;
|
|
5500
5533
|
}
|
|
5534
|
+
/**
|
|
5535
|
+
* Mark this field as **local-only** — it is typed and initialized on the
|
|
5536
|
+
* instance (so `.default()` and the inferred instance type still apply),
|
|
5537
|
+
* but is never registered for synchronization: it never enters change
|
|
5538
|
+
* tracking, never goes over the wire, and decoders never receive it.
|
|
5539
|
+
*
|
|
5540
|
+
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
5541
|
+
* want on the class for typing convenience without paying any sync cost.
|
|
5542
|
+
*
|
|
5543
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`, `.owned()`,
|
|
5544
|
+
* `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
|
|
5545
|
+
* them throws at `schema()` time.
|
|
5546
|
+
*
|
|
5547
|
+
* ```ts
|
|
5548
|
+
* const Player = schema({
|
|
5549
|
+
* hp: t.uint8().default(100), // synchronized
|
|
5550
|
+
* lastInputTick: t.number().noSync(), // local-only
|
|
5551
|
+
* }, 'Player');
|
|
5552
|
+
* ```
|
|
5553
|
+
*/
|
|
5554
|
+
noSync() {
|
|
5555
|
+
this._noSync = true;
|
|
5556
|
+
return this;
|
|
5557
|
+
}
|
|
5501
5558
|
/**
|
|
5502
5559
|
* Opt a collection field into priority-batched streaming delivery —
|
|
5503
5560
|
* ADDs drain at most `maxPerTick` per tick per view (or per broadcast
|
|
@@ -5552,6 +5609,11 @@
|
|
|
5552
5609
|
this._optional = true;
|
|
5553
5610
|
return this;
|
|
5554
5611
|
}
|
|
5612
|
+
/**
|
|
5613
|
+
* @internal — snapshot of the builder's configuration consumed by
|
|
5614
|
+
* `schema()`. `private` keeps it out of autocomplete; internal callers
|
|
5615
|
+
* reach it via element access (`builder['toDefinition']()`).
|
|
5616
|
+
*/
|
|
5555
5617
|
toDefinition() {
|
|
5556
5618
|
return {
|
|
5557
5619
|
type: this._type,
|
|
@@ -5566,6 +5628,7 @@
|
|
|
5566
5628
|
static: this._static,
|
|
5567
5629
|
stream: this._stream,
|
|
5568
5630
|
optional: this._optional,
|
|
5631
|
+
noSync: this._noSync,
|
|
5569
5632
|
streamPriority: this._streamPriority,
|
|
5570
5633
|
};
|
|
5571
5634
|
}
|
|
@@ -5581,7 +5644,8 @@
|
|
|
5581
5644
|
}
|
|
5582
5645
|
function resolveChild(child) {
|
|
5583
5646
|
if (isBuilder(child)) {
|
|
5584
|
-
|
|
5647
|
+
// `_type` is private; element access bypasses the visibility check.
|
|
5648
|
+
return child['_type'];
|
|
5585
5649
|
}
|
|
5586
5650
|
return child;
|
|
5587
5651
|
}
|
|
@@ -5591,7 +5655,7 @@
|
|
|
5591
5655
|
const collectionFactory = ((child) => new FieldBuilder({ collection: resolveChild(child) }));
|
|
5592
5656
|
const streamFactory = ((child) => {
|
|
5593
5657
|
const b = new FieldBuilder({ stream: resolveChild(child) });
|
|
5594
|
-
b
|
|
5658
|
+
b['_stream'] = true; // element access bypasses `private`
|
|
5595
5659
|
return b;
|
|
5596
5660
|
});
|
|
5597
5661
|
const refFactory = ((ctor) => new FieldBuilder(ctor));
|
|
@@ -6020,6 +6084,36 @@
|
|
|
6020
6084
|
});
|
|
6021
6085
|
};
|
|
6022
6086
|
}
|
|
6087
|
+
/**
|
|
6088
|
+
* Produce the auto-instantiated construction default for a builder type
|
|
6089
|
+
* (empty collection or zero-arg Schema ref), or `undefined` when the type
|
|
6090
|
+
* has no auto-default. Shared by synced and `.noSync()` field handling.
|
|
6091
|
+
*/
|
|
6092
|
+
function autoInstantiateDefault(rawType) {
|
|
6093
|
+
if (rawType && typeof rawType === "object") {
|
|
6094
|
+
if (rawType.array !== undefined) {
|
|
6095
|
+
return new ArraySchema();
|
|
6096
|
+
}
|
|
6097
|
+
if (rawType.map !== undefined) {
|
|
6098
|
+
return new MapSchema();
|
|
6099
|
+
}
|
|
6100
|
+
if (rawType.set !== undefined) {
|
|
6101
|
+
return new SetSchema();
|
|
6102
|
+
}
|
|
6103
|
+
if (rawType.collection !== undefined) {
|
|
6104
|
+
return new CollectionSchema();
|
|
6105
|
+
}
|
|
6106
|
+
if (rawType.stream !== undefined) {
|
|
6107
|
+
return new StreamSchema();
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6111
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6112
|
+
return new rawType();
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
return undefined;
|
|
6116
|
+
}
|
|
6023
6117
|
/**
|
|
6024
6118
|
* Define a Schema class declaratively.
|
|
6025
6119
|
*
|
|
@@ -6055,7 +6149,28 @@
|
|
|
6055
6149
|
for (const fieldName in fieldsAndMethods) {
|
|
6056
6150
|
const value = fieldsAndMethods[fieldName];
|
|
6057
6151
|
if (isBuilder(value)) {
|
|
6058
|
-
const def = value
|
|
6152
|
+
const def = value['toDefinition'](); // private; element access bypasses visibility
|
|
6153
|
+
if (def.noSync) {
|
|
6154
|
+
// Local-only field: skip metadata registration entirely so it is
|
|
6155
|
+
// never encoded/decoded, but still seed its construction default
|
|
6156
|
+
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6157
|
+
if (def.view !== undefined || def.owned || def.unreliable ||
|
|
6158
|
+
def.transient || def.static || def.stream) {
|
|
6159
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6160
|
+
`together with a sync-only modifier (.view/.owned/.unreliable/.transient/.static/.stream). ` +
|
|
6161
|
+
`A local-only field cannot be synchronized.`);
|
|
6162
|
+
}
|
|
6163
|
+
if (def.hasDefault) {
|
|
6164
|
+
defaultValues[fieldName] = def.default;
|
|
6165
|
+
}
|
|
6166
|
+
else if (!def.optional) {
|
|
6167
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6168
|
+
if (autoDefault !== undefined) {
|
|
6169
|
+
defaultValues[fieldName] = autoDefault;
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
continue;
|
|
6173
|
+
}
|
|
6059
6174
|
fields[fieldName] = getNormalizedType(def.type);
|
|
6060
6175
|
if (def.view !== undefined) {
|
|
6061
6176
|
viewTagFields[fieldName] = def.view;
|
|
@@ -6090,28 +6205,9 @@
|
|
|
6090
6205
|
else if (!def.optional) {
|
|
6091
6206
|
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
6092
6207
|
// `.optional()` opts out — field starts as undefined.
|
|
6093
|
-
const
|
|
6094
|
-
if (
|
|
6095
|
-
|
|
6096
|
-
defaultValues[fieldName] = new ArraySchema();
|
|
6097
|
-
}
|
|
6098
|
-
else if (rawType.map !== undefined) {
|
|
6099
|
-
defaultValues[fieldName] = new MapSchema();
|
|
6100
|
-
}
|
|
6101
|
-
else if (rawType.set !== undefined) {
|
|
6102
|
-
defaultValues[fieldName] = new SetSchema();
|
|
6103
|
-
}
|
|
6104
|
-
else if (rawType.collection !== undefined) {
|
|
6105
|
-
defaultValues[fieldName] = new CollectionSchema();
|
|
6106
|
-
}
|
|
6107
|
-
else if (rawType.stream !== undefined) {
|
|
6108
|
-
defaultValues[fieldName] = new StreamSchema();
|
|
6109
|
-
}
|
|
6110
|
-
}
|
|
6111
|
-
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6112
|
-
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6113
|
-
defaultValues[fieldName] = new rawType();
|
|
6114
|
-
}
|
|
6208
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6209
|
+
if (autoDefault !== undefined) {
|
|
6210
|
+
defaultValues[fieldName] = autoDefault;
|
|
6115
6211
|
}
|
|
6116
6212
|
}
|
|
6117
6213
|
}
|
|
@@ -7350,8 +7446,19 @@
|
|
|
7350
7446
|
// selected element is passed to `view.add()` which populates
|
|
7351
7447
|
// view.changes with the stream-link ADD + element-field ADDs.
|
|
7352
7448
|
this._emitStreamPriority(view);
|
|
7353
|
-
//
|
|
7354
|
-
|
|
7449
|
+
//
|
|
7450
|
+
// `view.changes` Map insertion order IS topological order:
|
|
7451
|
+
// - `view.add` walks the parent chain to root via `addParentOf`
|
|
7452
|
+
// (depth-first ancestor-first), inserting every ancestor's
|
|
7453
|
+
// entry before the descendant's.
|
|
7454
|
+
// - `view.remove` calls `_touchAncestorsOf` before its own
|
|
7455
|
+
// write to insert any missing ancestors at the front of the
|
|
7456
|
+
// chain — empty entries that get skipped by the size==0
|
|
7457
|
+
// check below but establish Map position.
|
|
7458
|
+
// No per-encode topo sort needed.
|
|
7459
|
+
//
|
|
7460
|
+
for (const refId of view.changes.keys()) {
|
|
7461
|
+
const changes = view.changes.get(refId);
|
|
7355
7462
|
const changeTree = this.root.changeTrees[refId];
|
|
7356
7463
|
if (changeTree === undefined) {
|
|
7357
7464
|
// detached instance, remove from view and skip.
|
|
@@ -8112,6 +8219,31 @@
|
|
|
8112
8219
|
const state = new (typeContext.get(reflection.rootType || 0))();
|
|
8113
8220
|
return new Decoder(state, typeContext);
|
|
8114
8221
|
};
|
|
8222
|
+
Reflection.makeEncodable = function (ctor) {
|
|
8223
|
+
const metadata = ctor[Symbol.metadata];
|
|
8224
|
+
if (!metadata)
|
|
8225
|
+
return ctor;
|
|
8226
|
+
const numFields = metadata[$numFields];
|
|
8227
|
+
if (numFields === undefined)
|
|
8228
|
+
return ctor;
|
|
8229
|
+
// Walk every field index across the inheritance chain. Repeat calls
|
|
8230
|
+
// are cheap: defineField overwrites the same descriptor and re-stamps
|
|
8231
|
+
// the same `metadata[$encoders]` slot (idempotent).
|
|
8232
|
+
for (let i = 0; i <= numFields; i++) {
|
|
8233
|
+
const field = metadata[i];
|
|
8234
|
+
if (!field)
|
|
8235
|
+
continue;
|
|
8236
|
+
Metadata.defineField(ctor, metadata, i, field.name, field.type);
|
|
8237
|
+
}
|
|
8238
|
+
// Invalidate any cached encode descriptor — `getEncodeDescriptor`
|
|
8239
|
+
// memoizes on the constructor. If something already constructed it
|
|
8240
|
+
// (e.g. a prior `InputEncoder(...)` call that threw), drop the stale
|
|
8241
|
+
// entry so the next read sees the upgraded metadata.
|
|
8242
|
+
if (Object.prototype.hasOwnProperty.call(ctor, $encodeDescriptor)) {
|
|
8243
|
+
delete ctor[$encodeDescriptor];
|
|
8244
|
+
}
|
|
8245
|
+
return ctor;
|
|
8246
|
+
};
|
|
8115
8247
|
|
|
8116
8248
|
/**
|
|
8117
8249
|
* Legacy callback system
|
|
@@ -8728,6 +8860,7 @@
|
|
|
8728
8860
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
8729
8861
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
8730
8862
|
}
|
|
8863
|
+
throw new Error('Invalid room or decoder');
|
|
8731
8864
|
},
|
|
8732
8865
|
getRawChanges(decoder, callback) {
|
|
8733
8866
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -9183,12 +9316,27 @@
|
|
|
9183
9316
|
if (!this.isVisible(changeTree)) {
|
|
9184
9317
|
// view must have all "changeTree" parent tree
|
|
9185
9318
|
this.markVisible(changeTree);
|
|
9186
|
-
//
|
|
9319
|
+
// Recurse all the way to the root regardless of whether the
|
|
9320
|
+
// parent is filtered. Walking the full chain is what makes
|
|
9321
|
+
// `view.changes` topologically ordered by construction — any
|
|
9322
|
+
// filtered ancestor up the chain is touched here, before the
|
|
9323
|
+
// descendant's entry. The actual entry-write below is gated
|
|
9324
|
+
// on `hasFilteredFields` so non-filtered ancestors don't
|
|
9325
|
+
// emit redundant wire bytes (the decoder already knows them
|
|
9326
|
+
// via the shared encode pass). Marking them visible is
|
|
9327
|
+
// still useful: it makes this short-circuit fire on the next
|
|
9328
|
+
// `view.add` instead of re-walking the chain.
|
|
9187
9329
|
const parentChangeTree = changeTree.parent?.[$changes];
|
|
9188
|
-
if (parentChangeTree
|
|
9330
|
+
if (parentChangeTree) {
|
|
9189
9331
|
this.addParentOf(changeTree, tag);
|
|
9190
9332
|
}
|
|
9191
9333
|
}
|
|
9334
|
+
// Skip the entry-write for non-filtered ancestors: their refIds
|
|
9335
|
+
// are already known to the decoder through the shared pass, and
|
|
9336
|
+
// an extra ADD on a non-filtered field's index would only emit
|
|
9337
|
+
// bytes for a no-op (`value === previousValue` on the decoder).
|
|
9338
|
+
if (!changeTree.hasFilteredFields)
|
|
9339
|
+
return;
|
|
9192
9340
|
// add parent's tag properties
|
|
9193
9341
|
if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
|
|
9194
9342
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
@@ -9200,6 +9348,45 @@
|
|
|
9200
9348
|
changes.set(parentIndex, exports.OPERATION.ADD);
|
|
9201
9349
|
}
|
|
9202
9350
|
}
|
|
9351
|
+
/**
|
|
9352
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
9353
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
9354
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
9355
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
9356
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
9357
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
9358
|
+
* `view.add`, this preserves the global invariant that
|
|
9359
|
+
* `view.changes` iteration order is topological.
|
|
9360
|
+
*
|
|
9361
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
9362
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
9363
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
9364
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
9365
|
+
* upheld by every prior caller).
|
|
9366
|
+
*/
|
|
9367
|
+
_touchAncestorsOf(tree) {
|
|
9368
|
+
let cursor = tree.parent?.[$changes];
|
|
9369
|
+
if (cursor === undefined)
|
|
9370
|
+
return;
|
|
9371
|
+
// Collect the missing prefix of the chain, deepest-first. Only
|
|
9372
|
+
// FILTERED ancestors need entries — non-filtered ones never
|
|
9373
|
+
// appear in `view.changes` (mirrors the addParentOf gate), so
|
|
9374
|
+
// they don't need a Map slot reserved either.
|
|
9375
|
+
const stack = [];
|
|
9376
|
+
while (cursor !== undefined) {
|
|
9377
|
+
if (cursor.hasFilteredFields) {
|
|
9378
|
+
const refId = cursor.ref[$refId];
|
|
9379
|
+
if (this.changes.has(refId))
|
|
9380
|
+
break;
|
|
9381
|
+
stack.push(cursor);
|
|
9382
|
+
}
|
|
9383
|
+
cursor = cursor.parent?.[$changes];
|
|
9384
|
+
}
|
|
9385
|
+
// Insert root-first so Map order is topological.
|
|
9386
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
9387
|
+
this.changes.set(stack[i].ref[$refId], new Map());
|
|
9388
|
+
}
|
|
9389
|
+
}
|
|
9203
9390
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
9204
9391
|
const changeTree = obj[$changes];
|
|
9205
9392
|
if (!changeTree) {
|
|
@@ -9260,6 +9447,11 @@
|
|
|
9260
9447
|
const ref = changeTree.ref;
|
|
9261
9448
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
9262
9449
|
const refId = ref[$refId];
|
|
9450
|
+
// Pre-insert any missing ancestors into view.changes so the Map's
|
|
9451
|
+
// iteration order stays topological — the entries we're about to
|
|
9452
|
+
// write (either on this obj, or on its parent collection below)
|
|
9453
|
+
// must come AFTER every ancestor in the chain on the wire.
|
|
9454
|
+
this._touchAncestorsOf(changeTree);
|
|
9263
9455
|
let changes = this.changes.get(refId);
|
|
9264
9456
|
if (changes === undefined) {
|
|
9265
9457
|
changes = new Map();
|