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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +148 -62
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +2234 -1510
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +2231 -1509
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +2233 -1509
  8. package/lib/Metadata.d.ts +21 -9
  9. package/lib/Metadata.js +169 -32
  10. package/lib/Metadata.js.map +1 -1
  11. package/lib/Reflection.d.ts +19 -4
  12. package/lib/Reflection.js +66 -31
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +12 -5
  15. package/lib/Schema.js +57 -56
  16. package/lib/Schema.js.map +1 -1
  17. package/lib/annotations.d.ts +31 -34
  18. package/lib/annotations.js +110 -160
  19. package/lib/annotations.js.map +1 -1
  20. package/lib/bench_encode.d.ts +1 -0
  21. package/lib/bench_encode.js +130 -0
  22. package/lib/bench_encode.js.map +1 -0
  23. package/lib/codegen/api.js +1 -2
  24. package/lib/codegen/api.js.map +1 -1
  25. package/lib/codegen/languages/cpp.js +1 -2
  26. package/lib/codegen/languages/cpp.js.map +1 -1
  27. package/lib/codegen/languages/csharp.js +2 -46
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +4 -2
  30. package/lib/codegen/languages/haxe.js.map +1 -1
  31. package/lib/codegen/languages/java.js +1 -2
  32. package/lib/codegen/languages/java.js.map +1 -1
  33. package/lib/codegen/languages/js.js +1 -2
  34. package/lib/codegen/languages/js.js.map +1 -1
  35. package/lib/codegen/languages/lua.js +23 -25
  36. package/lib/codegen/languages/lua.js.map +1 -1
  37. package/lib/codegen/languages/ts.js +1 -2
  38. package/lib/codegen/languages/ts.js.map +1 -1
  39. package/lib/codegen/parser.js +85 -3
  40. package/lib/codegen/parser.js.map +1 -1
  41. package/lib/codegen/types.js +6 -3
  42. package/lib/codegen/types.js.map +1 -1
  43. package/lib/debug.d.ts +1 -0
  44. package/lib/debug.js +51 -0
  45. package/lib/debug.js.map +1 -0
  46. package/lib/decoder/DecodeOperation.d.ts +3 -4
  47. package/lib/decoder/DecodeOperation.js +37 -19
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +6 -7
  50. package/lib/decoder/Decoder.js +14 -14
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +4 -2
  53. package/lib/decoder/ReferenceTracker.js.map +1 -1
  54. package/lib/decoder/strategy/RawChanges.js +1 -2
  55. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  56. package/lib/decoder/strategy/StateCallbacks.d.ts +44 -11
  57. package/lib/decoder/strategy/StateCallbacks.js +75 -65
  58. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  59. package/lib/encoder/ChangeTree.d.ts +28 -20
  60. package/lib/encoder/ChangeTree.js +242 -188
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +51 -65
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +9 -8
  66. package/lib/encoder/Encoder.js +134 -85
  67. package/lib/encoder/Encoder.js.map +1 -1
  68. package/lib/encoder/Root.d.ts +22 -0
  69. package/lib/encoder/Root.js +81 -0
  70. package/lib/encoder/Root.js.map +1 -0
  71. package/lib/encoder/StateView.d.ts +7 -7
  72. package/lib/encoder/StateView.js +72 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +7 -6
  75. package/lib/encoding/assert.js +13 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.d.ts +36 -19
  78. package/lib/encoding/decode.js +54 -84
  79. package/lib/encoding/decode.js.map +1 -1
  80. package/lib/encoding/encode.d.ts +36 -17
  81. package/lib/encoding/encode.js +82 -68
  82. package/lib/encoding/encode.js.map +1 -1
  83. package/lib/encoding/spec.d.ts +4 -5
  84. package/lib/encoding/spec.js +1 -2
  85. package/lib/encoding/spec.js.map +1 -1
  86. package/lib/index.d.ts +10 -9
  87. package/lib/index.js +24 -17
  88. package/lib/index.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +34 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/TypeContext.d.ts +29 -0
  92. package/lib/types/TypeContext.js +151 -0
  93. package/lib/types/TypeContext.js.map +1 -0
  94. package/lib/types/custom/ArraySchema.d.ts +2 -2
  95. package/lib/types/custom/ArraySchema.js +33 -22
  96. package/lib/types/custom/ArraySchema.js.map +1 -1
  97. package/lib/types/custom/CollectionSchema.js +1 -0
  98. package/lib/types/custom/CollectionSchema.js.map +1 -1
  99. package/lib/types/custom/MapSchema.d.ts +17 -15
  100. package/lib/types/custom/MapSchema.js +12 -4
  101. package/lib/types/custom/MapSchema.js.map +1 -1
  102. package/lib/types/custom/SetSchema.js +1 -0
  103. package/lib/types/custom/SetSchema.js.map +1 -1
  104. package/lib/types/registry.d.ts +8 -1
  105. package/lib/types/registry.js +23 -6
  106. package/lib/types/registry.js.map +1 -1
  107. package/lib/types/symbols.d.ts +8 -5
  108. package/lib/types/symbols.js +9 -6
  109. package/lib/types/symbols.js.map +1 -1
  110. package/lib/types/utils.js +1 -2
  111. package/lib/types/utils.js.map +1 -1
  112. package/lib/utils.js +9 -7
  113. package/lib/utils.js.map +1 -1
  114. package/package.json +19 -18
  115. package/src/Metadata.ts +190 -42
  116. package/src/Reflection.ts +76 -38
  117. package/src/Schema.ts +72 -70
  118. package/src/annotations.ts +156 -202
  119. package/src/bench_encode.ts +108 -0
  120. package/src/codegen/languages/csharp.ts +1 -47
  121. package/src/codegen/languages/haxe.ts +4 -0
  122. package/src/codegen/languages/lua.ts +19 -27
  123. package/src/codegen/parser.ts +107 -0
  124. package/src/codegen/types.ts +1 -0
  125. package/src/debug.ts +55 -0
  126. package/src/decoder/DecodeOperation.ts +46 -18
  127. package/src/decoder/Decoder.ts +17 -15
  128. package/src/decoder/ReferenceTracker.ts +5 -3
  129. package/src/decoder/strategy/StateCallbacks.ts +153 -82
  130. package/src/encoder/ChangeTree.ts +282 -209
  131. package/src/encoder/EncodeOperation.ts +78 -78
  132. package/src/encoder/Encoder.ts +161 -96
  133. package/src/encoder/Root.ts +93 -0
  134. package/src/encoder/StateView.ts +80 -88
  135. package/src/encoding/assert.ts +17 -8
  136. package/src/encoding/decode.ts +73 -93
  137. package/src/encoding/encode.ts +99 -65
  138. package/src/encoding/spec.ts +3 -5
  139. package/src/index.ts +12 -20
  140. package/src/types/HelperTypes.ts +54 -2
  141. package/src/types/TypeContext.ts +175 -0
  142. package/src/types/custom/ArraySchema.ts +49 -19
  143. package/src/types/custom/CollectionSchema.ts +1 -0
  144. package/src/types/custom/MapSchema.ts +30 -17
  145. package/src/types/custom/SetSchema.ts +1 -0
  146. package/src/types/registry.ts +22 -3
  147. package/src/types/symbols.ts +10 -7
  148. package/src/utils.ts +7 -3
@@ -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);
231
- }
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
- // });
255
+ else if (!isFinite(value)) {
256
+ return number$1(bytes, (value > 0) ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER, it);
241
257
  }
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);
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
+ }
268
267
  }
