@colyseus/schema 3.0.10 → 3.0.12

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.
@@ -859,14 +859,13 @@ const Metadata = {
859
859
  fieldIndex++;
860
860
  for (const field in fields) {
861
861
  const type = fields[field];
862
- const normalizedType = getNormalizedType(type);
863
862
  // FIXME: this code is duplicated from @type() annotation
864
863
  const complexTypeKlass = (Array.isArray(type))
865
864
  ? getType("array")
866
865
  : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
867
866
  const childType = (complexTypeKlass)
868
867
  ? Object.values(type)[0]
869
- : normalizedType;
868
+ : getNormalizedType(type);
870
869
  Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
871
870
  fieldIndex++;
872
871
  }
@@ -1238,6 +1237,7 @@ class ChangeTree {
1238
1237
  //
1239
1238
  this.root?.remove(previousValue[$changes]);
1240
1239
  }
1240
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
1241
1241
  //
1242
1242
  // FIXME: this is looking a ugly and repeated
1243
1243
  //
@@ -1246,7 +1246,6 @@ class ChangeTree {
1246
1246
  enqueueChangeTree(this.root, this, 'filteredChanges');
1247
1247
  }
1248
1248
  else {
1249
- deleteOperationAtIndex(this.allChanges, allChangesIndex);
1250
1249
  enqueueChangeTree(this.root, this, 'changes');
1251
1250
  }
1252
1251
  }
@@ -1533,19 +1532,6 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1533
1532
  //
1534
1533
  if (operation !== exports.OPERATION.DELETE_AND_ADD) {
1535
1534
  ref[$deleteByIndex](index);
1536
- // //
1537
- // // FIXME: is this in the correct place?
1538
- // // (This is sounding like a workaround just for ArraySchema, see
1539
- // // "should splice and move" test on ArraySchema.test.ts)
1540
- // //
1541
- // allChanges.push({
1542
- // ref,
1543
- // refId: decoder.currentRefId,
1544
- // op: OPERATION.DELETE,
1545
- // field: index as unknown as string,
1546
- // value: undefined,
1547
- // previousValue,
1548
- // });
1549
1535
  }
1550
1536
  value = undefined;
1551
1537
  }
