@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
@@ -32,37 +32,37 @@
32
32
 
33
33
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
34
34
 
35
- const $track = Symbol("$track");
36
- const $encoder = Symbol("$encoder");
37
- const $decoder = Symbol("$decoder");
38
- const $filter = Symbol("$filter");
39
- const $getByIndex = Symbol("$getByIndex");
40
- const $deleteByIndex = Symbol("$deleteByIndex");
35
+ const $track = "~track";
36
+ const $encoder = "~encoder";
37
+ const $decoder = "~decoder";
38
+ const $filter = "~filter";
39
+ const $getByIndex = "~getByIndex";
40
+ const $deleteByIndex = "~deleteByIndex";
41
41
  /**
42
42
  * Used to hold ChangeTree instances whitin the structures
43
43
  */
44
- const $changes = Symbol('$changes');
44
+ const $changes = '~changes';
45
45
  /**
46
46
  * Used to keep track of the type of the child elements of a collection
47
47
  * (MapSchema, ArraySchema, etc.)
48
48
  */
49
- const $childType = Symbol('$childType');
49
+ const $childType = '~childType';
50
50
  /**
51
51
  * Optional "discard" method for custom types (ArraySchema)
52
52
  * (Discards changes for next serialization)
53
53
  */
54
- const $onEncodeEnd = Symbol('$onEncodeEnd');
54
+ const $onEncodeEnd = '~onEncodeEnd';
55
55
  /**
56
56
  * When decoding, this method is called after the instance is fully decoded
57
57
  */
58
- const $onDecodeEnd = Symbol("$onDecodeEnd");
58
+ const $onDecodeEnd = "~onDecodeEnd";
59
59
  /**
60
60
  * Metadata
61
61
  */
62
- const $descriptors = Symbol("$descriptors");
63
- const $numFields = "$__numFields";
64
- const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
65
- const $viewFieldIndexes = "$__viewFieldIndexes";
62
+ const $descriptors = "~descriptors";
63
+ const $numFields = "~__numFields";
64
+ const $refTypeFieldIndexes = "~__refTypeFieldIndexes";
65
+ const $viewFieldIndexes = "~__viewFieldIndexes";
66
66
  const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
67
67
 
68
68
  /**
@@ -970,6 +970,24 @@
970
970
  function createChangeSet() {
971
971
  return { indexes: {}, operations: [] };
972
972
  }
973
+ // Linked list helper functions
974
+ function createChangeTreeList() {
975
+ return { next: undefined, tail: undefined, length: 0 };
976
+ }
977
+ function addToChangeTreeList(list, changeTree) {
978
+ const node = { changeTree, next: undefined, prev: undefined };
979
+ if (!list.next) {
980
+ list.next = node;
981
+ list.tail = node;
982
+ }
983
+ else {
984
+ node.prev = list.tail;
985
+ list.tail.next = node;
986
+ list.tail = node;
987
+ }
988
+ list.length++;
989
+ return node;
990
+ }
973
991
  function setOperationAtIndex(changeSet, index) {
974
992
  const operationsIndex = changeSet.indexes[index];
975
993
  if (operationsIndex === undefined) {
@@ -994,13 +1012,15 @@
994
1012
  changeSet.operations[operationsIndex] = undefined;
995
1013
  delete changeSet.indexes[index];
996
1014
  }
997
- function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
1015
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootNode = changeTree[changeSet].queueRootNode) {
1016
+ // skip
998
1017
  if (!root) {
999
- // skip
1000
1018
  return;
1001
1019
  }
1002
- else if (root[changeSet][queueRootIndex] !== changeTree) {
1003
- changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
1020
+ if (queueRootNode) ;
1021
+ else {
1022
+ // Add to linked list if not already present
1023
+ changeTree[changeSet].queueRootNode = addToChangeTreeList(root[changeSet], changeTree);
1004
1024
  }
1005
1025
  }
1006
1026
  class ChangeTree {
@@ -1024,83 +1044,58 @@
1024
1044
  */
1025
1045
  this.isNew = true;
1026
1046
  this.ref = ref;
1047
+ this.metadata = ref.constructor[Symbol.metadata];
1027
1048
  //
1028
1049
  // Does this structure have "filters" declared?
1029
1050
  //
