@colyseus/schema 2.0.32 → 3.0.0-alpha.1

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.
Files changed (160) hide show
  1. package/build/cjs/index.js +3428 -2677
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +3324 -2445
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +3428 -2677
  6. package/lib/Decoder.d.ts +16 -0
  7. package/lib/Decoder.js +182 -0
  8. package/lib/Decoder.js.map +1 -0
  9. package/lib/Encoder.d.ts +13 -0
  10. package/lib/Encoder.js +79 -0
  11. package/lib/Encoder.js.map +1 -0
  12. package/lib/Metadata.d.ts +36 -0
  13. package/lib/Metadata.js +91 -0
  14. package/lib/Metadata.js.map +1 -0
  15. package/lib/Reflection.d.ts +7 -5
  16. package/lib/Reflection.js +62 -58
  17. package/lib/Reflection.js.map +1 -1
  18. package/lib/Schema.d.ts +39 -51
  19. package/lib/Schema.js +189 -731
  20. package/lib/Schema.js.map +1 -1
  21. package/lib/annotations.d.ts +26 -45
  22. package/lib/annotations.js +363 -194
  23. package/lib/annotations.js.map +1 -1
  24. package/lib/changes/ChangeSet.d.ts +12 -0
  25. package/lib/changes/ChangeSet.js +35 -0
  26. package/lib/changes/ChangeSet.js.map +1 -0
  27. package/lib/changes/DecodeOperation.d.ts +15 -0
  28. package/lib/changes/DecodeOperation.js +186 -0
  29. package/lib/changes/DecodeOperation.js.map +1 -0
  30. package/lib/changes/EncodeOperation.d.ts +18 -0
  31. package/lib/changes/EncodeOperation.js +130 -0
  32. package/lib/changes/EncodeOperation.js.map +1 -0
  33. package/lib/changes/consts.d.ts +14 -0
  34. package/lib/changes/consts.js +18 -0
  35. package/lib/changes/consts.js.map +1 -0
  36. package/lib/decoder/DecodeOperation.d.ts +24 -0
  37. package/lib/decoder/DecodeOperation.js +256 -0
  38. package/lib/decoder/DecodeOperation.js.map +1 -0
  39. package/lib/decoder/Decoder.d.ts +21 -0
  40. package/lib/decoder/Decoder.js +114 -0
  41. package/lib/decoder/Decoder.js.map +1 -0
  42. package/lib/decoder/ReferenceTracker.d.ts +26 -0
  43. package/lib/decoder/ReferenceTracker.js +131 -0
  44. package/lib/decoder/ReferenceTracker.js.map +1 -0
  45. package/lib/decoder/strategy/RawChanges.d.ts +3 -0
  46. package/lib/decoder/strategy/RawChanges.js +8 -0
  47. package/lib/decoder/strategy/RawChanges.js.map +1 -0
  48. package/lib/decoder/strategy/StateCallbacks.d.ts +20 -0
  49. package/lib/decoder/strategy/StateCallbacks.js +240 -0
  50. package/lib/decoder/strategy/StateCallbacks.js.map +1 -0
  51. package/lib/decoding/decode.d.ts +48 -0
  52. package/lib/decoding/decode.js +267 -0
  53. package/lib/decoding/decode.js.map +1 -0
  54. package/lib/ecs.d.ts +11 -0
  55. package/lib/ecs.js +160 -0
  56. package/lib/ecs.js.map +1 -0
  57. package/lib/encoder/ChangeTree.d.ts +72 -0
  58. package/lib/encoder/ChangeTree.js +384 -0
  59. package/lib/encoder/ChangeTree.js.map +1 -0
  60. package/lib/encoder/EncodeOperation.d.ts +25 -0
  61. package/lib/encoder/EncodeOperation.js +156 -0
  62. package/lib/encoder/EncodeOperation.js.map +1 -0
  63. package/lib/encoder/Encoder.d.ts +23 -0
  64. package/lib/encoder/Encoder.js +192 -0
  65. package/lib/encoder/Encoder.js.map +1 -0
  66. package/lib/encoder/StateView.d.ts +21 -0
  67. package/lib/encoder/StateView.js +196 -0
  68. package/lib/encoder/StateView.js.map +1 -0
  69. package/lib/encoding/assert.d.ts +9 -0
  70. package/lib/encoding/assert.js +47 -0
  71. package/lib/encoding/assert.js.map +1 -0
  72. package/lib/encoding/decode.js +1 -1
  73. package/lib/encoding/decode.js.map +1 -1
  74. package/lib/encoding/encode.d.ts +19 -16
  75. package/lib/encoding/encode.js +88 -81
  76. package/lib/encoding/encode.js.map +1 -1
  77. package/lib/encoding/spec.d.ts +25 -0
  78. package/lib/encoding/spec.js +30 -0
  79. package/lib/encoding/spec.js.map +1 -0
  80. package/lib/index.d.ts +18 -10
  81. package/lib/index.js +39 -17
  82. package/lib/index.js.map +1 -1
  83. package/lib/symbol.shim.d.ts +6 -0
  84. package/lib/symbol.shim.js +4 -0
  85. package/lib/symbol.shim.js.map +1 -0
  86. package/lib/types/ArraySchema.d.ts +1 -1
  87. package/lib/types/ArraySchema.js +0 -7
  88. package/lib/types/ArraySchema.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +10 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/custom/ArraySchema.d.ts +245 -0
  92. package/lib/types/custom/ArraySchema.js +659 -0
  93. package/lib/types/custom/ArraySchema.js.map +1 -0
  94. package/lib/types/custom/CollectionSchema.d.ts +42 -0
  95. package/lib/types/custom/CollectionSchema.js +165 -0
  96. package/lib/types/custom/CollectionSchema.js.map +1 -0
  97. package/lib/types/custom/MapSchema.d.ts +43 -0
  98. package/lib/types/custom/MapSchema.js +200 -0
  99. package/lib/types/custom/MapSchema.js.map +1 -0
  100. package/lib/types/custom/SetSchema.d.ts +39 -0
  101. package/lib/types/custom/SetSchema.js +177 -0
  102. package/lib/types/custom/SetSchema.js.map +1 -0
  103. package/lib/types/registry.d.ts +6 -0
  104. package/lib/types/registry.js +19 -0
  105. package/lib/types/registry.js.map +1 -0
  106. package/lib/types/symbols.d.ts +29 -0
  107. package/lib/types/symbols.js +33 -0
  108. package/lib/types/symbols.js.map +1 -0
  109. package/lib/types/utils.d.ts +0 -8
  110. package/lib/types/utils.js +1 -33
  111. package/lib/types/utils.js.map +1 -1
  112. package/lib/usage.d.ts +1 -0
  113. package/lib/usage.js +22 -0
  114. package/lib/usage.js.map +1 -0
  115. package/lib/utils.d.ts +13 -2
  116. package/lib/utils.js +36 -15
  117. package/lib/utils.js.map +1 -1
  118. package/lib/v3.d.ts +1 -0
  119. package/lib/v3.js +427 -0
  120. package/lib/v3.js.map +1 -0
  121. package/lib/v3_bench.d.ts +1 -0
  122. package/lib/v3_bench.js +130 -0
  123. package/lib/v3_bench.js.map +1 -0
  124. package/lib/v3_experiment.d.ts +1 -0
  125. package/lib/v3_experiment.js +407 -0
  126. package/lib/v3_experiment.js.map +1 -0
  127. package/package.json +5 -5
  128. package/src/Metadata.ts +135 -0
  129. package/src/Reflection.ts +75 -66
  130. package/src/Schema.ts +213 -931
  131. package/src/annotations.ts +430 -243
  132. package/src/decoder/DecodeOperation.ts +372 -0
  133. package/src/decoder/Decoder.ts +155 -0
  134. package/src/decoder/ReferenceTracker.ts +151 -0
  135. package/src/decoder/strategy/RawChanges.ts +9 -0
  136. package/src/decoder/strategy/StateCallbacks.ts +326 -0
  137. package/src/encoder/ChangeTree.ts +492 -0
  138. package/src/encoder/EncodeOperation.ts +237 -0
  139. package/src/encoder/Encoder.ts +246 -0
  140. package/src/encoder/StateView.ts +229 -0
  141. package/src/encoding/assert.ts +58 -0
  142. package/src/encoding/decode.ts +1 -1
  143. package/src/encoding/encode.ts +91 -82
  144. package/src/encoding/spec.ts +29 -0
  145. package/src/index.ts +22 -19
  146. package/src/symbol.shim.ts +12 -0
  147. package/src/types/HelperTypes.ts +16 -2
  148. package/src/types/{ArraySchema.ts → custom/ArraySchema.ts} +342 -248
  149. package/src/types/{CollectionSchema.ts → custom/CollectionSchema.ts} +56 -46
  150. package/src/types/{MapSchema.ts → custom/MapSchema.ts} +88 -115
  151. package/src/types/{SetSchema.ts → custom/SetSchema.ts} +58 -47
  152. package/src/types/{typeRegistry.ts → registry.ts} +6 -6
  153. package/src/types/symbols.ts +36 -0
  154. package/src/types/utils.ts +0 -46
  155. package/src/utils.ts +50 -21
  156. package/src/v3_bench.ts +107 -0
  157. package/src/changes/ChangeTree.ts +0 -295
  158. package/src/changes/ReferenceTracker.ts +0 -91
  159. package/src/filters/index.ts +0 -23
  160. package/src/spec.ts +0 -49
