@colyseus/schema 3.0.0-alpha.42 → 3.0.0-alpha.44

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.
@@ -615,6 +615,11 @@ class TypeContext {
615
615
  this.hasFilters = false;
616
616
  this.parentFiltered = {};
617
617
  if (rootClass) {
618
+ //
619
+ // TODO:
620
+ // cache "discoverTypes" results for each rootClass
621
+ // to avoid re-discovering types for each new context/room
622
+ //
618
623
  this.discoverTypes(rootClass);
619
624
  }
620
625
  }
@@ -642,13 +647,17 @@ class TypeContext {
642
647
  getTypeId(klass) {
643
648
  return this.schemas.get(klass);
644
649
  }
645
- discoverTypes(klass, parentIndex, parentFieldViewTag) {
650
+ discoverTypes(klass, parentType, parentIndex, parentHasViewTag) {
651
+ if (parentHasViewTag) {
652
+ this.registerFilteredByParent(klass, parentType, parentIndex);
653
+ }
654
+ // skip if already registered
646
655
  if (!this.add(klass)) {
647
656
  return;
648
657
  }
649
658
  // add classes inherited from this base class
650
659
  TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
651
- this.discoverTypes(child, parentIndex, parentFieldViewTag);
660
+ this.discoverTypes(child, klass, parentIndex, parentHasViewTag);
652
661
  });
653
662
  // add parent classes
654
663
  let parent = klass;
@@ -663,13 +672,10 @@ class TypeContext {
663
672
  if (metadata[$viewFieldIndexes]) {
664
673
  this.hasFilters = true;
665
674
  }
666
- if (parentFieldViewTag !== undefined) {
667
- this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
668
- }
669
675
  for (const fieldIndex in metadata) {
670
676
  const index = fieldIndex;
671
677
  const fieldType = metadata[index].type;
672
- const viewTag = metadata[index].tag;
678
+ const fieldHasViewTag = (metadata[index].tag !== undefined);
673
679
  if (typeof (fieldType) === "string") {
674
680
  continue;
675
681
  }
@@ -679,10 +685,10 @@ class TypeContext {
679
685
  if (type === "string") {
680
686
  continue;
681
687
  }
682
- this.discoverTypes(type, index, viewTag);
688
+ this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag);
683
689
  }
684
690
  else if (typeof (fieldType) === "function") {
685
- this.discoverTypes(fieldType, viewTag);
691
+ this.discoverTypes(fieldType, klass, index, parentHasViewTag || fieldHasViewTag);
686
692
  }
687
693
  else {
688
694
  const type = Object.values(fieldType)[0];
@@ -690,10 +696,44 @@ class TypeContext {
690
696
  if (typeof (type) === "string") {
691
697
  continue;
692
698
  }
693
- this.discoverTypes(type, index, viewTag);
699
+ this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag);
694
700
  }
695
701
  }
696
702
  }
703
+ /**
704
+ * Keep track of which classes have filters applied.
705
+ * Format: `${typeid}-${parentTypeid}-${parentIndex}`
706
+ */
707
+ registerFilteredByParent(schema, parentType, parentIndex) {
708
+ const typeid = this.schemas.get(schema) ?? this.schemas.size;
709
+ let key = `${typeid}`;
710
+ if (parentType) {
711
+ key += `-${this.schemas.get(parentType)}`;
712
+ }
713
+ key += `-${parentIndex}`;
714
+ this.parentFiltered[key] = true;
715
+ }
716
+ debug() {
717
+ let parentFiltered = "";
718
+ for (const key in this.parentFiltered) {
719
+ const keys = key.split("-").map(Number);
720
+ const fieldIndex = keys.pop();
721
+ parentFiltered += `\n\t\t`;
722
+ parentFiltered += `${key}: ${keys.reverse().map((id, i) => {
723
+ const klass = this.types[id];
724
+ const metadata = klass[Symbol.metadata];
725
+ let txt = klass.name;
726
+ if (i === 0) {
727
+ txt += `[${metadata[fieldIndex].name}]`;
728
+ }
729
+ return `${txt}`;
730
+ }).join(" -> ")}`;
731
+ }
732
+ return `TypeContext ->\n` +
733
+ `\tSchema types: ${this.schemas.size}\n` +
734
+ `\thasFilters: ${this.hasFilters}\n` +
735
+ `\tparentFiltered:${parentFiltered}`;
736
+ }
697
737
  }
698
738
 
699
739
  function getNormalizedType(type) {
@@ -938,8 +978,10 @@ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeT
938
978
  }
