@colyseus/schema 3.0.19 → 3.0.21

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/src/Reflection.ts CHANGED
@@ -44,71 +44,104 @@ export class Reflection extends Schema {
44
44
  const rootType = context.schemas.get(encoder.state.constructor);
45
45
  if (rootType > 0) { reflection.rootType = rootType; }
46
46
 
47
- const buildType = (currentType: ReflectionType, metadata: Metadata) => {
48
- for (const fieldIndex in metadata) {
49
- const index = Number(fieldIndex);
50
- const fieldName = metadata[index].name;
51
-
52
- // skip fields from parent classes
53
- if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
54
- continue;
47
+ const includedTypeIds = new Set<number>();
48
+ const pendingReflectionTypes: { [typeid: number]: ReflectionType[] } = {};
49
+
50
+ // add type to reflection in a way that respects inheritance
51
+ // (parent types should be added before their children)
52
+ const addType = (type: ReflectionType) => {
53
+ if (type.extendsId === undefined || includedTypeIds.has(type.extendsId)) {
54
+ includedTypeIds.add(type.id);
55
+
56
+ reflection.types.push(type);
57
+
58
+ const deps = pendingReflectionTypes[type.id];
59
+ if (deps !== undefined) {
60
+ delete pendingReflectionTypes[type.id];
61
+ deps.forEach((childType) => addType(childType));
55
62
  }
63
+ } else {
64
+ if (pendingReflectionTypes[type.extendsId] === undefined) {
65
+ pendingReflectionTypes[type.extendsId] = [];
66
+ }
67
+ pendingReflectionTypes[type.extendsId].push(type);
68
+ }
69
+ };
56
70
 
57
- const field = new ReflectionField();
58
- field.name = fieldName;
71
+ context.schemas.forEach((typeid, klass) => {
72
+ const type = new ReflectionType();
73
+ type.id = Number(typeid);
59
74
 
60
- let fieldType: string;
75
+ // support inheritance
76
+ const inheritFrom = Object.getPrototypeOf(klass);
77
+ if (inheritFrom !== Schema) {
78
+ type.extendsId = context.schemas.get(inheritFrom);
79
+ }
61
80
 
62
- const type = metadata[index].type;
81
+ const metadata = klass[Symbol.metadata];
63
82
 
64
- if (typeof (type) === "string") {
65
- fieldType = type;
83
+ //
84
+ // FIXME: this is a workaround for inherited types without additional fields
85
+ // if metadata is the same reference as the parent class - it means the class has no own metadata
86
+ //
87
+ if (metadata !== inheritFrom[Symbol.metadata]) {
88
+ for (const fieldIndex in metadata) {
89
+ const index = Number(fieldIndex);
90
+ const fieldName = metadata[index].name;
66
91
 
67
- } else {
68
- let childTypeSchema: typeof Schema;
92
+ // skip fields from parent classes
93
+ if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
94
+ continue;
95
+ }
96
+
97
+ const reflectionField = new ReflectionField();
98
+ reflectionField.name = fieldName;
99
+
100
+ let fieldType: string;
69
101
 
70
- //
71
- // TODO: refactor below.
72
- //
73
- if (Schema.is(type)) {
74
- fieldType = "ref";
75
- childTypeSchema = type as typeof Schema;
102
+ const field = metadata[index];
103
+
104
+ if (typeof (field.type) === "string") {
105
+ fieldType = field.type;
76
106
 
77
107
  } else {
78
- fieldType = Object.keys(type)[0];
108
+ let childTypeSchema: typeof Schema;
79
109
 
80
- if (typeof(type[fieldType]) === "string") {
81
- fieldType += ":" + type[fieldType]; // array:string
110
+ //
111
+ // TODO: refactor below.
112
+ //
113
+ if (Schema.is(field.type)) {
114
+ fieldType = "ref";
115
+ childTypeSchema = field.type as typeof Schema;
82
116
 
83
117
  } else {
84
- childTypeSchema = type[fieldType];
118
+ fieldType = Object.keys(field.type)[0];
119
+
120
+ if (typeof (field.type[fieldType]) === "string") {
121
+ fieldType += ":" + field.type[fieldType]; // array:string
122
+
123
+ } else {
124
+ childTypeSchema = field.type[fieldType];
125
+ }
85
126
  }
127
+
128
+ reflectionField.referencedType = (childTypeSchema)
129
+ ? context.getTypeId(childTypeSchema)
130
+ : -1;
86
131
  }
87
132
 
88
- field.referencedType = (childTypeSchema)
89
- ? context.getTypeId(childTypeSchema)
90
- : -1;
133
+ reflectionField.type = fieldType;
134
+ type.fields.push(reflectionField);
91
135
  }
92
-
93
- field.type = fieldType;
94
- currentType.fields.push(field);
95
136
  }
96
137
 
97
- reflection.types.push(currentType);
98
- }
99
-
100
- for (let typeid in context.types) {
101
- const klass = context.types[typeid];
102
- const type = new ReflectionType();
103
- type.id = Number(typeid);
104
-
105
- // support inheritance
106
- const inheritFrom = Object.getPrototypeOf(klass);
107
- if (inheritFrom !== Schema) {
108
- type.extendsId = context.schemas.get(inheritFrom);
109
- }
138
+ addType(type);
139
+ });
110
140
 
111
- buildType(type, klass[Symbol.metadata]);
141
+ // in case there are types that were not added due to inheritance
142
+ for (const typeid in pendingReflectionTypes) {
143
+ pendingReflectionTypes[typeid].forEach((type) =>
144
+ reflection.types.push(type))
112
145
  }
113
146
 
114
147
  const buf = reflectionEncoder.encodeAll(it);
@@ -23,10 +23,12 @@ export class Encoder<T extends Schema = any> {
23
23
 
24
24
  constructor(state: T) {
25
25
  //
26
- // TODO: cache and restore "Context" based on root schema
27
- // (to avoid creating a new context for every new room)
26
+ // Use .cache() here to avoid re-creating a new context for every new room instance.
28
27
  //
29
- this.context = new TypeContext(state.constructor as typeof Schema);
28
+ // We may need to make this optional in case of dynamically created
29
+ // schemas - which would lead to memory leaks
30
+ //
31
+ this.context = TypeContext.cache(state.constructor as typeof Schema);
30
32
  this.root = new Root(this.context);
31
33
 
32
34
  this.setState(state);
@@ -64,7 +66,7 @@ export class Encoder<T extends Schema = any> {
64
66
  view.invisible.add(changeTree);
65
67
  continue; // skip this change tree
66
68
 
67
- } else if (view.invisible.has(changeTree)) {
69
+ } else {
68
70
  view.invisible.delete(changeTree); // remove from invisible list
69
71
  }
70
72
  }
@@ -90,6 +90,7 @@ export class StateView {
90
90
  const op = changeTree.indexedOperations[index] ?? OPERATION.ADD;
91
91
  const tagAtIndex = metadata?.[index].tag;
92
92
  if (
93
+ !changeTree.isNew && // new structures will be added as part of .encode() call, no need to force it to .encodeView()
93
94
  (
94
95
  isInvisible || // if "invisible", include all
95
96
  tagAtIndex === undefined || // "all change" with no tag
@@ -132,8 +133,8 @@ export class StateView {
132
133
  this.addParentOf(changeTree, tag);
133
134
  }
134
135
 
135
- // parent is already available, no need to add it!
136
- if (!this.invisible.has(changeTree)) { return; }
136
+ // // parent is already available, no need to add it!
137
+ // if (!this.invisible.has(changeTree)) { return; }
137
138
  }
138
139
 
139
140
  // add parent's tag properties
@@ -14,6 +14,7 @@ export class TypeContext {
14
14
  * Keeps track of which classes extends which. (parent -> children)
15
15
  */
16
16
  static inheritedTypes = new Map<typeof Schema, Set<typeof Schema>>();
17
+ static cachedContexts = new Map<typeof Schema, TypeContext>();
17
18
 
18
19
  static register(target: typeof Schema) {
19
20
  const parent = Object.getPrototypeOf(target);
@@ -27,13 +28,17 @@ export class TypeContext {
27
28
  }
28
29
  }
29
30
 
31
+ static cache (rootClass: typeof Schema) {
32
+ let context = TypeContext.cachedContexts.get(rootClass);
33
+ if (!context) {
34
+ context = new TypeContext(rootClass);
35
+ TypeContext.cachedContexts.set(rootClass, context);
36
+ }
37
+ return context;
38
+ }
39
+
30
40
  constructor(rootClass?: typeof Schema) {
31
41
  if (rootClass) {
32
- //
33
- // TODO:
34
- // cache "discoverTypes" results for each rootClass
35
- // to avoid re-discovering types for each new context/room
36
- //
37
42
  this.discoverTypes(rootClass);
38
43
  }
39
44
  }
@@ -1,4 +1,4 @@
1
- import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex } from "../symbols";
1
+ import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd } from "../symbols";
2
2
  import { ChangeTree } from "../../encoder/ChangeTree";
3
3
  import { OPERATION } from "../../encoding/spec";
4
4
  import { registerType } from "../registry";
@@ -10,8 +10,10 @@ import type { StateView } from "../../encoder/StateView";
10
10
  type K = number; // TODO: allow to specify K generic on MapSchema.
11
11
 
12
12
  export class CollectionSchema<V=any> implements Collection<K, V>{
13
+
13
14
  protected $items: Map<number, V> = new Map<number, V>();
14
15
  protected $indexes: Map<number, number> = new Map<number, number>();
16
+ protected deletedItems: { [field: string]: V } = {};
15
17
 
16
18
  protected $refId: number = 0;
17
19
 
@@ -31,7 +33,7 @@ export class CollectionSchema<V=any> implements Collection<K, V>{
31
33
  return (
32
34
  !view ||
33
35
  typeof (ref[$childType]) === "string" ||
34
- view.items.has(ref[$getByIndex](index)[$changes])
36
+ view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
35
37
  );
36
38
  }
37
39
 
@@ -101,7 +103,7 @@ export class CollectionSchema<V=any> implements Collection<K, V>{
101
103
  return false;
102
104
  }
103
105
 
104
- this[$changes].delete(index);
106
+ this.deletedItems[index] = this[$changes].delete(index);
105
107
  this.$indexes.delete(index);
106
108
 
107
109
  return this.$items.delete(index);
@@ -162,6 +164,10 @@ export class CollectionSchema<V=any> implements Collection<K, V>{
162
164
  this.$indexes.delete(index);
163
165
  }
164
166
 
167
+ protected [$onEncodeEnd]() {
168
+ this.deletedItems = {};
169
+ }
170
+
165
171
  toArray() {
166
172
  return Array.from(this.$items.values());
167
173
  }
@@ -143,7 +143,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
143
143
  delete(key: K) {
144
144
  const index = this[$changes].indexes[key];
145
145
 
146
- this.deletedItems[index] = this[$changes].delete(index);;
146
+ this.deletedItems[index] = this[$changes].delete(index);
147
147
 
148
148
  return this.$items.delete(key);
149
149
  }
@@ -1,6 +1,6 @@
1
1
  import { OPERATION } from "../../encoding/spec";
2
2
  import { registerType } from "../registry";
3
- import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex } from "../symbols";
3
+ import { $changes, $childType, $decoder, $deleteByIndex, $encoder, $filter, $getByIndex, $onEncodeEnd } from "../symbols";
4
4
  import { Collection } from "../HelperTypes";
5
5
  import { ChangeTree } from "../../encoder/ChangeTree";
6
6
  import { encodeKeyValueOperation } from "../../encoder/EncodeOperation";
@@ -11,6 +11,7 @@ export class SetSchema<V=any> implements Collection<number, V> {
11
11
 
12
12
  protected $items: Map<number, V> = new Map<number, V>();
13
13
  protected $indexes: Map<number, number> = new Map<number, number>();
14
+ protected deletedItems: { [field: string]: V } = {};
14
15
 
15
16
  protected $refId: number = 0;
16
17
 
@@ -30,7 +31,7 @@ export class SetSchema<V=any> implements Collection<number, V> {
30
31
  return (
31
32
  !view ||
32
33
  typeof (ref[$childType]) === "string" ||
33
- view.items.has(ref[$getByIndex](index)[$changes])
34
+ view.items.has((ref[$getByIndex](index) ?? ref.deletedItems[index])[$changes])
34
35
  );
35
36
  }
36
37
 
@@ -98,7 +99,7 @@ export class SetSchema<V=any> implements Collection<number, V> {
98
99
  return false;
99
100
  }
100
101
 
101
- this[$changes].delete(index);
102
+ this.deletedItems[index] = this[$changes].delete(index);
102
103
  this.$indexes.delete(index);
103
104
 
104
105
  return this.$items.delete(index);
@@ -172,6 +173,10 @@ export class SetSchema<V=any> implements Collection<number, V> {
172
173
  this.$indexes.delete(index);
173
174
  }
174
175
 
176
+ protected [$onEncodeEnd]() {
177
+ this.deletedItems = {};
178
+ }
179
+
175
180
  toArray() {
176
181
  return Array.from(this.$items.values());
177
182
  }