@colyseus/schema 2.0.31 → 3.0.0-alpha.0

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