@colyseus/schema 3.0.41 → 3.0.43

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 (48) hide show
  1. package/build/cjs/index.js +334 -185
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +334 -185
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +334 -185
  6. package/lib/Schema.d.ts +2 -1
  7. package/lib/Schema.js +21 -3
  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 +7 -8
  16. package/lib/decoder/Decoder.js.map +1 -1
  17. package/lib/encoder/ChangeTree.d.ts +57 -7
  18. package/lib/encoder/ChangeTree.js +171 -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 +8 -7
  23. package/lib/encoder/Root.js +81 -26
  24. package/lib/encoder/Root.js.map +1 -1
  25. package/lib/encoder/StateView.js +7 -0
  26. package/lib/encoder/StateView.js.map +1 -1
  27. package/lib/types/custom/ArraySchema.js +5 -3
  28. package/lib/types/custom/ArraySchema.js.map +1 -1
  29. package/lib/types/custom/MapSchema.js +7 -2
  30. package/lib/types/custom/MapSchema.js.map +1 -1
  31. package/lib/types/symbols.d.ts +14 -14
  32. package/lib/types/symbols.js +14 -14
  33. package/lib/types/symbols.js.map +1 -1
  34. package/lib/utils.js +7 -3
  35. package/lib/utils.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/Schema.ts +21 -5
  38. package/src/bench_encode.ts +108 -0
  39. package/src/debug.ts +55 -0
  40. package/src/decoder/Decoder.ts +8 -12
  41. package/src/encoder/ChangeTree.ts +201 -115
  42. package/src/encoder/Encoder.ts +21 -19
  43. package/src/encoder/Root.ts +87 -28
  44. package/src/encoder/StateView.ts +8 -0
  45. package/src/types/custom/ArraySchema.ts +6 -4
  46. package/src/types/custom/MapSchema.ts +8 -2
  47. package/src/types/symbols.ts +14 -14
  48. 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,58 @@ 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.moveToEndOfChanges(child);
1092
+ return;
1099
1093
  }
1094
+ child.setParent(this.ref, root, index);
1100
1095
  });
1101
1096
  }
1102
1097
  }
@@ -1104,21 +1099,23 @@ class ChangeTree {
1104
1099
  //
1105
1100
  // assign same parent on child structures
1106
1101
  //
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);
1102
+ if (this.ref[$childType]) {
1103
+ if (typeof (this.ref[$childType]) !== "string") {
1104
+ // MapSchema / ArraySchema, etc.
1105
+ for (const [key, value] of this.ref.entries()) {
1106
+ callback(value[$changes], key);
1114
1107
  }
1115
- });
1108
+ }
1116
1109
  }
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
- });
1110
+ else {
1111
+ for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {
1112
+ const field = this.metadata[index];
1113
+ const value = this.ref[field.name];
1114
+ if (!value) {
1115
+ continue;
1116
+ }
1117
+ callback(value[$changes], index);
1118
+ }
1122
1119
  }
1123
1120
  }
1124
1121
  operation(op) {
@@ -1134,8 +1131,7 @@ class ChangeTree {
1134
1131
  }
1135
1132
  }
1136
1133
  change(index, operation = exports.OPERATION.ADD) {
1137
- const metadata = this.ref.constructor[Symbol.metadata];
1138
- const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
1134
+ const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);
1139
1135
  const changeSet = (isFiltered)
1140
1136
  ? this.filteredChanges
1141
1137
  : this.changes;
@@ -1225,19 +1221,16 @@ class ChangeTree {
1225
1221
  }
1226
1222
  }
1227
1223
  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
- }
1224
+ return (
1225
+ //
1226
+ // Get the child type from parent structure.
1227
+ // - ["string"] => "string"
1228
+ // - { map: "string" } => "string"
1229
+ // - { set: "string" } => "string"
1230
+ //
1231
+ this.ref[$childType] || // ArraySchema | MapSchema | SetSchema | CollectionSchema
1232
+ this.metadata[index].type // Schema
1233
+ );
1241
1234
  }
