@colyseus/schema 3.0.0-alpha.5 → 3.0.0-alpha.50

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 (148) hide show
  1. package/README.md +148 -62
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +2234 -1510
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +2231 -1509
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +2233 -1509
  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 +2 -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 +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 +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 +75 -65
  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 +9 -8
  66. package/lib/encoder/Encoder.js +134 -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 -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 +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.js +1 -0
  98. package/lib/types/custom/CollectionSchema.js.map +1 -1
  99. package/lib/types/custom/MapSchema.d.ts +17 -15
  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 +19 -18
  115. package/src/Metadata.ts +190 -42
  116. package/src/Reflection.ts +76 -38
  117. package/src/Schema.ts +72 -70
  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/languages/haxe.ts +4 -0
  122. package/src/codegen/languages/lua.ts +19 -27
  123. package/src/codegen/parser.ts +107 -0
  124. package/src/codegen/types.ts +1 -0
  125. package/src/debug.ts +55 -0
  126. package/src/decoder/DecodeOperation.ts +46 -18
  127. package/src/decoder/Decoder.ts +17 -15
  128. package/src/decoder/ReferenceTracker.ts +5 -3
  129. package/src/decoder/strategy/StateCallbacks.ts +153 -82
  130. package/src/encoder/ChangeTree.ts +282 -209
  131. package/src/encoder/EncodeOperation.ts +78 -78
  132. package/src/encoder/Encoder.ts +161 -96
  133. package/src/encoder/Root.ts +93 -0
  134. package/src/encoder/StateView.ts +80 -88
  135. package/src/encoding/assert.ts +17 -8
  136. package/src/encoding/decode.ts +73 -93
  137. package/src/encoding/encode.ts +99 -65
  138. package/src/encoding/spec.ts +3 -5
  139. package/src/index.ts +12 -20
  140. package/src/types/HelperTypes.ts +54 -2
  141. package/src/types/TypeContext.ts +175 -0
  142. package/src/types/custom/ArraySchema.ts +49 -19
  143. package/src/types/custom/CollectionSchema.ts +1 -0
  144. package/src/types/custom/MapSchema.ts +30 -17
  145. package/src/types/custom/SetSchema.ts +1 -0
  146. package/src/types/registry.ts +22 -3
  147. package/src/types/symbols.ts +10 -7
  148. package/src/utils.ts +7 -3
@@ -24,11 +24,10 @@
24
24
  /**
25
25
  * ArraySchema operations
26
26
  */
27
- OPERATION[OPERATION["PUSH"] = 11] = "PUSH";
28
- OPERATION[OPERATION["UNSHIFT"] = 12] = "UNSHIFT";
29
27
  OPERATION[OPERATION["REVERSE"] = 15] = "REVERSE";
30
28
  OPERATION[OPERATION["MOVE"] = 32] = "MOVE";
31
29
  OPERATION[OPERATION["DELETE_BY_REFID"] = 33] = "DELETE_BY_REFID";
30
+ OPERATION[OPERATION["ADD_BY_REFID"] = 129] = "ADD_BY_REFID";
32
31
  })(exports.OPERATION || (exports.OPERATION = {}));
33
32
 
34
33
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
@@ -48,11 +47,6 @@
48
47
  * (MapSchema, ArraySchema, etc.)
49
48
  */
50
49
  const $childType = Symbol('$childType');
51
- /**
52
- * Special ChangeTree property to identify new instances
53
- * (Once they're encoded, they're not new anymore)
54
- */
55
- const $isNew = Symbol("$isNew");
56
50
  /**
57
51
  * Optional "discard" method for custom types (ArraySchema)
58
52
  * (Discards changes for next serialization)
@@ -62,476 +56,303 @@
62
56
  * When decoding, this method is called after the instance is fully decoded
63
57
  */
64
58
  const $onDecodeEnd = Symbol("$onDecodeEnd");
59
+ /**
60
+ * Metadata
61
+ */
62
+ const $descriptors = Symbol("$descriptors");
63
+ const $numFields = "$__numFields";
64
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
65
+ const $viewFieldIndexes = "$__viewFieldIndexes";
66
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
65
67
 
