@colyseus/schema 3.0.0-alpha.34 → 3.0.0-alpha.36

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 (71) hide show
  1. package/bin/schema-debug +4 -3
  2. package/build/cjs/index.js +612 -408
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +612 -408
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +612 -408
  7. package/lib/Metadata.d.ts +6 -6
  8. package/lib/Metadata.js +48 -21
  9. package/lib/Metadata.js.map +1 -1
  10. package/lib/Reflection.d.ts +17 -2
  11. package/lib/Reflection.js +19 -6
  12. package/lib/Reflection.js.map +1 -1
  13. package/lib/Schema.js +24 -17
  14. package/lib/Schema.js.map +1 -1
  15. package/lib/annotations.d.ts +1 -1
  16. package/lib/annotations.js +13 -16
  17. package/lib/annotations.js.map +1 -1
  18. package/lib/bench_encode.js +12 -5
  19. package/lib/bench_encode.js.map +1 -1
  20. package/lib/debug.js +1 -2
  21. package/lib/debug.js.map +1 -1
  22. package/lib/decoder/Decoder.js +1 -1
  23. package/lib/decoder/Decoder.js.map +1 -1
  24. package/lib/encoder/ChangeTree.d.ts +23 -7
  25. package/lib/encoder/ChangeTree.js +183 -106
  26. package/lib/encoder/ChangeTree.js.map +1 -1
  27. package/lib/encoder/EncodeOperation.d.ts +2 -1
  28. package/lib/encoder/EncodeOperation.js +2 -2
  29. package/lib/encoder/EncodeOperation.js.map +1 -1
  30. package/lib/encoder/Encoder.d.ts +3 -5
  31. package/lib/encoder/Encoder.js +97 -61
  32. package/lib/encoder/Encoder.js.map +1 -1
  33. package/lib/encoder/Root.d.ts +12 -7
  34. package/lib/encoder/Root.js +41 -20
  35. package/lib/encoder/Root.js.map +1 -1
  36. package/lib/encoder/StateView.d.ts +5 -5
  37. package/lib/encoder/StateView.js +29 -23
  38. package/lib/encoder/StateView.js.map +1 -1
  39. package/lib/encoding/encode.js +12 -9
  40. package/lib/encoding/encode.js.map +1 -1
  41. package/lib/types/TypeContext.js +2 -1
  42. package/lib/types/TypeContext.js.map +1 -1
  43. package/lib/types/custom/ArraySchema.js +27 -13
  44. package/lib/types/custom/ArraySchema.js.map +1 -1
  45. package/lib/types/custom/MapSchema.d.ts +3 -1
  46. package/lib/types/custom/MapSchema.js +7 -4
  47. package/lib/types/custom/MapSchema.js.map +1 -1
  48. package/lib/types/symbols.d.ts +8 -6
  49. package/lib/types/symbols.js +9 -7
  50. package/lib/types/symbols.js.map +1 -1
  51. package/lib/utils.js +6 -3
  52. package/lib/utils.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/Metadata.ts +51 -27
  55. package/src/Reflection.ts +21 -8
  56. package/src/Schema.ts +33 -25
  57. package/src/annotations.ts +14 -18
  58. package/src/bench_encode.ts +15 -6
  59. package/src/debug.ts +1 -2
  60. package/src/decoder/Decoder.ts +1 -1
  61. package/src/encoder/ChangeTree.ts +220 -115
  62. package/src/encoder/EncodeOperation.ts +5 -4
  63. package/src/encoder/Encoder.ts +115 -68
  64. package/src/encoder/Root.ts +41 -21
  65. package/src/encoder/StateView.ts +32 -28
  66. package/src/encoding/encode.ts +12 -9
  67. package/src/types/TypeContext.ts +2 -1
  68. package/src/types/custom/ArraySchema.ts +39 -17
  69. package/src/types/custom/MapSchema.ts +12 -5
  70. package/src/types/symbols.ts +10 -9
  71. package/src/utils.ts +7 -3
@@ -34,7 +34,6 @@ const $decoder = Symbol("$decoder");
34
34
  const $filter = Symbol("$filter");
35
35
  const $getByIndex = Symbol("$getByIndex");
36
36
  const $deleteByIndex = Symbol("$deleteByIndex");
37
- const $descriptors = Symbol("$descriptors");
38
37
  /**
39
38
  * Used to hold ChangeTree instances whitin the structures
40
39
  */
@@ -44,11 +43,6 @@ const $changes = Symbol('$changes');
44
43
  * (MapSchema, ArraySchema, etc.)
45
44
  */
46
45
  const $childType = Symbol('$childType');
47
- /**
48
- * Special ChangeTree property to identify new instances
49
- * (Once they're encoded, they're not new anymore)
50
- */
51
- const $isNew = Symbol("$isNew");
52
46
  /**
53
47
  * Optional "discard" method for custom types (ArraySchema)
54
48
  * (Discards changes for next serialization)
@@ -58,6 +52,14 @@ const $onEncodeEnd = Symbol('$onEncodeEnd');
58
52
  * When decoding, this method is called after the instance is fully decoded
59
53
  */
60
54
  const $onDecodeEnd = Symbol("$onDecodeEnd");
55
+ /**
56
+ * Metadata
57
+ */
58
+ const $descriptors = Symbol("$descriptors");
59
+ const $numFields = "$__numFields";
60
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
61
+ const $viewFieldIndexes = "$__viewFieldIndexes";
62
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
61
63
 
62
64
  const registeredTypes = {};
63
65
  const identifiers = new Map();
@@ -69,6 +71,102 @@ function getType(identifier) {
69
71
  return registeredTypes[identifier];
70
72
  }
71
73
 