1242
1235
  getChange(index) {
1243
1236
  return this.indexedOperations[index];
@@ -1297,9 +1290,7 @@ class ChangeTree {
1297
1290
  endEncode(changeSetName) {
1298
1291
  this.indexedOperations = {};
1299
1292
  // clear changeset
1300
- this[changeSetName].indexes = {};
1301
- this[changeSetName].operations.length = 0;
1302
- this[changeSetName].queueRootIndex = undefined;
1293
+ this[changeSetName] = createChangeSet();
1303
1294
  // ArraySchema and MapSchema have a custom "encode end" method
1304
1295
  this.ref[$onEncodeEnd]?.();
1305
1296
  // Not a new instance anymore
@@ -1313,20 +1304,14 @@ class ChangeTree {
1313
1304
  //
1314
1305
  this.ref[$onEncodeEnd]?.();
1315
1306
  this.indexedOperations = {};
1316
- this.changes.indexes = {};
1317
- this.changes.operations.length = 0;
1318
- this.changes.queueRootIndex = undefined;
1307
+ this.changes = createChangeSet();
1319
1308
  if (this.filteredChanges !== undefined) {
1320
- this.filteredChanges.indexes = {};
1321
- this.filteredChanges.operations.length = 0;
1322
- this.filteredChanges.queueRootIndex = undefined;
1309
+ this.filteredChanges = createChangeSet();
1323
1310
  }
1324
1311
  if (discardAll) {
1325
- this.allChanges.indexes = {};
1326
- this.allChanges.operations.length = 0;
1312
+ this.allChanges = createChangeSet();
1327
1313
  if (this.allFilteredChanges !== undefined) {
1328
- this.allFilteredChanges.indexes = {};
1329
- this.allFilteredChanges.operations.length = 0;
1314
+ this.allFilteredChanges = createChangeSet();
1330
1315
  }
1331
1316
  }
1332
1317
  }
@@ -1344,18 +1329,10 @@ class ChangeTree {
1344
1329
  }
1345
1330
  this.discard();
1346
1331
  }
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
1332
  get changed() {
1355
1333
  return (Object.entries(this.indexedOperations).length > 0);
1356
1334
  }
1357
- checkIsFiltered(parent, parentIndex) {
1358
- const isNewChangeTree = this.root.add(this);
1335
+ checkIsFiltered(parent, parentIndex, isNewChangeTree) {
1359
1336
  if (this.root.types.hasFilters) {
1360
1337
  //
1361
1338
  // At Schema initialization, the "root" structure might not be available
@@ -1367,14 +1344,14 @@ class ChangeTree {
1367
1344
  if (this.filteredChanges !== undefined) {
1368
1345
  enqueueChangeTree(this.root, this, 'filteredChanges');
1369
1346
  if (isNewChangeTree) {
1370
- this.root.allFilteredChanges.push(this);
1347
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
1371
1348
  }
1372
1349
  }
1373
1350
  }
1374
1351
  if (!this.isFiltered) {
1375
1352
  enqueueChangeTree(this.root, this, 'changes');
1376
1353
  if (isNewChangeTree) {
1377
- this.root.allChanges.push(this);
1354
+ enqueueChangeTree(this.root, this, 'allChanges');
1378
1355
  }
1379
1356
  }
1380
1357
  }
@@ -1431,6 +1408,90 @@ class ChangeTree {
1431
1408
  }
1432
1409
  }
1433
1410
  }
1411
+ /**
1412
+ * Get the immediate parent
1413
+ */
1414
+ get parent() {
1415
+ return this.parentChain?.ref;
1416
+ }
1417
+ /**
1418
+ * Get the immediate parent index
1419
+ */
1420
+ get parentIndex() {
1421
+ return this.parentChain?.index;
1422
+ }
1423
+ /**
1424
+ * Add a parent to the chain
1425
+ */
1426
+ addParent(parent, index) {
1427
+ // Check if this parent already exists in the chain
1428
+ if (this.hasParent((p, i) => p === parent && i === index)) {
1429
+ return;
1430
+ }
1431
+ this.parentChain = {
1432
+ ref: parent,
1433
+ index,
1434
+ next: this.parentChain
1435
+ };
1436
+ }
1437
+ /**
1438
+ * Remove a parent from the chain
1439
+ * @param parent - The parent to remove
1440
+ * @returns true if parent was removed
1441
+ */
1442
+ removeParent(parent) {
1443
+ let current = this.parentChain;
1444
+ let previous = null;
1445
+ while (current) {
1446
+ //
1447
+ // FIXME: it is required to check against `$changes` here because
1448
+ // ArraySchema is instance of Proxy
1449
+ //
1450
+ if (current.ref[$changes] === parent[$changes]) {
1451
+ if (previous) {
1452
+ previous.next = current.next;
1453
+ }
1454
+ else {
1455
+ this.parentChain = current.next;
1456
+ }
1457
+ return true;
1458
+ }
1459
+ previous = current;
1460
+ current = current.next;
1461
+ }
1462
+ return this.parentChain === undefined;
1463
+ }
1464
+ /**
1465
+ * Find a specific parent in the chain
1466
+ */
1467
+ findParent(predicate) {
1468
+ let current = this.parentChain;
1469
+ while (current) {
1470
+ if (predicate(current.ref, current.index)) {
1471
+ return current;
1472
+ }
1473
+ current = current.next;
1474
+ }
1475
+ return undefined;
1476
+ }
1477
+ /**
1478
+ * Check if this ChangeTree has a specific parent
1479
+ */
1480
+ hasParent(predicate) {
1481
+ return this.findParent(predicate) !== undefined;
1482
+ }
1483
+ /**
1484
+ * Get all parents as an array (for debugging/testing)
1485
+ */
1486
+ getAllParents() {
1487
+ const parents = [];
1488
+ let current = this.parentChain;
1489
+ while (current) {
1490
+ parents.push({ ref: current.ref, index: current.index });
1491
+ current = current.next;
1492
+ }
1493
+ return parents;
1494
+ }
1434
1495
  }
1435
1496
 
1436
1497
  function encodeValue(encoder, bytes, type, value, operation, it) {
@@ -1979,8 +2040,11 @@ class ArraySchema {
1979
2040
  return Reflect.has(obj, key);
1980
2041
  }
1981
2042
  });
