@colyseus/schema 3.0.0-alpha.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +148 -62
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +2222 -1513
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +2223 -1516
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +2225 -1516
  8. package/lib/Metadata.d.ts +21 -9
  9. package/lib/Metadata.js +169 -32
  10. package/lib/Metadata.js.map +1 -1
  11. package/lib/Reflection.d.ts +19 -4
  12. package/lib/Reflection.js +66 -31
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +12 -5
  15. package/lib/Schema.js +57 -56
  16. package/lib/Schema.js.map +1 -1
  17. package/lib/annotations.d.ts +31 -34
  18. package/lib/annotations.js +110 -160
  19. package/lib/annotations.js.map +1 -1
  20. package/lib/bench_encode.d.ts +1 -0
  21. package/lib/bench_encode.js +130 -0
  22. package/lib/bench_encode.js.map +1 -0
  23. package/lib/codegen/api.js +1 -2
  24. package/lib/codegen/api.js.map +1 -1
  25. package/lib/codegen/languages/cpp.js +1 -2
  26. package/lib/codegen/languages/cpp.js.map +1 -1
  27. package/lib/codegen/languages/csharp.js +9 -46
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +4 -2
  30. package/lib/codegen/languages/haxe.js.map +1 -1
  31. package/lib/codegen/languages/java.js +1 -2
  32. package/lib/codegen/languages/java.js.map +1 -1
  33. package/lib/codegen/languages/js.js +1 -2
  34. package/lib/codegen/languages/js.js.map +1 -1
  35. package/lib/codegen/languages/lua.js +23 -25
  36. package/lib/codegen/languages/lua.js.map +1 -1
  37. package/lib/codegen/languages/ts.js +1 -2
  38. package/lib/codegen/languages/ts.js.map +1 -1
  39. package/lib/codegen/parser.js +85 -3
  40. package/lib/codegen/parser.js.map +1 -1
  41. package/lib/codegen/types.js +6 -3
  42. package/lib/codegen/types.js.map +1 -1
  43. package/lib/debug.d.ts +1 -0
  44. package/lib/debug.js +51 -0
  45. package/lib/debug.js.map +1 -0
  46. package/lib/decoder/DecodeOperation.d.ts +3 -4
  47. package/lib/decoder/DecodeOperation.js +35 -17
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +5 -6
  50. package/lib/decoder/Decoder.js +10 -10
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +4 -2
  53. package/lib/decoder/ReferenceTracker.js.map +1 -1
  54. package/lib/decoder/strategy/RawChanges.js +1 -2
  55. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  56. package/lib/decoder/strategy/StateCallbacks.d.ts +44 -11
  57. package/lib/decoder/strategy/StateCallbacks.js +74 -64
  58. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  59. package/lib/encoder/ChangeTree.d.ts +28 -20
  60. package/lib/encoder/ChangeTree.js +242 -188
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +51 -65
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +8 -7
  66. package/lib/encoder/Encoder.js +128 -79
  67. package/lib/encoder/Encoder.js.map +1 -1
  68. package/lib/encoder/Root.d.ts +22 -0
  69. package/lib/encoder/Root.js +81 -0
  70. package/lib/encoder/Root.js.map +1 -0
  71. package/lib/encoder/StateView.d.ts +7 -7
  72. package/lib/encoder/StateView.js +72 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +7 -6
  75. package/lib/encoding/assert.js +13 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.d.ts +36 -19
  78. package/lib/encoding/decode.js +54 -84
  79. package/lib/encoding/decode.js.map +1 -1
  80. package/lib/encoding/encode.d.ts +36 -18
  81. package/lib/encoding/encode.js +61 -48
  82. package/lib/encoding/encode.js.map +1 -1
  83. package/lib/encoding/spec.d.ts +4 -5
  84. package/lib/encoding/spec.js +1 -2
  85. package/lib/encoding/spec.js.map +1 -1
  86. package/lib/index.d.ts +10 -9
  87. package/lib/index.js +24 -17
  88. package/lib/index.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +34 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/TypeContext.d.ts +29 -0
  92. package/lib/types/TypeContext.js +151 -0
  93. package/lib/types/TypeContext.js.map +1 -0
  94. package/lib/types/custom/ArraySchema.d.ts +2 -2
  95. package/lib/types/custom/ArraySchema.js +33 -22
  96. package/lib/types/custom/ArraySchema.js.map +1 -1
  97. package/lib/types/custom/CollectionSchema.d.ts +2 -2
  98. package/lib/types/custom/CollectionSchema.js +1 -0
  99. package/lib/types/custom/CollectionSchema.js.map +1 -1
  100. package/lib/types/custom/MapSchema.d.ts +18 -16
  101. package/lib/types/custom/MapSchema.js +12 -4
  102. package/lib/types/custom/MapSchema.js.map +1 -1
  103. package/lib/types/custom/SetSchema.d.ts +2 -2
  104. package/lib/types/custom/SetSchema.js +1 -0
  105. package/lib/types/custom/SetSchema.js.map +1 -1
  106. package/lib/types/registry.d.ts +8 -1
  107. package/lib/types/registry.js +23 -6
  108. package/lib/types/registry.js.map +1 -1
  109. package/lib/types/symbols.d.ts +8 -5
  110. package/lib/types/symbols.js +9 -6
  111. package/lib/types/symbols.js.map +1 -1
  112. package/lib/types/utils.js +1 -2
  113. package/lib/types/utils.js.map +1 -1
  114. package/lib/utils.js +9 -7
  115. package/lib/utils.js.map +1 -1
  116. package/package.json +19 -18
  117. package/src/Metadata.ts +190 -42
  118. package/src/Reflection.ts +76 -38
  119. package/src/Schema.ts +72 -70
  120. package/src/annotations.ts +156 -202
  121. package/src/bench_encode.ts +108 -0
  122. package/src/codegen/languages/csharp.ts +8 -47
  123. package/src/codegen/languages/haxe.ts +4 -0
  124. package/src/codegen/languages/lua.ts +19 -27
  125. package/src/codegen/parser.ts +107 -0
  126. package/src/codegen/types.ts +1 -0
  127. package/src/debug.ts +55 -0
  128. package/src/decoder/DecodeOperation.ts +43 -15
  129. package/src/decoder/Decoder.ts +12 -10
  130. package/src/decoder/ReferenceTracker.ts +5 -3
  131. package/src/decoder/strategy/StateCallbacks.ts +152 -81
  132. package/src/encoder/ChangeTree.ts +282 -209
  133. package/src/encoder/EncodeOperation.ts +78 -78
  134. package/src/encoder/Encoder.ts +152 -87
  135. package/src/encoder/Root.ts +93 -0
  136. package/src/encoder/StateView.ts +80 -88
  137. package/src/encoding/assert.ts +17 -8
  138. package/src/encoding/decode.ts +73 -93
  139. package/src/encoding/encode.ts +76 -45
  140. package/src/encoding/spec.ts +3 -5
  141. package/src/index.ts +12 -20
  142. package/src/types/HelperTypes.ts +54 -2
  143. package/src/types/TypeContext.ts +175 -0
  144. package/src/types/custom/ArraySchema.ts +49 -19
  145. package/src/types/custom/CollectionSchema.ts +1 -0
  146. package/src/types/custom/MapSchema.ts +30 -17
  147. package/src/types/custom/SetSchema.ts +1 -0
  148. package/src/types/registry.ts +22 -3
  149. package/src/types/symbols.ts +10 -7
  150. package/src/utils.ts +7 -3
