@colyseus/schema 5.0.9 → 5.0.10
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 -2
- package/build/index.cjs +163 -47
- package/build/index.cjs.map +1 -1
- package/build/index.js +163 -47
- package/build/index.mjs +163 -47
- 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 -10
- package/src/encoder/EncodeOperation.ts +14 -1
- package/src/types/custom/ArraySchema.ts +77 -30
- package/src/types/custom/MapSchema.ts +34 -1
package/build/index.mjs
CHANGED
|
@@ -2948,14 +2948,31 @@ class ChangeTree {
|
|
|
2948
2948
|
this.tagViews = undefined;
|
|
2949
2949
|
this.subscribedViews = undefined;
|
|
2950
2950
|
}
|
|
2951
|
-
|
|
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) {
|
|
2952
2961
|
if (this._isSchema)
|
|
2953
|
-
throw new Error("ChangeTree (Schema):
|
|
2962
|
+
throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
2954
2963
|
const src = this.collDirty;
|
|
2955
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, OPERATION.ADD);
|
|
2969
|
+
}
|
|
2956
2970
|
for (const [idx, val] of src)
|
|
2957
|
-
dst.set(idx +
|
|
2971
|
+
dst.set(idx + count, val);
|
|
2958
2972
|
this.collDirty = dst;
|
|
2973
|
+
this.unreliableRecorder?.shift(count);
|
|
2974
|
+
if (track)
|
|
2975
|
+
this.root?.enqueueChangeTree(this);
|
|
2959
2976
|
}
|
|
2960
2977
|
// Tree attachment + child iteration — see ./changeTree/treeAttachment.ts.
|
|
2961
2978
|
setRoot(root) { setRoot(this, root); }
|
|
@@ -3024,12 +3041,6 @@ class ChangeTree {
|
|
|
3024
3041
|
indexedOperation(index, operation) {
|
|
3025
3042
|
this._routeAndRecord(index, operation, true);
|
|
3026
3043
|
}
|
|
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
3044
|
getChange(index) {
|
|
3034
3045
|
return this.operationAt(index);
|
|
3035
3046
|
}
|
|
@@ -3370,7 +3381,8 @@ const encodeArray = function (encoder, bytes, changeTree, field, operation, it,
|
|
|
3370
3381
|
// ArraySchema stores its per-instance child type at `$childType`.
|
|
3371
3382
|
// This encoder is array-only — there's no Schema fallback to consider.
|
|
3372
3383
|
const type = ref[$childType];
|
|
3373
|
-
const
|
|
3384
|
+
const isSchemaChild = typeof type !== "string";
|
|
3385
|
+
const useOperationByRefId = hasView && changeTree.isFiltered && isSchemaChild;
|
|
3374
3386
|
let refOrIndex;
|
|
3375
3387
|
if (useOperationByRefId) {
|
|
3376
3388
|
const item = ref.tmpItems[field];
|
|
@@ -3386,6 +3398,20 @@ const encodeArray = function (encoder, bytes, changeTree, field, operation, it,
|
|
|
3386
3398
|
operation = OPERATION.ADD_BY_REFID;
|
|
3387
3399
|
}
|
|
3388
3400
|
}
|
|
3401
|
+
else if (operation === 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 = OPERATION.DELETE_BY_REFID;
|
|
3414
|
+
}
|
|
3389
3415
|
else {
|
|
3390
3416
|
refOrIndex = field;
|
|
3391
3417
|
}
|
|
@@ -3777,7 +3803,10 @@ const decodeKeyValueOperation = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3777
3803
|
tgt.$items.set(dynamicIndex, value);
|
|
3778
3804
|
break;
|
|
3779
3805
|
case CollectionKind.Array:
|
|
3780
|
-
|
|
3806
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3807
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
3808
|
+
? OPERATION.REPLACE
|
|
3809
|
+
: operation);
|
|
3781
3810
|
break;
|
|
3782
3811
|
// SetSchema / CollectionSchema / StreamSchema — use the wire-
|
|
3783
3812
|
// index we decoded above so server/client `$items` stay in sync
|
|
@@ -3844,9 +3873,13 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3844
3873
|
return;
|
|
3845
3874
|
}
|
|
3846
3875
|
else if (operation === OPERATION.DELETE_BY_REFID) {
|
|
3847
|
-
// TODO: refactor here, try to follow same flow as below
|
|
3848
3876
|
const refId = decode.number(bytes, it);
|
|
3849
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
|
+
}
|
|
3850
3883
|
// Decrement the removed child's ref-count so it can be garbage
|
|
3851
3884
|
// collected — this refId-based branch returns early and never reaches
|
|
3852
3885
|
// decodeValue(), so it must do the same DELETE bookkeeping itself.
|
|
@@ -3854,10 +3887,13 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3854
3887
|
// different type → "field not defined" / "definition mismatch"
|
|
3855
3888
|
// (surfaces under StateView when a filtered ArraySchema element is
|
|
3856
3889
|
// spliced while its parent moves through view membership churn).
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
}
|
|
3890
|
+
// Must run even when the item is absent from THIS array (view churn).
|
|
3891
|
+
decoder.root.removeRef(refId);
|
|
3860
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
|
+
}
|
|
3861
3897
|
tgt[$deleteByIndex](index);
|
|
3862
3898
|
allChanges?.push({
|
|
3863
3899
|
ref,
|
|
@@ -3898,9 +3934,12 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
|
|
|
3898
3934
|
resyncTouchEntry(decoder, ref, operation, index, previousValue, value, allChanges);
|
|
3899
3935
|
}
|
|
3900
3936
|
if (value !== null && value !== undefined &&
|
|
3901
|
-
value !== previousValue // avoid setting same value twice (
|
|
3937
|
+
value !== previousValue // avoid setting same value twice (an ADD at an occupied index would splice-insert)
|
|
3902
3938
|
) {
|
|
3903
|
-
|
|
3939
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
3940
|
+
tgt.$setAt(index, value, (decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
3941
|
+
? OPERATION.REPLACE
|
|
3942
|
+
: operation);
|
|
3904
3943
|
}
|
|
3905
3944
|
// add change
|
|
3906
3945
|
if (previousValue !== value) {
|
|
@@ -3957,30 +3996,35 @@ const ARRAY_PROXY_HANDLER = {
|
|
|
3957
3996
|
obj.$deleteAt(key);
|
|
3958
3997
|
}
|
|
3959
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;
|
|
3960
4003
|
if (setValue[$changes]) {
|
|
3961
4004
|
assertInstanceType(setValue, obj[$childType], obj, key);
|
|
3962
4005
|
const previousValue = obj.items[key];
|
|
3963
4006
|
if (!obj.isMovingItems) {
|
|
3964
|
-
obj.$changeAt(Number(key), setValue);
|
|
4007
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3965
4008
|
}
|
|
3966
4009
|
else {
|
|
4010
|
+
wireIndex = obj.$wireIndex(Number(key));
|
|
3967
4011
|
if (previousValue !== undefined) {
|
|
3968
4012
|
if (setValue[$changes].isNew) {
|
|
3969
|
-
obj[$changes].indexedOperation(
|
|
4013
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE_AND_ADD);
|
|
3970
4014
|
}
|
|
3971
4015
|
else {
|
|
3972
|
-
if ((obj[$changes].getChange(
|
|
3973
|
-
obj[$changes].indexedOperation(
|
|
4016
|
+
if ((obj[$changes].getChange(wireIndex) & OPERATION.DELETE) === OPERATION.DELETE) {
|
|
4017
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.DELETE_AND_MOVE);
|
|
3974
4018
|
}
|
|
3975
4019
|
else {
|
|
3976
|
-
obj[$changes].indexedOperation(
|
|
4020
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE);
|
|
3977
4021
|
}
|
|
3978
4022
|
}
|
|
3979
4023
|
}
|
|
3980
4024
|
else if (setValue[$changes].isNew) {
|
|
3981
|
-
obj[$changes].indexedOperation(
|
|
4025
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.ADD);
|
|
3982
4026
|
}
|
|
3983
|
-
setValue[$changes].setParent(obj, obj[$changes].root,
|
|
4027
|
+
setValue[$changes].setParent(obj, obj[$changes].root, wireIndex);
|
|
3984
4028
|
}
|
|
3985
4029
|
if (previousValue !== undefined) {
|
|
3986
4030
|
// remove root reference from previous value
|
|
@@ -3988,10 +4032,12 @@ const ARRAY_PROXY_HANDLER = {
|
|
|
3988
4032
|
}
|
|
3989
4033
|
}
|
|
3990
4034
|
else {
|
|
3991
|
-
obj.$changeAt(Number(key), setValue);
|
|
4035
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
3992
4036
|
}
|
|
3993
4037
|
obj.items[key] = setValue;
|
|
3994
|
-
|
|
4038
|
+
if (wireIndex !== undefined) {
|
|
4039
|
+
obj.tmpItems[wireIndex] = setValue;
|
|
4040
|
+
}
|
|
3995
4041
|
}
|
|
3996
4042
|
return true;
|
|
3997
4043
|
}
|
|
@@ -4174,40 +4220,68 @@ class ArraySchema {
|
|
|
4174
4220
|
index += this.length;
|
|
4175
4221
|
return this.items[index];
|
|
4176
4222
|
}
|
|
4177
|
-
|
|
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).
|
|
4178
4250
|
$changeAt(index, value) {
|
|
4179
4251
|
if (value === undefined || value === null) {
|
|
4180
|
-
console.error("ArraySchema items cannot be null nor undefined; Use `
|
|
4181
|
-
return;
|
|
4252
|
+
console.error("ArraySchema items cannot be null nor undefined; Use `splice(index, 1)` instead.");
|
|
4253
|
+
return undefined;
|
|
4182
4254
|
}
|
|
4183
4255
|
// skip if the value is the same as cached.
|
|
4184
4256
|
if (this.items[index] === value) {
|
|
4185
|
-
return;
|
|
4257
|
+
return undefined;
|
|
4186
4258
|
}
|
|
4187
4259
|
const operation = (this.items[index] !== undefined)
|
|
4188
4260
|
? typeof (value) === "object"
|
|
4189
4261
|
? OPERATION.DELETE_AND_ADD // schema child
|
|
4190
4262
|
: OPERATION.REPLACE // primitive
|
|
4191
4263
|
: OPERATION.ADD;
|
|
4264
|
+
const wireIndex = this.$wireIndex(index);
|
|
4192
4265
|
const changeTree = this[$changes];
|
|
4193
|
-
changeTree.change(
|
|
4266
|
+
changeTree.change(wireIndex, operation);
|
|
4194
4267
|
//
|
|
4195
4268
|
// set value's parent after the value is set
|
|
4196
4269
|
// (to avoid encoding "refId" operations before parent's "ADD" operation)
|
|
4197
4270
|
//
|
|
4198
|
-
value[$changes]?.setParent(this, changeTree.root,
|
|
4271
|
+
value[$changes]?.setParent(this, changeTree.root, wireIndex);
|
|
4272
|
+
return wireIndex;
|
|
4199
4273
|
}
|
|
4200
4274
|
// encoding only
|
|
4201
4275
|
$deleteAt(index, operation) {
|
|
4202
|
-
this[$changes].delete(index, operation);
|
|
4276
|
+
this[$changes].delete(this.$wireIndex(index), operation);
|
|
4203
4277
|
}
|
|
4204
4278
|
// decoding only
|
|
4205
4279
|
$setAt(index, value, operation) {
|
|
4206
|
-
if (
|
|
4207
|
-
operation === OPERATION.ADD &&
|
|
4280
|
+
if (operation === OPERATION.ADD &&
|
|
4208
4281
|
this.items[index] !== undefined) {
|
|
4209
|
-
//
|
|
4210
|
-
|
|
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);
|
|
4211
4285
|
}
|
|
4212
4286
|
else if (operation === OPERATION.DELETE_AND_MOVE) {
|
|
4213
4287
|
this.items.splice(index, 1);
|
|
@@ -4294,10 +4368,16 @@ class ArraySchema {
|
|
|
4294
4368
|
return undefined;
|
|
4295
4369
|
}
|
|
4296
4370
|
const changeTree = self[$changes];
|
|
4297
|
-
|
|
4298
|
-
|
|
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
|
+
}
|
|
4299
4379
|
changeTree.delete(index, OPERATION.DELETE);
|
|
4300
|
-
|
|
4380
|
+
deletedIndexes[index] = true;
|
|
4301
4381
|
return items.shift();
|
|
4302
4382
|
}
|
|
4303
4383
|
/**
|
|
@@ -4395,13 +4475,18 @@ class ArraySchema {
|
|
|
4395
4475
|
unshift(...items) {
|
|
4396
4476
|
const self = this[$proxyTarget];
|
|
4397
4477
|
const changeTree = self[$changes];
|
|
4398
|
-
//
|
|
4399
|
-
//
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
items.
|
|
4403
|
-
changeTree.
|
|
4404
|
-
}
|
|
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
|
+
}
|
|
4405
4490
|
self.tmpItems.unshift(...items);
|
|
4406
4491
|
return self.items.unshift(...items);
|
|
4407
4492
|
}
|
|
@@ -5020,6 +5105,37 @@ class MapSchema {
|
|
|
5020
5105
|
get(key) {
|
|
5021
5106
|
return this.$items.get(key);
|
|
5022
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
|
+
}
|
|
5023
5139
|
delete(key) {
|
|
5024
5140
|
if (!this.$items.has(key)) {
|
|
5025
5141
|
return false;
|