@colyseus/schema 3.0.75 → 4.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 (157) hide show
  1. package/build/cjs/index.js +780 -429
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/index.mjs +778 -430
  4. package/build/esm/index.mjs.map +1 -1
  5. package/build/umd/index.js +780 -429
  6. package/lib/Reflection.d.ts +50 -17
  7. package/lib/Reflection.js +151 -202
  8. package/lib/Reflection.js.map +1 -1
  9. package/lib/Schema.d.ts +13 -1
  10. package/lib/Schema.js +73 -9
  11. package/lib/Schema.js.map +1 -1
  12. package/lib/annotations.d.ts +6 -1
  13. package/lib/annotations.js +8 -34
  14. package/lib/annotations.js.map +1 -1
  15. package/lib/bench_encode.js +34 -1
  16. package/lib/bench_encode.js.map +1 -1
  17. package/lib/codegen/api.js +35 -2
  18. package/lib/codegen/api.js.map +1 -1
  19. package/lib/codegen/cli.js +4 -1
  20. package/lib/codegen/cli.js.map +1 -1
  21. package/lib/codegen/parser.js +35 -2
  22. package/lib/codegen/parser.js.map +1 -1
  23. package/lib/codegen/types.js +34 -1
  24. package/lib/codegen/types.js.map +1 -1
  25. package/lib/decoder/DecodeOperation.d.ts +2 -2
  26. package/lib/decoder/DecodeOperation.js +3 -3
  27. package/lib/decoder/DecodeOperation.js.map +1 -1
  28. package/lib/decoder/Decoder.d.ts +3 -3
  29. package/lib/decoder/Decoder.js +2 -2
  30. package/lib/decoder/Decoder.js.map +1 -1
  31. package/lib/decoder/ReferenceTracker.d.ts +0 -1
  32. package/lib/decoder/ReferenceTracker.js +9 -7
  33. package/lib/decoder/ReferenceTracker.js.map +1 -1
  34. package/lib/decoder/strategy/Callbacks.d.ts +154 -0
  35. package/lib/decoder/strategy/Callbacks.js +340 -0
  36. package/lib/decoder/strategy/Callbacks.js.map +1 -0
  37. package/lib/decoder/strategy/{StateCallbacks.d.ts → getDecoderStateCallbacks.d.ts} +6 -0
  38. package/lib/decoder/strategy/{StateCallbacks.js → getDecoderStateCallbacks.js} +17 -10
  39. package/lib/decoder/strategy/getDecoderStateCallbacks.js.map +1 -0
  40. package/lib/encoder/ChangeTree.d.ts +2 -2
  41. package/lib/encoder/ChangeTree.js.map +1 -1
  42. package/lib/encoder/EncodeOperation.d.ts +2 -2
  43. package/lib/encoder/EncodeOperation.js +3 -3
  44. package/lib/encoder/EncodeOperation.js.map +1 -1
  45. package/lib/encoder/Encoder.d.ts +6 -6
  46. package/lib/encoder/Encoder.js +19 -18
  47. package/lib/encoder/Encoder.js.map +1 -1
  48. package/lib/encoder/Root.js +17 -14
  49. package/lib/encoder/Root.js.map +1 -1
  50. package/lib/encoder/StateView.js +13 -12
  51. package/lib/encoder/StateView.js.map +1 -1
  52. package/lib/encoding/decode.d.ts +2 -2
  53. package/lib/encoding/encode.d.ts +3 -1
  54. package/lib/encoding/encode.js.map +1 -1
  55. package/lib/index.d.ts +3 -2
  56. package/lib/index.js +7 -3
  57. package/lib/index.js.map +1 -1
  58. package/lib/types/HelperTypes.d.ts +7 -14
  59. package/lib/types/HelperTypes.js.map +1 -1
  60. package/lib/types/custom/ArraySchema.d.ts +2 -1
  61. package/lib/types/custom/ArraySchema.js.map +1 -1
  62. package/lib/types/custom/CollectionSchema.d.ts +2 -1
  63. package/lib/types/custom/CollectionSchema.js.map +1 -1
  64. package/lib/types/custom/MapSchema.d.ts +3 -2
  65. package/lib/types/custom/MapSchema.js.map +1 -1
  66. package/lib/types/custom/SetSchema.d.ts +2 -1
  67. package/lib/types/custom/SetSchema.js.map +1 -1
  68. package/lib/types/symbols.d.ts +1 -0
  69. package/lib/types/symbols.js +2 -1
  70. package/lib/types/symbols.js.map +1 -1
  71. package/lib/utils.js +1 -1
  72. package/lib/utils.js.map +1 -1
  73. package/package.json +12 -16
  74. package/src/Reflection.ts +185 -174
  75. package/src/Schema.ts +81 -13
  76. package/src/annotations.ts +14 -40
  77. package/src/codegen/parser.ts +1 -1
  78. package/src/decoder/DecodeOperation.ts +9 -9
  79. package/src/decoder/Decoder.ts +6 -6
  80. package/src/decoder/ReferenceTracker.ts +10 -8
  81. package/src/decoder/strategy/Callbacks.ts +547 -0
  82. package/src/decoder/strategy/{StateCallbacks.ts → getDecoderStateCallbacks.ts} +17 -11
  83. package/src/encoder/ChangeTree.ts +4 -7
  84. package/src/encoder/EncodeOperation.ts +9 -9
  85. package/src/encoder/Encoder.ts +26 -18
  86. package/src/encoder/Root.ts +20 -15
  87. package/src/encoder/StateView.ts +15 -13
  88. package/src/encoding/encode.ts +1 -1
  89. package/src/index.ts +3 -2
  90. package/src/types/HelperTypes.ts +13 -11
  91. package/src/types/custom/ArraySchema.ts +2 -1
  92. package/src/types/custom/CollectionSchema.ts +4 -2
  93. package/src/types/custom/MapSchema.ts +4 -2
  94. package/src/types/custom/SetSchema.ts +3 -1
  95. package/src/types/symbols.ts +1 -0
  96. package/src/utils.ts +2 -2
  97. package/lib/Decoder.d.ts +0 -16
  98. package/lib/Decoder.js +0 -182
  99. package/lib/Decoder.js.map +0 -1
  100. package/lib/Encoder.d.ts +0 -13
  101. package/lib/Encoder.js +0 -79
  102. package/lib/Encoder.js.map +0 -1
  103. package/lib/changes/ChangeSet.d.ts +0 -12
  104. package/lib/changes/ChangeSet.js +0 -35
  105. package/lib/changes/ChangeSet.js.map +0 -1
  106. package/lib/changes/ChangeTree.d.ts +0 -53
  107. package/lib/changes/ChangeTree.js +0 -202
  108. package/lib/changes/ChangeTree.js.map +0 -1
  109. package/lib/changes/DecodeOperation.d.ts +0 -15
  110. package/lib/changes/DecodeOperation.js +0 -186
  111. package/lib/changes/DecodeOperation.js.map +0 -1
  112. package/lib/changes/EncodeOperation.d.ts +0 -18
  113. package/lib/changes/EncodeOperation.js +0 -130
  114. package/lib/changes/EncodeOperation.js.map +0 -1
  115. package/lib/changes/ReferenceTracker.d.ts +0 -14
  116. package/lib/changes/ReferenceTracker.js +0 -83
  117. package/lib/changes/ReferenceTracker.js.map +0 -1
  118. package/lib/changes/consts.d.ts +0 -14
  119. package/lib/changes/consts.js +0 -18
  120. package/lib/changes/consts.js.map +0 -1
  121. package/lib/decoder/strategy/StateCallbacks.js.map +0 -1
  122. package/lib/decoding/decode.d.ts +0 -48
  123. package/lib/decoding/decode.js +0 -267
  124. package/lib/decoding/decode.js.map +0 -1
  125. package/lib/ecs.d.ts +0 -11
  126. package/lib/ecs.js +0 -160
  127. package/lib/ecs.js.map +0 -1
  128. package/lib/filters/index.d.ts +0 -8
  129. package/lib/filters/index.js +0 -24
  130. package/lib/filters/index.js.map +0 -1
  131. package/lib/spec.d.ts +0 -13
  132. package/lib/spec.js +0 -42
  133. package/lib/spec.js.map +0 -1
  134. package/lib/types/ArraySchema.d.ts +0 -238
  135. package/lib/types/ArraySchema.js +0 -555
  136. package/lib/types/ArraySchema.js.map +0 -1
  137. package/lib/types/CollectionSchema.d.ts +0 -35
  138. package/lib/types/CollectionSchema.js +0 -150
  139. package/lib/types/CollectionSchema.js.map +0 -1
  140. package/lib/types/MapSchema.d.ts +0 -38
  141. package/lib/types/MapSchema.js +0 -215
  142. package/lib/types/MapSchema.js.map +0 -1
  143. package/lib/types/SetSchema.d.ts +0 -32
  144. package/lib/types/SetSchema.js +0 -162
  145. package/lib/types/SetSchema.js.map +0 -1
  146. package/lib/types/typeRegistry.d.ts +0 -5
  147. package/lib/types/typeRegistry.js +0 -13
  148. package/lib/types/typeRegistry.js.map +0 -1
  149. package/lib/usage.d.ts +0 -1
  150. package/lib/usage.js +0 -22
  151. package/lib/usage.js.map +0 -1
  152. package/lib/v3.d.ts +0 -1
  153. package/lib/v3.js +0 -427
  154. package/lib/v3.js.map +0 -1
  155. package/lib/v3_experiment.d.ts +0 -1
  156. package/lib/v3_experiment.js +0 -407
  157. package/lib/v3_experiment.js.map +0 -1