@@ -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);
261
+ else if (!isFinite(value)) {
262
+ return number$1(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it);
263
+ }
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
+ }
237
273
  }
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
- // });
274
+ bytes[it.offset++] = 0xcb;
275
+ float64$1(bytes, value, it);
276
+ return 9;
247
277
  }
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);
274
- }
275
- this.ensureRefId();
276
- this.forEachChild((changeTree, atIndex) => {
277
- changeTree.setParent(this.ref, root, atIndex);
278
- });
279
- }
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 >= -32) {
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 >= -128) {
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 >= -32768) {
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 >= -2147483648) {
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,698 +376,1150 @@
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
- const hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength);
569
- const utf8Length = (hasBufferByteLength)
570
- ? Buffer.byteLength // node
571
- : function (str, _) {
572
- var c = 0, length = 0;
573
- for (var i = 0, l = str.length; i < l; i++) {
574
- c = str.charCodeAt(i);
575
- if (c < 0x80) {
576
- length += 1;
577
- }
578
- else if (c < 0x800) {
579
- length += 2;
580
- }
581
- else if (c < 0xd800 || c >= 0xe000) {
582
- length += 3;
583
- }
584
- else {
585
- i++;
586
- length += 4;
587
- }
588
- }
589
- return length;
590
- };
591
- function utf8Write(view, str, it) {
592
- var c = 0;
593
- for (var i = 0, l = str.length; i < l; i++) {
594
- c = str.charCodeAt(i);
595
- if (c < 0x80) {
596
- view[it.offset++] = c;
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;
597
393
  }
598
- else if (c < 0x800) {
599
- view[it.offset++] = 0xc0 | (c >> 6);
600
- view[it.offset++] = 0x80 | (c & 0x3f);
394
+ if ((byte & 0xe0) === 0xc0) {
395
+ string += String.fromCharCode(((byte & 0x1f) << 6) |
396
+ (bytes[++i] & 0x3f));
397
+ continue;
601
398
  }
602
- else if (c < 0xd800 || c >= 0xe000) {
603
- view[it.offset++] = 0xe0 | (c >> 12);
604
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
605
- view[it.offset++] = 0x80 | (c & 0x3f);
399
+ if ((byte & 0xf0) === 0xe0) {
400
+ string += String.fromCharCode(((byte & 0x0f) << 12) |
401
+ ((bytes[++i] & 0x3f) << 6) |
402
+ ((bytes[++i] & 0x3f) << 0));
403
+ continue;
606
404
  }
607
- else {
608
- i++;
609
- c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
610
- view[it.offset++] = 0xf0 | (c >> 18);
611
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
612
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
613
- 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;
614
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));
615
422
  }
423
+ it.offset += length;
424
+ return string;
616
425
  }
617
- function int8$1(bytes, value, it) {
618
- bytes[it.offset++] = value & 255;
426
+ function int8(bytes, it) {
427
+ return uint8(bytes, it) << 24 >> 24;
619
428
  }
620
- function uint8$1(bytes, value, it) {
621
- bytes[it.offset++] = value & 255;
429
+ function uint8(bytes, it) {
430
+ return bytes[it.offset++];
622
431
  }
623
- function int16$1(bytes, value, it) {
624
- bytes[it.offset++] = value & 255;
625
- bytes[it.offset++] = (value >> 8) & 255;
432
+ function int16(bytes, it) {
433
+ return uint16(bytes, it) << 16 >> 16;
626
434
  }
627
- function uint16$1(bytes, value, it) {
628
- bytes[it.offset++] = value & 255;
629
- bytes[it.offset++] = (value >> 8) & 255;
435
+ function uint16(bytes, it) {
436
+ return bytes[it.offset++] | bytes[it.offset++] << 8;
630
437
  }
631
- function int32$1(bytes, value, it) {
632
- bytes[it.offset++] = value & 255;
633
- bytes[it.offset++] = (value >> 8) & 255;
634
- bytes[it.offset++] = (value >> 16) & 255;
635
- 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;
636
440
  }
637
- function uint32$1(bytes, value, it) {
638
- const b4 = value >> 24;
639
- const b3 = value >> 16;
640
- const b2 = value >> 8;
641
- const b1 = value;
642
- bytes[it.offset++] = b1 & 255;
643
- bytes[it.offset++] = b2 & 255;
644
- bytes[it.offset++] = b3 & 255;
645
- bytes[it.offset++] = b4 & 255;
441
+ function uint32(bytes, it) {
442
+ return int32(bytes, it) >>> 0;
646
443
  }
647
- function int64$1(bytes, value, it) {
648
- const high = Math.floor(value / Math.pow(2, 32));
649
- const low = value >>> 0;
650
- uint32$1(bytes, low, it);
651
- uint32$1(bytes, high, it);
444
+ function float32(bytes, it) {
445
+ _int32[0] = int32(bytes, it);
446
+ return _float32[0];
652
447
  }
653
- function uint64$1(bytes, value, it) {
654
- const high = (value / Math.pow(2, 32)) >> 0;
655
- const low = value >>> 0;
656
- uint32$1(bytes, low, it);
657
- 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];
658
452
  }
659
- function float32$1(bytes, value, it) {
660
- 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;
661
457
  }
662
- function float64$1(bytes, value, it) {
663
- 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;
664
462
  }
665
- const _int32$1 = new Int32Array(2);
666
- const _float32$1 = new Float32Array(_int32$1.buffer);
667
- const _float64$1 = new Float64Array(_int32$1.buffer);
668
- function writeFloat32(bytes, value, it) {
669
- _float32$1[0] = value;
670
- 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];
671
467
  }
672
- function writeFloat64(bytes, value, it) {
673
- _float64$1[0] = value;
674
- int32$1(bytes, _int32$1[0 ], it);
675
- 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];
676
472
  }
677
- function boolean$1(bytes, value, it) {
678
- bytes[it.offset++] = value ? 1 : 0; // uint8
473
+ function boolean(bytes, it) {
474
+ return uint8(bytes, it) > 0;
679
475
  }
680
- function string$1(bytes, value, it) {
681
- // encode `null` strings as empty.
682
- if (!value) {
683
- value = "";
684
- }
685
- let length = utf8Length(value, "utf8");
686
- let size = 0;
687
- // fixstr
688
- if (length < 0x20) {
689
- bytes[it.offset++] = length | 0xa0;
690
- size = 1;
691
- }
692
- // str 8
693
- else if (length < 0x100) {
694
- bytes[it.offset++] = 0xd9;
695
- bytes[it.offset++] = length % 255;
696
- 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;
697
482
  }
698
- // str 16
699
- else if (length < 0x10000) {
700
- bytes[it.offset++] = 0xda;
701
- uint16$1(bytes, length, it);
702
- size = 3;
483
+ else if (prefix === 0xd9) {
484
+ length = uint8(bytes, it);
703
485
  }
704
- // str 32
705
- else if (length < 0x100000000) {
706
- bytes[it.offset++] = 0xdb;
707
- uint32$1(bytes, length, it);
708
- size = 5;
486
+ else if (prefix === 0xda) {
487
+ length = uint16(bytes, it);
709
488
  }
710
- else {
711
- throw new Error('String too long');
489
+ else if (prefix === 0xdb) {
490
+ length = uint32(bytes, it);
712
491
  }
713
- utf8Write(bytes, value, it);
714
- return size + length;
492
+ return utf8Read(bytes, it, length);
715
493
  }
716
- function number$1(bytes, value, it) {
717
- if (isNaN(value)) {
718
- 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;
719
499
  }
720
- else if (!isFinite(value)) {
721
- 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);
722
503
  }
723
- else if (value !== (value | 0)) {
724
- bytes[it.offset++] = 0xcb;
725
- writeFloat64(bytes, value, it);
726
- return 9;
727
- // TODO: encode float 32?
728
- // is it possible to differentiate between float32 / float64 here?
729
- // // float 32
730
- // bytes.push(0xca);
731
- // writeFloat32(bytes, value);
732
- // return 5;
504
+ else if (prefix === 0xcb) {
505
+ // float 64
506
+ return float64(bytes, it);
733
507
  }
734
- if (value >= 0) {
735
- // positive fixnum
736
- if (value < 0x80) {
737
- bytes[it.offset++] = value & 255; // uint8
738
- return 1;
739
- }
508
+ else if (prefix === 0xcc) {
740
509
  // uint 8
741
- if (value < 0x100) {
742
- bytes[it.offset++] = 0xcc;
743
- bytes[it.offset++] = value & 255; // uint8
744
- return 2;
745
- }
510
+ return uint8(bytes, it);
511
+ }
512
+ else if (prefix === 0xcd) {
746
513
  // uint 16
747
- if (value < 0x10000) {
748
- bytes[it.offset++] = 0xcd;
749
- uint16$1(bytes, value, it);
750
- return 3;
751
- }
514
+ return uint16(bytes, it);
515
+ }
516
+ else if (prefix === 0xce) {
752
517
  // uint 32
753
- if (value < 0x100000000) {
754
- bytes[it.offset++] = 0xce;
755
- uint32$1(bytes, value, it);
756
- return 5;
757
- }
518
+ return uint32(bytes, it);
519
+ }
520
+ else if (prefix === 0xcf) {
758
521
  // uint 64
759
- bytes[it.offset++] = 0xcf;
760
- uint64$1(bytes, value, it);
761
- return 9;
522
+ return uint64(bytes, it);
762
523
  }
763
- else {
764
- // negative fixnum
765
- if (value >= -0x20) {
766
- bytes[it.offset++] = 0xe0 | (value + 0x20);
767
- return 1;
768
- }
524
+ else if (prefix === 0xd0) {
769
525
  // int 8
770
- if (value >= -0x80) {
771
- bytes[it.offset++] = 0xd0;
772
- int8$1(bytes, value, it);
773
- return 2;
774
- }
526
+ return int8(bytes, it);
527
+ }
528
+ else if (prefix === 0xd1) {
775
529
  // int 16
776
- if (value >= -0x8000) {
777
- bytes[it.offset++] = 0xd1;
778
- int16$1(bytes, value, it);
779
- return 3;
780
- }
530
+ return int16(bytes, it);
531
+ }
532
+ else if (prefix === 0xd2) {
781
533
  // int 32
782
- if (value >= -0x80000000) {
783
- bytes[it.offset++] = 0xd2;
784
- int32$1(bytes, value, it);
785
- return 5;
786
- }
787
- // int 64
788
- bytes[it.offset++] = 0xd3;
789
- int64$1(bytes, value, it);
790
- return 9;
534
+ return int32(bytes, it);
791
535
  }
792
- }
793
-
794
- var encode = /*#__PURE__*/Object.freeze({
795
- __proto__: null,
796
- utf8Length: utf8Length,
797
- utf8Write: utf8Write,
798
- int8: int8$1,
799
- uint8: uint8$1,
800
- int16: int16$1,
801
- uint16: uint16$1,
802
- int32: int32$1,
803
- uint32: uint32$1,
804
- int64: int64$1,
805
- uint64: uint64$1,
806
- float32: float32$1,
807
- float64: float64$1,
808
- writeFloat32: writeFloat32,
809
- writeFloat64: writeFloat64,
810
- boolean: boolean$1,
811
- string: string$1,
812
- number: number$1
813
- });
814
-
815
- class EncodeSchemaError extends Error {
816
- }
817
- function assertType(value, type, klass, field) {
818
- let typeofTarget;
819
- let allowNull = false;
820
- switch (type) {
821
- case "number":
822
- case "int8":
823
- case "uint8":
824
- case "int16":
825
- case "uint16":
826
- case "int32":
827
- case "uint32":
828
- case "int64":
829
- case "uint64":
830
- case "float32":
831
- case "float64":
832
- typeofTarget = "number";
833
- if (isNaN(value)) {
834
- console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
835
- }
836
- break;
837
- case "string":
838
- typeofTarget = "string";
839
- allowNull = true;
840
- break;
841
- case "boolean":
842
- // boolean is always encoded as true/false based on truthiness
843
- return;
536
+ else if (prefix === 0xd3) {
537
+ // int 64
538
+ return int64(bytes, it);
844
539
  }
845
- if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
846
- let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
847
- throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
540
+ else if (prefix > 0xdf) {
541
+ // negative fixint
542
+ return (0xff - prefix + 1) * -1;
848
543
  }
849
544
  }
850
- function assertInstanceType(value, type, klass, field) {
851
- if (!(value instanceof type)) {
852
- throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${klass.constructor.name}#${field}`);
853
- }
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);
854
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
+ };
855
576
 
856
- function encodePrimitiveType(type, bytes, value, klass, field, it) {
857
- assertType(value, type, klass, field);
858
- const encodeFunc = encode[type];
859
- if (encodeFunc) {
860
- encodeFunc(bytes, value, it);
861
- // encodeFunc(bytes, value);
862
- }
863
- else {
864
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
865
- }
866
- }
867
- function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
868
- if (type[Symbol.metadata] !== undefined) {
869
- // TODO: move this to the `@type()` annotation
870
- assertInstanceType(value, type, ref, field);
871
- //
872
- // Encode refId for this instance.
873
- // The actual instance is going to be encoded on next `changeTree` iteration.
874
- //
875
- number$1(bytes, value[$changes].refId, it);
876
- // Try to encode inherited TYPE_ID if it's an ADD operation.
877
- if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
878
- encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
879
- }
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;
880
583
  }
881
- else if (typeof (type) === "string") {
882
- //
883
- // Primitive values
884
- //
885
- encodePrimitiveType(type, bytes, value, ref, field, it);
584
+ if (definition.encode) {
585
+ encode[identifier] = definition.encode;
886
586
  }
887
- else {
888
- //
889
- // Custom type (MapSchema, ArraySchema, etc)
890
- //
891
- const definition = getType(Object.keys(type)[0]);
892
- //
893
- // ensure a ArraySchema has been provided
894
- //
895
- assertInstanceType(ref[field], definition.constructor, ref, field);
896
- //
897
- // Encode refId for this instance.
898
- // The actual instance is going to be encoded on next `changeTree` iteration.
899
- //
900
- number$1(bytes, value[$changes].refId, it);
587
+ if (definition.decode) {
588
+ decode[identifier] = definition.decode;
901
589
  }
902
590
  }
903
- /**
904
- * Used for Schema instances.
905
- * @private
906
- */
907
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
908
- const ref = changeTree.ref;
909
- const metadata = ref['constructor'][Symbol.metadata];
910
- const field = metadata[index];
911
- const type = metadata[field].type;
912
- const value = ref[field];
913
- // "compress" field index + operation
914
- bytes[it.offset++] = (index | operation) & 255;
915
- // Do not encode value for DELETE operations
916
- if (operation === exports.OPERATION.DELETE) {
917
- return;
918
- }
919
- // TODO: inline this function call small performance gain
920
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
921
- };
922
- /**
923
- * Used for collections (MapSchema, CollectionSchema, SetSchema)
924
- * @private
925
- */
926
- const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, operation, it) {
927
- const ref = changeTree.ref;
928
- // encode operation
929
- bytes[it.offset++] = operation & 255;
930
- // custom operations
931
- if (operation === exports.OPERATION.CLEAR) {
932
- return;
591
+ function getType(identifier) {
592
+ return registeredTypes[identifier];
593
+ }
594
+ function defineCustomTypes(types) {
595
+ for (const identifier in types) {
596
+ registerType(identifier, types[identifier]);
933
597
  }
934
- // encode index
935
- number$1(bytes, field, it);
936
- // Do not encode value for DELETE operations
937
- if (operation === exports.OPERATION.DELETE) {
938
- return;
598
+ return (t) => type(t);
599
+ }
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
+ }
939
617
  }
940
- //
941
- // encode "alias" for dynamic fields (maps)
942
- //
943
- if ((operation & exports.OPERATION.ADD) == exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
944
- 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) {
945
624
  //
946
- // MapSchema dynamic key
625
+ // TODO:
626
+ // cache "discoverTypes" results for each rootClass
627
+ // to avoid re-discovering types for each new context/room
947
628
  //
948
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
949
- string$1(bytes, dynamicIndex, it);
629
+ this.discoverTypes(rootClass);
950
630
  }
951
631
  }
952
- const type = changeTree.getType(field);
953
- const value = changeTree.getValue(field);
954
- // TODO: inline this function call small performance gain
955
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
956
- };
957
- /**
958
- * Used for collections (MapSchema, ArraySchema, etc.)
959
- * @private
960
- */
961
- const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
962
- const ref = changeTree.ref;
963
- if (hasView &&
964
- operation === exports.OPERATION.DELETE &&
965
- typeof (changeTree.getType(field)) !== "string") {
966
- // encode delete by refId (array of schemas)
967
- bytes[it.offset++] = exports.OPERATION.DELETE_BY_REFID;
968
- const value = ref['tmpItems'][field];
969
- const refId = value[$changes].refId;
970
- number$1(bytes, refId, it);
971
- return;
972
- }
973
- // encode operation
974
- bytes[it.offset++] = operation & 255;
975
- // custom operations
976
- if (operation === exports.OPERATION.CLEAR) {
977
- return;
632
+ has(schema) {
633
+ return this.schemas.has(schema);
978
634
  }
979
- // encode index
980
- number$1(bytes, field, it);
981
- // Do not encode value for DELETE operations
982
- if (operation === exports.OPERATION.DELETE) {
983
- return;
635
+ get(typeid) {
636
+ return this.types[typeid];
984
637
  }
985
- const type = changeTree.getType(field);
986
- const value = changeTree.getValue(field, isEncodeAll);
987
- // console.log("encodeArray -> ", {
988
- // ref: changeTree.ref.constructor.name,
989
- // field,
990
- // operation: OPERATION[operation],
991
- // value: value?.toJSON(),
992
- // items: ref.toJSON(),
993
- // });
994
- // TODO: inline this function call small performance gain
995
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
996
- };
997
-
998
- /**
999
- * Copyright (c) 2018 Endel Dreyer
1000
- * Copyright (c) 2014 Ion Drive Software Ltd.
1001
- *
1002
- * Permission is hereby granted, free of charge, to any person obtaining a copy
1003
- * of this software and associated documentation files (the "Software"), to deal
1004
- * in the Software without restriction, including without limitation the rights
1005
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1006
- * copies of the Software, and to permit persons to whom the Software is
1007
- * furnished to do so, subject to the following conditions:
1008
- *
1009
- * The above copyright notice and this permission notice shall be included in all
1010
- * copies or substantial portions of the Software.
1011
- *
1012
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1013
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1014
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1015
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1016
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1017
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1018
- * SOFTWARE
1019
- */
1020
- function utf8Read(bytes, it, length) {
1021
- var string = '', chr = 0;
1022
- for (var i = it.offset, end = it.offset + length; i < end; i++) {
1023
- var byte = bytes[i];
1024
- if ((byte & 0x80) === 0x00) {
1025
- string += String.fromCharCode(byte);
1026
- continue;
638
+ add(schema, typeid = this.schemas.size) {
639
+ // skip if already registered
640
+ if (this.schemas.has(schema)) {
641
+ return false;
1027
642
  }
1028
- if ((byte & 0xe0) === 0xc0) {
1029
- string += String.fromCharCode(((byte & 0x1f) << 6) |
1030
- (bytes[++i] & 0x3f));
1031
- continue;
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);
1032
649
  }
1033
- if ((byte & 0xf0) === 0xe0) {
1034
- string += String.fromCharCode(((byte & 0x0f) << 12) |
1035
- ((bytes[++i] & 0x3f) << 6) |
1036
- ((bytes[++i] & 0x3f) << 0));
1037
- continue;
650
+ this.schemas.set(schema, typeid);
651
+ return true;
652
+ }
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);
1038
659
  }
1039
- if ((byte & 0xf8) === 0xf0) {
1040
- chr = ((byte & 0x07) << 18) |
1041
- ((bytes[++i] & 0x3f) << 12) |
1042
- ((bytes[++i] & 0x3f) << 6) |
1043
- ((bytes[++i] & 0x3f) << 0);
1044
- if (chr >= 0x010000) { // surrogate pair
1045
- chr -= 0x010000;
1046
- string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
1047
- }
1048
- else {
1049
- string += String.fromCharCode(chr);
1050
- }
1051
- continue;
660
+ // skip if already registered
661
+ if (!this.add(klass)) {
662
+ return;
663
+ }
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;
680
+ }
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);
698
+ }
699
+ else {
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);
706
+ }
1052
707
  }
1053
- console.error('Invalid byte ' + byte.toString(16));
1054
- // (do not throw error to avoid server/client from crashing due to hack attemps)
1055
- // throw new Error('Invalid byte ' + byte.toString(16));
1056
708
  }
1057
- it.offset += length;
1058
- return string;
1059
- }
1060
- function int8(bytes, it) {
1061
- return uint8(bytes, it) << 24 >> 24;
1062
- }
1063
- function uint8(bytes, it) {
1064
- return bytes[it.offset++];
1065
- }
1066
- function int16(bytes, it) {
1067
- return uint16(bytes, it) << 16 >> 16;
1068
- }
1069
- function uint16(bytes, it) {
1070
- return bytes[it.offset++] | bytes[it.offset++] << 8;
1071
- }
1072
- function int32(bytes, it) {
1073
- return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24;
1074
- }
1075
- function uint32(bytes, it) {
1076
- return int32(bytes, it) >>> 0;
1077
- }
1078
- function float32(bytes, it) {
1079
- return readFloat32(bytes, it);
1080
- }
1081
- function float64(bytes, it) {
1082
- return readFloat64(bytes, it);
1083
- }
1084
- function int64(bytes, it) {
1085
- const low = uint32(bytes, it);
1086
- const high = int32(bytes, it) * Math.pow(2, 32);
1087
- 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
+ }
1088
743
  }
1089
- function uint64(bytes, it) {
1090
- const low = uint32(bytes, it);
1091
- const high = uint32(bytes, it) * Math.pow(2, 32);
1092
- 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;
1093
751
  }
1094
- const _int32 = new Int32Array(2);
1095
- const _float32 = new Float32Array(_int32.buffer);
1096
- const _float64 = new Float64Array(_int32.buffer);
1097
- function readFloat32(bytes, it) {
1098
- _int32[0] = int32(bytes, it);
1099
- 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
+ }
1100
972
  }
1101
- function readFloat64(bytes, it) {
1102
- _int32[0 ] = int32(bytes, it);
1103
- _int32[1 ] = int32(bytes, it);
1104
- 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];
1105
979
  }
