@colyseus/schema 3.0.0-alpha.42 → 3.0.0-alpha.44

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.
@@ -11,6 +11,8 @@ import { Root } from "./Root";
11
11
  import { Metadata } from "../Metadata";
12
12
  import type { EncodeOperation } from "./EncodeOperation";
13
13
  import type { DecodeOperation } from "../decoder/DecodeOperation";
14
+ import { TypeContext } from "../types/TypeContext";
15
+ import { ReferenceTracker } from "../decoder/ReferenceTracker";
14
16
 
15
17
  declare global {
16
18
  interface Object {
@@ -79,8 +81,10 @@ export class ChangeTree<T extends Ref=any> {
79
81
  parent?: Ref;
80
82
  parentIndex?: number;
81
83
 
84
+ /**
85
+ * Whether this structure is parent of a filtered structure.
86
+ */
82
87
  isFiltered: boolean = false;
83
- isPartiallyFiltered: boolean = false;
84
88
 
85
89
  indexedOperations: IndexedOperations = {};
86
90
 
@@ -109,7 +113,8 @@ export class ChangeTree<T extends Ref=any> {
109
113
  //
110
114
  // Does this structure have "filters" declared?
111
115
  //
112
- if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
116
+ const metadata = ref.constructor[Symbol.metadata];
117
+ if (metadata?.[$viewFieldIndexes]) {
113
118
  this.allFilteredChanges = { indexes: {}, operations: [] };
114
119
  this.filteredChanges = { indexes: {}, operations: [] };
115
120
  }
@@ -117,35 +122,10 @@ export class ChangeTree<T extends Ref=any> {
117
122
 
118
123
  setRoot(root: Root) {
119
124
  this.root = root;
120
- const isNewChangeTree = this.root.add(this);
121
-
122
- const metadata: Metadata = this.ref.constructor[Symbol.metadata];
123
-
124
- if (this.root.types.hasFilters) {
125
- //
126
- // At Schema initialization, the "root" structure might not be available
127
- // yet, as it only does once the "Encoder" has been set up.
128
- //
129
- // So the "parent" may be already set without a "root".
130
- //
131
- this.checkIsFiltered(metadata, this.parent, this.parentIndex);
132
-
133
- if (this.isFiltered || this.isPartiallyFiltered) {
134
- enqueueChangeTree(root, this, 'filteredChanges');
135
- if (isNewChangeTree) {
136
- this.root.allFilteredChanges.push(this);
137
- }
138
- }
139
- }
140
-
141
- if (!this.isFiltered) {
142
- enqueueChangeTree(root, this, 'changes');
143
- if (isNewChangeTree) {
144
- this.root.allChanges.push(this);
145
- }
146
- }
125
+ this.checkIsFiltered(this.parent, this.parentIndex);
147
126
 
148
127
  // Recursively set root on child structures
128
+ const metadata: Metadata = this.ref.constructor[Symbol.metadata];
149
129
  if (metadata) {
150
130
  metadata[$refTypeFieldIndexes]?.forEach((index) => {
151
131
  const field = metadata[index as any as number];
@@ -173,46 +153,22 @@ export class ChangeTree<T extends Ref=any> {
173
153
  // avoid setting parents with empty `root`
174
154
  if (!root) { return; }
175
155
 
176
- const metadata: Metadata = this.ref.constructor[Symbol.metadata];
177
-
178
156
  // skip if parent is already set
179
157
  if (root !== this.root) {
180
158
  this.root = root;
181
- const isNewChangeTree = root.add(this);
182
-
183
- if (root.types.hasFilters) {
184
- this.checkIsFiltered(metadata, parent, parentIndex);
185
-
186
- if (this.isFiltered || this.isPartiallyFiltered) {
187
- enqueueChangeTree(root, this, 'filteredChanges');
188
- if (isNewChangeTree) {
189
- this.root.allFilteredChanges.push(this);
190
- }
191
- }
192
- }
193
-
194
- if (!this.isFiltered) {
195
- enqueueChangeTree(root, this, 'changes');
196
- if (isNewChangeTree) {
197
- this.root.allChanges.push(this);
198
- }
199
- }
159
+ this.checkIsFiltered(parent, parentIndex);
200
160
 
201
161
  } else {
202
162
  root.add(this);
203
163
  }
204
164
 
205
165
  // assign same parent on child structures
166
+ const metadata: Metadata = this.ref.constructor[Symbol.metadata];
206
167
  if (metadata) {
207
168
  metadata[$refTypeFieldIndexes]?.forEach((index) => {
208
169
  const field = metadata[index as any as number];
209
170
  const value = this.ref[field.name];
210
171
  value?.[$changes].setParent(this.ref, root, index);
211
-
212
- // try { throw new Error(); } catch (e) {
213
- // console.log(e.stack);
214
- // }
215
-
216
172
  });
217
173
 
218
174
  } else if (this.ref[$childType] && typeof(this.ref[$childType]) !== "string") {
@@ -319,7 +275,7 @@ export class ChangeTree<T extends Ref=any> {
319
275
  //
320
276
  // - ArraySchema#splice()
321
277
  //
322
- if (this.isFiltered || this.isPartiallyFiltered) {
278
+ if (this.filteredChanges !== undefined) {
323
279
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allFilteredChanges);
324
280
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
325
281
 
@@ -352,7 +308,7 @@ export class ChangeTree<T extends Ref=any> {
352
308
  indexedOperation(index: number, operation: OPERATION, allChangesIndex: number = index) {
353
309
  this.indexedOperations[index] = operation;
354
310
 
355
- if (this.filteredChanges) {
311
+ if (this.filteredChanges !== undefined) {
356
312
  setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
357
313
  setOperationAtIndex(this.filteredChanges, index);
358
314
  enqueueChangeTree(this.root, this, 'filteredChanges');
@@ -404,7 +360,7 @@ export class ChangeTree<T extends Ref=any> {
404
360
  return;
405
361
  }
406
362
 
407
- const changeSet = (this.filteredChanges)
363
+ const changeSet = (this.filteredChanges !== undefined)
408
364
  ? this.filteredChanges
409
365
  : this.changes;
410
366
 
@@ -423,7 +379,7 @@ export class ChangeTree<T extends Ref=any> {
423
379
  // - This is due to using the concrete Schema class at decoding time.
424
380
  // - "Reflected" structures do not have this problem.
425
381
  //
426
- // (the property descriptors should NOT be used at decoding time. only at encoding time.)
382
+ // (The property descriptors should NOT be used at decoding time. only at encoding time.)
427
383
  //
428
384
  this.root?.remove(previousValue[$changes]);
429
385
  }
@@ -431,7 +387,7 @@ export class ChangeTree<T extends Ref=any> {
431
387
  //
432
388
  // FIXME: this is looking a ugly and repeated
433
389
  //
434
- if (this.filteredChanges) {
390
+ if (this.filteredChanges !== undefined) {
435
391
  deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
436
392
  enqueueChangeTree(this.root, this, 'filteredChanges');
437
393
 
@@ -519,19 +475,42 @@ export class ChangeTree<T extends Ref=any> {
519
475
  return (Object.entries(this.indexedOperations).length > 0);
520
476
  }
521
477
 
522
- protected checkIsFiltered(metadata: Metadata, parent: Ref, parentIndex: number) {
523
- // Detect if current structure has "filters" declared
524
- this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
478
+ protected checkIsFiltered(parent: Ref, parentIndex: number) {
479
+ const isNewChangeTree = this.root.add(this);
480
+
481
+ if (this.root.types.hasFilters) {
482
+ //
483
+ // At Schema initialization, the "root" structure might not be available
484
+ // yet, as it only does once the "Encoder" has been set up.
485
+ //
486
+ // So the "parent" may be already set without a "root".
487
+ //
488
+ this._checkFilteredByParent(parent, parentIndex);
525
489
 
526
- if (this.isPartiallyFiltered) {
527
- this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
528
- this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
490
+ if (this.filteredChanges !== undefined) {
491
+ enqueueChangeTree(this.root, this, 'filteredChanges');
492
+ if (isNewChangeTree) {
493
+ this.root.allFilteredChanges.push(this);
494
+ }
495
+ }
529
496
  }
530
497
 
531
- // skip if parent is not set
532
- if (!parent) {
533
- return;
498
+ if (!this.isFiltered) {
499
+ enqueueChangeTree(this.root, this, 'changes');
500
+ if (isNewChangeTree) {
501
+ this.root.allChanges.push(this);
502
+ }
534
503
  }
504
+ }
505
+
506
+ protected _checkFilteredByParent(parent: Ref, parentIndex: number) {
507
+ // skip if parent is not set
508
+ if (!parent) { return; }
509
+
510
+ // ArraySchema | MapSchema - get the child type
511
+ const ref = Metadata.isValidInstance(this.ref)
512
+ ? this.ref
513
+ : new this.ref[$childType];
535
514
 
536
515
  if (!Metadata.isValidInstance(parent)) {
537
516
  const parentChangeTree = parent[$changes];
@@ -539,14 +518,23 @@ export class ChangeTree<T extends Ref=any> {
539
518
  parentIndex = parentChangeTree.parentIndex;
540
519
  }
541
520
 
542
- const parentMetadata = parent.constructor?.[Symbol.metadata];
543
- this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
521
+ const parentConstructor = parent.constructor as typeof Schema;
522
+
523
+ let key = `${this.root.types.getTypeId(ref.constructor as typeof Schema)}`;
524
+ if (parentConstructor) { key += `-${this.root.types.schemas.get(parentConstructor)}`; }
525
+ key += `-${parentIndex}`;
526
+
527
+ this.isFiltered = this.root.types.parentFiltered[key];
528
+
529
+ // const parentMetadata = parentConstructor?.[Symbol.metadata];
530
+ // this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex) || this.root.types.parentFiltered[key];
544
531
 
545
532
  //
546
533
  // TODO: refactor this!
547
534
  //
548
535
  // swapping `changes` and `filteredChanges` is required here
549
536
  // because "isFiltered" may not be imedialely available on `change()`
537
+ // (this happens when instance is detached from root or parent)
550
538
  //
551
539
  if (this.isFiltered) {
552
540
  this.filteredChanges = { indexes: {}, operations: [] };
@@ -562,11 +550,6 @@ export class ChangeTree<T extends Ref=any> {
562
550
  const allFilteredChanges = this.allFilteredChanges;
563
551
  this.allFilteredChanges = this.allChanges;
564
552
  this.allChanges = allFilteredChanges;
565
-
566
- // console.log("SWAP =>", {
567
- // "this.allFilteredChanges": this.allFilteredChanges,
568
- // "this.allChanges": this.allChanges
569
- // })
570
553
  }
571
554
  }
572
555
  }
@@ -62,7 +62,7 @@ export class Root {
62
62
  this.removeChangeFromChangeSet("allChanges", changeTree);
63
63
  this.removeChangeFromChangeSet("changes", changeTree);
64
64
 
65
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
65
+ if (changeTree.filteredChanges) {
66
66
  this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
67
67
  this.removeChangeFromChangeSet("filteredChanges", changeTree);
68
68
  }
@@ -79,7 +79,7 @@ export class StateView {
79
79
 
80
80
  } else {
81
81
  const isInvisible = this.invisible.has(changeTree);
82
- const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
82
+ const changeSet = (changeTree.filteredChanges !== undefined)
83
83
  ? changeTree.allFilteredChanges
84
84
  : changeTree.allChanges;
85
85
 
@@ -87,7 +87,7 @@ export class StateView {
87
87
  const index = changeSet.operations[i];
88
88
  if (index === undefined) { continue; } // skip "undefined" indexes
89
89
 
90
- const op = changeTree.indexedOperations[index];
90
+ const op = changeTree.indexedOperations[index] ?? OPERATION.ADD;
91
91
  const tagAtIndex = metadata?.[index].tag;
92
92
  if (
93
93
  (
@@ -105,7 +105,11 @@ export class StateView {
105
105
  // Add children of this ChangeTree to this view
106
106
  changeTree.forEachChild((change, index) => {
107
107
  // Do not ADD children that don't have the same tag
108
- if (metadata && metadata[index].tag !== tag) {
108
+ if (
109
+ metadata &&
110
+ metadata[index].tag !== undefined &&
111
+ metadata[index].tag !== tag
112
+ ) {
109
113
  return;
110
114
  }
111
115
  this.add(change.ref, tag, false);
@@ -119,8 +123,8 @@ export class StateView {
119
123
  this.items.add(changeTree);
120
124
 
121
125
  // add parent's parent
122
- const parentChangeTree = changeTree.parent?.[$changes];
123
- if (parentChangeTree && (parentChangeTree.isFiltered || parentChangeTree.isPartiallyFiltered)) {
126
+ const parentChangeTree: ChangeTree = changeTree.parent?.[$changes];
127
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
124
128
  this.addParent(parentChangeTree, changeTree.parentIndex, tag);
125
129
  }
126
130
 
@@ -29,6 +29,11 @@ export class TypeContext {
29
29
 
30
30
  constructor(rootClass?: typeof Schema) {
31
31
  if (rootClass) {
32
+ //
33
+ // TODO:
34
+ // cache "discoverTypes" results for each rootClass
35
+ // to avoid re-discovering types for each new context/room
36
+ //
32
37
  this.discoverTypes(rootClass);
33
38
  }
34
39
  }
@@ -41,7 +46,7 @@ export class TypeContext {
41
46
  return this.types[typeid];
42
47
  }
43
48
 
44
- add(schema: typeof Schema, typeid: number = this.schemas.size) {
49
+ add(schema: typeof Schema, typeid = this.schemas.size) {
45
50
  // skip if already registered
46
51
  if (this.schemas.has(schema)) {
47
52
  return false;
@@ -64,14 +69,17 @@ export class TypeContext {
64
69
  return this.schemas.get(klass);
65
70
  }
66
71
 
67
- private discoverTypes(klass: typeof Schema, parentIndex?: number, parentFieldViewTag?: number) {
68
- if (!this.add(klass)) {
69
- return;
72
+ private discoverTypes(klass: typeof Schema, parentType?: typeof Schema, parentIndex?: number, parentHasViewTag?: boolean) {
73
+ if (parentHasViewTag) {
74
+ this.registerFilteredByParent(klass, parentType, parentIndex);
70
75
  }
71
76
 
77
+ // skip if already registered
78
+ if (!this.add(klass)) { return; }
79
+
72
80
  // add classes inherited from this base class
73
81
  TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
74
- this.discoverTypes(child, parentIndex, parentFieldViewTag);
82
+ this.discoverTypes(child, klass, parentIndex, parentHasViewTag);
75
83
  });
76
84
 
77
85
  // add parent classes
@@ -91,15 +99,11 @@ export class TypeContext {
91
99
  this.hasFilters = true;
92
100
  }
93
101
 
94
- if (parentFieldViewTag !== undefined) {
95
- this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
96
- }
97
-
98
102
  for (const fieldIndex in metadata) {
99
103
  const index = fieldIndex as any as number;
100
104
 
101
105
  const fieldType = metadata[index].type;
102
- const viewTag = metadata[index].tag;
106
+ const fieldHasViewTag = (metadata[index].tag !== undefined);
103
107
 
104
108
  if (typeof (fieldType) === "string") {
105
109
  continue;
@@ -113,10 +117,10 @@ export class TypeContext {
113
117
  continue;
114
118
  }
115
119
 
116
- this.discoverTypes(type as typeof Schema, index, viewTag);
120
+ this.discoverTypes(type as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);
117
121
 
118
122
  } else if (typeof (fieldType) === "function") {
119
- this.discoverTypes(fieldType as typeof Schema, viewTag);
123
+ this.discoverTypes(fieldType as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);
120
124
 
121
125
  } else {
122
126
  const type = Object.values(fieldType)[0];
@@ -126,8 +130,46 @@ export class TypeContext {
126
130
  continue;
127
131
  }
128
132
 
129
- this.discoverTypes(type as typeof Schema, index, viewTag);
133
+ this.discoverTypes(type as typeof Schema, klass, index, parentHasViewTag || fieldHasViewTag);
130
134
  }
131
135
  }
132
136
  }
137
+
138
+ /**
139
+ * Keep track of which classes have filters applied.
140
+ * Format: `${typeid}-${parentTypeid}-${parentIndex}`
141
+ */
142
+ private registerFilteredByParent(schema: typeof Schema, parentType?: typeof Schema, parentIndex?: number) {
143
+ const typeid = this.schemas.get(schema) ?? this.schemas.size;
144
+
145
+ let key = `${typeid}`;
146
+ if (parentType) { key += `-${this.schemas.get(parentType)}`; }
147
+
148
+ key += `-${parentIndex}`;
149
+ this.parentFiltered[key] = true;
150
+ }
151
+
152
+ debug() {
153
+ let parentFiltered = "";
154
+
155
+ for (const key in this.parentFiltered) {
156
+ const keys: number[] = key.split("-").map(Number);
157
+ const fieldIndex = keys.pop();
158
+
159
+ parentFiltered += `\n\t\t`;
160
+ parentFiltered += `${key}: ${keys.reverse().map((id, i) => {
161
+ const klass = this.types[id];
162
+ const metadata: Metadata = klass[Symbol.metadata];
163
+ let txt = klass.name;
164
+ if (i === 0) { txt += `[${metadata[fieldIndex].name}]`; }
165
+ return `${txt}`;
166
+ }).join(" -> ")}`;
167
+ }
168
+
169
+ return `TypeContext ->\n` +
170
+ `\tSchema types: ${this.schemas.size}\n` +
171
+ `\thasFilters: ${this.hasFilters}\n` +
172
+ `\tparentFiltered:${parentFiltered}`;
173
+ }
174
+
133
175
  }