@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
package/src/Schema.ts ADDED
@@ -0,0 +1,1024 @@
1
+ import { SWITCH_TO_STRUCTURE, TYPE_ID, OPERATION } from './spec';
2
+ import { ClientWithSessionId, PrimitiveType, Context, SchemaDefinition, DefinitionType } from "./annotations";
3
+
4
+ import * as encode from "./encoding/encode";
5
+ import * as decode from "./encoding/decode";
6
+ import type { Iterator } from "./encoding/decode"; // dts-bundle-generator
7
+
8
+ import { ArraySchema } from "./types/ArraySchema";
9
+ import { MapSchema } from "./types/MapSchema";
10
+ import { CollectionSchema } from './types/CollectionSchema';
11
+ import { SetSchema } from './types/SetSchema';
12
+
13
+ import { ChangeTree, Ref, ChangeOperation } from "./changes/ChangeTree";
14
+ import { NonFunctionPropNames } from './types/HelperTypes';
15
+ import { ClientState } from './filters';
16
+ import { getType } from './types/typeRegistry';
17
+ import { ReferenceTracker } from './changes/ReferenceTracker';
18
+ import { addCallback, spliceOne } from './types/utils';
19
+
20
+ export interface DataChange<T=any,F=string> {
21
+ refId: number,
22
+ op: OPERATION,
23
+ field: F;
24
+ dynamicIndex?: number | string;
25
+ value: T;
26
+ previousValue: T;
27
+ }
28
+
29
+ export interface SchemaDecoderCallbacks<TValue=any, TKey=any> {
30
+ $callbacks: { [operation: number]: Array<(item: TValue, key: TKey) => void> };
31
+
32
+ onAdd(callback: (item: any, key: any) => void, ignoreExisting?: boolean): () => void;
33
+ onRemove(callback: (item: any, key: any) => void): () => void;
34
+ onChange(callback: (item: any, key: any) => void): () => void;
35
+
36
+ clone(decoding?: boolean): SchemaDecoderCallbacks;
37
+ clear(changes?: DataChange[]);
38
+ decode?(byte, it: Iterator);
39
+ }
40
+
41
+ class EncodeSchemaError extends Error {}
42
+
43
+ function assertType(value: any, type: string, klass: Schema, field: string | number) {
44
+ let typeofTarget: string;
45
+ let allowNull: boolean = false;
46
+
47
+ switch (type) {
48
+ case "number":
49
+ case "int8":
50
+ case "uint8":
51
+ case "int16":
52
+ case "uint16":
53
+ case "int32":
54
+ case "uint32":
55
+ case "int64":
56
+ case "uint64":
57
+ case "float32":
58
+ case "float64":
59
+ typeofTarget = "number";
60
+ if (isNaN(value)) {
61
+ console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
62
+ }
63
+ break;
64
+ case "string":
65
+ typeofTarget = "string";
66
+ allowNull = true;
67
+ break;
68
+ case "boolean":
69
+ // boolean is always encoded as true/false based on truthiness
70
+ return;
71
+ }
72
+
73
+ if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
74
+ let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
75
+ throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
76
+ }
77
+ }
78
+
79
+ function assertInstanceType(
80
+ value: Schema,
81
+ type: typeof Schema
82
+ | typeof ArraySchema
83
+ | typeof MapSchema
84
+ | typeof CollectionSchema
85
+ | typeof SetSchema,
86
+ klass: Schema,
87
+ field: string | number,
88
+ ) {
89
+ if (!(value instanceof type)) {
90
+ throw new EncodeSchemaError(`a '${type.name}' was expected, but '${(value as any).constructor.name}' was provided in ${klass.constructor.name}#${field}`);
91
+ }
92
+ }
93
+
94
+ function encodePrimitiveType(
95
+ type: PrimitiveType,
96
+ bytes: number[],
97
+ value: any,
98
+ klass: Schema,
99
+ field: string | number,
100
+ ) {
101
+ assertType(value, type as string, klass, field);
102
+
103
+ const encodeFunc = encode[type as string];
104
+
105
+ if (encodeFunc) {
106
+ encodeFunc(bytes, value);
107
+
108
+ } else {
109
+ throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
110
+ }
111
+ }
112
+
113
+ function decodePrimitiveType (type: string, bytes: number[], it: Iterator) {
114
+ return decode[type as string](bytes, it);
115
+ }
116
+
117
+ /**
118
+ * Schema encoder / decoder
119
+ */
120
+ export abstract class Schema {
121
+ static _typeid: number;
122
+ static _context: Context;
123
+
124
+ static _definition: SchemaDefinition = SchemaDefinition.create();
125
+
126
+ static onError(e) {
127
+ console.error(e);
128
+ }
129
+
130
+ static is(type: DefinitionType) {
131
+ return (
132
+ type['_definition'] &&
133
+ type['_definition'].schema !== undefined
134
+ );
135
+ }
136
+
137
+ protected $changes: ChangeTree;
138
+
139
+ // TODO: refactor. this feature needs to be ported to other languages with potentially different API
140
+ // protected $listeners: { [field: string]: Array<(value: any, previousValue: any) => void> };
141
+ protected $callbacks: { [op: number]: Array<Function> };
142
+
143
+ public onChange(callback: (changes: DataChange[]) => void): () => void {
144
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.REPLACE, callback);
145
+ }
146
+ public onRemove(callback: () => void): () => void {
147
+ return addCallback((this.$callbacks || (this.$callbacks = [])), OPERATION.DELETE, callback);
148
+ }
149
+
150
+ // allow inherited classes to have a constructor
151
+ constructor(...args: any[]) {
152
+ // fix enumerability of fields for end-user
153
+ Object.defineProperties(this, {
154
+ $changes: {
155
+ value: new ChangeTree(this, undefined, new ReferenceTracker()),
156
+ enumerable: false,
157
+ writable: true
158
+ },
159
+
160
+ // $listeners: {
161
+ // value: undefined,
162
+ // enumerable: false,
163
+ // writable: true
164
+ // },
165
+
166
+ $callbacks: {
167
+ value: undefined,
168
+ enumerable: false,
169
+ writable: true
170
+ },
171
+ });
172
+
173
+ const descriptors = this._definition.descriptors;
174
+ if (descriptors) {
175
+ Object.defineProperties(this, descriptors);
176
+ }
177
+
178
+ //
179
+ // Assign initial values
180
+ //
181
+ if (args[0]) {
182
+ this.assign(args[0]);
183
+ }
184
+ }
185
+
186
+ public assign(
187
+ props: { [prop in NonFunctionPropNames<this>]?: this[prop] }
188
+ ) {
189
+ Object.assign(this, props);
190
+ return this;
191
+ }
192
+
193
+ protected get _definition () { return (this.constructor as typeof Schema)._definition; }
194
+
195
+ /**
196
+ * (Server-side): Flag a property to be encoded for the next patch.
197
+ * @param instance Schema instance
198
+ * @param property string representing the property name, or number representing the index of the property.
199
+ * @param operation OPERATION to perform (detected automatically)
200
+ */
201
+ public setDirty<K extends NonFunctionPropNames<this>>(property: K | number, operation?: OPERATION) {
202
+ this.$changes.change(property as any, operation);
203
+ }
204
+
205
+ public listen<K extends NonFunctionPropNames<this>>(attr: K, callback: (value: this[K], previousValue: this[K]) => void) {
206
+ if (!this.$callbacks) { this.$callbacks = {}; }
207
+ if (!this.$callbacks[attr as string]) { this.$callbacks[attr as string] = []; }
208
+
209
+ this.$callbacks[attr as string].push(callback);
210
+
211
+ // return un-register callback.
212
+ return () => spliceOne(this.$callbacks[attr as string], this.$callbacks[attr as string].indexOf(callback));
213
+ }
214
+
215
+ decode(
216
+ bytes: number[],
217
+ it: Iterator = { offset: 0 },
218
+ ref: Ref = this,
219
+ ) {
220
+ const allChanges: DataChange[] = [];
221
+
222
+ const $root = this.$changes.root;
223
+ const totalBytes = bytes.length;
224
+
225
+ let refId: number = 0;
226
+ $root.refs.set(refId, this);
227
+
228
+ while (it.offset < totalBytes) {
229
+ let byte = bytes[it.offset++];
230
+
231
+ if (byte == SWITCH_TO_STRUCTURE) {
232
+ refId = decode.number(bytes, it);
233
+ const nextRef = $root.refs.get(refId) as Schema;
234
+
235
+ //
236
+ // Trying to access a reference that haven't been decoded yet.
237
+ //
238
+ if (!nextRef) { throw new Error(`"refId" not found: ${refId}`); }
239
+ ref = nextRef;
240
+
241
+ continue;
242
+ }
243
+
244
+ const changeTree: ChangeTree = ref['$changes'];
245
+ const isSchema = (ref['_definition'] !== undefined);
246
+
247
+ const operation = (isSchema)
248
+ ? (byte >> 6) << 6 // "compressed" index + operation
249
+ : byte; // "uncompressed" index + operation (array/map items)
250
+
251
+ if (operation === OPERATION.CLEAR) {
252
+ //
253
+ // TODO: refactor me!
254
+ // The `.clear()` method is calling `$root.removeRef(refId)` for
255
+ // each item inside this collection
256
+ //
257
+ (ref as SchemaDecoderCallbacks).clear(allChanges);
258
+ continue;
259
+ }
260
+
261
+ const fieldIndex = (isSchema)
262
+ ? byte % (operation || 255) // if "REPLACE" operation (0), use 255
263
+ : decode.number(bytes, it);
264
+
265
+ const fieldName = (isSchema)
266
+ ? (ref['_definition'].fieldsByIndex[fieldIndex])
267
+ : "";
268
+
269
+ let type = changeTree.getType(fieldIndex);
270
+ let value: any;
271
+ let previousValue: any;
272
+
273
+ let dynamicIndex: number | string;
274
+
275
+ if (!isSchema) {
276
+ previousValue = ref['getByIndex'](fieldIndex);
277
+
278
+ if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
279
+ dynamicIndex = (ref instanceof MapSchema)
280
+ ? decode.string(bytes, it)
281
+ : fieldIndex;
282
+ ref['setIndex'](fieldIndex, dynamicIndex);
283
+
284
+ } else {
285
+ // here
286
+ dynamicIndex = ref['getIndex'](fieldIndex);
287
+ }
288
+
289
+ } else {
290
+ previousValue = ref[`_${fieldName}`];
291
+ }
292
+
293
+ //
294
+ // Delete operations
295
+ //
296
+ if ((operation & OPERATION.DELETE) === OPERATION.DELETE)
297
+ {
298
+ if (operation !== OPERATION.DELETE_AND_ADD) {
299
+ ref['deleteByIndex'](fieldIndex);
300
+ }
301
+
302
+ // Flag `refId` for garbage collection.
303
+ if (previousValue && previousValue['$changes']) {
304
+ $root.removeRef(previousValue['$changes'].refId);
305
+ }
306
+
307
+ value = null;
308
+ }
309
+
310
+ if (fieldName === undefined) {
311
+ console.warn("@colyseus/schema: definition mismatch");
312
+
313
+ //
314
+ // keep skipping next bytes until reaches a known structure
315
+ // by local decoder.
316
+ //
317
+ const nextIterator: Iterator = { offset: it.offset };
318
+ while (it.offset < totalBytes) {
319
+ if (decode.switchStructureCheck(bytes, it)) {
320
+ nextIterator.offset = it.offset + 1;
321
+ if ($root.refs.has(decode.number(bytes, nextIterator))) {
322
+ break;
323
+ }
324
+ }
325
+
326
+ it.offset++;
327
+ }
328
+
329
+ continue;
330
+
331
+ } else if (operation === OPERATION.DELETE) {
332
+ //
333
+ // FIXME: refactor me.
334
+ // Don't do anything.
335
+ //
336
+
337
+ } else if (Schema.is(type)) {
338
+ const refId = decode.number(bytes, it);
339
+ value = $root.refs.get(refId);
340
+
341
+ if (operation !== OPERATION.REPLACE) {
342
+ const childType = this.getSchemaType(bytes, it, type);
343
+
344
+ if (!value) {
345
+ value = this.createTypeInstance(childType);
346
+ value.$changes.refId = refId;
347
+
348
+ if (previousValue) {
349
+ value.$callbacks = previousValue.$callbacks;
350
+ // value.$listeners = previousValue.$listeners;
351
+
352
+ if (
353
+ previousValue['$changes'].refId &&
354
+ refId !== previousValue['$changes'].refId
355
+ ) {
356
+ $root.removeRef(previousValue['$changes'].refId);
357
+ }
358
+ }
359
+ }
360
+
361
+ $root.addRef(refId, value, (value !== previousValue));
362
+ }
363
+ } else if (typeof(type) === "string") {
364
+ //
365
+ // primitive value (number, string, boolean, etc)
366
+ //
367
+ value = decodePrimitiveType(type as string, bytes, it);
368
+
369
+ } else {
370
+ const typeDef = getType(Object.keys(type)[0]);
371
+ const refId = decode.number(bytes, it);
372
+
373
+ const valueRef: SchemaDecoderCallbacks = ($root.refs.has(refId))
374
+ ? previousValue || $root.refs.get(refId)
375
+ : new typeDef.constructor();
376
+
377
+ value = valueRef.clone(true);
378
+ value.$changes.refId = refId;
379
+
380
+ // preserve schema callbacks
381
+ if (previousValue) {
382
+ value['$callbacks'] = previousValue['$callbacks'];
383
+
384
+ if (
385
+ previousValue['$changes'].refId &&
386
+ refId !== previousValue['$changes'].refId
387
+ ) {
388
+ $root.removeRef(previousValue['$changes'].refId);
389
+
390
+ //
391
+ // Trigger onRemove if structure has been replaced.
392
+ //
393
+ const entries: IterableIterator<[any, any]> = previousValue.entries();
394
+ let iter: IteratorResult<[any, any]>;
395
+ while ((iter = entries.next()) && !iter.done) {
396
+ const [key, value] = iter.value;
397
+ allChanges.push({
398
+ refId,
399
+ op: OPERATION.DELETE,
400
+ field: key,
401
+ value: undefined,
402
+ previousValue: value,
403
+ });
404
+ }
405
+ }
406
+ }
407
+
408
+ $root.addRef(refId, value, (valueRef !== previousValue));
409
+ }
410
+
411
+ if (
412
+ value !== null &&
413
+ value !== undefined
414
+ ) {
415
+ if (value['$changes']) {
416
+ value['$changes'].setParent(
417
+ changeTree.ref,
418
+ changeTree.root,
419
+ fieldIndex,
420
+ );
421
+ }
422
+
423
+ if (ref instanceof Schema) {
424
+ ref[fieldName] = value;
425
+ // ref[`_${fieldName}`] = value;
426
+
427
+ } else if (ref instanceof MapSchema) {
428
+ // const key = ref['$indexes'].get(field);
429
+ const key = dynamicIndex as string;
430
+
431
+ // ref.set(key, value);
432
+ ref['$items'].set(key, value);
433
+ ref['$changes'].allChanges.add(fieldIndex);
434
+
435
+ } else if (ref instanceof ArraySchema) {
436
+ // const key = ref['$indexes'][field];
437
+ // console.log("SETTING FOR ArraySchema =>", { field, key, value });
438
+ // ref[key] = value;
439
+ ref.setAt(fieldIndex, value);
440
+
441
+ } else if (ref instanceof CollectionSchema) {
442
+ const index = ref.add(value);
443
+ ref['setIndex'](fieldIndex, index);
444
+
445
+ } else if (ref instanceof SetSchema) {
446
+ const index = ref.add(value);
447
+ if (index !== false) {
448
+ ref['setIndex'](fieldIndex, index);
449
+ }
450
+ }
451
+ }
452
+
453
+ if (previousValue !== value) {
454
+ allChanges.push({
455
+ refId,
456
+ op: operation,
457
+ field: fieldName,
458
+ dynamicIndex,
459
+ value,
460
+ previousValue,
461
+ });
462
+ }
463
+ }
464
+
465
+ this._triggerChanges(allChanges);
466
+
467
+ // drop references of unused schemas
468
+ $root.garbageCollectDeletedRefs();
469
+
470
+ return allChanges;
471
+ }
472
+
473
+ encode(
474
+ encodeAll = false,
475
+ bytes: number[] = [],
476
+ useFilters: boolean = false,
477
+ ) {
478
+ const rootChangeTree = this.$changes;
479
+ const refIdsVisited = new WeakSet<ChangeTree>();
480
+
481
+ const changeTrees: ChangeTree[] = [rootChangeTree];
482
+ let numChangeTrees = 1;
483
+
484
+ for (let i = 0; i < numChangeTrees; i++) {
485
+ const changeTree = changeTrees[i];
486
+ const ref = changeTree.ref;
487
+ const isSchema = (ref instanceof Schema);
488
+
489
+ // Generate unique refId for the ChangeTree.
490
+ changeTree.ensureRefId();
491
+
492
+ // mark this ChangeTree as visited.
493
+ refIdsVisited.add(changeTree);
494
+
495
+ // root `refId` is skipped.
496
+ if (
497
+ changeTree !== rootChangeTree &&
498
+ (changeTree.changed || encodeAll)
499
+ ) {
500
+ encode.uint8(bytes, SWITCH_TO_STRUCTURE);
501
+ encode.number(bytes, changeTree.refId);
502
+ }
503
+
504
+ const changes: ChangeOperation[] | number[] = (encodeAll)
505
+ ? Array.from(changeTree.allChanges)
506
+ : Array.from(changeTree.changes.values());
507
+
508
+ for (let j = 0, cl = changes.length; j < cl; j++) {
509
+ const operation: ChangeOperation = (encodeAll)
510
+ ? { op: OPERATION.ADD, index: changes[j] as number }
511
+ : changes[j] as ChangeOperation;
512
+
513
+ const fieldIndex = operation.index;
514
+
515
+ const field = (isSchema)
516
+ ? ref['_definition'].fieldsByIndex && ref['_definition'].fieldsByIndex[fieldIndex]
517
+ : fieldIndex;
518
+
519
+ // cache begin index if `useFilters`
520
+ const beginIndex = bytes.length;
521
+
522
+ // encode field index + operation
523
+ if (operation.op !== OPERATION.TOUCH) {
524
+ if (isSchema) {
525
+ //
526
+ // Compress `fieldIndex` + `operation` into a single byte.
527
+ // This adds a limitaion of 64 fields per Schema structure
528
+ //
529
+ encode.uint8(bytes, (fieldIndex | operation.op));
530
+
531
+ } else {
532
+ encode.uint8(bytes, operation.op);
533
+
534
+ // custom operations
535
+ if (operation.op === OPERATION.CLEAR) {
536
+ continue;
537
+ }
538
+
539
+ // indexed operations
540
+ encode.number(bytes, fieldIndex);
541
+ }
542
+ }
543
+
544
+ //
545
+ // encode "alias" for dynamic fields (maps)
546
+ //
547
+ if (
548
+ !isSchema &&
549
+ (operation.op & OPERATION.ADD) == OPERATION.ADD // ADD or DELETE_AND_ADD
550
+ ) {
551
+ if (ref instanceof MapSchema) {
552
+ //
553
+ // MapSchema dynamic key
554
+ //
555
+ const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
556
+ encode.string(bytes, dynamicIndex);
557
+ }
558
+ }
559
+
560
+ if (operation.op === OPERATION.DELETE) {
561
+ //
562
+ // TODO: delete from filter cache data.
563
+ //
564
+ // if (useFilters) {
565
+ // delete changeTree.caches[fieldIndex];
566
+ // }
567
+ continue;
568
+ }
569
+
570
+ // const type = changeTree.childType || ref._schema[field];
571
+ const type = changeTree.getType(fieldIndex);
572
+
573
+ // const type = changeTree.getType(fieldIndex);
574
+ const value = changeTree.getValue(fieldIndex);
575
+
576
+ // Enqueue ChangeTree to be visited
577
+ if (
578
+ value &&
579
+ value['$changes'] &&
580
+ !refIdsVisited.has(value['$changes'])
581
+ ) {
582
+ changeTrees.push(value['$changes']);
583
+ value['$changes'].ensureRefId();
584
+ numChangeTrees++;
585
+ }
586
+
587
+ if (operation.op === OPERATION.TOUCH) {
588
+ continue;
589
+ }
590
+
591
+ if (Schema.is(type)) {
592
+ assertInstanceType(value, type as typeof Schema, ref as Schema, field);
593
+
594
+ //
595
+ // Encode refId for this instance.
596
+ // The actual instance is going to be encoded on next `changeTree` iteration.
597
+ //
598
+ encode.number(bytes, value.$changes.refId);
599
+
600
+ // Try to encode inherited TYPE_ID if it's an ADD operation.
601
+ if ((operation.op & OPERATION.ADD) === OPERATION.ADD) {
602
+ this.tryEncodeTypeId(bytes, type as typeof Schema, value.constructor as typeof Schema);
603
+ }
604
+
605
+ } else if (typeof(type) === "string") {
606
+ //
607
+ // Primitive values
608
+ //
609
+ encodePrimitiveType(type as PrimitiveType, bytes, value, ref as Schema, field);
610
+
611
+ } else {
612
+ //
613
+ // Custom type (MapSchema, ArraySchema, etc)
614
+ //
615
+ const definition = getType(Object.keys(type)[0]);
616
+
617
+ //
618
+ // ensure a ArraySchema has been provided
619
+ //
620
+ assertInstanceType(ref[`_${field}`], definition.constructor, ref as Schema, field);
621
+
622
+ //
623
+ // Encode refId for this instance.
624
+ // The actual instance is going to be encoded on next `changeTree` iteration.
625
+ //
626
+ encode.number(bytes, value.$changes.refId);
627
+ }
628
+
629
+ if (useFilters) {
630
+ // cache begin / end index
631
+ changeTree.cache(fieldIndex as number, bytes.slice(beginIndex));
632
+ }
633
+ }
634
+
635
+ if (!encodeAll && !useFilters) {
636
+ changeTree.discard();
637
+ }
638
+ }
639
+
640
+ return bytes;
641
+ }
642
+
643
+ encodeAll (useFilters?: boolean) {
644
+ return this.encode(true, [], useFilters);
645
+ }
646
+
647
+ applyFilters(client: ClientWithSessionId, encodeAll: boolean = false) {
648
+ const root = this;
649
+ const refIdsDissallowed = new Set<number>();
650
+
651
+ const $filterState = ClientState.get(client);
652
+
653
+ const changeTrees = [this.$changes];
654
+ let numChangeTrees = 1;
655
+
656
+ let filteredBytes: number[] = [];
657
+
658
+ for (let i = 0; i < numChangeTrees; i++) {
659
+ const changeTree = changeTrees[i];
660
+
661
+ if (refIdsDissallowed.has(changeTree.refId)) {
662
+ // console.log("REFID IS NOT ALLOWED. SKIP.", { refId: changeTree.refId })
663
+ continue;
664
+ }
665
+
666
+ const ref = changeTree.ref as Ref;
667
+ const isSchema: boolean = ref instanceof Schema;
668
+
669
+ encode.uint8(filteredBytes, SWITCH_TO_STRUCTURE);
670
+ encode.number(filteredBytes, changeTree.refId);
671
+
672
+ const clientHasRefId = $filterState.refIds.has(changeTree);
673
+ const isEncodeAll = (encodeAll || !clientHasRefId);
674
+
675
+ // console.log("REF:", ref.constructor.name);
676
+ // console.log("Encode all?", isEncodeAll);
677
+
678
+ //
679
+ // include `changeTree` on list of known refIds by this client.
680
+ //
681
+ $filterState.addRefId(changeTree);
682
+
683
+ const containerIndexes = $filterState.containerIndexes.get(changeTree)
684
+ const changes = (isEncodeAll)
685
+ ? Array.from(changeTree.allChanges)
686
+ : Array.from(changeTree.changes.values());
687
+
688
+ //
689
+ // WORKAROUND: tries to re-evaluate previously not included @filter() attributes
690
+ // - see "DELETE a field of Schema" test case.
691
+ //
692
+ if (
693
+ !encodeAll &&
694
+ isSchema &&
695
+ (ref as Schema)._definition.indexesWithFilters
696
+ ) {
697
+ const indexesWithFilters = (ref as Schema)._definition.indexesWithFilters;
698
+ indexesWithFilters.forEach(indexWithFilter => {
699
+ if (
700
+ !containerIndexes.has(indexWithFilter) &&
701
+ changeTree.allChanges.has(indexWithFilter)
702
+ ) {
703
+ if (isEncodeAll) {
704
+ changes.push(indexWithFilter as any);
705
+
706
+ } else {
707
+ changes.push({ op: OPERATION.ADD, index: indexWithFilter, } as any);
708
+ }
709
+ }
710
+ });
711
+ }
712
+
713
+ for (let j = 0, cl = changes.length; j < cl; j++) {
714
+ const change: ChangeOperation = (isEncodeAll)
715
+ ? { op: OPERATION.ADD, index: changes[j] as number }
716
+ : changes[j] as ChangeOperation;
717
+
718
+ // custom operations
719
+ if (change.op === OPERATION.CLEAR) {
720
+ encode.uint8(filteredBytes, change.op);
721
+ continue;
722
+ }
723
+
724
+ const fieldIndex = change.index;
725
+
726
+ //
727
+ // Deleting fields: encode the operation + field index
728
+ //
729
+ if (change.op === OPERATION.DELETE) {
730
+ //
731
+ // DELETE operations also need to go through filtering.
732
+ //
733
+ // TODO: cache the previous value so we can access the value (primitive or `refId`)
734
+ // (check against `$filterState.refIds`)
735
+ //
736
+
737
+ if (isSchema) {
738
+ encode.uint8(filteredBytes, change.op | fieldIndex);
739
+
740
+ } else {
741
+ encode.uint8(filteredBytes, change.op);
742
+ encode.number(filteredBytes, fieldIndex);
743
+
744
+ }
745
+ continue;
746
+ }
747
+
748
+ // indexed operation
749
+ const value = changeTree.getValue(fieldIndex);
750
+ const type = changeTree.getType(fieldIndex);
751
+
752
+ if (isSchema) {
753
+ // Is a Schema!
754
+ const filter = (
755
+ (ref as Schema)._definition.filters &&
756
+ (ref as Schema)._definition.filters[fieldIndex]
757
+ );
758
+
759
+ if (filter && !filter.call(ref, client, value, root)) {
760
+ if (value && value['$changes']) {
761
+ refIdsDissallowed.add(value['$changes'].refId);;
762
+ }
763
+ continue;
764
+ }
765
+
766
+ } else {
767
+ // Is a collection! (map, array, etc.)
768
+ const parent = changeTree.parent as Ref;
769
+ const filter = changeTree.getChildrenFilter();
770
+
771
+ if (filter && !filter.call(parent, client, ref['$indexes'].get(fieldIndex), value, root)) {
772
+ if (value && value['$changes']) {
773
+ refIdsDissallowed.add(value['$changes'].refId);
774
+ }
775
+ continue;
776
+ }
777
+ }
778
+
779
+ // visit child ChangeTree on further iteration.
780
+ if (value['$changes']) {
781
+ changeTrees.push(value['$changes']);
782
+ numChangeTrees++;
783
+ }
784
+
785
+ //
786
+ // Copy cached bytes
787
+ //
788
+ if (change.op !== OPERATION.TOUCH) {
789
+
790
+ //
791
+ // TODO: refactor me!
792
+ //
793
+
794
+ if (change.op === OPERATION.ADD || isSchema) {
795
+ //
796
+ // use cached bytes directly if is from Schema type.
797
+ //
798
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
799
+ containerIndexes.add(fieldIndex);
800
+
801
+ } else {
802
+ if (containerIndexes.has(fieldIndex)) {
803
+ //
804
+ // use cached bytes if already has the field
805
+ //
806
+ filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []);
807
+
808
+ } else {
809
+ //
810
+ // force ADD operation if field is not known by this client.
811
+ //
812
+ containerIndexes.add(fieldIndex);
813
+
814
+ encode.uint8(filteredBytes, OPERATION.ADD);
815
+ encode.number(filteredBytes, fieldIndex);
816
+
817
+ if (ref instanceof MapSchema) {
818
+ //
819
+ // MapSchema dynamic key
820
+ //
821
+ const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
822
+ encode.string(filteredBytes, dynamicIndex);
823
+ }
824
+
825
+ if (value['$changes']) {
826
+ encode.number(filteredBytes, value['$changes'].refId);
827
+
828
+ } else {
829
+ // "encodePrimitiveType" without type checking.
830
+ // the type checking has been done on the first .encode() call.
831
+ encode[type as string](filteredBytes, value);
832
+ }
833
+ }
834
+ }
835
+
836
+ } else if (value['$changes'] && !isSchema) {
837
+ //
838
+ // TODO:
839
+ // - track ADD/REPLACE/DELETE instances on `$filterState`
840
+ // - do NOT always encode dynamicIndex for MapSchema.
841
+ // (If client already has that key, only the first index is necessary.)
842
+ //
843
+
844
+ encode.uint8(filteredBytes, OPERATION.ADD);
845
+ encode.number(filteredBytes, fieldIndex);
846
+
847
+ if (ref instanceof MapSchema) {
848
+ //
849
+ // MapSchema dynamic key
850
+ //
851
+ const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex);
852
+ encode.string(filteredBytes, dynamicIndex);
853
+ }
854
+
855
+ encode.number(filteredBytes, value['$changes'].refId);
856
+ }
857
+
858
+ };
859
+ }
860
+
861
+ return filteredBytes;
862
+ }
863
+
864
+ clone (): this {
865
+ const cloned = new ((this as any).constructor);
866
+ const schema = this._definition.schema;
867
+ for (let field in schema) {
868
+ if (
869
+ typeof (this[field]) === "object" &&
870
+ typeof (this[field]?.clone) === "function"
871
+ ) {
872
+ // deep clone
873
+ cloned[field] = this[field].clone();
874
+
875
+ } else {
876
+ // primitive values
877
+ cloned[field] = this[field];
878
+ }
879
+ }
880
+ return cloned;
881
+ }
882
+
883
+ toJSON () {
884
+ const schema = this._definition.schema;
885
+ const deprecated = this._definition.deprecated;
886
+
887
+ const obj = {}
888
+ for (let field in schema) {
889
+ if (!deprecated[field] && this[field] !== null && typeof (this[field]) !== "undefined") {
890
+ obj[field] = (typeof (this[field]['toJSON']) === "function")
891
+ ? this[field]['toJSON']()
892
+ : this[`_${field}`];
893
+ }
894
+ }
895
+ return obj;
896
+ }
897
+
898
+ discardAllChanges() {
899
+ this.$changes.discardAll();
900
+ }
901
+
902
+ protected getByIndex(index: number) {
903
+ return this[this._definition.fieldsByIndex[index]];
904
+ }
905
+
906
+ protected deleteByIndex(index: number) {
907
+ this[this._definition.fieldsByIndex[index]] = undefined;
908
+ }
909
+
910
+ private tryEncodeTypeId (bytes: number[], type: typeof Schema, targetType: typeof Schema) {
911
+ if (type._typeid !== targetType._typeid) {
912
+ encode.uint8(bytes, TYPE_ID);
913
+ encode.number(bytes, targetType._typeid);
914
+ }
915
+ }
916
+
917
+ private getSchemaType(bytes: number[], it: Iterator, defaultType: typeof Schema): typeof Schema {
918
+ let type: typeof Schema;
919
+
920
+ if (bytes[it.offset] === TYPE_ID) {
921
+ it.offset++;
922
+ type = (this.constructor as typeof Schema)._context.get(decode.number(bytes, it));
923
+ }
924
+
925
+ return type || defaultType;
926
+ }
927
+
928
+ private createTypeInstance (type: typeof Schema): Schema {
929
+ let instance: Schema = new (type as any)();
930
+
931
+ // assign root on $changes
932
+ instance.$changes.root = this.$changes.root;
933
+
934
+ return instance;
935
+ }
936
+
937
+ private _triggerChanges(changes: DataChange[]) {
938
+ const uniqueRefIds = new Set<number>();
939
+ const $refs = this.$changes.root.refs;
940
+
941
+ for (let i = 0; i < changes.length; i++) {
942
+ const change = changes[i];
943
+ const refId = change.refId;
944
+ const ref = $refs.get(refId);
945
+ const $callbacks: Schema['$callbacks'] | SchemaDecoderCallbacks['$callbacks'] = ref['$callbacks'];
946
+
947
+ //
948
+ // trigger onRemove on child structure.
949
+ //
950
+ if (
951
+ (change.op & OPERATION.DELETE) === OPERATION.DELETE &&
952
+ change.previousValue instanceof Schema
953
+ ) {
954
+ change.previousValue['$callbacks']?.[OPERATION.DELETE]?.forEach(callback => callback());
955
+ }
956
+
957
+ // no callbacks defined, skip this structure!
958
+ if (!$callbacks) { continue; }
959
+
960
+ if (ref instanceof Schema) {
961
+ if (!uniqueRefIds.has(refId)) {
962
+ try {
963
+ // trigger onChange
964
+ ($callbacks as Schema['$callbacks'])?.[OPERATION.REPLACE]?.forEach(callback =>
965
+ callback(changes));
966
+
967
+ } catch (e) {
968
+ Schema.onError(e);
969
+ }
970
+ }
971
+
972
+ try {
973
+ if ($callbacks.hasOwnProperty(change.field)) {
974
+ $callbacks[change.field]?.forEach((callback) =>
975
+ callback(change.value, change.previousValue));
976
+ }
977
+
978
+ } catch (e) {
979
+ Schema.onError(e);
980
+ }
981
+
982
+ } else {
983
+ // is a collection of items
984
+
985
+ if (change.op === OPERATION.ADD && change.previousValue === undefined) {
986
+ // triger onAdd
987
+ $callbacks[OPERATION.ADD]?.forEach(callback =>
988
+ callback(change.value, change.dynamicIndex ?? change.field));
989
+
990
+ } else if (change.op === OPERATION.DELETE) {
991
+ //
992
+ // FIXME: `previousValue` should always be available.
993
+ // ADD + DELETE operations are still encoding DELETE operation.
994
+ //
995
+ if (change.previousValue !== undefined) {
996
+ // triger onRemove
997
+ $callbacks[OPERATION.DELETE]?.forEach(callback =>
998
+ callback(change.previousValue, change.dynamicIndex ?? change.field));
999
+ }
1000
+
1001
+ } else if (change.op === OPERATION.DELETE_AND_ADD) {
1002
+ // triger onRemove
1003
+ if (change.previousValue !== undefined) {
1004
+ $callbacks[OPERATION.DELETE]?.forEach(callback =>
1005
+ callback(change.previousValue, change.dynamicIndex ?? change.field));
1006
+ }
1007
+
1008
+ // triger onAdd
1009
+ $callbacks[OPERATION.ADD]?.forEach(callback =>
1010
+ callback(change.value, change.dynamicIndex ?? change.field));
1011
+ }
1012
+
1013
+ // trigger onChange
1014
+ if (change.value !== change.previousValue) {
1015
+ $callbacks[OPERATION.REPLACE]?.forEach(callback =>
1016
+ callback(change.value, change.dynamicIndex ?? change.field));
1017
+ }
1018
+ }
1019
+
1020
+ uniqueRefIds.add(refId);
1021
+ }
1022
+
1023
+ }
1024
+ }