1106
- function boolean(bytes, it) {
1107
- 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
+ }
1108
984
  }
1109
- function string(bytes, it) {
1110
- const prefix = bytes[it.offset++];
1111
- let length;
1112
- if (prefix < 0xc0) {
1113
- // fixstr
1114
- 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
+ }
1115
1014
  }
1116
- else if (prefix === 0xd9) {
1117
- 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
+ }
1118
1033
  }
1119
- else if (prefix === 0xda) {
1120
- 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
+ }
1121
1064
  }
1122
- else if (prefix === 0xdb) {
1123
- 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
+ }
1124
1085
  }
1125
- return utf8Read(bytes, it, length);
1126
- }
1127
- function stringCheck(bytes, it) {
1128
- const prefix = bytes[it.offset];
1129
- return (
1130
- // fixstr
1131
- (prefix < 0xc0 && prefix > 0xa0) ||
1132
- // str 8
1133
- prefix === 0xd9 ||
1134
- // str 16
1135
- prefix === 0xda ||
1136
- // str 32
1137
- prefix === 0xdb);
1138
- }
1139
- function number(bytes, it) {
1140
- const prefix = bytes[it.offset++];
1141
- if (prefix < 0x80) {
1142
- // positive fixint
1143
- 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');
1144
1091
  }
1145
- else if (prefix === 0xca) {
1146
- // float 32
1147
- 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);
1148
1395
  }
1149
- else if (prefix === 0xcb) {
1150
- // float 64
1151
- 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
+ }
1152
1406
  }
1153
- else if (prefix === 0xcc) {
1154
- // uint 8
1155
- 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);
1156
1413
  }
1157
- else if (prefix === 0xcd) {
1158
- // uint 16
1159
- 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;
1160
1425
  }
1161
- else if (prefix === 0xce) {
1162
- // uint 32
1163
- 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;
1164
1441
  }
1165
- else if (prefix === 0xcf) {
1166
- // uint 64
1167
- 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;
1168
1447
  }
1169
- else if (prefix === 0xd0) {
1170
- // int 8
1171
- 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
+ }
1172
1460
  }
1173
- else if (prefix === 0xd1) {
1174
- // int 16
1175
- 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
+ }
1176
1494
  }
1177
- else if (prefix === 0xd2) {
1178
- // int 32
1179
- return int32(bytes, it);
1495
+ else {
1496
+ refOrIndex = field;
1180
1497
  }
1181
- else if (prefix === 0xd3) {
1182
- // int 64
1183
- 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;
1184
1504
  }
1185
- else if (prefix > 0xdf) {
1186
- // negative fixint
1187
- 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;
1188
1510
  }
1189
- }
1190
- function numberCheck(bytes, it) {
1191
- const prefix = bytes[it.offset];
1192
- // positive fixint - 0x00 - 0x7f
1193
- // float 32 - 0xca
1194
- // float 64 - 0xcb
1195
- // uint 8 - 0xcc
1196
- // uint 16 - 0xcd
1197
- // uint 32 - 0xce
1198
- // uint 64 - 0xcf
1199
- // int 8 - 0xd0
1200
- // int 16 - 0xd1
1201
- // int 32 - 0xd2
1202
- // int 64 - 0xd3
1203
- return (prefix < 0x80 ||
1204
- (prefix >= 0xca && prefix <= 0xd3));
1205
- }
1206
- function arrayCheck(bytes, it) {
1207
- return bytes[it.offset] < 0xa0;
1208
- // const prefix = bytes[it.offset] ;
1209
- // if (prefix < 0xa0) {
1210
- // return prefix;
1211
- // // array
1212
- // } else if (prefix === 0xdc) {
1213
- // it.offset += 2;
1214
- // } else if (0xdd) {
1215
- // it.offset += 4;
1216
- // }
1217
- // return prefix;
1218
- }
1219
- function switchStructureCheck(bytes, it) {
1220
- return (
1221
- // previous byte should be `SWITCH_TO_STRUCTURE`
1222
- bytes[it.offset - 1] === SWITCH_TO_STRUCTURE &&
1223
- // next byte should be a number
1224
- (bytes[it.offset] < 0x80 || (bytes[it.offset] >= 0xca && bytes[it.offset] <= 0xd3)));
1225
- }
1226
-
1227
- var decode = /*#__PURE__*/Object.freeze({
1228
- __proto__: null,
1229
- utf8Read: utf8Read,
1230
- int8: int8,
1231
- uint8: uint8,
1232
- int16: int16,
1233
- uint16: uint16,
1234
- int32: int32,
1235
- uint32: uint32,
1236
- float32: float32,
1237
- float64: float64,
1238
- int64: int64,
1239
- uint64: uint64,
1240
- readFloat32: readFloat32,
1241
- readFloat64: readFloat64,
1242
- boolean: boolean,
1243
- string: string,
1244
- stringCheck: stringCheck,
1245
- number: number,
1246
- numberCheck: numberCheck,
1247
- arrayCheck: arrayCheck,
1248
- switchStructureCheck: switchStructureCheck
1249
- });
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
+ };
1250
1523
 
1251
1524
  const DEFINITION_MISMATCH = -1;
1252
1525
  function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges) {
@@ -1282,7 +1555,7 @@
1282
1555
  }
1283
1556
  if (operation === exports.OPERATION.DELETE) ;
1284
1557
  else if (Schema.is(type)) {
1285
- const refId = number(bytes, it);
1558
+ const refId = decode.number(bytes, it);
1286
1559
  value = $root.refs.get(refId);
1287
1560
  if (previousValue) {
1288
1561
  const previousRefId = $root.refIds.get(previousValue);
@@ -1298,7 +1571,9 @@
1298
1571
  if (!value) {
1299
1572
  value = decoder.createInstanceOfType(childType);
1300
1573
  }
1301
- $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
+ ));
1302
1577
  }
1303
1578
  }
1304
1579
  else if (typeof (type) === "string") {
@@ -1309,7 +1584,7 @@
1309
1584
  }
1310
1585
  else {
1311
1586
  const typeDef = getType(Object.keys(type)[0]);
1312
- const refId = number(bytes, it);
1587
+ const refId = decode.number(bytes, it);
1313
1588
  const valueRef = ($root.refs.has(refId))
1314
1589
  ? previousValue || $root.refs.get(refId)
1315
1590
  : new typeDef.constructor();
@@ -1349,18 +1624,19 @@
1349
1624
  }
1350
1625
  const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1351
1626
  const first_byte = bytes[it.offset++];
1352
- const metadata = ref['constructor'][Symbol.metadata];
1627
+ const metadata = ref.constructor[Symbol.metadata];
1353
1628
  // "compressed" index + operation
1354
1629
  const operation = (first_byte >> 6) << 6;
1355
1630
  const index = first_byte % (operation || 255);
1356
1631
  // skip early if field is not defined
1357
1632
  const field = metadata[index];
1358
1633
  if (field === undefined) {
1634
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1359
1635
  return DEFINITION_MISMATCH;
1360
1636
  }
1361
- 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);
1362
1638
  if (value !== null && value !== undefined) {
1363
- ref[field] = value;
1639
+ ref[field.name] = value;
1364
1640
  }
1365
1641
  // add change
1366
1642
  if (previousValue !== value) {
@@ -1368,7 +1644,7 @@
1368
1644
  ref,
1369
1645
  refId: decoder.currentRefId,
1370
1646
  op: operation,
1371
- field: field,
1647
+ field: field.name,
1372
1648
  value,
1373
1649
  previousValue,
1374
1650
  });
@@ -1387,12 +1663,12 @@
1387
1663
  ref.clear();
1388
1664
  return;
1389
1665
  }
1390
- const index = number(bytes, it);
1666
+ const index = decode.number(bytes, it);
1391
1667
  const type = ref[$childType];
1392
1668
  let dynamicIndex;
1393
1669
  if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
1394
1670
  if (typeof (ref['set']) === "function") {
1395
- dynamicIndex = string(bytes, it); // MapSchema
1671
+ dynamicIndex = decode.string(bytes, it); // MapSchema
1396
1672
  ref['setIndex'](index, dynamicIndex);
1397
1673
  }
1398
1674
  else {
@@ -1436,7 +1712,8 @@
1436
1712
  };
1437
1713
  const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1438
1714
  // "uncompressed" index + operation (array/map items)
1439
- const operation = bytes[it.offset++];
1715
+ let operation = bytes[it.offset++];
1716
+ let index;
1440
1717
  if (operation === exports.OPERATION.CLEAR) {
1441
1718
  //
1442
1719
  // When decoding:
@@ -1447,11 +1724,15 @@
1447
1724
  ref.clear();
1448
1725
  return;
1449
1726
  }
1727
+ else if (operation === exports.OPERATION.REVERSE) {
1728
+ ref.reverse();
1729
+ return;
1730
+ }
1450
1731
  else if (operation === exports.OPERATION.DELETE_BY_REFID) {
1451
1732
  // TODO: refactor here, try to follow same flow as below
1452
- const refId = number(bytes, it);
1733
+ const refId = decode.number(bytes, it);
1453
1734
  const previousValue = decoder.root.refs.get(refId);
1454
- const index = ref.findIndex((value) => value === previousValue);
1735
+ index = ref.findIndex((value) => value === previousValue);
1455
1736
  ref[$deleteByIndex](index);
1456
1737
  allChanges.push({
1457
1738
  ref,
@@ -1464,7 +1745,17 @@
1464
1745
  });
1465
1746
  return;
1466
1747
  }
1467
- 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
+ }
1468
1759
  const type = ref[$childType];
1469
1760
  let dynamicIndex = index;
1470
1761
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges);
@@ -1488,6 +1779,55 @@
1488
1779
  }
1489
1780
  };
1490
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
+
1491
1831
  var _a$4, _b$4;
1492
1832
  const DEFAULT_SORT = (a, b) => {
1493
1833
  const A = a.toString();
@@ -1538,6 +1878,7 @@
1538
1878
  const proxy = new Proxy(this, {
1539
1879
  get: (obj, prop) => {
1540
1880
  if (typeof (prop) !== "symbol" &&
1881
+ // FIXME: d8 accuses this as low performance
1541
1882
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1542
1883
  ) {
1543
1884
  return this.items[prop];
@@ -1553,8 +1894,9 @@
1553
1894
  }
1554
1895
  else {
1555
1896
  if (setValue[$changes]) {
1897
+ assertInstanceType(setValue, obj[$childType], obj, key);
1556
1898
  if (obj.items[key] !== undefined) {
1557
- if (setValue[$changes][$isNew]) {
1899
+ if (setValue[$changes].isNew) {
1558
1900
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1559
1901
  }
1560
1902
  else {
@@ -1566,7 +1908,7 @@
1566
1908
  }
1567
1909
  }
1568
1910
  }
1569
- else if (setValue[$changes][$isNew]) {
1911
+ else if (setValue[$changes].isNew) {
1570
1912
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1571
1913
  }
1572
1914
  }
@@ -1599,7 +1941,10 @@
1599
1941
  }
1600
1942
  });
1601
1943
  this[$changes] = new ChangeTree(proxy);
1602
- this.push.apply(this, items);
1944
+ this[$changes].indexes = {};
1945
+ if (items.length > 0) {
1946
+ this.push(...items);
1947
+ }
1603
1948
  return proxy;
1604
1949
  }
1605
1950
  set length(newLength) {
@@ -1618,14 +1963,19 @@
1618
1963
  }
1619
1964
  push(...values) {
1620
1965
  let length = this.tmpItems.length;
1621
- values.forEach((value, i) => {
1622
- // 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];
1623
1970
  if (value === undefined || value === null) {
1971
+ // skip null values
1624
1972
  return;
1625
1973
  }
1626
- 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
+ }
1627
1978
  changeTree.indexedOperation(length, exports.OPERATION.ADD, this.items.length);
1628
- // changeTree.indexes[length] = length;
1629
1979
  this.items.push(value);
1630
1980
  this.tmpItems.push(value);
1631
1981
  //
@@ -1633,8 +1983,9 @@
1633
1983
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1634
1984
  //
1635
1985
  value[$changes]?.setParent(this, changeTree.root, length);
1636
- length++;
1637
- });
1986
+ }
1987
+ // length++;
1988
+ // });
1638
1989
  return length;
1639
1990
  }
