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