@colyseus/schema 3.0.0-alpha.4 → 3.0.0-alpha.41

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 (146) hide show
  1. package/README.md +148 -62
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +2201 -1507
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +2198 -1506
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +2208 -1514
  8. package/lib/Metadata.d.ts +21 -9
  9. package/lib/Metadata.js +169 -32
  10. package/lib/Metadata.js.map +1 -1
  11. package/lib/Reflection.d.ts +19 -4
  12. package/lib/Reflection.js +66 -32
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +4 -4
  15. package/lib/Schema.js +44 -50
  16. package/lib/Schema.js.map +1 -1
  17. package/lib/annotations.d.ts +31 -34
  18. package/lib/annotations.js +110 -160
  19. package/lib/annotations.js.map +1 -1
  20. package/lib/bench_encode.d.ts +1 -0
  21. package/lib/bench_encode.js +130 -0
  22. package/lib/bench_encode.js.map +1 -0
  23. package/lib/codegen/api.js +1 -2
  24. package/lib/codegen/api.js.map +1 -1
  25. package/lib/codegen/languages/cpp.js +1 -2
  26. package/lib/codegen/languages/cpp.js.map +1 -1
  27. package/lib/codegen/languages/csharp.js +2 -46
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +1 -2
  30. package/lib/codegen/languages/haxe.js.map +1 -1
  31. package/lib/codegen/languages/java.js +1 -2
  32. package/lib/codegen/languages/java.js.map +1 -1
  33. package/lib/codegen/languages/js.js +1 -2
  34. package/lib/codegen/languages/js.js.map +1 -1
  35. package/lib/codegen/languages/lua.js +1 -2
  36. package/lib/codegen/languages/lua.js.map +1 -1
  37. package/lib/codegen/languages/ts.js +1 -2
  38. package/lib/codegen/languages/ts.js.map +1 -1
  39. package/lib/codegen/parser.js +85 -3
  40. package/lib/codegen/parser.js.map +1 -1
  41. package/lib/codegen/types.js +6 -3
  42. package/lib/codegen/types.js.map +1 -1
  43. package/lib/debug.d.ts +1 -0
  44. package/lib/debug.js +51 -0
  45. package/lib/debug.js.map +1 -0
  46. package/lib/decoder/DecodeOperation.d.ts +3 -4
  47. package/lib/decoder/DecodeOperation.js +37 -19
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +6 -7
  50. package/lib/decoder/Decoder.js +14 -14
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +3 -2
  53. package/lib/decoder/ReferenceTracker.js.map +1 -1
  54. package/lib/decoder/strategy/RawChanges.js +1 -2
  55. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  56. package/lib/decoder/strategy/StateCallbacks.d.ts +44 -11
  57. package/lib/decoder/strategy/StateCallbacks.js +75 -65
  58. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  59. package/lib/encoder/ChangeTree.d.ts +27 -21
  60. package/lib/encoder/ChangeTree.js +246 -186
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +51 -65
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +9 -8
  66. package/lib/encoder/Encoder.js +168 -91
  67. package/lib/encoder/Encoder.js.map +1 -1
  68. package/lib/encoder/Root.d.ts +22 -0
  69. package/lib/encoder/Root.js +81 -0
  70. package/lib/encoder/Root.js.map +1 -0
  71. package/lib/encoder/StateView.d.ts +7 -7
  72. package/lib/encoder/StateView.js +70 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +7 -6
  75. package/lib/encoding/assert.js +13 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.d.ts +35 -20
  78. package/lib/encoding/decode.js +43 -87
  79. package/lib/encoding/decode.js.map +1 -1
  80. package/lib/encoding/encode.d.ts +36 -17
  81. package/lib/encoding/encode.js +82 -68
  82. package/lib/encoding/encode.js.map +1 -1
  83. package/lib/encoding/spec.d.ts +4 -5
  84. package/lib/encoding/spec.js +1 -2
  85. package/lib/encoding/spec.js.map +1 -1
  86. package/lib/index.d.ts +10 -9
  87. package/lib/index.js +24 -17
  88. package/lib/index.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +34 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/TypeContext.d.ts +23 -0
  92. package/lib/types/TypeContext.js +111 -0
  93. package/lib/types/TypeContext.js.map +1 -0
  94. package/lib/types/custom/ArraySchema.d.ts +2 -2
  95. package/lib/types/custom/ArraySchema.js +33 -22
  96. package/lib/types/custom/ArraySchema.js.map +1 -1
  97. package/lib/types/custom/CollectionSchema.js +1 -0
  98. package/lib/types/custom/CollectionSchema.js.map +1 -1
  99. package/lib/types/custom/MapSchema.d.ts +3 -1
  100. package/lib/types/custom/MapSchema.js +12 -4
  101. package/lib/types/custom/MapSchema.js.map +1 -1
  102. package/lib/types/custom/SetSchema.js +1 -0
  103. package/lib/types/custom/SetSchema.js.map +1 -1
  104. package/lib/types/registry.d.ts +8 -1
  105. package/lib/types/registry.js +23 -6
  106. package/lib/types/registry.js.map +1 -1
  107. package/lib/types/symbols.d.ts +8 -5
  108. package/lib/types/symbols.js +9 -6
  109. package/lib/types/symbols.js.map +1 -1
  110. package/lib/types/utils.js +1 -2
  111. package/lib/types/utils.js.map +1 -1
  112. package/lib/utils.js +9 -7
  113. package/lib/utils.js.map +1 -1
  114. package/package.json +7 -6
  115. package/src/Metadata.ts +190 -42
  116. package/src/Reflection.ts +77 -39
  117. package/src/Schema.ts +59 -64
  118. package/src/annotations.ts +156 -202
  119. package/src/bench_encode.ts +108 -0
  120. package/src/codegen/languages/csharp.ts +1 -47
  121. package/src/codegen/parser.ts +107 -0
  122. package/src/codegen/types.ts +1 -0
  123. package/src/debug.ts +55 -0
  124. package/src/decoder/DecodeOperation.ts +46 -18
  125. package/src/decoder/Decoder.ts +17 -15
  126. package/src/decoder/ReferenceTracker.ts +3 -2
  127. package/src/decoder/strategy/StateCallbacks.ts +153 -82
  128. package/src/encoder/ChangeTree.ts +286 -202
  129. package/src/encoder/EncodeOperation.ts +78 -78
  130. package/src/encoder/Encoder.ts +202 -97
  131. package/src/encoder/Root.ts +93 -0
  132. package/src/encoder/StateView.ts +76 -88
  133. package/src/encoding/assert.ts +17 -8
  134. package/src/encoding/decode.ts +62 -97
  135. package/src/encoding/encode.ts +99 -65
  136. package/src/encoding/spec.ts +3 -5
  137. package/src/index.ts +12 -20
  138. package/src/types/HelperTypes.ts +54 -2
  139. package/src/types/TypeContext.ts +133 -0
  140. package/src/types/custom/ArraySchema.ts +49 -19
  141. package/src/types/custom/CollectionSchema.ts +1 -0
  142. package/src/types/custom/MapSchema.ts +18 -5
  143. package/src/types/custom/SetSchema.ts +1 -0
  144. package/src/types/registry.ts +22 -3
  145. package/src/types/symbols.ts +10 -7
  146. package/src/utils.ts +7 -3
