@colyseus/schema 3.0.0-alpha.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +148 -62
  2. package/bin/schema-debug +94 -0
  3. package/build/cjs/index.js +2227 -1519
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/esm/index.mjs +2228 -1522
  6. package/build/esm/index.mjs.map +1 -1
  7. package/build/umd/index.js +2230 -1522
  8. package/lib/Metadata.d.ts +21 -9
  9. package/lib/Metadata.js +169 -32
  10. package/lib/Metadata.js.map +1 -1
  11. package/lib/Reflection.d.ts +19 -4
  12. package/lib/Reflection.js +66 -31
  13. package/lib/Reflection.js.map +1 -1
  14. package/lib/Schema.d.ts +12 -5
  15. package/lib/Schema.js +57 -56
  16. package/lib/Schema.js.map +1 -1
  17. package/lib/annotations.d.ts +31 -34
  18. package/lib/annotations.js +110 -160
  19. package/lib/annotations.js.map +1 -1
  20. package/lib/bench_encode.d.ts +1 -0
  21. package/lib/bench_encode.js +130 -0
  22. package/lib/bench_encode.js.map +1 -0
  23. package/lib/codegen/api.js +1 -2
  24. package/lib/codegen/api.js.map +1 -1
  25. package/lib/codegen/languages/cpp.js +1 -2
  26. package/lib/codegen/languages/cpp.js.map +1 -1
  27. package/lib/codegen/languages/csharp.js +9 -46
  28. package/lib/codegen/languages/csharp.js.map +1 -1
  29. package/lib/codegen/languages/haxe.js +4 -2
  30. package/lib/codegen/languages/haxe.js.map +1 -1
  31. package/lib/codegen/languages/java.js +1 -2
  32. package/lib/codegen/languages/java.js.map +1 -1
  33. package/lib/codegen/languages/js.js +1 -2
  34. package/lib/codegen/languages/js.js.map +1 -1
  35. package/lib/codegen/languages/lua.js +23 -25
  36. package/lib/codegen/languages/lua.js.map +1 -1
  37. package/lib/codegen/languages/ts.js +1 -2
  38. package/lib/codegen/languages/ts.js.map +1 -1
  39. package/lib/codegen/parser.js +85 -3
  40. package/lib/codegen/parser.js.map +1 -1
  41. package/lib/codegen/types.js +6 -3
  42. package/lib/codegen/types.js.map +1 -1
  43. package/lib/debug.d.ts +1 -0
  44. package/lib/debug.js +51 -0
  45. package/lib/debug.js.map +1 -0
  46. package/lib/decoder/DecodeOperation.d.ts +3 -4
  47. package/lib/decoder/DecodeOperation.js +35 -17
  48. package/lib/decoder/DecodeOperation.js.map +1 -1
  49. package/lib/decoder/Decoder.d.ts +5 -6
  50. package/lib/decoder/Decoder.js +10 -10
  51. package/lib/decoder/Decoder.js.map +1 -1
  52. package/lib/decoder/ReferenceTracker.js +4 -2
  53. package/lib/decoder/ReferenceTracker.js.map +1 -1
  54. package/lib/decoder/strategy/RawChanges.js +1 -2
  55. package/lib/decoder/strategy/RawChanges.js.map +1 -1
  56. package/lib/decoder/strategy/StateCallbacks.d.ts +44 -11
  57. package/lib/decoder/strategy/StateCallbacks.js +74 -64
  58. package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
  59. package/lib/encoder/ChangeTree.d.ts +28 -20
  60. package/lib/encoder/ChangeTree.js +242 -188
  61. package/lib/encoder/ChangeTree.js.map +1 -1
  62. package/lib/encoder/EncodeOperation.d.ts +3 -6
  63. package/lib/encoder/EncodeOperation.js +51 -65
  64. package/lib/encoder/EncodeOperation.js.map +1 -1
  65. package/lib/encoder/Encoder.d.ts +8 -7
  66. package/lib/encoder/Encoder.js +133 -85
  67. package/lib/encoder/Encoder.js.map +1 -1
  68. package/lib/encoder/Root.d.ts +22 -0
  69. package/lib/encoder/Root.js +81 -0
  70. package/lib/encoder/Root.js.map +1 -0
  71. package/lib/encoder/StateView.d.ts +7 -7
  72. package/lib/encoder/StateView.js +72 -74
  73. package/lib/encoder/StateView.js.map +1 -1
  74. package/lib/encoding/assert.d.ts +7 -6
  75. package/lib/encoding/assert.js +13 -5
  76. package/lib/encoding/assert.js.map +1 -1
  77. package/lib/encoding/decode.d.ts +36 -19
  78. package/lib/encoding/decode.js +54 -84
  79. package/lib/encoding/decode.js.map +1 -1
  80. package/lib/encoding/encode.d.ts +36 -18
  81. package/lib/encoding/encode.js +61 -48
  82. package/lib/encoding/encode.js.map +1 -1
  83. package/lib/encoding/spec.d.ts +4 -5
  84. package/lib/encoding/spec.js +1 -2
  85. package/lib/encoding/spec.js.map +1 -1
  86. package/lib/index.d.ts +10 -9
  87. package/lib/index.js +24 -17
  88. package/lib/index.js.map +1 -1
  89. package/lib/types/HelperTypes.d.ts +34 -2
  90. package/lib/types/HelperTypes.js.map +1 -1
  91. package/lib/types/TypeContext.d.ts +29 -0
  92. package/lib/types/TypeContext.js +151 -0
  93. package/lib/types/TypeContext.js.map +1 -0
  94. package/lib/types/custom/ArraySchema.d.ts +2 -2
  95. package/lib/types/custom/ArraySchema.js +33 -22
  96. package/lib/types/custom/ArraySchema.js.map +1 -1
  97. package/lib/types/custom/CollectionSchema.d.ts +2 -2
  98. package/lib/types/custom/CollectionSchema.js +1 -0
  99. package/lib/types/custom/CollectionSchema.js.map +1 -1
  100. package/lib/types/custom/MapSchema.d.ts +18 -16
  101. package/lib/types/custom/MapSchema.js +12 -4
  102. package/lib/types/custom/MapSchema.js.map +1 -1
  103. package/lib/types/custom/SetSchema.d.ts +2 -2
  104. package/lib/types/custom/SetSchema.js +1 -0
  105. package/lib/types/custom/SetSchema.js.map +1 -1
  106. package/lib/types/registry.d.ts +8 -1
  107. package/lib/types/registry.js +23 -6
  108. package/lib/types/registry.js.map +1 -1
  109. package/lib/types/symbols.d.ts +8 -5
  110. package/lib/types/symbols.js +9 -6
  111. package/lib/types/symbols.js.map +1 -1
  112. package/lib/types/utils.js +1 -2
  113. package/lib/types/utils.js.map +1 -1
  114. package/lib/utils.js +9 -7
  115. package/lib/utils.js.map +1 -1
  116. package/package.json +19 -18
  117. package/src/Metadata.ts +190 -42
  118. package/src/Reflection.ts +76 -38
  119. package/src/Schema.ts +72 -70
  120. package/src/annotations.ts +156 -202
  121. package/src/bench_encode.ts +108 -0
  122. package/src/codegen/languages/csharp.ts +8 -47
  123. package/src/codegen/languages/haxe.ts +4 -0
  124. package/src/codegen/languages/lua.ts +19 -27
  125. package/src/codegen/parser.ts +107 -0
  126. package/src/codegen/types.ts +1 -0
  127. package/src/debug.ts +55 -0
  128. package/src/decoder/DecodeOperation.ts +43 -15
  129. package/src/decoder/Decoder.ts +12 -10
  130. package/src/decoder/ReferenceTracker.ts +5 -3
  131. package/src/decoder/strategy/StateCallbacks.ts +152 -81
  132. package/src/encoder/ChangeTree.ts +282 -209
  133. package/src/encoder/EncodeOperation.ts +78 -78
  134. package/src/encoder/Encoder.ts +157 -93
  135. package/src/encoder/Root.ts +93 -0
  136. package/src/encoder/StateView.ts +80 -88
  137. package/src/encoding/assert.ts +17 -8
  138. package/src/encoding/decode.ts +73 -93
  139. package/src/encoding/encode.ts +76 -45
  140. package/src/encoding/spec.ts +3 -5
  141. package/src/index.ts +12 -20
  142. package/src/types/HelperTypes.ts +54 -2
  143. package/src/types/TypeContext.ts +175 -0
  144. package/src/types/custom/ArraySchema.ts +49 -19
  145. package/src/types/custom/CollectionSchema.ts +1 -0
  146. package/src/types/custom/MapSchema.ts +30 -17
  147. package/src/types/custom/SetSchema.ts +1 -0
  148. package/src/types/registry.ts +22 -3
  149. package/src/types/symbols.ts +10 -7
  150. package/src/utils.ts +7 -3
