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

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 (63) hide show
  1. package/bin/schema-debug +4 -3
  2. package/build/cjs/index.js +465 -303
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +465 -303
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +465 -303
  7. package/lib/Metadata.d.ts +5 -5
  8. package/lib/Metadata.js +17 -17
  9. package/lib/Metadata.js.map +1 -1
  10. package/lib/Schema.js +24 -17
  11. package/lib/Schema.js.map +1 -1
  12. package/lib/annotations.js +11 -11
  13. package/lib/annotations.js.map +1 -1
  14. package/lib/bench_encode.js +12 -5
  15. package/lib/bench_encode.js.map +1 -1
  16. package/lib/decoder/Decoder.js +1 -1
  17. package/lib/decoder/Decoder.js.map +1 -1
  18. package/lib/encoder/ChangeTree.d.ts +23 -7
  19. package/lib/encoder/ChangeTree.js +183 -106
  20. package/lib/encoder/ChangeTree.js.map +1 -1
  21. package/lib/encoder/EncodeOperation.d.ts +2 -1
  22. package/lib/encoder/EncodeOperation.js +2 -2
  23. package/lib/encoder/EncodeOperation.js.map +1 -1
  24. package/lib/encoder/Encoder.d.ts +3 -5
  25. package/lib/encoder/Encoder.js +93 -61
  26. package/lib/encoder/Encoder.js.map +1 -1
  27. package/lib/encoder/Root.d.ts +12 -7
  28. package/lib/encoder/Root.js +41 -20
  29. package/lib/encoder/Root.js.map +1 -1
  30. package/lib/encoder/StateView.d.ts +5 -5
  31. package/lib/encoder/StateView.js +29 -23
  32. package/lib/encoder/StateView.js.map +1 -1
  33. package/lib/encoding/encode.js +12 -9
  34. package/lib/encoding/encode.js.map +1 -1
  35. package/lib/types/TypeContext.js +2 -1
  36. package/lib/types/TypeContext.js.map +1 -1
  37. package/lib/types/custom/ArraySchema.js +27 -13
  38. package/lib/types/custom/ArraySchema.js.map +1 -1
  39. package/lib/types/custom/MapSchema.d.ts +3 -1
  40. package/lib/types/custom/MapSchema.js +7 -4
  41. package/lib/types/custom/MapSchema.js.map +1 -1
  42. package/lib/types/symbols.d.ts +8 -6
  43. package/lib/types/symbols.js +9 -7
  44. package/lib/types/symbols.js.map +1 -1
  45. package/lib/utils.js +6 -3
  46. package/lib/utils.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/Metadata.ts +22 -22
  49. package/src/Schema.ts +33 -25
  50. package/src/annotations.ts +12 -12
  51. package/src/bench_encode.ts +15 -6
  52. package/src/decoder/Decoder.ts +1 -1
  53. package/src/encoder/ChangeTree.ts +220 -115
  54. package/src/encoder/EncodeOperation.ts +5 -1
  55. package/src/encoder/Encoder.ts +110 -68
  56. package/src/encoder/Root.ts +41 -21
  57. package/src/encoder/StateView.ts +32 -28
  58. package/src/encoding/encode.ts +12 -9
  59. package/src/types/TypeContext.ts +2 -1
  60. package/src/types/custom/ArraySchema.ts +39 -17
  61. package/src/types/custom/MapSchema.ts +12 -5
  62. package/src/types/symbols.ts +10 -9
  63. package/src/utils.ts +7 -3
@@ -34,7 +34,6 @@ const $decoder = Symbol("$decoder");
34
34
  const $filter = Symbol("$filter");
35
35
  const $getByIndex = Symbol("$getByIndex");
36
36
  const $deleteByIndex = Symbol("$deleteByIndex");
37
- const $descriptors = Symbol("$descriptors");
38
37
  /**
39
38
  * Used to hold ChangeTree instances whitin the structures
40
39
  */
@@ -44,11 +43,6 @@ const $changes = Symbol('$changes');
44
43
  * (MapSchema, ArraySchema, etc.)
45
44
  */
46
45
  const $childType = Symbol('$childType');
47
- /**
48
- * Special ChangeTree property to identify new instances
49
- * (Once they're encoded, they're not new anymore)
50
- */
51
- const $isNew = Symbol("$isNew");
52
46
  /**
53
47
  * Optional "discard" method for custom types (ArraySchema)
54
48
  * (Discards changes for next serialization)
@@ -58,6 +52,14 @@ const $onEncodeEnd = Symbol('$onEncodeEnd');
58
52
  * When decoding, this method is called after the instance is fully decoded
59
53
  */
60
54
  const $onDecodeEnd = Symbol("$onDecodeEnd");
55
+ /**
56
+ * Metadata
57
+ */
58
+ const $descriptors = Symbol("$descriptors");
59
+ const $numFields = "$__numFields";
60
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
61
+ const $viewFieldIndexes = "$__viewFieldIndexes";
62
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
61
63
 
62
64
  const registeredTypes = {};
63
65
  const identifiers = new Map();
@@ -104,7 +106,7 @@ const Metadata = {
104
106
  };
105
107
  }
106
108
  // map -1 as last field index
