@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.
@@ -32,6 +32,22 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
32
32
  constructor(...items: V[]);
33
33
  set length(newLength: number);
34
34
  get length(): number;
35
+ /**
36
+ * Re-point children at their wire slot. `ChangeTree.parentIndex` caches
37
+ * the slot a child holds in `tmpItems`, and StateView addresses per-view
38
+ * ADD/DELETE with it — so a reorder that leaves it behind aims those ops
39
+ * at whichever element inherited the slot (issue #231).
40
+ *
41
+ * The filter check is a correctness boundary, not a tunable: StateView is
42
+ * the only reader, and an array without `filteredChanges` never has a slot
43
+ * read back. Everything else stops at that check instead of walking its
44
+ * children every tick.
45
+ *
46
+ * Callers name the lowest slot that moved as `from`. Compaction cannot, so
47
+ * it hands over the pre-compaction layout as `staged` and the unchanged
48
+ * prefix is skipped instead. Either way tail churn walks nothing.
49
+ */
50
+ protected $reindexChildren(from: number, staged?: V[]): void;
35
51
  push(...values: V[]): number;
36
52
  /**
37
53
  * Removes the last element from an array and returns it.
@@ -33,6 +33,23 @@ export declare class MapSchema<V = any, K extends string = string> implements Ma
33
33
  static get [Symbol.species](): typeof MapSchema;
34
34
  set(key: K, value: V): this;
35
35
  get(key: K): V | undefined;
36
+ /**
37
+ * Returns the value for `key` if present. Otherwise inserts `defaultValue`
38
+ * (tracked as an ADD change, like `set()`) and returns it.
39
+ *
40
+ * Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
41
+ * TypeScript 6's standard library).
42
+ */
43
+ getOrInsert(key: K, defaultValue: V): V;
44
+ /**
45
+ * Returns the value for `key` if present. Otherwise computes a value via
46
+ * `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
47
+ * and returns it. The callback is only invoked when the key is missing.
48
+ *
49
+ * Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
50
+ * typed in TypeScript 6's standard library).
51
+ */
52
+ getOrInsertComputed(key: K, callbackfn: (key: K) => V): V;
36
53
  delete(key: K): boolean;
37
54
  clear(): void;
38
55
  has(key: K): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "4.0.29",
3
+ "version": "4.0.31",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
@@ -652,6 +652,33 @@ export class ChangeTree<T extends Ref = any> {
652
652
  };
653
653
  }
654
654
 
655
+ /**
656
+ * Move `parent`'s existing chain entry to `index`, skipping the work
657
+ * `addParent` does. `parent` must already be a parent of this tree.
658
+ *
659
+ * Called by collections whose wire slots shift (ArraySchema): StateView
660
+ * addresses per-view ADD/DELETE by that index, so it has to follow the
661
+ * element it names.
662
+ */
663
+ setParentIndex(parent: Ref, index: number) {
664
+ const chain = this.parentChain;
665
+ if (chain === undefined) { return; }
666
+
667
+ if (chain.next === undefined) {
668
+ chain.index = index; // sole parent, so it is `parent`
669
+ return;
670
+ }
671
+ // Shared instance — move only the entry `parent` owns. Matching goes
672
+ // through `$changes` because ArraySchema arrives proxied (see
673
+ // removeParent below).
674
+ for (let entry = chain; entry !== undefined; entry = entry.next) {
675
+ if (entry.ref[$changes] === parent[$changes]) {
676
+ entry.index = index;
677
+ return;
678
+ }
679
+ }
680
+ }
681
+
655
682
  /**
656
683
  * Remove a parent from the chain
657
684
  * @param parent - The parent to remove
@@ -186,6 +186,34 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
186
186
  return this.items.length;
187
187
  }
188
188
 
189
+ /**
190
+ * Re-point children at their wire slot. `ChangeTree.parentIndex` caches
191
+ * the slot a child holds in `tmpItems`, and StateView addresses per-view
192
+ * ADD/DELETE with it — so a reorder that leaves it behind aims those ops
193
+ * at whichever element inherited the slot (issue #231).
194
+ *
195
+ * The filter check is a correctness boundary, not a tunable: StateView is
196
+ * the only reader, and an array without `filteredChanges` never has a slot
197
+ * read back. Everything else stops at that check instead of walking its
198
+ * children every tick.
199
+ *
200
+ * Callers name the lowest slot that moved as `from`. Compaction cannot, so
201
+ * it hands over the pre-compaction layout as `staged` and the unchanged
202
+ * prefix is skipped instead. Either way tail churn walks nothing.
203
+ */
204
+ protected $reindexChildren(from: number, staged?: V[]) {
205
+ if (this[$changes].filteredChanges === undefined) { return; } // nothing will read the cache
206
+ if (typeof this[$childType] === "string") { return; } // primitives have no child tree
207
+ const tmpItems = this.tmpItems;
208
+ const length = tmpItems.length;
209
+ if (staged !== undefined) {
210
+ while (from < length && tmpItems[from] === staged[from]) { from++; }
211
+ }
212
+ for (let i = from; i < length; i++) {
213
+ tmpItems[i]?.[$changes]?.setParentIndex(this, i);
214
+ }
215
+ }
216
+
189
217
  push(...values: V[]) {
190
218
  let length = this.tmpItems.length;
191
219
 
@@ -348,6 +376,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
348
376
  this[$changes].operation(OPERATION.REVERSE);
349
377
  this.items.reverse();
350
378
  this.tmpItems.reverse();
379
+ this.$reindexChildren(0);
351
380
  return this;
352
381
  }
