@colyseus/schema 5.0.11 → 5.0.13
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/build/Metadata.d.ts +20 -12
- package/build/annotations.d.ts +23 -10
- package/build/codegen/cli.cjs +615 -204
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/languages/dart.d.ts +20 -0
- package/build/codegen/types.d.ts +20 -0
- package/build/decoder/Resync.d.ts +3 -3
- package/build/encoder/ChangeTree.d.ts +25 -9
- package/build/encoder/EncodeDescriptor.d.ts +11 -12
- package/build/encoder/StateView.d.ts +26 -2
- package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
- package/build/encoder/changeTree/parentChain.d.ts +9 -0
- package/build/encoder/streaming.d.ts +7 -0
- package/build/index.cjs +449 -233
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.js +449 -233
- package/build/index.mjs +448 -232
- package/build/index.mjs.map +1 -1
- package/build/types/builder.d.ts +31 -22
- package/build/types/custom/ArraySchema.d.ts +17 -0
- package/build/types/custom/StreamSchema.d.ts +1 -1
- package/build/types/symbols.d.ts +4 -10
- package/package.json +1 -1
- package/src/Metadata.ts +58 -31
- package/src/annotations.ts +56 -32
- package/src/codegen/api.ts +2 -1
- package/src/codegen/languages/c.ts +21 -3
- package/src/codegen/languages/csharp.ts +7 -1
- package/src/codegen/languages/dart.ts +274 -0
- package/src/codegen/languages/haxe.ts +7 -1
- package/src/codegen/languages/lua.ts +16 -4
- package/src/codegen/languages/ts.ts +5 -0
- package/src/codegen/parser.ts +97 -3
- package/src/codegen/types.ts +24 -0
- package/src/decoder/Resync.ts +8 -8
- package/src/encoder/ChangeRecorder.ts +1 -1
- package/src/encoder/ChangeTree.ts +46 -26
- package/src/encoder/EncodeDescriptor.ts +17 -38
- package/src/encoder/EncodeOperation.ts +3 -1
- package/src/encoder/Encoder.ts +97 -21
- package/src/encoder/Root.ts +18 -20
- package/src/encoder/StateView.ts +102 -12
- package/src/encoder/changeTree/inheritedFlags.ts +10 -10
- package/src/encoder/changeTree/liveIteration.ts +9 -9
- package/src/encoder/changeTree/parentChain.ts +29 -0
- package/src/encoder/streaming.ts +8 -0
- package/src/encoding/spec.ts +1 -1
- package/src/index.ts +2 -2
- package/src/types/builder.ts +35 -31
- package/src/types/custom/ArraySchema.ts +40 -1
- package/src/types/custom/StreamSchema.ts +1 -1
- package/src/types/symbols.ts +4 -11
- package/src/bench_bloat.ts +0 -173
- package/src/bench_churn.ts +0 -121
- package/src/bench_decode.ts +0 -221
- package/src/bench_decode_mem.ts +0 -165
- package/src/bench_encode.ts +0 -108
- package/src/bench_init.ts +0 -150
- package/src/bench_static.ts +0 -109
- package/src/bench_stream.ts +0 -295
- package/src/bench_view_cmp.ts +0 -142
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Class,
|
|
3
|
+
Property,
|
|
4
|
+
File,
|
|
5
|
+
getCommentHeader,
|
|
6
|
+
Interface,
|
|
7
|
+
Enum,
|
|
8
|
+
Context,
|
|
9
|
+
} from "../types.js";
|
|
10
|
+
import { GenerateOptions } from "../api.js";
|
|
11
|
+
|
|
12
|
+
export const name = "Dart/Flutter";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Dart types for interface (plain message) properties. Schema scalar getters
|
|
16
|
+
* don't use this table: the `colyseus` package reads every numeric field as
|
|
17
|
+
* `double` through `SchemaView`, so all numeric schema types collapse there.
|
|
18
|
+
*/
|
|
19
|
+
const typeMaps: { [key: string]: string } = {
|
|
20
|
+
"string": "String",
|
|
21
|
+
"number": "double",
|
|
22
|
+
"boolean": "bool",
|
|
23
|
+
"int8": "double",
|
|
24
|
+
"uint8": "double",
|
|
25
|
+
"int16": "double",
|
|
26
|
+
"uint16": "double",
|
|
27
|
+
"int32": "double",
|
|
28
|
+
"uint32": "double",
|
|
29
|
+
"int64": "double",
|
|
30
|
+
"uint64": "double",
|
|
31
|
+
"float32": "double",
|
|
32
|
+
"float64": "double",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const enumNames = new Set<string>();
|
|
36
|
+
|
|
37
|
+
const COMMON_IMPORTS = `import 'package:colyseus/colyseus.dart';`;
|
|
38
|
+
|
|
39
|
+
// Field names come from the server schema and may not be lowerCamelCase.
|
|
40
|
+
const LINT_HEADER = `// ignore_for_file: non_constant_identifier_names, constant_identifier_names`;
|
|
41
|
+
|
|
42
|
+
const distinct = (value: string, index: number, self: string[]) =>
|
|
43
|
+
self.indexOf(value) === index;
|
|
44
|
+
|
|
45
|
+
const isSchemaType = (childType: string) =>
|
|
46
|
+
childType !== undefined && /^[A-Z]/.test(childType) && !enumNames.has(childType);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Dart Code Generator
|
|
50
|
+
*
|
|
51
|
+
* Emits typed façades over the `colyseus` Flutter package's runtime: one
|
|
52
|
+
* `SchemaRef` subclass per schema, with typed getters over the shared native
|
|
53
|
+
* handle. Collection getters return `MapSchema<T>` / `ArraySchema<T>`, which
|
|
54
|
+
* also carry the field they came from — that is what
|
|
55
|
+
* `callbacks.onAdd(state.players, ...)` registers against.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate individual files for each class/interface/enum
|
|
60
|
+
*/
|
|
61
|
+
export function generate(context: Context, options: GenerateOptions): File[] {
|
|
62
|
+
context.enums.forEach((structure) => enumNames.add(structure.name));
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
...context.classes.map(klass => ({
|
|
66
|
+
name: `${klass.name}.dart`,
|
|
67
|
+
content: generateClass(klass, context.classes)
|
|
68
|
+
})),
|
|
69
|
+
...context.interfaces.map(structure => ({
|
|
70
|
+
name: `${structure.name}.dart`,
|
|
71
|
+
content: generateInterface(structure),
|
|
72
|
+
})),
|
|
73
|
+
...context.enums.filter(structure => structure.name !== 'OPERATION').map((structure) => ({
|
|
74
|
+
name: `${structure.name}.dart`,
|
|
75
|
+
content: generateEnum(structure),
|
|
76
|
+
})),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate a single bundled file containing all classes, interfaces, and enums
|
|
82
|
+
*/
|
|
83
|
+
export function renderBundle(context: Context, options: GenerateOptions): File {
|
|
84
|
+
const fileName = options.namespace ? `${options.namespace}.dart` : "schema.dart";
|
|
85
|
+
|
|
86
|
+
context.enums.forEach((structure) => enumNames.add(structure.name));
|
|
87
|
+
|
|
88
|
+
const bodies = [
|
|
89
|
+
...context.classes.map(klass => generateClassBody(klass, context.classes)),
|
|
90
|
+
...context.interfaces.map(iface => generateInterfaceBody(iface)),
|
|
91
|
+
...context.enums
|
|
92
|
+
.filter(structure => structure.name !== 'OPERATION')
|
|
93
|
+
.map(e => generateEnumBody(e)),
|
|
94
|
+
].join("\n\n");
|
|
95
|
+
|
|
96
|
+
const content = `${getCommentHeader()}
|
|
97
|
+
${LINT_HEADER}
|
|
98
|
+
|
|
99
|
+
${COMMON_IMPORTS}
|
|
100
|
+
|
|
101
|
+
${bodies}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
return { name: fileName, content };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate just the class body (without imports) for bundling
|
|
109
|
+
*/
|
|
110
|
+
function generateClassBody(klass: Class, allClasses: Class[]): string {
|
|
111
|
+
// `SchemaRef` is a `base` class, so subclasses carry a modifier: `base`
|
|
112
|
+
// when the class is itself extended (extendable from any file), `final`
|
|
113
|
+
// otherwise.
|
|
114
|
+
const isExtended = allClasses.some(other => other.extends === klass.name);
|
|
115
|
+
const modifier = isExtended ? "base" : "final";
|
|
116
|
+
const parent = (klass.extends === "Schema") ? "SchemaRef" : klass.extends;
|
|
117
|
+
|
|
118
|
+
const getters = klass.properties
|
|
119
|
+
.map(prop => generateGetter(prop))
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join("\n");
|
|
122
|
+
|
|
123
|
+
return `${modifier} class ${klass.name} extends ${parent} {
|
|
124
|
+
${klass.name}(super.handle);
|
|
125
|
+
|
|
126
|
+
${getters}
|
|
127
|
+
}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate a complete class file with imports (for individual file mode)
|
|
132
|
+
*/
|
|
133
|
+
function generateClass(klass: Class, allClasses: Class[]) {
|
|
134
|
+
const localRefs = klass.properties
|
|
135
|
+
.filter(prop => isSchemaType(prop.childType))
|
|
136
|
+
.map(prop => prop.childType)
|
|
137
|
+
.concat(klass.extends !== "Schema" ? [klass.extends] : [])
|
|
138
|
+
.filter(distinct)
|
|
139
|
+
.filter(ref => ref !== klass.name)
|
|
140
|
+
.map(ref => `import '${ref}.dart';`)
|
|
141
|
+
.join("\n");
|
|
142
|
+
|
|
143
|
+
return `${getCommentHeader()}
|
|
144
|
+
${LINT_HEADER}
|
|
145
|
+
|
|
146
|
+
${COMMON_IMPORTS}
|
|
147
|
+
${localRefs ? localRefs + "\n" : ""}
|
|
148
|
+
${generateClassBody(klass, allClasses)}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The Dart type a scalar schema field reads as, or undefined when the field
|
|
154
|
+
* can only be read dynamically (enum-typed and unknown types).
|
|
155
|
+
*/
|
|
156
|
+
function scalarDartType(type: string): string | undefined {
|
|
157
|
+
if (type === "string") { return "String"; }
|
|
158
|
+
if (type === "boolean") { return "bool"; }
|
|
159
|
+
if (typeMaps[type] === "double" || type === "quantized" || type === "number") { return "double"; }
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function generateGetter(prop: Property): string {
|
|
164
|
+
const deprecation = (prop.deprecated)
|
|
165
|
+
? ` @Deprecated("field '${prop.name}' is deprecated.")\n`
|
|
166
|
+
: '';
|
|
167
|
+
|
|
168
|
+
let body: string;
|
|
169
|
+
|
|
170
|
+
if (prop.childType && isSchemaType(prop.childType)) {
|
|
171
|
+
if (prop.type === "ref") {
|
|
172
|
+
body = ` ${prop.childType}? get ${prop.name} => refOf('${prop.name}', ${prop.childType}.new);`;
|
|
173
|
+
} else if (prop.type === "map") {
|
|
174
|
+
body = ` MapSchema<${prop.childType}> get ${prop.name} => mapOf('${prop.name}', ${prop.childType}.new);`;
|
|
175
|
+
} else {
|
|
176
|
+
body = ` ArraySchema<${prop.childType}> get ${prop.name} => arrayOf('${prop.name}', ${prop.childType}.new);`;
|
|
177
|
+
}
|
|
178
|
+
} else if (prop.childType) {
|
|
179
|
+
const child = typeMaps[prop.childType] ?? "dynamic";
|
|
180
|
+
if (prop.type === "map") {
|
|
181
|
+
body = ` MapSchema<${child}> get ${prop.name} => primitiveMapOf('${prop.name}');`;
|
|
182
|
+
} else if (prop.type === "array") {
|
|
183
|
+
body = ` ArraySchema<${child}> get ${prop.name} => primitiveArrayOf('${prop.name}');`;
|
|
184
|
+
} else {
|
|
185
|
+
// A "ref" with a primitive child has no typed shape to offer.
|
|
186
|
+
body = ` dynamic get ${prop.name} => this['${prop.name}'];`;
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const dartType = scalarDartType(prop.type);
|
|
190
|
+
if (dartType === "String") {
|
|
191
|
+
body = ` String get ${prop.name} => view.getString('${prop.name}') ?? '';`;
|
|
192
|
+
} else if (dartType === "bool") {
|
|
193
|
+
body = ` bool get ${prop.name} => view.getBool('${prop.name}');`;
|
|
194
|
+
} else if (dartType === "double") {
|
|
195
|
+
body = ` double get ${prop.name} => view['${prop.name}'];`;
|
|
196
|
+
} else {
|
|
197
|
+
// Enum-typed or unknown: read through the untyped accessor.
|
|
198
|
+
body = ` dynamic get ${prop.name} => this['${prop.name}'];`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return deprecation + body;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate just the interface body for bundling
|
|
207
|
+
*/
|
|
208
|
+
function generateInterfaceBody(struct: Interface): string {
|
|
209
|
+
const fields = struct.properties
|
|
210
|
+
.map(prop => ` ${getInterfaceType(prop)}? ${prop.name};`)
|
|
211
|
+
.join("\n");
|
|
212
|
+
|
|
213
|
+
return `class ${struct.name} {
|
|
214
|
+
${fields}
|
|
215
|
+
}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generate a complete interface file (for individual file mode)
|
|
220
|
+
*/
|
|
221
|
+
function generateInterface(struct: Interface) {
|
|
222
|
+
const localRefs = struct.properties
|
|
223
|
+
.filter(prop => isSchemaType(prop.childType ?? (typeMaps[prop.type] ? undefined : prop.type)))
|
|
224
|
+
.map(prop => prop.childType ?? prop.type)
|
|
225
|
+
.filter(distinct)
|
|
226
|
+
.map(ref => `import '${ref}.dart';`)
|
|
227
|
+
.join("\n");
|
|
228
|
+
|
|
229
|
+
return `${getCommentHeader()}
|
|
230
|
+
${LINT_HEADER}
|
|
231
|
+
${localRefs ? "\n" + localRefs + "\n" : ""}
|
|
232
|
+
${generateInterfaceBody(struct)}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getInterfaceType(prop: Property): string {
|
|
237
|
+
if (prop.type === "array") {
|
|
238
|
+
return `List<${typeMaps[prop.childType] ?? prop.childType ?? "dynamic"}>`;
|
|
239
|
+
}
|
|
240
|
+
return typeMaps[prop.type] ?? prop.type ?? "dynamic";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate just the enum body for bundling: a namespace of consts, since
|
|
245
|
+
* Colyseus enums may carry string or float values Dart enums can't.
|
|
246
|
+
*/
|
|
247
|
+
function generateEnumBody(_enum: Enum): string {
|
|
248
|
+
const members = _enum.properties
|
|
249
|
+
.map((prop, i) => {
|
|
250
|
+
let value: string;
|
|
251
|
+
if (prop.type) {
|
|
252
|
+
value = isNaN(Number(prop.type)) ? `"${prop.type}"` : `${Number(prop.type)}`;
|
|
253
|
+
} else {
|
|
254
|
+
value = `${i}`;
|
|
255
|
+
}
|
|
256
|
+
return ` static const ${prop.name} = ${value};`;
|
|
257
|
+
})
|
|
258
|
+
.join("\n");
|
|
259
|
+
|
|
260
|
+
return `abstract final class ${_enum.name} {
|
|
261
|
+
${members}
|
|
262
|
+
}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate a complete enum file (for individual file mode)
|
|
267
|
+
*/
|
|
268
|
+
function generateEnum(_enum: Enum) {
|
|
269
|
+
return `${getCommentHeader()}
|
|
270
|
+
${LINT_HEADER}
|
|
271
|
+
|
|
272
|
+
${generateEnumBody(_enum)}
|
|
273
|
+
`;
|
|
274
|
+
}
|
|
@@ -94,7 +94,13 @@ function generateProperty(prop: Property) {
|
|
|
94
94
|
let initializer = "";
|
|
95
95
|
let typeArgs = `"${prop.type}"`;
|
|
96
96
|
|
|
97
|
-
if (prop.
|
|
97
|
+
if (prop.quantized) {
|
|
98
|
+
const q = prop.quantized;
|
|
99
|
+
typeArgs += `, {min: ${q.min}, max: ${q.max}, bits: ${q.bits}, mode: ${q.wrap ? 1 : 0}}`;
|
|
100
|
+
langType = "Float";
|
|
101
|
+
initializer = "0";
|
|
102
|
+
|
|
103
|
+
} else if (prop.childType) {
|
|
98
104
|
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
|
|
99
105
|
|
|
100
106
|
if (isUpcaseFirst) {
|
|
@@ -26,10 +26,14 @@ const typeMaps: { [key: string]: string } = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const COMMON_IMPORTS = `local schema = require 'colyseus.serializer.schema.schema'`;
|
|
29
|
+
const QUANTIZE_IMPORT = `local quantize = require 'colyseus.serializer.schema.quantize'`;
|
|
29
30
|
|
|
30
31
|
const distinct = (value: string, index: number, self: string[]) =>
|
|
31
32
|
self.indexOf(value) === index;
|
|
32
33
|
|
|
34
|
+
const hasQuantized = (classes: Class[]) =>
|
|
35
|
+
classes.some(klass => klass.properties.some(prop => prop.quantized));
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* Generate individual files for each class
|
|
35
39
|
*/
|
|
@@ -51,7 +55,7 @@ export function renderBundle(context: Context, options: GenerateOptions): File {
|
|
|
51
55
|
|
|
52
56
|
const content = `${getCommentHeader().replace(/\/\//mg, "--")}
|
|
53
57
|
|
|
54
|
-
${COMMON_IMPORTS}
|
|
58
|
+
${COMMON_IMPORTS}${hasQuantized(context.classes) ? `\n${QUANTIZE_IMPORT}` : ""}
|
|
55
59
|
|
|
56
60
|
${classBodies.join("\n\n")}
|
|
57
61
|
|
|
@@ -104,7 +108,7 @@ function generateClass(klass: Class, namespace: string, allClasses: Class[]) {
|
|
|
104
108
|
|
|
105
109
|
return `${getCommentHeader().replace(/\/\//mg, "--")}
|
|
106
110
|
|
|
107
|
-
${COMMON_IMPORTS}
|
|
111
|
+
${COMMON_IMPORTS}${hasQuantized([klass]) ? `\n${QUANTIZE_IMPORT}` : ""}
|
|
108
112
|
${localRequires}
|
|
109
113
|
|
|
110
114
|
${generateClassBody(klass)}
|
|
@@ -116,7 +120,12 @@ return ${klass.name}
|
|
|
116
120
|
function generatePropertyDeclaration(prop: Property) {
|
|
117
121
|
let typeArgs: string;
|
|
118
122
|
|
|
119
|
-
if (prop.
|
|
123
|
+
if (prop.quantized) {
|
|
124
|
+
// resolve at class-definition time — the decoder expects `.wire`/`.span`
|
|
125
|
+
const q = prop.quantized;
|
|
126
|
+
typeArgs = `{ quantized = quantize.resolve({ min = ${q.min}, max = ${q.max}, bits = ${q.bits}, mode = ${q.wrap ? 1 : 0} }) }`;
|
|
127
|
+
|
|
128
|
+
} else if (prop.childType) {
|
|
120
129
|
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
|
|
121
130
|
|
|
122
131
|
if (isUpcaseFirst) {
|
|
@@ -145,7 +154,10 @@ function generatePropertyDeclaration(prop: Property) {
|
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
function getLUATypeAnnotation(prop: Property) {
|
|
148
|
-
if (prop.type === "
|
|
157
|
+
if (prop.type === "quantized") {
|
|
158
|
+
return "number";
|
|
159
|
+
|
|
160
|
+
} else if (prop.type === "ref") {
|
|
149
161
|
return prop.childType;
|
|
150
162
|
|
|
151
163
|
} else if (prop.type === "array") {
|
|
@@ -158,6 +158,11 @@ function generateProperty(prop: Property) {
|
|
|
158
158
|
: `{ set: "${prop.childType}" }`;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
} else if (prop.quantized) {
|
|
162
|
+
const q = prop.quantized;
|
|
163
|
+
langType = "number";
|
|
164
|
+
typeArgs = `{ quantized: { min: ${q.min}, max: ${q.max}, bits: ${q.bits}${q.wrap ? `, mode: "wrap"` : ""} } }`;
|
|
165
|
+
|
|
161
166
|
} else {
|
|
162
167
|
langType = typeMaps[prop.type];
|
|
163
168
|
typeArgs = `"${prop.type}"`;
|
package/src/codegen/parser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
|
-
import { IStructure, Class, Interface, Property, Context, Enum } from "./types.js";
|
|
4
|
+
import { IStructure, Class, Interface, Property, Context, Enum, QuantizedProperty } from "./types.js";
|
|
5
5
|
|
|
6
6
|
let currentStructure: IStructure;
|
|
7
7
|
let currentProperty: Property;
|
|
@@ -36,6 +36,91 @@ function extractBuilderBase(node: ts.CallExpression): { methodName: string, firs
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Statically evaluate a numeric option expression. Codegen has no runtime, so
|
|
41
|
+
* only constant arithmetic is supported: literals, unary +/-, `Math.PI`-style
|
|
42
|
+
* constants and add/sub/mul/div combinations of those (e.g. `Math.PI * 2`).
|
|
43
|
+
* Returns
|
|
44
|
+
* undefined for anything it cannot resolve (a `const` reference, a call).
|
|
45
|
+
*/
|
|
46
|
+
function evalNumericExpression(node: ts.Expression): number | undefined {
|
|
47
|
+
if (ts.isNumericLiteral(node)) {
|
|
48
|
+
return Number(node.text);
|
|
49
|
+
}
|
|
50
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
51
|
+
return evalNumericExpression(node.expression);
|
|
52
|
+
}
|
|
53
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
54
|
+
const operand = evalNumericExpression(node.operand as ts.Expression);
|
|
55
|
+
if (operand === undefined) { return undefined; }
|
|
56
|
+
if (node.operator === ts.SyntaxKind.MinusToken) { return -operand; }
|
|
57
|
+
if (node.operator === ts.SyntaxKind.PlusToken) { return operand; }
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (ts.isPropertyAccessExpression(node) && node.expression.getText() === "Math") {
|
|
61
|
+
const constant = (Math as any)[node.name.text];
|
|
62
|
+
return (typeof constant === "number") ? constant : undefined;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isBinaryExpression(node)) {
|
|
65
|
+
const left = evalNumericExpression(node.left);
|
|
66
|
+
const right = evalNumericExpression(node.right);
|
|
67
|
+
if (left === undefined || right === undefined) { return undefined; }
|
|
68
|
+
switch (node.operatorToken.kind) {
|
|
69
|
+
case ts.SyntaxKind.PlusToken: return left + right;
|
|
70
|
+
case ts.SyntaxKind.MinusToken: return left - right;
|
|
71
|
+
case ts.SyntaxKind.AsteriskToken: return left * right;
|
|
72
|
+
case ts.SyntaxKind.SlashToken: return left / right;
|
|
73
|
+
default: return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract `{ min, max, bits?, mode? }` from a `t.quantized({...})` /
|
|
81
|
+
* `@type({ quantized: {...} })` object literal. Throws on anything codegen
|
|
82
|
+
* cannot statically resolve — silently dropping an option would generate a
|
|
83
|
+
* client that decodes every value of that field wrong.
|
|
84
|
+
*/
|
|
85
|
+
function parseQuantizedOptions(node: ts.Expression | undefined, propertyName: string): QuantizedProperty {
|
|
86
|
+
const fail = (reason: string): never => {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`schema-codegen: cannot statically resolve t.quantized() options of field '${propertyName}' — ${reason}. ` +
|
|
89
|
+
`Use literal numbers or constant Math expressions (e.g. \`Math.PI * 2\`).`
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!node || !ts.isObjectLiteralExpression(node)) {
|
|
94
|
+
return fail("expected an inline `{ min, max, ... }` object literal");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result: Partial<QuantizedProperty> & { mode?: string } = {};
|
|
98
|
+
for (const prop of node.properties) {
|
|
99
|
+
if (!ts.isPropertyAssignment(prop) || !prop.name) { continue; }
|
|
100
|
+
const key = (prop.name as ts.Identifier).text;
|
|
101
|
+
|
|
102
|
+
if (key === "mode") {
|
|
103
|
+
if (!ts.isStringLiteral(prop.initializer)) { return fail("`mode` must be a string literal"); }
|
|
104
|
+
result.mode = prop.initializer.text;
|
|
105
|
+
} else if (key === "min" || key === "max" || key === "bits") {
|
|
106
|
+
const value = evalNumericExpression(prop.initializer);
|
|
107
|
+
if (value === undefined) { return fail(`\`${key}\` is not a constant expression`); }
|
|
108
|
+
result[key] = value as any;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof result.min !== "number" || typeof result.max !== "number") {
|
|
113
|
+
return fail("`min` and `max` are required");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const bits = result.bits ?? 16;
|
|
117
|
+
if (bits !== 8 && bits !== 16 && bits !== 32) {
|
|
118
|
+
return fail("`bits` must be 8, 16 or 32");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { min: result.min, max: result.max, bits, wrap: result.mode === "wrap" };
|
|
122
|
+
}
|
|
123
|
+
|
|
39
124
|
function defineProperty(property: Property, initializer: any) {
|
|
40
125
|
// Builder-style: t.number(), t.array(Item), t.map(Item).view(), etc.
|
|
41
126
|
if (ts.isCallExpression(initializer)) {
|
|
@@ -51,6 +136,9 @@ function defineProperty(property: Property, initializer: any) {
|
|
|
51
136
|
if (base.firstArg) {
|
|
52
137
|
property.childType = (base.firstArg as any).text ?? base.firstArg.getText();
|
|
53
138
|
}
|
|
139
|
+
} else if (base.methodName === "quantized") {
|
|
140
|
+
property.type = "quantized";
|
|
141
|
+
property.quantized = parseQuantizedOptions(base.firstArg, property.name);
|
|
54
142
|
} else {
|
|
55
143
|
property.type = base.methodName;
|
|
56
144
|
}
|
|
@@ -63,8 +151,14 @@ function defineProperty(property: Property, initializer: any) {
|
|
|
63
151
|
property.childType = initializer.text;
|
|
64
152
|
|
|
65
153
|
} else if (initializer.kind == ts.SyntaxKind.ObjectLiteralExpression) {
|
|
66
|
-
|
|
67
|
-
|
|
154
|
+
if (initializer.properties[0].name.text === "quantized") {
|
|
155
|
+
// decorator-style: @type({ quantized: { min, max, ... } })
|
|
156
|
+
property.type = "quantized";
|
|
157
|
+
property.quantized = parseQuantizedOptions(initializer.properties[0].initializer, property.name);
|
|
158
|
+
} else {
|
|
159
|
+
property.type = initializer.properties[0].name.text;
|
|
160
|
+
property.childType = initializer.properties[0].initializer.text;
|
|
161
|
+
}
|
|
68
162
|
|
|
69
163
|
} else if (initializer.kind == ts.SyntaxKind.ArrayLiteralExpression) {
|
|
70
164
|
property.type = "array";
|
package/src/codegen/types.ts
CHANGED
|
@@ -125,11 +125,35 @@ export class Enum implements IStructure {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Statically-extracted `t.quantized()` options. `wrap` is already normalized
|
|
130
|
+
* from the source's `mode` string; emitters derive `range`/`span` via
|
|
131
|
+
* {@link resolveQuantized} so every language ships identical precomputed values.
|
|
132
|
+
*/
|
|
133
|
+
export interface QuantizedProperty {
|
|
134
|
+
min: number;
|
|
135
|
+
max: number;
|
|
136
|
+
bits: 8 | 16 | 32;
|
|
137
|
+
wrap: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Mirror of the runtime's `resolveQuantize()` scale math (wrap spreads 2^bits
|
|
142
|
+
* steps across [min,max); clamp maps the endpoints onto 0 and 2^bits-1).
|
|
143
|
+
*/
|
|
144
|
+
export function resolveQuantized(q: QuantizedProperty) {
|
|
145
|
+
return {
|
|
146
|
+
range: q.max - q.min,
|
|
147
|
+
span: q.wrap ? 2 ** q.bits : 2 ** q.bits - 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
export class Property {
|
|
129
152
|
index: number;
|
|
130
153
|
name: string;
|
|
131
154
|
type: string;
|
|
132
155
|
childType: string;
|
|
156
|
+
quantized?: QuantizedProperty;
|
|
133
157
|
deprecated?: boolean;
|
|
134
158
|
}
|
|
135
159
|
|
package/src/decoder/Resync.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { OPERATION } from "../encoding/spec.js";
|
|
2
2
|
import { Schema } from "../Schema.js";
|
|
3
|
-
import { $proxyTarget, $refId, $refTypeFieldIndexes, $resyncPrune, $
|
|
3
|
+
import { $proxyTarget, $refId, $refTypeFieldIndexes, $resyncPrune, $patchOnlyFieldIndexes } from "../types/symbols.js";
|
|
4
4
|
import type { Metadata } from "../Metadata.js";
|
|
5
5
|
import type { Decoder } from "./Decoder.js";
|
|
6
6
|
import type { DataChange } from "./DecodeOperation.js";
|
|
@@ -76,8 +76,8 @@ export function resyncTouchEntry(
|
|
|
76
76
|
/**
|
|
77
77
|
* Mark a collection as present in the payload — even with zero entries.
|
|
78
78
|
* The sweep only prunes collections reported here: absence means "not part
|
|
79
|
-
* of full-sync" (@
|
|
80
|
-
* live data. Reflected clients have no @
|
|
79
|
+
* of full-sync" (@patchOnly, view-invisible), where pruning would destroy
|
|
80
|
+
* live data. Reflected clients have no @patchOnly metadata, so payload
|
|
81
81
|
* presence is the only reliable signal.
|
|
82
82
|
*/
|
|
83
83
|
export function resyncMarkPresent(decoder: Decoder, refId: number) {
|
|
@@ -90,7 +90,7 @@ export function resyncMarkPresent(decoder: Decoder, refId: number) {
|
|
|
90
90
|
* entry the snapshot did not visit.
|
|
91
91
|
*
|
|
92
92
|
* Walks the tree from the root — NOT `root.refs` — for three reasons:
|
|
93
|
-
* `@
|
|
93
|
+
* `@patchOnly` fields are never part of a snapshot and must be left alone;
|
|
94
94
|
* entries of subtrees removed by the sweep itself are left to the GC's
|
|
95
95
|
* transitive walk (sweeping them directly would double-decrement shared
|
|
96
96
|
* children); and collections the snapshot never mentions (emptied
|
|
@@ -115,12 +115,12 @@ function sweepSchema(decoder: Decoder, ref: Schema, seen: Set<number>, allChange
|
|
|
115
115
|
const metadata: Metadata = (ref.constructor as typeof Schema)[Symbol.metadata];
|
|
116
116
|
const refIndexes = metadata?.[$refTypeFieldIndexes] as number[] | undefined;
|
|
117
117
|
if (refIndexes === undefined) { return; }
|
|
118
|
-
const
|
|
118
|
+
const patchOnly = metadata[$patchOnlyFieldIndexes] as number[] | undefined;
|
|
119
119
|
|
|
120
120
|
for (let i = 0; i < refIndexes.length; i++) {
|
|
121
121
|
const fieldIndex = refIndexes[i];
|
|
122
|
-
// @
|
|
123
|
-
if (
|
|
122
|
+
// @patchOnly fields are never in a snapshot — leave them alone.
|
|
123
|
+
if (patchOnly !== undefined && patchOnly.includes(fieldIndex)) { continue; }
|
|
124
124
|
|
|
125
125
|
const field = metadata[fieldIndex];
|
|
126
126
|
const value = (ref as any)[field.name];
|
|
@@ -142,7 +142,7 @@ function sweepCollection(decoder: Decoder, coll: any, seen: Set<number>, allChan
|
|
|
142
142
|
|
|
143
143
|
// `undefined` = the collection never appeared in the payload at all
|
|
144
144
|
// (not even as its parent's field op) — it is not part of full-sync
|
|
145
|
-
// (@
|
|
145
|
+
// (@patchOnly, view-invisible) and must be left alone. An empty Set
|
|
146
146
|
// means "present with zero entries" → prune everything.
|
|
147
147
|
const visited = decoder.resyncVisited!.get(refId);
|
|
148
148
|
if (visited === undefined) { return; }
|
|
@@ -76,7 +76,7 @@ const _invokeNoCtx = (
|
|
|
76
76
|
) => cb(index, op);
|
|
77
77
|
|
|
78
78
|
// ──────────────────────────────────────────────────────────────────────────
|
|
79
|
-
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤
|
|
79
|
+
// SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤63 fields)
|
|
80
80
|
// ──────────────────────────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
82
|
/**
|