@aventara/client 0.0.0-stage → 0.1.0-pilot.1
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/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +234 -2
- package/dist/avclient.bin.d.ts +2 -0
- package/dist/avclient.bin.js +15 -0
- package/dist/cli/command.parser.d.ts +37 -0
- package/dist/cli/command.parser.js +177 -0
- package/dist/cli/generate.command.d.ts +24 -0
- package/dist/cli/generate.command.js +41 -0
- package/dist/cli/generation-failure.renderer.d.ts +6 -0
- package/dist/cli/generation-failure.renderer.js +52 -0
- package/dist/cli/generation-success.renderer.d.ts +32 -0
- package/dist/cli/generation-success.renderer.js +47 -0
- package/dist/cli/terminal.prompter.d.ts +13 -0
- package/dist/cli/terminal.prompter.js +53 -0
- package/dist/cli/warning.renderer.d.ts +10 -0
- package/dist/cli/warning.renderer.js +14 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +75 -0
- package/dist/config/client-config.interface.d.ts +62 -0
- package/dist/config/client-config.interface.js +14 -0
- package/dist/config/config.loader.d.ts +41 -0
- package/dist/config/config.loader.js +95 -0
- package/dist/config/config.resolver.d.ts +50 -0
- package/dist/config/config.resolver.js +126 -0
- package/dist/config/env.cascade.d.ts +84 -0
- package/dist/config/env.cascade.js +126 -0
- package/dist/contract/contract.acceptance.d.ts +77 -0
- package/dist/contract/contract.acceptance.js +124 -0
- package/dist/contract/contract.fetcher.d.ts +64 -0
- package/dist/contract/contract.fetcher.js +85 -0
- package/dist/contract/contract.loader.d.ts +32 -0
- package/dist/contract/contract.loader.js +32 -0
- package/dist/emit/banner.emitter.d.ts +31 -0
- package/dist/emit/banner.emitter.js +42 -0
- package/dist/emit/client-surface.emitter.d.ts +32 -0
- package/dist/emit/client-surface.emitter.js +236 -0
- package/dist/emit/client-tree.emitter.d.ts +37 -0
- package/dist/emit/client-tree.emitter.js +103 -0
- package/dist/emit/contract-carrier.emitter.d.ts +13 -0
- package/dist/emit/contract-carrier.emitter.js +60 -0
- package/dist/emit/derivation.emitter.d.ts +45 -0
- package/dist/emit/derivation.emitter.js +233 -0
- package/dist/emit/descriptor.emitter.d.ts +4 -0
- package/dist/emit/descriptor.emitter.js +97 -0
- package/dist/emit/emitted-tree.interface.d.ts +61 -0
- package/dist/emit/emitted-tree.interface.js +18 -0
- package/dist/emit/enum.emitter.d.ts +24 -0
- package/dist/emit/enum.emitter.js +42 -0
- package/dist/emit/name.deriver.d.ts +153 -0
- package/dist/emit/name.deriver.js +411 -0
- package/dist/emit/named-type.emitter.d.ts +32 -0
- package/dist/emit/named-type.emitter.js +50 -0
- package/dist/emit/runtime.emitter.d.ts +87 -0
- package/dist/emit/runtime.emitter.js +707 -0
- package/dist/emit/scalar.codec.d.ts +63 -0
- package/dist/emit/scalar.codec.js +498 -0
- package/dist/emit/transaction.emitter.d.ts +17 -0
- package/dist/emit/transaction.emitter.js +438 -0
- package/dist/generate.d.ts +123 -0
- package/dist/generate.js +98 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/init/client-config.template.d.ts +11 -0
- package/dist/init/client-config.template.js +27 -0
- package/dist/init/client-init.errors.d.ts +9 -0
- package/dist/init/client-init.errors.js +9 -0
- package/dist/init/client-init.orchestrator.d.ts +3 -0
- package/dist/init/client-init.orchestrator.js +86 -0
- package/dist/init/client-init.planner.d.ts +27 -0
- package/dist/init/client-init.planner.js +99 -0
- package/dist/init/client-init.questions.d.ts +52 -0
- package/dist/init/client-init.questions.js +124 -0
- package/dist/init/client-project.inspector.d.ts +15 -0
- package/dist/init/client-project.inspector.js +32 -0
- package/dist/init/command.runner.d.ts +8 -0
- package/dist/init/command.runner.js +17 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/output/output.validator.d.ts +75 -0
- package/dist/output/output.validator.js +262 -0
- package/dist/output/output.writer.d.ts +162 -0
- package/dist/output/output.writer.js +499 -0
- package/package.json +47 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AvProtocol } from "@aventara/core/protocol";
|
|
2
|
+
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* The scalar codecs of §6.2, as the generated client carries them in
|
|
5
|
+
* `runtime/codec.ts`.
|
|
6
|
+
*
|
|
7
|
+
* # Keyed on `BuiltInScalar` alone
|
|
8
|
+
*
|
|
9
|
+
* §6.2's table is fixed by PROTOCOL VERSION, not carried in the Contract: the
|
|
10
|
+
* scalar registry is a key registry (`{ builtin: true }`) and scalars carry no
|
|
11
|
+
* position qualifier [M10]. So the codec reads no field, no capability and no
|
|
12
|
+
* operation descriptor — the caller names the scalar, and the codec converts one
|
|
13
|
+
* value. Walking a Resource's fields to find which scalar a value is belongs to
|
|
14
|
+
* the method surface, below Phase 12-partial's boundary.
|
|
15
|
+
*
|
|
16
|
+
* One source for the scalar set: the emitted `BuiltInScalar` union and the
|
|
17
|
+
* emitted table both come from core's `BUILT_IN_SCALARS`, in its order, and
|
|
18
|
+
* {@link SCALAR_CODEC_SOURCES} is a mapped type over core's `BuiltInScalar` — a
|
|
19
|
+
* scalar the protocol adds is a compile error here until its codec is written.
|
|
20
|
+
*
|
|
21
|
+
* # Untyped on purpose
|
|
22
|
+
*
|
|
23
|
+
* Every codec is `unknown → unknown`, checked at runtime. The TYPE of a scalar's
|
|
24
|
+
* application form and wire form is Phase 9's `ScalarValueType` and its `Forms`
|
|
25
|
+
* maps (architect decision Q3 = 3a); declaring a second scalar→type map here
|
|
26
|
+
* would be a parallel source of that fact. When the boundary lifts, the methods
|
|
27
|
+
* that call these codecs carry the types.
|
|
28
|
+
*
|
|
29
|
+
* # What the emitted codec promises
|
|
30
|
+
*
|
|
31
|
+
* - `encodeScalar`/`decodeScalar` convert between the generated runtime value
|
|
32
|
+
* and the JSON wire value of §6.2, and `decodeScalar(s, encodeScalar(s, v))`
|
|
33
|
+
* equals `v` for every scalar.
|
|
34
|
+
* - `null` passes through both: it is explicit, and whether a field admits it is
|
|
35
|
+
* the field's nullability, which the server validates (§6.2).
|
|
36
|
+
* - `undefined` is never a value: an optional property is omitted, and
|
|
37
|
+
* `serializeWireBody` strips every `undefined` property before transport.
|
|
38
|
+
* - A value outside a scalar's runtime or wire form is refused with a
|
|
39
|
+
* `TypeError` naming the scalar — never coerced. A `json` value carrying a
|
|
40
|
+
* `BigInt`, `Date`, `Uint8Array`, `Decimal`, function, symbol or `undefined`
|
|
41
|
+
* is refused (§6.2), with the path to the offending member.
|
|
42
|
+
* - Wire grammars are the server's, read from core — `AvProtocol.scalarFormats`
|
|
43
|
+
* (C2, R7) — and emitted by value as regular-expression literals
|
|
44
|
+
* ({@link wireGrammarLiteral}): bigint `-?(0|[1-9]\d*)`, datetime exactly
|
|
45
|
+
* `toISOString()`'s form, bytes RFC 4648 base64 with the standard alphabet and
|
|
46
|
+
* padding, canonical (zero pad bits, like the server's — C1). Base64 is written
|
|
47
|
+
* out rather than delegated to `atob`/`btoa`, so the module needs neither a DOM
|
|
48
|
+
* nor a Node global (§15.7, U5).
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The regular-expression literal of one of core's wire grammars, as emitted
|
|
52
|
+
* source: `/<source>/`, the grammar's own text. The sources are anchored,
|
|
53
|
+
* flagless and carry a `/` only inside a character class, which core's
|
|
54
|
+
* `av-protocol.spec.ts` pins, so the literal is the grammar itself.
|
|
55
|
+
*/
|
|
56
|
+
export declare function wireGrammarLiteral(scalar: keyof typeof AvProtocol.scalarFormats): string;
|
|
57
|
+
/**
|
|
58
|
+
* `runtime/codec.ts`: the `BuiltInScalar` union and the codec table in core's
|
|
59
|
+
* `BUILT_IN_SCALARS` order, the helpers the codecs share, and the three
|
|
60
|
+
* functions the generated transport calls. Nothing here is re-exported from the
|
|
61
|
+
* tree's `AvClient.ts`: the codec is the runtime's, not the consumer's.
|
|
62
|
+
*/
|
|
63
|
+
export declare function emitScalarCodecModule(): EmittedModule;
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { BUILT_IN_SCALARS } from "@aventara/core";
|
|
2
|
+
import { AvProtocol } from "@aventara/core/protocol";
|
|
3
|
+
/**
|
|
4
|
+
* The scalar codecs of §6.2, as the generated client carries them in
|
|
5
|
+
* `runtime/codec.ts`.
|
|
6
|
+
*
|
|
7
|
+
* # Keyed on `BuiltInScalar` alone
|
|
8
|
+
*
|
|
9
|
+
* §6.2's table is fixed by PROTOCOL VERSION, not carried in the Contract: the
|
|
10
|
+
* scalar registry is a key registry (`{ builtin: true }`) and scalars carry no
|
|
11
|
+
* position qualifier [M10]. So the codec reads no field, no capability and no
|
|
12
|
+
* operation descriptor — the caller names the scalar, and the codec converts one
|
|
13
|
+
* value. Walking a Resource's fields to find which scalar a value is belongs to
|
|
14
|
+
* the method surface, below Phase 12-partial's boundary.
|
|
15
|
+
*
|
|
16
|
+
* One source for the scalar set: the emitted `BuiltInScalar` union and the
|
|
17
|
+
* emitted table both come from core's `BUILT_IN_SCALARS`, in its order, and
|
|
18
|
+
* {@link SCALAR_CODEC_SOURCES} is a mapped type over core's `BuiltInScalar` — a
|
|
19
|
+
* scalar the protocol adds is a compile error here until its codec is written.
|
|
20
|
+
*
|
|
21
|
+
* # Untyped on purpose
|
|
22
|
+
*
|
|
23
|
+
* Every codec is `unknown → unknown`, checked at runtime. The TYPE of a scalar's
|
|
24
|
+
* application form and wire form is Phase 9's `ScalarValueType` and its `Forms`
|
|
25
|
+
* maps (architect decision Q3 = 3a); declaring a second scalar→type map here
|
|
26
|
+
* would be a parallel source of that fact. When the boundary lifts, the methods
|
|
27
|
+
* that call these codecs carry the types.
|
|
28
|
+
*
|
|
29
|
+
* # What the emitted codec promises
|
|
30
|
+
*
|
|
31
|
+
* - `encodeScalar`/`decodeScalar` convert between the generated runtime value
|
|
32
|
+
* and the JSON wire value of §6.2, and `decodeScalar(s, encodeScalar(s, v))`
|
|
33
|
+
* equals `v` for every scalar.
|
|
34
|
+
* - `null` passes through both: it is explicit, and whether a field admits it is
|
|
35
|
+
* the field's nullability, which the server validates (§6.2).
|
|
36
|
+
* - `undefined` is never a value: an optional property is omitted, and
|
|
37
|
+
* `serializeWireBody` strips every `undefined` property before transport.
|
|
38
|
+
* - A value outside a scalar's runtime or wire form is refused with a
|
|
39
|
+
* `TypeError` naming the scalar — never coerced. A `json` value carrying a
|
|
40
|
+
* `BigInt`, `Date`, `Uint8Array`, `Decimal`, function, symbol or `undefined`
|
|
41
|
+
* is refused (§6.2), with the path to the offending member.
|
|
42
|
+
* - Wire grammars are the server's, read from core — `AvProtocol.scalarFormats`
|
|
43
|
+
* (C2, R7) — and emitted by value as regular-expression literals
|
|
44
|
+
* ({@link wireGrammarLiteral}): bigint `-?(0|[1-9]\d*)`, datetime exactly
|
|
45
|
+
* `toISOString()`'s form, bytes RFC 4648 base64 with the standard alphabet and
|
|
46
|
+
* padding, canonical (zero pad bits, like the server's — C1). Base64 is written
|
|
47
|
+
* out rather than delegated to `atob`/`btoa`, so the module needs neither a DOM
|
|
48
|
+
* nor a Node global (§15.7, U5).
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* The regular-expression literal of one of core's wire grammars, as emitted
|
|
52
|
+
* source: `/<source>/`, the grammar's own text. The sources are anchored,
|
|
53
|
+
* flagless and carry a `/` only inside a character class, which core's
|
|
54
|
+
* `av-protocol.spec.ts` pins, so the literal is the grammar itself.
|
|
55
|
+
*/
|
|
56
|
+
export function wireGrammarLiteral(scalar) {
|
|
57
|
+
return `/${AvProtocol.scalarFormats[scalar]}/`;
|
|
58
|
+
}
|
|
59
|
+
const SCALAR_CODEC_SOURCES = {
|
|
60
|
+
string: {
|
|
61
|
+
encode: `(value) => (typeof value === "string" ? value : refuse("string", "runtime", "a string"))`,
|
|
62
|
+
decode: `(wire) => (typeof wire === "string" ? wire : refuse("string", "wire", "a string"))`,
|
|
63
|
+
},
|
|
64
|
+
boolean: {
|
|
65
|
+
encode: `(value) => (typeof value === "boolean" ? value : refuse("boolean", "runtime", "a boolean"))`,
|
|
66
|
+
decode: `(wire) => (typeof wire === "boolean" ? wire : refuse("boolean", "wire", "a boolean"))`,
|
|
67
|
+
},
|
|
68
|
+
int: {
|
|
69
|
+
encode: `(value) => (isInt32(value) ? value : refuse("int", "runtime", "a 32-bit integer"))`,
|
|
70
|
+
decode: `(wire) => (isInt32(wire) ? wire : refuse("int", "wire", "a 32-bit integer"))`,
|
|
71
|
+
},
|
|
72
|
+
float: {
|
|
73
|
+
encode: `(value) => (isFiniteNumber(value) ? value : refuse("float", "runtime", "a finite number"))`,
|
|
74
|
+
decode: `(wire) => (isFiniteNumber(wire) ? wire : refuse("float", "wire", "a finite number"))`,
|
|
75
|
+
},
|
|
76
|
+
bigint: {
|
|
77
|
+
encode: `(value) => (typeof value === "bigint" ? value.toString(10) : refuse("bigint", "runtime", "a bigint"))`,
|
|
78
|
+
decode: `(wire) =>
|
|
79
|
+
typeof wire === "string" && BIGINT_WIRE.test(wire)
|
|
80
|
+
? BigInt(wire)
|
|
81
|
+
: refuse("bigint", "wire", "a base-10 integer string")`,
|
|
82
|
+
},
|
|
83
|
+
decimal: {
|
|
84
|
+
encode: `(value) => (value instanceof Decimal ? value.toString() : refuse("decimal", "runtime", "a Decimal"))`,
|
|
85
|
+
decode: "decodeDecimal",
|
|
86
|
+
},
|
|
87
|
+
datetime: {
|
|
88
|
+
encode: "encodeDatetime",
|
|
89
|
+
decode: "decodeDatetime",
|
|
90
|
+
},
|
|
91
|
+
bytes: {
|
|
92
|
+
encode: `(value) => (value instanceof Uint8Array ? encodeBase64(value) : refuse("bytes", "runtime", "a Uint8Array"))`,
|
|
93
|
+
decode: `(wire) =>
|
|
94
|
+
(typeof wire === "string" ? decodeBase64(wire) : undefined) ??
|
|
95
|
+
refuse("bytes", "wire", "an RFC 4648 base64 string")`,
|
|
96
|
+
},
|
|
97
|
+
json: {
|
|
98
|
+
encode: `(value) => copyJson("runtime", value, [], new Set())`,
|
|
99
|
+
decode: `(wire) => copyJson("wire", wire, [], new Set())`,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const CODEC_MODULE_HEAD = `import { Decimal } from "./decimal.js";
|
|
103
|
+
import type { FieldDecoding } from "./descriptor.js";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The scalar codecs of Aventara protocol version 1 (§6.2): one per built-in
|
|
107
|
+
* scalar, converting between the generated runtime value and the JSON wire value.
|
|
108
|
+
* A value is never wrapped per field — the Contract names the scalar, so the
|
|
109
|
+
* caller says which scalar a value is.
|
|
110
|
+
*
|
|
111
|
+
* null is explicit in both directions and passes through; whether a field admits
|
|
112
|
+
* it is the field's nullability. undefined is never a value: an optional
|
|
113
|
+
* property is omitted (serializeWireBody). Anything outside a scalar's runtime or
|
|
114
|
+
* wire form is refused with a TypeError, never coerced.
|
|
115
|
+
*/
|
|
116
|
+
`;
|
|
117
|
+
const CODEC_MODULE_BODY = `
|
|
118
|
+
interface ScalarCodec {
|
|
119
|
+
/** The generated runtime value to its JSON wire value. */
|
|
120
|
+
readonly encode: (value: unknown) => unknown;
|
|
121
|
+
/** The JSON wire value to the generated runtime value. */
|
|
122
|
+
readonly decode: (wire: unknown) => unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type Side = "runtime" | "wire";
|
|
126
|
+
type Path = readonly (string | number)[];
|
|
127
|
+
|
|
128
|
+
const INT32_MIN = -(2 ** 31);
|
|
129
|
+
const INT32_MAX = 2 ** 31 - 1;
|
|
130
|
+
const BIGINT_WIRE = ${wireGrammarLiteral("bigint")};
|
|
131
|
+
const DATETIME_WIRE = ${wireGrammarLiteral("datetime")};
|
|
132
|
+
const BASE64_WIRE = ${wireGrammarLiteral("bytes")};
|
|
133
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
134
|
+
|
|
135
|
+
function refuse(scalar: BuiltInScalar, side: Side, expected: string): never {
|
|
136
|
+
throw new TypeError('A "' + scalar + '" ' + side + " value must be " + expected + ".");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isInt32(value: unknown): value is number {
|
|
140
|
+
return typeof value === "number" && Number.isInteger(value) && value >= INT32_MIN && value <= INT32_MAX;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
144
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function decodeDecimal(wire: unknown): Decimal {
|
|
148
|
+
if (typeof wire === "string") {
|
|
149
|
+
try {
|
|
150
|
+
return new Decimal(wire);
|
|
151
|
+
} catch {
|
|
152
|
+
// Refused below, naming the scalar.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return refuse("decimal", "wire", "a decimal string");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function encodeDatetime(value: unknown): string {
|
|
159
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
160
|
+
return refuse("datetime", "runtime", "a valid Date");
|
|
161
|
+
}
|
|
162
|
+
const wire = value.toISOString();
|
|
163
|
+
return DATETIME_WIRE.test(wire) ? wire : refuse("datetime", "runtime", "a Date in the years 0000 to 9999");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function decodeDatetime(wire: unknown): Date {
|
|
167
|
+
if (typeof wire === "string" && DATETIME_WIRE.test(wire)) {
|
|
168
|
+
const date = new Date(wire);
|
|
169
|
+
if (!Number.isNaN(date.getTime()) && date.toISOString() === wire) {
|
|
170
|
+
return date;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return refuse("datetime", "wire", "a canonical ISO-8601 UTC string");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function encodeBase64(bytes: Uint8Array): string {
|
|
177
|
+
let text = "";
|
|
178
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
179
|
+
const remaining = bytes.length - index;
|
|
180
|
+
const group = ((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
|
|
181
|
+
text += sextet(group, 18) + sextet(group, 12);
|
|
182
|
+
text += remaining > 1 ? sextet(group, 6) : "=";
|
|
183
|
+
text += remaining > 2 ? sextet(group, 0) : "=";
|
|
184
|
+
}
|
|
185
|
+
return text;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sextet(group: number, shift: number): string {
|
|
189
|
+
return BASE64_ALPHABET.charAt((group >> shift) & 63);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The bytes of a canonical RFC 4648 base64 string; undefined for any other string. */
|
|
193
|
+
function decodeBase64(wire: string): Uint8Array | undefined {
|
|
194
|
+
if (!BASE64_WIRE.test(wire)) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const padding = wire.endsWith("==") ? 2 : wire.endsWith("=") ? 1 : 0;
|
|
198
|
+
const bytes = new Uint8Array((wire.length / 4) * 3 - padding);
|
|
199
|
+
let written = 0;
|
|
200
|
+
for (let index = 0; index < wire.length; index += 4) {
|
|
201
|
+
let group = 0;
|
|
202
|
+
for (let offset = 0; offset < 4; offset += 1) {
|
|
203
|
+
const symbol = wire.charAt(index + offset);
|
|
204
|
+
group = (group << 6) | (symbol === "=" ? 0 : BASE64_ALPHABET.indexOf(symbol));
|
|
205
|
+
}
|
|
206
|
+
const count = index + 4 === wire.length ? 3 - padding : 3;
|
|
207
|
+
// Bits a padded group does not carry must be zero, or two strings would
|
|
208
|
+
// name one byte sequence.
|
|
209
|
+
if ((group & ((1 << (8 * (3 - count))) - 1)) !== 0) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
for (let byte = 0; byte < count; byte += 1) {
|
|
213
|
+
bytes[written] = (group >> (16 - 8 * byte)) & 255;
|
|
214
|
+
written += 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return bytes;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isPlainRecord(value: object): boolean {
|
|
221
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
222
|
+
return prototype === Object.prototype || prototype === null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What a value that is not JSON-safe is, for a refusal. */
|
|
226
|
+
function kindOf(value: unknown): string {
|
|
227
|
+
if (value === undefined) {
|
|
228
|
+
return "undefined";
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "bigint") {
|
|
231
|
+
return "a bigint";
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === "number") {
|
|
234
|
+
return "a non-finite number";
|
|
235
|
+
}
|
|
236
|
+
if (typeof value === "function") {
|
|
237
|
+
return "a function";
|
|
238
|
+
}
|
|
239
|
+
if (typeof value === "symbol") {
|
|
240
|
+
return "a symbol";
|
|
241
|
+
}
|
|
242
|
+
if (value instanceof Date) {
|
|
243
|
+
return "a Date";
|
|
244
|
+
}
|
|
245
|
+
if (value instanceof Uint8Array) {
|
|
246
|
+
return "a Uint8Array";
|
|
247
|
+
}
|
|
248
|
+
if (value instanceof Decimal) {
|
|
249
|
+
return "a Decimal";
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "object" && value !== null && isPlainRecord(value)) {
|
|
252
|
+
return "a symbol-keyed property";
|
|
253
|
+
}
|
|
254
|
+
return "a non-plain object";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A JSON Pointer (RFC 6901) to a member, or "the root". */
|
|
258
|
+
function pointerOf(path: Path): string {
|
|
259
|
+
return path.length === 0
|
|
260
|
+
? "the root"
|
|
261
|
+
: path.map((segment) => "/" + String(segment).replaceAll("~", "~0").replaceAll("/", "~1")).join("");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isJsonContainer(value: object): boolean {
|
|
265
|
+
return Array.isArray(value) || (isPlainRecord(value) && Object.getOwnPropertySymbols(value).length === 0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A copy of a JSON-safe value, or a refusal naming the first member that is not. */
|
|
269
|
+
function copyJson(side: Side, value: unknown, path: Path, ancestors: Set<object>): unknown {
|
|
270
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || isFiniteNumber(value)) {
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
if (typeof value !== "object" || !isJsonContainer(value)) {
|
|
274
|
+
return refuse("json", side, "JSON-safe, and " + kindOf(value) + " at " + pointerOf(path) + " is not");
|
|
275
|
+
}
|
|
276
|
+
if (ancestors.has(value)) {
|
|
277
|
+
return refuse("json", side, "acyclic, and " + pointerOf(path) + " repeats an ancestor");
|
|
278
|
+
}
|
|
279
|
+
ancestors.add(value);
|
|
280
|
+
let copy: unknown;
|
|
281
|
+
if (Array.isArray(value)) {
|
|
282
|
+
const items: unknown[] = [];
|
|
283
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
284
|
+
items.push(copyJson(side, value[index], [...path, index], ancestors));
|
|
285
|
+
}
|
|
286
|
+
copy = items;
|
|
287
|
+
} else {
|
|
288
|
+
// fromEntries defines each key, so "__proto__" stays an own key.
|
|
289
|
+
copy = Object.fromEntries(
|
|
290
|
+
Object.entries(value).map(([key, member]) => [key, copyJson(side, member, [...path, key], ancestors)]),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
ancestors.delete(value);
|
|
294
|
+
return copy;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function refuseBody(path: Path, what: string): never {
|
|
298
|
+
throw new TypeError(
|
|
299
|
+
"A request body must be JSON-safe once its scalars are encoded, and " + what + " at " + pointerOf(path) + " is not.",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function writeWireJson(value: unknown, path: Path, ancestors: Set<object>): string {
|
|
304
|
+
if (value === null) {
|
|
305
|
+
return "null";
|
|
306
|
+
}
|
|
307
|
+
if (typeof value === "string" || isFiniteNumber(value)) {
|
|
308
|
+
return JSON.stringify(value);
|
|
309
|
+
}
|
|
310
|
+
if (typeof value === "boolean") {
|
|
311
|
+
return value ? "true" : "false";
|
|
312
|
+
}
|
|
313
|
+
if (typeof value !== "object" || !isJsonContainer(value)) {
|
|
314
|
+
return refuseBody(path, kindOf(value));
|
|
315
|
+
}
|
|
316
|
+
if (ancestors.has(value)) {
|
|
317
|
+
return refuseBody(path, "a cycle");
|
|
318
|
+
}
|
|
319
|
+
ancestors.add(value);
|
|
320
|
+
let text: string;
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
const items: string[] = [];
|
|
323
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
324
|
+
// An undefined element is refused, not stripped: JSON would make it null.
|
|
325
|
+
items.push(writeWireJson(value[index], [...path, index], ancestors));
|
|
326
|
+
}
|
|
327
|
+
text = "[" + items.join(",") + "]";
|
|
328
|
+
} else {
|
|
329
|
+
const members: string[] = [];
|
|
330
|
+
for (const [key, member] of Object.entries(value)) {
|
|
331
|
+
if (member !== undefined) {
|
|
332
|
+
members.push(JSON.stringify(key) + ":" + writeWireJson(member, [...path, key], ancestors));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
text = "{" + members.join(",") + "}";
|
|
336
|
+
}
|
|
337
|
+
ancestors.delete(value);
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function codecOf(scalar: BuiltInScalar): ScalarCodec {
|
|
342
|
+
if (!Object.hasOwn(SCALAR_CODECS, scalar)) {
|
|
343
|
+
throw new TypeError('"' + String(scalar) + '" is not a built-in scalar of this protocol version.');
|
|
344
|
+
}
|
|
345
|
+
return SCALAR_CODECS[scalar];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The JSON wire value of a scalar's runtime value; null passes through. */
|
|
349
|
+
export function encodeScalar(scalar: BuiltInScalar, value: unknown): unknown {
|
|
350
|
+
const codec = codecOf(scalar);
|
|
351
|
+
return value === null ? null : codec.encode(value);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The runtime value of a scalar's JSON wire value; null passes through. */
|
|
355
|
+
export function decodeScalar(scalar: BuiltInScalar, wire: unknown): unknown {
|
|
356
|
+
const codec = codecOf(scalar);
|
|
357
|
+
return wire === null ? null : codec.decode(wire);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The JSON text of a request body whose scalars are already encoded. Every
|
|
362
|
+
* undefined property is stripped, at every depth — an optional property is
|
|
363
|
+
* omitted, never sent — and null is kept as the explicit value it is (§6.2).
|
|
364
|
+
* Anything else that is not JSON-safe is refused rather than coerced: it means a
|
|
365
|
+
* value escaped its scalar codec.
|
|
366
|
+
*/
|
|
367
|
+
export function serializeWireBody(body: unknown): string {
|
|
368
|
+
return writeWireJson(body, [], new Set());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* An operation's arguments with every scalar in its wire form, read off each
|
|
373
|
+
* value's runtime class (Phase 12-rest Q15 = a, the server's encode mirrored): a
|
|
374
|
+
* bigint, a Date, a Decimal or a Uint8Array is encoded wherever it stands — a
|
|
375
|
+
* filter operand, a data value, a cursor — and plain objects and arrays are
|
|
376
|
+
* walked. Every other value is left for serializeWireBody, which refuses what is
|
|
377
|
+
* not JSON-safe. A json value cannot hold one of the four classes (the types
|
|
378
|
+
* exclude them), so nothing inside one is ever re-read as a scalar.
|
|
379
|
+
*/
|
|
380
|
+
export function encodeArguments(value: unknown): unknown {
|
|
381
|
+
return encodeValue(value, [], new Set());
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function encodeValue(value: unknown, path: Path, ancestors: Set<object>): unknown {
|
|
385
|
+
if (typeof value === "bigint") {
|
|
386
|
+
return encodeScalar("bigint", value);
|
|
387
|
+
}
|
|
388
|
+
if (value instanceof Date) {
|
|
389
|
+
return encodeScalar("datetime", value);
|
|
390
|
+
}
|
|
391
|
+
if (value instanceof Decimal) {
|
|
392
|
+
return encodeScalar("decimal", value);
|
|
393
|
+
}
|
|
394
|
+
if (value instanceof Uint8Array) {
|
|
395
|
+
return encodeScalar("bytes", value);
|
|
396
|
+
}
|
|
397
|
+
if (typeof value !== "object" || value === null || !isJsonContainer(value)) {
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
if (ancestors.has(value)) {
|
|
401
|
+
return refuseBody(path, "a cycle");
|
|
402
|
+
}
|
|
403
|
+
ancestors.add(value);
|
|
404
|
+
const copy = Array.isArray(value)
|
|
405
|
+
? value.map((item, index) => encodeValue(item, [...path, index], ancestors))
|
|
406
|
+
: Object.fromEntries(
|
|
407
|
+
Object.entries(value).map(([key, member]) => [key, encodeValue(member, [...path, key], ancestors)]),
|
|
408
|
+
);
|
|
409
|
+
ancestors.delete(value);
|
|
410
|
+
return copy;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Which result fields of each Resource decode, and how (generated/runtime/descriptor.ts). */
|
|
414
|
+
export type DecodeTable = {
|
|
415
|
+
readonly [resource: string]: { readonly [field: string]: FieldDecoding };
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* An operation's result with every scalar in its runtime form, read off the decode
|
|
420
|
+
* table (P1): a number (a count), null, and the data of a Resource the table
|
|
421
|
+
* does not name pass through; a list is a list of
|
|
422
|
+
* records, anything else is one record of \`resource\`. In a record, a field the
|
|
423
|
+
* table names is revived — each item of a list field — a relation recurses with
|
|
424
|
+
* its target, and every other key passes through untouched: json is never
|
|
425
|
+
* revived. A to-many relation's value is a list, \`{ data, count }\` or
|
|
426
|
+
* \`{ count }\` — the three shapes the derivation gives it.
|
|
427
|
+
*
|
|
428
|
+
* @throws TypeError when a value is not the wire form its field's scalar takes —
|
|
429
|
+
* which the transport answers as a TransportError (architect, 2026-10-04).
|
|
430
|
+
*/
|
|
431
|
+
export function decodeResult(table: DecodeTable, resource: string, value: unknown): unknown {
|
|
432
|
+
// A Resource the table does not name has nothing to revive: its data passes.
|
|
433
|
+
if (value === null || typeof value === "number" || !Object.hasOwn(table, resource)) {
|
|
434
|
+
return value;
|
|
435
|
+
}
|
|
436
|
+
return Array.isArray(value)
|
|
437
|
+
? value.map((record) => decodeRecord(table, resource, record))
|
|
438
|
+
: decodeRecord(table, resource, value);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function decodeRecord(table: DecodeTable, resource: string, record: unknown): unknown {
|
|
442
|
+
if (record === null) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
if (typeof record !== "object" || Array.isArray(record)) {
|
|
446
|
+
throw new TypeError('A "' + resource + '" record must be an object.');
|
|
447
|
+
}
|
|
448
|
+
const fields = Object.hasOwn(table, resource) ? table[resource] : undefined;
|
|
449
|
+
return Object.fromEntries(
|
|
450
|
+
Object.entries(record).map(([key, member]) => {
|
|
451
|
+
const decoding = fields !== undefined && Object.hasOwn(fields, key) ? fields[key] : undefined;
|
|
452
|
+
return [key, decoding === undefined ? member : decodeField(table, decoding, member)];
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function decodeField(table: DecodeTable, decoding: FieldDecoding, value: unknown): unknown {
|
|
458
|
+
if (typeof decoding === "string") {
|
|
459
|
+
return Array.isArray(value) ? value.map((item) => decodeScalar(decoding, item)) : decodeScalar(decoding, value);
|
|
460
|
+
}
|
|
461
|
+
if (value === null || !decoding.many) {
|
|
462
|
+
return decodeRecord(table, decoding.relation, value);
|
|
463
|
+
}
|
|
464
|
+
if (Array.isArray(value)) {
|
|
465
|
+
return value.map((record) => decodeRecord(table, decoding.relation, record));
|
|
466
|
+
}
|
|
467
|
+
if (typeof value === "object" && Object.hasOwn(value, "data")) {
|
|
468
|
+
const envelope = value as { readonly data: unknown };
|
|
469
|
+
if (!Array.isArray(envelope.data)) {
|
|
470
|
+
throw new TypeError('A "' + decoding.relation + '" list must be an array.');
|
|
471
|
+
}
|
|
472
|
+
return { ...value, data: envelope.data.map((record) => decodeRecord(table, decoding.relation, record)) };
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
/**
|
|
478
|
+
* `runtime/codec.ts`: the `BuiltInScalar` union and the codec table in core's
|
|
479
|
+
* `BUILT_IN_SCALARS` order, the helpers the codecs share, and the three
|
|
480
|
+
* functions the generated transport calls. Nothing here is re-exported from the
|
|
481
|
+
* tree's `AvClient.ts`: the codec is the runtime's, not the consumer's.
|
|
482
|
+
*/
|
|
483
|
+
export function emitScalarCodecModule() {
|
|
484
|
+
const union = BUILT_IN_SCALARS.map((scalar) => `\t| ${JSON.stringify(scalar)}`).join("\n");
|
|
485
|
+
const table = BUILT_IN_SCALARS.map((scalar) => {
|
|
486
|
+
const { encode, decode } = SCALAR_CODEC_SOURCES[scalar];
|
|
487
|
+
return `\t${scalar}: {\n\t\tencode: ${encode},\n\t\tdecode: ${decode},\n\t},`;
|
|
488
|
+
}).join("\n");
|
|
489
|
+
return {
|
|
490
|
+
path: "runtime/codec.ts",
|
|
491
|
+
source: CODEC_MODULE_HEAD +
|
|
492
|
+
"\n/** The built-in scalars of the protocol (§6.1). */\n" +
|
|
493
|
+
`export type BuiltInScalar =\n${union};\n` +
|
|
494
|
+
CODEC_MODULE_BODY +
|
|
495
|
+
"\nconst SCALAR_CODECS: { readonly [S in BuiltInScalar]: ScalarCodec } = {\n" +
|
|
496
|
+
`${table}\n};\n`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EmittedModule } from "./emitted-tree.interface.js";
|
|
2
|
+
/**
|
|
3
|
+
* The transaction builder and runner (§14; Phase 12-rest S6, Q13, Q14, P3):
|
|
4
|
+
* `runtime/fingerprint.ts` and `runtime/transaction.ts`, emitted iff the
|
|
5
|
+
* ClientContract advertises `interactive` transactions (P3) — `client.ts` wires
|
|
6
|
+
* them to `avClient.tx` and `avClient.transaction`.
|
|
7
|
+
*
|
|
8
|
+
* Both are fixed by protocol version, not by the Contract, and mirror core's own
|
|
9
|
+
* seams: the plan is assembled as `transactions/transaction-plan-assembler.ts`
|
|
10
|
+
* assembles it — handles as pure data, a `$ref` bound lazily to its source
|
|
11
|
+
* handle's position in the list it runs with, the two refusals core answers in
|
|
12
|
+
* process (Q13) — and each node's fingerprint is core's
|
|
13
|
+
* `computeOperationFingerprint` over the node's WIRE arguments (§14.4), computed
|
|
14
|
+
* here by an emitted JCS and SHA-256 (Q14: `crypto.subtle` is absent from a
|
|
15
|
+
* browser page served over plain http from a non-localhost host).
|
|
16
|
+
*/
|
|
17
|
+
export declare function emitTransactionModules(): readonly EmittedModule[];
|