@colyseus/schema 3.0.0-alpha.8 → 3.0.0

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