@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.
Files changed (62) hide show
  1. package/build/Metadata.d.ts +20 -12
  2. package/build/annotations.d.ts +23 -10
  3. package/build/codegen/cli.cjs +615 -204
  4. package/build/codegen/cli.cjs.map +1 -1
  5. package/build/codegen/languages/dart.d.ts +20 -0
  6. package/build/codegen/types.d.ts +20 -0
  7. package/build/decoder/Resync.d.ts +3 -3
  8. package/build/encoder/ChangeTree.d.ts +25 -9
  9. package/build/encoder/EncodeDescriptor.d.ts +11 -12
  10. package/build/encoder/StateView.d.ts +26 -2
  11. package/build/encoder/changeTree/inheritedFlags.d.ts +1 -1
  12. package/build/encoder/changeTree/parentChain.d.ts +9 -0
  13. package/build/encoder/streaming.d.ts +7 -0
  14. package/build/index.cjs +449 -233
  15. package/build/index.cjs.map +1 -1
  16. package/build/index.d.ts +1 -1
  17. package/build/index.js +449 -233
  18. package/build/index.mjs +448 -232
  19. package/build/index.mjs.map +1 -1
  20. package/build/types/builder.d.ts +31 -22
  21. package/build/types/custom/ArraySchema.d.ts +17 -0
  22. package/build/types/custom/StreamSchema.d.ts +1 -1
  23. package/build/types/symbols.d.ts +4 -10
  24. package/package.json +1 -1
  25. package/src/Metadata.ts +58 -31
  26. package/src/annotations.ts +56 -32
  27. package/src/codegen/api.ts +2 -1
  28. package/src/codegen/languages/c.ts +21 -3
  29. package/src/codegen/languages/csharp.ts +7 -1
  30. package/src/codegen/languages/dart.ts +274 -0
  31. package/src/codegen/languages/haxe.ts +7 -1
  32. package/src/codegen/languages/lua.ts +16 -4
  33. package/src/codegen/languages/ts.ts +5 -0
  34. package/src/codegen/parser.ts +97 -3
  35. package/src/codegen/types.ts +24 -0
  36. package/src/decoder/Resync.ts +8 -8
  37. package/src/encoder/ChangeRecorder.ts +1 -1
  38. package/src/encoder/ChangeTree.ts +46 -26
  39. package/src/encoder/EncodeDescriptor.ts +17 -38
  40. package/src/encoder/EncodeOperation.ts +3 -1
  41. package/src/encoder/Encoder.ts +97 -21
  42. package/src/encoder/Root.ts +18 -20
  43. package/src/encoder/StateView.ts +102 -12
  44. package/src/encoder/changeTree/inheritedFlags.ts +10 -10
  45. package/src/encoder/changeTree/liveIteration.ts +9 -9
  46. package/src/encoder/changeTree/parentChain.ts +29 -0
  47. package/src/encoder/streaming.ts +8 -0
  48. package/src/encoding/spec.ts +1 -1
  49. package/src/index.ts +2 -2
  50. package/src/types/builder.ts +35 -31
  51. package/src/types/custom/ArraySchema.ts +40 -1
  52. package/src/types/custom/StreamSchema.ts +1 -1
  53. package/src/types/symbols.ts +4 -11
  54. package/src/bench_bloat.ts +0 -173
  55. package/src/bench_churn.ts +0 -121
  56. package/src/bench_decode.ts +0 -221
  57. package/src/bench_decode_mem.ts +0 -165
  58. package/src/bench_encode.ts +0 -108
  59. package/src/bench_init.ts +0 -150
  60. package/src/bench_static.ts +0 -109
  61. package/src/bench_stream.ts +0 -295
  62. 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.childType) {
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.childType) {
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 === "ref") {
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}"`;
@@ -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
- property.type = initializer.properties[0].name.text;
67
- property.childType = initializer.properties[0].initializer.text;
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";
@@ -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
 
@@ -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, $transientFieldIndexes } from "../types/symbols.js";
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" (@transient, view-invisible), where pruning would destroy
80
- * live data. Reflected clients have no @transient metadata, so payload
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
- * `@transient` fields are never part of a snapshot and must be left alone;
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 transient = metadata[$transientFieldIndexes] as number[] | undefined;
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
- // @transient fields are never in a snapshot — leave them alone.
123
- if (transient !== undefined && transient.includes(fieldIndex)) { continue; }
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
- // (@transient, view-invisible) and must be left alone. An empty Set
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 (≤64 fields)
79
+ // SchemaChangeRecorder — bitmask + Uint8Array, for Schema types (≤63 fields)
80
80
  // ──────────────────────────────────────────────────────────────────────────
81
81
 
82
82
  /**