107
- Object.defineProperty(metadata, -1, {
109
+ Object.defineProperty(metadata, $numFields, {
108
110
  value: index,
109
111
  enumerable: false,
110
112
  configurable: true
@@ -117,14 +119,14 @@ const Metadata = {
117
119
  });
118
120
  // if child Ref/complex type, add to -4
119
121
  if (typeof (metadata[index].type) !== "string") {
120
- if (metadata[-4] === undefined) {
121
- Object.defineProperty(metadata, -4, {
122
+ if (metadata[$refTypeFieldIndexes] === undefined) {
123
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
122
124
  value: [],
123
125
  enumerable: false,
124
126
  configurable: true,
125
127
  });
126
128
  }
127
- metadata[-4].push(index);
129
+ metadata[$refTypeFieldIndexes].push(index);
128
130
  }
129
131
  },
130
132
  setTag(metadata, fieldName, tag) {
@@ -132,25 +134,25 @@ const Metadata = {
132
134
  const field = metadata[index];
133
135
  // add 'tag' to the field
134
136
  field.tag = tag;
135
- if (!metadata[-2]) {
137
+ if (!metadata[$viewFieldIndexes]) {
136
138
  // -2: all field indexes with "view" tag
137
- Object.defineProperty(metadata, -2, {
139
+ Object.defineProperty(metadata, $viewFieldIndexes, {
138
140
  value: [],
139
141
  enumerable: false,
140
142
  configurable: true
141
143
  });
142
144
  // -3: field indexes by "view" tag
143
- Object.defineProperty(metadata, -3, {
145
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
144
146
  value: {},
145
147
  enumerable: false,
146
148
  configurable: true
147
149
  });
148
150
  }
149
- metadata[-2].push(index);
150
- if (!metadata[-3][tag]) {
151
- metadata[-3][tag] = [];
151
+ metadata[$viewFieldIndexes].push(index);
152
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
153
+ metadata[$fieldIndexesByViewTag][tag] = [];
152
154
  }
153
- metadata[-3][tag].push(index);
155
+ metadata[$fieldIndexesByViewTag][tag].push(index);
154
156
  },
155
157
  setFields(target, fields) {
156
158
  const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
@@ -175,7 +177,7 @@ const Metadata = {
175
177
  //
176
178
  const metadata = {};
177
179
  klass[Symbol.metadata] = metadata;
178
- Object.defineProperty(metadata, -1, {
180
+ Object.defineProperty(metadata, $numFields, {
179
181
  value: 0,
180
182
  enumerable: false,
181
183
  configurable: true,
@@ -189,7 +191,7 @@ const Metadata = {
189
191
  if (parentMetadata) {
190
192
  // assign parent metadata to current
191
193
  Object.assign(metadata, parentMetadata);
192
- for (let i = 0; i <= parentMetadata[-1]; i++) {
194
+ for (let i = 0; i <= parentMetadata[$numFields]; i++) {
193
195
  const fieldName = parentMetadata[i].name;
194
196
  Object.defineProperty(metadata, fieldName, {
195
197
  value: parentMetadata[fieldName],
@@ -197,8 +199,8 @@ const Metadata = {
197
199
  configurable: true,
198
200
  });
199
201
  }
200
- Object.defineProperty(metadata, -1, {
201
- value: parentMetadata[-1],
202
+ Object.defineProperty(metadata, $numFields, {
203
+ value: parentMetadata[$numFields],
202
204
  enumerable: false,
203
205
  configurable: true,
204
206
  writable: true,
@@ -210,40 +212,69 @@ const Metadata = {
210
212
  },
211
213
  isValidInstance(klass) {
212
214
  return (klass.constructor[Symbol.metadata] &&
213
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
215
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
214
216
  },
215
217
  getFields(klass) {
216
218
  const metadata = klass[Symbol.metadata];
217
219
  const fields = {};
218
- for (let i = 0; i <= metadata[-1]; i++) {
220
+ for (let i = 0; i <= metadata[$numFields]; i++) {
219
221
  fields[metadata[i].name] = metadata[i].type;
220
222
  }
221
223
  return fields;
222
224
  }
223
225
  };
224
226
 
225
- var _a$5;
227
+ function setOperationAtIndex(changeSet, index) {
228
+ const operationsIndex = changeSet.indexes[index];
229
+ if (operationsIndex === undefined) {
230
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
231
+ }
232
+ else {
233
+ changeSet.operations[operationsIndex] = index;
234
+ }
235
+ }
236
+ function deleteOperationAtIndex(changeSet, index) {
237
+ const operationsIndex = changeSet.indexes[index];
238
+ if (operationsIndex !== undefined) {
239
+ changeSet.operations[operationsIndex] = undefined;
240
+ }
241
+ delete changeSet.indexes[index];
242
+ }
243
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
244
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
245
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
246
+ }
247
+ }
226
248
  class ChangeTree {
227
- static { _a$5 = $isNew; }
228
249
  constructor(ref) {
229
250
  this.isFiltered = false;
230
251
  this.isPartiallyFiltered = false;
231
- this.currentOperationIndex = 0;
232
- this.changes = new Map();
233
- this.allChanges = new Map();
234
- this[_a$5] = true;
252
+ this.indexedOperations = {};
253
+ //
254
+ // TODO:
255
+ // try storing the index + operation per item.
256
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
257
+ //
258
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
259
+ //
260
+ this.changes = { indexes: {}, operations: [] };
261
+ this.allChanges = { indexes: {}, operations: [] };
262
+ /**
263
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
264
+ */
265
+ this.isNew = true;
235
266
  this.ref = ref;
236
267
  //
237
268
  // Does this structure have "filters" declared?
238
269
  //
239
- if (ref.constructor[Symbol.metadata]?.[-2]) {
240
- this.allFilteredChanges = new Map();
241
- this.filteredChanges = new Map();
270
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
271
+ this.allFilteredChanges = { indexes: {}, operations: [] };
272
+ this.filteredChanges = { indexes: {}, operations: [] };
242
273
  }
243
274
  }
244
275
  setRoot(root) {
245
276
  this.root = root;
246
- this.root.add(this);
277
+ const isNewChangeTree = this.root.add(this);
247
278
  const metadata = this.ref.constructor[Symbol.metadata];
248
279
  if (this.root.types.hasFilters) {
249
280
  //
@@ -254,22 +285,24 @@ class ChangeTree {
254
285
  //
255
286
  this.checkIsFiltered(metadata, this.parent, this.parentIndex);
256
287
  if (this.isFiltered || this.isPartiallyFiltered) {
257
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
258
- this.root.filteredChanges.set(this, this.filteredChanges);
288
+ enqueueChangeTree(root, this, 'filteredChanges');
289
+ if (isNewChangeTree) {
290
+ this.root.allFilteredChanges.push(this);
291
+ }
259
292
  }
260
293
  }
261
294
  if (!this.isFiltered) {
262
- this.root.changes.set(this, this.changes);
263
- this.root.allChanges.set(this, this.allChanges);
295
+ enqueueChangeTree(root, this, 'changes');
296
+ if (isNewChangeTree) {
297
+ this.root.allChanges.push(this);
298
+ }
264
299
  }
265
- this.ensureRefId();
300
+ // Recursively set root on child structures
266
301
  if (metadata) {
267
- metadata[-4]?.forEach((index) => {
302
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
268
303
  const field = metadata[index];
269
304
  const value = this.ref[field.name];
270
- if (value) {
271
- value[$changes].setRoot(root);
272
- }
305
+ value?.[$changes].setRoot(root);
273
306
  });
274
307
  }
275
308
  else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
@@ -286,31 +319,36 @@ class ChangeTree {
286
319
  if (!root) {
287
320
  return;
288
321
  }
289
- root.add(this);
290
322
  const metadata = this.ref.constructor[Symbol.metadata];
291
323
  // skip if parent is already set
292
324
  if (root !== this.root) {
293
325
  this.root = root;
326
+ const isNewChangeTree = root.add(this);
294
327
  if (root.types.hasFilters) {
295
328
  this.checkIsFiltered(metadata, parent, parentIndex);
296
329
  if (this.isFiltered || this.isPartiallyFiltered) {
297
- this.root.filteredChanges.set(this, this.filteredChanges);
298
- this.root.allFilteredChanges.set(this, this.filteredChanges);
330
+ enqueueChangeTree(root, this, 'filteredChanges');
331
+ if (isNewChangeTree) {
332
+ this.root.allFilteredChanges.push(this);
333
+ }
299
334
  }
300
335
  }
301
336
  if (!this.isFiltered) {
302
- this.root.changes.set(this, this.changes);
303
- this.root.allChanges.set(this, this.allChanges);
337
+ enqueueChangeTree(root, this, 'changes');
338
+ if (isNewChangeTree) {
339
+ this.root.allChanges.push(this);
340
+ }
304
341
  }
305
- this.ensureRefId();
342
+ }
343
+ else {
344
+ root.add(this);
306
345
  }
307
346
  // assign same parent on child structures
308
347
  if (metadata) {
309
- metadata[-4]?.forEach((index) => {
348
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
310
349
  const field = metadata[index];
311
350
  const value = this.ref[field.name];
312
351
  value?.[$changes].setParent(this.ref, root, index);
313
- // console.log(this.ref.constructor.name, field.name, value);
314
352
  // try { throw new Error(); } catch (e) {
315
353
  // console.log(e.stack);
316
354
  // }
@@ -329,7 +367,7 @@ class ChangeTree {
329
367
  //
330
368
  const metadata = this.ref.constructor[Symbol.metadata];
331
369
  if (metadata) {
332
- metadata[-4]?.forEach((index) => {
370
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
333
371
  const field = metadata[index];
334
372
  const value = this.ref[field.name];
335
373
  if (value) {
@@ -345,8 +383,10 @@ class ChangeTree {
345
383
  }
346
384
  }
347
385
  operation(op) {
348
- this.changes.set(--this.currentOperationIndex, op);
349
- this.root?.changes.set(this, this.changes);
386
+ // operations without index use negative values to represent them
387
+ // this is checked during .encode() time.
388
+ this.changes.operations.push(-op);
389
+ enqueueChangeTree(this.root, this, 'changes');
350
390
  }
351
391
  change(index, operation = OPERATION.ADD) {
352
392
  const metadata = this.ref.constructor[Symbol.metadata];
@@ -354,7 +394,7 @@ class ChangeTree {
354
394
  const changeSet = (isFiltered)
355
395
  ? this.filteredChanges
356
396
  : this.changes;
357
- const previousOperation = changeSet.get(index);
397
+ const previousOperation = this.indexedOperations[index];
358
398
  if (!previousOperation || previousOperation === OPERATION.DELETE) {
359
399
  const op = (!previousOperation)
360
400
  ? operation
@@ -364,16 +404,19 @@ class ChangeTree {
364
404
  //
365
405
  // TODO: are DELETE operations being encoded as ADD here ??
366
406
  //
367
- changeSet.set(index, op);
407
+ this.indexedOperations[index] = op;
368
408
  }
409
+ setOperationAtIndex(changeSet, index);
369
410
  if (isFiltered) {
370
- this.allFilteredChanges.set(index, OPERATION.ADD);
371
- this.root?.filteredChanges.set(this, this.filteredChanges);
372
- this.root?.allFilteredChanges.set(this, this.allFilteredChanges);
411
+ setOperationAtIndex(this.allFilteredChanges, index);
412
+ if (this.root) {
413
+ enqueueChangeTree(this.root, this, 'filteredChanges');
414
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
415
+ }
373
416
  }
374
417
  else {
375
- this.allChanges.set(index, OPERATION.ADD);
376
- this.root?.changes.set(this, this.changes);
418
+ setOperationAtIndex(this.allChanges, index);
419
+ enqueueChangeTree(this.root, this, 'changes');
377
420
  }
378
421
  }
379
422
  shiftChangeIndexes(shiftIndex) {
@@ -385,12 +428,15 @@ class ChangeTree {
385
428
  const changeSet = (this.isFiltered)
386
429
  ? this.filteredChanges
387
430
  : this.changes;
388
- const changeSetEntries = Array.from(changeSet.entries());
389
- changeSet.clear();
390
- // Re-insert each entry with the shifted index
391
- for (const [index, op] of changeSetEntries) {
392
- changeSet.set(index + shiftIndex, op);
431
+ const newIndexedOperations = {};
432
+ const newIndexes = {};
433
+ for (const index in this.indexedOperations) {
434
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
435
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
393
436
  }
437
+ this.indexedOperations = newIndexedOperations;
438
+ changeSet.indexes = newIndexes;
439
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
394
440
  }
395
441
  shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
396
442
  //
@@ -406,24 +452,36 @@ class ChangeTree {
406
452
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
407
453
  }
408
454
  }
409
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
410
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
411
- if (index >= startIndex) {
412
- allChangeSet.delete(index);
413
- allChangeSet.set(index + shiftIndex, op);
455
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
456
+ const newIndexes = {};
457
+ for (const key in changeSet.indexes) {
458
+ const index = changeSet.indexes[key];
459
+ if (index > startIndex) {
460
+ newIndexes[Number(key) + shiftIndex] = index;
414
461
  }
415
- });
462
+ else {
463
+ newIndexes[key] = index;
464
+ }
465
+ }
466
+ changeSet.indexes = newIndexes;
467
+ for (let i = 0; i < changeSet.operations.length; i++) {
468
+ const index = changeSet.operations[i];
469
+ if (index > startIndex) {
470
+ changeSet.operations[i] = index + shiftIndex;
471
+ }
472
+ }
416
473
  }
417
474
  indexedOperation(index, operation, allChangesIndex = index) {
418
- if (this.filteredChanges !== undefined) {
419
- this.allFilteredChanges.set(allChangesIndex, OPERATION.ADD);
420
- this.filteredChanges.set(index, operation);
421
- this.root?.filteredChanges.set(this, this.filteredChanges);
475
+ this.indexedOperations[index] = operation;
476
+ if (this.filteredChanges) {
477
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
478
+ setOperationAtIndex(this.filteredChanges, index);
479
+ enqueueChangeTree(this.root, this, 'filteredChanges');
422
480
  }
423
481
  else {
424
- this.allChanges.set(allChangesIndex, OPERATION.ADD);
425
- this.changes.set(index, operation);
426
- this.root?.changes.set(this, this.changes);
482
+ setOperationAtIndex(this.allChanges, allChangesIndex);
483
+ setOperationAtIndex(this.changes, index);
484
+ enqueueChangeTree(this.root, this, 'changes');
427
485
  }
428
486
  }
429
487
  getType(index) {
@@ -442,8 +500,7 @@ class ChangeTree {
442
500
  }
443
501
  }
444
502
  getChange(index) {
445
- // TODO: optimize this. avoid checking against multiple instances
446
- return this.changes.get(index) ?? this.filteredChanges?.get(index);
503
+ return this.indexedOperations[index];
447
504
  }
448
505
  //
449
506
  // used during `.encode()`
@@ -467,8 +524,9 @@ class ChangeTree {
467
524
  const changeSet = (this.filteredChanges)
468
525
  ? this.filteredChanges
469
526
  : this.changes;
527
+ this.indexedOperations[index] = operation ?? OPERATION.DELETE;
528
+ setOperationAtIndex(changeSet, index);
470
529
  const previousValue = this.getValue(index);
471
- changeSet.set(index, operation ?? OPERATION.DELETE);
472
530
  // remove `root` reference
473
531
  if (previousValue && previousValue[$changes]) {
474
532
  //
@@ -484,23 +542,26 @@ class ChangeTree {
484
542
  this.root?.remove(previousValue[$changes]);
485
543
  }
486
544
  //
487
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
545
+ // FIXME: this is looking a ugly and repeated
488
546
  //
489
547
  if (this.filteredChanges) {
490
- this.root?.filteredChanges.set(this, this.filteredChanges);
491
- this.allFilteredChanges.delete(allChangesIndex);
548
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
549
+ enqueueChangeTree(this.root, this, 'filteredChanges');
492
550
  }
493
551
  else {
494
- this.root?.changes.set(this, this.changes);
495
- this.allChanges.delete(allChangesIndex);
552
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
553
+ enqueueChangeTree(this.root, this, 'changes');
496
554
  }
497
555
  }
498
556
  endEncode() {
499
- this.changes.clear();
557
+ this.indexedOperations = {};
558
+ // // clear changes
559
+ // this.changes.indexes = {};
560
+ // this.changes.operations.length = 0;
500
561
  // ArraySchema and MapSchema have a custom "encode end" method
501
562
  this.ref[$onEncodeEnd]?.();
502
563
  // Not a new instance anymore
503
- delete this[$isNew];
564
+ this.isNew = false;
504
565
  }
505
566
  discard(discardAll = false) {
506
567
  //
@@ -509,13 +570,22 @@ class ChangeTree {
509
570
  // REPLACE in case same key is used on next patches.
510
571
  //
511
572
  this.ref[$onEncodeEnd]?.();
512
- this.changes.clear();
513
- this.filteredChanges?.clear();
514
- // reset operation index
515
- this.currentOperationIndex = 0;
573
+ this.indexedOperations = {};
574
+ this.changes.indexes = {};
575
+ this.changes.operations.length = 0;
576
+ this.changes.queueRootIndex = undefined;
577
+ if (this.filteredChanges !== undefined) {
578
+ this.filteredChanges.indexes = {};
579
+ this.filteredChanges.operations.length = 0;
580
+ this.filteredChanges.queueRootIndex = undefined;
581
+ }
516
582
  if (discardAll) {
517
- this.allChanges.clear();
518
- this.allFilteredChanges?.clear();
583
+ this.allChanges.indexes = {};
584
+ this.allChanges.operations.length = 0;
585
+ if (this.allFilteredChanges !== undefined) {
586
+ this.allFilteredChanges.indexes = {};
587
+ this.allFilteredChanges.operations.length = 0;
588
+ }
519
589
  // remove children references
520
590
  this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
521
591
  }
@@ -524,12 +594,13 @@ class ChangeTree {
524
594
  * Recursively discard all changes from this, and child structures.
525
595
  */
526
596
  discardAll() {
527
- this.changes.forEach((_, fieldIndex) => {
528
- const value = this.getValue(fieldIndex);
597
+ const keys = Object.keys(this.indexedOperations);
598
+ for (let i = 0, len = keys.length; i < len; i++) {
599
+ const value = this.getValue(Number(keys[i]));
529
600
  if (value && value[$changes]) {
530
601
  value[$changes].discardAll();
531
602
  }
532
- });
603
+ }
533
604
  this.discard();
534
605
  }
535
606
  ensureRefId() {
@@ -540,42 +611,48 @@ class ChangeTree {
540
611
  this.refId = this.root.getNextUniqueId();
541
612
  }
542
613
  get changed() {
543
- return this.changes.size > 0;
614
+ return (Object.entries(this.indexedOperations).length > 0);
544
615
  }
545
616
  checkIsFiltered(metadata, parent, parentIndex) {
546
617
  // Detect if current structure has "filters" declared
547
- this.isPartiallyFiltered = metadata?.[-2] !== undefined;
618
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
548
619
  if (this.isPartiallyFiltered) {
549
- this.filteredChanges = this.filteredChanges || new Map();
550
- this.allFilteredChanges = this.allFilteredChanges || new Map();
620
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
621
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
551
622
  }
552
- if (parent) {
553
- if (!Metadata.isValidInstance(parent)) {
554
- const parentChangeTree = parent[$changes];
555
- parent = parentChangeTree.parent;
556
- parentIndex = parentChangeTree.parentIndex;
557
- }
558
- const parentMetadata = parent?.constructor?.[Symbol.metadata];
559
- this.isFiltered = (parent && parentMetadata?.[-2]?.includes(parentIndex));
560
- //
561
- // TODO: refactor this!
562
- //
563
- // swapping `changes` and `filteredChanges` is required here
564
- // because "isFiltered" may not be imedialely available on `change()`
565
- //
566
- if (this.isFiltered) {
567
- this.filteredChanges = new Map();
568
- this.allFilteredChanges = new Map();
569
- if (this.changes.size > 0) {
570
- // swap changes reference
571
- const changes = this.changes;
572
- this.changes = this.filteredChanges;
573
- this.filteredChanges = changes;
574
- // swap "all changes" reference
575
- const allFilteredChanges = this.allFilteredChanges;
576
- this.allFilteredChanges = this.allChanges;
577
- this.allChanges = allFilteredChanges;
578
- }
623
+ // skip if parent is not set
624
+ if (!parent) {
625
+ return;
626
+ }
627
+ if (!Metadata.isValidInstance(parent)) {
628
+ const parentChangeTree = parent[$changes];
629
+ parent = parentChangeTree.parent;
630
+ parentIndex = parentChangeTree.parentIndex;
631
+ }
632
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
633
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
634
+ //
635
+ // TODO: refactor this!
636
+ //
637
+ // swapping `changes` and `filteredChanges` is required here
638
+ // because "isFiltered" may not be imedialely available on `change()`
639
+ //
640
+ if (this.isFiltered) {
641
+ this.filteredChanges = { indexes: {}, operations: [] };
642
+ this.allFilteredChanges = { indexes: {}, operations: [] };
643
+ if (this.changes.operations.length > 0) {
644
+ // swap changes reference
645
+ const changes = this.changes;
646
+ this.changes = this.filteredChanges;
647
+ this.filteredChanges = changes;
648
+ // swap "all changes" reference
649
+ const allFilteredChanges = this.allFilteredChanges;
650
+ this.allFilteredChanges = this.allChanges;
651
+ this.allChanges = allFilteredChanges;
652
+ // console.log("SWAP =>", {
653
+ // "this.allFilteredChanges": this.allFilteredChanges,
654
+ // "this.allChanges": this.allChanges
655
+ // })
579
656
  }
580
657
  }
581
658
  }
@@ -644,21 +721,24 @@ function utf8Write(view, str, it) {
644
721
  view[it.offset++] = c;
645
722
  }
646
723
  else if (c < 0x800) {
647
- view[it.offset++] = 0xc0 | (c >> 6);
648
- view[it.offset++] = 0x80 | (c & 0x3f);
724
+ view[it.offset] = 0xc0 | (c >> 6);
725
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
726
+ it.offset += 2;
649
727
  }
650
728
  else if (c < 0xd800 || c >= 0xe000) {
651
- view[it.offset++] = 0xe0 | (c >> 12);
652
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
653
- view[it.offset++] = 0x80 | (c & 0x3f);
729
+ view[it.offset] = 0xe0 | (c >> 12);
730
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
731
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
732
+ it.offset += 3;
654
733
  }
655
734
  else {
656
735
  i++;
657
736
  c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
658
- view[it.offset++] = 0xf0 | (c >> 18);
659
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
660
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
661
- view[it.offset++] = 0x80 | (c & 0x3f);
737
+ view[it.offset] = 0xf0 | (c >> 18);
738
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
739
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
740
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
741
+ it.offset += 4;
662
742
  }
663
743
  }
664
744
  }
@@ -887,7 +967,7 @@ function encodeValue(encoder, bytes, type, value, operation, it) {
887
967
  * Used for Schema instances.
888
968
  * @private
889
969
  */
890
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
970
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
891
971
  // "compress" field index + operation
892
972
  bytes[it.offset++] = (index | operation) & 255;
893
973
  // Do not encode value for DELETE operations
@@ -895,7 +975,7 @@ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, opera
895
975
  return;
896
976
  }
897
977
  const ref = changeTree.ref;
898
- const metadata = ref.constructor[Symbol.metadata];
978
+ // const metadata: Metadata = ref.constructor[Symbol.metadata];
899
979
  const field = metadata[index];
900
980
  // TODO: inline this function call small performance gain
901
981
  encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
@@ -1590,6 +1670,7 @@ class ArraySchema {
1590
1670
  const proxy = new Proxy(this, {
1591
1671
  get: (obj, prop) => {
1592
1672
  if (typeof (prop) !== "symbol" &&
1673
+ // FIXME: d8 accuses this as low performance
1593
1674
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1594
1675
  ) {
1595
1676
  return this.items[prop];
@@ -1607,7 +1688,7 @@ class ArraySchema {
1607
1688
  if (setValue[$changes]) {
1608
1689
  assertInstanceType(setValue, obj[$childType], obj, key);
1609
1690
  if (obj.items[key] !== undefined) {
1610
- if (setValue[$changes][$isNew]) {
1691
+ if (setValue[$changes].isNew) {
1611
1692
  this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
1612
1693
  }
1613
1694
  else {
@@ -1619,7 +1700,7 @@ class ArraySchema {
1619
1700
  }
1620
1701
  }
1621
1702
  }
1622
- else if (setValue[$changes][$isNew]) {
1703
+ else if (setValue[$changes].isNew) {
1623
1704
  this[$changes].indexedOperation(Number(key), OPERATION.ADD);
1624
1705
  }
1625
1706
  }
@@ -1653,7 +1734,9 @@ class ArraySchema {
1653
1734
  });
1654
1735
  this[$changes] = new ChangeTree(proxy);
1655
1736
  this[$changes].indexes = {};
1656
- this.push.apply(this, items);
1737
+ if (items.length > 0) {
1738
+ this.push(...items);
1739
+ }
1657
1740
  return proxy;
1658
1741
  }
1659
1742
  set length(newLength) {
@@ -1672,15 +1755,18 @@ class ArraySchema {
1672
1755
  }
1673
1756
  push(...values) {
1674
1757
  let length = this.tmpItems.length;
1675
- values.forEach((value, i) => {
1676
- // skip null values
1758
+ const changeTree = this[$changes];
1759
+ // values.forEach((value, i) => {
1760
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1761
+ const value = values[i];
1677
1762
  if (value === undefined || value === null) {
1763
+ // skip null values
1678
1764
  return;
1679
1765
  }
1680
1766
  else if (typeof (value) === "object" && this[$childType]) {
1681
1767
  assertInstanceType(value, this[$childType], this, i);
1768
+ // TODO: move value[$changes]?.setParent() to this block.
1682
1769
  }
1683
- const changeTree = this[$changes];
1684
1770
  changeTree.indexedOperation(length, OPERATION.ADD, this.items.length);
1685
1771
  this.items.push(value);
1686
1772
  this.tmpItems.push(value);
@@ -1689,8 +1775,9 @@ class ArraySchema {
1689
1775
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1690
1776
  //
1691
1777
  value[$changes]?.setParent(this, changeTree.root, length);
1692
- length++;
1693
- });
1778
+ }
1779
+ // length++;
1780
+ // });
1694
1781
  return length;
1695
1782
  }
1696
1783
  /**
@@ -1711,6 +1798,7 @@ class ArraySchema {
1711
1798
  }
1712
1799
  this[$changes].delete(index, undefined, this.items.length - 1);
1713
1800
  // this.tmpItems[index] = undefined;
1801
+ // this.tmpItems.pop();
1714
1802
  this.deletedIndexes[index] = true;
1715
1803
  return this.items.pop();
1716
1804
  }
@@ -1775,9 +1863,12 @@ class ArraySchema {
1775
1863
  //
1776
1864
  // TODO: do not use [$changes] at decoding time.
1777
1865
  //
1778
- changeTree.root?.changes.delete(changeTree);
1779
- changeTree.root?.allChanges.delete(changeTree);
1780
- changeTree.root?.allFilteredChanges.delete(changeTree);
1866
+ const root = changeTree.root;
1867
+ if (root !== undefined) {
1868
+ root.removeChangeFromChangeSet("changes", changeTree);
1869
+ root.removeChangeFromChangeSet("allChanges", changeTree);
1870
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
1871
+ }
1781
1872
  });
1782
1873
  changeTree.discard(true);
1783
1874
  changeTree.operation(OPERATION.CLEAR);
@@ -1821,6 +1912,7 @@ class ArraySchema {
1821
1912
  const changeTree = this[$changes];
1822
1913
  changeTree.delete(index);
1823
1914
  changeTree.shiftAllChangeIndexes(-1, index);
1915
+ // this.deletedIndexes[index] = true;
1824
1916
  return this.items.shift();
1825
1917
  }
1826
1918
  /**
@@ -1901,10 +1993,12 @@ class ArraySchema {
1901
1993
  changeTree.shiftChangeIndexes(items.length);
1902
1994
  // new index
1903
1995
  if (changeTree.isFiltered) {
1904
- changeTree.filteredChanges.set(this.items.length, OPERATION.ADD);
1996
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
1997
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1905
1998
  }
1906
1999
  else {
1907
- changeTree.allChanges.set(this.items.length, OPERATION.ADD);
2000
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2001
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1908
2002
  }
1909
2003
  // FIXME: should we use OPERATION.MOVE here instead?
1910
2004
  items.forEach((_, index) => {
@@ -2245,7 +2339,7 @@ class MapSchema {
2245
2339
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2246
2340
  const index = (isReplace)
2247
2341
  ? changeTree.indexes[key]
2248
- : changeTree.indexes[-1] ?? 0;
2342
+ : changeTree.indexes[$numFields] ?? 0;
2249
2343
  let operation = (isReplace)
2250
2344
  ? OPERATION.REPLACE
2251
2345
  : OPERATION.ADD;
@@ -2257,7 +2351,7 @@ class MapSchema {
2257
2351
  if (!isReplace) {
2258
2352
  this.$indexes.set(index, key);
2259
2353
  changeTree.indexes[key] = index;
2260
- changeTree.indexes[-1] = index + 1;
2354
+ changeTree.indexes[$numFields] = index + 1;
2261
2355
  }
2262
2356
  else if (!isRef &&
2263
2357
  this.$items.get(key) === value) {
@@ -2332,8 +2426,11 @@ class MapSchema {
2332
2426
  }
2333
2427
  [$onEncodeEnd]() {
2334
2428
  const changeTree = this[$changes];
2335
- const changes = changeTree.changes.entries();
2336
- for (const [fieldIndex, operation] of changes) {
2429
+ const keys = Object.keys(changeTree.indexedOperations);
2430
+ for (let i = 0, len = keys.length; i < len; i++) {
2431
+ const key = keys[i];
2432
+ const fieldIndex = Number(key);
2433
+ const operation = changeTree.indexedOperations[key];
2337
2434
  if (operation === OPERATION.DELETE) {
2338
2435
  const index = this[$getByIndex](fieldIndex);
2339
2436
  delete changeTree.indexes[index];
@@ -2436,7 +2533,7 @@ class TypeContext {
2436
2533
  });
2437
2534
  const metadata = (klass[Symbol.metadata] ??= {});
2438
2535
  // if any schema/field has filters, mark "context" as having filters.
2439
- if (metadata[-2]) {
2536
+ if (metadata[$viewFieldIndexes]) {
2440
2537
  this.hasFilters = true;
2441
2538
  }
2442
2539
  if (parentFieldViewTag !== undefined) {
@@ -2499,8 +2596,8 @@ const DEFAULT_VIEW_TAG = -1;
2499
2596
  // // detect index for this field, considering inheritance
2500
2597
  // //
2501
2598
  // const parent = Object.getPrototypeOf(context.metadata);
2502
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2503
- // ?? (parent && parent[-1]) // parent structure has fields defined
2599
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2600
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2504
2601
  // ?? -1; // no fields defined
2505
2602
  // fieldIndex++;
2506
2603
  // if (
@@ -2629,8 +2726,8 @@ function view(tag = DEFAULT_VIEW_TAG) {
2629
2726
  // //
2630
2727
  // metadata[fieldIndex] = {
2631
2728
  // type: undefined,
2632
- // index: (metadata[-1] // current structure already has fields defined
2633
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2729
+ // index: (metadata[$numFields] // current structure already has fields defined
2730
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2634
2731
  // ?? -1) + 1 // no fields defined
2635
2732
  // }
2636
2733
  // }
@@ -2673,8 +2770,8 @@ function type(type, options) {
2673
2770
  //
2674
2771
  // detect index for this field, considering inheritance
2675
2772
  //
2676
- fieldIndex = metadata[-1] // current structure already has fields defined
2677
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2773
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2774
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2678
2775
  ?? -1; // no fields defined
2679
2776
  fieldIndex++;
2680
2777
  }
@@ -2701,7 +2798,7 @@ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass)
2701
2798
  return {
2702
2799
  get: function () { return this[fieldCached]; },
2703
2800
  set: function (value) {
2704
- const previousValue = this[fieldCached] || undefined;
2801
+ const previousValue = this[fieldCached] ?? undefined;
2705
2802
  // skip if value is the same as cached.
2706
2803
  if (value === previousValue) {
2707
2804
  return;
@@ -2773,8 +2870,8 @@ function deprecated(throws = true) {
2773
2870
  // //
2774
2871
  // metadata[field] = {
2775
2872
  // type: undefined,
2776
- // index: (metadata[-1] // current structure already has fields defined
2777
- // ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2873
+ // index: (metadata[$numFields] // current structure already has fields defined
2874
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2778
2875
  // ?? -1) + 1 // no fields defined
2779
2876
  // }
2780
2877
  // }
@@ -2812,15 +2909,18 @@ function dumpChanges(schema) {
2812
2909
  ops: {},
2813
2910
  refs: []
2814
2911
  };
2815
- $root.changes.forEach((operations, changeTree) => {
2912
+ // for (const refId in $root.changes) {
2913
+ $root.changes.forEach(changeTree => {
2914
+ const changes = changeTree.indexedOperations;
2816
2915
  dump.refs.push(`refId#${changeTree.refId}`);
2817
- operations.forEach((op, index) => {
2916
+ for (const index in changes) {
2917
+ const op = changes[index];
2818
2918
  const opName = OPERATION[op];
2819
2919
  if (!dump.ops[opName]) {
2820
2920
  dump.ops[opName] = 0;
2821
2921
  }
2822
2922
  dump.ops[OPERATION[op]]++;
2823
- });
2923
+ }
2824
2924
  });
2825
2925
  return dump;
2826
2926
  }
@@ -2846,6 +2946,7 @@ var _a$2, _b$2;
2846
2946
  class Schema {
2847
2947
  static { this[_a$2] = encodeSchemaOperation; }
2848
2948
  static { this[_b$2] = decodeSchemaOperation; }
2949
+ // public [$changes]: ChangeTree;
2849
2950
  /**
2850
2951
  * Assign the property descriptors required to track changes on this instance.
2851
2952
  * @param instance
@@ -2905,12 +3006,7 @@ class Schema {
2905
3006
  // inline
2906
3007
  // Schema.initialize(this);
2907
3008
  //
2908
- Object.defineProperty(this, $changes, {
2909
- value: new ChangeTree(this),
2910
- enumerable: false,
2911
- writable: true
2912
- });
2913
- Object.defineProperties(this, this.constructor[Symbol.metadata]?.[$descriptors] || {});
3009
+ Schema.initialize(this);
2914
3010
  //
2915
3011
  // Assign initial values
2916
3012
  //
@@ -2984,7 +3080,7 @@ class Schema {
2984
3080
  const changeTree = ref[$changes];
2985
3081
  const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2986
3082
  let output = "";
2987
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
3083
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
2988
3084
  changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
2989
3085
  return output;
2990
3086
  }
@@ -3002,18 +3098,26 @@ class Schema {
3002
3098
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
3003
3099
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
3004
3100
  function dumpChangeSet(changeSet) {
3005
- Array.from(changeSet)
3006
- .sort((a, b) => a[0] - b[0])
3007
- .forEach(([index, operation]) => output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3101
+ changeSet.operations
3102
+ .filter(op => op)
3103
+ .forEach((index) => {
3104
+ const operation = changeTree.indexedOperations[index];
3105
+ console.log({ index, operation });
3106
+ output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3107
+ });
3008
3108
  }
3009
3109
  dumpChangeSet(changeSet);
3010
3110
  // display filtered changes
3011
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3111
+ if (!isEncodeAll &&
3112
+ changeTree.filteredChanges &&
3113
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
3012
3114
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
3013
3115
  dumpChangeSet(changeTree.filteredChanges);
3014
3116
  }
3015
3117
  // display filtered changes
3016
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3118
+ if (isEncodeAll &&
3119
+ changeTree.allFilteredChanges &&
3120
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
3017
3121
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
3018
3122
  dumpChangeSet(changeTree.allFilteredChanges);
3019
3123
  }
@@ -3022,10 +3126,12 @@ class Schema {
3022
3126
  static debugChangesDeep(ref, changeSetName = "changes") {
3023
3127
  let output = "";
3024
3128
  const rootChangeTree = ref[$changes];
3129
+ const root = rootChangeTree.root;
3025
3130
  const changeTrees = new Map();
3026
3131
  let totalInstances = 0;
3027
3132
  let totalOperations = 0;
3028
- for (const [changeTree, changes] of (rootChangeTree.root[changeSetName].entries())) {
3133
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3134
+ const changeTree = root.changeTrees[refId];
3029
3135
  let includeChangeTree = false;
3030
3136
  let parentChangeTrees = [];
3031
3137
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -3044,7 +3150,7 @@ class Schema {
3044
3150
  }
3045
3151
  if (includeChangeTree) {
3046
3152
  totalInstances += 1;
3047
- totalOperations += changes.size;
3153
+ totalOperations += Object.keys(changes).length;
3048
3154
  changeTrees.set(changeTree, parentChangeTrees.reverse());
3049
3155
  }
3050
3156
  }
@@ -3062,12 +3168,13 @@ class Schema {
3062
3168
  visitedParents.add(parentChangeTree);
3063
3169
  }
3064
3170
  });
3065
- const changes = changeTree.changes;
3171
+ const changes = changeTree.indexedOperations;
3066
3172
  const level = parentChangeTrees.length;
3067
3173
  const indent = getIndent(level);
3068
3174
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3069
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3070
- for (const [index, operation] of changes) {
3175
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3176
+ for (const index in changes) {
3177
+ const operation = changes[index];
3071
3178
  output += `${getIndent(level + 1)}${OPERATION[operation]}: ${index}\n`;
3072
3179
  }
3073
3180
  }
@@ -3428,59 +3535,90 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
3428
3535
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3429
3536
  };
3430
3537
 
3538
+ function spliceOne(arr, index) {
3539
+ // manually splice an array
3540
+ if (index === -1 || index >= arr.length) {
3541
+ return false;
3542
+ }
3543
+ const len = arr.length - 1;
3544
+ for (let i = index; i < len; i++) {
3545
+ arr[i] = arr[i + 1];
3546
+ }
3547
+ arr.length = len;
3548
+ return true;
3549
+ }
3550
+
3431
3551
  class Root {
3432
3552
  constructor(types) {
3433
3553
  this.types = types;
3434
3554
  this.nextUniqueId = 0;
3435
- this.refCount = new WeakMap();
3555
+ this.refCount = {};
3556
+ this.changeTrees = {};
3436
3557
  // all changes
3437
- this.allChanges = new Map();
3438
- this.allFilteredChanges = new Map();
3558
+ this.allChanges = [];
3559
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3439
3560
  // pending changes to be encoded
3440
- this.changes = new Map();
3441
- this.filteredChanges = new Map();
3561
+ this.changes = [];
3562
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3442
3563
  }
3443
3564
  getNextUniqueId() {
3444
3565
  return this.nextUniqueId++;
3445
3566
  }
3446
3567
  add(changeTree) {
3447
- const previousRefCount = this.refCount.get(changeTree);
3568
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3569
+ changeTree.ensureRefId();
3570
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3571
+ if (isNewChangeTree) {
3572
+ this.changeTrees[changeTree.refId] = changeTree;
3573
+ }
3574
+ const previousRefCount = this.refCount[changeTree.refId];
3448
3575
  if (previousRefCount === 0) {
3449
3576
  //
3450
3577
  // When a ChangeTree is re-added, it means that it was previously removed.
3451
3578
  // We need to re-add all changes to the `changes` map.
3452
3579
  //
3453
- changeTree.allChanges.forEach((operation, index) => {
3454
- changeTree.changes.set(index, operation);
3455
- });
3580
+ const ops = changeTree.allChanges.operations;
3581
+ let len = ops.length;
3582
+ while (len--) {
3583
+ changeTree.indexedOperations[ops[len]] = OPERATION.ADD;
3584
+ setOperationAtIndex(changeTree.changes, len);
3585
+ }
3456
3586
  }
3457
- const refCount = (previousRefCount || 0) + 1;
3458
- this.refCount.set(changeTree, refCount);
3459
- return refCount;
3587
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3588
+ return isNewChangeTree;
3460
3589
  }
3461
3590
  remove(changeTree) {
3462
- const refCount = (this.refCount.get(changeTree)) - 1;
3591
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3463
3592
  if (refCount <= 0) {
3464
3593
  //
3465
3594
  // Only remove "root" reference if it's the last reference
3466
3595
  //
3467
3596
  changeTree.root = undefined;
3468
- this.allChanges.delete(changeTree);
3469
- this.changes.delete(changeTree);
3597
+ delete this.changeTrees[changeTree.refId];
3598
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3599
+ this.removeChangeFromChangeSet("changes", changeTree);
3470
3600
  if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3471
- this.allFilteredChanges.delete(changeTree);
3472
- this.filteredChanges.delete(changeTree);
3601
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3602
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3473
3603
  }
3474
- this.refCount.set(changeTree, 0);
3604
+ this.refCount[changeTree.refId] = 0;
3475
3605
  }
3476
3606
  else {
3477
- this.refCount.set(changeTree, refCount);
3607
+ this.refCount[changeTree.refId] = refCount;
3478
3608
  }
3479
3609
  changeTree.forEachChild((child, _) => this.remove(child));
3480
3610
  return refCount;
3481
3611
  }
3612
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3613
+ const changeSet = this[changeSetName];
3614
+ const index = changeSet.indexOf(changeTree);
3615
+ if (index !== -1) {
3616
+ spliceOne(changeSet, index);
3617
+ // changeSet[index] = undefined;
3618
+ }
3619
+ }
3482
3620
  clear() {
3483
- this.changes.clear();
3621
+ this.changes.length = 0;
3484
3622
  }
3485
3623
  }
3486
3624
 
@@ -3504,20 +3642,26 @@ class Encoder {
3504
3642
  this.state = state;
3505
3643
  this.state[$changes].setRoot(this.root);
3506
3644
  }
3507
- encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes, isEncodeAll = this.root.allChanges === changeTrees, initialOffset = it.offset // cache current offset in case we need to resize the buffer
3645
+ encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeSetName = "changes", isEncodeAll = changeSetName === "allChanges", initialOffset = it.offset // cache current offset in case we need to resize the buffer
3508
3646
  ) {
3509
3647
  const hasView = (view !== undefined);
3510
3648
  const rootChangeTree = this.state[$changes];
3511
- const shouldClearChanges = !isEncodeAll && !hasView;
3512
- for (const [changeTree, changes] of changeTrees.entries()) {
3649
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3650
+ const changeTrees = this.root[changeSetName];
3651
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3652
+ const changeTree = changeTrees[i];
3653
+ // // Root#removeChangeFromChangeSet() is now removing instead of setting to "undefined"
3654
+ // if (changeTree === undefined) { continue; }
3655
+ const operations = changeTree[changeSetName];
3513
3656
  const ref = changeTree.ref;
3514
3657
  const ctor = ref.constructor;
3515
3658
  const encoder = ctor[$encoder];
3516
3659
  const filter = ctor[$filter];
3660
+ const metadata = ctor[Symbol.metadata];
3517
3661
  // try { throw new Error(); } catch (e) {
3518
3662
  // // only print if not coming from Reflection.ts
3519
3663
  // if (!e.stack.includes("src/Reflection.ts")) {
3520
- // console.log("ChangeTree:", { ref: ref.constructor.name, });
3664
+ // console.log("ChangeTree:", { refId: changeTree.refId, ref: ref.constructor.name });
3521
3665
  // }
3522
3666
  // }
3523
3667
  if (hasView) {
@@ -3535,7 +3679,13 @@ class Encoder {
3535
3679
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3536
3680
  number$1(buffer, changeTree.refId, it);
3537
3681
  }
3538
- for (const [fieldIndex, operation] of changes.entries()) {
3682
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3683
+ const fieldIndex = operations.operations[j];
3684
+ const operation = (fieldIndex < 0)
3685
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3686
+ : (isEncodeAll)
3687
+ ? OPERATION.ADD
3688
+ : changeTree.indexedOperations[fieldIndex];
3539
3689
  //
3540
3690
  // first pass (encodeAll), identify "filtered" operations without encoding them
3541
3691
  // they will be encoded per client, based on their view.
@@ -3543,7 +3693,7 @@ class Encoder {
3543
3693
  // TODO: how can we optimize filtering out "encode all" operations?
3544
3694
  // TODO: avoid checking if no view tags were defined
3545
3695
  //
3546
- if (filter && !filter(ref, fieldIndex, view)) {
3696
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3547
3697
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3548
3698
  // view?.invisible.add(changeTree);
3549
3699
  continue;
@@ -3558,16 +3708,14 @@ class Encoder {
3558
3708
  // });
3559
3709
  // }
3560
3710
  // }
3561
- encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3711
+ // console.log("encode...", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, fieldIndex, operation });
3712
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3713
+ }
3714
+ if (shouldDiscardChanges) {
3715
+ changeTree.discard();
3716
+ // Not a new instance anymore
3717
+ changeTree.isNew = false;
3562
3718
  }
3563
- // if (shouldClearChanges) {
3564
- // // changeTree.endEncode();
3565
- // changeTree.changes.clear();
3566
- // // ArraySchema and MapSchema have a custom "encode end" method
3567
- // changeTree.ref[$onEncodeEnd]?.();
3568
- // // Not a new instance anymore
3569
- // delete changeTree[$isNew];
3570
- // }
3571
3719
  }
3572
3720
  if (it.offset > buffer.byteLength) {
3573
3721
  const newSize = getNextPowerOf2(buffer.byteLength * 2);
@@ -3584,51 +3732,54 @@ class Encoder {
3584
3732
  if (buffer === this.sharedBuffer) {
3585
3733
  this.sharedBuffer = buffer;
3586
3734
  }
3587
- return this.encode({ offset: initialOffset }, view, buffer, changeTrees, isEncodeAll);
3735
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3588
3736
  }
3589
3737
  else {
3590
- //
3591
- // only clear changes after making sure buffer resize is not required.
3592
- //
3593
- if (shouldClearChanges) {
3594
- //
3595
- // FIXME: avoid iterating over change trees twice.
3596
- //
3597
- this.onEndEncode(changeTrees);
3598
- }
3738
+ // //
3739
+ // // only clear changes after making sure buffer resize is not required.
3740
+ // //
3741
+ // if (shouldClearChanges) {
3742
+ // //
3743
+ // // FIXME: avoid iterating over change trees twice.
3744
+ // //
3745
+ // this.onEndEncode(changeTrees);
3746
+ // }
3599
3747
  return buffer.subarray(0, it.offset);
3600
3748
  }
3601
3749
  }
3602
3750
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3603
- // console.log(`\nencodeAll(), this.root.allChanges (${this.root.allChanges.size})`);
3751
+ // console.log(`\nencodeAll(), this.root.allChanges (${(Object.keys(this.root.allChanges).length)})`);
3604
3752
  // this.debugChanges("allChanges");
3605
- return this.encode(it, undefined, buffer, this.root.allChanges, true);
3753
+ return this.encode(it, undefined, buffer, "allChanges", true);
3606
3754
  }
3607
3755
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3608
3756
  const viewOffset = it.offset;
3609
- // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
3757
+ // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${(Object.keys(this.root.allFilteredChanges).length)})`);
3610
3758
  // this.debugChanges("allFilteredChanges");
3759
+ // console.log("\n\nENCODE ALL FOR VIEW...\n\n")
3611
3760
  // try to encode "filtered" changes
3612
- this.encode(it, view, bytes, this.root.allFilteredChanges, true, viewOffset);
3761
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3613
3762
  return Buffer.concat([
3614
3763
  bytes.subarray(0, sharedOffset),
3615
3764
  bytes.subarray(viewOffset, it.offset)
3616
3765
  ]);
3617
3766
  }
3618
3767
  debugChanges(field) {
3619
- const changeSet = (typeof (field) === "string")
3768
+ const rootChangeSet = (typeof (field) === "string")
3620
3769
  ? this.root[field]
3621
3770
  : field;
3622
- Array.from(changeSet.entries()).map((item) => {
3623
- const metadata = item[0].ref.constructor[Symbol.metadata];
3624
- console.log("->", { ref: item[0].ref.constructor.name, refId: item[0].refId, changes: item[1].size });
3625
- item[1].forEach((op, index) => {
3771
+ rootChangeSet.forEach((changeTree) => {
3772
+ const changeSet = changeTree[field];
3773
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3774
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3775
+ for (const index in changeSet) {
3776
+ const op = changeSet[index];
3626
3777
  console.log(" ->", {
3627
3778
  index,
3628
3779
  field: metadata?.[index],
3629
3780
  op: OPERATION[op],
3630
3781
  });
3631
- });
3782
+ }
3632
3783
  });
3633
3784
  }
3634
3785
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
@@ -3638,23 +3789,31 @@ class Encoder {
3638
3789
  // console.log(`\nencodeView(), this.root.filteredChanges (${this.root.filteredChanges.size})`);
3639
3790
  // this.debugChanges("filteredChanges");
3640
3791
  // encode visibility changes (add/remove for this view)
3641
- const viewChangesIterator = view.changes.entries();
3642
- for (const [changeTree, changes] of viewChangesIterator) {
3643
- if (changes.size === 0) {
3644
- // FIXME: avoid having empty changes if no changes were made
3645
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3792
+ const refIds = Object.keys(view.changes);
3793
+ // console.log("ENCODE VIEW:", refIds);
3794
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3795
+ const refId = refIds[i];
3796
+ const changes = view.changes[refId];
3797
+ const changeTree = this.root.changeTrees[refId];
3798
+ if (changeTree === undefined ||
3799
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3800
+ ) {
3801
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3646
3802
  continue;
3647
3803
  }
3648
3804
  const ref = changeTree.ref;
3649
- const ctor = ref['constructor'];
3805
+ const ctor = ref.constructor;
3650
3806
  const encoder = ctor[$encoder];
3807
+ const metadata = ctor[Symbol.metadata];
3651
3808
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3652
3809
  number$1(bytes, changeTree.refId, it);
3653
- const changesIterator = changes.entries();
3654
- for (const [fieldIndex, operation] of changesIterator) {
3810
+ const keys = Object.keys(changes);
3811
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3812
+ const key = keys[i];
3813
+ const operation = changes[key];
3655
3814
  // isEncodeAll = false
3656
3815
  // hasView = true
3657
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3816
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3658
3817
  }
3659
3818
  }
3660
3819
  //
@@ -3662,35 +3821,46 @@ class Encoder {
3662
3821
  // (to allow re-using StateView's for multiple clients)
3663
3822
  //
3664
3823
  // clear "view" changes after encoding
3665
- view.changes.clear();
3824
+ view.changes = {};
3825
+ // console.log("FILTERED CHANGES:", this.root.filteredChanges);
3666
3826
  // try to encode "filtered" changes
3667
- this.encode(it, view, bytes, this.root.filteredChanges, false, viewOffset);
3827
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3668
3828
  return Buffer.concat([
3669
3829
  bytes.subarray(0, sharedOffset),
3670
3830
  bytes.subarray(viewOffset, it.offset)
3671
3831
  ]);
3672
3832
  }
3673
3833
  onEndEncode(changeTrees = this.root.changes) {
3674
- const changeTreesIterator = changeTrees.entries();
3675
- for (const [changeTree, _] of changeTreesIterator) {
3676
- changeTree.endEncode();
3677
- // changeTree.changes.clear();
3678
- // // ArraySchema and MapSchema have a custom "encode end" method
3679
- // changeTree.ref[$onEncodeEnd]?.();
3680
- // // Not a new instance anymore
3681
- // delete changeTree[$isNew];
3682
- }
3834
+ // changeTrees.forEach(function(changeTree) {
3835
+ // changeTree.endEncode();
3836
+ // });
3837
+ // for (const refId in changeTrees) {
3838
+ // const changeTree = this.root.changeTrees[refId];
3839
+ // changeTree.endEncode();
3840
+ // // changeTree.changes.clear();
3841
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3842
+ // // changeTree.ref[$onEncodeEnd]?.();
3843
+ // // // Not a new instance anymore
3844
+ // // delete changeTree[$isNew];
3845
+ // }
3683
3846
  }
3684
3847
  discardChanges() {
3848
+ // console.log("DISCARD CHANGES!");
3685
3849
  // discard shared changes
3686
- if (this.root.changes.size > 0) {
3687
- this.onEndEncode(this.root.changes);
3688
- this.root.changes.clear();
3850
+ let length = this.root.changes.length;
3851
+ if (length > 0) {
3852
+ while (length--) {
3853
+ this.root.changes[length]?.endEncode();
3854
+ }
3855
+ this.root.changes.length = 0;
3689
3856
  }
3690
3857
  // discard filtered changes
3691
- if (this.root.filteredChanges.size > 0) {
3692
- this.onEndEncode(this.root.filteredChanges);
3693
- this.root.filteredChanges.clear();
3858
+ length = this.root.filteredChanges.length;
3859
+ if (length > 0) {
3860
+ while (length--) {
3861
+ this.root.filteredChanges[length]?.endEncode();
3862
+ }
3863
+ this.root.filteredChanges.length = 0;
3694
3864
  }
3695
3865
  }
3696
3866
  tryEncodeTypeId(bytes, baseType, targetType, it) {
@@ -3703,19 +3873,6 @@ class Encoder {
3703
3873
  }
3704
3874
  }
3705
3875
 
3706
- function spliceOne(arr, index) {
3707
- // manually splice an array
3708
- if (index === -1 || index >= arr.length) {
3709
- return false;
3710
- }
3711
- const len = arr.length - 1;
3712
- for (let i = index; i < len; i++) {
3713
- arr[i] = arr[i + 1];
3714
- }
3715
- arr.length = len;
3716
- return true;
3717
- }
3718
-
3719
3876
  class DecodingWarning extends Error {
3720
3877
  constructor(message) {
3721
3878
  super(message);
@@ -3877,7 +4034,7 @@ class Decoder {
3877
4034
  }
3878
4035
  ref[$onDecodeEnd]?.();
3879
4036
  ref = nextRef;
3880
- decoder = ref['constructor'][$decoder];
4037
+ decoder = ref.constructor[$decoder];
3881
4038
  continue;
3882
4039
  }
3883
4040
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -4354,7 +4511,7 @@ class StateView {
4354
4511
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
4355
4512
  * (This is used to force encoding a property, even if it was not changed)
4356
4513
  */
4357
- this.changes = new Map();
4514
+ this.changes = {};
4358
4515
  }
4359
4516
  // TODO: allow to set multiple tags at once
4360
4517
  add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
@@ -4376,10 +4533,10 @@ class StateView {
4376
4533
  // TODO: when adding an item of a MapSchema, the changes may not
4377
4534
  // be set (only the parent's changes are set)
4378
4535
  //
4379
- let changes = this.changes.get(changeTree);
4536
+ let changes = this.changes[changeTree.refId];
4380
4537
  if (changes === undefined) {
4381
- changes = new Map();
4382
- this.changes.set(changeTree, changes);
4538
+ changes = {};
4539
+ this.changes[changeTree.refId] = changes;
4383
4540
  }
4384
4541
  // set tag
4385
4542
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -4396,9 +4553,9 @@ class StateView {
4396
4553
  }
4397
4554
  tags.add(tag);
4398
4555
  // Ref: add tagged properties
4399
- metadata?.[-3]?.[tag]?.forEach((index) => {
4556
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4400
4557
  if (changeTree.getChange(index) !== OPERATION.DELETE) {
4401
- changes.set(index, OPERATION.ADD);
4558
+ changes[index] = OPERATION.ADD;
4402
4559
  }
4403
4560
  });
4404
4561
  }
@@ -4407,16 +4564,21 @@ class StateView {
4407
4564
  const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4408
4565
  ? changeTree.allFilteredChanges
4409
4566
  : changeTree.allChanges;
4410
- changeSet.forEach((op, index) => {
4567
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4568
+ const index = changeSet.operations[i];
4569
+ if (index === undefined) {
4570
+ continue;
4571
+ } // skip "undefined" indexes
4572
+ const op = changeTree.indexedOperations[index];
4411
4573
  const tagAtIndex = metadata?.[index].tag;
4412
4574
  if ((isInvisible || // if "invisible", include all
4413
4575
  tagAtIndex === undefined || // "all change" with no tag
4414
4576
  tagAtIndex === tag // tagged property
4415
4577
  ) &&
4416
4578
  op !== OPERATION.DELETE) {
4417
- changes.set(index, op);
4579
+ changes[index] = op;
4418
4580
  }
4419
- });
4581
+ }
4420
4582
  }
4421
4583
  // Add children of this ChangeTree to this view
4422
4584
  changeTree.forEachChild((change, index) => {
@@ -4442,10 +4604,10 @@ class StateView {
4442
4604
  }
4443
4605
  // add parent's tag properties
4444
4606
  if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
4445
- let changes = this.changes.get(changeTree);
4607
+ let changes = this.changes[changeTree.refId];
4446
4608
  if (changes === undefined) {
4447
- changes = new Map();
4448
- this.changes.set(changeTree, changes);
4609
+ changes = {};
4610
+ this.changes[changeTree.refId] = changes;
4449
4611
  }
4450
4612
  if (!this.tags) {
4451
4613
  this.tags = new WeakMap();
@@ -4459,7 +4621,7 @@ class StateView {
4459
4621
  tags = this.tags.get(changeTree);
4460
4622
  }
4461
4623
  tags.add(tag);
4462
- changes.set(parentIndex, OPERATION.ADD);
4624
+ changes[parentIndex] = OPERATION.ADD;
4463
4625
  }
4464
4626
  }
4465
4627
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4471,32 +4633,32 @@ class StateView {
4471
4633
  this.items.delete(changeTree);
4472
4634
  const ref = changeTree.ref;
4473
4635
  const metadata = ref.constructor[Symbol.metadata];
4474
- let changes = this.changes.get(changeTree);
4636
+ let changes = this.changes[changeTree.refId];
4475
4637
  if (changes === undefined) {
4476
- changes = new Map();
4477
- this.changes.set(changeTree, changes);
4638
+ changes = {};
4639
+ this.changes[changeTree.refId] = changes;
4478
4640
  }
4479
4641
  if (tag === DEFAULT_VIEW_TAG) {
4480
4642
  // parent is collection (Map/Array)
4481
4643
  const parent = changeTree.parent;
4482
4644
  if (!Metadata.isValidInstance(parent)) {
4483
4645
  const parentChangeTree = parent[$changes];
4484
- let changes = this.changes.get(parentChangeTree);
4646
+ let changes = this.changes[parentChangeTree.refId];
4485
4647
  if (changes === undefined) {
4486
- changes = new Map();
4487
- this.changes.set(parentChangeTree, changes);
4648
+ changes = {};
4649
+ this.changes[parentChangeTree.refId] = changes;
4488
4650
  }
4489
4651
  // DELETE / DELETE BY REF ID
4490
- changes.set(changeTree.parentIndex, OPERATION.DELETE);
4652
+ changes[changeTree.parentIndex] = OPERATION.DELETE;
4491
4653
  }
4492
4654
  else {
4493
4655
  // delete all "tagged" properties.
4494
- metadata[-2].forEach((index) => changes.set(index, OPERATION.DELETE));
4656
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = OPERATION.DELETE);
4495
4657
  }
4496
4658
  }
4497
4659
  else {
4498
4660
  // delete only tagged properties
4499
- metadata[-3][tag].forEach((index) => changes.set(index, OPERATION.DELETE));
4661
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = OPERATION.DELETE);
4500
4662
  }
4501
4663
  // remove tag
4502
4664
  if (this.tags && this.tags.has(changeTree)) {