@@ -0,0 +1,547 @@
1
+ import { Metadata } from "../../Metadata";
2
+ import { Collection, NonFunctionPropNames } from "../../types/HelperTypes";
3
+ import { Ref } from "../../encoder/ChangeTree";
4
+ import { Decoder } from "../Decoder";
5
+ import { DataChange } from "../DecodeOperation";
6
+ import { OPERATION } from "../../encoding/spec";
7
+ import { Schema } from "../../Schema";
8
+ import { $refId } from "../../types/symbols";
9
+ import { MapSchema } from "../../types/custom/MapSchema";
10
+ import { ArraySchema } from "../../types/custom/ArraySchema";
11
+ import { getDecoderStateCallbacks, type SchemaCallbackProxy } from "./getDecoderStateCallbacks";
12
+ import { getRawChangesCallback } from "./RawChanges";
13
+
14
+ //
15
+ // C#-style Callbacks API
16
+ // Matches the API from: https://docs.colyseus.io/state/callbacks
17
+ //
18
+ // Key features:
19
+ // - Uses string property names with TypeScript auto-completion
20
+ // - Consistent parameter order (key, value) for collection callbacks
21
+ // - Overloaded methods for nested instance callbacks
22
+ //
23
+
24
+ type PropertyChangeCallback<K> = (currentValue: K, previousValue: K) => void;
25
+ type KeyValueCallback<K, V> = (key: K, value: V) => void;
26
+ type InstanceChangeCallback = () => void;
27
+
28
+ // Exclude internal properties from valid property names
29
+ type PublicPropNames<T> = Exclude<NonFunctionPropNames<T>, typeof $refId> & string;
30
+
31
+ // Extract only properties that extend Collection
32
+ type CollectionPropNames<T> = Exclude<{
33
+ [K in keyof T]: T[K] extends Collection<any, any> ? K : never
34
+ }[keyof T] & string, typeof $refId>;
35
+
36
+ // Infer the value type of a collection property
37
+ type CollectionValueType<T, K extends keyof T> =
38
+ T[K] extends MapSchema<infer V, any> ? V :
39
+ T[K] extends ArraySchema<infer V> ? V :
40
+ T[K] extends Collection<any, infer V, any> ? V : never;
41
+
42
+ // Infer the key type of a collection property
43
+ type CollectionKeyType<T, K extends keyof T> =
44
+ T[K] extends MapSchema<any, infer Key> ? Key :
45
+ T[K] extends ArraySchema<any> ? number :
46
+ T[K] extends Collection<infer Key, any, any> ? Key : never;
47
+
48
+ /**
49
+ * State Callbacks handler
50
+ *
51
+ * Usage:
52
+ * ```ts
53
+ * const $ = Callbacks.get(decoder);
54
+ *
55
+ * // Listen to property changes
56
+ * $.listen("currentTurn", (currentValue, previousValue) => { ... });
57
+ *
58
+ * // Listen to collection additions
59
+ * $.onAdd("entities", (sessionId, entity) => {
60
+ * // Nested property listening
61
+ * $.listen(entity, "hp", (currentHp, previousHp) => { ... });
62
+ * });
63
+ *
64
+ * // Listen to collection removals
65
+ * $.onRemove("entities", (sessionId, entity) => { ... });
66
+ *
67
+ * // Listen to any property change on an instance
68
+ * $.onChange(entity, () => { ... });
69
+ *
70
+ * // Bind properties to another object
71
+ * $.bindTo(player, playerVisual);
72
+ * ```
73
+ */
74
+ export class StateCallbackStrategy<TState extends Schema> {
75
+ protected decoder: Decoder<TState>;
76
+ protected uniqueRefIds: Set<number> = new Set();
77
+ protected isTriggering: boolean = false;
78
+
79
+ constructor(decoder: Decoder<TState>) {
80
+ this.decoder = decoder;
81
+ this.decoder.triggerChanges = this.triggerChanges.bind(this);
82
+ }
83
+
84
+ protected get callbacks() {
85
+ return this.decoder.root.callbacks;
86
+ }
87
+
88
+ protected get state() {
89
+ return this.decoder.state;
90
+ }
91
+
92
+ protected addCallback(
93
+ refId: number,
94
+ operationOrProperty: OPERATION | string,
95
+ handler: Function
96
+ ): () => void {
97
+ const $root = this.decoder.root;
98
+ return $root.addCallback(refId, operationOrProperty, handler);
99
+ }
100
+
101
+ protected addCallbackOrWaitCollectionAvailable<TInstance extends Schema, TReturn extends Ref>(
102
+ instance: TInstance,
103
+ propertyName: string,
104
+ operation: OPERATION,
105
+ handler: Function,
106
+ immediate: boolean = true
107
+ ): () => void {
108
+ let removeHandler: () => void = () => {};
109
+ const removeOnAdd = () => removeHandler();
110
+
111
+ const collection = (instance as any)[propertyName] as TReturn;
112
+
113
+ // Collection not available yet. Listen for its availability before attaching the handler.
114
+ if (collection === null || collection === undefined) {
115
+ removeHandler = this.addCallback(
116
+ instance[$refId],
117
+ propertyName,
118
+ (value: TReturn, _: TReturn) => {
119
+ if (value !== null && value !== undefined) {
120
+ removeHandler = this.addCallback(value[$refId], operation, handler);
121
+ }
122
+ }
123
+ );
124
+ return removeOnAdd;
125
+
126
+ } else {
127
+ //
128
+ // Call immediately if collection is already available, if it's an ADD operation.
129
+ //
130
+ immediate = immediate && this.isTriggering === false;
131
+
132
+ if (operation === OPERATION.ADD && immediate) {
133
+ (collection as Collection<any, any>).forEach((value: any, key: any) => {
134
+ handler(key, value);
135
+ });
136
+ }
137
+
138
+ return this.addCallback(collection[$refId], operation, handler);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Listen to property changes on the root state.
144
+ */
145
+ listen<K extends PublicPropNames<TState>>(
146
+ property: K,
147
+ handler: PropertyChangeCallback<TState[K]>,
148
+ immediate?: boolean
149
+ ): () => void;
150
+
151
+ /**
152
+ * Listen to property changes on a nested instance.
153
+ */
154
+ listen<TInstance extends Schema, K extends PublicPropNames<TInstance>>(
155
+ instance: TInstance,
156
+ property: K,
157
+ handler: PropertyChangeCallback<TInstance[K]>,
158
+ immediate?: boolean
159
+ ): () => void;
160
+
161
+ listen(...args: any[]): () => void {
162
+ if (typeof args[0] === 'string') {
163
+ // listen(property, handler, immediate?)
164
+ return this.listenInstance(this.state, args[0], args[1], args[2]);
165
+ } else {
166
+ // listen(instance, property, handler, immediate?)
167
+ return this.listenInstance(args[0], args[1], args[2], args[3]);
168
+ }
169
+ }
170
+
171
+ protected listenInstance<TInstance extends Schema>(
172
+ instance: TInstance,
173
+ propertyName: string,
174
+ handler: PropertyChangeCallback<any>,
175
+ immediate: boolean = true
176
+ ): () => void {
177
+ immediate = immediate && this.isTriggering === false;
178
+
179
+ //
180
+ // Call handler immediately if property is already available.
181
+ //
182
+ const currentValue = (instance as any)[propertyName];
183
+ if (immediate && currentValue !== null && currentValue !== undefined) {
184
+ handler(currentValue, undefined as any);
185
+ }
186
+
187
+ return this.addCallback(instance[$refId], propertyName, handler);
188
+ }
189
+
190
+ /**
191
+ * Listen to any property change on an instance.
192
+ */
193
+ onChange<TInstance extends Schema>(
194
+ instance: TInstance,
195
+ handler: InstanceChangeCallback
196
+ ): () => void;
197
+
198
+ /**
199
+ * Listen to item changes in a collection on root state.
200
+ */
201
+ onChange<K extends CollectionPropNames<TState>>(
202
+ property: K,
203
+ handler: KeyValueCallback<CollectionKeyType<TState, K>, CollectionValueType<TState, K>>
204
+ ): () => void;
205
+
206
+ /**
207
+ * Listen to item changes in a nested collection.
208
+ */
209
+ onChange<TInstance extends Schema, K extends CollectionPropNames<TInstance>>(
210
+ instance: TInstance,
211
+ property: K,
212
+ handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>
213
+ ): () => void;
214
+
215
+ onChange(...args: any[]): () => void {
216
+ if (args.length === 2 && typeof args[0] !== 'string') {
217
+ // onChange(instance, handler) - instance change
218
+ const instance = args[0] as Schema;
219
+ const handler = args[1] as InstanceChangeCallback;
220
+ return this.addCallback(instance[$refId], OPERATION.REPLACE, handler);
221
+ }
222
+
223
+ if (typeof args[0] === 'string') {
224
+ // onChange(property, handler) - collection on root state
225
+ return this.addCallbackOrWaitCollectionAvailable(
226
+ this.state,
227
+ args[0],
228
+ OPERATION.REPLACE,
229
+ args[1]
230
+ );
231
+ } else {
232
+ // onChange(instance, property, handler) - nested collection
233
+ return this.addCallbackOrWaitCollectionAvailable(
234
+ args[0],
235
+ args[1],
236
+ OPERATION.REPLACE,
237
+ args[2]
238
+ );
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Listen to items added to a collection on root state.
244
+ */
245
+ onAdd<K extends CollectionPropNames<TState>>(
246
+ property: K,
247
+ handler: KeyValueCallback<CollectionKeyType<TState, K>, CollectionValueType<TState, K>>,
248
+ immediate?: boolean
249
+ ): () => void;
250
+
251
+ /**
252
+ * Listen to items added to a nested collection.
253
+ */
254
+ onAdd<TInstance extends Schema, K extends CollectionPropNames<TInstance>>(
255
+ instance: TInstance,
256
+ property: K,
257
+ handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>,
258
+ immediate?: boolean
259
+ ): () => void;
260
+
261
+ onAdd(...args: any[]): () => void {
262
+ if (typeof args[0] === 'string') {
263
+ // onAdd(property, handler, immediate?) - collection on root state
264
+ return this.addCallbackOrWaitCollectionAvailable(
265
+ this.state,
266
+ args[0],
267
+ OPERATION.ADD,
268
+ args[1],
269
+ args[2] !== false
270
+ );
271
+ } else {
272
+ // onAdd(instance, property, handler, immediate?) - nested collection
273
+ return this.addCallbackOrWaitCollectionAvailable(
274
+ args[0],
275
+ args[1],
276
+ OPERATION.ADD,
277
+ args[2],
278
+ args[3] !== false
279
+ );
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Listen to items removed from a collection on root state.
285
+ */
286
+ onRemove<K extends CollectionPropNames<TState>>(
287
+ property: K,
288
+ handler: KeyValueCallback<CollectionKeyType<TState, K>, CollectionValueType<TState, K>>
289
+ ): () => void;
290
+
291
+ /**
292
+ * Listen to items removed from a nested collection.
293
+ */
294
+ onRemove<TInstance extends Schema, K extends CollectionPropNames<TInstance>>(
295
+ instance: TInstance,
296
+ property: K,
297
+ handler: KeyValueCallback<CollectionKeyType<TInstance, K>, CollectionValueType<TInstance, K>>
298
+ ): () => void;
299
+
300
+ onRemove(...args: any[]): () => void {
301
+ if (typeof args[0] === 'string') {
302
+ // onRemove(property, handler) - collection on root state
303
+ return this.addCallbackOrWaitCollectionAvailable(
304
+ this.state,
305
+ args[0],
306
+ OPERATION.DELETE,
307
+ args[1]
308
+ );
309
+ } else {
310
+ // onRemove(instance, property, handler) - nested collection
311
+ return this.addCallbackOrWaitCollectionAvailable(
312
+ args[0],
313
+ args[1],
314
+ OPERATION.DELETE,
315
+ args[2]
316
+ );
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Bind properties from a Schema instance to a target object.
322
+ * Changes will be automatically reflected on the target object.
323
+ */
324
+ bindTo<TInstance extends Schema, TTarget>(
325
+ from: TInstance,
326
+ to: TTarget,
327
+ properties?: string[],
328
+ immediate: boolean = true
329
+ ): () => void {
330
+ const metadata: Metadata = (from.constructor as typeof Schema)[Symbol.metadata];
331
+
332
+ // If no properties specified, bind all properties
333
+ if (!properties) {
334
+ properties = Object.keys(metadata)
335
+ .filter(key => !isNaN(Number(key)))
336
+ .map((index) => metadata[index as any as number].name);
337
+ }
338
+
339
+ const action = () => {
340
+ for (const prop of properties!) {
341
+ const fromValue = (from as any)[prop];
342
+ if (fromValue !== undefined) {
343
+ (to as any)[prop] = fromValue;
344
+ }
345
+ }
346
+ };
347
+
348
+ if (immediate) {
349
+ action();
350
+ }
351
+
352
+ return this.addCallback(from[$refId], OPERATION.REPLACE, action);
353
+ }
354
+
355
+ protected triggerChanges(allChanges: DataChange[]): void {
356
+ this.uniqueRefIds.clear();
357
+
358
+ for (let i = 0, l = allChanges.length; i < l; i++) {
359
+ const change = allChanges[i];
360
+ const refId = change.refId;
361
+ const ref = change.ref;
362
+
363
+ const $callbacks = this.callbacks[refId];
364
+ if (!$callbacks) {
365
+ continue;
366
+ }
367
+
368
+ //
369
+ // trigger onRemove on child structure.
370
+ //
371
+ if (
372
+ (change.op & OPERATION.DELETE) === OPERATION.DELETE &&
373
+ change.previousValue instanceof Schema
374
+ ) {
375
+ const childRefId = (change.previousValue as Ref)[$refId];
376
+ const deleteCallbacks = this.callbacks[childRefId]?.[OPERATION.DELETE];
377
+ if (deleteCallbacks) {
378
+ for (let j = deleteCallbacks.length - 1; j >= 0; j--) {
379
+ deleteCallbacks[j]();
380
+ }
381
+ }
382
+ }
383
+
384
+ if (ref instanceof Schema) {
385
+ //
386
+ // Handle Schema instance
387
+ //
388
+
389
+ if (!this.uniqueRefIds.has(refId)) {
390
+ // trigger onChange
391
+ const replaceCallbacks = $callbacks[OPERATION.REPLACE];
392
+ if (replaceCallbacks) {
393
+ for (let j = replaceCallbacks.length - 1; j >= 0; j--) {
394
+ try {
395
+ replaceCallbacks[j]();
396
+ } catch (e) {
397
+ console.error(e);
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ // trigger field callbacks
404
+ const fieldCallbacks = $callbacks[change.field];
405
+ if (fieldCallbacks) {
406
+ for (let j = fieldCallbacks.length - 1; j >= 0; j--) {
407
+ try {
408
+ this.isTriggering = true;
409
+ fieldCallbacks[j](change.value, change.previousValue);
410
+ } catch (e) {
411
+ console.error(e);
412
+ } finally {
413
+ this.isTriggering = false;
414
+ }
415
+ }
416
+ }
417
+
418
+ } else {
419
+ //
420
+ // Handle collection of items
421
+ //
422
+ const dynamicIndex = change.dynamicIndex ?? change.field;
423
+
424
+ if ((change.op & OPERATION.DELETE) === OPERATION.DELETE) {
425
+ //
426
+ // FIXME: `previousValue` should always be available.
427
+ //
428
+ if (change.previousValue !== undefined) {
429
+ // trigger onRemove (key, value)
430
+ const deleteCallbacks = $callbacks[OPERATION.DELETE];
431
+ if (deleteCallbacks) {
432
+ for (let j = deleteCallbacks.length - 1; j >= 0; j--) {
433
+ deleteCallbacks[j](dynamicIndex, change.previousValue);
434
+ }
435
+ }
436
+ }
437
+
438
+ // Handle DELETE_AND_ADD operation
439
+ if ((change.op & OPERATION.ADD) === OPERATION.ADD) {
440
+ const addCallbacks = $callbacks[OPERATION.ADD];
441
+ if (addCallbacks) {
442
+ this.isTriggering = true;
443
+ for (let j = addCallbacks.length - 1; j >= 0; j--) {
444
+ addCallbacks[j](dynamicIndex, change.value);
445
+ }
446
+ this.isTriggering = false;
447
+ }
448
+ }
449
+
450
+ } else if (
451
+ (change.op & OPERATION.ADD) === OPERATION.ADD &&
452
+ change.previousValue !== change.value
453
+ ) {
454
+ // trigger onAdd (key, value)
455
+ const addCallbacks = $callbacks[OPERATION.ADD];
456
+ if (addCallbacks) {
457
+ this.isTriggering = true;
458
+ for (let j = addCallbacks.length - 1; j >= 0; j--) {
459
+ addCallbacks[j](dynamicIndex, change.value);
460
+ }
461
+ this.isTriggering = false;
462
+ }
463
+ }
464
+
465
+ // trigger onChange (key, value)
466
+ if (change.value !== change.previousValue) {
467
+ const replaceCallbacks = $callbacks[OPERATION.REPLACE];
468
+ if (replaceCallbacks) {
469
+ for (let j = replaceCallbacks.length - 1; j >= 0; j--) {
470
+ replaceCallbacks[j](dynamicIndex, change.value);
471
+ }
472
+ }
473
+ }
474
+ }
475
+
476
+ this.uniqueRefIds.add(refId);
477
+ }
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Factory class for retrieving the callbacks API.
483
+ */
484
+ export const Callbacks = {
485
+ /**
486
+ * Get the new callbacks standard API.
487
+ *
488
+ * Usage:
489
+ * ```ts
490
+ * const callbacks = Callbacks.get(roomOrDecoder);
491
+ *
492
+ * // Listen to property changes
493
+ * callbacks.listen("currentTurn", (currentValue, previousValue) => { ... });
494
+ *
495
+ * // Listen to collection additions
496
+ * callbacks.onAdd("entities", (sessionId, entity) => {
497
+ * // Nested property listening
498
+ * callbacks.listen(entity, "hp", (currentHp, previousHp) => { ... });
499
+ * });
500
+ *
501
+ * // Listen to collection removals
502
+ * callbacks.onRemove("entities", (sessionId, entity) => { ... });
503
+ *
504
+ * // Listen to any property change on an instance
505
+ * callbacks.onChange(entity, () => { ... });
506
+ *
507
+ * // Bind properties to another object
508
+ * callbacks.bindTo(player, playerVisual);
509
+ * ```
510
+ *
511
+ * @param roomOrDecoder - Room or Decoder instance to get the callbacks for.
512
+ * @returns the new callbacks standard API.
513
+ */
514
+ get<T extends Schema>(roomOrDecoder: Decoder<T> | { serializer: { decoder: Decoder<T> } }): StateCallbackStrategy<T> {
515
+ if (roomOrDecoder instanceof Decoder) {
516
+ return new StateCallbackStrategy<T>(roomOrDecoder);
517
+
518
+ } else if (roomOrDecoder.serializer.decoder) {
519
+ return new StateCallbackStrategy<T>(roomOrDecoder.serializer.decoder);
520
+
521
+ } else {
522
+ throw new Error('Invalid room or decoder');
523
+ }
524
+ },
525
+
526
+ /**
527
+ * Get the legacy callbacks API.
528
+ *
529
+ * We aim to deprecate this API on 1.0, and iterate on improving Callbacks.get() API.
530
+ *
531
+ * @param roomOrDecoder - Room or Decoder instance to get the legacy callbacks for.
532
+ * @returns the legacy callbacks API.
533
+ */
534
+ getLegacy<T extends Schema>(roomOrDecoder: Decoder<T> | { serializer: { decoder: Decoder<T> } }): SchemaCallbackProxy<T> {
535
+ if (roomOrDecoder instanceof Decoder) {
536
+ return getDecoderStateCallbacks(roomOrDecoder);
537
+
538
+ } else if (roomOrDecoder.serializer.decoder) {
539
+ return getDecoderStateCallbacks(roomOrDecoder.serializer.decoder);
540
+ }
541
+ },
542
+
543
+ getRawChanges(decoder: Decoder, callback: (changes: DataChange[]) => void) {
544
+ return getRawChangesCallback(decoder, callback);
545
+ }
546
+ };
547
+
@@ -1,10 +1,11 @@
1
1
  import { Metadata } from "../../Metadata";
2
2
  import { Collection, NonFunctionNonPrimitivePropNames, NonFunctionPropNames } from "../../types/HelperTypes";
3
- import { Ref } from "../../encoder/ChangeTree";
3
+ import { IRef, Ref } from "../../encoder/ChangeTree";
4
4
  import { Decoder } from "../Decoder";
5
5
  import { DataChange } from "../DecodeOperation";
6
6
  import { OPERATION } from "../../encoding/spec";
7
7
  import { Schema } from "../../Schema";
8
+ import { $refId } from "../../types/symbols";
8
9
  import type { DefinitionType } from "../../annotations";
9
10
  import type { CollectionSchema } from "../../types/custom/CollectionSchema";
10
11
 
@@ -107,7 +108,12 @@ type CallContext = {
107
108
  onInstanceAvailable?: OnInstanceAvailableCallback,
108
109
  }
109
110
 
110
-
111
+ /**
112
+ * Legacy callback system
113
+ *
114
+ * @param decoder
115
+ * @returns
116
+ */
111
117
  export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>): SchemaCallbackProxy<T> {
112
118
  const $root = decoder.root;
113
119
  const callbacks = $root.callbacks;
@@ -133,7 +139,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
133
139
  (change.op & OPERATION.DELETE) === OPERATION.DELETE &&
134
140
  change.previousValue instanceof Schema
135
141
  ) {
136
- const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[OPERATION.DELETE];
142
+ const deleteCallbacks = callbacks[(change.previousValue as Ref)[$refId]]?.[OPERATION.DELETE];
137
143
  for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
138
144
  deleteCallbacks[i]();
139
145
  }
@@ -247,7 +253,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
247
253
  ) {
248
254
  callback(context.instance[prop], undefined);
249
255
  }
250
- return $root.addCallback($root.refIds.get(ref), prop, callback);
256
+ return $root.addCallback(ref[$refId], prop, callback);
251
257
  }
252
258
 
253
259
  /**
@@ -272,7 +278,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
272
278
 
273
279
  onChange: function onChange(callback: () => void) {
274
280
  return $root.addCallback(
275
- $root.refIds.get(context.instance),
281
+ context.instance[$refId],
276
282
  OPERATION.REPLACE,
277
283
  callback
278
284
  );
@@ -287,7 +293,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
287
293
  properties = Object.keys(metadata).map((index) => metadata[index as any as number].name);
288
294
  }
289
295
  return $root.addCallback(
290
- $root.refIds.get(context.instance),
296
+ context.instance[$refId],
291
297
  OPERATION.REPLACE,
292
298
  () => {
293
299
  properties.forEach((prop) =>
@@ -313,7 +319,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
313
319
  }, false);
314
320
 
315
321
  // has existing value
316
- if ($root.refIds.get(instance) !== undefined) {
322
+ if (instance?.[$refId] !== undefined) {
317
323
  callback(instance, true);
318
324
  }
319
325
  }
@@ -321,7 +327,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
321
327
 
322
328
  return getProxy(metadataField.type, {
323
329
  // make sure refId is available, otherwise need to wait for the instance to be available.
324
- instance: ($root.refIds.get(instance) && instance),
330
+ instance: (instance?.[$refId] !== undefined && instance),
325
331
  parentInstance: context.instance,
326
332
  onInstanceAvailable,
327
333
  });
@@ -347,7 +353,7 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
347
353
  (ref as CollectionSchema).forEach((v, k) => callback(v, k));
348
354
  }
349
355
 
350
- return $root.addCallback($root.refIds.get(ref), OPERATION.ADD, (value: any, key: any) => {
356
+ return $root.addCallback(ref[$refId], OPERATION.ADD, (value: any, key: any) => {
351
357
  onAddCalls.set(callback, true);
352
358
  currentOnAddCallback = callback;
353
359
  callback(value, key);
@@ -357,11 +363,11 @@ export function getDecoderStateCallbacks<T extends Schema>(decoder: Decoder<T>):
357
363
  };
358
364
 
359
365
  const onRemove = function (ref: Ref, callback: (value: any, key: any) => void) {
360
- return $root.addCallback($root.refIds.get(ref), OPERATION.DELETE, callback);
366
+ return $root.addCallback(ref[$refId], OPERATION.DELETE, callback);
361
367
  };
362
368
 
363
369
  const onChange = function (ref: Ref, callback: (value: any, key: any) => void) {
364
- return $root.addCallback($root.refIds.get(ref), OPERATION.REPLACE, callback);
370
+ return $root.addCallback(ref[$refId], OPERATION.REPLACE, callback);
365
371
  };
366
372
 
367
373
  return new Proxy({
@@ -1,6 +1,6 @@
1
1
  import { OPERATION } from "../encoding/spec";
2
2
  import { Schema } from "../Schema";
3
- import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex } from "../types/symbols";
3
+ import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refId, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex } from "../types/symbols";
4
4
 
5
5
  import type { MapSchema } from "../types/custom/MapSchema";
6
6
  import type { ArraySchema } from "../types/custom/ArraySchema";
@@ -16,6 +16,7 @@ declare global {
16
16
  interface Object {
17
17
  // FIXME: not a good practice to extend globals here
18
18
  [$changes]?: ChangeTree;
19
+ // [$refId]?: number;
19
20
  [$encoder]?: EncodeOperation,
20
21
  [$decoder]?: DecodeOperation,
21
22
  }
@@ -23,15 +24,12 @@ declare global {
23
24
 
24
25
  export interface IRef {
25
26
  [$changes]?: ChangeTree;
27
+ [$refId]?: number;
26
28
  [$getByIndex]?: (index: number, isEncodeAll?: boolean) => any;
27
29
  [$deleteByIndex]?: (index: number) => void;
28
30
  }
29
31
 
30
- export type Ref = Schema
31
- | ArraySchema
32
- | MapSchema
33
- | CollectionSchema
34
- | SetSchema;
32
+ export type Ref = Schema | ArraySchema | MapSchema | CollectionSchema | SetSchema;
35
33
 
36
34
  export type ChangeSetName = "changes"
37
35
  | "allChanges"
@@ -126,7 +124,6 @@ export interface ParentChain {
126
124
 
127
125
  export class ChangeTree<T extends Ref = any> {
128
126
  ref: T;
129
- refId: number;
130
127
  metadata: Metadata;
131
128
 
132
129
  root?: Root;