@@ -1,17 +1,15 @@
1
1
  import { OPERATION } from "../encoding/spec";
2
- import { $changes } from "../types/symbols";
3
- import { getType } from "../types/registry";
2
+ import { $changes, $childType, $getByIndex } from "../types/symbols";
4
3
 
5
- import * as encode from "../encoding/encode";
6
- import { EncodeSchemaError, assertInstanceType, assertType } from "../encoding/assert";
4
+ import { encode } from "../encoding/encode";
7
5
 
8
6
  import type { ChangeTree, Ref } from "./ChangeTree";
9
7
  import type { Encoder } from "./Encoder";
10
8
  import type { Schema } from "../Schema";
11
- import type { PrimitiveType } from "../annotations";
12
9
 
13
10
  import type { Iterator } from "../encoding/decode";
14
11
  import type { ArraySchema } from "../types/custom/ArraySchema";
12
+ import type { Metadata } from "../Metadata";
15
13
 
16
14
  export type EncodeOperation<T extends Ref = any> = (
17
15
  encoder: Encoder,
@@ -22,43 +20,21 @@ export type EncodeOperation<T extends Ref = any> = (
22
20
  it: Iterator,
23
21
  isEncodeAll: boolean,
24
22
  hasView: boolean,
23
+ metadata?: Metadata,
25
24
  ) => void;
26
25
 
27
- export function encodePrimitiveType(
28
- type: PrimitiveType,
29
- bytes: Buffer,
30
- value: any,
31
- klass: Schema,
32
- field: string | number,
33
- it: Iterator,
34
- ) {
35
- assertType(value, type as string, klass, field);
36
-
37
- const encodeFunc = encode[type as string];
38
-
39
- if (encodeFunc) {
40
- encodeFunc(bytes, value, it);
41
- // encodeFunc(bytes, value);
42
-
43
- } else {
44
- throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`);
45
- }
46
- };
47
-
48
26
  export function encodeValue(
49
27
  encoder: Encoder,
50
28
  bytes: Buffer,
51
- ref: Ref,
52
29
  type: any,
53
30
  value: any,
54
- field: string | number,
55
31
  operation: OPERATION,
56
32
  it: Iterator,
57
33
  ) {
58
- if (type[Symbol.metadata] !== undefined) {
59
- // TODO: move this to the `@type()` annotation
60
- assertInstanceType(value, type as typeof Schema, ref as Schema, field);
34
+ if (typeof (type) === "string") {
35
+ encode[type]?.(bytes, value, it);
61
36
 
37
+ } else if (type[Symbol.metadata] !== undefined) {
62
38
  //
63
39
  // Encode refId for this instance.
64
40
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -70,23 +46,7 @@ export function encodeValue(
70
46
  encoder.tryEncodeTypeId(bytes, type as typeof Schema, value.constructor as typeof Schema, it);
71
47
  }
72
48
 
73
- } else if (typeof (type) === "string") {
74
- //
75
- // Primitive values
76
- //
77
- encodePrimitiveType(type as PrimitiveType, bytes, value, ref as Schema, field, it);
78
-
79
49
  } else {
80
- //
81
- // Custom type (MapSchema, ArraySchema, etc)
82
- //
83
- const definition = getType(Object.keys(type)[0]);
84
-
85
- //
86
- // ensure a ArraySchema has been provided
87
- //
88
- assertInstanceType(ref[field], definition.constructor, ref as Schema, field);
89
-
90
50
  //
91
51
  // Encode refId for this instance.
92
52
  // The actual instance is going to be encoded on next `changeTree` iteration.
@@ -106,14 +66,10 @@ export const encodeSchemaOperation: EncodeOperation = function (
106
66
  index: number,
107
67
  operation: OPERATION,
108
68
  it: Iterator,
69
+ _: any,
70
+ __: any,
71
+ metadata: Metadata,
109
72
  ) {
110
- const ref = changeTree.ref;
111
- const metadata = ref['constructor'][Symbol.metadata];
112
-
113
- const field = metadata[index];
114
- const type = metadata[field].type;
115
- const value = ref[field];
116
-
117
73
  // "compress" field index + operation
118
74
  bytes[it.offset++] = (index | operation) & 255;
119
75
 
@@ -122,8 +78,18 @@ export const encodeSchemaOperation: EncodeOperation = function (
122
78
  return;
123
79
  }
124
80
 
81
+ const ref = changeTree.ref;
82
+ const field = metadata[index];
83
+
125
84
  // TODO: inline this function call small performance gain
126
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
85
+ encodeValue(
86
+ encoder,
87
+ bytes,
88
+ metadata[index].type,
89
+ ref[field.name],
90
+ operation,
91
+ it
92
+ );
127
93
  }
128
94
 
129
95
  /**
@@ -134,12 +100,10 @@ export const encodeKeyValueOperation: EncodeOperation = function (
134
100
  encoder: Encoder,
135
101
  bytes: Buffer,
136
102
  changeTree: ChangeTree,
137
- field: number,
103
+ index: number,
138
104
  operation: OPERATION,
139
105
  it: Iterator,
140
106
  ) {
141
- const ref = changeTree.ref;
142
-
143
107
  // encode operation
144
108
  bytes[it.offset++] = operation & 255;
145
109
 
@@ -149,31 +113,53 @@ export const encodeKeyValueOperation: EncodeOperation = function (
149
113
  }
150
114
 
151
115
  // encode index
152
- encode.number(bytes, field, it);
116
+ encode.number(bytes, index, it);
153
117
 
154
118
  // Do not encode value for DELETE operations
155
119
  if (operation === OPERATION.DELETE) {
156
120
  return;
157
121
  }
158
122
 
123
+ const ref = changeTree.ref;
124
+
159
125
  //
160
126
  // encode "alias" for dynamic fields (maps)
161
127
  //
162
- if ((operation & OPERATION.ADD) == OPERATION.ADD) { // ADD or DELETE_AND_ADD
128
+ if ((operation & OPERATION.ADD) === OPERATION.ADD) { // ADD or DELETE_AND_ADD
163
129
  if (typeof(ref['set']) === "function") {
164
130
  //
165
131
  // MapSchema dynamic key
166
132
  //
167
- const dynamicIndex = changeTree.ref['$indexes'].get(field);
133
+ const dynamicIndex = changeTree.ref['$indexes'].get(index);
168
134
  encode.string(bytes, dynamicIndex, it);
169
135
  }
170
136
  }
171
137
 
172
- const type = changeTree.getType(field);
173
- const value = changeTree.getValue(field);
138
+ const type = ref[$childType];
139
+ const value = ref[$getByIndex](index);
140
+
141
+ // try { throw new Error(); } catch (e) {
142
+ // // only print if not coming from Reflection.ts
143
+ // if (!e.stack.includes("src/Reflection.ts")) {
144
+ // console.log("encodeKeyValueOperation -> ", {
145
+ // ref: changeTree.ref.constructor.name,
146
+ // field,
147
+ // operation: OPERATION[operation],
148
+ // value: value?.toJSON(),
149
+ // items: ref.toJSON(),
150
+ // });
151
+ // }
152
+ // }
174
153
 
175
154
  // TODO: inline this function call small performance gain
176
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
155
+ encodeValue(
156
+ encoder,
157
+ bytes,
158
+ type,
159
+ value,
160
+ operation,
161
+ it
162
+ );
177
163
  }
178
164
 
179
165
  /**
@@ -191,30 +177,37 @@ export const encodeArray: EncodeOperation = function (
191
177
  hasView: boolean,
192
178
  ) {
193
179
  const ref = changeTree.ref;
180
+ const useOperationByRefId = hasView && changeTree.isFiltered && (typeof (changeTree.getType(field)) !== "string");
194
181
 
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;
182
+ let refOrIndex: number;
183
+
184
+ if (useOperationByRefId) {
185
+ refOrIndex = ref['tmpItems'][field][$changes].refId;
186
+
187
+ if (operation === OPERATION.DELETE) {
188
+ operation = OPERATION.DELETE_BY_REFID;
189
+
190
+ } else if (operation === OPERATION.ADD) {
191
+ operation = OPERATION.ADD_BY_REFID;
192
+ }
193
+
194
+ } else {
195
+ refOrIndex = field;
206
196
  }
207
197
 
208
198
  // encode operation
209
199
  bytes[it.offset++] = operation & 255;
210
200
 
211
201
  // custom operations
212
- if (operation === OPERATION.CLEAR) {
202
+ if (
203
+ operation === OPERATION.CLEAR ||
204
+ operation === OPERATION.REVERSE
205
+ ) {
213
206
  return;
214
207
  }
215
208
 
216
209
  // encode index
217
- encode.number(bytes, field, it);
210
+ encode.number(bytes, refOrIndex, it);
218
211
 
219
212
  // Do not encode value for DELETE operations
220
213
  if (operation === OPERATION.DELETE) {
@@ -233,5 +226,12 @@ export const encodeArray: EncodeOperation = function (
233
226
  // });
234
227
 
235
228
  // TODO: inline this function call small performance gain
236
- encodeValue(encoder, bytes, ref, type, value, field, operation, it);
229
+ encodeValue(
230
+ encoder,
231
+ bytes,
232
+ type,
233
+ value,
234
+ operation,
235
+ it
236
+ );
237
237
  }
@@ -1,32 +1,35 @@
1
1
  import type { Schema } from "../Schema";
2
- import { TypeContext } from "../annotations";
3
- import { $changes, $encoder, $filter } from "../types/symbols";
2
+ import { TypeContext } from "../types/TypeContext";
3
+ import { $changes, $encoder, $filter, $onEncodeEnd } from "../types/symbols";
4
4
 
5
- import * as encode from "../encoding/encode";
5
+ import { encode } from "../encoding/encode";
6
6
  import type { Iterator } from "../encoding/decode";
7
7
 
8
- import { SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec';
9
- import { Root } from "./ChangeTree";
8
+ import { OPERATION, SWITCH_TO_STRUCTURE, TYPE_ID } from '../encoding/spec';
9
+ import { Root } from "./Root";
10
10
  import { getNextPowerOf2 } from "../utils";
11
+
11
12
  import type { StateView } from "./StateView";
13
+ import type { Metadata } from "../Metadata";
12
14
 
13
15
  export class Encoder<T extends Schema = any> {
14
- static BUFFER_SIZE = 8 * 1024;// 8KB
15
- sharedBuffer = Buffer.allocUnsafeSlow(Encoder.BUFFER_SIZE);
16
+ static BUFFER_SIZE = Buffer.poolSize ?? 8 * 1024; // 8KB
17
+ sharedBuffer = Buffer.allocUnsafe(Encoder.BUFFER_SIZE);
16
18
 
17
19
  context: TypeContext;
18
20
  state: T;
19
21
 
20
22
  root: Root;
21
23
 
22
- constructor(root: T) {
23
- this.setRoot(root);
24
-
24
+ constructor(state: T) {
25
25
  //
26
26
  // TODO: cache and restore "Context" based on root schema
27
27
  // (to avoid creating a new context for every new room)
28
28
  //
29
- this.context = new TypeContext(root.constructor as typeof Schema);
29
+ this.context = new TypeContext(state.constructor as typeof Schema);
30
+ this.root = new Root(this.context);
31
+
32
+ this.setState(state);
30
33
 
31
34
  // console.log(">>>>>>>>>>>>>>>> Encoder types");
32
35
  // this.context.schemas.forEach((id, schema) => {
@@ -34,32 +37,35 @@ export class Encoder<T extends Schema = any> {
34
37
  // });
35
38
  }
36
39
 
37
- protected setRoot(state: T) {
38
- this.root = new Root();
40
+ protected setState(state: T) {
39
41
  this.state = state;
40
- state[$changes].setRoot(this.root);
42
+ this.state[$changes].setRoot(this.root);
41
43
  }
42
44
 
43
45
  encode(
44
46
  it: Iterator = { offset: 0 },
45
47
  view?: StateView,
46
48
  buffer = this.sharedBuffer,
47
- changeTrees = this.root.changes
49
+ changeSetName: "changes" | "allChanges" | "filteredChanges" | "allFilteredChanges" = "changes",
50
+ isEncodeAll = changeSetName === "allChanges",
51
+ initialOffset = it.offset // cache current offset in case we need to resize the buffer
48
52
  ): Buffer {
49
- const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
50
-
51
- const isEncodeAll = this.root.allChanges === changeTrees;
52
53
  const hasView = (view !== undefined);
53
54
  const rootChangeTree = this.state[$changes];
54
55
 
55
- const changeTreesIterator = changeTrees.entries();
56
+ const shouldDiscardChanges = !isEncodeAll && !hasView;
57
+ const changeTrees = this.root[changeSetName];
58
+
59
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
60
+ const changeTree = changeTrees[i];
56
61
 
57
- for (const [changeTree, changes] of changeTreesIterator) {
62
+ const operations = changeTree[changeSetName];
58
63
  const ref = changeTree.ref;
59
64
 
60
- const ctor = ref['constructor'];
65
+ const ctor = ref.constructor;
61
66
  const encoder = ctor[$encoder];
62
67
  const filter = ctor[$filter];
68
+ const metadata = ctor[Symbol.metadata];
63
69
 
64
70
  if (hasView) {
65
71
  if (!view.items.has(changeTree)) {
@@ -72,14 +78,21 @@ export class Encoder<T extends Schema = any> {
72
78
  }
73
79
 
74
80
  // skip root `refId` if it's the first change tree
75
- if (it.offset !== initialOffset || changeTree !== rootChangeTree) {
81
+ // (unless it "hasView", which will need to revisit the root)
82
+ if (hasView || it.offset > initialOffset || changeTree !== rootChangeTree) {
76
83
  buffer[it.offset++] = SWITCH_TO_STRUCTURE & 255;
77
84
  encode.number(buffer, changeTree.refId, it);
78
85
  }
79
86
 
80
- const changesIterator = changes.entries();
87
+ for (let j = 0, numChanges = operations.operations.length; j < numChanges; j++) {
88
+ const fieldIndex = operations.operations[j];
89
+
90
+ const operation = (fieldIndex < 0)
91
+ ? Math.abs(fieldIndex) // "pure" operation without fieldIndex (e.g. CLEAR, REVERSE, etc.)
92
+ : (isEncodeAll)
93
+ ? OPERATION.ADD
94
+ : changeTree.indexedOperations[fieldIndex];
81
95
 
82
- for (const [fieldIndex, operation] of changesIterator) {
83
96
  //
84
97
  // first pass (encodeAll), identify "filtered" operations without encoding them
85
98
  // they will be encoded per client, based on their view.
@@ -87,122 +100,138 @@ export class Encoder<T extends Schema = any> {
87
100
  // TODO: how can we optimize filtering out "encode all" operations?
88
101
  // TODO: avoid checking if no view tags were defined
89
102
  //
90
- if (filter && !filter(ref, fieldIndex, view)) {
91
- // console.log("SKIP FIELD:", { ref: changeTree.ref.constructor.name, fieldIndex, })
92
-
103
+ if (fieldIndex === undefined || operation === undefined || (filter && !filter(ref, fieldIndex, view))) {
93
104
  // console.log("ADD AS INVISIBLE:", fieldIndex, changeTree.ref.constructor.name)
94
105
  // view?.invisible.add(changeTree);
95
106
  continue;
96
107
  }
97
108
 
98
- // console.log("WILL ENCODE", {
99
- // ref: changeTree.ref.constructor.name,
100
- // fieldIndex,
101
- // operation: OPERATION[operation],
102
- // });
103
-
104
- encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
109
+ encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView, metadata);
105
110
  }
111
+
112
+ // if (shouldDiscardChanges) {
113
+ // changeTree.discard();
114
+ // changeTree.isNew = false; // Not a new instance anymore
115
+ // }
106
116
  }
107
117
 
108
118
  if (it.offset > buffer.byteLength) {
109
- const newSize = getNextPowerOf2(buffer.byteLength * 2);
110
- console.warn("@colyseus/schema encode buffer overflow. Current buffer size: " + buffer.byteLength + ", encoding offset: " + it.offset + ", new size: " + newSize);
119
+ // we can assume that n + 1 poolSize will suffice given that we are likely done with encoding at this point
120
+ // multiples of poolSize are faster to allocate than arbitrary sizes
121
+ // if we are on an older platform that doesn't implement pooling use 8kb as poolSize (that's the default for node)
122
+ const newSize = Math.ceil(it.offset / (Buffer.poolSize ?? 8 * 1024)) * (Buffer.poolSize ?? 8 * 1024);
123
+
124
+ console.warn(`@colyseus/schema buffer overflow. Encoded state is higher than default BUFFER_SIZE. Use the following to increase default BUFFER_SIZE:
125
+
126
+ import { Encoder } from "@colyseus/schema";
127
+ Encoder.BUFFER_SIZE = ${Math.round(newSize / 1024)} * 1024; // ${Math.round(newSize / 1024)} KB
128
+ `);
111
129
 
112
130
  //
113
131
  // resize buffer and re-encode (TODO: can we avoid re-encoding here?)
132
+ // -> No we probably can't unless we catch the need for resize before encoding which is likely more computationally expensive than resizing on demand
114
133
  //
115
- buffer = Buffer.allocUnsafeSlow(newSize);
134
+ buffer = Buffer.alloc(newSize, buffer); // fill with buffer here to memcpy previous encoding steps beyond the initialOffset
116
135
 
117
136
  // assign resized buffer to local sharedBuffer
118
137
  if (buffer === this.sharedBuffer) {
119
138
  this.sharedBuffer = buffer;
120
139
  }
121
140
 
122
- return this.encode({ offset: initialOffset }, view, buffer);
141
+ return this.encode({ offset: initialOffset }, view, buffer, changeSetName, isEncodeAll);
123
142
 
124
143
  } else {
125
144
  //
126
145
  // only clear changes after making sure buffer resize is not required.
127
146
  //
128
- if (!isEncodeAll && !hasView) {
147
+ if (shouldDiscardChanges) {
129
148
  //
130
- // FIXME: avoid iterating over change trees twice.
149
+ // TODO: avoid iterating over change trees twice.
131
150
  //
132
- this.onEndEncode(changeTrees);
151
+ for (let i = 0, numChangeTrees = changeTrees.length; i < numChangeTrees; i++) {
152
+ const changeTree = changeTrees[i];
153
+ changeTree.discard();
154
+ changeTree.isNew = false; // Not a new instance anymore
155
+ }
133
156
  }
134
157
 
135
- // return bytes;
136
- return buffer.slice(0, it.offset);
158
+ return buffer.subarray(0, it.offset);
137
159
  }
138
160
  }
139
161
 
140
162
  encodeAll(it: Iterator = { offset: 0 }, buffer: Buffer = this.sharedBuffer) {
141
- // console.log(`encodeAll(), this.$root.allChanges (${this.$root.allChanges.size})`);
142
-
143
- // Array.from(this.$root.allChanges.entries()).map((item) => {
144
- // console.log("->", item[0].refId, item[0].ref.toJSON());
145
- // });
146
-
147
- return this.encode(it, undefined, buffer, this.root.allChanges);
163
+ return this.encode(it, undefined, buffer, "allChanges", true);
148
164
  }
149
165
 
150
166
  encodeAllView(view: StateView, sharedOffset: number, it: Iterator, bytes = this.sharedBuffer) {
151
167
  const viewOffset = it.offset;
152
168
 
153
- // console.log(`encodeAllView(), this.$root.allFilteredChanges (${this.$root.allFilteredChanges.size})`);
154
- // this.debugAllFilteredChanges();
155
-
156
169
  // try to encode "filtered" changes
157
- this.encode(it, view, bytes, this.root.allFilteredChanges);
170
+ this.encode(it, view, bytes, "allFilteredChanges", true, viewOffset);
158
171
 
159
172
  return Buffer.concat([
160
- bytes.slice(0, sharedOffset),
161
- bytes.slice(viewOffset, it.offset)
173
+ bytes.subarray(0, sharedOffset),
174
+ bytes.subarray(viewOffset, it.offset)
162
175
  ]);
163
176
  }
164
177
 
165
-
166
- // debugAllFilteredChanges() {
167
- // Array.from(this.$root.allFilteredChanges.entries()).map((item) => {
168
- // console.log("->", { refId: item[0].refId }, item[0].ref.toJSON());
169
- // if (Array.isArray(item[0].ref.toJSON())) {
170
- // item[1].forEach((op, key) => {
171
- // console.log(" ->", { key, op: OPERATION[op] });
172
- // })
173
- // }
174
- // });
175
- // }
178
+ debugChanges(field: "changes" | "allFilteredChanges" | "allChanges" | "filteredChanges") {
179
+ const rootChangeSet = (typeof (field) === "string")
180
+ ? this.root[field]
181
+ : field;
182
+
183
+ rootChangeSet.forEach((changeTree) => {
184
+ const changeSet = changeTree[field];
185
+
186
+ const metadata: Metadata = changeTree.ref.constructor[Symbol.metadata];
187
+ console.log("->", { ref: changeTree.ref.constructor.name, refId: changeTree.refId, changes: Object.keys(changeSet).length });
188
+ for (const index in changeSet) {
189
+ const op = changeSet[index];
190
+ console.log(" ->", {
191
+ index,
192
+ field: metadata?.[index],
193
+ op: OPERATION[op],
194
+ });
195
+ }
196
+ });
197
+ }
176
198
 
177
199
  encodeView(view: StateView, sharedOffset: number, it: Iterator, bytes = this.sharedBuffer) {
178
200
  const viewOffset = it.offset;
179
201
 
180
- // try to encode "filtered" changes
181
- this.encode(it, view, bytes, this.root.filteredChanges);
182
-
183
202
  // encode visibility changes (add/remove for this view)
184
- const viewChangesIterator = view.changes.entries();
185
- for (const [changeTree, changes] of viewChangesIterator) {
186
- if (changes.size === 0) {
187
- // FIXME: avoid having empty changes if no changes were made
188
- // console.log("changes.size === 0", changeTree.ref.constructor.name);
203
+ const refIds = Object.keys(view.changes);
204
+
205
+ for (let i = 0, numRefIds = refIds.length; i < numRefIds; i++) {
206
+ const refId = refIds[i];
207
+ const changes = view.changes[refId];
208
+ const changeTree = this.root.changeTrees[refId];
209
+
210
+ if (
211
+ changeTree === undefined ||
212
+ Object.keys(changes).length === 0 // FIXME: avoid having empty changes if no changes were made
213
+ ) {
214
+ // console.log("changes.size === 0, skip", changeTree.ref.constructor.name);
189
215
  continue;
190
216
  }
191
217
 
192
218
  const ref = changeTree.ref;
193
219
 
194
- const ctor = ref['constructor'];
220
+ const ctor = ref.constructor;
195
221
  const encoder = ctor[$encoder];
222
+ const metadata = ctor[Symbol.metadata];
196
223
 
197
224
  bytes[it.offset++] = SWITCH_TO_STRUCTURE & 255;
198
225
  encode.number(bytes, changeTree.refId, it);
199
226
 
200
- const changesIterator = changes.entries();
227
+ const keys = Object.keys(changes);
228
+ for (let i = 0, numChanges = keys.length; i < numChanges; i++) {
229
+ const key = keys[i];
230
+ const operation = changes[key];
201
231
 
202
- for (const [fieldIndex, operation] of changesIterator) {
203
232
  // isEncodeAll = false
204
233
  // hasView = true
205
- encoder(this, bytes, changeTree, fieldIndex, operation, it, false, true);
234
+ encoder(this, bytes, changeTree, Number(key), operation, it, false, true, metadata);
206
235
  }
207
236
  }
208
237
 
@@ -211,31 +240,54 @@ export class Encoder<T extends Schema = any> {
211
240
  // (to allow re-using StateView's for multiple clients)
212
241
  //
213
242
  // clear "view" changes after encoding
214
- view.changes.clear();
243
+ view.changes = {};
244
+
245
+ // try to encode "filtered" changes
246
+ this.encode(it, view, bytes, "filteredChanges", false, viewOffset);
215
247
 
216
248
  return Buffer.concat([
217
- bytes.slice(0, sharedOffset),
218
- bytes.slice(viewOffset, it.offset)
249
+ bytes.subarray(0, sharedOffset),
250
+ bytes.subarray(viewOffset, it.offset)
219
251
  ]);
220
252
  }
221
253
 
222
254
  onEndEncode(changeTrees = this.root.changes) {
223
- const changeTreesIterator = changeTrees.entries();
224
- for (const [changeTree, _] of changeTreesIterator) {
225
- changeTree.endEncode();
226
- }
255
+ // changeTrees.forEach(function(changeTree) {
256
+ // changeTree.endEncode();
257
+ // });
258
+
259
+
260
+ // for (const refId in changeTrees) {
261
+ // const changeTree = this.root.changeTrees[refId];
262
+ // changeTree.endEncode();
263
+
264
+ // // changeTree.changes.clear();
265
+
266
+ // // // ArraySchema and MapSchema have a custom "encode end" method
267
+ // // changeTree.ref[$onEncodeEnd]?.();
268
+
269
+ // // // Not a new instance anymore
270
+ // // delete changeTree[$isNew];
271
+ // }
227
272
  }
228
273
 
229
274
  discardChanges() {
230
275
  // discard shared changes
231
- if (this.root.changes.size > 0) {
232
- this.onEndEncode(this.root.changes);
233
- this.root.changes.clear();
276
+ let length = this.root.changes.length;
277
+ if (length > 0) {
278
+ while (length--) {
279
+ this.root.changes[length]?.endEncode();
280
+ }
281
+ this.root.changes.length = 0;
234
282
  }
283
+
235
284
  // discard filtered changes
236
- if (this.root.filteredChanges.size > 0) {
237
- this.onEndEncode(this.root.filteredChanges);
238
- this.root.filteredChanges.clear();
285
+ length = this.root.filteredChanges.length;
286
+ if (length > 0) {
287
+ while (length--) {
288
+ this.root.filteredChanges[length]?.endEncode();
289
+ }
290
+ this.root.filteredChanges.length = 0;
239
291
  }
240
292
  }
241
293
 
@@ -243,9 +295,21 @@ export class Encoder<T extends Schema = any> {
243
295
  const baseTypeId = this.context.getTypeId(baseType);
244
296
  const targetTypeId = this.context.getTypeId(targetType);
245
297
 
298
+ if (targetTypeId === undefined) {
299
+ 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.`);
300
+ return;
301
+ }
302
+
246
303
  if (baseTypeId !== targetTypeId) {
247
304
  bytes[it.offset++] = TYPE_ID & 255;
248
305
  encode.number(bytes, targetTypeId, it);
249
306
  }
250
307
  }
251
- }
308
+
309
+ get hasChanges() {
310
+ return (
311
+ this.root.changes.length > 0 ||
312
+ this.root.filteredChanges.length > 0
313
+ );
314
+ }
315
+ }