@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.mjs
CHANGED
|
@@ -1212,6 +1212,33 @@ const Metadata = {
|
|
|
1212
1212
|
getStreamPriority(metadata, index) {
|
|
1213
1213
|
return metadata?.[$streamPriorities]?.[index];
|
|
1214
1214
|
},
|
|
1215
|
+
/**
|
|
1216
|
+
* Install a single field with full encoder wiring: accessor descriptor
|
|
1217
|
+
* on the prototype + `metadata[$encoders]` slot for primitives. Shared
|
|
1218
|
+
* between `Metadata.setFields` (build path) and
|
|
1219
|
+
* `Reflection.makeEncodable` (Reflection upgrade path).
|
|
1220
|
+
*/
|
|
1221
|
+
defineField(target, metadata, fieldIndex, fieldName, type) {
|
|
1222
|
+
const normalized = getNormalizedType(type);
|
|
1223
|
+
const { complexTypeKlass, childType } = resolveFieldType(normalized);
|
|
1224
|
+
Metadata.addField(metadata, fieldIndex, fieldName, normalized, getPropertyDescriptor(fieldName, fieldIndex, childType, complexTypeKlass));
|
|
1225
|
+
// Install accessor descriptor on the prototype (once per class field).
|
|
1226
|
+
if (metadata[$descriptors][fieldName]) {
|
|
1227
|
+
Object.defineProperty(target.prototype, fieldName, metadata[$descriptors][fieldName]);
|
|
1228
|
+
}
|
|
1229
|
+
// Pre-compute encoder function for primitive types.
|
|
1230
|
+
if (typeof normalized === "string") {
|
|
1231
|
+
if (!metadata[$encoders]) {
|
|
1232
|
+
Object.defineProperty(metadata, $encoders, {
|
|
1233
|
+
value: [],
|
|
1234
|
+
enumerable: false,
|
|
1235
|
+
configurable: true,
|
|
1236
|
+
writable: true,
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
metadata[$encoders][fieldIndex] = encode[normalized];
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1215
1242
|
setFields(target, fields) {
|
|
1216
1243
|
// for inheritance support
|
|
1217
1244
|
const constructor = target.prototype.constructor;
|
|
@@ -1249,17 +1276,7 @@ const Metadata = {
|
|
|
1249
1276
|
});
|
|
1250
1277
|
}
|
|
1251
1278
|
for (const field in fields) {
|
|
1252
|
-
|
|
1253
|
-
const { complexTypeKlass, childType } = resolveFieldType(type);
|
|
1254
|
-
Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(field, fieldIndex, childType, complexTypeKlass));
|
|
1255
|
-
// Install accessor descriptor on the prototype (once per class field).
|
|
1256
|
-
if (metadata[$descriptors][field]) {
|
|
1257
|
-
Object.defineProperty(target.prototype, field, metadata[$descriptors][field]);
|
|
1258
|
-
}
|
|
1259
|
-
// Pre-compute encoder function for primitive types.
|
|
1260
|
-
if (typeof type === "string") {
|
|
1261
|
-
metadata[$encoders][fieldIndex] = encode[type];
|
|
1262
|
-
}
|
|
1279
|
+
Metadata.defineField(constructor, metadata, fieldIndex, field, fields[field]);
|
|
1263
1280
|
fieldIndex++;
|
|
1264
1281
|
}
|
|
1265
1282
|
return target;
|
|
@@ -2091,11 +2108,24 @@ function checkInheritedFlags(tree, parent, parentIndex) {
|
|
|
2091
2108
|
const refType = Metadata.isValidInstance(tree.ref)
|
|
2092
2109
|
? tree.ref.constructor
|
|
2093
2110
|
: tree.ref[$childType];
|
|
2111
|
+
// #218: nested Schema fields inherit visibility from a @view-gated
|
|
2112
|
+
// parent regardless of whether the parent is a collection. The
|
|
2113
|
+
// `parentIsCollection` constraint that used to live here blocked
|
|
2114
|
+
// nested-Schema-field-of-@view-tagged-Schema from sharing visibility,
|
|
2115
|
+
// forcing users to wrap the child in an ArraySchema as a workaround.
|
|
2116
|
+
//
|
|
2117
|
+
// #226 (4.0.25): items inside a non-default-tag `@view(N)` collection
|
|
2118
|
+
// also inherit visibility from the parent collection, so items
|
|
2119
|
+
// pushed/set after `view.add(state, N)` show up automatically.
|
|
2120
|
+
// Default-tag `@view()` collections keep per-item gating —
|
|
2121
|
+
// `view.add(item)` is still required to opt each one in.
|
|
2122
|
+
// The `parentMetadata[parentIndex].tag` access is safe inside the
|
|
2123
|
+
// `fieldHasViewTag` short-circuit (the metadata entry and its `tag`
|
|
2124
|
+
// are guaranteed to exist when that flag is set).
|
|
2094
2125
|
tree.isVisibilitySharedWithParent = (parentChangeTree.isFiltered
|
|
2095
2126
|
&& typeof refType !== "string"
|
|
2096
|
-
&& !fieldHasViewTag
|
|
2097
2127
|
&& !fieldHasStream
|
|
2098
|
-
&& parentIsCollection);
|
|
2128
|
+
&& (!fieldHasViewTag || (parentIsCollection && parentMetadata[parentIndex].tag !== DEFAULT_VIEW_TAG)));
|
|
2099
2129
|
}
|
|
2100
2130
|
}
|
|
2101
2131
|
|
|
@@ -5428,8 +5458,10 @@ registerType("stream", { constructor: StreamSchema });
|
|
|
5428
5458
|
*/
|
|
5429
5459
|
class FieldBuilder {
|
|
5430
5460
|
[$builder] = true;
|
|
5431
|
-
// Internal configuration.
|
|
5432
|
-
//
|
|
5461
|
+
// Internal configuration. Declared `private` (soft-private): hidden from
|
|
5462
|
+
// editor autocomplete and from normal external `.field` access, but still
|
|
5463
|
+
// reachable at runtime via element access (e.g. `builder['_noSync']`) for
|
|
5464
|
+
// internal tooling/tests. Not meant to be mutated by end users.
|
|
5433
5465
|
_type;
|
|
5434
5466
|
_default = undefined;
|
|
5435
5467
|
_hasDefault = false;
|
|
@@ -5442,6 +5474,7 @@ class FieldBuilder {
|
|
|
5442
5474
|
_static = false;
|
|
5443
5475
|
_stream = false;
|
|
5444
5476
|
_optional = false;
|
|
5477
|
+
_noSync = false;
|
|
5445
5478
|
_streamPriority = undefined;
|
|
5446
5479
|
constructor(type) {
|
|
5447
5480
|
this._type = type;
|
|
@@ -5492,6 +5525,30 @@ class FieldBuilder {
|
|
|
5492
5525
|
this._static = true;
|
|
5493
5526
|
return this;
|
|
5494
5527
|
}
|
|
5528
|
+
/**
|
|
5529
|
+
* Mark this field as **local-only** — it is typed and initialized on the
|
|
5530
|
+
* instance (so `.default()` and the inferred instance type still apply),
|
|
5531
|
+
* but is never registered for synchronization: it never enters change
|
|
5532
|
+
* tracking, never goes over the wire, and decoders never receive it.
|
|
5533
|
+
*
|
|
5534
|
+
* Useful for server-side scratch state, per-peer UI state, or values you
|
|
5535
|
+
* want on the class for typing convenience without paying any sync cost.
|
|
5536
|
+
*
|
|
5537
|
+
* Mutually exclusive with the sync-only modifiers (`.view()`, `.owned()`,
|
|
5538
|
+
* `.unreliable()`, `.transient()`, `.static()`, `.stream()`) — combining
|
|
5539
|
+
* them throws at `schema()` time.
|
|
5540
|
+
*
|
|
5541
|
+
* ```ts
|
|
5542
|
+
* const Player = schema({
|
|
5543
|
+
* hp: t.uint8().default(100), // synchronized
|
|
5544
|
+
* lastInputTick: t.number().noSync(), // local-only
|
|
5545
|
+
* }, 'Player');
|
|
5546
|
+
* ```
|
|
5547
|
+
*/
|
|
5548
|
+
noSync() {
|
|
5549
|
+
this._noSync = true;
|
|
5550
|
+
return this;
|
|
5551
|
+
}
|
|
5495
5552
|
/**
|
|
5496
5553
|
* Opt a collection field into priority-batched streaming delivery —
|
|
5497
5554
|
* ADDs drain at most `maxPerTick` per tick per view (or per broadcast
|
|
@@ -5546,6 +5603,11 @@ class FieldBuilder {
|
|
|
5546
5603
|
this._optional = true;
|
|
5547
5604
|
return this;
|
|
5548
5605
|
}
|
|
5606
|
+
/**
|
|
5607
|
+
* @internal — snapshot of the builder's configuration consumed by
|
|
5608
|
+
* `schema()`. `private` keeps it out of autocomplete; internal callers
|
|
5609
|
+
* reach it via element access (`builder['toDefinition']()`).
|
|
5610
|
+
*/
|
|
5549
5611
|
toDefinition() {
|
|
5550
5612
|
return {
|
|
5551
5613
|
type: this._type,
|
|
@@ -5560,6 +5622,7 @@ class FieldBuilder {
|
|
|
5560
5622
|
static: this._static,
|
|
5561
5623
|
stream: this._stream,
|
|
5562
5624
|
optional: this._optional,
|
|
5625
|
+
noSync: this._noSync,
|
|
5563
5626
|
streamPriority: this._streamPriority,
|
|
5564
5627
|
};
|
|
5565
5628
|
}
|
|
@@ -5575,7 +5638,8 @@ function primitive(name) {
|
|
|
5575
5638
|
}
|
|
5576
5639
|
function resolveChild(child) {
|
|
5577
5640
|
if (isBuilder(child)) {
|
|
5578
|
-
|
|
5641
|
+
// `_type` is private; element access bypasses the visibility check.
|
|
5642
|
+
return child['_type'];
|
|
5579
5643
|
}
|
|
5580
5644
|
return child;
|
|
5581
5645
|
}
|
|
@@ -5585,7 +5649,7 @@ const setFactory = ((child) => new FieldBuilder({ set: resolveChild(child) }));
|
|
|
5585
5649
|
const collectionFactory = ((child) => new FieldBuilder({ collection: resolveChild(child) }));
|
|
5586
5650
|
const streamFactory = ((child) => {
|
|
5587
5651
|
const b = new FieldBuilder({ stream: resolveChild(child) });
|
|
5588
|
-
b
|
|
5652
|
+
b['_stream'] = true; // element access bypasses `private`
|
|
5589
5653
|
return b;
|
|
5590
5654
|
});
|
|
5591
5655
|
const refFactory = ((ctor) => new FieldBuilder(ctor));
|
|
@@ -6014,6 +6078,36 @@ function deprecated(throws = true) {
|
|
|
6014
6078
|
});
|
|
6015
6079
|
};
|
|
6016
6080
|
}
|
|
6081
|
+
/**
|
|
6082
|
+
* Produce the auto-instantiated construction default for a builder type
|
|
6083
|
+
* (empty collection or zero-arg Schema ref), or `undefined` when the type
|
|
6084
|
+
* has no auto-default. Shared by synced and `.noSync()` field handling.
|
|
6085
|
+
*/
|
|
6086
|
+
function autoInstantiateDefault(rawType) {
|
|
6087
|
+
if (rawType && typeof rawType === "object") {
|
|
6088
|
+
if (rawType.array !== undefined) {
|
|
6089
|
+
return new ArraySchema();
|
|
6090
|
+
}
|
|
6091
|
+
if (rawType.map !== undefined) {
|
|
6092
|
+
return new MapSchema();
|
|
6093
|
+
}
|
|
6094
|
+
if (rawType.set !== undefined) {
|
|
6095
|
+
return new SetSchema();
|
|
6096
|
+
}
|
|
6097
|
+
if (rawType.collection !== undefined) {
|
|
6098
|
+
return new CollectionSchema();
|
|
6099
|
+
}
|
|
6100
|
+
if (rawType.stream !== undefined) {
|
|
6101
|
+
return new StreamSchema();
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6105
|
+
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6106
|
+
return new rawType();
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
return undefined;
|
|
6110
|
+
}
|
|
6017
6111
|
/**
|
|
6018
6112
|
* Define a Schema class declaratively.
|
|
6019
6113
|
*
|
|
@@ -6049,7 +6143,28 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6049
6143
|
for (const fieldName in fieldsAndMethods) {
|
|
6050
6144
|
const value = fieldsAndMethods[fieldName];
|
|
6051
6145
|
if (isBuilder(value)) {
|
|
6052
|
-
const def = value
|
|
6146
|
+
const def = value['toDefinition'](); // private; element access bypasses visibility
|
|
6147
|
+
if (def.noSync) {
|
|
6148
|
+
// Local-only field: skip metadata registration entirely so it is
|
|
6149
|
+
// never encoded/decoded, but still seed its construction default
|
|
6150
|
+
// (honoring `.default()` and collection/ref auto-instantiation).
|
|
6151
|
+
if (def.view !== undefined || def.owned || def.unreliable ||
|
|
6152
|
+
def.transient || def.static || def.stream) {
|
|
6153
|
+
throw new Error(`schema(${name ? `'${name}'` : ""}): field '${fieldName}' uses .noSync() ` +
|
|
6154
|
+
`together with a sync-only modifier (.view/.owned/.unreliable/.transient/.static/.stream). ` +
|
|
6155
|
+
`A local-only field cannot be synchronized.`);
|
|
6156
|
+
}
|
|
6157
|
+
if (def.hasDefault) {
|
|
6158
|
+
defaultValues[fieldName] = def.default;
|
|
6159
|
+
}
|
|
6160
|
+
else if (!def.optional) {
|
|
6161
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6162
|
+
if (autoDefault !== undefined) {
|
|
6163
|
+
defaultValues[fieldName] = autoDefault;
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
continue;
|
|
6167
|
+
}
|
|
6053
6168
|
fields[fieldName] = getNormalizedType(def.type);
|
|
6054
6169
|
if (def.view !== undefined) {
|
|
6055
6170
|
viewTagFields[fieldName] = def.view;
|
|
@@ -6084,28 +6199,9 @@ function schema(fieldsAndMethods, name, inherits = Schema) {
|
|
|
6084
6199
|
else if (!def.optional) {
|
|
6085
6200
|
// Auto-instantiate collection/Schema defaults when none is provided.
|
|
6086
6201
|
// `.optional()` opts out — field starts as undefined.
|
|
6087
|
-
const
|
|
6088
|
-
if (
|
|
6089
|
-
|
|
6090
|
-
defaultValues[fieldName] = new ArraySchema();
|
|
6091
|
-
}
|
|
6092
|
-
else if (rawType.map !== undefined) {
|
|
6093
|
-
defaultValues[fieldName] = new MapSchema();
|
|
6094
|
-
}
|
|
6095
|
-
else if (rawType.set !== undefined) {
|
|
6096
|
-
defaultValues[fieldName] = new SetSchema();
|
|
6097
|
-
}
|
|
6098
|
-
else if (rawType.collection !== undefined) {
|
|
6099
|
-
defaultValues[fieldName] = new CollectionSchema();
|
|
6100
|
-
}
|
|
6101
|
-
else if (rawType.stream !== undefined) {
|
|
6102
|
-
defaultValues[fieldName] = new StreamSchema();
|
|
6103
|
-
}
|
|
6104
|
-
}
|
|
6105
|
-
else if (typeof rawType === "function" && Schema.is(rawType)) {
|
|
6106
|
-
if (!rawType.prototype.initialize || rawType.prototype.initialize.length === 0) {
|
|
6107
|
-
defaultValues[fieldName] = new rawType();
|
|
6108
|
-
}
|
|
6202
|
+
const autoDefault = autoInstantiateDefault(def.type);
|
|
6203
|
+
if (autoDefault !== undefined) {
|
|
6204
|
+
defaultValues[fieldName] = autoDefault;
|
|
6109
6205
|
}
|
|
6110
6206
|
}
|
|
6111
6207
|
}
|
|
@@ -7344,8 +7440,19 @@ class Encoder {
|
|
|
7344
7440
|
// selected element is passed to `view.add()` which populates
|
|
7345
7441
|
// view.changes with the stream-link ADD + element-field ADDs.
|
|
7346
7442
|
this._emitStreamPriority(view);
|
|
7347
|
-
//
|
|
7348
|
-
|
|
7443
|
+
//
|
|
7444
|
+
// `view.changes` Map insertion order IS topological order:
|
|
7445
|
+
// - `view.add` walks the parent chain to root via `addParentOf`
|
|
7446
|
+
// (depth-first ancestor-first), inserting every ancestor's
|
|
7447
|
+
// entry before the descendant's.
|
|
7448
|
+
// - `view.remove` calls `_touchAncestorsOf` before its own
|
|
7449
|
+
// write to insert any missing ancestors at the front of the
|
|
7450
|
+
// chain — empty entries that get skipped by the size==0
|
|
7451
|
+
// check below but establish Map position.
|
|
7452
|
+
// No per-encode topo sort needed.
|
|
7453
|
+
//
|
|
7454
|
+
for (const refId of view.changes.keys()) {
|
|
7455
|
+
const changes = view.changes.get(refId);
|
|
7349
7456
|
const changeTree = this.root.changeTrees[refId];
|
|
7350
7457
|
if (changeTree === undefined) {
|
|
7351
7458
|
// detached instance, remove from view and skip.
|
|
@@ -8106,6 +8213,31 @@ Reflection.decode = function (bytes, it) {
|
|
|
8106
8213
|
const state = new (typeContext.get(reflection.rootType || 0))();
|
|
8107
8214
|
return new Decoder(state, typeContext);
|
|
8108
8215
|
};
|
|
8216
|
+
Reflection.makeEncodable = function (ctor) {
|
|
8217
|
+
const metadata = ctor[Symbol.metadata];
|
|
8218
|
+
if (!metadata)
|
|
8219
|
+
return ctor;
|
|
8220
|
+
const numFields = metadata[$numFields];
|
|
8221
|
+
if (numFields === undefined)
|
|
8222
|
+
return ctor;
|
|
8223
|
+
// Walk every field index across the inheritance chain. Repeat calls
|
|
8224
|
+
// are cheap: defineField overwrites the same descriptor and re-stamps
|
|
8225
|
+
// the same `metadata[$encoders]` slot (idempotent).
|
|
8226
|
+
for (let i = 0; i <= numFields; i++) {
|
|
8227
|
+
const field = metadata[i];
|
|
8228
|
+
if (!field)
|
|
8229
|
+
continue;
|
|
8230
|
+
Metadata.defineField(ctor, metadata, i, field.name, field.type);
|
|
8231
|
+
}
|
|
8232
|
+
// Invalidate any cached encode descriptor — `getEncodeDescriptor`
|
|
8233
|
+
// memoizes on the constructor. If something already constructed it
|
|
8234
|
+
// (e.g. a prior `InputEncoder(...)` call that threw), drop the stale
|
|
8235
|
+
// entry so the next read sees the upgraded metadata.
|
|
8236
|
+
if (Object.prototype.hasOwnProperty.call(ctor, $encodeDescriptor)) {
|
|
8237
|
+
delete ctor[$encodeDescriptor];
|
|
8238
|
+
}
|
|
8239
|
+
return ctor;
|
|
8240
|
+
};
|
|
8109
8241
|
|
|
8110
8242
|
/**
|
|
8111
8243
|
* Legacy callback system
|
|
@@ -8722,6 +8854,7 @@ const Callbacks = {
|
|
|
8722
8854
|
else if ('decoder' in roomOrDecoder.serializer) {
|
|
8723
8855
|
return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
|
|
8724
8856
|
}
|
|
8857
|
+
throw new Error('Invalid room or decoder');
|
|
8725
8858
|
},
|
|
8726
8859
|
getRawChanges(decoder, callback) {
|
|
8727
8860
|
return getRawChangesCallback(decoder, callback);
|
|
@@ -9177,12 +9310,27 @@ class StateView {
|
|
|
9177
9310
|
if (!this.isVisible(changeTree)) {
|
|
9178
9311
|
// view must have all "changeTree" parent tree
|
|
9179
9312
|
this.markVisible(changeTree);
|
|
9180
|
-
//
|
|
9313
|
+
// Recurse all the way to the root regardless of whether the
|
|
9314
|
+
// parent is filtered. Walking the full chain is what makes
|
|
9315
|
+
// `view.changes` topologically ordered by construction — any
|
|
9316
|
+
// filtered ancestor up the chain is touched here, before the
|
|
9317
|
+
// descendant's entry. The actual entry-write below is gated
|
|
9318
|
+
// on `hasFilteredFields` so non-filtered ancestors don't
|
|
9319
|
+
// emit redundant wire bytes (the decoder already knows them
|
|
9320
|
+
// via the shared encode pass). Marking them visible is
|
|
9321
|
+
// still useful: it makes this short-circuit fire on the next
|
|
9322
|
+
// `view.add` instead of re-walking the chain.
|
|
9181
9323
|
const parentChangeTree = changeTree.parent?.[$changes];
|
|
9182
|
-
if (parentChangeTree
|
|
9324
|
+
if (parentChangeTree) {
|
|
9183
9325
|
this.addParentOf(changeTree, tag);
|
|
9184
9326
|
}
|
|
9185
9327
|
}
|
|
9328
|
+
// Skip the entry-write for non-filtered ancestors: their refIds
|
|
9329
|
+
// are already known to the decoder through the shared pass, and
|
|
9330
|
+
// an extra ADD on a non-filtered field's index would only emit
|
|
9331
|
+
// bytes for a no-op (`value === previousValue` on the decoder).
|
|
9332
|
+
if (!changeTree.hasFilteredFields)
|
|
9333
|
+
return;
|
|
9186
9334
|
// add parent's tag properties
|
|
9187
9335
|
if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
|
|
9188
9336
|
let changes = this.changes.get(changeTree.ref[$refId]);
|
|
@@ -9194,6 +9342,45 @@ class StateView {
|
|
|
9194
9342
|
changes.set(parentIndex, OPERATION.ADD);
|
|
9195
9343
|
}
|
|
9196
9344
|
}
|
|
9345
|
+
/**
|
|
9346
|
+
* Walk `tree`'s parent chain to root and insert an empty entry into
|
|
9347
|
+
* `view.changes` for any ancestor not already present. Empty entries
|
|
9348
|
+
* are skipped by `encodeView` (`changes.size === 0` continue), so no
|
|
9349
|
+
* wire bytes are emitted — but the Map's insertion order now puts
|
|
9350
|
+
* each ancestor BEFORE the descendant entry that the caller is about
|
|
9351
|
+
* to write. Combined with `addParentOf`'s full-recursion walk on
|
|
9352
|
+
* `view.add`, this preserves the global invariant that
|
|
9353
|
+
* `view.changes` iteration order is topological.
|
|
9354
|
+
*
|
|
9355
|
+
* Iterative (not recursive) so the stack is bounded by tree depth
|
|
9356
|
+
* regardless of call patterns. Stops the walk as soon as it hits an
|
|
9357
|
+
* ancestor that's already in `view.changes` — at that point the
|
|
9358
|
+
* remainder of the chain is guaranteed to also be present (invariant
|
|
9359
|
+
* upheld by every prior caller).
|
|
9360
|
+
*/
|
|
9361
|
+
_touchAncestorsOf(tree) {
|
|
9362
|
+
let cursor = tree.parent?.[$changes];
|
|
9363
|
+
if (cursor === undefined)
|
|
9364
|
+
return;
|
|
9365
|
+
// Collect the missing prefix of the chain, deepest-first. Only
|
|
9366
|
+
// FILTERED ancestors need entries — non-filtered ones never
|
|
9367
|
+
// appear in `view.changes` (mirrors the addParentOf gate), so
|
|
9368
|
+
// they don't need a Map slot reserved either.
|
|
9369
|
+
const stack = [];
|
|
9370
|
+
while (cursor !== undefined) {
|
|
9371
|
+
if (cursor.hasFilteredFields) {
|
|
9372
|
+
const refId = cursor.ref[$refId];
|
|
9373
|
+
if (this.changes.has(refId))
|
|
9374
|
+
break;
|
|
9375
|
+
stack.push(cursor);
|
|
9376
|
+
}
|
|
9377
|
+
cursor = cursor.parent?.[$changes];
|
|
9378
|
+
}
|
|
9379
|
+
// Insert root-first so Map order is topological.
|
|
9380
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
9381
|
+
this.changes.set(stack[i].ref[$refId], new Map());
|
|
9382
|
+
}
|
|
9383
|
+
}
|
|
9197
9384
|
remove(obj, tag = DEFAULT_VIEW_TAG, _isClear = false) {
|
|
9198
9385
|
const changeTree = obj[$changes];
|
|
9199
9386
|
if (!changeTree) {
|
|
@@ -9254,6 +9441,11 @@ class StateView {
|
|
|
9254
9441
|
const ref = changeTree.ref;
|
|
9255
9442
|
const metadata = ref.constructor[Symbol.metadata]; // ArraySchema/MapSchema do not have metadata
|
|
9256
9443
|
const refId = ref[$refId];
|
|
9444
|
+
// Pre-insert any missing ancestors into view.changes so the Map's
|
|
9445
|
+
// iteration order stays topological — the entries we're about to
|
|
9446
|
+
// write (either on this obj, or on its parent collection below)
|
|
9447
|
+
// must come AFTER every ancestor in the chain on the wire.
|
|
9448
|
+
this._touchAncestorsOf(changeTree);
|
|
9257
9449
|
let changes = this.changes.get(refId);
|
|
9258
9450
|
if (changes === undefined) {
|
|
9259
9451
|
changes = new Map();
|