@colyseus/schema 3.0.0-alpha.8 → 3.0.0

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