@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
@@ -40,7 +40,6 @@
40
40
  const $filter = Symbol("$filter");
41
41
  const $getByIndex = Symbol("$getByIndex");
42
42
  const $deleteByIndex = Symbol("$deleteByIndex");
43
- const $descriptors = Symbol("$descriptors");
44
43
  /**
45
44
  * Used to hold ChangeTree instances whitin the structures
46
45
  */
@@ -50,11 +49,6 @@
50
49
  * (MapSchema, ArraySchema, etc.)
51
50
  */
52
51
  const $childType = Symbol('$childType');
53
- /**
54
- * Special ChangeTree property to identify new instances
55
- * (Once they're encoded, they're not new anymore)
56
- */
57
- const $isNew = Symbol("$isNew");
58
52
  /**
59
53
  * Optional "discard" method for custom types (ArraySchema)
60
54
  * (Discards changes for next serialization)
@@ -64,6 +58,14 @@
64
58
  * When decoding, this method is called after the instance is fully decoded
65
59
  */
66
60
  const $onDecodeEnd = Symbol("$onDecodeEnd");
61
+ /**
62
+ * Metadata
63
+ */
64
+ const $descriptors = Symbol("$descriptors");
65
+ const $numFields = "$__numFields";
66
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
67
+ const $viewFieldIndexes = "$__viewFieldIndexes";
68
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
67
69
 
68
70
  const registeredTypes = {};
69
71
  const identifiers = new Map();
@@ -75,6 +77,102 @@
75
77
  return registeredTypes[identifier];
76
78
  }
77
79
 
