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