@colyseus/schema 3.0.26 → 3.0.28

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.
@@ -1,3 +1,5 @@
1
+ import * as util from "util";
2
+
1
3
  import { OPERATION } from "../encoding/spec";
2
4
  import { Schema } from "../Schema";
3
5
  import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refTypeFieldIndexes, $viewFieldIndexes } from "../types/symbols";
@@ -39,7 +41,7 @@ export interface IndexedOperations {
39
41
  export interface ChangeSet {
40
42
  // field index -> operation index
41
43
  indexes: { [index: number]: number };
42
- operations: OPERATION[]
44
+ operations: number[];
43
45
  queueRootIndex?: number; // index of ChangeTree structure in `root.changes` or `root.filteredChanges`
44
46
  }
45
47
 
@@ -56,14 +58,43 @@ export function setOperationAtIndex(changeSet: ChangeSet, index: number) {
56
58
  }
57
59
  }
58
60
 
59
- export function deleteOperationAtIndex(changeSet: ChangeSet, index: number) {
60
- const operationsIndex = changeSet.indexes[index];
61
- if (operationsIndex !== undefined) {
62
- changeSet.operations[operationsIndex] = undefined;
61
+ export function deleteOperationAtIndex(changeSet: ChangeSet, index: number | string) {
62
+ let operationsIndex = changeSet.indexes[index];
63
+ if (operationsIndex === undefined) {
64
+ //
65
+ // if index is not found, we need to find the last operation
66
+ // FIXME: this is not very efficient
67
+ //
68
+ // > See "should allow consecutive splices (same place)" tests
69
+ //
70
+ operationsIndex = Object.values(changeSet.indexes).at(-1);
71
+ index = Object.entries(changeSet.indexes).find(([_, value]) => value === operationsIndex)?.[0];
63
72
  }
73
+ changeSet.operations[operationsIndex] = undefined;
64
74
  delete changeSet.indexes[index];
65
75
  }
66
76
 
77
+ export function debugChangeSet(label: string, changeSet: ChangeSet) {
78
+ let indexes: string[] = [];
79
+ let operations: string[] = [];
80
+
81
+ for (const index in changeSet.indexes) {
82
+ indexes.push(`\t${util.inspect(index, { colors: true })} => [${util.inspect(changeSet.indexes[index], { colors: true })}]`);
83
+ }
84
+
85
+ for (let i = 0; i < changeSet.operations.length; i++) {
86
+ const index = changeSet.operations[i];
87
+ if (index !== undefined) {
88
+ operations.push(`\t[${util.inspect(i, { colors: true })}] => ${util.inspect(index, { colors: true })}`);
89
+ }
90
+ }
91
+
92
+ console.log(`${label} =>\nindexes (${Object.keys(changeSet.indexes).length}) {`);
93
+ console.log(indexes.join("\n"), "\n}");
94
+ console.log(`operations (${changeSet.operations.filter(op => op !== undefined).length}) {`);
95
+ console.log(operations.join("\n"), "\n}");
96
+ }
97
+
67
98
  export function enqueueChangeTree(
68
99
  root: Root,
69
100
  changeTree: ChangeTree,
@@ -91,6 +122,7 @@ export class ChangeTree<T extends Ref=any> {
91
122
  * Whether this structure is parent of a filtered structure.
92
123
  */
93
124
  isFiltered: boolean = false;
125
+ isVisibilitySharedWithParent?: boolean; // See test case: 'should not be required to manually call view.add() items to child arrays without @view() tag'
94
126
 
95
127
  indexedOperations: IndexedOperations = {};
96
128
 
@@ -292,14 +324,9 @@ export class ChangeTree<T extends Ref=any> {
292
324
 
293
325
  private _shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0, changeSet: ChangeSet) {
294
326
  const newIndexes = {};
295
-
327
+ let newKey = 0;
296
328
  for (const key in changeSet.indexes) {
297
- const index = changeSet.indexes[key];
298
- if (index > startIndex) {
299
- newIndexes[Number(key) + shiftIndex] = index;
300
- } else {
301
- newIndexes[key] = index;
302
- }
329
+ newIndexes[newKey++] = changeSet.indexes[key];
303
330
  }
304
331
  changeSet.indexes = newIndexes;
305
332
 
@@ -524,10 +551,16 @@ export class ChangeTree<T extends Ref=any> {
524
551
  ? this.ref.constructor
525
552
  : this.ref[$childType];
526
553
 
527
- if (!Metadata.isValidInstance(parent)) {
528
- const parentChangeTree = parent[$changes];
554
+ let parentChangeTree: ChangeTree;
555
+
556
+ let parentIsCollection = !Metadata.isValidInstance(parent);
557
+ if (parentIsCollection) {
558
+ parentChangeTree = parent[$changes];
529
559
  parent = parentChangeTree.parent;
530
560
  parentIndex = parentChangeTree.parentIndex;
561
+
562
+ } else {
563
+ parentChangeTree = parent[$changes]
531
564
  }
532
565
 
533
566
  const parentConstructor = parent.constructor as typeof Schema;
@@ -538,15 +571,25 @@ export class ChangeTree<T extends Ref=any> {
538
571
  }
539
572
  key += `-${parentIndex}`;
540
573
 
574
+ const fieldHasViewTag = parentConstructor?.[Symbol.metadata]?.[$viewFieldIndexes]?.includes(parentIndex);
575
+
541
576
  this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
542
577
  || this.root.types.parentFiltered[key]
543
- || parentConstructor?.[Symbol.metadata]?.[$viewFieldIndexes]?.includes(parentIndex);
578
+ || fieldHasViewTag;
544
579
 
545
580
  //
546
581
  // "isFiltered" may not be imedialely available during `change()` due to the instance not being attached to the root yet.
547
582
  // when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
548
583
  //
549
584
  if (this.isFiltered) {
585
+
586
+ this.isVisibilitySharedWithParent = (
587
+ parentChangeTree.isFiltered &&
588
+ typeof (refType) !== "string" &&
589
+ !fieldHasViewTag &&
590
+ parentIsCollection
591
+ );
592
+
550
593
  if (!this.filteredChanges) {
551
594
  this.filteredChanges = createChangeSet();
552
595
  this.allFilteredChanges = createChangeSet();
@@ -217,6 +217,8 @@ export const encodeArray: EncodeOperation = function (
217
217
  const type = changeTree.getType(field);
218
218
  const value = changeTree.getValue(field, isEncodeAll);
219
219
 
220
+ // console.log({ type, field, value });
221
+
220
222
  // console.log("encodeArray -> ", {
221
223
  // ref: changeTree.ref.constructor.name,
222
224
  // field,
@@ -10,7 +10,7 @@ import { Root } from "./Root";
10
10
 
11
11
  import type { StateView } from "./StateView";
12
12
  import type { Metadata } from "../Metadata";
13
- import type { ChangeTree } from "./ChangeTree";
13
+ import type { ChangeSetName, ChangeTree } from "./ChangeTree";
14
14
 
15
15
  export class Encoder<T extends Schema = any> {
16
16
  static BUFFER_SIZE = (typeof(Buffer) !== "undefined") && Buffer.poolSize || 8 * 1024; // 8KB
@@ -48,7 +48,7 @@ export class Encoder<T extends Schema = any> {
48
48
  it: Iterator = { offset: 0 },
49
49
  view?: StateView,
50
50
  buffer = this.sharedBuffer,
51
- changeSetName: "changes" | "allChanges" | "filteredChanges" | "allFilteredChanges" = "changes",
51
+ changeSetName: ChangeSetName = "changes",
52
52
  isEncodeAll = changeSetName === "allChanges",
53
53
  initialOffset = it.offset // cache current offset in case we need to resize the buffer
54
54
  ): Buffer {
@@ -61,20 +61,19 @@ export class Encoder<T extends Schema = any> {
61
61
  if (!changeTree) { continue; }
62
62
 
63
63
  if (hasView) {
64
- if (!view.visible.has(changeTree)) {
64
+ if (!view.isChangeTreeVisible(changeTree)) {
65
+ // console.log("MARK AS INVISIBLE:", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, raw: changeTree.ref.toJSON() });
65
66
  view.invisible.add(changeTree);
66
67
  continue; // skip this change tree
67
-
68
- } else {
69
- view.invisible.delete(changeTree); // remove from invisible list
70
68
  }
69
+ view.invisible.delete(changeTree); // remove from invisible list
71
70
  }
72
71
 
73
- const operations = changeTree[changeSetName];
72
+ const changeSet = changeTree[changeSetName];
74
73
  const ref = changeTree.ref;
75
74
 
76
75
  // TODO: avoid iterating over change tree if no changes were made
77
- const numChanges = operations.operations.length;
76
+ const numChanges = changeSet.operations.length;
78
77
  if (numChanges === 0) { continue; }
79
78
 
80
79
  const ctor = ref.constructor;
@@ -90,7 +89,7 @@ export class Encoder<T extends Schema = any> {
90
89
  }
91
90
 
92
91
  for (let j = 0; j < numChanges; j++) {
93
- const fieldIndex = operations.operations[j];
92
+ const fieldIndex = changeSet.operations[j];
94
93
 
95
94
  const operation = (fieldIndex < 0)
96
95
  ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
@@ -291,4 +291,28 @@ export class StateView {
291
291
  // clear items array
292
292
  this.items.length = 0;
293
293
  }
294
+
295
+ isChangeTreeVisible(changeTree: ChangeTree) {
296
+ let isVisible = this.visible.has(changeTree);
297
+
298
+ //
299
+ // TODO: avoid checking for parent visibility, most of the time it's not needed
300
+ // See test case: 'should not be required to manually call view.add() items to child arrays without @view() tag'
301
+ //
302
+ if (!isVisible && changeTree.isVisibilitySharedWithParent){
303
+
304
+ // console.log("CHECK AGAINST PARENT...", {
305
+ // ref: changeTree.ref.constructor.name,
306
+ // refId: changeTree.refId,
307
+ // parent: changeTree.parent.constructor.name,
308
+ // });
309
+
310
+ if (this.visible.has(changeTree.parent[$changes])) {
311
+ this.visible.add(changeTree);
312
+ isVisible = true;
313
+ }
314
+ }
315
+
316
+ return isVisible;
317
+ }
294
318
  }
package/src/index.ts CHANGED
@@ -50,7 +50,7 @@ export { getRawChangesCallback } from "./decoder/strategy/RawChanges";
50
50
 
51
51
  export { Encoder } from "./encoder/Encoder";
52
52
  export { encodeSchemaOperation, encodeArray, encodeKeyValueOperation } from "./encoder/EncodeOperation";
53
- export { ChangeTree, Ref } from "./encoder/ChangeTree";
53
+ export { ChangeTree, Ref, type ChangeSetName, type ChangeSet} from "./encoder/ChangeTree";
54
54
  export { StateView } from "./encoder/StateView";
55
55
 
56
56
  export { Decoder } from "./decoder/Decoder";
@@ -1,6 +1,6 @@
1
1
  import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd } from "../symbols";
2
2
  import type { Schema } from "../../Schema";
3
- import { ChangeTree, setOperationAtIndex } from "../../encoder/ChangeTree";
3
+ import { ChangeTree, debugChangeSet, deleteOperationAtIndex, enqueueChangeTree, setOperationAtIndex } from "../../encoder/ChangeTree";
4
4
  import { OPERATION } from "../../encoding/spec";
5
5
  import { registerType } from "../registry";
6
6
  import { Collection } from "../HelperTypes";
@@ -41,8 +41,7 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
41
41
  return (
42
42
  !view ||
43
43
  typeof (ref[$childType]) === "string" ||
44
- // view.items.has(ref[$getByIndex](index)[$changes])
45
- view.visible.has(ref['tmpItems'][index]?.[$changes])
44
+ view.isChangeTreeVisible(ref['tmpItems'][index]?.[$changes])
46
45
  );
47
46
  }
48
47
 
@@ -229,9 +228,6 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
229
228
 
230
229
  this[$changes].delete(index, undefined, this.items.length - 1);
231
230
 
232
- // this.tmpItems[index] = undefined;
233
- // this.tmpItems.pop();
234
-
235
231
  this.deletedIndexes[index] = true;
236
232
 
237
233
  return this.items.pop();
@@ -412,38 +408,63 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
412
408
  */
413
409
  splice(
414
410
  start: number,
415
- deleteCount: number = this.items.length - start,
411
+ deleteCount?: number,
416
412
  ...insertItems: V[]
417
413
  ): V[] {
418
414
  const changeTree = this[$changes];
419
415
 
416
+ const itemsLength = this.items.length;
420
417
  const tmpItemsLength = this.tmpItems.length;
421
418
  const insertCount = insertItems.length;
422
419
 
423
420
  // build up-to-date list of indexes, excluding removed values.
424
421
  const indexes: number[] = [];
425
422
  for (let i = 0; i < tmpItemsLength; i++) {
426
- // if (this.tmpItems[i] !== undefined) {
427
423
  if (this.deletedIndexes[i] !== true) {
428
424
  indexes.push(i);
429
425
  }
430
426
  }
431
427
 
432
- // delete operations at correct index
433
- for (let i = start; i < start + deleteCount; i++) {
434
- const index = indexes[i];
435
- changeTree.delete(index);
436
- // this.tmpItems[index] = undefined;
437
- this.deletedIndexes[index] = true;
428
+ if (itemsLength > start) {
429
+ // if deleteCount is not provided, delete all items from start to end
430
+ if (deleteCount === undefined) {
431
+ deleteCount = itemsLength - start;
432
+ }
433
+
434
+ //
435
+ // delete operations at correct index
436
+ //
437
+ for (let i = start; i < start + deleteCount; i++) {
438
+ const index = indexes[i];
439
+ changeTree.delete(index, OPERATION.DELETE);
440
+ this.deletedIndexes[index] = true;
441
+ }
442
+
443
+ } else {
444
+ // not enough items to delete
445
+ deleteCount = 0;
438
446
  }
439
447
 
440
- // force insert operations
441
- for (let i = 0; i < insertCount; i++) {
442
- const addIndex = indexes[start] + i;
443
- changeTree.indexedOperation(addIndex, OPERATION.ADD);
448
+ // insert operations
449
+ if (insertCount > 0) {
450
+ if (insertCount > deleteCount) {
451
+ console.error("Inserting more elements than deleting during ArraySchema#splice()");
452
+ throw new Error("ArraySchema#splice(): insertCount must be equal or lower than deleteCount.");
453
+ }
454
+
455
+ for (let i = 0; i < insertCount; i++) {
456
+ const addIndex = (indexes[start] ?? itemsLength) + i;
457
+
458
+ changeTree.indexedOperation(
459
+ addIndex,
460
+ (this.deletedIndexes[addIndex])
461
+ ? OPERATION.DELETE_AND_ADD
462
+ : OPERATION.ADD
463
+ );
444
464
 
445
- // set value's parent/root
446
- insertItems[i][$changes]?.setParent(this, changeTree.root, addIndex);
465
+ // set value's parent/root
466
+ insertItems[i][$changes]?.setParent(this, changeTree.root, addIndex);
467
+ }
447
468
  }
448
469
 
449
470
  //
@@ -452,6 +473,17 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
452
473
  //
453
474
  if (deleteCount > insertCount) {
454
475
  changeTree.shiftAllChangeIndexes(-(deleteCount - insertCount), indexes[start + insertCount]);
476
+ // debugChangeSet("AFTER SHIFT indexes", changeTree.allChanges);
477
+ }
478
+
479
+ //
480
+ // FIXME: this code block is duplicated on ChangeTree
481
+ //
482
+ if (changeTree.filteredChanges !== undefined) {
483
+ enqueueChangeTree(changeTree.root, changeTree, 'filteredChanges');
484
+
485
+ } else {
486
+ enqueueChangeTree(changeTree.root, changeTree, 'changes');
455
487
  }
456
488
 
457
489
  return this.items.splice(start, deleteCount, ...insertItems);
@@ -767,10 +799,6 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
767
799
  : this.deletedIndexes[index]
768
800
  ? this.items[index]
769
801
  : this.tmpItems[index] || this.items[index];
770
-
771
- // return (isEncodeAll)
772
- // ? this.items[index]
773
- // : this.tmpItems[index] ?? this.items[index];
774
802
  }
775
803
 
776
804
  protected [$deleteByIndex](index: number) {
@@ -33,7 +33,7 @@ export class CollectionSchema<V=any> implements Collection<K, V>{
33
33
  return (
34
34
  !view ||
35
35
  typeof (ref[$childType]) === "string" ||
36
- view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
36
+ view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
37
37
  );
38
38
  }
39
39
 
@@ -34,7 +34,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
34
34
  return (
35
35
  !view ||
36
36
  typeof (ref[$childType]) === "string" ||
37
- view.visible.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
37
+ view.isChangeTreeVisible((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
38
38
  );
39
39
  }
40
40