@colyseus/schema 2.0.4 → 2.0.6

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 (107) hide show
  1. package/README.md +0 -4
  2. package/build/cjs/index.js +48 -48
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +130 -104
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +50 -50
  7. package/lib/Reflection.js +87 -119
  8. package/lib/Reflection.js.map +1 -1
  9. package/lib/Schema.js +195 -257
  10. package/lib/Schema.js.map +1 -1
  11. package/lib/annotations.d.ts +6 -6
  12. package/lib/annotations.js +64 -92
  13. package/lib/annotations.js.map +1 -1
  14. package/lib/changes/ChangeTree.d.ts +1 -1
  15. package/lib/changes/ChangeTree.js +63 -70
  16. package/lib/changes/ChangeTree.js.map +1 -1
  17. package/lib/changes/ReferenceTracker.js +24 -27
  18. package/lib/changes/ReferenceTracker.js.map +1 -1
  19. package/lib/codegen/api.js +9 -9
  20. package/lib/codegen/api.js.map +1 -1
  21. package/lib/codegen/argv.d.ts +1 -1
  22. package/lib/codegen/argv.js +11 -11
  23. package/lib/codegen/argv.js.map +1 -1
  24. package/lib/codegen/cli.js +21 -10
  25. package/lib/codegen/cli.js.map +1 -1
  26. package/lib/codegen/languages/cpp.js +126 -77
  27. package/lib/codegen/languages/cpp.js.map +1 -1
  28. package/lib/codegen/languages/csharp.js +121 -62
  29. package/lib/codegen/languages/csharp.js.map +1 -1
  30. package/lib/codegen/languages/haxe.js +34 -26
  31. package/lib/codegen/languages/haxe.js.map +1 -1
  32. package/lib/codegen/languages/java.js +39 -27
  33. package/lib/codegen/languages/java.js.map +1 -1
  34. package/lib/codegen/languages/js.js +48 -32
  35. package/lib/codegen/languages/js.js.map +1 -1
  36. package/lib/codegen/languages/lua.js +35 -24
  37. package/lib/codegen/languages/lua.js.map +1 -1
  38. package/lib/codegen/languages/ts.js +63 -68
  39. package/lib/codegen/languages/ts.js.map +1 -1
  40. package/lib/codegen/parser.d.ts +9 -1
  41. package/lib/codegen/parser.js +88 -46
  42. package/lib/codegen/parser.js.map +1 -1
  43. package/lib/codegen/types.d.ts +8 -0
  44. package/lib/codegen/types.js +64 -54
  45. package/lib/codegen/types.js.map +1 -1
  46. package/lib/encoding/decode.js +15 -15
  47. package/lib/encoding/decode.js.map +1 -1
  48. package/lib/encoding/encode.js +14 -14
  49. package/lib/encoding/encode.js.map +1 -1
  50. package/lib/events/EventEmitter.d.ts +1 -1
  51. package/lib/events/EventEmitter.js +16 -47
  52. package/lib/events/EventEmitter.js.map +1 -1
  53. package/lib/filters/index.js +7 -8
  54. package/lib/filters/index.js.map +1 -1
  55. package/lib/index.js +11 -11
  56. package/lib/index.js.map +1 -1
  57. package/lib/types/ArraySchema.d.ts +1 -1
  58. package/lib/types/ArraySchema.js +161 -219
  59. package/lib/types/ArraySchema.js.map +1 -1
  60. package/lib/types/CollectionSchema.d.ts +1 -1
  61. package/lib/types/CollectionSchema.js +63 -71
  62. package/lib/types/CollectionSchema.js.map +1 -1
  63. package/lib/types/HelperTypes.d.ts +9 -9
  64. package/lib/types/MapSchema.d.ts +16 -16
  65. package/lib/types/MapSchema.js +68 -78
  66. package/lib/types/MapSchema.js.map +1 -1
  67. package/lib/types/SetSchema.js +62 -71
  68. package/lib/types/SetSchema.js.map +1 -1
  69. package/lib/types/index.js +1 -1
  70. package/lib/types/index.js.map +1 -1
  71. package/lib/types/typeRegistry.js +1 -1
  72. package/lib/types/typeRegistry.js.map +1 -1
  73. package/lib/types/utils.js +9 -10
  74. package/lib/types/utils.js.map +1 -1
  75. package/lib/utils.js +10 -13
  76. package/lib/utils.js.map +1 -1
  77. package/package.json +18 -15
  78. package/src/Reflection.ts +159 -0
  79. package/src/Schema.ts +1024 -0
  80. package/src/annotations.ts +400 -0
  81. package/src/changes/ChangeTree.ts +295 -0
  82. package/src/changes/ReferenceTracker.ts +81 -0
  83. package/src/codegen/api.ts +46 -0
  84. package/src/codegen/argv.ts +40 -0
  85. package/src/codegen/cli.ts +65 -0
  86. package/src/codegen/languages/cpp.ts +297 -0
  87. package/src/codegen/languages/csharp.ts +208 -0
  88. package/src/codegen/languages/haxe.ts +110 -0
  89. package/src/codegen/languages/java.ts +115 -0
  90. package/src/codegen/languages/js.ts +115 -0
  91. package/src/codegen/languages/lua.ts +125 -0
  92. package/src/codegen/languages/ts.ts +129 -0
  93. package/src/codegen/parser.ts +299 -0
  94. package/src/codegen/types.ts +177 -0
  95. package/src/encoding/decode.ts +278 -0
  96. package/src/encoding/encode.ts +283 -0
  97. package/src/filters/index.ts +23 -0
  98. package/src/index.ts +59 -0
  99. package/src/spec.ts +49 -0
  100. package/src/types/ArraySchema.ts +612 -0
  101. package/src/types/CollectionSchema.ts +199 -0
  102. package/src/types/HelperTypes.ts +34 -0
  103. package/src/types/MapSchema.ts +268 -0
  104. package/src/types/SetSchema.ts +208 -0
  105. package/src/types/typeRegistry.ts +19 -0
  106. package/src/types/utils.ts +62 -0
  107. package/src/utils.ts +28 -0
