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