@colyseus/schema 3.0.42 → 3.0.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.
Files changed (52) hide show
  1. package/build/cjs/index.js +365 -219
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +365 -219
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +365 -219
  6. package/lib/Schema.d.ts +4 -3
  7. package/lib/Schema.js +22 -4
  8. package/lib/Schema.js.map +1 -1
  9. package/lib/bench_encode.d.ts +1 -0
  10. package/lib/bench_encode.js +130 -0
  11. package/lib/bench_encode.js.map +1 -0
  12. package/lib/debug.d.ts +1 -0
  13. package/lib/debug.js +51 -0
  14. package/lib/debug.js.map +1 -0
  15. package/lib/decoder/Decoder.js +8 -9
  16. package/lib/decoder/Decoder.js.map +1 -1
  17. package/lib/encoder/ChangeTree.d.ts +57 -7
  18. package/lib/encoder/ChangeTree.js +172 -106
  19. package/lib/encoder/ChangeTree.js.map +1 -1
  20. package/lib/encoder/Encoder.js +19 -20
  21. package/lib/encoder/Encoder.js.map +1 -1
  22. package/lib/encoder/Root.d.ts +9 -8
  23. package/lib/encoder/Root.js +84 -27
  24. package/lib/encoder/Root.js.map +1 -1
  25. package/lib/encoder/StateView.d.ts +1 -1
  26. package/lib/encoder/StateView.js +28 -23
  27. package/lib/encoder/StateView.js.map +1 -1
  28. package/lib/types/custom/ArraySchema.js +7 -5
  29. package/lib/types/custom/ArraySchema.js.map +1 -1
  30. package/lib/types/custom/CollectionSchema.js +1 -1
  31. package/lib/types/custom/CollectionSchema.js.map +1 -1
  32. package/lib/types/custom/MapSchema.js +9 -4
  33. package/lib/types/custom/MapSchema.js.map +1 -1
  34. package/lib/types/symbols.d.ts +14 -14
  35. package/lib/types/symbols.js +14 -14
  36. package/lib/types/symbols.js.map +1 -1
  37. package/lib/utils.js +7 -3
  38. package/lib/utils.js.map +1 -1
  39. package/package.json +2 -1
  40. package/src/Schema.ts +23 -7
  41. package/src/bench_encode.ts +108 -0
  42. package/src/debug.ts +55 -0
  43. package/src/decoder/Decoder.ts +9 -13
  44. package/src/encoder/ChangeTree.ts +203 -116
  45. package/src/encoder/Encoder.ts +21 -19
  46. package/src/encoder/Root.ts +90 -29
  47. package/src/encoder/StateView.ts +34 -26
  48. package/src/types/custom/ArraySchema.ts +8 -6
  49. package/src/types/custom/CollectionSchema.ts +1 -1
  50. package/src/types/custom/MapSchema.ts +10 -4
  51. package/src/types/symbols.ts +15 -15
  52. package/src/utils.ts +9 -3
@@ -32,37 +32,37 @@
32
32
 
33
33
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
34
34
 
35
- const $track = Symbol("$track");
36
- const $encoder = Symbol("$encoder");
37
- const $decoder = Symbol("$decoder");
38
- const $filter = Symbol("$filter");
39
- const $getByIndex = Symbol("$getByIndex");
40
- const $deleteByIndex = Symbol("$deleteByIndex");
35
+ const $track = "~track";
36
+ const $encoder = "~encoder";
37
+ const $decoder = "~decoder";
38
+ const $filter = "~filter";
39
+ const $getByIndex = "~getByIndex";
40
+ const $deleteByIndex = "~deleteByIndex";
41
41
  /**
42
42
  * Used to hold ChangeTree instances whitin the structures
43
43
  */
44
- const $changes = Symbol('$changes');
44
+ const $changes = '~changes';
45
45
  /**
46
46
  * Used to keep track of the type of the child elements of a collection
47
47
  * (MapSchema, ArraySchema, etc.)
48
48
  */
49
- const $childType = Symbol('$childType');
49
+ const $childType = '~childType';
50
50
  /**
51
51
  * Optional "discard" method for custom types (ArraySchema)
52
52
  * (Discards changes for next serialization)
53
53
  */
54
- const $onEncodeEnd = Symbol('$onEncodeEnd');
54
+ const $onEncodeEnd = '~onEncodeEnd';
55
55
  /**
56
56
  * When decoding, this method is called after the instance is fully decoded
57
57
  */
58
- const $onDecodeEnd = Symbol("$onDecodeEnd");
58
+ const $onDecodeEnd = "~onDecodeEnd";
59
59
  /**
60
60
  * Metadata
61
61
  */
62
- const $descriptors = Symbol("$descriptors");
63
- const $numFields = "$__numFields";
64
- const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
65
- const $viewFieldIndexes = "$__viewFieldIndexes";
62
+ const $descriptors = "~descriptors";
63
+ const $numFields = "~__numFields";
64
+ const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
65
+ const $viewFieldIndexes = "~__viewFieldIndexes";
66
66
  const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
67
67
 
