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