@colyseus/schema 4.0.29 → 4.0.31

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.
package/build/index.js CHANGED
@@ -1456,6 +1456,33 @@
1456
1456
  next: this.parentChain
1457
1457
  };
1458
1458
  }
1459
+ /**
1460
+ * Move `parent`'s existing chain entry to `index`, skipping the work
1461
+ * `addParent` does. `parent` must already be a parent of this tree.
1462
+ *
1463
+ * Called by collections whose wire slots shift (ArraySchema): StateView
1464
+ * addresses per-view ADD/DELETE by that index, so it has to follow the
1465
+ * element it names.
1466
+ */
1467
+ setParentIndex(parent, index) {
1468
+ const chain = this.parentChain;
1469
+ if (chain === undefined) {
1470
+ return;
1471
+ }
1472
+ if (chain.next === undefined) {
1473
+ chain.index = index; // sole parent, so it is `parent`
1474
+ return;
1475
+ }
1476
+ // Shared instance — move only the entry `parent` owns. Matching goes
1477
+ // through `$changes` because ArraySchema arrives proxied (see
1478
+ // removeParent below).
1479
+ for (let entry = chain; entry !== undefined; entry = entry.next) {
1480
+ if (entry.ref[$changes] === parent[$changes]) {
1481
+ entry.index = index;
1482
+ return;
1483
+ }
1484
+ }
1485
+ }
1459
1486
  /**
1460
1487
  * Remove a parent from the chain
1461
1488
  * @param parent - The parent to remove
@@ -2092,6 +2119,39 @@
2092
2119
  get length() {
2093
2120
  return this.items.length;
2094
2121
  }
2122
+ /**
2123
+ * Re-point children at their wire slot. `ChangeTree.parentIndex` caches
2124
+ * the slot a child holds in `tmpItems`, and StateView addresses per-view
2125
+ * ADD/DELETE with it — so a reorder that leaves it behind aims those ops
2126
+ * at whichever element inherited the slot (issue #231).
2127
+ *
2128
+ * The filter check is a correctness boundary, not a tunable: StateView is
2129
+ * the only reader, and an array without `filteredChanges` never has a slot
2130
+ * read back. Everything else stops at that check instead of walking its
2131
+ * children every tick.
2132
+ *
2133
+ * Callers name the lowest slot that moved as `from`. Compaction cannot, so
2134
+ * it hands over the pre-compaction layout as `staged` and the unchanged
2135
+ * prefix is skipped instead. Either way tail churn walks nothing.
2136
+ */
2137
+ $reindexChildren(from, staged) {
2138
+ if (this[$changes].filteredChanges === undefined) {
2139
+ return;
2140
+ } // nothing will read the cache
2141
+ if (typeof this[$childType] === "string") {
2142
+ return;
2143
+ } // primitives have no child tree
2144
+ const tmpItems = this.tmpItems;
2145
+ const length = tmpItems.length;
2146
+ if (staged !== undefined) {
2147
+ while (from < length && tmpItems[from] === staged[from]) {
2148
+ from++;
2149
+ }
2150
+ }
2151
+ for (let i = from; i < length; i++) {
2152
+ tmpItems[i]?.[$changes]?.setParentIndex(this, i);
2153
+ }
2154
+ }
2095
2155
  push(...values) {
2096
2156
  let length = this.tmpItems.length;
2097
2157
  const changeTree = this[$changes];
@@ -2224,6 +2284,7 @@
2224
2284
  this[$changes].operation(exports.OPERATION.REVERSE);
2225
2285
  this.items.reverse();
2226
2286
  this.tmpItems.reverse();
2287
+ this.$reindexChildren(0);
2227
2288
  return this;
2228
2289
  }