66
- const registeredTypes = {};
67
- const identifiers = new Map();
68
- function registerType(identifier, definition) {
69
- identifiers.set(definition.constructor, identifier);
70
- registeredTypes[identifier] = definition;
71
- }
72
- function getType(identifier) {
73
- return registeredTypes[identifier];
68
+ /**
69
+ * Copyright (c) 2018 Endel Dreyer
70
+ * Copyright (c) 2014 Ion Drive Software Ltd.
71
+ *
72
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
73
+ * of this software and associated documentation files (the "Software"), to deal
74
+ * in the Software without restriction, including without limitation the rights
75
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
76
+ * copies of the Software, and to permit persons to whom the Software is
77
+ * furnished to do so, subject to the following conditions:
78
+ *
79
+ * The above copyright notice and this permission notice shall be included in all
80
+ * copies or substantial portions of the Software.
81
+ *
82
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
83
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
84
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
85
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
86
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
87
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
88
+ * SOFTWARE
89
+ */
90
+ /**
91
+ * msgpack implementation highly based on notepack.io
92
+ * https://github.com/darrachequesne/notepack
93
+ */
94
+ let textEncoder;
95
+ // @ts-ignore
96
+ try {
97
+ textEncoder = new TextEncoder();
74
98
  }
75
-
76
- const Metadata = {
77
- addField(metadata, index, field, type, descriptor) {
78
- if (index > 64) {
79
- throw new Error(`Can't define field '${field}'.\nSchema instances may only have up to 64 fields.`);
99
+ catch (e) { }
100
+ const _convoBuffer$1 = new ArrayBuffer(8);
101
+ const _int32$1 = new Int32Array(_convoBuffer$1);
102
+ const _float32$1 = new Float32Array(_convoBuffer$1);
103
+ const _float64$1 = new Float64Array(_convoBuffer$1);
104
+ const _int64$1 = new BigInt64Array(_convoBuffer$1);
105
+ const hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength);
106
+ const utf8Length = (hasBufferByteLength)
107
+ ? Buffer.byteLength // node
108
+ : function (str, _) {
109
+ var c = 0, length = 0;
110
+ for (var i = 0, l = str.length; i < l; i++) {
111
+ c = str.charCodeAt(i);
112
+ if (c < 0x80) {
113
+ length += 1;
114
+ }
115
+ else if (c < 0x800) {
116
+ length += 2;
117
+ }
118
+ else if (c < 0xd800 || c >= 0xe000) {
119
+ length += 3;
120
+ }
121
+ else {
122
+ i++;
123
+ length += 4;
124
+ }
80
125
  }
81
- metadata[field] = Object.assign(metadata[field] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
82
- {
83
- type: (Array.isArray(type))
84
- ? { array: type[0] }
85
- : type,
86
- index,
87
- descriptor,
88
- });
89
- // map -1 as last field index
90
- Object.defineProperty(metadata, -1, {
91
- value: index,
92
- enumerable: false,
93
- configurable: true
94
- });
95
- // map index => field name (non enumerable)
96
- Object.defineProperty(metadata, index, {
97
- value: field,
98
- enumerable: false,
99
- configurable: true,
100
- });
101
- },
102
- setTag(metadata, fieldName, tag) {
103
- // add 'tag' to the field
104
- const field = metadata[fieldName];
105
- field.tag = tag;
106
- if (!metadata[-2]) {
107
- // -2: all field indexes with "view" tag
108
- Object.defineProperty(metadata, -2, {
109
- value: [],
110
- enumerable: false,
111
- configurable: true
112
- });
113
- // -3: field indexes by "view" tag
114
- Object.defineProperty(metadata, -3, {
115
- value: {},
116
- enumerable: false,
117
- configurable: true
118
- });
126
+ return length;
127
+ };
128
+ function utf8Write(view, str, it) {
129
+ var c = 0;
130
+ for (var i = 0, l = str.length; i < l; i++) {
131
+ c = str.charCodeAt(i);
132
+ if (c < 0x80) {
133
+ view[it.offset++] = c;
119
134
  }
120
- metadata[-2].push(field.index);
121
- if (!metadata[-3][tag]) {
122
- metadata[-3][tag] = [];
135
+ else if (c < 0x800) {
136
+ view[it.offset] = 0xc0 | (c >> 6);
137
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
138
+ it.offset += 2;
123
139
  }
124
- metadata[-3][tag].push(field.index);
125
- },
126
- setFields(target, fields) {
127
- const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
128
- // target[$track] = function (changeTree, index: number, operation: OPERATION = OPERATION.ADD) {
129
- // changeTree.change(index, operation, encodeSchemaOperation);
130
- // };
131
- // target[$encoder] = encodeSchemaOperation;
132
- // target[$decoder] = decodeSchemaOperation;
133
- // if (!target.prototype.toJSON) { target.prototype.toJSON = Schema.prototype.toJSON; }
134
- let index = 0;
135
- for (const field in fields) {
136
- const type = fields[field];
137
- // FIXME: this code is duplicated from @type() annotation
138
- const complexTypeKlass = (Array.isArray(type))
139
- ? getType("array")
140
- : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
141
- Metadata.addField(metadata, index, field, type, getPropertyDescriptor(`_${field}`, index, type, complexTypeKlass, metadata, field));
142
- index++;
140
+ else if (c < 0xd800 || c >= 0xe000) {
141
+ view[it.offset] = 0xe0 | (c >> 12);
142
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
143
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
144
+ it.offset += 3;
143
145
  }
144
- },
145
- isDeprecated(metadata, field) {
146
- return metadata[field].deprecated === true;
147
- },
148
- isValidInstance(klass) {
149
- return (klass.constructor[Symbol.metadata] &&
150
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
151
- },
152
- getFields(klass) {
153
- const metadata = klass[Symbol.metadata];
154
- const fields = {};
155
- for (let i = 0; i <= metadata[-1]; i++) {
156
- fields[metadata[i]] = metadata[metadata[i]].type;
146
+ else {
147
+ i++;
148
+ c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
149
+ view[it.offset] = 0xf0 | (c >> 18);
150
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
151
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
152
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
153
+ it.offset += 4;
157
154
  }
158
- return fields;
159
155
  }
160
- };
161
-
162
- var _a$5;
163
- class Root {
164
- constructor() {
165
- this.nextUniqueId = 0;
166
- this.refCount = new WeakMap();
167
- // all changes
168
- this.allChanges = new Map();
169
- this.allFilteredChanges = new Map();
170
- // pending changes to be encoded
171
- this.changes = new Map();
172
- this.filteredChanges = new Map();
156
+ }
157
+ function int8$1(bytes, value, it) {
158
+ bytes[it.offset++] = value & 255;
159
+ }
160
+ function uint8$1(bytes, value, it) {
161
+ bytes[it.offset++] = value & 255;
162
+ }
163
+ function int16$1(bytes, value, it) {
164
+ bytes[it.offset++] = value & 255;
165
+ bytes[it.offset++] = (value >> 8) & 255;
166
+ }
167
+ function uint16$1(bytes, value, it) {
168
+ bytes[it.offset++] = value & 255;
169
+ bytes[it.offset++] = (value >> 8) & 255;
170
+ }
171
+ function int32$1(bytes, value, it) {
172
+ bytes[it.offset++] = value & 255;
173
+ bytes[it.offset++] = (value >> 8) & 255;
174
+ bytes[it.offset++] = (value >> 16) & 255;
175
+ bytes[it.offset++] = (value >> 24) & 255;
176
+ }
177
+ function uint32$1(bytes, value, it) {
178
+ const b4 = value >> 24;
179
+ const b3 = value >> 16;
180
+ const b2 = value >> 8;
181
+ const b1 = value;
182
+ bytes[it.offset++] = b1 & 255;
183
+ bytes[it.offset++] = b2 & 255;
184
+ bytes[it.offset++] = b3 & 255;
185
+ bytes[it.offset++] = b4 & 255;
186
+ }
187
+ function int64$1(bytes, value, it) {
188
+ const high = Math.floor(value / Math.pow(2, 32));
189
+ const low = value >>> 0;
190
+ uint32$1(bytes, low, it);
191
+ uint32$1(bytes, high, it);
192
+ }
193
+ function uint64$1(bytes, value, it) {
194
+ const high = (value / Math.pow(2, 32)) >> 0;
195
+ const low = value >>> 0;
196
+ uint32$1(bytes, low, it);
197
+ uint32$1(bytes, high, it);
198
+ }
199
+ function bigint64$1(bytes, value, it) {
200
+ _int64$1[0] = BigInt.asIntN(64, value);
201
+ int32$1(bytes, _int32$1[0], it);
202
+ int32$1(bytes, _int32$1[1], it);
203
+ }
204
+ function biguint64$1(bytes, value, it) {
205
+ _int64$1[0] = BigInt.asIntN(64, value);
206
+ int32$1(bytes, _int32$1[0], it);
207
+ int32$1(bytes, _int32$1[1], it);
208
+ }
209
+ function float32$1(bytes, value, it) {
210
+ _float32$1[0] = value;
211
+ int32$1(bytes, _int32$1[0], it);
212
+ }
213
+ function float64$1(bytes, value, it) {
214
+ _float64$1[0] = value;
215
+ int32$1(bytes, _int32$1[0 ], it);
216
+ int32$1(bytes, _int32$1[1 ], it);
217
+ }
218
+ function boolean$1(bytes, value, it) {
219
+ bytes[it.offset++] = value ? 1 : 0; // uint8
220
+ }
221
+ function string$1(bytes, value, it) {
222
+ // encode `null` strings as empty.
223
+ if (!value) {
224
+ value = "";
173
225
  }
174
- getNextUniqueId() {
175
- return this.nextUniqueId++;
226
+ let length = utf8Length(value, "utf8");
227
+ let size = 0;
228
+ // fixstr
229
+ if (length < 0x20) {
230
+ bytes[it.offset++] = length | 0xa0;
231
+ size = 1;
176
232
  }
177
- add(changeTree) {
178
- const refCount = this.refCount.get(changeTree) || 0;
179
- this.refCount.set(changeTree, refCount + 1);
233
+ // str 8
234
+ else if (length < 0x100) {
235
+ bytes[it.offset++] = 0xd9;
236
+ bytes[it.offset++] = length % 255;
237
+ size = 2;
180
238
  }
181
- remove(changeTree) {
182
- const refCount = this.refCount.get(changeTree);
183
- if (refCount <= 1) {
184
- this.allChanges.delete(changeTree);
185
- this.changes.delete(changeTree);
186
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
187
- this.allFilteredChanges.delete(changeTree);
188
- this.filteredChanges.delete(changeTree);
189
- }
190
- this.refCount.delete(changeTree);
191
- }
192
- else {
193
- this.refCount.set(changeTree, refCount - 1);
194
- }
195
- changeTree.forEachChild((child, _) => this.remove(child));
239
+ // str 16
240
+ else if (length < 0x10000) {
241
+ bytes[it.offset++] = 0xda;
242
+ uint16$1(bytes, length, it);
243
+ size = 3;
196
244
  }
197
- clear() {
198
- this.changes.clear();
245
+ // str 32
246
+ else if (length < 0x100000000) {
247
+ bytes[it.offset++] = 0xdb;
248
+ uint32$1(bytes, length, it);
249
+ size = 5;
250
+ }
251
+ else {
252
+ throw new Error('String too long');
199
253
  }
254
+ utf8Write(bytes, value, it);
255
+ return size + length;
200
256
  }
201
- class ChangeTree {
202
- static { _a$5 = $isNew; }
203
- ;
204
- constructor(ref) {
205
- this.indexes = {}; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)
206
- this.currentOperationIndex = 0;
207
- this.allChanges = new Map();
208
- this.allFilteredChanges = new Map();
209
- this.changes = new Map();
210
- this.filteredChanges = new Map();
211
- this[_a$5] = true;
212
- this.ref = ref;
257
+ function number$1(bytes, value, it) {
258
+ if (isNaN(value)) {
259
+ return number$1(bytes, 0, it);
213
260
  }
214
- setRoot(root) {
215
- this.root = root;
216
- this.root.add(this);
217
- //
218
- // At Schema initialization, the "root" structure might not be available
219
- // yet, as it only does once the "Encoder" has been set up.
220
- //
221
- // So the "parent" may be already set without a "root".
222
- //
223
- this.checkIsFiltered(this.parent, this.parentIndex);
224
- // unique refId for the ChangeTree.
225
- this.ensureRefId();
226
- if (!this.isFiltered) {
227
- this.root.changes.set(this, this.changes);
228
- }
229
- if (this.isFiltered || this.isPartiallyFiltered) {
230
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
231
- this.root.filteredChanges.set(this, this.filteredChanges);
232
- // } else {
233
- // this.root.allChanges.set(this, this.allChanges);
234
- }
235
- if (!this.isFiltered) {
236
- this.root.allChanges.set(this, this.allChanges);
237
- }
238
- this.forEachChild((changeTree, _) => {
239
- changeTree.setRoot(root);
240
- });
241
- // this.allChanges.forEach((_, index) => {
242
- // const childRef = this.ref[$getByIndex](index);
243
- // if (childRef && childRef[$changes]) {
244
- // childRef[$changes].setRoot(root);
245
- // }
246
- // });
261
+ else if (!isFinite(value)) {
262
+ return number$1(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it);
247
263
  }
248
- setParent(parent, root, parentIndex) {
249
- this.parent = parent;
250
- this.parentIndex = parentIndex;
251
- // avoid setting parents with empty `root`
252
- if (!root) {
253
- return;
254
- }
255
- root.add(this);
256
- // skip if parent is already set
257
- if (root === this.root) {
258
- this.forEachChild((changeTree, atIndex) => {
259
- changeTree.setParent(this.ref, root, atIndex);
260
- });
261
- return;
262
- }
263
- this.root = root;
264
- this.checkIsFiltered(parent, parentIndex);
265
- if (!this.isFiltered) {
266
- this.root.changes.set(this, this.changes);
267
- }
268
- if (this.isFiltered || this.isPartiallyFiltered) {
269
- this.root.filteredChanges.set(this, this.filteredChanges);
270
- this.root.allFilteredChanges.set(this, this.filteredChanges);
271
- }
272
- else {
273
- this.root.allChanges.set(this, this.allChanges);
264
+ else if (value !== (value | 0)) {
265
+ if (Math.abs(value) <= 3.4028235e+38) { // range check
266
+ _float32$1[0] = value;
267
+ if (Math.abs(Math.abs(_float32$1[0]) - Math.abs(value)) < 1e-4) { // precision check; adjust 1e-n (n = precision) to in-/decrease acceptable precision loss
268
+ // now we know value is in range for f32 and has acceptable precision for f32
269
+ bytes[it.offset++] = 0xca;
270
+ float32$1(bytes, value, it);
271
+ return 5;
272
+ }
274
273
  }
275
- this.ensureRefId();
276
- this.forEachChild((changeTree, atIndex) => {
277
- changeTree.setParent(this.ref, root, atIndex);
278
- });
274
+ bytes[it.offset++] = 0xcb;
275
+ float64$1(bytes, value, it);
276
+ return 9;
279
277
  }
280
- forEachChild(callback) {
281
- //
282
- // assign same parent on child structures
283
- //
284
- if (Metadata.isValidInstance(this.ref)) {
285
- const metadata = this.ref['constructor'][Symbol.metadata];
286
- // FIXME: need to iterate over parent metadata instead.
287
- for (const field in metadata) {
288
- const value = this.ref[field];
289
- if (value && value[$changes]) {
290
- callback(value[$changes], metadata[field].index);
291
- }
292
- }
293
- }
294
- else if (typeof (this.ref) === "object") {
295
- // MapSchema / ArraySchema, etc.
296
- this.ref.forEach((value, key) => {
297
- if (Metadata.isValidInstance(value)) {
298
- callback(value[$changes], this.ref[$changes].indexes[key]);
299
- }
300
- });
301
- }
302
- }
303
- operation(op) {
304
- this.changes.set(--this.currentOperationIndex, op);
305
- this.root?.changes.set(this, this.changes);
306
- }
307
- change(index, operation = exports.OPERATION.ADD) {
308
- const metadata = this.ref['constructor'][Symbol.metadata];
309
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
310
- const changeSet = (isFiltered)
311
- ? this.filteredChanges
312
- : this.changes;
313
- const previousOperation = changeSet.get(index);
314
- if (!previousOperation || previousOperation === exports.OPERATION.DELETE) {
315
- const op = (!previousOperation)
316
- ? operation
317
- : (previousOperation === exports.OPERATION.DELETE)
318
- ? exports.OPERATION.DELETE_AND_ADD
319
- : operation;
320
- changeSet.set(index, op);
321
- }
322
- //
323
- // TODO: are DELETE operations being encoded as ADD here ??
324
- //
325
- if (isFiltered) {
326
- this.allFilteredChanges.set(index, exports.OPERATION.ADD);
327
- this.root?.filteredChanges.set(this, this.filteredChanges);
328
- }
329
- else {
330
- this.allChanges.set(index, exports.OPERATION.ADD);
331
- this.root?.changes.set(this, this.changes);
332
- }
333
- }
334
- shiftChangeIndexes(shiftIndex) {
335
- //
336
- // Used only during:
337
- //
338
- // - ArraySchema#unshift()
339
- //
340
- const changeSet = (this.isFiltered)
341
- ? this.filteredChanges
342
- : this.changes;
343
- const changeSetEntries = Array.from(changeSet.entries());
344
- changeSet.clear();
345
- // Re-insert each entry with the shifted index
346
- for (const [index, op] of changeSetEntries) {
347
- changeSet.set(index + shiftIndex, op);
348
- }
349
- }
350
- shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
351
- //
352
- // Used only during:
353
- //
354
- // - ArraySchema#splice()
355
- //
356
- if (this.isFiltered || this.isPartiallyFiltered) {
357
- this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allFilteredChanges);
358
- this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
359
- }
360
- else {
361
- this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
362
- }
363
- }
364
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
365
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
366
- // console.log('shiftAllChangeIndexes', index >= startIndex, { index, op, shiftIndex, startIndex })
367
- if (index >= startIndex) {
368
- allChangeSet.delete(index);
369
- allChangeSet.set(index + shiftIndex, op);
370
- }
371
- });
372
- }
373
- indexedOperation(index, operation, allChangesIndex = index) {
374
- const metadata = this.ref['constructor'][Symbol.metadata];
375
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
376
- if (isFiltered) {
377
- this.allFilteredChanges.set(allChangesIndex, exports.OPERATION.ADD);
378
- this.filteredChanges.set(index, operation);
379
- this.root?.filteredChanges.set(this, this.filteredChanges);
380
- }
381
- else {
382
- this.allChanges.set(allChangesIndex, exports.OPERATION.ADD);
383
- this.changes.set(index, operation);
384
- this.root?.changes.set(this, this.changes);
385
- }
386
- }
387
- getType(index) {
388
- if (Metadata.isValidInstance(this.ref)) {
389
- const metadata = this.ref['constructor'][Symbol.metadata];
390
- return metadata[metadata[index]].type;
391
- }
392
- else {
393
- //
394
- // Get the child type from parent structure.
395
- // - ["string"] => "string"
396
- // - { map: "string" } => "string"
397
- // - { set: "string" } => "string"
398
- //
399
- return this.ref[$childType];
400
- }
401
- }
402
- getChange(index) {
403
- // TODO: optimize this. avoid checking against multiple instances
404
- return this.changes.get(index) ?? this.filteredChanges.get(index);
405
- }
406
- //
407
- // used during `.encode()`
408
- //
409
- getValue(index, isEncodeAll = false) {
410
- //
411
- // `isEncodeAll` param is only used by ArraySchema
412
- //
413
- return this.ref[$getByIndex](index, isEncodeAll);
414
- }
415
- delete(index, operation, allChangesIndex = index) {
416
- if (index === undefined) {
417
- try {
418
- throw new Error(`@colyseus/schema ${this.ref.constructor.name}: trying to delete non-existing index '${index}'`);
419
- }
420
- catch (e) {
421
- console.warn(e);
422
- }
423
- return;
278
+ if (value >= 0) {
279
+ // positive fixnum
280
+ if (value < 0x80) {
281
+ bytes[it.offset++] = value & 255; // uint8
282
+ return 1;
424
283
  }
425
- const metadata = this.ref['constructor'][Symbol.metadata];
426
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
427
- const changeSet = (isFiltered)
428
- ? this.filteredChanges
429
- : this.changes;
430
- const previousValue = this.getValue(index);
431
- changeSet.set(index, operation ?? exports.OPERATION.DELETE);
432
- // remove `root` reference
433
- if (previousValue && previousValue[$changes]) {
434
- previousValue[$changes].root = undefined;
435
- //
436
- // FIXME: this.root is "undefined"
437
- //
438
- // This method is being called at decoding time when a DELETE operation is found.
439
- //
440
- // - This is due to using the concrete Schema class at decoding time.
441
- // - "Reflected" structures do not have this problem.
442
- //
443
- // (the property descriptors should NOT be used at decoding time. only at encoding time.)
444
- //
445
- this.root?.remove(previousValue[$changes]);
284
+ // uint 8
285
+ if (value < 0x100) {
286
+ bytes[it.offset++] = 0xcc;
287
+ bytes[it.offset++] = value & 255; // uint8
288
+ return 2;
446
289
  }
447
- //
448
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
449
- //
450
- if (isFiltered) {
451
- this.root?.filteredChanges.set(this, this.filteredChanges);
452
- this.allFilteredChanges.delete(allChangesIndex);
290
+ // uint 16
291
+ if (value < 0x10000) {
292
+ bytes[it.offset++] = 0xcd;
293
+ uint16$1(bytes, value, it);
294
+ return 3;
453
295
  }
454
- else {
455
- this.root?.changes.set(this, this.changes);
456
- this.allChanges.delete(allChangesIndex);
296
+ // uint 32
297
+ if (value < 0x100000000) {
298
+ bytes[it.offset++] = 0xce;
299
+ uint32$1(bytes, value, it);
300
+ return 5;
457
301
  }
302
+ // uint 64
303
+ bytes[it.offset++] = 0xcf;
304
+ uint64$1(bytes, value, it);
305
+ return 9;
458
306
  }
459
- endEncode() {
460
- this.changes.clear();
461
- this.ref[$onEncodeEnd]?.();
462
- // Not a new instance anymore
463
- delete this[$isNew];
464
- }
465
- discard(discardAll = false) {
466
- //
467
- // > MapSchema:
468
- // Remove cached key to ensure ADD operations is unsed instead of
469
- // REPLACE in case same key is used on next patches.
470
- //
471
- this.ref[$onEncodeEnd]?.();
472
- this.changes.clear();
473
- this.filteredChanges.clear();
474
- // reset operation index
475
- this.currentOperationIndex = 0;
476
- if (discardAll) {
477
- this.allChanges.clear();
478
- this.allFilteredChanges.clear();
479
- // remove children references
480
- this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
307
+ else {
308
+ // negative fixnum
309
+ if (value >= -0x20) {
310
+ bytes[it.offset++] = 0xe0 | (value + 0x20);
311
+ return 1;
481
312
  }
482
- }
483
- /**
484
- * Recursively discard all changes from this, and child structures.
485
- */
486
- discardAll() {
487
- this.changes.forEach((_, fieldIndex) => {
488
- const value = this.getValue(fieldIndex);
489
- if (value && value[$changes]) {
490
- value[$changes].discardAll();
491
- }
492
- });
493
- this.discard();
494
- }
495
- ensureRefId() {
496
- // skip if refId is already set.
497
- if (this.refId !== undefined) {
498
- return;
313
+ // int 8
314
+ if (value >= -0x80) {
315
+ bytes[it.offset++] = 0xd0;
316
+ int8$1(bytes, value, it);
317
+ return 2;
499
318
  }
500
- this.refId = this.root.getNextUniqueId();
501
- }
502
- get changed() {
503
- return this.changes.size > 0;
504
- }
505
- checkIsFiltered(parent, parentIndex) {
506
- // Detect if current structure has "filters" declared
507
- this.isPartiallyFiltered = this.ref['constructor']?.[Symbol.metadata]?.[-2];
508
- // TODO: support "partially filtered", where the instance is visible, but only a field is not.
509
- // Detect if parent has "filters" declared
510
- while (parent && !this.isFiltered) {
511
- const metadata = parent['constructor'][Symbol.metadata];
512
- const fieldName = metadata?.[parentIndex];
513
- const isParentOwned = metadata?.[fieldName]?.tag !== undefined;
514
- this.isFiltered = isParentOwned || parent[$changes].isFiltered; // metadata?.[-2]
515
- parent = parent[$changes].parent;
319
+ // int 16
320
+ if (value >= -0x8000) {
321
+ bytes[it.offset++] = 0xd1;
322
+ int16$1(bytes, value, it);
323
+ return 3;
516
324
  }
517
- //
518
- // TODO: refactor this!
519
- //
520
- // swapping `changes` and `filteredChanges` is required here
521
- // because "isFiltered" may not be imedialely available on `change()`
522
- //
523
- if (this.isFiltered && this.changes.size > 0) {
524
- // swap changes reference
525
- const changes = this.changes;
526
- this.changes = this.filteredChanges;
527
- this.filteredChanges = changes;
528
- // swap "all changes" reference
529
- const allFilteredChanges = this.allFilteredChanges;
530
- this.allFilteredChanges = this.allChanges;
531
- this.allChanges = allFilteredChanges;
325
+ // int 32
326
+ if (value >= -0x80000000) {
327
+ bytes[it.offset++] = 0xd2;
328
+ int32$1(bytes, value, it);
329
+ return 5;
532
330
  }
331
+ // int 64
332
+ bytes[it.offset++] = 0xd3;
333
+ int64$1(bytes, value, it);
334
+ return 9;
533
335
  }
534
336
  }
337
+ const encode = {
338
+ int8: int8$1,
339
+ uint8: uint8$1,
340
+ int16: int16$1,
341
+ uint16: uint16$1,
342
+ int32: int32$1,
343
+ uint32: uint32$1,
344
+ int64: int64$1,
345
+ uint64: uint64$1,
346
+ bigint64: bigint64$1,
347
+ biguint64: biguint64$1,
348
+ float32: float32$1,
349
+ float64: float64$1,
350
+ boolean: boolean$1,
351
+ string: string$1,
352
+ number: number$1,
353
+ utf8Write,
354
+ utf8Length,
355
+ };
535
356
 
536
357
  /**
537
358
  * Copyright (c) 2018 Endel Dreyer
@@ -555,700 +376,1154 @@
555
376
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
556
377
  * SOFTWARE
557
378
  */
558
- /**
559
- * msgpack implementation highly based on notepack.io
560
- * https://github.com/darrachequesne/notepack
561
- */
562
- let textEncoder;
563
- // @ts-ignore
564
- try {
565
- textEncoder = new TextEncoder();
566
- }
567
- catch (e) { }
568
- function utf8Length(str) {
569
- var c = 0, length = 0;
570
- for (var i = 0, l = str.length; i < l; i++) {
571
- c = str.charCodeAt(i);
572
- if (c < 0x80) {
573
- length += 1;
574
- }
575
- else if (c < 0x800) {
576
- length += 2;
379
+ // force little endian to facilitate decoding on multiple implementations
380
+ const _convoBuffer = new ArrayBuffer(8);
381
+ const _int32 = new Int32Array(_convoBuffer);
382
+ const _float32 = new Float32Array(_convoBuffer);
383
+ const _float64 = new Float64Array(_convoBuffer);
384
+ const _uint64 = new BigUint64Array(_convoBuffer);
385
+ const _int64 = new BigInt64Array(_convoBuffer);
386
+ function utf8Read(bytes, it, length) {
387
+ var string = '', chr = 0;
388
+ for (var i = it.offset, end = it.offset + length; i < end; i++) {
389
+ var byte = bytes[i];
390
+ if ((byte & 0x80) === 0x00) {
391
+ string += String.fromCharCode(byte);
392
+ continue;
577
393
  }
578
- else if (c < 0xd800 || c >= 0xe000) {
579
- length += 3;
394
+ if ((byte & 0xe0) === 0xc0) {
395
+ string += String.fromCharCode(((byte & 0x1f) << 6) |
396
+ (bytes[++i] & 0x3f));
397
+ continue;
580
398
  }
581
- else {
582
- i++;
583
- length += 4;
399
+ if ((byte & 0xf0) === 0xe0) {
400
+ string += String.fromCharCode(((byte & 0x0f) << 12) |
401
+ ((bytes[++i] & 0x3f) << 6) |
402
+ ((bytes[++i] & 0x3f) << 0));
403
+ continue;
584
404
  }
585
- }
586
- return length;
587
- }
588
- function utf8Write(view, str, it) {
589
- var c = 0;
590
- for (var i = 0, l = str.length; i < l; i++) {
591
- c = str.charCodeAt(i);
592
- if (c < 0x80) {
593
- view[it.offset++] = c;
594
- }
595
- else if (c < 0x800) {
596
- view[it.offset++] = 0xc0 | (c >> 6);
597
- view[it.offset++] = 0x80 | (c & 0x3f);
598
- }
599
- else if (c < 0xd800 || c >= 0xe000) {
600
- view[it.offset++] = 0xe0 | (c >> 12);
601
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
602
- view[it.offset++] = 0x80 | (c & 0x3f);
603
- }
604
- else {
605
- i++;
606
- c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
607
- view[it.offset++] = 0xf0 | (c >> 18);
608
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
609
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
610
- view[it.offset++] = 0x80 | (c & 0x3f);
405
+ if ((byte & 0xf8) === 0xf0) {
406
+ chr = ((byte & 0x07) << 18) |
407
+ ((bytes[++i] & 0x3f) << 12) |
408
+ ((bytes[++i] & 0x3f) << 6) |
409
+ ((bytes[++i] & 0x3f) << 0);
410
+ if (chr >= 0x010000) { // surrogate pair
411
+ chr -= 0x010000;
412
+ string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
413
+ }
414
+ else {
415
+ string += String.fromCharCode(chr);
416
+ }
417
+ continue;
611
418
  }
419
+ console.error('Invalid byte ' + byte.toString(16));
420
+ // (do not throw error to avoid server/client from crashing due to hack attemps)
421
+ // throw new Error('Invalid byte ' + byte.toString(16));
612
422
  }
423
+ it.offset += length;
424
+ return string;
613
425
  }
614
- function int8$1(bytes, value, it) {
615
- bytes[it.offset++] = value & 255;
426
+ function int8(bytes, it) {
427
+ return uint8(bytes, it) << 24 >> 24;
616
428
  }
617
- function uint8$1(bytes, value, it) {
618
- bytes[it.offset++] = value & 255;
429
+ function uint8(bytes, it) {
430
+ return bytes[it.offset++];
619
431
  }
620
- function int16$1(bytes, value, it) {
621
- bytes[it.offset++] = value & 255;
622
- bytes[it.offset++] = (value >> 8) & 255;
432
+ function int16(bytes, it) {
433
+ return uint16(bytes, it) << 16 >> 16;
623
434
  }
624
- function uint16$1(bytes, value, it) {
625
- bytes[it.offset++] = value & 255;
626
- bytes[it.offset++] = (value >> 8) & 255;
435
+ function uint16(bytes, it) {
436
+ return bytes[it.offset++] | bytes[it.offset++] << 8;
627
437
  }
628
- function int32$1(bytes, value, it) {
629
- bytes[it.offset++] = value & 255;
630
- bytes[it.offset++] = (value >> 8) & 255;
631
- bytes[it.offset++] = (value >> 16) & 255;
632
- bytes[it.offset++] = (value >> 24) & 255;
438
+ function int32(bytes, it) {
439
+ return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24;
633
440
  }
634
- function uint32$1(bytes, value, it) {
635
- const b4 = value >> 24;
636
- const b3 = value >> 16;
637
- const b2 = value >> 8;
638
- const b1 = value;
639
- bytes[it.offset++] = b1 & 255;
640
- bytes[it.offset++] = b2 & 255;
641
- bytes[it.offset++] = b3 & 255;
642
- bytes[it.offset++] = b4 & 255;
441
+ function uint32(bytes, it) {
442
+ return int32(bytes, it) >>> 0;
643
443
  }
644
- function int64$1(bytes, value, it) {
645
- const high = Math.floor(value / Math.pow(2, 32));
646
- const low = value >>> 0;
647
- uint32$1(bytes, low, it);
648
- uint32$1(bytes, high, it);
444
+ function float32(bytes, it) {
445
+ _int32[0] = int32(bytes, it);
446
+ return _float32[0];
649
447
  }
650
- function uint64$1(bytes, value, it) {
651
- const high = (value / Math.pow(2, 32)) >> 0;
652
- const low = value >>> 0;
653
- uint32$1(bytes, low, it);
654
- uint32$1(bytes, high, it);
448
+ function float64(bytes, it) {
449
+ _int32[0 ] = int32(bytes, it);
450
+ _int32[1 ] = int32(bytes, it);
451
+ return _float64[0];
655
452
  }
656
- function float32$1(bytes, value, it) {
657
- writeFloat32(bytes, value, it);
453
+ function int64(bytes, it) {
454
+ const low = uint32(bytes, it);
455
+ const high = int32(bytes, it) * Math.pow(2, 32);
456
+ return high + low;
658
457
  }
659
- function float64$1(bytes, value, it) {
660
- writeFloat64(bytes, value, it);
458
+ function uint64(bytes, it) {
459
+ const low = uint32(bytes, it);
460
+ const high = uint32(bytes, it) * Math.pow(2, 32);
461
+ return high + low;
661
462
  }
662
- const _int32$1 = new Int32Array(2);
663
- const _float32$1 = new Float32Array(_int32$1.buffer);
664
- const _float64$1 = new Float64Array(_int32$1.buffer);
665
- function writeFloat32(bytes, value, it) {
666
- _float32$1[0] = value;
667
- int32$1(bytes, _int32$1[0], it);
463
+ function bigint64(bytes, it) {
464
+ _int32[0] = int32(bytes, it);
465
+ _int32[1] = int32(bytes, it);
466
+ return _int64[0];
668
467
  }
669
- function writeFloat64(bytes, value, it) {
670
- _float64$1[0] = value;
671
- int32$1(bytes, _int32$1[0 ], it);
672
- int32$1(bytes, _int32$1[1 ], it);
468
+ function biguint64(bytes, it) {
469
+ _int32[0] = int32(bytes, it);
470
+ _int32[1] = int32(bytes, it);
471
+ return _uint64[0];
673
472
  }
674
- function boolean$1(bytes, value, it) {
675
- bytes[it.offset++] = value ? 1 : 0; // uint8
473
+ function boolean(bytes, it) {
474
+ return uint8(bytes, it) > 0;
676
475
  }
677
- function string$1(bytes, value, it) {
678
- // encode `null` strings as empty.
679
- if (!value) {
680
- value = "";
681
- }
682
- // let length = utf8Length(value);
683
- let length = Buffer.byteLength(value, "utf8");
684
- let size = 0;
685
- // fixstr
686
- if (length < 0x20) {
687
- bytes[it.offset++] = length | 0xa0;
688
- size = 1;
689
- }
690
- // str 8
691
- else if (length < 0x100) {
692
- bytes[it.offset++] = 0xd9;
693
- bytes[it.offset++] = length % 255;
694
- size = 2;
476
+ function string(bytes, it) {
477
+ const prefix = bytes[it.offset++];
478
+ let length;
479
+ if (prefix < 0xc0) {
480
+ // fixstr
481
+ length = prefix & 0x1f;
695
482
  }
696
- // str 16
697
- else if (length < 0x10000) {
698
- bytes[it.offset++] = 0xda;
699
- uint16$1(bytes, length, it);
700
- size = 3;
483
+ else if (prefix === 0xd9) {
484
+ length = uint8(bytes, it);
701
485
  }
702
- // str 32
703
- else if (length < 0x100000000) {
704
- bytes[it.offset++] = 0xdb;
705
- uint32$1(bytes, length, it);
706
- size = 5;
486
+ else if (prefix === 0xda) {
487
+ length = uint16(bytes, it);
707
488
  }
708
- else {
709
- throw new Error('String too long');
489
+ else if (prefix === 0xdb) {
490
+ length = uint32(bytes, it);
710
491
  }
711
- utf8Write(bytes, value, it);
712
- return size + length;
492
+ return utf8Read(bytes, it, length);
713
493
  }
714
- function number$1(bytes, value, it) {
715
- if (isNaN(value)) {
716
- return number$1(bytes, 0, it);
494
+ function number(bytes, it) {
495
+ const prefix = bytes[it.offset++];
496
+ if (prefix < 0x80) {
497
+ // positive fixint
498
+ return prefix;
717
499
  }
718
- else if (!isFinite(value)) {
719
- return number$1(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it);
500
+ else if (prefix === 0xca) {
501
+ // float 32
502
+ return float32(bytes, it);
720
503
  }
721
- else if (value !== (value | 0)) {
722
- bytes[it.offset++] = 0xcb;
723
- writeFloat64(bytes, value, it);
724
- return 9;
725
- // TODO: encode float 32?
726
- // is it possible to differentiate between float32 / float64 here?
727
- // // float 32
728
- // bytes.push(0xca);
729
- // writeFloat32(bytes, value);
730
- // return 5;
504
+ else if (prefix === 0xcb) {
505
+ // float 64
506
+ return float64(bytes, it);
731
507
  }
732
- if (value >= 0) {
733
- // positive fixnum
734
- if (value < 0x80) {
735
- bytes[it.offset++] = value & 255; // uint8
736
- return 1;
737
- }
508
+ else if (prefix === 0xcc) {
738
509
  // uint 8
739
- if (value < 0x100) {
740
- bytes[it.offset++] = 0xcc;
741
- bytes[it.offset++] = value & 255; // uint8
742
- return 2;
743
- }
510
+ return uint8(bytes, it);
511
+ }
512
+ else if (prefix === 0xcd) {
744
513
  // uint 16
745
- if (value < 0x10000) {
746
- bytes[it.offset++] = 0xcd;
747
- uint16$1(bytes, value, it);
748
- return 3;
749
- }
514
+ return uint16(bytes, it);
515
+ }
516
+ else if (prefix === 0xce) {
750
517
  // uint 32
751
- if (value < 0x100000000) {
752
- bytes[it.offset++] = 0xce;
753
- uint32$1(bytes, value, it);
754
- return 5;
755
- }
518
+ return uint32(bytes, it);
519
+ }
520
+ else if (prefix === 0xcf) {
756
521
  // uint 64
757
- bytes[it.offset++] = 0xcf;
758
- uint64$1(bytes, value, it);
759
- return 9;
522
+ return uint64(bytes, it);
760
523
  }
761
- else {
762
- // negative fixnum
763
- if (value >= -0x20) {
764
- bytes[it.offset++] = 0xe0 | (value + 0x20);
765
- return 1;
766
- }
524
+ else if (prefix === 0xd0) {
767
525
  // int 8
768
- if (value >= -0x80) {
769
- bytes[it.offset++] = 0xd0;
770
- int8$1(bytes, value, it);
771
- return 2;
772
- }
526
+ return int8(bytes, it);
527
+ }
528
+ else if (prefix === 0xd1) {
773
529
  // int 16
774
- if (value >= -0x8000) {
775
- bytes[it.offset++] = 0xd1;
776
- int16$1(bytes, value, it);
777
- return 3;
778
- }
530
+ return int16(bytes, it);
531
+ }
532
+ else if (prefix === 0xd2) {
779
533
  // int 32
780
- if (value >= -0x80000000) {
781
- bytes[it.offset++] = 0xd2;
782
- int32$1(bytes, value, it);
783
- return 5;
784
- }
534
+ return int32(bytes, it);
535
+ }
536
+ else if (prefix === 0xd3) {
785
537
  // int 64
786
- bytes[it.offset++] = 0xd3;
787
- int64$1(bytes, value, it);
788
- return 9;
538
+ return int64(bytes, it);
539
+ }
540
+ else if (prefix > 0xdf) {
541
+ // negative fixint
542
+ return (0xff - prefix + 1) * -1;
789
543
  }
790
544
  }
545
+ function stringCheck(bytes, it) {
546
+ const prefix = bytes[it.offset];
547
+ return (
548
+ // fixstr
549
+ (prefix < 0xc0 && prefix > 0xa0) ||
550
+ // str 8
551
+ prefix === 0xd9 ||
552
+ // str 16
553
+ prefix === 0xda ||
554
+ // str 32
555
+ prefix === 0xdb);
556
+ }
557
+ const decode = {
558
+ utf8Read,
559
+ int8,
560
+ uint8,
561
+ int16,
562
+ uint16,
563
+ int32,
564
+ uint32,
565
+ float32,
566
+ float64,
567
+ int64,
568
+ uint64,
569
+ bigint64,
570
+ biguint64,
571
+ boolean,
572
+ string,
573
+ number,
574
+ stringCheck,
575
+ };
791
576
 
792
- var encode = /*#__PURE__*/Object.freeze({
793
- __proto__: null,
794
- utf8Length: utf8Length,
795
- utf8Write: utf8Write,
796
- int8: int8$1,
797
- uint8: uint8$1,
798
- int16: int16$1,
799
- uint16: uint16$1,
800
- int32: int32$1,
801
- uint32: uint32$1,
802
- int64: int64$1,
803
- uint64: uint64$1,
804
- float32: float32$1,
805
- float64: float64$1,
806
- writeFloat32: writeFloat32,
807
- writeFloat64: writeFloat64,
808
- boolean: boolean$1,
809
- string: string$1,
810
- number: number$1
811
- });
812
-
813
- class EncodeSchemaError extends Error {
814
- }
815
- function assertType(value, type, klass, field) {
816
- let typeofTarget;
817
- let allowNull = false;
818
- switch (type) {
819
- case "number":
820
- case "int8":
821
- case "uint8":
822
- case "int16":
823
- case "uint16":
824
- case "int32":
825
- case "uint32":
826
- case "int64":
827
- case "uint64":
828
- case "float32":
829
- case "float64":
830
- typeofTarget = "number";
831
- if (isNaN(value)) {
832
- console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
833
- }
834
- break;
835
- case "string":
836
- typeofTarget = "string";
837
- allowNull = true;
838
- break;
839
- case "boolean":
840
- // boolean is always encoded as true/false based on truthiness
841
- return;
577
+ const registeredTypes = {};
578
+ const identifiers = new Map();
579
+ function registerType(identifier, definition) {
580
+ if (definition.constructor) {
581
+ identifiers.set(definition.constructor, identifier);
582
+ registeredTypes[identifier] = definition;
842
583
  }
843
- if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
844
- let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
845
- throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
584
+ if (definition.encode) {
585
+ encode[identifier] = definition.encode;
846
586
  }
847
- }
848
- function assertInstanceType(value, type, klass, field) {
849
- if (!(value instanceof type)) {
850
- throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${klass.constructor.name}#${field}`);
587
+ if (definition.decode) {
588
+ decode[identifier] = definition.decode;
851
589
  }
852
590
  }
853
-
854
- function encodePrimitiveType(type, bytes, value, klass, field, it) {
855
- assertType(value, type, klass, field);
856
- const encodeFunc = encode[type];
857
- if (encodeFunc) {
858
- encodeFunc(bytes, value, it);
859
- // encodeFunc(bytes, value);
860
- }
861
- else {
862
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
863
- }
591
+ function getType(identifier) {
592
+ return registeredTypes[identifier];
864
593
  }
865
- function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
866
- if (type[Symbol.metadata] !== undefined) {
867
- // TODO: move this to the `@type()` annotation
868
- assertInstanceType(value, type, ref, field);
869
- //
870
- // Encode refId for this instance.
871
- // The actual instance is going to be encoded on next `changeTree` iteration.
872
- //
873
- number$1(bytes, value[$changes].refId, it);
874
- // Try to encode inherited TYPE_ID if it's an ADD operation.
875
- if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
876
- encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
877
- }
878
- }
879
- else if (typeof (type) === "string") {
880
- //
881
- // Primitive values
882
- //
883
- encodePrimitiveType(type, bytes, value, ref, field, it);
884
- }
885
- else {
886
- //
887
- // Custom type (MapSchema, ArraySchema, etc)
888
- //
889
- const definition = getType(Object.keys(type)[0]);
890
- //
891
- // ensure a ArraySchema has been provided
892
- //
893
- assertInstanceType(ref[field], definition.constructor, ref, field);
894
- //
895
- // Encode refId for this instance.
896
- // The actual instance is going to be encoded on next `changeTree` iteration.
897
- //
898
- number$1(bytes, value[$changes].refId, it);
594
+ function defineCustomTypes(types) {
595
+ for (const identifier in types) {
596
+ registerType(identifier, types[identifier]);
899
597
  }
598
+ return (t) => type(t);
900
599
  }
901
- /**
902
- * Used for Schema instances.
903
- * @private
904
- */
905
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
906
- const ref = changeTree.ref;
907
- const metadata = ref['constructor'][Symbol.metadata];
908
- const field = metadata[index];
909
- const type = metadata[field].type;
910
- const value = ref[field];
911
- // "compress" field index + operation
912
- bytes[it.offset++] = (index | operation) & 255;
913
- // Do not encode value for DELETE operations
914
- if (operation === exports.OPERATION.DELETE) {
915
- return;
916
- }
917
- // TODO: inline this function call small performance gain
918
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
919
- };
920
- /**
921
- * Used for collections (MapSchema, CollectionSchema, SetSchema)
922
- * @private
923
- */
924
- const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, operation, it) {
925
- const ref = changeTree.ref;
926
- // encode operation
927
- bytes[it.offset++] = operation & 255;
928
- // custom operations
929
- if (operation === exports.OPERATION.CLEAR) {
930
- return;
931
- }
932
- // encode index
933
- number$1(bytes, field, it);
934
- // Do not encode value for DELETE operations
935
- if (operation === exports.OPERATION.DELETE) {
936
- return;
600
+
601
+ class TypeContext {
602
+ /**
603
+ * For inheritance support
604
+ * Keeps track of which classes extends which. (parent -> children)
605
+ */
606
+ static { this.inheritedTypes = new Map(); }
607
+ static register(target) {
608
+ const parent = Object.getPrototypeOf(target);
609
+ if (parent !== Schema) {
610
+ let inherits = TypeContext.inheritedTypes.get(parent);
611
+ if (!inherits) {
612
+ inherits = new Set();
613
+ TypeContext.inheritedTypes.set(parent, inherits);
614
+ }
615
+ inherits.add(target);
616
+ }
937
617
  }
938
- //
939
- // encode "alias" for dynamic fields (maps)
940
- //
941
- if ((operation & exports.OPERATION.ADD) == exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
942
- if (typeof (ref['set']) === "function") {
618
+ constructor(rootClass) {
619
+ this.types = {};
620
+ this.schemas = new Map();
621
+ this.hasFilters = false;
622
+ this.parentFiltered = {};
623
+ if (rootClass) {
943
624
  //
944
- // MapSchema dynamic key
625
+ // TODO:
626
+ // cache "discoverTypes" results for each rootClass
627
+ // to avoid re-discovering types for each new context/room
945
628
  //
946
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
947
- string$1(bytes, dynamicIndex, it);
629
+ this.discoverTypes(rootClass);
948
630
  }
949
631
  }
950
- const type = changeTree.getType(field);
951
- const value = changeTree.getValue(field);
952
- // TODO: inline this function call small performance gain
953
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
954
- };
955
- /**
956
- * Used for collections (MapSchema, ArraySchema, etc.)
957
- * @private
958
- */
959
- const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
960
- const ref = changeTree.ref;
961
- if (hasView &&
962
- operation === exports.OPERATION.DELETE &&
963
- typeof (changeTree.getType(field)) !== "string") {
964
- // encode delete by refId (array of schemas)
965
- bytes[it.offset++] = exports.OPERATION.DELETE_BY_REFID;
966
- const value = ref['tmpItems'][field];
967
- const refId = value[$changes].refId;
968
- number$1(bytes, refId, it);
969
- return;
632
+ has(schema) {
633
+ return this.schemas.has(schema);
970
634
  }
971
- // encode operation
972
- bytes[it.offset++] = operation & 255;
973
- // custom operations
974
- if (operation === exports.OPERATION.CLEAR) {
975
- return;
635
+ get(typeid) {
636
+ return this.types[typeid];
976
637
  }
977
- // encode index
978
- number$1(bytes, field, it);
979
- // Do not encode value for DELETE operations
980
- if (operation === exports.OPERATION.DELETE) {
981
- return;
638
+ add(schema, typeid = this.schemas.size) {
639
+ // skip if already registered
640
+ if (this.schemas.has(schema)) {
641
+ return false;
642
+ }
643
+ this.types[typeid] = schema;
644
+ //
645
+ // Workaround to allow using an empty Schema (with no `@type()` fields)
646
+ //
647
+ if (schema[Symbol.metadata] === undefined) {
648
+ Metadata.initialize(schema);
649
+ }
650
+ this.schemas.set(schema, typeid);
651
+ return true;
982
652
  }
983
- const type = changeTree.getType(field);
984
- const value = changeTree.getValue(field, isEncodeAll);
985
- // console.log("encodeArray -> ", {
986
- // ref: changeTree.ref.constructor.name,
987
- // field,
988
- // operation: OPERATION[operation],
989
- // value: value?.toJSON(),
990
- // items: ref.toJSON(),
991
- // });
992
- // TODO: inline this function call small performance gain
993
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
994
- };
995
-
996
- /**
997
- * Copyright (c) 2018 Endel Dreyer
998
- * Copyright (c) 2014 Ion Drive Software Ltd.
999
- *
1000
- * Permission is hereby granted, free of charge, to any person obtaining a copy
1001
- * of this software and associated documentation files (the "Software"), to deal
1002
- * in the Software without restriction, including without limitation the rights
1003
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1004
- * copies of the Software, and to permit persons to whom the Software is
1005
- * furnished to do so, subject to the following conditions:
1006
- *
1007
- * The above copyright notice and this permission notice shall be included in all
1008
- * copies or substantial portions of the Software.
1009
- *
1010
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1011
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1012
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1013
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1014
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1015
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1016
- * SOFTWARE
1017
- */
1018
- function utf8Read(bytes, it, length) {
1019
- var string = '', chr = 0;
1020
- for (var i = it.offset, end = it.offset + length; i < end; i++) {
1021
- var byte = bytes[i];
1022
- if ((byte & 0x80) === 0x00) {
1023
- string += String.fromCharCode(byte);
1024
- continue;
653
+ getTypeId(klass) {
654
+ return this.schemas.get(klass);
655
+ }
656
+ discoverTypes(klass, parentType, parentIndex, parentHasViewTag) {
657
+ if (parentHasViewTag) {
658
+ this.registerFilteredByParent(klass, parentType, parentIndex);
1025
659
  }
1026
- if ((byte & 0xe0) === 0xc0) {
1027
- string += String.fromCharCode(((byte & 0x1f) << 6) |
1028
- (bytes[++i] & 0x3f));
1029
- continue;
660
+ // skip if already registered
661
+ if (!this.add(klass)) {
662
+ return;
1030
663
  }
1031
- if ((byte & 0xf0) === 0xe0) {
1032
- string += String.fromCharCode(((byte & 0x0f) << 12) |
1033
- ((bytes[++i] & 0x3f) << 6) |
1034
- ((bytes[++i] & 0x3f) << 0));
1035
- continue;
664
+ // add classes inherited from this base class
665
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
666
+ this.discoverTypes(child, parentType, parentIndex, parentHasViewTag);
667
+ });
668
+ // add parent classes
669
+ let parent = klass;
670
+ while ((parent = Object.getPrototypeOf(parent)) &&
671
+ parent !== Schema && // stop at root (Schema)
672
+ parent !== Function.prototype // stop at root (non-Schema)
673
+ ) {
674
+ this.discoverTypes(parent);
675
+ }
676
+ const metadata = (klass[Symbol.metadata] ??= {});
677
+ // if any schema/field has filters, mark "context" as having filters.
678
+ if (metadata[$viewFieldIndexes]) {
679
+ this.hasFilters = true;
1036
680
  }
1037
- if ((byte & 0xf8) === 0xf0) {
1038
- chr = ((byte & 0x07) << 18) |
1039
- ((bytes[++i] & 0x3f) << 12) |
1040
- ((bytes[++i] & 0x3f) << 6) |
1041
- ((bytes[++i] & 0x3f) << 0);
1042
- if (chr >= 0x010000) { // surrogate pair
1043
- chr -= 0x010000;
1044
- string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
681
+ for (const fieldIndex in metadata) {
682
+ const index = fieldIndex;
683
+ const fieldType = metadata[index].type;
684
+ const fieldHasViewTag = (metadata[index].tag !== undefined);
685
+ if (typeof (fieldType) === "string") {
686
+ continue;
687
+ }
688
+ if (Array.isArray(fieldType)) {
689
+ const type = fieldType[0];
690
+ // skip primitive types
691
+ if (type === "string") {
692
+ continue;
693
+ }
694
+ this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag);
695
+ }
696
+ else if (typeof (fieldType) === "function") {
697
+ this.discoverTypes(fieldType, klass, index, parentHasViewTag || fieldHasViewTag);
1045
698
  }
1046
699
  else {
1047
- string += String.fromCharCode(chr);
700
+ const type = Object.values(fieldType)[0];
701
+ // skip primitive types
702
+ if (typeof (type) === "string") {
703
+ continue;
704
+ }
705
+ this.discoverTypes(type, klass, index, parentHasViewTag || fieldHasViewTag);
1048
706
  }
1049
- continue;
1050
707
  }
1051
- console.error('Invalid byte ' + byte.toString(16));
1052
- // (do not throw error to avoid server/client from crashing due to hack attemps)
1053
- // throw new Error('Invalid byte ' + byte.toString(16));
1054
708
  }
1055
- it.offset += length;
1056
- return string;
1057
- }
1058
- function int8(bytes, it) {
1059
- return uint8(bytes, it) << 24 >> 24;
1060
- }
1061
- function uint8(bytes, it) {
1062
- return bytes[it.offset++];
1063
- }
1064
- function int16(bytes, it) {
1065
- return uint16(bytes, it) << 16 >> 16;
1066
- }
1067
- function uint16(bytes, it) {
1068
- return bytes[it.offset++] | bytes[it.offset++] << 8;
1069
- }
1070
- function int32(bytes, it) {
1071
- return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24;
1072
- }
1073
- function uint32(bytes, it) {
1074
- return int32(bytes, it) >>> 0;
1075
- }
1076
- function float32(bytes, it) {
1077
- return readFloat32(bytes, it);
1078
- }
1079
- function float64(bytes, it) {
1080
- return readFloat64(bytes, it);
1081
- }
1082
- function int64(bytes, it) {
1083
- const low = uint32(bytes, it);
1084
- const high = int32(bytes, it) * Math.pow(2, 32);
1085
- return high + low;
709
+ /**
710
+ * Keep track of which classes have filters applied.
711
+ * Format: `${typeid}-${parentTypeid}-${parentIndex}`
712
+ */
713
+ registerFilteredByParent(schema, parentType, parentIndex) {
714
+ const typeid = this.schemas.get(schema) ?? this.schemas.size;
715
+ let key = `${typeid}`;
716
+ if (parentType) {
717
+ key += `-${this.schemas.get(parentType)}`;
718
+ }
719
+ key += `-${parentIndex}`;
720
+ this.parentFiltered[key] = true;
721
+ }
722
+ debug() {
723
+ let parentFiltered = "";
724
+ for (const key in this.parentFiltered) {
725
+ const keys = key.split("-").map(Number);
726
+ const fieldIndex = keys.pop();
727
+ parentFiltered += `\n\t\t`;
728
+ parentFiltered += `${key}: ${keys.reverse().map((id, i) => {
729
+ const klass = this.types[id];
730
+ const metadata = klass[Symbol.metadata];
731
+ let txt = klass.name;
732
+ if (i === 0) {
733
+ txt += `[${metadata[fieldIndex].name}]`;
734
+ }
735
+ return `${txt}`;
736
+ }).join(" -> ")}`;
737
+ }
738
+ return `TypeContext ->\n` +
739
+ `\tSchema types: ${this.schemas.size}\n` +
740
+ `\thasFilters: ${this.hasFilters}\n` +
741
+ `\tparentFiltered:${parentFiltered}`;
742
+ }
1086
743
  }
1087
- function uint64(bytes, it) {
1088
- const low = uint32(bytes, it);
1089
- const high = uint32(bytes, it) * Math.pow(2, 32);
1090
- return high + low;
744
+
745
+ function getNormalizedType(type) {
746
+ return (Array.isArray(type))
747
+ ? { array: type[0] }
748
+ : (typeof (type['type']) !== "undefined")
749
+ ? type['type']
750
+ : type;
1091
751
  }
1092
- const _int32 = new Int32Array(2);
1093
- const _float32 = new Float32Array(_int32.buffer);
1094
- const _float64 = new Float64Array(_int32.buffer);
1095
- function readFloat32(bytes, it) {
1096
- _int32[0] = int32(bytes, it);
1097
- return _float32[0];
752
+ const Metadata = {
753
+ addField(metadata, index, name, type, descriptor) {
754
+ if (index > 64) {
755
+ throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to 64 fields.`);
756
+ }
757
+ metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
758
+ {
759
+ type: getNormalizedType(type),
760
+ index,
761
+ name,
762
+ });
763
+ // create "descriptors" map
764
+ Object.defineProperty(metadata, $descriptors, {
765
+ value: metadata[$descriptors] || {},
766
+ enumerable: false,
767
+ configurable: true,
768
+ });
769
+ if (descriptor) {
770
+ // for encoder
771
+ metadata[$descriptors][name] = descriptor;
772
+ metadata[$descriptors][`_${name}`] = {
773
+ value: undefined,
774
+ writable: true,
775
+ enumerable: false,
776
+ configurable: true,
777
+ };
778
+ }
779
+ else {
780
+ // for decoder
781
+ metadata[$descriptors][name] = {
782
+ value: undefined,
783
+ writable: true,
784
+ enumerable: true,
785
+ configurable: true,
786
+ };
787
+ }
788
+ // map -1 as last field index
789
+ Object.defineProperty(metadata, $numFields, {
790
+ value: index,
791
+ enumerable: false,
792
+ configurable: true
793
+ });
794
+ // map field name => index (non enumerable)
795
+ Object.defineProperty(metadata, name, {
796
+ value: index,
797
+ enumerable: false,
798
+ configurable: true,
799
+ });
800
+ // if child Ref/complex type, add to -4
801
+ if (typeof (metadata[index].type) !== "string") {
802
+ if (metadata[$refTypeFieldIndexes] === undefined) {
803
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
804
+ value: [],
805
+ enumerable: false,
806
+ configurable: true,
807
+ });
808
+ }
809
+ metadata[$refTypeFieldIndexes].push(index);
810
+ }
811
+ },
812
+ setTag(metadata, fieldName, tag) {
813
+ const index = metadata[fieldName];
814
+ const field = metadata[index];
815
+ // add 'tag' to the field
816
+ field.tag = tag;
817
+ if (!metadata[$viewFieldIndexes]) {
818
+ // -2: all field indexes with "view" tag
819
+ Object.defineProperty(metadata, $viewFieldIndexes, {
820
+ value: [],
821
+ enumerable: false,
822
+ configurable: true
823
+ });
824
+ // -3: field indexes by "view" tag
825
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
826
+ value: {},
827
+ enumerable: false,
828
+ configurable: true
829
+ });
830
+ }
831
+ metadata[$viewFieldIndexes].push(index);
832
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
833
+ metadata[$fieldIndexesByViewTag][tag] = [];
834
+ }
835
+ metadata[$fieldIndexesByViewTag][tag].push(index);
836
+ },
837
+ setFields(target, fields) {
838
+ // for inheritance support
839
+ const constructor = target.prototype.constructor;
840
+ TypeContext.register(constructor);
841
+ const parentClass = Object.getPrototypeOf(constructor);
842
+ const parentMetadata = parentClass && parentClass[Symbol.metadata];
843
+ const metadata = Metadata.initialize(constructor);
844
+ // Use Schema's methods if not defined in the class
845
+ if (!constructor[$track]) {
846
+ constructor[$track] = Schema[$track];
847
+ }
848
+ if (!constructor[$encoder]) {
849
+ constructor[$encoder] = Schema[$encoder];
850
+ }
851
+ if (!constructor[$decoder]) {
852
+ constructor[$decoder] = Schema[$decoder];
853
+ }
854
+ if (!constructor.prototype.toJSON) {
855
+ constructor.prototype.toJSON = Schema.prototype.toJSON;
856
+ }
857
+ //
858
+ // detect index for this field, considering inheritance
859
+ //
860
+ let fieldIndex = metadata[$numFields] // current structure already has fields defined
861
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
862
+ ?? -1; // no fields defined
863
+ fieldIndex++;
864
+ for (const field in fields) {
865
+ const type = fields[field];
866
+ const normalizedType = getNormalizedType(type);
867
+ // FIXME: this code is duplicated from @type() annotation
868
+ const complexTypeKlass = (Array.isArray(type))
869
+ ? getType("array")
870
+ : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
871
+ const childType = (complexTypeKlass)
872
+ ? Object.values(type)[0]
873
+ : normalizedType;
874
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
875
+ fieldIndex++;
876
+ }
877
+ return target;
878
+ },
879
+ isDeprecated(metadata, field) {
880
+ return metadata[field].deprecated === true;
881
+ },
882
+ init(klass) {
883
+ //
884
+ // Used only to initialize an empty Schema (Encoder#constructor)
885
+ // TODO: remove/refactor this...
886
+ //
887
+ const metadata = {};
888
+ klass[Symbol.metadata] = metadata;
889
+ Object.defineProperty(metadata, $numFields, {
890
+ value: 0,
891
+ enumerable: false,
892
+ configurable: true,
893
+ });
894
+ },
895
+ initialize(constructor) {
896
+ const parentClass = Object.getPrototypeOf(constructor);
897
+ const parentMetadata = parentClass[Symbol.metadata];
898
+ let metadata = constructor[Symbol.metadata] ?? Object.create(null);
899
+ // make sure inherited classes have their own metadata object.
900
+ if (parentClass !== Schema && metadata === parentMetadata) {
901
+ metadata = Object.create(null);
902
+ if (parentMetadata) {
903
+ //
904
+ // assign parent metadata to current
905
+ //
906
+ Object.setPrototypeOf(metadata, parentMetadata);
907
+ // $numFields
908
+ Object.defineProperty(metadata, $numFields, {
909
+ value: parentMetadata[$numFields],
910
+ enumerable: false,
911
+ configurable: true,
912
+ writable: true,
913
+ });
914
+ // $viewFieldIndexes / $fieldIndexesByViewTag
915
+ if (parentMetadata[$viewFieldIndexes] !== undefined) {
916
+ Object.defineProperty(metadata, $viewFieldIndexes, {
917
+ value: [...parentMetadata[$viewFieldIndexes]],
918
+ enumerable: false,
919
+ configurable: true,
920
+ writable: true,
921
+ });
922
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
923
+ value: { ...parentMetadata[$fieldIndexesByViewTag] },
924
+ enumerable: false,
925
+ configurable: true,
926
+ writable: true,
927
+ });
928
+ }
929
+ // $refTypeFieldIndexes
930
+ if (parentMetadata[$refTypeFieldIndexes] !== undefined) {
931
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
932
+ value: [...parentMetadata[$refTypeFieldIndexes]],
933
+ enumerable: false,
934
+ configurable: true,
935
+ writable: true,
936
+ });
937
+ }
938
+ // $descriptors
939
+ Object.defineProperty(metadata, $descriptors, {
940
+ value: { ...parentMetadata[$descriptors] },
941
+ enumerable: false,
942
+ configurable: true,
943
+ writable: true,
944
+ });
945
+ }
946
+ }
947
+ constructor[Symbol.metadata] = metadata;
948
+ return metadata;
949
+ },
950
+ isValidInstance(klass) {
951
+ return (klass.constructor[Symbol.metadata] &&
952
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
953
+ },
954
+ getFields(klass) {
955
+ const metadata = klass[Symbol.metadata];
956
+ const fields = {};
957
+ for (let i = 0; i <= metadata[$numFields]; i++) {
958
+ fields[metadata[i].name] = metadata[i].type;
959
+ }
960
+ return fields;
961
+ }
962
+ };
963
+
964
+ function setOperationAtIndex(changeSet, index) {
965
+ const operationsIndex = changeSet.indexes[index];
966
+ if (operationsIndex === undefined) {
967
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
968
+ }
969
+ else {
970
+ changeSet.operations[operationsIndex] = index;
971
+ }
1098
972
  }
1099
- function readFloat64(bytes, it) {
1100
- _int32[0 ] = int32(bytes, it);
1101
- _int32[1 ] = int32(bytes, it);
1102
- return _float64[0];
973
+ function deleteOperationAtIndex(changeSet, index) {
974
+ const operationsIndex = changeSet.indexes[index];
975
+ if (operationsIndex !== undefined) {
976
+ changeSet.operations[operationsIndex] = undefined;
977
+ }
978
+ delete changeSet.indexes[index];
1103
979
  }
1104
- function boolean(bytes, it) {
1105
- return uint8(bytes, it) > 0;
980
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
981
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
982
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
983
+ }
1106
984
  }
1107
- function string(bytes, it) {
1108
- const prefix = bytes[it.offset++];
1109
- let length;
1110
- if (prefix < 0xc0) {
1111
- // fixstr
1112
- length = prefix & 0x1f;
985
+ class ChangeTree {
986
+ constructor(ref) {
987
+ /**
988
+ * Whether this structure is parent of a filtered structure.
989
+ */
990
+ this.isFiltered = false;
991
+ this.indexedOperations = {};
992
+ //
993
+ // TODO:
994
+ // try storing the index + operation per item.
995
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
996
+ //
997
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
998
+ //
999
+ this.changes = { indexes: {}, operations: [] };
1000
+ this.allChanges = { indexes: {}, operations: [] };
1001
+ /**
1002
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
1003
+ */
1004
+ this.isNew = true;
1005
+ this.ref = ref;
1006
+ //
1007
+ // Does this structure have "filters" declared?
1008
+ //
1009
+ const metadata = ref.constructor[Symbol.metadata];
1010
+ if (metadata?.[$viewFieldIndexes]) {
1011
+ this.allFilteredChanges = { indexes: {}, operations: [] };
1012
+ this.filteredChanges = { indexes: {}, operations: [] };
1013
+ }
1113
1014
  }
1114
- else if (prefix === 0xd9) {
1115
- length = uint8(bytes, it);
1015
+ setRoot(root) {
1016
+ this.root = root;
1017
+ this.checkIsFiltered(this.parent, this.parentIndex);
1018
+ // Recursively set root on child structures
1019
+ const metadata = this.ref.constructor[Symbol.metadata];
1020
+ if (metadata) {
1021
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
1022
+ const field = metadata[index];
1023
+ const value = this.ref[field.name];
1024
+ value?.[$changes].setRoot(root);
1025
+ });
1026
+ }
1027
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1028
+ // MapSchema / ArraySchema, etc.
1029
+ this.ref.forEach((value, key) => {
1030
+ value[$changes].setRoot(root);
1031
+ });
1032
+ }
1116
1033
  }
1117
- else if (prefix === 0xda) {
1118
- length = uint16(bytes, it);
1034
+ setParent(parent, root, parentIndex) {
1035
+ this.parent = parent;
1036
+ this.parentIndex = parentIndex;
1037
+ // avoid setting parents with empty `root`
1038
+ if (!root) {
1039
+ return;
1040
+ }
1041
+ // skip if parent is already set
1042
+ if (root !== this.root) {
1043
+ this.root = root;
1044
+ this.checkIsFiltered(parent, parentIndex);
1045
+ }
1046
+ else {
1047
+ root.add(this);
1048
+ }
1049
+ // assign same parent on child structures
1050
+ const metadata = this.ref.constructor[Symbol.metadata];
1051
+ if (metadata) {
1052
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
1053
+ const field = metadata[index];
1054
+ const value = this.ref[field.name];
1055
+ value?.[$changes].setParent(this.ref, root, index);
1056
+ });
1057
+ }
1058
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1059
+ // MapSchema / ArraySchema, etc.
1060
+ this.ref.forEach((value, key) => {
1061
+ value[$changes].setParent(this.ref, root, this.indexes[key] ?? key);
1062
+ });
1063
+ }
1119
1064
  }
1120
- else if (prefix === 0xdb) {
1121
- length = uint32(bytes, it);
1065
+ forEachChild(callback) {
1066
+ //
1067
+ // assign same parent on child structures
1068
+ //
1069
+ const metadata = this.ref.constructor[Symbol.metadata];
1070
+ if (metadata) {
1071
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
1072
+ const field = metadata[index];
1073
+ const value = this.ref[field.name];
1074
+ if (value) {
1075
+ callback(value[$changes], index);
1076
+ }
1077
+ });
1078
+ }
1079
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
1080
+ // MapSchema / ArraySchema, etc.
1081
+ this.ref.forEach((value, key) => {
1082
+ callback(value[$changes], this.indexes[key] ?? key);
1083
+ });
1084
+ }
1122
1085
  }
1123
- return utf8Read(bytes, it, length);
1124
- }
1125
- function stringCheck(bytes, it) {
1126
- const prefix = bytes[it.offset];
1127
- return (
1128
- // fixstr
1129
- (prefix < 0xc0 && prefix > 0xa0) ||
1130
- // str 8
1131
- prefix === 0xd9 ||
1132
- // str 16
1133
- prefix === 0xda ||
1134
- // str 32
1135
- prefix === 0xdb);
1136
- }
1137
- function number(bytes, it) {
1138
- const prefix = bytes[it.offset++];
1139
- if (prefix < 0x80) {
1140
- // positive fixint
1141
- return prefix;
1086
+ operation(op) {
1087
+ // operations without index use negative values to represent them
1088
+ // this is checked during .encode() time.
1089
+ this.changes.operations.push(-op);
1090
+ enqueueChangeTree(this.root, this, 'changes');
1142
1091
  }
1143
- else if (prefix === 0xca) {
1144
- // float 32
1145
- return readFloat32(bytes, it);
1092
+ change(index, operation = exports.OPERATION.ADD) {
1093
+ const metadata = this.ref.constructor[Symbol.metadata];
1094
+ const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
1095
+ const changeSet = (isFiltered)
1096
+ ? this.filteredChanges
1097
+ : this.changes;
1098
+ const previousOperation = this.indexedOperations[index];
1099
+ if (!previousOperation || previousOperation === exports.OPERATION.DELETE) {
1100
+ const op = (!previousOperation)
1101
+ ? operation
1102
+ : (previousOperation === exports.OPERATION.DELETE)
1103
+ ? exports.OPERATION.DELETE_AND_ADD
1104
+ : operation;
1105
+ //
1106
+ // TODO: are DELETE operations being encoded as ADD here ??
1107
+ //
1108
+ this.indexedOperations[index] = op;
1109
+ }
1110
+ setOperationAtIndex(changeSet, index);
1111
+ if (isFiltered) {
1112
+ setOperationAtIndex(this.allFilteredChanges, index);
1113
+ if (this.root) {
1114
+ enqueueChangeTree(this.root, this, 'filteredChanges');
1115
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
1116
+ }
1117
+ }
1118
+ else {
1119
+ setOperationAtIndex(this.allChanges, index);
1120
+ enqueueChangeTree(this.root, this, 'changes');
1121
+ }
1122
+ }
1123
+ shiftChangeIndexes(shiftIndex) {
1124
+ //
1125
+ // Used only during:
1126
+ //
1127
+ // - ArraySchema#unshift()
1128
+ //
1129
+ const changeSet = (this.isFiltered)
1130
+ ? this.filteredChanges
1131
+ : this.changes;
1132
+ const newIndexedOperations = {};
1133
+ const newIndexes = {};
1134
+ for (const index in this.indexedOperations) {
1135
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
1136
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
1137
+ }
1138
+ this.indexedOperations = newIndexedOperations;
1139
+ changeSet.indexes = newIndexes;
1140
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
1141
+ }
1142
+ shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
1143
+ //
1144
+ // Used only during:
1145
+ //
1146
+ // - ArraySchema#splice()
1147
+ //
1148
+ if (this.filteredChanges !== undefined) {
1149
+ this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allFilteredChanges);
1150
+ this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
1151
+ }
1152
+ else {
1153
+ this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
1154
+ }
1155
+ }
1156
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
1157
+ const newIndexes = {};
1158
+ for (const key in changeSet.indexes) {
1159
+ const index = changeSet.indexes[key];
1160
+ if (index > startIndex) {
1161
+ newIndexes[Number(key) + shiftIndex] = index;
1162
+ }
1163
+ else {
1164
+ newIndexes[key] = index;
1165
+ }
1166
+ }
1167
+ changeSet.indexes = newIndexes;
1168
+ for (let i = 0; i < changeSet.operations.length; i++) {
1169
+ const index = changeSet.operations[i];
1170
+ if (index > startIndex) {
1171
+ changeSet.operations[i] = index + shiftIndex;
1172
+ }
1173
+ }
1174
+ }
1175
+ indexedOperation(index, operation, allChangesIndex = index) {
1176
+ this.indexedOperations[index] = operation;
1177
+ if (this.filteredChanges !== undefined) {
1178
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1179
+ setOperationAtIndex(this.filteredChanges, index);
1180
+ enqueueChangeTree(this.root, this, 'filteredChanges');
1181
+ }
1182
+ else {
1183
+ setOperationAtIndex(this.allChanges, allChangesIndex);
1184
+ setOperationAtIndex(this.changes, index);
1185
+ enqueueChangeTree(this.root, this, 'changes');
1186
+ }
1187
+ }
1188
+ getType(index) {
1189
+ if (Metadata.isValidInstance(this.ref)) {
1190
+ const metadata = this.ref.constructor[Symbol.metadata];
1191
+ return metadata[index].type;
1192
+ }
1193
+ else {
1194
+ //
1195
+ // Get the child type from parent structure.
1196
+ // - ["string"] => "string"
1197
+ // - { map: "string" } => "string"
1198
+ // - { set: "string" } => "string"
1199
+ //
1200
+ return this.ref[$childType];
1201
+ }
1202
+ }
1203
+ getChange(index) {
1204
+ return this.indexedOperations[index];
1205
+ }
1206
+ //
1207
+ // used during `.encode()`
1208
+ //
1209
+ getValue(index, isEncodeAll = false) {
1210
+ //
1211
+ // `isEncodeAll` param is only used by ArraySchema
1212
+ //
1213
+ return this.ref[$getByIndex](index, isEncodeAll);
1214
+ }
1215
+ delete(index, operation, allChangesIndex = index) {
1216
+ if (index === undefined) {
1217
+ try {
1218
+ throw new Error(`@colyseus/schema ${this.ref.constructor.name}: trying to delete non-existing index '${index}'`);
1219
+ }
1220
+ catch (e) {
1221
+ console.warn(e);
1222
+ }
1223
+ return;
1224
+ }
1225
+ const changeSet = (this.filteredChanges !== undefined)
1226
+ ? this.filteredChanges
1227
+ : this.changes;
1228
+ this.indexedOperations[index] = operation ?? exports.OPERATION.DELETE;
1229
+ setOperationAtIndex(changeSet, index);
1230
+ const previousValue = this.getValue(index);
1231
+ // remove `root` reference
1232
+ if (previousValue && previousValue[$changes]) {
1233
+ //
1234
+ // FIXME: this.root is "undefined"
1235
+ //
1236
+ // This method is being called at decoding time when a DELETE operation is found.
1237
+ //
1238
+ // - This is due to using the concrete Schema class at decoding time.
1239
+ // - "Reflected" structures do not have this problem.
1240
+ //
1241
+ // (The property descriptors should NOT be used at decoding time. only at encoding time.)
1242
+ //
1243
+ this.root?.remove(previousValue[$changes]);
1244
+ }
1245
+ //
1246
+ // FIXME: this is looking a ugly and repeated
1247
+ //
1248
+ if (this.filteredChanges !== undefined) {
1249
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
1250
+ enqueueChangeTree(this.root, this, 'filteredChanges');
1251
+ }
1252
+ else {
1253
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
1254
+ enqueueChangeTree(this.root, this, 'changes');
1255
+ }
1256
+ }
1257
+ endEncode() {
1258
+ this.indexedOperations = {};
1259
+ // // clear changes
1260
+ // this.changes.indexes = {};
1261
+ // this.changes.operations.length = 0;
1262
+ // ArraySchema and MapSchema have a custom "encode end" method
1263
+ this.ref[$onEncodeEnd]?.();
1264
+ // Not a new instance anymore
1265
+ this.isNew = false;
1266
+ }
1267
+ discard(discardAll = false) {
1268
+ //
1269
+ // > MapSchema:
1270
+ // Remove cached key to ensure ADD operations is unsed instead of
1271
+ // REPLACE in case same key is used on next patches.
1272
+ //
1273
+ this.ref[$onEncodeEnd]?.();
1274
+ this.indexedOperations = {};
1275
+ this.changes.indexes = {};
1276
+ this.changes.operations.length = 0;
1277
+ this.changes.queueRootIndex = undefined;
1278
+ if (this.filteredChanges !== undefined) {
1279
+ this.filteredChanges.indexes = {};
1280
+ this.filteredChanges.operations.length = 0;
1281
+ this.filteredChanges.queueRootIndex = undefined;
1282
+ }
1283
+ if (discardAll) {
1284
+ this.allChanges.indexes = {};
1285
+ this.allChanges.operations.length = 0;
1286
+ if (this.allFilteredChanges !== undefined) {
1287
+ this.allFilteredChanges.indexes = {};
1288
+ this.allFilteredChanges.operations.length = 0;
1289
+ }
1290
+ // remove children references
1291
+ this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
1292
+ }
1293
+ }
1294
+ /**
1295
+ * Recursively discard all changes from this, and child structures.
1296
+ */
1297
+ discardAll() {
1298
+ const keys = Object.keys(this.indexedOperations);
1299
+ for (let i = 0, len = keys.length; i < len; i++) {
1300
+ const value = this.getValue(Number(keys[i]));
1301
+ if (value && value[$changes]) {
1302
+ value[$changes].discardAll();
1303
+ }
1304
+ }
1305
+ this.discard();
1306
+ }
1307
+ ensureRefId() {
1308
+ // skip if refId is already set.
1309
+ if (this.refId !== undefined) {
1310
+ return;
1311
+ }
1312
+ this.refId = this.root.getNextUniqueId();
1313
+ }
1314
+ get changed() {
1315
+ return (Object.entries(this.indexedOperations).length > 0);
1316
+ }
1317
+ checkIsFiltered(parent, parentIndex) {
1318
+ const isNewChangeTree = this.root.add(this);
1319
+ if (this.root.types.hasFilters) {
1320
+ //
1321
+ // At Schema initialization, the "root" structure might not be available
1322
+ // yet, as it only does once the "Encoder" has been set up.
1323
+ //
1324
+ // So the "parent" may be already set without a "root".
1325
+ //
1326
+ this._checkFilteredByParent(parent, parentIndex);
1327
+ if (this.filteredChanges !== undefined) {
1328
+ enqueueChangeTree(this.root, this, 'filteredChanges');
1329
+ if (isNewChangeTree) {
1330
+ this.root.allFilteredChanges.push(this);
1331
+ }
1332
+ }
1333
+ }
1334
+ if (!this.isFiltered) {
1335
+ enqueueChangeTree(this.root, this, 'changes');
1336
+ if (isNewChangeTree) {
1337
+ this.root.allChanges.push(this);
1338
+ }
1339
+ }
1340
+ }
1341
+ _checkFilteredByParent(parent, parentIndex) {
1342
+ // skip if parent is not set
1343
+ if (!parent) {
1344
+ return;
1345
+ }
1346
+ //
1347
+ // ArraySchema | MapSchema - get the child type
1348
+ // (if refType is typeof string, the parentFiltered[key] below will always be invalid)
1349
+ //
1350
+ const refType = Metadata.isValidInstance(this.ref)
1351
+ ? this.ref.constructor
1352
+ : this.ref[$childType];
1353
+ if (!Metadata.isValidInstance(parent)) {
1354
+ const parentChangeTree = parent[$changes];
1355
+ parent = parentChangeTree.parent;
1356
+ parentIndex = parentChangeTree.parentIndex;
1357
+ }
1358
+ const parentConstructor = parent.constructor;
1359
+ let key = `${this.root.types.getTypeId(refType)}`;
1360
+ if (parentConstructor) {
1361
+ key += `-${this.root.types.schemas.get(parentConstructor)}`;
1362
+ }
1363
+ key += `-${parentIndex}`;
1364
+ this.isFiltered = parent[$changes].isFiltered // in case parent is already filtered
1365
+ || this.root.types.parentFiltered[key];
1366
+ // const parentMetadata = parentConstructor?.[Symbol.metadata];
1367
+ // this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex) || this.root.types.parentFiltered[key];
1368
+ //
1369
+ // TODO: refactor this!
1370
+ //
1371
+ // swapping `changes` and `filteredChanges` is required here
1372
+ // because "isFiltered" may not be imedialely available on `change()`
1373
+ // (this happens when instance is detached from root or parent)
1374
+ //
1375
+ if (this.isFiltered) {
1376
+ this.filteredChanges = { indexes: {}, operations: [] };
1377
+ this.allFilteredChanges = { indexes: {}, operations: [] };
1378
+ if (this.changes.operations.length > 0) {
1379
+ // swap changes reference
1380
+ const changes = this.changes;
1381
+ this.changes = this.filteredChanges;
1382
+ this.filteredChanges = changes;
1383
+ // swap "all changes" reference
1384
+ const allFilteredChanges = this.allFilteredChanges;
1385
+ this.allFilteredChanges = this.allChanges;
1386
+ this.allChanges = allFilteredChanges;
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ function encodeValue(encoder, bytes, type, value, operation, it) {
1393
+ if (typeof (type) === "string") {
1394
+ encode[type]?.(bytes, value, it);
1146
1395
  }
1147
- else if (prefix === 0xcb) {
1148
- // float 64
1149
- return readFloat64(bytes, it);
1396
+ else if (type[Symbol.metadata] !== undefined) {
1397
+ //
1398
+ // Encode refId for this instance.
1399
+ // The actual instance is going to be encoded on next `changeTree` iteration.
1400
+ //
1401
+ encode.number(bytes, value[$changes].refId, it);
1402
+ // Try to encode inherited TYPE_ID if it's an ADD operation.
1403
+ if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
1404
+ encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
1405
+ }
1150
1406
  }
1151
- else if (prefix === 0xcc) {
1152
- // uint 8
1153
- return uint8(bytes, it);
1407
+ else {
1408
+ //
1409
+ // Encode refId for this instance.
1410
+ // The actual instance is going to be encoded on next `changeTree` iteration.
1411
+ //
1412
+ encode.number(bytes, value[$changes].refId, it);
1154
1413
  }
1155
- else if (prefix === 0xcd) {
1156
- // uint 16
1157
- return uint16(bytes, it);
1414
+ }
1415
+ /**
1416
+ * Used for Schema instances.
1417
+ * @private
1418
+ */
1419
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
1420
+ // "compress" field index + operation
1421
+ bytes[it.offset++] = (index | operation) & 255;
1422
+ // Do not encode value for DELETE operations
1423
+ if (operation === exports.OPERATION.DELETE) {
1424
+ return;
1158
1425
  }
1159
- else if (prefix === 0xce) {
1160
- // uint 32
1161
- return uint32(bytes, it);
1426
+ const ref = changeTree.ref;
1427
+ const field = metadata[index];
1428
+ // TODO: inline this function call small performance gain
1429
+ encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
1430
+ };
1431
+ /**
1432
+ * Used for collections (MapSchema, CollectionSchema, SetSchema)
1433
+ * @private
1434
+ */
1435
+ const encodeKeyValueOperation = function (encoder, bytes, changeTree, index, operation, it) {
1436
+ // encode operation
1437
+ bytes[it.offset++] = operation & 255;
1438
+ // custom operations
1439
+ if (operation === exports.OPERATION.CLEAR) {
1440
+ return;
1162
1441
  }
1163
- else if (prefix === 0xcf) {
1164
- // uint 64
1165
- return uint64(bytes, it);
1442
+ // encode index
1443
+ encode.number(bytes, index, it);
1444
+ // Do not encode value for DELETE operations
1445
+ if (operation === exports.OPERATION.DELETE) {
1446
+ return;
1166
1447
  }
1167
- else if (prefix === 0xd0) {
1168
- // int 8
1169
- return int8(bytes, it);
1448
+ const ref = changeTree.ref;
1449
+ //
1450
+ // encode "alias" for dynamic fields (maps)
1451
+ //
1452
+ if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
1453
+ if (typeof (ref['set']) === "function") {
1454
+ //
1455
+ // MapSchema dynamic key
1456
+ //
1457
+ const dynamicIndex = changeTree.ref['$indexes'].get(index);
1458
+ encode.string(bytes, dynamicIndex, it);
1459
+ }
1170
1460
  }
1171
- else if (prefix === 0xd1) {
1172
- // int 16
1173
- return int16(bytes, it);
1461
+ const type = ref[$childType];
1462
+ const value = ref[$getByIndex](index);
1463
+ // try { throw new Error(); } catch (e) {
1464
+ // // only print if not coming from Reflection.ts
1465
+ // if (!e.stack.includes("src/Reflection.ts")) {
1466
+ // console.log("encodeKeyValueOperation -> ", {
1467
+ // ref: changeTree.ref.constructor.name,
1468
+ // field,
1469
+ // operation: OPERATION[operation],
1470
+ // value: value?.toJSON(),
1471
+ // items: ref.toJSON(),
1472
+ // });
1473
+ // }
1474
+ // }
1475
+ // TODO: inline this function call small performance gain
1476
+ encodeValue(encoder, bytes, type, value, operation, it);
1477
+ };
1478
+ /**
1479
+ * Used for collections (MapSchema, ArraySchema, etc.)
1480
+ * @private
1481
+ */
1482
+ const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
1483
+ const ref = changeTree.ref;
1484
+ const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
1485
+ let refOrIndex;
1486
+ if (useOperationByRefId) {
1487
+ refOrIndex = ref['tmpItems'][field][$changes].refId;
1488
+ if (operation === exports.OPERATION.DELETE) {
1489
+ operation = exports.OPERATION.DELETE_BY_REFID;
1490
+ }
1491
+ else if (operation === exports.OPERATION.ADD) {
1492
+ operation = exports.OPERATION.ADD_BY_REFID;
1493
+ }
1174
1494
  }
1175
- else if (prefix === 0xd2) {
1176
- // int 32
1177
- return int32(bytes, it);
1495
+ else {
1496
+ refOrIndex = field;
1178
1497
  }
1179
- else if (prefix === 0xd3) {
1180
- // int 64
1181
- return int64(bytes, it);
1498
+ // encode operation
1499
+ bytes[it.offset++] = operation & 255;
1500
+ // custom operations
1501
+ if (operation === exports.OPERATION.CLEAR ||
1502
+ operation === exports.OPERATION.REVERSE) {
1503
+ return;
1182
1504
  }
1183
- else if (prefix > 0xdf) {
1184
- // negative fixint
1185
- return (0xff - prefix + 1) * -1;
1505
+ // encode index
1506
+ encode.number(bytes, refOrIndex, it);
1507
+ // Do not encode value for DELETE operations
1508
+ if (operation === exports.OPERATION.DELETE) {
1509
+ return;
1186
1510
  }
1187
- }
1188
- function numberCheck(bytes, it) {
1189
- const prefix = bytes[it.offset];
1190
- // positive fixint - 0x00 - 0x7f
1191
- // float 32 - 0xca
1192
- // float 64 - 0xcb
1193
- // uint 8 - 0xcc
1194
- // uint 16 - 0xcd
1195
- // uint 32 - 0xce
1196
- // uint 64 - 0xcf
1197
- // int 8 - 0xd0
1198
- // int 16 - 0xd1
1199
- // int 32 - 0xd2
1200
- // int 64 - 0xd3
1201
- return (prefix < 0x80 ||
1202
- (prefix >= 0xca && prefix <= 0xd3));
1203
- }
1204
- function arrayCheck(bytes, it) {
1205
- return bytes[it.offset] < 0xa0;
1206
- // const prefix = bytes[it.offset] ;
1207
- // if (prefix < 0xa0) {
1208
- // return prefix;
1209
- // // array
1210
- // } else if (prefix === 0xdc) {
1211
- // it.offset += 2;
1212
- // } else if (0xdd) {
1213
- // it.offset += 4;
1214
- // }
1215
- // return prefix;
1216
- }
1217
- function switchStructureCheck(bytes, it) {
1218
- return (
1219
- // previous byte should be `SWITCH_TO_STRUCTURE`
1220
- bytes[it.offset - 1] === SWITCH_TO_STRUCTURE &&
1221
- // next byte should be a number
1222
- (bytes[it.offset] < 0x80 || (bytes[it.offset] >= 0xca && bytes[it.offset] <= 0xd3)));
1223
- }
1224
-
1225
- var decode = /*#__PURE__*/Object.freeze({
1226
- __proto__: null,
1227
- utf8Read: utf8Read,
1228
- int8: int8,
1229
- uint8: uint8,
1230
- int16: int16,
1231
- uint16: uint16,
1232
- int32: int32,
1233
- uint32: uint32,
1234
- float32: float32,
1235
- float64: float64,
1236
- int64: int64,
1237
- uint64: uint64,
1238
- readFloat32: readFloat32,
1239
- readFloat64: readFloat64,
1240
- boolean: boolean,
1241
- string: string,
1242
- stringCheck: stringCheck,
1243
- number: number,
1244
- numberCheck: numberCheck,
1245
- arrayCheck: arrayCheck,
1246
- switchStructureCheck: switchStructureCheck
1247
- });
1511
+ const type = changeTree.getType(field);
1512
+ const value = changeTree.getValue(field, isEncodeAll);
1513
+ // console.log("encodeArray -> ", {
1514
+ // ref: changeTree.ref.constructor.name,
1515
+ // field,
1516
+ // operation: OPERATION[operation],
1517
+ // value: value?.toJSON(),
1518
+ // items: ref.toJSON(),
1519
+ // });
1520
+ // TODO: inline this function call small performance gain
1521
+ encodeValue(encoder, bytes, type, value, operation, it);
1522
+ };
1248
1523
 
1249
1524
  const DEFINITION_MISMATCH = -1;
1250
1525
  function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges) {
1251
- const $root = decoder.$root;
1526
+ const $root = decoder.root;
1252
1527
  const previousValue = ref[$getByIndex](index);
1253
1528
  let value;
1254
1529
  if ((operation & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
@@ -1280,7 +1555,7 @@
1280
1555
  }
1281
1556
  if (operation === exports.OPERATION.DELETE) ;
1282
1557
  else if (Schema.is(type)) {
1283
- const refId = number(bytes, it);
1558
+ const refId = decode.number(bytes, it);
1284
1559
  value = $root.refs.get(refId);
1285
1560
  if (previousValue) {
1286
1561
  const previousRefId = $root.refIds.get(previousValue);
@@ -1296,7 +1571,9 @@
1296
1571
  if (!value) {
1297
1572
  value = decoder.createInstanceOfType(childType);
1298
1573
  }
1299
- $root.addRef(refId, value, (value !== previousValue));
1574
+ $root.addRef(refId, value, (value !== previousValue || // increment ref count if value has changed
1575
+ (operation === exports.OPERATION.DELETE_AND_ADD && value === previousValue) // increment ref count if the same instance is being added again
1576
+ ));
1300
1577
  }
1301
1578
  }
1302
1579
  else if (typeof (type) === "string") {
@@ -1307,7 +1584,7 @@
1307
1584
  }
1308
1585
  else {
1309
1586
  const typeDef = getType(Object.keys(type)[0]);
1310
- const refId = number(bytes, it);
1587
+ const refId = decode.number(bytes, it);
1311
1588
  const valueRef = ($root.refs.has(refId))
1312
1589
  ? previousValue || $root.refs.get(refId)
1313
1590
  : new typeDef.constructor();
@@ -1347,18 +1624,19 @@
1347
1624
  }
1348
1625
  const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1349
1626
  const first_byte = bytes[it.offset++];
1350
- const metadata = ref['constructor'][Symbol.metadata];
1627
+ const metadata = ref.constructor[Symbol.metadata];
1351
1628
  // "compressed" index + operation
1352
1629
  const operation = (first_byte >> 6) << 6;
1353
1630
  const index = first_byte % (operation || 255);
1354
1631
  // skip early if field is not defined
1355
1632
  const field = metadata[index];
1356
1633
  if (field === undefined) {
1634
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1357
1635
  return DEFINITION_MISMATCH;
1358
1636
  }
1359
- const { value, previousValue } = decodeValue(decoder, operation, ref, index, metadata[field].type, bytes, it, allChanges);
1637
+ const { value, previousValue } = decodeValue(decoder, operation, ref, index, field.type, bytes, it, allChanges);
1360
1638
  if (value !== null && value !== undefined) {
1361
- ref[field] = value;
1639
+ ref[field.name] = value;
1362
1640
  }
1363
1641
  // add change
1364
1642
  if (previousValue !== value) {
@@ -1366,7 +1644,7 @@
1366
1644
  ref,
1367
1645
  refId: decoder.currentRefId,
1368
1646
  op: operation,
1369
- field: field,
1647
+ field: field.name,
1370
1648
  value,
1371
1649
  previousValue,
1372
1650
  });
@@ -1385,12 +1663,12 @@
1385
1663
  ref.clear();
1386
1664
  return;
1387
1665
  }
1388
- const index = number(bytes, it);
1666
+ const index = decode.number(bytes, it);
1389
1667
  const type = ref[$childType];
1390
1668
  let dynamicIndex;
1391
1669
  if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
1392
1670
  if (typeof (ref['set']) === "function") {
1393
- dynamicIndex = string(bytes, it); // MapSchema
1671
+ dynamicIndex = decode.string(bytes, it); // MapSchema
1394
1672
  ref['setIndex'](index, dynamicIndex);
1395
1673
  }
1396
1674
  else {
@@ -1434,7 +1712,8 @@
1434
1712
  };
1435
1713
  const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1436
1714
  // "uncompressed" index + operation (array/map items)
1437
- const operation = bytes[it.offset++];
1715
+ let operation = bytes[it.offset++];
1716
+ let index;
1438
1717
  if (operation === exports.OPERATION.CLEAR) {
1439
1718
  //
1440
1719
  // When decoding:
@@ -1445,11 +1724,15 @@
1445
1724
  ref.clear();
1446
1725
  return;
1447
1726
  }
1727
+ else if (operation === exports.OPERATION.REVERSE) {
1728
+ ref.reverse();
1729
+ return;
1730
+ }
1448
1731
  else if (operation === exports.OPERATION.DELETE_BY_REFID) {
1449
1732
  // TODO: refactor here, try to follow same flow as below
1450
- const refId = number(bytes, it);
1451
- const previousValue = decoder.$root.refs.get(refId);
1452
- const index = ref.findIndex((value) => value === previousValue);
1733
+ const refId = decode.number(bytes, it);
1734
+ const previousValue = decoder.root.refs.get(refId);
1735
+ index = ref.findIndex((value) => value === previousValue);
1453
1736
  ref[$deleteByIndex](index);
1454
1737
  allChanges.push({
1455
1738
  ref,
@@ -1462,7 +1745,17 @@
1462
1745
  });
1463
1746
  return;
1464
1747
  }
1465
- const index = number(bytes, it);
1748
+ else if (operation === exports.OPERATION.ADD_BY_REFID) {
1749
+ const refId = decode.number(bytes, it);
1750
+ const itemByRefId = decoder.root.refs.get(refId);
1751
+ // use existing index, or push new value
1752
+ index = (itemByRefId)
1753
+ ? ref.findIndex((value) => value === itemByRefId)
1754
+ : ref.length;
1755
+ }
1756
+ else {
1757
+ index = decode.number(bytes, it);
1758
+ }
1466
1759
  const type = ref[$childType];
1467
1760
  let dynamicIndex = index;
1468
1761
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges);
@@ -1486,6 +1779,55 @@
1486
1779
  }
1487
1780
  };
1488
1781
 
1782
+ class EncodeSchemaError extends Error {
1783
+ }
1784
+ function assertType(value, type, klass, field) {
1785
+ let typeofTarget;
1786
+ let allowNull = false;
1787
+ switch (type) {
1788
+ case "number":
1789
+ case "int8":
1790
+ case "uint8":
1791
+ case "int16":
1792
+ case "uint16":
1793
+ case "int32":
1794
+ case "uint32":
1795
+ case "int64":
1796
+ case "uint64":
1797
+ case "float32":
1798
+ case "float64":
1799
+ typeofTarget = "number";
1800
+ if (isNaN(value)) {
1801
+ console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
1802
+ }
1803
+ break;
1804
+ case "bigint64":
1805
+ case "biguint64":
1806
+ typeofTarget = "bigint";
1807
+ break;
1808
+ case "string":
1809
+ typeofTarget = "string";
1810
+ allowNull = true;
1811
+ break;
1812
+ case "boolean":
1813
+ // boolean is always encoded as true/false based on truthiness
1814
+ return;
1815
+ default:
1816
+ // skip assertion for custom types
1817
+ // TODO: allow custom types to define their own assertions
1818
+ return;
1819
+ }
1820
+ if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
1821
+ let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
1822
+ throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
1823
+ }
1824
+ }
1825
+ function assertInstanceType(value, type, instance, field) {
1826
+ if (!(value instanceof type)) {
1827
+ throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${instance.constructor.name}#${field}`);
1828
+ }
1829
+ }
1830
+
1489
1831
  var _a$4, _b$4;
1490
1832
  const DEFAULT_SORT = (a, b) => {
1491
1833
  const A = a.toString();
@@ -1536,6 +1878,7 @@
1536
1878
  const proxy = new Proxy(this, {
1537
1879
  get: (obj, prop) => {
1538
1880
  if (typeof (prop) !== "symbol" &&
1881
+ // FIXME: d8 accuses this as low performance
1539
1882
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1540
1883
  ) {
1541
1884
  return this.items[prop];
@@ -1551,8 +1894,9 @@
1551
1894
  }
1552
1895
  else {
1553
1896
  if (setValue[$changes]) {
1897
+ assertInstanceType(setValue, obj[$childType], obj, key);
1554
1898
  if (obj.items[key] !== undefined) {
1555
- if (setValue[$changes][$isNew]) {
1899
+ if (setValue[$changes].isNew) {
1556
1900
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1557
1901
  }
1558
1902
  else {
@@ -1564,7 +1908,7 @@
1564
1908
  }
1565
1909
  }
1566
1910
  }
1567
- else if (setValue[$changes][$isNew]) {
1911
+ else if (setValue[$changes].isNew) {
1568
1912
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1569
1913
  }
1570
1914
  }
@@ -1597,7 +1941,10 @@
1597
1941
  }
1598
1942
  });
1599
1943
  this[$changes] = new ChangeTree(proxy);
1600
- this.push.apply(this, items);
1944
+ this[$changes].indexes = {};
1945
+ if (items.length > 0) {
1946
+ this.push(...items);
1947
+ }
1601
1948
  return proxy;
1602
1949
  }
1603
1950
  set length(newLength) {
@@ -1616,14 +1963,19 @@
1616
1963
  }
1617
1964
  push(...values) {
1618
1965
  let length = this.tmpItems.length;
1619
- values.forEach((value, i) => {
1620
- // skip null values
1966
+ const changeTree = this[$changes];
1967
+ // values.forEach((value, i) => {
1968
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1969
+ const value = values[i];
1621
1970
  if (value === undefined || value === null) {
1971
+ // skip null values
1622
1972
  return;
1623
1973
  }
1624
- const changeTree = this[$changes];
1974
+ else if (typeof (value) === "object" && this[$childType]) {
1975
+ assertInstanceType(value, this[$childType], this, i);
1976
+ // TODO: move value[$changes]?.setParent() to this block.
1977
+ }
1625
1978
  changeTree.indexedOperation(length, exports.OPERATION.ADD, this.items.length);
1626
- // changeTree.indexes[length] = length;
1627
1979
  this.items.push(value);
1628
1980
  this.tmpItems.push(value);
1629
1981
  //
@@ -1631,8 +1983,9 @@
1631
1983
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1632
1984
  //
1633
1985
  value[$changes]?.setParent(this, changeTree.root, length);
1634
- length++;
1635
- });
1986
+ }
1987
+ // length++;
1988
+ // });
1636
1989
  return length;
1637
1990
  }
1638
1991
  /**
@@ -1653,6 +2006,7 @@
1653
2006
  }
1654
2007
  this[$changes].delete(index, undefined, this.items.length - 1);
1655
2008
  // this.tmpItems[index] = undefined;
2009
+ // this.tmpItems.pop();
1656
2010
  this.deletedIndexes[index] = true;
1657
2011
  return this.items.pop();
1658
2012
  }
@@ -1717,9 +2071,12 @@
1717
2071
  //
1718
2072
  // TODO: do not use [$changes] at decoding time.
1719
2073
  //
1720
- changeTree.root?.changes.delete(changeTree);
1721
- changeTree.root?.allChanges.delete(changeTree);
1722
- changeTree.root?.allFilteredChanges.delete(changeTree);
2074
+ const root = changeTree.root;
2075
+ if (root !== undefined) {
2076
+ root.removeChangeFromChangeSet("changes", changeTree);
2077
+ root.removeChangeFromChangeSet("allChanges", changeTree);
2078
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
2079
+ }
1723
2080
  });
1724
2081
  changeTree.discard(true);
1725
2082
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -1763,6 +2120,7 @@
1763
2120
  const changeTree = this[$changes];
1764
2121
  changeTree.delete(index);
1765
2122
  changeTree.shiftAllChangeIndexes(-1, index);
2123
+ // this.deletedIndexes[index] = true;
1766
2124
  return this.items.shift();
1767
2125
  }
1768
2126
  /**
@@ -1843,10 +2201,12 @@
1843
2201
  changeTree.shiftChangeIndexes(items.length);
1844
2202
  // new index
1845
2203
  if (changeTree.isFiltered) {
1846
- changeTree.filteredChanges.set(this.items.length, exports.OPERATION.ADD);
2204
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2205
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1847
2206
  }
1848
2207
  else {
1849
- changeTree.allChanges.set(this.items.length, exports.OPERATION.ADD);
2208
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2209
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1850
2210
  }
1851
2211
  // FIXME: should we use OPERATION.MOVE here instead?
1852
2212
  items.forEach((_, index) => {
@@ -1871,14 +2231,6 @@
1871
2231
  lastIndexOf(searchElement, fromIndex = this.length - 1) {
1872
2232
  return this.items.lastIndexOf(searchElement, fromIndex);
1873
2233
  }
1874
- /**
1875
- * Determines whether all the members of an array satisfy the specified test.
1876
- * @param callbackfn A function that accepts up to three arguments. The every method calls
1877
- * the callbackfn function for each element in the array until the callbackfn returns a value
1878
- * which is coercible to the Boolean value false, or until the end of the array.
1879
- * @param thisArg An object to which the this keyword can refer in the callbackfn function.
1880
- * If thisArg is omitted, undefined is used as the this value.
1881
- */
1882
2234
  every(callbackfn, thisArg) {
1883
2235
  return this.items.every(callbackfn, thisArg);
1884
2236
  }
@@ -2157,6 +2509,7 @@
2157
2509
  this.$items = new Map();
2158
2510
  this.$indexes = new Map();
2159
2511
  this[$changes] = new ChangeTree(this);
2512
+ this[$changes].indexes = {};
2160
2513
  if (initialValues) {
2161
2514
  if (initialValues instanceof Map ||
2162
2515
  initialValues instanceof MapSchema) {
@@ -2183,6 +2536,9 @@
2183
2536
  if (value === undefined || value === null) {
2184
2537
  throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
2185
2538
  }
2539
+ else if (typeof (value) === "object" && this[$childType]) {
2540
+ assertInstanceType(value, this[$childType], this, key);
2541
+ }
2186
2542
  // Force "key" as string
2187
2543
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2188
2544
  key = key.toString();
@@ -2191,7 +2547,7 @@
2191
2547
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2192
2548
  const index = (isReplace)
2193
2549
  ? changeTree.indexes[key]
2194
- : changeTree.indexes[-1] ?? 0;
2550
+ : changeTree.indexes[$numFields] ?? 0;
2195
2551
  let operation = (isReplace)
2196
2552
  ? exports.OPERATION.REPLACE
2197
2553
  : exports.OPERATION.ADD;
@@ -2203,7 +2559,7 @@
2203
2559
  if (!isReplace) {
2204
2560
  this.$indexes.set(index, key);
2205
2561
  changeTree.indexes[key] = index;
2206
- changeTree.indexes[-1] = index + 1;
2562
+ changeTree.indexes[$numFields] = index + 1;
2207
2563
  }
2208
2564
  else if (!isRef &&
2209
2565
  this.$items.get(key) === value) {
@@ -2278,8 +2634,11 @@
2278
2634
  }
2279
2635
  [$onEncodeEnd]() {
2280
2636
  const changeTree = this[$changes];
2281
- const changes = changeTree.changes.entries();
2282
- for (const [fieldIndex, operation] of changes) {
2637
+ const keys = Object.keys(changeTree.indexedOperations);
2638
+ for (let i = 0, len = keys.length; i < len; i++) {
2639
+ const key = keys[i];
2640
+ const fieldIndex = Number(key);
2641
+ const operation = changeTree.indexedOperations[key];
2283
2642
  if (operation === exports.OPERATION.DELETE) {
2284
2643
  const index = this[$getByIndex](fieldIndex);
2285
2644
  delete changeTree.indexes[index];
@@ -2307,109 +2666,22 @@
2307
2666
  }
2308
2667
  else {
2309
2668
  // server-side
2310
- cloned = new MapSchema();
2311
- this.forEach((value, key) => {
2312
- if (value[$changes]) {
2313
- cloned.set(key, value['clone']());
2314
- }
2315
- else {
2316
- cloned.set(key, value);
2317
- }
2318
- });
2319
- }
2320
- return cloned;
2321
- }
2322
- }
2323
- registerType("map", { constructor: MapSchema });
2324
-
2325
- const DEFAULT_VIEW_TAG = -1;
2326
- class TypeContext {
2327
- /**
2328
- * For inheritance support
2329
- * Keeps track of which classes extends which. (parent -> children)
2330
- */
2331
- static { this.inheritedTypes = new Map(); }
2332
- static register(target) {
2333
- const parent = Object.getPrototypeOf(target);
2334
- if (parent !== Schema) {
2335
- let inherits = TypeContext.inheritedTypes.get(parent);
2336
- if (!inherits) {
2337
- inherits = new Set();
2338
- TypeContext.inheritedTypes.set(parent, inherits);
2339
- }
2340
- inherits.add(target);
2341
- }
2342
- }
2343
- constructor(rootClass) {
2344
- this.types = {};
2345
- this.schemas = new Map();
2346
- this.hasFilters = false;
2347
- if (rootClass) {
2348
- this.discoverTypes(rootClass);
2349
- }
2350
- }
2351
- has(schema) {
2352
- return this.schemas.has(schema);
2353
- }
2354
- get(typeid) {
2355
- return this.types[typeid];
2356
- }
2357
- add(schema, typeid = this.schemas.size) {
2358
- // skip if already registered
2359
- if (this.schemas.has(schema)) {
2360
- return false;
2361
- }
2362
- this.types[typeid] = schema;
2363
- this.schemas.set(schema, typeid);
2364
- return true;
2365
- }
2366
- getTypeId(klass) {
2367
- return this.schemas.get(klass);
2368
- }
2369
- discoverTypes(klass) {
2370
- if (!this.add(klass)) {
2371
- return;
2372
- }
2373
- // add classes inherited from this base class
2374
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2375
- this.discoverTypes(child);
2376
- });
2377
- // skip if no fields are defined for this class.
2378
- if (klass[Symbol.metadata] === undefined) {
2379
- klass[Symbol.metadata] = {};
2380
- }
2381
- // const metadata = Metadata.getFor(klass);
2382
- const metadata = klass[Symbol.metadata];
2383
- // if any schema/field has filters, mark "context" as having filters.
2384
- if (metadata[-2]) {
2385
- this.hasFilters = true;
2386
- }
2387
- for (const field in metadata) {
2388
- const fieldType = metadata[field].type;
2389
- if (typeof (fieldType) === "string") {
2390
- continue;
2391
- }
2392
- if (Array.isArray(fieldType)) {
2393
- const type = fieldType[0];
2394
- if (type === "string") {
2395
- continue;
2669
+ cloned = new MapSchema();
2670
+ this.forEach((value, key) => {
2671
+ if (value[$changes]) {
2672
+ cloned.set(key, value['clone']());
2396
2673
  }
2397
- this.discoverTypes(type);
2398
- }
2399
- else if (typeof (fieldType) === "function") {
2400
- this.discoverTypes(fieldType);
2401
- }
2402
- else {
2403
- const type = Object.values(fieldType)[0];
2404
- // skip primitive types
2405
- if (typeof (type) === "string") {
2406
- continue;
2674
+ else {
2675
+ cloned.set(key, value);
2407
2676
  }
2408
- this.discoverTypes(type);
2409
- }
2677
+ });
2410
2678
  }
2679
+ return cloned;
2411
2680
  }
2412
2681
  }
2682
+ registerType("map", { constructor: MapSchema });
2683
+
2684
+ const DEFAULT_VIEW_TAG = -1;
2413
2685
  /**
2414
2686
  * [See documentation](https://docs.colyseus.io/state/schema/)
2415
2687
  *
@@ -2436,8 +2708,8 @@
2436
2708
  // // detect index for this field, considering inheritance
2437
2709
  // //
2438
2710
  // const parent = Object.getPrototypeOf(context.metadata);
2439
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2440
- // ?? (parent && parent[-1]) // parent structure has fields defined
2711
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2712
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2441
2713
  // ?? -1; // no fields defined
2442
2714
  // fieldIndex++;
2443
2715
  // if (
@@ -2557,18 +2829,20 @@
2557
2829
  const constructor = target.constructor;
2558
2830
  const parentClass = Object.getPrototypeOf(constructor);
2559
2831
  const parentMetadata = parentClass[Symbol.metadata];
2832
+ // TODO: use Metadata.initialize()
2560
2833
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2561
- if (!metadata[fieldName]) {
2562
- //
2563
- // detect index for this field, considering inheritance
2564
- //
2565
- metadata[fieldName] = {
2566
- type: undefined,
2567
- index: (metadata[-1] // current structure already has fields defined
2568
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2569
- ?? -1) + 1 // no fields defined
2570
- };
2571
- }
2834
+ // const fieldIndex = metadata[fieldName];
2835
+ // if (!metadata[fieldIndex]) {
2836
+ // //
2837
+ // // detect index for this field, considering inheritance
2838
+ // //
2839
+ // metadata[fieldIndex] = {
2840
+ // type: undefined,
2841
+ // index: (metadata[$numFields] // current structure already has fields defined
2842
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2843
+ // ?? -1) + 1 // no fields defined
2844
+ // }
2845
+ // }
2572
2846
  Metadata.setTag(metadata, fieldName, tag);
2573
2847
  };
2574
2848
  }
@@ -2582,17 +2856,17 @@
2582
2856
  TypeContext.register(constructor);
2583
2857
  const parentClass = Object.getPrototypeOf(constructor);
2584
2858
  const parentMetadata = parentClass[Symbol.metadata];
2585
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2586
- let fieldIndex;
2859
+ const metadata = Metadata.initialize(constructor);
2860
+ let fieldIndex = metadata[field];
2587
2861
  /**
2588
2862
  * skip if descriptor already exists for this field (`@deprecated()`)
2589
2863
  */
2590
- if (metadata[field]) {
2591
- if (metadata[field].deprecated) {
2864
+ if (metadata[fieldIndex] !== undefined) {
2865
+ if (metadata[fieldIndex].deprecated) {
2592
2866
  // do not create accessors for deprecated properties.
2593
2867
  return;
2594
2868
  }
2595
- else if (metadata[field].descriptor !== undefined) {
2869
+ else if (metadata[fieldIndex].type !== undefined) {
2596
2870
  // trying to define same property multiple times across inheritance.
2597
2871
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
2598
2872
  try {
@@ -2603,16 +2877,13 @@
2603
2877
  throw new Error(`${e.message} ${definitionAtLine}`);
2604
2878
  }
2605
2879
  }
2606
- else {
2607
- fieldIndex = metadata[field].index;
2608
- }
2609
2880
  }
2610
2881
  else {
2611
2882
  //
2612
2883
  // detect index for this field, considering inheritance
2613
2884
  //
2614
- fieldIndex = metadata[-1] // current structure already has fields defined
2615
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2885
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2886
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2616
2887
  ?? -1; // no fields defined
2617
2888
  fieldIndex++;
2618
2889
  }
@@ -2631,15 +2902,15 @@
2631
2902
  const childType = (complexTypeKlass)
2632
2903
  ? Object.values(type)[0]
2633
2904
  : type;
2634
- Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass, metadata, field));
2905
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
2635
2906
  }
2636
2907
  };
2637
2908
  }
2638
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass, metadata, field) {
2909
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
2639
2910
  return {
2640
2911
  get: function () { return this[fieldCached]; },
2641
2912
  set: function (value) {
2642
- const previousValue = this[fieldCached] || undefined;
2913
+ const previousValue = this[fieldCached] ?? undefined;
2643
2914
  // skip if value is the same as cached.
2644
2915
  if (value === previousValue) {
2645
2916
  return;
@@ -2657,22 +2928,27 @@
2657
2928
  }
2658
2929
  value[$childType] = type;
2659
2930
  }
2931
+ else if (typeof (type) !== "string") {
2932
+ assertInstanceType(value, type, this, fieldCached.substring(1));
2933
+ }
2934
+ else {
2935
+ assertType(value, type, this, fieldCached.substring(1));
2936
+ }
2937
+ const changeTree = this[$changes];
2660
2938
  //
2661
2939
  // Replacing existing "ref", remove it from root.
2662
2940
  // TODO: if there are other references to this instance, we should not remove it from root.
2663
2941
  //
2664
2942
  if (previousValue !== undefined && previousValue[$changes]) {
2665
- this[$changes].root?.remove(previousValue[$changes]);
2943
+ changeTree.root?.remove(previousValue[$changes]);
2666
2944
  }
2667
2945
  // flag the change for encoding.
2668
- this.constructor[$track](this[$changes], fieldIndex, exports.OPERATION.ADD);
2946
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2669
2947
  //
2670
2948
  // call setParent() recursively for this and its child
2671
2949
  // structures.
2672
2950
  //
2673
- if (value[$changes]) {
2674
- value[$changes].setParent(this, this[$changes].root, metadata[field].index);
2675
- }
2951
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
2676
2952
  }
2677
2953
  else if (previousValue !== undefined) {
2678
2954
  //
@@ -2699,20 +2975,22 @@
2699
2975
  const parentClass = Object.getPrototypeOf(constructor);
2700
2976
  const parentMetadata = parentClass[Symbol.metadata];
2701
2977
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2702
- if (!metadata[field]) {
2703
- //
2704
- // detect index for this field, considering inheritance
2705
- //
2706
- metadata[field] = {
2707
- type: undefined,
2708
- index: (metadata[-1] // current structure already has fields defined
2709
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2710
- ?? -1) + 1 // no fields defined
2711
- };
2712
- }
2713
- metadata[field].deprecated = true;
2978
+ const fieldIndex = metadata[field];
2979
+ // if (!metadata[field]) {
2980
+ // //
2981
+ // // detect index for this field, considering inheritance
2982
+ // //
2983
+ // metadata[field] = {
2984
+ // type: undefined,
2985
+ // index: (metadata[$numFields] // current structure already has fields defined
2986
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2987
+ // ?? -1) + 1 // no fields defined
2988
+ // }
2989
+ // }
2990
+ metadata[fieldIndex].deprecated = true;
2714
2991
  if (throws) {
2715
- metadata[field].descriptor = {
2992
+ metadata[$descriptors] ??= {};
2993
+ metadata[$descriptors][field] = {
2716
2994
  get: function () { throw new Error(`${field} is deprecated.`); },
2717
2995
  set: function (value) { },
2718
2996
  enumerable: false,
@@ -2720,8 +2998,8 @@
2720
2998
  };
2721
2999
  }
2722
3000
  // flag metadata[field] as non-enumerable
2723
- Object.defineProperty(metadata, field, {
2724
- value: metadata[field],
3001
+ Object.defineProperty(metadata, fieldIndex, {
3002
+ value: metadata[fieldIndex],
2725
3003
  enumerable: false,
2726
3004
  configurable: true
2727
3005
  });
@@ -2733,6 +3011,37 @@
2733
3011
  }
2734
3012
  return target;
2735
3013
  }
3014
+ function schema(fields, name, inherits = Schema) {
3015
+ const defaultValues = {};
3016
+ const viewTagFields = {};
3017
+ for (let fieldName in fields) {
3018
+ const field = fields[fieldName];
3019
+ if (typeof (field) === "object") {
3020
+ if (field['default'] !== undefined) {
3021
+ defaultValues[fieldName] = field['default'];
3022
+ }
3023
+ if (field['view'] !== undefined) {
3024
+ viewTagFields[fieldName] = (typeof (field['view']) === "boolean")
3025
+ ? DEFAULT_VIEW_TAG
3026
+ : field['view'];
3027
+ }
3028
+ }
3029
+ }
3030
+ const klass = Metadata.setFields(class extends inherits {
3031
+ constructor(...args) {
3032
+ args[0] = Object.assign({}, defaultValues, args[0]);
3033
+ super(...args);
3034
+ }
3035
+ }, fields);
3036
+ for (let fieldName in viewTagFields) {
3037
+ view(viewTagFields[fieldName])(klass.prototype, fieldName);
3038
+ }
3039
+ if (name) {
3040
+ Object.defineProperty(klass, "name", { value: name });
3041
+ }
3042
+ klass.extends = (fields, name) => schema(fields, name, klass);
3043
+ return klass;
3044
+ }
2736
3045
 
2737
3046
  function getIndent(level) {
2738
3047
  return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
@@ -2743,15 +3052,18 @@
2743
3052
  ops: {},
2744
3053
  refs: []
2745
3054
  };
2746
- $root.changes.forEach((operations, changeTree) => {
3055
+ // for (const refId in $root.changes) {
3056
+ $root.changes.forEach(changeTree => {
3057
+ const changes = changeTree.indexedOperations;
2747
3058
  dump.refs.push(`refId#${changeTree.refId}`);
2748
- operations.forEach((op, index) => {
3059
+ for (const index in changes) {
3060
+ const op = changes[index];
2749
3061
  const opName = exports.OPERATION[op];
2750
3062
  if (!dump.ops[opName]) {
2751
3063
  dump.ops[opName] = 0;
2752
3064
  }
2753
3065
  dump.ops[exports.OPERATION[op]]++;
2754
- });
3066
+ }
2755
3067
  });
2756
3068
  return dump;
2757
3069
  }
@@ -2777,6 +3089,7 @@
2777
3089
  class Schema {
2778
3090
  static { this[_a$2] = encodeSchemaOperation; }
2779
3091
  static { this[_b$2] = decodeSchemaOperation; }
3092
+ // public [$changes]: ChangeTree;
2780
3093
  /**
2781
3094
  * Assign the property descriptors required to track changes on this instance.
2782
3095
  * @param instance
@@ -2787,35 +3100,7 @@
2787
3100
  enumerable: false,
2788
3101
  writable: true
2789
3102
  });
2790
- const metadata = instance.constructor[Symbol.metadata];
2791
- // Define property descriptors
2792
- for (const field in metadata) {
2793
- if (metadata[field].descriptor) {
2794
- // for encoder
2795
- Object.defineProperty(instance, `_${field}`, {
2796
- value: undefined,
2797
- writable: true,
2798
- enumerable: false,
2799
- configurable: true,
2800
- });
2801
- Object.defineProperty(instance, field, metadata[field].descriptor);
2802
- }
2803
- else {
2804
- // for decoder
2805
- Object.defineProperty(instance, field, {
2806
- value: undefined,
2807
- writable: true,
2808
- enumerable: true,
2809
- configurable: true,
2810
- });
2811
- }
2812
- // Object.defineProperty(instance, field, {
2813
- // ...instance.constructor[Symbol.metadata][field].descriptor
2814
- // });
2815
- // if (args[0]?.hasOwnProperty(field)) {
2816
- // instance[field] = args[0][field];
2817
- // }
2818
- }
3103
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2819
3104
  }
2820
3105
  static is(type) {
2821
3106
  return typeof (type[Symbol.metadata]) === "object";
@@ -2839,7 +3124,7 @@
2839
3124
  */
2840
3125
  static [$filter](ref, index, view) {
2841
3126
  const metadata = ref.constructor[Symbol.metadata];
2842
- const tag = metadata[metadata[index]].tag;
3127
+ const tag = metadata[index]?.tag;
2843
3128
  if (view === undefined) {
2844
3129
  // shared pass/encode: encode if doesn't have a tag
2845
3130
  return tag === undefined;
@@ -2860,12 +3145,16 @@
2860
3145
  }
2861
3146
  // allow inherited classes to have a constructor
2862
3147
  constructor(...args) {
3148
+ //
3149
+ // inline
3150
+ // Schema.initialize(this);
3151
+ //
2863
3152
  Schema.initialize(this);
2864
3153
  //
2865
3154
  // Assign initial values
2866
3155
  //
2867
3156
  if (args[0]) {
2868
- this.assign(args[0]);
3157
+ Object.assign(this, args[0]);
2869
3158
  }
2870
3159
  }
2871
3160
  assign(props) {
@@ -2879,7 +3168,8 @@
2879
3168
  * @param operation OPERATION to perform (detected automatically)
2880
3169
  */
2881
3170
  setDirty(property, operation) {
2882
- this[$changes].change(this.constructor[Symbol.metadata][property].index, operation);
3171
+ const metadata = this.constructor[Symbol.metadata];
3172
+ this[$changes].change(metadata[metadata[property]].index, operation);
2883
3173
  }
2884
3174
  clone() {
2885
3175
  const cloned = new (this.constructor);
@@ -2888,7 +3178,9 @@
2888
3178
  // TODO: clone all properties, not only annotated ones
2889
3179
  //
2890
3180
  // for (const field in this) {
2891
- for (const field in metadata) {
3181
+ for (const fieldIndex in metadata) {
3182
+ // const field = metadata[metadata[fieldIndex]].name;
3183
+ const field = metadata[fieldIndex].name;
2892
3184
  if (typeof (this[field]) === "object" &&
2893
3185
  typeof (this[field]?.clone) === "function") {
2894
3186
  // deep clone
@@ -2902,10 +3194,11 @@
2902
3194
  return cloned;
2903
3195
  }
2904
3196
  toJSON() {
2905
- const metadata = this.constructor[Symbol.metadata];
2906
3197
  const obj = {};
2907
- for (const fieldName in metadata) {
2908
- const field = metadata[fieldName];
3198
+ const metadata = this.constructor[Symbol.metadata];
3199
+ for (const index in metadata) {
3200
+ const field = metadata[index];
3201
+ const fieldName = field.name;
2909
3202
  if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
2910
3203
  obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
2911
3204
  ? this[fieldName]['toJSON']()
@@ -2918,18 +3211,27 @@
2918
3211
  this[$changes].discardAll();
2919
3212
  }
2920
3213
  [$getByIndex](index) {
2921
- return this[this.constructor[Symbol.metadata][index]];
3214
+ const metadata = this.constructor[Symbol.metadata];
3215
+ return this[metadata[index].name];
2922
3216
  }
2923
3217
  [$deleteByIndex](index) {
2924
- this[this.constructor[Symbol.metadata][index]] = undefined;
3218
+ const metadata = this.constructor[Symbol.metadata];
3219
+ this[metadata[index].name] = undefined;
2925
3220
  }
2926
- static debugRefIds(instance, jsonContents = true, level = 0) {
3221
+ /**
3222
+ * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3223
+ *
3224
+ * @param instance Schema instance
3225
+ * @param showContents display JSON contents of the instance
3226
+ * @returns
3227
+ */
3228
+ static debugRefIds(instance, showContents = false, level = 0) {
2927
3229
  const ref = instance;
2928
3230
  const changeTree = ref[$changes];
2929
- const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3231
+ const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2930
3232
  let output = "";
2931
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
2932
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
3233
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
3234
+ changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1));
2933
3235
  return output;
2934
3236
  }
2935
3237
  /**
@@ -2946,30 +3248,40 @@
2946
3248
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
2947
3249
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
2948
3250
  function dumpChangeSet(changeSet) {
2949
- Array.from(changeSet)
2950
- .sort((a, b) => a[0] - b[0])
2951
- .forEach(([index, operation]) => output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3251
+ changeSet.operations
3252
+ .filter(op => op)
3253
+ .forEach((index) => {
3254
+ const operation = changeTree.indexedOperations[index];
3255
+ console.log({ index, operation });
3256
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3257
+ });
2952
3258
  }
2953
3259
  dumpChangeSet(changeSet);
2954
3260
  // display filtered changes
2955
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3261
+ if (!isEncodeAll &&
3262
+ changeTree.filteredChanges &&
3263
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
2956
3264
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
2957
3265
  dumpChangeSet(changeTree.filteredChanges);
2958
3266
  }
2959
3267
  // display filtered changes
2960
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3268
+ if (isEncodeAll &&
3269
+ changeTree.allFilteredChanges &&
3270
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
2961
3271
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
2962
3272
  dumpChangeSet(changeTree.allFilteredChanges);
2963
3273
  }
2964
3274
  return output;
2965
3275
  }
2966
- static debugChangesDeep(ref) {
3276
+ static debugChangesDeep(ref, changeSetName = "changes") {
2967
3277
  let output = "";
2968
3278
  const rootChangeTree = ref[$changes];
3279
+ const root = rootChangeTree.root;
2969
3280
  const changeTrees = new Map();
2970
- let totalInstances = 0;
3281
+ const instanceRefIds = [];
2971
3282
  let totalOperations = 0;
2972
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3283
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3284
+ const changeTree = root.changeTrees[refId];
2973
3285
  let includeChangeTree = false;
2974
3286
  let parentChangeTrees = [];
2975
3287
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -2987,14 +3299,14 @@
2987
3299
  }
2988
3300
  }
2989
3301
  if (includeChangeTree) {
2990
- totalInstances += 1;
2991
- totalOperations += changes.size;
3302
+ instanceRefIds.push(changeTree.refId);
3303
+ totalOperations += Object.keys(changes).length;
2992
3304
  changeTrees.set(changeTree, parentChangeTrees.reverse());
2993
3305
  }
2994
3306
  }
2995
3307
  output += "---\n";
2996
3308
  output += `root refId: ${rootChangeTree.refId}\n`;
2997
- output += `Total instances: ${totalInstances}\n`;
3309
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
2998
3310
  output += `Total changes: ${totalOperations}\n`;
2999
3311
  output += "---\n";
3000
3312
  // based on root.changes, display a tree of changes that has the "ref" instance as parent
@@ -3006,12 +3318,13 @@
3006
3318
  visitedParents.add(parentChangeTree);
3007
3319
  }
3008
3320
  });
3009
- const changes = changeTree.changes;
3321
+ const changes = changeTree.indexedOperations;
3010
3322
  const level = parentChangeTrees.length;
3011
3323
  const indent = getIndent(level);
3012
3324
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3013
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3014
- for (const [index, operation] of changes) {
3325
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3326
+ for (const index in changes) {
3327
+ const operation = changes[index];
3015
3328
  output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3016
3329
  }
3017
3330
  }
@@ -3045,6 +3358,7 @@
3045
3358
  this.$indexes = new Map();
3046
3359
  this.$refId = 0;
3047
3360
  this[$changes] = new ChangeTree(this);
3361
+ this[$changes].indexes = {};
3048
3362
  if (initialValues) {
3049
3363
  initialValues.forEach((v) => this.add(v));
3050
3364
  }
@@ -3200,6 +3514,7 @@
3200
3514
  this.$indexes = new Map();
3201
3515
  this.$refId = 0;
3202
3516
  this[$changes] = new ChangeTree(this);
3517
+ this[$changes].indexes = {};
3203
3518
  if (initialValues) {
3204
3519
  initialValues.forEach((v) => this.add(v));
3205
3520
  }
@@ -3355,6 +3670,8 @@
3355
3670
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3356
3671
  PERFORMANCE OF THIS SOFTWARE.
3357
3672
  ***************************************************************************** */
3673
+ /* global Reflect, Promise, SuppressedError, Symbol */
3674
+
3358
3675
 
3359
3676
  function __decorate(decorators, target, key, desc) {
3360
3677
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3368,37 +3685,127 @@
3368
3685
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3369
3686
  };
3370
3687
 
3688
+ function spliceOne(arr, index) {
3689
+ // manually splice an array
3690
+ if (index === -1 || index >= arr.length) {
3691
+ return false;
3692
+ }
3693
+ const len = arr.length - 1;
3694
+ for (let i = index; i < len; i++) {
3695
+ arr[i] = arr[i + 1];
3696
+ }
3697
+ arr.length = len;
3698
+ return true;
3699
+ }
3700
+
3701
+ class Root {
3702
+ constructor(types) {
3703
+ this.types = types;
3704
+ this.nextUniqueId = 0;
3705
+ this.refCount = {};
3706
+ this.changeTrees = {};
3707
+ // all changes
3708
+ this.allChanges = [];
3709
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3710
+ // pending changes to be encoded
3711
+ this.changes = [];
3712
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3713
+ }
3714
+ getNextUniqueId() {
3715
+ return this.nextUniqueId++;
3716
+ }
3717
+ add(changeTree) {
3718
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3719
+ changeTree.ensureRefId();
3720
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3721
+ if (isNewChangeTree) {
3722
+ this.changeTrees[changeTree.refId] = changeTree;
3723
+ }
3724
+ const previousRefCount = this.refCount[changeTree.refId];
3725
+ if (previousRefCount === 0) {
3726
+ //
3727
+ // When a ChangeTree is re-added, it means that it was previously removed.
3728
+ // We need to re-add all changes to the `changes` map.
3729
+ //
3730
+ const ops = changeTree.allChanges.operations;
3731
+ let len = ops.length;
3732
+ while (len--) {
3733
+ changeTree.indexedOperations[ops[len]] = exports.OPERATION.ADD;
3734
+ setOperationAtIndex(changeTree.changes, len);
3735
+ }
3736
+ }
3737
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3738
+ return isNewChangeTree;
3739
+ }
3740
+ remove(changeTree) {
3741
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3742
+ if (refCount <= 0) {
3743
+ //
3744
+ // Only remove "root" reference if it's the last reference
3745
+ //
3746
+ changeTree.root = undefined;
3747
+ delete this.changeTrees[changeTree.refId];
3748
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3749
+ this.removeChangeFromChangeSet("changes", changeTree);
3750
+ if (changeTree.filteredChanges) {
3751
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3752
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3753
+ }
3754
+ this.refCount[changeTree.refId] = 0;
3755
+ }
3756
+ else {
3757
+ this.refCount[changeTree.refId] = refCount;
3758
+ }
3759
+ changeTree.forEachChild((child, _) => this.remove(child));
3760
+ return refCount;
3761
+ }
3762
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3763
+ const changeSet = this[changeSetName];
3764
+ const index = changeSet.indexOf(changeTree);
3765
+ if (index !== -1) {
3766
+ spliceOne(changeSet, index);
3767
+ // changeSet[index] = undefined;
3768
+ }
3769
+ }
3770
+ clear() {
3771
+ this.changes.length = 0;
3772
+ }
3773
+ }
3774
+
3371
3775
  class Encoder {
3372
3776
  static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3373
- constructor(root) {
3777
+ constructor(state) {
3374
3778
  this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3375
- this.setRoot(root);
3376
3779
  //
3377
3780
  // TODO: cache and restore "Context" based on root schema
3378
3781
  // (to avoid creating a new context for every new room)
3379
3782
  //
3380
- this.context = new TypeContext(root.constructor);
3783
+ this.context = new TypeContext(state.constructor);
3784
+ this.root = new Root(this.context);
3785
+ this.setState(state);
3381
3786
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3382
3787
  // this.context.schemas.forEach((id, schema) => {
3383
3788
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3384
3789
  // });
3385
3790
  }
3386
- setRoot(state) {
3387
- this.root = new Root();
3791
+ setState(state) {
3388
3792
  this.state = state;
3389
- state[$changes].setRoot(this.root);
3793
+ this.state[$changes].setRoot(this.root);
3390
3794
  }
3391
- encode(it = { offset: 0 }, view, bytes = this.sharedBuffer, changeTrees = this.root.changes) {
3392
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3393
- const isEncodeAll = this.root.allChanges === changeTrees;
3795
+ encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeSetName = "changes", isEncodeAll = changeSetName === "allChanges", initialOffset = it.offset // cache current offset in case we need to resize the buffer
3796
+ ) {
3394
3797
  const hasView = (view !== undefined);
3395
3798
  const rootChangeTree = this.state[$changes];
3396
- const changeTreesIterator = changeTrees.entries();
3397
- for (const [changeTree, changes] of changeTreesIterator) {
3799
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3800
+ const changeTrees = this.root[changeSetName];
3801
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3802
+ const changeTree = changeTrees[i];
3803
+ const operations = changeTree[changeSetName];
3398
3804
  const ref = changeTree.ref;
3399
- const ctor = ref['constructor'];
3805
+ const ctor = ref.constructor;
3400
3806
  const encoder = ctor[$encoder];
3401
3807
  const filter = ctor[$filter];
3808
+ const metadata = ctor[Symbol.metadata];
3402
3809
  if (hasView) {
3403
3810
  if (!view.items.has(changeTree)) {
3404
3811
  view.invisible.add(changeTree);
@@ -3409,12 +3816,18 @@
3409
3816
  }
3410
3817
  }
3411
3818
  // skip root `refId` if it's the first change tree
3412
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
3413
- bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3414
- number$1(bytes, changeTree.refId, it);
3819
+ // (unless it "hasView", which will need to revisit the root)
3820
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3821
+ buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3822
+ encode.number(buffer, changeTree.refId, it);
3415
3823
  }
3416
- const changesIterator = changes.entries();
3417
- for (const [fieldIndex, operation] of changesIterator) {
3824
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3825
+ const fieldIndex = operations.operations[j];
3826
+ const operation = (fieldIndex < 0)
3827
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3828
+ : (isEncodeAll)
3829
+ ? exports.OPERATION.ADD
3830
+ : changeTree.indexedOperations[fieldIndex];
3418
3831
  //
3419
3832
  // first pass (encodeAll), identify "filtered" operations without encoding them
3420
3833
  // they will be encoded per client, based on their view.
@@ -3422,93 +3835,109 @@
3422
3835
  // TODO: how can we optimize filtering out "encode all" operations?
3423
3836
  // TODO: avoid checking if no view tags were defined
3424
3837
  //
3425
- if (filter && !filter(ref, fieldIndex, view)) {
3426
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3838
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3427
3839
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3428
3840
  // view?.invisible.add(changeTree);
3429
3841
  continue;
3430
3842
  }
3431
- // console.log("WILL ENCODE", {
3432
- // ref: changeTree.ref.constructor.name,
3433
- // fieldIndex,
3434
- // operation: OPERATION[operation],
3435
- // });
3436
- encoder(this, bytes, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3843
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3437
3844
  }
3845
+ // if (shouldDiscardChanges) {
3846
+ // changeTree.discard();
3847
+ // changeTree.isNew = false; // Not a new instance anymore
3848
+ // }
3438
3849
  }
3439
- if (it.offset > bytes.byteLength) {
3440
- const newSize = getNextPowerOf2(this.sharedBuffer.byteLength * 2);
3441
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + bytes.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
3850
+ if (it.offset > buffer.byteLength) {
3851
+ const newSize = getNextPowerOf2(buffer.byteLength * 2);
3852
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
3853
+
3854
+ import { Encoder } from "@colyseus/schema";
3855
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
3856
+ `);
3442
3857
  //
3443
3858
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
3444
3859
  //
3445
- this.sharedBuffer = Buffer.allocUnsafeSlow(newSize);
3446
- return this.encode({ offset: initialOffset }, view);
3860
+ buffer = Buffer.alloc(newSize);
3861
+ // assign resized buffer to local sharedBuffer
3862
+ if (buffer === this.sharedBuffer) {
3863
+ this.sharedBuffer = buffer;
3864
+ }
3865
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3447
3866
  }
3448
3867
  else {
3449
3868
  //
3450
3869
  // only clear changes after making sure buffer resize is not required.
3451
3870
  //
3452
- if (!isEncodeAll && !hasView) {
3871
+ if (shouldDiscardChanges) {
3453
3872
  //
3454
- // FIXME: avoid iterating over change trees twice.
3873
+ // TODO: avoid iterating over change trees twice.
3455
3874
  //
3456
- this.onEndEncode(changeTrees);
3875
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3876
+ const changeTree = changeTrees[i];
3877
+ changeTree.discard();
3878
+ changeTree.isNew = false; // Not a new instance anymore
3879
+ }
3457
3880
  }
3458
- // return bytes;
3459
- return bytes.slice(0, it.offset);
3881
+ return buffer.subarray(0, it.offset);
3460
3882
  }
3461
3883
  }
3462
- encodeAll(it = { offset: 0 }) {
3463
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
3464
- // Array.from(this.$root.allChanges.entries()).map((item) => {
3465
- // console.log("->", item[0].refId, item[0].ref.toJSON());
3466
- // });
3467
- return this.encode(it, undefined, this.sharedBuffer, this.root.allChanges);
3884
+ encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3885
+ return this.encode(it, undefined, buffer, "allChanges", true);
3468
3886
  }
3469
3887
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3470
3888
  const viewOffset = it.offset;
3471
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
3472
- // this.debugAllFilteredChanges();
3473
3889
  // try to encode "filtered" changes
3474
- this.encode(it, view, bytes, this.root.allFilteredChanges);
3890
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3475
3891
  return Buffer.concat([
3476
- bytes.slice(0, sharedOffset),
3477
- bytes.slice(viewOffset, it.offset)
3892
+ bytes.subarray(0, sharedOffset),
3893
+ bytes.subarray(viewOffset, it.offset)
3478
3894
  ]);
3479
3895
  }
3480
- // debugAllFilteredChanges() {
3481
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
3482
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
3483
- // if (Array.isArray(item[0].ref.toJSON())) {
3484
- // item[1].forEach((op, key) => {
3485
- // console.log(" ->", { key, op: OPERATION[op] });
3486
- // })
3487
- // }
3488
- // });
3489
- // }
3896
+ debugChanges(field) {
3897
+ const rootChangeSet = (typeof (field) === "string")
3898
+ ? this.root[field]
3899
+ : field;
3900
+ rootChangeSet.forEach((changeTree) => {
3901
+ const changeSet = changeTree[field];
3902
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3903
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3904
+ for (const index in changeSet) {
3905
+ const op = changeSet[index];
3906
+ console.log(" ->", {
3907
+ index,
3908
+ field: metadata?.[index],
3909
+ op: exports.OPERATION[op],
3910
+ });
3911
+ }
3912
+ });
3913
+ }
3490
3914
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3491
3915
  const viewOffset = it.offset;
3492
- // try to encode "filtered" changes
3493
- this.encode(it, view, bytes, this.root.filteredChanges);
3494
3916
  // encode visibility changes (add/remove for this view)
3495
- const viewChangesIterator = view.changes.entries();
3496
- for (const [changeTree, changes] of viewChangesIterator) {
3497
- if (changes.size === 0) {
3498
- // FIXME: avoid having empty changes if no changes were made
3499
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3917
+ const refIds = Object.keys(view.changes);
3918
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3919
+ const refId = refIds[i];
3920
+ const changes = view.changes[refId];
3921
+ const changeTree = this.root.changeTrees[refId];
3922
+ if (changeTree === undefined ||
3923
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3924
+ ) {
3925
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3500
3926
  continue;
3501
3927
  }
3502
3928
  const ref = changeTree.ref;
3503
- const ctor = ref['constructor'];
3929
+ const ctor = ref.constructor;
3504
3930
  const encoder = ctor[$encoder];
3931
+ const metadata = ctor[Symbol.metadata];
3505
3932
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3506
- number$1(bytes, changeTree.refId, it);
3507
- const changesIterator = changes.entries();
3508
- for (const [fieldIndex, operation] of changesIterator) {
3933
+ encode.number(bytes, changeTree.refId, it);
3934
+ const keys = Object.keys(changes);
3935
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3936
+ const key = keys[i];
3937
+ const operation = changes[key];
3509
3938
  // isEncodeAll = false
3510
3939
  // hasView = true
3511
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3940
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3512
3941
  }
3513
3942
  }
3514
3943
  //
@@ -3516,51 +3945,62 @@
3516
3945
  // (to allow re-using StateView's for multiple clients)
3517
3946
  //
3518
3947
  // clear "view" changes after encoding
3519
- view.changes.clear();
3948
+ view.changes = {};
3949
+ // try to encode "filtered" changes
3950
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3520
3951
  return Buffer.concat([
3521
- bytes.slice(0, sharedOffset),
3522
- bytes.slice(viewOffset, it.offset)
3952
+ bytes.subarray(0, sharedOffset),
3953
+ bytes.subarray(viewOffset, it.offset)
3523
3954
  ]);
3524
3955
  }
3525
3956
  onEndEncode(changeTrees = this.root.changes) {
3526
- const changeTreesIterator = changeTrees.entries();
3527
- for (const [changeTree, _] of changeTreesIterator) {
3528
- changeTree.endEncode();
3529
- }
3957
+ // changeTrees.forEach(function(changeTree) {
3958
+ // changeTree.endEncode();
3959
+ // });
3960
+ // for (const refId in changeTrees) {
3961
+ // const changeTree = this.root.changeTrees[refId];
3962
+ // changeTree.endEncode();
3963
+ // // changeTree.changes.clear();
3964
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3965
+ // // changeTree.ref[$onEncodeEnd]?.();
3966
+ // // // Not a new instance anymore
3967
+ // // delete changeTree[$isNew];
3968
+ // }
3530
3969
  }
3531
3970
  discardChanges() {
3532
3971
  // discard shared changes
3533
- if (this.root.changes.size > 0) {
3534
- this.onEndEncode(this.root.changes);
3535
- this.root.changes.clear();
3972
+ let length = this.root.changes.length;
3973
+ if (length > 0) {
3974
+ while (length--) {
3975
+ this.root.changes[length]?.endEncode();
3976
+ }
3977
+ this.root.changes.length = 0;
3536
3978
  }
3537
3979
  // discard filtered changes
3538
- if (this.root.filteredChanges.size > 0) {
3539
- this.onEndEncode(this.root.filteredChanges);
3540
- this.root.filteredChanges.clear();
3980
+ length = this.root.filteredChanges.length;
3981
+ if (length > 0) {
3982
+ while (length--) {
3983
+ this.root.filteredChanges[length]?.endEncode();
3984
+ }
3985
+ this.root.filteredChanges.length = 0;
3541
3986
  }
3542
3987
  }
3543
3988
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3544
3989
  const baseTypeId = this.context.getTypeId(baseType);
3545
3990
  const targetTypeId = this.context.getTypeId(targetType);
3991
+ if (targetTypeId === undefined) {
3992
+ console.warn(`@colyseus/schema WARNING: Class "${targetType.name}" is not registered on TypeRegistry - Please either tag the class with @entity or define a @type() field.`);
3993
+ return;
3994
+ }
3546
3995
  if (baseTypeId !== targetTypeId) {
3547
3996
  bytes[it.offset++] = TYPE_ID & 255;
3548
- number$1(bytes, targetTypeId, it);
3997
+ encode.number(bytes, targetTypeId, it);
3549
3998
  }
3550
3999
  }
3551
- }
3552
-
3553
- function spliceOne(arr, index) {
3554
- // manually splice an array
3555
- if (index === -1 || index >= arr.length) {
3556
- return false;
3557
- }
3558
- const len = arr.length - 1;
3559
- for (let i = index; i < len; i++) {
3560
- arr[i] = arr[i + 1];
4000
+ get hasChanges() {
4001
+ return (this.root.changes.length > 0 ||
4002
+ this.root.filteredChanges.length > 0);
3561
4003
  }
3562
- arr.length = len;
3563
- return true;
3564
4004
  }
3565
4005
 
3566
4006
  class DecodingWarning extends Error {
@@ -3625,6 +4065,7 @@
3625
4065
  clearRefs() {
3626
4066
  this.refs.clear();
3627
4067
  this.deletedRefs.clear();
4068
+ this.callbacks = {};
3628
4069
  this.refCounts = {};
3629
4070
  }
3630
4071
  // for decoding
@@ -3641,8 +4082,9 @@
3641
4082
  // Ensure child schema instances have their references removed as well.
3642
4083
  //
3643
4084
  if (Metadata.isValidInstance(ref)) {
3644
- const metadata = ref['constructor'][Symbol.metadata];
3645
- for (const field in metadata) {
4085
+ const metadata = ref.constructor[Symbol.metadata];
4086
+ for (const index in metadata) {
4087
+ const field = metadata[index].name;
3646
4088
  const childRefId = typeof (ref[field]) === "object" && this.refIds.get(ref[field]);
3647
4089
  if (childRefId) {
3648
4090
  this.removeRef(childRefId);
@@ -3689,21 +4131,21 @@
3689
4131
  class Decoder {
3690
4132
  constructor(root, context) {
3691
4133
  this.currentRefId = 0;
3692
- this.setRoot(root);
4134
+ this.setState(root);
3693
4135
  this.context = context || new TypeContext(root.constructor);
3694
4136
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3695
4137
  // this.context.schemas.forEach((id, schema) => {
3696
4138
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3697
4139
  // });
3698
4140
  }
3699
- setRoot(root) {
4141
+ setState(root) {
3700
4142
  this.state = root;
3701
- this.$root = new ReferenceTracker();
3702
- this.$root.addRef(0, root);
4143
+ this.root = new ReferenceTracker();
4144
+ this.root.addRef(0, root);
3703
4145
  }
3704
4146
  decode(bytes, it = { offset: 0 }, ref = this.state) {
3705
4147
  const allChanges = [];
3706
- const $root = this.$root;
4148
+ const $root = this.root;
3707
4149
  const totalBytes = bytes.byteLength;
3708
4150
  let decoder = ref['constructor'][$decoder];
3709
4151
  this.currentRefId = 0;
@@ -3713,7 +4155,7 @@
3713
4155
  //
3714
4156
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
3715
4157
  it.offset++;
3716
- this.currentRefId = number(bytes, it);
4158
+ this.currentRefId = decode.number(bytes, it);
3717
4159
  const nextRef = $root.refs.get(this.currentRefId);
3718
4160
  //
3719
4161
  // Trying to access a reference that haven't been decoded yet.
@@ -3723,7 +4165,7 @@
3723
4165
  }
3724
4166
  ref[$onDecodeEnd]?.();
3725
4167
  ref = nextRef;
3726
- decoder = ref['constructor'][$decoder];
4168
+ decoder = ref.constructor[$decoder];
3727
4169
  continue;
3728
4170
  }
3729
4171
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3735,9 +4177,9 @@
3735
4177
  //
3736
4178
  const nextIterator = { offset: it.offset };
3737
4179
  while (it.offset < totalBytes) {
3738
- if (switchStructureCheck(bytes, it)) {
4180
+ if (bytes[it.offset] === SWITCH_TO_STRUCTURE) {
3739
4181
  nextIterator.offset = it.offset + 1;
3740
- if ($root.refs.has(number(bytes, nextIterator))) {
4182
+ if ($root.refs.has(decode.number(bytes, nextIterator))) {
3741
4183
  break;
3742
4184
  }
3743
4185
  }
@@ -3758,7 +4200,7 @@
3758
4200
  let type;
3759
4201
  if (bytes[it.offset] === TYPE_ID) {
3760
4202
  it.offset++;
3761
- const type_id = number(bytes, it);
4203
+ const type_id = decode.number(bytes, it);
3762
4204
  type = this.context.get(type_id);
3763
4205
  }
3764
4206
  return type || defaultType;
@@ -3784,7 +4226,7 @@
3784
4226
  previousValue: value
3785
4227
  });
3786
4228
  if (needRemoveRef) {
3787
- this.$root.removeRef(this.$root.refIds.get(value));
4229
+ this.root.removeRef(this.root.refIds.get(value));
3788
4230
  }
3789
4231
  });
3790
4232
  }
@@ -3824,14 +4266,27 @@
3824
4266
  super(...arguments);
3825
4267
  this.types = new ArraySchema();
3826
4268
  }
3827
- static encode(instance, context, it = { offset: 0 }) {
3828
- if (!context) {
3829
- context = new TypeContext(instance.constructor);
3830
- }
4269
+ /**
4270
+ * Encodes the TypeContext of an Encoder into a buffer.
4271
+ *
4272
+ * @param encoder Encoder instance
4273
+ * @param it
4274
+ * @returns
4275
+ */
4276
+ static encode(encoder, it = { offset: 0 }) {
4277
+ const context = encoder.context;
3831
4278
  const reflection = new Reflection();
3832
- const encoder = new Encoder(reflection);
4279
+ const reflectionEncoder = new Encoder(reflection);
4280
+ // rootType is usually the first schema passed to the Encoder
4281
+ // (unless it inherits from another schema)
4282
+ const rootType = context.schemas.get(encoder.state.constructor);
4283
+ if (rootType > 0) {
4284
+ reflection.rootType = rootType;
4285
+ }
3833
4286
  const buildType = (currentType, metadata) => {
3834
- for (const fieldName in metadata) {
4287
+ for (const fieldIndex in metadata) {
4288
+ const index = Number(fieldIndex);
4289
+ const fieldName = metadata[index].name;
3835
4290
  // skip fields from parent classes
3836
4291
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
3837
4292
  continue;
@@ -3839,7 +4294,7 @@
3839
4294
  const field = new ReflectionField();
3840
4295
  field.name = fieldName;
3841
4296
  let fieldType;
3842
- const type = metadata[fieldName].type;
4297
+ const type = metadata[index].type;
3843
4298
  if (typeof (type) === "string") {
3844
4299
  fieldType = type;
3845
4300
  }
@@ -3881,65 +4336,335 @@
3881
4336
  }
3882
4337
  buildType(type, klass[Symbol.metadata]);
3883
4338
  }
3884
- const buf = encoder.encodeAll(it);
4339
+ const buf = reflectionEncoder.encodeAll(it);
3885
4340
  return Buffer.from(buf, 0, it.offset);
3886
4341
  }
4342
+ /**
4343
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4344
+ *
4345
+ * @param bytes Reflection.encode() output
4346
+ * @param it
4347
+ * @returns Decoder instance
4348
+ */
3887
4349
  static decode(bytes, it) {
3888
4350
  const reflection = new Reflection();
3889
4351
  const reflectionDecoder = new Decoder(reflection);
3890
4352
  reflectionDecoder.decode(bytes, it);
3891
- const context = new TypeContext();
3892
- const schemaTypes = reflection.types.reduce((types, reflectionType) => {
3893
- const parentKlass = types[reflectionType.extendsId] || Schema;
3894
- const schema = class _ extends parentKlass {
4353
+ const typeContext = new TypeContext();
4354
+ // 1st pass, initialize metadata + inheritance
4355
+ reflection.types.forEach((reflectionType) => {
4356
+ const parentClass = typeContext.get(reflectionType.extendsId) ?? Schema;
4357
+ const schema = class _ extends parentClass {
3895
4358
  };
3896
- // const _metadata = Object.create(_classSuper[Symbol.metadata] ?? null);
3897
- const _metadata = parentKlass && parentKlass[Symbol.metadata] || Object.create(null);
3898
- Object.defineProperty(schema, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
3899
4359
  // register for inheritance support
3900
4360
  TypeContext.register(schema);
3901
- const typeid = reflectionType.id;
3902
- types[typeid] = schema;
3903
- context.add(schema, typeid);
3904
- return types;
4361
+ // // for inheritance support
4362
+ // Metadata.initialize(schema);
4363
+ typeContext.add(schema, reflectionType.id);
3905
4364
  }, {});
3906
- reflection.types.forEach((reflectionType) => {
3907
- const schemaType = schemaTypes[reflectionType.id];
3908
- const metadata = schemaType[Symbol.metadata];
3909
- const parentKlass = reflection.types[reflectionType.extendsId];
3910
- const parentFieldIndex = parentKlass && parentKlass.fields.length || 0;
4365
+ // define fields
4366
+ const addFields = (metadata, reflectionType, parentFieldIndex) => {
3911
4367
  reflectionType.fields.forEach((field, i) => {
3912
4368
  const fieldIndex = parentFieldIndex + i;
3913
4369
  if (field.referencedType !== undefined) {
3914
4370
  let fieldType = field.type;
3915
- let refType = schemaTypes[field.referencedType];
4371
+ let refType = typeContext.get(field.referencedType);
3916
4372
  // map or array of primitive type (-1)
3917
4373
  if (!refType) {
3918
4374
  const typeInfo = field.type.split(":");
3919
4375
  fieldType = typeInfo[0];
3920
- refType = typeInfo[1];
4376
+ refType = typeInfo[1]; // string
3921
4377
  }
3922
4378
  if (fieldType === "ref") {
3923
- // type(refType)(schemaType.prototype, field.name);
3924
4379
  Metadata.addField(metadata, fieldIndex, field.name, refType);
3925
4380
  }
3926
4381
  else {
3927
- // type({ [fieldType]: refType } as DefinitionType)(schemaType.prototype, field.name);
3928
4382
  Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
3929
4383
  }
3930
4384
  }
3931
4385
  else {
3932
- // type(field.type as PrimitiveType)(schemaType.prototype, field.name);
3933
4386
  Metadata.addField(metadata, fieldIndex, field.name, field.type);
3934
4387
  }
3935
4388
  });
4389
+ };
4390
+ // 2nd pass, set fields
4391
+ reflection.types.forEach((reflectionType) => {
4392
+ const schema = typeContext.get(reflectionType.id);
4393
+ // for inheritance support
4394
+ const metadata = Metadata.initialize(schema);
4395
+ const inheritedTypes = [];
4396
+ let parentType = reflectionType;
4397
+ do {
4398
+ inheritedTypes.push(parentType);
4399
+ parentType = reflection.types.find((t) => t.id === parentType.extendsId);
4400
+ } while (parentType);
4401
+ let parentFieldIndex = 0;
4402
+ inheritedTypes.reverse().forEach((reflectionType) => {
4403
+ // add fields from all inherited classes
4404
+ // TODO: refactor this to avoid adding fields from parent classes
4405
+ addFields(metadata, reflectionType, parentFieldIndex);
4406
+ parentFieldIndex += reflectionType.fields.length;
4407
+ });
3936
4408
  });
3937
- return new (schemaTypes[0])();
4409
+ const state = new (typeContext.get(reflection.rootType || 0))();
4410
+ return new Decoder(state, typeContext);
3938
4411
  }
3939
4412
  }
3940
4413
  __decorate([
3941
4414
  type([ReflectionType])
3942
4415
  ], Reflection.prototype, "types", void 0);
4416
+ __decorate([
4417
+ type("number")
4418
+ ], Reflection.prototype, "rootType", void 0);
4419
+
4420
+ function getDecoderStateCallbacks(decoder) {
4421
+ const $root = decoder.root;
4422
+ const callbacks = $root.callbacks;
4423
+ const onAddCalls = new WeakMap();
4424
+ let currentOnAddCallback;
4425
+ decoder.triggerChanges = function (allChanges) {
4426
+ const uniqueRefIds = new Set();
4427
+ for (let i = 0, l = allChanges.length; i < l; i++) {
4428
+ const change = allChanges[i];
4429
+ const refId = change.refId;
4430
+ const ref = change.ref;
4431
+ const $callbacks = callbacks[refId];
4432
+ if (!$callbacks) {
4433
+ continue;
4434
+ }
4435
+ //
4436
+ // trigger onRemove on child structure.
4437
+ //
4438
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE &&
4439
+ change.previousValue instanceof Schema) {
4440
+ const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[exports.OPERATION.DELETE];
4441
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4442
+ deleteCallbacks[i]();
4443
+ }
4444
+ }
4445
+ if (ref instanceof Schema) {
4446
+ //
4447
+ // Handle schema instance
4448
+ //
4449
+ if (!uniqueRefIds.has(refId)) {
4450
+ // trigger onChange
4451
+ const replaceCallbacks = $callbacks?.[exports.OPERATION.REPLACE];
4452
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4453
+ replaceCallbacks[i]();
4454
+ // try {
4455
+ // } catch (e) {
4456
+ // console.error(e);
4457
+ // }
4458
+ }
4459
+ }
4460
+ if ($callbacks.hasOwnProperty(change.field)) {
4461
+ const fieldCallbacks = $callbacks[change.field];
4462
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
4463
+ fieldCallbacks[i](change.value, change.previousValue);
4464
+ // try {
4465
+ // } catch (e) {
4466
+ // console.error(e);
4467
+ // }
4468
+ }
4469
+ }
4470
+ }
4471
+ else {
4472
+ //
4473
+ // Handle collection of items
4474
+ //
4475
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
4476
+ //
4477
+ // FIXME: `previousValue` should always be available.
4478
+ //
4479
+ if (change.previousValue !== undefined) {
4480
+ // triger onRemove
4481
+ const deleteCallbacks = $callbacks[exports.OPERATION.DELETE];
4482
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4483
+ deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
4484
+ }
4485
+ }
4486
+ // Handle DELETE_AND_ADD operations
4487
+ if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
4488
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4489
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4490
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4491
+ }
4492
+ }
4493
+ }
4494
+ else if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD && change.previousValue === undefined) {
4495
+ // triger onAdd
4496
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4497
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4498
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4499
+ }
4500
+ }
4501
+ // trigger onChange
4502
+ if (change.value !== change.previousValue) {
4503
+ const replaceCallbacks = $callbacks[exports.OPERATION.REPLACE];
4504
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4505
+ replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4506
+ }
4507
+ }
4508
+ }
4509
+ uniqueRefIds.add(refId);
4510
+ }
4511
+ };
4512
+ function getProxy(metadataOrType, context) {
4513
+ let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
4514
+ let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
4515
+ (metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
4516
+ if (metadata && !isCollection) {
4517
+ const onAddListen = function (ref, prop, callback, immediate) {
4518
+ // immediate trigger
4519
+ if (immediate &&
4520
+ context.instance[prop] !== undefined &&
4521
+ !onAddCalls.has(currentOnAddCallback) // Workaround for https://github.com/colyseus/schema/issues/147
4522
+ ) {
4523
+ callback(context.instance[prop], undefined);
4524
+ }
4525
+ return $root.addCallback($root.refIds.get(ref), prop, callback);
4526
+ };
4527
+ /**
4528
+ * Schema instances
4529
+ */
4530
+ return new Proxy({
4531
+ listen: function listen(prop, callback, immediate = true) {
4532
+ if (context.instance) {
4533
+ return onAddListen(context.instance, prop, callback, immediate);
4534
+ }
4535
+ else {
4536
+ // collection instance not received yet
4537
+ let detachCallback = () => { };
4538
+ context.onInstanceAvailable((ref, existing) => {
4539
+ detachCallback = onAddListen(ref, prop, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4540
+ });
4541
+ return () => detachCallback();
4542
+ }
4543
+ },
4544
+ onChange: function onChange(callback) {
4545
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
4546
+ },
4547
+ //
4548
+ // TODO: refactor `bindTo()` implementation.
4549
+ // There is room for improvement.
4550
+ //
4551
+ bindTo: function bindTo(targetObject, properties) {
4552
+ if (!properties) {
4553
+ properties = Object.keys(metadata).map((index) => metadata[index].name);
4554
+ }
4555
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, () => {
4556
+ properties.forEach((prop) => targetObject[prop] = context.instance[prop]);
4557
+ });
4558
+ }
4559
+ }, {
4560
+ get(target, prop) {
4561
+ const metadataField = metadata[metadata[prop]];
4562
+ if (metadataField) {
4563
+ const instance = context.instance?.[prop];
4564
+ const onInstanceAvailable = ((callback) => {
4565
+ const unbind = $(context.instance).listen(prop, (value, _) => {
4566
+ callback(value, false);
4567
+ // FIXME: by "unbinding" the callback here,
4568
+ // it will not support when the server
4569
+ // re-instantiates the instance.
4570
+ //
4571
+ unbind?.();
4572
+ }, false);
4573
+ // has existing value
4574
+ if ($root.refIds.get(instance) !== undefined) {
4575
+ callback(instance, true);
4576
+ }
4577
+ });
4578
+ return getProxy(metadataField.type, {
4579
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4580
+ instance: ($root.refIds.get(instance) && instance),
4581
+ parentInstance: context.instance,
4582
+ onInstanceAvailable,
4583
+ });
4584
+ }
4585
+ else {
4586
+ // accessing the function
4587
+ return target[prop];
4588
+ }
4589
+ },
4590
+ has(target, prop) { return metadata[prop] !== undefined; },
4591
+ set(_, _1, _2) { throw new Error("not allowed"); },
4592
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4593
+ });
4594
+ }
4595
+ else {
4596
+ /**
4597
+ * Collection instances
4598
+ */
4599
+ const onAdd = function (ref, callback, immediate) {
4600
+ // Trigger callback on existing items
4601
+ if (immediate) {
4602
+ ref.forEach((v, k) => callback(v, k));
4603
+ }
4604
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, (value, key) => {
4605
+ onAddCalls.set(callback, true);
4606
+ currentOnAddCallback = callback;
4607
+ callback(value, key);
4608
+ onAddCalls.delete(callback);
4609
+ currentOnAddCallback = undefined;
4610
+ });
4611
+ };
4612
+ const onRemove = function (ref, callback) {
4613
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
4614
+ };
4615
+ return new Proxy({
4616
+ onAdd: function (callback, immediate = true) {
4617
+ //
4618
+ // https://github.com/colyseus/schema/issues/147
4619
+ // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4620
+ //
4621
+ if (context.instance) {
4622
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4623
+ }
4624
+ else if (context.onInstanceAvailable) {
4625
+ // collection instance not received yet
4626
+ let detachCallback = () => { };
4627
+ context.onInstanceAvailable((ref, existing) => {
4628
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4629
+ });
4630
+ return () => detachCallback();
4631
+ }
4632
+ },
4633
+ onRemove: function (callback) {
4634
+ if (context.onInstanceAvailable) {
4635
+ // collection instance not received yet
4636
+ let detachCallback = () => { };
4637
+ context.onInstanceAvailable((ref) => {
4638
+ detachCallback = onRemove(ref, callback);
4639
+ });
4640
+ return () => detachCallback();
4641
+ }
4642
+ else if (context.instance) {
4643
+ return onRemove(context.instance, callback);
4644
+ }
4645
+ },
4646
+ }, {
4647
+ get(target, prop) {
4648
+ if (!target[prop]) {
4649
+ throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
4650
+ }
4651
+ return target[prop];
4652
+ },
4653
+ has(target, prop) { return target[prop] !== undefined; },
4654
+ set(_, _1, _2) { throw new Error("not allowed"); },
4655
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4656
+ });
4657
+ }
4658
+ }
4659
+ function $(instance) {
4660
+ return getProxy(undefined, { instance });
4661
+ }
4662
+ return $;
4663
+ }
4664
+
4665
+ function getRawChangesCallback(decoder, callback) {
4666
+ decoder.triggerChanges = callback;
4667
+ }
3943
4668
 
3944
4669
  class StateView {
3945
4670
  constructor() {
@@ -3955,31 +4680,32 @@
3955
4680
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
3956
4681
  * (This is used to force encoding a property, even if it was not changed)
3957
4682
  */
3958
- this.changes = new Map();
4683
+ this.changes = {};
3959
4684
  }
3960
4685
  // TODO: allow to set multiple tags at once
3961
- add(obj, tag = DEFAULT_VIEW_TAG) {
4686
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
3962
4687
  if (!obj[$changes]) {
3963
4688
  console.warn("StateView#add(), invalid object:", obj);
3964
4689
  return this;
3965
4690
  }
3966
- let changeTree = obj[$changes];
3967
- this.items.add(changeTree);
3968
- // Add children of this ChangeTree to this view
3969
- changeTree.forEachChild((change, _) => this.add(change.ref, tag));
3970
- // FIXME: ArraySchema/MapSchema does not have metadata
4691
+ // FIXME: ArraySchema/MapSchema do not have metadata
3971
4692
  const metadata = obj.constructor[Symbol.metadata];
3972
- // add parent ChangeTree's, if they are invisible to this view
3973
- // TODO: REFACTOR addParent()
3974
- this.addParent(changeTree, tag);
4693
+ const changeTree = obj[$changes];
4694
+ this.items.add(changeTree);
4695
+ // add parent ChangeTree's
4696
+ // - if it was invisible to this view
4697
+ // - if it were previously filtered out
4698
+ if (checkIncludeParent && changeTree.parent) {
4699
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4700
+ }
3975
4701
  //
3976
4702
  // TODO: when adding an item of a MapSchema, the changes may not
3977
4703
  // be set (only the parent's changes are set)
3978
4704
  //
3979
- let changes = this.changes.get(changeTree);
4705
+ let changes = this.changes[changeTree.refId];
3980
4706
  if (changes === undefined) {
3981
- changes = new Map();
3982
- this.changes.set(changeTree, changes);
4707
+ changes = {};
4708
+ this.changes[changeTree.refId] = changes;
3983
4709
  }
3984
4710
  // set tag
3985
4711
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -3995,82 +4721,78 @@
3995
4721
  tags = this.tags.get(changeTree);
3996
4722
  }
3997
4723
  tags.add(tag);
3998
- // console.log("BY TAG:", tag);
3999
4724
  // Ref: add tagged properties
4000
- metadata?.[-3]?.[tag]?.forEach((index) => {
4725
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4001
4726
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4002
- changes.set(index, exports.OPERATION.ADD);
4727
+ changes[index] = exports.OPERATION.ADD;
4003
4728
  }
4004
4729
  });
4005
4730
  }
4006
4731
  else {
4007
- // console.log("DEFAULT TAG", changeTree.allChanges);
4008
- // // add default tag properties
4009
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4010
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4011
- // changes.set(index, OPERATION.ADD);
4012
- // }
4013
- // });
4014
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4732
+ const isInvisible = this.invisible.has(changeTree);
4733
+ const changeSet = (changeTree.filteredChanges !== undefined)
4015
4734
  ? changeTree.allFilteredChanges
4016
4735
  : changeTree.allChanges;
4017
- const it = allChangesSet.keys();
4018
- const isInvisible = this.invisible.has(changeTree);
4019
- for (const index of it) {
4020
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4021
- changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4022
- changes.set(index, exports.OPERATION.ADD);
4736
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4737
+ const index = changeSet.operations[i];
4738
+ if (index === undefined) {
4739
+ continue;
4740
+ } // skip "undefined" indexes
4741
+ const op = changeTree.indexedOperations[index] ?? exports.OPERATION.ADD;
4742
+ const tagAtIndex = metadata?.[index].tag;
4743
+ if ((isInvisible || // if "invisible", include all
4744
+ tagAtIndex === undefined || // "all change" with no tag
4745
+ tagAtIndex === tag // tagged property
4746
+ ) &&
4747
+ op !== exports.OPERATION.DELETE) {
4748
+ changes[index] = op;
4023
4749
  }
4024
4750
  }
4025
4751
  }
4026
- // TODO: avoid unnecessary iteration here
4027
- while (changeTree.parent &&
4028
- (changeTree = changeTree.parent[$changes]) &&
4029
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4030
- this.items.add(changeTree);
4031
- }
4752
+ // Add children of this ChangeTree to this view
4753
+ changeTree.forEachChild((change, index) => {
4754
+ // Do not ADD children that don't have the same tag
4755
+ if (metadata &&
4756
+ metadata[index].tag !== undefined &&
4757
+ metadata[index].tag !== tag) {
4758
+ return;
4759
+ }
4760
+ this.add(change.ref, tag, false);
4761
+ });
4032
4762
  return this;
4033
4763
  }
4034
- addParent(changeTree, tag) {
4035
- const parentRef = changeTree.parent;
4036
- if (!parentRef) {
4037
- return;
4764
+ addParent(changeTree, parentIndex, tag) {
4765
+ // view must have all "changeTree" parent tree
4766
+ this.items.add(changeTree);
4767
+ // add parent's parent
4768
+ const parentChangeTree = changeTree.parent?.[$changes];
4769
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
4770
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4038
4771
  }
4039
- const parentChangeTree = parentRef[$changes];
4040
- const parentIndex = changeTree.parentIndex;
4041
- if (!this.invisible.has(parentChangeTree)) {
4042
- // parent is already available, no need to add it!
4772
+ // parent is already available, no need to add it!
4773
+ if (!this.invisible.has(changeTree)) {
4043
4774
  return;
4044
4775
  }
4045
- this.addParent(parentChangeTree, tag);
4046
4776
  // add parent's tag properties
4047
- if (parentChangeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4048
- let parentChanges = this.changes.get(parentChangeTree);
4049
- if (parentChanges === undefined) {
4050
- parentChanges = new Map();
4051
- this.changes.set(parentChangeTree, parentChanges);
4777
+ if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4778
+ let changes = this.changes[changeTree.refId];
4779
+ if (changes === undefined) {
4780
+ changes = {};
4781
+ this.changes[changeTree.refId] = changes;
4052
4782
  }
4053
- // console.log("add parent change", {
4054
- // parentIndex,
4055
- // parentChanges,
4056
- // parentChange: (
4057
- // parentChangeTree.getChange(parentIndex) &&
4058
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4059
- // ),
4060
- // })
4061
4783
  if (!this.tags) {
4062
4784
  this.tags = new WeakMap();
4063
4785
  }
4064
4786
  let tags;
4065
- if (!this.tags.has(parentChangeTree)) {
4787
+ if (!this.tags.has(changeTree)) {
4066
4788
  tags = new Set();
4067
- this.tags.set(parentChangeTree, tags);
4789
+ this.tags.set(changeTree, tags);
4068
4790
  }
4069
4791
  else {
4070
- tags = this.tags.get(parentChangeTree);
4792
+ tags = this.tags.get(changeTree);
4071
4793
  }
4072
4794
  tags.add(tag);
4073
- parentChanges.set(parentIndex, exports.OPERATION.ADD);
4795
+ changes[parentIndex] = exports.OPERATION.ADD;
4074
4796
  }
4075
4797
  }
4076
4798
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4082,32 +4804,32 @@
4082
4804
  this.items.delete(changeTree);
4083
4805
  const ref = changeTree.ref;
4084
4806
  const metadata = ref.constructor[Symbol.metadata];
4085
- let changes = this.changes.get(changeTree);
4807
+ let changes = this.changes[changeTree.refId];
4086
4808
  if (changes === undefined) {
4087
- changes = new Map();
4088
- this.changes.set(changeTree, changes);
4809
+ changes = {};
4810
+ this.changes[changeTree.refId] = changes;
4089
4811
  }
4090
4812
  if (tag === DEFAULT_VIEW_TAG) {
4091
4813
  // parent is collection (Map/Array)
4092
4814
  const parent = changeTree.parent;
4093
4815
  if (!Metadata.isValidInstance(parent)) {
4094
4816
  const parentChangeTree = parent[$changes];
4095
- let changes = this.changes.get(parentChangeTree);
4817
+ let changes = this.changes[parentChangeTree.refId];
4096
4818
  if (changes === undefined) {
4097
- changes = new Map();
4098
- this.changes.set(parentChangeTree, changes);
4819
+ changes = {};
4820
+ this.changes[parentChangeTree.refId] = changes;
4099
4821
  }
4100
4822
  // DELETE / DELETE BY REF ID
4101
- changes.set(changeTree.parentIndex, exports.OPERATION.DELETE);
4823
+ changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
4102
4824
  }
4103
4825
  else {
4104
4826
  // delete all "tagged" properties.
4105
- metadata[-2].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4827
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4106
4828
  }
4107
4829
  }
4108
4830
  else {
4109
4831
  // delete only tagged properties
4110
- metadata[-3][tag].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4832
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4111
4833
  }
4112
4834
  // remove tag
4113
4835
  if (this.tags && this.tags.has(changeTree)) {
@@ -4159,16 +4881,18 @@
4159
4881
  exports.decode = decode;
4160
4882
  exports.decodeKeyValueOperation = decodeKeyValueOperation;
4161
4883
  exports.decodeSchemaOperation = decodeSchemaOperation;
4884
+ exports.defineCustomTypes = defineCustomTypes;
4162
4885
  exports.defineTypes = defineTypes;
4163
4886
  exports.deprecated = deprecated;
4164
4887
  exports.dumpChanges = dumpChanges;
4165
4888
  exports.encode = encode;
4166
4889
  exports.encodeKeyValueOperation = encodeArray;
4167
4890
  exports.encodeSchemaOperation = encodeSchemaOperation;
4891
+ exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
4892
+ exports.getRawChangesCallback = getRawChangesCallback;
4168
4893
  exports.registerType = registerType;
4894
+ exports.schema = schema;
4169
4895
  exports.type = type;
4170
4896
  exports.view = view;
4171
4897
 
4172
- Object.defineProperty(exports, '__esModule', { value: true });
4173
-
4174
4898
  }));