@colyseus/schema 3.0.0-alpha.4 → 3.0.0-alpha.40

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 (141) hide show
  1. package/README.md +131 -61
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +1521 -809
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +1519 -808
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +1528 -816
  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 -32
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +4 -4
  15. package/lib/Schema.js +44 -50
  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 +1 -2
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +1 -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 +1 -2
  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 +0 -1
  47. package/lib/decoder/DecodeOperation.js +30 -12
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +6 -7
  50. package/lib/decoder/Decoder.js +9 -9
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +3 -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 +27 -21
  60. package/lib/encoder/ChangeTree.js +246 -186
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +47 -61
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +9 -8
  66. package/lib/encoder/Encoder.js +165 -88
  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 +70 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +2 -1
  75. package/lib/encoding/assert.js +5 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.js +20 -21
  78. package/lib/encoding/decode.js.map +1 -1
  79. package/lib/encoding/encode.d.ts +2 -2
  80. package/lib/encoding/encode.js +52 -48
  81. package/lib/encoding/encode.js.map +1 -1
  82. package/lib/encoding/spec.d.ts +2 -1
  83. package/lib/encoding/spec.js +1 -0
  84. package/lib/encoding/spec.js.map +1 -1
  85. package/lib/index.d.ts +6 -3
  86. package/lib/index.js +19 -13
  87. package/lib/index.js.map +1 -1
  88. package/lib/types/HelperTypes.d.ts +34 -2
  89. package/lib/types/HelperTypes.js.map +1 -1
  90. package/lib/types/TypeContext.d.ts +23 -0
  91. package/lib/types/TypeContext.js +111 -0
  92. package/lib/types/TypeContext.js.map +1 -0
  93. package/lib/types/custom/ArraySchema.d.ts +2 -2
  94. package/lib/types/custom/ArraySchema.js +33 -22
  95. package/lib/types/custom/ArraySchema.js.map +1 -1
  96. package/lib/types/custom/CollectionSchema.js +1 -0
  97. package/lib/types/custom/CollectionSchema.js.map +1 -1
  98. package/lib/types/custom/MapSchema.d.ts +3 -1
  99. package/lib/types/custom/MapSchema.js +12 -4
  100. package/lib/types/custom/MapSchema.js.map +1 -1
  101. package/lib/types/custom/SetSchema.js +1 -0
  102. package/lib/types/custom/SetSchema.js.map +1 -1
  103. package/lib/types/registry.js +3 -4
  104. package/lib/types/registry.js.map +1 -1
  105. package/lib/types/symbols.d.ts +8 -5
  106. package/lib/types/symbols.js +9 -6
  107. package/lib/types/symbols.js.map +1 -1
  108. package/lib/types/utils.js +1 -2
  109. package/lib/types/utils.js.map +1 -1
  110. package/lib/utils.js +9 -7
  111. package/lib/utils.js.map +1 -1
  112. package/package.json +7 -6
  113. package/src/Metadata.ts +190 -42
  114. package/src/Reflection.ts +77 -39
  115. package/src/Schema.ts +59 -64
  116. package/src/annotations.ts +155 -203
  117. package/src/bench_encode.ts +108 -0
  118. package/src/codegen/parser.ts +107 -0
  119. package/src/codegen/types.ts +1 -0
  120. package/src/debug.ts +55 -0
  121. package/src/decoder/DecodeOperation.ts +40 -12
  122. package/src/decoder/Decoder.ts +14 -12
  123. package/src/decoder/ReferenceTracker.ts +3 -2
  124. package/src/decoder/strategy/StateCallbacks.ts +153 -82
  125. package/src/encoder/ChangeTree.ts +286 -202
  126. package/src/encoder/EncodeOperation.ts +77 -77
  127. package/src/encoder/Encoder.ts +201 -96
  128. package/src/encoder/Root.ts +93 -0
  129. package/src/encoder/StateView.ts +76 -88
  130. package/src/encoding/assert.ts +4 -3
  131. package/src/encoding/encode.ts +37 -31
  132. package/src/encoding/spec.ts +1 -0
  133. package/src/index.ts +8 -14
  134. package/src/types/HelperTypes.ts +54 -2
  135. package/src/types/TypeContext.ts +133 -0
  136. package/src/types/custom/ArraySchema.ts +49 -19
  137. package/src/types/custom/CollectionSchema.ts +1 -0
  138. package/src/types/custom/MapSchema.ts +18 -5
  139. package/src/types/custom/SetSchema.ts +1 -0
  140. package/src/types/symbols.ts +10 -7
  141. package/src/utils.ts +7 -3
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  const SWITCH_TO_STRUCTURE = 255; // (decoding collides with DELETE_AND_ADD + fieldIndex = 63)
6
4
  const TYPE_ID = 213;
7
5
  /**
@@ -27,6 +25,7 @@ exports.OPERATION = void 0;
27
25
  OPERATION[OPERATION["REVERSE"] = 15] = "REVERSE";
28
26
  OPERATION[OPERATION["MOVE"] = 32] = "MOVE";
29
27
  OPERATION[OPERATION["DELETE_BY_REFID"] = 33] = "DELETE_BY_REFID";
28
+ OPERATION[OPERATION["ADD_BY_REFID"] = 129] = "ADD_BY_REFID";
30
29
  })(exports.OPERATION || (exports.OPERATION = {}));
31
30
 
32
31
  Symbol.metadata ??= Symbol.for("Symbol.metadata");
@@ -46,11 +45,6 @@ const $changes = Symbol('$changes');
46
45
  * (MapSchema, ArraySchema, etc.)
47
46
  */
48
47
  const $childType = Symbol('$childType');
49
- /**
50
- * Special ChangeTree property to identify new instances
51
- * (Once they're encoded, they're not new anymore)
52
- */
53
- const $isNew = Symbol("$isNew");
54
48
  /**
55
49
  * Optional "discard" method for custom types (ArraySchema)
56
50
  * (Discards changes for next serialization)
@@ -60,6 +54,14 @@ const $onEncodeEnd = Symbol('$onEncodeEnd');
60
54
  * When decoding, this method is called after the instance is fully decoded
61
55
  */
62
56
  const $onDecodeEnd = Symbol("$onDecodeEnd");
57
+ /**
58
+ * Metadata
59
+ */
60
+ const $descriptors = Symbol("$descriptors");
61
+ const $numFields = "$__numFields";
62
+ const $refTypeFieldIndexes = "$__refTypeFieldIndexes";
63
+ const $viewFieldIndexes = "$__viewFieldIndexes";
64
+ const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
63
65
 
64
66
  const registeredTypes = {};
65
67
  const identifiers = new Map();
@@ -71,177 +73,416 @@ function getType(identifier) {
71
73
  return registeredTypes[identifier];
72
74
  }
73
75
 