68
68
  /**
@@ -970,6 +970,24 @@
970
970
  function createChangeSet() {
971
971
  return { indexes: {}, operations: [] };
972
972
  }
973
+ // Linked list helper functions
974
+ function createChangeTreeList() {
975
+ return { next: undefined, tail: undefined, length: 0 };
976
+ }
977
+ function addToChangeTreeList(list, changeTree) {
978
+ const node = { changeTree, next: undefined, prev: undefined };
979
+ if (!list.next) {
980
+ list.next = node;
981
+ list.tail = node;
982
+ }
983
+ else {
984
+ node.prev = list.tail;
985
+ list.tail.next = node;
986
+ list.tail = node;
987
+ }
988
+ list.length++;
989
+ return node;
990
+ }
973
991
  function setOperationAtIndex(changeSet, index) {
974
992
  const operationsIndex = changeSet.indexes[index];
975
993
  if (operationsIndex === undefined) {
@@ -994,13 +1012,15 @@
994
1012
  changeSet.operations[operationsIndex] = undefined;
995
1013
  delete changeSet.indexes[index];
996
1014
  }
997
- function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
1015
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
1016
+ // skip
998
1017
  if (!root) {
999
- // skip
1000
1018
  return;
1001
1019
  }
1002
- else if (root[changeSet][queueRootIndex] !== changeTree) {
1003
- changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
1020
+ if (queueRootNode) ;
1021
+ else {
1022
+ // Add to linked list if not already present
1023
+ changeTree[changeSet].queueRootNode = addToChangeTreeList(root[changeSet], changeTree);
1004
1024
  }
1005
1025
  }
1006
1026
  class ChangeTree {
@@ -1024,83 +1044,59 @@
1024
1044
  */
1025
1045
  this.isNew = true;
1026
1046
  this.ref = ref;
1047
+ this.metadata = ref.constructor[Symbol.metadata];
1027
1048
  //
1028
1049
  // Does this structure have "filters" declared?
1029
1050
  //
1030
- const metadata = ref.constructor[Symbol.metadata];
1031
- if (metadata?.[$viewFieldIndexes]) {
1051
+ if (this.metadata?.[$viewFieldIndexes]) {
1032
1052
  this.allFilteredChanges = { indexes: {}, operations: [] };
1033
1053
  this.filteredChanges = { indexes: {}, operations: [] };
1034
1054
  }
1035
1055
  }
1036
1056
  setRoot(root) {
1037
1057
  this.root = root;
1038
- this.checkIsFiltered(this.parent, this.parentIndex);
1039
- //
1040
- // TODO: refactor and possibly unify .setRoot() and .setParent()
1041
- //
1058
+ const isNewChangeTree = this.root.add(this);
1059
+ this.checkIsFiltered(this.parent, this.parentIndex, isNewChangeTree);
1042
1060
  // Recursively set root on child structures
1043
- const metadata = this.ref.constructor[Symbol.metadata];
1044
- if (metadata) {
1045
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1046
- const field = metadata[index];
1047
- const changeTree = this.ref[field.name]?.[$changes];
1048
- if (changeTree) {
1049
- if (changeTree.root !== root) {
1050
- changeTree.setRoot(root);
1051
- }
1052
- else {
1053
- root.add(changeTree); // increment refCount
1054
- }
1055
- }
1056
- });
1057
- }
1058
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1059
- // MapSchema / ArraySchema, etc.
1060
- this.ref.forEach((value, key) => {
1061
- const changeTree = value[$changes];
1062
- if (changeTree.root !== root) {
1063
- changeTree.setRoot(root);
1061
+ if (isNewChangeTree) {
1062
+ this.forEachChild((child, _) => {
1063
+ if (child.root !== root) {
1064
+ child.setRoot(root);
1064
1065
  }
1065
1066
  else {
1066
- root.add(changeTree); // increment refCount
1067
+ root.add(child); // increment refCount
1067
1068
  }
1068
1069
  });
1069
1070
  }
1070
1071
  }
1071
1072
  setParent(parent, root, parentIndex) {
1072
- this.parent = parent;
1073
- this.parentIndex = parentIndex;
1073
+ this.addParent(parent, parentIndex);
1074
1074
  // avoid setting parents with empty `root`
1075
1075
  if (!root) {
1076
1076
  return;
1077
1077
  }
1078
+ const isNewChangeTree = root.add(this);
1078
1079
  // skip if parent is already set
1079
1080
  if (root !== this.root) {
1080
1081
  this.root = root;
1081
- this.checkIsFiltered(parent, parentIndex);
1082
- }
1083
- else {
1084
- root.add(this);
1082
+ this.checkIsFiltered(parent, parentIndex, isNewChangeTree);
1085
1083
  }
1086
1084
  // assign same parent on child structures
1087
- const metadata = this.ref.constructor[Symbol.metadata];
1088
- if (metadata) {
1089
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1090
- const field = metadata[index];
1091
- const changeTree = this.ref[field.name]?.[$changes];
1092
- if (changeTree && changeTree.root !== root) {
1093
- changeTree.setParent(this.ref, root, index);
1094
- }
1095
- });
1096
- }
1097
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1098
- // MapSchema / ArraySchema, etc.
1099
- this.ref.forEach((value, key) => {
1100
- const changeTree = value[$changes];
1101
- if (changeTree.root !== root) {
1102
- changeTree.setParent(this.ref, root, this.indexes[key] ?? key);
1085
+ if (isNewChangeTree) {
1086
+ //
1087
+ // assign same parent on child structures
1088
+ //
1089
+ this.forEachChild((child, index) => {
1090
+ if (child.root === root) {
1091
+ //
1092
+ // re-assigning a child of the same root, move it to the end
1093
+ // of the changes queue so encoding order is preserved
1094
+ //
1095
+ root.add(child);
1096
+ root.moveToEndOfChanges(child);
1097
+ return;
1103
1098
  }
1099
+ child.setParent(this.ref, root, index);
1104
1100
  });
1105
1101
  }
1106
1102
  }
@@ -1108,21 +1104,23 @@
1108
1104
  //
1109
1105
  // assign same parent on child structures
1110
1106
  //
1111
- const metadata = this.ref.constructor[Symbol.metadata];
1112
- if (metadata) {
1113
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1114
- const field = metadata[index];
1115
- const value = this.ref[field.name];
1116
- if (value) {
1117
- callback(value[$changes], index);
1107
+ if (this.ref[$childType]) {
1108
+ if (typeof (this.ref[$childType]) !== "string") {
1109
+ // MapSchema / ArraySchema, etc.
1110
+ for (const [key, value] of this.ref.entries()) {
1111
+ callback(value[$changes], key);
1118
1112
  }
1119
- });
1113
+ }
1120
1114
  }
1121
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1122
- // MapSchema / ArraySchema, etc.
1123
- this.ref.forEach((value, key) => {
1124
- callback(value[$changes], this.indexes[key] ?? key);
1125
- });
1115
+ else {
1116
+ for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {
1117
+ const field = this.metadata[index];
1118
+ const value = this.ref[field.name];
1119
+ if (!value) {
1120
+ continue;
1121
+ }
1122
+ callback(value[$changes], index);
1123
+ }
1126
1124
  }
1127
1125
  }
1128
1126
  operation(op) {
@@ -1138,8 +1136,7 @@
1138
1136
  }
1139
1137
  }
1140
1138
  change(index, operation = exports.OPERATION.ADD) {
1141
- const metadata = this.ref.constructor[Symbol.metadata];
1142
- const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
1139
+ const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);
1143
1140
  const changeSet = (isFiltered)
1144
1141
  ? this.filteredChanges
1145
1142
  : this.changes;
@@ -1229,19 +1226,16 @@
1229
1226
  }