@@ -1,8 +1,11 @@
1
- import { ChangeTree } from './changes/ChangeTree';
1
+ import "./symbol.shim";
2
2
  import { Schema } from './Schema';
3
- import { ArraySchema, getArrayProxy } from './types/ArraySchema';
4
- import { MapSchema, getMapProxy } from './types/MapSchema';
5
- import { getType } from './types/typeRegistry';
3
+ import { ArraySchema } from './types/custom/ArraySchema';
4
+ import { MapSchema } from './types/custom/MapSchema';
5
+ import { Metadata } from "./Metadata";
6
+ import { $changes, $childType, $track } from "./types/symbols";
7
+ import { TypeDefinition, getType } from "./types/registry";
8
+ import { OPERATION } from "./encoding/spec";
6
9
 
7
10
  /**
8
11
  * Data types
@@ -21,7 +24,8 @@ export type PrimitiveType =
21
24
  "uint64" |
22
25
  "float32" |
23
26
  "float64" |
24
- typeof Schema;
27
+ typeof Schema |
28
+ object;
25
29
 
26
30
  export type DefinitionType = PrimitiveType
27
31
  | PrimitiveType[]
@@ -31,149 +35,128 @@ export type DefinitionType = PrimitiveType
31
35
  | { set: PrimitiveType };
32
36
 
33
37
  export type Definition = { [field: string]: DefinitionType };
34
- export type FilterCallback<
35
- T extends Schema = any,
36
- V = any,
37
- R extends Schema = any
38
- > = (
39
- ((this: T, client: ClientWithSessionId, value: V) => boolean) |
40
- ((this: T, client: ClientWithSessionId, value: V, root: R) => boolean)
41
- );
42
-
43
- export type FilterChildrenCallback<
44
- T extends Schema = any,
45
- K = any,
46
- V = any,
47
- R extends Schema = any
48
- > = (
49
- ((this: T, client: ClientWithSessionId, key: K, value: V) => boolean) |
50
- ((this: T, client: ClientWithSessionId, key: K, value: V, root: R) => boolean)
51
- )
52
-
53
- export class SchemaDefinition {
54
- schema: Definition;
55
38
 
56
- //
57
- // TODO: use a "field" structure combining all these properties per-field.
58
- //
59
-
60
- indexes: { [field: string]: number } = {};
61
- fieldsByIndex: { [index: number]: string } = {};
62
-
63
- filters: { [field: string]: FilterCallback };
64
- indexesWithFilters: number[];
65
- childFilters: { [field: string]: FilterChildrenCallback }; // childFilters are used on Map, Array, Set items.
39
+ export interface TypeOptions {
40
+ manual?: boolean,
41
+ }
66
42
 
67
- deprecated: { [field: string]: boolean } = {};
68
- descriptors: PropertyDescriptorMap & ThisType<any> = {};
43
+ export const DEFAULT_VIEW_TAG = -1;
69
44
 
70
- static create(parent?: SchemaDefinition) {
71
- const definition = new SchemaDefinition();
45
+ export class TypeContext {
46
+ types: {[id: number]: typeof Schema} = {};
47
+ schemas = new Map<typeof Schema, number>();
72
48
 
73
- // support inheritance
74
- definition.schema = Object.assign({}, parent && parent.schema || {});
75
- definition.indexes = Object.assign({}, parent && parent.indexes || {});
76
- definition.fieldsByIndex = Object.assign({}, parent && parent.fieldsByIndex || {});
77
- definition.descriptors = Object.assign({}, parent && parent.descriptors || {});
78
- definition.deprecated = Object.assign({}, parent && parent.deprecated || {});
49
+ hasFilters: boolean = false;
50
+
51
+ /**
52
+ * For inheritance support
53
+ * Keeps track of which classes extends which. (parent -> children)
54
+ */
55
+ static inheritedTypes = new Map<typeof Schema, Set<typeof Schema>>();
56
+
57
+ static register(target: typeof Schema) {
58
+ const parent = Object.getPrototypeOf(target);
59
+ if (parent !== Schema) {
60
+ let inherits = TypeContext.inheritedTypes.get(parent);
61
+ if (!inherits) {
62
+ inherits = new Set<typeof Schema>();
63
+ TypeContext.inheritedTypes.set(parent, inherits);
64
+ }
65
+ inherits.add(target);
66
+ }
67
+ }
79
68
 