76
+ class TypeContext {
77
+ /**
78
+ * For inheritance support
79
+ * Keeps track of which classes extends which. (parent -> children)
80
+ */
81
+ static { this.inheritedTypes = new Map(); }
82
+ static register(target) {
83
+ const parent = Object.getPrototypeOf(target);
84
+ if (parent !== Schema) {
85
+ let inherits = TypeContext.inheritedTypes.get(parent);
86
+ if (!inherits) {
87
+ inherits = new Set();
88
+ TypeContext.inheritedTypes.set(parent, inherits);
89
+ }
90
+ inherits.add(target);
91
+ }
92
+ }
93
+ constructor(rootClass) {
94
+ this.types = {};
95
+ this.schemas = new Map();
96
+ this.hasFilters = false;
97
+ this.parentFiltered = {};
98
+ if (rootClass) {
99
+ this.discoverTypes(rootClass);
100
+ }
101
+ }
102
+ has(schema) {
103
+ return this.schemas.has(schema);
104
+ }
105
+ get(typeid) {
106
+ return this.types[typeid];
107
+ }
108
+ add(schema, typeid = this.schemas.size) {
109
+ // skip if already registered
110
+ if (this.schemas.has(schema)) {
111
+ return false;
112
+ }
113
+ this.types[typeid] = schema;
114
+ //
115
+ // Workaround to allow using an empty Schema (with no `@type()` fields)
116
+ //
117
+ if (schema[Symbol.metadata] === undefined) {
118
+ Metadata.initialize(schema);
119
+ }
120
+ this.schemas.set(schema, typeid);
121
+ return true;
122
+ }
123
+ getTypeId(klass) {
124
+ return this.schemas.get(klass);
125
+ }
126
+ discoverTypes(klass, parentIndex, parentFieldViewTag) {
127
+ if (!this.add(klass)) {
128
+ return;
129
+ }
130
+ // add classes inherited from this base class
131
+ TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
132
+ this.discoverTypes(child, parentIndex, parentFieldViewTag);
133
+ });
134
+ // add parent classes
135
+ let parent = klass;
136
+ while ((parent = Object.getPrototypeOf(parent)) &&
137
+ parent !== Schema && // stop at root (Schema)
138
+ parent !== Function.prototype // stop at root (non-Schema)
139
+ ) {
140
+ this.discoverTypes(parent);
141
+ }
142
+ const metadata = (klass[Symbol.metadata] ??= {});
143
+ // if any schema/field has filters, mark "context" as having filters.
144
+ if (metadata[$viewFieldIndexes]) {
145
+ this.hasFilters = true;
146
+ }
147
+ if (parentFieldViewTag !== undefined) {
148
+ this.parentFiltered[`${this.schemas.get(klass)}-${parentIndex}`] = true;
149
+ }
150
+ for (const fieldIndex in metadata) {
151
+ const index = fieldIndex;
152
+ const fieldType = metadata[index].type;
153
+ const viewTag = metadata[index].tag;
154
+ if (typeof (fieldType) === "string") {
155
+ continue;
156
+ }
157
+ if (Array.isArray(fieldType)) {
158
+ const type = fieldType[0];
159
+ // skip primitive types
160
+ if (type === "string") {
161
+ continue;
162
+ }
163
+ this.discoverTypes(type, index, viewTag);
164
+ }
165
+ else if (typeof (fieldType) === "function") {
166
+ this.discoverTypes(fieldType, viewTag);
167
+ }
168
+ else {
169
+ const type = Object.values(fieldType)[0];
170
+ // skip primitive types
171
+ if (typeof (type) === "string") {
172
+ continue;
173
+ }
174
+ this.discoverTypes(type, index, viewTag);
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ function getNormalizedType(type) {
181
+ return (Array.isArray(type))
182
+ ? { array: type[0] }
183
+ : (typeof (type['type']) !== "undefined")
184
+ ? type['type']
185
+ : type;
186
+ }
74
187
  const Metadata = {
75
- addField(metadata, index, field, type, descriptor) {
188
+ addField(metadata, index, name, type, descriptor) {
76
189
  if (index > 64) {
77
- throw new Error(`Can't define field '${field}'.\nSchema instances may only have up to 64 fields.`);
190
+ throw new Error(`Can't define field '${name}'.\nSchema instances may only have up to 64 fields.`);
78
191
  }
79
- metadata[field] = Object.assign(metadata[field] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
192
+ metadata[index] = Object.assign(metadata[index] || {}, // avoid overwriting previous field metadata (@owned / @deprecated)
80
193
  {
81
- type: (Array.isArray(type))
82
- ? { array: type[0] }
83
- : type,
194
+ type: getNormalizedType(type),
84
195
  index,
85
- descriptor,
196
+ name,
86
197
  });
198
+ // create "descriptors" map
199
+ Object.defineProperty(metadata, $descriptors, {
200
+ value: metadata[$descriptors] || {},
201
+ enumerable: false,
202
+ configurable: true,
203
+ });
204
+ if (descriptor) {
205
+ // for encoder
206
+ metadata[$descriptors][name] = descriptor;
207
+ metadata[$descriptors][`_${name}`] = {
208
+ value: undefined,
209
+ writable: true,
210
+ enumerable: false,
211
+ configurable: true,
212
+ };
213
+ }
214
+ else {
215
+ // for decoder
216
+ metadata[$descriptors][name] = {
217
+ value: undefined,
218
+ writable: true,
219
+ enumerable: true,
220
+ configurable: true,
221
+ };
222
+ }
87
223
  // map -1 as last field index
88
- Object.defineProperty(metadata, -1, {
224
+ Object.defineProperty(metadata, $numFields, {
89
225
  value: index,
90
226
  enumerable: false,
91
227
  configurable: true
92
228
  });
93
- // map index => field name (non enumerable)
94
- Object.defineProperty(metadata, index, {
95
- value: field,
229
+ // map field name => index (non enumerable)
230
+ Object.defineProperty(metadata, name, {
231
+ value: index,
96
232
  enumerable: false,
97
233
  configurable: true,
98
234
  });
235
+ // if child Ref/complex type, add to -4
236
+ if (typeof (metadata[index].type) !== "string") {
237
+ if (metadata[$refTypeFieldIndexes] === undefined) {
238
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
239
+ value: [],
240
+ enumerable: false,
241
+ configurable: true,
242
+ });
243
+ }
244
+ metadata[$refTypeFieldIndexes].push(index);
245
+ }
99
246
  },
100
247
  setTag(metadata, fieldName, tag) {
248
+ const index = metadata[fieldName];
249
+ const field = metadata[index];
101
250
  // add 'tag' to the field
102
- const field = metadata[fieldName];
103
251
  field.tag = tag;
104
- if (!metadata[-2]) {
252
+ if (!metadata[$viewFieldIndexes]) {
105
253
  // -2: all field indexes with "view" tag
106
- Object.defineProperty(metadata, -2, {
254
+ Object.defineProperty(metadata, $viewFieldIndexes, {
107
255
  value: [],
108
256
  enumerable: false,
109
257
  configurable: true
110
258
  });
111
259
  // -3: field indexes by "view" tag
112
- Object.defineProperty(metadata, -3, {
260
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
113
261
  value: {},
114
262
  enumerable: false,
115
263
  configurable: true
116
264
  });
117
265
  }
118
- metadata[-2].push(field.index);
119
- if (!metadata[-3][tag]) {
120
- metadata[-3][tag] = [];
266
+ metadata[$viewFieldIndexes].push(index);
267
+ if (!metadata[$fieldIndexesByViewTag][tag]) {
268
+ metadata[$fieldIndexesByViewTag][tag] = [];
121
269
  }
122
- metadata[-3][tag].push(field.index);
270
+ metadata[$fieldIndexesByViewTag][tag].push(index);
123
271
  },
124
272
  setFields(target, fields) {
125
- const metadata = (target.prototype.constructor[Symbol.metadata] ??= {});
126
- // target[$track] = function (changeTree, index: number, operation: OPERATION = OPERATION.ADD) {
127
- // changeTree.change(index, operation, encodeSchemaOperation);
128
- // };
129
- // target[$encoder] = encodeSchemaOperation;
130
- // target[$decoder] = decodeSchemaOperation;
131
- // if (!target.prototype.toJSON) { target.prototype.toJSON = Schema.prototype.toJSON; }
132
- let index = 0;
273
+ // for inheritance support
274
+ const constructor = target.prototype.constructor;
275
+ TypeContext.register(constructor);
276
+ const parentClass = Object.getPrototypeOf(constructor);
277
+ const parentMetadata = parentClass && parentClass[Symbol.metadata];
278
+ const metadata = Metadata.initialize(constructor);
279
+ // Use Schema's methods if not defined in the class
280
+ if (!constructor[$track]) {
281
+ constructor[$track] = Schema[$track];
282
+ }
283
+ if (!constructor[$encoder]) {
284
+ constructor[$encoder] = Schema[$encoder];
285
+ }
286
+ if (!constructor[$decoder]) {
287
+ constructor[$decoder] = Schema[$decoder];
288
+ }
289
+ if (!constructor.prototype.toJSON) {
290
+ constructor.prototype.toJSON = Schema.prototype.toJSON;
291
+ }
292
+ //
293
+ // detect index for this field, considering inheritance
294
+ //
295
+ let fieldIndex = metadata[$numFields] // current structure already has fields defined
296
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
297
+ ?? -1; // no fields defined
298
+ fieldIndex++;
133
299
  for (const field in fields) {
134
300
  const type = fields[field];
301
+ const normalizedType = getNormalizedType(type);
135
302
  // FIXME: this code is duplicated from @type() annotation
136
303
  const complexTypeKlass = (Array.isArray(type))
137
304
  ? getType("array")
138
305
  : (typeof (Object.keys(type)[0]) === "string") && getType(Object.keys(type)[0]);
139
- Metadata.addField(metadata, index, field, type, getPropertyDescriptor(`_${field}`, index, type, complexTypeKlass, metadata, field));
140
- index++;
306
+ const childType = (complexTypeKlass)
307
+ ? Object.values(type)[0]
308
+ : normalizedType;
309
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
310
+ fieldIndex++;
141
311
  }
312
+ return target;
142
313
  },
143
314
  isDeprecated(metadata, field) {
144
315
  return metadata[field].deprecated === true;
145
316
  },
317
+ init(klass) {
318
+ //
319
+ // Used only to initialize an empty Schema (Encoder#constructor)
320
+ // TODO: remove/refactor this...
321
+ //
322
+ const metadata = {};
323
+ klass[Symbol.metadata] = metadata;
324
+ Object.defineProperty(metadata, $numFields, {
325
+ value: 0,
326
+ enumerable: false,
327
+ configurable: true,
328
+ });
329
+ },
330
+ initialize(constructor) {
331
+ const parentClass = Object.getPrototypeOf(constructor);
332
+ const parentMetadata = parentClass[Symbol.metadata];
333
+ let metadata = constructor[Symbol.metadata] ?? Object.create(null);
334
+ // make sure inherited classes have their own metadata object.
335
+ if (parentClass !== Schema && metadata === parentMetadata) {
336
+ metadata = Object.create(null);
337
+ if (parentMetadata) {
338
+ //
339
+ // assign parent metadata to current
340
+ //
341
+ Object.setPrototypeOf(metadata, parentMetadata);
342
+ // $numFields
343
+ Object.defineProperty(metadata, $numFields, {
344
+ value: parentMetadata[$numFields],
345
+ enumerable: false,
346
+ configurable: true,
347
+ writable: true,
348
+ });
349
+ // $viewFieldIndexes / $fieldIndexesByViewTag
350
+ if (parentMetadata[$viewFieldIndexes] !== undefined) {
351
+ Object.defineProperty(metadata, $viewFieldIndexes, {
352
+ value: [...parentMetadata[$viewFieldIndexes]],
353
+ enumerable: false,
354
+ configurable: true,
355
+ writable: true,
356
+ });
357
+ Object.defineProperty(metadata, $fieldIndexesByViewTag, {
358
+ value: { ...parentMetadata[$fieldIndexesByViewTag] },
359
+ enumerable: false,
360
+ configurable: true,
361
+ writable: true,
362
+ });
363
+ }
364
+ // $refTypeFieldIndexes
365
+ if (parentMetadata[$refTypeFieldIndexes] !== undefined) {
366
+ Object.defineProperty(metadata, $refTypeFieldIndexes, {
367
+ value: [...parentMetadata[$refTypeFieldIndexes]],
368
+ enumerable: false,
369
+ configurable: true,
370
+ writable: true,
371
+ });
372
+ }
373
+ // $descriptors
374
+ Object.defineProperty(metadata, $descriptors, {
375
+ value: { ...parentMetadata[$descriptors] },
376
+ enumerable: false,
377
+ configurable: true,
378
+ writable: true,
379
+ });
380
+ }
381
+ }
382
+ constructor[Symbol.metadata] = metadata;
383
+ return metadata;
384
+ },
146
385
  isValidInstance(klass) {
147
386
  return (klass.constructor[Symbol.metadata] &&
148
- Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], -1));
387
+ Object.prototype.hasOwnProperty.call(klass.constructor[Symbol.metadata], $numFields));
149
388
  },
150
389
  getFields(klass) {
151
390
  const metadata = klass[Symbol.metadata];
152
391
  const fields = {};
153
- for (let i = 0; i <= metadata[-1]; i++) {
154
- fields[metadata[i]] = metadata[metadata[i]].type;
392
+ for (let i = 0; i <= metadata[$numFields]; i++) {
393
+ fields[metadata[i].name] = metadata[i].type;
155
394
  }
156
395
  return fields;
157
396
  }
158
397
  };
159
398
 
160
- var _a$5;
161
- class Root {
162
- constructor() {
163
- this.nextUniqueId = 0;
164
- this.refCount = new WeakMap();
165
- // all changes
166
- this.allChanges = new Map();
167
- this.allFilteredChanges = new Map();
168
- // pending changes to be encoded
169
- this.changes = new Map();
170
- this.filteredChanges = new Map();
171
- }
172
- getNextUniqueId() {
173
- return this.nextUniqueId++;
399
+ function setOperationAtIndex(changeSet, index) {
400
+ const operationsIndex = changeSet.indexes[index];
401
+ if (operationsIndex === undefined) {
402
+ changeSet.indexes[index] = changeSet.operations.push(index) - 1;
174
403
  }
175
- add(changeTree) {
176
- const refCount = this.refCount.get(changeTree) || 0;
177
- this.refCount.set(changeTree, refCount + 1);
404
+ else {
405
+ changeSet.operations[operationsIndex] = index;
178
406
  }
179
- remove(changeTree) {
180
- const refCount = this.refCount.get(changeTree);
181
- if (refCount <= 1) {
182
- this.allChanges.delete(changeTree);
183
- this.changes.delete(changeTree);
184
- if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
185
- this.allFilteredChanges.delete(changeTree);
186
- this.filteredChanges.delete(changeTree);
187
- }
188
- this.refCount.delete(changeTree);
189
- }
190
- else {
191
- this.refCount.set(changeTree, refCount - 1);
192
- }
193
- changeTree.forEachChild((child, _) => this.remove(child));
407
+ }
408
+ function deleteOperationAtIndex(changeSet, index) {
409
+ const operationsIndex = changeSet.indexes[index];
410
+ if (operationsIndex !== undefined) {
411
+ changeSet.operations[operationsIndex] = undefined;
194
412
  }
195
- clear() {
196
- this.changes.clear();
413
+ delete changeSet.indexes[index];
414
+ }
415
+ function enqueueChangeTree(root, changeTree, changeSet, queueRootIndex = changeTree[changeSet].queueRootIndex) {
416
+ if (root && root[changeSet][queueRootIndex] !== changeTree) {
417
+ changeTree[changeSet].queueRootIndex = root[changeSet].push(changeTree) - 1;
197
418
  }
198
419
  }
199
420
  class ChangeTree {
200
- static { _a$5 = $isNew; }
201
- ;
202
421
  constructor(ref) {
203
- this.indexes = {}; // TODO: remove this, only used by MapSchema/SetSchema/CollectionSchema (`encodeKeyValueOperation`)
204
- this.currentOperationIndex = 0;
205
- this.allChanges = new Map();
206
- this.allFilteredChanges = new Map();
207
- this.changes = new Map();
208
- this.filteredChanges = new Map();
209
- this[_a$5] = true;
422
+ this.isFiltered = false;
423
+ this.isPartiallyFiltered = false;
424
+ this.indexedOperations = {};
425
+ //
426
+ // TODO:
427
+ // try storing the index + operation per item.
428
+ // example: 1024 & 1025 => ADD, 1026 => DELETE
429
+ //
430
+ // => https://chatgpt.com/share/67107d0c-bc20-8004-8583-83b17dd7c196
431
+ //
432
+ this.changes = { indexes: {}, operations: [] };
433
+ this.allChanges = { indexes: {}, operations: [] };
434
+ /**
435
+ * Is this a new instance? Used on ArraySchema to determine OPERATION.MOVE_AND_ADD operation.
436
+ */
437
+ this.isNew = true;
210
438
  this.ref = ref;
439
+ //
440
+ // Does this structure have "filters" declared?
441
+ //
442
+ if (ref.constructor[Symbol.metadata]?.[$viewFieldIndexes]) {
443
+ this.allFilteredChanges = { indexes: {}, operations: [] };
444
+ this.filteredChanges = { indexes: {}, operations: [] };
445
+ }
211
446
  }
212
447
  setRoot(root) {
213
448
  this.root = root;
214
- this.root.add(this);
215
- //
216
- // At Schema initialization, the "root" structure might not be available
217
- // yet, as it only does once the "Encoder" has been set up.
218
- //
219
- // So the "parent" may be already set without a "root".
220
- //
221
- this.checkIsFiltered(this.parent, this.parentIndex);
222
- // unique refId for the ChangeTree.
223
- this.ensureRefId();
449
+ const isNewChangeTree = this.root.add(this);
450
+ const metadata = this.ref.constructor[Symbol.metadata];
451
+ if (this.root.types.hasFilters) {
452
+ //
453
+ // At Schema initialization, the "root" structure might not be available
454
+ // yet, as it only does once the "Encoder" has been set up.
455
+ //
456
+ // So the "parent" may be already set without a "root".
457
+ //
458
+ this.checkIsFiltered(metadata, this.parent, this.parentIndex);
459
+ if (this.isFiltered || this.isPartiallyFiltered) {
460
+ enqueueChangeTree(root, this, 'filteredChanges');
461
+ if (isNewChangeTree) {
462
+ this.root.allFilteredChanges.push(this);
463
+ }
464
+ }
465
+ }
224
466
  if (!this.isFiltered) {
225
- this.root.changes.set(this, this.changes);
467
+ enqueueChangeTree(root, this, 'changes');
468
+ if (isNewChangeTree) {
469
+ this.root.allChanges.push(this);
470
+ }
226
471
  }
227
- if (this.isFiltered || this.isPartiallyFiltered) {
228
- this.root.allFilteredChanges.set(this, this.allFilteredChanges);
229
- this.root.filteredChanges.set(this, this.filteredChanges);
230
- // } else {
231
- // this.root.allChanges.set(this, this.allChanges);
472
+ // Recursively set root on child structures
473
+ if (metadata) {
474
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
475
+ const field = metadata[index];
476
+ const value = this.ref[field.name];
477
+ value?.[$changes].setRoot(root);
478
+ });
232
479
  }
233
- if (!this.isFiltered) {
234
- this.root.allChanges.set(this, this.allChanges);
480
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
481
+ // MapSchema / ArraySchema, etc.
482
+ this.ref.forEach((value, key) => {
483
+ value[$changes].setRoot(root);
484
+ });
235
485
  }
236
- this.forEachChild((changeTree, _) => {
237
- changeTree.setRoot(root);
238
- });
239
- // this.allChanges.forEach((_, index) => {
240
- // const childRef = this.ref[$getByIndex](index);
241
- // if (childRef && childRef[$changes]) {
242
- // childRef[$changes].setRoot(root);
243
- // }
244
- // });
245
486
  }
246
487
  setParent(parent, root, parentIndex) {
247
488
  this.parent = parent;
@@ -250,83 +491,104 @@ class ChangeTree {
250
491
  if (!root) {
251
492
  return;
252
493
  }
253
- root.add(this);
494
+ const metadata = this.ref.constructor[Symbol.metadata];
254
495
  // skip if parent is already set
255
- if (root === this.root) {
256
- this.forEachChild((changeTree, atIndex) => {
257
- changeTree.setParent(this.ref, root, atIndex);
258
- });
259
- return;
496
+ if (root !== this.root) {
497
+ this.root = root;
498
+ const isNewChangeTree = root.add(this);
499
+ if (root.types.hasFilters) {
500
+ this.checkIsFiltered(metadata, parent, parentIndex);
501
+ if (this.isFiltered || this.isPartiallyFiltered) {
502
+ enqueueChangeTree(root, this, 'filteredChanges');
503
+ if (isNewChangeTree) {
504
+ this.root.allFilteredChanges.push(this);
505
+ }
506
+ }
507
+ }
508
+ if (!this.isFiltered) {
509
+ enqueueChangeTree(root, this, 'changes');
510
+ if (isNewChangeTree) {
511
+ this.root.allChanges.push(this);
512
+ }
513
+ }
260
514
  }
261
- this.root = root;
262
- this.checkIsFiltered(parent, parentIndex);
263
- if (!this.isFiltered) {
264
- this.root.changes.set(this, this.changes);
515
+ else {
516
+ root.add(this);
265
517
  }
266
- if (this.isFiltered || this.isPartiallyFiltered) {
267
- this.root.filteredChanges.set(this, this.filteredChanges);
268
- this.root.allFilteredChanges.set(this, this.filteredChanges);
518
+ // assign same parent on child structures
519
+ if (metadata) {
520
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
521
+ const field = metadata[index];
522
+ const value = this.ref[field.name];
523
+ value?.[$changes].setParent(this.ref, root, index);
524
+ // try { throw new Error(); } catch (e) {
525
+ // console.log(e.stack);
526
+ // }
527
+ });
269
528
  }
270
- else {
271
- this.root.allChanges.set(this, this.allChanges);
529
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
530
+ // MapSchema / ArraySchema, etc.
531
+ this.ref.forEach((value, key) => {
532
+ value[$changes].setParent(this.ref, root, this.indexes[key] ?? key);
533
+ });
272
534
  }
273
- this.ensureRefId();
274
- this.forEachChild((changeTree, atIndex) => {
275
- changeTree.setParent(this.ref, root, atIndex);
276
- });
277
535
  }
278
536
  forEachChild(callback) {
279
537
  //
280
538
  // assign same parent on child structures
281
539
  //
282
- if (Metadata.isValidInstance(this.ref)) {
283
- const metadata = this.ref['constructor'][Symbol.metadata];
284
- // FIXME: need to iterate over parent metadata instead.
285
- for (const field in metadata) {
286
- const value = this.ref[field];
287
- if (value && value[$changes]) {
288
- callback(value[$changes], metadata[field].index);
540
+ const metadata = this.ref.constructor[Symbol.metadata];
541
+ if (metadata) {
542
+ metadata[$refTypeFieldIndexes]?.forEach((index) => {
543
+ const field = metadata[index];
544
+ const value = this.ref[field.name];
545
+ if (value) {
546
+ callback(value[$changes], index);
289
547
  }
290
- }
548
+ });
291
549
  }
292
- else if (typeof (this.ref) === "object") {
550
+ else if (this.ref[$childType] && typeof (this.ref[$childType]) !== "string") {
293
551
  // MapSchema / ArraySchema, etc.
294
552
  this.ref.forEach((value, key) => {
295
- if (Metadata.isValidInstance(value)) {
296
- callback(value[$changes], this.ref[$changes].indexes[key]);
297
- }
553
+ callback(value[$changes], this.indexes[key] ?? key);
298
554
  });
299
555
  }
300
556
  }
301
557
  operation(op) {
302
- this.changes.set(--this.currentOperationIndex, op);
303
- this.root?.changes.set(this, this.changes);
558
+ // operations without index use negative values to represent them
559
+ // this is checked during .encode() time.
560
+ this.changes.operations.push(-op);
561
+ enqueueChangeTree(this.root, this, 'changes');
304
562
  }
305
563
  change(index, operation = exports.OPERATION.ADD) {
306
- const metadata = this.ref['constructor'][Symbol.metadata];
307
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
564
+ const metadata = this.ref.constructor[Symbol.metadata];
565
+ const isFiltered = this.isFiltered || (metadata?.[index]?.tag !== undefined);
308
566
  const changeSet = (isFiltered)
309
567
  ? this.filteredChanges
310
568
  : this.changes;
311
- const previousOperation = changeSet.get(index);
569
+ const previousOperation = this.indexedOperations[index];
312
570
  if (!previousOperation || previousOperation === exports.OPERATION.DELETE) {
313
571
  const op = (!previousOperation)
314
572
  ? operation
315
573
  : (previousOperation === exports.OPERATION.DELETE)
316
574
  ? exports.OPERATION.DELETE_AND_ADD
317
575
  : operation;
318
- changeSet.set(index, op);
576
+ //
577
+ // TODO: are DELETE operations being encoded as ADD here ??
578
+ //
579
+ this.indexedOperations[index] = op;
319
580
  }
320
- //
321
- // TODO: are DELETE operations being encoded as ADD here ??
322
- //
581
+ setOperationAtIndex(changeSet, index);
323
582
  if (isFiltered) {
324
- this.allFilteredChanges.set(index, exports.OPERATION.ADD);
325
- this.root?.filteredChanges.set(this, this.filteredChanges);
583
+ setOperationAtIndex(this.allFilteredChanges, index);
584
+ if (this.root) {
585
+ enqueueChangeTree(this.root, this, 'filteredChanges');
586
+ enqueueChangeTree(this.root, this, 'allFilteredChanges');
587
+ }
326
588
  }
327
589
  else {
328
- this.allChanges.set(index, exports.OPERATION.ADD);
329
- this.root?.changes.set(this, this.changes);
590
+ setOperationAtIndex(this.allChanges, index);
591
+ enqueueChangeTree(this.root, this, 'changes');
330
592
  }
331
593
  }
332
594
  shiftChangeIndexes(shiftIndex) {
@@ -338,12 +600,15 @@ class ChangeTree {
338
600
  const changeSet = (this.isFiltered)
339
601
  ? this.filteredChanges
340
602
  : this.changes;
341
- const changeSetEntries = Array.from(changeSet.entries());
342
- changeSet.clear();
343
- // Re-insert each entry with the shifted index
344
- for (const [index, op] of changeSetEntries) {
345
- changeSet.set(index + shiftIndex, op);
603
+ const newIndexedOperations = {};
604
+ const newIndexes = {};
605
+ for (const index in this.indexedOperations) {
606
+ newIndexedOperations[Number(index) + shiftIndex] = this.indexedOperations[index];
607
+ newIndexes[Number(index) + shiftIndex] = changeSet[index];
346
608
  }
609
+ this.indexedOperations = newIndexedOperations;
610
+ changeSet.indexes = newIndexes;
611
+ changeSet.operations = changeSet.operations.map((index) => index + shiftIndex);
347
612
  }
348
613
  shiftAllChangeIndexes(shiftIndex, startIndex = 0) {
349
614
  //
@@ -359,33 +624,42 @@ class ChangeTree {
359
624
  this._shiftAllChangeIndexes(shiftIndex, startIndex, this.allChanges);
360
625
  }
361
626
  }
362
- _shiftAllChangeIndexes(shiftIndex, startIndex = 0, allChangeSet) {
363
- Array.from(allChangeSet.entries()).forEach(([index, op]) => {
364
- // console.log('shiftAllChangeIndexes', index >= startIndex, { index, op, shiftIndex, startIndex })
365
- if (index >= startIndex) {
366
- allChangeSet.delete(index);
367
- allChangeSet.set(index + shiftIndex, op);
627
+ _shiftAllChangeIndexes(shiftIndex, startIndex = 0, changeSet) {
628
+ const newIndexes = {};
629
+ for (const key in changeSet.indexes) {
630
+ const index = changeSet.indexes[key];
631
+ if (index > startIndex) {
632
+ newIndexes[Number(key) + shiftIndex] = index;
368
633
  }
369
- });
634
+ else {
635
+ newIndexes[key] = index;
636
+ }
637
+ }
638
+ changeSet.indexes = newIndexes;
639
+ for (let i = 0; i < changeSet.operations.length; i++) {
640
+ const index = changeSet.operations[i];
641
+ if (index > startIndex) {
642
+ changeSet.operations[i] = index + shiftIndex;
643
+ }
644
+ }
370
645
  }
371
646
  indexedOperation(index, operation, allChangesIndex = index) {
372
- const metadata = this.ref['constructor'][Symbol.metadata];
373
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
374
- if (isFiltered) {
375
- this.allFilteredChanges.set(allChangesIndex, exports.OPERATION.ADD);
376
- this.filteredChanges.set(index, operation);
377
- this.root?.filteredChanges.set(this, this.filteredChanges);
647
+ this.indexedOperations[index] = operation;
648
+ if (this.filteredChanges) {
649
+ setOperationAtIndex(this.allFilteredChanges, allChangesIndex);
650
+ setOperationAtIndex(this.filteredChanges, index);
651
+ enqueueChangeTree(this.root, this, 'filteredChanges');
378
652
  }
379
653
  else {
380
- this.allChanges.set(allChangesIndex, exports.OPERATION.ADD);
381
- this.changes.set(index, operation);
382
- this.root?.changes.set(this, this.changes);
654
+ setOperationAtIndex(this.allChanges, allChangesIndex);
655
+ setOperationAtIndex(this.changes, index);
656
+ enqueueChangeTree(this.root, this, 'changes');
383
657
  }
384
658
  }
385
659
  getType(index) {
386
660
  if (Metadata.isValidInstance(this.ref)) {
387
- const metadata = this.ref['constructor'][Symbol.metadata];
388
- return metadata[metadata[index]].type;
661
+ const metadata = this.ref.constructor[Symbol.metadata];
662
+ return metadata[index].type;
389
663
  }
390
664
  else {
391
665
  //
@@ -398,8 +672,7 @@ class ChangeTree {
398
672
  }
399
673
  }
400
674
  getChange(index) {
401
- // TODO: optimize this. avoid checking against multiple instances
402
- return this.changes.get(index) ?? this.filteredChanges.get(index);
675
+ return this.indexedOperations[index];
403
676
  }
404
677
  //
405
678
  // used during `.encode()`
@@ -420,16 +693,14 @@ class ChangeTree {
420
693
  }
421
694
  return;
422
695
  }
423
- const metadata = this.ref['constructor'][Symbol.metadata];
424
- const isFiltered = this.isFiltered || (metadata && metadata[metadata[index]].tag !== undefined);
425
- const changeSet = (isFiltered)
696
+ const changeSet = (this.filteredChanges)
426
697
  ? this.filteredChanges
427
698
  : this.changes;
699
+ this.indexedOperations[index] = operation ?? exports.OPERATION.DELETE;
700
+ setOperationAtIndex(changeSet, index);
428
701
  const previousValue = this.getValue(index);
429
- changeSet.set(index, operation ?? exports.OPERATION.DELETE);
430
702
  // remove `root` reference
431
703
  if (previousValue && previousValue[$changes]) {
432
- previousValue[$changes].root = undefined;
433
704
  //
434
705
  // FIXME: this.root is "undefined"
435
706
  //
@@ -443,22 +714,26 @@ class ChangeTree {
443
714
  this.root?.remove(previousValue[$changes]);
444
715
  }
445
716
  //
446
- // FIXME: this is looking a bit ugly (and repeated from `.change()`)
717
+ // FIXME: this is looking a ugly and repeated
447
718
  //
448
- if (isFiltered) {
449
- this.root?.filteredChanges.set(this, this.filteredChanges);
450
- this.allFilteredChanges.delete(allChangesIndex);
719
+ if (this.filteredChanges) {
720
+ deleteOperationAtIndex(this.allFilteredChanges, allChangesIndex);
721
+ enqueueChangeTree(this.root, this, 'filteredChanges');
451
722
  }
452
723
  else {
453
- this.root?.changes.set(this, this.changes);
454
- this.allChanges.delete(allChangesIndex);
724
+ deleteOperationAtIndex(this.allChanges, allChangesIndex);
725
+ enqueueChangeTree(this.root, this, 'changes');
455
726
  }
456
727
  }
457
728
  endEncode() {
458
- this.changes.clear();
729
+ this.indexedOperations = {};
730
+ // // clear changes
731
+ // this.changes.indexes = {};
732
+ // this.changes.operations.length = 0;
733
+ // ArraySchema and MapSchema have a custom "encode end" method
459
734
  this.ref[$onEncodeEnd]?.();
460
735
  // Not a new instance anymore
461
- delete this[$isNew];
736
+ this.isNew = false;
462
737
  }
463
738
  discard(discardAll = false) {
464
739
  //
@@ -467,13 +742,22 @@ class ChangeTree {
467
742
  // REPLACE in case same key is used on next patches.
468
743
  //
469
744
  this.ref[$onEncodeEnd]?.();
470
- this.changes.clear();
471
- this.filteredChanges.clear();
472
- // reset operation index
473
- this.currentOperationIndex = 0;
745
+ this.indexedOperations = {};
746
+ this.changes.indexes = {};
747
+ this.changes.operations.length = 0;
748
+ this.changes.queueRootIndex = undefined;
749
+ if (this.filteredChanges !== undefined) {
750
+ this.filteredChanges.indexes = {};
751
+ this.filteredChanges.operations.length = 0;
752
+ this.filteredChanges.queueRootIndex = undefined;
753
+ }
474
754
  if (discardAll) {
475
- this.allChanges.clear();
476
- this.allFilteredChanges.clear();
755
+ this.allChanges.indexes = {};
756
+ this.allChanges.operations.length = 0;
757
+ if (this.allFilteredChanges !== undefined) {
758
+ this.allFilteredChanges.indexes = {};
759
+ this.allFilteredChanges.operations.length = 0;
760
+ }
477
761
  // remove children references
478
762
  this.forEachChild((changeTree, _) => this.root?.remove(changeTree));
479
763
  }
@@ -482,12 +766,13 @@ class ChangeTree {
482
766
  * Recursively discard all changes from this, and child structures.
483
767
  */
484
768
  discardAll() {
485
- this.changes.forEach((_, fieldIndex) => {
486
- const value = this.getValue(fieldIndex);
769
+ const keys = Object.keys(this.indexedOperations);
770
+ for (let i = 0, len = keys.length; i < len; i++) {
771
+ const value = this.getValue(Number(keys[i]));
487
772
  if (value && value[$changes]) {
488
773
  value[$changes].discardAll();
489
774
  }
490
- });
775
+ }
491
776
  this.discard();
492
777
  }
493
778
  ensureRefId() {
@@ -498,35 +783,49 @@ class ChangeTree {
498
783
  this.refId = this.root.getNextUniqueId();
499
784
  }
500
785
  get changed() {
501
- return this.changes.size > 0;
786
+ return (Object.entries(this.indexedOperations).length > 0);
502
787
  }
503
- checkIsFiltered(parent, parentIndex) {
788
+ checkIsFiltered(metadata, parent, parentIndex) {
504
789
  // Detect if current structure has "filters" declared
505
- this.isPartiallyFiltered = this.ref['constructor']?.[Symbol.metadata]?.[-2];
506
- // TODO: support "partially filtered", where the instance is visible, but only a field is not.
507
- // Detect if parent has "filters" declared
508
- while (parent && !this.isFiltered) {
509
- const metadata = parent['constructor'][Symbol.metadata];
510
- const fieldName = metadata?.[parentIndex];
511
- const isParentOwned = metadata?.[fieldName]?.tag !== undefined;
512
- this.isFiltered = isParentOwned || parent[$changes].isFiltered; // metadata?.[-2]
513
- parent = parent[$changes].parent;
790
+ this.isPartiallyFiltered = metadata?.[$viewFieldIndexes] !== undefined;
791
+ if (this.isPartiallyFiltered) {
792
+ this.filteredChanges = this.filteredChanges || { indexes: {}, operations: [] };
793
+ this.allFilteredChanges = this.allFilteredChanges || { indexes: {}, operations: [] };
514
794
  }
795
+ // skip if parent is not set
796
+ if (!parent) {
797
+ return;
798
+ }
799
+ if (!Metadata.isValidInstance(parent)) {
800
+ const parentChangeTree = parent[$changes];
801
+ parent = parentChangeTree.parent;
802
+ parentIndex = parentChangeTree.parentIndex;
803
+ }
804
+ const parentMetadata = parent.constructor?.[Symbol.metadata];
805
+ this.isFiltered = parentMetadata?.[$viewFieldIndexes]?.includes(parentIndex);
515
806
  //
516
807
  // TODO: refactor this!
517
808
  //
518
809
  // swapping `changes` and `filteredChanges` is required here
519
810
  // because "isFiltered" may not be imedialely available on `change()`
520
811
  //
521
- if (this.isFiltered && this.changes.size > 0) {
522
- // swap changes reference
523
- const changes = this.changes;
524
- this.changes = this.filteredChanges;
525
- this.filteredChanges = changes;
526
- // swap "all changes" reference
527
- const allFilteredChanges = this.allFilteredChanges;
528
- this.allFilteredChanges = this.allChanges;
529
- this.allChanges = allFilteredChanges;
812
+ if (this.isFiltered) {
813
+ this.filteredChanges = { indexes: {}, operations: [] };
814
+ this.allFilteredChanges = { indexes: {}, operations: [] };
815
+ if (this.changes.operations.length > 0) {
816
+ // swap changes reference
817
+ const changes = this.changes;
818
+ this.changes = this.filteredChanges;
819
+ this.filteredChanges = changes;
820
+ // swap "all changes" reference
821
+ const allFilteredChanges = this.allFilteredChanges;
822
+ this.allFilteredChanges = this.allChanges;
823
+ this.allChanges = allFilteredChanges;
824
+ // console.log("SWAP =>", {
825
+ // "this.allFilteredChanges": this.allFilteredChanges,
826
+ // "this.allChanges": this.allChanges
827
+ // })
828
+ }
530
829
  }
531
830
  }
532
831
  }
@@ -563,26 +862,29 @@ try {
563
862
  textEncoder = new TextEncoder();
564
863
  }
565
864
  catch (e) { }
566
- function utf8Length(str) {
567
- var c = 0, length = 0;
568
- for (var i = 0, l = str.length; i < l; i++) {
569
- c = str.charCodeAt(i);
570
- if (c < 0x80) {
571
- length += 1;
572
- }
573
- else if (c < 0x800) {
574
- length += 2;
575
- }
576
- else if (c < 0xd800 || c >= 0xe000) {
577
- length += 3;
578
- }
579
- else {
580
- i++;
581
- length += 4;
865
+ const hasBufferByteLength = (typeof Buffer !== 'undefined' && Buffer.byteLength);
866
+ const utf8Length = (hasBufferByteLength)
867
+ ? Buffer.byteLength // node
868
+ : function (str, _) {
869
+ var c = 0, length = 0;
870
+ for (var i = 0, l = str.length; i < l; i++) {
871
+ c = str.charCodeAt(i);
872
+ if (c < 0x80) {
873
+ length += 1;
874
+ }
875
+ else if (c < 0x800) {
876
+ length += 2;
877
+ }
878
+ else if (c < 0xd800 || c >= 0xe000) {
879
+ length += 3;
880
+ }
881
+ else {
882
+ i++;
883
+ length += 4;
884
+ }
582
885
  }
583
- }
584
- return length;
585
- }
886
+ return length;
887
+ };
586
888
  function utf8Write(view, str, it) {
587
889
  var c = 0;
588
890
  for (var i = 0, l = str.length; i < l; i++) {
@@ -591,21 +893,24 @@ function utf8Write(view, str, it) {
591
893
  view[it.offset++] = c;
592
894
  }
593
895
  else if (c < 0x800) {
594
- view[it.offset++] = 0xc0 | (c >> 6);
595
- view[it.offset++] = 0x80 | (c & 0x3f);
896
+ view[it.offset] = 0xc0 | (c >> 6);
897
+ view[it.offset + 1] = 0x80 | (c & 0x3f);
898
+ it.offset += 2;
596
899
  }
597
900
  else if (c < 0xd800 || c >= 0xe000) {
598
- view[it.offset++] = 0xe0 | (c >> 12);
599
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
600
- view[it.offset++] = 0x80 | (c & 0x3f);
901
+ view[it.offset] = 0xe0 | (c >> 12);
902
+ view[it.offset + 1] = 0x80 | (c >> 6 & 0x3f);
903
+ view[it.offset + 2] = 0x80 | (c & 0x3f);
904
+ it.offset += 3;
601
905
  }
602
906
  else {
603
907
  i++;
604
908
  c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
605
- view[it.offset++] = 0xf0 | (c >> 18);
606
- view[it.offset++] = 0x80 | (c >> 12 & 0x3f);
607
- view[it.offset++] = 0x80 | (c >> 6 & 0x3f);
608
- view[it.offset++] = 0x80 | (c & 0x3f);
909
+ view[it.offset] = 0xf0 | (c >> 18);
910
+ view[it.offset + 1] = 0x80 | (c >> 12 & 0x3f);
911
+ view[it.offset + 2] = 0x80 | (c >> 6 & 0x3f);
912
+ view[it.offset + 3] = 0x80 | (c & 0x3f);
913
+ it.offset += 4;
609
914
  }
610
915
  }
611
916
  }
@@ -677,8 +982,7 @@ function string$1(bytes, value, it) {
677
982
  if (!value) {
678
983
  value = "";
679
984
  }
680
- // let length = utf8Length(value);
681
- let length = Buffer.byteLength(value, "utf8");
985
+ let length = utf8Length(value, "utf8");
682
986
  let size = 0;
683
987
  // fixstr
684
988
  if (length < 0x20) {
@@ -789,81 +1093,30 @@ function number$1(bytes, value, it) {
789
1093
 
790
1094
  var encode = /*#__PURE__*/Object.freeze({
791
1095
  __proto__: null,
792
- utf8Length: utf8Length,
793
- utf8Write: utf8Write,
794
- int8: int8$1,
795
- uint8: uint8$1,
1096
+ boolean: boolean$1,
1097
+ float32: float32$1,
1098
+ float64: float64$1,
796
1099
  int16: int16$1,
797
- uint16: uint16$1,
798
1100
  int32: int32$1,
799
- uint32: uint32$1,
800
1101
  int64: int64$1,
1102
+ int8: int8$1,
1103
+ number: number$1,
1104
+ string: string$1,
1105
+ uint16: uint16$1,
1106
+ uint32: uint32$1,
801
1107
  uint64: uint64$1,
802
- float32: float32$1,
803
- float64: float64$1,
1108
+ uint8: uint8$1,
1109
+ utf8Length: utf8Length,
1110
+ utf8Write: utf8Write,
804
1111
  writeFloat32: writeFloat32,
805
- writeFloat64: writeFloat64,
806
- boolean: boolean$1,
807
- string: string$1,
808
- number: number$1
1112
+ writeFloat64: writeFloat64
809
1113
  });
810
1114
 
811
- class EncodeSchemaError extends Error {
812
- }
813
- function assertType(value, type, klass, field) {
814
- let typeofTarget;
815
- let allowNull = false;
816
- switch (type) {
817
- case "number":
818
- case "int8":
819
- case "uint8":
820
- case "int16":
821
- case "uint16":
822
- case "int32":
823
- case "uint32":
824
- case "int64":
825
- case "uint64":
826
- case "float32":
827
- case "float64":
828
- typeofTarget = "number";
829
- if (isNaN(value)) {
830
- console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
831
- }
832
- break;
833
- case "string":
834
- typeofTarget = "string";
835
- allowNull = true;
836
- break;
837
- case "boolean":
838
- // boolean is always encoded as true/false based on truthiness
839
- return;
840
- }
841
- if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
842
- let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
843
- throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
844
- }
845
- }
846
- function assertInstanceType(value, type, klass, field) {
847
- if (!(value instanceof type)) {
848
- throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${klass.constructor.name}#${field}`);
849
- }
850
- }
851
-
852
- function encodePrimitiveType(type, bytes, value, klass, field, it) {
853
- assertType(value, type, klass, field);
854
- const encodeFunc = encode[type];
855
- if (encodeFunc) {
856
- encodeFunc(bytes, value, it);
857
- // encodeFunc(bytes, value);
1115
+ function encodeValue(encoder, bytes, type, value, operation, it) {
1116
+ if (typeof (type) === "string") {
1117
+ encode[type]?.(bytes, value, it);
858
1118
  }
859
- else {
860
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
861
- }
862
- }
863
- function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
864
- if (type[Symbol.metadata] !== undefined) {
865
- // TODO: move this to the `@type()` annotation
866
- assertInstanceType(value, type, ref, field);
1119
+ else if (type[Symbol.metadata] !== undefined) {
867
1120
  //
868
1121
  // Encode refId for this instance.
869
1122
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -874,21 +1127,7 @@ function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
874
1127
  encoder.tryEncodeTypeId(bytes, type, value.constructor, it);
875
1128
  }
876
1129
  }
877
- else if (typeof (type) === "string") {
878
- //
879
- // Primitive values
880
- //
881
- encodePrimitiveType(type, bytes, value, ref, field, it);
882
- }
883
1130
  else {
884
- //
885
- // Custom type (MapSchema, ArraySchema, etc)
886
- //
887
- const definition = getType(Object.keys(type)[0]);
888
- //
889
- // ensure a ArraySchema has been provided
890
- //
891
- assertInstanceType(ref[field], definition.constructor, ref, field);
892
1131
  //
893
1132
  // Encode refId for this instance.
894
1133
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -900,27 +1139,23 @@ function encodeValue(encoder, bytes, ref, type, value, field, operation, it) {
900
1139
  * Used for Schema instances.
901
1140
  * @private
902
1141
  */
903
- const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it) {
904
- const ref = changeTree.ref;
905
- const metadata = ref['constructor'][Symbol.metadata];
906
- const field = metadata[index];
907
- const type = metadata[field].type;
908
- const value = ref[field];
1142
+ const encodeSchemaOperation = function (encoder, bytes, changeTree, index, operation, it, _, __, metadata) {
909
1143
  // "compress" field index + operation
910
1144
  bytes[it.offset++] = (index | operation) & 255;
911
1145
  // Do not encode value for DELETE operations
912
1146
  if (operation === exports.OPERATION.DELETE) {
913
1147
  return;
914
1148
  }
1149
+ const ref = changeTree.ref;
1150
+ const field = metadata[index];
915
1151
  // TODO: inline this function call small performance gain
916
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1152
+ encodeValue(encoder, bytes, metadata[index].type, ref[field.name], operation, it);
917
1153
  };
918
1154
  /**
919
1155
  * Used for collections (MapSchema, CollectionSchema, SetSchema)
920
1156
  * @private
921
1157
  */
922
- const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, operation, it) {
923
- const ref = changeTree.ref;
1158
+ const encodeKeyValueOperation = function (encoder, bytes, changeTree, index, operation, it) {
924
1159
  // encode operation
925
1160
  bytes[it.offset++] = operation & 255;
926
1161
  // custom operations
@@ -928,27 +1163,40 @@ const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, ope
928
1163
  return;
929
1164
  }
930
1165
  // encode index
931
- number$1(bytes, field, it);
1166
+ number$1(bytes, index, it);
932
1167
  // Do not encode value for DELETE operations
933
1168
  if (operation === exports.OPERATION.DELETE) {
934
1169
  return;
935
1170
  }
1171
+ const ref = changeTree.ref;
936
1172
  //
937
1173
  // encode "alias" for dynamic fields (maps)
938
1174
  //
939
- if ((operation & exports.OPERATION.ADD) == exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
1175
+ if ((operation & exports.OPERATION.ADD) === exports.OPERATION.ADD) { // ADD or DELETE_AND_ADD
940
1176
  if (typeof (ref['set']) === "function") {
941
1177
  //
942
1178
  // MapSchema dynamic key
943
1179
  //
944
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
1180
+ const dynamicIndex = changeTree.ref['$indexes'].get(index);
945
1181
  string$1(bytes, dynamicIndex, it);
946
1182
  }
947
1183
  }
948
- const type = changeTree.getType(field);
949
- const value = changeTree.getValue(field);
1184
+ const type = ref[$childType];
1185
+ const value = ref[$getByIndex](index);
1186
+ // try { throw new Error(); } catch (e) {
1187
+ // // only print if not coming from Reflection.ts
1188
+ // if (!e.stack.includes("src/Reflection.ts")) {
1189
+ // console.log("encodeKeyValueOperation -> ", {
1190
+ // ref: changeTree.ref.constructor.name,
1191
+ // field,
1192
+ // operation: OPERATION[operation],
1193
+ // value: value?.toJSON(),
1194
+ // items: ref.toJSON(),
1195
+ // });
1196
+ // }
1197
+ // }
950
1198
  // TODO: inline this function call small performance gain
951
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1199
+ encodeValue(encoder, bytes, type, value, operation, it);
952
1200
  };
953
1201
  /**
954
1202
  * Used for collections (MapSchema, ArraySchema, etc.)
@@ -956,24 +1204,29 @@ const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, ope
956
1204
  */
957
1205
  const encodeArray = function (encoder, bytes, changeTree, field, operation, it, isEncodeAll, hasView) {
958
1206
  const ref = changeTree.ref;
959
- if (hasView &&
960
- operation === exports.OPERATION.DELETE &&
961
- typeof (changeTree.getType(field)) !== "string") {
962
- // encode delete by refId (array of schemas)
963
- bytes[it.offset++] = exports.OPERATION.DELETE_BY_REFID;
964
- const value = ref['tmpItems'][field];
965
- const refId = value[$changes].refId;
966
- number$1(bytes, refId, it);
967
- return;
1207
+ const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
1208
+ let refOrIndex;
1209
+ if (useOperationByRefId) {
1210
+ refOrIndex = ref['tmpItems'][field][$changes].refId;
1211
+ if (operation === exports.OPERATION.DELETE) {
1212
+ operation = exports.OPERATION.DELETE_BY_REFID;
1213
+ }
1214
+ else if (operation === exports.OPERATION.ADD) {
1215
+ operation = exports.OPERATION.ADD_BY_REFID;
1216
+ }
1217
+ }
1218
+ else {
1219
+ refOrIndex = field;
968
1220
  }
969
1221
  // encode operation
970
1222
  bytes[it.offset++] = operation & 255;
971
1223
  // custom operations
972
- if (operation === exports.OPERATION.CLEAR) {
1224
+ if (operation === exports.OPERATION.CLEAR ||
1225
+ operation === exports.OPERATION.REVERSE) {
973
1226
  return;
974
1227
  }
975
1228
  // encode index
976
- number$1(bytes, field, it);
1229
+ number$1(bytes, refOrIndex, it);
977
1230
  // Do not encode value for DELETE operations
978
1231
  if (operation === exports.OPERATION.DELETE) {
979
1232
  return;
@@ -988,7 +1241,7 @@ const encodeArray = function (encoder, bytes, changeTree, field, operation, it,
988
1241
  // items: ref.toJSON(),
989
1242
  // });
990
1243
  // TODO: inline this function call small performance gain
991
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
1244
+ encodeValue(encoder, bytes, type, value, operation, it);
992
1245
  };
993
1246
 
994
1247
  /**
@@ -1222,31 +1475,31 @@ function switchStructureCheck(bytes, it) {
1222
1475
 
1223
1476
  var decode = /*#__PURE__*/Object.freeze({
1224
1477
  __proto__: null,
1225
- utf8Read: utf8Read,
1226
- int8: int8,
1227
- uint8: uint8,
1228
- int16: int16,
1229
- uint16: uint16,
1230
- int32: int32,
1231
- uint32: uint32,
1478
+ arrayCheck: arrayCheck,
1479
+ boolean: boolean,
1232
1480
  float32: float32,
1233
1481
  float64: float64,
1482
+ int16: int16,
1483
+ int32: int32,
1234
1484
  int64: int64,
1235
- uint64: uint64,
1485
+ int8: int8,
1486
+ number: number,
1487
+ numberCheck: numberCheck,
1236
1488
  readFloat32: readFloat32,
1237
1489
  readFloat64: readFloat64,
1238
- boolean: boolean,
1239
1490
  string: string,
1240
1491
  stringCheck: stringCheck,
1241
- number: number,
1242
- numberCheck: numberCheck,
1243
- arrayCheck: arrayCheck,
1244
- switchStructureCheck: switchStructureCheck
1492
+ switchStructureCheck: switchStructureCheck,
1493
+ uint16: uint16,
1494
+ uint32: uint32,
1495
+ uint64: uint64,
1496
+ uint8: uint8,
1497
+ utf8Read: utf8Read
1245
1498
  });
1246
1499
 
1247
1500
  const DEFINITION_MISMATCH = -1;
1248
1501
  function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges) {
1249
- const $root = decoder.$root;
1502
+ const $root = decoder.root;
1250
1503
  const previousValue = ref[$getByIndex](index);
1251
1504
  let value;
1252
1505
  if ((operation & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
@@ -1294,7 +1547,9 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1294
1547
  if (!value) {
1295
1548
  value = decoder.createInstanceOfType(childType);
1296
1549
  }
1297
- $root.addRef(refId, value, (value !== previousValue));
1550
+ $root.addRef(refId, value, (value !== previousValue || // increment ref count if value has changed
1551
+ (operation === exports.OPERATION.DELETE_AND_ADD && value === previousValue) // increment ref count if the same instance is being added again
1552
+ ));
1298
1553
  }
1299
1554
  }
1300
1555
  else if (typeof (type) === "string") {
@@ -1345,18 +1600,19 @@ function decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges
1345
1600
  }
1346
1601
  const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1347
1602
  const first_byte = bytes[it.offset++];
1348
- const metadata = ref['constructor'][Symbol.metadata];
1603
+ const metadata = ref.constructor[Symbol.metadata];
1349
1604
  // "compressed" index + operation
1350
1605
  const operation = (first_byte >> 6) << 6;
1351
1606
  const index = first_byte % (operation || 255);
1352
1607
  // skip early if field is not defined
1353
1608
  const field = metadata[index];
1354
1609
  if (field === undefined) {
1610
+ console.warn("@colyseus/schema: field not defined at", { index, ref: ref.constructor.name, metadata });
1355
1611
  return DEFINITION_MISMATCH;
1356
1612
  }
1357
- const { value, previousValue } = decodeValue(decoder, operation, ref, index, metadata[field].type, bytes, it, allChanges);
1613
+ const { value, previousValue } = decodeValue(decoder, operation, ref, index, field.type, bytes, it, allChanges);
1358
1614
  if (value !== null && value !== undefined) {
1359
- ref[field] = value;
1615
+ ref[field.name] = value;
1360
1616
  }
1361
1617
  // add change
1362
1618
  if (previousValue !== value) {
@@ -1364,7 +1620,7 @@ const decodeSchemaOperation = function (decoder, bytes, it, ref, allChanges) {
1364
1620
  ref,
1365
1621
  refId: decoder.currentRefId,
1366
1622
  op: operation,
1367
- field: field,
1623
+ field: field.name,
1368
1624
  value,
1369
1625
  previousValue,
1370
1626
  });
@@ -1432,7 +1688,8 @@ const decodeKeyValueOperation = function (decoder, bytes, it, ref, allChanges) {
1432
1688
  };
1433
1689
  const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1434
1690
  // "uncompressed" index + operation (array/map items)
1435
- const operation = bytes[it.offset++];
1691
+ let operation = bytes[it.offset++];
1692
+ let index;
1436
1693
  if (operation === exports.OPERATION.CLEAR) {
1437
1694
  //
1438
1695
  // When decoding:
@@ -1443,11 +1700,15 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1443
1700
  ref.clear();
1444
1701
  return;
1445
1702
  }
1703
+ else if (operation === exports.OPERATION.REVERSE) {
1704
+ ref.reverse();
1705
+ return;
1706
+ }
1446
1707
  else if (operation === exports.OPERATION.DELETE_BY_REFID) {
1447
1708
  // TODO: refactor here, try to follow same flow as below
1448
1709
  const refId = number(bytes, it);
1449
- const previousValue = decoder.$root.refs.get(refId);
1450
- const index = ref.findIndex((value) => value === previousValue);
1710
+ const previousValue = decoder.root.refs.get(refId);
1711
+ index = ref.findIndex((value) => value === previousValue);
1451
1712
  ref[$deleteByIndex](index);
1452
1713
  allChanges.push({
1453
1714
  ref,
@@ -1460,7 +1721,17 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1460
1721
  });
1461
1722
  return;
1462
1723
  }
1463
- const index = number(bytes, it);
1724
+ else if (operation === exports.OPERATION.ADD_BY_REFID) {
1725
+ const refId = number(bytes, it);
1726
+ const itemByRefId = decoder.root.refs.get(refId);
1727
+ // use existing index, or push new value
1728
+ index = (itemByRefId)
1729
+ ? ref.findIndex((value) => value === itemByRefId)
1730
+ : ref.length;
1731
+ }
1732
+ else {
1733
+ index = number(bytes, it);
1734
+ }
1464
1735
  const type = ref[$childType];
1465
1736
  let dynamicIndex = index;
1466
1737
  const { value, previousValue } = decodeValue(decoder, operation, ref, index, type, bytes, it, allChanges);
@@ -1484,6 +1755,47 @@ const decodeArray = function (decoder, bytes, it, ref, allChanges) {
1484
1755
  }
1485
1756
  };
1486
1757
 
1758
+ class EncodeSchemaError extends Error {
1759
+ }
1760
+ function assertType(value, type, klass, field) {
1761
+ let typeofTarget;
1762
+ let allowNull = false;
1763
+ switch (type) {
1764
+ case "number":
1765
+ case "int8":
1766
+ case "uint8":
1767
+ case "int16":
1768
+ case "uint16":
1769
+ case "int32":
1770
+ case "uint32":
1771
+ case "int64":
1772
+ case "uint64":
1773
+ case "float32":
1774
+ case "float64":
1775
+ typeofTarget = "number";
1776
+ if (isNaN(value)) {
1777
+ console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`);
1778
+ }
1779
+ break;
1780
+ case "string":
1781
+ typeofTarget = "string";
1782
+ allowNull = true;
1783
+ break;
1784
+ case "boolean":
1785
+ // boolean is always encoded as true/false based on truthiness
1786
+ return;
1787
+ }
1788
+ if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) {
1789
+ let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`;
1790
+ throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`);
1791
+ }
1792
+ }
1793
+ function assertInstanceType(value, type, instance, field) {
1794
+ if (!(value instanceof type)) {
1795
+ throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value && value.constructor.name}' was provided in ${instance.constructor.name}#${field}`);
1796
+ }
1797
+ }
1798
+
1487
1799
  var _a$4, _b$4;
1488
1800
  const DEFAULT_SORT = (a, b) => {
1489
1801
  const A = a.toString();
@@ -1534,6 +1846,7 @@ class ArraySchema {
1534
1846
  const proxy = new Proxy(this, {
1535
1847
  get: (obj, prop) => {
1536
1848
  if (typeof (prop) !== "symbol" &&
1849
+ // FIXME: d8 accuses this as low performance
1537
1850
  !isNaN(prop) // https://stackoverflow.com/a/175787/892698
1538
1851
  ) {
1539
1852
  return this.items[prop];
@@ -1549,8 +1862,9 @@ class ArraySchema {
1549
1862
  }
1550
1863
  else {
1551
1864
  if (setValue[$changes]) {
1865
+ assertInstanceType(setValue, obj[$childType], obj, key);
1552
1866
  if (obj.items[key] !== undefined) {
1553
- if (setValue[$changes][$isNew]) {
1867
+ if (setValue[$changes].isNew) {
1554
1868
  this[$changes].indexedOperation(Number(key), exports.OPERATION.MOVE_AND_ADD);
1555
1869
  }
1556
1870
  else {
@@ -1562,7 +1876,7 @@ class ArraySchema {
1562
1876
  }
1563
1877
  }
1564
1878
  }
1565
- else if (setValue[$changes][$isNew]) {
1879
+ else if (setValue[$changes].isNew) {
1566
1880
  this[$changes].indexedOperation(Number(key), exports.OPERATION.ADD);
1567
1881
  }
1568
1882
  }
@@ -1595,7 +1909,10 @@ class ArraySchema {
1595
1909
  }
1596
1910
  });
1597
1911
  this[$changes] = new ChangeTree(proxy);
1598
- this.push.apply(this, items);
1912
+ this[$changes].indexes = {};
1913
+ if (items.length > 0) {
1914
+ this.push(...items);
1915
+ }
1599
1916
  return proxy;
1600
1917
  }
1601
1918
  set length(newLength) {
@@ -1614,14 +1931,19 @@ class ArraySchema {
1614
1931
  }
1615
1932
  push(...values) {
1616
1933
  let length = this.tmpItems.length;
1617
- values.forEach((value, i) => {
1618
- // skip null values
1934
+ const changeTree = this[$changes];
1935
+ // values.forEach((value, i) => {
1936
+ for (let i = 0, l = values.length; i < values.length; i++, length++) {
1937
+ const value = values[i];
1619
1938
  if (value === undefined || value === null) {
1939
+ // skip null values
1620
1940
  return;
1621
1941
  }
1622
- const changeTree = this[$changes];
1942
+ else if (typeof (value) === "object" && this[$childType]) {
1943
+ assertInstanceType(value, this[$childType], this, i);
1944
+ // TODO: move value[$changes]?.setParent() to this block.
1945
+ }
1623
1946
  changeTree.indexedOperation(length, exports.OPERATION.ADD, this.items.length);
1624
- // changeTree.indexes[length] = length;
1625
1947
  this.items.push(value);
1626
1948
  this.tmpItems.push(value);
1627
1949
  //
@@ -1629,8 +1951,9 @@ class ArraySchema {
1629
1951
  // (to avoid encoding "refId" operations before parent's "ADD" operation)
1630
1952
  //
1631
1953
  value[$changes]?.setParent(this, changeTree.root, length);
1632
- length++;
1633
- });
1954
+ }
1955
+ // length++;
1956
+ // });
1634
1957
  return length;
1635
1958
  }
1636
1959
  /**
@@ -1651,6 +1974,7 @@ class ArraySchema {
1651
1974
  }
1652
1975
  this[$changes].delete(index, undefined, this.items.length - 1);
1653
1976
  // this.tmpItems[index] = undefined;
1977
+ // this.tmpItems.pop();
1654
1978
  this.deletedIndexes[index] = true;
1655
1979
  return this.items.pop();
1656
1980
  }
@@ -1715,9 +2039,12 @@ class ArraySchema {
1715
2039
  //
1716
2040
  // TODO: do not use [$changes] at decoding time.
1717
2041
  //
1718
- changeTree.root?.changes.delete(changeTree);
1719
- changeTree.root?.allChanges.delete(changeTree);
1720
- changeTree.root?.allFilteredChanges.delete(changeTree);
2042
+ const root = changeTree.root;
2043
+ if (root !== undefined) {
2044
+ root.removeChangeFromChangeSet("changes", changeTree);
2045
+ root.removeChangeFromChangeSet("allChanges", changeTree);
2046
+ root.removeChangeFromChangeSet("allFilteredChanges", changeTree);
2047
+ }
1721
2048
  });
1722
2049
  changeTree.discard(true);
1723
2050
  changeTree.operation(exports.OPERATION.CLEAR);
@@ -1761,6 +2088,7 @@ class ArraySchema {
1761
2088
  const changeTree = this[$changes];
1762
2089
  changeTree.delete(index);
1763
2090
  changeTree.shiftAllChangeIndexes(-1, index);
2091
+ // this.deletedIndexes[index] = true;
1764
2092
  return this.items.shift();
1765
2093
  }
1766
2094
  /**
@@ -1841,10 +2169,12 @@ class ArraySchema {
1841
2169
  changeTree.shiftChangeIndexes(items.length);
1842
2170
  // new index
1843
2171
  if (changeTree.isFiltered) {
1844
- changeTree.filteredChanges.set(this.items.length, exports.OPERATION.ADD);
2172
+ setOperationAtIndex(changeTree.filteredChanges, this.items.length);
2173
+ // changeTree.filteredChanges[this.items.length] = OPERATION.ADD;
1845
2174
  }
1846
2175
  else {
1847
- changeTree.allChanges.set(this.items.length, exports.OPERATION.ADD);
2176
+ setOperationAtIndex(changeTree.allChanges, this.items.length);
2177
+ // changeTree.allChanges[this.items.length] = OPERATION.ADD;
1848
2178
  }
1849
2179
  // FIXME: should we use OPERATION.MOVE here instead?
1850
2180
  items.forEach((_, index) => {
@@ -1869,14 +2199,6 @@ class ArraySchema {
1869
2199
  lastIndexOf(searchElement, fromIndex = this.length - 1) {
1870
2200
  return this.items.lastIndexOf(searchElement, fromIndex);
1871
2201
  }
1872
- /**
1873
- * Determines whether all the members of an array satisfy the specified test.
1874
- * @param callbackfn A function that accepts up to three arguments. The every method calls
1875
- * the callbackfn function for each element in the array until the callbackfn returns a value
1876
- * which is coercible to the Boolean value false, or until the end of the array.
1877
- * @param thisArg An object to which the this keyword can refer in the callbackfn function.
1878
- * If thisArg is omitted, undefined is used as the this value.
1879
- */
1880
2202
  every(callbackfn, thisArg) {
1881
2203
  return this.items.every(callbackfn, thisArg);
1882
2204
  }
@@ -2155,6 +2477,7 @@ class MapSchema {
2155
2477
  this.$items = new Map();
2156
2478
  this.$indexes = new Map();
2157
2479
  this[$changes] = new ChangeTree(this);
2480
+ this[$changes].indexes = {};
2158
2481
  if (initialValues) {
2159
2482
  if (initialValues instanceof Map ||
2160
2483
  initialValues instanceof MapSchema) {
@@ -2181,6 +2504,9 @@ class MapSchema {
2181
2504
  if (value === undefined || value === null) {
2182
2505
  throw new Error(`MapSchema#set('${key}', ${value}): trying to set ${value} value on '${key}'.`);
2183
2506
  }
2507
+ else if (typeof (value) === "object" && this[$childType]) {
2508
+ assertInstanceType(value, this[$childType], this, key);
2509
+ }
2184
2510
  // Force "key" as string
2185
2511
  // See: https://github.com/colyseus/colyseus/issues/561#issuecomment-1646733468
2186
2512
  key = key.toString();
@@ -2189,7 +2515,7 @@ class MapSchema {
2189
2515
  const isReplace = typeof (changeTree.indexes[key]) !== "undefined";
2190
2516
  const index = (isReplace)
2191
2517
  ? changeTree.indexes[key]
2192
- : changeTree.indexes[-1] ?? 0;
2518
+ : changeTree.indexes[$numFields] ?? 0;
2193
2519
  let operation = (isReplace)
2194
2520
  ? exports.OPERATION.REPLACE
2195
2521
  : exports.OPERATION.ADD;
@@ -2201,7 +2527,7 @@ class MapSchema {
2201
2527
  if (!isReplace) {
2202
2528
  this.$indexes.set(index, key);
2203
2529
  changeTree.indexes[key] = index;
2204
- changeTree.indexes[-1] = index + 1;
2530
+ changeTree.indexes[$numFields] = index + 1;
2205
2531
  }
2206
2532
  else if (!isRef &&
2207
2533
  this.$items.get(key) === value) {
@@ -2276,8 +2602,11 @@ class MapSchema {
2276
2602
  }
2277
2603
  [$onEncodeEnd]() {
2278
2604
  const changeTree = this[$changes];
2279
- const changes = changeTree.changes.entries();
2280
- for (const [fieldIndex, operation] of changes) {
2605
+ const keys = Object.keys(changeTree.indexedOperations);
2606
+ for (let i = 0, len = keys.length; i < len; i++) {
2607
+ const key = keys[i];
2608
+ const fieldIndex = Number(key);
2609
+ const operation = changeTree.indexedOperations[key];
2281
2610
  if (operation === exports.OPERATION.DELETE) {
2282
2611
  const index = this[$getByIndex](fieldIndex);
2283
2612
  delete changeTree.indexes[index];
@@ -2310,104 +2639,17 @@ class MapSchema {
2310
2639
  if (value[$changes]) {
2311
2640
  cloned.set(key, value['clone']());
2312
2641
  }
2313
- else {
2314
- cloned.set(key, value);
2315
- }
2316
- });
2317
- }
2318
- return cloned;
2319
- }
2320
- }
2321
- registerType("map", { constructor: MapSchema });
2322
-
2323
- const DEFAULT_VIEW_TAG = -1;
2324
- class TypeContext {
2325
- /**
2326
- * For inheritance support
2327
- * Keeps track of which classes extends which. (parent -> children)
2328
- */
2329
- static { this.inheritedTypes = new Map(); }
2330
- static register(target) {
2331
- const parent = Object.getPrototypeOf(target);
2332
- if (parent !== Schema) {
2333
- let inherits = TypeContext.inheritedTypes.get(parent);
2334
- if (!inherits) {
2335
- inherits = new Set();
2336
- TypeContext.inheritedTypes.set(parent, inherits);
2337
- }
2338
- inherits.add(target);
2339
- }
2340
- }
2341
- constructor(rootClass) {
2342
- this.types = {};
2343
- this.schemas = new Map();
2344
- this.hasFilters = false;
2345
- if (rootClass) {
2346
- this.discoverTypes(rootClass);
2347
- }
2348
- }
2349
- has(schema) {
2350
- return this.schemas.has(schema);
2351
- }
2352
- get(typeid) {
2353
- return this.types[typeid];
2354
- }
2355
- add(schema, typeid = this.schemas.size) {
2356
- // skip if already registered
2357
- if (this.schemas.has(schema)) {
2358
- return false;
2359
- }
2360
- this.types[typeid] = schema;
2361
- this.schemas.set(schema, typeid);
2362
- return true;
2363
- }
2364
- getTypeId(klass) {
2365
- return this.schemas.get(klass);
2366
- }
2367
- discoverTypes(klass) {
2368
- if (!this.add(klass)) {
2369
- return;
2370
- }
2371
- // add classes inherited from this base class
2372
- TypeContext.inheritedTypes.get(klass)?.forEach((child) => {
2373
- this.discoverTypes(child);
2374
- });
2375
- // skip if no fields are defined for this class.
2376
- if (klass[Symbol.metadata] === undefined) {
2377
- klass[Symbol.metadata] = {};
2378
- }
2379
- // const metadata = Metadata.getFor(klass);
2380
- const metadata = klass[Symbol.metadata];
2381
- // if any schema/field has filters, mark "context" as having filters.
2382
- if (metadata[-2]) {
2383
- this.hasFilters = true;
2384
- }
2385
- for (const field in metadata) {
2386
- const fieldType = metadata[field].type;
2387
- if (typeof (fieldType) === "string") {
2388
- continue;
2389
- }
2390
- if (Array.isArray(fieldType)) {
2391
- const type = fieldType[0];
2392
- if (type === "string") {
2393
- continue;
2394
- }
2395
- this.discoverTypes(type);
2396
- }
2397
- else if (typeof (fieldType) === "function") {
2398
- this.discoverTypes(fieldType);
2399
- }
2400
- else {
2401
- const type = Object.values(fieldType)[0];
2402
- // skip primitive types
2403
- if (typeof (type) === "string") {
2404
- continue;
2642
+ else {
2643
+ cloned.set(key, value);
2405
2644
  }
2406
- this.discoverTypes(type);
2407
- }
2645
+ });
2408
2646
  }
2647
+ return cloned;
2409
2648
  }
2410
2649
  }
2650
+ registerType("map", { constructor: MapSchema });
2651
+
2652
+ const DEFAULT_VIEW_TAG = -1;
2411
2653
  /**
2412
2654
  * [See documentation](https://docs.colyseus.io/state/schema/)
2413
2655
  *
@@ -2434,8 +2676,8 @@ class TypeContext {
2434
2676
  // // detect index for this field, considering inheritance
2435
2677
  // //
2436
2678
  // const parent = Object.getPrototypeOf(context.metadata);
2437
- // let fieldIndex: number = context.metadata[-1] // current structure already has fields defined
2438
- // ?? (parent && parent[-1]) // parent structure has fields defined
2679
+ // let fieldIndex: number = context.metadata[$numFields] // current structure already has fields defined
2680
+ // ?? (parent && parent[$numFields]) // parent structure has fields defined
2439
2681
  // ?? -1; // no fields defined
2440
2682
  // fieldIndex++;
2441
2683
  // if (
@@ -2555,18 +2797,20 @@ function view(tag = DEFAULT_VIEW_TAG) {
2555
2797
  const constructor = target.constructor;
2556
2798
  const parentClass = Object.getPrototypeOf(constructor);
2557
2799
  const parentMetadata = parentClass[Symbol.metadata];
2800
+ // TODO: use Metadata.initialize()
2558
2801
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2559
- if (!metadata[fieldName]) {
2560
- //
2561
- // detect index for this field, considering inheritance
2562
- //
2563
- metadata[fieldName] = {
2564
- type: undefined,
2565
- index: (metadata[-1] // current structure already has fields defined
2566
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2567
- ?? -1) + 1 // no fields defined
2568
- };
2569
- }
2802
+ // const fieldIndex = metadata[fieldName];
2803
+ // if (!metadata[fieldIndex]) {
2804
+ // //
2805
+ // // detect index for this field, considering inheritance
2806
+ // //
2807
+ // metadata[fieldIndex] = {
2808
+ // type: undefined,
2809
+ // index: (metadata[$numFields] // current structure already has fields defined
2810
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2811
+ // ?? -1) + 1 // no fields defined
2812
+ // }
2813
+ // }
2570
2814
  Metadata.setTag(metadata, fieldName, tag);
2571
2815
  };
2572
2816
  }
@@ -2580,17 +2824,17 @@ function type(type, options) {
2580
2824
  TypeContext.register(constructor);
2581
2825
  const parentClass = Object.getPrototypeOf(constructor);
2582
2826
  const parentMetadata = parentClass[Symbol.metadata];
2583
- const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2584
- let fieldIndex;
2827
+ const metadata = Metadata.initialize(constructor);
2828
+ let fieldIndex = metadata[field];
2585
2829
  /**
2586
2830
  * skip if descriptor already exists for this field (`@deprecated()`)
2587
2831
  */
2588
- if (metadata[field]) {
2589
- if (metadata[field].deprecated) {
2832
+ if (metadata[fieldIndex] !== undefined) {
2833
+ if (metadata[fieldIndex].deprecated) {
2590
2834
  // do not create accessors for deprecated properties.
2591
2835
  return;
2592
2836
  }
2593
- else if (metadata[field].descriptor !== undefined) {
2837
+ else if (metadata[fieldIndex].type !== undefined) {
2594
2838
  // trying to define same property multiple times across inheritance.
2595
2839
  // https://github.com/colyseus/colyseus-unity3d/issues/131#issuecomment-814308572
2596
2840
  try {
@@ -2601,16 +2845,13 @@ function type(type, options) {
2601
2845
  throw new Error(`${e.message} ${definitionAtLine}`);
2602
2846
  }
2603
2847
  }
2604
- else {
2605
- fieldIndex = metadata[field].index;
2606
- }
2607
2848
  }
2608
2849
  else {
2609
2850
  //
2610
2851
  // detect index for this field, considering inheritance
2611
2852
  //
2612
- fieldIndex = metadata[-1] // current structure already has fields defined
2613
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2853
+ fieldIndex = metadata[$numFields] // current structure already has fields defined
2854
+ ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2614
2855
  ?? -1; // no fields defined
2615
2856
  fieldIndex++;
2616
2857
  }
@@ -2629,15 +2870,15 @@ function type(type, options) {
2629
2870
  const childType = (complexTypeKlass)
2630
2871
  ? Object.values(type)[0]
2631
2872
  : type;
2632
- Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass, metadata, field));
2873
+ Metadata.addField(metadata, fieldIndex, field, type, getPropertyDescriptor(`_${field}`, fieldIndex, childType, complexTypeKlass));
2633
2874
  }
2634
2875
  };
2635
2876
  }
2636
- function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass, metadata, field) {
2877
+ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass) {
2637
2878
  return {
2638
2879
  get: function () { return this[fieldCached]; },
2639
2880
  set: function (value) {
2640
- const previousValue = this[fieldCached] || undefined;
2881
+ const previousValue = this[fieldCached] ?? undefined;
2641
2882
  // skip if value is the same as cached.
2642
2883
  if (value === previousValue) {
2643
2884
  return;
@@ -2655,22 +2896,27 @@ function getPropertyDescriptor(fieldCached, fieldIndex, type, complexTypeKlass,
2655
2896
  }
2656
2897
  value[$childType] = type;
2657
2898
  }
2899
+ else if (typeof (type) !== "string") {
2900
+ assertInstanceType(value, type, this, fieldCached.substring(1));
2901
+ }
2902
+ else {
2903
+ assertType(value, type, this, fieldCached.substring(1));
2904
+ }
2905
+ const changeTree = this[$changes];
2658
2906
  //
2659
2907
  // Replacing existing "ref", remove it from root.
2660
2908
  // TODO: if there are other references to this instance, we should not remove it from root.
2661
2909
  //
2662
2910
  if (previousValue !== undefined && previousValue[$changes]) {
2663
- this[$changes].root?.remove(previousValue[$changes]);
2911
+ changeTree.root?.remove(previousValue[$changes]);
2664
2912
  }
2665
2913
  // flag the change for encoding.
2666
- this.constructor[$track](this[$changes], fieldIndex, exports.OPERATION.ADD);
2914
+ this.constructor[$track](changeTree, fieldIndex, exports.OPERATION.ADD);
2667
2915
  //
2668
2916
  // call setParent() recursively for this and its child
2669
2917
  // structures.
2670
2918
  //
2671
- if (value[$changes]) {
2672
- value[$changes].setParent(this, this[$changes].root, metadata[field].index);
2673
- }
2919
+ value[$changes]?.setParent(this, changeTree.root, fieldIndex);
2674
2920
  }
2675
2921
  else if (previousValue !== undefined) {
2676
2922
  //
@@ -2697,20 +2943,22 @@ function deprecated(throws = true) {
2697
2943
  const parentClass = Object.getPrototypeOf(constructor);
2698
2944
  const parentMetadata = parentClass[Symbol.metadata];
2699
2945
  const metadata = (constructor[Symbol.metadata] ??= Object.assign({}, constructor[Symbol.metadata], parentMetadata ?? Object.create(null)));
2700
- if (!metadata[field]) {
2701
- //
2702
- // detect index for this field, considering inheritance
2703
- //
2704
- metadata[field] = {
2705
- type: undefined,
2706
- index: (metadata[-1] // current structure already has fields defined
2707
- ?? (parentMetadata && parentMetadata[-1]) // parent structure has fields defined
2708
- ?? -1) + 1 // no fields defined
2709
- };
2710
- }
2711
- metadata[field].deprecated = true;
2946
+ const fieldIndex = metadata[field];
2947
+ // if (!metadata[field]) {
2948
+ // //
2949
+ // // detect index for this field, considering inheritance
2950
+ // //
2951
+ // metadata[field] = {
2952
+ // type: undefined,
2953
+ // index: (metadata[$numFields] // current structure already has fields defined
2954
+ // ?? (parentMetadata && parentMetadata[$numFields]) // parent structure has fields defined
2955
+ // ?? -1) + 1 // no fields defined
2956
+ // }
2957
+ // }
2958
+ metadata[fieldIndex].deprecated = true;
2712
2959
  if (throws) {
2713
- metadata[field].descriptor = {
2960
+ metadata[$descriptors] ??= {};
2961
+ metadata[$descriptors][field] = {
2714
2962
  get: function () { throw new Error(`${field} is deprecated.`); },
2715
2963
  set: function (value) { },
2716
2964
  enumerable: false,
@@ -2718,8 +2966,8 @@ function deprecated(throws = true) {
2718
2966
  };
2719
2967
  }
2720
2968
  // flag metadata[field] as non-enumerable
2721
- Object.defineProperty(metadata, field, {
2722
- value: metadata[field],
2969
+ Object.defineProperty(metadata, fieldIndex, {
2970
+ value: metadata[fieldIndex],
2723
2971
  enumerable: false,
2724
2972
  configurable: true
2725
2973
  });
@@ -2731,6 +2979,37 @@ function defineTypes(target, fields, options) {
2731
2979
  }
2732
2980
  return target;
2733
2981
  }
2982
+ function schema(fields, name, inherits = Schema) {
2983
+ const defaultValues = {};
2984
+ const viewTagFields = {};
2985
+ for (let fieldName in fields) {
2986
+ const field = fields[fieldName];
2987
+ if (typeof (field) === "object") {
2988
+ if (field['default'] !== undefined) {
2989
+ defaultValues[fieldName] = field['default'];
2990
+ }
2991
+ if (field['view'] !== undefined) {
2992
+ viewTagFields[fieldName] = (typeof (field['view']) === "boolean")
2993
+ ? DEFAULT_VIEW_TAG
2994
+ : field['view'];
2995
+ }
2996
+ }
2997
+ }
2998
+ const klass = Metadata.setFields(class extends inherits {
2999
+ constructor(...args) {
3000
+ args[0] = Object.assign({}, defaultValues, args[0]);
3001
+ super(...args);
3002
+ }
3003
+ }, fields);
3004
+ for (let fieldName in viewTagFields) {
3005
+ view(viewTagFields[fieldName])(klass.prototype, fieldName);
3006
+ }
3007
+ if (name) {
3008
+ Object.defineProperty(klass, "name", { value: name });
3009
+ }
3010
+ klass.extends = (fields, name) => schema(fields, name, klass);
3011
+ return klass;
3012
+ }
2734
3013
 
2735
3014
  function getIndent(level) {
2736
3015
  return (new Array(level).fill(0)).map((_, i) => (i === level - 1) ? `└─ ` : ` `).join("");
@@ -2741,15 +3020,18 @@ function dumpChanges(schema) {
2741
3020
  ops: {},
2742
3021
  refs: []
2743
3022
  };
2744
- $root.changes.forEach((operations, changeTree) => {
3023
+ // for (const refId in $root.changes) {
3024
+ $root.changes.forEach(changeTree => {
3025
+ const changes = changeTree.indexedOperations;
2745
3026
  dump.refs.push(`refId#${changeTree.refId}`);
2746
- operations.forEach((op, index) => {
3027
+ for (const index in changes) {
3028
+ const op = changes[index];
2747
3029
  const opName = exports.OPERATION[op];
2748
3030
  if (!dump.ops[opName]) {
2749
3031
  dump.ops[opName] = 0;
2750
3032
  }
2751
3033
  dump.ops[exports.OPERATION[op]]++;
2752
- });
3034
+ }
2753
3035
  });
2754
3036
  return dump;
2755
3037
  }
@@ -2775,6 +3057,7 @@ var _a$2, _b$2;
2775
3057
  class Schema {
2776
3058
  static { this[_a$2] = encodeSchemaOperation; }
2777
3059
  static { this[_b$2] = decodeSchemaOperation; }
3060
+ // public [$changes]: ChangeTree;
2778
3061
  /**
2779
3062
  * Assign the property descriptors required to track changes on this instance.
2780
3063
  * @param instance
@@ -2785,35 +3068,7 @@ class Schema {
2785
3068
  enumerable: false,
2786
3069
  writable: true
2787
3070
  });
2788
- const metadata = instance.constructor[Symbol.metadata];
2789
- // Define property descriptors
2790
- for (const field in metadata) {
2791
- if (metadata[field].descriptor) {
2792
- // for encoder
2793
- Object.defineProperty(instance, `_${field}`, {
2794
- value: undefined,
2795
- writable: true,
2796
- enumerable: false,
2797
- configurable: true,
2798
- });
2799
- Object.defineProperty(instance, field, metadata[field].descriptor);
2800
- }
2801
- else {
2802
- // for decoder
2803
- Object.defineProperty(instance, field, {
2804
- value: undefined,
2805
- writable: true,
2806
- enumerable: true,
2807
- configurable: true,
2808
- });
2809
- }
2810
- // Object.defineProperty(instance, field, {
2811
- // ...instance.constructor[Symbol.metadata][field].descriptor
2812
- // });
2813
- // if (args[0]?.hasOwnProperty(field)) {
2814
- // instance[field] = args[0][field];
2815
- // }
2816
- }
3071
+ Object.defineProperties(instance, instance.constructor[Symbol.metadata]?.[$descriptors] || {});
2817
3072
  }
2818
3073
  static is(type) {
2819
3074
  return typeof (type[Symbol.metadata]) === "object";
@@ -2837,7 +3092,7 @@ class Schema {
2837
3092
  */
2838
3093
  static [$filter](ref, index, view) {
2839
3094
  const metadata = ref.constructor[Symbol.metadata];
2840
- const tag = metadata[metadata[index]].tag;
3095
+ const tag = metadata[index]?.tag;
2841
3096
  if (view === undefined) {
2842
3097
  // shared pass/encode: encode if doesn't have a tag
2843
3098
  return tag === undefined;
@@ -2858,12 +3113,16 @@ class Schema {
2858
3113
  }
2859
3114
  // allow inherited classes to have a constructor
2860
3115
  constructor(...args) {
3116
+ //
3117
+ // inline
3118
+ // Schema.initialize(this);
3119
+ //
2861
3120
  Schema.initialize(this);
2862
3121
  //
2863
3122
  // Assign initial values
2864
3123
  //
2865
3124
  if (args[0]) {
2866
- this.assign(args[0]);
3125
+ Object.assign(this, args[0]);
2867
3126
  }
2868
3127
  }
2869
3128
  assign(props) {
@@ -2877,7 +3136,8 @@ class Schema {
2877
3136
  * @param operation OPERATION to perform (detected automatically)
2878
3137
  */
2879
3138
  setDirty(property, operation) {
2880
- this[$changes].change(this.constructor[Symbol.metadata][property].index, operation);
3139
+ const metadata = this.constructor[Symbol.metadata];
3140
+ this[$changes].change(metadata[metadata[property]].index, operation);
2881
3141
  }
2882
3142
  clone() {
2883
3143
  const cloned = new (this.constructor);
@@ -2886,7 +3146,9 @@ class Schema {
2886
3146
  // TODO: clone all properties, not only annotated ones
2887
3147
  //
2888
3148
  // for (const field in this) {
2889
- for (const field in metadata) {
3149
+ for (const fieldIndex in metadata) {
3150
+ // const field = metadata[metadata[fieldIndex]].name;
3151
+ const field = metadata[fieldIndex].name;
2890
3152
  if (typeof (this[field]) === "object" &&
2891
3153
  typeof (this[field]?.clone) === "function") {
2892
3154
  // deep clone
@@ -2900,10 +3162,11 @@ class Schema {
2900
3162
  return cloned;
2901
3163
  }
2902
3164
  toJSON() {
2903
- const metadata = this.constructor[Symbol.metadata];
2904
3165
  const obj = {};
2905
- for (const fieldName in metadata) {
2906
- const field = metadata[fieldName];
3166
+ const metadata = this.constructor[Symbol.metadata];
3167
+ for (const index in metadata) {
3168
+ const field = metadata[index];
3169
+ const fieldName = field.name;
2907
3170
  if (!field.deprecated && this[fieldName] !== null && typeof (this[fieldName]) !== "undefined") {
2908
3171
  obj[fieldName] = (typeof (this[fieldName]['toJSON']) === "function")
2909
3172
  ? this[fieldName]['toJSON']()
@@ -2916,17 +3179,19 @@ class Schema {
2916
3179
  this[$changes].discardAll();
2917
3180
  }
2918
3181
  [$getByIndex](index) {
2919
- return this[this.constructor[Symbol.metadata][index]];
3182
+ const metadata = this.constructor[Symbol.metadata];
3183
+ return this[metadata[index].name];
2920
3184
  }
2921
3185
  [$deleteByIndex](index) {
2922
- this[this.constructor[Symbol.metadata][index]] = undefined;
3186
+ const metadata = this.constructor[Symbol.metadata];
3187
+ this[metadata[index].name] = undefined;
2923
3188
  }
2924
3189
  static debugRefIds(instance, jsonContents = true, level = 0) {
2925
3190
  const ref = instance;
2926
3191
  const changeTree = ref[$changes];
2927
3192
  const contents = (jsonContents) ? ` - ${JSON.stringify(ref.toJSON())}` : "";
2928
3193
  let output = "";
2929
- output += `${getIndent(level)}${ref.constructor.name} (${ref[$changes].refId})${contents}\n`;
3194
+ output += `${getIndent(level)}${ref.constructor.name} (refId: ${ref[$changes].refId})${contents}\n`;
2930
3195
  changeTree.forEachChild((childChangeTree) => output += this.debugRefIds(childChangeTree.ref, jsonContents, level + 1));
2931
3196
  return output;
2932
3197
  }
@@ -2944,30 +3209,40 @@ class Schema {
2944
3209
  const changeSetName = (isEncodeAll) ? "allChanges" : "changes";
2945
3210
  let output = `${instance.constructor.name} (${changeTree.refId}) -> .${changeSetName}:\n`;
2946
3211
  function dumpChangeSet(changeSet) {
2947
- Array.from(changeSet)
2948
- .sort((a, b) => a[0] - b[0])
2949
- .forEach(([index, operation]) => output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(index, isEncodeAll))})\n`);
3212
+ changeSet.operations
3213
+ .filter(op => op)
3214
+ .forEach((index) => {
3215
+ const operation = changeTree.indexedOperations[index];
3216
+ console.log({ index, operation });
3217
+ output += `- [${index}]: ${exports.OPERATION[operation]} (${JSON.stringify(changeTree.getValue(Number(index), isEncodeAll))})\n`;
3218
+ });
2950
3219
  }
2951
3220
  dumpChangeSet(changeSet);
2952
3221
  // display filtered changes
2953
- if (!isEncodeAll && changeTree.filteredChanges?.size > 0) {
3222
+ if (!isEncodeAll &&
3223
+ changeTree.filteredChanges &&
3224
+ (changeTree.filteredChanges.operations).filter(op => op).length > 0) {
2954
3225
  output += `${instance.constructor.name} (${changeTree.refId}) -> .filteredChanges:\n`;
2955
3226
  dumpChangeSet(changeTree.filteredChanges);
2956
3227
  }
2957
3228
  // display filtered changes
2958
- if (isEncodeAll && changeTree.allFilteredChanges?.size > 0) {
3229
+ if (isEncodeAll &&
3230
+ changeTree.allFilteredChanges &&
3231
+ (changeTree.allFilteredChanges.operations).filter(op => op).length > 0) {
2959
3232
  output += `${instance.constructor.name} (${changeTree.refId}) -> .allFilteredChanges:\n`;
2960
3233
  dumpChangeSet(changeTree.allFilteredChanges);
2961
3234
  }
2962
3235
  return output;
2963
3236
  }
2964
- static debugChangesDeep(ref) {
3237
+ static debugChangesDeep(ref, changeSetName = "changes") {
2965
3238
  let output = "";
2966
3239
  const rootChangeTree = ref[$changes];
3240
+ const root = rootChangeTree.root;
2967
3241
  const changeTrees = new Map();
2968
3242
  let totalInstances = 0;
2969
3243
  let totalOperations = 0;
2970
- for (const [changeTree, changes] of (rootChangeTree.root.changes.entries())) {
3244
+ for (const [refId, changes] of Object.entries(root[changeSetName])) {
3245
+ const changeTree = root.changeTrees[refId];
2971
3246
  let includeChangeTree = false;
2972
3247
  let parentChangeTrees = [];
2973
3248
  let parentChangeTree = changeTree.parent?.[$changes];
@@ -2986,7 +3261,7 @@ class Schema {
2986
3261
  }
2987
3262
  if (includeChangeTree) {
2988
3263
  totalInstances += 1;
2989
- totalOperations += changes.size;
3264
+ totalOperations += Object.keys(changes).length;
2990
3265
  changeTrees.set(changeTree, parentChangeTrees.reverse());
2991
3266
  }
2992
3267
  }
@@ -3004,12 +3279,13 @@ class Schema {
3004
3279
  visitedParents.add(parentChangeTree);
3005
3280
  }
3006
3281
  });
3007
- const changes = changeTree.changes;
3282
+ const changes = changeTree.indexedOperations;
3008
3283
  const level = parentChangeTrees.length;
3009
3284
  const indent = getIndent(level);
3010
3285
  const parentIndex = (level > 0) ? `(${changeTree.parentIndex}) ` : "";
3011
- output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${changes.size}\n`;
3012
- for (const [index, operation] of changes) {
3286
+ output += `${indent}${parentIndex}${changeTree.ref.constructor.name} (refId: ${changeTree.refId}) - changes: ${Object.keys(changes).length}\n`;
3287
+ for (const index in changes) {
3288
+ const operation = changes[index];
3013
3289
  output += `${getIndent(level + 1)}${exports.OPERATION[operation]}: ${index}\n`;
3014
3290
  }
3015
3291
  }
@@ -3043,6 +3319,7 @@ class CollectionSchema {
3043
3319
  this.$indexes = new Map();
3044
3320
  this.$refId = 0;
3045
3321
  this[$changes] = new ChangeTree(this);
3322
+ this[$changes].indexes = {};
3046
3323
  if (initialValues) {
3047
3324
  initialValues.forEach((v) => this.add(v));
3048
3325
  }
@@ -3198,6 +3475,7 @@ class SetSchema {
3198
3475
  this.$indexes = new Map();
3199
3476
  this.$refId = 0;
3200
3477
  this[$changes] = new ChangeTree(this);
3478
+ this[$changes].indexes = {};
3201
3479
  if (initialValues) {
3202
3480
  initialValues.forEach((v) => this.add(v));
3203
3481
  }
@@ -3353,6 +3631,8 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
3353
3631
  OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
3354
3632
  PERFORMANCE OF THIS SOFTWARE.
3355
3633
  ***************************************************************************** */
3634
+ /* global Reflect, Promise, SuppressedError, Symbol */
3635
+
3356
3636
 
3357
3637
  function __decorate(decorators, target, key, desc) {
3358
3638
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -3366,37 +3646,135 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
3366
3646
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
3367
3647
  };
3368
3648
 
3649
+ function spliceOne(arr, index) {
3650
+ // manually splice an array
3651
+ if (index === -1 || index >= arr.length) {
3652
+ return false;
3653
+ }
3654
+ const len = arr.length - 1;
3655
+ for (let i = index; i < len; i++) {
3656
+ arr[i] = arr[i + 1];
3657
+ }
3658
+ arr.length = len;
3659
+ return true;
3660
+ }
3661
+
3662
+ class Root {
3663
+ constructor(types) {
3664
+ this.types = types;
3665
+ this.nextUniqueId = 0;
3666
+ this.refCount = {};
3667
+ this.changeTrees = {};
3668
+ // all changes
3669
+ this.allChanges = [];
3670
+ this.allFilteredChanges = []; // TODO: do not initialize it if filters are not used
3671
+ // pending changes to be encoded
3672
+ this.changes = [];
3673
+ this.filteredChanges = []; // TODO: do not initialize it if filters are not used
3674
+ }
3675
+ getNextUniqueId() {
3676
+ return this.nextUniqueId++;
3677
+ }
3678
+ add(changeTree) {
3679
+ // FIXME: move implementation of `ensureRefId` to `Root` class
3680
+ changeTree.ensureRefId();
3681
+ const isNewChangeTree = (this.changeTrees[changeTree.refId] === undefined);
3682
+ if (isNewChangeTree) {
3683
+ this.changeTrees[changeTree.refId] = changeTree;
3684
+ }
3685
+ const previousRefCount = this.refCount[changeTree.refId];
3686
+ if (previousRefCount === 0) {
3687
+ //
3688
+ // When a ChangeTree is re-added, it means that it was previously removed.
3689
+ // We need to re-add all changes to the `changes` map.
3690
+ //
3691
+ const ops = changeTree.allChanges.operations;
3692
+ let len = ops.length;
3693
+ while (len--) {
3694
+ changeTree.indexedOperations[ops[len]] = exports.OPERATION.ADD;
3695
+ setOperationAtIndex(changeTree.changes, len);
3696
+ }
3697
+ }
3698
+ this.refCount[changeTree.refId] = (previousRefCount || 0) + 1;
3699
+ return isNewChangeTree;
3700
+ }
3701
+ remove(changeTree) {
3702
+ const refCount = (this.refCount[changeTree.refId]) - 1;
3703
+ if (refCount <= 0) {
3704
+ //
3705
+ // Only remove "root" reference if it's the last reference
3706
+ //
3707
+ changeTree.root = undefined;
3708
+ delete this.changeTrees[changeTree.refId];
3709
+ this.removeChangeFromChangeSet("allChanges", changeTree);
3710
+ this.removeChangeFromChangeSet("changes", changeTree);
3711
+ if (changeTree.isFiltered || changeTree.isPartiallyFiltered) {
3712
+ this.removeChangeFromChangeSet("allFilteredChanges", changeTree);
3713
+ this.removeChangeFromChangeSet("filteredChanges", changeTree);
3714
+ }
3715
+ this.refCount[changeTree.refId] = 0;
3716
+ }
3717
+ else {
3718
+ this.refCount[changeTree.refId] = refCount;
3719
+ }
3720
+ changeTree.forEachChild((child, _) => this.remove(child));
3721
+ return refCount;
3722
+ }
3723
+ removeChangeFromChangeSet(changeSetName, changeTree) {
3724
+ const changeSet = this[changeSetName];
3725
+ const index = changeSet.indexOf(changeTree);
3726
+ if (index !== -1) {
3727
+ spliceOne(changeSet, index);
3728
+ // changeSet[index] = undefined;
3729
+ }
3730
+ }
3731
+ clear() {
3732
+ this.changes.length = 0;
3733
+ }
3734
+ }
3735
+
3369
3736
  class Encoder {
3370
3737
  static { this.BUFFER_SIZE = 8 * 1024; } // 8KB
3371
- constructor(root) {
3738
+ constructor(state) {
3372
3739
  this.sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
3373
- this.setRoot(root);
3374
3740
  //
3375
3741
  // TODO: cache and restore "Context" based on root schema
3376
3742
  // (to avoid creating a new context for every new room)
3377
3743
  //
3378
- this.context = new TypeContext(root.constructor);
3744
+ this.context = new TypeContext(state.constructor);
3745
+ this.root = new Root(this.context);
3746
+ this.setState(state);
3379
3747
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
3380
3748
  // this.context.schemas.forEach((id, schema) => {
3381
3749
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3382
3750
  // });
3383
3751
  }
3384
- setRoot(state) {
3385
- this.root = new Root();
3752
+ setState(state) {
3386
3753
  this.state = state;
3387
- state[$changes].setRoot(this.root);
3754
+ this.state[$changes].setRoot(this.root);
3388
3755
  }
3389
- encode(it = { offset: 0 }, view, bytes = this.sharedBuffer, changeTrees = this.root.changes) {
3390
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
3391
- const isEncodeAll = this.root.allChanges === changeTrees;
3756
+ 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
3757
+ ) {
3392
3758
  const hasView = (view !== undefined);
3393
3759
  const rootChangeTree = this.state[$changes];
3394
- const changeTreesIterator = changeTrees.entries();
3395
- for (const [changeTree, changes] of changeTreesIterator) {
3760
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
3761
+ const changeTrees = this.root[changeSetName];
3762
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
3763
+ const changeTree = changeTrees[i];
3764
+ // // Root#removeChangeFromChangeSet() is now removing instead of setting to "undefined"
3765
+ // if (changeTree === undefined) { continue; }
3766
+ const operations = changeTree[changeSetName];
3396
3767
  const ref = changeTree.ref;
3397
- const ctor = ref['constructor'];
3768
+ const ctor = ref.constructor;
3398
3769
  const encoder = ctor[$encoder];
3399
3770
  const filter = ctor[$filter];
3771
+ const metadata = ctor[Symbol.metadata];
3772
+ // try { throw new Error(); } catch (e) {
3773
+ // // only print if not coming from Reflection.ts
3774
+ // if (!e.stack.includes("src/Reflection.ts")) {
3775
+ // console.log("ChangeTree:", { refId: changeTree.refId, ref: ref.constructor.name });
3776
+ // }
3777
+ // }
3400
3778
  if (hasView) {
3401
3779
  if (!view.items.has(changeTree)) {
3402
3780
  view.invisible.add(changeTree);
@@ -3407,12 +3785,18 @@ class Encoder {
3407
3785
  }
3408
3786
  }
3409
3787
  // skip root `refId` if it's the first change tree
3410
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
3411
- bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3412
- number$1(bytes, changeTree.refId, it);
3788
+ // (unless it "hasView", which will need to revisit the root)
3789
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
3790
+ buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3791
+ number$1(buffer, changeTree.refId, it);
3413
3792
  }
3414
- const changesIterator = changes.entries();
3415
- for (const [fieldIndex, operation] of changesIterator) {
3793
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
3794
+ const fieldIndex = operations.operations[j];
3795
+ const operation = (fieldIndex < 0)
3796
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
3797
+ : (isEncodeAll)
3798
+ ? exports.OPERATION.ADD
3799
+ : changeTree.indexedOperations[fieldIndex];
3416
3800
  //
3417
3801
  // first pass (encodeAll), identify "filtered" operations without encoding them
3418
3802
  // they will be encoded per client, based on their view.
@@ -3420,93 +3804,127 @@ class Encoder {
3420
3804
  // TODO: how can we optimize filtering out "encode all" operations?
3421
3805
  // TODO: avoid checking if no view tags were defined
3422
3806
  //
3423
- if (filter && !filter(ref, fieldIndex, view)) {
3424
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
3807
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
3425
3808
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
3426
3809
  // view?.invisible.add(changeTree);
3427
3810
  continue;
3428
3811
  }
3429
- // console.log("WILL ENCODE", {
3430
- // ref: changeTree.ref.constructor.name,
3431
- // fieldIndex,
3432
- // operation: OPERATION[operation],
3433
- // });
3434
- encoder(this, bytes, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
3812
+ // try { throw new Error(); } catch (e) {
3813
+ // // only print if not coming from Reflection.ts
3814
+ // if (!e.stack.includes("src/Reflection.ts")) {
3815
+ // console.log("WILL ENCODE", {
3816
+ // ref: changeTree.ref.constructor.name,
3817
+ // fieldIndex,
3818
+ // operation: OPERATION[operation],
3819
+ // });
3820
+ // }
3821
+ // }
3822
+ // console.log("encode...", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, fieldIndex, operation });
3823
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
3824
+ }
3825
+ if (shouldDiscardChanges) {
3826
+ changeTree.discard();
3827
+ // Not a new instance anymore
3828
+ changeTree.isNew = false;
3435
3829
  }
3436
3830
  }
3437
- if (it.offset > bytes.byteLength) {
3438
- const newSize = getNextPowerOf2(this.sharedBuffer.byteLength * 2);
3439
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + bytes.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
3831
+ if (it.offset > buffer.byteLength) {
3832
+ const newSize = getNextPowerOf2(buffer.byteLength * 2);
3833
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
3834
+
3835
+ import { Encoder } from "@colyseus/schema";
3836
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
3837
+ `);
3440
3838
  //
3441
3839
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
3442
3840
  //
3443
- this.sharedBuffer = Buffer.allocUnsafeSlow(newSize);
3444
- return this.encode({ offset: initialOffset }, view);
3841
+ buffer = Buffer.allocUnsafeSlow(newSize);
3842
+ // assign resized buffer to local sharedBuffer
3843
+ if (buffer === this.sharedBuffer) {
3844
+ this.sharedBuffer = buffer;
3845
+ }
3846
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
3445
3847
  }
3446
3848
  else {
3447
- //
3448
- // only clear changes after making sure buffer resize is not required.
3449
- //
3450
- if (!isEncodeAll && !hasView) {
3451
- //
3452
- // FIXME: avoid iterating over change trees twice.
3453
- //
3454
- this.onEndEncode(changeTrees);
3455
- }
3456
- // return bytes;
3457
- return bytes.slice(0, it.offset);
3849
+ // //
3850
+ // // only clear changes after making sure buffer resize is not required.
3851
+ // //
3852
+ // if (shouldClearChanges) {
3853
+ // //
3854
+ // // FIXME: avoid iterating over change trees twice.
3855
+ // //
3856
+ // this.onEndEncode(changeTrees);
3857
+ // }
3858
+ return buffer.subarray(0, it.offset);
3458
3859
  }
3459
3860
  }
3460
- encodeAll(it = { offset: 0 }) {
3461
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
3462
- // Array.from(this.$root.allChanges.entries()).map((item) => {
3463
- // console.log("->", item[0].refId, item[0].ref.toJSON());
3464
- // });
3465
- return this.encode(it, undefined, this.sharedBuffer, this.root.allChanges);
3861
+ encodeAll(it = { offset: 0 }, buffer = this.sharedBuffer) {
3862
+ // console.log(`\nencodeAll(), this.root.allChanges (${(Object.keys(this.root.allChanges).length)})`);
3863
+ // this.debugChanges("allChanges");
3864
+ return this.encode(it, undefined, buffer, "allChanges", true);
3466
3865
  }
3467
3866
  encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3468
3867
  const viewOffset = it.offset;
3469
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
3470
- // this.debugAllFilteredChanges();
3868
+ // console.log(`\nencodeAllView(), this.root.allFilteredChanges (${(Object.keys(this.root.allFilteredChanges).length)})`);
3869
+ // this.debugChanges("allFilteredChanges");
3870
+ // console.log("\n\nENCODE ALL FOR VIEW...\n\n")
3471
3871
  // try to encode "filtered" changes
3472
- this.encode(it, view, bytes, this.root.allFilteredChanges);
3872
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
3473
3873
  return Buffer.concat([
3474
- bytes.slice(0, sharedOffset),
3475
- bytes.slice(viewOffset, it.offset)
3874
+ bytes.subarray(0, sharedOffset),
3875
+ bytes.subarray(viewOffset, it.offset)
3476
3876
  ]);
3477
3877
  }
3478
- // debugAllFilteredChanges() {
3479
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
3480
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
3481
- // if (Array.isArray(item[0].ref.toJSON())) {
3482
- // item[1].forEach((op, key) => {
3483
- // console.log(" ->", { key, op: OPERATION[op] });
3484
- // })
3485
- // }
3486
- // });
3487
- // }
3878
+ debugChanges(field) {
3879
+ const rootChangeSet = (typeof (field) === "string")
3880
+ ? this.root[field]
3881
+ : field;
3882
+ rootChangeSet.forEach((changeTree) => {
3883
+ const changeSet = changeTree[field];
3884
+ const metadata = changeTree.ref.constructor[Symbol.metadata];
3885
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
3886
+ for (const index in changeSet) {
3887
+ const op = changeSet[index];
3888
+ console.log(" ->", {
3889
+ index,
3890
+ field: metadata?.[index],
3891
+ op: exports.OPERATION[op],
3892
+ });
3893
+ }
3894
+ });
3895
+ }
3488
3896
  encodeView(view, sharedOffset, it, bytes = this.sharedBuffer) {
3489
3897
  const viewOffset = it.offset;
3490
- // try to encode "filtered" changes
3491
- this.encode(it, view, bytes, this.root.filteredChanges);
3898
+ // console.log(`\nencodeView(), view.changes (${view.changes.size})`);
3899
+ // this.debugChanges(view.changes);
3900
+ // console.log(`\nencodeView(), this.root.filteredChanges (${this.root.filteredChanges.size})`);
3901
+ // this.debugChanges("filteredChanges");
3492
3902
  // encode visibility changes (add/remove for this view)
3493
- const viewChangesIterator = view.changes.entries();
3494
- for (const [changeTree, changes] of viewChangesIterator) {
3495
- if (changes.size === 0) {
3496
- // FIXME: avoid having empty changes if no changes were made
3497
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
3903
+ const refIds = Object.keys(view.changes);
3904
+ // console.log("ENCODE VIEW:", refIds);
3905
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
3906
+ const refId = refIds[i];
3907
+ const changes = view.changes[refId];
3908
+ const changeTree = this.root.changeTrees[refId];
3909
+ if (changeTree === undefined ||
3910
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
3911
+ ) {
3912
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
3498
3913
  continue;
3499
3914
  }
3500
3915
  const ref = changeTree.ref;
3501
- const ctor = ref['constructor'];
3916
+ const ctor = ref.constructor;
3502
3917
  const encoder = ctor[$encoder];
3918
+ const metadata = ctor[Symbol.metadata];
3503
3919
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
3504
3920
  number$1(bytes, changeTree.refId, it);
3505
- const changesIterator = changes.entries();
3506
- for (const [fieldIndex, operation] of changesIterator) {
3921
+ const keys = Object.keys(changes);
3922
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
3923
+ const key = keys[i];
3924
+ const operation = changes[key];
3507
3925
  // isEncodeAll = false
3508
3926
  // hasView = true
3509
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
3927
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
3510
3928
  }
3511
3929
  }
3512
3930
  //
@@ -3514,51 +3932,64 @@ class Encoder {
3514
3932
  // (to allow re-using StateView's for multiple clients)
3515
3933
  //
3516
3934
  // clear "view" changes after encoding
3517
- view.changes.clear();
3935
+ view.changes = {};
3936
+ // console.log("FILTERED CHANGES:", this.root.filteredChanges);
3937
+ // try to encode "filtered" changes
3938
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
3518
3939
  return Buffer.concat([
3519
- bytes.slice(0, sharedOffset),
3520
- bytes.slice(viewOffset, it.offset)
3940
+ bytes.subarray(0, sharedOffset),
3941
+ bytes.subarray(viewOffset, it.offset)
3521
3942
  ]);
3522
3943
  }
3523
3944
  onEndEncode(changeTrees = this.root.changes) {
3524
- const changeTreesIterator = changeTrees.entries();
3525
- for (const [changeTree, _] of changeTreesIterator) {
3526
- changeTree.endEncode();
3527
- }
3945
+ // changeTrees.forEach(function(changeTree) {
3946
+ // changeTree.endEncode();
3947
+ // });
3948
+ // for (const refId in changeTrees) {
3949
+ // const changeTree = this.root.changeTrees[refId];
3950
+ // changeTree.endEncode();
3951
+ // // changeTree.changes.clear();
3952
+ // // // ArraySchema and MapSchema have a custom "encode end" method
3953
+ // // changeTree.ref[$onEncodeEnd]?.();
3954
+ // // // Not a new instance anymore
3955
+ // // delete changeTree[$isNew];
3956
+ // }
3528
3957
  }
3529
3958
  discardChanges() {
3959
+ // console.log("DISCARD CHANGES!");
3530
3960
  // discard shared changes
3531
- if (this.root.changes.size > 0) {
3532
- this.onEndEncode(this.root.changes);
3533
- this.root.changes.clear();
3961
+ let length = this.root.changes.length;
3962
+ if (length > 0) {
3963
+ while (length--) {
3964
+ this.root.changes[length]?.endEncode();
3965
+ }
3966
+ this.root.changes.length = 0;
3534
3967
  }
3535
3968
  // discard filtered changes
3536
- if (this.root.filteredChanges.size > 0) {
3537
- this.onEndEncode(this.root.filteredChanges);
3538
- this.root.filteredChanges.clear();
3969
+ length = this.root.filteredChanges.length;
3970
+ if (length > 0) {
3971
+ while (length--) {
3972
+ this.root.filteredChanges[length]?.endEncode();
3973
+ }
3974
+ this.root.filteredChanges.length = 0;
3539
3975
  }
3540
3976
  }
3541
3977
  tryEncodeTypeId(bytes, baseType, targetType, it) {
3542
3978
  const baseTypeId = this.context.getTypeId(baseType);
3543
3979
  const targetTypeId = this.context.getTypeId(targetType);
3980
+ if (targetTypeId === undefined) {
3981
+ 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.`);
3982
+ return;
3983
+ }
3544
3984
  if (baseTypeId !== targetTypeId) {
3545
3985
  bytes[it.offset++] = TYPE_ID & 255;
3546
3986
  number$1(bytes, targetTypeId, it);
3547
3987
  }
3548
3988
  }
3549
- }
3550
-
3551
- function spliceOne(arr, index) {
3552
- // manually splice an array
3553
- if (index === -1 || index >= arr.length) {
3554
- return false;
3555
- }
3556
- const len = arr.length - 1;
3557
- for (let i = index; i < len; i++) {
3558
- arr[i] = arr[i + 1];
3989
+ get hasChanges() {
3990
+ return (this.root.changes.length > 0 ||
3991
+ this.root.filteredChanges.length > 0);
3559
3992
  }
3560
- arr.length = len;
3561
- return true;
3562
3993
  }
3563
3994
 
3564
3995
  class DecodingWarning extends Error {
@@ -3639,8 +4070,9 @@ class ReferenceTracker {
3639
4070
  // Ensure child schema instances have their references removed as well.
3640
4071
  //
3641
4072
  if (Metadata.isValidInstance(ref)) {
3642
- const metadata = ref['constructor'][Symbol.metadata];
3643
- for (const field in metadata) {
4073
+ const metadata = ref.constructor[Symbol.metadata];
4074
+ for (const index in metadata) {
4075
+ const field = metadata[index].name;
3644
4076
  const childRefId = typeof (ref[field]) === "object" && this.refIds.get(ref[field]);
3645
4077
  if (childRefId) {
3646
4078
  this.removeRef(childRefId);
@@ -3687,21 +4119,21 @@ class ReferenceTracker {
3687
4119
  class Decoder {
3688
4120
  constructor(root, context) {
3689
4121
  this.currentRefId = 0;
3690
- this.setRoot(root);
4122
+ this.setState(root);
3691
4123
  this.context = context || new TypeContext(root.constructor);
3692
4124
  // console.log(">>>>>>>>>>>>>>>> Decoder types");
3693
4125
  // this.context.schemas.forEach((id, schema) => {
3694
4126
  // console.log("type:", id, schema.name, Object.keys(schema[Symbol.metadata]));
3695
4127
  // });
3696
4128
  }
3697
- setRoot(root) {
4129
+ setState(root) {
3698
4130
  this.state = root;
3699
- this.$root = new ReferenceTracker();
3700
- this.$root.addRef(0, root);
4131
+ this.root = new ReferenceTracker();
4132
+ this.root.addRef(0, root);
3701
4133
  }
3702
4134
  decode(bytes, it = { offset: 0 }, ref = this.state) {
3703
4135
  const allChanges = [];
3704
- const $root = this.$root;
4136
+ const $root = this.root;
3705
4137
  const totalBytes = bytes.byteLength;
3706
4138
  let decoder = ref['constructor'][$decoder];
3707
4139
  this.currentRefId = 0;
@@ -3721,7 +4153,7 @@ class Decoder {
3721
4153
  }
3722
4154
  ref[$onDecodeEnd]?.();
3723
4155
  ref = nextRef;
3724
- decoder = ref['constructor'][$decoder];
4156
+ decoder = ref.constructor[$decoder];
3725
4157
  continue;
3726
4158
  }
3727
4159
  const result = decoder(this, bytes, it, ref, allChanges);
@@ -3782,7 +4214,7 @@ class Decoder {
3782
4214
  previousValue: value
3783
4215
  });
3784
4216
  if (needRemoveRef) {
3785
- this.$root.removeRef(this.$root.refIds.get(value));
4217
+ this.root.removeRef(this.root.refIds.get(value));
3786
4218
  }
3787
4219
  });
3788
4220
  }
@@ -3822,14 +4254,27 @@ class Reflection extends Schema {
3822
4254
  super(...arguments);
3823
4255
  this.types = new ArraySchema();
3824
4256
  }
3825
- static encode(instance, context) {
3826
- if (!context) {
3827
- context = new TypeContext(instance.constructor);
3828
- }
4257
+ /**
4258
+ * Encodes the TypeContext of an Encoder into a buffer.
4259
+ *
4260
+ * @param encoder Encoder instance
4261
+ * @param it
4262
+ * @returns
4263
+ */
4264
+ static encode(encoder, it = { offset: 0 }) {
4265
+ const context = encoder.context;
3829
4266
  const reflection = new Reflection();
3830
- const encoder = new Encoder(reflection);
4267
+ const reflectionEncoder = new Encoder(reflection);
4268
+ // rootType is usually the first schema passed to the Encoder
4269
+ // (unless it inherits from another schema)
4270
+ const rootType = context.schemas.get(encoder.state.constructor);
4271
+ if (rootType > 0) {
4272
+ reflection.rootType = rootType;
4273
+ }
3831
4274
  const buildType = (currentType, metadata) => {
3832
- for (const fieldName in metadata) {
4275
+ for (const fieldIndex in metadata) {
4276
+ const index = Number(fieldIndex);
4277
+ const fieldName = metadata[index].name;
3833
4278
  // skip fields from parent classes
3834
4279
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
3835
4280
  continue;
@@ -3837,7 +4282,7 @@ class Reflection extends Schema {
3837
4282
  const field = new ReflectionField();
3838
4283
  field.name = fieldName;
3839
4284
  let fieldType;
3840
- const type = metadata[fieldName].type;
4285
+ const type = metadata[index].type;
3841
4286
  if (typeof (type) === "string") {
3842
4287
  fieldType = type;
3843
4288
  }
@@ -3879,66 +4324,335 @@ class Reflection extends Schema {
3879
4324
  }
3880
4325
  buildType(type, klass[Symbol.metadata]);
3881
4326
  }
3882
- const it = { offset: 0 };
3883
- const buf = encoder.encodeAll(it);
4327
+ const buf = reflectionEncoder.encodeAll(it);
3884
4328
  return Buffer.from(buf, 0, it.offset);
3885
4329
  }
4330
+ /**
4331
+ * Decodes the TypeContext from a buffer into a Decoder instance.
4332
+ *
4333
+ * @param bytes Reflection.encode() output
4334
+ * @param it
4335
+ * @returns Decoder instance
4336
+ */
3886
4337
  static decode(bytes, it) {
3887
4338
  const reflection = new Reflection();
3888
4339
  const reflectionDecoder = new Decoder(reflection);
3889
4340
  reflectionDecoder.decode(bytes, it);
3890
- const context = new TypeContext();
3891
- const schemaTypes = reflection.types.reduce((types, reflectionType) => {
3892
- const parentKlass = types[reflectionType.extendsId] || Schema;
3893
- const schema = class _ extends parentKlass {
4341
+ const typeContext = new TypeContext();
4342
+ // 1st pass, initialize metadata + inheritance
4343
+ reflection.types.forEach((reflectionType) => {
4344
+ const parentClass = typeContext.get(reflectionType.extendsId) ?? Schema;
4345
+ const schema = class _ extends parentClass {
3894
4346
  };
3895
- // const _metadata = Object.create(_classSuper[Symbol.metadata] ?? null);
3896
- const _metadata = parentKlass && parentKlass[Symbol.metadata] || Object.create(null);
3897
- Object.defineProperty(schema, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
3898
4347
  // register for inheritance support
3899
4348
  TypeContext.register(schema);
3900
- const typeid = reflectionType.id;
3901
- types[typeid] = schema;
3902
- context.add(schema, typeid);
3903
- return types;
4349
+ // // for inheritance support
4350
+ // Metadata.initialize(schema);
4351
+ typeContext.add(schema, reflectionType.id);
3904
4352
  }, {});
3905
- reflection.types.forEach((reflectionType) => {
3906
- const schemaType = schemaTypes[reflectionType.id];
3907
- const metadata = schemaType[Symbol.metadata];
3908
- const parentKlass = reflection.types[reflectionType.extendsId];
3909
- const parentFieldIndex = parentKlass && parentKlass.fields.length || 0;
4353
+ // define fields
4354
+ const addFields = (metadata, reflectionType, parentFieldIndex) => {
3910
4355
  reflectionType.fields.forEach((field, i) => {
3911
4356
  const fieldIndex = parentFieldIndex + i;
3912
4357
  if (field.referencedType !== undefined) {
3913
4358
  let fieldType = field.type;
3914
- let refType = schemaTypes[field.referencedType];
4359
+ let refType = typeContext.get(field.referencedType);
3915
4360
  // map or array of primitive type (-1)
3916
4361
  if (!refType) {
3917
4362
  const typeInfo = field.type.split(":");
3918
4363
  fieldType = typeInfo[0];
3919
- refType = typeInfo[1];
4364
+ refType = typeInfo[1]; // string
3920
4365
  }
3921
4366
  if (fieldType === "ref") {
3922
- // type(refType)(schemaType.prototype, field.name);
3923
4367
  Metadata.addField(metadata, fieldIndex, field.name, refType);
3924
4368
  }
3925
4369
  else {
3926
- // type({ [fieldType]: refType } as DefinitionType)(schemaType.prototype, field.name);
3927
4370
  Metadata.addField(metadata, fieldIndex, field.name, { [fieldType]: refType });
3928
4371
  }
3929
4372
  }
3930
4373
  else {
3931
- // type(field.type as PrimitiveType)(schemaType.prototype, field.name);
3932
4374
  Metadata.addField(metadata, fieldIndex, field.name, field.type);
3933
4375
  }
3934
4376
  });
4377
+ };
4378
+ // 2nd pass, set fields
4379
+ reflection.types.forEach((reflectionType) => {
4380
+ const schema = typeContext.get(reflectionType.id);
4381
+ // for inheritance support
4382
+ const metadata = Metadata.initialize(schema);
4383
+ const inheritedTypes = [];
4384
+ let parentType = reflectionType;
4385
+ do {
4386
+ inheritedTypes.push(parentType);
4387
+ parentType = reflection.types.find((t) => t.id === parentType.extendsId);
4388
+ } while (parentType);
4389
+ let parentFieldIndex = 0;
4390
+ inheritedTypes.reverse().forEach((reflectionType) => {
4391
+ // add fields from all inherited classes
4392
+ // TODO: refactor this to avoid adding fields from parent classes
4393
+ addFields(metadata, reflectionType, parentFieldIndex);
4394
+ parentFieldIndex += reflectionType.fields.length;
4395
+ });
3935
4396
  });
3936
- return new (schemaTypes[0])();
4397
+ const state = new (typeContext.get(reflection.rootType || 0))();
4398
+ return new Decoder(state, typeContext);
3937
4399
  }
3938
4400
  }
3939
4401
  __decorate([
3940
4402
  type([ReflectionType])
3941
4403
  ], Reflection.prototype, "types", void 0);
4404
+ __decorate([
4405
+ type("number")
4406
+ ], Reflection.prototype, "rootType", void 0);
4407
+
4408
+ function getDecoderStateCallbacks(decoder) {
4409
+ const $root = decoder.root;
4410
+ const callbacks = $root.callbacks;
4411
+ const onAddCalls = new WeakMap();
4412
+ let currentOnAddCallback;
4413
+ decoder.triggerChanges = function (allChanges) {
4414
+ const uniqueRefIds = new Set();
4415
+ for (let i = 0, l = allChanges.length; i < l; i++) {
4416
+ const change = allChanges[i];
4417
+ const refId = change.refId;
4418
+ const ref = change.ref;
4419
+ const $callbacks = callbacks[refId];
4420
+ if (!$callbacks) {
4421
+ continue;
4422
+ }
4423
+ //
4424
+ // trigger onRemove on child structure.
4425
+ //
4426
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE &&
4427
+ change.previousValue instanceof Schema) {
4428
+ const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[exports.OPERATION.DELETE];
4429
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4430
+ deleteCallbacks[i]();
4431
+ }
4432
+ }
4433
+ if (ref instanceof Schema) {
4434
+ //
4435
+ // Handle schema instance
4436
+ //
4437
+ if (!uniqueRefIds.has(refId)) {
4438
+ // trigger onChange
4439
+ const replaceCallbacks = $callbacks?.[exports.OPERATION.REPLACE];
4440
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4441
+ replaceCallbacks[i]();
4442
+ // try {
4443
+ // } catch (e) {
4444
+ // console.error(e);
4445
+ // }
4446
+ }
4447
+ }
4448
+ if ($callbacks.hasOwnProperty(change.field)) {
4449
+ const fieldCallbacks = $callbacks[change.field];
4450
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
4451
+ fieldCallbacks[i](change.value, change.previousValue);
4452
+ // try {
4453
+ // } catch (e) {
4454
+ // console.error(e);
4455
+ // }
4456
+ }
4457
+ }
4458
+ }
4459
+ else {
4460
+ //
4461
+ // Handle collection of items
4462
+ //
4463
+ if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
4464
+ //
4465
+ // FIXME: `previousValue` should always be available.
4466
+ //
4467
+ if (change.previousValue !== undefined) {
4468
+ // triger onRemove
4469
+ const deleteCallbacks = $callbacks[exports.OPERATION.DELETE];
4470
+ for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
4471
+ deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
4472
+ }
4473
+ }
4474
+ // Handle DELETE_AND_ADD operations
4475
+ if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
4476
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4477
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4478
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4479
+ }
4480
+ }
4481
+ }
4482
+ else if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD && change.previousValue === undefined) {
4483
+ // triger onAdd
4484
+ const addCallbacks = $callbacks[exports.OPERATION.ADD];
4485
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
4486
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4487
+ }
4488
+ }
4489
+ // trigger onChange
4490
+ if (change.value !== change.previousValue) {
4491
+ const replaceCallbacks = $callbacks[exports.OPERATION.REPLACE];
4492
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
4493
+ replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
4494
+ }
4495
+ }
4496
+ }
4497
+ uniqueRefIds.add(refId);
4498
+ }
4499
+ };
4500
+ function getProxy(metadataOrType, context) {
4501
+ let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
4502
+ let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
4503
+ (metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
4504
+ if (metadata && !isCollection) {
4505
+ const onAddListen = function (ref, prop, callback, immediate) {
4506
+ // immediate trigger
4507
+ if (immediate &&
4508
+ context.instance[prop] !== undefined &&
4509
+ !onAddCalls.has(currentOnAddCallback) // Workaround for https://github.com/colyseus/schema/issues/147
4510
+ ) {
4511
+ callback(context.instance[prop], undefined);
4512
+ }
4513
+ return $root.addCallback($root.refIds.get(ref), prop, callback);
4514
+ };
4515
+ /**
4516
+ * Schema instances
4517
+ */
4518
+ return new Proxy({
4519
+ listen: function listen(prop, callback, immediate = true) {
4520
+ if (context.instance) {
4521
+ return onAddListen(context.instance, prop, callback, immediate);
4522
+ }
4523
+ else {
4524
+ // collection instance not received yet
4525
+ let detachCallback = () => { };
4526
+ context.onInstanceAvailable((ref, existing) => {
4527
+ detachCallback = onAddListen(ref, prop, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4528
+ });
4529
+ return () => detachCallback();
4530
+ }
4531
+ },
4532
+ onChange: function onChange(callback) {
4533
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
4534
+ },
4535
+ //
4536
+ // TODO: refactor `bindTo()` implementation.
4537
+ // There is room for improvement.
4538
+ //
4539
+ bindTo: function bindTo(targetObject, properties) {
4540
+ if (!properties) {
4541
+ properties = Object.keys(metadata).map((index) => metadata[index].name);
4542
+ }
4543
+ return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, () => {
4544
+ properties.forEach((prop) => targetObject[prop] = context.instance[prop]);
4545
+ });
4546
+ }
4547
+ }, {
4548
+ get(target, prop) {
4549
+ const metadataField = metadata[metadata[prop]];
4550
+ if (metadataField) {
4551
+ const instance = context.instance?.[prop];
4552
+ const onInstanceAvailable = ((callback) => {
4553
+ const unbind = $(context.instance).listen(prop, (value, _) => {
4554
+ callback(value, false);
4555
+ // FIXME: by "unbinding" the callback here,
4556
+ // it will not support when the server
4557
+ // re-instantiates the instance.
4558
+ //
4559
+ unbind?.();
4560
+ }, false);
4561
+ // has existing value
4562
+ if ($root.refIds.get(instance) !== undefined) {
4563
+ callback(instance, true);
4564
+ }
4565
+ });
4566
+ return getProxy(metadataField.type, {
4567
+ // make sure refId is available, otherwise need to wait for the instance to be available.
4568
+ instance: ($root.refIds.get(instance) && instance),
4569
+ parentInstance: context.instance,
4570
+ onInstanceAvailable,
4571
+ });
4572
+ }
4573
+ else {
4574
+ // accessing the function
4575
+ return target[prop];
4576
+ }
4577
+ },
4578
+ has(target, prop) { return metadata[prop] !== undefined; },
4579
+ set(_, _1, _2) { throw new Error("not allowed"); },
4580
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4581
+ });
4582
+ }
4583
+ else {
4584
+ /**
4585
+ * Collection instances
4586
+ */
4587
+ const onAdd = function (ref, callback, immediate) {
4588
+ // Trigger callback on existing items
4589
+ if (immediate) {
4590
+ ref.forEach((v, k) => callback(v, k));
4591
+ }
4592
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, (value, key) => {
4593
+ onAddCalls.set(callback, true);
4594
+ currentOnAddCallback = callback;
4595
+ callback(value, key);
4596
+ onAddCalls.delete(callback);
4597
+ currentOnAddCallback = undefined;
4598
+ });
4599
+ };
4600
+ const onRemove = function (ref, callback) {
4601
+ return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
4602
+ };
4603
+ return new Proxy({
4604
+ onAdd: function (callback, immediate = true) {
4605
+ //
4606
+ // https://github.com/colyseus/schema/issues/147
4607
+ // If parent instance has "onAdd" registered, avoid triggering immediate callback.
4608
+ //
4609
+ if (context.instance) {
4610
+ return onAdd(context.instance, callback, immediate && !onAddCalls.has(currentOnAddCallback));
4611
+ }
4612
+ else if (context.onInstanceAvailable) {
4613
+ // collection instance not received yet
4614
+ let detachCallback = () => { };
4615
+ context.onInstanceAvailable((ref, existing) => {
4616
+ detachCallback = onAdd(ref, callback, immediate && existing && !onAddCalls.has(currentOnAddCallback));
4617
+ });
4618
+ return () => detachCallback();
4619
+ }
4620
+ },
4621
+ onRemove: function (callback) {
4622
+ if (context.onInstanceAvailable) {
4623
+ // collection instance not received yet
4624
+ let detachCallback = () => { };
4625
+ context.onInstanceAvailable((ref) => {
4626
+ detachCallback = onRemove(ref, callback);
4627
+ });
4628
+ return () => detachCallback();
4629
+ }
4630
+ else if (context.instance) {
4631
+ return onRemove(context.instance, callback);
4632
+ }
4633
+ },
4634
+ }, {
4635
+ get(target, prop) {
4636
+ if (!target[prop]) {
4637
+ throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
4638
+ }
4639
+ return target[prop];
4640
+ },
4641
+ has(target, prop) { return target[prop] !== undefined; },
4642
+ set(_, _1, _2) { throw new Error("not allowed"); },
4643
+ deleteProperty(_, _1) { throw new Error("not allowed"); },
4644
+ });
4645
+ }
4646
+ }
4647
+ function $(instance) {
4648
+ return getProxy(undefined, { instance });
4649
+ }
4650
+ return $;
4651
+ }
4652
+
4653
+ function getRawChangesCallback(decoder, callback) {
4654
+ decoder.triggerChanges = callback;
4655
+ }
3942
4656
 
3943
4657
  class StateView {
3944
4658
  constructor() {
@@ -3954,31 +4668,32 @@ class StateView {
3954
4668
  * Manual "ADD" operations for changes per ChangeTree, specific to this view.
3955
4669
  * (This is used to force encoding a property, even if it was not changed)
3956
4670
  */
3957
- this.changes = new Map();
4671
+ this.changes = {};
3958
4672
  }
3959
4673
  // TODO: allow to set multiple tags at once
3960
- add(obj, tag = DEFAULT_VIEW_TAG) {
4674
+ add(obj, tag = DEFAULT_VIEW_TAG, checkIncludeParent = true) {
3961
4675
  if (!obj[$changes]) {
3962
4676
  console.warn("StateView#add(), invalid object:", obj);
3963
4677
  return this;
3964
4678
  }
3965
- let changeTree = obj[$changes];
3966
- this.items.add(changeTree);
3967
- // Add children of this ChangeTree to this view
3968
- changeTree.forEachChild((change, _) => this.add(change.ref, tag));
3969
- // FIXME: ArraySchema/MapSchema does not have metadata
4679
+ // FIXME: ArraySchema/MapSchema do not have metadata
3970
4680
  const metadata = obj.constructor[Symbol.metadata];
3971
- // add parent ChangeTree's, if they are invisible to this view
3972
- // TODO: REFACTOR addParent()
3973
- this.addParent(changeTree, tag);
4681
+ const changeTree = obj[$changes];
4682
+ this.items.add(changeTree);
4683
+ // add parent ChangeTree's
4684
+ // - if it was invisible to this view
4685
+ // - if it were previously filtered out
4686
+ if (checkIncludeParent && changeTree.parent) {
4687
+ this.addParent(changeTree.parent[$changes], changeTree.parentIndex, tag);
4688
+ }
3974
4689
  //
3975
4690
  // TODO: when adding an item of a MapSchema, the changes may not
3976
4691
  // be set (only the parent's changes are set)
3977
4692
  //
3978
- let changes = this.changes.get(changeTree);
4693
+ let changes = this.changes[changeTree.refId];
3979
4694
  if (changes === undefined) {
3980
- changes = new Map();
3981
- this.changes.set(changeTree, changes);
4695
+ changes = {};
4696
+ this.changes[changeTree.refId] = changes;
3982
4697
  }
3983
4698
  // set tag
3984
4699
  if (tag !== DEFAULT_VIEW_TAG) {
@@ -3994,82 +4709,76 @@ class StateView {
3994
4709
  tags = this.tags.get(changeTree);
3995
4710
  }
3996
4711
  tags.add(tag);
3997
- // console.log("BY TAG:", tag);
3998
4712
  // Ref: add tagged properties
3999
- metadata?.[-3]?.[tag]?.forEach((index) => {
4713
+ metadata?.[$fieldIndexesByViewTag]?.[tag]?.forEach((index) => {
4000
4714
  if (changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4001
- changes.set(index, exports.OPERATION.ADD);
4715
+ changes[index] = exports.OPERATION.ADD;
4002
4716
  }
4003
4717
  });
4004
4718
  }
4005
4719
  else {
4006
- // console.log("DEFAULT TAG", changeTree.allChanges);
4007
- // // add default tag properties
4008
- // metadata?.[-3]?.[DEFAULT_VIEW_TAG]?.forEach((index) => {
4009
- // if (changeTree.getChange(index) !== OPERATION.DELETE) {
4010
- // changes.set(index, OPERATION.ADD);
4011
- // }
4012
- // });
4013
- const allChangesSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4720
+ const isInvisible = this.invisible.has(changeTree);
4721
+ const changeSet = (changeTree.isFiltered || changeTree.isPartiallyFiltered)
4014
4722
  ? changeTree.allFilteredChanges
4015
4723
  : changeTree.allChanges;
4016
- const it = allChangesSet.keys();
4017
- const isInvisible = this.invisible.has(changeTree);
4018
- for (const index of it) {
4019
- if ((isInvisible || metadata?.[metadata?.[index]].tag === tag) &&
4020
- changeTree.getChange(index) !== exports.OPERATION.DELETE) {
4021
- changes.set(index, exports.OPERATION.ADD);
4724
+ for (let i = 0, len = changeSet.operations.length; i < len; i++) {
4725
+ const index = changeSet.operations[i];
4726
+ if (index === undefined) {
4727
+ continue;
4728
+ } // skip "undefined" indexes
4729
+ const op = changeTree.indexedOperations[index];
4730
+ const tagAtIndex = metadata?.[index].tag;
4731
+ if ((isInvisible || // if "invisible", include all
4732
+ tagAtIndex === undefined || // "all change" with no tag
4733
+ tagAtIndex === tag // tagged property
4734
+ ) &&
4735
+ op !== exports.OPERATION.DELETE) {
4736
+ changes[index] = op;
4022
4737
  }
4023
4738
  }
4024
4739
  }
4025
- // TODO: avoid unnecessary iteration here
4026
- while (changeTree.parent &&
4027
- (changeTree = changeTree.parent[$changes]) &&
4028
- (changeTree.isFiltered || changeTree.isPartiallyFiltered)) {
4029
- this.items.add(changeTree);
4030
- }
4740
+ // Add children of this ChangeTree to this view
4741
+ changeTree.forEachChild((change, index) => {
4742
+ // Do not ADD children that don't have the same tag
4743
+ if (metadata && metadata[index].tag !== tag) {
4744
+ return;
4745
+ }
4746
+ this.add(change.ref, tag, false);
4747
+ });
4031
4748
  return this;
4032
4749
  }
4033
- addParent(changeTree, tag) {
4034
- const parentRef = changeTree.parent;
4035
- if (!parentRef) {
4036
- return;
4750
+ addParent(changeTree, parentIndex, tag) {
4751
+ // view must have all "changeTree" parent tree
4752
+ this.items.add(changeTree);
4753
+ // add parent's parent
4754
+ const parentChangeTree = changeTree.parent?.[$changes];
4755
+ if (parentChangeTree && (parentChangeTree.isFiltered || parentChangeTree.isPartiallyFiltered)) {
4756
+ this.addParent(parentChangeTree, changeTree.parentIndex, tag);
4037
4757
  }
4038
- const parentChangeTree = parentRef[$changes];
4039
- const parentIndex = changeTree.parentIndex;
4040
- if (!this.invisible.has(parentChangeTree)) {
4041
- // parent is already available, no need to add it!
4758
+ // parent is already available, no need to add it!
4759
+ if (!this.invisible.has(changeTree)) {
4042
4760
  return;
4043
4761
  }
4044
- this.addParent(parentChangeTree, tag);
4045
4762
  // add parent's tag properties
4046
- if (parentChangeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4047
- let parentChanges = this.changes.get(parentChangeTree);
4048
- if (parentChanges === undefined) {
4049
- parentChanges = new Map();
4050
- this.changes.set(parentChangeTree, parentChanges);
4763
+ if (changeTree.getChange(parentIndex) !== exports.OPERATION.DELETE) {
4764
+ let changes = this.changes[changeTree.refId];
4765
+ if (changes === undefined) {
4766
+ changes = {};
4767
+ this.changes[changeTree.refId] = changes;
4051
4768
  }
4052
- // console.log("add parent change", {
4053
- // parentIndex,
4054
- // parentChanges,
4055
- // parentChange: (
4056
- // parentChangeTree.getChange(parentIndex) &&
4057
- // OPERATION[parentChangeTree.getChange(parentIndex)]
4058
- // ),
4059
- // })
4060
4769
  if (!this.tags) {
4061
4770
  this.tags = new WeakMap();
4062
4771
  }
4063
4772
  let tags;
4064
- if (!this.tags.has(parentChangeTree)) {
4773
+ if (!this.tags.has(changeTree)) {
4065
4774
  tags = new Set();
4066
- this.tags.set(parentChangeTree, tags);
4775
+ this.tags.set(changeTree, tags);
4067
4776
  }
4068
4777
  else {
4069
- tags = this.tags.get(parentChangeTree);
4778
+ tags = this.tags.get(changeTree);
4070
4779
  }
4071
4780
  tags.add(tag);
4072
- parentChanges.set(parentIndex, exports.OPERATION.ADD);
4781
+ changes[parentIndex] = exports.OPERATION.ADD;
4073
4782
  }
4074
4783
  }
4075
4784
  remove(obj, tag = DEFAULT_VIEW_TAG) {
@@ -4081,32 +4790,32 @@ class StateView {
4081
4790
  this.items.delete(changeTree);
4082
4791
  const ref = changeTree.ref;
4083
4792
  const metadata = ref.constructor[Symbol.metadata];
4084
- let changes = this.changes.get(changeTree);
4793
+ let changes = this.changes[changeTree.refId];
4085
4794
  if (changes === undefined) {
4086
- changes = new Map();
4087
- this.changes.set(changeTree, changes);
4795
+ changes = {};
4796
+ this.changes[changeTree.refId] = changes;
4088
4797
  }
4089
4798
  if (tag === DEFAULT_VIEW_TAG) {
4090
4799
  // parent is collection (Map/Array)
4091
4800
  const parent = changeTree.parent;
4092
4801
  if (!Metadata.isValidInstance(parent)) {
4093
4802
  const parentChangeTree = parent[$changes];
4094
- let changes = this.changes.get(parentChangeTree);
4803
+ let changes = this.changes[parentChangeTree.refId];
4095
4804
  if (changes === undefined) {
4096
- changes = new Map();
4097
- this.changes.set(parentChangeTree, changes);
4805
+ changes = {};
4806
+ this.changes[parentChangeTree.refId] = changes;
4098
4807
  }
4099
4808
  // DELETE / DELETE BY REF ID
4100
- changes.set(changeTree.parentIndex, exports.OPERATION.DELETE);
4809
+ changes[changeTree.parentIndex] = exports.OPERATION.DELETE;
4101
4810
  }
4102
4811
  else {
4103
4812
  // delete all "tagged" properties.
4104
- metadata[-2].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4813
+ metadata[$viewFieldIndexes].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4105
4814
  }
4106
4815
  }
4107
4816
  else {
4108
4817
  // delete only tagged properties
4109
- metadata[-3][tag].forEach((index) => changes.set(index, exports.OPERATION.DELETE));
4818
+ metadata[$fieldIndexesByViewTag][tag].forEach((index) => changes[index] = exports.OPERATION.DELETE);
4110
4819
  }
4111
4820
  // remove tag
4112
4821
  if (this.tags && this.tags.has(changeTree)) {
@@ -4164,7 +4873,10 @@ exports.dumpChanges = dumpChanges;
4164
4873
  exports.encode = encode;
4165
4874
  exports.encodeKeyValueOperation = encodeArray;
4166
4875
  exports.encodeSchemaOperation = encodeSchemaOperation;
4876
+ exports.getDecoderStateCallbacks = getDecoderStateCallbacks;
4877
+ exports.getRawChangesCallback = getRawChangesCallback;
4167
4878
  exports.registerType = registerType;
4879
+ exports.schema = schema;
4168
4880
  exports.type = type;
4169
4881
  exports.view = view;
4170
4882
  //# sourceMappingURL=index.js.map