1230
1227
  }
1231
1228
  getType(index) {
1232
- if (Metadata.isValidInstance(this.ref)) {
1233
- const metadata = this.ref.constructor[Symbol.metadata];
1234
- return metadata[index].type;
1235
- }
1236
- else {
1237
- //
1238
- // Get the child type from parent structure.
1239
- // - ["string"] => "string"
1240
- // - { map: "string" } => "string"
1241
- // - { set: "string" } => "string"
1242
- //
1243
- return this.ref[$childType];
1244
- }
1229
+ return (
1230
+ //
1231
+ // Get the child type from parent structure.
1232
+ // - ["string"] => "string"
1233
+ // - { map: "string" } => "string"
1234
+ // - { set: "string" } => "string"
1235
+ //
1236
+ this.ref[$childType] || // ArraySchema | MapSchema | SetSchema | CollectionSchema
1237
+ this.metadata[index].type // Schema
1238
+ );
1245
1239
  }
1246
1240
  getChange(index) {
1247
1241
  return this.indexedOperations[index];
@@ -1301,9 +1295,7 @@
1301
1295
  endEncode(changeSetName) {
1302
1296
  this.indexedOperations = {};
1303
1297
  // clear changeset
1304
- this[changeSetName].indexes = {};
1305
- this[changeSetName].operations.length = 0;
1306
- this[changeSetName].queueRootIndex = undefined;
1298
+ this[changeSetName] = createChangeSet();
1307
1299
  // ArraySchema and MapSchema have a custom "encode end" method
1308
1300
  this.ref[$onEncodeEnd]?.();
1309
1301
  // Not a new instance anymore
@@ -1317,20 +1309,14 @@
1317
1309
  //
1318
1310
  this.ref[$onEncodeEnd]?.();
1319
1311
  this.indexedOperations = {};
1320
- this.changes.indexes = {};
1321
- this.changes.operations.length = 0;
1322
- this.changes.queueRootIndex = undefined;
1312
+ this.changes = createChangeSet();
1323
1313
  if (this.filteredChanges !== undefined) {
1324
- this.filteredChanges.indexes = {};
1325
- this.filteredChanges.operations.length = 0;
1326
- this.filteredChanges.queueRootIndex = undefined;
1314
+ this.filteredChanges = createChangeSet();
1327
1315
  }
1328
1316
  if (discardAll) {
1329
- this.allChanges.indexes = {};
1330
- this.allChanges.operations.length = 0;
1317
+ this.allChanges = createChangeSet();
1331
1318
  if (this.allFilteredChanges !== undefined) {
1332
- this.allFilteredChanges.indexes = {};
1333
- this.allFilteredChanges.operations.length = 0;
1319
+ this.allFilteredChanges = createChangeSet();
1334
1320
  }
1335
1321
  }
1336
1322
  }
@@ -1348,18 +1334,10 @@
1348
1334
  }
1349
1335
  this.discard();
1350
1336
  }
