@colyseus/schema 2.0.32 → 3.0.0-alpha.1

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 (160) hide show
  1. package/build/cjs/index.js +3428 -2677
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +3324 -2445
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +3428 -2677
  6. package/lib/Decoder.d.ts +16 -0
  7. package/lib/Decoder.js +182 -0
  8. package/lib/Decoder.js.map +1 -0
  9. package/lib/Encoder.d.ts +13 -0
  10. package/lib/Encoder.js +79 -0
  11. package/lib/Encoder.js.map +1 -0
  12. package/lib/Metadata.d.ts +36 -0
  13. package/lib/Metadata.js +91 -0
  14. package/lib/Metadata.js.map +1 -0
  15. package/lib/Reflection.d.ts +7 -5
  16. package/lib/Reflection.js +62 -58
  17. package/lib/Reflection.js.map +1 -1
  18. package/lib/Schema.d.ts +39 -51
  19. package/lib/Schema.js +189 -731
  20. package/lib/Schema.js.map +1 -1
  21. package/lib/annotations.d.ts +26 -45
  22. package/lib/annotations.js +363 -194
  23. package/lib/annotations.js.map +1 -1
  24. package/lib/changes/ChangeSet.d.ts +12 -0
  25. package/lib/changes/ChangeSet.js +35 -0
  26. package/lib/changes/ChangeSet.js.map +1 -0
  27. package/lib/changes/DecodeOperation.d.ts +15 -0
  28. package/lib/changes/DecodeOperation.js +186 -0
  29. package/lib/changes/DecodeOperation.js.map +1 -0
  30. package/lib/changes/EncodeOperation.d.ts +18 -0
  31. package/lib/changes/EncodeOperation.js +130 -0
  32. package/lib/changes/EncodeOperation.js.map +1 -0
  33. package/lib/changes/consts.d.ts +14 -0
  34. package/lib/changes/consts.js +18 -0
  35. package/lib/changes/consts.js.map +1 -0
  36. package/lib/decoder/DecodeOperation.d.ts +24 -0
  37. package/lib/decoder/DecodeOperation.js +256 -0
  38. package/lib/decoder/DecodeOperation.js.map +1 -0
  39. package/lib/decoder/Decoder.d.ts +21 -0
  40. package/lib/decoder/Decoder.js +114 -0
  41. package/lib/decoder/Decoder.js.map +1 -0
  42. package/lib/decoder/ReferenceTracker.d.ts +26 -0
  43. package/lib/decoder/ReferenceTracker.js +131 -0
  44. package/lib/decoder/ReferenceTracker.js.map +1 -0
  45. package/lib/decoder/strategy/RawChanges.d.ts +3 -0
  46. package/lib/decoder/strategy/RawChanges.js +8 -0
  47. package/lib/decoder/strategy/RawChanges.js.map +1 -0
  48. package/lib/decoder/strategy/StateCallbacks.d.ts +20 -0
  49. package/lib/decoder/strategy/StateCallbacks.js +240 -0
  50. package/lib/decoder/strategy/StateCallbacks.js.map +1 -0
  51. package/lib/decoding/decode.d.ts +48 -0
  52. package/lib/decoding/decode.js +267 -0
  53. package/lib/decoding/decode.js.map +1 -0
  54. package/lib/ecs.d.ts +11 -0
  55. package/lib/ecs.js +160 -0
  56. package/lib/ecs.js.map +1 -0
  57. package/lib/encoder/ChangeTree.d.ts +72 -0
  58. package/lib/encoder/ChangeTree.js +384 -0
  59. package/lib/encoder/ChangeTree.js.map +1 -0
  60. package/lib/encoder/EncodeOperation.d.ts +25 -0
  61. package/lib/encoder/EncodeOperation.js +156 -0
  62. package/lib/encoder/EncodeOperation.js.map +1 -0
  63. package/lib/encoder/Encoder.d.ts +23 -0
  64. package/lib/encoder/Encoder.js +192 -0
  65. package/lib/encoder/Encoder.js.map +1 -0
  66. package/lib/encoder/StateView.d.ts +21 -0
  67. package/lib/encoder/StateView.js +196 -0
  68. package/lib/encoder/StateView.js.map +1 -0
  69. package/lib/encoding/assert.d.ts +9 -0
  70. package/lib/encoding/assert.js +47 -0
  71. package/lib/encoding/assert.js.map +1 -0
  72. package/lib/encoding/decode.js +1 -1
  73. package/lib/encoding/decode.js.map +1 -1
  74. package/lib/encoding/encode.d.ts +19 -16
  75. package/lib/encoding/encode.js +88 -81
  76. package/lib/encoding/encode.js.map +1 -1
  77. package/lib/encoding/spec.d.ts +25 -0
  78. package/lib/encoding/spec.js +30 -0
  79. package/lib/encoding/spec.js.map +1 -0
  80. package/lib/index.d.ts +18 -10
  81. package/lib/index.js +39 -17
  82. package/lib/index.js.map +1 -1
  83. package/lib/symbol.shim.d.ts +6 -0
  84. package/lib/symbol.shim.js +4 -0
  85. package/lib/symbol.shim.js.map +1 -0
  86. package/lib/types/ArraySchema.d.ts +1 -1
  87. package/lib/types/ArraySchema.js +0 -7
  88. package/lib/types/ArraySchema.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +10 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/custom/ArraySchema.d.ts +245 -0
  92. package/lib/types/custom/ArraySchema.js +659 -0
  93. package/lib/types/custom/ArraySchema.js.map +1 -0
  94. package/lib/types/custom/CollectionSchema.d.ts +42 -0
  95. package/lib/types/custom/CollectionSchema.js +165 -0
  96. package/lib/types/custom/CollectionSchema.js.map +1 -0
  97. package/lib/types/custom/MapSchema.d.ts +43 -0
  98. package/lib/types/custom/MapSchema.js +200 -0
  99. package/lib/types/custom/MapSchema.js.map +1 -0
  100. package/lib/types/custom/SetSchema.d.ts +39 -0
  101. package/lib/types/custom/SetSchema.js +177 -0
  102. package/lib/types/custom/SetSchema.js.map +1 -0
  103. package/lib/types/registry.d.ts +6 -0
  104. package/lib/types/registry.js +19 -0
  105. package/lib/types/registry.js.map +1 -0
  106. package/lib/types/symbols.d.ts +29 -0
  107. package/lib/types/symbols.js +33 -0
  108. package/lib/types/symbols.js.map +1 -0
  109. package/lib/types/utils.d.ts +0 -8
  110. package/lib/types/utils.js +1 -33
  111. package/lib/types/utils.js.map +1 -1
  112. package/lib/usage.d.ts +1 -0
  113. package/lib/usage.js +22 -0
  114. package/lib/usage.js.map +1 -0
  115. package/lib/utils.d.ts +13 -2
  116. package/lib/utils.js +36 -15
  117. package/lib/utils.js.map +1 -1
  118. package/lib/v3.d.ts +1 -0
  119. package/lib/v3.js +427 -0
  120. package/lib/v3.js.map +1 -0
  121. package/lib/v3_bench.d.ts +1 -0
  122. package/lib/v3_bench.js +130 -0
  123. package/lib/v3_bench.js.map +1 -0
  124. package/lib/v3_experiment.d.ts +1 -0
  125. package/lib/v3_experiment.js +407 -0
  126. package/lib/v3_experiment.js.map +1 -0
  127. package/package.json +5 -5
  128. package/src/Metadata.ts +135 -0
  129. package/src/Reflection.ts +75 -66
  130. package/src/Schema.ts +213 -931
  131. package/src/annotations.ts +430 -243
  132. package/src/decoder/DecodeOperation.ts +372 -0
  133. package/src/decoder/Decoder.ts +155 -0
  134. package/src/decoder/ReferenceTracker.ts +151 -0
  135. package/src/decoder/strategy/RawChanges.ts +9 -0
  136. package/src/decoder/strategy/StateCallbacks.ts +326 -0
  137. package/src/encoder/ChangeTree.ts +492 -0
  138. package/src/encoder/EncodeOperation.ts +237 -0
  139. package/src/encoder/Encoder.ts +246 -0
  140. package/src/encoder/StateView.ts +229 -0
  141. package/src/encoding/assert.ts +58 -0
  142. package/src/encoding/decode.ts +1 -1
  143. package/src/encoding/encode.ts +91 -82
  144. package/src/encoding/spec.ts +29 -0
  145. package/src/index.ts +22 -19
  146. package/src/symbol.shim.ts +12 -0
  147. package/src/types/HelperTypes.ts +16 -2
  148. package/src/types/{ArraySchema.ts → custom/ArraySchema.ts} +342 -248
  149. package/src/types/{CollectionSchema.ts → custom/CollectionSchema.ts} +56 -46
  150. package/src/types/{MapSchema.ts → custom/MapSchema.ts} +88 -115
  151. package/src/types/{SetSchema.ts → custom/SetSchema.ts} +58 -47
  152. package/src/types/{typeRegistry.ts → registry.ts} +6 -6
  153. package/src/types/symbols.ts +36 -0
  154. package/src/types/utils.ts +0 -46
  155. package/src/utils.ts +50 -21
  156. package/src/v3_bench.ts +107 -0
  157. package/src/changes/ChangeTree.ts +0 -295
  158. package/src/changes/ReferenceTracker.ts +0 -91
  159. package/src/filters/index.ts +0 -23
  160. package/src/spec.ts +0 -49