939
979
  class ChangeTree {
940
980
  constructor(ref) {
981
+ /**
982
+ * Whether this structure is parent of a filtered structure.
983
+ */
941
984
  this.isFiltered = false;
942
- this.isPartiallyFiltered = false;
943
985
  this.indexedOperations = {};
944
986
  //
945
987
  // TODO:
@@ -958,37 +1000,17 @@ class ChangeTree {
958
1000
  //
959
1001
  // Does this structure have "filters" declared?
960
1002
  //
961
- if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
1003
+ const metadata = ref.constructor[Symbol.metadata];
1004
+ if (metadata?.[$viewFieldIndexes]) {
962
1005
  this.allFilteredChanges = { indexes: {}, operations: [] };
963
1006
  this.filteredChanges = { indexes: {}, operations: [] };
964
1007
  }
965
1008
  }
966
1009
  setRoot(root) {
967
1010
  this.root = root;
968
- const isNewChangeTree = this.root.add(this);
969
- const metadata = this.ref.constructor[Symbol.metadata];
970
- if (this.root.types.hasFilters) {
971
- //
972
- // At Schema initialization, the "root" structure might not be available
973
- // yet, as it only does once the "Encoder" has been set up.
974
- //
975
- // So the "parent" may be already set without a "root".
976
- //
977
- this.checkIsFiltered(metadata, this.parent, this.parentIndex);
978
- if (this.isFiltered || this.isPartiallyFiltered) {
979
- enqueueChangeTree(root, this, 'filteredChanges');
980
- if (isNewChangeTree) {
981
- this.root.allFilteredChanges.push(this);
982
- }
983
- }
984
- }
985
- if (!this.isFiltered) {
986
- enqueueChangeTree(root, this, 'changes');
987
- if (isNewChangeTree) {
988
- this.root.allChanges.push(this);
989
- }
990
- }
1011
+ this.checkIsFiltered(this.parent, this.parentIndex);
991
1012
  // Recursively set root on child structures
1013
+ const metadata = this.ref.constructor[Symbol.metadata];
992
1014
  if (metadata) {
993
1015
  metadata[$refTypeFieldIndexes]?.forEach((index) => {
994
1016
  const field = metadata[index];
@@ -1010,39 +1032,21 @@ class ChangeTree {
1010
1032
  if (!root) {
1011
1033
  return;
1012
1034
  }
1013
- const metadata = this.ref.constructor[Symbol.metadata];
1014
1035
  // skip if parent is already set
1015
1036
  if (root !== this.root) {
1016
1037
  this.root = root;
1017
- const isNewChangeTree = root.add(this);
1018
- if (root.types.hasFilters) {
1019
- this.checkIsFiltered(metadata, parent, parentIndex);
1020
- if (this.isFiltered || this.isPartiallyFiltered) {
1021
- enqueueChangeTree(root, this, 'filteredChanges');
1022
- if (isNewChangeTree) {
1023
- this.root.allFilteredChanges.push(this);
1024
- }
1025
- }
1026
- }
1027
- if (!this.isFiltered) {
1028
- enqueueChangeTree(root, this, 'changes');
1029
- if (isNewChangeTree) {
1030
- this.root.allChanges.push(this);
1031
- }
1032
- }
1038
+ this.checkIsFiltered(parent, parentIndex);
1033
1039
  }
1034
1040
  else {
1035
1041
  root.add(this);
1036
1042
  }
1037
1043
  // assign same parent on child structures
1044
+ const metadata = this.ref.constructor[Symbol.metadata];
1038
1045
  if (metadata) {
1039
1046
  metadata[$refTypeFieldIndexes]?.forEach((index) => {
1040
1047
  const field = metadata[index];
1041
1048
  const value = this.ref[field.name];
1042
1049
  value?.[$changes].setParent(this.ref, root, index);
1043
- // try { throw new Error(); } catch (e) {
1044
- // console.log(e.stack);
1045
- // }
1046
1050
  });
1047
1051
  }
1048
1052
  else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
@@ -1135,7 +1139,7 @@ class ChangeTree {
1135
1139
  //
1136
1140
  // - ArraySchema#splice()
1137
1141
  //
1138
- if (this.isFiltered || this.isPartiallyFiltered) {
1142
+ if (this.filteredChanges !== undefined) {
1139
1143
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allFilteredChanges);
1140
1144
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
1141
1145
  }
@@ -1164,7 +1168,7 @@ class ChangeTree {
1164
1168
  }
1165
1169
  indexedOperation(index, operation, allChangesIndex = index) {
1166
1170
  this.indexedOperations[index] = operation;
1167
- if (this.filteredChanges) {
1171
+ if (this.filteredChanges !== undefined) {
1168
1172
  setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1169
1173
  setOperationAtIndex(this.filteredChanges, index);
1170
1174
  enqueueChangeTree(this.root, this, 'filteredChanges');
@@ -1212,7 +1216,7 @@ class ChangeTree {
1212
1216
  }
1213
1217
  return;
1214
1218
  }
1215
- const changeSet = (this.filteredChanges)
1219
+ const changeSet = (this.filteredChanges !== undefined)
1216
1220
  ? this.filteredChanges
1217
1221
  : this.changes;
1218
1222
  this.indexedOperations[index] = operation ?? OPERATION.DELETE;
@@ -1228,14 +1232,14 @@ class ChangeTree {
1228
1232
  // - This is due to using the concrete Schema class at decoding time.
1229
1233
  // - "Reflected" structures do not have this problem.
1230
1234
  //
1231
- // (the property descriptors should NOT be used at decoding time. only at encoding time.)
1235
+ // (The property descriptors should NOT be used at decoding time. only at encoding time.)
1232
1236
  //
1233
1237
  this.root?.remove(previousValue[$changes]);
1234
1238
  }
1235
1239
  //
1236
1240
  // FIXME: this is looking a ugly and repeated
1237
1241
  //
1238
- if (this.filteredChanges) {
1242
+ if (this.filteredChanges !== undefined) {
1239
1243
  deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1240
1244
  enqueueChangeTree(this.root, this, 'filteredChanges');
1241
1245
  }
@@ -1304,29 +1308,59 @@ class ChangeTree {
1304
1308
  get changed() {
1305
1309
  return (Object.entries(this.indexedOperations).length > 0);
1306
1310
  }
1307
- checkIsFiltered(metadata, parent, parentIndex) {
1308
- // Detect if current structure has "filters" declared
1309
- this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
1310
- if (this.isPartiallyFiltered) {
1311
- this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
1312
- this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
1311
+ checkIsFiltered(parent, parentIndex) {
1312
+ const isNewChangeTree = this.root.add(this);
1313
+ if (this.root.types.hasFilters) {
1314
+ //
1315
+ // At Schema initialization, the "root" structure might not be available
1316
+ // yet, as it only does once the "Encoder" has been set up.
1317
+ //
1318
+ // So the "parent" may be already set without a "root".
1319
+ //
1320
+ this._checkFilteredByParent(parent, parentIndex);
1321
+ if (this.filteredChanges !== undefined) {
1322
+ enqueueChangeTree(this.root, this, 'filteredChanges');
1323
+ if (isNewChangeTree) {
1324
+ this.root.allFilteredChanges.push(this);
1325
+ }
1326
+ }
1313
1327
  }
1328
+ if (!this.isFiltered) {
1329
+ enqueueChangeTree(this.root, this, 'changes');
1330
+ if (isNewChangeTree) {
1331
+ this.root.allChanges.push(this);
1332
+ }
1333
+ }
1334
+ }
1335
+ _checkFilteredByParent(parent, parentIndex) {
1314
1336
  // skip if parent is not set
1315
1337
  if (!parent) {
1316
1338
  return;
1317
1339
  }
1340
+ // ArraySchema | MapSchema - get the child type
1341
+ const ref = Metadata.isValidInstance(this.ref)
1342
+ ? this.ref
1343
+ : new this.ref[$childType];
1318
1344
  if (!Metadata.isValidInstance(parent)) {
1319
1345
  const parentChangeTree = parent[$changes];
1320
1346
  parent = parentChangeTree.parent;
1321
1347
  parentIndex = parentChangeTree.parentIndex;
1322
1348
  }
1323
- const parentMetadata = parent.constructor?.[Symbol.metadata];
1324
- this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
1349
+ const parentConstructor = parent.constructor;
1350
+ let key = `${this.root.types.getTypeId(ref.constructor)}`;
1351
+ if (parentConstructor) {
1352
+ key += `-${this.root.types.schemas.get(parentConstructor)}`;
1353
+ }
1354
+ key += `-${parentIndex}`;
1355
+ this.isFiltered = this.root.types.parentFiltered[key];
1356
+ // const parentMetadata = parentConstructor?.[Symbol.metadata];
1357
+ // this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex) || this.root.types.parentFiltered[key];
1325
1358
  //
1326
1359
  // TODO: refactor this!
1327
1360
  //
1328
1361
  // swapping `changes` and `filteredChanges` is required here
1329
1362
  // because "isFiltered" may not be imedialely available on `change()`
1363
+ // (this happens when instance is detached from root or parent)
1330
1364
  //
1331
1365
  if (this.isFiltered) {
1332
1366
  this.filteredChanges = { indexes: {}, operations: [] };
@@ -1340,10 +1374,6 @@ class ChangeTree {
1340
1374
  const allFilteredChanges = this.allFilteredChanges;
1341
1375
  this.allFilteredChanges = this.allChanges;
1342
1376
  this.allChanges = allFilteredChanges;
1343
- // console.log("SWAP =>", {
1344
- // "this.allFilteredChanges": this.allFilteredChanges,
1345
- // "this.allChanges": this.allChanges
1346
- // })
1347
1377
  }
1348
1378
  }
1349
1379
  }
@@ -3231,7 +3261,7 @@ class Schema {
3231
3261
  const rootChangeTree = ref[$changes];
3232
3262
  const root = rootChangeTree.root;
3233
3263
  const changeTrees = new Map();
3234
- let totalInstances = 0;
3264
+ const instanceRefIds = [];
3235
3265
  let totalOperations = 0;
3236
3266
  for (const [refId, changes] of Object.entries(root[changeSetName])) {
3237
3267
  const changeTree = root.changeTrees[refId];
@@ -3252,14 +3282,14 @@ class Schema {
3252
3282
  }
3253
3283
  }
3254
3284
  if (includeChangeTree) {
3255
- totalInstances += 1;
3285
+ instanceRefIds.push(changeTree.refId);
3256
3286
  totalOperations += Object.keys(changes).length;
3257
3287
  changeTrees.set(changeTree, parentChangeTrees.reverse());
3258
3288
  }
3259
3289
  }
3260
3290
  output += "---\n";
3261
3291
  output += `root refId: ${rootChangeTree.refId}\n`;
3262
- output += `Total instances: ${totalInstances}\n`;
3292
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3263
3293
  output += `Total changes: ${totalOperations}\n`;
3264
3294
  output += "---\n";
3265
3295
  // based on root.changes, display a tree of changes that has the "ref" instance as parent
@@ -3700,7 +3730,7 @@ class Root {
3700
3730
  delete this.changeTrees[changeTree.refId];
3701
3731
  this.removeChangeFromChangeSet("allChanges", changeTree);
3702
3732
  this.removeChangeFromChangeSet("changes", changeTree);
3703
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3733
+ if (changeTree.filteredChanges) {
3704
3734
  this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3705
3735
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
3706
3736
  }
@@ -4046,6 +4076,7 @@ class ReferenceTracker {
4046
4076
  clearRefs() {
4047
4077
  this.refs.clear();
4048
4078
  this.deletedRefs.clear();
4079
+ this.callbacks = {};
4049
4080
  this.refCounts = {};
4050
4081
  }
4051
4082
  // for decoding
@@ -4710,7 +4741,7 @@ class StateView {
4710
4741
  }
4711
4742
  else {
4712
4743
  const isInvisible = this.invisible.has(changeTree);
4713
- const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4744
+ const changeSet = (changeTree.filteredChanges !== undefined)
4714
4745
  ? changeTree.allFilteredChanges
4715
4746
  : changeTree.allChanges;
4716
4747
  for (let i = 0, len = changeSet.operations.length; i < len; i++) {
@@ -4718,7 +4749,7 @@ class StateView {
4718
4749
  if (index === undefined) {
4719
4750
  continue;
4720
4751
  } // skip "undefined" indexes
4721
- const op = changeTree.indexedOperations[index];
4752
+ const op = changeTree.indexedOperations[index] ?? OPERATION.ADD;
4722
4753
  const tagAtIndex = metadata?.[index].tag;
4723
4754
  if ((isInvisible || // if "invisible", include all
4724
4755
  tagAtIndex === undefined || // "all change" with no tag
@@ -4732,7 +4763,9 @@ class StateView {
4732
4763
  // Add children of this ChangeTree to this view
4733
4764
  changeTree.forEachChild((change, index) => {
4734
4765
  // Do not ADD children that don't have the same tag
4735
- if (metadata && metadata[index].tag !== tag) {
4766
+ if (metadata &&
4767
+ metadata[index].tag !== undefined &&
4768
+ metadata[index].tag !== tag) {
4736
4769
  return;
4737
4770
  }
4738
4771
  this.add(change.ref, tag, false);
@@ -4744,7 +4777,7 @@ class StateView {
4744
4777
  this.items.add(changeTree);
4745
4778
  // add parent's parent
4746
4779
  const parentChangeTree = changeTree.parent?.[$changes];
4747
- if (parentChangeTree && (parentChangeTree.isFiltered || parentChangeTree.isPartiallyFiltered)) {
4780
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
4748
4781
  this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4749
4782
  }
4750
4783
  // parent is already available, no need to add it!