@@ -1,16 +1,16 @@
1
1
  import { OPERATION } from "../encoding/spec";
2
2
  import { Schema } from "../Schema";
3
- import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $isNew } from "../types/symbols";
3
+ import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refTypeFieldIndexes, $viewFieldIndexes } from "../types/symbols";
4
4
 
5
5
  import type { MapSchema } from "../types/custom/MapSchema";
6
6
  import type { ArraySchema } from "../types/custom/ArraySchema";
7
7
  import type { CollectionSchema } from "../types/custom/CollectionSchema";
8
8
  import type { SetSchema } from "../types/custom/SetSchema";
9
9
 
10
+ import { Root } from "./Root";
10
11
  import { Metadata } from "../Metadata";
11
12
  import type { EncodeOperation } from "./EncodeOperation";
12
13
  import type { DecodeOperation } from "../decoder/DecodeOperation";
13
- import type { StateView } from "./StateView";
14
14
 
15
15
  declare global {
16
16
  interface Object {
@@ -27,50 +27,47 @@ export type Ref = Schema
27
27
  | CollectionSchema
28
28
  | SetSchema;
29
29
 
30
- export class Root {
31
- protected nextUniqueId: number = 0;
32
- refCount = new WeakMap<ChangeTree, number>();
30
+ export type ChangeSetName = "changes"
31
+ | "allChanges"
32
+ | "filteredChanges"
33
+ | "allFilteredChanges";
33
34
 
34
- // all changes
35
- allChanges = new Map<ChangeTree, Map<number, OPERATION>>();
36
- allFilteredChanges = new Map<ChangeTree, Map<number, OPERATION>>();
37
-
38
- // pending changes to be encoded
39
- changes = new Map<ChangeTree, Map<number, OPERATION>>();
40
- filteredChanges = new Map<ChangeTree, Map<number, OPERATION>>();
35
+ export interface IndexedOperations {
36
+ [index: number]: OPERATION;
37
+ }
41
38
 
42
- getNextUniqueId() {
43
- return this.nextUniqueId++;
44
- }
39
+ export interface ChangeSet {
40
+ // field index -> operation index
41
+ indexes: { [index: number]: number };
42
+ operations: OPERATION[]
43
+ queueRootIndex?: number; // index of ChangeTree structure in `root.changes` or `root.filteredChanges`
44
+ }
45
45
 
46
- add (changeTree: ChangeTree) {
47
- const refCount = this.refCount.get(changeTree) || 0;
48
- this.refCount.set(changeTree, refCount + 1);
46
+ export function setOperationAtIndex(changeSet: ChangeSet, index: number) {
47
+ const operationsIndex = changeSet.indexes[index];
48
+ if (operationsIndex === undefined) {
49
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
50
+ } else {
51
+ changeSet.operations[operationsIndex] = index;
49
52
  }
53
+ }
50
54
 
51
- remove(changeTree: ChangeTree) {
52
- const refCount = this.refCount.get(changeTree);
53
- if (refCount <= 1) {
54
- this.allChanges.delete(changeTree);
55
- this.changes.delete(changeTree);
56
-
57
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
58
- this.allFilteredChanges.delete(changeTree);
59
- this.filteredChanges.delete(changeTree);
60
- }
61
-
62
- this.refCount.delete(changeTree);
63
-
64
- } else {
65
- this.refCount.set(changeTree, refCount - 1);
66
- }
67
-
68
- changeTree.forEachChild((child, _) =>
69
- this.remove(child));
55
+ export function deleteOperationAtIndex(changeSet: ChangeSet, index: number) {
56
+ const operationsIndex = changeSet.indexes[index];
57
+ if (operationsIndex !== undefined) {
58
+ changeSet.operations[operationsIndex] = undefined;
70
59
  }
60
+ delete changeSet.indexes[index];
61
+ }
71
62
 
72
- clear() {
73
- this.changes.clear();
63
+ function enqueueChangeTree(
64
+ root: Root,
65
+ changeTree: ChangeTree,
66
+ changeSet: 'changes' | 'filteredChanges' | 'allFilteredChanges',
67
+ queueRootIndex = changeTree[changeSet].queueRootIndex
68
+ ) {
69
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
70
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
74
71
  }
75
72
  }
76
73
 
@@ -79,69 +76,90 @@ export class ChangeTree<T extends Ref=any> {
79
76
  refId: number;
80
77
 
81
78
  root?: Root;
82
-
83
- isFiltered?: boolean;
84
- isPartiallyFiltered?: boolean;
85
-
86
79
  parent?: Ref;
87
80
  parentIndex?: number;
88
81
 
89
- indexes: {[index: string]: any} = {}; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)
90
- currentOperationIndex: number = 0;
82
+ isFiltered: boolean = false;
83
+ isPartiallyFiltered: boolean = false;
91
84
 
92
- allChanges = new Map<number, OPERATION>();
93
- allFilteredChanges = new Map<number, OPERATION>();
85
+ indexedOperations: IndexedOperations = {};
94
86
 
95
- changes = new Map<number, OPERATION>();
96
- filteredChanges = new Map<number, OPERATION>();;
87
+ //
88
+ // TODO:
89
+ // try storing the index + operation per item.
90
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
91
+ //
92
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
93
+ //
94
+ changes: ChangeSet = { indexes: {}, operations: [] };
95
+ allChanges: ChangeSet = { indexes: {}, operations: [] };
96
+ filteredChanges: ChangeSet;
97
+ allFilteredChanges: ChangeSet;
97
98
 
98
- [$isNew] = true;
99
+ indexes: {[index: string]: any}; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)
100
+
101
+ /**
102
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
103
+ */
104
+ isNew = true;
99
105
 
100
106
  constructor(ref: T) {
101
107
  this.ref = ref;
102
- }
103
-
104
- setRoot(root: Root) {
105
- this.root = root;
106
- this.root.add(this);
107
108
 
108
109
  //
109
- // At Schema initialization, the "root" structure might not be available
110
- // yet, as it only does once the "Encoder" has been set up.
111
- //
112
- // So the "parent" may be already set without a "root".
110
+ // Does this structure have "filters" declared?
113
111
  //
114
- this.checkIsFiltered(this.parent, this.parentIndex);
112
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
113
+ this.allFilteredChanges = { indexes: {}, operations: [] };
114
+ this.filteredChanges = { indexes: {}, operations: [] };
115
+ }
116
+ }
115
117
 
116
- // unique refId for the ChangeTree.
117
- this.ensureRefId();
118
+ setRoot(root: Root) {
119
+ this.root = root;
120
+ const isNewChangeTree = this.root.add(this);
118
121
 
119
- if (!this.isFiltered) {
120
- this.root.changes.set(this, this.changes);
121
- }
122
+ const metadata: Metadata = this.ref.constructor[Symbol.metadata];
122
123
 
123
- if (this.isFiltered || this.isPartiallyFiltered) {
124
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
125
- this.root.filteredChanges.set(this, this.filteredChanges);
124
+ if (this.root.types.hasFilters) {
125
+ //
126
+ // At Schema initialization, the "root" structure might not be available
127
+ // yet, as it only does once the "Encoder" has been set up.
128
+ //
129
+ // So the "parent" may be already set without a "root".
130
+ //
131
+ this.checkIsFiltered(metadata, this.parent, this.parentIndex);
126
132
 
127
- // } else {
128
- // this.root.allChanges.set(this, this.allChanges);
133
+ if (this.isFiltered || this.isPartiallyFiltered) {
134
+ enqueueChangeTree(root, this, 'filteredChanges');
135
+ if (isNewChangeTree) {
136
+ this.root.allFilteredChanges.push(this);
137
+ }
138
+ }
129
139
  }
130
140
 
131
141
  if (!this.isFiltered) {
132
- this.root.allChanges.set(this, this.allChanges);
142
+ enqueueChangeTree(root, this, 'changes');
143
+ if (isNewChangeTree) {
144
+ this.root.allChanges.push(this);
145
+ }
133
146
  }
134
147
 
135
- this.forEachChild((changeTree, _) => {
136
- changeTree.setRoot(root);
137
- });
148
+ // Recursively set root on child structures
149
+ if (metadata) {
150
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
151
+ const field = metadata[index as any as number];
152
+ const value = this.ref[field.name];
153
+ value?.[$changes].setRoot(root);
154
+ });
155
+
156
+ } else if (this.ref[$childType] && typeof(this.ref[$childType]) !== "string") {
157
+ // MapSchema / ArraySchema, etc.
158
+ (this.ref as MapSchema).forEach((value, key) => {
159
+ value[$changes].setRoot(root);
160
+ });
161
+ }
138
162
 
139
- // this.allChanges.forEach((_, index) => {
140
- // const childRef = this.ref[$getByIndex](index);
141
- // if (childRef && childRef[$changes]) {
142
- // childRef[$changes].setRoot(root);
143
- // }
144
- // });
145
163
  }
146
164
 
147
165
  setParent(
@@ -155,97 +173,121 @@ export class ChangeTree<T extends Ref=any> {
155
173
  // avoid setting parents with empty `root`
156
174
  if (!root) { return; }
157
175
 
158
- root.add(this);
176
+ const metadata: Metadata = this.ref.constructor[Symbol.metadata];
159
177
 
160
178
  // skip if parent is already set
161
- if (root === this.root) {
162
- this.forEachChild((changeTree, atIndex) => {
163
- changeTree.setParent(this.ref, root, atIndex);
164
- });
165
- return;
166
- }
167
-
168
- this.root = root;
169
- this.checkIsFiltered(parent, parentIndex);
179
+ if (root !== this.root) {
180
+ this.root = root;
181
+ const isNewChangeTree = root.add(this);
182
+
183
+ if (root.types.hasFilters) {
184
+ this.checkIsFiltered(metadata, parent, parentIndex);
185
+
186
+ if (this.isFiltered || this.isPartiallyFiltered) {
187
+ enqueueChangeTree(root, this, 'filteredChanges');
188
+ if (isNewChangeTree) {
189
+ this.root.allFilteredChanges.push(this);
190
+ }
191
+ }
192
+ }
170
193
 
171
- if (!this.isFiltered) {
172
- this.root.changes.set(this, this.changes);
173
- }
194
+ if (!this.isFiltered) {
195
+ enqueueChangeTree(root, this, 'changes');
196
+ if (isNewChangeTree) {
197
+ this.root.allChanges.push(this);
198
+ }
199
+ }
174
200
 
175
- if (this.isFiltered || this.isPartiallyFiltered) {
176
- this.root.filteredChanges.set(this, this.filteredChanges);
177
- this.root.allFilteredChanges.set(this, this.filteredChanges);
178
201
  } else {
179
- this.root.allChanges.set(this, this.allChanges);
202
+ root.add(this);
180
203
  }
181
204
 
182
- this.ensureRefId();
205
+ // assign same parent on child structures
206
+ if (metadata) {
207
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
208
+ const field = metadata[index as any as number];
209
+ const value = this.ref[field.name];
210
+ value?.[$changes].setParent(this.ref, root, index);
211
+
212
+ // try { throw new Error(); } catch (e) {
213
+ // console.log(e.stack);
214
+ // }
215
+
216
+ });
217
+
218
+ } else if (this.ref[$childType] && typeof(this.ref[$childType]) !== "string") {
219
+ // MapSchema / ArraySchema, etc.
220
+ (this.ref as MapSchema).forEach((value, key) => {
221
+ value[$changes].setParent(this.ref, root, this.indexes[key] ?? key);
222
+ });
223
+ }
183
224
 
184
- this.forEachChild((changeTree, atIndex) => {
185
- changeTree.setParent(this.ref, root, atIndex);
186
- });
187
225
  }
188
226
 
189
227
  forEachChild(callback: (change: ChangeTree, atIndex: number) => void) {
190
228
  //
191
229
  // assign same parent on child structures
192
230
  //
193
- if (Metadata.isValidInstance(this.ref)) {
194
- const metadata: Metadata = this.ref['constructor'][Symbol.metadata];
195
-
196
- // FIXME: need to iterate over parent metadata instead.
197
- for (const field in metadata) {
198
- const value = this.ref[field];
199
-
200
- if (value && value[$changes]) {
201
- callback(value[$changes], metadata[field].index);
231
+ const metadata: Metadata = this.ref.constructor[Symbol.metadata];
232
+ if (metadata) {
233
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
234
+ const field = metadata[index as any as number];
235
+ const value = this.ref[field.name];
236
+ if (value) {
237
+ callback(value[$changes], index);
202
238
  }
203
- }
239
+ });
204
240
 
205
- } else if (typeof (this.ref) === "object") {
241
+ } else if (this.ref[$childType] && typeof(this.ref[$childType]) !== "string") {
206
242
  // MapSchema / ArraySchema, etc.
207
243
  (this.ref as MapSchema).forEach((value, key) => {
208
- if (Metadata.isValidInstance(value)) {
209
- callback(value[$changes], this.ref[$changes].indexes[key]);
210
- }
244
+ callback(value[$changes], this.indexes[key] ?? key);
211
245
  });
212
246
  }
213
247
  }
214
248
 
215
249
  operation(op: OPERATION) {
216
- this.changes.set(--this.currentOperationIndex, op);
217
- this.root?.changes.set(this, this.changes);
250
+ // operations without index use negative values to represent them
251
+ // this is checked during .encode() time.
252
+ this.changes.operations.push(-op);
253
+
254
+ enqueueChangeTree(this.root, this, 'changes');
218
255
  }
219
256
 
220
257
  change(index: number, operation: OPERATION = OPERATION.ADD) {
221
- const metadata = this.ref['constructor'][Symbol.metadata] as Metadata;
258
+ const metadata = this.ref.constructor[Symbol.metadata] as Metadata;
222
259
 
223
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
260
+ const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
224
261
  const changeSet = (isFiltered)
225
262
  ? this.filteredChanges
226
263
  : this.changes;
227
264
 
228
- const previousOperation = changeSet.get(index);
265
+ const previousOperation = this.indexedOperations[index];
229
266
  if (!previousOperation || previousOperation === OPERATION.DELETE) {
230
267
  const op = (!previousOperation)
231
268
  ? operation
232
269
  : (previousOperation === OPERATION.DELETE)
233
270
  ? OPERATION.DELETE_AND_ADD
234
271
  : operation
235
- changeSet.set(index, op);
272
+ //
273
+ // TODO: are DELETE operations being encoded as ADD here ??
274
+ //
275
+ this.indexedOperations[index] = op;
236
276
  }
237
277
 
238
- //
239
- // TODO: are DELETE operations being encoded as ADD here ??
240
- //
278
+ setOperationAtIndex(changeSet, index);
241
279
 
242
280
  if (isFiltered) {
243
- this.allFilteredChanges.set(index, OPERATION.ADD);
244
- this.root?.filteredChanges.set(this, this.filteredChanges);
281
+ setOperationAtIndex(this.allFilteredChanges, index);
282
+
283
+ if (this.root) {
284
+ enqueueChangeTree(this.root, this, 'filteredChanges');
285
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
286
+ }
245
287
 
246
288
  } else {
247
- this.allChanges.set(index, OPERATION.ADD);
248
- this.root?.changes.set(this, this.changes);
289
+ setOperationAtIndex(this.allChanges, index);
290
+ enqueueChangeTree(this.root, this, 'changes');
249
291
  }
250
292
  }
251
293
 
@@ -259,13 +301,16 @@ export class ChangeTree<T extends Ref=any> {
259
301
  ? this.filteredChanges
260
302
  : this.changes;
261
303
 
262
- const changeSetEntries = Array.from(changeSet.entries());
263
- changeSet.clear();
264
-
265
- // Re-insert each entry with the shifted index
266
- for (const [index, op] of changeSetEntries) {
267
- changeSet.set(index + shiftIndex, op);
304
+ const newIndexedOperations = {};
305
+ const newIndexes = {};
306
+ for (const index in this.indexedOperations) {
307
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
308
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
268
309
  }
310
+ this.indexedOperations = newIndexedOperations;
311
+ changeSet.indexes = newIndexes;
312
+
313
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
269
314
  }
270
315
 
271
316
  shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0) {
@@ -283,36 +328,46 @@ export class ChangeTree<T extends Ref=any> {
283
328
  }
284
329
  }
285
330
 
286
- private _shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0, allChangeSet: Map<number, OPERATION>) {
287
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
288
- // console.log('shiftAllChangeIndexes', index >= startIndex, { index, op, shiftIndex, startIndex })
289
- if (index >= startIndex) {
290
- allChangeSet.delete(index);
291
- allChangeSet.set(index + shiftIndex, op);
331
+ private _shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0, changeSet: ChangeSet) {
332
+ const newIndexes = {};
333
+
334
+ for (const key in changeSet.indexes) {
335
+ const index = changeSet.indexes[key];
336
+ if (index > startIndex) {
337
+ newIndexes[Number(key) + shiftIndex] = index;
338
+ } else {
339
+ newIndexes[key] = index;
292
340
  }
293
- });
341
+ }
342
+ changeSet.indexes = newIndexes;
343
+
344
+ for (let i = 0; i < changeSet.operations.length; i++) {
345
+ const index = changeSet.operations[i];
346
+ if (index > startIndex) {
347
+ changeSet.operations[i] = index + shiftIndex;
348
+ }
349
+ }
294
350
  }