74
+ class TypeContext {
75
+ /**
76
+ * For inheritance support
77
+ * Keeps track of which classes extends which. (parent -> children)
78
+ */
79
+ static { this.inheritedTypes = new Map(); }
80
+ static register(target) {
81
+ const parent = Object.getPrototypeOf(target);
82
+ if (parent !== Schema) {
83
+ let inherits = TypeContext.inheritedTypes.get(parent);
84
+ if (!inherits) {
85
+ inherits = new Set();
86
+ TypeContext.inheritedTypes.set(parent, inherits);
87
+ }
88
+ inherits.add(target);
89
+ }
90
+ }
91
+ constructor(rootClass) {
92
+ this.types = {};
93
+ this.schemas = new Map();
94
+ this.hasFilters = false;
95
+ this.parentFiltered = {};
96
+ if (rootClass) {
97
+ this.discoverTypes(rootClass);
98
+ }
99
+ }
100
+ has(schema) {
101
+ return this.schemas.has(schema);
102
+ }
103
+ get(typeid) {
104
+ return this.types[typeid];
105
+ }
106
+ add(schema, typeid = this.schemas.size) {
107
+ // skip if already registered
108
+ if (this.schemas.has(schema)) {
109
+ return false;
110
+ }
111
+ this.types[typeid] = schema;
112
+ //
113
+ // Workaround to allow using an empty Schema (with no `@type()` fields)
114
+ //
115
+ if (schema[Symbol.metadata] === undefined) {
116
+ Metadata.init(schema);
117
+ }
118
+ this.schemas.set(schema, typeid);
119
+ return true;
120
+ }
121
+ getTypeId(klass) {
122
+ return this.schemas.get(klass);
123
+ }
124
+ discoverTypes(klass, parentIndex, parentFieldViewTag) {
125
+ if (!this.add(klass)) {
126
+ return;
127
+ }
128
+ // add classes inherited from this base class
129
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
130
+ this.discoverTypes(child, parentIndex, parentFieldViewTag);
131
+ });
132
+ const metadata = (klass[Symbol.metadata] ??= {});
133
+ // if any schema/field has filters, mark "context" as having filters.
134
+ if (metadata[$viewFieldIndexes]) {
135
+ this.hasFilters = true;
136
+ }
137
+ if (parentFieldViewTag !== undefined) {
138
+ this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
139
+ }
140
+ for (const fieldIndex in metadata) {
141
+ const index = fieldIndex;
142
+ const fieldType = metadata[index].type;
143
+ const viewTag = metadata[index].tag;
144
+ if (typeof (fieldType) === "string") {
145
+ continue;
146
+ }
147
+ if (Array.isArray(fieldType)) {
148
+ const type = fieldType[0];
149
+ // skip primitive types
150
+ if (type === "string") {
151
+ continue;
152
+ }
153
+ this.discoverTypes(type, index, viewTag);
154
+ }
155
+ else if (typeof (fieldType) === "function") {
156
+ this.discoverTypes(fieldType, viewTag);
157
+ }
158
+ else {
159
+ const type = Object.values(fieldType)[0];
160
+ // skip primitive types
161
+ if (typeof (type) === "string") {
162
+ continue;
163
+ }
164
+ this.discoverTypes(type, index, viewTag);
165
+ }
166
+ }
167
+ }
168
+ }
169
+
72
170
  const Metadata = {
73
171
  addField(metadata, index, name, type, descriptor) {
74
172
  if (index > 64) {
@@ -104,7 +202,7 @@ const Metadata = {
104
202
  };
105
203
  }
106
204
  // map -1 as last field index
107
- Object.defineProperty(metadata, -1, {
205
+ Object.defineProperty(metadata, $numFields, {
108
206
  value: index,
109
207
  enumerable: false,
110
208
  configurable: true
@@ -117,14 +215,14 @@ const Metadata = {
117
215
  });
118
216
  // if child Ref/complex type, add to -4
119
217
  if (typeof (metadata[index].type) !== "string") {
120
- if (metadata[-4] === undefined) {
121
- Object.defineProperty(metadata, -4, {
218
+ if (metadata[$refTypeFieldIndexes] === undefined) {
219
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
122
220
  value: [],
123
221
  enumerable: false,
124
222
  configurable: true,
125
223
  });
126
224
  }
127
- metadata[-4].push(index);
225
+ metadata[$refTypeFieldIndexes].push(index);
128
226
  }
129
227
  },
130
228
  setTag(metadata, fieldName, tag) {
@@ -132,38 +230,63 @@ const Metadata = {
132
230
  const field = metadata[index];
133
231
  // add 'tag' to the field
134
232
  field.tag = tag;
135
- if (!metadata[-2]) {
233
+ if (!metadata[$viewFieldIndexes]) {
136
234
  // -2: all field indexes with "view" tag
137
- Object.defineProperty(metadata, -2, {
235
+ Object.defineProperty(metadata, $viewFieldIndexes, {
138
236
  value: [],
139
237
  enumerable: false,
140
238
  configurable: true
141
239
  });
142
240
  // -3: field indexes by "view" tag
143
- Object.defineProperty(metadata, -3, {
241
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
144
242
  value: {},
145
243
  enumerable: false,
146
244
  configurable: true
147
245
  });
148
246
  }
149
- metadata[-2].push(index);
150
- if (!metadata[-3][tag]) {
151
- metadata[-3][tag] = [];
247
+ metadata[$viewFieldIndexes].push(index);
248
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
249
+ metadata[$fieldIndexesByViewTag][tag] = [];
152
250
  }
153
- metadata[-3][tag].push(index);
251
+ metadata[$fieldIndexesByViewTag][tag].push(index);
154
252
  },
155
253
  setFields(target, fields) {
156
- const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
157
- let index = 0;
254
+ // for inheritance support
255
+ const constructor = target.prototype.constructor;
256
+ TypeContext.register(constructor);
257
+ const parentClass = Object.getPrototypeOf(constructor);
258
+ const parentMetadata = parentClass && parentClass[Symbol.metadata];
259
+ const metadata = Metadata.initialize(constructor, parentMetadata);
260
+ // Use Schema's methods if not defined in the class
261
+ if (!constructor[$track]) {
262
+ constructor[$track] = Schema[$track];
263
+ }
264
+ if (!constructor[$encoder]) {
265
+ constructor[$encoder] = Schema[$encoder];
266
+ }
267
+ if (!constructor[$decoder]) {
268
+ constructor[$decoder] = Schema[$decoder];
269
+ }
270
+ if (!constructor.prototype.toJSON) {
271
+ constructor.prototype.toJSON = Schema.prototype.toJSON;
272
+ }
273
+ //
274
+ // detect index for this field, considering inheritance
275
+ //
276
+ let fieldIndex = metadata[$numFields] // current structure already has fields defined
277
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
278
+ ?? -1; // no fields defined
279
+ fieldIndex++;
158
280
  for (const field in fields) {
159
281
  const type = fields[field];
160
282
  // FIXME: this code is duplicated from @type() annotation
161
283
  const complexTypeKlass = (Array.isArray(type))
162
284
  ? getType("array")
163
285
  : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
164
- Metadata.addField(metadata, index, field, type, getPropertyDescriptor(`_${field}`, index, type, complexTypeKlass));
165
- index++;
286
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, type, complexTypeKlass));
287
+ fieldIndex++;
166
288
  }
289
+ return target;
167
290
  },
168
291
  isDeprecated(metadata, field) {
169
292
  return metadata[field].deprecated === true;
@@ -175,7 +298,7 @@ const Metadata = {
175
298
  //
176
299
  const metadata = {};
177
300
  klass[Symbol.metadata] = metadata;
178
- Object.defineProperty(metadata, -1, {
301
+ Object.defineProperty(metadata, $numFields, {
179
302
  value: 0,
180
303
  enumerable: false,
181
304
  configurable: true,
@@ -189,7 +312,7 @@ const Metadata = {
189
312
  if (parentMetadata) {
190
313
  // assign parent metadata to current
191
314
  Object.assign(metadata, parentMetadata);
192
- for (let i = 0; i <= parentMetadata[-1]; i++) {
315
+ for (let i = 0; i <= parentMetadata[$numFields]; i++) {
193
316
  const fieldName = parentMetadata[i].name;
194
317
  Object.defineProperty(metadata, fieldName, {
195
318
  value: parentMetadata[fieldName],
@@ -197,8 +320,8 @@ const Metadata = {
197
320
  configurable: true,
198
321
  });
199
322
  }
200
- Object.defineProperty(metadata, -1, {
201
- value: parentMetadata[-1],
323
+ Object.defineProperty(metadata, $numFields, {
324
+ value: parentMetadata[$numFields],
202
325
  enumerable: false,
203
326
  configurable: true,
204
327
  writable: true,
@@ -210,40 +333,69 @@ const Metadata = {
210
333
  },
211
334
  isValidInstance(klass) {
212
335
  return (klass.constructor[Symbol.metadata] &&
213
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
336
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
214
337
  },
215
338
  getFields(klass) {
216
339
  const metadata = klass[Symbol.metadata];
217
340
  const fields = {};
218
- for (let i = 0; i <= metadata[-1]; i++) {
341
+ for (let i = 0; i <= metadata[$numFields]; i++) {
219
342
  fields[metadata[i].name] = metadata[i].type;
220
343
  }
221
344
  return fields;
222
345
  }
223
346
  };
224
347
 
225
- var _a$5;
348
+ function setOperationAtIndex(changeSet, index) {
349
+ const operationsIndex = changeSet.indexes[index];
350
+ if (operationsIndex === undefined) {
351
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
352
+ }
353
+ else {
354
+ changeSet.operations[operationsIndex] = index;
355
+ }
356
+ }
357
+ function deleteOperationAtIndex(changeSet, index) {
358
+ const operationsIndex = changeSet.indexes[index];
359
+ if (operationsIndex !== undefined) {
360
+ changeSet.operations[operationsIndex] = undefined;
361
+ }
362
+ delete changeSet.indexes[index];
363
+ }
364
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
365
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
366
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
367
+ }
368
+ }
226
369
  class ChangeTree {
227
- static { _a$5 = $isNew; }
228
370
  constructor(ref) {
229
371
  this.isFiltered = false;
230
372
  this.isPartiallyFiltered = false;
231
- this.currentOperationIndex = 0;
232
- this.changes = new Map();
233
- this.allChanges = new Map();
234
- this[_a$5] = true;
373
+ this.indexedOperations = {};
374
+ //
375
+ // TODO:
376
+ // try storing the index + operation per item.
377
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
378
+ //
379
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
380
+ //
381
+ this.changes = { indexes: {}, operations: [] };
382
+ this.allChanges = { indexes: {}, operations: [] };
383
+ /**
384
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
385
+ */
386
+ this.isNew = true;
235
387
  this.ref = ref;
236
388
  //
237
389
  // Does this structure have "filters" declared?
238
390
  //
239
- if (ref.constructor[Symbol.metadata]?.[-2]) {
240
- this.allFilteredChanges = new Map();
241
- this.filteredChanges = new Map();
391
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
392
+ this.allFilteredChanges = { indexes: {}, operations: [] };
393
+ this.filteredChanges = { indexes: {}, operations: [] };
242
394
  }
243
395
  }
244
396
  setRoot(root) {
245
397
  this.root = root;
246
- this.root.add(this);
398
+ const isNewChangeTree = this.root.add(this);
247
399
  const metadata = this.ref.constructor[Symbol.metadata];
248
400
  if (this.root.types.hasFilters) {
249
401
  //
@@ -254,22 +406,24 @@ class ChangeTree {
254
406
  //
255
407
  this.checkIsFiltered(metadata, this.parent, this.parentIndex);
256
408
  if (this.isFiltered || this.isPartiallyFiltered) {
257
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
258
- this.root.filteredChanges.set(this, this.filteredChanges);
409
+ enqueueChangeTree(root, this, 'filteredChanges');
410
+ if (isNewChangeTree) {
411
+ this.root.allFilteredChanges.push(this);
412
+ }
259
413
  }
260
414
  }
261
415
  if (!this.isFiltered) {
262
- this.root.changes.set(this, this.changes);
263
- this.root.allChanges.set(this, this.allChanges);
416
+ enqueueChangeTree(root, this, 'changes');
417
+ if (isNewChangeTree) {
418
+ this.root.allChanges.push(this);
419
+ }
264
420
  }
265
- this.ensureRefId();
421
+ // Recursively set root on child structures
266
422
  if (metadata) {
267
- metadata[-4]?.forEach((index) => {
423
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
268
424
  const field = metadata[index];
269
425
  const value = this.ref[field.name];
270
- if (value) {
271
- value[$changes].setRoot(root);
272
- }
426
+ value?.[$changes].setRoot(root);
273
427
  });
274
428
  }
275
429
  else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
@@ -286,31 +440,36 @@ class ChangeTree {
286
440
  if (!root) {
287
441
  return;
288
442
  }
289
- root.add(this);
290
443
  const metadata = this.ref.constructor[Symbol.metadata];
291
444
  // skip if parent is already set
292
445
  if (root !== this.root) {
293
446
  this.root = root;
447
+ const isNewChangeTree = root.add(this);
294
448
  if (root.types.hasFilters) {
295
449
  this.checkIsFiltered(metadata, parent, parentIndex);
296
450
  if (this.isFiltered || this.isPartiallyFiltered) {
297
- this.root.filteredChanges.set(this, this.filteredChanges);
298
- this.root.allFilteredChanges.set(this, this.filteredChanges);
451
+ enqueueChangeTree(root, this, 'filteredChanges');
452
+ if (isNewChangeTree) {
453
+ this.root.allFilteredChanges.push(this);
454
+ }
299
455
  }
300
456
  }
301
457
  if (!this.isFiltered) {
302
- this.root.changes.set(this, this.changes);
303
- this.root.allChanges.set(this, this.allChanges);
458
+ enqueueChangeTree(root, this, 'changes');
459
+ if (isNewChangeTree) {
460
+ this.root.allChanges.push(this);
461
+ }
304
462
  }
305
- this.ensureRefId();
463
+ }
464
+ else {
465
+ root.add(this);
306
466
  }
307
467
  // assign same parent on child structures
308
468
  if (metadata) {
309
- metadata[-4]?.forEach((index) => {
469
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
310
470
  const field = metadata[index];
311
471
  const value = this.ref[field.name];
312
472
  value?.[$changes].setParent(this.ref, root, index);
313
- // console.log(this.ref.constructor.name, field.name, value);
314
473
  // try { throw new Error(); } catch (e) {
315
474
  // console.log(e.stack);
316
475
  // }
@@ -329,7 +488,7 @@ class ChangeTree {
329
488
  //
330
489
  const metadata = this.ref.constructor[Symbol.metadata];
331
490
  if (metadata) {
332
- metadata[-4]?.forEach((index) => {
491
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
333
492
  const field = metadata[index];
334
493
  const value = this.ref[field.name];
335
494
  if (value) {
@@ -345,8 +504,10 @@ class ChangeTree {
345
504
  }
346
505
  }
347
506
  operation(op) {
348
- this.changes.set(--this.currentOperationIndex, op);
349
- this.root?.changes.set(this, this.changes);
507
+ // operations without index use negative values to represent them
508
+ // this is checked during .encode() time.
509
+ this.changes.operations.push(-op);
510
+ enqueueChangeTree(this.root, this, 'changes');
350
511
  }
351
512
  change(index, operation = OPERATION.ADD) {
352
513
  const metadata = this.ref.constructor[Symbol.metadata];
@@ -354,7 +515,7 @@ class ChangeTree {
354
515
  const changeSet = (isFiltered)
355
516
  ? this.filteredChanges
356
517
  : this.changes;
357
- const previousOperation = changeSet.get(index);
518
+ const previousOperation = this.indexedOperations[index];
358
519
  if (!previousOperation || previousOperation === OPERATION.DELETE) {
359
520
  const op = (!previousOperation)
360
521
  ? operation
@@ -364,16 +525,19 @@ class ChangeTree {
364
525
  //
365
526
  // TODO: are DELETE operations being encoded as ADD here ??
366
527
  //
367
- changeSet.set(index, op);
528
+ this.indexedOperations[index] = op;
368
529
  }
530
+ setOperationAtIndex(changeSet, index);
369
531
  if (isFiltered) {
370
- this.allFilteredChanges.set(index, OPERATION.ADD);
371
- this.root?.filteredChanges.set(this, this.filteredChanges);
372
- this.root?.allFilteredChanges.set(this, this.allFilteredChanges);
532
+ setOperationAtIndex(this.allFilteredChanges, index);
533
+ if (this.root) {
534
+ enqueueChangeTree(this.root, this, 'filteredChanges');
535
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
536
+ }
373
537
  }
374
538
  else {
375
- this.allChanges.set(index, OPERATION.ADD);
376
- this.root?.changes.set(this, this.changes);
539
+ setOperationAtIndex(this.allChanges, index);
540
+ enqueueChangeTree(this.root, this, 'changes');
377
541
  }
378
542
  }
379
543
  shiftChangeIndexes(shiftIndex) {
@@ -385,12 +549,15 @@ class ChangeTree {
385
549
  const changeSet = (this.isFiltered)
386
550
  ? this.filteredChanges
387
551
  : this.changes;
388
- const changeSetEntries = Array.from(changeSet.entries());
389
- changeSet.clear();
390
- // Re-insert each entry with the shifted index
391
- for (const [index, op] of changeSetEntries) {
392
- changeSet.set(index + shiftIndex, op);
552
+ const newIndexedOperations = {};
553
+ const newIndexes = {};
554
+ for (const index in this.indexedOperations) {
555
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
556
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
393
557
  }
558
+ this.indexedOperations = newIndexedOperations;
559
+ changeSet.indexes = newIndexes;
560
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
394
561
  }
395
562
  shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
396
563
  //
@@ -406,24 +573,36 @@ class ChangeTree {
406
573
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
407
574
  }
408
575
  }
409
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
410
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
411
- if (index >= startIndex) {
412
- allChangeSet.delete(index);
413
- allChangeSet.set(index + shiftIndex, op);
576
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
577
+ const newIndexes = {};
578
+ for (const key in changeSet.indexes) {
579
+ const index = changeSet.indexes[key];
580
+ if (index > startIndex) {
581
+ newIndexes[Number(key) + shiftIndex] = index;
414
582
  }
415
- });
583
+ else {
584
+ newIndexes[key] = index;
585
+ }
586
+ }
587
+ changeSet.indexes = newIndexes;
588
+ for (let i = 0; i < changeSet.operations.length; i++) {
589
+ const index = changeSet.operations[i];
590
+ if (index > startIndex) {
591
+ changeSet.operations[i] = index + shiftIndex;
592
+ }
593
+ }
416
594
  }
417
595
  indexedOperation(index, operation, allChangesIndex = index) {
418
- if (this.filteredChanges !== undefined) {
419
- this.allFilteredChanges.set(allChangesIndex, OPERATION.ADD);
420
- this.filteredChanges.set(index, operation);
421
- this.root?.filteredChanges.set(this, this.filteredChanges);
596
+ this.indexedOperations[index] = operation;
597
+ if (this.filteredChanges) {
598
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
599
+ setOperationAtIndex(this.filteredChanges, index);
600
+ enqueueChangeTree(this.root, this, 'filteredChanges');
422
601
  }
423
602
  else {
424
- this.allChanges.set(allChangesIndex, OPERATION.ADD);
425
- this.changes.set(index, operation);
426
- this.root?.changes.set(this, this.changes);
603
+ setOperationAtIndex(this.allChanges, allChangesIndex);
604
+ setOperationAtIndex(this.changes, index);
605
+ enqueueChangeTree(this.root, this, 'changes');
427
606
  }
428
607
  }
429
608
  getType(index) {
@@ -442,8 +621,7 @@ class ChangeTree {
442
621
  }
443
622
  }
444
623
  getChange(index) {
445
- // TODO: optimize this. avoid checking against multiple instances
446
- return this.changes.get(index) ?? this.filteredChanges?.get(index);
624
+ return this.indexedOperations[index];
447
625
  }
448
626
  //
449
627
  // used during `.encode()`
@@ -467,8 +645,9 @@ class ChangeTree {
467
645
  const changeSet = (this.filteredChanges)
468
646
  ? this.filteredChanges
469
647
  : this.changes;
648
+ this.indexedOperations[index] = operation ?? OPERATION.DELETE;
649
+ setOperationAtIndex(changeSet, index);
470
650
  const previousValue = this.getValue(index);
471
- changeSet.set(index, operation ?? OPERATION.DELETE);
472
651
  // remove `root` reference
473
652
  if (previousValue && previousValue[$changes]) {
474
653
  //
@@ -484,23 +663,26 @@ class ChangeTree {
484
663
  this.root?.remove(previousValue[$changes]);
485
664
  }
486
665
  //
487
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
666
+ // FIXME: this is looking a ugly and repeated
488
667
  //
489
668
  if (this.filteredChanges) {
490
- this.root?.filteredChanges.set(this, this.filteredChanges);
491
- this.allFilteredChanges.delete(allChangesIndex);
669
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
670
+ enqueueChangeTree(this.root, this, 'filteredChanges');
492
671
  }
493
672
  else {
494
- this.root?.changes.set(this, this.changes);
495
- this.allChanges.delete(allChangesIndex);
673
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
674
+ enqueueChangeTree(this.root, this, 'changes');
496
675
  }
497
676
  }
498
677
  endEncode() {
499
- this.changes.clear();
678
+ this.indexedOperations = {};
679
+ // // clear changes
680
+ // this.changes.indexes = {};
681
+ // this.changes.operations.length = 0;
500
682
  // ArraySchema and MapSchema have a custom "encode end" method
501
683
  this.ref[$onEncodeEnd]?.();
502
684
  // Not a new instance anymore
503
- delete this[$isNew];
685
+ this.isNew = false;
504
686
  }
505
687
  discard(discardAll = false) {
506
688
  //
@@ -509,13 +691,22 @@ class ChangeTree {
509
691
  // REPLACE in case same key is used on next patches.
510
692
  //
511
693
  this.ref[$onEncodeEnd]?.();
512
- this.changes.clear();
513
- this.filteredChanges?.clear();
514
- // reset operation index
515
- this.currentOperationIndex = 0;
694
+ this.indexedOperations = {};
695
+ this.changes.indexes = {};
696
+ this.changes.operations.length = 0;
697
+ this.changes.queueRootIndex = undefined;
698
+ if (this.filteredChanges !== undefined) {
699
+ this.filteredChanges.indexes = {};
700
+ this.filteredChanges.operations.length = 0;
701
+ this.filteredChanges.queueRootIndex = undefined;
702
+ }
516
703
  if (discardAll) {
517
- this.allChanges.clear();
518
- this.allFilteredChanges?.clear();
704
+ this.allChanges.indexes = {};
705
+ this.allChanges.operations.length = 0;
706
+ if (this.allFilteredChanges !== undefined) {
707
+ this.allFilteredChanges.indexes = {};
708
+ this.allFilteredChanges.operations.length = 0;
709
+ }
519
710
  // remove children references
520
711
  this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
521
712
  }
@@ -524,12 +715,13 @@ class ChangeTree {
524
715
  * Recursively discard all changes from this, and child structures.
525
716
  */
526
717
  discardAll() {
527
- this.changes.forEach((_, fieldIndex) => {
528
- const value = this.getValue(fieldIndex);
718
+ const keys = Object.keys(this.indexedOperations);
719
+ for (let i = 0, len = keys.length; i < len; i++) {
720
+ const value = this.getValue(Number(keys[i]));
529
721
  if (value && value[$changes]) {
530
722
  value[$changes].discardAll();
531
723
  }
532
- });
724
+ }
533
725
  this.discard();
534
726
  }
535
727
  ensureRefId() {
@@ -540,42 +732,48 @@ class ChangeTree {
540
732
  this.refId = this.root.getNextUniqueId();
541
733
  }
542
734
  get changed() {
543
- return this.changes.size > 0;
735
+ return (Object.entries(this.indexedOperations).length > 0);
544
736
  }
545
737
  checkIsFiltered(metadata, parent, parentIndex) {
546
738
  // Detect if current structure has "filters" declared
547
- this.isPartiallyFiltered = metadata?.[-2] !== undefined;
739
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
548
740
  if (this.isPartiallyFiltered) {
549
- this.filteredChanges = this.filteredChanges || new Map();
550
- this.allFilteredChanges = this.allFilteredChanges || new Map();
741
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
742
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
551
743
  }
552
- if (parent) {
553
- if (!Metadata.isValidInstance(parent)) {
554
- const parentChangeTree = parent[$changes];
555
- parent = parentChangeTree.parent;
556
- parentIndex = parentChangeTree.parentIndex;
557
- }
558
- const parentMetadata = parent?.constructor?.[Symbol.metadata];
559
- this.isFiltered = (parent && parentMetadata?.[-2]?.includes(parentIndex));
560
- //
561
- // TODO: refactor this!
562
- //
563
- // swapping `changes` and `filteredChanges` is required here
564
- // because "isFiltered" may not be imedialely available on `change()`
565
- //
566
- if (this.isFiltered) {
567
- this.filteredChanges = new Map();
568
- this.allFilteredChanges = new Map();
569
- if (this.changes.size > 0) {
570
- // swap changes reference
571
- const changes = this.changes;
572
- this.changes = this.filteredChanges;
573
- this.filteredChanges = changes;
574
- // swap "all changes" reference
575
- const allFilteredChanges = this.allFilteredChanges;
576
- this.allFilteredChanges = this.allChanges;
577
- this.allChanges = allFilteredChanges;
578
- }
744
+ // skip if parent is not set
745
+ if (!parent) {
746
+ return;
747
+ }
748
+ if (!Metadata.isValidInstance(parent)) {
749
+ const parentChangeTree = parent[$changes];
750
+ parent = parentChangeTree.parent;
751
+ parentIndex = parentChangeTree.parentIndex;
752
+ }
753
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
754
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
755
+ //
756
+ // TODO: refactor this!
757
+ //
758
+ // swapping `changes` and `filteredChanges` is required here
759
+ // because "isFiltered" may not be imedialely available on `change()`
760
+ //
761
+ if (this.isFiltered) {
762
+ this.filteredChanges = { indexes: {}, operations: [] };
763
+ this.allFilteredChanges = { indexes: {}, operations: [] };
764
+ if (this.changes.operations.length > 0) {
765
+ // swap changes reference
766
+ const changes = this.changes;
767
+ this.changes = this.filteredChanges;
768
+ this.filteredChanges = changes;
769
+ // swap "all changes" reference
770
+ const allFilteredChanges = this.allFilteredChanges;
771
+ this.allFilteredChanges = this.allChanges;
772
+ this.allChanges = allFilteredChanges;
773
+ // console.log("SWAP =>", {
774
+ // "this.allFilteredChanges": this.allFilteredChanges,
775
+ // "this.allChanges": this.allChanges
776
+ // })
579
777
  }
580
778
  }
581
779
  }
@@ -644,21 +842,24 @@ function utf8Write(view, str, it) {
644
842
  view[it.offset++] = c;
645
843
  }
646
844
  else if (c < 0x800) {
647
- view[it.offset++] = 0xc0 | (c >> 6);
648
- view[it.offset++] = 0x80 | (c & 0x3f);
845
+ view[it.offset] = 0xc0 | (c >> 6);
846
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
847
+ it.offset += 2;
649
848
  }
650
849
  else if (c < 0xd800 || c >= 0xe000) {
651
- view[it.offset++] = 0xe0 | (c >> 12);
652
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
653
- view[it.offset++] = 0x80 | (c & 0x3f);
850
+ view[it.offset] = 0xe0 | (c >> 12);
851
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
852
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
853
+ it.offset += 3;
654
854
  }
655
855
  else {
656
856
  i++;
657
857
  c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
658
- view[it.offset++] = 0xf0 | (c >> 18);
659
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
660
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
661
- view[it.offset++] = 0x80 | (c & 0x3f);
858
+ view[it.offset] = 0xf0 | (c >> 18);
859
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
860
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
861
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
862
+ it.offset += 4;
662
863
  }
663
864
  }
664
865
  }
@@ -887,7 +1088,7 @@ function encodeValue(encoder, bytes, type, value, operation, it) {
887
1088
  * Used for Schema instances.
888
1089
  * @private
889
1090
  */
890
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
1091
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
891
1092
  // "compress" field index + operation
892
1093
  bytes[it.offset++] = (index | operation) & 255;
893
1094
  // Do not encode value for DELETE operations
@@ -895,7 +1096,7 @@ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, opera
895
1096
  return;
896
1097
  }
897
1098
  const ref = changeTree.ref;
898
- const metadata = ref.constructor[Symbol.metadata];
1099
+ // const metadata: Metadata = ref.constructor[Symbol.metadata];
899
1100
  const field = metadata[index];
900
1101
  // TODO: inline this function call small performance gain
901
1102
  encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
@@ -1590,6 +1791,7 @@ class ArraySchema {
1590
1791
  const proxy = new Proxy(this, {
1591
1792
  get: (obj, prop) => {
1592
1793
  if (typeof (prop) !== "symbol" &&
1794
+ // FIXME: d8 accuses this as low performance
1593
1795
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1594
1796
  ) {
1595
1797
  return this.items[prop];
@@ -1607,7 +1809,7 @@ class ArraySchema {
1607
1809
  if (setValue[$changes]) {
1608
1810
  assertInstanceType(setValue, obj[$childType], obj, key);
1609
1811
  if (obj.items[key] !== undefined) {
1610
- if (setValue[$changes][$isNew]) {
1812
+ if (setValue[$changes].isNew) {
1611
1813
  this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
1612
1814
  }
1613
1815
  else {
@@ -1619,7 +1821,7 @@ class ArraySchema {
1619
1821
  }
1620
1822
  }
1621
1823
  }
1622
- else if (setValue[$changes][$isNew]) {
1824
+ else if (setValue[$changes].isNew) {
1623
1825
  this[$changes].indexedOperation(Number(key), OPERATION.ADD);
1624
1826
  }
1625
1827
  }
@@ -1653,7 +1855,9 @@ class ArraySchema {
1653
1855
  });
1654
1856
  this[$changes] = new ChangeTree(proxy);
1655
1857
  this[$changes].indexes = {};
1656
- this.push.apply(this, items);
1858
+ if (items.length > 0) {
1859
+ this.push(...items);
1860
+ }
1657
1861
  return proxy;
1658
1862
  }
1659
1863
  set length(newLength) {
@@ -1672,15 +1876,18 @@ class ArraySchema {
1672
1876
  }
1673
1877
  push(...values) {
1674
1878
  let length = this.tmpItems.length;
1675
- values.forEach((value, i) => {
1676
- // skip null values
1879
+ const changeTree = this[$changes];
1880
+ // values.forEach((value, i) => {
1881
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1882
+ const value = values[i];
1677
1883
  if (value === undefined || value === null) {
1884
+ // skip null values
1678
1885
  return;
1679
1886
  }
1680
1887
  else if (typeof (value) === "object" && this[$childType]) {
1681
1888
  assertInstanceType(value, this[$childType], this, i);
1889
+ // TODO: move value[$changes]?.setParent() to this block.
1682
1890
  }
1683
- const changeTree = this[$changes];
1684
1891
  changeTree.indexedOperation(length, OPERATION.ADD, this.items.length);
1685
1892
  this.items.push(value);
1686
1893
  this.tmpItems.push(value);
@@ -1689,8 +1896,9 @@ class ArraySchema {
1689
1896
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1690
1897
  //
1691
1898
  value[$changes]?.setParent(this, changeTree.root, length);
1692
- length++;
1693
- });
1899
+ }
1900
+ // length++;
1901
+ // });
1694
1902
  return length;
1695
1903
  }
1696
1904
  /**
@@ -1711,6 +1919,7 @@ class ArraySchema {
1711
1919
  }
1712
1920
  this[$changes].delete(index, undefined, this.items.length - 1);
1713
1921
  // this.tmpItems[index] = undefined;
1922
+ // this.tmpItems.pop();
1714
1923
  this.deletedIndexes[index] = true;
1715
1924
  return this.items.pop();
1716
1925
  }
@@ -1775,9 +1984,12 @@ class ArraySchema {
1775
1984
  //
1776
1985
  // TODO: do not use [$changes] at decoding time.
1777
1986
  //
1778
- changeTree.root?.changes.delete(changeTree);
1779
- changeTree.root?.allChanges.delete(changeTree);
1780
- changeTree.root?.allFilteredChanges.delete(changeTree);
1987
+ const root = changeTree.root;
1988
+ if (root !== undefined) {
1989
+ root.removeChangeFromChangeSet("changes", changeTree);
1990
+ root.removeChangeFromChangeSet("allChanges", changeTree);
1991
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
1992
+ }
1781
1993
  });
1782
1994
  changeTree.discard(true);
1783
1995
  changeTree.operation(OPERATION.CLEAR);
@@ -1821,6 +2033,7 @@ class ArraySchema {
1821
2033
  const changeTree = this[$changes];
1822
2034
  changeTree.delete(index);
1823
2035
  changeTree.shiftAllChangeIndexes(-1, index);
2036
+ // this.deletedIndexes[index] = true;
1824
2037
  return this.items.shift();
1825
2038
  }
1826
2039
  /**
@@ -1901,10 +2114,12 @@ class ArraySchema {
1901
2114
  changeTree.shiftChangeIndexes(items.length);
1902
2115
  // new index
1903
2116
  if (changeTree.isFiltered) {
1904
- changeTree.filteredChanges.set(this.items.length, OPERATION.ADD);
2117
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2118
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1905
2119
  }
1906
2120
  else {
1907
- changeTree.allChanges.set(this.items.length, OPERATION.ADD);
2121
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2122
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1908
2123
  }
1909
2124
  // FIXME: should we use OPERATION.MOVE here instead?
1910
2125
  items.forEach((_, index) => {
@@ -2245,7 +2460,7 @@ class MapSchema {
2245
2460
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2246
2461
  const index = (isReplace)
2247
2462
  ? changeTree.indexes[key]
2248
- : changeTree.indexes[-1] ?? 0;
2463
+ : changeTree.indexes[$numFields] ?? 0;
2249
2464
  let operation = (isReplace)
2250
2465
  ? OPERATION.REPLACE
2251
2466
  : OPERATION.ADD;
@@ -2257,7 +2472,7 @@ class MapSchema {
2257
2472
  if (!isReplace) {
2258
2473
  this.$indexes.set(index, key);
2259
2474
  changeTree.indexes[key] = index;
2260
- changeTree.indexes[-1] = index + 1;
2475
+ changeTree.indexes[$numFields] = index + 1;
2261
2476
  }
2262
2477
  else if (!isRef &&
2263
2478
  this.$items.get(key) === value) {
@@ -2332,8 +2547,11 @@ class MapSchema {
2332
2547
  }
2333
2548
  [$onEncodeEnd]() {
2334
2549
  const changeTree = this[$changes];
2335
- const changes = changeTree.changes.entries();
2336
- for (const [fieldIndex, operation] of changes) {
2550
+ const keys = Object.keys(changeTree.indexedOperations);
2551
+ for (let i = 0, len = keys.length; i < len; i++) {
2552
+ const key = keys[i];
2553
+ const fieldIndex = Number(key);
2554
+ const operation = changeTree.indexedOperations[key];
2337
2555
  if (operation === OPERATION.DELETE) {
2338
2556
  const index = this[$getByIndex](fieldIndex);
2339
2557
  delete changeTree.indexes[index];
@@ -2376,102 +2594,6 @@ class MapSchema {
2376
2594
  }
2377
2595
  registerType("map", { constructor: MapSchema });
2378
2596
 
2379
- class TypeContext {
2380
- /**
2381
- * For inheritance support
2382
- * Keeps track of which classes extends which. (parent -> children)
2383
- */
2384
- static { this.inheritedTypes = new Map(); }
2385
- static register(target) {
2386
- const parent = Object.getPrototypeOf(target);
2387
- if (parent !== Schema) {
2388
- let inherits = TypeContext.inheritedTypes.get(parent);
2389
- if (!inherits) {
2390
- inherits = new Set();
2391
- TypeContext.inheritedTypes.set(parent, inherits);
2392
- }
2393
- inherits.add(target);
2394
- }
2395
- }
2396
- constructor(rootClass) {
2397
- this.types = {};
2398
- this.schemas = new Map();
2399
- this.hasFilters = false;
2400
- this.parentFiltered = {};
2401
- if (rootClass) {
2402
- this.discoverTypes(rootClass);
2403
- }
2404
- }
2405
- has(schema) {
2406
- return this.schemas.has(schema);
2407
- }
2408
- get(typeid) {
2409
- return this.types[typeid];
2410
- }
2411
- add(schema, typeid = this.schemas.size) {
2412
- // skip if already registered
2413
- if (this.schemas.has(schema)) {
2414
- return false;
2415
- }
2416
- this.types[typeid] = schema;
2417
- //
2418
- // Workaround to allow using an empty Schema (with no `@type()` fields)
2419
- //
2420
- if (schema[Symbol.metadata] === undefined) {
2421
- Metadata.init(schema);
2422
- }
2423
- this.schemas.set(schema, typeid);
2424
- return true;
2425
- }
2426
- getTypeId(klass) {
2427
- return this.schemas.get(klass);
2428
- }
2429
- discoverTypes(klass, parentIndex, parentFieldViewTag) {
2430
- if (!this.add(klass)) {
2431
- return;
2432
- }
2433
- // add classes inherited from this base class
2434
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2435
- this.discoverTypes(child, parentIndex, parentFieldViewTag);
2436
- });
2437
- const metadata = (klass[Symbol.metadata] ??= {});
2438
- // if any schema/field has filters, mark "context" as having filters.
2439
- if (metadata[-2]) {
2440
- this.hasFilters = true;
2441
- }
2442
- if (parentFieldViewTag !== undefined) {
2443
- this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
2444
- }
2445
- for (const fieldIndex in metadata) {
2446
- const index = fieldIndex;
2447
- const fieldType = metadata[index].type;
2448
- const viewTag = metadata[index].tag;
2449
- if (typeof (fieldType) === "string") {
2450
- continue;
2451
- }
2452
- if (Array.isArray(fieldType)) {
2453
- const type = fieldType[0];
2454
- // skip primitive types
2455
- if (type === "string") {
2456
- continue;
2457
- }
2458
- this.discoverTypes(type, index, viewTag);
2459
- }
2460
- else if (typeof (fieldType) === "function") {
2461
- this.discoverTypes(fieldType, viewTag);
2462
- }
2463
- else {
2464
- const type = Object.values(fieldType)[0];
2465
- // skip primitive types
2466
- if (typeof (type) === "string") {
2467
- continue;
2468
- }
2469
- this.discoverTypes(type, index, viewTag);
2470
- }
2471
- }
2472
- }
2473
- }
2474
-
2475
2597
  const DEFAULT_VIEW_TAG = -1;
2476
2598
  /**
2477
2599
  * [See documentation](https://docs.colyseus.io/state/schema/)
@@ -2499,8 +2621,8 @@ const DEFAULT_VIEW_TAG = -1;
2499
2621
  // // detect index for this field, considering inheritance
2500
2622
  // //
2501
2623
  // const parent = Object.getPrototypeOf(context.metadata);
2502
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2503
- // ?? (parent && parent[-1]) // parent structure has fields defined
2624
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2625
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2504
2626
  // ?? -1; // no fields defined
2505
2627
  // fieldIndex++;
2506
2628
  // if (
@@ -2629,8 +2751,8 @@ function view(tag = DEFAULT_VIEW_TAG) {
2629
2751
  // //
2630
2752
  // metadata[fieldIndex] = {
2631
2753
  // type: undefined,
2632
- // index: (metadata[-1] // current structure already has fields defined
2633
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2754
+ // index: (metadata[$numFields] // current structure already has fields defined
2755
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2634
2756
  // ?? -1) + 1 // no fields defined
2635
2757
  // }
2636
2758
  // }
@@ -2673,8 +2795,8 @@ function type(type, options) {
2673
2795
  //
2674
2796
  // detect index for this field, considering inheritance
2675
2797
  //
2676
- fieldIndex = metadata[-1] // current structure already has fields defined
2677
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2798
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2799
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2678
2800
  ?? -1; // no fields defined
2679
2801
  fieldIndex++;
2680
2802
  }
@@ -2701,7 +2823,7 @@ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass)
2701
2823
  return {
2702
2824
  get: function () { return this[fieldCached]; },
2703
2825
  set: function (value) {
2704
- const previousValue = this[fieldCached] || undefined;
2826
+ const previousValue = this[fieldCached] ?? undefined;
2705
2827
  // skip if value is the same as cached.
2706
2828
  if (value === previousValue) {
2707
2829
  return;
@@ -2773,8 +2895,8 @@ function deprecated(throws = true) {
2773
2895
  // //
2774
2896
  // metadata[field] = {
2775
2897
  // type: undefined,
2776
- // index: (metadata[-1] // current structure already has fields defined
2777
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2898
+ // index: (metadata[$numFields] // current structure already has fields defined
2899
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2778
2900
  // ?? -1) + 1 // no fields defined
2779
2901
  // }
2780
2902
  // }
@@ -2812,15 +2934,18 @@ function dumpChanges(schema) {
2812
2934
  ops: {},
2813
2935
  refs: []
2814
2936
  };
2815
- $root.changes.forEach((operations, changeTree) => {
2937
+ // for (const refId in $root.changes) {
2938
+ $root.changes.forEach(changeTree => {
2939
+ const changes = changeTree.indexedOperations;
2816
2940
  dump.refs.push(`refId#${changeTree.refId}`);
2817
- operations.forEach((op, index) => {
2941
+ for (const index in changes) {
2942
+ const op = changes[index];
2818
2943
  const opName = OPERATION[op];
2819
2944
  if (!dump.ops[opName]) {
2820
2945
  dump.ops[opName] = 0;
2821
2946
  }
2822
2947
  dump.ops[OPERATION[op]]++;
2823
- });
2948
+ }
2824
2949
  });
2825
2950
  return dump;
2826
2951
  }
@@ -2846,6 +2971,7 @@ var _a$2, _b$2;
2846
2971
  class Schema {
2847
2972
  static { this[_a$2] = encodeSchemaOperation; }
2848
2973
  static { this[_b$2] = decodeSchemaOperation; }
2974
+ // public [$changes]: ChangeTree;
2849
2975
  /**
2850
2976
  * Assign the property descriptors required to track changes on this instance.
2851
2977
  * @param instance
@@ -2905,12 +3031,7 @@ class Schema {
2905
3031
  // inline
2906
3032
  // Schema.initialize(this);
2907
3033
  //
2908
- Object.defineProperty(this, $changes, {
2909
- value: new ChangeTree(this),
2910
- enumerable: false,
2911
- writable: true
2912
- });
2913
- Object.defineProperties(this, this.constructor[Symbol.metadata]?.[$descriptors] || {});
3034
+ Schema.initialize(this);
2914
3035
  //
2915
3036
  // Assign initial values
2916
3037
  //
@@ -2984,7 +3105,7 @@ class Schema {
2984
3105
  const changeTree = ref[$changes];
2985
3106
  const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2986
3107
  let output = "";
2987
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
3108
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
2988
3109
  changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
2989
3110
  return output;
2990
3111
  }
@@ -3002,18 +3123,26 @@ class Schema {
3002
3123
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3003
3124
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3004
3125
  function dumpChangeSet(changeSet) {
3005
- Array.from(changeSet)
3006
- .sort((a, b) => a[0] - b[0])
3007
- .forEach(([index, operation]) => output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3126
+ changeSet.operations
3127
+ .filter(op => op)
3128
+ .forEach((index) => {
3129
+ const operation = changeTree.indexedOperations[index];
3130
+ console.log({ index, operation });
3131
+ output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3132
+ });
3008
3133
  }
3009
3134
  dumpChangeSet(changeSet);
3010
3135
  // display filtered changes
3011
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3136
+ if (!isEncodeAll &&
3137
+ changeTree.filteredChanges &&
3138
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3012
3139
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3013
3140
  dumpChangeSet(changeTree.filteredChanges);
3014
3141
  }
3015
3142
  // display filtered changes
3016
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3143
+ if (isEncodeAll &&
3144
+ changeTree.allFilteredChanges &&
3145
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3017
3146
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3018
3147
  dumpChangeSet(changeTree.allFilteredChanges);
3019
3148
  }
@@ -3022,10 +3151,12 @@ class Schema {
3022
3151
  static debugChangesDeep(ref, changeSetName = "changes") {
3023
3152
  let output = "";
3024
3153
  const rootChangeTree = ref[$changes];
3154
+ const root = rootChangeTree.root;
3025
3155
  const changeTrees = new Map();
3026
3156
  let totalInstances = 0;
3027
3157
  let totalOperations = 0;
3028
- for (const [changeTree, changes] of (rootChangeTree.root[changeSetName].entries())) {
3158
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3159
+ const changeTree = root.changeTrees[refId];
3029
3160
  let includeChangeTree = false;
3030
3161
  let parentChangeTrees = [];
3031
3162
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3044,7 +3175,7 @@ class Schema {
3044
3175
  }
3045
3176
  if (includeChangeTree) {
3046
3177
  totalInstances += 1;
3047
- totalOperations += changes.size;
3178
+ totalOperations += Object.keys(changes).length;
3048
3179
  changeTrees.set(changeTree, parentChangeTrees.reverse());
3049
3180
  }
3050
3181
  }
@@ -3062,12 +3193,13 @@ class Schema {
3062
3193
  visitedParents.add(parentChangeTree);
3063
3194
  }
3064
3195
  });
3065
- const changes = changeTree.changes;
3196
+ const changes = changeTree.indexedOperations;
3066
3197
  const level = parentChangeTrees.length;
3067
3198
  const indent = getIndent(level);
3068
3199
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3069
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3070
- for (const [index, operation] of changes) {
3200
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3201
+ for (const index in changes) {
3202
+ const operation = changes[index];
3071
3203
  output += `${getIndent(level + 1)}${OPERATION[operation]}: ${index}\n`;
3072
3204
  }
3073
3205
  }
@@ -3428,59 +3560,90 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
3428
3560
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3429
3561
  };
3430
3562
 
3563
+ function spliceOne(arr, index) {
3564
+ // manually splice an array
3565
+ if (index === -1 || index >= arr.length) {
3566
+ return false;
3567
+ }
3568
+ const len = arr.length - 1;
3569
+ for (let i = index; i < len; i++) {
3570
+ arr[i] = arr[i + 1];
3571
+ }
3572
+ arr.length = len;
3573
+ return true;
3574
+ }
3575
+
3431
3576
  class Root {
3432
3577
  constructor(types) {
3433
3578
  this.types = types;
3434
3579
  this.nextUniqueId = 0;
3435
- this.refCount = new WeakMap();
3580
+ this.refCount = {};
3581
+ this.changeTrees = {};
3436
3582
  // all changes
3437
- this.allChanges = new Map();
3438
- this.allFilteredChanges = new Map();
3583
+ this.allChanges = [];
3584
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3439
3585
  // pending changes to be encoded
3440
- this.changes = new Map();
3441
- this.filteredChanges = new Map();
3586
+ this.changes = [];
3587
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3442
3588
  }
3443
3589
  getNextUniqueId() {
3444
3590
  return this.nextUniqueId++;
3445
3591
  }
3446
3592
  add(changeTree) {
3447
- const previousRefCount = this.refCount.get(changeTree);
3593
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3594
+ changeTree.ensureRefId();
3595
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3596
+ if (isNewChangeTree) {
3597
+ this.changeTrees[changeTree.refId] = changeTree;
3598
+ }
3599
+ const previousRefCount = this.refCount[changeTree.refId];
3448
3600
  if (previousRefCount === 0) {
3449
3601
  //
3450
3602
  // When a ChangeTree is re-added, it means that it was previously removed.
3451
3603
  // We need to re-add all changes to the `changes` map.
3452
3604
  //
3453
- changeTree.allChanges.forEach((operation, index) => {
3454
- changeTree.changes.set(index, operation);
3455
- });
3605
+ const ops = changeTree.allChanges.operations;
3606
+ let len = ops.length;
3607
+ while (len--) {
3608
+ changeTree.indexedOperations[ops[len]] = OPERATION.ADD;
3609
+ setOperationAtIndex(changeTree.changes, len);
3610
+ }
3456
3611
  }
3457
- const refCount = (previousRefCount || 0) + 1;
3458
- this.refCount.set(changeTree, refCount);
3459
- return refCount;
3612
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3613
+ return isNewChangeTree;
3460
3614
  }
3461
3615
  remove(changeTree) {
3462
- const refCount = (this.refCount.get(changeTree)) - 1;
3616
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3463
3617
  if (refCount <= 0) {
3464
3618
  //
3465
3619
  // Only remove "root" reference if it's the last reference
3466
3620
  //
3467
3621
  changeTree.root = undefined;
3468
- this.allChanges.delete(changeTree);
3469
- this.changes.delete(changeTree);
3622
+ delete this.changeTrees[changeTree.refId];
3623
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3624
+ this.removeChangeFromChangeSet("changes", changeTree);
3470
3625
  if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3471
- this.allFilteredChanges.delete(changeTree);
3472
- this.filteredChanges.delete(changeTree);
3626
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3627
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3473
3628
  }
3474
- this.refCount.set(changeTree, 0);
3629
+ this.refCount[changeTree.refId] = 0;
3475
3630
  }
3476
3631
  else {
3477
- this.refCount.set(changeTree, refCount);
3632
+ this.refCount[changeTree.refId] = refCount;
3478
3633
  }
3479
3634
  changeTree.forEachChild((child, _) => this.remove(child));
3480
3635
  return refCount;
3481
3636
  }
3637
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3638
+ const changeSet = this[changeSetName];
3639
+ const index = changeSet.indexOf(changeTree);
3640
+ if (index !== -1) {
3641
+ spliceOne(changeSet, index);
3642
+ // changeSet[index] = undefined;
3643
+ }
3644
+ }
3482
3645
  clear() {
3483
- this.changes.clear();
3646
+ this.changes.length = 0;
3484
3647
  }
3485
3648
  }
3486
3649
 
@@ -3504,20 +3667,26 @@ class Encoder {
3504
3667
  this.state = state;
3505
3668
  this.state[$changes].setRoot(this.root);
3506
3669
  }
3507
- encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes, isEncodeAll = this.root.allChanges === changeTrees, initialOffset = it.offset // cache current offset in case we need to resize the buffer
3670
+ encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeSetName = "changes", isEncodeAll = changeSetName === "allChanges", initialOffset = it.offset // cache current offset in case we need to resize the buffer
3508
3671
  ) {
3509
3672
  const hasView = (view !== undefined);
3510
3673
  const rootChangeTree = this.state[$changes];
3511
- const shouldClearChanges = !isEncodeAll && !hasView;
3512
- for (const [changeTree, changes] of changeTrees.entries()) {
3674
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3675
+ const changeTrees = this.root[changeSetName];
3676
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3677
+ const changeTree = changeTrees[i];
3678
+ // // Root#removeChangeFromChangeSet() is now removing instead of setting to "undefined"
3679
+ // if (changeTree === undefined) { continue; }
3680
+ const operations = changeTree[changeSetName];
3513
3681
  const ref = changeTree.ref;
3514
3682
  const ctor = ref.constructor;
3515
3683
  const encoder = ctor[$encoder];
3516
3684
  const filter = ctor[$filter];
3685
+ const metadata = ctor[Symbol.metadata];
3517
3686
  // try { throw new Error(); } catch (e) {
3518
3687
  // // only print if not coming from Reflection.ts
3519
3688
  // if (!e.stack.includes("src/Reflection.ts")) {
3520
- // console.log("ChangeTree:", { ref: ref.constructor.name, });
3689
+ // console.log("ChangeTree:", { refId: changeTree.refId, ref: ref.constructor.name });
3521
3690
  // }
3522
3691
  // }
3523
3692
  if (hasView) {
@@ -3535,7 +3704,13 @@ class Encoder {
3535
3704
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3536
3705
  number$1(buffer, changeTree.refId, it);
3537
3706
  }
3538
- for (const [fieldIndex, operation] of changes.entries()) {
3707
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3708
+ const fieldIndex = operations.operations[j];
3709
+ const operation = (fieldIndex < 0)
3710
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3711
+ : (isEncodeAll)
3712
+ ? OPERATION.ADD
3713
+ : changeTree.indexedOperations[fieldIndex];
3539
3714
  //
3540
3715
  // first pass (encodeAll), identify "filtered" operations without encoding them
3541
3716
  // they will be encoded per client, based on their view.
@@ -3543,7 +3718,7 @@ class Encoder {
3543
3718
  // TODO: how can we optimize filtering out "encode all" operations?
3544
3719
  // TODO: avoid checking if no view tags were defined
3545
3720
  //
3546
- if (filter && !filter(ref, fieldIndex, view)) {
3721
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3547
3722
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3548
3723
  // view?.invisible.add(changeTree);
3549
3724
  continue;
@@ -3558,16 +3733,14 @@ class Encoder {
3558
3733
  // });
3559
3734
  // }
3560
3735
  // }
3561
- encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3736
+ // console.log("encode...", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, fieldIndex, operation });
3737
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3738
+ }
3739
+ if (shouldDiscardChanges) {
3740
+ changeTree.discard();
3741
+ // Not a new instance anymore
3742
+ changeTree.isNew = false;
3562
3743
  }
3563
- // if (shouldClearChanges) {
3564
- // // changeTree.endEncode();
3565
- // changeTree.changes.clear();
3566
- // // ArraySchema and MapSchema have a custom "encode end" method
3567
- // changeTree.ref[$onEncodeEnd]?.();
3568
- // // Not a new instance anymore
3569
- // delete changeTree[$isNew];
3570
- // }
3571
3744
  }
3572
3745
  if (it.offset > buffer.byteLength) {
3573
3746
  const newSize = getNextPowerOf2(buffer.byteLength * 2);
@@ -3584,51 +3757,54 @@ class Encoder {
3584
3757
  if (buffer === this.sharedBuffer) {
3585
3758
  this.sharedBuffer = buffer;
3586
3759
  }
3587
- return this.encode({ offset: initialOffset }, view, buffer, changeTrees, isEncodeAll);
3760
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3588
3761
  }
3589
3762
  else {
3590
- //
3591
- // only clear changes after making sure buffer resize is not required.
3592
- //
3593
- if (shouldClearChanges) {
3594
- //
3595
- // FIXME: avoid iterating over change trees twice.
3596
- //
3597
- this.onEndEncode(changeTrees);
3598
- }
3763
+ // //
3764
+ // // only clear changes after making sure buffer resize is not required.
3765
+ // //
3766
+ // if (shouldClearChanges) {
3767
+ // //
3768
+ // // FIXME: avoid iterating over change trees twice.
3769
+ // //
3770
+ // this.onEndEncode(changeTrees);
3771
+ // }
3599
3772
  return buffer.subarray(0, it.offset);
3600
3773
  }
3601
3774
  }
3602
3775
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3603
- // console.log(`\nencodeAll(), this.root.allChanges (${this.root.allChanges.size})`);
3776
+ // console.log(`\nencodeAll(), this.root.allChanges (${(Object.keys(this.root.allChanges).length)})`);
3604
3777
  // this.debugChanges("allChanges");
3605
- return this.encode(it, undefined, buffer, this.root.allChanges, true);
3778
+ return this.encode(it, undefined, buffer, "allChanges", true);
3606
3779
  }
3607
3780
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3608
3781
  const viewOffset = it.offset;
3609
- // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
3782
+ // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${(Object.keys(this.root.allFilteredChanges).length)})`);
3610
3783
  // this.debugChanges("allFilteredChanges");
3784
+ // console.log("\n\nENCODE ALL FOR VIEW...\n\n")
3611
3785
  // try to encode "filtered" changes
3612
- this.encode(it, view, bytes, this.root.allFilteredChanges, true, viewOffset);
3786
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3613
3787
  return Buffer.concat([
3614
3788
  bytes.subarray(0, sharedOffset),
3615
3789
  bytes.subarray(viewOffset, it.offset)
3616
3790
  ]);
3617
3791
  }
3618
3792
  debugChanges(field) {
3619
- const changeSet = (typeof (field) === "string")
3793
+ const rootChangeSet = (typeof (field) === "string")
3620
3794
  ? this.root[field]
3621
3795
  : field;
3622
- Array.from(changeSet.entries()).map((item) => {
3623
- const metadata = item[0].ref.constructor[Symbol.metadata];
3624
- console.log("->", { ref: item[0].ref.constructor.name, refId: item[0].refId, changes: item[1].size });
3625
- item[1].forEach((op, index) => {
3796
+ rootChangeSet.forEach((changeTree) => {
3797
+ const changeSet = changeTree[field];
3798
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3799
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3800
+ for (const index in changeSet) {
3801
+ const op = changeSet[index];
3626
3802
  console.log(" ->", {
3627
3803
  index,
3628
3804
  field: metadata?.[index],
3629
3805
  op: OPERATION[op],
3630
3806
  });
3631
- });
3807
+ }
3632
3808
  });
3633
3809
  }
3634
3810
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
@@ -3638,23 +3814,31 @@ class Encoder {
3638
3814
  // console.log(`\nencodeView(), this.root.filteredChanges (${this.root.filteredChanges.size})`);
3639
3815
  // this.debugChanges("filteredChanges");
3640
3816
  // encode visibility changes (add/remove for this view)
3641
- const viewChangesIterator = view.changes.entries();
3642
- for (const [changeTree, changes] of viewChangesIterator) {
3643
- if (changes.size === 0) {
3644
- // FIXME: avoid having empty changes if no changes were made
3645
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3817
+ const refIds = Object.keys(view.changes);
3818
+ // console.log("ENCODE VIEW:", refIds);
3819
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3820
+ const refId = refIds[i];
3821
+ const changes = view.changes[refId];
3822
+ const changeTree = this.root.changeTrees[refId];
3823
+ if (changeTree === undefined ||
3824
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3825
+ ) {
3826
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3646
3827
  continue;
3647
3828
  }
3648
3829
  const ref = changeTree.ref;
3649
- const ctor = ref['constructor'];
3830
+ const ctor = ref.constructor;
3650
3831
  const encoder = ctor[$encoder];
3832
+ const metadata = ctor[Symbol.metadata];
3651
3833
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3652
3834
  number$1(bytes, changeTree.refId, it);
3653
- const changesIterator = changes.entries();
3654
- for (const [fieldIndex, operation] of changesIterator) {
3835
+ const keys = Object.keys(changes);
3836
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3837
+ const key = keys[i];
3838
+ const operation = changes[key];
3655
3839
  // isEncodeAll = false
3656
3840
  // hasView = true
3657
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3841
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3658
3842
  }
3659
3843
  }
3660
3844
  //
@@ -3662,40 +3846,55 @@ class Encoder {
3662
3846
  // (to allow re-using StateView's for multiple clients)
3663
3847
  //
3664
3848
  // clear "view" changes after encoding
3665
- view.changes.clear();
3849
+ view.changes = {};
3850
+ // console.log("FILTERED CHANGES:", this.root.filteredChanges);
3666
3851
  // try to encode "filtered" changes
3667
- this.encode(it, view, bytes, this.root.filteredChanges, false, viewOffset);
3852
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3668
3853
  return Buffer.concat([
3669
3854
  bytes.subarray(0, sharedOffset),
3670
3855
  bytes.subarray(viewOffset, it.offset)
3671
3856
  ]);
3672
3857
  }
3673
3858
  onEndEncode(changeTrees = this.root.changes) {
3674
- const changeTreesIterator = changeTrees.entries();
3675
- for (const [changeTree, _] of changeTreesIterator) {
3676
- changeTree.endEncode();
3677
- // changeTree.changes.clear();
3678
- // // ArraySchema and MapSchema have a custom "encode end" method
3679
- // changeTree.ref[$onEncodeEnd]?.();
3680
- // // Not a new instance anymore
3681
- // delete changeTree[$isNew];
3682
- }
3859
+ // changeTrees.forEach(function(changeTree) {
3860
+ // changeTree.endEncode();
3861
+ // });
3862
+ // for (const refId in changeTrees) {
3863
+ // const changeTree = this.root.changeTrees[refId];
3864
+ // changeTree.endEncode();
3865
+ // // changeTree.changes.clear();
3866
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3867
+ // // changeTree.ref[$onEncodeEnd]?.();
3868
+ // // // Not a new instance anymore
3869
+ // // delete changeTree[$isNew];
3870
+ // }
3683
3871
  }
3684
3872
  discardChanges() {
3873
+ // console.log("DISCARD CHANGES!");
3685
3874
  // discard shared changes
3686
- if (this.root.changes.size > 0) {
3687
- this.onEndEncode(this.root.changes);
3688
- this.root.changes.clear();
3875
+ let length = this.root.changes.length;
3876
+ if (length > 0) {
3877
+ while (length--) {
3878
+ this.root.changes[length]?.endEncode();
3879
+ }
3880
+ this.root.changes.length = 0;
3689
3881
  }
3690
3882
  // discard filtered changes
3691
- if (this.root.filteredChanges.size > 0) {
3692
- this.onEndEncode(this.root.filteredChanges);
3693
- this.root.filteredChanges.clear();
3883
+ length = this.root.filteredChanges.length;
3884
+ if (length > 0) {
3885
+ while (length--) {
3886
+ this.root.filteredChanges[length]?.endEncode();
3887
+ }
3888
+ this.root.filteredChanges.length = 0;
3694
3889
  }
3695
3890
  }
3696
3891
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3697
3892
  const baseTypeId = this.context.getTypeId(baseType);
3698
3893
  const targetTypeId = this.context.getTypeId(targetType);
3894
+ if (targetTypeId === undefined) {
3895
+ console.warn(`@colyseus/schema WARNING: Class "${targetType.name}" is not registered on TypeRegistry - Please either tag the class with @entity or define a @type() field.`);
3896
+ return;
3897
+ }
3699
3898
  if (baseTypeId !== targetTypeId) {
3700
3899
  bytes[it.offset++] = TYPE_ID & 255;
3701
3900
  number$1(bytes, targetTypeId, it);
@@ -3703,19 +3902,6 @@ class Encoder {
3703
3902
  }
3704
3903
  }
3705
3904
 
3706
- function spliceOne(arr, index) {
3707
- // manually splice an array
3708
- if (index === -1 || index >= arr.length) {
3709
- return false;
3710
- }
3711
- const len = arr.length - 1;
3712
- for (let i = index; i < len; i++) {
3713
- arr[i] = arr[i + 1];
3714
- }
3715
- arr.length = len;
3716
- return true;
3717
- }
3718
-
3719
3905
  class DecodingWarning extends Error {
3720
3906
  constructor(message) {
3721
3907
  super(message);
@@ -3877,7 +4063,7 @@ class Decoder {
3877
4063
  }
3878
4064
  ref[$onDecodeEnd]?.();
3879
4065
  ref = nextRef;
3880
- decoder = ref['constructor'][$decoder];
4066
+ decoder = ref.constructor[$decoder];
3881
4067
  continue;
3882
4068
  }
3883
4069
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3978,10 +4164,16 @@ class Reflection extends Schema {
3978
4164
  super(...arguments);
3979
4165
  this.types = new ArraySchema();
3980
4166
  }
3981
- static encode(instance, context, it = { offset: 0 }) {
3982
- context ??= new TypeContext(instance.constructor);
4167
+ /**
4168
+ * Encodes the TypeContext of an Encoder into a buffer.
4169
+ *
4170
+ * @param context TypeContext instance
4171
+ * @param it
4172
+ * @returns
4173
+ */
4174
+ static encode(context, it = { offset: 0 }) {
3983
4175
  const reflection = new Reflection();
3984
- const encoder = new Encoder(reflection);
4176
+ const reflectionEncoder = new Encoder(reflection);
3985
4177
  const buildType = (currentType, metadata) => {
3986
4178
  for (const fieldIndex in metadata) {
3987
4179
  const index = Number(fieldIndex);
@@ -4035,9 +4227,16 @@ class Reflection extends Schema {
4035
4227
  }
4036
4228
  buildType(type, klass[Symbol.metadata]);
4037
4229
  }
4038
- const buf = encoder.encodeAll(it);
4230
+ const buf = reflectionEncoder.encodeAll(it);
4039
4231
  return Buffer.from(buf, 0, it.offset);
4040
4232
  }
4233
+ /**
4234
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4235
+ *
4236
+ * @param bytes Reflection.encode() output
4237
+ * @param it
4238
+ * @returns Decoder instance
4239
+ */
4041
4240
  static decode(bytes, it) {
4042
4241
  const reflection = new Reflection();
4043
4242
  const reflectionDecoder = new Decoder(reflection);
@@ -4083,8 +4282,8 @@ class Reflection extends Schema {
4083
4282
  }
4084
4283
  });
4085
4284
  });
4086
- // @ts-ignore
4087
- return new (typeContext.get(0))();
4285
+ const state = new (typeContext.get(0))();
4286
+ return new Decoder(state, typeContext);
4088
4287
  }
4089
4288
  }
4090
4289
  __decorate([
@@ -4354,7 +4553,7 @@ class StateView {
4354
4553
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
4355
4554
  * (This is used to force encoding a property, even if it was not changed)
4356
4555
  */
4357
- this.changes = new Map();
4556
+ this.changes = {};
4358
4557
  }
4359
4558
  // TODO: allow to set multiple tags at once
4360
4559
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
@@ -4376,10 +4575,10 @@ class StateView {
4376
4575
  // TODO: when adding an item of a MapSchema, the changes may not
4377
4576
  // be set (only the parent's changes are set)
4378
4577
  //
4379
- let changes = this.changes.get(changeTree);
4578
+ let changes = this.changes[changeTree.refId];
4380
4579
  if (changes === undefined) {
4381
- changes = new Map();
4382
- this.changes.set(changeTree, changes);
4580
+ changes = {};
4581
+ this.changes[changeTree.refId] = changes;
4383
4582
  }
4384
4583
  // set tag
4385
4584
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -4396,9 +4595,9 @@ class StateView {
4396
4595
  }
4397
4596
  tags.add(tag);
4398
4597
  // Ref: add tagged properties
4399
- metadata?.[-3]?.[tag]?.forEach((index) => {
4598
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4400
4599
  if (changeTree.getChange(index) !== OPERATION.DELETE) {
4401
- changes.set(index, OPERATION.ADD);
4600
+ changes[index] = OPERATION.ADD;
4402
4601
  }
4403
4602
  });
4404
4603
  }
@@ -4407,16 +4606,21 @@ class StateView {
4407
4606
  const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4408
4607
  ? changeTree.allFilteredChanges
4409
4608
  : changeTree.allChanges;
4410
- changeSet.forEach((op, index) => {
4609
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4610
+ const index = changeSet.operations[i];
4611
+ if (index === undefined) {
4612
+ continue;
4613
+ } // skip "undefined" indexes
4614
+ const op = changeTree.indexedOperations[index];
4411
4615
  const tagAtIndex = metadata?.[index].tag;
4412
4616
  if ((isInvisible || // if "invisible", include all
4413
4617
  tagAtIndex === undefined || // "all change" with no tag
4414
4618
  tagAtIndex === tag // tagged property
4415
4619
  ) &&
4416
4620
  op !== OPERATION.DELETE) {
4417
- changes.set(index, op);
4621
+ changes[index] = op;
4418
4622
  }
4419
- });
4623
+ }
4420
4624
  }
4421
4625
  // Add children of this ChangeTree to this view
4422
4626
  changeTree.forEachChild((change, index) => {
@@ -4442,10 +4646,10 @@ class StateView {
4442
4646
  }
4443
4647
  // add parent's tag properties
4444
4648
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
4445
- let changes = this.changes.get(changeTree);
4649
+ let changes = this.changes[changeTree.refId];
4446
4650
  if (changes === undefined) {
4447
- changes = new Map();
4448
- this.changes.set(changeTree, changes);
4651
+ changes = {};
4652
+ this.changes[changeTree.refId] = changes;
4449
4653
  }
4450
4654
  if (!this.tags) {
4451
4655
  this.tags = new WeakMap();
@@ -4459,7 +4663,7 @@ class StateView {
4459
4663
  tags = this.tags.get(changeTree);
4460
4664
  }
4461
4665
  tags.add(tag);
4462
- changes.set(parentIndex, OPERATION.ADD);
4666
+ changes[parentIndex] = OPERATION.ADD;
4463
4667
  }
4464
4668
  }
4465
4669
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4471,32 +4675,32 @@ class StateView {
4471
4675
  this.items.delete(changeTree);
4472
4676
  const ref = changeTree.ref;
4473
4677
  const metadata = ref.constructor[Symbol.metadata];
4474
- let changes = this.changes.get(changeTree);
4678
+ let changes = this.changes[changeTree.refId];
4475
4679
  if (changes === undefined) {
4476
- changes = new Map();
4477
- this.changes.set(changeTree, changes);
4680
+ changes = {};
4681
+ this.changes[changeTree.refId] = changes;
4478
4682
  }
4479
4683
  if (tag === DEFAULT_VIEW_TAG) {
4480
4684
  // parent is collection (Map/Array)
4481
4685
  const parent = changeTree.parent;
4482
4686
  if (!Metadata.isValidInstance(parent)) {
4483
4687
  const parentChangeTree = parent[$changes];
4484
- let changes = this.changes.get(parentChangeTree);
4688
+ let changes = this.changes[parentChangeTree.refId];
4485
4689
  if (changes === undefined) {
4486
- changes = new Map();
4487
- this.changes.set(parentChangeTree, changes);
4690
+ changes = {};
4691
+ this.changes[parentChangeTree.refId] = changes;
4488
4692
  }
4489
4693
  // DELETE / DELETE BY REF ID
4490
- changes.set(changeTree.parentIndex, OPERATION.DELETE);
4694
+ changes[changeTree.parentIndex] = OPERATION.DELETE;
4491
4695
  }
4492
4696
  else {
4493
4697
  // delete all "tagged" properties.
4494
- metadata[-2].forEach((index) => changes.set(index, OPERATION.DELETE));
4698
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = OPERATION.DELETE);
4495
4699
  }
4496
4700
  }
4497
4701
  else {
4498
4702
  // delete only tagged properties
4499
- metadata[-3][tag].forEach((index) => changes.set(index, OPERATION.DELETE));
4703
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = OPERATION.DELETE);
4500
4704
  }
4501
4705
  // remove tag
4502
4706
  if (this.tags && this.tags.has(changeTree)) {