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