@colyseus/schema 5.0.8 → 5.0.10
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 +87 -51
- package/build/annotations.d.ts +8 -0
- package/build/codegen/cli.cjs +42 -5
- package/build/codegen/cli.cjs.map +1 -1
- package/build/encoder/ChangeTree.d.ts +10 -2
- package/build/index.cjs +182 -47
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +182 -47
- package/build/index.mjs +182 -48
- package/build/index.mjs.map +1 -1
- package/build/types/custom/ArraySchema.d.ts +9 -1
- package/build/types/custom/MapSchema.d.ts +18 -1
- package/package.json +9 -3
- package/src/annotations.ts +24 -0
- package/src/codegen/cli.ts +4 -4
- package/src/codegen/parser.ts +50 -1
- package/src/decoder/DecodeOperation.ts +23 -7
- package/src/encoder/ChangeTree.ts +18 -10
- package/src/encoder/EncodeOperation.ts +14 -1
- package/src/index.ts +1 -0
- package/src/types/custom/ArraySchema.ts +77 -30
- package/src/types/custom/MapSchema.ts +34 -1
package/README.md
CHANGED
|
@@ -20,7 +20,33 @@
|
|
|
20
20
|
|
|
21
21
|
## Schema definition
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Define synchronizable structures with `schema()` and `t.*` field builders:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { schema, t, type SchemaType } from '@colyseus/schema';
|
|
27
|
+
|
|
28
|
+
export const Player = schema({
|
|
29
|
+
name: t.string(),
|
|
30
|
+
x: t.number(),
|
|
31
|
+
y: t.number(),
|
|
32
|
+
}, "Player");
|
|
33
|
+
export type Player = SchemaType<typeof Player>;
|
|
34
|
+
|
|
35
|
+
export const MyState = schema({
|
|
36
|
+
fieldString: t.string(),
|
|
37
|
+
fieldNumber: t.number(),
|
|
38
|
+
player: Player,
|
|
39
|
+
arrayOfPlayers: t.array(Player),
|
|
40
|
+
mapOfPlayers: t.map(Player),
|
|
41
|
+
}, "MyState");
|
|
42
|
+
export type MyState = SchemaType<typeof MyState>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`schema()` returns a real class (`instanceof` works) and runs in plain JavaScript and TypeScript alike, with no compiler configuration. The `type` aliases are TypeScript-only sugar — omit them in plain JS.
|
|
46
|
+
|
|
47
|
+
### Decorators (still supported)
|
|
48
|
+
|
|
49
|
+
The classic `@type()` decorator style remains fully supported, and both styles produce the identical wire format:
|
|
24
50
|
|
|
25
51
|
```typescript
|
|
26
52
|
import { Schema, type, ArraySchema, MapSchema } from '@colyseus/schema';
|
|
@@ -30,16 +56,30 @@ export class Player extends Schema {
|
|
|
30
56
|
@type("number") x: number;
|
|
31
57
|
@type("number") y: number;
|
|
32
58
|
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
We are moving away from decorators due to ecosystem compatibility issues: they depend on the legacy `experimentalDecorators` implementation and `useDefineForClassFields: false`, which conflict with modern toolchain defaults (esbuild, SWC, Vite), diverge from the TC39 decorators specification, and aren't available in plain JavaScript. See the [Decorators reference](https://docs.colyseus.io/state/schema/decorators) for setup and the full decorator documentation.
|
|
62
|
+
|
|
63
|
+
## TypeScript support
|
|
64
|
+
|
|
65
|
+
Compatible with TypeScript **5.x**, **6.x** and **7.x**.
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
The `@type()` decorator uses legacy decorators — enable them in your `tsconfig.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"compilerOptions": {
|
|
72
|
+
"experimentalDecorators": true,
|
|
73
|
+
"useDefineForClassFields": false
|
|
74
|
+
}
|
|
40
75
|
}
|
|
41
76
|
```
|
|
42
77
|
|
|
78
|
+
> **Note:** `schema-codegen` requires TypeScript 5.x or 6.x installed in your
|
|
79
|
+
> project — TypeScript 7's native compiler no longer ships the JS compiler API
|
|
80
|
+
> that codegen uses to parse your schema files. The runtime and your build are
|
|
81
|
+
> not affected by this.
|
|
82
|
+
|
|
43
83
|
## Supported types
|
|
44
84
|
|
|
45
85
|
### Primitive Types
|
|
@@ -62,28 +102,25 @@ export class MyState extends Schema {
|
|
|
62
102
|
|
|
63
103
|
### Declaration:
|
|
64
104
|
|
|
105
|
+
Each primitive type is declared through its `t.*` factory (`t.string()`, `t.uint8()`, …). The string names above remain valid as collection child types (`t.array("string")`).
|
|
106
|
+
|
|
65
107
|
#### Primitive types (`string`, `number`, `boolean`, etc)
|
|
66
108
|
|
|
67
109
|
```typescript
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@type("int32")
|
|
72
|
-
name: number;
|
|
110
|
+
name: t.string(),
|
|
111
|
+
health: t.int32(),
|
|
73
112
|
```
|
|
74
113
|
|
|
75
114
|
#### Child `Schema` structures
|
|
76
115
|
|
|
77
116
|
```typescript
|
|
78
|
-
|
|
79
|
-
player: Player;
|
|
117
|
+
player: Player, // shorthand for t.ref(Player)
|
|
80
118
|
```
|
|
81
119
|
|
|
82
120
|
#### Array of `Schema` structure
|
|
83
121
|
|
|
84
122
|
```typescript
|
|
85
|
-
|
|
86
|
-
arrayOfPlayers: ArraySchema<Player>;
|
|
123
|
+
arrayOfPlayers: t.array(Player),
|
|
87
124
|
```
|
|
88
125
|
|
|
89
126
|
#### Array of a primitive type
|
|
@@ -91,18 +128,14 @@ arrayOfPlayers: ArraySchema<Player>;
|
|
|
91
128
|
You can't mix types inside arrays.
|
|
92
129
|
|
|
93
130
|
```typescript
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@type([ "string" ])
|
|
98
|
-
arrayOfStrings: ArraySchema<string>;
|
|
131
|
+
arrayOfNumbers: t.array("number"),
|
|
132
|
+
arrayOfStrings: t.array("string"),
|
|
99
133
|
```
|
|
100
134
|
|
|
101
135
|
#### Map of `Schema` structure
|
|
102
136
|
|
|
103
137
|
```typescript
|
|
104
|
-
|
|
105
|
-
mapOfPlayers: MapSchema<Player>;
|
|
138
|
+
mapOfPlayers: t.map(Player),
|
|
106
139
|
```
|
|
107
140
|
|
|
108
141
|
#### Map of a primitive type
|
|
@@ -110,11 +143,8 @@ mapOfPlayers: MapSchema<Player>;
|
|
|
110
143
|
You can't mix primitive types inside maps.
|
|
111
144
|
|
|
112
145
|
```typescript
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@type({ map: "string" })
|
|
117
|
-
mapOfStrings: MapSchema<string>;
|
|
146
|
+
mapOfNumbers: t.map("number"),
|
|
147
|
+
mapOfStrings: t.map("string"),
|
|
118
148
|
```
|
|
119
149
|
|
|
120
150
|
### Reflection
|
|
@@ -124,35 +154,38 @@ definition implementation in the server-side, and just send the encoded
|
|
|
124
154
|
reflection to the client-side, for example:
|
|
125
155
|
|
|
126
156
|
```typescript
|
|
127
|
-
import {
|
|
157
|
+
import { schema, t, Encoder, Reflection } from "@colyseus/schema";
|
|
128
158
|
|
|
129
|
-
|
|
130
|
-
|
|
159
|
+
const MyState = schema({
|
|
160
|
+
currentTurn: t.string(),
|
|
131
161
|
// ... more definitions
|
|
132
|
-
}
|
|
162
|
+
}, "MyState");
|
|
133
163
|
|
|
134
|
-
//
|
|
135
|
-
const
|
|
164
|
+
// server-side: encode the schema definition itself
|
|
165
|
+
const encoder = new Encoder(new MyState());
|
|
166
|
+
const encodedStateSchema = Reflection.encode(encoder);
|
|
167
|
+
// ... send `encodedStateSchema` across the network
|
|
136
168
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
169
|
+
// client-side: rebuild the state without having its definition
|
|
170
|
+
const decoder = Reflection.decode(encodedStateSchema);
|
|
171
|
+
const myState = decoder.state;
|
|
139
172
|
```
|
|
140
173
|
|
|
141
|
-
### `StateView` /
|
|
174
|
+
### `StateView` / `.view()`
|
|
142
175
|
|
|
143
|
-
You can use
|
|
176
|
+
You can use the `.view()` field modifier to filter properties that should be sent only to `StateView`'s that have access to it.
|
|
144
177
|
|
|
145
178
|
```typescript
|
|
146
|
-
import {
|
|
179
|
+
import { schema, t } from "@colyseus/schema";
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
181
|
+
const Player = schema({
|
|
182
|
+
secret: t.string().view(),
|
|
183
|
+
notSecret: t.string(),
|
|
184
|
+
}, "Player");
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
186
|
+
const MyState = schema({
|
|
187
|
+
players: t.map(Player),
|
|
188
|
+
}, "MyState");
|
|
156
189
|
```
|
|
157
190
|
|
|
158
191
|
Using the `StateView`
|
|
@@ -168,7 +201,7 @@ There are 3 major features of the `Encoder` class:
|
|
|
168
201
|
|
|
169
202
|
- Encoding the full state
|
|
170
203
|
- Encoding the state changes
|
|
171
|
-
- Encoding state with filters (properties
|
|
204
|
+
- Encoding state with filters (properties tagged with `.view()`)
|
|
172
205
|
|
|
173
206
|
```typescript
|
|
174
207
|
import { Encoder } from "@colyseus/schema";
|
|
@@ -193,7 +226,7 @@ const changesBuffer = encoder.encode();
|
|
|
193
226
|
|
|
194
227
|
### Encoding with views
|
|
195
228
|
|
|
196
|
-
When using
|
|
229
|
+
When using `.view()` and `StateView`'s, a single "full encode" must be used for multiple views. Each view also must add its own changes.
|
|
197
230
|
|
|
198
231
|
```typescript
|
|
199
232
|
// shared buffer iterator
|
|
@@ -250,7 +283,7 @@ decoder.decode(encodedBytes);
|
|
|
250
283
|
|
|
251
284
|
Backwards/forwards compatibility is possible by declaring new fields at the
|
|
252
285
|
end of existing structures, and earlier declarations to not be removed, but
|
|
253
|
-
be marked
|
|
286
|
+
be marked `.deprecated()` when needed.
|
|
254
287
|
|
|
255
288
|
This is particularly useful for native-compiled targets, such as C#, C++,
|
|
256
289
|
Haxe, etc - where the client-side can potentially not have the most
|
|
@@ -260,7 +293,7 @@ up-to-date version of the schema definitions.
|
|
|
260
293
|
## Limitations and best practices
|
|
261
294
|
|
|
262
295
|
- Each `Schema` structure can hold up to `64` fields. If you need more fields, use nested structures.
|
|
263
|
-
- Fields tagged with
|
|
296
|
+
- Fields tagged with `.view()`, `.unreliable()`, or `.static()` at field indexes `≥ 32` use a slower per-mutation classification path (linear scan over the tagged-field list instead of a single bitwise op). For schemas with more than 32 fields, declare frequently-mutated tagged fields earlier so they fall in the bitmask fast path.
|
|
264
297
|
- Schemas with `≤ 8` fields store per-field operation bytes inline in two numbers (no allocation per instance). Schemas with `> 8` fields allocate a small `Uint8Array` per instance for op storage. The difference is only material when allocating thousands of instances per tick — prefer narrower nested structures in that regime.
|
|
265
298
|
- `NaN` or `null` numbers are encoded as `0`
|
|
266
299
|
- `null` strings are encoded as `""`
|
|
@@ -276,7 +309,10 @@ up-to-date version of the schema definitions.
|
|
|
276
309
|
> If you're using JavaScript or LUA, there's no need to bother about this.
|
|
277
310
|
> Interpreted programming languages are able to re-build the Schema locally through the use of `Reflection`.
|
|
278
311
|
|
|
279
|
-
You can generate the client-side schema files based on
|
|
312
|
+
You can generate the client-side schema files based on your server-side schema definitions automatically — both `schema()` and decorator styles are supported.
|
|
313
|
+
|
|
314
|
+
> `schema-codegen` requires TypeScript 5.x or 6.x installed in your project
|
|
315
|
+
> (TypeScript 7+ no longer ships the JS compiler API it uses for parsing).
|
|
280
316
|
|
|
281
317
|
```
|
|
282
318
|
# C#/Unity
|
package/build/annotations.d.ts
CHANGED
|
@@ -103,6 +103,14 @@ export declare function getPropertyDescriptor(fieldName: string, fieldIndex: num
|
|
|
103
103
|
* The previous `@type()` annotation should remain along with this one.
|
|
104
104
|
*/
|
|
105
105
|
export declare function deprecated(throws?: boolean): PropertyDecorator;
|
|
106
|
+
/**
|
|
107
|
+
* Adds synchronizable fields to an existing `Schema` subclass — the pre-5.0
|
|
108
|
+
* helper for plain JavaScript users.
|
|
109
|
+
*
|
|
110
|
+
* @deprecated Use `schema()` with `t.*` field builders instead:
|
|
111
|
+
* https://docs.colyseus.io/state/schema
|
|
112
|
+
*/
|
|
113
|
+
export declare function defineTypes(target: typeof Schema, fields: Definition, options?: TypeOptions): typeof Schema;
|
|
106
114
|
type ExtractInitProps<T> = T extends {
|
|
107
115
|
initialize: (...args: infer P) => void;
|
|
108
116
|
} ? P extends readonly [] ? never : P extends readonly [infer First] ? First extends object ? First : P : P : BuilderInitProps<T>;
|
package/build/codegen/cli.cjs
CHANGED
|
@@ -209,6 +209,7 @@ function getInheritanceTree(klass, allClasses, includeSelf = true) {
|
|
|
209
209
|
let currentStructure;
|
|
210
210
|
let currentProperty;
|
|
211
211
|
let globalContext;
|
|
212
|
+
let defineTypesWarned = false;
|
|
212
213
|
const BUILDER_COLLECTION_KINDS = new Set(["array", "map", "set", "collection"]);
|
|
213
214
|
/**
|
|
214
215
|
* For a t.*().chain().calls() expression, walk down to the base `t.X(...)`
|
|
@@ -419,6 +420,34 @@ function inspectNode(node, context, decoratorName) {
|
|
|
419
420
|
defineProperty(property, prop.initializer);
|
|
420
421
|
}
|
|
421
422
|
}
|
|
423
|
+
else if (node.getText() === "defineTypes" &&
|
|
424
|
+
(node.parent.kind === ts__namespace.SyntaxKind.CallExpression ||
|
|
425
|
+
node.parent.kind === ts__namespace.SyntaxKind.PropertyAccessExpression)) {
|
|
426
|
+
/**
|
|
427
|
+
* JavaScript source file (`.js`)
|
|
428
|
+
* Using `defineTypes()` (deprecated)
|
|
429
|
+
*/
|
|
430
|
+
const callExpression = (node.parent.kind === ts__namespace.SyntaxKind.PropertyAccessExpression)
|
|
431
|
+
? node.parent.parent
|
|
432
|
+
: node.parent;
|
|
433
|
+
if (callExpression.kind !== ts__namespace.SyntaxKind.CallExpression) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (!defineTypesWarned) {
|
|
437
|
+
defineTypesWarned = true;
|
|
438
|
+
console.warn("schema-codegen: defineTypes() is deprecated and will be removed in a future release. Use schema() with t.* field builders instead → https://docs.colyseus.io/state/schema");
|
|
439
|
+
}
|
|
440
|
+
const className = callExpression.arguments[0].getText();
|
|
441
|
+
currentStructure.name = className;
|
|
442
|
+
const types = callExpression.arguments[1];
|
|
443
|
+
for (let i = 0; i < types.properties.length; i++) {
|
|
444
|
+
const prop = types.properties[i];
|
|
445
|
+
const property = currentProperty || new Property();
|
|
446
|
+
property.name = prop.name.escapedText;
|
|
447
|
+
currentStructure.addProperty(property);
|
|
448
|
+
defineProperty(property, prop.initializer);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
422
451
|
if (node.parent.kind === ts__namespace.SyntaxKind.ClassDeclaration) {
|
|
423
452
|
currentStructure.name = node.getText();
|
|
424
453
|
}
|
|
@@ -522,6 +551,11 @@ function inspectNode(node, context, decoratorName) {
|
|
|
522
551
|
}
|
|
523
552
|
let parsedFiles;
|
|
524
553
|
function parseFiles(fileNames, decoratorName = "type", context = new Context()) {
|
|
554
|
+
if (typeof ts__namespace.createSourceFile !== "function") {
|
|
555
|
+
// typescript@7+ (native) no longer ships the JS compiler API
|
|
556
|
+
throw new Error(`schema-codegen requires the TypeScript compiler API, which the installed "typescript@${ts__namespace.version}" package does not provide.\n` +
|
|
557
|
+
`TypeScript 7+ no longer ships the JS compiler API — install typescript 5.x or 6.x (e.g. \`npm install --save-dev typescript@6\`) to use schema-codegen.`);
|
|
558
|
+
}
|
|
525
559
|
/**
|
|
526
560
|
* Re-set globalContext for each test case
|
|
527
561
|
*/
|
|
@@ -558,7 +592,10 @@ function parseFiles(fileNames, decoratorName = "type", context = new Context())
|
|
|
558
592
|
break;
|
|
559
593
|
}
|
|
560
594
|
catch (e) {
|
|
561
|
-
//
|
|
595
|
+
// only swallow fs errors (ENOENT/EISDIR) while probing alternatives
|
|
596
|
+
if (!e?.code) {
|
|
597
|
+
throw e;
|
|
598
|
+
}
|
|
562
599
|
}
|
|
563
600
|
}
|
|
564
601
|
if (sourceFile) {
|
|
@@ -2336,7 +2373,7 @@ function recursiveFiles(dir) {
|
|
|
2336
2373
|
return collect;
|
|
2337
2374
|
}
|
|
2338
2375
|
|
|
2339
|
-
function displayHelp() {
|
|
2376
|
+
function displayHelp(exitCode = 0) {
|
|
2340
2377
|
console.log(`\nschema-codegen [path/to/Schema.ts]
|
|
2341
2378
|
|
|
2342
2379
|
Usage (C#/Unity)
|
|
@@ -2355,7 +2392,7 @@ ${Object.
|
|
|
2355
2392
|
Optional:
|
|
2356
2393
|
--namespace: generate namespace on output code
|
|
2357
2394
|
--decorator: custom name for @type decorator to scan for`);
|
|
2358
|
-
process.exit();
|
|
2395
|
+
process.exit(exitCode);
|
|
2359
2396
|
}
|
|
2360
2397
|
const args = argv(process.argv.slice(2));
|
|
2361
2398
|
if (args.help) {
|
|
@@ -2369,7 +2406,7 @@ for (let target in generators) {
|
|
|
2369
2406
|
}
|
|
2370
2407
|
if (!args.output) {
|
|
2371
2408
|
console.error("You must provide a valid --output directory.");
|
|
2372
|
-
displayHelp();
|
|
2409
|
+
displayHelp(1);
|
|
2373
2410
|
}
|
|
2374
2411
|
try {
|
|
2375
2412
|
args.files = args._;
|
|
@@ -2384,6 +2421,6 @@ try {
|
|
|
2384
2421
|
catch (e) {
|
|
2385
2422
|
console.error(e.message);
|
|
2386
2423
|
console.error(e.stack);
|
|
2387
|
-
displayHelp();
|
|
2424
|
+
displayHelp(1);
|
|
2388
2425
|
}
|
|
2389
2426
|
//# sourceMappingURL=cli.cjs.map
|