269
- this.ensureRefId();
270
- this.forEachChild((changeTree, atIndex) => {
271
- changeTree.setParent(this.ref, root, atIndex);
272
- });
268
+ bytes[it.offset++] = 0xcb;
269
+ float64$1(bytes, value, it);
270
+ return 9;
273
271
  }
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 >= -0x20) {
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 >= -0x80) {
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 >= -0x8000) {
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 >= -0x80000000) {
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,700 +370,1154 @@ 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
- function utf8Length(str) {
563
- var c = 0, length = 0;
564
- for (var i = 0, l = str.length; i < l; i++) {
565
- c = str.charCodeAt(i);
566
- if (c < 0x80) {
567
- length += 1;
568
- }
569
- else if (c < 0x800) {
570
- length += 2;
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;
571
387
  }
572
- else if (c < 0xd800 || c >= 0xe000) {
573
- length += 3;
388
+ if ((byte & 0xe0) === 0xc0) {
389
+ string += String.fromCharCode(((byte & 0x1f) << 6) |
390
+ (bytes[++i] & 0x3f));
391
+ continue;
574
392
  }
575
- else {
576
- i++;
577
- length += 4;
393
+ if ((byte & 0xf0) === 0xe0) {
394
+ string += String.fromCharCode(((byte & 0x0f) << 12) |
395
+ ((bytes[++i] & 0x3f) << 6) |
396
+ ((bytes[++i] & 0x3f) << 0));
397
+ continue;
578
398
  }
579
- }
580
- return length;
581
- }
582
- function utf8Write(view, str, it) {
583
- var c = 0;
584
- for (var i = 0, l = str.length; i < l; i++) {
585
- c = str.charCodeAt(i);
586
- if (c < 0x80) {
587
- view[it.offset++] = c;
588
- }
589
- else if (c < 0x800) {
590
- view[it.offset++] = 0xc0 | (c >> 6);
591
- view[it.offset++] = 0x80 | (c & 0x3f);
592
- }
593
- else if (c < 0xd800 || c >= 0xe000) {
594
- view[it.offset++] = 0xe0 | (c >> 12);
595
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
596
- view[it.offset++] = 0x80 | (c & 0x3f);
597
- }
598
- else {
599
- i++;
600
- c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
601
- view[it.offset++] = 0xf0 | (c >> 18);
602
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
603
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
604
- 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;
605
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));
606
416
  }
417
+ it.offset += length;
418
+ return string;
607
419
  }
608
- function int8$1(bytes, value, it) {
609
- bytes[it.offset++] = value & 255;
420
+ function int8(bytes, it) {
421
+ return uint8(bytes, it) << 24 >> 24;
610
422
  }
611
- function uint8$1(bytes, value, it) {
612
- bytes[it.offset++] = value & 255;
423
+ function uint8(bytes, it) {
424
+ return bytes[it.offset++];
613
425
  }
614
- function int16$1(bytes, value, it) {
615
- bytes[it.offset++] = value & 255;
616
- bytes[it.offset++] = (value >> 8) & 255;
426
+ function int16(bytes, it) {
427
+ return uint16(bytes, it) << 16 >> 16;
617
428
  }
618
- function uint16$1(bytes, value, it) {
619
- bytes[it.offset++] = value & 255;
620
- bytes[it.offset++] = (value >> 8) & 255;
429
+ function uint16(bytes, it) {
430
+ return bytes[it.offset++] | bytes[it.offset++] << 8;
621
431
  }
622
- function int32$1(bytes, value, it) {
623
- bytes[it.offset++] = value & 255;
624
- bytes[it.offset++] = (value >> 8) & 255;
625
- bytes[it.offset++] = (value >> 16) & 255;
626
- 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;
627
434
  }
628
- function uint32$1(bytes, value, it) {
629
- const b4 = value >> 24;
630
- const b3 = value >> 16;
631
- const b2 = value >> 8;
632
- const b1 = value;
633
- bytes[it.offset++] = b1 & 255;
634
- bytes[it.offset++] = b2 & 255;
635
- bytes[it.offset++] = b3 & 255;
636
- bytes[it.offset++] = b4 & 255;
435
+ function uint32(bytes, it) {
436
+ return int32(bytes, it) >>> 0;
637
437
  }
638
- function int64$1(bytes, value, it) {
639
- const high = Math.floor(value / Math.pow(2, 32));
640
- const low = value >>> 0;
641
- uint32$1(bytes, low, it);
642
- uint32$1(bytes, high, it);
438
+ function float32(bytes, it) {
439
+ _int32[0] = int32(bytes, it);
440
+ return _float32[0];
643
441
  }
644
- function uint64$1(bytes, value, it) {
645
- const high = (value / Math.pow(2, 32)) >> 0;
646
- const low = value >>> 0;
647
- uint32$1(bytes, low, it);
648
- 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];
649
446
  }
650
- function float32$1(bytes, value, it) {
651
- 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;
652
451
  }
653
- function float64$1(bytes, value, it) {
654
- 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;
655
456
  }
656
- const _int32$1 = new Int32Array(2);
657
- const _float32$1 = new Float32Array(_int32$1.buffer);
658
- const _float64$1 = new Float64Array(_int32$1.buffer);
659
- function writeFloat32(bytes, value, it) {
660
- _float32$1[0] = value;
661
- 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];
662
461
  }
663
- function writeFloat64(bytes, value, it) {
664
- _float64$1[0] = value;
665
- int32$1(bytes, _int32$1[0 ], it);
666
- 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];
667
466
  }
668
- function boolean$1(bytes, value, it) {
669
- bytes[it.offset++] = value ? 1 : 0; // uint8
467
+ function boolean(bytes, it) {
468
+ return uint8(bytes, it) > 0;
670
469
  }
671
- function string$1(bytes, value, it) {
672
- // encode `null` strings as empty.
673
- if (!value) {
674
- value = "";
675
- }
676
- // let length = utf8Length(value);
677
- let length = Buffer.byteLength(value, "utf8");
678
- let size = 0;
679
- // fixstr
680
- if (length < 0x20) {
681
- bytes[it.offset++] = length | 0xa0;
682
- size = 1;
683
- }
684
- // str 8
685
- else if (length < 0x100) {
686
- bytes[it.offset++] = 0xd9;
687
- bytes[it.offset++] = length % 255;
688
- 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;
689
476
  }
690
- // str 16
691
- else if (length < 0x10000) {
692
- bytes[it.offset++] = 0xda;
693
- uint16$1(bytes, length, it);
694
- size = 3;
477
+ else if (prefix === 0xd9) {
478
+ length = uint8(bytes, it);
695
479
  }
696
- // str 32
697
- else if (length < 0x100000000) {
698
- bytes[it.offset++] = 0xdb;
699
- uint32$1(bytes, length, it);
700
- size = 5;
480
+ else if (prefix === 0xda) {
481
+ length = uint16(bytes, it);
701
482
  }
702
- else {
703
- throw new Error('String too long');
483
+ else if (prefix === 0xdb) {
484
+ length = uint32(bytes, it);
704
485
  }
705
- utf8Write(bytes, value, it);
706
- return size + length;
486
+ return utf8Read(bytes, it, length);
707
487
  }
708
- function number$1(bytes, value, it) {
709
- if (isNaN(value)) {
710
- 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;
711
493
  }
712
- else if (!isFinite(value)) {
713
- 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);
714
497
  }
715
- else if (value !== (value | 0)) {
716
- bytes[it.offset++] = 0xcb;
717
- writeFloat64(bytes, value, it);
718
- return 9;
719
- // TODO: encode float 32?
720
- // is it possible to differentiate between float32 / float64 here?
721
- // // float 32
722
- // bytes.push(0xca);
723
- // writeFloat32(bytes, value);
724
- // return 5;
498
+ else if (prefix === 0xcb) {
499
+ // float 64
500
+ return float64(bytes, it);
725
501
  }
726
- if (value >= 0) {
727
- // positive fixnum
728
- if (value < 0x80) {
729
- bytes[it.offset++] = value & 255; // uint8
730
- return 1;
731
- }
502
+ else if (prefix === 0xcc) {
732
503
  // uint 8
733
- if (value < 0x100) {
734
- bytes[it.offset++] = 0xcc;
735
- bytes[it.offset++] = value & 255; // uint8
736
- return 2;
737
- }
504
+ return uint8(bytes, it);
505
+ }
506
+ else if (prefix === 0xcd) {
738
507
  // uint 16
739
- if (value < 0x10000) {
740
- bytes[it.offset++] = 0xcd;
741
- uint16$1(bytes, value, it);
742
- return 3;
743
- }
508
+ return uint16(bytes, it);
509
+ }
510
+ else if (prefix === 0xce) {
744
511
  // uint 32
745
- if (value < 0x100000000) {
746
- bytes[it.offset++] = 0xce;
747
- uint32$1(bytes, value, it);
748
- return 5;
749
- }
512
+ return uint32(bytes, it);
513
+ }
514
+ else if (prefix === 0xcf) {
750
515
  // uint 64
751
- bytes[it.offset++] = 0xcf;
752
- uint64$1(bytes, value, it);
753
- return 9;
516
+ return uint64(bytes, it);
754
517
  }
755
- else {
756
- // negative fixnum
757
- if (value >= -0x20) {
758
- bytes[it.offset++] = 0xe0 | (value + 0x20);
759
- return 1;
760
- }
518
+ else if (prefix === 0xd0) {
761
519
  // int 8
762
- if (value >= -0x80) {
763
- bytes[it.offset++] = 0xd0;
764
- int8$1(bytes, value, it);
765
- return 2;
766
- }
520
+ return int8(bytes, it);
521
+ }
522
+ else if (prefix === 0xd1) {
767
523
  // int 16
768
- if (value >= -0x8000) {
769
- bytes[it.offset++] = 0xd1;
770
- int16$1(bytes, value, it);
771
- return 3;
772
- }
524
+ return int16(bytes, it);
525
+ }
526
+ else if (prefix === 0xd2) {
773
527
  // int 32
774
- if (value >= -0x80000000) {
775
- bytes[it.offset++] = 0xd2;
776
- int32$1(bytes, value, it);
777
- return 5;
778
- }
528
+ return int32(bytes, it);
529
+ }
530
+ else if (prefix === 0xd3) {
779
531
  // int 64
780
- bytes[it.offset++] = 0xd3;
781
- int64$1(bytes, value, it);
782
- return 9;
532
+ return int64(bytes, it);
533
+ }
534
+ else if (prefix > 0xdf) {
535
+ // negative fixint
536
+ return (0xff - prefix + 1) * -1;
783
537
  }
784
538
  }
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);
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
+ };
785
570
 
786
- var encode = /*#__PURE__*/Object.freeze({
787
- __proto__: null,
788
- utf8Length: utf8Length,
789
- utf8Write: utf8Write,
790
- int8: int8$1,
791
- uint8: uint8$1,
792
- int16: int16$1,
793
- uint16: uint16$1,
794
- int32: int32$1,
795
- uint32: uint32$1,
796
- int64: int64$1,
797
- uint64: uint64$1,
798
- float32: float32$1,
799
- float64: float64$1,
800
- writeFloat32: writeFloat32,
801
- writeFloat64: writeFloat64,
802
- boolean: boolean$1,
803
- string: string$1,
804
- number: number$1
805
- });
806
-
807
- class EncodeSchemaError extends Error {
808
- }
809
- function assertType(value, type, klass, field) {
810
- let typeofTarget;
811
- let allowNull = false;
812
- switch (type) {
813
- case "number":
814
- case "int8":
815
- case "uint8":
816
- case "int16":
817
- case "uint16":
818
- case "int32":
819
- case "uint32":
820
- case "int64":
821
- case "uint64":
822
- case "float32":
823
- case "float64":
824
- typeofTarget = "number";
825
- if (isNaN(value)) {
826
- console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
827
- }
828
- break;
829
- case "string":
830
- typeofTarget = "string";
831
- allowNull = true;
832
- break;
833
- case "boolean":
834
- // boolean is always encoded as true/false based on truthiness
835
- return;
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;
836
577
  }
837
- if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
838
- let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
839
- throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
578
+ if (definition.encode) {
579
+ encode[identifier] = definition.encode;
840
580
  }
841
- }
842
- function assertInstanceType(value, type, klass, field) {
843
- if (!(value instanceof type)) {
844
- throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${klass.constructor.name}#${field}`);
581
+ if (definition.decode) {
582
+ decode[identifier] = definition.decode;
845
583
  }
846
584
  }
847
-
848
- function encodePrimitiveType(type, bytes, value, klass, field, it) {
849
- assertType(value, type, klass, field);
850
- const encodeFunc = encode[type];
851
- if (encodeFunc) {
852
- encodeFunc(bytes, value, it);
853
- // encodeFunc(bytes, value);
854
- }
855
- else {
856
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
857
- }
585
+ function getType(identifier) {
586
+ return registeredTypes[identifier];
858
587
  }
859
- function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
860
- if (type[Symbol.metadata] !== undefined) {
861
- // TODO: move this to the `@type()` annotation
862
- assertInstanceType(value, type, ref, field);
863
- //
864
- // Encode refId for this instance.
865
- // The actual instance is going to be encoded on next `changeTree` iteration.
866
- //
867
- number$1(bytes, value[$changes].refId, it);
868
- // Try to encode inherited TYPE_ID if it's an ADD operation.
869
- if ((operation & OPERATION.ADD) === OPERATION.ADD) {
870
- encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
871
- }
872
- }
873
- else if (typeof (type) === "string") {
874
- //
875
- // Primitive values
876
- //
877
- encodePrimitiveType(type, bytes, value, ref, field, it);
878
- }
879
- else {
880
- //
881
- // Custom type (MapSchema, ArraySchema, etc)
882
- //
883
- const definition = getType(Object.keys(type)[0]);
884
- //
885
- // ensure a ArraySchema has been provided
886
- //
887
- assertInstanceType(ref[field], definition.constructor, ref, field);
888
- //
889
- // Encode refId for this instance.
890
- // The actual instance is going to be encoded on next `changeTree` iteration.
891
- //
892
- number$1(bytes, value[$changes].refId, it);
588
+ function defineCustomTypes(types) {
589
+ for (const identifier in types) {
590
+ registerType(identifier, types[identifier]);
893
591
  }
592
+ return (t) => type(t);
894
593
  }
895
- /**
896
- * Used for Schema instances.
897
- * @private
898
- */
899
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
900
- const ref = changeTree.ref;
901
- const metadata = ref['constructor'][Symbol.metadata];
902
- const field = metadata[index];
903
- const type = metadata[field].type;
904
- const value = ref[field];
905
- // "compress" field index + operation
906
- bytes[it.offset++] = (index | operation) & 255;
907
- // Do not encode value for DELETE operations
908
- if (operation === OPERATION.DELETE) {
909
- return;
910
- }
911
- // TODO: inline this function call small performance gain
912
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
913
- };
914
- /**
915
- * Used for collections (MapSchema, CollectionSchema, SetSchema)
916
- * @private
917
- */
918
- const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, operation, it) {
919
- const ref = changeTree.ref;
920
- // encode operation
921
- bytes[it.offset++] = operation & 255;
922
- // custom operations
923
- if (operation === OPERATION.CLEAR) {
924
- return;
925
- }
926
- // encode index
927
- number$1(bytes, field, it);
928
- // Do not encode value for DELETE operations
929
- if (operation === OPERATION.DELETE) {
930
- return;
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
+ }
931
611
  }
932
- //
933
- // encode "alias" for dynamic fields (maps)
934
- //
935
- if ((operation & OPERATION.ADD) == OPERATION.ADD) { // ADD or DELETE_AND_ADD
936
- 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) {
937
618
  //
938
- // MapSchema dynamic key
619
+ // TODO:
620
+ // cache "discoverTypes" results for each rootClass
621
+ // to avoid re-discovering types for each new context/room
939
622
  //
940
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
941
- string$1(bytes, dynamicIndex, it);
623
+ this.discoverTypes(rootClass);
942
624
  }
943
625
  }
944
- const type = changeTree.getType(field);
945
- const value = changeTree.getValue(field);
946
- // TODO: inline this function call small performance gain
947
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
948
- };
949
- /**
950
- * Used for collections (MapSchema, ArraySchema, etc.)
951
- * @private
952
- */
953
- const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
954
- const ref = changeTree.ref;
955
- if (hasView &&
956
- operation === OPERATION.DELETE &&
957
- typeof (changeTree.getType(field)) !== "string") {
958
- // encode delete by refId (array of schemas)
959
- bytes[it.offset++] = OPERATION.DELETE_BY_REFID;
960
- const value = ref['tmpItems'][field];
961
- const refId = value[$changes].refId;
962
- number$1(bytes, refId, it);
963
- return;
626
+ has(schema) {
627
+ return this.schemas.has(schema);
964
628
  }
965
- // encode operation
966
- bytes[it.offset++] = operation & 255;
967
- // custom operations
968
- if (operation === OPERATION.CLEAR) {
969
- return;
629
+ get(typeid) {
630
+ return this.types[typeid];
970
631
  }
971
- // encode index
972
- number$1(bytes, field, it);
973
- // Do not encode value for DELETE operations
974
- if (operation === OPERATION.DELETE) {
975
- return;
632
+ add(schema, typeid = this.schemas.size) {
633
+ // skip if already registered
634
+ if (this.schemas.has(schema)) {
635
+ return false;
636
+ }
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);
643
+ }
644
+ this.schemas.set(schema, typeid);
645
+ return true;
976
646
  }
977
- const type = changeTree.getType(field);
978
- const value = changeTree.getValue(field, isEncodeAll);
979
- // console.log("encodeArray -> ", {
980
- // ref: changeTree.ref.constructor.name,
981
- // field,
982
- // operation: OPERATION[operation],
983
- // value: value?.toJSON(),
984
- // items: ref.toJSON(),
985
- // });
986
- // TODO: inline this function call small performance gain
987
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
988
- };
989
-
990
- /**
991
- * Copyright (c) 2018 Endel Dreyer
992
- * Copyright (c) 2014 Ion Drive Software Ltd.
993
- *
994
- * Permission is hereby granted, free of charge, to any person obtaining a copy
995
- * of this software and associated documentation files (the "Software"), to deal
996
- * in the Software without restriction, including without limitation the rights
997
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
998
- * copies of the Software, and to permit persons to whom the Software is
999
- * furnished to do so, subject to the following conditions:
1000
- *
1001
- * The above copyright notice and this permission notice shall be included in all
1002
- * copies or substantial portions of the Software.
1003
- *
1004
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1005
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1006
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1007
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1008
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1009
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1010
- * SOFTWARE
1011
- */
1012
- function utf8Read(bytes, it, length) {
1013
- var string = '', chr = 0;
1014
- for (var i = it.offset, end = it.offset + length; i < end; i++) {
1015
- var byte = bytes[i];
1016
- if ((byte & 0x80) === 0x00) {
1017
- string += String.fromCharCode(byte);
1018
- continue;
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);
1019
653
  }
1020
- if ((byte & 0xe0) === 0xc0) {
1021
- string += String.fromCharCode(((byte & 0x1f) << 6) |
1022
- (bytes[++i] & 0x3f));
1023
- continue;
654
+ // skip if already registered
655
+ if (!this.add(klass)) {
656
+ return;
1024
657
  }
1025
- if ((byte & 0xf0) === 0xe0) {
1026
- string += String.fromCharCode(((byte & 0x0f) << 12) |
1027
- ((bytes[++i] & 0x3f) << 6) |
1028
- ((bytes[++i] & 0x3f) << 0));
1029
- continue;
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;
1030
674
  }
1031
- if ((byte & 0xf8) === 0xf0) {
1032
- chr = ((byte & 0x07) << 18) |
1033
- ((bytes[++i] & 0x3f) << 12) |
1034
- ((bytes[++i] & 0x3f) << 6) |
1035
- ((bytes[++i] & 0x3f) << 0);
1036
- if (chr >= 0x010000) { // surrogate pair
1037
- chr -= 0x010000;
1038
- string += String.fromCharCode((chr >>> 10) + 0xD800, (chr & 0x3FF) + 0xDC00);
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);
1039
692
  }
1040
693
  else {
1041
- string += String.fromCharCode(chr);
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);
1042
700
  }
1043
- continue;
1044
701
  }
1045
- console.error('Invalid byte ' + byte.toString(16));
1046
- // (do not throw error to avoid server/client from crashing due to hack attemps)
1047
- // throw new Error('Invalid byte ' + byte.toString(16));
1048
702
  }
1049
- it.offset += length;
1050
- return string;
1051
- }
1052
- function int8(bytes, it) {
1053
- return uint8(bytes, it) << 24 >> 24;
1054
- }
1055
- function uint8(bytes, it) {
1056
- return bytes[it.offset++];
1057
- }
1058
- function int16(bytes, it) {
1059
- return uint16(bytes, it) << 16 >> 16;
1060
- }
1061
- function uint16(bytes, it) {
1062
- return bytes[it.offset++] | bytes[it.offset++] << 8;
1063
- }
1064
- function int32(bytes, it) {
1065
- return bytes[it.offset++] | bytes[it.offset++] << 8 | bytes[it.offset++] << 16 | bytes[it.offset++] << 24;
1066
- }
1067
- function uint32(bytes, it) {
1068
- return int32(bytes, it) >>> 0;
1069
- }
1070
- function float32(bytes, it) {
1071
- return readFloat32(bytes, it);
1072
- }
1073
- function float64(bytes, it) {
1074
- return readFloat64(bytes, it);
1075
- }
1076
- function int64(bytes, it) {
1077
- const low = uint32(bytes, it);
1078
- const high = int32(bytes, it) * Math.pow(2, 32);
1079
- 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
+ }
1080
737
  }
1081
- function uint64(bytes, it) {
1082
- const low = uint32(bytes, it);
1083
- const high = uint32(bytes, it) * Math.pow(2, 32);
1084
- 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;
1085
745
  }
1086
- const _int32 = new Int32Array(2);
1087
- const _float32 = new Float32Array(_int32.buffer);
1088
- const _float64 = new Float64Array(_int32.buffer);
1089
- function readFloat32(bytes, it) {
1090
- _int32[0] = int32(bytes, it);
1091
- 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
+ }
1092
966
  }
1093
- function readFloat64(bytes, it) {
1094
- _int32[0 ] = int32(bytes, it);
1095
- _int32[1 ] = int32(bytes, it);
1096
- 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];
1097
973
  }
1098
- function boolean(bytes, it) {
1099
- 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
+ }
1100
978
  }
1101
- function string(bytes, it) {
1102
- const prefix = bytes[it.offset++];
1103
- let length;
1104
- if (prefix < 0xc0) {
1105
- // fixstr
1106
- 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
+ }
1107
1008
  }
1108
- else if (prefix === 0xd9) {
1109
- 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
+ }
1110
1027
  }
1111
- else if (prefix === 0xda) {
1112
- 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
+ }
1113
1058
  }
