@colyseus/schema 3.0.0-alpha.2 → 3.0.0-alpha.22

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 (99) hide show
  1. package/README.md +131 -61
  2. package/build/cjs/index.js +472 -150
  3. package/build/cjs/index.js.map +1 -1
  4. package/build/esm/index.mjs +471 -149
  5. package/build/esm/index.mjs.map +1 -1
  6. package/build/umd/index.js +472 -150
  7. package/lib/Metadata.d.ts +2 -0
  8. package/lib/Metadata.js +39 -0
  9. package/lib/Metadata.js.map +1 -1
  10. package/lib/Reflection.d.ts +1 -2
  11. package/lib/Reflection.js +28 -22
  12. package/lib/Reflection.js.map +1 -1
  13. package/lib/Schema.d.ts +1 -1
  14. package/lib/annotations.js +12 -10
  15. package/lib/annotations.js.map +1 -1
  16. package/lib/bench_encode.d.ts +1 -0
  17. package/lib/bench_encode.js +93 -0
  18. package/lib/bench_encode.js.map +1 -0
  19. package/lib/codegen/api.js +1 -2
  20. package/lib/codegen/api.js.map +1 -1
  21. package/lib/codegen/languages/cpp.js +1 -2
  22. package/lib/codegen/languages/cpp.js.map +1 -1
  23. package/lib/codegen/languages/csharp.js +1 -2
  24. package/lib/codegen/languages/csharp.js.map +1 -1
  25. package/lib/codegen/languages/haxe.js +1 -2
  26. package/lib/codegen/languages/haxe.js.map +1 -1
  27. package/lib/codegen/languages/java.js +1 -2
  28. package/lib/codegen/languages/java.js.map +1 -1
  29. package/lib/codegen/languages/js.js +1 -2
  30. package/lib/codegen/languages/js.js.map +1 -1
  31. package/lib/codegen/languages/lua.js +1 -2
  32. package/lib/codegen/languages/lua.js.map +1 -1
  33. package/lib/codegen/languages/ts.js +1 -2
  34. package/lib/codegen/languages/ts.js.map +1 -1
  35. package/lib/codegen/parser.js +2 -3
  36. package/lib/codegen/parser.js.map +1 -1
  37. package/lib/codegen/types.js +3 -3
  38. package/lib/codegen/types.js.map +1 -1
  39. package/lib/decoder/DecodeOperation.d.ts +0 -1
  40. package/lib/decoder/DecodeOperation.js +22 -7
  41. package/lib/decoder/DecodeOperation.js.map +1 -1
  42. package/lib/decoder/Decoder.d.ts +1 -2
  43. package/lib/decoder/Decoder.js +4 -4
  44. package/lib/decoder/Decoder.js.map +1 -1
  45. package/lib/decoder/strategy/RawChanges.js +1 -2
  46. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  47. package/lib/decoder/strategy/StateCallbacks.d.ts +36 -7
  48. package/lib/decoder/strategy/StateCallbacks.js +39 -46
  49. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  50. package/lib/encoder/ChangeTree.js +2 -5
  51. package/lib/encoder/ChangeTree.js.map +1 -1
  52. package/lib/encoder/EncodeOperation.d.ts +0 -1
  53. package/lib/encoder/EncodeOperation.js +29 -13
  54. package/lib/encoder/EncodeOperation.js.map +1 -1
  55. package/lib/encoder/Encoder.d.ts +5 -4
  56. package/lib/encoder/Encoder.js +56 -38
  57. package/lib/encoder/Encoder.js.map +1 -1
  58. package/lib/encoder/StateView.js +11 -6
  59. package/lib/encoder/StateView.js.map +1 -1
  60. package/lib/encoding/assert.js +3 -3
  61. package/lib/encoding/assert.js.map +1 -1
  62. package/lib/encoding/decode.d.ts +21 -19
  63. package/lib/encoding/decode.js +24 -25
  64. package/lib/encoding/decode.js.map +1 -1
  65. package/lib/encoding/encode.d.ts +2 -2
  66. package/lib/encoding/encode.js +40 -39
  67. package/lib/encoding/encode.js.map +1 -1
  68. package/lib/encoding/spec.d.ts +2 -1
  69. package/lib/encoding/spec.js +1 -0
  70. package/lib/encoding/spec.js.map +1 -1
  71. package/lib/index.d.ts +3 -0
  72. package/lib/index.js +5 -1
  73. package/lib/index.js.map +1 -1
  74. package/lib/types/custom/ArraySchema.d.ts +2 -2
  75. package/lib/types/custom/ArraySchema.js +0 -8
  76. package/lib/types/custom/ArraySchema.js.map +1 -1
  77. package/lib/types/registry.js +3 -4
  78. package/lib/types/registry.js.map +1 -1
  79. package/lib/types/utils.js +1 -2
  80. package/lib/types/utils.js.map +1 -1
  81. package/lib/utils.js +3 -4
  82. package/lib/utils.js.map +1 -1
  83. package/package.json +5 -5
  84. package/src/Metadata.ts +47 -0
  85. package/src/Reflection.ts +31 -22
  86. package/src/annotations.ts +6 -2
  87. package/src/bench_encode.ts +66 -0
  88. package/src/decoder/DecodeOperation.ts +26 -6
  89. package/src/decoder/Decoder.ts +5 -5
  90. package/src/decoder/strategy/StateCallbacks.ts +93 -53
  91. package/src/encoder/ChangeTree.ts +2 -4
  92. package/src/encoder/EncodeOperation.ts +29 -12
  93. package/src/encoder/Encoder.ts +65 -41
  94. package/src/encoder/StateView.ts +10 -7
  95. package/src/encoding/decode.ts +24 -25
  96. package/src/encoding/encode.ts +25 -22
  97. package/src/encoding/spec.ts +1 -0
  98. package/src/index.ts +5 -0
  99. package/src/types/custom/ArraySchema.ts +2 -1