80
+ class TypeContext {
81
+ /**
82
+ * For inheritance support
83
+ * Keeps track of which classes extends which. (parent -> children)
84
+ */
85
+ static { this.inheritedTypes = new Map(); }
86
+ static register(target) {
87
+ const parent = Object.getPrototypeOf(target);
88
+ if (parent !== Schema) {
89
+ let inherits = TypeContext.inheritedTypes.get(parent);
90
+ if (!inherits) {
91
+ inherits = new Set();
92
+ TypeContext.inheritedTypes.set(parent, inherits);
93
+ }
94
+ inherits.add(target);
95
+ }
96
+ }
97
+ constructor(rootClass) {
98
+ this.types = {};
99
+ this.schemas = new Map();
100
+ this.hasFilters = false;
101
+ this.parentFiltered = {};
102
+ if (rootClass) {
103
+ this.discoverTypes(rootClass);
104
+ }
105
+ }
106
+ has(schema) {
107
+ return this.schemas.has(schema);
108
+ }
109
+ get(typeid) {
110
+ return this.types[typeid];
111
+ }
112
+ add(schema, typeid = this.schemas.size) {
113
+ // skip if already registered
114
+ if (this.schemas.has(schema)) {
115
+ return false;
116
+ }
117
+ this.types[typeid] = schema;
118
+ //
119
+ // Workaround to allow using an empty Schema (with no `@type()` fields)
120
+ //
121
+ if (schema[Symbol.metadata] === undefined) {
122
+ Metadata.init(schema);
123
+ }
124
+ this.schemas.set(schema, typeid);
125
+ return true;
126
+ }
127
+ getTypeId(klass) {
128
+ return this.schemas.get(klass);
129
+ }
130
+ discoverTypes(klass, parentIndex, parentFieldViewTag) {
131
+ if (!this.add(klass)) {
132
+ return;
133
+ }
134
+ // add classes inherited from this base class
135
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
136
+ this.discoverTypes(child, parentIndex, parentFieldViewTag);
137
+ });
138
+ const metadata = (klass[Symbol.metadata] ??= {});
139
+ // if any schema/field has filters, mark "context" as having filters.
140
+ if (metadata[$viewFieldIndexes]) {
141
+ this.hasFilters = true;
142
+ }
143
+ if (parentFieldViewTag !== undefined) {
144
+ this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
145
+ }
146
+ for (const fieldIndex in metadata) {
147
+ const index = fieldIndex;
148
+ const fieldType = metadata[index].type;
149
+ const viewTag = metadata[index].tag;
150
+ if (typeof (fieldType) === "string") {
151
+ continue;
152
+ }
153
+ if (Array.isArray(fieldType)) {
154
+ const type = fieldType[0];
155
+ // skip primitive types
156
+ if (type === "string") {
157
+ continue;
158
+ }
159
+ this.discoverTypes(type, index, viewTag);
160
+ }
161
+ else if (typeof (fieldType) === "function") {
162
+ this.discoverTypes(fieldType, viewTag);
163
+ }
164
+ else {
165
+ const type = Object.values(fieldType)[0];
166
+ // skip primitive types
167
+ if (typeof (type) === "string") {
168
+ continue;
169
+ }
170
+ this.discoverTypes(type, index, viewTag);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
78
176
  const Metadata = {
79
177
  addField(metadata, index, name, type, descriptor) {
80
178
  if (index > 64) {
@@ -110,7 +208,7 @@
110
208
  };
111
209
  }
112
210
  // map -1 as last field index
113
- Object.defineProperty(metadata, -1, {
211
+ Object.defineProperty(metadata, $numFields, {
114
212
  value: index,
115
213
  enumerable: false,
116
214
  configurable: true
@@ -123,14 +221,14 @@
123
221
  });
124
222
  // if child Ref/complex type, add to -4
125
223
  if (typeof (metadata[index].type) !== "string") {
126
- if (metadata[-4] === undefined) {
127
- Object.defineProperty(metadata, -4, {
224
+ if (metadata[$refTypeFieldIndexes] === undefined) {
225
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
128
226
  value: [],
129
227
  enumerable: false,
130
228
  configurable: true,
131
229
  });
132
230
  }
133
- metadata[-4].push(index);
231
+ metadata[$refTypeFieldIndexes].push(index);
134
232
  }
135
233
  },
136
234
  setTag(metadata, fieldName, tag) {
@@ -138,38 +236,63 @@
138
236
  const field = metadata[index];
139
237
  // add 'tag' to the field
140
238
  field.tag = tag;
141
- if (!metadata[-2]) {
239
+ if (!metadata[$viewFieldIndexes]) {
142
240
  // -2: all field indexes with "view" tag
143
- Object.defineProperty(metadata, -2, {
241
+ Object.defineProperty(metadata, $viewFieldIndexes, {
144
242
  value: [],
145
243
  enumerable: false,
146
244
  configurable: true
147
245
  });
148
246
  // -3: field indexes by "view" tag
149
- Object.defineProperty(metadata, -3, {
247
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
150
248
  value: {},
151
249
  enumerable: false,
152
250
  configurable: true
153
251
  });
154
252
  }
155
- metadata[-2].push(index);
156
- if (!metadata[-3][tag]) {
157
- metadata[-3][tag] = [];
253
+ metadata[$viewFieldIndexes].push(index);
254
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
255
+ metadata[$fieldIndexesByViewTag][tag] = [];
158
256
  }
159
- metadata[-3][tag].push(index);
257
+ metadata[$fieldIndexesByViewTag][tag].push(index);
160
258
  },
161
259
  setFields(target, fields) {
162
- const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
163
- let index = 0;
260
+ // for inheritance support
261
+ const constructor = target.prototype.constructor;
262
+ TypeContext.register(constructor);
263
+ const parentClass = Object.getPrototypeOf(constructor);
264
+ const parentMetadata = parentClass && parentClass[Symbol.metadata];
265
+ const metadata = Metadata.initialize(constructor, parentMetadata);
266
+ // Use Schema's methods if not defined in the class
267
+ if (!constructor[$track]) {
268
+ constructor[$track] = Schema[$track];
269
+ }
270
+ if (!constructor[$encoder]) {
271
+ constructor[$encoder] = Schema[$encoder];
272
+ }
273
+ if (!constructor[$decoder]) {
274
+ constructor[$decoder] = Schema[$decoder];
275
+ }
276
+ if (!constructor.prototype.toJSON) {
277
+ constructor.prototype.toJSON = Schema.prototype.toJSON;
278
+ }
279
+ //
280
+ // detect index for this field, considering inheritance
281
+ //
282
+ let fieldIndex = metadata[$numFields] // current structure already has fields defined
283
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
284
+ ?? -1; // no fields defined
285
+ fieldIndex++;
164
286
  for (const field in fields) {
165
287
  const type = fields[field];
166
288
  // FIXME: this code is duplicated from @type() annotation
167
289
  const complexTypeKlass = (Array.isArray(type))
168
290
  ? getType("array")
169
291
  : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
170
- Metadata.addField(metadata, index, field, type, getPropertyDescriptor(`_${field}`, index, type, complexTypeKlass));
171
- index++;
292
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, type, complexTypeKlass));
293
+ fieldIndex++;
172
294
  }
295
+ return target;
173
296
  },
174
297
  isDeprecated(metadata, field) {
175
298
  return metadata[field].deprecated === true;
@@ -181,7 +304,7 @@
181
304
  //
182
305
  const metadata = {};
183
306
  klass[Symbol.metadata] = metadata;
184
- Object.defineProperty(metadata, -1, {
307
+ Object.defineProperty(metadata, $numFields, {
185
308
  value: 0,
186
309
  enumerable: false,
187
310
  configurable: true,
@@ -195,7 +318,7 @@
195
318
  if (parentMetadata) {
196
319
  // assign parent metadata to current
197
320
  Object.assign(metadata, parentMetadata);
198
- for (let i = 0; i <= parentMetadata[-1]; i++) {
321
+ for (let i = 0; i <= parentMetadata[$numFields]; i++) {
199
322
  const fieldName = parentMetadata[i].name;
200
323
  Object.defineProperty(metadata, fieldName, {
201
324
  value: parentMetadata[fieldName],
@@ -203,8 +326,8 @@
203
326
  configurable: true,
204
327
  });
205
328
  }
206
- Object.defineProperty(metadata, -1, {
207
- value: parentMetadata[-1],
329
+ Object.defineProperty(metadata, $numFields, {
330
+ value: parentMetadata[$numFields],
208
331
  enumerable: false,
209
332
  configurable: true,
210
333
  writable: true,
@@ -216,40 +339,69 @@
216
339
  },
217
340
  isValidInstance(klass) {
218
341
  return (klass.constructor[Symbol.metadata] &&
219
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
342
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
220
343
  },
221
344
  getFields(klass) {
222
345
  const metadata = klass[Symbol.metadata];
223
346
  const fields = {};
224
- for (let i = 0; i <= metadata[-1]; i++) {
347
+ for (let i = 0; i <= metadata[$numFields]; i++) {
225
348
  fields[metadata[i].name] = metadata[i].type;
226
349
  }
227
350
  return fields;
228
351
  }
229
352
  };
230
353
 
231
- var _a$5;
354
+ function setOperationAtIndex(changeSet, index) {
355
+ const operationsIndex = changeSet.indexes[index];
356
+ if (operationsIndex === undefined) {
357
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
358
+ }
359
+ else {
360
+ changeSet.operations[operationsIndex] = index;
361
+ }
362
+ }
363
+ function deleteOperationAtIndex(changeSet, index) {
364
+ const operationsIndex = changeSet.indexes[index];
365
+ if (operationsIndex !== undefined) {
366
+ changeSet.operations[operationsIndex] = undefined;
367
+ }
368
+ delete changeSet.indexes[index];
369
+ }
370
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
371
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
372
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
373
+ }
374
+ }
232
375
  class ChangeTree {
233
- static { _a$5 = $isNew; }
234
376
  constructor(ref) {
235
377
  this.isFiltered = false;
236
378
  this.isPartiallyFiltered = false;
237
- this.currentOperationIndex = 0;
238
- this.changes = new Map();
239
- this.allChanges = new Map();
240
- this[_a$5] = true;
379
+ this.indexedOperations = {};
380
+ //
381
+ // TODO:
382
+ // try storing the index + operation per item.
383
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
384
+ //
385
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
386
+ //
387
+ this.changes = { indexes: {}, operations: [] };
388
+ this.allChanges = { indexes: {}, operations: [] };
389
+ /**
390
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
391
+ */
392
+ this.isNew = true;
241
393
  this.ref = ref;
242
394
  //
243
395
  // Does this structure have "filters" declared?
244
396
  //
245
- if (ref.constructor[Symbol.metadata]?.[-2]) {
246
- this.allFilteredChanges = new Map();
247
- this.filteredChanges = new Map();
397
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
398
+ this.allFilteredChanges = { indexes: {}, operations: [] };
399
+ this.filteredChanges = { indexes: {}, operations: [] };
248
400
  }
249
401
  }
250
402
  setRoot(root) {
251
403
  this.root = root;
252
- this.root.add(this);
404
+ const isNewChangeTree = this.root.add(this);
253
405
  const metadata = this.ref.constructor[Symbol.metadata];
254
406
  if (this.root.types.hasFilters) {
255
407
  //
@@ -260,22 +412,24 @@
260
412
  //
261
413
  this.checkIsFiltered(metadata, this.parent, this.parentIndex);
262
414
  if (this.isFiltered || this.isPartiallyFiltered) {
263
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
264
- this.root.filteredChanges.set(this, this.filteredChanges);
415
+ enqueueChangeTree(root, this, 'filteredChanges');
416
+ if (isNewChangeTree) {
417
+ this.root.allFilteredChanges.push(this);
418
+ }
265
419
  }
266
420
  }
267
421
  if (!this.isFiltered) {
268
- this.root.changes.set(this, this.changes);
269
- this.root.allChanges.set(this, this.allChanges);
422
+ enqueueChangeTree(root, this, 'changes');
423
+ if (isNewChangeTree) {
424
+ this.root.allChanges.push(this);
425
+ }
270
426
  }
271
- this.ensureRefId();
427
+ // Recursively set root on child structures
272
428
  if (metadata) {
273
- metadata[-4]?.forEach((index) => {
429
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
274
430
  const field = metadata[index];
275
431
  const value = this.ref[field.name];
276
- if (value) {
277
- value[$changes].setRoot(root);
278
- }
432
+ value?.[$changes].setRoot(root);
279
433
  });
280
434
  }
281
435
  else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
@@ -292,31 +446,36 @@
292
446
  if (!root) {
293
447
  return;
294
448
  }
295
- root.add(this);
296
449
  const metadata = this.ref.constructor[Symbol.metadata];
297
450
  // skip if parent is already set
298
451
  if (root !== this.root) {
299
452
  this.root = root;
453
+ const isNewChangeTree = root.add(this);
300
454
  if (root.types.hasFilters) {
301
455
  this.checkIsFiltered(metadata, parent, parentIndex);
302
456
  if (this.isFiltered || this.isPartiallyFiltered) {
303
- this.root.filteredChanges.set(this, this.filteredChanges);
304
- this.root.allFilteredChanges.set(this, this.filteredChanges);
457
+ enqueueChangeTree(root, this, 'filteredChanges');
458
+ if (isNewChangeTree) {
459
+ this.root.allFilteredChanges.push(this);
460
+ }
305
461
  }
306
462
  }
307
463
  if (!this.isFiltered) {
308
- this.root.changes.set(this, this.changes);
309
- this.root.allChanges.set(this, this.allChanges);
464
+ enqueueChangeTree(root, this, 'changes');
465
+ if (isNewChangeTree) {
466
+ this.root.allChanges.push(this);
467
+ }
310
468
  }
311
- this.ensureRefId();
469
+ }
470
+ else {
471
+ root.add(this);
312
472
  }
313
473
  // assign same parent on child structures
314
474
  if (metadata) {
315
- metadata[-4]?.forEach((index) => {
475
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
316
476
  const field = metadata[index];
317
477
  const value = this.ref[field.name];
318
478
  value?.[$changes].setParent(this.ref, root, index);
319
- // console.log(this.ref.constructor.name, field.name, value);
320
479
  // try { throw new Error(); } catch (e) {
321
480
  // console.log(e.stack);
322
481
  // }
@@ -335,7 +494,7 @@
335
494
  //
336
495
  const metadata = this.ref.constructor[Symbol.metadata];
337
496
  if (metadata) {
338
- metadata[-4]?.forEach((index) => {
497
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
339
498
  const field = metadata[index];
340
499
  const value = this.ref[field.name];
341
500
  if (value) {
@@ -351,8 +510,10 @@
351
510
  }
352
511
  }
353
512
  operation(op) {
354
- this.changes.set(--this.currentOperationIndex, op);
355
- this.root?.changes.set(this, this.changes);
513
+ // operations without index use negative values to represent them
514
+ // this is checked during .encode() time.
515
+ this.changes.operations.push(-op);
516
+ enqueueChangeTree(this.root, this, 'changes');
356
517
  }
357
518
  change(index, operation = exports.OPERATION.ADD) {
358
519
  const metadata = this.ref.constructor[Symbol.metadata];
@@ -360,7 +521,7 @@
360
521
  const changeSet = (isFiltered)
361
522
  ? this.filteredChanges
362
523
  : this.changes;
363
- const previousOperation = changeSet.get(index);
524
+ const previousOperation = this.indexedOperations[index];
364
525
  if (!previousOperation || previousOperation === exports.OPERATION.DELETE) {
365
526
  const op = (!previousOperation)
366
527
  ? operation
@@ -370,16 +531,19 @@
370
531
  //
371
532
  // TODO: are DELETE operations being encoded as ADD here ??
372
533
  //
373
- changeSet.set(index, op);
534
+ this.indexedOperations[index] = op;
374
535
  }
536
+ setOperationAtIndex(changeSet, index);
375
537
  if (isFiltered) {
376
- this.allFilteredChanges.set(index, exports.OPERATION.ADD);
377
- this.root?.filteredChanges.set(this, this.filteredChanges);
378
- this.root?.allFilteredChanges.set(this, this.allFilteredChanges);
538
+ setOperationAtIndex(this.allFilteredChanges, index);
539
+ if (this.root) {
540
+ enqueueChangeTree(this.root, this, 'filteredChanges');
541
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
542
+ }
379
543
  }
380
544
  else {
381
- this.allChanges.set(index, exports.OPERATION.ADD);
382
- this.root?.changes.set(this, this.changes);
545
+ setOperationAtIndex(this.allChanges, index);
546
+ enqueueChangeTree(this.root, this, 'changes');
383
547
  }
384
548
  }
385
549
  shiftChangeIndexes(shiftIndex) {
@@ -391,12 +555,15 @@
391
555
  const changeSet = (this.isFiltered)
392
556
  ? this.filteredChanges
393
557
  : this.changes;
394
- const changeSetEntries = Array.from(changeSet.entries());
395
- changeSet.clear();
396
- // Re-insert each entry with the shifted index
397
- for (const [index, op] of changeSetEntries) {
398
- changeSet.set(index + shiftIndex, op);
558
+ const newIndexedOperations = {};
559
+ const newIndexes = {};
560
+ for (const index in this.indexedOperations) {
561
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
562
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
399
563
  }
564
+ this.indexedOperations = newIndexedOperations;
565
+ changeSet.indexes = newIndexes;
566
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
400
567
  }
401
568
  shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
402
569
  //
@@ -412,24 +579,36 @@
412
579
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
413
580
  }
414
581
  }
415
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
416
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
417
- if (index >= startIndex) {
418
- allChangeSet.delete(index);
419
- allChangeSet.set(index + shiftIndex, op);
582
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
583
+ const newIndexes = {};
584
+ for (const key in changeSet.indexes) {
585
+ const index = changeSet.indexes[key];
586
+ if (index > startIndex) {
587
+ newIndexes[Number(key) + shiftIndex] = index;
420
588
  }
421
- });
589
+ else {
590
+ newIndexes[key] = index;
591
+ }
592
+ }
593
+ changeSet.indexes = newIndexes;
594
+ for (let i = 0; i < changeSet.operations.length; i++) {
595
+ const index = changeSet.operations[i];
596
+ if (index > startIndex) {
597
+ changeSet.operations[i] = index + shiftIndex;
598
+ }
599
+ }
422
600
  }
423
601
  indexedOperation(index, operation, allChangesIndex = index) {
424
- if (this.filteredChanges !== undefined) {
425
- this.allFilteredChanges.set(allChangesIndex, exports.OPERATION.ADD);
426
- this.filteredChanges.set(index, operation);
427
- this.root?.filteredChanges.set(this, this.filteredChanges);
602
+ this.indexedOperations[index] = operation;
603
+ if (this.filteredChanges) {
604
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
605
+ setOperationAtIndex(this.filteredChanges, index);
606
+ enqueueChangeTree(this.root, this, 'filteredChanges');
428
607
  }
429
608
  else {
430
- this.allChanges.set(allChangesIndex, exports.OPERATION.ADD);
431
- this.changes.set(index, operation);
432
- this.root?.changes.set(this, this.changes);
609
+ setOperationAtIndex(this.allChanges, allChangesIndex);
610
+ setOperationAtIndex(this.changes, index);
611
+ enqueueChangeTree(this.root, this, 'changes');
433
612
  }
434
613
  }
435
614
  getType(index) {
@@ -448,8 +627,7 @@
448
627
  }
449
628
  }
450
629
  getChange(index) {
451
- // TODO: optimize this. avoid checking against multiple instances
452
- return this.changes.get(index) ?? this.filteredChanges?.get(index);
630
+ return this.indexedOperations[index];
453
631
  }
454
632
  //
455
633
  // used during `.encode()`
@@ -473,8 +651,9 @@
473
651
  const changeSet = (this.filteredChanges)
474
652
  ? this.filteredChanges
475
653
  : this.changes;
654
+ this.indexedOperations[index] = operation ?? exports.OPERATION.DELETE;
655
+ setOperationAtIndex(changeSet, index);
476
656
  const previousValue = this.getValue(index);
477
- changeSet.set(index, operation ?? exports.OPERATION.DELETE);
478
657
  // remove `root` reference
479
658
  if (previousValue && previousValue[$changes]) {
480
659
  //
@@ -490,23 +669,26 @@
490
669
  this.root?.remove(previousValue[$changes]);
491
670
  }
492
671
  //
493
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
672
+ // FIXME: this is looking a ugly and repeated
494
673
  //
495
674
  if (this.filteredChanges) {
496
- this.root?.filteredChanges.set(this, this.filteredChanges);
497
- this.allFilteredChanges.delete(allChangesIndex);
675
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
676
+ enqueueChangeTree(this.root, this, 'filteredChanges');
498
677
  }
499
678
  else {
500
- this.root?.changes.set(this, this.changes);
501
- this.allChanges.delete(allChangesIndex);
679
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
680
+ enqueueChangeTree(this.root, this, 'changes');
502
681
  }
503
682
  }
504
683
  endEncode() {
505
- this.changes.clear();
684
+ this.indexedOperations = {};
685
+ // // clear changes
686
+ // this.changes.indexes = {};
687
+ // this.changes.operations.length = 0;
506
688
  // ArraySchema and MapSchema have a custom "encode end" method
507
689
  this.ref[$onEncodeEnd]?.();
508
690
  // Not a new instance anymore
509
- delete this[$isNew];
691
+ this.isNew = false;
510
692
  }
511
693
  discard(discardAll = false) {
512
694
  //
@@ -515,13 +697,22 @@
515
697
  // REPLACE in case same key is used on next patches.
516
698
  //
517
699
  this.ref[$onEncodeEnd]?.();
518
- this.changes.clear();
519
- this.filteredChanges?.clear();
520
- // reset operation index
521
- this.currentOperationIndex = 0;
700
+ this.indexedOperations = {};
701
+ this.changes.indexes = {};
702
+ this.changes.operations.length = 0;
703
+ this.changes.queueRootIndex = undefined;
704
+ if (this.filteredChanges !== undefined) {
705
+ this.filteredChanges.indexes = {};
706
+ this.filteredChanges.operations.length = 0;
707
+ this.filteredChanges.queueRootIndex = undefined;
708
+ }
522
709
  if (discardAll) {
523
- this.allChanges.clear();
524
- this.allFilteredChanges?.clear();
710
+ this.allChanges.indexes = {};
711
+ this.allChanges.operations.length = 0;
712
+ if (this.allFilteredChanges !== undefined) {
713
+ this.allFilteredChanges.indexes = {};
714
+ this.allFilteredChanges.operations.length = 0;
715
+ }
525
716
  // remove children references
526
717
  this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
527
718
  }
@@ -530,12 +721,13 @@
530
721
  * Recursively discard all changes from this, and child structures.
531
722
  */
532
723
  discardAll() {
533
- this.changes.forEach((_, fieldIndex) => {
534
- const value = this.getValue(fieldIndex);
724
+ const keys = Object.keys(this.indexedOperations);
725
+ for (let i = 0, len = keys.length; i < len; i++) {
726
+ const value = this.getValue(Number(keys[i]));
535
727
  if (value && value[$changes]) {
536
728
  value[$changes].discardAll();
537
729
  }
538
- });
730
+ }
539
731
  this.discard();
540
732
  }
541
733
  ensureRefId() {
@@ -546,42 +738,48 @@
546
738
  this.refId = this.root.getNextUniqueId();
547
739
  }
548
740
  get changed() {
549
- return this.changes.size > 0;
741
+ return (Object.entries(this.indexedOperations).length > 0);
550
742
  }
551
743
  checkIsFiltered(metadata, parent, parentIndex) {
552
744
  // Detect if current structure has "filters" declared
553
- this.isPartiallyFiltered = metadata?.[-2] !== undefined;
745
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
554
746
  if (this.isPartiallyFiltered) {
555
- this.filteredChanges = this.filteredChanges || new Map();
556
- this.allFilteredChanges = this.allFilteredChanges || new Map();
747
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
748
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
557
749
  }
558
- if (parent) {
559
- if (!Metadata.isValidInstance(parent)) {
560
- const parentChangeTree = parent[$changes];
561
- parent = parentChangeTree.parent;
562
- parentIndex = parentChangeTree.parentIndex;
563
- }
564
- const parentMetadata = parent?.constructor?.[Symbol.metadata];
565
- this.isFiltered = (parent && parentMetadata?.[-2]?.includes(parentIndex));
566
- //
567
- // TODO: refactor this!
568
- //
569
- // swapping `changes` and `filteredChanges` is required here
570
- // because "isFiltered" may not be imedialely available on `change()`
571
- //
572
- if (this.isFiltered) {
573
- this.filteredChanges = new Map();
574
- this.allFilteredChanges = new Map();
575
- if (this.changes.size > 0) {
576
- // swap changes reference
577
- const changes = this.changes;
578
- this.changes = this.filteredChanges;
579
- this.filteredChanges = changes;
580
- // swap "all changes" reference
581
- const allFilteredChanges = this.allFilteredChanges;
582
- this.allFilteredChanges = this.allChanges;
583
- this.allChanges = allFilteredChanges;
584
- }
750
+ // skip if parent is not set
751
+ if (!parent) {
752
+ return;
753
+ }
754
+ if (!Metadata.isValidInstance(parent)) {
755
+ const parentChangeTree = parent[$changes];
756
+ parent = parentChangeTree.parent;
757
+ parentIndex = parentChangeTree.parentIndex;
758
+ }
759
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
760
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
761
+ //
762
+ // TODO: refactor this!
763
+ //
764
+ // swapping `changes` and `filteredChanges` is required here
765
+ // because "isFiltered" may not be imedialely available on `change()`
766
+ //
767
+ if (this.isFiltered) {
768
+ this.filteredChanges = { indexes: {}, operations: [] };
769
+ this.allFilteredChanges = { indexes: {}, operations: [] };
770
+ if (this.changes.operations.length > 0) {
771
+ // swap changes reference
772
+ const changes = this.changes;
773
+ this.changes = this.filteredChanges;
774
+ this.filteredChanges = changes;
775
+ // swap "all changes" reference
776
+ const allFilteredChanges = this.allFilteredChanges;
777
+ this.allFilteredChanges = this.allChanges;
778
+ this.allChanges = allFilteredChanges;
779
+ // console.log("SWAP =>", {
780
+ // "this.allFilteredChanges": this.allFilteredChanges,
781
+ // "this.allChanges": this.allChanges
782
+ // })
585
783
  }
586
784
  }
587
785
  }
@@ -650,21 +848,24 @@
650
848
  view[it.offset++] = c;
651
849
  }
652
850
  else if (c < 0x800) {
653
- view[it.offset++] = 0xc0 | (c >> 6);
654
- view[it.offset++] = 0x80 | (c & 0x3f);
851
+ view[it.offset] = 0xc0 | (c >> 6);
852
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
853
+ it.offset += 2;
655
854
  }
656
855
  else if (c < 0xd800 || c >= 0xe000) {
657
- view[it.offset++] = 0xe0 | (c >> 12);
658
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
659
- view[it.offset++] = 0x80 | (c & 0x3f);
856
+ view[it.offset] = 0xe0 | (c >> 12);
857
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
858
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
859
+ it.offset += 3;
660
860
  }
661
861
  else {
662
862
  i++;
663
863
  c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
664
- view[it.offset++] = 0xf0 | (c >> 18);
665
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
666
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
667
- view[it.offset++] = 0x80 | (c & 0x3f);
864
+ view[it.offset] = 0xf0 | (c >> 18);
865
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
866
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
867
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
868
+ it.offset += 4;
668
869
  }
669
870
  }
670
871
  }
@@ -893,7 +1094,7 @@
893
1094
  * Used for Schema instances.
894
1095
  * @private
895
1096
  */
896
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
1097
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
897
1098
  // "compress" field index + operation
898
1099
  bytes[it.offset++] = (index | operation) & 255;
899
1100
  // Do not encode value for DELETE operations
@@ -901,7 +1102,7 @@
901
1102
  return;
902
1103
  }
903
1104
  const ref = changeTree.ref;
904
- const metadata = ref.constructor[Symbol.metadata];
1105
+ // const metadata: Metadata = ref.constructor[Symbol.metadata];
905
1106
  const field = metadata[index];
906
1107
  // TODO: inline this function call small performance gain
907
1108
  encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
@@ -1596,6 +1797,7 @@
1596
1797
  const proxy = new Proxy(this, {
1597
1798
  get: (obj, prop) => {
1598
1799
  if (typeof (prop) !== "symbol" &&
1800
+ // FIXME: d8 accuses this as low performance
1599
1801
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1600
1802
  ) {
1601
1803
  return this.items[prop];
@@ -1613,7 +1815,7 @@
1613
1815
  if (setValue[$changes]) {
1614
1816
  assertInstanceType(setValue, obj[$childType], obj, key);
1615
1817
  if (obj.items[key] !== undefined) {
1616
- if (setValue[$changes][$isNew]) {
1818
+ if (setValue[$changes].isNew) {
1617
1819
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1618
1820
  }
1619
1821
  else {
@@ -1625,7 +1827,7 @@
1625
1827
  }
1626
1828
  }
1627
1829
  }
1628
- else if (setValue[$changes][$isNew]) {
1830
+ else if (setValue[$changes].isNew) {
1629
1831
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1630
1832
  }
1631
1833
  }
@@ -1659,7 +1861,9 @@
1659
1861
  });
1660
1862
  this[$changes] = new ChangeTree(proxy);
1661
1863
  this[$changes].indexes = {};
1662
- this.push.apply(this, items);
1864
+ if (items.length > 0) {
1865
+ this.push(...items);
1866
+ }
1663
1867
  return proxy;
1664
1868
  }
1665
1869
  set length(newLength) {
@@ -1678,15 +1882,18 @@
1678
1882
  }
1679
1883
  push(...values) {
1680
1884
  let length = this.tmpItems.length;
1681
- values.forEach((value, i) => {
1682
- // skip null values
1885
+ const changeTree = this[$changes];
1886
+ // values.forEach((value, i) => {
1887
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1888
+ const value = values[i];
1683
1889
  if (value === undefined || value === null) {
1890
+ // skip null values
1684
1891
  return;
1685
1892
  }
1686
1893
  else if (typeof (value) === "object" && this[$childType]) {
1687
1894
  assertInstanceType(value, this[$childType], this, i);
1895
+ // TODO: move value[$changes]?.setParent() to this block.
1688
1896
  }
1689
- const changeTree = this[$changes];
1690
1897
  changeTree.indexedOperation(length, exports.OPERATION.ADD, this.items.length);
1691
1898
  this.items.push(value);
1692
1899
  this.tmpItems.push(value);
@@ -1695,8 +1902,9 @@
1695
1902
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1696
1903
  //
1697
1904
  value[$changes]?.setParent(this, changeTree.root, length);
1698
- length++;
1699
- });
1905
+ }
1906
+ // length++;
1907
+ // });
1700
1908
  return length;
1701
1909
  }
1702
1910
  /**
@@ -1717,6 +1925,7 @@
1717
1925
  }
1718
1926
  this[$changes].delete(index, undefined, this.items.length - 1);
1719
1927
  // this.tmpItems[index] = undefined;
1928
+ // this.tmpItems.pop();
1720
1929
  this.deletedIndexes[index] = true;
1721
1930
  return this.items.pop();
1722
1931
  }
@@ -1781,9 +1990,12 @@
1781
1990
  //
1782
1991
  // TODO: do not use [$changes] at decoding time.
1783
1992
  //
1784
- changeTree.root?.changes.delete(changeTree);
1785
- changeTree.root?.allChanges.delete(changeTree);
1786
- changeTree.root?.allFilteredChanges.delete(changeTree);
1993
+ const root = changeTree.root;
1994
+ if (root !== undefined) {
1995
+ root.removeChangeFromChangeSet("changes", changeTree);
1996
+ root.removeChangeFromChangeSet("allChanges", changeTree);
1997
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
1998
+ }
1787
1999
  });
1788
2000
  changeTree.discard(true);
1789
2001
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -1827,6 +2039,7 @@
1827
2039
  const changeTree = this[$changes];
1828
2040
  changeTree.delete(index);
1829
2041
  changeTree.shiftAllChangeIndexes(-1, index);
2042
+ // this.deletedIndexes[index] = true;
1830
2043
  return this.items.shift();
1831
2044
  }
1832
2045
  /**
@@ -1907,10 +2120,12 @@
1907
2120
  changeTree.shiftChangeIndexes(items.length);
1908
2121
  // new index
1909
2122
  if (changeTree.isFiltered) {
1910
- changeTree.filteredChanges.set(this.items.length, exports.OPERATION.ADD);
2123
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2124
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1911
2125
  }
1912
2126
  else {
1913
- changeTree.allChanges.set(this.items.length, exports.OPERATION.ADD);
2127
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2128
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1914
2129
  }
1915
2130
  // FIXME: should we use OPERATION.MOVE here instead?
1916
2131
  items.forEach((_, index) => {
@@ -2251,7 +2466,7 @@
2251
2466
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2252
2467
  const index = (isReplace)
2253
2468
  ? changeTree.indexes[key]
2254
- : changeTree.indexes[-1] ?? 0;
2469
+ : changeTree.indexes[$numFields] ?? 0;
2255
2470
  let operation = (isReplace)
2256
2471
  ? exports.OPERATION.REPLACE
2257
2472
  : exports.OPERATION.ADD;
@@ -2263,7 +2478,7 @@
2263
2478
  if (!isReplace) {
2264
2479
  this.$indexes.set(index, key);
2265
2480
  changeTree.indexes[key] = index;
2266
- changeTree.indexes[-1] = index + 1;
2481
+ changeTree.indexes[$numFields] = index + 1;
2267
2482
  }
2268
2483
  else if (!isRef &&
2269
2484
  this.$items.get(key) === value) {
@@ -2338,8 +2553,11 @@
2338
2553
  }
2339
2554
  [$onEncodeEnd]() {
2340
2555
  const changeTree = this[$changes];
2341
- const changes = changeTree.changes.entries();
2342
- for (const [fieldIndex, operation] of changes) {
2556
+ const keys = Object.keys(changeTree.indexedOperations);
2557
+ for (let i = 0, len = keys.length; i < len; i++) {
2558
+ const key = keys[i];
2559
+ const fieldIndex = Number(key);
2560
+ const operation = changeTree.indexedOperations[key];
2343
2561
  if (operation === exports.OPERATION.DELETE) {
2344
2562
  const index = this[$getByIndex](fieldIndex);
2345
2563
  delete changeTree.indexes[index];
@@ -2382,102 +2600,6 @@
2382
2600
  }
2383
2601
  registerType("map", { constructor: MapSchema });
2384
2602
 
2385
- class TypeContext {
2386
- /**
2387
- * For inheritance support
2388
- * Keeps track of which classes extends which. (parent -> children)
2389
- */
2390
- static { this.inheritedTypes = new Map(); }
2391
- static register(target) {
2392
- const parent = Object.getPrototypeOf(target);
2393
- if (parent !== Schema) {
2394
- let inherits = TypeContext.inheritedTypes.get(parent);
2395
- if (!inherits) {
2396
- inherits = new Set();
2397
- TypeContext.inheritedTypes.set(parent, inherits);
2398
- }
2399
- inherits.add(target);
2400
- }
2401
- }
2402
- constructor(rootClass) {
2403
- this.types = {};
2404
- this.schemas = new Map();
2405
- this.hasFilters = false;
2406
- this.parentFiltered = {};
2407
- if (rootClass) {
2408
- this.discoverTypes(rootClass);
2409
- }
2410
- }
2411
- has(schema) {
2412
- return this.schemas.has(schema);
2413
- }
2414
- get(typeid) {
2415
- return this.types[typeid];
2416
- }
2417
- add(schema, typeid = this.schemas.size) {
2418
- // skip if already registered
2419
- if (this.schemas.has(schema)) {
2420
- return false;
2421
- }
2422
- this.types[typeid] = schema;
2423
- //
2424
- // Workaround to allow using an empty Schema (with no `@type()` fields)
2425
- //
2426
- if (schema[Symbol.metadata] === undefined) {
2427
- Metadata.init(schema);
2428
- }
2429
- this.schemas.set(schema, typeid);
2430
- return true;
2431
- }
2432
- getTypeId(klass) {
2433
- return this.schemas.get(klass);
2434
- }
2435
- discoverTypes(klass, parentIndex, parentFieldViewTag) {
2436
- if (!this.add(klass)) {
2437
- return;
2438
- }
2439
- // add classes inherited from this base class
2440
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2441
- this.discoverTypes(child, parentIndex, parentFieldViewTag);
2442
- });
2443
- const metadata = (klass[Symbol.metadata] ??= {});
2444
- // if any schema/field has filters, mark "context" as having filters.
2445
- if (metadata[-2]) {
2446
- this.hasFilters = true;
2447
- }
2448
- if (parentFieldViewTag !== undefined) {
2449
- this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
2450
- }
2451
- for (const fieldIndex in metadata) {
2452
- const index = fieldIndex;
2453
- const fieldType = metadata[index].type;
2454
- const viewTag = metadata[index].tag;
2455
- if (typeof (fieldType) === "string") {
2456
- continue;
2457
- }
2458
- if (Array.isArray(fieldType)) {
2459
- const type = fieldType[0];
2460
- // skip primitive types
2461
- if (type === "string") {
2462
- continue;
2463
- }
2464
- this.discoverTypes(type, index, viewTag);
2465
- }
2466
- else if (typeof (fieldType) === "function") {
2467
- this.discoverTypes(fieldType, viewTag);
2468
- }
2469
- else {
2470
- const type = Object.values(fieldType)[0];
2471
- // skip primitive types
2472
- if (typeof (type) === "string") {
2473
- continue;
2474
- }
2475
- this.discoverTypes(type, index, viewTag);
2476
- }
2477
- }
2478
- }
2479
- }
2480
-
2481
2603
  const DEFAULT_VIEW_TAG = -1;
2482
2604
  /**
2483
2605
  * [See documentation](https://docs.colyseus.io/state/schema/)
@@ -2505,8 +2627,8 @@
2505
2627
  // // detect index for this field, considering inheritance
2506
2628
  // //
2507
2629
  // const parent = Object.getPrototypeOf(context.metadata);
2508
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2509
- // ?? (parent && parent[-1]) // parent structure has fields defined
2630
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2631
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2510
2632
  // ?? -1; // no fields defined
2511
2633
  // fieldIndex++;
2512
2634
  // if (
@@ -2635,8 +2757,8 @@
2635
2757
  // //
2636
2758
  // metadata[fieldIndex] = {
2637
2759
  // type: undefined,
2638
- // index: (metadata[-1] // current structure already has fields defined
2639
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2760
+ // index: (metadata[$numFields] // current structure already has fields defined
2761
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2640
2762
  // ?? -1) + 1 // no fields defined
2641
2763
  // }
2642
2764
  // }
@@ -2679,8 +2801,8 @@
2679
2801
  //
2680
2802
  // detect index for this field, considering inheritance
2681
2803
  //
2682
- fieldIndex = metadata[-1] // current structure already has fields defined
2683
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2804
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2805
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2684
2806
  ?? -1; // no fields defined
2685
2807
  fieldIndex++;
2686
2808
  }
@@ -2707,7 +2829,7 @@
2707
2829
  return {
2708
2830
  get: function () { return this[fieldCached]; },
2709
2831
  set: function (value) {
2710
- const previousValue = this[fieldCached] || undefined;
2832
+ const previousValue = this[fieldCached] ?? undefined;
2711
2833
  // skip if value is the same as cached.
2712
2834
  if (value === previousValue) {
2713
2835
  return;
@@ -2779,8 +2901,8 @@
2779
2901
  // //
2780
2902
  // metadata[field] = {
2781
2903
  // type: undefined,
2782
- // index: (metadata[-1] // current structure already has fields defined
2783
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2904
+ // index: (metadata[$numFields] // current structure already has fields defined
2905
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2784
2906
  // ?? -1) + 1 // no fields defined
2785
2907
  // }
2786
2908
  // }
@@ -2818,15 +2940,18 @@
2818
2940
  ops: {},
2819
2941
  refs: []
2820
2942
  };
2821
- $root.changes.forEach((operations, changeTree) => {
2943
+ // for (const refId in $root.changes) {
2944
+ $root.changes.forEach(changeTree => {
2945
+ const changes = changeTree.indexedOperations;
2822
2946
  dump.refs.push(`refId#${changeTree.refId}`);
2823
- operations.forEach((op, index) => {
2947
+ for (const index in changes) {
2948
+ const op = changes[index];
2824
2949
  const opName = exports.OPERATION[op];
2825
2950
  if (!dump.ops[opName]) {
2826
2951
  dump.ops[opName] = 0;
2827
2952
  }
2828
2953
  dump.ops[exports.OPERATION[op]]++;
2829
- });
2954
+ }
2830
2955
  });
2831
2956
  return dump;
2832
2957
  }
@@ -2852,6 +2977,7 @@
2852
2977
  class Schema {
2853
2978
  static { this[_a$2] = encodeSchemaOperation; }
2854
2979
  static { this[_b$2] = decodeSchemaOperation; }
2980
+ // public [$changes]: ChangeTree;
2855
2981
  /**
2856
2982
  * Assign the property descriptors required to track changes on this instance.
2857
2983
  * @param instance
@@ -2911,12 +3037,7 @@
2911
3037
  // inline
2912
3038
  // Schema.initialize(this);
2913
3039
  //
2914
- Object.defineProperty(this, $changes, {
2915
- value: new ChangeTree(this),
2916
- enumerable: false,
2917
- writable: true
2918
- });
2919
- Object.defineProperties(this, this.constructor[Symbol.metadata]?.[$descriptors] || {});
3040
+ Schema.initialize(this);
2920
3041
  //
2921
3042
  // Assign initial values
2922
3043
  //
@@ -2990,7 +3111,7 @@
2990
3111
  const changeTree = ref[$changes];
2991
3112
  const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2992
3113
  let output = "";
2993
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
3114
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
2994
3115
  changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
2995
3116
  return output;
2996
3117
  }
@@ -3008,18 +3129,26 @@
3008
3129
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3009
3130
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3010
3131
  function dumpChangeSet(changeSet) {
3011
- Array.from(changeSet)
3012
- .sort((a, b) => a[0] - b[0])
3013
- .forEach(([index, operation]) => output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3132
+ changeSet.operations
3133
+ .filter(op => op)
3134
+ .forEach((index) => {
3135
+ const operation = changeTree.indexedOperations[index];
3136
+ console.log({ index, operation });
3137
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3138
+ });
3014
3139
  }
3015
3140
  dumpChangeSet(changeSet);
3016
3141
  // display filtered changes
3017
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3142
+ if (!isEncodeAll &&
3143
+ changeTree.filteredChanges &&
3144
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3018
3145
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3019
3146
  dumpChangeSet(changeTree.filteredChanges);
3020
3147
  }
3021
3148
  // display filtered changes
3022
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3149
+ if (isEncodeAll &&
3150
+ changeTree.allFilteredChanges &&
3151
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3023
3152
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3024
3153
  dumpChangeSet(changeTree.allFilteredChanges);
3025
3154
  }
@@ -3028,10 +3157,12 @@
3028
3157
  static debugChangesDeep(ref, changeSetName = "changes") {
3029
3158
  let output = "";
3030
3159
  const rootChangeTree = ref[$changes];
3160
+ const root = rootChangeTree.root;
3031
3161
  const changeTrees = new Map();
3032
3162
  let totalInstances = 0;
3033
3163
  let totalOperations = 0;
3034
- for (const [changeTree, changes] of (rootChangeTree.root[changeSetName].entries())) {
3164
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3165
+ const changeTree = root.changeTrees[refId];
3035
3166
  let includeChangeTree = false;
3036
3167
  let parentChangeTrees = [];
3037
3168
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3050,7 +3181,7 @@
3050
3181
  }
3051
3182
  if (includeChangeTree) {
3052
3183
  totalInstances += 1;
3053
- totalOperations += changes.size;
3184
+ totalOperations += Object.keys(changes).length;
3054
3185
  changeTrees.set(changeTree, parentChangeTrees.reverse());
3055
3186
  }
3056
3187
  }
@@ -3068,12 +3199,13 @@
3068
3199
  visitedParents.add(parentChangeTree);
3069
3200
  }
3070
3201
  });
3071
- const changes = changeTree.changes;
3202
+ const changes = changeTree.indexedOperations;
3072
3203
  const level = parentChangeTrees.length;
3073
3204
  const indent = getIndent(level);
3074
3205
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3075
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3076
- for (const [index, operation] of changes) {
3206
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3207
+ for (const index in changes) {
3208
+ const operation = changes[index];
3077
3209
  output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3078
3210
  }
3079
3211
  }
@@ -3434,59 +3566,90 @@
3434
3566
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3435
3567
  };
3436
3568
 
3569
+ function spliceOne(arr, index) {
3570
+ // manually splice an array
3571
+ if (index === -1 || index >= arr.length) {
3572
+ return false;
3573
+ }
3574
+ const len = arr.length - 1;
3575
+ for (let i = index; i < len; i++) {
3576
+ arr[i] = arr[i + 1];
3577
+ }
3578
+ arr.length = len;
3579
+ return true;
3580
+ }
3581
+
3437
3582
  class Root {
3438
3583
  constructor(types) {
3439
3584
  this.types = types;
3440
3585
  this.nextUniqueId = 0;
3441
- this.refCount = new WeakMap();
3586
+ this.refCount = {};
3587
+ this.changeTrees = {};
3442
3588
  // all changes
3443
- this.allChanges = new Map();
3444
- this.allFilteredChanges = new Map();
3589
+ this.allChanges = [];
3590
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3445
3591
  // pending changes to be encoded
3446
- this.changes = new Map();
3447
- this.filteredChanges = new Map();
3592
+ this.changes = [];
3593
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3448
3594
  }
3449
3595
  getNextUniqueId() {
3450
3596
  return this.nextUniqueId++;
3451
3597
  }
3452
3598
  add(changeTree) {
3453
- const previousRefCount = this.refCount.get(changeTree);
3599
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3600
+ changeTree.ensureRefId();
3601
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3602
+ if (isNewChangeTree) {
3603
+ this.changeTrees[changeTree.refId] = changeTree;
3604
+ }
3605
+ const previousRefCount = this.refCount[changeTree.refId];
3454
3606
  if (previousRefCount === 0) {
3455
3607
  //
3456
3608
  // When a ChangeTree is re-added, it means that it was previously removed.
3457
3609
  // We need to re-add all changes to the `changes` map.
3458
3610
  //
3459
- changeTree.allChanges.forEach((operation, index) => {
3460
- changeTree.changes.set(index, operation);
3461
- });
3611
+ const ops = changeTree.allChanges.operations;
3612
+ let len = ops.length;
3613
+ while (len--) {
3614
+ changeTree.indexedOperations[ops[len]] = exports.OPERATION.ADD;
3615
+ setOperationAtIndex(changeTree.changes, len);
3616
+ }
3462
3617
  }
3463
- const refCount = (previousRefCount || 0) + 1;
3464
- this.refCount.set(changeTree, refCount);
3465
- return refCount;
3618
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3619
+ return isNewChangeTree;
3466
3620
  }
3467
3621
  remove(changeTree) {
3468
- const refCount = (this.refCount.get(changeTree)) - 1;
3622
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3469
3623
  if (refCount <= 0) {
3470
3624
  //
3471
3625
  // Only remove "root" reference if it's the last reference
3472
3626
  //
3473
3627
  changeTree.root = undefined;
3474
- this.allChanges.delete(changeTree);
3475
- this.changes.delete(changeTree);
3628
+ delete this.changeTrees[changeTree.refId];
3629
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3630
+ this.removeChangeFromChangeSet("changes", changeTree);
3476
3631
  if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3477
- this.allFilteredChanges.delete(changeTree);
3478
- this.filteredChanges.delete(changeTree);
3632
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3633
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3479
3634
  }
3480
- this.refCount.set(changeTree, 0);
3635
+ this.refCount[changeTree.refId] = 0;
3481
3636
  }
3482
3637
  else {
3483
- this.refCount.set(changeTree, refCount);
3638
+ this.refCount[changeTree.refId] = refCount;
3484
3639
  }
3485
3640
  changeTree.forEachChild((child, _) => this.remove(child));
3486
3641
  return refCount;
3487
3642
  }
3643
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3644
+ const changeSet = this[changeSetName];
3645
+ const index = changeSet.indexOf(changeTree);
3646
+ if (index !== -1) {
3647
+ spliceOne(changeSet, index);
3648
+ // changeSet[index] = undefined;
3649
+ }
3650
+ }
3488
3651
  clear() {
3489
- this.changes.clear();
3652
+ this.changes.length = 0;
3490
3653
  }
3491
3654
  }
3492
3655
 
@@ -3510,20 +3673,26 @@
3510
3673
  this.state = state;
3511
3674
  this.state[$changes].setRoot(this.root);
3512
3675
  }
3513
- 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
3676
+ 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
3514
3677
  ) {
3515
3678
  const hasView = (view !== undefined);
3516
3679
  const rootChangeTree = this.state[$changes];
3517
- const shouldClearChanges = !isEncodeAll && !hasView;
3518
- for (const [changeTree, changes] of changeTrees.entries()) {
3680
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3681
+ const changeTrees = this.root[changeSetName];
3682
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3683
+ const changeTree = changeTrees[i];
3684
+ // // Root#removeChangeFromChangeSet() is now removing instead of setting to "undefined"
3685
+ // if (changeTree === undefined) { continue; }
3686
+ const operations = changeTree[changeSetName];
3519
3687
  const ref = changeTree.ref;
3520
3688
  const ctor = ref.constructor;
3521
3689
  const encoder = ctor[$encoder];
3522
3690
  const filter = ctor[$filter];
3691
+ const metadata = ctor[Symbol.metadata];
3523
3692
  // try { throw new Error(); } catch (e) {
3524
3693
  // // only print if not coming from Reflection.ts
3525
3694
  // if (!e.stack.includes("src/Reflection.ts")) {
3526
- // console.log("ChangeTree:", { ref: ref.constructor.name, });
3695
+ // console.log("ChangeTree:", { refId: changeTree.refId, ref: ref.constructor.name });
3527
3696
  // }
3528
3697
  // }
3529
3698
  if (hasView) {
@@ -3541,7 +3710,13 @@
3541
3710
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3542
3711
  number$1(buffer, changeTree.refId, it);
3543
3712
  }
3544
- for (const [fieldIndex, operation] of changes.entries()) {
3713
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3714
+ const fieldIndex = operations.operations[j];
3715
+ const operation = (fieldIndex < 0)
3716
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3717
+ : (isEncodeAll)
3718
+ ? exports.OPERATION.ADD
3719
+ : changeTree.indexedOperations[fieldIndex];
3545
3720
  //
3546
3721
  // first pass (encodeAll), identify "filtered" operations without encoding them
3547
3722
  // they will be encoded per client, based on their view.
@@ -3549,7 +3724,7 @@
3549
3724
  // TODO: how can we optimize filtering out "encode all" operations?
3550
3725
  // TODO: avoid checking if no view tags were defined
3551
3726
  //
3552
- if (filter && !filter(ref, fieldIndex, view)) {
3727
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3553
3728
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3554
3729
  // view?.invisible.add(changeTree);
3555
3730
  continue;
@@ -3564,16 +3739,14 @@
3564
3739
  // });
3565
3740
  // }
3566
3741
  // }
3567
- encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3742
+ // console.log("encode...", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, fieldIndex, operation });
3743
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3744
+ }
3745
+ if (shouldDiscardChanges) {
3746
+ changeTree.discard();
3747
+ // Not a new instance anymore
3748
+ changeTree.isNew = false;
3568
3749
  }
3569
- // if (shouldClearChanges) {
3570
- // // changeTree.endEncode();
3571
- // changeTree.changes.clear();
3572
- // // ArraySchema and MapSchema have a custom "encode end" method
3573
- // changeTree.ref[$onEncodeEnd]?.();
3574
- // // Not a new instance anymore
3575
- // delete changeTree[$isNew];
3576
- // }
3577
3750
  }
3578
3751
  if (it.offset > buffer.byteLength) {
3579
3752
  const newSize = getNextPowerOf2(buffer.byteLength * 2);
@@ -3590,51 +3763,54 @@
3590
3763
  if (buffer === this.sharedBuffer) {
3591
3764
  this.sharedBuffer = buffer;
3592
3765
  }
3593
- return this.encode({ offset: initialOffset }, view, buffer, changeTrees, isEncodeAll);
3766
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3594
3767
  }
3595
3768
  else {
3596
- //
3597
- // only clear changes after making sure buffer resize is not required.
3598
- //
3599
- if (shouldClearChanges) {
3600
- //
3601
- // FIXME: avoid iterating over change trees twice.
3602
- //
3603
- this.onEndEncode(changeTrees);
3604
- }
3769
+ // //
3770
+ // // only clear changes after making sure buffer resize is not required.
3771
+ // //
3772
+ // if (shouldClearChanges) {
3773
+ // //
3774
+ // // FIXME: avoid iterating over change trees twice.
3775
+ // //
3776
+ // this.onEndEncode(changeTrees);
3777
+ // }
3605
3778
  return buffer.subarray(0, it.offset);
3606
3779
  }
3607
3780
  }
3608
3781
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3609
- // console.log(`\nencodeAll(), this.root.allChanges (${this.root.allChanges.size})`);
3782
+ // console.log(`\nencodeAll(), this.root.allChanges (${(Object.keys(this.root.allChanges).length)})`);
3610
3783
  // this.debugChanges("allChanges");
3611
- return this.encode(it, undefined, buffer, this.root.allChanges, true);
3784
+ return this.encode(it, undefined, buffer, "allChanges", true);
3612
3785
  }
3613
3786
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3614
3787
  const viewOffset = it.offset;
3615
- // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
3788
+ // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${(Object.keys(this.root.allFilteredChanges).length)})`);
3616
3789
  // this.debugChanges("allFilteredChanges");
3790
+ // console.log("\n\nENCODE ALL FOR VIEW...\n\n")
3617
3791
  // try to encode "filtered" changes
3618
- this.encode(it, view, bytes, this.root.allFilteredChanges, true, viewOffset);
3792
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3619
3793
  return Buffer.concat([
3620
3794
  bytes.subarray(0, sharedOffset),
3621
3795
  bytes.subarray(viewOffset, it.offset)
3622
3796
  ]);
3623
3797
  }
3624
3798
  debugChanges(field) {
3625
- const changeSet = (typeof (field) === "string")
3799
+ const rootChangeSet = (typeof (field) === "string")
3626
3800
  ? this.root[field]
3627
3801
  : field;
3628
- Array.from(changeSet.entries()).map((item) => {
3629
- const metadata = item[0].ref.constructor[Symbol.metadata];
3630
- console.log("->", { ref: item[0].ref.constructor.name, refId: item[0].refId, changes: item[1].size });
3631
- item[1].forEach((op, index) => {
3802
+ rootChangeSet.forEach((changeTree) => {
3803
+ const changeSet = changeTree[field];
3804
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3805
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3806
+ for (const index in changeSet) {
3807
+ const op = changeSet[index];
3632
3808
  console.log(" ->", {
3633
3809
  index,
3634
3810
  field: metadata?.[index],
3635
3811
  op: exports.OPERATION[op],
3636
3812
  });
3637
- });
3813
+ }
3638
3814
  });
3639
3815
  }
3640
3816
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
@@ -3644,23 +3820,31 @@
3644
3820
  // console.log(`\nencodeView(), this.root.filteredChanges (${this.root.filteredChanges.size})`);
3645
3821
  // this.debugChanges("filteredChanges");
3646
3822
  // encode visibility changes (add/remove for this view)
3647
- const viewChangesIterator = view.changes.entries();
3648
- for (const [changeTree, changes] of viewChangesIterator) {
3649
- if (changes.size === 0) {
3650
- // FIXME: avoid having empty changes if no changes were made
3651
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3823
+ const refIds = Object.keys(view.changes);
3824
+ // console.log("ENCODE VIEW:", refIds);
3825
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3826
+ const refId = refIds[i];
3827
+ const changes = view.changes[refId];
3828
+ const changeTree = this.root.changeTrees[refId];
3829
+ if (changeTree === undefined ||
3830
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3831
+ ) {
3832
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3652
3833
  continue;
3653
3834
  }
3654
3835
  const ref = changeTree.ref;
3655
- const ctor = ref['constructor'];
3836
+ const ctor = ref.constructor;
3656
3837
  const encoder = ctor[$encoder];
3838
+ const metadata = ctor[Symbol.metadata];
3657
3839
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3658
3840
  number$1(bytes, changeTree.refId, it);
3659
- const changesIterator = changes.entries();
3660
- for (const [fieldIndex, operation] of changesIterator) {
3841
+ const keys = Object.keys(changes);
3842
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3843
+ const key = keys[i];
3844
+ const operation = changes[key];
3661
3845
  // isEncodeAll = false
3662
3846
  // hasView = true
3663
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3847
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3664
3848
  }
3665
3849
  }
3666
3850
  //
@@ -3668,40 +3852,55 @@
3668
3852
  // (to allow re-using StateView's for multiple clients)
3669
3853
  //
3670
3854
  // clear "view" changes after encoding
3671
- view.changes.clear();
3855
+ view.changes = {};
3856
+ // console.log("FILTERED CHANGES:", this.root.filteredChanges);
3672
3857
  // try to encode "filtered" changes
3673
- this.encode(it, view, bytes, this.root.filteredChanges, false, viewOffset);
3858
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3674
3859
  return Buffer.concat([
3675
3860
  bytes.subarray(0, sharedOffset),
3676
3861
  bytes.subarray(viewOffset, it.offset)
3677
3862
  ]);
3678
3863
  }
3679
3864
  onEndEncode(changeTrees = this.root.changes) {
3680
- const changeTreesIterator = changeTrees.entries();
3681
- for (const [changeTree, _] of changeTreesIterator) {
3682
- changeTree.endEncode();
3683
- // changeTree.changes.clear();
3684
- // // ArraySchema and MapSchema have a custom "encode end" method
3685
- // changeTree.ref[$onEncodeEnd]?.();
3686
- // // Not a new instance anymore
3687
- // delete changeTree[$isNew];
3688
- }
3865
+ // changeTrees.forEach(function(changeTree) {
3866
+ // changeTree.endEncode();
3867
+ // });
3868
+ // for (const refId in changeTrees) {
3869
+ // const changeTree = this.root.changeTrees[refId];
3870
+ // changeTree.endEncode();
3871
+ // // changeTree.changes.clear();
3872
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3873
+ // // changeTree.ref[$onEncodeEnd]?.();
3874
+ // // // Not a new instance anymore
3875
+ // // delete changeTree[$isNew];
3876
+ // }
3689
3877
  }
3690
3878
  discardChanges() {
3879
+ // console.log("DISCARD CHANGES!");
3691
3880
  // discard shared changes
3692
- if (this.root.changes.size > 0) {
3693
- this.onEndEncode(this.root.changes);
3694
- this.root.changes.clear();
3881
+ let length = this.root.changes.length;
3882
+ if (length > 0) {
3883
+ while (length--) {
3884
+ this.root.changes[length]?.endEncode();
3885
+ }
3886
+ this.root.changes.length = 0;
3695
3887
  }
3696
3888
  // discard filtered changes
3697
- if (this.root.filteredChanges.size > 0) {
3698
- this.onEndEncode(this.root.filteredChanges);
3699
- this.root.filteredChanges.clear();
3889
+ length = this.root.filteredChanges.length;
3890
+ if (length > 0) {
3891
+ while (length--) {
3892
+ this.root.filteredChanges[length]?.endEncode();
3893
+ }
3894
+ this.root.filteredChanges.length = 0;
3700
3895
  }
3701
3896
  }
3702
3897
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3703
3898
  const baseTypeId = this.context.getTypeId(baseType);
3704
3899
  const targetTypeId = this.context.getTypeId(targetType);
3900
+ if (targetTypeId === undefined) {
3901
+ 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.`);
3902
+ return;
3903
+ }
3705
3904
  if (baseTypeId !== targetTypeId) {
3706
3905
  bytes[it.offset++] = TYPE_ID & 255;
3707
3906
  number$1(bytes, targetTypeId, it);
@@ -3709,19 +3908,6 @@
3709
3908
  }
3710
3909
  }
3711
3910
 
3712
- function spliceOne(arr, index) {
3713
- // manually splice an array
3714
- if (index === -1 || index >= arr.length) {
3715
- return false;
3716
- }
3717
- const len = arr.length - 1;
3718
- for (let i = index; i < len; i++) {
3719
- arr[i] = arr[i + 1];
3720
- }
3721
- arr.length = len;
3722
- return true;
3723
- }
3724
-
3725
3911
  class DecodingWarning extends Error {
3726
3912
  constructor(message) {
3727
3913
  super(message);
@@ -3883,7 +4069,7 @@
3883
4069
  }
3884
4070
  ref[$onDecodeEnd]?.();
3885
4071
  ref = nextRef;
3886
- decoder = ref['constructor'][$decoder];
4072
+ decoder = ref.constructor[$decoder];
3887
4073
  continue;
3888
4074
  }
3889
4075
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3984,10 +4170,16 @@
3984
4170
  super(...arguments);
3985
4171
  this.types = new ArraySchema();
3986
4172
  }
3987
- static encode(instance, context, it = { offset: 0 }) {
3988
- context ??= new TypeContext(instance.constructor);
4173
+ /**
4174
+ * Encodes the TypeContext of an Encoder into a buffer.
4175
+ *
4176
+ * @param context TypeContext instance
4177
+ * @param it
4178
+ * @returns
4179
+ */
4180
+ static encode(context, it = { offset: 0 }) {
3989
4181
  const reflection = new Reflection();
3990
- const encoder = new Encoder(reflection);
4182
+ const reflectionEncoder = new Encoder(reflection);
3991
4183
  const buildType = (currentType, metadata) => {
3992
4184
  for (const fieldIndex in metadata) {
3993
4185
  const index = Number(fieldIndex);
@@ -4041,9 +4233,16 @@
4041
4233
  }
4042
4234
  buildType(type, klass[Symbol.metadata]);
4043
4235
  }
4044
- const buf = encoder.encodeAll(it);
4236
+ const buf = reflectionEncoder.encodeAll(it);
4045
4237
  return Buffer.from(buf, 0, it.offset);
4046
4238
  }
4239
+ /**
4240
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4241
+ *
4242
+ * @param bytes Reflection.encode() output
4243
+ * @param it
4244
+ * @returns Decoder instance
4245
+ */
4047
4246
  static decode(bytes, it) {
4048
4247
  const reflection = new Reflection();
4049
4248
  const reflectionDecoder = new Decoder(reflection);
@@ -4089,8 +4288,8 @@
4089
4288
  }
4090
4289
  });
4091
4290
  });
4092
- // @ts-ignore
4093
- return new (typeContext.get(0))();
4291
+ const state = new (typeContext.get(0))();
4292
+ return new Decoder(state, typeContext);
4094
4293
  }
4095
4294
  }
4096
4295
  __decorate([
@@ -4360,7 +4559,7 @@
4360
4559
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
4361
4560
  * (This is used to force encoding a property, even if it was not changed)
4362
4561
  */
4363
- this.changes = new Map();
4562
+ this.changes = {};
4364
4563
  }
4365
4564
  // TODO: allow to set multiple tags at once
4366
4565
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
@@ -4382,10 +4581,10 @@
4382
4581
  // TODO: when adding an item of a MapSchema, the changes may not
4383
4582
  // be set (only the parent's changes are set)
4384
4583
  //
4385
- let changes = this.changes.get(changeTree);
4584
+ let changes = this.changes[changeTree.refId];
4386
4585
  if (changes === undefined) {
4387
- changes = new Map();
4388
- this.changes.set(changeTree, changes);
4586
+ changes = {};
4587
+ this.changes[changeTree.refId] = changes;
4389
4588
  }
4390
4589
  // set tag
4391
4590
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -4402,9 +4601,9 @@
4402
4601
  }
4403
4602
  tags.add(tag);
4404
4603
  // Ref: add tagged properties
4405
- metadata?.[-3]?.[tag]?.forEach((index) => {
4604
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4406
4605
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4407
- changes.set(index, exports.OPERATION.ADD);
4606
+ changes[index] = exports.OPERATION.ADD;
4408
4607
  }
4409
4608
  });
4410
4609
  }
@@ -4413,16 +4612,21 @@
4413
4612
  const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4414
4613
  ? changeTree.allFilteredChanges
4415
4614
  : changeTree.allChanges;
4416
- changeSet.forEach((op, index) => {
4615
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4616
+ const index = changeSet.operations[i];
4617
+ if (index === undefined) {
4618
+ continue;
4619
+ } // skip "undefined" indexes
4620
+ const op = changeTree.indexedOperations[index];
4417
4621
  const tagAtIndex = metadata?.[index].tag;
4418
4622
  if ((isInvisible || // if "invisible", include all
4419
4623
  tagAtIndex === undefined || // "all change" with no tag
4420
4624
  tagAtIndex === tag // tagged property
4421
4625
  ) &&
4422
4626
  op !== exports.OPERATION.DELETE) {
4423
- changes.set(index, op);
4627
+ changes[index] = op;
4424
4628
  }
4425
- });
4629
+ }
4426
4630
  }
4427
4631
  // Add children of this ChangeTree to this view
4428
4632
  changeTree.forEachChild((change, index) => {
@@ -4448,10 +4652,10 @@
4448
4652
  }
4449
4653
  // add parent's tag properties
4450
4654
  if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4451
- let changes = this.changes.get(changeTree);
4655
+ let changes = this.changes[changeTree.refId];
4452
4656
  if (changes === undefined) {
4453
- changes = new Map();
4454
- this.changes.set(changeTree, changes);
4657
+ changes = {};
4658
+ this.changes[changeTree.refId] = changes;
4455
4659
  }
4456
4660
  if (!this.tags) {
4457
4661
  this.tags = new WeakMap();
@@ -4465,7 +4669,7 @@
4465
4669
  tags = this.tags.get(changeTree);
4466
4670
  }
4467
4671
  tags.add(tag);
4468
- changes.set(parentIndex, exports.OPERATION.ADD);
4672
+ changes[parentIndex] = exports.OPERATION.ADD;
4469
4673
  }
4470
4674
  }
4471
4675
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4477,32 +4681,32 @@
4477
4681
  this.items.delete(changeTree);
4478
4682
  const ref = changeTree.ref;
4479
4683
  const metadata = ref.constructor[Symbol.metadata];
4480
- let changes = this.changes.get(changeTree);
4684
+ let changes = this.changes[changeTree.refId];
4481
4685
  if (changes === undefined) {
4482
- changes = new Map();
4483
- this.changes.set(changeTree, changes);
4686
+ changes = {};
4687
+ this.changes[changeTree.refId] = changes;
4484
4688
  }
4485
4689
  if (tag === DEFAULT_VIEW_TAG) {
4486
4690
  // parent is collection (Map/Array)
4487
4691
  const parent = changeTree.parent;
4488
4692
  if (!Metadata.isValidInstance(parent)) {
4489
4693
  const parentChangeTree = parent[$changes];
4490
- let changes = this.changes.get(parentChangeTree);
4694
+ let changes = this.changes[parentChangeTree.refId];
4491
4695
  if (changes === undefined) {
4492
- changes = new Map();
4493
- this.changes.set(parentChangeTree, changes);
4696
+ changes = {};
4697
+ this.changes[parentChangeTree.refId] = changes;
4494
4698
  }
4495
4699
  // DELETE / DELETE BY REF ID
4496
- changes.set(changeTree.parentIndex, exports.OPERATION.DELETE);
4700
+ changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
4497
4701
  }
4498
4702
  else {
4499
4703
  // delete all "tagged" properties.
4500
- metadata[-2].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4704
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4501
4705
  }
4502
4706
  }
4503
4707
  else {
4504
4708
  // delete only tagged properties
4505
- metadata[-3][tag].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4709
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4506
4710
  }
4507
4711
  // remove tag
4508
4712
  if (this.tags && this.tags.has(changeTree)) {