353
382
 
@@ -400,6 +429,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
400
429
  sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
401
430
 
402
431
  this.tmpItems.sort(compareFn);
432
+ this.$reindexChildren(0);
403
433
 
404
434
  this.isMovingItems = false;
405
435
  return this;
@@ -519,6 +549,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
519
549
  });
520
550
 
521
551
  this.tmpItems.unshift(...items);
552
+ this.$reindexChildren(0); // from 0: nothing above placed the new items either
522
553
 
523
554
  return this.items.unshift(...items);
524
555
  }
@@ -845,7 +876,15 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
845
876
  }
846
877
 
847
878
  protected [$onEncodeEnd]() {
879
+ const staged = this.tmpItems;
848
880
  this.tmpItems = this.items.slice();
881
+
882
+ // Compaction just closed the staged holes — everything above the
883
+ // lowest one slid down a slot. There is no cheap "were there any"
884
+ // test to gate this on: `deletedIndexes` is an object here, and a
885
+ // `for...in` probe measured slower than the prefix scan it skips.
886
+ this.$reindexChildren(0, staged);
887
+
849
888
  this.deletedIndexes = {};
850
889
  }
851
890
 
@@ -152,6 +152,39 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
152
152
  return this.$items.get(key);
153
153
  }
154
154
 
155
+ /**
156
+ * Returns the value for `key` if present. Otherwise inserts `defaultValue`
157
+ * (tracked as an ADD change, like `set()`) and returns it.
158
+ *
159
+ * Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
160
+ * TypeScript 6's standard library).
161
+ */
162
+ getOrInsert(key: K, defaultValue: V): V {
163
+ if (this.$items.has(key)) {
164
+ return this.$items.get(key);
165
+ }
166
+ this.set(key, defaultValue);
167
+ return defaultValue;
168
+ }
169
+
170
+ /**
171
+ * Returns the value for `key` if present. Otherwise computes a value via
172
+ * `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
173
+ * and returns it. The callback is only invoked when the key is missing.
174
+ *
175
+ * Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
176
+ * typed in TypeScript 6's standard library).
177
+ */
178
+ getOrInsertComputed(key: K, callbackfn: (key: K) => V): V {
179
+ if (this.$items.has(key)) {
180
+ return this.$items.get(key);
181
+ }
182
+ const value = callbackfn(key);
183
+ // per spec: overwrites even if callbackfn itself inserted `key`
184
+ this.set(key, value);
185
+ return value;
186
+ }
187
+
155
188
  delete(key: K) {
156
189
  if (!this.$items.has(key)) {
157
190
  return false;