1030
- const metadata = ref.constructor[Symbol.metadata];
1031
- if (metadata?.[$viewFieldIndexes]) {
1051
+ if (this.metadata?.[$viewFieldIndexes]) {
1032
1052
  this.allFilteredChanges = { indexes: {}, operations: [] };
1033
1053
  this.filteredChanges = { indexes: {}, operations: [] };
1034
1054
  }
1035
1055
  }
1036
1056
  setRoot(root) {
1037
1057
  this.root = root;
1038
- this.checkIsFiltered(this.parent, this.parentIndex);
1039
- //
1040
- // TODO: refactor and possibly unify .setRoot() and .setParent()
1041
- //
1058
+ const isNewChangeTree = this.root.add(this);
1059
+ this.checkIsFiltered(this.parent, this.parentIndex, isNewChangeTree);
1042
1060
  // Recursively set root on child structures
1043
- const metadata = this.ref.constructor[Symbol.metadata];
1044
- if (metadata) {
1045
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1046
- const field = metadata[index];
1047
- const changeTree = this.ref[field.name]?.[$changes];
1048
- if (changeTree) {
1049
- if (changeTree.root !== root) {
1050
- changeTree.setRoot(root);
1051
- }
1052
- else {
1053
- root.add(changeTree); // increment refCount
1054
- }
1055
- }
1056
- });
1057
- }
1058
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1059
- // MapSchema / ArraySchema, etc.
1060
- this.ref.forEach((value, key) => {
1061
- const changeTree = value[$changes];
1062
- if (changeTree.root !== root) {
1063
- changeTree.setRoot(root);
1061
+ if (isNewChangeTree) {
1062
+ this.forEachChild((child, _) => {
1063
+ if (child.root !== root) {
1064
+ child.setRoot(root);
1064
1065
  }
1065
1066
  else {
1066
- root.add(changeTree); // increment refCount
1067
+ root.add(child); // increment refCount
1067
1068
  }
1068
1069
  });
1069
1070
  }
1070
1071
  }
1071
1072
  setParent(parent, root, parentIndex) {
1072
- this.parent = parent;
1073
- this.parentIndex = parentIndex;
1073
+ this.addParent(parent, parentIndex);
1074
1074
  // avoid setting parents with empty `root`
1075
1075
  if (!root) {
1076
1076
  return;
1077
1077
  }
1078
+ const isNewChangeTree = root.add(this);
1078
1079
  // skip if parent is already set
1079
1080
  if (root !== this.root) {
1080
1081
  this.root = root;
1081
- this.checkIsFiltered(parent, parentIndex);
1082
- }
1083
- else {
1084
- root.add(this);
1082
+ this.checkIsFiltered(parent, parentIndex, isNewChangeTree);
1085
1083
  }
1086
1084
  // assign same parent on child structures
1087
- const metadata = this.ref.constructor[Symbol.metadata];
1088
- if (metadata) {
1089
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1090
- const field = metadata[index];
1091
- const changeTree = this.ref[field.name]?.[$changes];
1092
- if (changeTree && changeTree.root !== root) {
1093
- changeTree.setParent(this.ref, root, index);
1094
- }
1095
- });
1096
- }
1097
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1098
- // MapSchema / ArraySchema, etc.
1099
- this.ref.forEach((value, key) => {
1100
- const changeTree = value[$changes];
1101
- if (changeTree.root !== root) {
1102
- changeTree.setParent(this.ref, root, this.indexes[key] ?? key);
1085
+ if (isNewChangeTree) {
1086
+ //
1087
+ // assign same parent on child structures
1088
+ //
1089
+ this.forEachChild((child, index) => {
1090
+ if (child.root === root) {
1091
+ //
1092
+ // re-assigning a child of the same root, move it to the end
1093
+ // of the changes queue so encoding order is preserved
1094
+ //
1095
+ root.moveToEndOfChanges(child);
1096
+ return;
1103
1097
  }
1098
+ child.setParent(this.ref, root, index);
1104
1099
  });
1105
1100
  }
1106
1101
  }
@@ -1108,21 +1103,23 @@
1108
1103
  //
1109
1104
  // assign same parent on child structures
1110
1105
  //
1111
- const metadata = this.ref.constructor[Symbol.metadata];
1112
- if (metadata) {
1113
- metadata[$refTypeFieldIndexes]?.forEach((index) => {
1114
- const field = metadata[index];
1115
- const value = this.ref[field.name];
1116
- if (value) {
1117
- callback(value[$changes], index);
1106
+ if (this.ref[$childType]) {
1107
+ if (typeof (this.ref[$childType]) !== "string") {
1108
+ // MapSchema / ArraySchema, etc.
1109
+ for (const [key, value] of this.ref.entries()) {
1110
+ callback(value[$changes], key);
1118
1111
  }
1119
- });
1112
+ }
1120
1113
  }
1121
- else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1122
- // MapSchema / ArraySchema, etc.
1123
- this.ref.forEach((value, key) => {
1124
- callback(value[$changes], this.indexes[key] ?? key);
1125
- });
1114
+ else {
1115
+ for (const index of this.metadata?.[$refTypeFieldIndexes] ?? []) {
1116
+ const field = this.metadata[index];
1117
+ const value = this.ref[field.name];
1118
+ if (!value) {
1119
+ continue;
1120
+ }
1121
+ callback(value[$changes], index);
1122
+ }
1126
1123
  }
1127
1124
  }
1128
1125
  operation(op) {
@@ -1138,8 +1135,7 @@
1138
1135
  }
1139
1136
  }
1140
1137
  change(index, operation = exports.OPERATION.ADD) {
1141
- const metadata = this.ref.constructor[Symbol.metadata];
1142
- const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
1138
+ const isFiltered = this.isFiltered || (this.metadata?.[index]?.tag !== undefined);
1143
1139
  const changeSet = (isFiltered)
1144
1140
  ? this.filteredChanges
1145
1141
  : this.changes;
@@ -1229,19 +1225,16 @@
1229
1225
  }
1230
1226
  }
1231
1227
  getType(index) {
1232
- if (Metadata.isValidInstance(this.ref)) {
1233
- const metadata = this.ref.constructor[Symbol.metadata];
1234
- return metadata[index].type;
1235
- }
1236
- else {
1237
- //
1238
- // Get the child type from parent structure.
1239
- // - ["string"] => "string"
1240
- // - { map: "string" } => "string"
1241
- // - { set: "string" } => "string"
1242
- //
1243
- return this.ref[$childType];
1244
- }
1228
+ return (
1229
+ //
1230
+ // Get the child type from parent structure.
1231
+ // - ["string"] => "string"
1232
+ // - { map: "string" } => "string"
1233
+ // - { set: "string" } => "string"
1234
+ //
1235
+ this.ref[$childType] || // ArraySchema | MapSchema | SetSchema | CollectionSchema
1236
+ this.metadata[index].type // Schema
1237
+ );
1245
1238
  }
1246
1239
  getChange(index) {
1247
1240
  return this.indexedOperations[index];
@@ -1301,9 +1294,7 @@
1301
1294
  endEncode(changeSetName) {
1302
1295
  this.indexedOperations = {};
1303
1296
  // clear changeset
1304
- this[changeSetName].indexes = {};
1305
- this[changeSetName].operations.length = 0;
1306
- this[changeSetName].queueRootIndex = undefined;
1297
+ this[changeSetName] = createChangeSet();
1307
1298
  // ArraySchema and MapSchema have a custom "encode end" method
1308
1299
  this.ref[$onEncodeEnd]?.();
1309
1300
  // Not a new instance anymore
@@ -1317,20 +1308,14 @@
1317
1308
  //
1318
1309
  this.ref[$onEncodeEnd]?.();
1319
1310
  this.indexedOperations = {};
1320
- this.changes.indexes = {};
1321
- this.changes.operations.length = 0;
1322
- this.changes.queueRootIndex = undefined;
1311
+ this.changes = createChangeSet();
1323
1312
  if (this.filteredChanges !== undefined) {
1324
- this.filteredChanges.indexes = {};
1325
- this.filteredChanges.operations.length = 0;
1326
- this.filteredChanges.queueRootIndex = undefined;
1313
+ this.filteredChanges = createChangeSet();
1327
1314
  }
1328
1315
  if (discardAll) {
1329
- this.allChanges.indexes = {};
1330
- this.allChanges.operations.length = 0;
1316
+ this.allChanges = createChangeSet();
1331
1317
  if (this.allFilteredChanges !== undefined) {
1332
- this.allFilteredChanges.indexes = {};
1333
- this.allFilteredChanges.operations.length = 0;
1318
+ this.allFilteredChanges = createChangeSet();
1334
1319
  }
1335
1320
  }
1336
1321
  }
@@ -1348,18 +1333,10 @@
1348
1333
  }
1349
1334
  this.discard();
1350
1335
  }
1351
- ensureRefId() {
1352
- // skip if refId is already set.
1353
- if (this.refId !== undefined) {
1354
- return;
1355
- }
1356
- this.refId = this.root.getNextUniqueId();
1357
- }
1358
1336
  get changed() {
1359
1337
  return (Object.entries(this.indexedOperations).length > 0);
1360
1338
  }
1361
- checkIsFiltered(parent, parentIndex) {
1362
- const isNewChangeTree = this.root.add(this);
1339
+ checkIsFiltered(parent, parentIndex, isNewChangeTree) {
1363
1340
  if (this.root.types.hasFilters) {
1364
1341
  //
1365
1342
  // At Schema initialization, the "root" structure might not be available
@@ -1371,14 +1348,14 @@
1371
1348
  if (this.filteredChanges !== undefined) {
1372
1349
  enqueueChangeTree(this.root, this, 'filteredChanges');
1373
1350
  if (isNewChangeTree) {
1374
- this.root.allFilteredChanges.push(this);
1351
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
1375
1352
  }
1376
1353
  }
1377
1354
  }
1378
1355
  if (!this.isFiltered) {
1379
1356
  enqueueChangeTree(this.root, this, 'changes');
1380
1357
  if (isNewChangeTree) {
1381
- this.root.allChanges.push(this);
1358
+ enqueueChangeTree(this.root, this, 'allChanges');
1382
1359
  }
1383
1360
  }
1384
1361
  }
@@ -1435,6 +1412,90 @@
1435
1412
  }
1436
1413
  }