80
- return definition;
69
+ constructor(rootClass?: typeof Schema) {
70
+ if (rootClass) {
71
+ this.discoverTypes(rootClass);
72
+ }
81
73
  }
82
74
 
83
- addField(field: string, type: DefinitionType) {
84
- const index = this.getNextFieldIndex();
85
- this.fieldsByIndex[index] = field;
86
- this.indexes[field] = index;
87
- this.schema[field] = (Array.isArray(type))
88
- ? { array: type[0] }
89
- : type;
75
+ has(schema: typeof Schema) {
76
+ return this.schemas.has(schema);
90
77
  }
91
78
 
92
- hasField(field: string) {
93
- return this.indexes[field] !== undefined;
79
+ get(typeid: number) {
80
+ return this.types[typeid];
94
81
  }
95
82
 
96
- addFilter(field: string, cb: FilterCallback) {
97
- if (!this.filters) {
98
- this.filters = {};
99
- this.indexesWithFilters = [];
83
+ add(schema: typeof Schema, typeid: number = this.schemas.size) {
84
+ // skip if already registered
85
+ if (this.schemas.has(schema)) {
86
+ return false;
100
87
  }
101
- this.filters[this.indexes[field]] = cb;
102
- this.indexesWithFilters.push(this.indexes[field]);
88
+
89
+ this.types[typeid] = schema;
90
+ this.schemas.set(schema, typeid);
103
91
  return true;
104
92
  }
105
93
 
106
- addChildrenFilter(field: string, cb: FilterChildrenCallback) {
107
- const index = this.indexes[field];
108
- const type = this.schema[field];
109
-
110
- if (getType(Object.keys(type)[0])) {
111
- if (!this.childFilters) { this.childFilters = {}; }
112
-
113
- this.childFilters[index] = cb;
114
- return true;
115
-
116
- } else {
117
- console.warn(`@filterChildren: field '${field}' can't have children. Ignoring filter.`);
118
- }
94
+ getTypeId(klass: typeof Schema) {
95
+ return this.schemas.get(klass);
119
96
  }
120
97
 
121
- getChildrenFilter(field: string) {
122
- return this.childFilters && this.childFilters[this.indexes[field]];
123
- }
98
+ private discoverTypes(klass: typeof Schema) {
99
+ if (!this.add(klass)) {
100
+ return;
101
+ }
124
102
 
125
- getNextFieldIndex() {
126
- return Object.keys(this.schema || {}).length;
127
- }
128
- }
103
+ // add classes inherited from this base class
104
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
105
+ this.discoverTypes(child);
106
+ });
129
107
 
130
- export function hasFilter(klass: typeof Schema) {
131
- return klass._context && klass._context.useFilters;
132
- }
108
+ // skip if no fields are defined for this class.
109
+ if (klass[Symbol.metadata] === undefined) {
110
+ klass[Symbol.metadata] = {};
111
+ }
133
112
 
134
- // Colyseus integration
135
- export type ClientWithSessionId = { sessionId: string } & any;
113
+ // const metadata = Metadata.getFor(klass);
114
+ const metadata = klass[Symbol.metadata];
136
115
 
137
- export interface TypeOptions {
138
- manual?: boolean,
139
- context?: Context,
140
- }
116
+ // if any schema/field has filters, mark "context" as having filters.
117
+ if (metadata[-2]) {
118
+ this.hasFilters = true;
119
+ }
141
120
 
142
- export class Context {
143
- types: {[id: number]: typeof Schema} = {};
144
- schemas = new Map<typeof Schema, number>();
145
- useFilters = false;
121
+ for (const field in metadata) {
122
+ const fieldType = metadata[field].type;
146
123
 
147
- has(schema: typeof Schema) {
148
- return this.schemas.has(schema);
149
- }
124
+ if (typeof(fieldType) === "string") {
125
+ continue;
126
+ }
150
127
 
151
- get(typeid: number) {
152
- return this.types[typeid];
153
- }
128
+ if (Array.isArray(fieldType)) {
129
+ const type = fieldType[0];
130
+ if (type === "string") {
131
+ continue;
132
+ }
133
+ this.discoverTypes(type as typeof Schema);
154
134
 
155
- add(schema: typeof Schema, typeid: number = this.schemas.size) {
156
- // FIXME: move this to somewhere else?
157
- // support inheritance
158
- schema._definition = SchemaDefinition.create(schema._definition);
135
+ } else if (typeof(fieldType) === "function") {
136
+ this.discoverTypes(fieldType);
159
137
 
160
- schema._typeid = typeid;
161
- this.types[typeid] = schema;
162
- this.schemas.set(schema, typeid);
163
- }
138
+ } else {
139
+ const type = Object.values(fieldType)[0];
164
140
 
141
+ // skip primitive types
142
+ if (typeof(type) === "string") {
143
+ continue;
144
+ }
165
145
 
166
- static create(options: TypeOptions = {}) {
167
- return function (definition: DefinitionType) {
168
- if (!options.context) {
169
- options.context = new Context();
146
+ this.discoverTypes(type as typeof Schema);
170
147
  }
171
- return type(definition, options);
172
148
  }
173
149
  }
174
150
  }
175
151
 
176
- export const globalContext = new Context();
152
+ export function entity(constructor, context: ClassDecoratorContext) {
153
+ if (!constructor._definition) {
154
+ // for inheritance support
155
+ TypeContext.register(constructor);
156
+ }
157
+
158
+ return constructor;
159
+ }
177
160
 
178
161
  /**
179
162
  * [See documentation](https://docs.colyseus.io/state/schema/)
@@ -191,38 +174,235 @@ export const globalContext = new Context();
191
174
  * \@type("string", { manual: true })
192
175
  * ```
193
176
  */
177
+ // export function type(type: DefinitionType, options?: TypeOptions) {
178
+ // return function ({ get, set }, context: ClassAccessorDecoratorContext): ClassAccessorDecoratorResult<Schema, any> {
179
+ // if (context.kind !== "accessor") {
180
+ // throw new Error("@type() is only supported for class accessor properties");
181
+ // }
182
+
183
+ // const field = context.name.toString();
184
+
185
+ // //
186
+ // // detect index for this field, considering inheritance
187
+ // //
188
+ // const parent = Object.getPrototypeOf(context.metadata);
189
+ // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
190
+ // ?? (parent && parent[-1]) // parent structure has fields defined
191
+ // ?? -1; // no fields defined
192
+ // fieldIndex++;
193
+
194
+ // if (
195
+ // !parent && // the parent already initializes the `$changes` property
196
+ // !Metadata.hasFields(context.metadata)
197
+ // ) {
198
+ // context.addInitializer(function (this: Ref) {
199
+ // Object.defineProperty(this, $changes, {
200
+ // value: new ChangeTree(this),
201
+ // enumerable: false,
202
+ // writable: true
203
+ // });
204
+ // });
205
+ // }
206
+
207
+ // Metadata.addField(context.metadata, fieldIndex, field, type);
208
+
209
+ // const isArray = ArraySchema.is(type);
210
+ // const isMap = !isArray && MapSchema.is(type);
211
+
212
+ // // if (options && options.manual) {
213
+ // // // do not declare getter/setter descriptor
214
+ // // definition.descriptors[field] = {
215
+ // // enumerable: true,
216
+ // // configurable: true,
217
+ // // writable: true,
218
+ // // };
219
+ // // return;
220
+ // // }
221
+
222
+ // return {
223
+ // init(value) {
224
+ // // TODO: may need to convert ArraySchema/MapSchema here
225
+
226
+ // // do not flag change if value is undefined.
227
+ // if (value !== undefined) {
228
+ // this[$changes].change(fieldIndex);
229
+
230
+ // // automaticallty transform Array into ArraySchema
231
+ // if (isArray) {
232
+ // if (!(value instanceof ArraySchema)) {
233
+ // value = new ArraySchema(...value);
234
+ // }
235
+ // value[$childType] = Object.values(type)[0];
236
+ // }
237
+
238
+ // // automaticallty transform Map into MapSchema
239
+ // if (isMap) {
240
+ // if (!(value instanceof MapSchema)) {
241
+ // value = new MapSchema(value);
242
+ // }
243
+ // value[$childType] = Object.values(type)[0];
244
+ // }
245
+
246
+ // // try to turn provided structure into a Proxy
247
+ // if (value['$proxy'] === undefined) {
248
+ // if (isMap) {
249
+ // value = getMapProxy(value);
250
+ // }
251
+ // }
252
+
253
+ // }
254
+
255
+ // return value;
256
+ // },
257
+
258
+ // get() {
259
+ // return get.call(this);
260
+ // },
261
+
262
+ // set(value: any) {
263
+ // /**
264
+ // * Create Proxy for array or map items
265
+ // */
266
+
267
+ // // skip if value is the same as cached.
268
+ // if (value === get.call(this)) {
269
+ // return;
270
+ // }
271
+
272
+ // if (
273
+ // value !== undefined &&
274
+ // value !== null
275
+ // ) {
276
+ // // automaticallty transform Array into ArraySchema
277
+ // if (isArray) {
278
+ // if (!(value instanceof ArraySchema)) {
279
+ // value = new ArraySchema(...value);
280
+ // }
281
+ // value[$childType] = Object.values(type)[0];
282
+ // }
283
+
284
+ // // automaticallty transform Map into MapSchema
285
+ // if (isMap) {
286
+ // if (!(value instanceof MapSchema)) {
287
+ // value = new MapSchema(value);
288
+ // }
289
+ // value[$childType] = Object.values(type)[0];
290
+ // }
291
+
292
+ // // try to turn provided structure into a Proxy
293
+ // if (value['$proxy'] === undefined) {
294
+ // if (isMap) {
295
+ // value = getMapProxy(value);
296
+ // }
297
+ // }
298
+
299
+ // // flag the change for encoding.
300
+ // this[$changes].change(fieldIndex);
301
+
302
+ // //
303
+ // // call setParent() recursively for this and its child
304
+ // // structures.
305
+ // //
306
+ // if (value[$changes]) {
307
+ // value[$changes].setParent(
308
+ // this,
309
+ // this[$changes].root,
310
+ // Metadata.getIndex(context.metadata, field),
311
+ // );
312
+ // }
313
+
314
+ // } else if (get.call(this)) {
315
+ // //
316
+ // // Setting a field to `null` or `undefined` will delete it.
317
+ // //
318
+ // this[$changes].delete(field);
319
+ // }
320
+
321
+ // set.call(this, value);
322
+ // },
323
+ // };
324
+ // }
325
+ // }
326
+
327
+ export function view<T> (tag: number = DEFAULT_VIEW_TAG) {
328
+ return function(target: T, fieldName: string) {
329
+ const constructor = target.constructor as typeof Schema;
330
+
331
+ const parentClass = Object.getPrototypeOf(constructor);
332
+ const parentMetadata = parentClass[Symbol.metadata];
333
+ const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
334
+
335
+ if (!metadata[fieldName]) {
336
+ //
337
+ // detect index for this field, considering inheritance
338
+ //
339
+ metadata[fieldName] = {
340
+ type: undefined,
341
+ index: (metadata[-1] // current structure already has fields defined
342
+ ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
343
+ ?? -1) + 1 // no fields defined
344
+ }
345
+ }
346
+
347
+ Metadata.setTag(metadata, fieldName, tag);
348
+ }
349
+ }
350
+
351
+ export function unreliable<T> (target: T, field: string) {
352
+ //
353
+ // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
354
+ //
355
+ const constructor = target.constructor as typeof Schema;
356
+
357
+ const parentClass = Object.getPrototypeOf(constructor);
358
+ const parentMetadata = parentClass[Symbol.metadata];
359
+ const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
360
+
361
+ if (!metadata[field]) {
362
+ //
363
+ // detect index for this field, considering inheritance
364
+ //
365
+ metadata[field] = {
366
+ type: undefined,
367
+ index: (metadata[-1] // current structure already has fields defined
368
+ ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
369
+ ?? -1) + 1 // no fields defined
370
+ }
371
+ }
372
+
373
+ // add owned flag to the field
374
+ metadata[field].unreliable = true;
375
+ }
376
+
194
377
  export function type (
195
378
  type: DefinitionType,
196
- options: TypeOptions = {}
379
+ options?: TypeOptions
197
380
  ): PropertyDecorator {
198
381
  return function (target: typeof Schema, field: string) {
199
- const context = options.context || globalContext;
200
382
  const constructor = target.constructor as typeof Schema;
201
- constructor._context = context;
202
383
 
203
384
  if (!type) {
204
385
  throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
205
386
  }
206
387
 
207
- /*
208
- * static schema
209
- */
210
- if (!context.has(constructor)) {
211
- context.add(constructor);
212
- }
388
+ // for inheritance support
389
+ TypeContext.register(constructor);
390
+
391
+ const parentClass = Object.getPrototypeOf(constructor);
392
+ const parentMetadata = parentClass[Symbol.metadata];
393
+ const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
213
394
 
214
- const definition = constructor._definition;
215
- definition.addField(field, type);
395
+ let fieldIndex: number;
216
396
 
217
397
  /**
218
398
  * skip if descriptor already exists for this field (`@deprecated()`)
219
399
  */
220
- if (definition.descriptors[field]) {
221
- if (definition.deprecated[field]) {
400
+ if (metadata[field]) {
401
+ if (metadata[field].deprecated) {
222
402
  // do not create accessors for deprecated properties.
223
403
  return;
224
404
 
225
- } else {
405
+ } else if (metadata[field].descriptor !== undefined) {
226
406
  // trying to define same property multiple times across inheritance.
227
407
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
228
408
  try {
@@ -232,167 +412,174 @@ export function type (
232
412
  const definitionAtLine = e.stack.split("\n")[4].trim();
233
413
  throw new Error(`${e.message} ${definitionAtLine}`);
234
414
  }
235
- }
236
- }
237
-
238
- const isArray = ArraySchema.is(type);
239
- const isMap = !isArray && MapSchema.is(type);
240
415
 
241
- // TODO: refactor me.
242
- // Allow abstract intermediary classes with no fields to be serialized
243
- // (See "should support an inheritance with a Schema type without fields" test)
244
- if (typeof (type) !== "string" && !Schema.is(type)) {
245
- const childType = Object.values(type)[0];
246
- if (typeof (childType) !== "string" && !context.has(childType)) {
247
- context.add(childType);
416
+ } else {
417
+ fieldIndex = metadata[field].index;
248
418
  }
419
+
420
+ } else {
421
+ //
422
+ // detect index for this field, considering inheritance
423
+ //
424
+ fieldIndex = metadata[-1] // current structure already has fields defined
425
+ ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
426
+ ?? -1; // no fields defined
427
+ fieldIndex++;
249
428
  }
250
429
 
251
- if (options.manual) {
252
- // do not declare getter/setter descriptor
253
- definition.descriptors[field] = {
430
+ if (options && options.manual) {
431
+ Metadata.addField(metadata, fieldIndex, field, type, {
432
+ // do not declare getter/setter descriptor
254
433
  enumerable: true,
255
434
  configurable: true,
256
435
  writable: true,
257
- };
258
- return;
259
- }
436
+ });
260
437
 
261
- const fieldCached = `_${field}`;
262
- definition.descriptors[fieldCached] = {
263
- enumerable: false,
264
- configurable: false,
265
- writable: true,
266
- };
267
-
268
- definition.descriptors[field] = {
269
- get: function () {
270
- return this[fieldCached];
271
- },
272
-
273
- set: function (this: Schema, value: any) {
274
- /**
275
- * Create Proxy for array or map items
276
- */
277
-
278
- // skip if value is the same as cached.
279
- if (value === this[fieldCached]) {
280
- return;
281
- }
438
+ } else {
439
+ const complexTypeKlass = (Array.isArray(type))
440
+ ? getType("array")
441
+ : (typeof(Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
442
+
443
+ const childType = (complexTypeKlass)
444
+ ? Object.values(type)[0]
445
+ : type;
446
+
447
+ Metadata.addField(
448
+ metadata,
449
+ fieldIndex,
450
+ field,
451
+ type,
452
+ getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass, metadata, field)
453
+ );
454
+ }
455
+ }
456
+ }
282
457
 
283
- if (
284
- value !== undefined &&
285
- value !== null
286
- ) {
458
+ export function getPropertyDescriptor(
459
+ fieldCached: string,
460
+ fieldIndex: number,
461
+ type: DefinitionType,
462
+ complexTypeKlass: TypeDefinition,
463
+ metadata: Metadata,
464
+ field: string,
465
+ ) {
466
+ return {
467
+ get: function () { return this[fieldCached]; },
468
+ set: function (this: Schema, value: any) {
469
+ const previousValue = this[fieldCached] || undefined;
470
+
471
+ // skip if value is the same as cached.
472
+ if (value === previousValue) { return; }
473
+
474
+ if (
475
+ value !== undefined &&
476
+ value !== null
477
+ ) {
478
+ if (complexTypeKlass) {
287
479
  // automaticallty transform Array into ArraySchema
288
- if (isArray && !(value instanceof ArraySchema)) {
480
+ if (complexTypeKlass.constructor === ArraySchema && !(value instanceof ArraySchema)) {
289
481
  value = new ArraySchema(...value);
290
482
  }
291
483
 
292
484
  // automaticallty transform Map into MapSchema
293
- if (isMap && !(value instanceof MapSchema)) {
485
+ if (complexTypeKlass.constructor === MapSchema && !(value instanceof MapSchema)) {
294
486
  value = new MapSchema(value);
295
487
  }
296
488
 
297
- // try to turn provided structure into a Proxy
298
- if (value['$proxy'] === undefined) {
299
- if (isMap) {
300
- value = getMapProxy(value);
301
-
302
- } else if (isArray) {
303
- value = getArrayProxy(value);
304
- }
305
- }
306
-
307
- // flag the change for encoding.
308
- this.$changes.change(field);
309
-
310
- //
311
- // call setParent() recursively for this and its child
312
- // structures.
313
- //
314
- if (value['$changes']) {
315
- (value['$changes'] as ChangeTree).setParent(
316
- this,
317
- this.$changes.root,
318
- this._definition.indexes[field],
319
- );
320
- }
321
-
322
- } else if (this[fieldCached]) {
323
- //
324
- // Setting a field to `null` or `undefined` will delete it.
325
- //
326
- this.$changes.delete(field);
489
+ value[$childType] = type;
327
490
  }
328
491
 
329
- this[fieldCached] = value;
330
- },
331
-
332
- enumerable: true,
333
- configurable: true
334
- };
335
- }
336
- }
492
+ //
493
+ // Replacing existing "ref", remove it from root.
494
+ // TODO: if there are other references to this instance, we should not remove it from root.
495
+ //
496
+ if (previousValue !== undefined && previousValue[$changes]) {
497
+ this[$changes].root?.remove(previousValue[$changes]);
498
+ }
337
499
 
338
- /**
339
- * `@filter()` decorator for defining data filters per client
340
- */
500
+ // flag the change for encoding.
501
+ this.constructor[$track](this[$changes], fieldIndex, OPERATION.ADD);
502
+
503
+ //
504
+ // call setParent() recursively for this and its child
505
+ // structures.
506
+ //
507
+ if (value[$changes]) {
508
+ value[$changes].setParent(
509
+ this,
510
+ this[$changes].root,
511
+ metadata[field].index,
512
+ );
513
+ }
341
514
 
342
- export function filter<T extends Schema, V, R extends Schema>(cb: FilterCallback<T, V, R>): PropertyDecorator {
343
- return function (target: any, field: string) {
344
- const constructor = target.constructor as typeof Schema;
345
- const definition = constructor._definition;
515
+ } else if (previousValue !== undefined) {
516
+ //
517
+ // Setting a field to `null` or `undefined` will delete it.
518
+ //
519
+ this[$changes].delete(fieldIndex);
520
+ }
346
521
 
347
- if (definition.addFilter(field, cb)) {
348
- constructor._context.useFilters = true;
349
- }
350
- }
351
- }
522
+ this[fieldCached] = value;
523
+ },
352
524
 
353
- export function filterChildren<T extends Schema, K, V, R extends Schema>(cb: FilterChildrenCallback<T, K, V, R>): PropertyDecorator {
354
- return function (target: any, field: string) {
355
- const constructor = target.constructor as typeof Schema;
356
- const definition = constructor._definition;
357
- if (definition.addChildrenFilter(field, cb)) {
358
- constructor._context.useFilters = true;
359
- }
360
- }
525
+ enumerable: true,
526
+ configurable: true
527
+ };
361
528
  }
362
529
 
363
-
364
530
  /**
365
531
  * `@deprecated()` flag a field as deprecated.
366
532
  * The previous `@type()` annotation should remain along with this one.
367
533
  */
368
534
 
369
535
  export function deprecated(throws: boolean = true): PropertyDecorator {
370
- return function (target: typeof Schema, field: string) {
371
- const constructor = target.constructor as typeof Schema;
372
- const definition = constructor._definition;
536
+ return function (klass: typeof Schema, field: string) {
537
+ //
538
+ // FIXME: the following block of code is repeated across `@type()`, `@deprecated()` and `@unreliable()` decorators.
539
+ //
540
+ const constructor = klass.constructor as typeof Schema;
541
+
542
+ const parentClass = Object.getPrototypeOf(constructor);
543
+ const parentMetadata = parentClass[Symbol.metadata];
544
+ const metadata: Metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
545
+
546
+ if (!metadata[field]) {
547
+ //
548
+ // detect index for this field, considering inheritance
549
+ //
550
+ metadata[field] = {
551
+ type: undefined,
552
+ index: (metadata[-1] // current structure already has fields defined
553
+ ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
554
+ ?? -1) + 1 // no fields defined
555
+ }
556
+ }
373
557
 
374
- definition.deprecated[field] = true;
558
+ metadata[field].deprecated = true;
375
559
 
376
560
  if (throws) {
377
- definition.descriptors[field] = {
561
+ metadata[field].descriptor = {
378
562
  get: function () { throw new Error(`${field} is deprecated.`); },
379
563
  set: function (this: Schema, value: any) { /* throw new Error(`${field} is deprecated.`); */ },
380
564
  enumerable: false,
381
565
  configurable: true
382
566
  };
383
567
  }
568
+
569
+ // flag metadata[field] as non-enumerable
570
+ Object.defineProperty(metadata, field, {
571
+ value: metadata[field],
572
+ enumerable: false,
573
+ configurable: true
574
+ });
384
575
  }
385
576
  }
386
577
 
387
578
  export function defineTypes(
388
579
  target: typeof Schema,
389
580
  fields: { [property: string]: DefinitionType },
390
- options: TypeOptions = {}
581
+ options?: TypeOptions
391
582
  ) {
392
- if (!options.context) {
393
- options.context = target._context || options.context || globalContext;
394
- }
395
-
396
583
  for (let field in fields) {
397
584
  type(fields[field], options)(target.prototype, field);
398
585
  }