@colyseus/schema 5.0.9 → 5.0.11
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 +87 -51
- package/build/codegen/cli.cjs +13 -5
- package/build/codegen/cli.cjs.map +1 -1
- package/build/encoder/ChangeTree.d.ts +10 -3
- package/build/encoder/StateView.d.ts +0 -6
- package/build/index.cjs +182 -102
- package/build/index.cjs.map +1 -1
- package/build/index.js +182 -102
- package/build/index.mjs +182 -102
- package/build/index.mjs.map +1 -1
- package/build/types/custom/ArraySchema.d.ts +9 -1
- package/build/types/custom/MapSchema.d.ts +18 -1
- package/package.json +9 -3
- package/src/codegen/cli.ts +4 -4
- package/src/codegen/parser.ts +10 -1
- package/src/decoder/DecodeOperation.ts +23 -7
- package/src/encoder/ChangeTree.ts +18 -12
- package/src/encoder/EncodeOperation.ts +14 -1
- package/src/encoder/Encoder.ts +3 -16
- package/src/encoder/StateView.ts +16 -35
- package/src/types/custom/ArraySchema.ts +77 -30
- package/src/types/custom/MapSchema.ts +34 -1
package/build/index.js
CHANGED
|
@@ -2598,7 +2598,6 @@
|
|
|
2598
2598
|
// per-view WeakSet lookups with direct bitwise ops.
|
|
2599
2599
|
// Lazy: undefined until the tree participates in any view.
|
|
2600
2600
|
visibleViews;
|
|
2601
|
-
invisibleViews;
|
|
2602
2601
|
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
2603
2602
|
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
2604
2603
|
tagViews;
|
|
@@ -2950,18 +2949,34 @@
|
|
|
2950
2949
|
// per-view visibility lives on the tree (NOT keyed by refId), so a
|
|
2951
2950
|
// recycled tree must not inherit its previous life's view membership.
|
|
2952
2951
|
this.visibleViews = undefined;
|
|
2953
|
-
this.invisibleViews = undefined;
|
|
2954
2952
|
this.tagViews = undefined;
|
|
2955
2953
|
this.subscribedViews = undefined;
|
|
2956
2954
|
}
|
|
2957
|
-
|
|
2955
|
+
/**
|
|
2956
|
+
* ArraySchema#unshift(): re-key pending ops on both channels by
|
|
2957
|
+
* `+count`, then record ADDs for the new items at indexes 0..count-1.
|
|
2958
|
+
*
|
|
2959
|
+
* The rebuilt map's insertion order IS the wire order: new ADDs first
|
|
2960
|
+
* (ascending — the decoder splice-inserts each one, which only works
|
|
2961
|
+
* lowest-index-first), then prior ops in their original relative order
|
|
2962
|
+
* at their shifted positions. See ArraySchema#$setAt.
|
|
2963
|
+
*/
|
|
2964
|
+
unshift(count) {
|
|
2958
2965
|
if (this._isSchema)
|
|
2959
|
-
throw new Error("ChangeTree (Schema):
|
|
2966
|
+
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2960
2967
|
const src = this.collDirty;
|
|
2961
2968
|
const dst = new Map();
|
|
2969
|
+
const track = !this.paused && !this.isStatic;
|
|
2970
|
+
if (track) {
|
|
2971
|
+
for (let i = 0; i < count; i++)
|
|
2972
|
+
dst.set(i, exports.OPERATION.ADD);
|
|
2973
|
+
}
|
|
2962
2974
|
for (const [idx, val] of src)
|
|
2963
|
-
dst.set(idx +
|
|
2975
|
+
dst.set(idx + count, val);
|
|
2964
2976
|
this.collDirty = dst;
|
|
2977
|
+
this.unreliableRecorder?.shift(count);
|
|
2978
|
+
if (track)
|
|
2979
|
+
this.root?.enqueueChangeTree(this);
|
|
2965
2980
|
}
|
|
2966
2981
|
// Tree attachment + child iteration — see ./changeTree/treeAttachment.ts.
|
|
2967
2982
|
setRoot(root) { setRoot(this, root); }
|
|
@@ -3030,12 +3045,6 @@
|
|
|
3030
3045
|
indexedOperation(index, operation) {
|
|
3031
3046
|
this._routeAndRecord(index, operation, true);
|
|
3032
3047
|
}
|
|
3033
|
-
// ArraySchema#unshift(): apply shift to both channels.
|
|
3034
|
-
// Unreliable recorder on an array is always a CollectionChangeRecorder.
|
|
3035
|
-
shiftChangeIndexes(shiftIndex) {
|
|
3036
|
-
this.shift(shiftIndex);
|
|
3037
|
-
this.unreliableRecorder?.shift(shiftIndex);
|
|
3038
|
-
}
|
|
3039
3048
|
getChange(index) {
|
|
3040
3049
|
return this.operationAt(index);
|
|
3041
3050
|
}
|
|
@@ -3376,7 +3385,8 @@
|
|
|
3376
3385
|
// ArraySchema stores its per-instance child type at `$childType`.
|
|
3377
3386
|
// This encoder is array-only — there's no Schema fallback to consider.
|
|
3378
3387
|
const type = ref[$childType];
|
|
3379
|
-
const
|
|
3388
|
+
const isSchemaChild = typeof type !== "string";
|
|
3389
|
+
const useOperationByRefId = hasView && changeTree.isFiltered && isSchemaChild;
|
|
3380
3390
|
let refOrIndex;
|
|
3381
3391
|
if (useOperationByRefId) {
|
|
3382
3392
|
const item = ref.tmpItems[field];
|
|
@@ -3392,6 +3402,20 @@
|
|
|
3392
3402
|
operation = exports.OPERATION.ADD_BY_REFID;
|
|
3393
3403
|
}
|
|
3394
3404
|
}
|
|
3405
|
+
else if (operation === exports.OPERATION.DELETE && isSchemaChild) {
|
|
3406
|
+
//
|
|
3407
|
+
// DELETE by identity: idempotent, so a stale positional DELETE
|
|
3408
|
+
// (pending in the shared queue when a client bootstraps via
|
|
3409
|
+
// encodeAll) can't corrupt that client — its snapshot no longer
|
|
3410
|
+
// holds the item, the refId is unknown, and the decoder skips.
|
|
3411
|
+
//
|
|
3412
|
+
const item = ref.tmpItems[field];
|
|
3413
|
+
if (!item) {
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
refOrIndex = item[$refId];
|
|
3417
|
+
operation = exports.OPERATION.DELETE_BY_REFID;
|
|
3418
|
+
}
|
|
3395
3419
|
else {
|
|
3396
3420
|
refOrIndex = field;
|
|
3397
3421
|
}
|
|
@@ -3783,7 +3807,10 @@
|
|
|
3783
3807
|
tgt.$items.set(dynamicIndex, value);
|
|
3784
3808
|
break;
|
|
3785
3809
|
case CollectionKind.Array:
|
|
3786
|
-
|
|
3810
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3811
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === exports.OPERATION.ADD)
|
|
3812
|
+
? exports.OPERATION.REPLACE
|
|
3813
|
+
: operation);
|
|
3787
3814
|
break;
|
|
3788
3815
|
// SetSchema / CollectionSchema / StreamSchema — use the wire-
|
|
3789
3816
|
// index we decoded above so server/client `$items` stay in sync
|
|
@@ -3850,9 +3877,13 @@
|
|
|
3850
3877
|
return;
|
|
3851
3878
|
}
|
|
3852
3879
|
else if (operation === exports.OPERATION.DELETE_BY_REFID) {
|
|
3853
|
-
// TODO: refactor here, try to follow same flow as below
|
|
3854
3880
|
const refId = decode.number(bytes, it);
|
|
3855
3881
|
const previousValue = decoder.root.refs.get(refId);
|
|
3882
|
+
// Stale DELETE — refId unknown to this decoder (e.g. it
|
|
3883
|
+
// bootstrapped via encodeAll after the item was already removed).
|
|
3884
|
+
if (previousValue === undefined) {
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3856
3887
|
// Decrement the removed child's ref-count so it can be garbage
|
|
3857
3888
|
// collected — this refId-based branch returns early and never reaches
|
|
3858
3889
|
// decodeValue(), so it must do the same DELETE bookkeeping itself.
|
|
@@ -3860,10 +3891,13 @@
|
|
|
3860
3891
|
// different type → "field not defined" / "definition mismatch"
|
|
3861
3892
|
// (surfaces under StateView when a filtered ArraySchema element is
|
|
3862
3893
|
// spliced while its parent moves through view membership churn).
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
}
|
|
3894
|
+
// Must run even when the item is absent from THIS array (view churn).
|
|
3895
|
+
decoder.root.removeRef(refId);
|
|
3866
3896
|
index = tgt.findIndex((value) => value === previousValue);
|
|
3897
|
+
// Item not present in this decoder's array — nothing to remove locally.
|
|
3898
|
+
if (index === -1) {
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3867
3901
|
tgt[$deleteByIndex](index);
|
|
3868
3902
|
allChanges?.push({
|
|
3869
3903
|
ref,
|
|
@@ -3904,9 +3938,12 @@
|
|
|
3904
3938
|
resyncTouchEntry(decoder, ref, operation, index, previousValue, value, allChanges);
|
|
3905
3939
|
}
|
|
3906
3940
|
if (value !== null && value !== undefined &&
|
|
3907
|
-
value !== previousValue // avoid setting same value twice (
|
|
3941
|
+
value !== previousValue // avoid setting same value twice (an ADD at an occupied index would splice-insert)
|
|
3908
3942
|
) {
|
|
3909
|
-
|
|
3943
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3944
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === exports.OPERATION.ADD)
|
|
3945
|
+
? exports.OPERATION.REPLACE
|
|
3946
|
+
: operation);
|
|
3910
3947
|
}
|
|
3911
3948
|
// add change
|
|
3912
3949
|
if (previousValue !== value) {
|
|
@@ -3963,30 +4000,35 @@
|
|
|
3963
4000
|
obj.$deleteAt(key);
|
|
3964
4001
|
}
|
|
3965
4002
|
else {
|
|
4003
|
+
// wire slot the write was recorded at; undefined = nothing
|
|
4004
|
+
// recorded (same value / skipped) — must NOT touch tmpItems
|
|
4005
|
+
// then, or a same-tick shifted layout gets clobbered.
|
|
4006
|
+
let wireIndex;
|
|
3966
4007
|
if (setValue[$changes]) {
|
|
3967
4008
|
assertInstanceType(setValue, obj[$childType], obj, key);
|
|
3968
4009
|
const previousValue = obj.items[key];
|
|
3969
4010
|
if (!obj.isMovingItems) {
|
|
3970
|
-
obj.$changeAt(Number(key), setValue);
|
|
4011
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3971
4012
|
}
|
|
3972
4013
|
else {
|
|
4014
|
+
wireIndex = obj.$wireIndex(Number(key));
|
|
3973
4015
|
if (previousValue !== undefined) {
|
|
3974
4016
|
if (setValue[$changes].isNew) {
|
|
3975
|
-
obj[$changes].indexedOperation(
|
|
4017
|
+
obj[$changes].indexedOperation(wireIndex, exports.OPERATION.MOVE_AND_ADD);
|
|
3976
4018
|
}
|
|
3977
4019
|
else {
|
|
3978
|
-
if ((obj[$changes].getChange(
|
|
3979
|
-
obj[$changes].indexedOperation(
|
|
4020
|
+
if ((obj[$changes].getChange(wireIndex) & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
|
|
4021
|
+
obj[$changes].indexedOperation(wireIndex, exports.OPERATION.DELETE_AND_MOVE);
|
|
3980
4022
|
}
|
|
3981
4023
|
else {
|
|
3982
|
-
obj[$changes].indexedOperation(
|
|
4024
|
+
obj[$changes].indexedOperation(wireIndex, exports.OPERATION.MOVE);
|
|
3983
4025
|
}
|
|
3984
4026
|
}
|
|
3985
4027
|
}
|
|
3986
4028
|
else if (setValue[$changes].isNew) {
|
|
3987
|
-
obj[$changes].indexedOperation(
|
|
4029
|
+
obj[$changes].indexedOperation(wireIndex, exports.OPERATION.ADD);
|
|
3988
4030
|
}
|
|
3989
|
-
setValue[$changes].setParent(obj, obj[$changes].root,
|
|
4031
|
+
setValue[$changes].setParent(obj, obj[$changes].root, wireIndex);
|
|
3990
4032
|
}
|
|
3991
4033
|
if (previousValue !== undefined) {
|
|
3992
4034
|
// remove root reference from previous value
|
|
@@ -3994,10 +4036,12 @@
|
|
|
3994
4036
|
}
|
|
3995
4037
|
}
|
|
3996
4038
|
else {
|
|
3997
|
-
obj.$changeAt(Number(key), setValue);
|
|
4039
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3998
4040
|
}
|
|
3999
4041
|
obj.items[key] = setValue;
|
|
4000
|
-
|
|
4042
|
+
if (wireIndex !== undefined) {
|
|
4043
|
+
obj.tmpItems[wireIndex] = setValue;
|
|
4044
|
+
}
|
|
4001
4045
|
}
|
|
4002
4046
|
return true;
|
|
4003
4047
|
}
|
|
@@ -4180,40 +4224,68 @@
|
|
|
4180
4224
|
index += this.length;
|
|
4181
4225
|
return this.items[index];
|
|
4182
4226
|
}
|
|
4183
|
-
|
|
4227
|
+
/**
|
|
4228
|
+
* items-index → wire (tmpItems) index. Identity while no deletions are
|
|
4229
|
+
* staged this tick; otherwise maps to the index-th live (non-deleted)
|
|
4230
|
+
* tmpItems slot — the same live-index walk `splice()` uses. Without the
|
|
4231
|
+
* translation, index writes recorded after a same-tick `shift()`/`splice()`
|
|
4232
|
+
* land on the wrong wire slots.
|
|
4233
|
+
*/
|
|
4234
|
+
$wireIndex(index) {
|
|
4235
|
+
const deletedIndexes = this.deletedIndexes;
|
|
4236
|
+
if (deletedIndexes.length === 0) {
|
|
4237
|
+
return index;
|
|
4238
|
+
}
|
|
4239
|
+
const tmpItems = this.tmpItems;
|
|
4240
|
+
let live = 0;
|
|
4241
|
+
for (let i = 0; i < tmpItems.length; i++) {
|
|
4242
|
+
if (deletedIndexes[i] !== true) {
|
|
4243
|
+
if (live === index) {
|
|
4244
|
+
return i;
|
|
4245
|
+
}
|
|
4246
|
+
live++;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
// beyond the live range: appends land after the staged tmpItems tail
|
|
4250
|
+
return tmpItems.length + (index - live);
|
|
4251
|
+
}
|
|
4252
|
+
// encoding only. Returns the wire index the change was recorded at
|
|
4253
|
+
// (undefined when nothing was recorded).
|
|
4184
4254
|
$changeAt(index, value) {
|
|
4185
4255
|
if (value === undefined || value === null) {
|
|
4186
|
-
console.error("ArraySchema items cannot be null nor undefined; Use `
|
|
4187
|
-
return;
|
|
4256
|
+
console.error("ArraySchema items cannot be null nor undefined; Use `splice(index, 1)` instead.");
|
|
4257
|
+
return undefined;
|
|
4188
4258
|
}
|
|
4189
4259
|
// skip if the value is the same as cached.
|
|
4190
4260
|
if (this.items[index] === value) {
|
|
4191
|
-
return;
|
|
4261
|
+
return undefined;
|
|
4192
4262
|
}
|
|
4193
4263
|
const operation = (this.items[index] !== undefined)
|
|
4194
4264
|
? typeof (value) === "object"
|
|
4195
4265
|
? exports.OPERATION.DELETE_AND_ADD // schema child
|
|
4196
4266
|
: exports.OPERATION.REPLACE // primitive
|
|
4197
4267
|
: exports.OPERATION.ADD;
|
|
4268
|
+
const wireIndex = this.$wireIndex(index);
|
|
4198
4269
|
const changeTree = this[$changes];
|
|
4199
|
-
changeTree.change(
|
|
4270
|
+
changeTree.change(wireIndex, operation);
|
|
4200
4271
|
//
|
|
4201
4272
|
// set value's parent after the value is set
|
|
4202
4273
|
// (to avoid encoding "refId" operations before parent's "ADD" operation)
|
|
4203
4274
|
//
|
|
4204
|
-
value[$changes]?.setParent(this, changeTree.root,
|
|
4275
|
+
value[$changes]?.setParent(this, changeTree.root, wireIndex);
|
|
4276
|
+
return wireIndex;
|
|
4205
4277
|
}
|
|
4206
4278
|
// encoding only
|
|
4207
4279
|
$deleteAt(index, operation) {
|
|
4208
|
-
this[$changes].delete(index, operation);
|
|
4280
|
+
this[$changes].delete(this.$wireIndex(index), operation);
|
|
4209
4281
|
}
|
|
4210
4282
|
// decoding only
|
|
4211
4283
|
$setAt(index, value, operation) {
|
|
4212
|
-
if (
|
|
4213
|
-
operation === exports.OPERATION.ADD &&
|
|
4284
|
+
if (operation === exports.OPERATION.ADD &&
|
|
4214
4285
|
this.items[index] !== undefined) {
|
|
4215
|
-
//
|
|
4216
|
-
|
|
4286
|
+
// ADD at an occupied index = insert (unshift / splice-insert):
|
|
4287
|
+
// shift existing items up instead of overwriting.
|
|
4288
|
+
this.items.splice(index, 0, value);
|
|
4217
4289
|
}
|
|
4218
4290
|
else if (operation === exports.OPERATION.DELETE_AND_MOVE) {
|
|
4219
4291
|
this.items.splice(index, 1);
|
|
@@ -4300,10 +4372,16 @@
|
|
|
4300
4372
|
return undefined;
|
|
4301
4373
|
}
|
|
4302
4374
|
const changeTree = self[$changes];
|
|
4303
|
-
|
|
4304
|
-
|
|
4375
|
+
// items[0] ≡ first live (non-deleted) tmpItems slot. Value-based
|
|
4376
|
+
// findIndex is unsafe here: same-tick index writes can duplicate a
|
|
4377
|
+
// value across tmp slots and resolve the wrong one.
|
|
4378
|
+
const deletedIndexes = self.deletedIndexes;
|
|
4379
|
+
let index = 0;
|
|
4380
|
+
while (deletedIndexes[index] === true) {
|
|
4381
|
+
index++;
|
|
4382
|
+
}
|
|
4305
4383
|
changeTree.delete(index, exports.OPERATION.DELETE);
|
|
4306
|
-
|
|
4384
|
+
deletedIndexes[index] = true;
|
|
4307
4385
|
return items.shift();
|
|
4308
4386
|
}
|
|
4309
4387
|
/**
|
|
@@ -4401,13 +4479,18 @@
|
|
|
4401
4479
|
unshift(...items) {
|
|
4402
4480
|
const self = this[$proxyTarget];
|
|
4403
4481
|
const changeTree = self[$changes];
|
|
4404
|
-
//
|
|
4405
|
-
//
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
items.
|
|
4409
|
-
changeTree.
|
|
4410
|
-
}
|
|
4482
|
+
// single recorder op: shifts pending indexes up and records the new
|
|
4483
|
+
// ADDs lowest-first (the decoder splice-inserts in ascending order).
|
|
4484
|
+
changeTree.unshift(items.length);
|
|
4485
|
+
// attach ref-type items — parent set AFTER recording, as in $changeAt
|
|
4486
|
+
for (let i = 0; i < items.length; i++) {
|
|
4487
|
+
items[i]?.[$changes]?.setParent(this, changeTree.root, i);
|
|
4488
|
+
}
|
|
4489
|
+
// keep staged-delete flags aligned with the prepended tmp slots
|
|
4490
|
+
const deletedIndexes = self.deletedIndexes;
|
|
4491
|
+
if (deletedIndexes.length > 0) {
|
|
4492
|
+
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
4493
|
+
}
|
|
4411
4494
|
self.tmpItems.unshift(...items);
|
|
4412
4495
|
return self.items.unshift(...items);
|
|
4413
4496
|
}
|
|
@@ -5026,6 +5109,37 @@
|
|
|
5026
5109
|
get(key) {
|
|
5027
5110
|
return this.$items.get(key);
|
|
5028
5111
|
}
|
|
5112
|
+
/**
|
|
5113
|
+
* Returns the value for `key` if present. Otherwise inserts `defaultValue`
|
|
5114
|
+
* (tracked as an ADD change, like `set()`) and returns it.
|
|
5115
|
+
*
|
|
5116
|
+
* Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
|
|
5117
|
+
* TypeScript 6's standard library).
|
|
5118
|
+
*/
|
|
5119
|
+
getOrInsert(key, defaultValue) {
|
|
5120
|
+
if (this.$items.has(key)) {
|
|
5121
|
+
return this.$items.get(key);
|
|
5122
|
+
}
|
|
5123
|
+
this.set(key, defaultValue);
|
|
5124
|
+
return defaultValue;
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* Returns the value for `key` if present. Otherwise computes a value via
|
|
5128
|
+
* `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
|
|
5129
|
+
* and returns it. The callback is only invoked when the key is missing.
|
|
5130
|
+
*
|
|
5131
|
+
* Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
|
|
5132
|
+
* typed in TypeScript 6's standard library).
|
|
5133
|
+
*/
|
|
5134
|
+
getOrInsertComputed(key, callbackfn) {
|
|
5135
|
+
if (this.$items.has(key)) {
|
|
5136
|
+
return this.$items.get(key);
|
|
5137
|
+
}
|
|
5138
|
+
const value = callbackfn(key);
|
|
5139
|
+
// per spec: overwrites even if callbackfn itself inserted `key`
|
|
5140
|
+
this.set(key, value);
|
|
5141
|
+
return value;
|
|
5142
|
+
}
|
|
5029
5143
|
delete(key) {
|
|
5030
5144
|
if (!this.$items.has(key)) {
|
|
5031
5145
|
return false;
|
|
@@ -7894,17 +8008,7 @@
|
|
|
7894
8008
|
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
7895
8009
|
// nothing itself but we still recurse so descendants (possibly added to
|
|
7896
8010
|
// the view explicitly) are reachable.
|
|
7897
|
-
|
|
7898
|
-
if (ctx.hasView) {
|
|
7899
|
-
const view = ctx.view;
|
|
7900
|
-
if (!view.isChangeTreeVisible(changeTree)) {
|
|
7901
|
-
view.markInvisible(changeTree);
|
|
7902
|
-
visibleHere = false;
|
|
7903
|
-
}
|
|
7904
|
-
else {
|
|
7905
|
-
view.unmarkInvisible(changeTree);
|
|
7906
|
-
}
|
|
7907
|
-
}
|
|
8011
|
+
const visibleHere = !ctx.hasView || ctx.view.isChangeTreeVisible(changeTree);
|
|
7908
8012
|
if (visibleHere) {
|
|
7909
8013
|
const desc = changeTree.encDescriptor;
|
|
7910
8014
|
ctx.changeTree = changeTree;
|
|
@@ -8053,12 +8157,8 @@
|
|
|
8053
8157
|
let current = queue;
|
|
8054
8158
|
while (current = current.next) {
|
|
8055
8159
|
const changeTree = current.changeTree;
|
|
8056
|
-
if (hasView) {
|
|
8057
|
-
|
|
8058
|
-
view.markInvisible(changeTree);
|
|
8059
|
-
continue;
|
|
8060
|
-
}
|
|
8061
|
-
view.unmarkInvisible(changeTree);
|
|
8160
|
+
if (hasView && !view.isChangeTreeVisible(changeTree)) {
|
|
8161
|
+
continue;
|
|
8062
8162
|
}
|
|
8063
8163
|
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
8064
8164
|
if (!recorder || !recorder.has()) {
|
|
@@ -9750,9 +9850,6 @@
|
|
|
9750
9850
|
const v = tree.visibleViews;
|
|
9751
9851
|
if (v !== undefined && slot < v.length)
|
|
9752
9852
|
v[slot] &= clearMask;
|
|
9753
|
-
const i = tree.invisibleViews;
|
|
9754
|
-
if (i !== undefined && slot < i.length)
|
|
9755
|
-
i[slot] &= clearMask;
|
|
9756
9853
|
const s = tree.subscribedViews;
|
|
9757
9854
|
if (s !== undefined && slot < s.length)
|
|
9758
9855
|
s[slot] &= clearMask;
|
|
@@ -9904,32 +10001,6 @@
|
|
|
9904
10001
|
if (slot < arr.length)
|
|
9905
10002
|
arr[slot] &= ~this._bit;
|
|
9906
10003
|
}
|
|
9907
|
-
/** True iff this view has previously marked `tree` as invisible. */
|
|
9908
|
-
isInvisible(tree) {
|
|
9909
|
-
const arr = tree.invisibleViews;
|
|
9910
|
-
const slot = this._slot;
|
|
9911
|
-
return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
|
|
9912
|
-
}
|
|
9913
|
-
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
9914
|
-
markInvisible(tree) {
|
|
9915
|
-
const slot = this._slot;
|
|
9916
|
-
let arr = tree.invisibleViews;
|
|
9917
|
-
if (arr === undefined) {
|
|
9918
|
-
arr = tree.invisibleViews = [];
|
|
9919
|
-
}
|
|
9920
|
-
while (arr.length <= slot)
|
|
9921
|
-
arr.push(0);
|
|
9922
|
-
arr[slot] |= this._bit;
|
|
9923
|
-
}
|
|
9924
|
-
/** Clear invisible bit. */
|
|
9925
|
-
unmarkInvisible(tree) {
|
|
9926
|
-
const arr = tree.invisibleViews;
|
|
9927
|
-
if (arr === undefined)
|
|
9928
|
-
return;
|
|
9929
|
-
const slot = this._slot;
|
|
9930
|
-
if (slot < arr.length)
|
|
9931
|
-
arr[slot] &= ~this._bit;
|
|
9932
|
-
}
|
|
9933
10004
|
// ──────────────────────────────────────────────────────────────────
|
|
9934
10005
|
// Per-tag, per-view bitmap. Replaces the legacy
|
|
9935
10006
|
// `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
|
|
@@ -10073,11 +10144,18 @@
|
|
|
10073
10144
|
// below use `metadata?.[...]` null-safe access. Only Schema
|
|
10074
10145
|
// subclasses yield a real Metadata object.
|
|
10075
10146
|
const metadata = obj.constructor[Symbol.metadata];
|
|
10076
|
-
|
|
10077
|
-
//
|
|
10078
|
-
|
|
10147
|
+
// Add to iterable list (only the explicitly added items), deduping
|
|
10148
|
+
// re-adds of an already-visible instance. isVisible must be read
|
|
10149
|
+
// BEFORE markVisible; indexOf runs only on the re-add path.
|
|
10150
|
+
// NOTE: dedup applies to `items` only — a re-add still re-queues the
|
|
10151
|
+
// full snapshot on purpose (shared-view bootstrap re-add: a
|
|
10152
|
+
// late-attached client may not have consumed earlier drains).
|
|
10153
|
+
// Callers wanting cheap idempotence can guard with `view.has(obj)`.
|
|
10154
|
+
if (this.iterable && checkIncludeParent
|
|
10155
|
+
&& (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
|
|
10079
10156
|
this.items.push(obj);
|
|
10080
10157
|
}
|
|
10158
|
+
this.markVisible(changeTree);
|
|
10081
10159
|
// add parent ChangeTree's
|
|
10082
10160
|
// - if it was invisible to this view
|
|
10083
10161
|
// - if it were previously filtered out
|
|
@@ -10160,18 +10238,20 @@
|
|
|
10160
10238
|
}
|
|
10161
10239
|
else if (!changeTree.isNew || isChildAdded) {
|
|
10162
10240
|
// new structures will be added as part of .encode() call, no need to force it to .encodeView()
|
|
10163
|
-
const isInvisible = this.isInvisible(changeTree);
|
|
10164
10241
|
// Full-sync snapshot: walk the live ref structurally instead of
|
|
10165
10242
|
// iterating a cumulative recorder bucket. Every populated index
|
|
10166
10243
|
// is emitted as ADD (matching the op-coercion previously done
|
|
10167
10244
|
// at encode time). Per-field tags come from the descriptor's
|
|
10168
10245
|
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
10169
10246
|
// object hop.
|
|
10247
|
+
//
|
|
10248
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
10249
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
10250
|
+
// so anything added leaks straight to the wire.
|
|
10170
10251
|
const tags = changeTree.encDescriptor.tags;
|
|
10171
10252
|
changeTree.forEachLive((index) => {
|
|
10172
10253
|
const tagAtIndex = tags[index];
|
|
10173
|
-
if (
|
|
10174
|
-
tagAtIndex === undefined || // "all change" with no tag
|
|
10254
|
+
if (tagAtIndex === undefined || // "all change" with no tag
|
|
10175
10255
|
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
10176
10256
|
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
10177
10257
|
) {
|
|
@@ -10323,7 +10403,7 @@
|
|
|
10323
10403
|
}
|
|
10324
10404
|
// ── Streamable-collection unsubscribe (the stream itself) ─────
|
|
10325
10405
|
// Flush DELETE for every sent position and drop pending. After
|
|
10326
|
-
// this, the stream is
|
|
10406
|
+
// this, the stream is no longer visible to this view — any future
|
|
10327
10407
|
// `stream.add()` would still seed broadcast pending (if no views)
|
|
10328
10408
|
// but would NOT re-seed per-view pending (user must re-subscribe).
|
|
10329
10409
|
if (changeTree.isStreamCollection) {
|