@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.mjs
CHANGED
|
@@ -2592,7 +2592,6 @@ class ChangeTree {
|
|
|
2592
2592
|
// per-view WeakSet lookups with direct bitwise ops.
|
|
2593
2593
|
// Lazy: undefined until the tree participates in any view.
|
|
2594
2594
|
visibleViews;
|
|
2595
|
-
invisibleViews;
|
|
2596
2595
|
// Per-(view, tag) bitmap, indexed by tag. Custom tags only —
|
|
2597
2596
|
// DEFAULT_VIEW_TAG visibility lives in `visibleViews`.
|
|
2598
2597
|
tagViews;
|
|
@@ -2944,18 +2943,34 @@ class ChangeTree {
|
|
|
2944
2943
|
// per-view visibility lives on the tree (NOT keyed by refId), so a
|
|
2945
2944
|
// recycled tree must not inherit its previous life's view membership.
|
|
2946
2945
|
this.visibleViews = undefined;
|
|
2947
|
-
this.invisibleViews = undefined;
|
|
2948
2946
|
this.tagViews = undefined;
|
|
2949
2947
|
this.subscribedViews = undefined;
|
|
2950
2948
|
}
|
|
2951
|
-
|
|
2949
|
+
/**
|
|
2950
|
+
* ArraySchema#unshift(): re-key pending ops on both channels by
|
|
2951
|
+
* `+count`, then record ADDs for the new items at indexes 0..count-1.
|
|
2952
|
+
*
|
|
2953
|
+
* The rebuilt map's insertion order IS the wire order: new ADDs first
|
|
2954
|
+
* (ascending — the decoder splice-inserts each one, which only works
|
|
2955
|
+
* lowest-index-first), then prior ops in their original relative order
|
|
2956
|
+
* at their shifted positions. See ArraySchema#$setAt.
|
|
2957
|
+
*/
|
|
2958
|
+
unshift(count) {
|
|
2952
2959
|
if (this._isSchema)
|
|
2953
|
-
throw new Error("ChangeTree (Schema):
|
|
2960
|
+
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2954
2961
|
const src = this.collDirty;
|
|
2955
2962
|
const dst = new Map();
|
|
2963
|
+
const track = !this.paused && !this.isStatic;
|
|
2964
|
+
if (track) {
|
|
2965
|
+
for (let i = 0; i < count; i++)
|
|
2966
|
+
dst.set(i, OPERATION.ADD);
|
|
2967
|
+
}
|
|
2956
2968
|
for (const [idx, val] of src)
|
|
2957
|
-
dst.set(idx +
|
|
2969
|
+
dst.set(idx + count, val);
|
|
2958
2970
|
this.collDirty = dst;
|
|
2971
|
+
this.unreliableRecorder?.shift(count);
|
|
2972
|
+
if (track)
|
|
2973
|
+
this.root?.enqueueChangeTree(this);
|
|
2959
2974
|
}
|
|
2960
2975
|
// Tree attachment + child iteration — see ./changeTree/treeAttachment.ts.
|
|
2961
2976
|
setRoot(root) { setRoot(this, root); }
|
|
@@ -3024,12 +3039,6 @@ class ChangeTree {
|
|
|
3024
3039
|
indexedOperation(index, operation) {
|
|
3025
3040
|
this._routeAndRecord(index, operation, true);
|
|
3026
3041
|
}
|
|
3027
|
-
// ArraySchema#unshift(): apply shift to both channels.
|
|
3028
|
-
// Unreliable recorder on an array is always a CollectionChangeRecorder.
|
|
3029
|
-
shiftChangeIndexes(shiftIndex) {
|
|
3030
|
-
this.shift(shiftIndex);
|
|
3031
|
-
this.unreliableRecorder?.shift(shiftIndex);
|
|
3032
|
-
}
|
|
3033
3042
|
getChange(index) {
|
|
3034
3043
|
return this.operationAt(index);
|
|
3035
3044
|
}
|
|
@@ -3370,7 +3379,8 @@ const encodeArray = function (encoder, bytes, changeTree, field, operation, it,
|
|
|
3370
3379
|
// ArraySchema stores its per-instance child type at `$childType`.
|
|
3371
3380
|
// This encoder is array-only — there's no Schema fallback to consider.
|
|
3372
3381
|
const type = ref[$childType];
|
|
3373
|
-
const
|
|
3382
|
+
const isSchemaChild = typeof type !== "string";
|
|
3383
|
+
const useOperationByRefId = hasView && changeTree.isFiltered && isSchemaChild;
|
|
3374
3384
|
let refOrIndex;
|
|
3375
3385
|
if (useOperationByRefId) {
|
|
3376
3386
|
const item = ref.tmpItems[field];
|
|
@@ -3386,6 +3396,20 @@ const encodeArray = function (encoder, bytes, changeTree, field, operation, it,
|
|
|
3386
3396
|
operation = OPERATION.ADD_BY_REFID;
|
|
3387
3397
|
}
|
|
3388
3398
|
}
|
|
3399
|
+
else if (operation === OPERATION.DELETE && isSchemaChild) {
|
|
3400
|
+
//
|
|
3401
|
+
// DELETE by identity: idempotent, so a stale positional DELETE
|
|
3402
|
+
// (pending in the shared queue when a client bootstraps via
|
|
3403
|
+
// encodeAll) can't corrupt that client — its snapshot no longer
|
|
3404
|
+
// holds the item, the refId is unknown, and the decoder skips.
|
|
3405
|
+
//
|
|
3406
|
+
const item = ref.tmpItems[field];
|
|
3407
|
+
if (!item) {
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
refOrIndex = item[$refId];
|
|
3411
|
+
operation = OPERATION.DELETE_BY_REFID;
|
|
3412
|
+
}
|
|
3389
3413
|
else {
|
|
3390
3414
|
refOrIndex = field;
|
|
3391
3415
|
}
|
|
@@ -3777,7 +3801,10 @@ const decodeKeyValueOperation = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3777
3801
|
tgt.$items.set(dynamicIndex, value);
|
|
3778
3802
|
break;
|
|
3779
3803
|
case CollectionKind.Array:
|
|
3780
|
-
|
|
3804
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3805
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
3806
|
+
? OPERATION.REPLACE
|
|
3807
|
+
: operation);
|
|
3781
3808
|
break;
|
|
3782
3809
|
// SetSchema / CollectionSchema / StreamSchema — use the wire-
|
|
3783
3810
|
// index we decoded above so server/client `$items` stay in sync
|
|
@@ -3844,9 +3871,13 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3844
3871
|
return;
|
|
3845
3872
|
}
|
|
3846
3873
|
else if (operation === OPERATION.DELETE_BY_REFID) {
|
|
3847
|
-
// TODO: refactor here, try to follow same flow as below
|
|
3848
3874
|
const refId = decode.number(bytes, it);
|
|
3849
3875
|
const previousValue = decoder.root.refs.get(refId);
|
|
3876
|
+
// Stale DELETE — refId unknown to this decoder (e.g. it
|
|
3877
|
+
// bootstrapped via encodeAll after the item was already removed).
|
|
3878
|
+
if (previousValue === undefined) {
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3850
3881
|
// Decrement the removed child's ref-count so it can be garbage
|
|
3851
3882
|
// collected — this refId-based branch returns early and never reaches
|
|
3852
3883
|
// decodeValue(), so it must do the same DELETE bookkeeping itself.
|
|
@@ -3854,10 +3885,13 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3854
3885
|
// different type → "field not defined" / "definition mismatch"
|
|
3855
3886
|
// (surfaces under StateView when a filtered ArraySchema element is
|
|
3856
3887
|
// spliced while its parent moves through view membership churn).
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
}
|
|
3888
|
+
// Must run even when the item is absent from THIS array (view churn).
|
|
3889
|
+
decoder.root.removeRef(refId);
|
|
3860
3890
|
index = tgt.findIndex((value) => value === previousValue);
|
|
3891
|
+
// Item not present in this decoder's array — nothing to remove locally.
|
|
3892
|
+
if (index === -1) {
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3861
3895
|
tgt[$deleteByIndex](index);
|
|
3862
3896
|
allChanges?.push({
|
|
3863
3897
|
ref,
|
|
@@ -3898,9 +3932,12 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3898
3932
|
resyncTouchEntry(decoder, ref, operation, index, previousValue, value, allChanges);
|
|
3899
3933
|
}
|
|
3900
3934
|
if (value !== null && value !== undefined &&
|
|
3901
|
-
value !== previousValue // avoid setting same value twice (
|
|
3935
|
+
value !== previousValue // avoid setting same value twice (an ADD at an occupied index would splice-insert)
|
|
3902
3936
|
) {
|
|
3903
|
-
|
|
3937
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3938
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
3939
|
+
? OPERATION.REPLACE
|
|
3940
|
+
: operation);
|
|
3904
3941
|
}
|
|
3905
3942
|
// add change
|
|
3906
3943
|
if (previousValue !== value) {
|
|
@@ -3957,30 +3994,35 @@ const ARRAY_PROXY_HANDLER = {
|
|
|
3957
3994
|
obj.$deleteAt(key);
|
|
3958
3995
|
}
|
|
3959
3996
|
else {
|
|
3997
|
+
// wire slot the write was recorded at; undefined = nothing
|
|
3998
|
+
// recorded (same value / skipped) — must NOT touch tmpItems
|
|
3999
|
+
// then, or a same-tick shifted layout gets clobbered.
|
|
4000
|
+
let wireIndex;
|
|
3960
4001
|
if (setValue[$changes]) {
|
|
3961
4002
|
assertInstanceType(setValue, obj[$childType], obj, key);
|
|
3962
4003
|
const previousValue = obj.items[key];
|
|
3963
4004
|
if (!obj.isMovingItems) {
|
|
3964
|
-
obj.$changeAt(Number(key), setValue);
|
|
4005
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3965
4006
|
}
|
|
3966
4007
|
else {
|
|
4008
|
+
wireIndex = obj.$wireIndex(Number(key));
|
|
3967
4009
|
if (previousValue !== undefined) {
|
|
3968
4010
|
if (setValue[$changes].isNew) {
|
|
3969
|
-
obj[$changes].indexedOperation(
|
|
4011
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE_AND_ADD);
|
|
3970
4012
|
}
|
|
3971
4013
|
else {
|
|
3972
|
-
if ((obj[$changes].getChange(
|
|
3973
|
-
obj[$changes].indexedOperation(
|
|
4014
|
+
if ((obj[$changes].getChange(wireIndex) & OPERATION.DELETE) === OPERATION.DELETE) {
|
|
4015
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.DELETE_AND_MOVE);
|
|
3974
4016
|
}
|
|
3975
4017
|
else {
|
|
3976
|
-
obj[$changes].indexedOperation(
|
|
4018
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE);
|
|
3977
4019
|
}
|
|
3978
4020
|
}
|
|
3979
4021
|
}
|
|
3980
4022
|
else if (setValue[$changes].isNew) {
|
|
3981
|
-
obj[$changes].indexedOperation(
|
|
4023
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.ADD);
|
|
3982
4024
|
}
|
|
3983
|
-
setValue[$changes].setParent(obj, obj[$changes].root,
|
|
4025
|
+
setValue[$changes].setParent(obj, obj[$changes].root, wireIndex);
|
|
3984
4026
|
}
|
|
3985
4027
|
if (previousValue !== undefined) {
|
|
3986
4028
|
// remove root reference from previous value
|
|
@@ -3988,10 +4030,12 @@ const ARRAY_PROXY_HANDLER = {
|
|
|
3988
4030
|
}
|
|
3989
4031
|
}
|
|
3990
4032
|
else {
|
|
3991
|
-
obj.$changeAt(Number(key), setValue);
|
|
4033
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3992
4034
|
}
|
|
3993
4035
|
obj.items[key] = setValue;
|
|
3994
|
-
|
|
4036
|
+
if (wireIndex !== undefined) {
|
|
4037
|
+
obj.tmpItems[wireIndex] = setValue;
|
|
4038
|
+
}
|
|
3995
4039
|
}
|
|
3996
4040
|
return true;
|
|
3997
4041
|
}
|
|
@@ -4174,40 +4218,68 @@ class ArraySchema {
|
|
|
4174
4218
|
index += this.length;
|
|
4175
4219
|
return this.items[index];
|
|
4176
4220
|
}
|
|
4177
|
-
|
|
4221
|
+
/**
|
|
4222
|
+
* items-index → wire (tmpItems) index. Identity while no deletions are
|
|
4223
|
+
* staged this tick; otherwise maps to the index-th live (non-deleted)
|
|
4224
|
+
* tmpItems slot — the same live-index walk `splice()` uses. Without the
|
|
4225
|
+
* translation, index writes recorded after a same-tick `shift()`/`splice()`
|
|
4226
|
+
* land on the wrong wire slots.
|
|
4227
|
+
*/
|
|
4228
|
+
$wireIndex(index) {
|
|
4229
|
+
const deletedIndexes = this.deletedIndexes;
|
|
4230
|
+
if (deletedIndexes.length === 0) {
|
|
4231
|
+
return index;
|
|
4232
|
+
}
|
|
4233
|
+
const tmpItems = this.tmpItems;
|
|
4234
|
+
let live = 0;
|
|
4235
|
+
for (let i = 0; i < tmpItems.length; i++) {
|
|
4236
|
+
if (deletedIndexes[i] !== true) {
|
|
4237
|
+
if (live === index) {
|
|
4238
|
+
return i;
|
|
4239
|
+
}
|
|
4240
|
+
live++;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
// beyond the live range: appends land after the staged tmpItems tail
|
|
4244
|
+
return tmpItems.length + (index - live);
|
|
4245
|
+
}
|
|
4246
|
+
// encoding only. Returns the wire index the change was recorded at
|
|
4247
|
+
// (undefined when nothing was recorded).
|
|
4178
4248
|
$changeAt(index, value) {
|
|
4179
4249
|
if (value === undefined || value === null) {
|
|
4180
|
-
console.error("ArraySchema items cannot be null nor undefined; Use `
|
|
4181
|
-
return;
|
|
4250
|
+
console.error("ArraySchema items cannot be null nor undefined; Use `splice(index, 1)` instead.");
|
|
4251
|
+
return undefined;
|
|
4182
4252
|
}
|
|
4183
4253
|
// skip if the value is the same as cached.
|
|
4184
4254
|
if (this.items[index] === value) {
|
|
4185
|
-
return;
|
|
4255
|
+
return undefined;
|
|
4186
4256
|
}
|
|
4187
4257
|
const operation = (this.items[index] !== undefined)
|
|
4188
4258
|
? typeof (value) === "object"
|
|
4189
4259
|
? OPERATION.DELETE_AND_ADD // schema child
|
|
4190
4260
|
: OPERATION.REPLACE // primitive
|
|
4191
4261
|
: OPERATION.ADD;
|
|
4262
|
+
const wireIndex = this.$wireIndex(index);
|
|
4192
4263
|
const changeTree = this[$changes];
|
|
4193
|
-
changeTree.change(
|
|
4264
|
+
changeTree.change(wireIndex, operation);
|
|
4194
4265
|
//
|
|
4195
4266
|
// set value's parent after the value is set
|
|
4196
4267
|
// (to avoid encoding "refId" operations before parent's "ADD" operation)
|
|
4197
4268
|
//
|
|
4198
|
-
value[$changes]?.setParent(this, changeTree.root,
|
|
4269
|
+
value[$changes]?.setParent(this, changeTree.root, wireIndex);
|
|
4270
|
+
return wireIndex;
|
|
4199
4271
|
}
|
|
4200
4272
|
// encoding only
|
|
4201
4273
|
$deleteAt(index, operation) {
|
|
4202
|
-
this[$changes].delete(index, operation);
|
|
4274
|
+
this[$changes].delete(this.$wireIndex(index), operation);
|
|
4203
4275
|
}
|
|
4204
4276
|
// decoding only
|
|
4205
4277
|
$setAt(index, value, operation) {
|
|
4206
|
-
if (
|
|
4207
|
-
operation === OPERATION.ADD &&
|
|
4278
|
+
if (operation === OPERATION.ADD &&
|
|
4208
4279
|
this.items[index] !== undefined) {
|
|
4209
|
-
//
|
|
4210
|
-
|
|
4280
|
+
// ADD at an occupied index = insert (unshift / splice-insert):
|
|
4281
|
+
// shift existing items up instead of overwriting.
|
|
4282
|
+
this.items.splice(index, 0, value);
|
|
4211
4283
|
}
|
|
4212
4284
|
else if (operation === OPERATION.DELETE_AND_MOVE) {
|
|
4213
4285
|
this.items.splice(index, 1);
|
|
@@ -4294,10 +4366,16 @@ class ArraySchema {
|
|
|
4294
4366
|
return undefined;
|
|
4295
4367
|
}
|
|
4296
4368
|
const changeTree = self[$changes];
|
|
4297
|
-
|
|
4298
|
-
|
|
4369
|
+
// items[0] ≡ first live (non-deleted) tmpItems slot. Value-based
|
|
4370
|
+
// findIndex is unsafe here: same-tick index writes can duplicate a
|
|
4371
|
+
// value across tmp slots and resolve the wrong one.
|
|
4372
|
+
const deletedIndexes = self.deletedIndexes;
|
|
4373
|
+
let index = 0;
|
|
4374
|
+
while (deletedIndexes[index] === true) {
|
|
4375
|
+
index++;
|
|
4376
|
+
}
|
|
4299
4377
|
changeTree.delete(index, OPERATION.DELETE);
|
|
4300
|
-
|
|
4378
|
+
deletedIndexes[index] = true;
|
|
4301
4379
|
return items.shift();
|
|
4302
4380
|
}
|
|
4303
4381
|
/**
|
|
@@ -4395,13 +4473,18 @@ class ArraySchema {
|
|
|
4395
4473
|
unshift(...items) {
|
|
4396
4474
|
const self = this[$proxyTarget];
|
|
4397
4475
|
const changeTree = self[$changes];
|
|
4398
|
-
//
|
|
4399
|
-
//
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
items.
|
|
4403
|
-
changeTree.
|
|
4404
|
-
}
|
|
4476
|
+
// single recorder op: shifts pending indexes up and records the new
|
|
4477
|
+
// ADDs lowest-first (the decoder splice-inserts in ascending order).
|
|
4478
|
+
changeTree.unshift(items.length);
|
|
4479
|
+
// attach ref-type items — parent set AFTER recording, as in $changeAt
|
|
4480
|
+
for (let i = 0; i < items.length; i++) {
|
|
4481
|
+
items[i]?.[$changes]?.setParent(this, changeTree.root, i);
|
|
4482
|
+
}
|
|
4483
|
+
// keep staged-delete flags aligned with the prepended tmp slots
|
|
4484
|
+
const deletedIndexes = self.deletedIndexes;
|
|
4485
|
+
if (deletedIndexes.length > 0) {
|
|
4486
|
+
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
4487
|
+
}
|
|
4405
4488
|
self.tmpItems.unshift(...items);
|
|
4406
4489
|
return self.items.unshift(...items);
|
|
4407
4490
|
}
|
|
@@ -5020,6 +5103,37 @@ class MapSchema {
|
|
|
5020
5103
|
get(key) {
|
|
5021
5104
|
return this.$items.get(key);
|
|
5022
5105
|
}
|
|
5106
|
+
/**
|
|
5107
|
+
* Returns the value for `key` if present. Otherwise inserts `defaultValue`
|
|
5108
|
+
* (tracked as an ADD change, like `set()`) and returns it.
|
|
5109
|
+
*
|
|
5110
|
+
* Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
|
|
5111
|
+
* TypeScript 6's standard library).
|
|
5112
|
+
*/
|
|
5113
|
+
getOrInsert(key, defaultValue) {
|
|
5114
|
+
if (this.$items.has(key)) {
|
|
5115
|
+
return this.$items.get(key);
|
|
5116
|
+
}
|
|
5117
|
+
this.set(key, defaultValue);
|
|
5118
|
+
return defaultValue;
|
|
5119
|
+
}
|
|
5120
|
+
/**
|
|
5121
|
+
* Returns the value for `key` if present. Otherwise computes a value via
|
|
5122
|
+
* `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
|
|
5123
|
+
* and returns it. The callback is only invoked when the key is missing.
|
|
5124
|
+
*
|
|
5125
|
+
* Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
|
|
5126
|
+
* typed in TypeScript 6's standard library).
|
|
5127
|
+
*/
|
|
5128
|
+
getOrInsertComputed(key, callbackfn) {
|
|
5129
|
+
if (this.$items.has(key)) {
|
|
5130
|
+
return this.$items.get(key);
|
|
5131
|
+
}
|
|
5132
|
+
const value = callbackfn(key);
|
|
5133
|
+
// per spec: overwrites even if callbackfn itself inserted `key`
|
|
5134
|
+
this.set(key, value);
|
|
5135
|
+
return value;
|
|
5136
|
+
}
|
|
5023
5137
|
delete(key) {
|
|
5024
5138
|
if (!this.$items.has(key)) {
|
|
5025
5139
|
return false;
|
|
@@ -7888,17 +8002,7 @@ function _fullSyncWalk(ctx, changeTree) {
|
|
|
7888
8002
|
// Visibility gate: when a view is active, a non-visible tree contributes
|
|
7889
8003
|
// nothing itself but we still recurse so descendants (possibly added to
|
|
7890
8004
|
// the view explicitly) are reachable.
|
|
7891
|
-
|
|
7892
|
-
if (ctx.hasView) {
|
|
7893
|
-
const view = ctx.view;
|
|
7894
|
-
if (!view.isChangeTreeVisible(changeTree)) {
|
|
7895
|
-
view.markInvisible(changeTree);
|
|
7896
|
-
visibleHere = false;
|
|
7897
|
-
}
|
|
7898
|
-
else {
|
|
7899
|
-
view.unmarkInvisible(changeTree);
|
|
7900
|
-
}
|
|
7901
|
-
}
|
|
8005
|
+
const visibleHere = !ctx.hasView || ctx.view.isChangeTreeVisible(changeTree);
|
|
7902
8006
|
if (visibleHere) {
|
|
7903
8007
|
const desc = changeTree.encDescriptor;
|
|
7904
8008
|
ctx.changeTree = changeTree;
|
|
@@ -8047,12 +8151,8 @@ class Encoder {
|
|
|
8047
8151
|
let current = queue;
|
|
8048
8152
|
while (current = current.next) {
|
|
8049
8153
|
const changeTree = current.changeTree;
|
|
8050
|
-
if (hasView) {
|
|
8051
|
-
|
|
8052
|
-
view.markInvisible(changeTree);
|
|
8053
|
-
continue;
|
|
8054
|
-
}
|
|
8055
|
-
view.unmarkInvisible(changeTree);
|
|
8154
|
+
if (hasView && !view.isChangeTreeVisible(changeTree)) {
|
|
8155
|
+
continue;
|
|
8056
8156
|
}
|
|
8057
8157
|
const recorder = unreliable ? changeTree.unreliableRecorder : changeTree;
|
|
8058
8158
|
if (!recorder || !recorder.has()) {
|
|
@@ -9744,9 +9844,6 @@ function _clearViewBitFromAllTrees(root, slot, bit) {
|
|
|
9744
9844
|
const v = tree.visibleViews;
|
|
9745
9845
|
if (v !== undefined && slot < v.length)
|
|
9746
9846
|
v[slot] &= clearMask;
|
|
9747
|
-
const i = tree.invisibleViews;
|
|
9748
|
-
if (i !== undefined && slot < i.length)
|
|
9749
|
-
i[slot] &= clearMask;
|
|
9750
9847
|
const s = tree.subscribedViews;
|
|
9751
9848
|
if (s !== undefined && slot < s.length)
|
|
9752
9849
|
s[slot] &= clearMask;
|
|
@@ -9898,32 +9995,6 @@ class StateView {
|
|
|
9898
9995
|
if (slot < arr.length)
|
|
9899
9996
|
arr[slot] &= ~this._bit;
|
|
9900
9997
|
}
|
|
9901
|
-
/** True iff this view has previously marked `tree` as invisible. */
|
|
9902
|
-
isInvisible(tree) {
|
|
9903
|
-
const arr = tree.invisibleViews;
|
|
9904
|
-
const slot = this._slot;
|
|
9905
|
-
return arr !== undefined && slot < arr.length && (arr[slot] & this._bit) !== 0;
|
|
9906
|
-
}
|
|
9907
|
-
/** Mark `tree` as invisible to this view (used by encode loop). */
|
|
9908
|
-
markInvisible(tree) {
|
|
9909
|
-
const slot = this._slot;
|
|
9910
|
-
let arr = tree.invisibleViews;
|
|
9911
|
-
if (arr === undefined) {
|
|
9912
|
-
arr = tree.invisibleViews = [];
|
|
9913
|
-
}
|
|
9914
|
-
while (arr.length <= slot)
|
|
9915
|
-
arr.push(0);
|
|
9916
|
-
arr[slot] |= this._bit;
|
|
9917
|
-
}
|
|
9918
|
-
/** Clear invisible bit. */
|
|
9919
|
-
unmarkInvisible(tree) {
|
|
9920
|
-
const arr = tree.invisibleViews;
|
|
9921
|
-
if (arr === undefined)
|
|
9922
|
-
return;
|
|
9923
|
-
const slot = this._slot;
|
|
9924
|
-
if (slot < arr.length)
|
|
9925
|
-
arr[slot] &= ~this._bit;
|
|
9926
|
-
}
|
|
9927
9998
|
// ──────────────────────────────────────────────────────────────────
|
|
9928
9999
|
// Per-tag, per-view bitmap. Replaces the legacy
|
|
9929
10000
|
// `tags: WeakMap<ChangeTree, Set<number>>` storage. Hot read site is
|
|
@@ -10067,11 +10138,18 @@ class StateView {
|
|
|
10067
10138
|
// below use `metadata?.[...]` null-safe access. Only Schema
|
|
10068
10139
|
// subclasses yield a real Metadata object.
|
|
10069
10140
|
const metadata = obj.constructor[Symbol.metadata];
|
|
10070
|
-
|
|
10071
|
-
//
|
|
10072
|
-
|
|
10141
|
+
// Add to iterable list (only the explicitly added items), deduping
|
|
10142
|
+
// re-adds of an already-visible instance. isVisible must be read
|
|
10143
|
+
// BEFORE markVisible; indexOf runs only on the re-add path.
|
|
10144
|
+
// NOTE: dedup applies to `items` only — a re-add still re-queues the
|
|
10145
|
+
// full snapshot on purpose (shared-view bootstrap re-add: a
|
|
10146
|
+
// late-attached client may not have consumed earlier drains).
|
|
10147
|
+
// Callers wanting cheap idempotence can guard with `view.has(obj)`.
|
|
10148
|
+
if (this.iterable && checkIncludeParent
|
|
10149
|
+
&& (!this.isVisible(changeTree) || this.items.indexOf(obj) === -1)) {
|
|
10073
10150
|
this.items.push(obj);
|
|
10074
10151
|
}
|
|
10152
|
+
this.markVisible(changeTree);
|
|
10075
10153
|
// add parent ChangeTree's
|
|
10076
10154
|
// - if it was invisible to this view
|
|
10077
10155
|
// - if it were previously filtered out
|
|
@@ -10154,18 +10232,20 @@ class StateView {
|
|
|
10154
10232
|
}
|
|
10155
10233
|
else if (!changeTree.isNew || isChildAdded) {
|
|
10156
10234
|
// new structures will be added as part of .encode() call, no need to force it to .encodeView()
|
|
10157
|
-
const isInvisible = this.isInvisible(changeTree);
|
|
10158
10235
|
// Full-sync snapshot: walk the live ref structurally instead of
|
|
10159
10236
|
// iterating a cumulative recorder bucket. Every populated index
|
|
10160
10237
|
// is emitted as ADD (matching the op-coercion previously done
|
|
10161
10238
|
// at encode time). Per-field tags come from the descriptor's
|
|
10162
10239
|
// precomputed `tags[]` array — direct index vs a metadata[i].tag
|
|
10163
10240
|
// object hop.
|
|
10241
|
+
//
|
|
10242
|
+
// Non-matching custom-tagged fields are NEVER included here —
|
|
10243
|
+
// `view.changes` is drained without a per-field tag re-check,
|
|
10244
|
+
// so anything added leaks straight to the wire.
|
|
10164
10245
|
const tags = changeTree.encDescriptor.tags;
|
|
10165
10246
|
changeTree.forEachLive((index) => {
|
|
10166
10247
|
const tagAtIndex = tags[index];
|
|
10167
|
-
if (
|
|
10168
|
-
tagAtIndex === undefined || // "all change" with no tag
|
|
10248
|
+
if (tagAtIndex === undefined || // "all change" with no tag
|
|
10169
10249
|
tagAtIndex === DEFAULT_VIEW_TAG || // visible to all clients
|
|
10170
10250
|
(tag !== DEFAULT_VIEW_TAG && (tagAtIndex & tag) !== 0) // tag bits overlap
|
|
10171
10251
|
) {
|
|
@@ -10317,7 +10397,7 @@ class StateView {
|
|
|
10317
10397
|
}
|
|
10318
10398
|
// ── Streamable-collection unsubscribe (the stream itself) ─────
|
|
10319
10399
|
// Flush DELETE for every sent position and drop pending. After
|
|
10320
|
-
// this, the stream is
|
|
10400
|
+
// this, the stream is no longer visible to this view — any future
|
|
10321
10401
|
// `stream.add()` would still seed broadcast pending (if no views)
|
|
10322
10402
|
// but would NOT re-seed per-view pending (user must re-subscribe).
|
|
10323
10403
|
if (changeTree.isStreamCollection) {
|