1982
- this[$changes] = new ChangeTree(proxy);
1983
- this[$changes].indexes = {};
2043
+ Object.defineProperty(this, $changes, {
2044
+ value: new ChangeTree(proxy),
2045
+ enumerable: false,
2046
+ writable: true,
2047
+ });
1984
2048
  if (items.length > 0) {
1985
2049
  this.push(...items);
1986
2050
  }
@@ -2141,7 +2205,6 @@ class ArraySchema {
2141
2205
  if (this.items.length === 0) {
2142
2206
  return undefined;
2143
2207
  }
2144
- // const index = Number(Object.keys(changeTree.indexes)[0]);
2145
2208
  const changeTree = this[$changes];
2146
2209
  const index = this.tmpItems.findIndex(item => item === this.items[0]);
2147
2210
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
@@ -2600,8 +2663,13 @@ class MapSchema {
2600
2663
  this.$items = new Map();
2601
2664
  this.$indexes = new Map();
2602
2665
  this.deletedItems = {};
2603
- this[$changes] = new ChangeTree(this);
2604
- this[$changes].indexes = {};
2666
+ const changeTree = new ChangeTree(this);
2667
+ changeTree.indexes = {};
2668
+ Object.defineProperty(this, $changes, {
2669
+ value: changeTree,
2670
+ enumerable: false,
2671
+ writable: true,
2672
+ });
2605
2673
  if (initialValues) {
2606
2674
  if (initialValues instanceof Map ||
2607
2675
  initialValues instanceof MapSchema) {
@@ -3145,10 +3213,13 @@ function dumpChanges(schema) {
3145
3213
  refs: []
3146
3214
  };
3147
3215
  // for (const refId in $root.changes) {
3148
- $root.changes.forEach(changeTree => {
3216
+ let current = $root.changes.next;
3217
+ while (current) {
3218
+ const changeTree = current.changeTree;
3149
3219
  // skip if ChangeTree is undefined
3150
3220
  if (changeTree === undefined) {
3151
- return;
3221
+ current = current.next;
3222
+ continue;
3152
3223
  }
3153
3224
  const changes = changeTree.indexedOperations;
3154
3225
  dump.refs.push(`refId#${changeTree.refId}`);
@@ -3160,7 +3231,8 @@ function dumpChanges(schema) {
3160
3231
  }
3161
3232
  dump.ops[exports.OPERATION[op]]++;
3162
3233
  }
3163
- });
3234
+ current = current.next;
3235
+ }
3164
3236
  return dump;
3165
3237
  }
3166
3238
 
@@ -3310,7 +3382,7 @@ class Schema {
3310
3382
  * @param showContents display JSON contents of the instance
3311
3383
  * @returns
3312
3384
  */
3313
- static debugRefIds(ref, showContents = false, level = 0, decoder) {
3385
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3314
3386
  const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3315
3387
  const changeTree = ref[$changes];
3316
3388
  const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
@@ -3319,10 +3391,24 @@ class Schema {
3319
3391
  const refCount = (root?.refCount?.[refId] > 1)
3320
3392
  ? ` [×${root.refCount[refId]}]`
3321
3393
  : '';
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));
3394
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3395
+ changeTree.forEachChild((childChangeTree, key) => {
3396
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3397
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3398
+ });
3324
3399
  return output;
3325
3400
  }
3401
+ static debugRefIdEncodeOrder(ref) {
3402
+ let encodeOrder = [];
3403
+ let current = ref[$changes].root.allChanges.next;
3404
+ while (current) {
3405
+ if (current.changeTree) {
3406
+ encodeOrder.push(current.changeTree.refId);
3407
+ }
3408
+ current = current.next;
3409
+ }
3410
+ return encodeOrder;
3411
+ }
3326
3412
  static debugRefIdsDecoder(decoder) {
3327
3413
  return this.debugRefIds(decoder.state, false, 0, decoder);
3328
3414
  }
@@ -3372,8 +3458,12 @@ class Schema {
3372
3458
  const changeTrees = new Map();
3373
3459
  const instanceRefIds = [];
3374
3460
  let totalOperations = 0;
3461
+ // TODO: FIXME: this method is not working as expected
3375
3462
  for (const [refId, changes] of Object.entries(root[changeSetName])) {
3376
3463
  const changeTree = root.changeTrees[refId];
3464
+ if (!changeTree) {
3465
+ continue;
3466
+ }
3377
3467
  let includeChangeTree = false;
3378
3468
  let parentChangeTrees = [];
3379
3469
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3796,18 +3886,20 @@ class Root {
3796
3886
  this.refCount = {};
3797
3887
  this.changeTrees = {};
3798
3888
  // all changes
3799
- this.allChanges = [];
3800
- this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3889
+ this.allChanges = createChangeTreeList();
3890
+ this.allFilteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3801
3891
  // pending changes to be encoded
3802
- this.changes = [];
3803
- this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3892
+ this.changes = createChangeTreeList();
3893
+ this.filteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3804
3894
  }
3805
3895
  getNextUniqueId() {
3806
3896
  return this.nextUniqueId++;
3807
3897
  }
3808
3898
  add(changeTree) {
3809
- // FIXME: move implementation of `ensureRefId` to `Root` class
3810
- changeTree.ensureRefId();
3899
+ // Assign unique `refId` to changeTree if it doesn't have one yet.
3900
+ if (changeTree.refId === undefined) {
3901
+ changeTree.refId = this.getNextUniqueId();
3902
+ }
3811
3903
  const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3812
3904
  if (isNewChangeTree) {
3813
3905
  this.changeTrees[changeTree.refId] = changeTree;
@@ -3843,7 +3935,19 @@ class Root {
3843
3935
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
3844
3936
  }
3845
3937
  this.refCount[changeTree.refId] = 0;
3846
- changeTree.forEachChild((child, _) => this.remove(child));
3938
+ changeTree.forEachChild((child, _) => {
3939
+ if (child.removeParent(changeTree.ref)) {
3940
+ if ((child.parentChain === undefined || // no parent, remove it
3941
+ (child.parentChain && this.refCount[child.refId] > 1) // parent is still in use, but has more than one reference, remove it
3942
+ )) {
3943
+ this.remove(child);
3944
+ }
3945
+ else if (child.parentChain) {
3946
+ // re-assigning a child of the same root, move it to the end
3947
+ this.moveToEndOfChanges(child);
3948
+ }
3949
+ }
3950
+ });
3847
3951
  }
3848
3952
  else {
3849
3953
  this.refCount[changeTree.refId] = refCount;
@@ -3856,32 +3960,73 @@ class Root {
3856
3960
  // containing instance is not available, the Decoder will throw
3857
3961
  // "refId not found" error.
3858
3962
  //
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
- }
3963
+ this.moveToEndOfChanges(changeTree);
3964
+ changeTree.forEachChild((child, _) => this.moveToEndOfChanges(child));
3867
3965
  }
3868
3966
  return refCount;
3869
3967
  }
3968
+ moveToEndOfChanges(changeTree) {
3969
+ if (changeTree.filteredChanges) {
3970
+ this.moveToEndOfChangeTreeList("filteredChanges", changeTree);
3971
+ this.moveToEndOfChangeTreeList("allFilteredChanges", changeTree);
3972
+ }
3973
+ else {
3974
+ this.moveToEndOfChangeTreeList("changes", changeTree);
3975
+ this.moveToEndOfChangeTreeList("allChanges", changeTree);
3976
+ }
3977
+ }
3978
+ moveToEndOfChangeTreeList(changeSetName, changeTree) {
3979
+ const changeSet = this[changeSetName];
3980
+ const node = changeTree[changeSetName].queueRootNode;
3981
+ if (!node || node === changeSet.tail)
3982
+ return;
3983
+ // Remove from current position
3984
+ if (node.prev) {
3985
+ node.prev.next = node.next;
3986
+ }
3987
+ else {
3988
+ changeSet.next = node.next;
3989
+ }
3990
+ if (node.next) {
3991
+ node.next.prev = node.prev;
3992
+ }
3993
+ else {
3994
+ changeSet.tail = node.prev;
3995
+ }
3996
+ // Add to end
3997
+ node.prev = changeSet.tail;
3998
+ node.next = undefined;
3999
+ if (changeSet.tail) {
4000
+ changeSet.tail.next = node;
4001
+ }
4002
+ else {
4003
+ changeSet.next = node;
4004
+ }
4005
+ changeSet.tail = node;
4006
+ }
3870
4007
  removeChangeFromChangeSet(changeSetName, changeTree) {
3871
4008
  const changeSet = this[changeSetName];
3872
- const changeSetIndex = changeSet.indexOf(changeTree);
3873
- if (changeSetIndex !== -1) {
3874
- changeTree[changeSetName].queueRootIndex = -1;
3875
- changeSet[changeSetIndex] = undefined;
4009
+ const node = changeTree[changeSetName].queueRootNode;
4010
+ if (node && node.changeTree === changeTree) {
4011
+ // Remove the node from the linked list
4012
+ if (node.prev) {
4013
+ node.prev.next = node.next;
4014
+ }
4015
+ else {
4016
+ changeSet.next = node.next;
4017
+ }
4018
+ if (node.next) {
4019
+ node.next.prev = node.prev;
4020
+ }
4021
+ else {
4022
+ changeSet.tail = node.prev;
4023
+ }
4024
+ changeSet.length--;
4025
+ // Clear ChangeTree reference
4026
+ changeTree[changeSetName].queueRootNode = undefined;
3876
4027
  return true;
3877
4028
  }
3878
- // if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
3879
- // changeTree[changeSetName].queueRootIndex = -1;
3880
- // return true;
3881
- // }
3882
- }
3883
- clear() {
3884
- this.changes.length = 0;
4029
+ return false;
3885
4030
  }
3886
4031
  }
3887
4032
 
@@ -3911,12 +4056,9 @@ class Encoder {
3911
4056
  ) {
3912
4057
  const hasView = (view !== undefined);
3913
4058
  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
- }
4059
+ let current = this.root[changeSetName];
4060
+ while (current = current.next) {
4061
+ const changeTree = current.changeTree;
3920
4062
  if (hasView) {
3921
4063
  if (!view.isChangeTreeVisible(changeTree)) {
3922
4064
  // console.log("MARK AS INVISIBLE:", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, raw: changeTree.ref.toJSON() });
@@ -4005,7 +4147,9 @@ class Encoder {
4005
4147
  const rootChangeSet = (typeof (field) === "string")
4006
4148
  ? this.root[field]
4007
4149
  : field;
4008
- rootChangeSet.forEach((changeTree) => {
4150
+ let current = rootChangeSet.next;
4151
+ while (current) {
4152
+ const changeTree = current.changeTree;
4009
4153
  const changeSet = changeTree[field];
4010
4154
  const metadata = changeTree.ref.constructor[Symbol.metadata];
4011
4155
  console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
@@ -4017,7 +4161,8 @@ class Encoder {
4017
4161
  op: exports.OPERATION[op],
4018
4162
  });
4019
4163
  }
4020
- });
4164
+ current = current.next;
4165
+ }
4021
4166
  }
4022
4167
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4023
4168
  const viewOffset = it.offset;
@@ -4067,21 +4212,19 @@ class Encoder {
4067
4212
  }
4068
4213
  discardChanges() {
4069
4214
  // 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;
4215
+ let current = this.root.changes.next;
4216
+ while (current) {
4217
+ current.changeTree.endEncode('changes');
4218
+ current = current.next;
4076
4219
  }
4220
+ this.root.changes = createChangeTreeList();
4077
4221
  // 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;
4222
+ current = this.root.filteredChanges.next;
4223
+ while (current) {
4224
+ current.changeTree.endEncode('filteredChanges');
4225
+ current = current.next;
4084
4226
  }
4227
+ this.root.filteredChanges = createChangeTreeList();
4085
4228
  }
4086
4229
  tryEncodeTypeId(bytes, baseType, targetType, it) {
4087
4230
  const baseTypeId = this.context.getTypeId(baseType);
@@ -4271,21 +4414,24 @@ class Decoder {
4271
4414
  //
4272
4415
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
4273
4416
  it.offset++;
4417
+ ref[$onDecodeEnd]?.();
4274
4418
  const nextRefId = decode.number(bytes, it);
4275
4419
  const nextRef = $root.refs.get(nextRefId);
4276
4420
  //
4277
4421
  // Trying to access a reference that haven't been decoded yet.
4278
4422
  //
4279
4423
  if (!nextRef) {
4424
+ // throw new Error(`"refId" not found: ${nextRefId}`);
4280
4425
  console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
4281
4426
  console.warn("Please report this to the developers. All refIds =>");
4282
4427
  console.warn(Schema.debugRefIdsDecoder(this));
4283
4428
  this.skipCurrentStructure(bytes, it, totalBytes);
4284
4429
  }
4285
- ref[$onDecodeEnd]?.();
4286
- this.currentRefId = nextRefId;
4287
- ref = nextRef;
4288
- decoder = ref.constructor[$decoder];
4430
+ else {
4431
+ ref = nextRef;
4432
+ decoder = ref.constructor[$decoder];
4433
+ this.currentRefId = nextRefId;
4434
+ }
4289
4435
  continue;
4290
4436
  }
4291
4437
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -4329,10 +4475,6 @@ class Decoder {
4329
4475
  return type || defaultType;
4330
4476
  }
4331
4477
  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
4478
  return new type();
4337
4479
  }
4338
4480
  removeChildRefs(ref, allChanges) {
@@ -5018,6 +5160,13 @@ class StateView {
5018
5160
  changes = {};
5019
5161
  this.changes.set(parentChangeTree.refId, changes);
5020
5162
  }
5163
+ else if (changes[changeTree.parentIndex] === exports.OPERATION.ADD) {
5164
+ //
5165
+ // SAME PATCH ADD + REMOVE:
5166
+ // The 'changes' of deleted structure should be ignored.
5167
+ //
5168
+ this.changes.delete(changeTree.refId);
5169
+ }
5021
5170
  // DELETE / DELETE BY REF ID
5022
5171
  changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
5023
5172
  // Remove child schema from visible set