295
351
 
296
- indexedOperation(index: number, operation: OPERATION, allChangesIndex = index) {
297
- const metadata = this.ref['constructor'][Symbol.metadata] as Metadata;
298
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
352
+ indexedOperation(index: number, operation: OPERATION, allChangesIndex: number = index) {
353
+ this.indexedOperations[index] = operation;
299
354
 
300
- if (isFiltered) {
301
- this.allFilteredChanges.set(allChangesIndex, OPERATION.ADD);
302
- this.filteredChanges.set(index, operation);
303
- this.root?.filteredChanges.set(this, this.filteredChanges);
355
+ if (this.filteredChanges) {
356
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
357
+ setOperationAtIndex(this.filteredChanges, index);
358
+ enqueueChangeTree(this.root, this, 'filteredChanges');
304
359
 
305
360
  } else {
306
- this.allChanges.set(allChangesIndex, OPERATION.ADD);
307
- this.changes.set(index, operation);
308
- this.root?.changes.set(this, this.changes);
361
+ setOperationAtIndex(this.allChanges, allChangesIndex);
362
+ setOperationAtIndex(this.changes, index);
363
+ enqueueChangeTree(this.root, this, 'changes');
309
364
  }
310
365
  }
311
366
 
312
367
  getType(index?: number) {
313
368
  if (Metadata.isValidInstance(this.ref)) {
314
- const metadata = this.ref['constructor'][Symbol.metadata] as Metadata;
315
- return metadata[metadata[index]].type;
369
+ const metadata = this.ref.constructor[Symbol.metadata] as Metadata;
370
+ return metadata[index].type;
316
371
 
317
372
  } else {
318
373
  //
@@ -326,8 +381,7 @@ export class ChangeTree<T extends Ref=any> {
326
381
  }
327
382
 
328
383
  getChange(index: number) {
329
- // TODO: optimize this. avoid checking against multiple instances
330
- return this.changes.get(index) ?? this.filteredChanges.get(index);
384
+ return this.indexedOperations[index];
331
385
  }
332
386
 
333
387
  //
@@ -350,20 +404,17 @@ export class ChangeTree<T extends Ref=any> {
350
404
  return;
351
405
  }
352
406
 
353
- const metadata = this.ref['constructor'][Symbol.metadata] as Metadata;
354
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
355
- const changeSet = (isFiltered)
407
+ const changeSet = (this.filteredChanges)
356
408
  ? this.filteredChanges
357
409
  : this.changes;
358
410
 
359
- const previousValue = this.getValue(index);
411
+ this.indexedOperations[index] = operation ?? OPERATION.DELETE;
412
+ setOperationAtIndex(changeSet, index);
360
413
 
361
- changeSet.set(index, operation ?? OPERATION.DELETE);
414
+ const previousValue = this.getValue(index);
362
415
 
363
416
  // remove `root` reference
364
417
  if (previousValue && previousValue[$changes]) {
365
- previousValue[$changes].root = undefined;
366
-
367
418
  //
368
419
  // FIXME: this.root is "undefined"
369
420
  //
@@ -378,24 +429,30 @@ export class ChangeTree<T extends Ref=any> {
378
429
  }
379
430
 
380
431
  //
381
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
432
+ // FIXME: this is looking a ugly and repeated
382
433
  //
383
- if (isFiltered) {
384
- this.root?.filteredChanges.set(this, this.filteredChanges);
385
- this.allFilteredChanges.delete(allChangesIndex);
434
+ if (this.filteredChanges) {
435
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
436
+ enqueueChangeTree(this.root, this, 'filteredChanges');
386
437
 
387
438
  } else {
388
- this.root?.changes.set(this, this.changes);
389
- this.allChanges.delete(allChangesIndex);
439
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
440
+ enqueueChangeTree(this.root, this, 'changes');
390
441
  }
391
442
  }
392
443
 
393
444
  endEncode() {
394
- this.changes.clear();
445
+ this.indexedOperations = {};
446
+
447
+ // // clear changes
448
+ // this.changes.indexes = {};
449
+ // this.changes.operations.length = 0;
450
+
451
+ // ArraySchema and MapSchema have a custom "encode end" method
395
452
  this.ref[$onEncodeEnd]?.();
396
453
 
397
454
  // Not a new instance anymore
398
- delete this[$isNew];
455
+ this.isNew = false;
399
456
  }
400
457
 
401
458
  discard(discardAll: boolean = false) {
@@ -406,15 +463,26 @@ export class ChangeTree<T extends Ref=any> {
406
463
  //
407
464
  this.ref[$onEncodeEnd]?.();
408
465
 
409
- this.changes.clear();
410
- this.filteredChanges.clear();
466
+ this.indexedOperations = {};
411
467
 
412
- // reset operation index
413
- this.currentOperationIndex = 0;
468
+ this.changes.indexes = {};
469
+ this.changes.operations.length = 0;
470
+ this.changes.queueRootIndex = undefined;
471
+
472
+ if (this.filteredChanges !== undefined) {
473
+ this.filteredChanges.indexes = {};
474
+ this.filteredChanges.operations.length = 0;
475
+ this.filteredChanges.queueRootIndex = undefined;
476
+ }
414
477
 
415
478
  if (discardAll) {
416
- this.allChanges.clear();
417
- this.allFilteredChanges.clear();
479
+ this.allChanges.indexes = {};
480
+ this.allChanges.operations.length = 0;
481
+
482
+ if (this.allFilteredChanges !== undefined) {
483
+ this.allFilteredChanges.indexes = {};
484
+ this.allFilteredChanges.operations.length = 0;
485
+ }
418
486
 
419
487
  // remove children references
420
488
  this.forEachChild((changeTree, _) =>
@@ -426,13 +494,14 @@ export class ChangeTree<T extends Ref=any> {
426
494
  * Recursively discard all changes from this, and child structures.
427
495
  */
428
496
  discardAll() {
429
- this.changes.forEach((_, fieldIndex) => {
430
- const value = this.getValue(fieldIndex);
497
+ const keys = Object.keys(this.indexedOperations);
498
+ for (let i = 0, len = keys.length; i < len; i++) {
499
+ const value = this.getValue(Number(keys[i]));
431
500
 
432
501
  if (value && value[$changes]) {
433
502
  value[$changes].discardAll();
434
503
  }
435
- });
504
+ }
436
505
 
437
506
  this.discard();
438
507
  }
@@ -447,26 +516,31 @@ export class ChangeTree<T extends Ref=any> {
447
516
  }
448
517
 
449
518
  get changed() {
450
- return this.changes.size > 0;
519
+ return (Object.entries(this.indexedOperations).length > 0);
451
520
  }
452
521
 
453
- protected checkIsFiltered(parent: Ref, parentIndex: number) {
522
+ protected checkIsFiltered(metadata: Metadata, parent: Ref, parentIndex: number) {
454
523
  // Detect if current structure has "filters" declared
455
- this.isPartiallyFiltered = this.ref['constructor']?.[Symbol.metadata]?.[-2];
524
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
456
525
 
457
- // TODO: support "partially filtered", where the instance is visible, but only a field is not.
458
-
459
- // Detect if parent has "filters" declared
460
- while (parent && !this.isFiltered) {
461
- const metadata: Metadata = parent['constructor'][Symbol.metadata];
526
+ if (this.isPartiallyFiltered) {
527
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
528
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
529
+ }
462
530
 
463
- const fieldName = metadata?.[parentIndex];
464
- const isParentOwned = metadata?.[fieldName]?.tag !== undefined;
531
+ // skip if parent is not set
532
+ if (!parent) {
533
+ return;
534
+ }
465
535
 
466
- this.isFiltered = isParentOwned || parent[$changes].isFiltered; // metadata?.[-2]
536
+ if (!Metadata.isValidInstance(parent)) {
537
+ const parentChangeTree = parent[$changes];
538
+ parent = parentChangeTree.parent;
539
+ parentIndex = parentChangeTree.parentIndex;
540
+ }
467
541
 
468
- parent = parent[$changes].parent;
469
- };
542
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
543
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
470
544
 
471
545
  //
472
546
  // TODO: refactor this!
@@ -474,16 +548,26 @@ export class ChangeTree<T extends Ref=any> {
474
548
  // swapping `changes` and `filteredChanges` is required here
475
549
  // because "isFiltered" may not be imedialely available on `change()`
476
550
  //
477
- if (this.isFiltered && this.changes.size > 0) {
478
- // swap changes reference
479
- const changes = this.changes;
480
- this.changes = this.filteredChanges;
481
- this.filteredChanges = changes;
482
-
483
- // swap "all changes" reference
484
- const allFilteredChanges = this.allFilteredChanges;
485
- this.allFilteredChanges = this.allChanges;
486
- this.allChanges = allFilteredChanges;
551
+ if (this.isFiltered) {
552
+ this.filteredChanges = { indexes: {}, operations: [] };
553
+ this.allFilteredChanges = { indexes: {}, operations: [] };
554
+
555
+ if (this.changes.operations.length > 0) {
556
+ // swap changes reference
557
+ const changes = this.changes;
558
+ this.changes = this.filteredChanges;
559
+ this.filteredChanges = changes;
560
+
561
+ // swap "all changes" reference
562
+ const allFilteredChanges = this.allFilteredChanges;
563
+ this.allFilteredChanges = this.allChanges;
564
+ this.allChanges = allFilteredChanges;
565
+
566
+ // console.log("SWAP =>", {
567
+ // "this.allFilteredChanges": this.allFilteredChanges,
568
+ // "this.allChanges": this.allChanges
569
+ // })
570
+ }
487
571
  }
488
572
  }
489
573