@@ -1893,7 +1879,8 @@ class ArraySchema {
1893
1879
  else {
1894
1880
  if (setValue[$changes]) {
1895
1881
  assertInstanceType(setValue, obj[$childType], obj, key);
1896
- if (obj.items[key] !== undefined) {
1882
+ const previousValue = obj.items[key];
1883
+ if (previousValue !== undefined) {
1897
1884
  if (setValue[$changes].isNew) {
1898
1885
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1899
1886
  }
@@ -1905,10 +1892,13 @@ class ArraySchema {
1905
1892
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE);
1906
1893
  }
1907
1894
  }
1895
+ // remove root reference from previous value
1896
+ previousValue[$changes].root?.remove(previousValue[$changes]);
1908
1897
  }
1909
1898
  else if (setValue[$changes].isNew) {
1910
1899
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1911
1900
  }
1901
+ setValue[$changes].setParent(this, obj[$changes].root, key);
1912
1902
  }
1913
1903
  else {
1914
1904
  obj.$changeAt(Number(key), setValue);
@@ -2546,33 +2536,34 @@ class MapSchema {
2546
2536
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2547
2537
  key = key.toString();
2548
2538
  const changeTree = this[$changes];
2549
- // get "index" for this value.
2550
- const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2551
- const index = (isReplace)
2552
- ? changeTree.indexes[key]
2553
- : changeTree.indexes[$numFields] ?? 0;
2554
- let operation = (isReplace)
2555
- ? exports.OPERATION.REPLACE
2556
- : exports.OPERATION.ADD;
2557
2539
  const isRef = (value[$changes]) !== undefined;
2558
- //
2559
- // (encoding)
2560
- // set a unique id to relate directly with this key/value.
2561
- //
2562
- if (!isReplace) {
2540
+ let index;
2541
+ let operation;
2542
+ // IS REPLACE?
2543
+ if (typeof (changeTree.indexes[key]) !== "undefined") {
2544
+ index = changeTree.indexes[key];
2545
+ operation = exports.OPERATION.REPLACE;
2546
+ const previousValue = this.$items.get(key);
2547
+ if (previousValue === value) {
2548
+ // if value is the same, avoid re-encoding it.
2549
+ return;
2550
+ }
2551
+ else if (isRef) {
2552
+ // if is schema, force ADD operation if value differ from previous one.
2553
+ operation = exports.OPERATION.DELETE_AND_ADD;
2554
+ // remove reference from previous value
2555
+ if (previousValue !== undefined) {
2556
+ previousValue[$changes].root?.remove(previousValue[$changes]);
2557
+ }
2558
+ }
2559
+ }
2560
+ else {
2561
+ index = changeTree.indexes[$numFields] ?? 0;
2562
+ operation = exports.OPERATION.ADD;
2563
2563
  this.$indexes.set(index, key);
2564
2564
  changeTree.indexes[key] = index;
2565
2565
  changeTree.indexes[$numFields] = index + 1;
2566
2566
  }
2567
- else if (!isRef &&
2568
- this.$items.get(key) === value) {
2569
- // if value is the same, avoid re-encoding it.
2570
- return;
2571
- }
2572
- else if (isRef && // if is schema, force ADD operation if value differ from previous one.
2573
- this.$items.get(key) !== value) {
2574
- operation = exports.OPERATION.ADD;
2575
- }
2576
2567
  this.$items.set(key, value);
2577
2568
  changeTree.change(index, operation);
2578
2569
  //
@@ -2944,13 +2935,14 @@ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass)
2944
2935
  const changeTree = this[$changes];
2945
2936
  //
2946
2937
  // Replacing existing "ref", remove it from root.
2947
- // TODO: if there are other references to this instance, we should not remove it from root.
2948
2938
  //
2949
2939
  if (previousValue !== undefined && previousValue[$changes]) {
2950
2940
  changeTree.root?.remove(previousValue[$changes]);
2941
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.DELETE_AND_ADD);
2942
+ }
2943
+ else {
2944
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2951
2945
  }
2952
- // flag the change for encoding.
2953
- this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2954
2946
  //
2955
2947
  // call setParent() recursively for this and its child
2956
2948
  // structures.
@@ -3747,16 +3739,33 @@ class Root {
3747
3739
  }
3748
3740
  else {
3749
3741
  this.refCount[changeTree.refId] = refCount;
3742
+ //
3743
+ // When losing a reference to an instance, it is best to move the
3744
+ // ChangeTree to the end of the encoding queue.
3745
+ //
3746
+ // This way, at decoding time, the instance that contains the
3747
+ // ChangeTree will be available before the ChangeTree itself. If the
3748
+ // containing instance is not available, the Decoder will throw
3749
+ // "refId not found" error.
3750
+ //
3751
+ if (changeTree.filteredChanges !== undefined) {
3752
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3753
+ enqueueChangeTree(this, changeTree, "filteredChanges");
3754
+ }
3755
+ else {
3756
+ this.removeChangeFromChangeSet("changes", changeTree);
3757
+ enqueueChangeTree(this, changeTree, "changes");
3758
+ }
3750
3759
  }
3751
3760
  changeTree.forEachChild((child, _) => this.remove(child));
3752
3761
  return refCount;
3753
3762
  }
3754
3763
  removeChangeFromChangeSet(changeSetName, changeTree) {
3755
3764
  const changeSet = this[changeSetName];
3756
- const index = changeSet.indexOf(changeTree);
3757
- if (index !== -1) {
3758
- spliceOne(changeSet, index);
3765
+ if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
3766
+ changeTree[changeSetName].queueRootIndex = -1;
3759
3767
  // changeSet[index] = undefined;
3768
+ return true;
3760
3769
  }
3761
3770
  }
3762
3771
  clear() {
@@ -3792,12 +3801,6 @@ class Encoder {
3792
3801
  const changeTrees = this.root[changeSetName];
3793
3802
  for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3794
3803
  const changeTree = changeTrees[i];
3795
- const operations = changeTree[changeSetName];
3796
- const ref = changeTree.ref;
3797
- const ctor = ref.constructor;
3798
- const encoder = ctor[$encoder];
3799
- const filter = ctor[$filter];
3800
- const metadata = ctor[Symbol.metadata];
3801
3804
  if (hasView) {
3802
3805
  if (!view.items.has(changeTree)) {
3803
3806
  view.invisible.add(changeTree);
@@ -3807,13 +3810,24 @@ class Encoder {
3807
3810
  view.invisible.delete(changeTree); // remove from invisible list
3808
3811
  }
3809
3812
  }
3813
+ const operations = changeTree[changeSetName];
3814
+ const ref = changeTree.ref;
3815
+ // TODO: avoid iterating over change tree if no changes were made
3816
+ const numChanges = operations.operations.length;
3817
+ if (numChanges === 0) {
3818
+ continue;
3819
+ }
3820
+ const ctor = ref.constructor;
3821
+ const encoder = ctor[$encoder];
3822
+ const filter = ctor[$filter];
3823
+ const metadata = ctor[Symbol.metadata];
3810
3824
  // skip root `refId` if it's the first change tree
3811
3825
  // (unless it "hasView", which will need to revisit the root)
3812
3826
  if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3813
3827
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3814
3828
  encode.number(buffer, changeTree.refId, it);
3815
3829
  }
3816
- for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3830
+ for (let j = 0; j < numChanges; j++) {
3817
3831
  const fieldIndex = operations.operations[j];
3818
3832
  const operation = (fieldIndex < 0)
3819
3833
  ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
@@ -3910,14 +3924,16 @@ class Encoder {
3910
3924
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3911
3925
  const viewOffset = it.offset;
3912
3926
  // encode visibility changes (add/remove for this view)
3913
- const refIds = Object.keys(view.changes);
3914
- for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3915
- const refId = refIds[i];
3916
- const changes = view.changes[refId];
3927
+ for (const [refId, changes] of view.changes) {
3917
3928
  const changeTree = this.root.changeTrees[refId];
3918
- if (changeTree === undefined ||
3919
- Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3920
- ) {
3929
+ if (changeTree === undefined) {
3930
+ // detached instance, remove from view and skip.
3931
+ view.changes.delete(refId);
3932
+ continue;
3933
+ }
3934
+ const keys = Object.keys(changes);
3935
+ if (keys.length === 0) {
3936
+ // FIXME: avoid having empty changes if no changes were made
3921
3937
  // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3922
3938
  continue;
3923
3939
  }
@@ -3927,7 +3943,6 @@ class Encoder {
3927
3943
  const metadata = ctor[Symbol.metadata];
3928
3944
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3929
3945
  encode.number(bytes, changeTree.refId, it);
3930
- const keys = Object.keys(changes);
3931
3946
  for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3932
3947
  const key = keys[i];
3933
3948
  const operation = changes[key];
@@ -3941,7 +3956,7 @@ class Encoder {
3941
3956
  // (to allow re-using StateView's for multiple clients)
3942
3957
  //
3943
3958
  // clear "view" changes after encoding
3944
- view.changes = {};
3959
+ view.changes.clear();
3945
3960
  // try to encode "filtered" changes
3946
3961
  this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3947
3962
  return Buffer.concat([
@@ -3949,20 +3964,6 @@ class Encoder {
3949
3964
  bytes.subarray(viewOffset, it.offset)
3950
3965
  ]);
3951
3966
  }
3952
- onEndEncode(changeTrees = this.root.changes) {
3953
- // changeTrees.forEach(function(changeTree) {
3954
- // changeTree.endEncode();
3955
- // });
3956
- // for (const refId in changeTrees) {
3957
- // const changeTree = this.root.changeTrees[refId];
3958
- // changeTree.endEncode();
3959
- // // changeTree.changes.clear();
3960
- // // // ArraySchema and MapSchema have a custom "encode end" method
3961
- // // changeTree.ref[$onEncodeEnd]?.();
3962
- // // // Not a new instance anymore
3963
- // // delete changeTree[$isNew];
3964
- // }
3965
- }
3966
3967
  discardChanges() {
3967
3968
  // discard shared changes
3968
3969
  let length = this.root.changes.length;
@@ -4694,7 +4695,8 @@ class StateView {
4694
4695
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
4695
4696
  * (This is used to force encoding a property, even if it was not changed)
4696
4697
  */
4697
- this.changes = {};
4698
+ // TODO: use map here!? may fix encode ordering issue
4699
+ this.changes = new Map();
4698
4700
  }
4699
4701
  // TODO: allow to set multiple tags at once
4700
4702
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
@@ -4710,16 +4712,16 @@ class StateView {
4710
4712
  // - if it was invisible to this view
4711
4713
  // - if it were previously filtered out
4712
4714
  if (checkIncludeParent && changeTree.parent) {
4713
- this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4715
+ this.addParentOf(changeTree, tag);
4714
4716
  }
4715
4717
  //
4716
4718
  // TODO: when adding an item of a MapSchema, the changes may not
4717
4719
  // be set (only the parent's changes are set)
4718
4720
  //
4719
- let changes = this.changes[changeTree.refId];
4721
+ let changes = this.changes.get(changeTree.refId);
4720
4722
  if (changes === undefined) {
4721
4723
  changes = {};
4722
- this.changes[changeTree.refId] = changes;
4724
+ this.changes.set(changeTree.refId, changes);
4723
4725
  }
4724
4726
  // set tag
4725
4727
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -4775,24 +4777,29 @@ class StateView {
4775
4777
  });
4776
4778
  return this;
4777
4779
  }
4778
- addParent(changeTree, parentIndex, tag) {
4780
+ addParentOf(childChangeTree, tag) {
4781
+ const changeTree = childChangeTree.parent[$changes];
4782
+ const parentIndex = childChangeTree.parentIndex;
4779
4783
  // view must have all "changeTree" parent tree
4780
4784
  this.items.add(changeTree);
4781
4785
  // add parent's parent
4782
4786
  const parentChangeTree = changeTree.parent?.[$changes];
4783
4787
  if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
4784
- this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4788
+ this.addParentOf(changeTree, tag);
4785
4789
  }
4790
+ if (
4786
4791
  // parent is already available, no need to add it!
4787
- if (!this.invisible.has(changeTree)) {
4792
+ !this.invisible.has(changeTree) &&
4793
+ // item is being replaced, no need to add parent
4794
+ changeTree.indexedOperations[parentIndex] !== exports.OPERATION.DELETE_AND_ADD) {
4788
4795
  return;
4789
4796
  }
4790
4797
  // add parent's tag properties
4791
4798
  if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4792
- let changes = this.changes[changeTree.refId];
4799
+ let changes = this.changes.get(changeTree.refId);
4793
4800
  if (changes === undefined) {
4794
4801
  changes = {};
4795
- this.changes[changeTree.refId] = changes;
4802
+ this.changes.set(changeTree.refId, changes);
4796
4803
  }
4797
4804
  if (!this.tags) {
4798
4805
  this.tags = new WeakMap();
@@ -4818,20 +4825,20 @@ class StateView {
4818
4825
  this.items.delete(changeTree);
4819
4826
  const ref = changeTree.ref;
4820
4827
  const metadata = ref.constructor[Symbol.metadata];
4821
- let changes = this.changes[changeTree.refId];
4828
+ let changes = this.changes.get(changeTree.refId);
4822
4829
  if (changes === undefined) {
4823
4830
  changes = {};
4824
- this.changes[changeTree.refId] = changes;
4831
+ this.changes.set(changeTree.refId, changes);
4825
4832
  }
4826
4833
  if (tag === DEFAULT_VIEW_TAG) {
4827
4834
  // parent is collection (Map/Array)
4828
4835
  const parent = changeTree.parent;
4829
4836
  if (!Metadata.isValidInstance(parent)) {
4830
4837
  const parentChangeTree = parent[$changes];
4831
- let changes = this.changes[parentChangeTree.refId];
4838
+ let changes = this.changes.get(parentChangeTree.refId);
4832
4839
  if (changes === undefined) {
4833
4840
  changes = {};
4834
- this.changes[parentChangeTree.refId] = changes;
4841
+ this.changes.set(parentChangeTree.refId, changes);
4835
4842
  }
4836
4843
  // DELETE / DELETE BY REF ID
4837
4844
  changes[changeTree.parentIndex] = exports.OPERATION.DELETE;