1114
- else if (prefix === 0xdb) {
1115
- 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
+ }
1116
1079
  }
1117
- return utf8Read(bytes, it, length);
1118
- }
1119
- function stringCheck(bytes, it) {
1120
- const prefix = bytes[it.offset];
1121
- return (
1122
- // fixstr
1123
- (prefix < 0xc0 && prefix > 0xa0) ||
1124
- // str 8
1125
- prefix === 0xd9 ||
1126
- // str 16
1127
- prefix === 0xda ||
1128
- // str 32
1129
- prefix === 0xdb);
1130
- }
1131
- function number(bytes, it) {
1132
- const prefix = bytes[it.offset++];
1133
- if (prefix < 0x80) {
1134
- // positive fixint
1135
- 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');
1136
1085
  }
1137
- else if (prefix === 0xca) {
1138
- // float 32
1139
- 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
+ }
1140
1116
  }
1141
- else if (prefix === 0xcb) {
1142
- // float 64
1143
- return readFloat64(bytes, it);
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);
1144
1389
  }
1145
- else if (prefix === 0xcc) {
1146
- // uint 8
1147
- return uint8(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
+ }
1148
1400
  }
1149
- else if (prefix === 0xcd) {
1150
- // uint 16
1151
- return uint16(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);
1152
1407
  }
1153
- else if (prefix === 0xce) {
1154
- // uint 32
1155
- return uint32(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;
1156
1419
  }
1157
- else if (prefix === 0xcf) {
1158
- // uint 64
1159
- return uint64(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;
1160
1435
  }
1161
- else if (prefix === 0xd0) {
1162
- // int 8
1163
- return int8(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;
1164
1441
  }
1165
- else if (prefix === 0xd1) {
1166
- // int 16
1167
- return int16(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
+ }
1168
1454
  }
1169
- else if (prefix === 0xd2) {
1170
- // int 32
1171
- return int32(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
+ }
1172
1488
  }
1173
- else if (prefix === 0xd3) {
1174
- // int 64
1175
- return int64(bytes, it);
1489
+ else {
1490
+ refOrIndex = field;
1176
1491
  }
1177
- else if (prefix > 0xdf) {
1178
- // negative fixint
1179
- return (0xff - prefix + 1) * -1;
1492
+ // encode operation
1493
+ bytes[it.offset++] = operation & 255;
1494
+ // custom operations
1495
+ if (operation === OPERATION.CLEAR ||
1496
+ operation === OPERATION.REVERSE) {
1497
+ return;
1180
1498
  }
1181
- }
1182
- function numberCheck(bytes, it) {
1183
- const prefix = bytes[it.offset];
1184
- // positive fixint - 0x00 - 0x7f
1185
- // float 32 - 0xca
1186
- // float 64 - 0xcb
1187
- // uint 8 - 0xcc
1188
- // uint 16 - 0xcd
1189
- // uint 32 - 0xce
1190
- // uint 64 - 0xcf
1191
- // int 8 - 0xd0
1192
- // int 16 - 0xd1
1193
- // int 32 - 0xd2
1194
- // int 64 - 0xd3
1195
- return (prefix < 0x80 ||
1196
- (prefix >= 0xca && prefix <= 0xd3));
1197
- }
1198
- function arrayCheck(bytes, it) {
1199
- return bytes[it.offset] < 0xa0;
1200
- // const prefix = bytes[it.offset] ;
1201
- // if (prefix < 0xa0) {
1202
- // return prefix;
1203
- // // array
1204
- // } else if (prefix === 0xdc) {
1205
- // it.offset += 2;
1206
- // } else if (0xdd) {
1207
- // it.offset += 4;
1208
- // }
1209
- // return prefix;
1210
- }
1211
- function switchStructureCheck(bytes, it) {
1212
- return (
1213
- // previous byte should be `SWITCH_TO_STRUCTURE`
1214
- bytes[it.offset - 1] === SWITCH_TO_STRUCTURE &&
1215
- // next byte should be a number
1216
- (bytes[it.offset] < 0x80 || (bytes[it.offset] >= 0xca && bytes[it.offset] <= 0xd3)));
1217
- }
1218
-
1219
- var decode = /*#__PURE__*/Object.freeze({
1220
- __proto__: null,
1221
- utf8Read: utf8Read,
1222
- int8: int8,
1223
- uint8: uint8,
1224
- int16: int16,
1225
- uint16: uint16,
1226
- int32: int32,
1227
- uint32: uint32,
1228
- float32: float32,
1229
- float64: float64,
1230
- int64: int64,
1231
- uint64: uint64,
1232
- readFloat32: readFloat32,
1233
- readFloat64: readFloat64,
1234
- boolean: boolean,
1235
- string: string,
1236
- stringCheck: stringCheck,
1237
- number: number,
1238
- numberCheck: numberCheck,
1239
- arrayCheck: arrayCheck,
1240
- switchStructureCheck: switchStructureCheck
1241
- });
1499
+ // encode index
1500
+ encode.number(bytes, refOrIndex, it);
1501
+ // Do not encode value for DELETE operations
1502
+ if (operation === OPERATION.DELETE) {
1503
+ return;
1504
+ }
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
+ };
1242
1517
 
1243
1518
  const DEFINITION_MISMATCH = -1;
1244
1519
  function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges) {
1245
- const $root = decoder.$root;
1520
+ const $root = decoder.root;
1246
1521
  const previousValue = ref[$getByIndex](index);
1247
1522
  let value;
1248
1523
  if ((operation & OPERATION.DELETE) === OPERATION.DELETE) {
@@ -1274,7 +1549,7 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1274
1549
  }
1275
1550
  if (operation === OPERATION.DELETE) ;
1276
1551
  else if (Schema.is(type)) {
1277
- const refId = number(bytes, it);
1552
+ const refId = decode.number(bytes, it);
1278
1553
  value = $root.refs.get(refId);
1279
1554
  if (previousValue) {
1280
1555
  const previousRefId = $root.refIds.get(previousValue);
@@ -1290,7 +1565,9 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1290
1565
  if (!value) {
1291
1566
  value = decoder.createInstanceOfType(childType);
1292
1567
  }
1293
- $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
+ ));
1294
1571
  }
1295
1572
  }
1296
1573
  else if (typeof (type) === "string") {
@@ -1301,7 +1578,7 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1301
1578
  }
1302
1579
  else {
1303
1580
  const typeDef = getType(Object.keys(type)[0]);
1304
- const refId = number(bytes, it);
1581
+ const refId = decode.number(bytes, it);
1305
1582
  const valueRef = ($root.refs.has(refId))
1306
1583
  ? previousValue || $root.refs.get(refId)
1307
1584
  : new typeDef.constructor();
@@ -1341,18 +1618,19 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1341
1618
  }
1342
1619
  const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1343
1620
  const first_byte = bytes[it.offset++];
1344
- const metadata = ref['constructor'][Symbol.metadata];
1621
+ const metadata = ref.constructor[Symbol.metadata];
1345
1622
  // "compressed" index + operation
1346
1623
  const operation = (first_byte >> 6) << 6;
1347
1624
  const index = first_byte % (operation || 255);
1348
1625
  // skip early if field is not defined
1349
1626
  const field = metadata[index];
1350
1627
  if (field === undefined) {
1628
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1351
1629
  return DEFINITION_MISMATCH;
1352
1630
  }
1353
- 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);
1354
1632
  if (value !== null && value !== undefined) {
1355
- ref[field] = value;
1633
+ ref[field.name] = value;
1356
1634
  }
1357
1635
  // add change
1358
1636
  if (previousValue !== value) {
@@ -1360,7 +1638,7 @@ const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1360
1638
  ref,
1361
1639
  refId: decoder.currentRefId,
1362
1640
  op: operation,
1363
- field: field,
1641
+ field: field.name,
1364
1642
  value,
1365
1643
  previousValue,
1366
1644
  });
@@ -1379,12 +1657,12 @@ const decodeKeyValueOperation = function (decoder, bytes, it, ref, allChanges) {
1379
1657
  ref.clear();
1380
1658
  return;
1381
1659
  }
1382
- const index = number(bytes, it);
1660
+ const index = decode.number(bytes, it);
1383
1661
  const type = ref[$childType];
1384
1662
  let dynamicIndex;
1385
1663
  if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
1386
1664
  if (typeof (ref['set']) === "function") {
1387
- dynamicIndex = string(bytes, it); // MapSchema
1665
+ dynamicIndex = decode.string(bytes, it); // MapSchema
1388
1666
  ref['setIndex'](index, dynamicIndex);
1389
1667
  }
1390
1668
  else {
@@ -1428,7 +1706,8 @@ const decodeKeyValueOperation = function (decoder, bytes, it, ref, allChanges) {
1428
1706
  };
1429
1707
  const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1430
1708
  // "uncompressed" index + operation (array/map items)
1431
- const operation = bytes[it.offset++];
1709
+ let operation = bytes[it.offset++];
1710
+ let index;
1432
1711
  if (operation === OPERATION.CLEAR) {
1433
1712
  //
1434
1713
  // When decoding:
@@ -1439,11 +1718,15 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1439
1718
  ref.clear();
1440
1719
  return;
1441
1720
  }
1721
+ else if (operation === OPERATION.REVERSE) {
1722
+ ref.reverse();
1723
+ return;
1724
+ }
1442
1725
  else if (operation === OPERATION.DELETE_BY_REFID) {
1443
1726
  // TODO: refactor here, try to follow same flow as below
1444
- const refId = number(bytes, it);
1445
- const previousValue = decoder.$root.refs.get(refId);
1446
- const index = ref.findIndex((value) => value === previousValue);
1727
+ const refId = decode.number(bytes, it);
1728
+ const previousValue = decoder.root.refs.get(refId);
1729
+ index = ref.findIndex((value) => value === previousValue);
1447
1730
  ref[$deleteByIndex](index);
1448
1731
  allChanges.push({
1449
1732
  ref,
@@ -1456,7 +1739,17 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1456
1739
  });
1457
1740
  return;
1458
1741
  }
1459
- 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
+ }
1460
1753
  const type = ref[$childType];
1461
1754
  let dynamicIndex = index;
1462
1755
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges);
@@ -1480,6 +1773,55 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1480
1773
  }
1481
1774
  };
1482
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
+
1483
1825
  var _a$4, _b$4;