1351
- ensureRefId() {
1352
- // skip if refId is already set.
1353
- if (this.refId !== undefined) {
1354
- return;
1355
- }
1356
- this.refId = this.root.getNextUniqueId();
1357
- }
1358
1337
  get changed() {
1359
1338
  return (Object.entries(this.indexedOperations).length > 0);
1360
1339
  }
1361
- checkIsFiltered(parent, parentIndex) {
1362
- const isNewChangeTree = this.root.add(this);
1340
+ checkIsFiltered(parent, parentIndex, isNewChangeTree) {
1363
1341
  if (this.root.types.hasFilters) {
1364
1342
  //
1365
1343
  // At Schema initialization, the "root" structure might not be available
@@ -1371,14 +1349,14 @@
1371
1349
  if (this.filteredChanges !== undefined) {
1372
1350
  enqueueChangeTree(this.root, this, 'filteredChanges');
1373
1351
  if (isNewChangeTree) {
1374
- this.root.allFilteredChanges.push(this);
1352
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
1375
1353
  }
1376
1354
  }
1377
1355
  }
1378
1356
  if (!this.isFiltered) {
1379
1357
  enqueueChangeTree(this.root, this, 'changes');
1380
1358
  if (isNewChangeTree) {
1381
- this.root.allChanges.push(this);
1359
+ enqueueChangeTree(this.root, this, 'allChanges');
1382
1360
  }
1383
1361
  }
1384
1362
  }
@@ -1435,6 +1413,90 @@
1435
1413
  }
1436
1414
  }
1437
1415
  }
1416
+ /**
1417
+ * Get the immediate parent
1418
+ */
1419
+ get parent() {
1420
+ return this.parentChain?.ref;
1421
+ }
1422
+ /**
1423
+ * Get the immediate parent index
1424
+ */
1425
+ get parentIndex() {
1426
+ return this.parentChain?.index;
1427
+ }
1428
+ /**
1429
+ * Add a parent to the chain
1430
+ */
1431
+ addParent(parent, index) {
1432
+ // Check if this parent already exists in the chain
1433
+ if (this.hasParent((p, i) => p[$changes] === parent[$changes] && i === index)) {
1434
+ return;
1435
+ }
1436
+ this.parentChain = {
1437
+ ref: parent,
1438
+ index,
1439
+ next: this.parentChain
1440
+ };
1441
+ }
1442
+ /**
1443
+ * Remove a parent from the chain
1444
+ * @param parent - The parent to remove
1445
+ * @returns true if parent was removed
1446
+ */
1447
+ removeParent(parent = this.parent) {
1448
+ let current = this.parentChain;
1449
+ let previous = null;
1450
+ while (current) {
1451
+ //
1452
+ // FIXME: it is required to check against `$changes` here because
1453
+ // ArraySchema is instance of Proxy
1454
+ //
1455
+ if (current.ref[$changes] === parent[$changes]) {
1456
+ if (previous) {
1457
+ previous.next = current.next;
1458
+ }
1459
+ else {
1460
+ this.parentChain = current.next;
1461
+ }
1462
+ return true;
1463
+ }
1464
+ previous = current;
1465
+ current = current.next;
1466
+ }
1467
+ return this.parentChain === undefined;
1468
+ }
1469
+ /**
1470
+ * Find a specific parent in the chain
1471
+ */
1472
+ findParent(predicate) {
1473
+ let current = this.parentChain;
1474
+ while (current) {
1475
+ if (predicate(current.ref, current.index)) {
1476
+ return current;
1477
+ }
1478
+ current = current.next;
1479
+ }
1480
+ return undefined;
1481
+ }
1482
+ /**
1483
+ * Check if this ChangeTree has a specific parent
1484
+ */
1485
+ hasParent(predicate) {
1486
+ return this.findParent(predicate) !== undefined;
1487
+ }
1488
+ /**
1489
+ * Get all parents as an array (for debugging/testing)
1490
+ */
1491
+ getAllParents() {
1492
+ const parents = [];
1493
+ let current = this.parentChain;
1494
+ while (current) {
1495
+ parents.push({ ref: current.ref, index: current.index });
1496
+ current = current.next;
1497
+ }
1498
+ return parents;
1499
+ }
1438
1500
  }
1439
1501
 
1440
1502
  function encodeValue(encoder, bytes, type, value, operation, it) {
@@ -1952,7 +2014,7 @@
1952
2014
  }
1953
2015
  if (previousValue !== undefined) {
1954
2016
  // remove root reference from previous value
1955
- previousValue[$changes].root?.remove(previousValue[$changes]);
2017
+ previousValue[$changes].root?.remove(previousValue[$changes], obj);
1956
2018
  }
1957
2019
  }
1958
2020
  else {
@@ -1983,8 +2045,11 @@
1983
2045
  return Reflect.has(obj, key);
1984
2046
  }
1985
2047
  });
1986
- this[$changes] = new ChangeTree(proxy);
1987
- this[$changes].indexes = {};
2048
+ Object.defineProperty(this, $changes, {
2049
+ value: new ChangeTree(proxy),
2050
+ enumerable: false,
2051
+ writable: true,
2052
+ });
1988
2053
  if (items.length > 0) {
1989
2054
  this.push(...items);
1990
2055
  }
@@ -2106,7 +2171,7 @@
2106
2171
  const changeTree = this[$changes];
2107
2172
  // remove children references