@@ -0,0 +1,400 @@
1
+ import { ChangeTree } from './changes/ChangeTree';
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';
6
+
7
+ /**
8
+ * Data types
9
+ */
10
+ export type PrimitiveType =
11
+ "string" |
12
+ "number" |
13
+ "boolean" |
14
+ "int8" |
15
+ "uint8" |
16
+ "int16" |
17
+ "uint16" |
18
+ "int32" |
19
+ "uint32" |
20
+ "int64" |
21
+ "uint64" |
22
+ "float32" |
23
+ "float64" |
24
+ typeof Schema;
25
+
26
+ export type DefinitionType = PrimitiveType
27
+ | PrimitiveType[]
28
+ | { array: PrimitiveType }
29
+ | { map: PrimitiveType }
30
+ | { collection: PrimitiveType }
31
+ | { set: PrimitiveType };
32
+
33
+ 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
+
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.
66
+
67
+ deprecated: { [field: string]: boolean } = {};
68
+ descriptors: PropertyDescriptorMap & ThisType<any> = {};
69
+
70
+ static create(parent?: SchemaDefinition) {
71
+ const definition = new SchemaDefinition();
72
+
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 || {});
79
+
80
+ return definition;
81
+ }
82
+
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;
90
+ }
91
+
92
+ hasField(field: string) {
93
+ return this.indexes[field] !== undefined;
94
+ }
95
+
96
+ addFilter(field: string, cb: FilterCallback) {
97
+ if (!this.filters) {
98
+ this.filters = {};
99
+ this.indexesWithFilters = [];
100
+ }
101
+ this.filters[this.indexes[field]] = cb;
102
+ this.indexesWithFilters.push(this.indexes[field]);
103
+ return true;
104
+ }
105
+
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
+ }
119
+ }
120
+
121
+ getChildrenFilter(field: string) {
122
+ return this.childFilters && this.childFilters[this.indexes[field]];
123
+ }
124
+
125
+ getNextFieldIndex() {
126
+ return Object.keys(this.schema || {}).length;
127
+ }
128
+ }
129
+
130
+ export function hasFilter(klass: typeof Schema) {
131
+ return klass._context && klass._context.useFilters;
132
+ }
133
+
134
+ // Colyseus integration
135
+ export type ClientWithSessionId = { sessionId: string } & any;
136
+
137
+ export interface TypeOptions {
138
+ manual?: boolean,
139
+ context?: Context,
140
+ }
141
+
142
+ export class Context {
143
+ types: {[id: number]: typeof Schema} = {};
144
+ schemas = new Map<typeof Schema, number>();
145
+ useFilters = false;
146
+
147
+ has(schema: typeof Schema) {
148
+ return this.schemas.has(schema);
149
+ }
150
+
151
+ get(typeid: number) {
152
+ return this.types[typeid];
153
+ }
154
+
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);
159
+
160
+ schema._typeid = typeid;
161
+ this.types[typeid] = schema;
162
+ this.schemas.set(schema, typeid);
163
+ }
164
+
165
+
166
+ static create(options: TypeOptions = {}) {
167
+ return function (definition: DefinitionType) {
168
+ if (!options.context) {
169
+ options.context = new Context();
170
+ }
171
+ return type(definition, options);
172
+ }
173
+ }
174
+ }
175
+
176
+ export const globalContext = new Context();
177
+
178
+ /**
179
+ * [See documentation](https://docs.colyseus.io/state/schema/)
180
+ *
181
+ * Annotate a Schema property to be serializeable.
182
+ * \@type()'d fields are automatically flagged as "dirty" for the next patch.
183
+ *
184
+ * @example Standard usage, with automatic change tracking.
185
+ * ```
186
+ * \@type("string") propertyName: string;
187
+ * ```
188
+ *
189
+ * @example You can provide the "manual" option if you'd like to manually control your patches via .setDirty().
190
+ * ```
191
+ * \@type("string", { manual: true })
192
+ * ```
193
+ */
194
+ export function type (
195
+ type: DefinitionType,
196
+ options: TypeOptions = {}
197
+ ): PropertyDecorator {
198
+ return function (target: typeof Schema, field: string) {
199
+ const context = options.context || globalContext;
200
+ const constructor = target.constructor as typeof Schema;
201
+ constructor._context = context;
202
+
203
+ if (!type) {
204
+ throw new Error(`${constructor.name}: @type() reference provided for "${field}" is undefined. Make sure you don't have any circular dependencies.`);
205
+ }
206
+
207
+ /*
208
+ * static schema
209
+ */
210
+ if (!context.has(constructor)) {
211
+ context.add(constructor);
212
+ }
213
+
214
+ const definition = constructor._definition;
215
+ definition.addField(field, type);
216
+
217
+ /**
218
+ * skip if descriptor already exists for this field (`@deprecated()`)
219
+ */
220
+ if (definition.descriptors[field]) {
221
+ if (definition.deprecated[field]) {
222
+ // do not create accessors for deprecated properties.
223
+ return;
224
+
225
+ } else {
226
+ // trying to define same property multiple times across inheritance.
227
+ // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
228
+ try {
229
+ throw new Error(`@colyseus/schema: Duplicate '${field}' definition on '${constructor.name}'.\nCheck @type() annotation`);
230
+
231
+ } catch (e) {
232
+ const definitionAtLine = e.stack.split("\n")[4].trim();
233
+ throw new Error(`${e.message} ${definitionAtLine}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ const isArray = ArraySchema.is(type);
239
+ const isMap = !isArray && MapSchema.is(type);
240
+
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);
248
+ }
249
+ }
250
+
251
+ if (options.manual) {
252
+ // do not declare getter/setter descriptor
253
+ definition.descriptors[field] = {
254
+ enumerable: true,
255
+ configurable: true,
256
+ writable: true,
257
+ };
258
+ return;
259
+ }
260
+
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
+ }
282
+
283
+ if (
284
+ value !== undefined &&
285
+ value !== null
286
+ ) {
287
+ // automaticallty transform Array into ArraySchema
288
+ if (isArray && !(value instanceof ArraySchema)) {
289
+ value = new ArraySchema(...value);
290
+ }
291
+
292
+ // automaticallty transform Map into MapSchema
293
+ if (isMap && !(value instanceof MapSchema)) {
294
+ value = new MapSchema(value);
295
+ }
296
+
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);
327
+ }
328
+
329
+ this[fieldCached] = value;
330
+ },
331
+
332
+ enumerable: true,
333
+ configurable: true
334
+ };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * `@filter()` decorator for defining data filters per client
340
+ */
341
+
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;
346
+
347
+ if (definition.addFilter(field, cb)) {
348
+ constructor._context.useFilters = true;
349
+ }
350
+ }
351
+ }
352
+
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
+ }
361
+ }
362
+
363
+
364
+ /**
365
+ * `@deprecated()` flag a field as deprecated.
366
+ * The previous `@type()` annotation should remain along with this one.
367
+ */
368
+
369
+ 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;
373
+
374
+ definition.deprecated[field] = true;
375
+
376
+ if (throws) {
377
+ definition.descriptors[field] = {
378
+ get: function () { throw new Error(`${field} is deprecated.`); },
379
+ set: function (this: Schema, value: any) { /* throw new Error(`${field} is deprecated.`); */ },
380
+ enumerable: false,
381
+ configurable: true
382
+ };
383
+ }
384
+ }
385
+ }
386
+
387
+ export function defineTypes(
388
+ target: typeof Schema,
389
+ fields: { [property: string]: DefinitionType },
390
+ options: TypeOptions = {}
391
+ ) {
392
+ if (!options.context) {
393
+ options.context = target._context || options.context || globalContext;
394
+ }
395
+
396
+ for (let field in fields) {
397
+ type(fields[field], options)(target.prototype, field);
398
+ }
399
+ return target;
400
+ }
@@ -0,0 +1,295 @@
1
+ import { OPERATION } from "../spec";
2
+ import { Schema } from "../Schema";
3
+ import { SchemaDefinition, FilterChildrenCallback } from "../annotations";
4
+
5
+ import { MapSchema } from "../types/MapSchema";
6
+ import { ArraySchema } from "../types/ArraySchema";
7
+ import { CollectionSchema } from "../types/CollectionSchema";
8
+ import { SetSchema } from "../types/SetSchema";
9
+ import { ReferenceTracker } from "./ReferenceTracker";
10
+
11
+ export type Ref = Schema
12
+ | ArraySchema
13
+ | MapSchema
14
+ | CollectionSchema
15
+ | SetSchema;
16
+
17
+ export interface ChangeOperation {
18
+ op: OPERATION,
19
+ index: number,
20
+ }
21
+
22
+ //
23
+ // FieldCache is used for @filter()
24
+ //
25
+ export interface FieldCache {
26
+ beginIndex: number;
27
+ endIndex: number;
28
+ }
29
+
30
+
31
+ export class ChangeTree {
32
+ ref: Ref;
33
+ refId: number;
34
+
35
+ root?: ReferenceTracker;
36
+
37
+ parent?: Ref;
38
+ parentIndex?: number;
39
+
40
+ indexes: {[index: string]: any};
41
+
42
+ changed: boolean = false;
43
+ changes = new Map<number, ChangeOperation>();
44
+ allChanges = new Set<number>();
45
+
46
+ // cached indexes for filtering
47
+ caches: {[field: number]: number[]} = {};
48
+
49
+ currentCustomOperation: number = 0;
50
+
51
+ constructor(ref: Ref, parent?: Ref, root?: ReferenceTracker) {
52
+ this.ref = ref;
53
+ this.setParent(parent, root);
54
+ }
55
+
56
+ setParent(
57
+ parent: Ref,
58
+ root?: ReferenceTracker,
59
+ parentIndex?: number,
60
+ ) {
61
+ if (!this.indexes) {
62
+ this.indexes = (this.ref instanceof Schema)
63
+ ? this.ref['_definition'].indexes
64
+ : {};
65
+ }
66
+
67
+ this.parent = parent;
68
+ this.parentIndex = parentIndex;
69
+
70
+ // avoid setting parents with empty `root`
71
+ if (!root) { return; }
72
+ this.root = root;
73
+
74
+ //
75
+ // assign same parent on child structures
76
+ //
77
+ if (this.ref instanceof Schema) {
78
+ const definition: SchemaDefinition = this.ref['_definition'];
79
+
80
+ for (let field in definition.schema) {
81
+ const value = this.ref[field];
82
+
83
+ if (value && value['$changes']) {
84
+ const parentIndex = definition.indexes[field];
85
+
86
+ (value['$changes'] as ChangeTree).setParent(
87
+ this.ref,
88
+ root,
89
+ parentIndex,
90
+ );
91
+ }
92
+ }
93
+
94
+ } else if (typeof (this.ref) === "object") {
95
+ this.ref.forEach((value, key) => {
96
+ if (value instanceof Schema) {
97
+ const changeTreee = value['$changes'];
98
+ const parentIndex = this.ref['$changes'].indexes[key];
99
+
100
+ changeTreee.setParent(
101
+ this.ref,
102
+ this.root,
103
+ parentIndex,
104
+ );
105
+ }
106
+ });
107
+ }
108
+ }
109
+
110
+ operation(op: ChangeOperation) {
111
+ this.changes.set(--this.currentCustomOperation, op);
112
+ }
113
+
114
+ change(fieldName: string | number, operation: OPERATION = OPERATION.ADD) {
115
+ const index = (typeof (fieldName) === "number")
116
+ ? fieldName
117
+ : this.indexes[fieldName];
118
+
119
+ this.assertValidIndex(index, fieldName);
120
+
121
+ const previousChange = this.changes.get(index);
122
+
123
+ if (
124
+ !previousChange ||
125
+ previousChange.op === OPERATION.DELETE ||
126
+ previousChange.op === OPERATION.TOUCH // (mazmorra.io's BattleAction issue)
127
+ ) {
128
+ this.changes.set(index, {
129
+ op: (!previousChange)
130
+ ? operation
131
+ : (previousChange.op === OPERATION.DELETE)
132
+ ? OPERATION.DELETE_AND_ADD
133
+ : operation,
134
+ // : OPERATION.REPLACE,
135
+ index
136
+ });
137
+ }
138
+
139
+ this.allChanges.add(index);
140
+
141
+ this.changed = true;
142
+ this.touchParents();
143
+ }
144
+
145
+ touch(fieldName: string | number) {
146
+ const index = (typeof (fieldName) === "number")
147
+ ? fieldName
148
+ : this.indexes[fieldName];
149
+
150
+ this.assertValidIndex(index, fieldName);
151
+
152
+ if (!this.changes.has(index)) {
153
+ this.changes.set(index, { op: OPERATION.TOUCH, index });
154
+ }
155
+
156
+ this.allChanges.add(index);
157
+
158
+ // ensure touch is placed until the $root is found.
159
+ this.touchParents();
160
+ }
161
+
162
+ touchParents() {
163
+ if (this.parent) {
164
+ (this.parent['$changes'] as ChangeTree).touch(this.parentIndex);
165
+ }
166
+ }
167
+
168
+ getType(index?: number) {
169
+ if (this.ref['_definition']) {
170
+ const definition = (this.ref as Schema)['_definition'];
171
+ return definition.schema[ definition.fieldsByIndex[index] ];
172
+
173
+ } else {
174
+ const definition = (this.parent as Schema)['_definition'];
175
+ const parentType = definition.schema[ definition.fieldsByIndex[this.parentIndex] ];
176
+
177
+ //
178
+ // Get the child type from parent structure.
179
+ // - ["string"] => "string"
180
+ // - { map: "string" } => "string"
181
+ // - { set: "string" } => "string"
182
+ //
183
+ return Object.values(parentType)[0];
184
+ }
185
+ }
186
+
187
+ getChildrenFilter(): FilterChildrenCallback {
188
+ const childFilters = (this.parent as Schema)['_definition'].childFilters;
189
+ return childFilters && childFilters[this.parentIndex];
190
+ }
191
+
192
+ //
193
+ // used during `.encode()`
194
+ //
195
+ getValue(index: number) {
196
+ return this.ref['getByIndex'](index);
197
+ }
198
+
199
+ delete(fieldName: string | number) {
200
+ const index = (typeof (fieldName) === "number")
201
+ ? fieldName
202
+ : this.indexes[fieldName];
203
+
204
+ if (index === undefined) {
205
+ console.warn(`@colyseus/schema ${this.ref.constructor.name}: trying to delete non-existing index: ${fieldName} (${index})`);
206
+ return;
207
+ }
208
+
209
+ const previousValue = this.getValue(index);
210
+ // console.log("$changes.delete =>", { fieldName, index, previousValue });
211
+
212
+ this.changes.set(index, { op: OPERATION.DELETE, index });
213
+
214
+ this.allChanges.delete(index);
215
+
216
+ // delete cache
217
+ delete this.caches[index];
218
+
219
+ // remove `root` reference
220
+ if (previousValue && previousValue['$changes']) {
221
+ previousValue['$changes'].parent = undefined;
222
+ }
223
+
224
+ this.changed = true;
225
+ this.touchParents();
226
+ }
227
+
228
+ discard(changed: boolean = false, discardAll: boolean = false) {
229
+ //
230
+ // Map, Array, etc:
231
+ // Remove cached key to ensure ADD operations is unsed instead of
232
+ // REPLACE in case same key is used on next patches.
233
+ //
234
+ // TODO: refactor this. this is not relevant for Collection and Set.
235
+ //
236
+ if (!(this.ref instanceof Schema)) {
237
+ this.changes.forEach((change) => {
238
+ if (change.op === OPERATION.DELETE) {
239
+ const index = this.ref['getIndex'](change.index)
240
+ delete this.indexes[index];
241
+ }
242
+ });
243
+ }
244
+
245
+ this.changes.clear();
246
+ this.changed = changed;
247
+
248
+ if (discardAll) {
249
+ this.allChanges.clear();
250
+ }
251
+
252
+ // re-set `currentCustomOperation`
253
+ this.currentCustomOperation = 0;
254
+ }
255
+
256
+ /**
257
+ * Recursively discard all changes from this, and child structures.
258
+ */
259
+ discardAll() {
260
+ this.changes.forEach((change) => {
261
+ const value = this.getValue(change.index);
262
+
263
+ if (value && value['$changes']) {
264
+ value['$changes'].discardAll();
265
+ }
266
+ });
267
+
268
+ this.discard();
269
+ }
270
+
271
+ // cache(field: number, beginIndex: number, endIndex: number) {
272
+ cache(field: number, cachedBytes: number[]) {
273
+ this.caches[field] = cachedBytes;
274
+ }
275
+
276
+ clone() {
277
+ return new ChangeTree(this.ref, this.parent, this.root);
278
+ }
279
+
280
+ ensureRefId() {
281
+ // skip if refId is already set.
282
+ if (this.refId !== undefined) {
283
+ return;
284
+ }
285
+
286
+ this.refId = this.root.getNextUniqueId();
287
+ }
288
+
289
+ protected assertValidIndex(index: number, fieldName: string | number) {
290
+ if (index === undefined) {
291
+ throw new Error(`ChangeTree: missing index for field "${fieldName}"`);
292
+ }
293
+ }
294
+
295
+ }