@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
@@ -28,37 +28,37 @@ exports.OPERATION = void 0;
28
28
 
29
29
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
30
30
 
31
- const $track = Symbol("$track");
32
- const $encoder = Symbol("$encoder");
33
- const $decoder = Symbol("$decoder");
34
- const $filter = Symbol("$filter");
35
- const $getByIndex = Symbol("$getByIndex");
36
- const $deleteByIndex = Symbol("$deleteByIndex");
31
+ const $track = "~track";
32
+ const $encoder = "~encoder";
33
+ const $decoder = "~decoder";
34
+ const $filter = "~filter";
35
+ const $getByIndex = "~getByIndex";
36
+ const $deleteByIndex = "~deleteByIndex";
37
37
  /**
38
38
  * Used to hold ChangeTree instances whitin the structures
39
39
  */
40
- const $changes = Symbol('$changes');
40
+ const $changes = '~changes';
41
41
  /**
42
42
  * Used to keep track of the type of the child elements of a collection
43
43
  * (MapSchema, ArraySchema, etc.)
44
44
  */
45
- const $childType = Symbol('$childType');
45
+ const $childType = '~childType';
46
46
  /**
47
47
  * Optional "discard" method for custom types (ArraySchema)
48
48
  * (Discards changes for next serialization)
49
49
  */
50
- const $onEncodeEnd = Symbol('$onEncodeEnd');
50
+ const $onEncodeEnd = '~onEncodeEnd';
51
51
  /**
52
52
  * When decoding, this method is called after the instance is fully decoded
53
53
  */
54
- const $onDecodeEnd = Symbol("$onDecodeEnd");
54
+ const $onDecodeEnd = "~onDecodeEnd";
55
55
  /**
56
56
  * Metadata
57
57
  */
58
- const $descriptors = Symbol("$descriptors");
59
- const $numFields = "$__numFields";
60
- const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
61
- const $viewFieldIndexes = "$__viewFieldIndexes";
58
+ const $descriptors = "~descriptors";
59
+ const $numFields = "~__numFields";
60
+ const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
61
+ const $viewFieldIndexes = "~__viewFieldIndexes";
62
62
  const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
63
63
 
64
64
  /**
@@ -966,6 +966,24 @@ const Metadata = {
966
966
  function createChangeSet() {
967
967
  return { indexes: {}, operations: [] };
968
968
  }
969
+ // Linked list helper functions
970
+ function createChangeTreeList() {
971
+ return { next: undefined, tail: undefined, length: 0 };
972
+ }
973
+ function addToChangeTreeList(list, changeTree) {
974
+ const node = { changeTree, next: undefined, prev: undefined };
975
+ if (!list.next) {
976
+ list.next = node;
977
+ list.tail = node;
978
+ }
979
+ else {
980
+ node.prev = list.tail;
981
+ list.tail.next = node;
982
+ list.tail = node;
983
+ }
984
+ list.length++;
985
+ return node;
986
+ }
969
987
  function setOperationAtIndex(changeSet, index) {
970
988
  const operationsIndex = changeSet.indexes[index];
971
989
  if (operationsIndex === undefined) {
@@ -990,13 +1008,15 @@ function deleteOperationAtIndex(changeSet, index) {
990
1008
  changeSet.operations[operationsIndex] = undefined;
991
1009
  delete changeSet.indexes[index];
992
1010
  }
993
- function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
1011
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
1012
+ // skip
994
1013
  if (!root) {
995
- // skip
996
1014
  return;
997
1015
  }
998
- else if (root[changeSet][queueRootIndex] !== changeTree) {
999
- changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
1016
+ if (queueRootNode) ;
1017
+ else {
1018
+ // Add to linked list if not already present
1019
+ changeTree[changeSet].queueRootNode = addToChangeTreeList(root[changeSet], changeTree);
1000
1020
  }
1001
1021
  }
1002
1022
  class ChangeTree {
@@ -1020,83 +1040,59 @@ class ChangeTree {
1020
1040
  */
1021
1041
  this.isNew = true;
1022
1042
  this.ref = ref;
1043
+ this.metadata = ref.constructor[Symbol.metadata];
1023
1044
  //
1024
1045
  // Does this structure have "filters" declared?
1025
1046
  //
1026
- const metadata = ref.constructor[Symbol.metadata];
1027
- if (metadata?.[$viewFieldIndexes]) {
1047
+ if (this.metadata?.[$viewFieldIndexes]) {
1028
1048
  this.allFilteredChanges = { indexes: {}, operations: [] };
1029
1049
  this.filteredChanges = { indexes: {}, operations: [] };
1030
1050
  }
1031
1051
  }