@@ -4,7 +4,7 @@ import { Schema } from "../Schema";
4
4
  import type { Ref } from "../encoder/ChangeTree";
5
5
  import type { Decoder } from "./Decoder";
6
6
  import * as decode from "../encoding/decode";
7
- import { $changes, $childType, $deleteByIndex, $getByIndex } from "../types/symbols";
7
+ import { $childType, $deleteByIndex, $getByIndex } from "../types/symbols";
8
8
 
9
9
  import type { MapSchema } from "../types/custom/MapSchema";
10
10
  import type { ArraySchema } from "../types/custom/ArraySchema";
@@ -43,7 +43,7 @@ export function decodeValue(
43
43
  it: decode.Iterator,
44
44
  allChanges: DataChange[],
45
45
  ) {
46
- const $root = decoder.$root;
46
+ const $root = decoder.root;
47
47
  const previousValue = ref[$getByIndex](index);
48
48
 
49
49
  let value: any;
@@ -251,6 +251,7 @@ export const decodeKeyValueOperation: DecodeOperation = function (
251
251
  dynamicIndex = ref['getIndex'](index);
252
252
  }
253
253
 
254
+
254
255
  const { value, previousValue } = decodeValue(
255
256
  decoder,
256
257
  operation,
@@ -303,7 +304,10 @@ export const decodeArray: DecodeOperation = function (
303
304
  allChanges: DataChange[]
304
305
  ) {
305
306
  // "uncompressed" index + operation (array/map items)
306
- const operation = bytes[it.offset++];
307
+ let operation = bytes[it.offset++];
308
+
309
+ let isSchemaChild: boolean;
310
+ let index: number;
307
311
 
308
312
  if (operation === OPERATION.CLEAR) {
309
313
  //
@@ -318,8 +322,8 @@ export const decodeArray: DecodeOperation = function (
318
322
  } else if (operation === OPERATION.DELETE_BY_REFID) {
319
323
  // TODO: refactor here, try to follow same flow as below
320
324
  const refId = decode.number(bytes, it);
321
- const previousValue = decoder.$root.refs.get(refId);
322
- const index = ref.findIndex((value) => value === previousValue);
325
+ const previousValue = decoder.root.refs.get(refId);
326
+ index = ref.findIndex((value) => value === previousValue);
323
327
  ref[$deleteByIndex](index);
324
328
  allChanges.push({
325
329
  ref,
@@ -330,10 +334,26 @@ export const decodeArray: DecodeOperation = function (
330
334
  value: undefined,
331
335
  previousValue,
332
336
  });
337
+
333
338
  return;
339
+
340
+ } else if (operation === OPERATION.ADD_BY_REFID) {
341
+ isSchemaChild = true;
342
+ // operation = OPERATION.ADD;
343
+
344
+ const refId = decode.number(bytes, it);
345
+ const itemByRefId = decoder.root.refs.get(refId);
346
+
347
+ // use existing index, or push new value
348
+ index = (itemByRefId)
349
+ ? ref.findIndex((value) => value === itemByRefId)
350
+ : ref.length;
351
+
352
+ } else {
353
+ isSchemaChild = false;
354
+ index = decode.number(bytes, it);
334
355
  }
335
356
 
336
- const index = decode.number(bytes, it);
337
357
  const type = ref[$childType];
338
358
 
339
359
  let dynamicIndex: number | string = index;
@@ -14,7 +14,7 @@ export class Decoder<T extends Schema = any> {
14
14
  context: TypeContext;
15
15
 
16
16
  state: T;
17
- $root: ReferenceTracker;
17
+ root: ReferenceTracker;
18
18
 
19
19
  currentRefId: number = 0;
20
20
 
@@ -32,8 +32,8 @@ export class Decoder<T extends Schema = any> {
32
32
 
33
33
  protected setRoot(root: T) {
34
34
  this.state = root;
35
- this.$root = new ReferenceTracker();
36
- this.$root.addRef(0, root);
35
+ this.root = new ReferenceTracker();
36
+ this.root.addRef(0, root);
37
37
  }
38
38
 
39
39
  decode(
@@ -43,7 +43,7 @@ export class Decoder<T extends Schema = any> {
43
43
  ) {
44
44
  const allChanges: DataChange[] = [];
45
45
 
46
- const $root = this.$root;
46
+ const $root = this.root;
47
47
  const totalBytes = bytes.byteLength;
48
48
 
49
49
  let decoder: DecodeOperation = ref['constructor'][$decoder];
@@ -146,7 +146,7 @@ export class Decoder<T extends Schema = any> {
146
146
  });
147
147
 
148
148
  if (needRemoveRef) {
149
- this.$root.removeRef(this.$root.refIds.get(value));
149
+ this.root.removeRef(this.root.refIds.get(value));
150
150
  }
151
151
  });
152
152
  }
@@ -17,27 +17,69 @@ import type { ArraySchema } from "../../types/custom/ArraySchema";
17
17
  // - Avoid closures by allowing to pass a context. (https://github.com/colyseus/schema/issues/155#issuecomment-1804694081)
18
18
  //
19
19
 
20
- type GetProxyType<T> = unknown extends T // is "any"?
20
+ export type GetCallbackProxy = (<T extends Schema>(instance: T) => CallbackProxy<T>);
21
+
22
+ export type CallbackProxy<T> = unknown extends T // is "any"?
21
23
  ? InstanceCallback<T> & CollectionCallback<any, any>
22
24
  : T extends Collection<infer K, infer V, infer _>
23
25
  ? CollectionCallback<K, V>
24
- : InstanceCallback<T>
26
+ : InstanceCallback<T>;
25
27
 
26
28
  type InstanceCallback<T> = {
29
+ /**
30
+ * Trigger callback when value of a property changes.
31
+ *
32
+ * @param prop name of the property
33
+ * @param callback callback to be triggered on property change
34
+ * @param immediate trigger immediatelly if property has been already set.
35
+ */
27
36
  listen<K extends NonFunctionPropNames<T>>(
28
37
  prop: K,
29
38
  callback: (value: T[K], previousValue: T[K]) => void,
30
39
  immediate?: boolean,
31
40
  )
41
+ /**
42
+ * Trigger callback whenever any property changed within this instance.
43
+ *
44
+ * @param prop name of the property
45
+ * @param callback callback to be triggered on property change
46
+ * @param immediate trigger immediatelly if property has been already set.
47
+ */
32
48
  onChange(callback: () => void): void;
49
+
50
+ /**
51
+ * Bind properties to another object. Changes on the properties will be reflected on the target object.
52
+ *
53
+ * @param targetObject object to bind properties to
54
+ * @param properties list of properties to bind. If not provided, all properties will be bound.
55
+ */
33
56
  bindTo(targetObject: any, properties?: Array<NonFunctionPropNames<T>>): void;
34
57
  } & {
35
- [K in NonFunctionNonPrimitivePropNames<T>]: GetProxyType<T[K]>;
58
+ [K in NonFunctionNonPrimitivePropNames<T>]: CallbackProxy<T[K]>;
36
59
  }
37
60
 
38
61
  type CollectionCallback<K, V> = {
62
+ /**
63
+ * Trigger callback when an item has been added to the collection.
64
+ *
65
+ * @param callback
66
+ * @param immediate
67
+ */
39
68
  onAdd(callback: (item: V, index: K) => void, immediate?: boolean): void;
69
+
70
+ /**
71
+ * Trigger callback when an item has been removed to the collection.
72
+ *
73
+ * @param callback
74
+ */
40
75
  onRemove(callback: (item: V, index: K) => void): void;
76
+
77
+ // /**
78
+ // * Trigger callback when an item has been removed to the collection.
79
+ // *
80
+ // * @param callback
81
+ // */
82
+ // onChange(callback: (item: V, index: K) => void): void;
41
83
  };
42
84
 
43
85
  type OnInstanceAvailableCallback = (callback: (ref: Ref, existing: boolean) => void) => void;
@@ -48,8 +90,9 @@ type CallContext = {
48
90
  onInstanceAvailable?: OnInstanceAvailableCallback,
49
91
  }
50
92
 
51
- export function getStateCallbacks(decoder: Decoder) {
52
- const $root = decoder.$root;
93
+
94
+ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>): GetCallbackProxy {
95
+ const $root = decoder.root;
53
96
  const callbacks = $root.callbacks;
54
97
 
55
98
  let isTriggeringOnAdd = false;
@@ -76,8 +119,6 @@ export function getStateCallbacks(decoder: Decoder) {
76
119
  for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
77
120
  deleteCallbacks[i]();
78
121
  }
79
- // callbacks[$root.refIds.get(change.previousValue)]?.[OPERATION.DELETE]?.forEach(callback =>
80
- // callback());
81
122
  }
82
123
 
83
124
  if (ref instanceof Schema) {
@@ -86,47 +127,35 @@ export function getStateCallbacks(decoder: Decoder) {
86
127
  //
87
128
 
88
129
  if (!uniqueRefIds.has(refId)) {
89
- try {
90
- // trigger onChange
91
- const replaceCallbacks = $callbacks?.[OPERATION.REPLACE];
92
- for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
93
- replaceCallbacks[i]();
94
- }
95
-
96
- } catch (e) {
97
- console.error(e);
130
+ // trigger onChange
131
+ const replaceCallbacks = $callbacks?.[OPERATION.REPLACE];
132
+ for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
133
+ replaceCallbacks[i]();
134
+ // try {
135
+ // } catch (e) {
136
+ // console.error(e);
137
+ // }
98
138
  }
99
139
  }
100
140
 
101
- try {
102
- if ($callbacks.hasOwnProperty(change.field)) {
103
- const fieldCallbacks = $callbacks[change.field];
104
- for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
105
- fieldCallbacks[i](change.value, change.previousValue);
106
- }
141
+ if ($callbacks.hasOwnProperty(change.field)) {
142
+ const fieldCallbacks = $callbacks[change.field];
143
+ for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
144
+ fieldCallbacks[i](change.value, change.previousValue);
145
+ // try {
146
+ // } catch (e) {
147
+ // console.error(e);
148
+ // }
107
149
  }
108
-
109
- } catch (e) {
110
- //
111
- console.error(e);
112
150
  }
113
151
 
152
+
114
153
  } else {
115
154
  //
116
155
  // Handle collection of items
117
156
  //
118
157
 
119
- if (change.op === OPERATION.ADD && change.previousValue === undefined) {
120
- // triger onAdd
121
-
122
- isTriggeringOnAdd = true;
123
- const addCallbacks = $callbacks[OPERATION.ADD];
124
- for (let i = addCallbacks?.length - 1; i >= 0; i--) {
125
- addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
126
- }
127
- isTriggeringOnAdd = false;
128
-
129
- } else if ((change.op & OPERATION.DELETE) === OPERATION.DELETE) {
158
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE) {
130
159
  //
131
160
  // FIXME: `previousValue` should always be available.
132
161
  //
@@ -146,6 +175,16 @@ export function getStateCallbacks(decoder: Decoder) {
146
175
  addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
147
176
  }
148
177
  }
178
+
179
+ } else if ((change.op & OPERATION.ADD) === OPERATION.ADD && change.previousValue === undefined) {
180
+ // triger onAdd
181
+
182
+ isTriggeringOnAdd = true;
183
+ const addCallbacks = $callbacks[OPERATION.ADD];
184
+ for (let i = addCallbacks?.length - 1; i >= 0; i--) {
185
+ addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
186
+ }
187
+ isTriggeringOnAdd = false;
149
188
  }
150
189
 
151
190
  // trigger onChange
@@ -206,10 +245,22 @@ export function getStateCallbacks(decoder: Decoder) {
206
245
  OPERATION.REPLACE,
207
246
  callback
208
247
  );
209
-
210
248
  },
211
249
  bindTo: function bindTo(targetObject: any, properties?: string[]) {
212
- console.log("bindTo", targetObject, properties);
250
+ //
251
+ // TODO: refactor this implementation. There is room for improvement here.
252
+ //
253
+ if (!properties) {
254
+ properties = Object.keys(metadata);
255
+ }
256
+ return $root.addCallback(
257
+ $root.refIds.get(context.instance),
258
+ OPERATION.REPLACE,
259
+ () => {
260
+ properties.forEach((prop) =>
261
+ targetObject[prop] = context.instance[prop])
262
+ }
263
+ );
213
264
  }
214
265
  }, {
215
266
  get(target, prop: string) {
@@ -307,20 +358,9 @@ export function getStateCallbacks(decoder: Decoder) {
307
358
  }
308
359
  }
309
360
 
310
- function $<T>(instance: T): GetProxyType<T> {
311
- return getProxy(undefined, { instance }) as GetProxyType<T>;
312
- }
313
-
314
- return {
315
- $,
316
- trigger: function trigger(changes: DataChange[]) {
317
- for (let i = 0, l = changes.length; i < l; i++) {
318
- const change = changes[i];
319
-
320
- change.op
321
- change.ref
322
- }
323
- }
361
+ function $<T>(instance: T): CallbackProxy<T> {
362
+ return getProxy(undefined, { instance }) as CallbackProxy<T>;
324
363
  }
325
364
 
365
+ return $;
326
366
  }
@@ -170,13 +170,12 @@ export class ChangeTree<T extends Ref=any> {
170
170
 
171
171
  if (!this.isFiltered) {
172
172
  this.root.changes.set(this, this.changes);
173
+ this.root.allChanges.set(this, this.allChanges);
173
174
  }
174
175
 
175
176
  if (this.isFiltered || this.isPartiallyFiltered) {
176
177
  this.root.filteredChanges.set(this, this.filteredChanges);
177
178
  this.root.allFilteredChanges.set(this, this.filteredChanges);
178
- } else {
179
- this.root.allChanges.set(this, this.allChanges);
180
179
  }
181
180
 
182
181
  this.ensureRefId();
@@ -285,7 +284,6 @@ export class ChangeTree<T extends Ref=any> {
285
284
 
286
285
  private _shiftAllChangeIndexes(shiftIndex: number, startIndex: number = 0, allChangeSet: Map<number, OPERATION>) {
287
286
  Array.from(allChangeSet.entries()).forEach(([index, op]) => {
288
- // console.log('shiftAllChangeIndexes', index >= startIndex, { index, op, shiftIndex, startIndex })
289
287
  if (index >= startIndex) {
290
288
  allChangeSet.delete(index);
291
289
  allChangeSet.set(index + shiftIndex, op);
@@ -452,7 +450,7 @@ export class ChangeTree<T extends Ref=any> {
452
450
 
453
451
  protected checkIsFiltered(parent: Ref, parentIndex: number) {
454
452
  // Detect if current structure has "filters" declared
455
- this.isPartiallyFiltered = this.ref['constructor']?.[Symbol.metadata]?.[-2];
453
+ this.isPartiallyFiltered = (this.ref['constructor']?.[Symbol.metadata]?.[-2] !== undefined);
456
454
 
457
455
  // TODO: support "partially filtered", where the instance is visible, but only a field is not.
458
456
 
@@ -172,6 +172,19 @@ export const encodeKeyValueOperation: EncodeOperation = function (
172
172
  const type = changeTree.getType(field);
173
173
  const value = changeTree.getValue(field);
174
174
 
175
+ // try { throw new Error(); } catch (e) {
176
+ // // only print if not coming from Reflection.ts
177
+ // if (!e.stack.includes("src/Reflection.ts")) {
178
+ // console.log("encodeKeyValueOperation -> ", {
179
+ // ref: changeTree.ref.constructor.name,
180
+ // field,
181
+ // operation: OPERATION[operation],
182
+ // value: value?.toJSON(),
183
+ // items: ref.toJSON(),
184
+ // });
185
+ // }
186
+ // }
187
+
175
188
  // TODO: inline this function call small performance gain
176
189
  encodeValue(encoder, bytes, ref, type, value, field, operation, it);
177
190
  }
@@ -191,18 +204,22 @@ export const encodeArray: EncodeOperation = function (
191
204
  hasView: boolean,
192
205
  ) {
193
206
  const ref = changeTree.ref;
207
+ const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
194
208
 
195
- if (
196
- hasView &&
197
- operation === OPERATION.DELETE &&
198
- typeof (changeTree.getType(field)) !== "string"
199
- ) {
200
- // encode delete by refId (array of schemas)
201
- bytes[it.offset++] = OPERATION.DELETE_BY_REFID;
202
- const value = ref['tmpItems'][field];
203
- const refId = value[$changes].refId;
204
- encode.number(bytes, refId, it);
205
- return;
209
+ let refOrIndex: number;
210
+
211
+ if (useOperationByRefId) {
212
+ refOrIndex = ref['tmpItems'][field][$changes].refId;
213
+
214
+ if (operation === OPERATION.DELETE) {
215
+ operation = OPERATION.DELETE_BY_REFID;
216
+
217
+ } else if (operation === OPERATION.ADD) {
218
+ operation = OPERATION.ADD_BY_REFID;
219
+ }
220
+
221
+ } else {
222
+ refOrIndex = field;
206
223
  }
207
224
 
208
225
  // encode operation
@@ -214,7 +231,7 @@ export const encodeArray: EncodeOperation = function (
214
231
  }
215
232
 
216
233
  // encode index
217
- encode.number(bytes, field, it);
234
+ encode.number(bytes, refOrIndex, it);
218
235
 
219
236
  // Do not encode value for DELETE operations
220
237
  if (operation === OPERATION.DELETE) {
@@ -5,10 +5,11 @@ import { $changes, $encoder, $filter } from "../types/symbols";
5
5
  import * as encode from "../encoding/encode";
6
6
  import type { Iterator } from "../encoding/decode";
7
7
 
8
- import { SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec';
8
+ import { OPERATION, SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec';
9
9
  import { Root } from "./ChangeTree";
10
10
  import { getNextPowerOf2 } from "../utils";
11
11
  import type { StateView } from "./StateView";
12
+ import { Metadata } from "../Metadata";
12
13
 
13
14
  export class Encoder<T extends Schema = any> {
14
15
  static BUFFER_SIZE = 8 * 1024;// 8KB
@@ -37,18 +38,24 @@ export class Encoder<T extends Schema = any> {
37
38
  protected setRoot(state: T) {
38
39
  this.root = new Root();
39
40
  this.state = state;
41
+
42
+ // Workaround to allow using an empty Schema.
43
+ if (state.constructor[Symbol.metadata] === undefined) {
44
+ Metadata.init(state);
45
+ }
46
+
40
47
  state[$changes].setRoot(this.root);
41
48
  }
42
49
 
43
50
  encode(
44
51
  it: Iterator = { offset: 0 },
45
52
  view?: StateView,
46
- bytes = this.sharedBuffer,
47
- changeTrees = this.root.changes
53
+ buffer = this.sharedBuffer,
54
+ changeTrees = this.root.changes,
55
+ isEncodeAll = this.root.allChanges === changeTrees,
48
56
  ): Buffer {
49
57
  const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
50
58
 
51
- const isEncodeAll = this.root.allChanges === changeTrees;
52
59
  const hasView = (view !== undefined);
53
60
  const rootChangeTree = this.state[$changes];
54
61
 
@@ -61,6 +68,13 @@ export class Encoder<T extends Schema = any> {
61
68
  const encoder = ctor[$encoder];
62
69
  const filter = ctor[$filter];
63
70
 
71
+ // try { throw new Error(); } catch (e) {
72
+ // // only print if not coming from Reflection.ts
73
+ // if (!e.stack.includes("src/Reflection.ts")) {
74
+ // console.log("ChangeTree:", { ref: ref.constructor.name, });
75
+ // }
76
+ // }
77
+
64
78
  if (hasView) {
65
79
  if (!view.items.has(changeTree)) {
66
80
  view.invisible.add(changeTree);
@@ -73,8 +87,8 @@ export class Encoder<T extends Schema = any> {
73
87
 
74
88
  // skip root `refId` if it's the first change tree
75
89
  if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
76
- bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
77
- encode.number(bytes, changeTree.refId, it);
90
+ buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
91
+ encode.number(buffer, changeTree.refId, it);
78
92
  }
79
93
 
80
94
  const changesIterator = changes.entries();
@@ -95,25 +109,36 @@ export class Encoder<T extends Schema = any> {
95
109
  continue;
96
110
  }
97
111
 
98
- // console.log("WILL ENCODE", {
99
- // ref: changeTree.ref.constructor.name,
100
- // fieldIndex,
101
- // operation: OPERATION[operation],
102
- // });
103
-
104
- encoder(this, bytes, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
112
+ // try { throw new Error(); } catch (e) {
113
+ // // only print if not coming from Reflection.ts
114
+ // if (!e.stack.includes("src/Reflection.ts")) {
115
+ // console.log("WILL ENCODE", {
116
+ // ref: changeTree.ref.constructor.name,
117
+ // fieldIndex,
118
+ // operation: OPERATION[operation],
119
+ // });
120
+ // }
121
+ // }
122
+
123
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
105
124
  }
106
125
  }
107
126
 
108
- if (it.offset > bytes.byteLength) {
109
- const newSize = getNextPowerOf2(this.sharedBuffer.byteLength * 2);
110
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + bytes.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
127
+ if (it.offset > buffer.byteLength) {
128
+ const newSize = getNextPowerOf2(buffer.byteLength * 2);
129
+ console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + buffer.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
111
130
 
112
131
  //
113
132
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
114
133
  //
115
- this.sharedBuffer = Buffer.allocUnsafeSlow(newSize);
116
- return this.encode({ offset: initialOffset }, view);
134
+ buffer = Buffer.allocUnsafeSlow(newSize);
135
+
136
+ // assign resized buffer to local sharedBuffer
137
+ if (buffer === this.sharedBuffer) {
138
+ this.sharedBuffer = buffer;
139
+ }
140
+
141
+ return this.encode({ offset: initialOffset }, view, buffer, changeTrees, isEncodeAll);
117
142
 
118
143
  } else {
119
144
  //
@@ -126,47 +151,46 @@ export class Encoder<T extends Schema = any> {
126
151
  this.onEndEncode(changeTrees);
127
152
  }
128
153
 
129
- // return bytes;
130
- return bytes.slice(0, it.offset);
154
+ return buffer.subarray(0, it.offset);
131
155
  }
132
156
  }
133
157
 
134
- encodeAll(it: Iterator = { offset: 0 }) {
135
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
158
+ encodeAll(it: Iterator = { offset: 0 }, buffer: Buffer = this.sharedBuffer) {
159
+ // console.log(`encodeAll(), this.root.allChanges (${this.root.allChanges.size})`);
136
160
 
137
- // Array.from(this.$root.allChanges.entries()).map((item) => {
138
- // console.log("->", item[0].refId, item[0].ref.toJSON());
161
+ // Array.from(this.root.allChanges.entries()).map((item) => {
162
+ // console.log("->", { ref: item[0].ref.constructor.name, refId: item[0].refId, changes: item[1].size });
139
163
  // });
140
164
 
141
- return this.encode(it, undefined, this.sharedBuffer, this.root.allChanges);
165
+ return this.encode(it, undefined, buffer, this.root.allChanges, true);
142
166
  }
143
167
 
144
168
  encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes = this.sharedBuffer) {
145
169
  const viewOffset = it.offset;
146
170
 
147
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
171
+ // console.log(`encodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
148
172
  // this.debugAllFilteredChanges();
149
173
 
150
174
  // try to encode "filtered" changes
151
- this.encode(it, view, bytes, this.root.allFilteredChanges);
175
+ this.encode(it, view, bytes, this.root.allFilteredChanges, true);
152
176
 
153
177
  return Buffer.concat([
154
- bytes.slice(0, sharedOffset),
155
- bytes.slice(viewOffset, it.offset)
178
+ bytes.subarray(0, sharedOffset),
179
+ bytes.subarray(viewOffset, it.offset)
156
180
  ]);
157
181
  }
158
182
 
159
183
 
160
- // debugAllFilteredChanges() {
161
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
162
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
163
- // if (Array.isArray(item[0].ref.toJSON())) {
164
- // item[1].forEach((op, key) => {
165
- // console.log(" ->", { key, op: OPERATION[op] });
166
- // })
167
- // }
168
- // });
169
- // }
184
+ debugAllFilteredChanges() {
185
+ Array.from(this.root.allFilteredChanges.entries()).map((item) => {
186
+ console.log("->", { refId: item[0].refId, changes: item[1].size }, item[0].ref.toJSON());
187
+ if (Array.isArray(item[0].ref.toJSON())) {
188
+ item[1].forEach((op, key) => {
189
+ console.log(" ->", { key, op: OPERATION[op] });
190
+ })
191
+ }
192
+ });
193
+ }
170
194
 
171
195
  encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes = this.sharedBuffer) {
172
196
  const viewOffset = it.offset;
@@ -208,8 +232,8 @@ export class Encoder<T extends Schema = any> {
208
232
  view.changes.clear();
209
233
 
210
234
  return Buffer.concat([
211
- bytes.slice(0, sharedOffset),
212
- bytes.slice(viewOffset, it.offset)
235
+ bytes.subarray(0, sharedOffset),
236
+ bytes.subarray(viewOffset, it.offset)
213
237
  ]);
214
238
  }
215
239
 
@@ -34,15 +34,20 @@ export class StateView {
34
34
  return this;
35
35
  }
36
36
 
37
+ // FIXME: ArraySchema/MapSchema does not have metadata
38
+ const metadata: Metadata = obj.constructor[Symbol.metadata];
39
+
37
40
  let changeTree: ChangeTree = obj[$changes];
38
41
  this.items.add(changeTree);
39
42
 
40
43
  // Add children of this ChangeTree to this view
41
- changeTree.forEachChild((change, _) =>
42
- this.add(change.ref, tag));
43
-
44
- // FIXME: ArraySchema/MapSchema does not have metadata
45
- const metadata: Metadata = obj.constructor[Symbol.metadata];
44
+ changeTree.forEachChild((change, index) => {
45
+ // Do not ADD children that don't have the same tag
46
+ if (metadata && metadata[metadata[index]].tag !== tag) {
47
+ return;
48
+ }
49
+ this.add(change.ref, tag);
50
+ });
46
51
 
47
52
  // add parent ChangeTree's, if they are invisible to this view
48
53
  // TODO: REFACTOR addParent()
@@ -72,8 +77,6 @@ export class StateView {
72
77
  }
73
78
  tags.add(tag);
74
79
 
75
- // console.log("BY TAG:", tag);
76
-
77
80
  // Ref: add tagged properties
78
81
  metadata?.[-3]?.[tag]?.forEach((index) => {
79
82
  if (changeTree.getChange(index) !== OPERATION.DELETE) {