1640
1991
  /**
@@ -1655,6 +2006,7 @@
1655
2006
  }
1656
2007
  this[$changes].delete(index, undefined, this.items.length - 1);
1657
2008
  // this.tmpItems[index] = undefined;
2009
+ // this.tmpItems.pop();
1658
2010
  this.deletedIndexes[index] = true;
1659
2011
  return this.items.pop();
1660
2012
  }
@@ -1719,9 +2071,12 @@
1719
2071
  //
1720
2072
  // TODO: do not use [$changes] at decoding time.
1721
2073
  //
1722
- changeTree.root?.changes.delete(changeTree);
1723
- changeTree.root?.allChanges.delete(changeTree);
1724
- 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
+ }
1725
2080
  });
1726
2081
  changeTree.discard(true);
1727
2082
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -1765,6 +2120,7 @@
1765
2120
  const changeTree = this[$changes];
1766
2121
  changeTree.delete(index);
1767
2122
  changeTree.shiftAllChangeIndexes(-1, index);
2123
+ // this.deletedIndexes[index] = true;
1768
2124
  return this.items.shift();
1769
2125
  }
1770
2126
  /**
@@ -1845,10 +2201,12 @@
1845
2201
  changeTree.shiftChangeIndexes(items.length);
1846
2202
  // new index
1847
2203
  if (changeTree.isFiltered) {
1848
- changeTree.filteredChanges.set(this.items.length, exports.OPERATION.ADD);
2204
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2205
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1849
2206
  }
1850
2207
  else {
1851
- changeTree.allChanges.set(this.items.length, exports.OPERATION.ADD);
2208
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2209
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1852
2210
  }
1853
2211
  // FIXME: should we use OPERATION.MOVE here instead?
1854
2212
  items.forEach((_, index) => {
@@ -1873,14 +2231,6 @@
1873
2231
  lastIndexOf(searchElement, fromIndex = this.length - 1) {
1874
2232
  return this.items.lastIndexOf(searchElement, fromIndex);
1875
2233
  }
1876
- /**
1877
- * Determines whether all the members of an array satisfy the specified test.
1878
- * @param callbackfn A function that accepts up to three arguments. The every method calls
1879
- * the callbackfn function for each element in the array until the callbackfn returns a value
1880
- * which is coercible to the Boolean value false, or until the end of the array.
1881
- * @param thisArg An object to which the this keyword can refer in the callbackfn function.
1882
- * If thisArg is omitted, undefined is used as the this value.
1883
- */
1884
2234
  every(callbackfn, thisArg) {
1885
2235
  return this.items.every(callbackfn, thisArg);
1886
2236
  }
@@ -2159,6 +2509,7 @@
2159
2509
  this.$items = new Map();
2160
2510
  this.$indexes = new Map();
2161
2511
  this[$changes] = new ChangeTree(this);
2512
+ this[$changes].indexes = {};
2162
2513
  if (initialValues) {
2163
2514
  if (initialValues instanceof Map ||
2164
2515
  initialValues instanceof MapSchema) {
@@ -2185,6 +2536,9 @@
2185
2536
  if (value === undefined || value === null) {
2186
2537
  throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
2187
2538
  }
2539
+ else if (typeof (value) === "object" && this[$childType]) {
2540
+ assertInstanceType(value, this[$childType], this, key);
2541
+ }
2188
2542
  // Force "key" as string
2189
2543
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2190
2544
  key = key.toString();
@@ -2193,7 +2547,7 @@
2193
2547
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2194
2548
  const index = (isReplace)
2195
2549
  ? changeTree.indexes[key]
2196
- : changeTree.indexes[-1] ?? 0;
2550
+ : changeTree.indexes[$numFields] ?? 0;
2197
2551
  let operation = (isReplace)
2198
2552
  ? exports.OPERATION.REPLACE
2199
2553
  : exports.OPERATION.ADD;
@@ -2205,7 +2559,7 @@
2205
2559
  if (!isReplace) {
2206
2560
  this.$indexes.set(index, key);
2207
2561
  changeTree.indexes[key] = index;
2208
- changeTree.indexes[-1] = index + 1;
2562
+ changeTree.indexes[$numFields] = index + 1;
2209
2563
  }
2210
2564
  else if (!isRef &&
2211
2565
  this.$items.get(key) === value) {
@@ -2280,8 +2634,11 @@
2280
2634
  }
2281
2635
  [$onEncodeEnd]() {
2282
2636
  const changeTree = this[$changes];
2283
- const changes = changeTree.changes.entries();
2284
- 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];
2285
2642
  if (operation === exports.OPERATION.DELETE) {
2286
2643
  const index = this[$getByIndex](fieldIndex);
2287
2644
  delete changeTree.indexes[index];
@@ -2310,108 +2667,21 @@
2310
2667
  else {
2311
2668
  // server-side
2312
2669
  cloned = new MapSchema();
2313
- this.forEach((value, key) => {
2314
- if (value[$changes]) {
2315
- cloned.set(key, value['clone']());
2316
- }
2317
- else {
2318
- cloned.set(key, value);
2319
- }
2320
- });
2321
- }
2322
- return cloned;
2323
- }
2324
- }
2325
- registerType("map", { constructor: MapSchema });
2326
-
2327
- const DEFAULT_VIEW_TAG = -1;
2328
- class TypeContext {
2329
- /**
2330
- * For inheritance support
2331
- * Keeps track of which classes extends which. (parent -> children)
2332
- */
2333
- static { this.inheritedTypes = new Map(); }
2334
- static register(target) {
2335
- const parent = Object.getPrototypeOf(target);
2336
- if (parent !== Schema) {
2337
- let inherits = TypeContext.inheritedTypes.get(parent);
2338
- if (!inherits) {
2339
- inherits = new Set();
2340
- TypeContext.inheritedTypes.set(parent, inherits);
2341
- }
2342
- inherits.add(target);
2343
- }
2344
- }
2345
- constructor(rootClass) {
2346
- this.types = {};
2347
- this.schemas = new Map();
2348
- this.hasFilters = false;
2349
- if (rootClass) {
2350
- this.discoverTypes(rootClass);
2351
- }
2352
- }
2353
- has(schema) {
2354
- return this.schemas.has(schema);
2355
- }
2356
- get(typeid) {
2357
- return this.types[typeid];
2358
- }
2359
- add(schema, typeid = this.schemas.size) {
2360
- // skip if already registered
2361
- if (this.schemas.has(schema)) {
2362
- return false;
2363
- }
2364
- this.types[typeid] = schema;
2365
- this.schemas.set(schema, typeid);
2366
- return true;
2367
- }
2368
- getTypeId(klass) {
2369
- return this.schemas.get(klass);
2370
- }
2371
- discoverTypes(klass) {
2372
- if (!this.add(klass)) {
2373
- return;
2374
- }
2375
- // add classes inherited from this base class
2376
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2377
- this.discoverTypes(child);
2378
- });
2379
- // skip if no fields are defined for this class.
2380
- if (klass[Symbol.metadata] === undefined) {
2381
- klass[Symbol.metadata] = {};
2382
- }
2383
- // const metadata = Metadata.getFor(klass);
2384
- const metadata = klass[Symbol.metadata];
2385
- // if any schema/field has filters, mark "context" as having filters.
2386
- if (metadata[-2]) {
2387
- this.hasFilters = true;
2388
- }
2389
- for (const field in metadata) {
2390
- const fieldType = metadata[field].type;
2391
- if (typeof (fieldType) === "string") {
2392
- continue;
2393
- }
2394
- if (Array.isArray(fieldType)) {
2395
- const type = fieldType[0];
2396
- if (type === "string") {
2397
- continue;
2670
+ this.forEach((value, key) => {
2671
+ if (value[$changes]) {
2672
+ cloned.set(key, value['clone']());
2398
2673
  }
2399
- this.discoverTypes(type);
2400
- }
2401
- else if (typeof (fieldType) === "function") {
2402
- this.discoverTypes(fieldType);
2403
- }
2404
- else {
2405
- const type = Object.values(fieldType)[0];
2406
- // skip primitive types
2407
- if (typeof (type) === "string") {
2408
- continue;
2674
+ else {
2675
+ cloned.set(key, value);
2409
2676
  }
2410
- this.discoverTypes(type);
2411
- }
2677
+ });
2412
2678
  }
2679
+ return cloned;
2413
2680
  }
2414
2681
  }
2682
+ registerType("map", { constructor: MapSchema });
2683
+
2684
+ const DEFAULT_VIEW_TAG = -1;
2415
2685
  /**
2416
2686
  * [See documentation](https://docs.colyseus.io/state/schema/)
2417
2687
  *
@@ -2438,8 +2708,8 @@
2438
2708
  // // detect index for this field, considering inheritance
2439
2709
  // //
2440
2710
  // const parent = Object.getPrototypeOf(context.metadata);
2441
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2442
- // ?? (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
2443
2713
  // ?? -1; // no fields defined
2444
2714
  // fieldIndex++;
2445
2715
  // if (
@@ -2559,18 +2829,20 @@
2559
2829
  const constructor = target.constructor;
2560
2830
  const parentClass = Object.getPrototypeOf(constructor);
2561
2831
  const parentMetadata = parentClass[Symbol.metadata];
2832
+ // TODO: use Metadata.initialize()
2562
2833
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2563
- if (!metadata[fieldName]) {
2564
- //
2565
- // detect index for this field, considering inheritance
2566
- //
2567
- metadata[fieldName] = {
2568
- type: undefined,
2569
- index: (metadata[-1] // current structure already has fields defined
2570
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2571
- ?? -1) + 1 // no fields defined
2572
- };
2573
- }
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
+ // }
2574
2846
  Metadata.setTag(metadata, fieldName, tag);
2575
2847
  };
2576
2848
  }
@@ -2584,17 +2856,17 @@
2584
2856
  TypeContext.register(constructor);
2585
2857
  const parentClass = Object.getPrototypeOf(constructor);
2586
2858
  const parentMetadata = parentClass[Symbol.metadata];
2587
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2588
- let fieldIndex;
2859
+ const metadata = Metadata.initialize(constructor);
2860
+ let fieldIndex = metadata[field];
2589
2861
  /**
2590
2862
  * skip if descriptor already exists for this field (`@deprecated()`)
2591
2863
  */
2592
- if (metadata[field]) {
2593
- if (metadata[field].deprecated) {
2864
+ if (metadata[fieldIndex] !== undefined) {
2865
+ if (metadata[fieldIndex].deprecated) {
2594
2866
  // do not create accessors for deprecated properties.
2595
2867
  return;
2596
2868
  }
2597
- else if (metadata[field].descriptor !== undefined) {
2869
+ else if (metadata[fieldIndex].type !== undefined) {
2598
2870
  // trying to define same property multiple times across inheritance.
2599
2871
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
2600
2872
  try {
@@ -2605,16 +2877,13 @@
2605
2877
  throw new Error(`${e.message} ${definitionAtLine}`);
2606
2878
  }
2607
2879
  }
2608
- else {
2609
- fieldIndex = metadata[field].index;
2610
- }
2611
2880
  }
2612
2881
  else {
2613
2882
  //
2614
2883
  // detect index for this field, considering inheritance
2615
2884
  //
2616
- fieldIndex = metadata[-1] // current structure already has fields defined
2617
- ?? (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
2618
2887
  ?? -1; // no fields defined
2619
2888
  fieldIndex++;
2620
2889
  }
@@ -2633,15 +2902,15 @@
2633
2902
  const childType = (complexTypeKlass)
2634
2903
  ? Object.values(type)[0]
2635
2904
  : type;
2636
- 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));
2637
2906
  }
2638
2907
  };
2639
2908
  }
