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