1437
1414
  }
1415
+ /**
1416
+ * Get the immediate parent
1417
+ */
1418
+ get parent() {
1419
+ return this.parentChain?.ref;
1420
+ }
1421
+ /**
1422
+ * Get the immediate parent index
1423
+ */
1424
+ get parentIndex() {
1425
+ return this.parentChain?.index;
1426
+ }
1427
+ /**
1428
+ * Add a parent to the chain
1429
+ */
1430
+ addParent(parent, index) {
1431
+ // Check if this parent already exists in the chain
1432
+ if (this.hasParent((p, i) => p === parent && i === index)) {
1433
+ return;
1434
+ }
1435
+ this.parentChain = {
1436
+ ref: parent,
1437
+ index,
1438
+ next: this.parentChain
1439
+ };
1440
+ }
1441
+ /**
1442
+ * Remove a parent from the chain
1443
+ * @param parent - The parent to remove
1444
+ * @returns true if parent was removed
1445
+ */
1446
+ removeParent(parent) {
1447
+ let current = this.parentChain;
1448
+ let previous = null;
1449
+ while (current) {
1450
+ //
1451
+ // FIXME: it is required to check against `$changes` here because
1452
+ // ArraySchema is instance of Proxy
1453
+ //
1454
+ if (current.ref[$changes] === parent[$changes]) {
1455
+ if (previous) {
1456
+ previous.next = current.next;
1457
+ }
1458
+ else {
1459
+ this.parentChain = current.next;
1460
+ }
1461
+ return true;
1462
+ }
1463
+ previous = current;
1464
+ current = current.next;
1465
+ }
1466
+ return this.parentChain === undefined;
1467
+ }
1468
+ /**
1469
+ * Find a specific parent in the chain
1470
+ */
1471
+ findParent(predicate) {
1472
+ let current = this.parentChain;
1473
+ while (current) {
1474
+ if (predicate(current.ref, current.index)) {
1475
+ return current;
1476
+ }
1477
+ current = current.next;
1478
+ }
1479
+ return undefined;
1480
+ }
1481
+ /**
1482
+ * Check if this ChangeTree has a specific parent
1483
+ */
1484
+ hasParent(predicate) {
1485
+ return this.findParent(predicate) !== undefined;
1486
+ }
1487
+ /**
1488
+ * Get all parents as an array (for debugging/testing)
1489
+ */
1490
+ getAllParents() {
1491
+ const parents = [];
1492
+ let current = this.parentChain;
1493
+ while (current) {
1494
+ parents.push({ ref: current.ref, index: current.index });
1495
+ current = current.next;
1496
+ }
1497
+ return parents;
1498
+ }
1438
1499
  }