2640
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass, metadata, field) {
2909
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
2641
2910
  return {
2642
2911
  get: function () { return this[fieldCached]; },
2643
2912
  set: function (value) {
2644
- const previousValue = this[fieldCached] || undefined;
2913
+ const previousValue = this[fieldCached] ?? undefined;
2645
2914
  // skip if value is the same as cached.
2646
2915
  if (value === previousValue) {
2647
2916
  return;
@@ -2659,22 +2928,27 @@
2659
2928
  }
2660
2929
  value[$childType] = type;
2661
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];
2662
2938
  //
2663
2939
  // Replacing existing "ref", remove it from root.
2664
2940
  // TODO: if there are other references to this instance, we should not remove it from root.
2665
2941
  //
2666
2942
  if (previousValue !== undefined && previousValue[$changes]) {
2667
- this[$changes].root?.remove(previousValue[$changes]);
2943
+ changeTree.root?.remove(previousValue[$changes]);
2668
2944
  }
2669
2945
  // flag the change for encoding.
2670
- this.constructor[$track](this[$changes], fieldIndex, exports.OPERATION.ADD);
2946
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2671
2947
  //
2672
2948
  // call setParent() recursively for this and its child
2673
2949
  // structures.
2674
2950
  //
2675
- if (value[$changes]) {
2676
- value[$changes].setParent(this, this[$changes].root, metadata[field].index);
2677
- }
2951
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
2678
2952
  }
2679
2953
  else if (previousValue !== undefined) {
2680
2954
  //
@@ -2701,20 +2975,22 @@
2701
2975
  const parentClass = Object.getPrototypeOf(constructor);
2702
2976
  const parentMetadata = parentClass[Symbol.metadata];
2703
2977
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2704
- if (!metadata[field]) {
2705
- //
2706
- // detect index for this field, considering inheritance
2707
- //
2708
- metadata[field] = {
2709
- type: undefined,
2710
- index: (metadata[-1] // current structure already has fields defined
2711
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2712
- ?? -1) + 1 // no fields defined
2713
- };
2714
- }
2715
- 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;
2716
2991
  if (throws) {
2717
- metadata[field].descriptor = {
2992
+ metadata[$descriptors] ??= {};
2993
+ metadata[$descriptors][field] = {
2718
2994
  get: function () { throw new Error(`${field} is deprecated.`); },
2719
2995
  set: function (value) { },
2720
2996
  enumerable: false,
@@ -2722,8 +2998,8 @@
2722
2998
  };
2723
2999
  }
2724
3000
  // flag metadata[field] as non-enumerable
2725
- Object.defineProperty(metadata, field, {
2726
- value: metadata[field],
3001
+ Object.defineProperty(metadata, fieldIndex, {
3002
+ value: metadata[fieldIndex],
2727
3003
  enumerable: false,
2728
3004
  configurable: true
2729
3005
  });
@@ -2735,6 +3011,37 @@
2735
3011
  }
2736
3012
  return target;
2737
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
+ }
2738
3045
 
2739
3046
  function getIndent(level) {
2740
3047
  return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
@@ -2745,32 +3052,21 @@
2745
3052
  ops: {},
2746
3053
  refs: []
2747
3054
  };
2748
- $root.changes.forEach((operations, changeTree) => {
3055
+ // for (const refId in $root.changes) {
3056
+ $root.changes.forEach(changeTree => {
3057
+ const changes = changeTree.indexedOperations;
2749
3058
  dump.refs.push(`refId#${changeTree.refId}`);
2750
- operations.forEach((op, index) => {
3059
+ for (const index in changes) {
3060
+ const op = changes[index];
2751
3061
  const opName = exports.OPERATION[op];
2752
3062
  if (!dump.ops[opName]) {
2753
3063
  dump.ops[opName] = 0;
2754
3064
  }
2755
3065
  dump.ops[exports.OPERATION[op]]++;
2756
- });
3066
+ }
2757
3067
  });
2758
3068
  return dump;
2759
3069
  }
2760
- function getNextPowerOf2(number) {
2761
- // If number is already a power of 2, return it
2762
- if ((number & (number - 1)) === 0) {
2763
- return number;
2764
- }
2765
- // Find the position of the most significant bit
2766
- let msbPosition = 0;
2767
- while (number > 0) {
2768
- number >>= 1;
2769
- msbPosition++;
2770
- }
2771
- // Return the next power of 2
2772
- return 1 << msbPosition;
2773
- }
2774
3070
 
2775
3071
  var _a$2, _b$2;
2776
3072
  /**
@@ -2779,6 +3075,7 @@
2779
3075
  class Schema {
2780
3076
  static { this[_a$2] = encodeSchemaOperation; }
2781
3077
  static { this[_b$2] = decodeSchemaOperation; }
3078
+ // public [$changes]: ChangeTree;
2782
3079
  /**
2783
3080
  * Assign the property descriptors required to track changes on this instance.
2784
3081
  * @param instance
@@ -2789,35 +3086,7 @@
2789
3086
  enumerable: false,
2790
3087
  writable: true
2791
3088
  });
2792
- const metadata = instance.constructor[Symbol.metadata];
2793
- // Define property descriptors
2794
- for (const field in metadata) {
2795
- if (metadata[field].descriptor) {
2796
- // for encoder
2797
- Object.defineProperty(instance, `_${field}`, {
2798
- value: undefined,
2799
- writable: true,
2800
- enumerable: false,
2801
- configurable: true,
2802
- });
2803
- Object.defineProperty(instance, field, metadata[field].descriptor);
2804
- }
2805
- else {
2806
- // for decoder
2807
- Object.defineProperty(instance, field, {
2808
- value: undefined,
2809
- writable: true,
2810
- enumerable: true,
2811
- configurable: true,
2812
- });
2813
- }
2814
- // Object.defineProperty(instance, field, {
2815
- // ...instance.constructor[Symbol.metadata][field].descriptor
2816
- // });
2817
- // if (args[0]?.hasOwnProperty(field)) {
2818
- // instance[field] = args[0][field];
2819
- // }
2820
- }
3089
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2821
3090
  }
2822
3091
  static is(type) {
2823
3092
  return typeof (type[Symbol.metadata]) === "object";
@@ -2841,7 +3110,7 @@
2841
3110
  */
2842
3111
  static [$filter](ref, index, view) {
2843
3112
  const metadata = ref.constructor[Symbol.metadata];
2844
- const tag = metadata[metadata[index]].tag;
3113
+ const tag = metadata[index]?.tag;
2845
3114
  if (view === undefined) {
2846
3115
  // shared pass/encode: encode if doesn't have a tag
2847
3116
  return tag === undefined;
@@ -2862,12 +3131,16 @@
2862
3131
  }
2863
3132
  // allow inherited classes to have a constructor
2864
3133
  constructor(...args) {
3134
+ //
3135
+ // inline
3136
+ // Schema.initialize(this);
3137
+ //
2865
3138
  Schema.initialize(this);
2866
3139
  //
2867
3140
  // Assign initial values
2868
3141
  //
2869
3142
  if (args[0]) {
2870
- this.assign(args[0]);
3143
+ Object.assign(this, args[0]);
2871
3144
  }
2872
3145
  }
2873
3146
  assign(props) {
@@ -2881,7 +3154,8 @@
2881
3154
  * @param operation OPERATION to perform (detected automatically)
2882
3155
  */
2883
3156
  setDirty(property, operation) {
2884
- this[$changes].change(this.constructor[Symbol.metadata][property].index, operation);
3157
+ const metadata = this.constructor[Symbol.metadata];
3158
+ this[$changes].change(metadata[metadata[property]].index, operation);
2885
3159
  }
2886
3160
  clone() {
2887
3161
  const cloned = new (this.constructor);
@@ -2890,7 +3164,9 @@
2890
3164
  // TODO: clone all properties, not only annotated ones
2891
3165
  //
2892
3166
  // for (const field in this) {
2893
- for (const field in metadata) {
3167
+ for (const fieldIndex in metadata) {
3168
+ // const field = metadata[metadata[fieldIndex]].name;
3169
+ const field = metadata[fieldIndex].name;
2894
3170
  if (typeof (this[field]) === "object" &&
2895
3171
  typeof (this[field]?.clone) === "function") {
2896
3172
  // deep clone
@@ -2904,10 +3180,11 @@
2904
3180
  return cloned;
2905
3181
  }
2906
3182
  toJSON() {
2907
- const metadata = this.constructor[Symbol.metadata];
2908
3183
  const obj = {};
2909
- for (const fieldName in metadata) {
2910
- const field = metadata[fieldName];
3184
+ const metadata = this.constructor[Symbol.metadata];
3185
+ for (const index in metadata) {
3186
+ const field = metadata[index];
3187
+ const fieldName = field.name;
2911
3188
  if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
2912
3189
  obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
2913
3190
  ? this[fieldName]['toJSON']()
@@ -2920,18 +3197,27 @@
2920
3197
  this[$changes].discardAll();
2921
3198
  }
2922
3199
  [$getByIndex](index) {
2923
- return this[this.constructor[Symbol.metadata][index]];
3200
+ const metadata = this.constructor[Symbol.metadata];
3201
+ return this[metadata[index].name];
2924
3202
  }
2925
3203
  [$deleteByIndex](index) {
2926
- this[this.constructor[Symbol.metadata][index]] = undefined;
3204
+ const metadata = this.constructor[Symbol.metadata];
3205
+ this[metadata[index].name] = undefined;
2927
3206
  }
2928
- static debugRefIds(instance, jsonContents = true, level = 0) {
3207
+ /**
3208
+ * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3209
+ *
3210
+ * @param instance Schema instance
3211
+ * @param showContents display JSON contents of the instance
3212
+ * @returns
3213
+ */
3214
+ static debugRefIds(instance, showContents = false, level = 0) {
2929
3215
  const ref = instance;
2930
3216
  const changeTree = ref[$changes];
2931
- const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3217
+ const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2932
3218
  let output = "";
2933
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
2934
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
3219
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
3220
+ changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1));
2935
3221
  return output;
2936
3222
  }
2937
3223
  /**
@@ -2948,30 +3234,40 @@
2948
3234
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
2949
3235
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
2950
3236
  function dumpChangeSet(changeSet) {
2951
- Array.from(changeSet)
2952
- .sort((a, b) => a[0] - b[0])
2953
- .forEach(([index, operation]) => output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3237
+ changeSet.operations
3238
+ .filter(op => op)
3239
+ .forEach((index) => {
3240
+ const operation = changeTree.indexedOperations[index];
3241
+ console.log({ index, operation });
3242
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3243
+ });
2954
3244
  }
2955
3245
  dumpChangeSet(changeSet);
2956
3246
  // display filtered changes
2957
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3247
+ if (!isEncodeAll &&
3248
+ changeTree.filteredChanges &&
3249
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
2958
3250
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
2959
3251
  dumpChangeSet(changeTree.filteredChanges);
2960
3252
  }
2961
3253
  // display filtered changes
2962
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3254
+ if (isEncodeAll &&
3255
+ changeTree.allFilteredChanges &&
3256
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
2963
3257
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
2964
3258
  dumpChangeSet(changeTree.allFilteredChanges);
2965
3259
  }
2966
3260
  return output;
2967
3261
  }
2968
- static debugChangesDeep(ref) {
3262
+ static debugChangesDeep(ref, changeSetName = "changes") {
2969
3263
  let output = "";
2970
3264
  const rootChangeTree = ref[$changes];
3265
+ const root = rootChangeTree.root;
2971
3266
  const changeTrees = new Map();
2972
- let totalInstances = 0;
3267
+ const instanceRefIds = [];
2973
3268
  let totalOperations = 0;
2974
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3269
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3270
+ const changeTree = root.changeTrees[refId];
2975
3271
  let includeChangeTree = false;
2976
3272
  let parentChangeTrees = [];
2977
3273
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -2989,14 +3285,14 @@
2989
3285
  }
2990
3286
  }
2991
3287
  if (includeChangeTree) {
2992
- totalInstances += 1;
2993
- totalOperations += changes.size;
3288
+ instanceRefIds.push(changeTree.refId);
3289
+ totalOperations += Object.keys(changes).length;
2994
3290
  changeTrees.set(changeTree, parentChangeTrees.reverse());
2995
3291
  }
2996
3292
  }
2997
3293
  output += "---\n";
2998
3294
  output += `root refId: ${rootChangeTree.refId}\n`;
2999
- output += `Total instances: ${totalInstances}\n`;
3295
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
3000
3296
  output += `Total changes: ${totalOperations}\n`;
3001
3297
  output += "---\n";
3002
3298
  // based on root.changes, display a tree of changes that has the "ref" instance as parent
@@ -3008,12 +3304,13 @@
3008
3304
  visitedParents.add(parentChangeTree);
3009
3305
  }
3010
3306
  });
3011
- const changes = changeTree.changes;
3307
+ const changes = changeTree.indexedOperations;
3012
3308
  const level = parentChangeTrees.length;
3013
3309
  const indent = getIndent(level);
3014
3310
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3015
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3016
- for (const [index, operation] of changes) {
3311
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3312
+ for (const index in changes) {
3313
+ const operation = changes[index];
3017
3314
  output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3018
3315
  }
3019
3316
  }
@@ -3047,6 +3344,7 @@
3047
3344
  this.$indexes = new Map();
3048
3345
  this.$refId = 0;
3049
3346
  this[$changes] = new ChangeTree(this);
3347
+ this[$changes].indexes = {};
3050
3348
  if (initialValues) {
3051
3349
  initialValues.forEach((v) => this.add(v));
3052
3350
  }
@@ -3202,6 +3500,7 @@
3202
3500
  this.$indexes = new Map();
3203
3501
  this.$refId = 0;
3204
3502
  this[$changes] = new ChangeTree(this);
3503
+ this[$changes].indexes = {};
3205
3504
  if (initialValues) {
3206
3505
  initialValues.forEach((v) => this.add(v));
3207
3506
  }
@@ -3357,6 +3656,8 @@
3357
3656
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3358
3657
  PERFORMANCE OF THIS SOFTWARE.
3359
3658
  ***************************************************************************** */
3659
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
3660
+
3360
3661
 
3361
3662
  function __decorate(decorators, target, key, desc) {
3362
3663
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3370,37 +3671,127 @@
3370
3671
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3371
3672
  };
3372
3673
 
3674
+ function spliceOne(arr, index) {
3675
+ // manually splice an array
3676
+ if (index === -1 || index >= arr.length) {
3677
+ return false;
3678
+ }
3679
+ const len = arr.length - 1;
3680
+ for (let i = index; i < len; i++) {
3681
+ arr[i] = arr[i + 1];
3682
+ }
3683
+ arr.length = len;
3684
+ return true;
3685
+ }
3686
+
3687
+ class Root {
3688
+ constructor(types) {
3689
+ this.types = types;
3690
+ this.nextUniqueId = 0;
3691
+ this.refCount = {};
3692
+ this.changeTrees = {};
3693
+ // all changes
3694
+ this.allChanges = [];
3695
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3696
+ // pending changes to be encoded
3697
+ this.changes = [];
3698
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3699
+ }
3700
+ getNextUniqueId() {
3701
+ return this.nextUniqueId++;
3702
+ }
3703
+ add(changeTree) {
3704
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3705
+ changeTree.ensureRefId();
3706
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3707
+ if (isNewChangeTree) {
3708
+ this.changeTrees[changeTree.refId] = changeTree;
3709
+ }
3710
+ const previousRefCount = this.refCount[changeTree.refId];
3711
+ if (previousRefCount === 0) {
3712
+ //
3713
+ // When a ChangeTree is re-added, it means that it was previously removed.
3714
+ // We need to re-add all changes to the `changes` map.
3715
+ //
3716
+ const ops = changeTree.allChanges.operations;
3717
+ let len = ops.length;
3718
+ while (len--) {
3719
+ changeTree.indexedOperations[ops[len]] = exports.OPERATION.ADD;
3720
+ setOperationAtIndex(changeTree.changes, len);
3721
+ }
3722
+ }
3723
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3724
+ return isNewChangeTree;
3725
+ }
3726
+ remove(changeTree) {
3727
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3728
+ if (refCount <= 0) {
3729
+ //
3730
+ // Only remove "root" reference if it's the last reference
3731
+ //
3732
+ changeTree.root = undefined;
3733
+ delete this.changeTrees[changeTree.refId];
3734
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3735
+ this.removeChangeFromChangeSet("changes", changeTree);
3736
+ if (changeTree.filteredChanges) {
3737
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3738
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3739
+ }
3740
+ this.refCount[changeTree.refId] = 0;
3741
+ }
3742
+ else {
3743
+ this.refCount[changeTree.refId] = refCount;
3744
+ }
3745
+ changeTree.forEachChild((child, _) => this.remove(child));
3746
+ return refCount;
3747
+ }
3748
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3749
+ const changeSet = this[changeSetName];
3750
+ const index = changeSet.indexOf(changeTree);
3751
+ if (index !== -1) {
3752
+ spliceOne(changeSet, index);
3753
+ // changeSet[index] = undefined;
3754
+ }
3755
+ }
3756
+ clear() {
3757
+ this.changes.length = 0;
3758
+ }
3759
+ }
3760
+
3373
3761
  class Encoder {
3374
- static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3375
- constructor(root) {
3376
- this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3377
- this.setRoot(root);
3762
+ static { this.BUFFER_SIZE = Buffer.poolSize ?? 8 * 1024; } // 8KB
3763
+ constructor(state) {
3764
+ this.sharedBuffer = Buffer.allocUnsafe(Encoder.BUFFER_SIZE);
3378
3765
  //
3379
3766
  // TODO: cache and restore "Context" based on root schema
3380
3767
  // (to avoid creating a new context for every new room)
3381
3768
  //
3382
- this.context = new TypeContext(root.constructor);
3769
+ this.context = new TypeContext(state.constructor);
3770
+ this.root = new Root(this.context);
3771
+ this.setState(state);
3383
3772
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3384
3773
  // this.context.schemas.forEach((id, schema) => {
3385
3774
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3386
3775
  // });
3387
3776
  }
3388
- setRoot(state) {
3389
- this.root = new Root();
3777
+ setState(state) {
3390
3778
  this.state = state;
3391
- state[$changes].setRoot(this.root);
3779
+ this.state[$changes].setRoot(this.root);
3392
3780
  }
3393
- encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes) {
3394
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3395
- const isEncodeAll = this.root.allChanges === changeTrees;
3781
+ 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
3782
+ ) {
3396
3783
  const hasView = (view !== undefined);
3397
3784
  const rootChangeTree = this.state[$changes];
3398
- const changeTreesIterator = changeTrees.entries();
3399
- for (const [changeTree, changes] of changeTreesIterator) {
3785
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3786
+ const changeTrees = this.root[changeSetName];
3787
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3788
+ const changeTree = changeTrees[i];
3789
+ const operations = changeTree[changeSetName];
3400
3790
  const ref = changeTree.ref;
3401
- const ctor = ref['constructor'];
3791
+ const ctor = ref.constructor;
3402
3792
  const encoder = ctor[$encoder];
3403
3793
  const filter = ctor[$filter];
3794
+ const metadata = ctor[Symbol.metadata];
3404
3795
  if (hasView) {
3405
3796
  if (!view.items.has(changeTree)) {
3406
3797
  view.invisible.add(changeTree);
@@ -3411,12 +3802,18 @@
3411
3802
  }
3412
3803
  }
3413
3804
  // skip root `refId` if it's the first change tree
3414
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
3805
+ // (unless it "hasView", which will need to revisit the root)
3806
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3415
3807
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3416
- number$1(buffer, changeTree.refId, it);
3808
+ encode.number(buffer, changeTree.refId, it);
3417
3809
  }
3418
- const changesIterator = changes.entries();
3419
- for (const [fieldIndex, operation] of changesIterator) {
3810
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3811
+ const fieldIndex = operations.operations[j];
3812
+ const operation = (fieldIndex < 0)
3813
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3814
+ : (isEncodeAll)
3815
+ ? exports.OPERATION.ADD
3816
+ : changeTree.indexedOperations[fieldIndex];
3420
3817
  //
3421
3818
  // first pass (encodeAll), identify "filtered" operations without encoding them
3422
3819
  // they will be encoded per client, based on their view.
@@ -3424,96 +3821,113 @@
3424
3821
  // TODO: how can we optimize filtering out "encode all" operations?
3425
3822
  // TODO: avoid checking if no view tags were defined
3426
3823
  //
3427
- if (filter && !filter(ref, fieldIndex, view)) {
3428
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3824
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3429
3825
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3430
3826
  // view?.invisible.add(changeTree);
3431
3827
  continue;
3432
3828
  }
3433
- // console.log("WILL ENCODE", {
3434
- // ref: changeTree.ref.constructor.name,
3435
- // fieldIndex,
3436
- // operation: OPERATION[operation],
3437
- // });
3438
- encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3829
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3439
3830
  }
3831
+ // if (shouldDiscardChanges) {
3832
+ // changeTree.discard();
3833
+ // changeTree.isNew = false; // Not a new instance anymore
3834
+ // }
3440
3835
  }
3441
3836
  if (it.offset > buffer.byteLength) {
3442
- const newSize = getNextPowerOf2(buffer.byteLength * 2);
3443
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + buffer.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
3837
+ // we can assume that n + 1 poolSize will suffice given that we are likely done with encoding at this point
3838
+ // multiples of poolSize are faster to allocate than arbitrary sizes
3839
+ // if we are on an older platform that doesn't implement pooling use 8kb as poolSize (that's the default for node)
3840
+ const newSize = Math.ceil(it.offset / (Buffer.poolSize ?? 8 * 1024)) * (Buffer.poolSize ?? 8 * 1024);
3841
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
3842
+
3843
+ import { Encoder } from "@colyseus/schema";
3844
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
3845
+ `);
3444
3846
  //
3445
3847
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
3848
+ // -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
3446
3849
  //
3447
- buffer = Buffer.allocUnsafeSlow(newSize);
3850
+ buffer = Buffer.alloc(newSize, buffer); // fill with buffer here to memcpy previous encoding steps beyond the initialOffset
3448
3851
  // assign resized buffer to local sharedBuffer
3449
3852
  if (buffer === this.sharedBuffer) {
3450
3853
  this.sharedBuffer = buffer;
3451
3854
  }
3452
- return this.encode({ offset: initialOffset }, view, buffer);
3855
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3453
3856
  }
3454
3857
  else {
3455
3858
  //
3456
3859
  // only clear changes after making sure buffer resize is not required.
3457
3860
  //
3458
- if (!isEncodeAll && !hasView) {
3861
+ if (shouldDiscardChanges) {
3459
3862
  //
3460
- // FIXME: avoid iterating over change trees twice.
3863
+ // TODO: avoid iterating over change trees twice.
3461
3864
  //
3462
- this.onEndEncode(changeTrees);
3865
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3866
+ const changeTree = changeTrees[i];
3867
+ changeTree.discard();
3868
+ changeTree.isNew = false; // Not a new instance anymore
3869
+ }
3463
3870
  }
3464
3871
  return buffer.subarray(0, it.offset);
3465
3872
  }
3466
3873
  }
3467
3874
  encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3468
- // console.log(`encodeAll(), this.root.allChanges (${this.root.allChanges.size})`);
3469
- // Array.from(this.root.allChanges.entries()).map((item) => {
3470
- // console.log("->", item[0].refId, item[0].ref.toJSON());
3471
- // });
3472
- return this.encode(it, undefined, buffer, this.root.allChanges);
3875
+ return this.encode(it, undefined, buffer, "allChanges", true);
3473
3876
  }
3474
3877
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3475
3878
  const viewOffset = it.offset;
3476
- // console.log(`encodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
3477
- // this.debugAllFilteredChanges();
3478
3879
  // try to encode "filtered" changes
3479
- this.encode(it, view, bytes, this.root.allFilteredChanges);
3880
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3480
3881
  return Buffer.concat([
3481
3882
  bytes.subarray(0, sharedOffset),
3482
3883
  bytes.subarray(viewOffset, it.offset)
3483
3884
  ]);
3484
3885
  }
3485
- // debugAllFilteredChanges() {
3486
- // Array.from(this.root.allFilteredChanges.entries()).map((item) => {
3487
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
3488
- // if (Array.isArray(item[0].ref.toJSON())) {
3489
- // item[1].forEach((op, key) => {
3490
- // console.log(" ->", { key, op: OPERATION[op] });
3491
- // })
3492
- // }
3493
- // });
3494
- // }
3886
+ debugChanges(field) {
3887
+ const rootChangeSet = (typeof (field) === "string")
3888
+ ? this.root[field]
3889
+ : field;
3890
+ rootChangeSet.forEach((changeTree) => {
3891
+ const changeSet = changeTree[field];
3892
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3893
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3894
+ for (const index in changeSet) {
3895
+ const op = changeSet[index];
3896
+ console.log(" ->", {
3897
+ index,
3898
+ field: metadata?.[index],
3899
+ op: exports.OPERATION[op],
3900
+ });
3901
+ }
3902
+ });
3903
+ }
3495
3904
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3496
3905
  const viewOffset = it.offset;
3497
- // try to encode "filtered" changes
3498
- this.encode(it, view, bytes, this.root.filteredChanges);
3499
3906
  // encode visibility changes (add/remove for this view)
3500
- const viewChangesIterator = view.changes.entries();
3501
- for (const [changeTree, changes] of viewChangesIterator) {
3502
- if (changes.size === 0) {
3503
- // FIXME: avoid having empty changes if no changes were made
3504
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3907
+ const refIds = Object.keys(view.changes);
3908
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3909
+ const refId = refIds[i];
3910
+ const changes = view.changes[refId];
3911
+ const changeTree = this.root.changeTrees[refId];
3912
+ if (changeTree === undefined ||
3913
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3914
+ ) {
3915
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3505
3916
  continue;
3506
3917
  }
3507
3918
  const ref = changeTree.ref;
3508
- const ctor = ref['constructor'];
3919
+ const ctor = ref.constructor;
3509
3920
  const encoder = ctor[$encoder];
3921
+ const metadata = ctor[Symbol.metadata];
3510
3922
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3511
- number$1(bytes, changeTree.refId, it);
3512
- const changesIterator = changes.entries();
3513
- for (const [fieldIndex, operation] of changesIterator) {
3923
+ encode.number(bytes, changeTree.refId, it);
3924
+ const keys = Object.keys(changes);
3925
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3926
+ const key = keys[i];
3927
+ const operation = changes[key];
3514
3928
  // isEncodeAll = false
3515
3929
  // hasView = true
3516
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3930
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3517
3931
  }
3518
3932
  }
3519
3933
  //
@@ -3521,51 +3935,62 @@
3521
3935
  // (to allow re-using StateView's for multiple clients)
3522
3936
  //
3523
3937
  // clear "view" changes after encoding
3524
- view.changes.clear();
3938
+ view.changes = {};
3939
+ // try to encode "filtered" changes
3940
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3525
3941
  return Buffer.concat([
3526
3942
  bytes.subarray(0, sharedOffset),
3527
3943
  bytes.subarray(viewOffset, it.offset)
3528
3944
  ]);
3529
3945
  }
3530
3946
  onEndEncode(changeTrees = this.root.changes) {
3531
- const changeTreesIterator = changeTrees.entries();
3532
- for (const [changeTree, _] of changeTreesIterator) {
3533
- changeTree.endEncode();
3534
- }
3947
+ // changeTrees.forEach(function(changeTree) {
3948
+ // changeTree.endEncode();
3949
+ // });
3950
+ // for (const refId in changeTrees) {
3951
+ // const changeTree = this.root.changeTrees[refId];
3952
+ // changeTree.endEncode();
3953
+ // // changeTree.changes.clear();
3954
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3955
+ // // changeTree.ref[$onEncodeEnd]?.();
3956
+ // // // Not a new instance anymore
3957
+ // // delete changeTree[$isNew];
3958
+ // }
3535
3959
  }
3536
3960
  discardChanges() {
3537
3961
  // discard shared changes
3538
- if (this.root.changes.size > 0) {
3539
- this.onEndEncode(this.root.changes);
3540
- this.root.changes.clear();
3962
+ let length = this.root.changes.length;
3963
+ if (length > 0) {
3964
+ while (length--) {
3965
+ this.root.changes[length]?.endEncode();
3966
+ }
3967
+ this.root.changes.length = 0;
3541
3968
  }
3542
3969
  // discard filtered changes
3543
- if (this.root.filteredChanges.size > 0) {
3544
- this.onEndEncode(this.root.filteredChanges);
3545
- this.root.filteredChanges.clear();
3970
+ length = this.root.filteredChanges.length;
3971
+ if (length > 0) {
3972
+ while (length--) {
3973
+ this.root.filteredChanges[length]?.endEncode();
3974
+ }
3975
+ this.root.filteredChanges.length = 0;
3546
3976
  }
3547
3977
  }
3548
3978
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3549
3979
  const baseTypeId = this.context.getTypeId(baseType);
3550
3980
  const targetTypeId = this.context.getTypeId(targetType);
3981
+ if (targetTypeId === undefined) {
3982
+ 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.`);
3983
+ return;
3984
+ }
3551
3985
  if (baseTypeId !== targetTypeId) {
3552
3986
  bytes[it.offset++] = TYPE_ID & 255;
3553
- number$1(bytes, targetTypeId, it);
3987
+ encode.number(bytes, targetTypeId, it);
3554
3988
  }
3555
3989
  }
3556
- }
3557
-
3558
- function spliceOne(arr, index) {
3559
- // manually splice an array
3560
- if (index === -1 || index >= arr.length) {
3561
- return false;
3562
- }
3563
- const len = arr.length - 1;
3564
- for (let i = index; i < len; i++) {
3565
- arr[i] = arr[i + 1];
3990
+ get hasChanges() {
3991
+ return (this.root.changes.length > 0 ||
3992
+ this.root.filteredChanges.length > 0);
3566
3993
  }
3567
- arr.length = len;
3568
- return true;
3569
3994
  }
3570
3995
 
3571
3996
  class DecodingWarning extends Error {
@@ -3630,6 +4055,7 @@
3630
4055
  clearRefs() {
3631
4056
  this.refs.clear();
3632
4057
  this.deletedRefs.clear();
4058
+ this.callbacks = {};
3633
4059
  this.refCounts = {};
3634
4060
  }
3635
4061
  // for decoding
@@ -3646,8 +4072,9 @@
3646
4072
  // Ensure child schema instances have their references removed as well.
3647
4073
  //
3648
4074
  if (Metadata.isValidInstance(ref)) {
3649
- const metadata = ref['constructor'][Symbol.metadata];
3650
- for (const field in metadata) {
4075
+ const metadata = ref.constructor[Symbol.metadata];
4076
+ for (const index in metadata) {
4077
+ const field = metadata[index].name;
3651
4078
  const childRefId = typeof (ref[field]) === "object" && this.refIds.get(ref[field]);
3652
4079
  if (childRefId) {
3653
4080
  this.removeRef(childRefId);
@@ -3694,14 +4121,14 @@
3694
4121
  class Decoder {
3695
4122
  constructor(root, context) {
3696
4123
  this.currentRefId = 0;
3697
- this.setRoot(root);
4124
+ this.setState(root);
3698
4125
  this.context = context || new TypeContext(root.constructor);
3699
4126
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3700
4127
  // this.context.schemas.forEach((id, schema) => {
3701
4128
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3702
4129
  // });
3703
4130
  }
3704
- setRoot(root) {
4131
+ setState(root) {
3705
4132
  this.state = root;
3706
4133
  this.root = new ReferenceTracker();
3707
4134
  this.root.addRef(0, root);
@@ -3718,7 +4145,7 @@
3718
4145
  //
3719
4146
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
3720
4147
  it.offset++;
3721
- this.currentRefId = number(bytes, it);
4148
+ this.currentRefId = decode.number(bytes, it);
3722
4149
  const nextRef = $root.refs.get(this.currentRefId);
3723
4150
  //
3724
4151
  // Trying to access a reference that haven't been decoded yet.
@@ -3728,7 +4155,7 @@
3728
4155
  }
3729
4156
  ref[$onDecodeEnd]?.();
3730
4157
  ref = nextRef;
3731
- decoder = ref['constructor'][$decoder];
4158
+ decoder = ref.constructor[$decoder];
3732
4159
  continue;
3733
4160
  }
3734
4161
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3740,9 +4167,9 @@
3740
4167
  //
3741
4168
  const nextIterator = { offset: it.offset };
3742
4169
  while (it.offset < totalBytes) {
3743
- if (switchStructureCheck(bytes, it)) {
4170
+ if (bytes[it.offset] === SWITCH_TO_STRUCTURE) {
3744
4171
  nextIterator.offset = it.offset + 1;
3745
- if ($root.refs.has(number(bytes, nextIterator))) {
4172
+ if ($root.refs.has(decode.number(bytes, nextIterator))) {
3746
4173
  break;
3747
4174
  }
3748
4175
  }
@@ -3763,7 +4190,7 @@
3763
4190
  let type;
3764
4191
  if (bytes[it.offset] === TYPE_ID) {
3765
4192
  it.offset++;
3766
- const type_id = number(bytes, it);
4193
+ const type_id = decode.number(bytes, it);
3767
4194
  type = this.context.get(type_id);
3768
4195
  }
3769
4196
  return type || defaultType;
@@ -3829,14 +4256,27 @@
3829
4256
  super(...arguments);
3830
4257
  this.types = new ArraySchema();
3831
4258
  }
3832
- static encode(instance, context, it = { offset: 0 }) {
3833
- if (!context) {
3834
- context = new TypeContext(instance.constructor);
3835
- }
4259
+ /**
4260
+ * Encodes the TypeContext of an Encoder into a buffer.
4261
+ *
4262
+ * @param encoder Encoder instance
4263
+ * @param it
4264
+ * @returns
4265
+ */
4266
+ static encode(encoder, it = { offset: 0 }) {
4267
+ const context = encoder.context;
3836
4268
  const reflection = new Reflection();
3837
- const encoder = new Encoder(reflection);
4269
+ const reflectionEncoder = new Encoder(reflection);
4270
+ // rootType is usually the first schema passed to the Encoder
4271
+ // (unless it inherits from another schema)
4272
+ const rootType = context.schemas.get(encoder.state.constructor);
4273
+ if (rootType > 0) {
4274
+ reflection.rootType = rootType;
4275
+ }
3838
4276
  const buildType = (currentType, metadata) => {
3839
- for (const fieldName in metadata) {
4277
+ for (const fieldIndex in metadata) {
4278
+ const index = Number(fieldIndex);
4279
+ const fieldName = metadata[index].name;
3840
4280
  // skip fields from parent classes
3841
4281
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
3842
4282
  continue;
@@ -3844,7 +4284,7 @@
3844
4284
  const field = new ReflectionField();
3845
4285
  field.name = fieldName;
3846
4286
  let fieldType;
3847
- const type = metadata[fieldName].type;
4287
+ const type = metadata[index].type;
3848
4288
  if (typeof (type) === "string") {
3849
4289
  fieldType = type;
3850
4290
  }
@@ -3886,65 +4326,335 @@
3886
4326
  }
3887
4327
  buildType(type, klass[Symbol.metadata]);
3888
4328
  }
3889
- const buf = encoder.encodeAll(it);
4329
+ const buf = reflectionEncoder.encodeAll(it);
3890
4330
  return Buffer.from(buf, 0, it.offset);
3891
4331
  }
4332
+ /**
4333
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4334
+ *
4335
+ * @param bytes Reflection.encode() output
4336
+ * @param it
4337
+ * @returns Decoder instance
4338
+ */
3892
4339
  static decode(bytes, it) {
3893
4340
  const reflection = new Reflection();
3894
4341
  const reflectionDecoder = new Decoder(reflection);
3895
4342
  reflectionDecoder.decode(bytes, it);
3896
- const context = new TypeContext();
3897
- const schemaTypes = reflection.types.reduce((types, reflectionType) => {
3898
- const parentKlass = types[reflectionType.extendsId] || Schema;
3899
- const schema = class _ extends parentKlass {
4343
+ const typeContext = new TypeContext();
4344
+ // 1st pass, initialize metadata + inheritance
4345
+ reflection.types.forEach((reflectionType) => {
4346
+ const parentClass = typeContext.get(reflectionType.extendsId) ?? Schema;
4347
+ const schema = class _ extends parentClass {
3900
4348
  };
3901
- // const _metadata = Object.create(_classSuper[Symbol.metadata] ?? null);
3902
- const _metadata = parentKlass && parentKlass[Symbol.metadata] || Object.create(null);
3903
- Object.defineProperty(schema, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
3904
4349
  // register for inheritance support
3905
4350
  TypeContext.register(schema);
3906
- const typeid = reflectionType.id;
3907
- types[typeid] = schema;
3908
- context.add(schema, typeid);
3909
- return types;
4351
+ // // for inheritance support
4352
+ // Metadata.initialize(schema);
4353
+ typeContext.add(schema, reflectionType.id);
3910
4354
  }, {});
3911
- reflection.types.forEach((reflectionType) => {
3912
- const schemaType = schemaTypes[reflectionType.id];
3913
- const metadata = schemaType[Symbol.metadata];
3914
- const parentKlass = reflection.types[reflectionType.extendsId];
3915
- const parentFieldIndex = parentKlass && parentKlass.fields.length || 0;
4355
+ // define fields
4356
+ const addFields = (metadata, reflectionType, parentFieldIndex) => {
3916
4357
  reflectionType.fields.forEach((field, i) => {
3917
4358
  const fieldIndex = parentFieldIndex + i;
3918
4359
  if (field.referencedType !== undefined) {
3919
4360
  let fieldType = field.type;
3920
- let refType = schemaTypes[field.referencedType];
4361
+ let refType = typeContext.get(field.referencedType);
3921
4362
  // map or array of primitive type (-1)
3922
4363
  if (!refType) {
3923
4364
  const typeInfo = field.type.split(":");
3924
4365
  fieldType = typeInfo[0];
3925
- refType = typeInfo[1];
4366
+ refType = typeInfo[1]; // string
3926
4367
  }
3927
4368
  if (fieldType === "ref") {
3928
- // type(refType)(schemaType.prototype, field.name);
3929
4369
  Metadata.addField(metadata, fieldIndex, field.name, refType);
3930
4370
  }
3931
4371
  else {
3932
- // type({ [fieldType]: refType } as DefinitionType)(schemaType.prototype, field.name);
3933
4372
  Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
3934
4373
  }
3935
4374
  }
3936
4375
  else {
3937
- // type(field.type as PrimitiveType)(schemaType.prototype, field.name);
3938
4376
  Metadata.addField(metadata, fieldIndex, field.name, field.type);
3939
4377
  }
3940
4378
  });
4379
+ };
4380
+ // 2nd pass, set fields
4381
+ reflection.types.forEach((reflectionType) => {
4382
+ const schema = typeContext.get(reflectionType.id);
4383
+ // for inheritance support
4384
+ const metadata = Metadata.initialize(schema);
4385
+ const inheritedTypes = [];
4386
+ let parentType = reflectionType;
4387
+ do {
4388
+ inheritedTypes.push(parentType);
4389
+ parentType = reflection.types.find((t) => t.id === parentType.extendsId);
4390
+ } while (parentType);
4391
+ let parentFieldIndex = 0;
4392
+ inheritedTypes.reverse().forEach((reflectionType) => {
4393
+ // add fields from all inherited classes
4394
+ // TODO: refactor this to avoid adding fields from parent classes
4395
+ addFields(metadata, reflectionType, parentFieldIndex);
4396
+ parentFieldIndex += reflectionType.fields.length;
4397
+ });
3941
4398
  });
3942
- return new (schemaTypes[0])();
4399
+ const state = new (typeContext.get(reflection.rootType || 0))();
4400
+ return new Decoder(state, typeContext);
3943
4401
  }
3944
4402
  }
3945
4403
  __decorate([
3946
4404
  type([ReflectionType])
3947
4405
  ], Reflection.prototype, "types", void 0);
4406
+ __decorate([
4407
+ type("number")
4408
+ ], Reflection.prototype, "rootType", void 0);
4409
+
4410
+ function getDecoderStateCallbacks(decoder) {
4411
+ const $root = decoder.root;
4412
+ const callbacks = $root.callbacks;
4413
+ const onAddCalls = new WeakMap();
4414
+ let currentOnAddCallback;
4415
+ decoder.triggerChanges = function (allChanges) {
4416
+ const uniqueRefIds = new Set();
4417
+ for (let i = 0, l = allChanges.length; i < l; i++) {
4418
+ const change = allChanges[i];
4419
+ const refId = change.refId;
4420
+ const ref = change.ref;
4421
+ const $callbacks = callbacks[refId];
4422
+ if (!$callbacks) {
4423
+ continue;
4424
+ }
4425
+ //
4426
+ // trigger onRemove on child structure.
4427
+ //
4428
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE &&
4429
+ change.previousValue instanceof Schema) {
4430
+ const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[exports.OPERATION.DELETE];
4431
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4432
+ deleteCallbacks[i]();
4433
+ }
4434
+ }
4435
+ if (ref instanceof Schema) {
4436
+ //
4437
+ // Handle schema instance
4438
+ //
4439
+ if (!uniqueRefIds.has(refId)) {
4440
+ // trigger onChange
4441
+ const replaceCallbacks = $callbacks?.[exports.OPERATION.REPLACE];
4442
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4443
+ replaceCallbacks[i]();
4444
+ // try {
4445
+ // } catch (e) {
4446
+ // console.error(e);
4447
+ // }
4448
+ }
4449
+ }
4450
+ if ($callbacks.hasOwnProperty(change.field)) {
4451
+ const fieldCallbacks = $callbacks[change.field];
4452
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
4453
+ fieldCallbacks[i](change.value, change.previousValue);
4454
+ // try {
4455
+ // } catch (e) {
4456
+ // console.error(e);
4457
+ // }
4458
+ }
4459
+ }
4460
+ }
4461
+ else {
4462
+ //
4463
+ // Handle collection of items
4464
+ //
4465
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
4466
+ //
4467
+ // FIXME: `previousValue` should always be available.
4468
+ //
4469
+ if (change.previousValue !== undefined) {
4470
+ // triger onRemove
4471
+ const deleteCallbacks = $callbacks[exports.OPERATION.DELETE];
4472
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4473
+ deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
4474
+ }
4475
+ }
4476
+ // Handle DELETE_AND_ADD operations
4477
+ if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
4478
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4479
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4480
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4481
+ }
4482
+ }
4483
+ }
4484
+ else if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD && change.previousValue === undefined) {
4485
+ // triger onAdd
4486
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4487
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4488
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4489
+ }
4490
+ }
4491
+ // trigger onChange
4492
+ if (change.value !== change.previousValue) {
4493
+ const replaceCallbacks = $callbacks[exports.OPERATION.REPLACE];
4494
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4495
+ replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4496
+ }
4497
+ }
4498
+ }
4499
+ uniqueRefIds.add(refId);
4500
+ }
4501
+ };
4502
+ function getProxy(metadataOrType, context) {
4503
+ let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
4504
+ let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
4505
+ (metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
4506
+ if (metadata && !isCollection) {
4507
+ const onAddListen = function (ref, prop, callback, immediate) {
4508
+ // immediate trigger
4509
+ if (immediate &&
4510
+ context.instance[prop] !== undefined &&
4511
+ !onAddCalls.has(currentOnAddCallback) // Workaround for https://github.com/colyseus/schema/issues/147
4512
+ ) {
4513
+ callback(context.instance[prop], undefined);
4514
+ }
4515
+ return $root.addCallback($root.refIds.get(ref), prop, callback);
4516
+ };
4517
+ /**
4518
+ * Schema instances
4519
+ */
4520
+ return new Proxy({
4521
+ listen: function listen(prop, callback, immediate = true) {
4522
+ if (context.instance) {
4523
+ return onAddListen(context.instance, prop, callback, immediate);
4524
+ }
4525
+ else {
4526
+ // collection instance not received yet
4527
+ let detachCallback = () => { };
4528
+ context.onInstanceAvailable((ref, existing) => {
4529
+ detachCallback = onAddListen(ref, prop, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4530
+ });
4531
+ return () => detachCallback();
4532
+ }
4533
+ },
4534
+ onChange: function onChange(callback) {
4535
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
4536
+ },
4537
+ //
4538
+ // TODO: refactor `bindTo()` implementation.
4539
+ // There is room for improvement.
4540
+ //
4541
+ bindTo: function bindTo(targetObject, properties) {
4542
+ if (!properties) {
4543
+ properties = Object.keys(metadata).map((index) => metadata[index].name);
4544
+ }
4545
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, () => {
4546
+ properties.forEach((prop) => targetObject[prop] = context.instance[prop]);
4547
+ });
4548
+ }
4549
+ }, {
4550
+ get(target, prop) {
4551
+ const metadataField = metadata[metadata[prop]];
4552
+ if (metadataField) {
4553
+ const instance = context.instance?.[prop];
4554
+ const onInstanceAvailable = ((callback) => {
4555
+ const unbind = $(context.instance).listen(prop, (value, _) => {
4556
+ callback(value, false);
4557
+ // FIXME: by "unbinding" the callback here,
4558
+ // it will not support when the server
4559
+ // re-instantiates the instance.
4560
+ //
4561
+ unbind?.();
4562
+ }, false);
4563
+ // has existing value
4564
+ if ($root.refIds.get(instance) !== undefined) {
4565
+ callback(instance, true);
4566
+ }
4567
+ });
4568
+ return getProxy(metadataField.type, {
4569
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4570
+ instance: ($root.refIds.get(instance) && instance),
4571
+ parentInstance: context.instance,
4572
+ onInstanceAvailable,
4573
+ });
4574
+ }
4575
+ else {
4576
+ // accessing the function
4577
+ return target[prop];
4578
+ }
4579
+ },
4580
+ has(target, prop) { return metadata[prop] !== undefined; },
4581
+ set(_, _1, _2) { throw new Error("not allowed"); },
4582
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4583
+ });
4584
+ }
4585
+ else {
4586
+ /**
4587
+ * Collection instances
4588
+ */
4589
+ const onAdd = function (ref, callback, immediate) {
4590
+ // Trigger callback on existing items
4591
+ if (immediate) {
4592
+ ref.forEach((v, k) => callback(v, k));
4593
+ }
4594
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, (value, key) => {
4595
+ onAddCalls.set(callback, true);
4596
+ currentOnAddCallback = callback;
4597
+ callback(value, key);
4598
+ onAddCalls.delete(callback);
4599
+ currentOnAddCallback = undefined;
4600
+ });
4601
+ };
4602
+ const onRemove = function (ref, callback) {
4603
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
4604
+ };
4605
+ return new Proxy({
4606
+ onAdd: function (callback, immediate = true) {
4607
+ //
4608
+ // https://github.com/colyseus/schema/issues/147
4609
+ // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4610
+ //
4611
+ if (context.instance) {
4612
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4613
+ }
4614
+ else if (context.onInstanceAvailable) {
4615
+ // collection instance not received yet
4616
+ let detachCallback = () => { };
4617
+ context.onInstanceAvailable((ref, existing) => {
4618
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4619
+ });
4620
+ return () => detachCallback();
4621
+ }
4622
+ },
4623
+ onRemove: function (callback) {
4624
+ if (context.onInstanceAvailable) {
4625
+ // collection instance not received yet
4626
+ let detachCallback = () => { };
4627
+ context.onInstanceAvailable((ref) => {
4628
+ detachCallback = onRemove(ref, callback);
4629
+ });
4630
+ return () => detachCallback();
4631
+ }
4632
+ else if (context.instance) {
4633
+ return onRemove(context.instance, callback);
4634
+ }
4635
+ },
4636
+ }, {
4637
+ get(target, prop) {
4638
+ if (!target[prop]) {
4639
+ throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
4640
+ }
4641
+ return target[prop];
4642
+ },
4643
+ has(target, prop) { return target[prop] !== undefined; },
4644
+ set(_, _1, _2) { throw new Error("not allowed"); },
4645
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4646
+ });
4647
+ }
4648
+ }
4649
+ function $(instance) {
4650
+ return getProxy(undefined, { instance });
4651
+ }
4652
+ return $;
4653
+ }
4654
+
4655
+ function getRawChangesCallback(decoder, callback) {
4656
+ decoder.triggerChanges = callback;
4657
+ }
3948
4658
 
3949
4659
  class StateView {
3950
4660
  constructor() {
@@ -3960,31 +4670,32 @@
3960
4670
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
3961
4671
  * (This is used to force encoding a property, even if it was not changed)
3962
4672
  */
3963
- this.changes = new Map();
4673
+ this.changes = {};
3964
4674
  }
3965
4675
  // TODO: allow to set multiple tags at once
3966
- add(obj, tag = DEFAULT_VIEW_TAG) {
4676
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
3967
4677
  if (!obj[$changes]) {
3968
4678
  console.warn("StateView#add(), invalid object:", obj);
3969
4679
  return this;
3970
4680
  }
3971
- let changeTree = obj[$changes];
3972
- this.items.add(changeTree);
3973
- // Add children of this ChangeTree to this view
3974
- changeTree.forEachChild((change, _) => this.add(change.ref, tag));
3975
- // FIXME: ArraySchema/MapSchema does not have metadata
4681
+ // FIXME: ArraySchema/MapSchema do not have metadata
3976
4682
  const metadata = obj.constructor[Symbol.metadata];
3977
- // add parent ChangeTree's, if they are invisible to this view
3978
- // TODO: REFACTOR addParent()
3979
- this.addParent(changeTree, tag);
4683
+ const changeTree = obj[$changes];
4684
+ this.items.add(changeTree);
4685
+ // add parent ChangeTree's
4686
+ // - if it was invisible to this view
4687
+ // - if it were previously filtered out
4688
+ if (checkIncludeParent && changeTree.parent) {
4689
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4690
+ }
3980
4691
  //
3981
4692
  // TODO: when adding an item of a MapSchema, the changes may not
3982
4693
  // be set (only the parent's changes are set)
3983
4694
  //
3984
- let changes = this.changes.get(changeTree);
4695
+ let changes = this.changes[changeTree.refId];
3985
4696
  if (changes === undefined) {
3986
- changes = new Map();
3987
- this.changes.set(changeTree, changes);
4697
+ changes = {};
4698
+ this.changes[changeTree.refId] = changes;
3988
4699
  }
3989
4700
  // set tag
3990
4701
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -4000,82 +4711,78 @@
4000
4711
  tags = this.tags.get(changeTree);
4001
4712
  }
4002
4713
  tags.add(tag);
4003
- // console.log("BY TAG:", tag);
4004
4714
  // Ref: add tagged properties
4005
- metadata?.[-3]?.[tag]?.forEach((index) => {
4715
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4006
4716
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4007
- changes.set(index, exports.OPERATION.ADD);
4717
+ changes[index] = exports.OPERATION.ADD;
4008
4718
  }
4009
4719
  });
4010
4720
  }
4011
4721
  else {
4012
- // console.log("DEFAULT TAG", changeTree.allChanges);
4013
- // // add default tag properties
4014
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4015
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4016
- // changes.set(index, OPERATION.ADD);
4017
- // }
4018
- // });
4019
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4722
+ const isInvisible = this.invisible.has(changeTree);
4723
+ const changeSet = (changeTree.filteredChanges !== undefined)
4020
4724
  ? changeTree.allFilteredChanges
4021
4725
  : changeTree.allChanges;
4022
- const it = allChangesSet.keys();
4023
- const isInvisible = this.invisible.has(changeTree);
4024
- for (const index of it) {
4025
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4026
- changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4027
- changes.set(index, exports.OPERATION.ADD);
4726
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4727
+ const index = changeSet.operations[i];
4728
+ if (index === undefined) {
4729
+ continue;
4730
+ } // skip "undefined" indexes
4731
+ const op = changeTree.indexedOperations[index] ?? exports.OPERATION.ADD;
4732
+ const tagAtIndex = metadata?.[index].tag;
4733
+ if ((isInvisible || // if "invisible", include all
4734
+ tagAtIndex === undefined || // "all change" with no tag
4735
+ tagAtIndex === tag // tagged property
4736
+ ) &&
4737
+ op !== exports.OPERATION.DELETE) {
4738
+ changes[index] = op;
4028
4739
  }
4029
4740
  }
4030
4741
  }
4031
- // TODO: avoid unnecessary iteration here
4032
- while (changeTree.parent &&
4033
- (changeTree = changeTree.parent[$changes]) &&
4034
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4035
- this.items.add(changeTree);
4036
- }
4742
+ // Add children of this ChangeTree to this view
4743
+ changeTree.forEachChild((change, index) => {
4744
+ // Do not ADD children that don't have the same tag
4745
+ if (metadata &&
4746
+ metadata[index].tag !== undefined &&
4747
+ metadata[index].tag !== tag) {
4748
+ return;
4749
+ }
4750
+ this.add(change.ref, tag, false);
4751
+ });
4037
4752
  return this;
4038
4753
  }
4039
- addParent(changeTree, tag) {
4040
- const parentRef = changeTree.parent;
4041
- if (!parentRef) {
4042
- return;
4754
+ addParent(changeTree, parentIndex, tag) {
4755
+ // view must have all "changeTree" parent tree
4756
+ this.items.add(changeTree);
4757
+ // add parent's parent
4758
+ const parentChangeTree = changeTree.parent?.[$changes];
4759
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
4760
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4043
4761
  }
4044
- const parentChangeTree = parentRef[$changes];
4045
- const parentIndex = changeTree.parentIndex;
4046
- if (!this.invisible.has(parentChangeTree)) {
4047
- // parent is already available, no need to add it!
4762
+ // parent is already available, no need to add it!
4763
+ if (!this.invisible.has(changeTree)) {
4048
4764
  return;
4049
4765
  }
4050
- this.addParent(parentChangeTree, tag);
4051
4766
  // add parent's tag properties
4052
- if (parentChangeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4053
- let parentChanges = this.changes.get(parentChangeTree);
4054
- if (parentChanges === undefined) {
4055
- parentChanges = new Map();
4056
- this.changes.set(parentChangeTree, parentChanges);
4767
+ if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4768
+ let changes = this.changes[changeTree.refId];
4769
+ if (changes === undefined) {
4770
+ changes = {};
4771
+ this.changes[changeTree.refId] = changes;
4057
4772
  }
4058
- // console.log("add parent change", {
4059
- // parentIndex,
4060
- // parentChanges,
4061
- // parentChange: (
4062
- // parentChangeTree.getChange(parentIndex) &&
4063
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4064
- // ),
4065
- // })
4066
4773
  if (!this.tags) {
4067
4774
  this.tags = new WeakMap();
4068
4775
  }
4069
4776
  let tags;
4070
- if (!this.tags.has(parentChangeTree)) {
4777
+ if (!this.tags.has(changeTree)) {
4071
4778
  tags = new Set();
4072
- this.tags.set(parentChangeTree, tags);
4779
+ this.tags.set(changeTree, tags);
4073
4780
  }
4074
4781
  else {
4075
- tags = this.tags.get(parentChangeTree);
4782
+ tags = this.tags.get(changeTree);
4076
4783
  }
4077
4784
  tags.add(tag);
4078
- parentChanges.set(parentIndex, exports.OPERATION.ADD);
4785
+ changes[parentIndex] = exports.OPERATION.ADD;
4079
4786
  }
4080
4787
  }
4081
4788
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4087,32 +4794,32 @@
4087
4794
  this.items.delete(changeTree);
4088
4795
  const ref = changeTree.ref;
4089
4796
  const metadata = ref.constructor[Symbol.metadata];
4090
- let changes = this.changes.get(changeTree);
4797
+ let changes = this.changes[changeTree.refId];
4091
4798
  if (changes === undefined) {
4092
- changes = new Map();
4093
- this.changes.set(changeTree, changes);
4799
+ changes = {};
4800
+ this.changes[changeTree.refId] = changes;
4094
4801
  }
4095
4802
  if (tag === DEFAULT_VIEW_TAG) {
4096
4803
  // parent is collection (Map/Array)
4097
4804
  const parent = changeTree.parent;
4098
4805
  if (!Metadata.isValidInstance(parent)) {
4099
4806
  const parentChangeTree = parent[$changes];
4100
- let changes = this.changes.get(parentChangeTree);
4807
+ let changes = this.changes[parentChangeTree.refId];
4101
4808
  if (changes === undefined) {
4102
- changes = new Map();
4103
- this.changes.set(parentChangeTree, changes);
4809
+ changes = {};
4810
+ this.changes[parentChangeTree.refId] = changes;
4104
4811
  }
4105
4812
  // DELETE / DELETE BY REF ID
4106
- changes.set(changeTree.parentIndex, exports.OPERATION.DELETE);
4813
+ changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
4107
4814
  }
4108
4815
  else {
4109
4816
  // delete all "tagged" properties.
4110
- metadata[-2].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4817
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4111
4818
  }
4112
4819
  }
4113
4820
  else {
4114
4821
  // delete only tagged properties
4115
- metadata[-3][tag].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4822
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4116
4823
  }
4117
4824
  // remove tag
4118
4825
  if (this.tags && this.tags.has(changeTree)) {
@@ -4164,16 +4871,18 @@
4164
4871
  exports.decode = decode;
4165
4872
  exports.decodeKeyValueOperation = decodeKeyValueOperation;
4166
4873
  exports.decodeSchemaOperation = decodeSchemaOperation;
4874
+ exports.defineCustomTypes = defineCustomTypes;
4167
4875
  exports.defineTypes = defineTypes;
4168
4876
  exports.deprecated = deprecated;
4169
4877
  exports.dumpChanges = dumpChanges;
4170
4878
  exports.encode = encode;
4171
4879
  exports.encodeKeyValueOperation = encodeArray;
4172
4880
  exports.encodeSchemaOperation = encodeSchemaOperation;
4881
+ exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
4882
+ exports.getRawChangesCallback = getRawChangesCallback;
4173
4883
  exports.registerType = registerType;
4884
+ exports.schema = schema;
4174
4885
  exports.type = type;
4175
4886
  exports.view = view;
4176
4887
 
4177
- Object.defineProperty(exports, '__esModule', { value: true });
4178
-
4179
4888
  }));