2108
2173
  changeTree.forEachChild((childChangeTree, _) => {
2109
- changeTree.root?.remove(childChangeTree);
2174
+ changeTree.root?.remove(childChangeTree, this);
2110
2175
  });
2111
2176
  changeTree.discard(true);
2112
2177
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -2145,7 +2210,6 @@
2145
2210
  if (this.items.length === 0) {
2146
2211
  return undefined;
2147
2212
  }
2148
- // const index = Number(Object.keys(changeTree.indexes)[0]);
2149
2213
  const changeTree = this[$changes];
2150
2214
  const index = this.tmpItems.findIndex(item => item === this.items[0]);
2151
2215
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
@@ -2604,8 +2668,13 @@
2604
2668
  this.$items = new Map();
2605
2669
  this.$indexes = new Map();
2606
2670
  this.deletedItems = {};
2607
- this[$changes] = new ChangeTree(this);
2608
- this[$changes].indexes = {};
2671
+ const changeTree = new ChangeTree(this);
2672
+ changeTree.indexes = {};
2673
+ Object.defineProperty(this, $changes, {
2674
+ value: changeTree,
2675
+ enumerable: false,
2676
+ writable: true,
2677
+ });
2609
2678
  if (initialValues) {
2610
2679
  if (initialValues instanceof Map ||
2611
2680
  initialValues instanceof MapSchema) {
@@ -2656,7 +2725,7 @@
2656
2725
  operation = exports.OPERATION.DELETE_AND_ADD;
2657
2726
  // remove reference from previous value
2658
2727
  if (previousValue !== undefined) {
2659
- previousValue[$changes].root?.remove(previousValue[$changes]);
2728
+ previousValue[$changes].root?.remove(previousValue[$changes], this);
2660
2729
  }
2661
2730
  }
2662
2731
  }
@@ -2693,7 +2762,7 @@
2693
2762
  changeTree.indexes = {};
2694
2763
  // remove children references
2695
2764
  changeTree.forEachChild((childChangeTree, _) => {
2696
- changeTree.root?.remove(childChangeTree);
2765
+ changeTree.root?.remove(childChangeTree, this);
2697
2766
  });
2698
2767
  // clear previous indexes
2699
2768
  this.$indexes.clear();
@@ -3149,10 +3218,13 @@
3149
3218
  refs: []
3150
3219
  };
3151
3220
  // for (const refId in $root.changes) {
3152
- $root.changes.forEach(changeTree => {
3221
+ let current = $root.changes.next;
3222
+ while (current) {
3223
+ const changeTree = current.changeTree;
3153
3224
  // skip if ChangeTree is undefined
3154
3225
  if (changeTree === undefined) {
3155
- return;
3226
+ current = current.next;
3227
+ continue;
3156
3228
  }
3157
3229
  const changes = changeTree.indexedOperations;
3158
3230
  dump.refs.push(`refId#${changeTree.refId}`);
@@ -3164,7 +3236,8 @@
3164
3236
  }
3165
3237
  dump.ops[exports.OPERATION[op]]++;
3166
3238
  }
3167
- });
3239
+ current = current.next;
3240
+ }
3168
3241
  return dump;
3169
3242
  }
3170
3243
 
@@ -3314,7 +3387,7 @@
3314
3387
  * @param showContents display JSON contents of the instance
3315
3388
  * @returns
3316
3389
  */