1439
1500
 
1440
1501
  function encodeValue(encoder, bytes, type, value, operation, it) {
@@ -1983,8 +2044,11 @@
1983
2044
  return Reflect.has(obj, key);
1984
2045
  }
1985
2046
  });
1986
- this[$changes] = new ChangeTree(proxy);
1987
- this[$changes].indexes = {};
2047
+ Object.defineProperty(this, $changes, {
2048
+ value: new ChangeTree(proxy),
2049
+ enumerable: false,
2050
+ writable: true,
2051
+ });
1988
2052
  if (items.length > 0) {
1989
2053
  this.push(...items);
1990
2054
  }
@@ -2145,7 +2209,6 @@
2145
2209
  if (this.items.length === 0) {
2146
2210
  return undefined;
2147
2211
  }
2148
- // const index = Number(Object.keys(changeTree.indexes)[0]);
2149
2212
  const changeTree = this[$changes];
2150
2213
  const index = this.tmpItems.findIndex(item => item === this.items[0]);
2151
2214
  const allChangesIndex = this.items.findIndex(item => item === this.items[0]);
@@ -2604,8 +2667,13 @@
2604
2667
  this.$items = new Map();
2605
2668
  this.$indexes = new Map();
2606
2669
  this.deletedItems = {};
2607
- this[$changes] = new ChangeTree(this);
2608
- this[$changes].indexes = {};
2670
+ const changeTree = new ChangeTree(this);
2671
+ changeTree.indexes = {};
2672
+ Object.defineProperty(this, $changes, {
2673
+ value: changeTree,
2674
+ enumerable: false,
2675
+ writable: true,
2676
+ });
2609
2677
  if (initialValues) {
2610
2678
  if (initialValues instanceof Map ||
2611
2679
  initialValues instanceof MapSchema) {
@@ -3149,10 +3217,13 @@
3149
3217
  refs: []
3150
3218
  };
3151
3219
  // for (const refId in $root.changes) {
3152
- $root.changes.forEach(changeTree => {
3220
+ let current = $root.changes.next;
3221
+ while (current) {
3222
+ const changeTree = current.changeTree;
3153
3223
  // skip if ChangeTree is undefined
3154
3224
  if (changeTree === undefined) {
3155
- return;
3225
+ current = current.next;
3226
+ continue;
3156
3227
  }
3157
3228
  const changes = changeTree.indexedOperations;
3158
3229
  dump.refs.push(`refId#${changeTree.refId}`);
@@ -3164,7 +3235,8 @@
3164
3235
  }
3165
3236
  dump.ops[exports.OPERATION[op]]++;
3166
3237
  }
3167
- });
3238
+ current = current.next;
3239
+ }
3168
3240
  return dump;