@@ -1,8 +1,13 @@
1
- import { ChangeTree } from "../changes/ChangeTree";
2
- import { OPERATION } from "../spec";
3
- import { SchemaDecoderCallbacks, Schema } from "../Schema";
4
- import { addCallback, removeChildRefs } from "./utils";
5
- import { DataChange } from "..";
1
+ import { $changes, $childType, $decoder, $deleteByIndex, $onEncodeEnd, $encoder, $filter, $getByIndex, $onDecodeEnd, $isNew } from "../symbols";
2
+ import type { Schema } from "../../Schema";
3
+ import { ChangeTree } from "../../encoder/ChangeTree";
4
+ import { OPERATION } from "../../encoding/spec";
5
+ import { registerType } from "../registry";
6
+ import { Collection } from "../HelperTypes";
7
+
8
+ import { encodeArray } from "../../encoder/EncodeOperation";
9
+ import { decodeArray } from "../../decoder/DecodeOperation";
10
+ import type { StateView } from "../../encoder/StateView";
6
11
 
7
12
  const DEFAULT_SORT = (a: any, b: any) => {
8
13
  const A = a.toString();
@@ -12,100 +17,34 @@ const DEFAULT_SORT = (a: any, b: any) => {
12
17
  else return 0
13
18
  }
14
19
 
15
- export function getArrayProxy(value: ArraySchema) {
16
- value['$proxy'] = true;
17
-
18
- //
19
- // compatibility with @colyseus/schema 0.5.x
20
- // - allow `map["key"]`
21
- // - allow `map["key"] = "xxx"`
22
- // - allow `delete map["key"]`
23
- //
24
- value = new Proxy(value, {
25
- get: (obj, prop) => {
26
- if (
27
- typeof (prop) !== "symbol" &&
28
- !isNaN(prop as any) // https://stackoverflow.com/a/175787/892698
29
- ) {
30
- return obj.at(prop as unknown as number);
31
-
32
- } else {
33
- return obj[prop];
34
- }
35
- },
36
-
37
- set: (obj, prop, setValue) => {
38
- if (
39
- typeof (prop) !== "symbol" &&
40
- !isNaN(prop as any)
41
- ) {
42
- const indexes = Array.from(obj['$items'].keys());
43
- const key = parseInt(indexes[prop] || prop);
44
- if (setValue === undefined || setValue === null) {
45
- obj.deleteAt(key);
46
-
47
- } else {
48
- obj.setAt(key, setValue);
49
- }
50
-
51
- } else {
52
- obj[prop] = setValue;
53
- }
54
-
55
- return true;
56
- },
57
-
58
- deleteProperty: (obj, prop) => {
59
- if (typeof (prop) === "number") {
60
- obj.deleteAt(prop);
61
-
62
- } else {
63
- delete obj[prop];
64
- }
65
-
66
- return true;
67
- },
68
-
69
- has: (obj, key) => {
70
- if (
71
- typeof (key) !== "symbol" &&
72
- !isNaN(Number(key))
73
- ) {
74
- return obj['$items'].has(Number(key))
75
- }
76
- return Reflect.has(obj, key)
77
- }
78
- });
79
-
80
- return value;
81
- }
82
-
83
- export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
84
- protected $changes: ChangeTree = new ChangeTree(this);
85
-
86
- protected $items: Map<number, V> = new Map<number, V>();
87
- protected $indexes: Map<number, number> = new Map<number, number>();
20
+ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
21
+ [n: number]: V;
88
22
 
89
- protected $refId: number = 0;
23
+ protected items: V[] = [];
24
+ protected tmpItems: V[] = [];
25
+ protected deletedIndexes: {[index: number]: boolean} = {};
90
26
 
91
- [n: number]: V;
27
+ static [$encoder] = encodeArray;
28
+ static [$decoder] = decodeArray;
92
29
 
93
- //
94
- // Decoding callbacks
95
- //
96
- public $callbacks: { [operation: number]: Array<(item: V, key: number) => void> };
97
- public onAdd(callback: (item: V, key: number) => void, triggerAll: boolean = true) {
98
- return addCallback(
99
- (this.$callbacks || (this.$callbacks = {})),
100
- OPERATION.ADD,
101
- callback,
102
- (triggerAll)
103
- ? this.$items
104
- : undefined
30
+ /**
31
+ * Determine if a property must be filtered.
32
+ * - If returns false, the property is NOT going to be encoded.
33
+ * - If returns true, the property is going to be encoded.
34
+ *
35
+ * Encoding with "filters" happens in two steps:
36
+ * - First, the encoder iterates over all "not owned" properties and encodes them.
37
+ * - Then, the encoder iterates over all "owned" properties per instance and encodes them.
38
+ */
39
+ static [$filter] (ref: ArraySchema, index: number, view: StateView) {
40
+ // console.log("ArraSchema[$filter] VIEW??", !view)
41
+ return (
42
+ !view ||
43
+ typeof (ref[$childType]) === "string" ||
44
+ // view.items.has(ref[$getByIndex](index)[$changes])
45
+ view.items.has(ref['tmpItems'][index]?.[$changes])
105
46
  );
106
47
  }
107
- public onRemove(callback: (item: V, key: number) => void) { return addCallback(this.$callbacks || (this.$callbacks = {}), OPERATION.DELETE, callback); }
108
- public onChange(callback: (item: V, key: number) => void) { return addCallback(this.$callbacks || (this.$callbacks = {}), OPERATION.REPLACE, callback); }
109
48
 
110
49
  static is(type: any) {
111
50
  return (
@@ -118,129 +57,237 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
118
57
  }
119
58
 
120
59
  constructor (...items: V[]) {
60
+
61
+ Object.defineProperty(this, $childType, {
62
+ value: undefined,
63
+ enumerable: false,
64
+ writable: true,
65
+ configurable: true,
66
+ });
67
+
68
+ const proxy = new Proxy(this, {
69
+ get: (obj, prop) => {
70
+ if (
71
+ typeof (prop) !== "symbol" &&
72
+ !isNaN(prop as any) // https://stackoverflow.com/a/175787/892698
73
+ ) {
74
+ return this.items[prop];
75
+
76
+ } else {
77
+ return Reflect.get(obj, prop);
78
+ }
79
+ },
80
+
81
+ set: (obj, key, setValue) => {
82
+ if (typeof (key) !== "symbol" && !isNaN(key as any)) {
83
+ if (setValue === undefined || setValue === null) {
84
+ obj.$deleteAt(key as unknown as number);
85
+
86
+ } else {
87
+ if (setValue[$changes]) {
88
+ if (obj.items[key as unknown as number] !== undefined) {
89
+ if (setValue[$changes][$isNew]) {
90
+ this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
91
+
92
+ } else {
93
+ if ((obj[$changes].getChange(Number(key)) & OPERATION.DELETE) === OPERATION.DELETE) {
94
+ this[$changes].indexedOperation(Number(key), OPERATION.DELETE_AND_MOVE);
95
+ } else {
96
+ this[$changes].indexedOperation(Number(key), OPERATION.MOVE);
97
+ }
98
+ }
99
+ } else if (setValue[$changes][$isNew]) {
100
+ this[$changes].indexedOperation(Number(key), OPERATION.ADD);
101
+ }
102
+ } else {
103
+ obj.$changeAt(Number(key), setValue);
104
+ }
105
+
106
+ this.items[key as unknown as number] = setValue;
107
+ this.tmpItems[key as unknown as number] = setValue;
108
+ }
109
+
110
+ return true;
111
+ } else {
112
+ return Reflect.set(obj, key, setValue);
113
+ }
114
+ },
115
+
116
+ deleteProperty: (obj, prop) => {
117
+ if (typeof (prop) === "number") {
118
+ obj.$deleteAt(prop);
119
+
120
+ } else {
121
+ delete obj[prop];
122
+ }
123
+
124
+ return true;
125
+ },
126
+
127
+ has: (obj, key) => {
128
+ if (typeof (key) !== "symbol" && !isNaN(Number(key))) {
129
+ return Reflect.has(this.items, key);
130
+ }
131
+ return Reflect.has(obj, key)
132
+ }
133
+ });
134
+
135
+ this[$changes] = new ChangeTree(proxy);
121
136
  this.push.apply(this, items);
137
+
138
+ return proxy;
122
139
  }
123
140
 
124
- set length (value: number) {
125
- if (value === 0) {
141
+ set length (newLength: number) {
142
+ if (newLength === 0) {
126
143
  this.clear();
127
-
144
+ } else if (newLength < this.items.length) {
145
+ this.splice(newLength, this.length - newLength);
128
146
  } else {
129
- this.splice(value, this.length - value);
147
+ console.warn("ArraySchema: can't set .length to a higher value than its length.");
130
148
  }
131
149
  }
132
150
 
133
151
  get length() {
134
- return this.$items.size;
152
+ return this.items.length;
135
153
  }
136
154
 
137
155
  push(...values: V[]) {
138
- let lastIndex: number;
156
+ let length = this.tmpItems.length;
157
+
158
+ values.forEach((value, i) => {
159
+ // skip null values
160
+ if (value === undefined || value === null) {
161
+ return;
162
+ }
139
163
 
140
- values.forEach(value => {
141
- // set "index" for reference.
142
- lastIndex = this.$refId++;
164
+ const changeTree = this[$changes];
165
+ changeTree.indexedOperation(length, OPERATION.ADD, this.items.length);
166
+ // changeTree.indexes[length] = length;
143
167
 
144
- this.setAt(lastIndex, value);
168
+ this.items.push(value);
169
+ this.tmpItems.push(value);
170
+
171
+ //
172
+ // set value's parent after the value is set
173
+ // (to avoid encoding "refId" operations before parent's "ADD" operation)
174
+ //
175
+ value[$changes]?.setParent(this, changeTree.root, length);
176
+
177
+ length++;
145
178
  });
146
179
 
147
- return lastIndex;
180
+ return length;
148
181
  }
149
182
 
150
183
  /**
151
184
  * Removes the last element from an array and returns it.
152
185
  */
153
186
  pop(): V | undefined {
154
- const key = Array.from(this.$indexes.values()).pop();
155
- if (key === undefined) { return undefined; }
187
+ let index: number = -1;
188
+
189
+ // find last non-undefined index
190
+ for (let i = this.tmpItems.length - 1; i >= 0; i--) {
191
+ // if (this.tmpItems[i] !== undefined) {
192
+ if (this.deletedIndexes[i] !== true) {
193
+ index = i;
194
+ break;
195
+ }
196
+ }
156
197
 
157
- this.$changes.delete(key);
158
- this.$indexes.delete(key);
198
+ if (index < 0) {
199
+ return undefined;
200
+ }
201
+
202
+ this[$changes].delete(index, undefined, this.items.length - 1);
159
203
 
160
- const value = this.$items.get(key);
161
- this.$items.delete(key);
204
+ // this.tmpItems[index] = undefined;
205
+ this.deletedIndexes[index] = true;
162
206
 
163
- return value;
207
+ return this.items.pop();
164
208
  }
165
209
 
166
210
  at(index: number) {
167
- //
168
- // FIXME: this should be O(1)
169
- //
170
-
171
- index = Math.trunc(index) || 0;
172
- // Allow negative indexing from the end
173
- if (index < 0) index += this.length;
174
- // OOB access is guaranteed to return undefined
175
- if (index < 0 || index >= this.length) return undefined;
176
-
177
- const key = Array.from(this.$items.keys())[index];
178
- return this.$items.get(key);
211
+ // Allow negative indexing from the end
212
+ if (index < 0) index += this.length;
213
+ return this.items[index];
179
214
  }
180
215
 
181
- setAt(index: number, value: V) {
216
+ // encoding only
217
+ protected $changeAt(index: number, value: V) {
182
218
  if (value === undefined || value === null) {
183
219
  console.error("ArraySchema items cannot be null nor undefined; Use `deleteAt(index)` instead.");
184
220
  return;
185
221
  }
186
222
 
187
223
  // skip if the value is the same as cached.
188
- if (this.$items.get(index) === value) {
224
+ if (this.items[index] === value) {
189
225
  return;
190
226
  }
191
227
 
192
- if (value['$changes'] !== undefined) {
193
- (value['$changes'] as ChangeTree).setParent(this, this.$changes.root, index);
194
- }
195
-
196
- const operation = this.$changes.indexes[index]?.op ?? OPERATION.ADD;
197
-
198
- this.$changes.indexes[index] = index;
228
+ const changeTree = this[$changes];
229
+ const operation = changeTree.indexes?.[index]?.op ?? OPERATION.ADD;
199
230
 
200
- this.$indexes.set(index, index);
201
- this.$items.set(index, value);
231
+ changeTree.change(index, operation);
202
232
 
203
- this.$changes.change(index, operation);
233
+ //
234
+ // set value's parent after the value is set
235
+ // (to avoid encoding "refId" operations before parent's "ADD" operation)
236
+ //
237
+ value[$changes]?.setParent(this, changeTree.root, index);
204
238
  }
205
239
 
206
- deleteAt(index: number) {
207
- const key = Array.from(this.$items.keys())[index];
208
- if (key === undefined) { return false; }
209
- return this.$deleteAt(key);
240
+ // encoding only
241
+ protected $deleteAt(index: number, operation?: OPERATION) {
242
+ this[$changes].delete(index, operation);
210
243
  }
211
244
 
212
- protected $deleteAt(index) {
213
- // delete at internal index
214
- this.$changes.delete(index);
215
- this.$indexes.delete(index);
245
+ // decoding only
246
+ protected $setAt(index: number, value: V, operation: OPERATION) {
247
+ if (
248
+ index === 0 &&
249
+ operation === OPERATION.ADD &&
250
+ this.items[index] !== undefined
251
+ ) {
252
+ // handle decoding unshift
253
+ this.items.unshift(value);
216
254
 
217
- return this.$items.delete(index);
218
- }
255
+ } else if (operation === OPERATION.DELETE_AND_MOVE) {
256
+ this.items.splice(index, 1);
257
+ this.items[index] = value;
219
258
 
220
- clear(changes?: DataChange[]) {
221
- // discard previous operations.
222
- this.$changes.discard(true, true);
223
- this.$changes.indexes = {};
224
-
225
- // clear previous indexes
226
- this.$indexes.clear();
227
-
228
- //
229
- // When decoding:
230
- // - enqueue items for DELETE callback.
231
- // - flag child items for garbage collection.
232
- //
233
- if (changes) {
234
- removeChildRefs.call(this, changes);
259
+ } else {
260
+ this.items[index] = value;
235
261
  }
262
+ }
236
263
 
237
- // clear items
238
- this.$items.clear();
264
+ clear() {
265
+ // skip if already clear
266
+ if (this.items.length === 0) { return; }
267
+
268
+ // discard previous operations.
269
+ const changeTree = this[$changes]
270
+
271
+ // discard children
272
+ changeTree.forEachChild((changeTree, _) => {
273
+ changeTree.discard(true);
274
+
275
+ //
276
+ // TODO: add tests with instance sharing + .clear()
277
+ // FIXME: this.root? is required because it is being called at decoding time.
278
+ //
279
+ // TODO: do not use [$changes] at decoding time.
280
+ //
281
+ changeTree.root?.changes.delete(changeTree);
282
+ changeTree.root?.allChanges.delete(changeTree);
283
+ changeTree.root?.allFilteredChanges.delete(changeTree);
284
+ });
239
285
 
240
- this.$changes.operation({ index: 0, op: OPERATION.CLEAR });
286
+ changeTree.discard(true);
287
+ changeTree.operation(OPERATION.CLEAR);
241
288
 
242
- // touch all structures until reach root
243
- this.$changes.touchParents();
289
+ this.items.length = 0;
290
+ this.tmpItems.length = 0;
244
291
  }
245
292
 
246
293
  /**
@@ -249,7 +296,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
249
296
  */
250
297
  // @ts-ignore
251
298
  concat(...items: (V | ConcatArray<V>)[]): ArraySchema<V> {
252
- return new ArraySchema(...Array.from(this.$items.values()).concat(...items));
299
+ return new ArraySchema(...this.items.concat(...items));
253
300
  }
254
301
 
255
302
  /**
@@ -257,7 +304,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
257
304
  * @param separator A string used to separate one element of an array from the next in the resulting String. If omitted, the array elements are separated with a comma.
258
305
  */
259
306
  join(separator?: string): string {
260
- return Array.from(this.$items.values()).join(separator);
307
+ return this.items.join(separator);
261
308
  }
262
309
 
263
310
  /**
@@ -265,13 +312,9 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
265
312
  */
266
313
  // @ts-ignore
267
314
  reverse(): ArraySchema<V> {
268
- const indexes = Array.from(this.$items.keys());
269
- const reversedItems = Array.from(this.$items.values()).reverse();
270
-
271
- reversedItems.forEach((item, i) => {
272
- this.setAt(indexes[i], item);
273
- });
274
-
315
+ this[$changes].operation(OPERATION.REVERSE);
316
+ this.items.reverse();
317
+ this.tmpItems.reverse();
275
318
  return this;
276
319
  }
277
320
 
@@ -279,15 +322,16 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
279
322
  * Removes the first element from an array and returns it.
280
323
  */
281
324
  shift(): V | undefined {
282
- const indexes = Array.from(this.$items.keys());
325
+ if (this.items.length === 0) { return undefined; }
283
326
 
284
- const shiftAt = indexes.shift();
285
- if (shiftAt === undefined) { return undefined; }
327
+ // const index = Number(Object.keys(changeTree.indexes)[0]);
328
+ const index = this.tmpItems.findIndex((item, i) => item === this.items[0]);
329
+ const changeTree = this[$changes];
286
330
 
287
- const value = this.$items.get(shiftAt);
288
- this.$deleteAt(shiftAt);
331
+ changeTree.delete(index);
332
+ changeTree.shiftAllChangeIndexes(-1, index);
289
333
 
290
- return value;
334
+ return this.items.shift();
291
335
  }
292
336
 
293
337
  /**
@@ -297,7 +341,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
297
341
  */
298
342
  slice(start?: number, end?: number): V[] {
299
343
  const sliced = new ArraySchema<V>();
300
- sliced.push(...Array.from(this.$items.values()).slice(start, end));
344
+ sliced.push(...this.items.slice(start, end));
301
345
  return sliced as unknown as V[];
302
346
  }
303
347
 
@@ -311,13 +355,13 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
311
355
  * ```
312
356
  */
313
357
  sort(compareFn: (a: V, b: V) => number = DEFAULT_SORT): this {
314
- const indexes = Array.from(this.$items.keys());
315
- const sortedItems = Array.from(this.$items.values()).sort(compareFn);
358
+ const changeTree = this[$changes];
359
+ const sortedItems = this.items.sort(compareFn);
316
360
 
317
- sortedItems.forEach((item, i) => {
318
- this.setAt(indexes[i], item);
319
- });
361
+ // wouldn't OPERATION.MOVE make more sense here?
362
+ sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
320
363
 
364
+ this.tmpItems.sort(compareFn);
321
365
  return this;
322
366
  }
323
367
 
@@ -325,26 +369,53 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
325
369
  * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements.
326
370
  * @param start The zero-based location in the array from which to start removing elements.
327
371
  * @param deleteCount The number of elements to remove.
328
- * @param items Elements to insert into the array in place of the deleted elements.
372
+ * @param insertItems Elements to insert into the array in place of the deleted elements.
329
373
  */
330
374
  splice(
331
375
  start: number,
332
- deleteCount: number = this.length - start,
333
- ...items: V[]
376
+ deleteCount: number = this.items.length - start,
377
+ ...insertItems: V[]
334
378
  ): V[] {
335
- const indexes = Array.from(this.$items.keys());
336
- const removedItems: V[] = [];
379
+ const changeTree = this[$changes];
337
380
 
381
+ const tmpItemsLength = this.tmpItems.length;
382
+ const insertCount = insertItems.length;
383
+
384
+ // build up-to-date list of indexes, excluding removed values.
385
+ const indexes: number[] = [];
386
+ for (let i = 0; i < tmpItemsLength; i++) {
387
+ // if (this.tmpItems[i] !== undefined) {
388
+ if (this.deletedIndexes[i] !== true) {
389
+ indexes.push(i);
390
+ }
391
+ }
392
+
393
+ // delete operations at correct index
338
394
  for (let i = start; i < start + deleteCount; i++) {
339
- removedItems.push(this.$items.get(indexes[i]));
340
- this.$deleteAt(indexes[i]);
395
+ const index = indexes[i];
396
+ changeTree.delete(index);
397
+ // this.tmpItems[index] = undefined;
398
+ this.deletedIndexes[index] = true;
399
+ }
400
+
401
+ // force insert operations
402
+ for (let i = 0; i < insertCount; i++) {
403
+ const addIndex = indexes[start] + i;
404
+ changeTree.indexedOperation(addIndex, OPERATION.ADD);
405
+
406
+ // set value's parent/root
407
+ insertItems[i][$changes]?.setParent(this, changeTree.root, addIndex);
341
408
  }
342
409
 
343
- for (let i = 0; i < items.length; i++) {
344
- this.setAt(start + i, items[i]);
410
+ //
411
+ // delete exceeding indexes from "allChanges"
412
+ // (prevent .encodeAll() from encoding non-existing items)
413
+ //
414
+ if (deleteCount > insertCount) {
415
+ changeTree.shiftAllChangeIndexes(-(deleteCount - insertCount), indexes[start + insertCount]);
345
416
  }
346
417
 
347
- return removedItems;
418
+ return this.items.splice(start, deleteCount, ...insertItems);
348
419
  }
349
420
 
350
421
  /**
@@ -352,21 +423,26 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
352
423
  * @param items Elements to insert at the start of the Array.
353
424
  */
354
425
  unshift(...items: V[]): number {
355
- const length = this.length;
356
- const addedLength = items.length;
426
+ const changeTree = this[$changes];
357
427
 
358
- // const indexes = Array.from(this.$items.keys());
359
- const previousValues = Array.from(this.$items.values());
428
+ // shift indexes
429
+ changeTree.shiftChangeIndexes(items.length);
360
430
 
361
- items.forEach((item, i) => {
362
- this.setAt(i, item);
363
- });
431
+ // new index
432
+ if (changeTree.isFiltered) {
433
+ changeTree.filteredChanges.set(this.items.length, OPERATION.ADD);
434
+ } else {
435
+ changeTree.allChanges.set(this.items.length, OPERATION.ADD);
436
+ }
364
437
 
365
- previousValues.forEach((previousValue, i) => {
366
- this.setAt(addedLength + i, previousValue);
438
+ // FIXME: should we use OPERATION.MOVE here instead?
439
+ items.forEach((_, index) => {
440
+ changeTree.change(index, OPERATION.ADD)
367
441
  });
368
442
 
369
- return length + addedLength;
443
+ this.tmpItems.unshift(...items);
444
+
445
+ return this.items.unshift(...items);
370
446
  }
371
447
 
372
448
  /**
@@ -375,7 +451,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
375
451
  * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0.
376
452
  */
377
453
  indexOf(searchElement: V, fromIndex?: number): number {
378
- return Array.from(this.$items.values()).indexOf(searchElement, fromIndex);
454
+ return this.items.indexOf(searchElement, fromIndex);
379
455
  }
380
456
 
381
457
  /**
@@ -384,7 +460,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
384
460
  * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at the last index in the array.
385
461
  */
386
462
  lastIndexOf(searchElement: V, fromIndex: number = this.length - 1): number {
387
- return Array.from(this.$items.values()).lastIndexOf(searchElement, fromIndex);
463
+ return this.items.lastIndexOf(searchElement, fromIndex);
388
464
  }
389
465
 
390
466
  /**
@@ -396,7 +472,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
396
472
  * If thisArg is omitted, undefined is used as the this value.
397
473
  */
398
474
  every(callbackfn: (value: V, index: number, array: V[]) => unknown, thisArg?: any): boolean {
399
- return Array.from(this.$items.values()).every(callbackfn, thisArg);
475
+ return this.items.every(callbackfn, thisArg);
400
476
  }
401
477
 
402
478
  /**
@@ -408,7 +484,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
408
484
  * If thisArg is omitted, undefined is used as the this value.
409
485
  */
410
486
  some(callbackfn: (value: V, index: number, array: V[]) => unknown, thisArg?: any): boolean {
411
- return Array.from(this.$items.values()).some(callbackfn, thisArg);
487
+ return this.items.some(callbackfn, thisArg);
412
488
  }
413
489
 
414
490
  /**
@@ -417,7 +493,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
417
493
  * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
418
494
  */
419
495
  forEach(callbackfn: (value: V, index: number, array: V[]) => void, thisArg?: any): void {
420
- Array.from(this.$items.values()).forEach(callbackfn, thisArg);
496
+ return this.items.forEach(callbackfn, thisArg);
421
497
  }
422
498
 
423
499
  /**
@@ -426,7 +502,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
426
502
  * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
427
503
  */
428
504
  map<U>(callbackfn: (value: V, index: number, array: V[]) => U, thisArg?: any): U[] {
429
- return Array.from(this.$items.values()).map(callbackfn, thisArg);
505
+ return this.items.map(callbackfn, thisArg);
430
506
  }
431
507
 
432
508
  /**
@@ -436,7 +512,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
436
512
  */
437
513
  filter(callbackfn: (value: V, index: number, array: V[]) => unknown, thisArg?: any): V[]
438
514
  filter<S extends V>(callbackfn: (value: V, index: number, array: V[]) => value is S, thisArg?: any): V[] {
439
- return Array.from(this.$items.values()).filter(callbackfn, thisArg);
515
+ return this.items.filter(callbackfn, thisArg);
440
516
  }
441
517
 
442
518
  /**
@@ -445,7 +521,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
445
521
  * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
446
522
  */
447
523
  reduce<U=V>(callbackfn: (previousValue: U, currentValue: V, currentIndex: number, array: V[]) => U, initialValue?: U): U {
448
- return Array.prototype.reduce.apply(Array.from(this.$items.values()), arguments);
524
+ return this.items.reduce(callbackfn, initialValue);
449
525
  }
450
526
 
451
527
  /**
@@ -454,7 +530,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
454
530
  * @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
455
531
  */
456
532
  reduceRight<U=V>(callbackfn: (previousValue: U, currentValue: V, currentIndex: number, array: V[]) => U, initialValue?: U): U {
457
- return Array.prototype.reduceRight.apply(Array.from(this.$items.values()), arguments);
533
+ return this.items.reduceRight(callbackfn, initialValue);
458
534
  }
459
535
 
460
536
  /**
@@ -467,7 +543,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
467
543
  * predicate. If it is not provided, undefined is used instead.
468
544
  */
469
545
  find(predicate: (value: V, index: number, obj: V[]) => boolean, thisArg?: any): V | undefined {
470
- return Array.from(this.$items.values()).find(predicate, thisArg);
546
+ return this.items.find(predicate, thisArg);
471
547
  }
472
548
 
473
549
  /**
@@ -480,7 +556,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
480
556
  * predicate. If it is not provided, undefined is used instead.
481
557
  */
482
558
  findIndex(predicate: (value: V, index: number, obj: V[]) => unknown, thisArg?: any): number {
483
- return Array.from(this.$items.values()).findIndex(predicate, thisArg);
559
+ return this.items.findIndex(predicate, thisArg);
484
560
  }
485
561
 
486
562
  /**
@@ -521,16 +597,20 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
521
597
  /**
522
598
  * Returns a string representation of an array.
523
599
  */
524
- toString(): string { return this.$items.toString(); }
600
+ toString(): string {
601
+ return this.items.toString();
602
+ }
525
603
 
526
604
  /**
527
605
  * Returns a string representation of an array. The elements are converted to string using their toLocalString methods.
528
606
  */
529
- toLocaleString(): string { return this.$items.toLocaleString() };
607
+ toLocaleString(): string {
608
+ return this.items.toLocaleString()
609
+ };
530
610
 
531
611
  /** Iterator */
532
612
  [Symbol.iterator](): IterableIterator<V> {
533
- return Array.from(this.$items.values())[Symbol.iterator]();
613
+ return this.items[Symbol.iterator]();
534
614
  }
535
615
 
536
616
  static get [Symbol.species]() {
@@ -545,17 +625,17 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
545
625
  /**
546
626
  * Returns an iterable of key, value pairs for every entry in the array
547
627
  */
548
- entries(): IterableIterator<[number, V]> { return this.$items.entries(); }
628
+ entries(): IterableIterator<[number, V]> { return this.items.entries(); }
549
629
 
550
630
  /**
551
631
  * Returns an iterable of keys in the array
552
632
  */
553
- keys(): IterableIterator<number> { return this.$items.keys(); }
633
+ keys(): IterableIterator<number> { return this.items.keys(); }
554
634
 
555
635
  /**
556
636
  * Returns an iterable of values in the array
557
637
  */
558
- values(): IterableIterator<V> { return this.$items.values(); }
638
+ values(): IterableIterator<V> { return this.items.values(); }
559
639
 
560
640
  /**
561
641
  * Determines whether an array includes a certain element, returning true or false as appropriate.
@@ -563,7 +643,7 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
563
643
  * @param fromIndex The position in this array at which to begin searching for searchElement.
564
644
  */
565
645
  includes(searchElement: V, fromIndex?: number): boolean {
566
- return Array.from(this.$items.values()).includes(searchElement, fromIndex);
646
+ return this.items.includes(searchElement, fromIndex);
567
647
  }
568
648
 
569
649
  //
@@ -598,60 +678,71 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
598
678
  }
599
679
 
600
680
  findLast() {
601
- const arr = Array.from(this.$items.values());
602
681
  // @ts-ignore
603
- return arr.findLast.apply(arr, arguments);
682
+ return this.items.findLast.apply(this.items, arguments);
604
683
  }
605
684
 
606
685
  findLastIndex(...args) {
607
- const arr = Array.from(this.$items.values());
608
686
  // @ts-ignore
609
- return arr.findLastIndex.apply(arr, arguments);
687
+ return this.items.findLastIndex.apply(this.items, arguments);
610
688
  }
611
689
 
612
690
  //
613
691
  // ES2023
614
692
  //
615
- with(index: number, value: V): ArraySchema<V> {
616
- const copy = Array.from(this.$items.values());
693
+ with(index: number, value: V): V[] {
694
+ const copy = this.items.slice();
617
695
  copy[index] = value;
618
696
  return new ArraySchema(...copy);
619
697
  }
620
698
  toReversed(): V[] {
621
- return Array.from(this.$items.values()).reverse();
699
+ return this.items.slice().reverse();
622
700
  }
623
701
  toSorted(compareFn?: (a: V, b: V) => number): V[] {
624
- return Array.from(this.$items.values()).sort(compareFn);
702
+ return this.items.slice().sort(compareFn);
625
703
  }
626
704
  toSpliced(start: number, deleteCount: number, ...items: V[]): V[];
627
705
  toSpliced(start: number, deleteCount?: number): V[];
628
706
  // @ts-ignore
629
707
  toSpliced(start: unknown, deleteCount?: unknown, ...items?: unknown[]): V[] {
630
- const copy = Array.from(this.$items.values());
631
708
  // @ts-ignore
632
- return copy.toSpliced.apply(copy, arguments);
709
+ return this.items.toSpliced.apply(copy, arguments);
633
710
  }
634
711
 
635
- protected setIndex(index: number, key: number) {
636
- this.$indexes.set(index, key);
712
+ protected [$getByIndex](index: number, isEncodeAll: boolean = false) {
713
+ //
714
+ // TODO: avoid unecessary `this.tmpItems` check during decoding.
715
+ //
716
+ // ENCODING uses `this.tmpItems` (or `this.items` if `isEncodeAll` is true)
717
+ // DECODING uses `this.items`
718
+ //
719
+
720
+ return (isEncodeAll)
721
+ ? this.items[index]
722
+ : this.deletedIndexes[index]
723
+ ? this.items[index]
724
+ : this.tmpItems[index] || this.items[index];
725
+
726
+ // return (isEncodeAll)
727
+ // ? this.items[index]
728
+ // : this.tmpItems[index] ?? this.items[index];
637
729
  }
638
730
 
639
- protected getIndex(index: number) {
640
- return this.$indexes.get(index);
731
+ protected [$deleteByIndex](index: number) {
732
+ this.items[index] = undefined;
641
733
  }
642
734
 
643
- protected getByIndex(index: number) {
644
- return this.$items.get(this.$indexes.get(index));
735
+ protected [$onEncodeEnd]() {
736
+ this.tmpItems = this.items.slice();
737
+ this.deletedIndexes = {};
645
738
  }
646
739
 
647
- protected deleteByIndex(index: number) {
648
- const key = this.$indexes.get(index);
649
- this.$items.delete(key);
650
- this.$indexes.delete(index);
740
+ protected [$onDecodeEnd]() {
741
+ this.items = this.items.filter((item) => item !== undefined);
651
742
  }
652
743
 
653
744
  toArray() {
654
- return Array.from(this.$items.values());
745
+ return this.items.slice(0);
655
746
  }
656
747
 
657
748
  toJSON() {
@@ -669,11 +760,12 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
669
760
  let cloned: ArraySchema;
670
761
 
671
762
  if (isDecoding) {
672
- cloned = new ArraySchema(...Array.from(this.$items.values()));
763
+ cloned = new ArraySchema();
764
+ cloned.push(...this.items);
673
765
 
674
766
  } else {
675
767
  cloned = new ArraySchema(...this.map(item => (
676
- (item['$changes'])
768
+ (item[$changes])
677
769
  ? (item as any as Schema).clone()
678
770
  : item
679
771
  )));
@@ -683,3 +775,5 @@ export class ArraySchema<V = any> implements Array<V>, SchemaDecoderCallbacks {
683
775
  };
684
776
 
685
777
  }
778
+
779
+ registerType("array", { constructor: ArraySchema });