1484
1826
  const DEFAULT_SORT = (a, b) => {
1485
1827
  const A = a.toString();
@@ -1530,6 +1872,7 @@ class ArraySchema {
1530
1872
  const proxy = new Proxy(this, {
1531
1873
  get: (obj, prop) => {
1532
1874
  if (typeof (prop) !== "symbol" &&
1875
+ // FIXME: d8 accuses this as low performance
1533
1876
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1534
1877
  ) {
1535
1878
  return this.items[prop];
@@ -1545,8 +1888,9 @@ class ArraySchema {
1545
1888
  }
1546
1889
  else {
1547
1890
  if (setValue[$changes]) {
1891
+ assertInstanceType(setValue, obj[$childType], obj, key);
1548
1892
  if (obj.items[key] !== undefined) {
1549
- if (setValue[$changes][$isNew]) {
1893
+ if (setValue[$changes].isNew) {
1550
1894
  this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
1551
1895
  }
1552
1896
  else {
@@ -1558,7 +1902,7 @@ class ArraySchema {
1558
1902
  }
1559
1903
  }
1560
1904
  }
1561
- else if (setValue[$changes][$isNew]) {
1905
+ else if (setValue[$changes].isNew) {
1562
1906
  this[$changes].indexedOperation(Number(key), OPERATION.ADD);
1563
1907
  }
1564
1908
  }
@@ -1591,7 +1935,10 @@ class ArraySchema {
1591
1935
  }
1592
1936
  });
1593
1937
  this[$changes] = new ChangeTree(proxy);
1594
- this.push.apply(this, items);
1938
+ this[$changes].indexes = {};
1939
+ if (items.length > 0) {
1940
+ this.push(...items);
1941
+ }
1595
1942
  return proxy;
1596
1943
  }
1597
1944
  set length(newLength) {
@@ -1610,14 +1957,19 @@ class ArraySchema {
1610
1957
  }
1611
1958
  push(...values) {
1612
1959
  let length = this.tmpItems.length;
1613
- values.forEach((value, i) => {
1614
- // 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];
1615
1964
  if (value === undefined || value === null) {
1965
+ // skip null values
1616
1966
  return;
1617
1967
  }
1618
- 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
+ }
1619
1972
  changeTree.indexedOperation(length, OPERATION.ADD, this.items.length);
1620
- // changeTree.indexes[length] = length;
1621
1973
  this.items.push(value);
1622
1974
  this.tmpItems.push(value);
1623
1975
  //
@@ -1625,8 +1977,9 @@ class ArraySchema {
1625
1977
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1626
1978
  //
1627
1979
  value[$changes]?.setParent(this, changeTree.root, length);
1628
- length++;
1629
- });
1980
+ }
1981
+ // length++;
1982
+ // });
1630
1983
  return length;
1631
1984
  }
1632
1985
  /**
@@ -1647,6 +2000,7 @@ class ArraySchema {
1647
2000
  }
1648
2001
  this[$changes].delete(index, undefined, this.items.length - 1);
1649
2002
  // this.tmpItems[index] = undefined;
2003
+ // this.tmpItems.pop();
1650
2004
  this.deletedIndexes[index] = true;
1651
2005
  return this.items.pop();
1652
2006
  }
@@ -1711,9 +2065,12 @@ class ArraySchema {
1711
2065
  //
1712
2066
  // TODO: do not use [$changes] at decoding time.
1713
2067
  //
1714
- changeTree.root?.changes.delete(changeTree);
1715
- changeTree.root?.allChanges.delete(changeTree);
1716
- 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
+ }
1717
2074
  });
1718
2075
  changeTree.discard(true);
1719
2076
  changeTree.operation(OPERATION.CLEAR);
@@ -1757,6 +2114,7 @@ class ArraySchema {
1757
2114
  const changeTree = this[$changes];
1758
2115
  changeTree.delete(index);
1759
2116
  changeTree.shiftAllChangeIndexes(-1, index);
2117
+ // this.deletedIndexes[index] = true;
1760
2118
  return this.items.shift();
1761
2119
  }
1762
2120
  /**
@@ -1837,10 +2195,12 @@ class ArraySchema {
1837
2195
  changeTree.shiftChangeIndexes(items.length);
1838
2196
  // new index
1839
2197
  if (changeTree.isFiltered) {
1840
- changeTree.filteredChanges.set(this.items.length, OPERATION.ADD);
2198
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2199
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1841
2200
  }
1842
2201
  else {
1843
- changeTree.allChanges.set(this.items.length, OPERATION.ADD);
2202
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2203
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1844
2204
  }
1845
2205
  // FIXME: should we use OPERATION.MOVE here instead?
1846
2206
  items.forEach((_, index) => {
@@ -1865,14 +2225,6 @@ class ArraySchema {
1865
2225
  lastIndexOf(searchElement, fromIndex = this.length - 1) {
1866
2226
  return this.items.lastIndexOf(searchElement, fromIndex);
1867
2227
  }
1868
- /**
1869
- * Determines whether all the members of an array satisfy the specified test.
1870
- * @param callbackfn A function that accepts up to three arguments. The every method calls
1871
- * the callbackfn function for each element in the array until the callbackfn returns a value
1872
- * which is coercible to the Boolean value false, or until the end of the array.
1873
- * @param thisArg An object to which the this keyword can refer in the callbackfn function.
1874
- * If thisArg is omitted, undefined is used as the this value.
1875
- */
1876
2228
  every(callbackfn, thisArg) {
1877
2229
  return this.items.every(callbackfn, thisArg);
1878
2230
  }
@@ -2151,6 +2503,7 @@ class MapSchema {
2151
2503
  this.$items = new Map();
2152
2504
  this.$indexes = new Map();
2153
2505
  this[$changes] = new ChangeTree(this);
2506
+ this[$changes].indexes = {};
2154
2507
  if (initialValues) {
2155
2508
  if (initialValues instanceof Map ||
2156
2509
  initialValues instanceof MapSchema) {
@@ -2177,6 +2530,9 @@ class MapSchema {
2177
2530
  if (value === undefined || value === null) {
2178
2531
  throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
2179
2532
  }
2533
+ else if (typeof (value) === "object" && this[$childType]) {
2534
+ assertInstanceType(value, this[$childType], this, key);
2535
+ }
2180
2536
  // Force "key" as string
2181
2537
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2182
2538
  key = key.toString();
@@ -2185,7 +2541,7 @@ class MapSchema {
2185
2541
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2186
2542
  const index = (isReplace)
2187
2543
  ? changeTree.indexes[key]
2188
- : changeTree.indexes[-1] ?? 0;
2544
+ : changeTree.indexes[$numFields] ?? 0;
2189
2545
  let operation = (isReplace)
2190
2546
  ? OPERATION.REPLACE
2191
2547
  : OPERATION.ADD;
@@ -2197,7 +2553,7 @@ class MapSchema {
2197
2553
  if (!isReplace) {
2198
2554
  this.$indexes.set(index, key);
2199
2555
  changeTree.indexes[key] = index;
2200
- changeTree.indexes[-1] = index + 1;
2556
+ changeTree.indexes[$numFields] = index + 1;
2201
2557
  }
2202
2558
  else if (!isRef &&
2203
2559
  this.$items.get(key) === value) {
@@ -2272,8 +2628,11 @@ class MapSchema {
2272
2628
  }
2273
2629
  [$onEncodeEnd]() {
2274
2630
  const changeTree = this[$changes];
2275
- const changes = changeTree.changes.entries();
2276
- 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];
2277
2636
  if (operation === OPERATION.DELETE) {
2278
2637
  const index = this[$getByIndex](fieldIndex);
2279
2638
  delete changeTree.indexes[index];
@@ -2300,110 +2659,23 @@ class MapSchema {
2300
2659
  cloned = Object.assign(new MapSchema(), this);
2301
2660
  }
2302
2661
  else {
2303
- // server-side
2304
- cloned = new MapSchema();
2305
- this.forEach((value, key) => {
2306
- if (value[$changes]) {
2307
- cloned.set(key, value['clone']());
2308
- }
2309
- else {
2310
- cloned.set(key, value);
2311
- }
2312
- });
2313
- }
2314
- return cloned;
2315
- }
2316
- }
2317
- registerType("map", { constructor: MapSchema });
2318
-
2319
- const DEFAULT_VIEW_TAG = -1;
2320
- class TypeContext {
2321
- /**
2322
- * For inheritance support
2323
- * Keeps track of which classes extends which. (parent -> children)
2324
- */
2325
- static { this.inheritedTypes = new Map(); }
2326
- static register(target) {
2327
- const parent = Object.getPrototypeOf(target);
2328
- if (parent !== Schema) {
2329
- let inherits = TypeContext.inheritedTypes.get(parent);
2330
- if (!inherits) {
2331
- inherits = new Set();
2332
- TypeContext.inheritedTypes.set(parent, inherits);
2333
- }
2334
- inherits.add(target);
2335
- }
2336
- }
2337
- constructor(rootClass) {
2338
- this.types = {};
2339
- this.schemas = new Map();
2340
- this.hasFilters = false;
2341
- if (rootClass) {
2342
- this.discoverTypes(rootClass);
2343
- }
2344
- }
2345
- has(schema) {
2346
- return this.schemas.has(schema);
2347
- }
2348
- get(typeid) {
2349
- return this.types[typeid];
2350
- }
2351
- add(schema, typeid = this.schemas.size) {
2352
- // skip if already registered
2353
- if (this.schemas.has(schema)) {
2354
- return false;
2355
- }
2356
- this.types[typeid] = schema;
2357
- this.schemas.set(schema, typeid);
2358
- return true;
2359
- }
2360
- getTypeId(klass) {
2361
- return this.schemas.get(klass);
2362
- }
2363
- discoverTypes(klass) {
2364
- if (!this.add(klass)) {
2365
- return;
2366
- }
2367
- // add classes inherited from this base class
2368
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2369
- this.discoverTypes(child);
2370
- });
2371
- // skip if no fields are defined for this class.
2372
- if (klass[Symbol.metadata] === undefined) {
2373
- klass[Symbol.metadata] = {};
2374
- }
2375
- // const metadata = Metadata.getFor(klass);
2376
- const metadata = klass[Symbol.metadata];
2377
- // if any schema/field has filters, mark "context" as having filters.
2378
- if (metadata[-2]) {
2379
- this.hasFilters = true;
2380
- }
2381
- for (const field in metadata) {
2382
- const fieldType = metadata[field].type;
2383
- if (typeof (fieldType) === "string") {
2384
- continue;
2385
- }
2386
- if (Array.isArray(fieldType)) {
2387
- const type = fieldType[0];
2388
- if (type === "string") {
2389
- continue;
2662
+ // server-side
2663
+ cloned = new MapSchema();
2664
+ this.forEach((value, key) => {
2665
+ if (value[$changes]) {
2666
+ cloned.set(key, value['clone']());
2390
2667
  }
2391
- this.discoverTypes(type);
2392
- }
2393
- else if (typeof (fieldType) === "function") {
2394
- this.discoverTypes(fieldType);
2395
- }
2396
- else {
2397
- const type = Object.values(fieldType)[0];
2398
- // skip primitive types
2399
- if (typeof (type) === "string") {
2400
- continue;
2668
+ else {
2669
+ cloned.set(key, value);
2401
2670
  }
2402
- this.discoverTypes(type);
2403
- }
2671
+ });
2404
2672
  }
2673
+ return cloned;
2405
2674
  }
2406
2675
  }
2676
+ registerType("map", { constructor: MapSchema });
2677
+
2678
+ const DEFAULT_VIEW_TAG = -1;
2407
2679
  /**
2408
2680
  * [See documentation](https://docs.colyseus.io/state/schema/)
2409
2681
  *
@@ -2430,8 +2702,8 @@ class TypeContext {
2430
2702
  // // detect index for this field, considering inheritance
2431
2703
  // //
2432
2704
  // const parent = Object.getPrototypeOf(context.metadata);
2433
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2434
- // ?? (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
2435
2707
  // ?? -1; // no fields defined
2436
2708
  // fieldIndex++;
2437
2709
  // if (
@@ -2551,18 +2823,20 @@ function view(tag = DEFAULT_VIEW_TAG) {
2551
2823
  const constructor = target.constructor;
2552
2824
  const parentClass = Object.getPrototypeOf(constructor);
2553
2825
  const parentMetadata = parentClass[Symbol.metadata];
2826
+ // TODO: use Metadata.initialize()
2554
2827
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2555
- if (!metadata[fieldName]) {
2556
- //
2557
- // detect index for this field, considering inheritance
2558
- //
2559
- metadata[fieldName] = {
2560
- type: undefined,
2561
- index: (metadata[-1] // current structure already has fields defined
2562
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2563
- ?? -1) + 1 // no fields defined
2564
- };
2565
- }
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
+ // }
2566
2840
  Metadata.setTag(metadata, fieldName, tag);
2567
2841
  };
2568
2842
  }
@@ -2576,17 +2850,17 @@ function type(type, options) {
2576
2850
  TypeContext.register(constructor);
2577
2851
  const parentClass = Object.getPrototypeOf(constructor);
2578
2852
  const parentMetadata = parentClass[Symbol.metadata];
2579
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2580
- let fieldIndex;
2853
+ const metadata = Metadata.initialize(constructor);
2854
+ let fieldIndex = metadata[field];
2581
2855
  /**
2582
2856
  * skip if descriptor already exists for this field (`@deprecated()`)
2583
2857
  */
2584
- if (metadata[field]) {
2585
- if (metadata[field].deprecated) {
2858
+ if (metadata[fieldIndex] !== undefined) {
2859
+ if (metadata[fieldIndex].deprecated) {
2586
2860
  // do not create accessors for deprecated properties.
2587
2861
  return;
2588
2862
  }
2589
- else if (metadata[field].descriptor !== undefined) {
2863
+ else if (metadata[fieldIndex].type !== undefined) {
2590
2864
  // trying to define same property multiple times across inheritance.
2591
2865
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
2592
2866
  try {
@@ -2597,16 +2871,13 @@ function type(type, options) {
2597
2871
  throw new Error(`${e.message} ${definitionAtLine}`);
2598
2872
  }
2599
2873
  }
2600
- else {
2601
- fieldIndex = metadata[field].index;
2602
- }
2603
2874
  }
2604
2875
  else {
2605
2876
  //
2606
2877
  // detect index for this field, considering inheritance
2607
2878
  //
2608
- fieldIndex = metadata[-1] // current structure already has fields defined
2609
- ?? (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
2610
2881
  ?? -1; // no fields defined
2611
2882
  fieldIndex++;
2612
2883
  }
@@ -2625,15 +2896,15 @@ function type(type, options) {
2625
2896
  const childType = (complexTypeKlass)
2626
2897
  ? Object.values(type)[0]
2627
2898
  : type;
2628
- 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));
2629
2900
  }
2630
2901
  };
2631
2902
  }
2632
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass, metadata, field) {
2903
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
2633
2904
  return {
2634
2905
  get: function () { return this[fieldCached]; },
2635
2906
  set: function (value) {
2636
- const previousValue = this[fieldCached] || undefined;
2907
+ const previousValue = this[fieldCached] ?? undefined;
2637
2908
  // skip if value is the same as cached.
2638
2909
  if (value === previousValue) {
2639
2910
  return;
@@ -2651,22 +2922,27 @@ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass,
2651
2922
  }
2652
2923
  value[$childType] = type;
2653
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];
2654
2932
  //
2655
2933
  // Replacing existing "ref", remove it from root.
2656
2934
  // TODO: if there are other references to this instance, we should not remove it from root.
2657
2935
  //
2658
2936
  if (previousValue !== undefined && previousValue[$changes]) {
2659
- this[$changes].root?.remove(previousValue[$changes]);
2937
+ changeTree.root?.remove(previousValue[$changes]);
2660
2938
  }
2661
2939
  // flag the change for encoding.
2662
- this.constructor[$track](this[$changes], fieldIndex, OPERATION.ADD);
2940
+ this.constructor[$track](changeTree, fieldIndex, OPERATION.ADD);
2663
2941
  //
2664
2942
  // call setParent() recursively for this and its child
2665
2943
  // structures.
2666
2944
  //
2667
- if (value[$changes]) {
2668
- value[$changes].setParent(this, this[$changes].root, metadata[field].index);
2669
- }
2945
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
2670
2946
  }
2671
2947
  else if (previousValue !== undefined) {
2672
2948
  //
@@ -2693,20 +2969,22 @@ function deprecated(throws = true) {
2693
2969
  const parentClass = Object.getPrototypeOf(constructor);
2694
2970
  const parentMetadata = parentClass[Symbol.metadata];
2695
2971
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2696
- if (!metadata[field]) {
2697
- //
2698
- // detect index for this field, considering inheritance
2699
- //
2700
- metadata[field] = {
2701
- type: undefined,
2702
- index: (metadata[-1] // current structure already has fields defined
2703
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2704
- ?? -1) + 1 // no fields defined
2705
- };
2706
- }
2707
- 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;
2708
2985
  if (throws) {
2709
- metadata[field].descriptor = {
2986
+ metadata[$descriptors] ??= {};
2987
+ metadata[$descriptors][field] = {
2710
2988
  get: function () { throw new Error(`${field} is deprecated.`); },
2711
2989
  set: function (value) { },
2712
2990
  enumerable: false,
@@ -2714,8 +2992,8 @@ function deprecated(throws = true) {
2714
2992
  };
2715
2993
  }
2716
2994
  // flag metadata[field] as non-enumerable
2717
- Object.defineProperty(metadata, field, {
2718
- value: metadata[field],
2995
+ Object.defineProperty(metadata, fieldIndex, {
2996
+ value: metadata[fieldIndex],
2719
2997
  enumerable: false,
2720
2998
  configurable: true
2721
2999
  });
@@ -2727,6 +3005,37 @@ function defineTypes(target, fields, options) {
2727
3005
  }
2728
3006
  return target;
2729
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
+ }
2730
3039
 
2731
3040
  function getIndent(level) {
2732
3041
  return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
@@ -2737,15 +3046,18 @@ function dumpChanges(schema) {
2737
3046
  ops: {},
2738
3047
  refs: []
2739
3048
  };
2740
- $root.changes.forEach((operations, changeTree) => {
3049
+ // for (const refId in $root.changes) {
3050
+ $root.changes.forEach(changeTree => {
3051
+ const changes = changeTree.indexedOperations;
2741
3052
  dump.refs.push(`refId#${changeTree.refId}`);
2742
- operations.forEach((op, index) => {
3053
+ for (const index in changes) {
3054
+ const op = changes[index];
2743
3055
  const opName = OPERATION[op];
2744
3056
  if (!dump.ops[opName]) {
2745
3057
  dump.ops[opName] = 0;
2746
3058
  }
2747
3059
  dump.ops[OPERATION[op]]++;
2748
- });
3060
+ }
2749
3061
  });
2750
3062
  return dump;
2751
3063
  }
@@ -2771,6 +3083,7 @@ var _a$2, _b$2;
2771
3083
  class Schema {
2772
3084
  static { this[_a$2] = encodeSchemaOperation; }
2773
3085
  static { this[_b$2] = decodeSchemaOperation; }
3086
+ // public [$changes]: ChangeTree;
2774
3087
  /**
2775
3088
  * Assign the property descriptors required to track changes on this instance.
2776
3089
  * @param instance
@@ -2781,35 +3094,7 @@ class Schema {
2781
3094
  enumerable: false,
2782
3095
  writable: true
2783
3096
  });
2784
- const metadata = instance.constructor[Symbol.metadata];
2785
- // Define property descriptors
2786
- for (const field in metadata) {
2787
- if (metadata[field].descriptor) {
2788
- // for encoder
2789
- Object.defineProperty(instance, `_${field}`, {
2790
- value: undefined,
2791
- writable: true,
2792
- enumerable: false,
2793
- configurable: true,
2794
- });
2795
- Object.defineProperty(instance, field, metadata[field].descriptor);
2796
- }
2797
- else {
2798
- // for decoder
2799
- Object.defineProperty(instance, field, {
2800
- value: undefined,
2801
- writable: true,
2802
- enumerable: true,
2803
- configurable: true,
2804
- });
2805
- }
2806
- // Object.defineProperty(instance, field, {
2807
- // ...instance.constructor[Symbol.metadata][field].descriptor
2808
- // });
2809
- // if (args[0]?.hasOwnProperty(field)) {
2810
- // instance[field] = args[0][field];
2811
- // }
2812
- }
3097
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2813
3098
  }
2814
3099
  static is(type) {
2815
3100
  return typeof (type[Symbol.metadata]) === "object";
@@ -2833,7 +3118,7 @@ class Schema {
2833
3118
  */
2834
3119
  static [$filter](ref, index, view) {
2835
3120
  const metadata = ref.constructor[Symbol.metadata];
2836
- const tag = metadata[metadata[index]].tag;
3121
+ const tag = metadata[index]?.tag;
2837
3122
  if (view === undefined) {
2838
3123
  // shared pass/encode: encode if doesn't have a tag
2839
3124
  return tag === undefined;
@@ -2854,12 +3139,16 @@ class Schema {
2854
3139
  }
2855
3140
  // allow inherited classes to have a constructor
2856
3141
  constructor(...args) {
3142
+ //
3143
+ // inline
3144
+ // Schema.initialize(this);
3145
+ //
2857
3146
  Schema.initialize(this);
2858
3147
  //
2859
3148
  // Assign initial values
2860
3149
  //
2861
3150
  if (args[0]) {
2862
- this.assign(args[0]);
3151
+ Object.assign(this, args[0]);
2863
3152
  }
2864
3153
  }
2865
3154
  assign(props) {
@@ -2873,7 +3162,8 @@ class Schema {
2873
3162
  * @param operation OPERATION to perform (detected automatically)
2874
3163
  */
2875
3164
  setDirty(property, operation) {
2876
- this[$changes].change(this.constructor[Symbol.metadata][property].index, operation);
3165
+ const metadata = this.constructor[Symbol.metadata];
3166
+ this[$changes].change(metadata[metadata[property]].index, operation);
2877
3167
  }
2878
3168
  clone() {
2879
3169
  const cloned = new (this.constructor);
@@ -2882,7 +3172,9 @@ class Schema {
2882
3172
  // TODO: clone all properties, not only annotated ones
2883
3173
  //
2884
3174
  // for (const field in this) {
2885
- for (const field in metadata) {
3175
+ for (const fieldIndex in metadata) {
3176
+ // const field = metadata[metadata[fieldIndex]].name;
3177
+ const field = metadata[fieldIndex].name;
2886
3178
  if (typeof (this[field]) === "object" &&
2887
3179
  typeof (this[field]?.clone) === "function") {
2888
3180
  // deep clone
@@ -2896,10 +3188,11 @@ class Schema {
2896
3188
  return cloned;
2897
3189
  }
2898
3190
  toJSON() {
2899
- const metadata = this.constructor[Symbol.metadata];
2900
3191
  const obj = {};
2901
- for (const fieldName in metadata) {
2902
- const field = metadata[fieldName];
3192
+ const metadata = this.constructor[Symbol.metadata];
3193
+ for (const index in metadata) {
3194
+ const field = metadata[index];
3195
+ const fieldName = field.name;
2903
3196
  if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
2904
3197
  obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
2905
3198
  ? this[fieldName]['toJSON']()
@@ -2912,18 +3205,27 @@ class Schema {
2912
3205
  this[$changes].discardAll();
2913
3206
  }
2914
3207
  [$getByIndex](index) {
2915
- return this[this.constructor[Symbol.metadata][index]];
3208
+ const metadata = this.constructor[Symbol.metadata];
3209
+ return this[metadata[index].name];
2916
3210
  }
2917
3211
  [$deleteByIndex](index) {
2918
- this[this.constructor[Symbol.metadata][index]] = undefined;
3212
+ const metadata = this.constructor[Symbol.metadata];
3213
+ this[metadata[index].name] = undefined;
2919
3214
  }
2920
- static debugRefIds(instance, jsonContents = true, level = 0) {
3215
+ /**
3216
+ * Inspect the `refId` of all Schema instances in the tree. Optionally display the contents of the instance.
3217
+ *
3218
+ * @param instance Schema instance
3219
+ * @param showContents display JSON contents of the instance
3220
+ * @returns
3221
+ */
3222
+ static debugRefIds(instance, showContents = false, level = 0) {
2921
3223
  const ref = instance;
2922
3224
  const changeTree = ref[$changes];
2923
- const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
3225
+ const contents = (showContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2924
3226
  let output = "";
2925
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
2926
- changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
3227
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
3228
+ changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, showContents, level + 1));
2927
3229
  return output;
2928
3230
  }
2929
3231
  /**
@@ -2940,30 +3242,40 @@ class Schema {
2940
3242
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
2941
3243
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
2942
3244
  function dumpChangeSet(changeSet) {
2943
- Array.from(changeSet)
2944
- .sort((a, b) => a[0] - b[0])
2945
- .forEach(([index, operation]) => output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3245
+ changeSet.operations
3246
+ .filter(op => op)
3247
+ .forEach((index) => {
3248
+ const operation = changeTree.indexedOperations[index];
3249
+ console.log({ index, operation });
3250
+ output += `- [${index}]: ${OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3251
+ });
2946
3252
  }
2947
3253
  dumpChangeSet(changeSet);
2948
3254
  // display filtered changes
2949
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3255
+ if (!isEncodeAll &&
3256
+ changeTree.filteredChanges &&
3257
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
2950
3258
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
2951
3259
  dumpChangeSet(changeTree.filteredChanges);
2952
3260
  }
2953
3261
  // display filtered changes
2954
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3262
+ if (isEncodeAll &&
3263
+ changeTree.allFilteredChanges &&
3264
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
2955
3265
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
2956
3266
  dumpChangeSet(changeTree.allFilteredChanges);
2957
3267
  }
2958
3268
  return output;
2959
3269
  }
2960
- static debugChangesDeep(ref) {
3270
+ static debugChangesDeep(ref, changeSetName = "changes") {
2961
3271
  let output = "";
2962
3272
  const rootChangeTree = ref[$changes];
3273
+ const root = rootChangeTree.root;
2963
3274
  const changeTrees = new Map();
2964
- let totalInstances = 0;
3275
+ const instanceRefIds = [];
2965
3276
  let totalOperations = 0;
2966
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3277
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3278
+ const changeTree = root.changeTrees[refId];
2967
3279
  let includeChangeTree = false;
2968
3280
  let parentChangeTrees = [];
2969
3281
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -2981,14 +3293,14 @@ class Schema {
2981
3293
  }
2982
3294
  }
2983
3295
  if (includeChangeTree) {
2984
- totalInstances += 1;
2985
- totalOperations += changes.size;
3296
+ instanceRefIds.push(changeTree.refId);
3297
+ totalOperations += Object.keys(changes).length;
2986
3298
  changeTrees.set(changeTree, parentChangeTrees.reverse());
2987
3299
  }
2988
3300
  }
2989
3301
  output += "---\n";
2990
3302
  output += `root refId: ${rootChangeTree.refId}\n`;
2991
- output += `Total instances: ${totalInstances}\n`;
3303
+ output += `Total instances: ${instanceRefIds.length} (refIds: ${instanceRefIds.join(", ")})\n`;
2992
3304
  output += `Total changes: ${totalOperations}\n`;
2993
3305
  output += "---\n";
2994
3306
  // based on root.changes, display a tree of changes that has the "ref" instance as parent
@@ -3000,12 +3312,13 @@ class Schema {
3000
3312
  visitedParents.add(parentChangeTree);
3001
3313
  }
3002
3314
  });
3003
- const changes = changeTree.changes;
3315
+ const changes = changeTree.indexedOperations;
3004
3316
  const level = parentChangeTrees.length;
3005
3317
  const indent = getIndent(level);
3006
3318
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3007
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3008
- for (const [index, operation] of changes) {
3319
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3320
+ for (const index in changes) {
3321
+ const operation = changes[index];
3009
3322
  output += `${getIndent(level + 1)}${OPERATION[operation]}: ${index}\n`;
3010
3323
  }
3011
3324
  }
@@ -3039,6 +3352,7 @@ class CollectionSchema {
3039
3352
  this.$indexes = new Map();
3040
3353
  this.$refId = 0;
3041
3354
  this[$changes] = new ChangeTree(this);
3355
+ this[$changes].indexes = {};
3042
3356
  if (initialValues) {
3043
3357
  initialValues.forEach((v) => this.add(v));
3044
3358
  }
@@ -3194,6 +3508,7 @@ class SetSchema {
3194
3508
  this.$indexes = new Map();
3195
3509
  this.$refId = 0;
3196
3510
  this[$changes] = new ChangeTree(this);
3511
+ this[$changes].indexes = {};
3197
3512
  if (initialValues) {
3198
3513
  initialValues.forEach((v) => this.add(v));
3199
3514
  }
@@ -3349,6 +3664,8 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
3349
3664
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3350
3665
  PERFORMANCE OF THIS SOFTWARE.
3351
3666
  ***************************************************************************** */
3667
+ /* global Reflect, Promise, SuppressedError, Symbol */
3668
+
3352
3669
 
3353
3670
  function __decorate(decorators, target, key, desc) {
3354
3671
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3362,37 +3679,127 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
3362
3679
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3363
3680
  };
3364
3681
 
3682
+ function spliceOne(arr, index) {
3683
+ // manually splice an array
3684
+ if (index === -1 || index >= arr.length) {
3685
+ return false;
3686
+ }
3687
+ const len = arr.length - 1;
3688
+ for (let i = index; i < len; i++) {
3689
+ arr[i] = arr[i + 1];
3690
+ }
3691
+ arr.length = len;
3692
+ return true;
3693
+ }
3694
+
3695
+ class Root {
3696
+ constructor(types) {
3697
+ this.types = types;
3698
+ this.nextUniqueId = 0;
3699
+ this.refCount = {};
3700
+ this.changeTrees = {};
3701
+ // all changes
3702
+ this.allChanges = [];
3703
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3704
+ // pending changes to be encoded
3705
+ this.changes = [];
3706
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3707
+ }
3708
+ getNextUniqueId() {
3709
+ return this.nextUniqueId++;
3710
+ }
3711
+ add(changeTree) {
3712
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3713
+ changeTree.ensureRefId();
3714
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3715
+ if (isNewChangeTree) {
3716
+ this.changeTrees[changeTree.refId] = changeTree;
3717
+ }
3718
+ const previousRefCount = this.refCount[changeTree.refId];
3719
+ if (previousRefCount === 0) {
3720
+ //
3721
+ // When a ChangeTree is re-added, it means that it was previously removed.
3722
+ // We need to re-add all changes to the `changes` map.
3723
+ //
3724
+ const ops = changeTree.allChanges.operations;
3725
+ let len = ops.length;
3726
+ while (len--) {
3727
+ changeTree.indexedOperations[ops[len]] = OPERATION.ADD;
3728
+ setOperationAtIndex(changeTree.changes, len);
3729
+ }
3730
+ }
3731
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3732
+ return isNewChangeTree;
3733
+ }
3734
+ remove(changeTree) {
3735
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3736
+ if (refCount <= 0) {
3737
+ //
3738
+ // Only remove "root" reference if it's the last reference
3739
+ //
3740
+ changeTree.root = undefined;
3741
+ delete this.changeTrees[changeTree.refId];
3742
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3743
+ this.removeChangeFromChangeSet("changes", changeTree);
3744
+ if (changeTree.filteredChanges) {
3745
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3746
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3747
+ }
3748
+ this.refCount[changeTree.refId] = 0;
3749
+ }
3750
+ else {
3751
+ this.refCount[changeTree.refId] = refCount;
3752
+ }
3753
+ changeTree.forEachChild((child, _) => this.remove(child));
3754
+ return refCount;
3755
+ }
3756
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3757
+ const changeSet = this[changeSetName];
3758
+ const index = changeSet.indexOf(changeTree);
3759
+ if (index !== -1) {
3760
+ spliceOne(changeSet, index);
3761
+ // changeSet[index] = undefined;
3762
+ }
3763
+ }
3764
+ clear() {
3765
+ this.changes.length = 0;
3766
+ }
3767
+ }
3768
+
3365
3769
  class Encoder {
3366
3770
  static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3367
- constructor(root) {
3771
+ constructor(state) {
3368
3772
  this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3369
- this.setRoot(root);
3370
3773
  //
3371
3774
  // TODO: cache and restore "Context" based on root schema
3372
3775
  // (to avoid creating a new context for every new room)
3373
3776
  //
3374
- this.context = new TypeContext(root.constructor);
3777
+ this.context = new TypeContext(state.constructor);
3778
+ this.root = new Root(this.context);
3779
+ this.setState(state);
3375
3780
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3376
3781
  // this.context.schemas.forEach((id, schema) => {
3377
3782
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3378
3783
  // });
3379
3784
  }
3380
- setRoot(state) {
3381
- this.root = new Root();
3785
+ setState(state) {
3382
3786
  this.state = state;
3383
- state[$changes].setRoot(this.root);
3787
+ this.state[$changes].setRoot(this.root);
3384
3788
  }
3385
- encode(it = { offset: 0 }, view, bytes = this.sharedBuffer, changeTrees = this.root.changes) {
3386
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3387
- const isEncodeAll = this.root.allChanges === changeTrees;
3789
+ 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
3790
+ ) {
3388
3791
  const hasView = (view !== undefined);
3389
3792
  const rootChangeTree = this.state[$changes];
3390
- const changeTreesIterator = changeTrees.entries();
3391
- for (const [changeTree, changes] of changeTreesIterator) {
3793
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3794
+ const changeTrees = this.root[changeSetName];
3795
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3796
+ const changeTree = changeTrees[i];
3797
+ const operations = changeTree[changeSetName];
3392
3798
  const ref = changeTree.ref;
3393
- const ctor = ref['constructor'];
3799
+ const ctor = ref.constructor;
3394
3800
  const encoder = ctor[$encoder];
3395
3801
  const filter = ctor[$filter];
3802
+ const metadata = ctor[Symbol.metadata];
3396
3803
  if (hasView) {
3397
3804
  if (!view.items.has(changeTree)) {
3398
3805
  view.invisible.add(changeTree);
@@ -3403,12 +3810,18 @@ class Encoder {
3403
3810
  }
3404
3811
  }
3405
3812
  // skip root `refId` if it's the first change tree
3406
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
3407
- bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3408
- number$1(bytes, changeTree.refId, it);
3813
+ // (unless it "hasView", which will need to revisit the root)
3814
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3815
+ buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3816
+ encode.number(buffer, changeTree.refId, it);
3409
3817
  }
3410
- const changesIterator = changes.entries();
3411
- for (const [fieldIndex, operation] of changesIterator) {
3818
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3819
+ const fieldIndex = operations.operations[j];
3820
+ const operation = (fieldIndex < 0)
3821
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3822
+ : (isEncodeAll)
3823
+ ? OPERATION.ADD
3824
+ : changeTree.indexedOperations[fieldIndex];
3412
3825
  //
3413
3826
  // first pass (encodeAll), identify "filtered" operations without encoding them
3414
3827
  // they will be encoded per client, based on their view.
@@ -3416,93 +3829,109 @@ class Encoder {
3416
3829
  // TODO: how can we optimize filtering out "encode all" operations?
3417
3830
  // TODO: avoid checking if no view tags were defined
3418
3831
  //
3419
- if (filter && !filter(ref, fieldIndex, view)) {
3420
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3832
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3421
3833
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3422
3834
  // view?.invisible.add(changeTree);
3423
3835
  continue;
3424
3836
  }
3425
- // console.log("WILL ENCODE", {
3426
- // ref: changeTree.ref.constructor.name,
3427
- // fieldIndex,
3428
- // operation: OPERATION[operation],
3429
- // });
3430
- encoder(this, bytes, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3837
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3431
3838
  }
3839
+ // if (shouldDiscardChanges) {
3840
+ // changeTree.discard();
3841
+ // changeTree.isNew = false; // Not a new instance anymore
3842
+ // }
3432
3843
  }
3433
- if (it.offset > bytes.byteLength) {
3434
- const newSize = getNextPowerOf2(this.sharedBuffer.byteLength * 2);
3435
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + bytes.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
3844
+ if (it.offset > buffer.byteLength) {
3845
+ const newSize = getNextPowerOf2(buffer.byteLength * 2);
3846
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
3847
+
3848
+ import { Encoder } from "@colyseus/schema";
3849
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
3850
+ `);
3436
3851
  //
3437
3852
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
3438
3853
  //
3439
- this.sharedBuffer = Buffer.allocUnsafeSlow(newSize);
3440
- return this.encode({ offset: initialOffset }, view);
3854
+ buffer = Buffer.alloc(newSize);
3855
+ // assign resized buffer to local sharedBuffer
3856
+ if (buffer === this.sharedBuffer) {
3857
+ this.sharedBuffer = buffer;
3858
+ }
3859
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3441
3860
  }
3442
3861
  else {
3443
3862
  //
3444
3863
  // only clear changes after making sure buffer resize is not required.
3445
3864
  //
3446
- if (!isEncodeAll && !hasView) {
3865
+ if (shouldDiscardChanges) {
3447
3866
  //
3448
- // FIXME: avoid iterating over change trees twice.
3867
+ // TODO: avoid iterating over change trees twice.
3449
3868
  //
3450
- this.onEndEncode(changeTrees);
3869
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3870
+ const changeTree = changeTrees[i];
3871
+ changeTree.discard();
3872
+ changeTree.isNew = false; // Not a new instance anymore
3873
+ }
3451
3874
  }
3452
- // return bytes;
3453
- return bytes.slice(0, it.offset);
3875
+ return buffer.subarray(0, it.offset);
3454
3876
  }
3455
3877
  }
3456
- encodeAll(it = { offset: 0 }) {
3457
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
3458
- // Array.from(this.$root.allChanges.entries()).map((item) => {
3459
- // console.log("->", item[0].refId, item[0].ref.toJSON());
3460
- // });
3461
- return this.encode(it, undefined, this.sharedBuffer, this.root.allChanges);
3878
+ encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3879
+ return this.encode(it, undefined, buffer, "allChanges", true);
3462
3880
  }
3463
3881
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3464
3882
  const viewOffset = it.offset;
3465
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
3466
- // this.debugAllFilteredChanges();
3467
3883
  // try to encode "filtered" changes
3468
- this.encode(it, view, bytes, this.root.allFilteredChanges);
3884
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3469
3885
  return Buffer.concat([
3470
- bytes.slice(0, sharedOffset),
3471
- bytes.slice(viewOffset, it.offset)
3886
+ bytes.subarray(0, sharedOffset),
3887
+ bytes.subarray(viewOffset, it.offset)
3472
3888
  ]);
3473
3889
  }
3474
- // debugAllFilteredChanges() {
3475
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
3476
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
3477
- // if (Array.isArray(item[0].ref.toJSON())) {
3478
- // item[1].forEach((op, key) => {
3479
- // console.log(" ->", { key, op: OPERATION[op] });
3480
- // })
3481
- // }
3482
- // });
3483
- // }
3890
+ debugChanges(field) {
3891
+ const rootChangeSet = (typeof (field) === "string")
3892
+ ? this.root[field]
3893
+ : field;
3894
+ rootChangeSet.forEach((changeTree) => {
3895
+ const changeSet = changeTree[field];
3896
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3897
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3898
+ for (const index in changeSet) {
3899
+ const op = changeSet[index];
3900
+ console.log(" ->", {
3901
+ index,
3902
+ field: metadata?.[index],
3903
+ op: OPERATION[op],
3904
+ });
3905
+ }
3906
+ });
3907
+ }
3484
3908
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3485
3909
  const viewOffset = it.offset;
3486
- // try to encode "filtered" changes
3487
- this.encode(it, view, bytes, this.root.filteredChanges);
3488
3910
  // encode visibility changes (add/remove for this view)
3489
- const viewChangesIterator = view.changes.entries();
3490
- for (const [changeTree, changes] of viewChangesIterator) {
3491
- if (changes.size === 0) {
3492
- // FIXME: avoid having empty changes if no changes were made
3493
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3911
+ const refIds = Object.keys(view.changes);
3912
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3913
+ const refId = refIds[i];
3914
+ const changes = view.changes[refId];
3915
+ const changeTree = this.root.changeTrees[refId];
3916
+ if (changeTree === undefined ||
3917
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3918
+ ) {
3919
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3494
3920
  continue;
3495
3921
  }
3496
3922
  const ref = changeTree.ref;
3497
- const ctor = ref['constructor'];
3923
+ const ctor = ref.constructor;
3498
3924
  const encoder = ctor[$encoder];
3925
+ const metadata = ctor[Symbol.metadata];
3499
3926
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3500
- number$1(bytes, changeTree.refId, it);
3501
- const changesIterator = changes.entries();
3502
- for (const [fieldIndex, operation] of changesIterator) {
3927
+ encode.number(bytes, changeTree.refId, it);
3928
+ const keys = Object.keys(changes);
3929
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3930
+ const key = keys[i];
3931
+ const operation = changes[key];
3503
3932
  // isEncodeAll = false
3504
3933
  // hasView = true
3505
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3934
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3506
3935
  }
3507
3936
  }
3508
3937
  //
@@ -3510,51 +3939,62 @@ class Encoder {
3510
3939
  // (to allow re-using StateView's for multiple clients)
3511
3940
  //
3512
3941
  // clear "view" changes after encoding
3513
- view.changes.clear();
3942
+ view.changes = {};
3943
+ // try to encode "filtered" changes
3944
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3514
3945
  return Buffer.concat([
3515
- bytes.slice(0, sharedOffset),
3516
- bytes.slice(viewOffset, it.offset)
3946
+ bytes.subarray(0, sharedOffset),
3947
+ bytes.subarray(viewOffset, it.offset)
3517
3948
  ]);
3518
3949
  }
3519
3950
  onEndEncode(changeTrees = this.root.changes) {
3520
- const changeTreesIterator = changeTrees.entries();
3521
- for (const [changeTree, _] of changeTreesIterator) {
3522
- changeTree.endEncode();
3523
- }
3951
+ // changeTrees.forEach(function(changeTree) {
3952
+ // changeTree.endEncode();
3953
+ // });
3954
+ // for (const refId in changeTrees) {
3955
+ // const changeTree = this.root.changeTrees[refId];
3956
+ // changeTree.endEncode();
3957
+ // // changeTree.changes.clear();
3958
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3959
+ // // changeTree.ref[$onEncodeEnd]?.();
3960
+ // // // Not a new instance anymore
3961
+ // // delete changeTree[$isNew];
3962
+ // }
3524
3963
  }
3525
3964
  discardChanges() {
3526
3965
  // discard shared changes
3527
- if (this.root.changes.size > 0) {
3528
- this.onEndEncode(this.root.changes);
3529
- this.root.changes.clear();
3966
+ let length = this.root.changes.length;
3967
+ if (length > 0) {
3968
+ while (length--) {
3969
+ this.root.changes[length]?.endEncode();
3970
+ }
3971
+ this.root.changes.length = 0;
3530
3972
  }
3531
3973
  // discard filtered changes
3532
- if (this.root.filteredChanges.size > 0) {
3533
- this.onEndEncode(this.root.filteredChanges);
3534
- this.root.filteredChanges.clear();
3974
+ length = this.root.filteredChanges.length;
3975
+ if (length > 0) {
3976
+ while (length--) {
3977
+ this.root.filteredChanges[length]?.endEncode();
3978
+ }
3979
+ this.root.filteredChanges.length = 0;
3535
3980
  }
3536
3981
  }
3537
3982
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3538
3983
  const baseTypeId = this.context.getTypeId(baseType);
3539
3984
  const targetTypeId = this.context.getTypeId(targetType);
3985
+ if (targetTypeId === undefined) {
3986
+ 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.`);
3987
+ return;
3988
+ }
3540
3989
  if (baseTypeId !== targetTypeId) {
3541
3990
  bytes[it.offset++] = TYPE_ID & 255;
3542
- number$1(bytes, targetTypeId, it);
3991
+ encode.number(bytes, targetTypeId, it);
3543
3992
  }
3544
3993
  }
3545
- }
3546
-
3547
- function spliceOne(arr, index) {
3548
- // manually splice an array
3549
- if (index === -1 || index >= arr.length) {
3550
- return false;
3551
- }
3552
- const len = arr.length - 1;
3553
- for (let i = index; i < len; i++) {
3554
- arr[i] = arr[i + 1];
3994
+ get hasChanges() {
3995
+ return (this.root.changes.length > 0 ||
3996
+ this.root.filteredChanges.length > 0);
3555
3997
  }
3556
- arr.length = len;
3557
- return true;
3558
3998
  }
3559
3999
 
3560
4000
  class DecodingWarning extends Error {
@@ -3619,6 +4059,7 @@ class ReferenceTracker {
3619
4059
  clearRefs() {
3620
4060
  this.refs.clear();
3621
4061
  this.deletedRefs.clear();
4062
+ this.callbacks = {};
3622
4063
  this.refCounts = {};
3623
4064
  }
3624
4065
  // for decoding
@@ -3635,8 +4076,9 @@ class ReferenceTracker {
3635
4076
  // Ensure child schema instances have their references removed as well.
3636
4077
  //
3637
4078
  if (Metadata.isValidInstance(ref)) {
3638
- const metadata = ref['constructor'][Symbol.metadata];
3639
- for (const field in metadata) {
4079
+ const metadata = ref.constructor[Symbol.metadata];
4080
+ for (const index in metadata) {
4081
+ const field = metadata[index].name;
3640
4082
  const childRefId = typeof (ref[field]) === "object" && this.refIds.get(ref[field]);
3641
4083
  if (childRefId) {
3642
4084
  this.removeRef(childRefId);
@@ -3683,21 +4125,21 @@ class ReferenceTracker {
3683
4125
  class Decoder {
3684
4126
  constructor(root, context) {
3685
4127
  this.currentRefId = 0;
3686
- this.setRoot(root);
4128
+ this.setState(root);
3687
4129
  this.context = context || new TypeContext(root.constructor);
3688
4130
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3689
4131
  // this.context.schemas.forEach((id, schema) => {
3690
4132
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3691
4133
  // });
3692
4134
  }
3693
- setRoot(root) {
4135
+ setState(root) {
3694
4136
  this.state = root;
3695
- this.$root = new ReferenceTracker();
3696
- this.$root.addRef(0, root);
4137
+ this.root = new ReferenceTracker();
4138
+ this.root.addRef(0, root);
3697
4139
  }
3698
4140
  decode(bytes, it = { offset: 0 }, ref = this.state) {
3699
4141
  const allChanges = [];
3700
- const $root = this.$root;
4142
+ const $root = this.root;
3701
4143
  const totalBytes = bytes.byteLength;
3702
4144
  let decoder = ref['constructor'][$decoder];
3703
4145
  this.currentRefId = 0;
@@ -3707,7 +4149,7 @@ class Decoder {
3707
4149
  //
3708
4150
  if (bytes[it.offset] == SWITCH_TO_STRUCTURE) {
3709
4151
  it.offset++;
3710
- this.currentRefId = number(bytes, it);
4152
+ this.currentRefId = decode.number(bytes, it);
3711
4153
  const nextRef = $root.refs.get(this.currentRefId);
3712
4154
  //
3713
4155
  // Trying to access a reference that haven't been decoded yet.
@@ -3717,7 +4159,7 @@ class Decoder {
3717
4159
  }
3718
4160
  ref[$onDecodeEnd]?.();
3719
4161
  ref = nextRef;
3720
- decoder = ref['constructor'][$decoder];
4162
+ decoder = ref.constructor[$decoder];
3721
4163
  continue;
3722
4164
  }
3723
4165
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3729,9 +4171,9 @@ class Decoder {
3729
4171
  //
3730
4172
  const nextIterator = { offset: it.offset };
3731
4173
  while (it.offset < totalBytes) {
3732
- if (switchStructureCheck(bytes, it)) {
4174
+ if (bytes[it.offset] === SWITCH_TO_STRUCTURE) {
3733
4175
  nextIterator.offset = it.offset + 1;
3734
- if ($root.refs.has(number(bytes, nextIterator))) {
4176
+ if ($root.refs.has(decode.number(bytes, nextIterator))) {
3735
4177
  break;
3736
4178
  }
3737
4179
  }
@@ -3752,7 +4194,7 @@ class Decoder {
3752
4194
  let type;
3753
4195
  if (bytes[it.offset] === TYPE_ID) {
3754
4196
  it.offset++;
3755
- const type_id = number(bytes, it);
4197
+ const type_id = decode.number(bytes, it);
3756
4198
  type = this.context.get(type_id);
3757
4199
  }
3758
4200
  return type || defaultType;
@@ -3778,7 +4220,7 @@ class Decoder {
3778
4220
  previousValue: value
3779
4221
  });
3780
4222
  if (needRemoveRef) {
3781
- this.$root.removeRef(this.$root.refIds.get(value));
4223
+ this.root.removeRef(this.root.refIds.get(value));
3782
4224
  }
3783
4225
  });
3784
4226
  }
@@ -3818,14 +4260,27 @@ class Reflection extends Schema {
3818
4260
  super(...arguments);
3819
4261
  this.types = new ArraySchema();
3820
4262
  }
3821
- static encode(instance, context, it = { offset: 0 }) {
3822
- if (!context) {
3823
- context = new TypeContext(instance.constructor);
3824
- }
4263
+ /**
4264
+ * Encodes the TypeContext of an Encoder into a buffer.
4265
+ *
4266
+ * @param encoder Encoder instance
4267
+ * @param it
4268
+ * @returns
4269
+ */
4270
+ static encode(encoder, it = { offset: 0 }) {
4271
+ const context = encoder.context;
3825
4272
  const reflection = new Reflection();
3826
- const encoder = new Encoder(reflection);
4273
+ const reflectionEncoder = new Encoder(reflection);
4274
+ // rootType is usually the first schema passed to the Encoder
4275
+ // (unless it inherits from another schema)
4276
+ const rootType = context.schemas.get(encoder.state.constructor);
4277
+ if (rootType > 0) {
4278
+ reflection.rootType = rootType;
4279
+ }
3827
4280
  const buildType = (currentType, metadata) => {
3828
- for (const fieldName in metadata) {
4281
+ for (const fieldIndex in metadata) {
4282
+ const index = Number(fieldIndex);
4283
+ const fieldName = metadata[index].name;
3829
4284
  // skip fields from parent classes
3830
4285
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
3831
4286
  continue;
@@ -3833,7 +4288,7 @@ class Reflection extends Schema {
3833
4288
  const field = new ReflectionField();
3834
4289
  field.name = fieldName;
3835
4290
  let fieldType;
3836
- const type = metadata[fieldName].type;
4291
+ const type = metadata[index].type;
3837
4292
  if (typeof (type) === "string") {
3838
4293
  fieldType = type;
3839
4294
  }
@@ -3875,65 +4330,335 @@ class Reflection extends Schema {
3875
4330
  }
3876
4331
  buildType(type, klass[Symbol.metadata]);
3877
4332
  }
3878
- const buf = encoder.encodeAll(it);
4333
+ const buf = reflectionEncoder.encodeAll(it);
3879
4334
  return Buffer.from(buf, 0, it.offset);
3880
4335
  }
4336
+ /**
4337
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4338
+ *
4339
+ * @param bytes Reflection.encode() output
4340
+ * @param it
4341
+ * @returns Decoder instance
4342
+ */
3881
4343
  static decode(bytes, it) {
3882
4344
  const reflection = new Reflection();
3883
4345
  const reflectionDecoder = new Decoder(reflection);
3884
4346
  reflectionDecoder.decode(bytes, it);
3885
- const context = new TypeContext();
3886
- const schemaTypes = reflection.types.reduce((types, reflectionType) => {
3887
- const parentKlass = types[reflectionType.extendsId] || Schema;
3888
- const schema = class _ extends parentKlass {
4347
+ const typeContext = new TypeContext();
4348
+ // 1st pass, initialize metadata + inheritance
4349
+ reflection.types.forEach((reflectionType) => {
4350
+ const parentClass = typeContext.get(reflectionType.extendsId) ?? Schema;
4351
+ const schema = class _ extends parentClass {
3889
4352
  };
3890
- // const _metadata = Object.create(_classSuper[Symbol.metadata] ?? null);
3891
- const _metadata = parentKlass && parentKlass[Symbol.metadata] || Object.create(null);
3892
- Object.defineProperty(schema, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
3893
4353
  // register for inheritance support
3894
4354
  TypeContext.register(schema);
3895
- const typeid = reflectionType.id;
3896
- types[typeid] = schema;
3897
- context.add(schema, typeid);
3898
- return types;
4355
+ // // for inheritance support
4356
+ // Metadata.initialize(schema);
4357
+ typeContext.add(schema, reflectionType.id);
3899
4358
  }, {});
3900
- reflection.types.forEach((reflectionType) => {
3901
- const schemaType = schemaTypes[reflectionType.id];
3902
- const metadata = schemaType[Symbol.metadata];
3903
- const parentKlass = reflection.types[reflectionType.extendsId];
3904
- const parentFieldIndex = parentKlass && parentKlass.fields.length || 0;
4359
+ // define fields
4360
+ const addFields = (metadata, reflectionType, parentFieldIndex) => {
3905
4361
  reflectionType.fields.forEach((field, i) => {
3906
4362
  const fieldIndex = parentFieldIndex + i;
3907
4363
  if (field.referencedType !== undefined) {
3908
4364
  let fieldType = field.type;
3909
- let refType = schemaTypes[field.referencedType];
4365
+ let refType = typeContext.get(field.referencedType);
3910
4366
  // map or array of primitive type (-1)
3911
4367
  if (!refType) {
3912
4368
  const typeInfo = field.type.split(":");
3913
4369
  fieldType = typeInfo[0];
3914
- refType = typeInfo[1];
4370
+ refType = typeInfo[1]; // string
3915
4371
  }
3916
4372
  if (fieldType === "ref") {
3917
- // type(refType)(schemaType.prototype, field.name);
3918
4373
  Metadata.addField(metadata, fieldIndex, field.name, refType);
3919
4374
  }
3920
4375
  else {
3921
- // type({ [fieldType]: refType } as DefinitionType)(schemaType.prototype, field.name);
3922
4376
  Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
3923
4377
  }
3924
4378
  }
3925
4379
  else {
3926
- // type(field.type as PrimitiveType)(schemaType.prototype, field.name);
3927
4380
  Metadata.addField(metadata, fieldIndex, field.name, field.type);
3928
4381
  }
3929
4382
  });
4383
+ };
4384
+ // 2nd pass, set fields
4385
+ reflection.types.forEach((reflectionType) => {
4386
+ const schema = typeContext.get(reflectionType.id);
4387
+ // for inheritance support
4388
+ const metadata = Metadata.initialize(schema);
4389
+ const inheritedTypes = [];
4390
+ let parentType = reflectionType;
4391
+ do {
4392
+ inheritedTypes.push(parentType);
4393
+ parentType = reflection.types.find((t) => t.id === parentType.extendsId);
4394
+ } while (parentType);
4395
+ let parentFieldIndex = 0;
4396
+ inheritedTypes.reverse().forEach((reflectionType) => {
4397
+ // add fields from all inherited classes
4398
+ // TODO: refactor this to avoid adding fields from parent classes
4399
+ addFields(metadata, reflectionType, parentFieldIndex);
4400
+ parentFieldIndex += reflectionType.fields.length;
4401
+ });
3930
4402
  });
3931
- return new (schemaTypes[0])();
4403
+ const state = new (typeContext.get(reflection.rootType || 0))();
4404
+ return new Decoder(state, typeContext);
3932
4405
  }
3933
4406
  }
3934
4407
  __decorate([
3935
4408
  type([ReflectionType])
3936
4409
  ], Reflection.prototype, "types", void 0);
4410
+ __decorate([
4411
+ type("number")
4412
+ ], Reflection.prototype, "rootType", void 0);
4413
+
4414
+ function getDecoderStateCallbacks(decoder) {
4415
+ const $root = decoder.root;
4416
+ const callbacks = $root.callbacks;
4417
+ const onAddCalls = new WeakMap();
4418
+ let currentOnAddCallback;
4419
+ decoder.triggerChanges = function (allChanges) {
4420
+ const uniqueRefIds = new Set();
4421
+ for (let i = 0, l = allChanges.length; i < l; i++) {
4422
+ const change = allChanges[i];
4423
+ const refId = change.refId;
4424
+ const ref = change.ref;
4425
+ const $callbacks = callbacks[refId];
4426
+ if (!$callbacks) {
4427
+ continue;
4428
+ }
4429
+ //
4430
+ // trigger onRemove on child structure.
4431
+ //
4432
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE &&
4433
+ change.previousValue instanceof Schema) {
4434
+ const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[OPERATION.DELETE];
4435
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4436
+ deleteCallbacks[i]();
4437
+ }
4438
+ }
4439
+ if (ref instanceof Schema) {
4440
+ //
4441
+ // Handle schema instance
4442
+ //
4443
+ if (!uniqueRefIds.has(refId)) {
4444
+ // trigger onChange
4445
+ const replaceCallbacks = $callbacks?.[OPERATION.REPLACE];
4446
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4447
+ replaceCallbacks[i]();
4448
+ // try {
4449
+ // } catch (e) {
4450
+ // console.error(e);
4451
+ // }
4452
+ }
4453
+ }
4454
+ if ($callbacks.hasOwnProperty(change.field)) {
4455
+ const fieldCallbacks = $callbacks[change.field];
4456
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
4457
+ fieldCallbacks[i](change.value, change.previousValue);
4458
+ // try {
4459
+ // } catch (e) {
4460
+ // console.error(e);
4461
+ // }
4462
+ }
4463
+ }
4464
+ }
4465
+ else {
4466
+ //
4467
+ // Handle collection of items
4468
+ //
4469
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE) {
4470
+ //
4471
+ // FIXME: `previousValue` should always be available.
4472
+ //
4473
+ if (change.previousValue !== undefined) {
4474
+ // triger onRemove
4475
+ const deleteCallbacks = $callbacks[OPERATION.DELETE];
4476
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4477
+ deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
4478
+ }
4479
+ }
4480
+ // Handle DELETE_AND_ADD operations
4481
+ if ((change.op & OPERATION.ADD) === OPERATION.ADD) {
4482
+ const addCallbacks = $callbacks[OPERATION.ADD];
4483
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4484
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4485
+ }
4486
+ }
4487
+ }
4488
+ else if ((change.op & OPERATION.ADD) === OPERATION.ADD && change.previousValue === undefined) {
4489
+ // triger onAdd
4490
+ const addCallbacks = $callbacks[OPERATION.ADD];
4491
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4492
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4493
+ }
4494
+ }
4495
+ // trigger onChange
4496
+ if (change.value !== change.previousValue) {
4497
+ const replaceCallbacks = $callbacks[OPERATION.REPLACE];
4498
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4499
+ replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4500
+ }
4501
+ }
4502
+ }
4503
+ uniqueRefIds.add(refId);
4504
+ }
4505
+ };
4506
+ function getProxy(metadataOrType, context) {
4507
+ let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
4508
+ let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
4509
+ (metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
4510
+ if (metadata && !isCollection) {
4511
+ const onAddListen = function (ref, prop, callback, immediate) {
4512
+ // immediate trigger
4513
+ if (immediate &&
4514
+ context.instance[prop] !== undefined &&
4515
+ !onAddCalls.has(currentOnAddCallback) // Workaround for https://github.com/colyseus/schema/issues/147
4516
+ ) {
4517
+ callback(context.instance[prop], undefined);
4518
+ }
4519
+ return $root.addCallback($root.refIds.get(ref), prop, callback);
4520
+ };
4521
+ /**
4522
+ * Schema instances
4523
+ */
4524
+ return new Proxy({
4525
+ listen: function listen(prop, callback, immediate = true) {
4526
+ if (context.instance) {
4527
+ return onAddListen(context.instance, prop, callback, immediate);
4528
+ }
4529
+ else {
4530
+ // collection instance not received yet
4531
+ let detachCallback = () => { };
4532
+ context.onInstanceAvailable((ref, existing) => {
4533
+ detachCallback = onAddListen(ref, prop, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4534
+ });
4535
+ return () => detachCallback();
4536
+ }
4537
+ },
4538
+ onChange: function onChange(callback) {
4539
+ return $root.addCallback($root.refIds.get(context.instance), OPERATION.REPLACE, callback);
4540
+ },
4541
+ //
4542
+ // TODO: refactor `bindTo()` implementation.
4543
+ // There is room for improvement.
4544
+ //
4545
+ bindTo: function bindTo(targetObject, properties) {
4546
+ if (!properties) {
4547
+ properties = Object.keys(metadata).map((index) => metadata[index].name);
4548
+ }
4549
+ return $root.addCallback($root.refIds.get(context.instance), OPERATION.REPLACE, () => {
4550
+ properties.forEach((prop) => targetObject[prop] = context.instance[prop]);
4551
+ });
4552
+ }
4553
+ }, {
4554
+ get(target, prop) {
4555
+ const metadataField = metadata[metadata[prop]];
4556
+ if (metadataField) {
4557
+ const instance = context.instance?.[prop];
4558
+ const onInstanceAvailable = ((callback) => {
4559
+ const unbind = $(context.instance).listen(prop, (value, _) => {
4560
+ callback(value, false);
4561
+ // FIXME: by "unbinding" the callback here,
4562
+ // it will not support when the server
4563
+ // re-instantiates the instance.
4564
+ //
4565
+ unbind?.();
4566
+ }, false);
4567
+ // has existing value
4568
+ if ($root.refIds.get(instance) !== undefined) {
4569
+ callback(instance, true);
4570
+ }
4571
+ });
4572
+ return getProxy(metadataField.type, {
4573
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4574
+ instance: ($root.refIds.get(instance) && instance),
4575
+ parentInstance: context.instance,
4576
+ onInstanceAvailable,
4577
+ });
4578
+ }
4579
+ else {
4580
+ // accessing the function
4581
+ return target[prop];
4582
+ }
4583
+ },
4584
+ has(target, prop) { return metadata[prop] !== undefined; },
4585
+ set(_, _1, _2) { throw new Error("not allowed"); },
4586
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4587
+ });
4588
+ }
4589
+ else {
4590
+ /**
4591
+ * Collection instances
4592
+ */
4593
+ const onAdd = function (ref, callback, immediate) {
4594
+ // Trigger callback on existing items
4595
+ if (immediate) {
4596
+ ref.forEach((v, k) => callback(v, k));
4597
+ }
4598
+ return $root.addCallback($root.refIds.get(ref), OPERATION.ADD, (value, key) => {
4599
+ onAddCalls.set(callback, true);
4600
+ currentOnAddCallback = callback;
4601
+ callback(value, key);
4602
+ onAddCalls.delete(callback);
4603
+ currentOnAddCallback = undefined;
4604
+ });
4605
+ };
4606
+ const onRemove = function (ref, callback) {
4607
+ return $root.addCallback($root.refIds.get(ref), OPERATION.DELETE, callback);
4608
+ };
4609
+ return new Proxy({
4610
+ onAdd: function (callback, immediate = true) {
4611
+ //
4612
+ // https://github.com/colyseus/schema/issues/147
4613
+ // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4614
+ //
4615
+ if (context.instance) {
4616
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4617
+ }
4618
+ else if (context.onInstanceAvailable) {
4619
+ // collection instance not received yet
4620
+ let detachCallback = () => { };
4621
+ context.onInstanceAvailable((ref, existing) => {
4622
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4623
+ });
4624
+ return () => detachCallback();
4625
+ }
4626
+ },
4627
+ onRemove: function (callback) {
4628
+ if (context.onInstanceAvailable) {
4629
+ // collection instance not received yet
4630
+ let detachCallback = () => { };
4631
+ context.onInstanceAvailable((ref) => {
4632
+ detachCallback = onRemove(ref, callback);
4633
+ });
4634
+ return () => detachCallback();
4635
+ }
4636
+ else if (context.instance) {
4637
+ return onRemove(context.instance, callback);
4638
+ }
4639
+ },
4640
+ }, {
4641
+ get(target, prop) {
4642
+ if (!target[prop]) {
4643
+ throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
4644
+ }
4645
+ return target[prop];
4646
+ },
4647
+ has(target, prop) { return target[prop] !== undefined; },
4648
+ set(_, _1, _2) { throw new Error("not allowed"); },
4649
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4650
+ });
4651
+ }
4652
+ }
4653
+ function $(instance) {
4654
+ return getProxy(undefined, { instance });
4655
+ }
4656
+ return $;
4657
+ }
4658
+
4659
+ function getRawChangesCallback(decoder, callback) {
4660
+ decoder.triggerChanges = callback;
4661
+ }
3937
4662
 
3938
4663
  class StateView {
3939
4664
  constructor() {
@@ -3949,31 +4674,32 @@ class StateView {
3949
4674
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
3950
4675
  * (This is used to force encoding a property, even if it was not changed)
3951
4676
  */
3952
- this.changes = new Map();
4677
+ this.changes = {};
3953
4678
  }
3954
4679
  // TODO: allow to set multiple tags at once
3955
- add(obj, tag = DEFAULT_VIEW_TAG) {
4680
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
3956
4681
  if (!obj[$changes]) {
3957
4682
  console.warn("StateView#add(), invalid object:", obj);
3958
4683
  return this;
3959
4684
  }
3960
- let changeTree = obj[$changes];
3961
- this.items.add(changeTree);
3962
- // Add children of this ChangeTree to this view
3963
- changeTree.forEachChild((change, _) => this.add(change.ref, tag));
3964
- // FIXME: ArraySchema/MapSchema does not have metadata
4685
+ // FIXME: ArraySchema/MapSchema do not have metadata
3965
4686
  const metadata = obj.constructor[Symbol.metadata];
3966
- // add parent ChangeTree's, if they are invisible to this view
3967
- // TODO: REFACTOR addParent()
3968
- this.addParent(changeTree, tag);
4687
+ const changeTree = obj[$changes];
4688
+ this.items.add(changeTree);
4689
+ // add parent ChangeTree's
4690
+ // - if it was invisible to this view
4691
+ // - if it were previously filtered out
4692
+ if (checkIncludeParent && changeTree.parent) {
4693
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4694
+ }
3969
4695
  //
3970
4696
  // TODO: when adding an item of a MapSchema, the changes may not
3971
4697
  // be set (only the parent's changes are set)
3972
4698
  //
3973
- let changes = this.changes.get(changeTree);
4699
+ let changes = this.changes[changeTree.refId];
3974
4700
  if (changes === undefined) {
3975
- changes = new Map();
3976
- this.changes.set(changeTree, changes);
4701
+ changes = {};
4702
+ this.changes[changeTree.refId] = changes;
3977
4703
  }
3978
4704
  // set tag
3979
4705
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -3989,82 +4715,78 @@ class StateView {
3989
4715
  tags = this.tags.get(changeTree);
3990
4716
  }
3991
4717
  tags.add(tag);
3992
- // console.log("BY TAG:", tag);
3993
4718
  // Ref: add tagged properties
3994
- metadata?.[-3]?.[tag]?.forEach((index) => {
4719
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
3995
4720
  if (changeTree.getChange(index) !== OPERATION.DELETE) {
3996
- changes.set(index, OPERATION.ADD);
4721
+ changes[index] = OPERATION.ADD;
3997
4722
  }
3998
4723
  });
3999
4724
  }
4000
4725
  else {
4001
- // console.log("DEFAULT TAG", changeTree.allChanges);
4002
- // // add default tag properties
4003
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4004
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4005
- // changes.set(index, OPERATION.ADD);
4006
- // }
4007
- // });
4008
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4726
+ const isInvisible = this.invisible.has(changeTree);
4727
+ const changeSet = (changeTree.filteredChanges !== undefined)
4009
4728
  ? changeTree.allFilteredChanges
4010
4729
  : changeTree.allChanges;
4011
- const it = allChangesSet.keys();
4012
- const isInvisible = this.invisible.has(changeTree);
4013
- for (const index of it) {
4014
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4015
- changeTree.getChange(index) !== OPERATION.DELETE) {
4016
- changes.set(index, OPERATION.ADD);
4730
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4731
+ const index = changeSet.operations[i];
4732
+ if (index === undefined) {
4733
+ continue;
4734
+ } // skip "undefined" indexes
4735
+ const op = changeTree.indexedOperations[index] ?? OPERATION.ADD;
4736
+ const tagAtIndex = metadata?.[index].tag;
4737
+ if ((isInvisible || // if "invisible", include all
4738
+ tagAtIndex === undefined || // "all change" with no tag
4739
+ tagAtIndex === tag // tagged property
4740
+ ) &&
4741
+ op !== OPERATION.DELETE) {
4742
+ changes[index] = op;
4017
4743
  }
4018
4744
  }
4019
4745
  }
4020
- // TODO: avoid unnecessary iteration here
4021
- while (changeTree.parent &&
4022
- (changeTree = changeTree.parent[$changes]) &&
4023
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4024
- this.items.add(changeTree);
4025
- }
4746
+ // Add children of this ChangeTree to this view
4747
+ changeTree.forEachChild((change, index) => {
4748
+ // Do not ADD children that don't have the same tag
4749
+ if (metadata &&
4750
+ metadata[index].tag !== undefined &&
4751
+ metadata[index].tag !== tag) {
4752
+ return;
4753
+ }
4754
+ this.add(change.ref, tag, false);
4755
+ });
4026
4756
  return this;
4027
4757
  }
4028
- addParent(changeTree, tag) {
4029
- const parentRef = changeTree.parent;
4030
- if (!parentRef) {
4031
- return;
4758
+ addParent(changeTree, parentIndex, tag) {
4759
+ // view must have all "changeTree" parent tree
4760
+ this.items.add(changeTree);
4761
+ // add parent's parent
4762
+ const parentChangeTree = changeTree.parent?.[$changes];
4763
+ if (parentChangeTree && (parentChangeTree.filteredChanges !== undefined)) {
4764
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4032
4765
  }
4033
- const parentChangeTree = parentRef[$changes];
4034
- const parentIndex = changeTree.parentIndex;
4035
- if (!this.invisible.has(parentChangeTree)) {
4036
- // parent is already available, no need to add it!
4766
+ // parent is already available, no need to add it!
4767
+ if (!this.invisible.has(changeTree)) {
4037
4768
  return;
4038
4769
  }
4039
- this.addParent(parentChangeTree, tag);
4040
4770
  // add parent's tag properties
4041
- if (parentChangeTree.getChange(parentIndex) !== OPERATION.DELETE) {
4042
- let parentChanges = this.changes.get(parentChangeTree);
4043
- if (parentChanges === undefined) {
4044
- parentChanges = new Map();
4045
- this.changes.set(parentChangeTree, parentChanges);
4771
+ if (changeTree.getChange(parentIndex) !== OPERATION.DELETE) {
4772
+ let changes = this.changes[changeTree.refId];
4773
+ if (changes === undefined) {
4774
+ changes = {};
4775
+ this.changes[changeTree.refId] = changes;
4046
4776
  }
4047
- // console.log("add parent change", {
4048
- // parentIndex,
4049
- // parentChanges,
4050
- // parentChange: (
4051
- // parentChangeTree.getChange(parentIndex) &&
4052
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4053
- // ),
4054
- // })
4055
4777
  if (!this.tags) {
4056
4778
  this.tags = new WeakMap();
4057
4779
  }
4058
4780
  let tags;
4059
- if (!this.tags.has(parentChangeTree)) {
4781
+ if (!this.tags.has(changeTree)) {
4060
4782
  tags = new Set();
4061
- this.tags.set(parentChangeTree, tags);
4783
+ this.tags.set(changeTree, tags);
4062
4784
  }
4063
4785
  else {
4064
- tags = this.tags.get(parentChangeTree);
4786
+ tags = this.tags.get(changeTree);
4065
4787
  }
4066
4788
  tags.add(tag);
4067
- parentChanges.set(parentIndex, OPERATION.ADD);
4789
+ changes[parentIndex] = OPERATION.ADD;
4068
4790
  }
4069
4791
  }
4070
4792
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4076,32 +4798,32 @@ class StateView {
4076
4798
  this.items.delete(changeTree);
4077
4799
  const ref = changeTree.ref;
4078
4800
  const metadata = ref.constructor[Symbol.metadata];
4079
- let changes = this.changes.get(changeTree);
4801
+ let changes = this.changes[changeTree.refId];
4080
4802
  if (changes === undefined) {
4081
- changes = new Map();
4082
- this.changes.set(changeTree, changes);
4803
+ changes = {};
4804
+ this.changes[changeTree.refId] = changes;
4083
4805
  }
4084
4806
  if (tag === DEFAULT_VIEW_TAG) {
4085
4807
  // parent is collection (Map/Array)
4086
4808
  const parent = changeTree.parent;
4087
4809
  if (!Metadata.isValidInstance(parent)) {
4088
4810
  const parentChangeTree = parent[$changes];
4089
- let changes = this.changes.get(parentChangeTree);
4811
+ let changes = this.changes[parentChangeTree.refId];
4090
4812
  if (changes === undefined) {
4091
- changes = new Map();
4092
- this.changes.set(parentChangeTree, changes);
4813
+ changes = {};
4814
+ this.changes[parentChangeTree.refId] = changes;
4093
4815
  }
4094
4816
  // DELETE / DELETE BY REF ID
4095
- changes.set(changeTree.parentIndex, OPERATION.DELETE);
4817
+ changes[changeTree.parentIndex] = OPERATION.DELETE;
4096
4818
  }
4097
4819
  else {
4098
4820
  // delete all "tagged" properties.
4099
- metadata[-2].forEach((index) => changes.set(index, OPERATION.DELETE));
4821
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = OPERATION.DELETE);
4100
4822
  }
4101
4823
  }
4102
4824
  else {
4103
4825
  // delete only tagged properties
4104
- metadata[-3][tag].forEach((index) => changes.set(index, OPERATION.DELETE));
4826
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = OPERATION.DELETE);
4105
4827
  }
4106
4828
  // remove tag
4107
4829
  if (this.tags && this.tags.has(changeTree)) {
@@ -4128,5 +4850,5 @@ registerType("array", { constructor: ArraySchema });
4128
4850
  registerType("set", { constructor: SetSchema });
4129
4851
  registerType("collection", { constructor: CollectionSchema, });
4130
4852
 
4131
- 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 };
4853
+ 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 };
4132
4854
  //# sourceMappingURL=index.mjs.map