1032
1052
  setRoot(root) {
1033
1053
  this.root = root;
1034
- this.checkIsFiltered(this.parent, this.parentIndex);
1035
- //
1036
- // TODO: refactor and possibly unify .setRoot() and .setParent()
1037
- //
1054
+ const isNewChangeTree = this.root.add(this);
1055
+ this.checkIsFiltered(this.parent, this.parentIndex, isNewChangeTree);
1038
1056
  // Recursively set root on child structures
1039
- const metadata = this.ref.constructor[Symbol.metadata];
1040
- if (metadata) {
1041
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1042
- const field = metadata[index];
1043
- const changeTree = this.ref[field.name]?.[$changes];
1044
- if (changeTree) {
1045
- if (changeTree.root !== root) {
1046
- changeTree.setRoot(root);
1047
- }
1048
- else {
1049
- root.add(changeTree); // increment refCount
1050
- }
1051
- }
1052
- });
1053
- }
1054
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1055
- // MapSchema / ArraySchema, etc.
1056
- this.ref.forEach((value, key) => {
1057
- const changeTree = value[$changes];
1058
- if (changeTree.root !== root) {
1059
- changeTree.setRoot(root);
1057
+ if (isNewChangeTree) {
1058
+ this.forEachChild((child, _) => {
1059
+ if (child.root !== root) {
1060
+ child.setRoot(root);
1060
1061
  }
1061
1062
  else {
1062
- root.add(changeTree); // increment refCount
1063
+ root.add(child); // increment refCount
1063
1064
  }
1064
1065
  });
1065
1066
  }
1066
1067
  }
1067
1068
  setParent(parent, root, parentIndex) {
1068
- this.parent = parent;
1069
- this.parentIndex = parentIndex;
1069
+ this.addParent(parent, parentIndex);
1070
1070
  // avoid setting parents with empty `root`
1071
1071
  if (!root) {
1072
1072
  return;
1073
1073
  }
1074
+ const isNewChangeTree = root.add(this);
1074
1075
  // skip if parent is already set
1075
1076
  if (root !== this.root) {
1076
1077
  this.root = root;
1077
- this.checkIsFiltered(parent, parentIndex);
1078
- }
1079
- else {
1080
- root.add(this);
1078
+ this.checkIsFiltered(parent, parentIndex, isNewChangeTree);
1081
1079
  }
1082
1080
  // assign same parent on child structures
1083
- const metadata = this.ref.constructor[Symbol.metadata];
1084
- if (metadata) {
1085
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1086
- const field = metadata[index];
1087
- const changeTree = this.ref[field.name]?.[$changes];
1088
- if (changeTree && changeTree.root !== root) {
1089
- changeTree.setParent(this.ref, root, index);
1090
- }
1091
- });
1092
- }
1093
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1094
- // MapSchema / ArraySchema, etc.
1095
- this.ref.forEach((value, key) => {
1096
- const changeTree = value[$changes];
1097
- if (changeTree.root !== root) {
1098
- changeTree.setParent(this.ref, root, this.indexes[key] ?? key);
1081
+ if (isNewChangeTree) {
1082
+ //
1083
+ // assign same parent on child structures
1084
+ //
1085
+ this.forEachChild((child, index) => {
1086
+ if (child.root === root) {
1087
+ //
1088
+ // re-assigning a child of the same root, move it to the end
1089
+ // of the changes queue so encoding order is preserved
1090
+ //
1091
+ root.add(child);
1092
+ root.moveToEndOfChanges(child);
1093
+ return;
1099
1094
  }
1095
+ child.setParent(this.ref, root, index);
1100
1096
  });
1101
1097
  }
1102
1098
  }
@@ -1104,21 +1100,23 @@ class ChangeTree {
1104
1100
  //
1105
1101
  // assign same parent on child structures
1106
1102
  //
1107
- const metadata = this.ref.constructor[Symbol.metadata];
1108
- if (metadata) {
1109
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1110
- const field = metadata[index];
1111
- const value = this.ref[field.name];
1112
- if (value) {
1113
- callback(value[$changes], index);
1103
+ if (this.ref[$childType]) {
1104
+ if (typeof (this.ref[$childType]) !== "string") {
1105
+ // MapSchema / ArraySchema, etc.
1106
+ for (const [key, value] of this.ref.entries()) {
1107
+ callback(value[$changes], key);
1114
1108
  }
1115
- });
1109
+ }
1116
1110
  }
1117
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1118
- // MapSchema / ArraySchema, etc.
1119
- this.ref.forEach((value, key) => {
1120
- callback(value[$changes], this.indexes[key] ?? key);
1121
- });
1111
+ else {
1112
+ for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {
1113
+ const field = this.metadata[index];
1114
+ const value = this.ref[field.name];
1115
+ if (!value) {
1116
+ continue;
1117
+ }
1118
+ callback(value[$changes], index);
1119
+ }
1122
1120
  }
1123
1121
  }
1124
1122
  operation(op) {
@@ -1134,8 +1132,7 @@ class ChangeTree {
1134
1132
  }
1135
1133
  }
1136
1134
  change(index, operation = exports.OPERATION.ADD) {
1137
- const metadata = this.ref.constructor[Symbol.metadata];
1138
- const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
1135
+ const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);
1139
1136
  const changeSet = (isFiltered)
1140
1137
  ? this.filteredChanges
1141
1138
  : this.changes;
@@ -1225,19 +1222,16 @@ class ChangeTree {
1225
1222
  }
1226
1223
  }
1227
1224
  getType(index) {
1228
- if (Metadata.isValidInstance(this.ref)) {
1229
- const metadata = this.ref.constructor[Symbol.metadata];
1230
- return metadata[index].type;
1231
- }
1232
- else {
1233
- //
1234
- // Get the child type from parent structure.
1235
- // - ["string"] => "string"
1236
- // - { map: "string" } => "string"
1237
- // - { set: "string" } => "string"
1238
- //
1239
- return this.ref[$childType];
1240
- }
1225
+ return (
1226
+ //
1227
+ // Get the child type from parent structure.
1228
+ // - ["string"] => "string"
1229
+ // - { map: "string" } => "string"
1230
+ // - { set: "string" } => "string"
1231
+ //
1232
+ this.ref[$childType] || // ArraySchema | MapSchema | SetSchema | CollectionSchema
1233
+ this.metadata[index].type // Schema
1234
+ );
1241
1235
  }
1242
1236
  getChange(index) {
1243
1237
  return this.indexedOperations[index];
@@ -1297,9 +1291,7 @@ class ChangeTree {
1297
1291
  endEncode(changeSetName) {
1298
1292
  this.indexedOperations = {};
1299
1293
  // clear changeset
1300
- this[changeSetName].indexes = {};
1301
- this[changeSetName].operations.length = 0;
1302
- this[changeSetName].queueRootIndex = undefined;
1294
+ this[changeSetName] = createChangeSet();
1303
1295
  // ArraySchema and MapSchema have a custom "encode end" method
1304
1296
  this.ref[$onEncodeEnd]?.();
1305
1297
  // Not a new instance anymore
@@ -1313,20 +1305,14 @@ class ChangeTree {
1313
1305
  //
1314
1306
  this.ref[$onEncodeEnd]?.();
1315
1307
  this.indexedOperations = {};
1316
- this.changes.indexes = {};
1317
- this.changes.operations.length = 0;
1318
- this.changes.queueRootIndex = undefined;
1308
+ this.changes = createChangeSet();
1319
1309
  if (this.filteredChanges !== undefined) {
1320
- this.filteredChanges.indexes = {};
1321
- this.filteredChanges.operations.length = 0;
1322
- this.filteredChanges.queueRootIndex = undefined;
1310
+ this.filteredChanges = createChangeSet();
1323
1311
  }
1324
1312
  if (discardAll) {
1325
- this.allChanges.indexes = {};
1326
- this.allChanges.operations.length = 0;
1313
+ this.allChanges = createChangeSet();
1327
1314
  if (this.allFilteredChanges !== undefined) {
1328
- this.allFilteredChanges.indexes = {};
1329
- this.allFilteredChanges.operations.length = 0;
1315
+ this.allFilteredChanges = createChangeSet();
1330
1316
  }
1331
1317
  }
1332
1318
  }
@@ -1344,18 +1330,10 @@ class ChangeTree {
1344
1330
  }
1345
1331
  this.discard();
1346
1332
  }
1347
- ensureRefId() {
1348
- // skip if refId is already set.
1349
- if (this.refId !== undefined) {
1350
- return;
1351
- }
1352
- this.refId = this.root.getNextUniqueId();
1353
- }
1354
1333
  get changed() {
1355
1334
  return (Object.entries(this.indexedOperations).length > 0);
1356
1335
  }
1357
- checkIsFiltered(parent, parentIndex) {
1358
- const isNewChangeTree = this.root.add(this);
1336
+ checkIsFiltered(parent, parentIndex, isNewChangeTree) {
1359
1337
  if (this.root.types.hasFilters) {
1360
1338
  //
1361
1339
  // At Schema initialization, the "root" structure might not be available
@@ -1367,14 +1345,14 @@ class ChangeTree {
1367
1345
  if (this.filteredChanges !== undefined) {
1368
1346
  enqueueChangeTree(this.root, this, 'filteredChanges');
1369
1347
  if (isNewChangeTree) {
1370
- this.root.allFilteredChanges.push(this);
1348
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
1371
1349
  }
1372
1350
  }
1373
1351
  }
1374
1352
  if (!this.isFiltered) {
1375
1353
  enqueueChangeTree(this.root, this, 'changes');
1376
1354
  if (isNewChangeTree) {
1377
- this.root.allChanges.push(this);
1355
+ enqueueChangeTree(this.root, this, 'allChanges');
1378
1356
  }
1379
1357
  }
1380
1358
  }
@@ -1431,6 +1409,90 @@ class ChangeTree {
1431
1409
  }
1432
1410
  }
1433
1411
  }
1412
+ /**
1413
+ * Get the immediate parent
1414
+ */
1415
+ get parent() {
1416
+ return this.parentChain?.ref;
1417
+ }
1418
+ /**
1419
+ * Get the immediate parent index
1420
+ */
1421
+ get parentIndex() {
1422
+ return this.parentChain?.index;
1423
+ }
1424
+ /**
1425
+ * Add a parent to the chain
1426
+ */
1427
+ addParent(parent, index) {
1428
+ // Check if this parent already exists in the chain
1429
+ if (this.hasParent((p, i) => p[$changes] === parent[$changes] && i === index)) {
1430
+ return;
1431
+ }
1432
+ this.parentChain = {
1433
+ ref: parent,
1434
+ index,
1435
+ next: this.parentChain
1436
+ };
1437
+ }
1438
+ /**
1439
+ * Remove a parent from the chain
1440
+ * @param parent - The parent to remove
1441
+ * @returns true if parent was removed
1442
+ */
1443
+ removeParent(parent = this.parent) {
1444
+ let current = this.parentChain;
1445
+ let previous = null;
1446
+ while (current) {
1447
+ //
1448
+ // FIXME: it is required to check against `$changes` here because
1449
+ // ArraySchema is instance of Proxy
1450
+ //
1451
+ if (current.ref[$changes] === parent[$changes]) {
1452
+ if (previous) {
1453
+ previous.next = current.next;
1454
+ }
1455
+ else {
1456
+ this.parentChain = current.next;
1457
+ }
1458
+ return true;
1459
+ }
1460
+ previous = current;
1461
+ current = current.next;
1462
+ }
1463
+ return this.parentChain === undefined;
1464
+ }
1465
+ /**
1466
+ * Find a specific parent in the chain
1467
+ */
1468
+ findParent(predicate) {
1469
+ let current = this.parentChain;
1470
+ while (current) {
1471
+ if (predicate(current.ref, current.index)) {
1472
+ return current;
1473
+ }
1474
+ current = current.next;
1475
+ }
1476
+ return undefined;
1477
+ }
1478
+ /**
1479
+ * Check if this ChangeTree has a specific parent
1480
+ */
1481
+ hasParent(predicate) {
1482
+ return this.findParent(predicate) !== undefined;
1483
+ }
1484
+ /**
1485
+ * Get all parents as an array (for debugging/testing)
1486
+ */
1487
+ getAllParents() {
1488
+ const parents = [];
1489
+ let current = this.parentChain;
1490
+ while (current) {
1491
+ parents.push({ ref: current.ref, index: current.index });
1492
+ current = current.next;
1493
+ }
1494
+ return parents;
1495
+ }
1434
1496
  }
1435
1497
 
1436
1498
  function encodeValue(encoder, bytes, type, value, operation, it) {
@@ -1948,7 +2010,7 @@ class ArraySchema {
1948
2010
  }
1949
2011
  if (previousValue !== undefined) {
1950
2012
  // remove root reference from previous value
1951
- previousValue[$changes].root?.remove(previousValue[$changes]);
2013
+ previousValue[$changes].root?.remove(previousValue[$changes], obj);
1952
2014
  }
1953
2015
  }
1954
2016
  else {
@@ -1979,8 +2041,11 @@ class ArraySchema {
1979
2041
  return Reflect.has(obj, key);
1980
2042
  }
1981
2043
  });
1982
- this[$changes] = new ChangeTree(proxy);
1983
- this[$changes].indexes = {};
2044
+ Object.defineProperty(this, $changes, {
2045
+ value: new ChangeTree(proxy),
2046
+ enumerable: false,
2047
+ writable: true,
2048
+ });
1984
2049
  if (items.length > 0) {
1985
2050
  this.push(...items);
1986
2051
  }
@@ -2102,7 +2167,7 @@ class ArraySchema {
2102
2167
  const changeTree = this[$changes];
2103
2168
  // remove children references
2104
2169
  changeTree.forEachChild((childChangeTree, _) => {
2105
- changeTree.root?.remove(childChangeTree);
2170
+ changeTree.root?.remove(childChangeTree, this);
2106
2171
  });
2107
2172
  changeTree.discard(true);
2108
2173
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -2141,7 +2206,6 @@ class ArraySchema {
2141
2206
  if (this.items.length === 0) {
2142
2207
  return undefined;
2143
2208
  }
2144
- // const index = Number(Object.keys(changeTree.indexes)[0]);
2145
2209
  const changeTree = this[$changes];
2146
2210
  const index = this.tmpItems.findIndex(item => item === this.items[0]);
2147
2211
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
@@ -2600,8 +2664,13 @@ class MapSchema {
2600
2664
  this.$items = new Map();
2601
2665
  this.$indexes = new Map();
2602
2666
  this.deletedItems = {};
2603
- this[$changes] = new ChangeTree(this);
2604
- this[$changes].indexes = {};
2667
+ const changeTree = new ChangeTree(this);
2668
+ changeTree.indexes = {};
2669
+ Object.defineProperty(this, $changes, {
2670
+ value: changeTree,
2671
+ enumerable: false,
2672
+ writable: true,
2673
+ });
2605
2674
  if (initialValues) {
2606
2675
  if (initialValues instanceof Map ||
2607
2676
  initialValues instanceof MapSchema) {
@@ -2652,7 +2721,7 @@ class MapSchema {
2652
2721
  operation = exports.OPERATION.DELETE_AND_ADD;
2653
2722
  // remove reference from previous value
2654
2723
  if (previousValue !== undefined) {
2655
- previousValue[$changes].root?.remove(previousValue[$changes]);
2724
+ previousValue[$changes].root?.remove(previousValue[$changes], this);
2656
2725
  }
2657
2726
  }
2658
2727
  }
@@ -2689,7 +2758,7 @@ class MapSchema {
2689
2758
  changeTree.indexes = {};
2690
2759
  // remove children references
2691
2760
  changeTree.forEachChild((childChangeTree, _) => {
2692
- changeTree.root?.remove(childChangeTree);
2761
+ changeTree.root?.remove(childChangeTree, this);
2693
2762
  });
2694
2763
  // clear previous indexes
2695
2764
  this.$indexes.clear();
@@ -3145,10 +3214,13 @@ function dumpChanges(schema) {
3145
3214
  refs: []
3146
3215
  };
3147
3216
  // for (const refId in $root.changes) {
3148
- $root.changes.forEach(changeTree => {
3217
+ let current = $root.changes.next;
3218
+ while (current) {
3219
+ const changeTree = current.changeTree;
3149
3220
  // skip if ChangeTree is undefined
3150
3221
  if (changeTree === undefined) {
3151
- return;
3222
+ current = current.next;
3223
+ continue;
3152
3224
  }
3153
3225
  const changes = changeTree.indexedOperations;
3154
3226
  dump.refs.push(`refId#${changeTree.refId}`);
@@ -3160,7 +3232,8 @@ function dumpChanges(schema) {
3160
3232
  }
3161
3233
  dump.ops[exports.OPERATION[op]]++;
3162
3234
  }
3163
- });
3235
+ current = current.next;
3236
+ }
3164
3237
  return dump;
3165
3238
  }
3166
3239
 
@@ -3310,7 +3383,7 @@ class Schema {
3310
3383
  * @param showContents display JSON contents of the instance
3311
3384
  * @returns
3312
3385
  */
3313
- static debugRefIds(ref, showContents = false, level = 0, decoder) {
3386
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3314
3387
  const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3315
3388
  const changeTree = ref[$changes];
3316
3389
  const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
@@ -3319,11 +3392,25 @@ class Schema {
3319
3392
  const refCount = (root?.refCount?.[refId] > 1)
3320
3393
  ? ` [×${root.refCount[refId]}]`
3321
3394
  : '';
3322
- let output = `${getIndent(level)}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3323
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder));
3395
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3396
+ changeTree.forEachChild((childChangeTree, key) => {
3397
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3398
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3399
+ });
3324
3400
  return output;
3325
3401
  }
3326
- static debugRefIdsDecoder(decoder) {
3402
+ static debugRefIdEncodingOrder(ref, changeSet = 'allChanges') {
3403
+ let encodeOrder = [];
3404
+ let current = ref[$changes].root[changeSet].next;
3405
+ while (current) {
3406
+ if (current.changeTree) {
3407
+ encodeOrder.push(current.changeTree.refId);
3408
+ }
3409
+ current = current.next;
3410
+ }
3411
+ return encodeOrder;
3412
+ }
3413
+ static debugRefIdsFromDecoder(decoder) {
3327
3414
  return this.debugRefIds(decoder.state, false, 0, decoder);
3328
3415
  }
3329
3416
  /**
@@ -3372,8 +3459,12 @@ class Schema {
3372
3459
  const changeTrees = new Map();
3373
3460
  const instanceRefIds = [];
3374
3461
  let totalOperations = 0;
3462
+ // TODO: FIXME: this method is not working as expected
3375
3463
  for (const [refId, changes] of Object.entries(root[changeSetName])) {
3376
3464
  const changeTree = root.changeTrees[refId];
3465
+ if (!changeTree) {
3466
+ continue;
3467
+ }
3377
3468
  let includeChangeTree = false;
3378
3469
  let parentChangeTrees = [];
3379
3470
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3509,7 +3600,7 @@ class CollectionSchema {
3509
3600
  changeTree.indexes = {};
3510
3601
  // remove children references
3511
3602
  changeTree.forEachChild((childChangeTree, _) => {
3512
- changeTree.root?.remove(childChangeTree);
3603
+ changeTree.root?.remove(childChangeTree, this);
3513
3604
  });
3514
3605
  // clear previous indexes
3515
3606
  this.$indexes.clear();
@@ -3796,18 +3887,20 @@ class Root {
3796
3887
  this.refCount = {};
3797
3888
  this.changeTrees = {};
3798
3889
  // all changes
3799
- this.allChanges = [];
3800
- this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3890
+ this.allChanges = createChangeTreeList();
3891
+ this.allFilteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3801
3892
  // pending changes to be encoded
3802
- this.changes = [];
3803
- this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3893
+ this.changes = createChangeTreeList();
3894
+ this.filteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3804
3895
  }
3805
3896
  getNextUniqueId() {
3806
3897
  return this.nextUniqueId++;
3807
3898
  }
3808
3899
  add(changeTree) {
3809
- // FIXME: move implementation of `ensureRefId` to `Root` class
3810
- changeTree.ensureRefId();
3900
+ // Assign unique `refId` to changeTree if it doesn't have one yet.
3901
+ if (changeTree.refId === undefined) {
3902
+ changeTree.refId = this.getNextUniqueId();
3903
+ }
3811
3904
  const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3812
3905
  if (isNewChangeTree) {
3813
3906
  this.changeTrees[changeTree.refId] = changeTree;
@@ -3826,10 +3919,12 @@ class Root {
3826
3919
  }
3827
3920
  }
3828
3921
  this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3922
+ // console.log("ADD", { refId: changeTree.refId, refCount: this.refCount[changeTree.refId] });
3829
3923
  return isNewChangeTree;
3830
3924
  }
3831
- remove(changeTree) {
3925
+ remove(changeTree, parent) {
3832
3926
  const refCount = (this.refCount[changeTree.refId]) - 1;
3927
+ // console.log("REMOVE", { refId: changeTree.refId, refCount });
3833
3928
  if (refCount <= 0) {
3834
3929
  //
3835
3930
  // Only remove "root" reference if it's the last reference
@@ -3843,7 +3938,19 @@ class Root {
3843
3938
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
3844
3939
  }
3845
3940
  this.refCount[changeTree.refId] = 0;
3846
- changeTree.forEachChild((child, _) => this.remove(child));
3941
+ changeTree.forEachChild((child, _) => {
3942
+ if (child.removeParent(changeTree.ref)) {
3943
+ if ((child.parentChain === undefined || // no parent, remove it
3944
+ (child.parentChain && this.refCount[child.refId] > 1) // parent is still in use, but has more than one reference, remove it
3945
+ )) {
3946
+ this.remove(child, changeTree.ref);
3947
+ }
3948
+ else if (child.parentChain) {
3949
+ // re-assigning a child of the same root, move it to the end
3950
+ this.moveToEndOfChanges(child);
3951
+ }
3952
+ }
3953
+ });
3847
3954
  }
3848
3955
  else {
3849
3956
  this.refCount[changeTree.refId] = refCount;
@@ -3856,32 +3963,73 @@ class Root {
3856
3963
  // containing instance is not available, the Decoder will throw
3857
3964
  // "refId not found" error.
3858
3965
  //
3859
- if (changeTree.filteredChanges !== undefined) {
3860
- this.removeChangeFromChangeSet("filteredChanges", changeTree);
3861
- enqueueChangeTree(this, changeTree, "filteredChanges");
3862
- }
3863
- else {
3864
- this.removeChangeFromChangeSet("changes", changeTree);
3865
- enqueueChangeTree(this, changeTree, "changes");
3866
- }
3966
+ this.moveToEndOfChanges(changeTree);
3967
+ changeTree.forEachChild((child, _) => this.moveToEndOfChanges(child));
3867
3968
  }
3868
3969
  return refCount;
3869
3970
  }
3971
+ moveToEndOfChanges(changeTree) {
3972
+ if (changeTree.filteredChanges) {
3973
+ this.moveToEndOfChangeTreeList("filteredChanges", changeTree);
3974
+ this.moveToEndOfChangeTreeList("allFilteredChanges", changeTree);
3975
+ }
3976
+ else {
3977
+ this.moveToEndOfChangeTreeList("changes", changeTree);
3978
+ this.moveToEndOfChangeTreeList("allChanges", changeTree);
3979
+ }
3980
+ }
3981
+ moveToEndOfChangeTreeList(changeSetName, changeTree) {
3982
+ const changeSet = this[changeSetName];
3983
+ const node = changeTree[changeSetName].queueRootNode;
3984
+ if (!node || node === changeSet.tail)
3985
+ return;
3986
+ // Remove from current position
3987
+ if (node.prev) {
3988
+ node.prev.next = node.next;
3989
+ }
3990
+ else {
3991
+ changeSet.next = node.next;
3992
+ }
3993
+ if (node.next) {
3994
+ node.next.prev = node.prev;
3995
+ }
3996
+ else {
3997
+ changeSet.tail = node.prev;
3998
+ }
3999
+ // Add to end
4000
+ node.prev = changeSet.tail;
4001
+ node.next = undefined;
4002
+ if (changeSet.tail) {
4003
+ changeSet.tail.next = node;
4004
+ }
4005
+ else {
4006
+ changeSet.next = node;
4007
+ }
4008
+ changeSet.tail = node;
4009
+ }
3870
4010
  removeChangeFromChangeSet(changeSetName, changeTree) {
3871
4011
  const changeSet = this[changeSetName];
3872
- const changeSetIndex = changeSet.indexOf(changeTree);
3873
- if (changeSetIndex !== -1) {
3874
- changeTree[changeSetName].queueRootIndex = -1;
3875
- changeSet[changeSetIndex] = undefined;
4012
+ const node = changeTree[changeSetName].queueRootNode;
4013
+ if (node && node.changeTree === changeTree) {
4014
+ // Remove the node from the linked list
4015
+ if (node.prev) {
4016
+ node.prev.next = node.next;
4017
+ }
4018
+ else {
4019
+ changeSet.next = node.next;
4020
+ }
4021
+ if (node.next) {
4022
+ node.next.prev = node.prev;
4023
+ }
4024
+ else {
4025
+ changeSet.tail = node.prev;
4026
+ }
4027
+ changeSet.length--;
4028
+ // Clear ChangeTree reference
4029
+ changeTree[changeSetName].queueRootNode = undefined;
3876
4030
  return true;
3877
4031
  }
3878
- // if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
3879
- // changeTree[changeSetName].queueRootIndex = -1;
3880
- // return true;
3881
- // }
3882
- }
3883
- clear() {
3884
- this.changes.length = 0;
4032
+ return false;
3885
4033
  }
3886
4034
  }
3887
4035
 
@@ -3911,12 +4059,9 @@ class Encoder {
3911
4059
  ) {
3912
4060
  const hasView = (view !== undefined);
3913
4061
  const rootChangeTree = this.state[$changes];
3914
- const changeTrees = this.root[changeSetName];
3915
- for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3916
- const changeTree = changeTrees[i];
3917
- if (!changeTree) {
3918
- continue;
3919
- }
4062
+ let current = this.root[changeSetName];
4063
+ while (current = current.next) {
4064
+ const changeTree = current.changeTree;
3920
4065
  if (hasView) {
3921
4066
  if (!view.isChangeTreeVisible(changeTree)) {
3922
4067
  // console.log("MARK AS INVISIBLE:", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, raw: changeTree.ref.toJSON() });
@@ -4005,7 +4150,9 @@ class Encoder {
4005
4150
  const rootChangeSet = (typeof (field) === "string")
4006
4151
  ? this.root[field]
4007
4152
  : field;
4008
- rootChangeSet.forEach((changeTree) => {
4153
+ let current = rootChangeSet.next;
4154
+ while (current) {
4155
+ const changeTree = current.changeTree;
4009
4156
  const changeSet = changeTree[field];
4010
4157
  const metadata = changeTree.ref.constructor[Symbol.metadata];
4011
4158
  console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
@@ -4017,7 +4164,8 @@ class Encoder {
4017
4164
  op: exports.OPERATION[op],
4018
4165
  });
4019
4166
  }
4020
- });
4167
+ current = current.next;
4168
+ }
4021
4169
  }
4022
4170
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4023
4171
  const viewOffset = it.offset;
@@ -4067,21 +4215,19 @@ class Encoder {
4067
4215
  }
4068
4216
  discardChanges() {
4069
4217
  // discard shared changes
4070
- let length = this.root.changes.length;
4071
- if (length > 0) {
4072
- while (length--) {
4073
- this.root.changes[length]?.endEncode('changes');
4074
- }
4075
- this.root.changes.length = 0;
4218
+ let current = this.root.changes.next;
4219
+ while (current) {
4220
+ current.changeTree.endEncode('changes');
4221
+ current = current.next;
4076
4222
  }
4223
+ this.root.changes = createChangeTreeList();
4077
4224
  // discard filtered changes
4078
- length = this.root.filteredChanges.length;
4079
- if (length > 0) {
4080
- while (length--) {
4081
- this.root.filteredChanges[length]?.endEncode('filteredChanges');
4082
- }
4083
- this.root.filteredChanges.length = 0;
4225
+ current = this.root.filteredChanges.next;
4226
+ while (current) {
4227
+ current.changeTree.endEncode('filteredChanges');
4228
+ current = current.next;
4084
4229
  }
4230
+ this.root.filteredChanges = createChangeTreeList();
4085
4231
  }
4086
4232
  tryEncodeTypeId(bytes, baseType, targetType, it) {
4087
4233
  const baseTypeId = this.context.getTypeId(baseType);
@@ -4271,21 +4417,20 @@ class Decoder {
4271
4417
  //
4272
4418
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
4273
4419
  it.offset++;
4420
+ ref[$onDecodeEnd]?.();
4274
4421
  const nextRefId = decode.number(bytes, it);
4275
4422
  const nextRef = $root.refs.get(nextRefId);
4276
4423
  //
4277
4424
  // Trying to access a reference that haven't been decoded yet.
4278
4425
  //
4279
4426
  if (!nextRef) {
4280
- console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
4281
- console.warn("Please report this to the developers. All refIds =>");
4282
- console.warn(Schema.debugRefIdsDecoder(this));
4283
- this.skipCurrentStructure(bytes, it, totalBytes);
4427
+ throw new Error(`"refId" not found: ${nextRefId}`);
4428
+ }
4429
+ else {
4430
+ ref = nextRef;
4431
+ decoder = ref.constructor[$decoder];
4432
+ this.currentRefId = nextRefId;
4284
4433
  }
4285
- ref[$onDecodeEnd]?.();
4286
- this.currentRefId = nextRefId;
4287
- ref = nextRef;
4288
- decoder = ref.constructor[$decoder];
4289
4434
  continue;
4290
4435
  }
4291
4436
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -4329,10 +4474,6 @@ class Decoder {
4329
4474
  return type || defaultType;
4330
4475
  }
4331
4476
  createInstanceOfType(type) {
4332
- // let instance: Schema = new (type as any)();
4333
- // // assign root on $changes
4334
- // instance[$changes].root = this.root[$changes].root;
4335
- // return instance;
4336
4477
  return new type();
4337
4478
  }
4338
4479
  removeChildRefs(ref, allChanges) {
@@ -4859,11 +5000,12 @@ class StateView {
4859
5000
  // TODO: allow to set multiple tags at once
4860
5001
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
4861
5002
  const changeTree = obj?.[$changes];
5003
+ const parentChangeTree = changeTree.parent;
4862
5004
  if (!changeTree) {
4863
5005
  console.warn("StateView#add(), invalid object:", obj);
4864
- return this;
5006
+ return false;
4865
5007
  }
4866
- else if (!changeTree.parent &&
5008
+ else if (!parentChangeTree &&
4867
5009
  changeTree.refId !== 0 // allow root object
4868
5010
  ) {
4869
5011
  /**
@@ -4885,18 +5027,31 @@ class StateView {
4885
5027
  // add parent ChangeTree's
4886
5028
  // - if it was invisible to this view
4887
5029
  // - if it were previously filtered out
4888
- if (checkIncludeParent && changeTree.parent) {
5030
+ if (checkIncludeParent && parentChangeTree) {
4889
5031
  this.addParentOf(changeTree, tag);
4890
5032
  }
4891
- //
4892
- // TODO: when adding an item of a MapSchema, the changes may not
4893
- // be set (only the parent's changes are set)
4894
- //
4895
5033
  let changes = this.changes.get(changeTree.refId);
4896
5034
  if (changes === undefined) {
4897
5035
  changes = {};
5036
+ // FIXME / OPTIMIZE: do not add if no changes are needed
4898
5037
  this.changes.set(changeTree.refId, changes);
4899
5038
  }
5039
+ let isChildAdded = false;
5040
+ //
5041
+ // Add children of this ChangeTree first.
5042
+ // If successful, we must link the current ChangeTree to the child.
5043
+ //
5044
+ changeTree.forEachChild((change, index) => {
5045
+ // Do not ADD children that don't have the same tag
5046
+ if (metadata &&
5047
+ metadata[index].tag !== undefined &&
5048
+ metadata[index].tag !== tag) {
5049
+ return;
5050
+ }
5051
+ if (this.add(change.ref, tag, false)) {
5052
+ isChildAdded = true;
5053
+ }
5054
+ });
4900
5055
  // set tag
4901
5056
  if (tag !== DEFAULT_VIEW_TAG) {
4902
5057
  if (!this.tags) {
@@ -4918,11 +5073,12 @@ class StateView {
4918
5073
  }
4919
5074
  });
4920
5075
  }
4921
- else {
4922
- const isInvisible = this.invisible.has(changeTree);
5076
+ else if (!changeTree.isNew || isChildAdded) {
5077
+ // new structures will be added as part of .encode() call, no need to force it to .encodeView()
4923
5078
  const changeSet = (changeTree.filteredChanges !== undefined)
4924
5079
  ? changeTree.allFilteredChanges
4925
5080
  : changeTree.allChanges;
5081
+ const isInvisible = this.invisible.has(changeTree);
4926
5082
  for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4927
5083
  const index = changeSet.operations[i];
4928
5084
  if (index === undefined) {
@@ -4930,27 +5086,17 @@ class StateView {
4930
5086
  } // skip "undefined" indexes
4931
5087
  const op = changeTree.indexedOperations[index] ?? exports.OPERATION.ADD;
4932
5088
  const tagAtIndex = metadata?.[index].tag;
4933
- if (!changeTree.isNew && // new structures will be added as part of .encode() call, no need to force it to .encodeView()
5089
+ if (op !== exports.OPERATION.DELETE &&
4934
5090
  (isInvisible || // if "invisible", include all
4935
5091
  tagAtIndex === undefined || // "all change" with no tag
4936
5092
  tagAtIndex === tag // tagged property
4937
- ) &&
4938
- op !== exports.OPERATION.DELETE) {
5093
+ )) {
4939
5094
  changes[index] = op;
5095
+ isChildAdded = true; // FIXME: assign only once
4940
5096
  }
4941
5097
  }
4942
5098
  }
4943
- // Add children of this ChangeTree to this view
4944
- changeTree.forEachChild((change, index) => {
4945
- // Do not ADD children that don't have the same tag
4946
- if (metadata &&
4947
- metadata[index].tag !== undefined &&
4948
- metadata[index].tag !== tag) {
4949
- return;
4950
- }
4951
- this.add(change.ref, tag, false);
4952
- });
4953
- return this;
5099
+ return isChildAdded;
4954
5100
  }
4955
5101
  addParentOf(childChangeTree, tag) {
4956
5102
  const changeTree = childChangeTree.parent[$changes];