3169
3241
  }
3170
3242
 
@@ -3314,7 +3386,7 @@
3314
3386
  * @param showContents display JSON contents of the instance
3315
3387
  * @returns
3316
3388
  */
3317
- static debugRefIds(ref, showContents = false, level = 0, decoder) {
3389
+ static debugRefIds(ref, showContents = false, level = 0, decoder, keyPrefix = "") {
3318
3390
  const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3319
3391
  const changeTree = ref[$changes];
3320
3392
  const refId = (decoder) ? decoder.root.refIds.get(ref) : changeTree.refId;
@@ -3323,10 +3395,24 @@
3323
3395
  const refCount = (root?.refCount?.[refId] > 1)
3324
3396
  ? ` [×${root.refCount[refId]}]`
3325
3397
  : '';
3326
- let output = `${getIndent(level)}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3327
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder));
3398
+ let output = `${getIndent(level)}${keyPrefix}${ref.constructor.name} (refId: ${refId})${refCount}${contents}\n`;
3399
+ changeTree.forEachChild((childChangeTree, key) => {
3400
+ const keyPrefix = (ref['forEach'] !== undefined && key !== undefined) ? `["${key}"]: ` : "";
3401
+ output += this.debugRefIds(childChangeTree.ref, showContents, level + 1, decoder, keyPrefix);
3402
+ });
3328
3403
  return output;
3329
3404
  }
3405
+ static debugRefIdEncodeOrder(ref) {
3406
+ let encodeOrder = [];
3407
+ let current = ref[$changes].root.allChanges.next;
3408
+ while (current) {
3409
+ if (current.changeTree) {
3410
+ encodeOrder.push(current.changeTree.refId);
3411
+ }
3412
+ current = current.next;
3413
+ }
3414
+ return encodeOrder;
3415
+ }
3330
3416
  static debugRefIdsDecoder(decoder) {
3331
3417
  return this.debugRefIds(decoder.state, false, 0, decoder);
3332
3418
  }
@@ -3376,8 +3462,12 @@
3376
3462
  const changeTrees = new Map();
3377
3463
  const instanceRefIds = [];
3378
3464
  let totalOperations = 0;
3465
+ // TODO: FIXME: this method is not working as expected
3379
3466
  for (const [refId, changes] of Object.entries(root[changeSetName])) {
3380
3467
  const changeTree = root.changeTrees[refId];
3468
+ if (!changeTree) {
3469
+ continue;
3470
+ }
3381
3471
  let includeChangeTree = false;
3382
3472
  let parentChangeTrees = [];
3383
3473
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3800,18 +3890,20 @@
3800
3890
  this.refCount = {};
3801
3891
  this.changeTrees = {};
3802
3892
  // all changes
3803
- this.allChanges = [];
3804
- this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3893
+ this.allChanges = createChangeTreeList();
3894
+ this.allFilteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3805
3895
  // pending changes to be encoded
3806
- this.changes = [];
3807
- this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3896
+ this.changes = createChangeTreeList();
3897
+ this.filteredChanges = createChangeTreeList(); // TODO: do not initialize it if filters are not used
3808
3898
  }
3809
3899
  getNextUniqueId() {
3810
3900
  return this.nextUniqueId++;
3811
3901
  }
3812
3902
  add(changeTree) {
3813
- // FIXME: move implementation of `ensureRefId` to `Root` class
3814
- changeTree.ensureRefId();
3903
+ // Assign unique `refId` to changeTree if it doesn't have one yet.
3904
+ if (changeTree.refId === undefined) {
3905
+ changeTree.refId = this.getNextUniqueId();
3906
+ }
3815
3907
  const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3816
3908
  if (isNewChangeTree) {
3817
3909
  this.changeTrees[changeTree.refId] = changeTree;
@@ -3847,7 +3939,19 @@
3847
3939
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
3848
3940
  }
3849
3941
  this.refCount[changeTree.refId] = 0;
3850
- changeTree.forEachChild((child, _) => this.remove(child));
3942
+ changeTree.forEachChild((child, _) => {
3943
+ if (child.removeParent(changeTree.ref)) {
3944
+ if ((child.parentChain === undefined || // no parent, remove it
3945
+ (child.parentChain && this.refCount[child.refId] > 1) // parent is still in use, but has more than one reference, remove it
3946
+ )) {
3947
+ this.remove(child);
3948
+ }
3949
+ else if (child.parentChain) {
3950
+ // re-assigning a child of the same root, move it to the end
3951
+ this.moveToEndOfChanges(child);
3952
+ }
3953
+ }
3954
+ });
3851
3955
  }
3852
3956
  else {
3853
3957
  this.refCount[changeTree.refId] = refCount;
@@ -3860,32 +3964,73 @@
3860
3964
  // containing instance is not available, the Decoder will throw
3861
3965
  // "refId not found" error.
3862
3966
  //
3863
- if (changeTree.filteredChanges !== undefined) {
3864
- this.removeChangeFromChangeSet("filteredChanges", changeTree);
3865
- enqueueChangeTree(this, changeTree, "filteredChanges");
3866
- }
3867
- else {
3868
- this.removeChangeFromChangeSet("changes", changeTree);
3869
- enqueueChangeTree(this, changeTree, "changes");
3870
- }
3967
+ this.moveToEndOfChanges(changeTree);
3968
+ changeTree.forEachChild((child, _) => this.moveToEndOfChanges(child));
3871
3969
  }
3872
3970
  return refCount;
3873
3971
  }
3972
+ moveToEndOfChanges(changeTree) {
3973
+ if (changeTree.filteredChanges) {
3974
+ this.moveToEndOfChangeTreeList("filteredChanges", changeTree);
3975
+ this.moveToEndOfChangeTreeList("allFilteredChanges", changeTree);
3976
+ }
3977
+ else {
3978
+ this.moveToEndOfChangeTreeList("changes", changeTree);
3979
+ this.moveToEndOfChangeTreeList("allChanges", changeTree);
3980
+ }
3981
+ }
3982
+ moveToEndOfChangeTreeList(changeSetName, changeTree) {
3983
+ const changeSet = this[changeSetName];
3984
+ const node = changeTree[changeSetName].queueRootNode;
3985
+ if (!node || node === changeSet.tail)
3986
+ return;
3987
+ // Remove from current position
3988
+ if (node.prev) {
3989
+ node.prev.next = node.next;
3990
+ }
3991
+ else {
3992
+ changeSet.next = node.next;
3993
+ }
3994
+ if (node.next) {
3995
+ node.next.prev = node.prev;
3996
+ }
3997
+ else {
3998
+ changeSet.tail = node.prev;
3999
+ }
4000
+ // Add to end
4001
+ node.prev = changeSet.tail;
4002
+ node.next = undefined;
4003
+ if (changeSet.tail) {
4004
+ changeSet.tail.next = node;
4005
+ }
4006
+ else {
4007
+ changeSet.next = node;
4008
+ }
4009
+ changeSet.tail = node;
4010
+ }
3874
4011
  removeChangeFromChangeSet(changeSetName, changeTree) {
3875
4012
  const changeSet = this[changeSetName];
3876
- const changeSetIndex = changeSet.indexOf(changeTree);
3877
- if (changeSetIndex !== -1) {
3878
- changeTree[changeSetName].queueRootIndex = -1;
3879
- changeSet[changeSetIndex] = undefined;
4013
+ const node = changeTree[changeSetName].queueRootNode;
4014
+ if (node && node.changeTree === changeTree) {
4015
+ // Remove the node from the linked list
4016
+ if (node.prev) {
4017
+ node.prev.next = node.next;
4018
+ }
4019
+ else {
4020
+ changeSet.next = node.next;
4021
+ }
4022
+ if (node.next) {
4023
+ node.next.prev = node.prev;
4024
+ }
4025
+ else {
4026
+ changeSet.tail = node.prev;
4027
+ }
4028
+ changeSet.length--;
4029
+ // Clear ChangeTree reference
4030
+ changeTree[changeSetName].queueRootNode = undefined;
3880
4031
  return true;
3881
4032
  }
3882
- // if (spliceOne(changeSet, changeSet.indexOf(changeTree))) {
3883
- // changeTree[changeSetName].queueRootIndex = -1;
3884
- // return true;
3885
- // }
3886
- }
3887
- clear() {
3888
- this.changes.length = 0;
4033
+ return false;
3889
4034
  }
3890
4035
  }
3891
4036
 
@@ -3915,12 +4060,9 @@
3915
4060
  ) {
3916
4061
  const hasView = (view !== undefined);
3917
4062
  const rootChangeTree = this.state[$changes];
3918
- const changeTrees = this.root[changeSetName];
3919
- for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3920
- const changeTree = changeTrees[i];
3921
- if (!changeTree) {
3922
- continue;
3923
- }
4063
+ let current = this.root[changeSetName];
4064
+ while (current = current.next) {
4065
+ const changeTree = current.changeTree;
3924
4066
  if (hasView) {
3925
4067
  if (!view.isChangeTreeVisible(changeTree)) {
3926
4068
  // console.log("MARK AS INVISIBLE:", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, raw: changeTree.ref.toJSON() });
@@ -4009,7 +4151,9 @@
4009
4151
  const rootChangeSet = (typeof (field) === "string")
4010
4152
  ? this.root[field]
4011
4153
  : field;
4012
- rootChangeSet.forEach((changeTree) => {
4154
+ let current = rootChangeSet.next;
4155
+ while (current) {
4156
+ const changeTree = current.changeTree;
4013
4157
  const changeSet = changeTree[field];
4014
4158
  const metadata = changeTree.ref.constructor[Symbol.metadata];
4015
4159
  console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
@@ -4021,7 +4165,8 @@
4021
4165
  op: exports.OPERATION[op],
4022
4166
  });
4023
4167
  }
4024
- });
4168
+ current = current.next;
4169
+ }
4025
4170
  }
4026
4171
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
4027
4172
  const viewOffset = it.offset;
@@ -4071,21 +4216,19 @@
4071
4216
  }
4072
4217
  discardChanges() {
4073
4218
  // discard shared changes
4074
- let length = this.root.changes.length;
4075
- if (length > 0) {
4076
- while (length--) {
4077
- this.root.changes[length]?.endEncode('changes');
4078
- }
4079
- this.root.changes.length = 0;
4219
+ let current = this.root.changes.next;
4220
+ while (current) {
4221
+ current.changeTree.endEncode('changes');
4222
+ current = current.next;
4080
4223
  }
4224
+ this.root.changes = createChangeTreeList();
4081
4225
  // discard filtered changes
4082
- length = this.root.filteredChanges.length;
4083
- if (length > 0) {
4084
- while (length--) {
4085
- this.root.filteredChanges[length]?.endEncode('filteredChanges');
4086
- }
4087
- this.root.filteredChanges.length = 0;
4226
+ current = this.root.filteredChanges.next;
4227
+ while (current) {
4228
+ current.changeTree.endEncode('filteredChanges');
4229
+ current = current.next;
4088
4230
  }
4231
+ this.root.filteredChanges = createChangeTreeList();
4089
4232
  }
4090
4233
  tryEncodeTypeId(bytes, baseType, targetType, it) {
4091
4234
  const baseTypeId = this.context.getTypeId(baseType);
@@ -4275,21 +4418,24 @@
4275
4418
  //
4276
4419
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
4277
4420
  it.offset++;
4421
+ ref[$onDecodeEnd]?.();
4278
4422
  const nextRefId = decode.number(bytes, it);
4279
4423
  const nextRef = $root.refs.get(nextRefId);
4280
4424
  //
4281
4425
  // Trying to access a reference that haven't been decoded yet.
4282
4426
  //
4283
4427
  if (!nextRef) {
4428
+ // throw new Error(`"refId" not found: ${nextRefId}`);
4284
4429
  console.error(`"refId" not found: ${nextRefId}`, { previousRef: ref, previousRefId: this.currentRefId });
4285
4430
  console.warn("Please report this to the developers. All refIds =>");
4286
4431
  console.warn(Schema.debugRefIdsDecoder(this));
4287
4432
  this.skipCurrentStructure(bytes, it, totalBytes);
4288
4433
  }
4289
- ref[$onDecodeEnd]?.();
4290
- this.currentRefId = nextRefId;
4291
- ref = nextRef;
4292
- decoder = ref.constructor[$decoder];
4434
+ else {
4435
+ ref = nextRef;
4436
+ decoder = ref.constructor[$decoder];
4437
+ this.currentRefId = nextRefId;
4438
+ }
4293
4439
  continue;
4294
4440
  }
4295
4441
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -4333,10 +4479,6 @@
4333
4479
  return type || defaultType;
4334
4480
  }
4335
4481
  createInstanceOfType(type) {
4336
- // let instance: Schema = new (type as any)();
4337
- // // assign root on $changes
4338
- // instance[$changes].root = this.root[$changes].root;
4339
- // return instance;
4340
4482
  return new type();
4341
4483
  }
4342
4484
  removeChildRefs(ref, allChanges) {
@@ -5022,6 +5164,13 @@
5022
5164
  changes = {};
5023
5165
  this.changes.set(parentChangeTree.refId, changes);
5024
5166
  }
5167
+ else if (changes[changeTree.parentIndex] === exports.OPERATION.ADD) {
5168
+ //
5169
+ // SAME PATCH ADD + REMOVE:
5170
+ // The 'changes' of deleted structure should be ignored.
5171
+ //
5172
+ this.changes.delete(changeTree.refId);
5173
+ }
5025
5174
  // DELETE / DELETE BY REF ID
5026
5175
  changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
5027
5176
  // Remove child schema from visible set