2229
2290
  /**
@@ -2267,6 +2328,7 @@
2267
2328
  // wouldn't OPERATION.MOVE make more sense here?
2268
2329
  sortedItems.forEach((_, i) => changeTree.change(i, exports.OPERATION.REPLACE));
2269
2330
  this.tmpItems.sort(compareFn);
2331
+ this.$reindexChildren(0);
2270
2332
  this.isMovingItems = false;
2271
2333
  return this;
2272
2334
  }
@@ -2362,6 +2424,7 @@
2362
2424
  changeTree.change(index, exports.OPERATION.ADD);
2363
2425
  });
2364
2426
  this.tmpItems.unshift(...items);
2427
+ this.$reindexChildren(0); // from 0: nothing above placed the new items either
2365
2428
  return this.items.unshift(...items);
2366
2429
  }
2367
2430
  /**
@@ -2633,7 +2696,13 @@
2633
2696
  this.tmpItems[index] = undefined; // TODO: do not try to get "tmpItems" at decoding time.
2634
2697
  }
2635
2698
  [$onEncodeEnd]() {
2699
+ const staged = this.tmpItems;
2636
2700
  this.tmpItems = this.items.slice();
2701
+ // Compaction just closed the staged holes — everything above the
2702
+ // lowest one slid down a slot. There is no cheap "were there any"
2703
+ // test to gate this on: `deletedIndexes` is an object here, and a
2704
+ // `for...in` probe measured slower than the prefix scan it skips.
2705
+ this.$reindexChildren(0, staged);
2637
2706
  this.deletedIndexes = {};
2638
2707
  }
2639
2708
  [$onDecodeEnd]() {
@@ -2783,6 +2852,37 @@
2783
2852
  get(key) {
2784
2853
  return this.$items.get(key);
2785
2854
  }
2855
+ /**
2856
+ * Returns the value for `key` if present. Otherwise inserts `defaultValue`
2857
+ * (tracked as an ADD change, like `set()`) and returns it.
2858
+ *
2859
+ * Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
2860
+ * TypeScript 6's standard library).
2861
+ */
2862
+ getOrInsert(key, defaultValue) {
2863
+ if (this.$items.has(key)) {
2864
+ return this.$items.get(key);
2865
+ }
2866
+ this.set(key, defaultValue);
2867
+ return defaultValue;
2868
+ }
2869
+ /**
2870
+ * Returns the value for `key` if present. Otherwise computes a value via
2871
+ * `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
2872
+ * and returns it. The callback is only invoked when the key is missing.
2873
+ *
2874
+ * Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
2875
+ * typed in TypeScript 6's standard library).
2876
+ */
2877
+ getOrInsertComputed(key, callbackfn) {
2878
+ if (this.$items.has(key)) {
2879
+ return this.$items.get(key);
2880
+ }
2881
+ const value = callbackfn(key);
2882
+ // per spec: overwrites even if callbackfn itself inserted `key`
2883
+ this.set(key, value);
2884
+ return value;
2885
+ }
2786
2886
  delete(key) {
2787
2887
  if (!this.$items.has(key)) {
2788
2888
  return false;
package/build/index.mjs CHANGED
@@ -1450,6 +1450,33 @@ class ChangeTree {
1450
1450
  next: this.parentChain
1451
1451
  };
1452
1452
  }
1453
+ /**
1454
+ * Move `parent`'s existing chain entry to `index`, skipping the work
1455
+ * `addParent` does. `parent` must already be a parent of this tree.
1456
+ *
1457
+ * Called by collections whose wire slots shift (ArraySchema): StateView
1458
+ * addresses per-view ADD/DELETE by that index, so it has to follow the
1459
+ * element it names.
1460
+ */
1461
+ setParentIndex(parent, index) {
1462
+ const chain = this.parentChain;
1463
+ if (chain === undefined) {
1464
+ return;
1465
+ }
1466
+ if (chain.next === undefined) {
1467
+ chain.index = index; // sole parent, so it is `parent`
1468
+ return;
1469
+ }
1470
+ // Shared instance — move only the entry `parent` owns. Matching goes
1471
+ // through `$changes` because ArraySchema arrives proxied (see
1472
+ // removeParent below).
1473
+ for (let entry = chain; entry !== undefined; entry = entry.next) {
1474
+ if (entry.ref[$changes] === parent[$changes]) {
1475
+ entry.index = index;
1476
+ return;
1477
+ }
1478
+ }
1479
+ }
1453
1480
  /**
1454
1481
  * Remove a parent from the chain
1455
1482
  * @param parent - The parent to remove
@@ -2086,6 +2113,39 @@ class ArraySchema {
2086
2113
  get length() {
2087
2114
  return this.items.length;
2088
2115
  }
2116
+ /**
2117
+ * Re-point children at their wire slot. `ChangeTree.parentIndex` caches
2118
+ * the slot a child holds in `tmpItems`, and StateView addresses per-view
2119
+ * ADD/DELETE with it — so a reorder that leaves it behind aims those ops
2120
+ * at whichever element inherited the slot (issue #231).
2121
+ *
2122
+ * The filter check is a correctness boundary, not a tunable: StateView is
2123
+ * the only reader, and an array without `filteredChanges` never has a slot
2124
+ * read back. Everything else stops at that check instead of walking its
2125
+ * children every tick.
2126
+ *
2127
+ * Callers name the lowest slot that moved as `from`. Compaction cannot, so
2128
+ * it hands over the pre-compaction layout as `staged` and the unchanged
2129
+ * prefix is skipped instead. Either way tail churn walks nothing.
2130
+ */
2131
+ $reindexChildren(from, staged) {
2132
+ if (this[$changes].filteredChanges === undefined) {
2133
+ return;
2134
+ } // nothing will read the cache
2135
+ if (typeof this[$childType] === "string") {
2136
+ return;
2137
+ } // primitives have no child tree
2138
+ const tmpItems = this.tmpItems;
2139
+ const length = tmpItems.length;
2140
+ if (staged !== undefined) {
2141
+ while (from < length && tmpItems[from] === staged[from]) {
2142
+ from++;
2143
+ }
2144
+ }
2145
+ for (let i = from; i < length; i++) {
2146
+ tmpItems[i]?.[$changes]?.setParentIndex(this, i);
2147
+ }
2148
+ }
2089
2149
  push(...values) {
2090
2150
  let length = this.tmpItems.length;
2091
2151
  const changeTree = this[$changes];
@@ -2218,6 +2278,7 @@ class ArraySchema {
2218
2278
  this[$changes].operation(OPERATION.REVERSE);
2219
2279
  this.items.reverse();
2220
2280
  this.tmpItems.reverse();
2281
+ this.$reindexChildren(0);
2221
2282
  return this;
2222
2283
  }
2223
2284
  /**
@@ -2261,6 +2322,7 @@ class ArraySchema {
2261
2322
  // wouldn't OPERATION.MOVE make more sense here?
2262
2323
  sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
2263
2324
  this.tmpItems.sort(compareFn);
2325
+ this.$reindexChildren(0);
2264
2326
  this.isMovingItems = false;
2265
2327
  return this;
2266
2328
  }
@@ -2356,6 +2418,7 @@ class ArraySchema {
2356
2418
  changeTree.change(index, OPERATION.ADD);
2357
2419
  });
2358
2420
  this.tmpItems.unshift(...items);
2421
+ this.$reindexChildren(0); // from 0: nothing above placed the new items either
2359
2422
  return this.items.unshift(...items);
2360
2423
  }
2361
2424
  /**
@@ -2627,7 +2690,13 @@ class ArraySchema {
2627
2690
  this.tmpItems[index] = undefined; // TODO: do not try to get "tmpItems" at decoding time.
2628
2691
  }
2629
2692
  [$onEncodeEnd]() {
2693
+ const staged = this.tmpItems;
2630
2694
  this.tmpItems = this.items.slice();
2695
+ // Compaction just closed the staged holes — everything above the
2696
+ // lowest one slid down a slot. There is no cheap "were there any"
2697
+ // test to gate this on: `deletedIndexes` is an object here, and a
2698
+ // `for...in` probe measured slower than the prefix scan it skips.
2699
+ this.$reindexChildren(0, staged);
2631
2700
  this.deletedIndexes = {};
2632
2701
  }
2633
2702
  [$onDecodeEnd]() {
@@ -2777,6 +2846,37 @@ class MapSchema {
2777
2846
  get(key) {
2778
2847
  return this.$items.get(key);
2779
2848
  }
2849
+ /**
2850
+ * Returns the value for `key` if present. Otherwise inserts `defaultValue`
2851
+ * (tracked as an ADD change, like `set()`) and returns it.
2852
+ *
2853
+ * Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
2854
+ * TypeScript 6's standard library).
2855
+ */
2856
+ getOrInsert(key, defaultValue) {
2857
+ if (this.$items.has(key)) {
2858
+ return this.$items.get(key);
2859
+ }
2860
+ this.set(key, defaultValue);
2861
+ return defaultValue;
2862
+ }
2863
+ /**
2864
+ * Returns the value for `key` if present. Otherwise computes a value via
2865
+ * `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
2866
+ * and returns it. The callback is only invoked when the key is missing.
2867
+ *
2868
+ * Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
2869
+ * typed in TypeScript 6's standard library).
2870
+ */
2871
+ getOrInsertComputed(key, callbackfn) {
2872
+ if (this.$items.has(key)) {
2873
+ return this.$items.get(key);
2874
+ }
2875
+ const value = callbackfn(key);
2876
+ // per spec: overwrites even if callbackfn itself inserted `key`
2877
+ this.set(key, value);
2878
+ return value;
2879
+ }
2780
2880
  delete(key) {
2781
2881
  if (!this.$items.has(key)) {
2782
2882
  return false;