@colyseus/schema 3.0.0-alpha.10 → 3.0.0-alpha.12
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.
- package/README.md +131 -61
- package/build/cjs/index.js +266 -10
- package/build/cjs/index.js.map +1 -1
- package/build/esm/index.mjs +265 -11
- package/build/esm/index.mjs.map +1 -1
- package/build/umd/index.js +266 -10
- package/lib/decoder/strategy/StateCallbacks.d.ts +4 -7
- package/lib/decoder/strategy/StateCallbacks.js +6 -12
- package/lib/decoder/strategy/StateCallbacks.js.map +1 -1
- package/lib/encoder/EncodeOperation.js +12 -0
- package/lib/encoder/EncodeOperation.js.map +1 -1
- package/lib/encoder/Encoder.d.ts +1 -1
- package/lib/encoder/Encoder.js +20 -10
- package/lib/encoder/Encoder.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/decoder/strategy/StateCallbacks.ts +11 -20
- package/src/encoder/EncodeOperation.ts +13 -0
- package/src/encoder/Encoder.ts +22 -10
- package/src/index.ts +3 -0
package/README.md
CHANGED
|
@@ -1,55 +1,47 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="logo.png?raw=true" />
|
|
2
|
+
<img src="logo.png?raw=true" width="50%" />
|
|
3
3
|
<br>
|
|
4
|
-
<br>
|
|
5
|
-
|
|
6
4
|
<p>
|
|
7
5
|
An incremental binary state serializer with delta encoding for games.<br>
|
|
8
|
-
|
|
6
|
+
Made for <a href="https://github.com/colyseus/colyseus">Colyseus</a>, yet can be used standalone.
|
|
9
7
|
</p>
|
|
10
8
|
</div>
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
# Features
|
|
11
|
+
|
|
12
|
+
- Flexible Schema Definition
|
|
13
|
+
- Optimized Data Encoding
|
|
14
|
+
- Automatic State Synchronization
|
|
15
|
+
- Client-side Change Detection
|
|
16
|
+
- Per-client portions of the state
|
|
17
|
+
- Type Safety
|
|
18
|
+
- *...decoders available for multiple languages (C#, Lua, Haxe)*
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
## Schema definition
|
|
21
|
+
|
|
22
|
+
`@colyseus/schema` uses type annotations to define types of synchronized properties.
|
|
15
23
|
|
|
16
24
|
```typescript
|
|
17
25
|
import { Schema, type, ArraySchema, MapSchema } from '@colyseus/schema';
|
|
18
26
|
|
|
19
27
|
export class Player extends Schema {
|
|
20
|
-
@type("string")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@type("number")
|
|
24
|
-
x: number;
|
|
25
|
-
|
|
26
|
-
@type("number")
|
|
27
|
-
y: number;
|
|
28
|
+
@type("string") name: string;
|
|
29
|
+
@type("number") x: number;
|
|
30
|
+
@type("number") y: number;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
export class
|
|
31
|
-
@type('string')
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@type(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@type(Player)
|
|
38
|
-
player: Player;
|
|
39
|
-
|
|
40
|
-
@type([ Player ])
|
|
41
|
-
arrayOfPlayers: ArraySchema<Player>;
|
|
42
|
-
|
|
43
|
-
@type({ map: Player })
|
|
44
|
-
mapOfPlayers: MapSchema<Player>;
|
|
33
|
+
export class MyState extends Schema {
|
|
34
|
+
@type('string') fieldString: string;
|
|
35
|
+
@type('number') fieldNumber: number;
|
|
36
|
+
@type(Player) player: Player;
|
|
37
|
+
@type([ Player ]) arrayOfPlayers: ArraySchema<Player>;
|
|
38
|
+
@type({ map: Player }) mapOfPlayers: MapSchema<Player>;
|
|
45
39
|
}
|
|
46
40
|
```
|
|
47
41
|
|
|
48
|
-
See [example](test/Schema.ts).
|
|
49
|
-
|
|
50
42
|
## Supported types
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
### Primitive Types
|
|
53
45
|
|
|
54
46
|
| Type | Description | Limitation |
|
|
55
47
|
|------|-------------|------------|
|
|
@@ -79,14 +71,14 @@ name: string;
|
|
|
79
71
|
name: number;
|
|
80
72
|
```
|
|
81
73
|
|
|
82
|
-
####
|
|
74
|
+
#### Child `Schema` structures
|
|
83
75
|
|
|
84
76
|
```typescript
|
|
85
77
|
@type(Player)
|
|
86
78
|
player: Player;
|
|
87
79
|
```
|
|
88
80
|
|
|
89
|
-
#### Array of
|
|
81
|
+
#### Array of `Schema` structure
|
|
90
82
|
|
|
91
83
|
```typescript
|
|
92
84
|
@type([ Player ])
|
|
@@ -105,7 +97,7 @@ arrayOfNumbers: ArraySchema<number>;
|
|
|
105
97
|
arrayOfStrings: ArraySchema<string>;
|
|
106
98
|
```
|
|
107
99
|
|
|
108
|
-
#### Map of
|
|
100
|
+
#### Map of `Schema` structure
|
|
109
101
|
|
|
110
102
|
```typescript
|
|
111
103
|
@type({ map: Player })
|
|
@@ -114,7 +106,7 @@ mapOfPlayers: MapSchema<Player>;
|
|
|
114
106
|
|
|
115
107
|
#### Map of a primitive type
|
|
116
108
|
|
|
117
|
-
You can't mix types inside maps.
|
|
109
|
+
You can't mix primitive types inside maps.
|
|
118
110
|
|
|
119
111
|
```typescript
|
|
120
112
|
@type({ map: "number" })
|
|
@@ -124,16 +116,6 @@ mapOfNumbers: MapSchema<number>;
|
|
|
124
116
|
mapOfStrings: MapSchema<string>;
|
|
125
117
|
```
|
|
126
118
|
|
|
127
|
-
### Backwards/forwards compability
|
|
128
|
-
|
|
129
|
-
Backwards/fowards compatibility is possible by declaring new fields at the
|
|
130
|
-
end of existing structures, and earlier declarations to not be removed, but
|
|
131
|
-
be marked `@deprecated()` when needed.
|
|
132
|
-
|
|
133
|
-
This is particularly useful for native-compiled targets, such as C#, C++,
|
|
134
|
-
Haxe, etc - where the client-side can potentially not have the most
|
|
135
|
-
up-to-date version of the schema definitions.
|
|
136
|
-
|
|
137
119
|
### Reflection
|
|
138
120
|
|
|
139
121
|
The Schema definitions can encode itself through `Reflection`. You can have the
|
|
@@ -144,10 +126,8 @@ reflection to the client-side, for example:
|
|
|
144
126
|
import { Schema, type, Reflection } from "@colyseus/schema";
|
|
145
127
|
|
|
146
128
|
class MyState extends Schema {
|
|
147
|
-
@type("string")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// more definitions relating to more Schema types.
|
|
129
|
+
@type("string") currentTurn: string;
|
|
130
|
+
// ... more definitions
|
|
151
131
|
}
|
|
152
132
|
|
|
153
133
|
// send `encodedStateSchema` across the network
|
|
@@ -157,22 +137,113 @@ const encodedStateSchema = Reflection.encode(new MyState());
|
|
|
157
137
|
const myState = Reflection.decode(encodedStateSchema);
|
|
158
138
|
```
|
|
159
139
|
|
|
160
|
-
###
|
|
140
|
+
### `StateView` / `@view()`
|
|
161
141
|
|
|
162
|
-
|
|
142
|
+
You can use `@view()` to filter properties that should be sent only to `StateView`'s that have access to it.
|
|
163
143
|
|
|
164
144
|
```typescript
|
|
165
|
-
import { Schema, type,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
@
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
@type({ map: Card })
|
|
172
|
-
cards = new MapSchema<Card>();
|
|
145
|
+
import { Schema, type, view } from "@colyseus/schema";
|
|
146
|
+
|
|
147
|
+
class Player extends Schema {
|
|
148
|
+
@view() @type("string") secret: string;
|
|
149
|
+
@type("string") notSecret: string;
|
|
173
150
|
}
|
|
151
|
+
|
|
152
|
+
class MyState extends Schema {
|
|
153
|
+
@type({ map: Player }) players = new MapSchema<Player>();
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Using the `StateView`
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const view = new StateView();
|
|
161
|
+
view.add(player);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Encoder
|
|
165
|
+
|
|
166
|
+
There are 3 majour features of the `Encoder` class:
|
|
167
|
+
|
|
168
|
+
- Encoding the full state
|
|
169
|
+
- Encoding the state changes
|
|
170
|
+
- Encoding state with filters (properties using `@view()` tag)
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { Encoder } from "@colyseus/schema";
|
|
174
|
+
|
|
175
|
+
const state = new MyState();
|
|
176
|
+
const encoder = new Encoder(state);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
New clients must receive the full state on their first connection:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const fullEncode = encoder.encodeAll();
|
|
183
|
+
// ... send "fullEncode" to client and decode it
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Further state changes must be sent in order:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const changesBuffer = encoder.encode();
|
|
190
|
+
// ... send "changesBuffer" to client and decode it
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Encoding with views
|
|
194
|
+
|
|
195
|
+
When using `@view()` and `StateView`'s, a single "full encode" must be used for multiple views. Each view also must add its own changes.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// shared buffer iterator
|
|
199
|
+
const it = { offset: 0 };
|
|
200
|
+
|
|
201
|
+
// shared full encode
|
|
202
|
+
encoder.encodeAll(it);
|
|
203
|
+
const sharedOffset = it.offset;
|
|
204
|
+
|
|
205
|
+
// view 1
|
|
206
|
+
const fullEncode1 = encoder.encodeAllView(view1, sharedOffset, it);
|
|
207
|
+
// ... send "fullEncode1" to client1 and decode it
|
|
208
|
+
|
|
209
|
+
// view 2
|
|
210
|
+
const fullEncode2 = encoder.encodeAllView(view2, sharedOffset, it);
|
|
211
|
+
// ... send "fullEncode" to client2 and decode it
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Encoding changes per views:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// shared buffer iterator
|
|
218
|
+
const it = { offset: 0 };
|
|
219
|
+
|
|
220
|
+
// shared changes encode
|
|
221
|
+
encoder.encode(it);
|
|
222
|
+
const sharedOffset = it.offset;
|
|
223
|
+
|
|
224
|
+
// view 1
|
|
225
|
+
const view1Encoded = this.encoder.encodeView(view1, sharedOffset, it);
|
|
226
|
+
// ... send "view1Encoded" to client1 and decode it
|
|
227
|
+
|
|
228
|
+
// view 2
|
|
229
|
+
const view2Encoded = this.encoder.encodeView(view2, sharedOffset, it);
|
|
230
|
+
// ... send "view2Encoded" to client2 and decode it
|
|
231
|
+
|
|
232
|
+
// discard all changes after encoding is done.
|
|
233
|
+
encoder.discardChanges();
|
|
174
234
|
```
|
|
175
235
|
|
|
236
|
+
### Backwards/forwards compability
|
|
237
|
+
|
|
238
|
+
Backwards/fowards compatibility is possible by declaring new fields at the
|
|
239
|
+
end of existing structures, and earlier declarations to not be removed, but
|
|
240
|
+
be marked `@deprecated()` when needed.
|
|
241
|
+
|
|
242
|
+
This is particularly useful for native-compiled targets, such as C#, C++,
|
|
243
|
+
Haxe, etc - where the client-side can potentially not have the most
|
|
244
|
+
up-to-date version of the schema definitions.
|
|
245
|
+
|
|
246
|
+
|
|
176
247
|
## Limitations and best practices
|
|
177
248
|
|
|
178
249
|
- Each `Schema` structure can hold up to `64` fields. If you need more fields, use nested structures.
|
|
@@ -184,7 +255,6 @@ export class State extends Schema {
|
|
|
184
255
|
- `@colyseus/schema` encodes only field values in the specified order.
|
|
185
256
|
- Both encoder (server) and decoder (client) must have same schema definition.
|
|
186
257
|
- The order of the fields must be the same.
|
|
187
|
-
- Avoid manipulating indexes of an array. This result in at least `2` extra bytes for each index change. **Example:** If you have an array of 20 items, and remove the first item (through `shift()`) this means `38` extra bytes to be serialized.
|
|
188
258
|
|
|
189
259
|
## Generating client-side schema files (for strictly typed languages)
|
|
190
260
|
|
package/build/cjs/index.js
CHANGED
|
@@ -946,6 +946,18 @@ const encodeKeyValueOperation = function (encoder, bytes, changeTree, field, ope
|
|
|
946
946
|
}
|
|
947
947
|
const type = changeTree.getType(field);
|
|
948
948
|
const value = changeTree.getValue(field);
|
|
949
|
+
// try { throw new Error(); } catch (e) {
|
|
950
|
+
// // only print if not coming from Reflection.ts
|
|
951
|
+
// if (!e.stack.includes("src/Reflection.ts")) {
|
|
952
|
+
// console.log("encodeKeyValueOperation -> ", {
|
|
953
|
+
// ref: changeTree.ref.constructor.name,
|
|
954
|
+
// field,
|
|
955
|
+
// operation: OPERATION[operation],
|
|
956
|
+
// value: value?.toJSON(),
|
|
957
|
+
// items: ref.toJSON(),
|
|
958
|
+
// });
|
|
959
|
+
// }
|
|
960
|
+
// }
|
|
949
961
|
// TODO: inline this function call small performance gain
|
|
950
962
|
encodeValue(encoder, bytes, ref, type, value, field, operation, it);
|
|
951
963
|
};
|
|
@@ -3385,9 +3397,8 @@ class Encoder {
|
|
|
3385
3397
|
this.state = state;
|
|
3386
3398
|
state[$changes].setRoot(this.root);
|
|
3387
3399
|
}
|
|
3388
|
-
encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes) {
|
|
3400
|
+
encode(it = { offset: 0 }, view, buffer = this.sharedBuffer, changeTrees = this.root.changes, isEncodeAll = this.root.allChanges === changeTrees) {
|
|
3389
3401
|
const initialOffset = it.offset; // cache current offset in case we need to resize the buffer
|
|
3390
|
-
const isEncodeAll = this.root.allChanges === changeTrees;
|
|
3391
3402
|
const hasView = (view !== undefined);
|
|
3392
3403
|
const rootChangeTree = this.state[$changes];
|
|
3393
3404
|
const changeTreesIterator = changeTrees.entries();
|
|
@@ -3396,6 +3407,12 @@ class Encoder {
|
|
|
3396
3407
|
const ctor = ref['constructor'];
|
|
3397
3408
|
const encoder = ctor[$encoder];
|
|
3398
3409
|
const filter = ctor[$filter];
|
|
3410
|
+
// try { throw new Error(); } catch (e) {
|
|
3411
|
+
// // only print if not coming from Reflection.ts
|
|
3412
|
+
// if (!e.stack.includes("src/Reflection.ts")) {
|
|
3413
|
+
// console.log("ChangeTree:", { ref: ref.constructor.name, });
|
|
3414
|
+
// }
|
|
3415
|
+
// }
|
|
3399
3416
|
if (hasView) {
|
|
3400
3417
|
if (!view.items.has(changeTree)) {
|
|
3401
3418
|
view.invisible.add(changeTree);
|
|
@@ -3425,11 +3442,16 @@ class Encoder {
|
|
|
3425
3442
|
// view?.invisible.add(changeTree);
|
|
3426
3443
|
continue;
|
|
3427
3444
|
}
|
|
3428
|
-
//
|
|
3429
|
-
//
|
|
3430
|
-
//
|
|
3431
|
-
//
|
|
3432
|
-
//
|
|
3445
|
+
// try { throw new Error(); } catch (e) {
|
|
3446
|
+
// // only print if not coming from Reflection.ts
|
|
3447
|
+
// if (!e.stack.includes("src/Reflection.ts")) {
|
|
3448
|
+
// // console.log("WILL ENCODE", {
|
|
3449
|
+
// // ref: changeTree.ref.constructor.name,
|
|
3450
|
+
// // fieldIndex,
|
|
3451
|
+
// // operation: OPERATION[operation],
|
|
3452
|
+
// // });
|
|
3453
|
+
// }
|
|
3454
|
+
// }
|
|
3433
3455
|
encoder(this, buffer, changeTree, fieldIndex, operation, it, isEncodeAll, hasView);
|
|
3434
3456
|
}
|
|
3435
3457
|
}
|
|
@@ -3444,7 +3466,7 @@ class Encoder {
|
|
|
3444
3466
|
if (buffer === this.sharedBuffer) {
|
|
3445
3467
|
this.sharedBuffer = buffer;
|
|
3446
3468
|
}
|
|
3447
|
-
return this.encode({ offset: initialOffset }, view, buffer);
|
|
3469
|
+
return this.encode({ offset: initialOffset }, view, buffer, changeTrees, isEncodeAll);
|
|
3448
3470
|
}
|
|
3449
3471
|
else {
|
|
3450
3472
|
//
|
|
@@ -3464,14 +3486,14 @@ class Encoder {
|
|
|
3464
3486
|
// Array.from(this.root.allChanges.entries()).map((item) => {
|
|
3465
3487
|
// console.log("->", { ref: item[0].ref.constructor.name, refId: item[0].refId, changes: item[1].size });
|
|
3466
3488
|
// });
|
|
3467
|
-
return this.encode(it, undefined, buffer, this.root.allChanges);
|
|
3489
|
+
return this.encode(it, undefined, buffer, this.root.allChanges, true);
|
|
3468
3490
|
}
|
|
3469
3491
|
encodeAllView(view, sharedOffset, it, bytes = this.sharedBuffer) {
|
|
3470
3492
|
const viewOffset = it.offset;
|
|
3471
3493
|
// console.log(`encodeAllView(), this.root.allFilteredChanges (${this.root.allFilteredChanges.size})`);
|
|
3472
3494
|
// this.debugAllFilteredChanges();
|
|
3473
3495
|
// try to encode "filtered" changes
|
|
3474
|
-
this.encode(it, view, bytes, this.root.allFilteredChanges);
|
|
3496
|
+
this.encode(it, view, bytes, this.root.allFilteredChanges, true);
|
|
3475
3497
|
return Buffer.concat([
|
|
3476
3498
|
bytes.subarray(0, sharedOffset),
|
|
3477
3499
|
bytes.subarray(viewOffset, it.offset)
|
|
@@ -3941,6 +3963,238 @@ __decorate([
|
|
|
3941
3963
|
type([ReflectionType])
|
|
3942
3964
|
], Reflection.prototype, "types", void 0);
|
|
3943
3965
|
|
|
3966
|
+
function getStateCallbacks(decoder) {
|
|
3967
|
+
const $root = decoder.root;
|
|
3968
|
+
const callbacks = $root.callbacks;
|
|
3969
|
+
let isTriggeringOnAdd = false;
|
|
3970
|
+
decoder.triggerChanges = function (allChanges) {
|
|
3971
|
+
const uniqueRefIds = new Set();
|
|
3972
|
+
for (let i = 0, l = allChanges.length; i < l; i++) {
|
|
3973
|
+
const change = allChanges[i];
|
|
3974
|
+
const refId = change.refId;
|
|
3975
|
+
const ref = change.ref;
|
|
3976
|
+
const $callbacks = callbacks[refId];
|
|
3977
|
+
if (!$callbacks) {
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
//
|
|
3981
|
+
// trigger onRemove on child structure.
|
|
3982
|
+
//
|
|
3983
|
+
if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE &&
|
|
3984
|
+
change.previousValue instanceof Schema) {
|
|
3985
|
+
const deleteCallbacks = callbacks[$root.refIds.get(change.previousValue)]?.[exports.OPERATION.DELETE];
|
|
3986
|
+
for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
|
|
3987
|
+
deleteCallbacks[i]();
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (ref instanceof Schema) {
|
|
3991
|
+
//
|
|
3992
|
+
// Handle schema instance
|
|
3993
|
+
//
|
|
3994
|
+
if (!uniqueRefIds.has(refId)) {
|
|
3995
|
+
try {
|
|
3996
|
+
// trigger onChange
|
|
3997
|
+
const replaceCallbacks = $callbacks?.[exports.OPERATION.REPLACE];
|
|
3998
|
+
for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
|
|
3999
|
+
replaceCallbacks[i]();
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
catch (e) {
|
|
4003
|
+
console.error(e);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
try {
|
|
4007
|
+
if ($callbacks.hasOwnProperty(change.field)) {
|
|
4008
|
+
const fieldCallbacks = $callbacks[change.field];
|
|
4009
|
+
for (let i = fieldCallbacks?.length - 1; i >= 0; i--) {
|
|
4010
|
+
fieldCallbacks[i](change.value, change.previousValue);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
catch (e) {
|
|
4015
|
+
//
|
|
4016
|
+
console.error(e);
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
else {
|
|
4020
|
+
//
|
|
4021
|
+
// Handle collection of items
|
|
4022
|
+
//
|
|
4023
|
+
if (change.op === exports.OPERATION.ADD && change.previousValue === undefined) {
|
|
4024
|
+
// triger onAdd
|
|
4025
|
+
isTriggeringOnAdd = true;
|
|
4026
|
+
const addCallbacks = $callbacks[exports.OPERATION.ADD];
|
|
4027
|
+
for (let i = addCallbacks?.length - 1; i >= 0; i--) {
|
|
4028
|
+
addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
|
|
4029
|
+
}
|
|
4030
|
+
isTriggeringOnAdd = false;
|
|
4031
|
+
}
|
|
4032
|
+
else if ((change.op & exports.OPERATION.DELETE) === exports.OPERATION.DELETE) {
|
|
4033
|
+
//
|
|
4034
|
+
// FIXME: `previousValue` should always be available.
|
|
4035
|
+
//
|
|
4036
|
+
if (change.previousValue !== undefined) {
|
|
4037
|
+
// triger onRemove
|
|
4038
|
+
const deleteCallbacks = $callbacks[exports.OPERATION.DELETE];
|
|
4039
|
+
for (let i = deleteCallbacks?.length - 1; i >= 0; i--) {
|
|
4040
|
+
deleteCallbacks[i](change.previousValue, change.dynamicIndex ?? change.field);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
// Handle DELETE_AND_ADD operations
|
|
4044
|
+
// FIXME: should we set "isTriggeringOnAdd" here?
|
|
4045
|
+
if ((change.op & exports.OPERATION.ADD) === exports.OPERATION.ADD) {
|
|
4046
|
+
const addCallbacks = $callbacks[exports.OPERATION.ADD];
|
|
4047
|
+
for (let i = addCallbacks?.length - 1; i >= 0; i--) {
|
|
4048
|
+
addCallbacks[i](change.value, change.dynamicIndex ?? change.field);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
// trigger onChange
|
|
4053
|
+
if (change.value !== change.previousValue) {
|
|
4054
|
+
const replaceCallbacks = $callbacks[exports.OPERATION.REPLACE];
|
|
4055
|
+
for (let i = replaceCallbacks?.length - 1; i >= 0; i--) {
|
|
4056
|
+
replaceCallbacks[i](change.value, change.dynamicIndex ?? change.field);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
uniqueRefIds.add(refId);
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
function getProxy(metadataOrType, context) {
|
|
4064
|
+
let metadata = context.instance?.constructor[Symbol.metadata] || metadataOrType;
|
|
4065
|
+
let isCollection = ((context.instance && typeof (context.instance['forEach']) === "function") ||
|
|
4066
|
+
(metadataOrType && typeof (metadataOrType[Symbol.metadata]) === "undefined"));
|
|
4067
|
+
if (metadata && !isCollection) {
|
|
4068
|
+
const onAdd = function (ref, prop, callback, immediate) {
|
|
4069
|
+
// immediate trigger
|
|
4070
|
+
if (immediate &&
|
|
4071
|
+
context.instance[prop] !== undefined &&
|
|
4072
|
+
!isTriggeringOnAdd // FIXME: This is a workaround (https://github.com/colyseus/schema/issues/147)
|
|
4073
|
+
) {
|
|
4074
|
+
callback(context.instance[prop], undefined);
|
|
4075
|
+
}
|
|
4076
|
+
return $root.addCallback($root.refIds.get(ref), prop, callback);
|
|
4077
|
+
};
|
|
4078
|
+
/**
|
|
4079
|
+
* Schema instances
|
|
4080
|
+
*/
|
|
4081
|
+
return new Proxy({
|
|
4082
|
+
listen: function listen(prop, callback, immediate = true) {
|
|
4083
|
+
if (context.instance) {
|
|
4084
|
+
return onAdd(context.instance, prop, callback, immediate);
|
|
4085
|
+
}
|
|
4086
|
+
else {
|
|
4087
|
+
// collection instance not received yet
|
|
4088
|
+
context.onInstanceAvailable((ref, existing) => onAdd(ref, prop, callback, immediate && existing));
|
|
4089
|
+
}
|
|
4090
|
+
},
|
|
4091
|
+
onChange: function onChange(callback) {
|
|
4092
|
+
return $root.addCallback($root.refIds.get(context.instance), exports.OPERATION.REPLACE, callback);
|
|
4093
|
+
},
|
|
4094
|
+
bindTo: function bindTo(targetObject, properties) {
|
|
4095
|
+
// return $root.addCallback(
|
|
4096
|
+
// $root.refIds.get(context.instance),
|
|
4097
|
+
// OPERATION.BIND,
|
|
4098
|
+
// callback
|
|
4099
|
+
// );
|
|
4100
|
+
console.log("bindTo", targetObject, properties);
|
|
4101
|
+
}
|
|
4102
|
+
}, {
|
|
4103
|
+
get(target, prop) {
|
|
4104
|
+
if (metadata[prop]) {
|
|
4105
|
+
const instance = context.instance?.[prop];
|
|
4106
|
+
const onInstanceAvailable = ((callback) => {
|
|
4107
|
+
const unbind = $(context.instance).listen(prop, (value, _) => {
|
|
4108
|
+
callback(value, false);
|
|
4109
|
+
// FIXME: by "unbinding" the callback here,
|
|
4110
|
+
// it will not support when the server
|
|
4111
|
+
// re-instantiates the instance.
|
|
4112
|
+
//
|
|
4113
|
+
unbind?.();
|
|
4114
|
+
}, false);
|
|
4115
|
+
// has existing value
|
|
4116
|
+
if ($root.refIds.get(instance) !== undefined) {
|
|
4117
|
+
callback(instance, true);
|
|
4118
|
+
}
|
|
4119
|
+
});
|
|
4120
|
+
return getProxy(metadata[prop].type, {
|
|
4121
|
+
instance,
|
|
4122
|
+
parentInstance: context.instance,
|
|
4123
|
+
onInstanceAvailable,
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
else {
|
|
4127
|
+
// accessing the function
|
|
4128
|
+
return target[prop];
|
|
4129
|
+
}
|
|
4130
|
+
},
|
|
4131
|
+
has(target, prop) { return metadata[prop] !== undefined; },
|
|
4132
|
+
set(_, _1, _2) { throw new Error("not allowed"); },
|
|
4133
|
+
deleteProperty(_, _1) { throw new Error("not allowed"); },
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
else {
|
|
4137
|
+
/**
|
|
4138
|
+
* Collection instances
|
|
4139
|
+
*/
|
|
4140
|
+
const onAdd = function (ref, callback, immediate) {
|
|
4141
|
+
// Trigger callback on existing items
|
|
4142
|
+
if (immediate) {
|
|
4143
|
+
ref.forEach((v, k) => callback(v, k));
|
|
4144
|
+
}
|
|
4145
|
+
return $root.addCallback($root.refIds.get(ref), exports.OPERATION.ADD, callback);
|
|
4146
|
+
};
|
|
4147
|
+
const onRemove = function (ref, callback) {
|
|
4148
|
+
return $root.addCallback($root.refIds.get(ref), exports.OPERATION.DELETE, callback);
|
|
4149
|
+
};
|
|
4150
|
+
return new Proxy({
|
|
4151
|
+
onAdd: function (callback, immediate = true) {
|
|
4152
|
+
//
|
|
4153
|
+
// https://github.com/colyseus/schema/issues/147
|
|
4154
|
+
// If parent instance has "onAdd" registered, avoid triggering immediate callback.
|
|
4155
|
+
//
|
|
4156
|
+
// FIXME: "isTriggeringOnAdd" is a workaround. We should find a better way to handle this.
|
|
4157
|
+
//
|
|
4158
|
+
if (context.onInstanceAvailable) {
|
|
4159
|
+
// collection instance not received yet
|
|
4160
|
+
context.onInstanceAvailable((ref, existing) => onAdd(ref, callback, immediate && existing && !isTriggeringOnAdd));
|
|
4161
|
+
}
|
|
4162
|
+
else if (context.instance) {
|
|
4163
|
+
onAdd(context.instance, callback, immediate && !isTriggeringOnAdd);
|
|
4164
|
+
}
|
|
4165
|
+
},
|
|
4166
|
+
onRemove: function (callback) {
|
|
4167
|
+
if (context.onInstanceAvailable) {
|
|
4168
|
+
// collection instance not received yet
|
|
4169
|
+
context.onInstanceAvailable((ref) => onRemove(ref, callback));
|
|
4170
|
+
}
|
|
4171
|
+
else if (context.instance) {
|
|
4172
|
+
onRemove(context.instance, callback);
|
|
4173
|
+
}
|
|
4174
|
+
},
|
|
4175
|
+
}, {
|
|
4176
|
+
get(target, prop) {
|
|
4177
|
+
if (!target[prop]) {
|
|
4178
|
+
throw new Error(`Can't access '${prop}' through callback proxy. access the instance directly.`);
|
|
4179
|
+
}
|
|
4180
|
+
return target[prop];
|
|
4181
|
+
},
|
|
4182
|
+
has(target, prop) { return target[prop] !== undefined; },
|
|
4183
|
+
set(_, _1, _2) { throw new Error("not allowed"); },
|
|
4184
|
+
deleteProperty(_, _1) { throw new Error("not allowed"); },
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
function $(instance) {
|
|
4189
|
+
return getProxy(undefined, { instance });
|
|
4190
|
+
}
|
|
4191
|
+
return $(decoder.state);
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
function getRawChangesCallback(decoder, callback) {
|
|
4195
|
+
decoder.triggerChanges = callback;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
3944
4198
|
class StateView {
|
|
3945
4199
|
constructor() {
|
|
3946
4200
|
/**
|
|
@@ -4165,6 +4419,8 @@ exports.dumpChanges = dumpChanges;
|
|
|
4165
4419
|
exports.encode = encode;
|
|
4166
4420
|
exports.encodeKeyValueOperation = encodeArray;
|
|
4167
4421
|
exports.encodeSchemaOperation = encodeSchemaOperation;
|
|
4422
|
+
exports.getRawChangesCallback = getRawChangesCallback;
|
|
4423
|
+
exports.getStateCallbacks = getStateCallbacks;
|
|
4168
4424
|
exports.registerType = registerType;
|
|
4169
4425
|
exports.type = type;
|
|
4170
4426
|
exports.view = view;
|