3317
- static debugRefIds(ref, showContents = false, level = 0, decoder) {
3390
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3318
3391
  const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3319
3392
  const changeTree = ref[$changes];
3320
3393
  const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
@@ -3323,11 +3396,25 @@
3323
3396
  const refCount = (root?.refCount?.[refId] > 1)
3324
3397
  ? ` [×${root.refCount[refId]}]`
3325
3398
  : '';
3326
- let output = `${getIndent(level)}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3327
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder));
3399
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3400
+ changeTree.forEachChild((childChangeTree, key) => {
3401
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3402
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3403
+ });
3328
3404
  return output;
3329
3405
  }
3330
- static debugRefIdsDecoder(decoder) {
3406
+ static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3407
+ let encodeOrder = [];
3408
+ let current = ref[$changes].root[changeSet].next;
3409
+ while (current) {
3410
+ if (current.changeTree) {
3411
+ encodeOrder.push(current.changeTree.refId);
3412
+ }
3413
+ current = current.next;
3414
+ }
3415
+ return encodeOrder;
3416
+ }
3417
+ static debugRefIdsFromDecoder(decoder) {
3331
3418
  return this.debugRefIds(decoder.state, false, 0, decoder);
3332
3419
  }
3333
3420
  /**
@@ -3376,8 +3463,12 @@
3376
3463
  const changeTrees = new Map();
3377
3464
  const instanceRefIds = [];
3378
3465
  let totalOperations = 0;
3466
+ // TODO: FIXME: this method is not working as expected
3379
3467
  for (const [refId, changes] of Object.entries(root[changeSetName])) {
3380
3468
  const changeTree = root.changeTrees[refId];
3469
+ if (!changeTree) {
3470
+ continue;
3471
+ }
3381
3472
  let includeChangeTree = false;
3382
3473
  let parentChangeTrees = [];
3383
3474
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3513,7 +3604,7 @@
3513
3604
  changeTree.indexes = {};
3514
3605
  // remove children references
3515
3606
  changeTree.forEachChild((childChangeTree, _) => {
3516
- changeTree.root?.remove(childChangeTree);
3607
+ changeTree.root?.remove(childChangeTree, this);
3517
3608
  });
3518
3609
  // clear previous indexes
3519
3610
  this.$indexes.clear();
@@ -3800,18 +3891,20 @@
3800
3891
  this.refCount = {};
3801
3892
  this.changeTrees = {};
3802
3893
  // all changes
3803
- this.allChanges = [];
3804
- this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3894
+ this.allChanges = createChangeTreeList();
3895
+ this.allFilteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3805
3896
  // pending changes to be encoded
3806
- this.changes = [];
3807
- this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3897
+ this.changes = createChangeTreeList();
3898
+ this.filteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3808
3899
  }
3809
3900
  getNextUniqueId() {
3810
3901
  return this.nextUniqueId++;
3811
3902
  }
3812
3903
  add(changeTree) {
3813
- // FIXME: move implementation of `ensureRefId` to `Root` class
3814
- changeTree.ensureRefId();
3904
+ // Assign unique `refId` to changeTree if it doesn't have one yet.
3905
+ if (changeTree.refId === undefined) {
3906
+ changeTree.refId = this.getNextUniqueId();
3907
+ }
3815
3908
  const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3816
3909
  if (isNewChangeTree) {
3817
3910
  this.changeTrees[changeTree.refId] = changeTree;
@@ -3830,10 +3923,12 @@
3830
3923
  }
3831
3924
  }
3832
3925
  this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3926
+ // console.log("ADD", { refId: changeTree.refId, refCount: this.refCount[changeTree.refId] });
3833
3927
  return isNewChangeTree;
3834
3928
  }
3835
- remove(changeTree) {
3929
+ remove(changeTree, parent) {
3836
3930
  const refCount = (this.refCount[changeTree.refId]) - 1;
3931
+ // console.log("REMOVE", { refId: changeTree.refId, refCount });
3837
3932
  if (refCount <= 0) {
3838
3933
  //
3839
3934
  // Only remove "root" reference if it's the last reference
@@ -3847,7 +3942,19 @@
3847
3942
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
3848
3943
  }
3849
3944
  this.refCount[changeTree.refId] = 0;
3850
- changeTree.forEachChild((child, _) => this.remove(child));
3945
+ changeTree.forEachChild((child, _) => {
3946
+ if (child.removeParent(changeTree.ref)) {
3947
+ if ((child.parentChain === undefined || // no parent, remove it
3948
+ (child.parentChain && this.refCount[child.refId] > 1) // parent is still in use, but has more than one reference, remove it
3949
+ )) {
3950
+ this.remove(child, changeTree.ref);
3951
+ }
3952
+ else if (child.parentChain) {
3953
+ // re-assigning a child of the same root, move it to the end
3954
+ this.moveToEndOfChanges(child);
3955
+ }
3956
+ }
3957
+ });
3851
3958
  }
3852
3959
  else {
3853
3960
  this.refCount[changeTree.refId] = refCount;
@@ -3860,32 +3967,73 @@
3860
3967
  // containing instance is not available, the Decoder will throw
3861
3968
  // "refId not found" error.
3862
3969
  //
3863
- if (changeTree.filteredChanges !== undefined) {
3864
- this.removeChangeFromChangeSet("filteredChanges", changeTree);
3865
- enqueueChangeTree(this, changeTree, "filteredChanges");
3866
- }
3867
- else {
3868
- this.removeChangeFromChangeSet("changes", changeTree);
3869
- enqueueChangeTree(this, changeTree, "changes");
3870
- }
3970
+ this.moveToEndOfChanges(changeTree);
3971
+ changeTree.forEachChild((child, _) => this.moveToEndOfChanges(child));
3871
3972
  }
3872
3973
  return refCount;
3873
3974
  }
3975
+ moveToEndOfChanges(changeTree) {
3976
+ if (changeTree.filteredChanges) {
3977
+ this.moveToEndOfChangeTreeList("filteredChanges", changeTree);
3978
+ this.moveToEndOfChangeTreeList("allFilteredChanges", changeTree);
3979
+ }
3980
+ else {
3981
+ this.moveToEndOfChangeTreeList("changes", changeTree);
3982
+ this.moveToEndOfChangeTreeList("allChanges", changeTree);
3983
+ }
3984
+ }
3985
+ moveToEndOfChangeTreeList(changeSetName, changeTree) {
3986
+ const changeSet = this[changeSetName];
3987
+ const node = changeTree[changeSetName].queueRootNode;
3988
+ if (!node || node === changeSet.tail)
3989
+ return;
3990
+ // Remove from current position
3991
+ if (node.prev) {
3992
+ node.prev.next = node.next;
3993
+ }
3994
+ else {
3995
+ changeSet.next = node.next;
3996
+ }
3997
+ if (node.next) {
3998
+ node.next.prev = node.prev;
3999
+ }
4000
+ else {
4001
+ changeSet.tail = node.prev;
4002
+ }
4003
+ // Add to end
4004
+ node.prev = changeSet.tail;
4005
+ node.next = undefined;
4006
+ if (changeSet.tail) {
4007
+ changeSet.tail.next = node;
4008
+ }
4009
+ else {
4010
+ changeSet.next = node;
4011
+ }
4012
+ changeSet.tail = node;
4013
+ }
3874
4014
  removeChangeFromChangeSet(changeSetName, changeTree) {
3875
4015
  const changeSet = this[changeSetName];
3876
- const changeSetIndex = changeSet.indexOf(changeTree);
3877
- if (changeSetIndex !== -1) {
3878
- changeTree[changeSetName].queueRootIndex = -1;
3879
- changeSet[changeSetIndex] = undefined;
4016
+ const node = changeTree[changeSetName].queueRootNode;
4017
+ if (node && node.changeTree === changeTree) {
4018
+ // Remove the node from the linked list
4019
+ if (node.prev) {
4020
+ node.prev.next = node.next;
4021
+ }
4022
+ else {
4023
+ changeSet.next = node.next;
4024
+ }
4025
+ if (node.next) {
4026
+ node.next.prev = node.prev;
4027
+ }
4028
+ else {
4029
+ changeSet.tail = node.prev;
4030
+ }
4031
+ changeSet.length--;
4032
+ // Clear ChangeTree reference
4033
+ changeTree[changeSetName].queueRootNode = undefined;
3880
4034
  return true;
3881
4035
  }
3882
- // if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
3883
- // changeTree[changeSetName].queueRootIndex = -1;
3884
- // return true;
3885
- // }
3886
- }
3887
- clear() {
3888
- this.changes.length = 0;
4036
+ return false;
3889
4037
  }
3890
4038
  }
3891
4039
 
@@ -3915,12 +4063,9 @@
3915
4063
  ) {
3916
4064
  const hasView = (view !== undefined);
3917
4065
  const rootChangeTree = this.state[$changes];
3918
- const changeTrees = this.root[changeSetName];
3919
- for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3920
- const changeTree = changeTrees[i];
3921
- if (!changeTree) {
3922
- continue;
3923
- }
4066
+ let current = this.root[changeSetName];
4067
+ while (current = current.next) {
4068
+ const changeTree = current.changeTree;
3924
4069
  if (hasView) {
3925
4070
  if (!view.isChangeTreeVisible(changeTree)) {
3926
4071
  // console.log("MARK AS INVISIBLE:", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, raw: changeTree.ref.toJSON() });
@@ -4009,7 +4154,9 @@
4009
4154
  const rootChangeSet = (typeof (field) === "string")
4010
4155
  ? this.root[field]
4011
4156
  : field;
4012
- rootChangeSet.forEach((changeTree) => {
4157
+ let current = rootChangeSet.next;
4158
+ while (current) {
4159
+ const changeTree = current.changeTree;
4013
4160
  const changeSet = changeTree[field];
4014
4161
  const metadata = changeTree.ref.constructor[Symbol.metadata];
4015
4162
  console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
@@ -4021,7 +4168,8 @@
4021
4168
  op: exports.OPERATION[op],
4022
4169
  });
4023
4170
  }
4024
- });
4171
+ current = current.next;
4172
+ }
4025
4173
  }
4026
4174
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4027
4175
  const viewOffset = it.offset;
@@ -4071,21 +4219,19 @@
4071
4219
  }
4072
4220
  discardChanges() {
4073
4221
  // discard shared changes
4074
- let length = this.root.changes.length;
4075
- if (length > 0) {
4076
- while (length--) {
4077
- this.root.changes[length]?.endEncode('changes');
4078
- }
4079
- this.root.changes.length = 0;
4222
+ let current = this.root.changes.next;
4223
+ while (current) {
4224
+ current.changeTree.endEncode('changes');
4225
+ current = current.next;
4080
4226
  }
4227
+ this.root.changes = createChangeTreeList();
4081
4228
  // discard filtered changes
4082
- length = this.root.filteredChanges.length;
4083
- if (length > 0) {
4084
- while (length--) {
4085
- this.root.filteredChanges[length]?.endEncode('filteredChanges');
4086
- }
4087
- this.root.filteredChanges.length = 0;
4229
+ current = this.root.filteredChanges.next;
4230
+ while (current) {
4231
+ current.changeTree.endEncode('filteredChanges');
4232
+ current = current.next;
4088
4233
  }
4234
+ this.root.filteredChanges = createChangeTreeList();
4089
4235
  }
4090
4236
  tryEncodeTypeId(bytes, baseType, targetType, it) {
4091
4237
  const baseTypeId = this.context.getTypeId(baseType);
@@ -4275,21 +4421,20 @@
4275
4421
  //
4276
4422
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
4277
4423
  it.offset++;
4424
+ ref[$onDecodeEnd]?.();
4278
4425
  const nextRefId = decode.number(bytes, it);
4279
4426
  const nextRef = $root.refs.get(nextRefId);
4280
4427
  //
4281
4428
  // Trying to access a reference that haven't been decoded yet.
4282
4429
  //
4283
4430
  if (!nextRef) {
4284
- console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
4285
- console.warn("Please report this to the developers. All refIds =>");
4286
- console.warn(Schema.debugRefIdsDecoder(this));
4287
- this.skipCurrentStructure(bytes, it, totalBytes);
4431
+ throw new Error(`"refId" not found: ${nextRefId}`);
4432
+ }
4433
+ else {
4434
+ ref = nextRef;
4435
+ decoder = ref.constructor[$decoder];
4436
+ this.currentRefId = nextRefId;
4288
4437
  }
4289
- ref[$onDecodeEnd]?.();
4290
- this.currentRefId = nextRefId;
4291
- ref = nextRef;
4292
- decoder = ref.constructor[$decoder];
4293
4438
  continue;
4294
4439
  }
4295
4440
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -4333,10 +4478,6 @@
4333
4478
  return type || defaultType;
4334
4479
  }
4335
4480
  createInstanceOfType(type) {
4336
- // let instance: Schema = new (type as any)();
4337
- // // assign root on $changes
4338
- // instance[$changes].root = this.root[$changes].root;
4339
- // return instance;
4340
4481
  return new type();
4341
4482
  }
4342
4483
  removeChildRefs(ref, allChanges) {
@@ -4863,11 +5004,12 @@
4863
5004
  // TODO: allow to set multiple tags at once
4864
5005
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
4865
5006
  const changeTree = obj?.[$changes];
5007
+ const parentChangeTree = changeTree.parent;
4866
5008
  if (!changeTree) {
4867
5009
  console.warn("StateView#add(), invalid object:", obj);
4868
- return this;
5010
+ return false;
4869
5011
  }
4870
- else if (!changeTree.parent &&
5012
+ else if (!parentChangeTree &&
4871
5013
  changeTree.refId !== 0 // allow root object
4872
5014
  ) {
4873
5015
  /**
@@ -4889,18 +5031,31 @@
4889
5031
  // add parent ChangeTree's
4890
5032
  // - if it was invisible to this view
4891
5033
  // - if it were previously filtered out
4892
- if (checkIncludeParent && changeTree.parent) {
5034
+ if (checkIncludeParent && parentChangeTree) {
4893
5035
  this.addParentOf(changeTree, tag);
4894
5036
  }
4895
- //
4896
- // TODO: when adding an item of a MapSchema, the changes may not
4897
- // be set (only the parent's changes are set)
4898
- //
4899
5037
  let changes = this.changes.get(changeTree.refId);
4900
5038
  if (changes === undefined) {
4901
5039
  changes = {};
5040
+ // FIXME / OPTIMIZE: do not add if no changes are needed
4902
5041
  this.changes.set(changeTree.refId, changes);
4903
5042
  }
5043
+ let isChildAdded = false;
5044
+ //
5045
+ // Add children of this ChangeTree first.
5046
+ // If successful, we must link the current ChangeTree to the child.
5047
+ //
5048
+ changeTree.forEachChild((change, index) => {
5049
+ // Do not ADD children that don't have the same tag
5050
+ if (metadata &&
5051
+ metadata[index].tag !== undefined &&
5052
+ metadata[index].tag !== tag) {
5053
+ return;
5054
+ }
5055
+ if (this.add(change.ref, tag, false)) {
5056
+ isChildAdded = true;
5057
+ }
5058
+ });
4904
5059
  // set tag
4905
5060
  if (tag !== DEFAULT_VIEW_TAG) {
4906
5061
  if (!this.tags) {
@@ -4922,11 +5077,12 @@
4922
5077
  }
4923
5078
  });
4924
5079
  }
4925
- else {
4926
- const isInvisible = this.invisible.has(changeTree);
5080
+ else if (!changeTree.isNew || isChildAdded) {
5081
+ // new structures will be added as part of .encode() call, no need to force it to .encodeView()
4927
5082
  const changeSet = (changeTree.filteredChanges !== undefined)
4928
5083
  ? changeTree.allFilteredChanges
4929
5084
  : changeTree.allChanges;
5085
+ const isInvisible = this.invisible.has(changeTree);
4930
5086
  for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4931
5087
  const index = changeSet.operations[i];
4932
5088
  if (index === undefined) {
@@ -4934,27 +5090,17 @@
4934
5090
  } // skip "undefined" indexes
4935
5091
  const op = changeTree.indexedOperations[index] ?? exports.OPERATION.ADD;
4936
5092
  const tagAtIndex = metadata?.[index].tag;
4937
- if (!changeTree.isNew && // new structures will be added as part of .encode() call, no need to force it to .encodeView()
5093
+ if (op !== exports.OPERATION.DELETE &&
4938
5094
  (isInvisible || // if "invisible", include all
4939
5095
  tagAtIndex === undefined || // "all change" with no tag
4940
5096
  tagAtIndex === tag // tagged property
4941
- ) &&
4942
- op !== exports.OPERATION.DELETE) {
5097
+ )) {
4943
5098
  changes[index] = op;
5099
+ isChildAdded = true; // FIXME: assign only once
4944
5100
  }
4945
5101
  }
4946
5102
  }
4947
- // Add children of this ChangeTree to this view
4948
- changeTree.forEachChild((change, index) => {
4949
- // Do not ADD children that don't have the same tag
4950
- if (metadata &&
4951
- metadata[index].tag !== undefined &&
4952
- metadata[index].tag !== tag) {
4953
- return;
4954
- }
4955
- this.add(change.ref, tag, false);
4956
- });
4957
- return this;
5103
+ return isChildAdded;
4958
5104
  }
4959
5105
  addParentOf(childChangeTree, tag) {
4960
5106
  const changeTree = childChangeTree.parent[$changes];