okengine 0.21.0 → 0.21.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/package.json +2 -7
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/client/create.ts +13 -26
- package/src/client/live.ts +44 -101
- package/src/client/sse.ts +20 -66
- package/src/client/stream.ts +25 -67
- package/src/client/transport.ts +28 -73
- package/src/client/wire.ts +119 -0
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/interpret.ts +45 -0
- package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Bgt9bq2r.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CSKumwS2.js → agent-disclosure-B43CXZZR.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-92uM5MO7.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-DtGVPtcs.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-DN7l6zmC.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-CwoV56jn.js → duration-tone-BmIR9FV8.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-BMHs-IzK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-BlAEVgNW.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-BDf7OAHv.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-Cul17AcV.js → index-BKpaes3n.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-oY9vdYBk.js → observability-page-WnVLI-0j.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-B3GLNVfF.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-DMbnAe3f.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CL0D9dOq.js → store-page-KvFDingJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-1PlT19ft.js → units-page-C4NdNuxP.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-CBYl0LW_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/elements/store/sql-session.ts +20 -4
- package/src/kernel/app.ts +9 -2
- package/src/kernel/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +23 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors.ts +90 -138
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.ts +37 -329
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/release/build-lib.ts +9 -0
package/src/client/stream.ts
CHANGED
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import type { ClientFetch,
|
|
7
|
+
import { iterateSseFrames, sseError } from "./sse.ts";
|
|
8
|
+
import type { ClientFetch, ClientOptions, ClientRouteMap } from "./types.ts";
|
|
9
|
+
import {
|
|
10
|
+
applyAuthHeader,
|
|
11
|
+
applyHeaderBag,
|
|
12
|
+
interpolatePath,
|
|
13
|
+
methodAndPath,
|
|
14
|
+
resolveHeaders,
|
|
15
|
+
toQuery,
|
|
16
|
+
walkContracts,
|
|
17
|
+
} from "./wire.ts";
|
|
9
18
|
|
|
10
19
|
/** Flow id → REST route for stream-only (non-live) SSE. */
|
|
11
20
|
export type StreamByFlow = Readonly<
|
|
@@ -19,20 +28,12 @@ export type StreamByFlow = Readonly<
|
|
|
19
28
|
*/
|
|
20
29
|
export function flattenStreamRoutes($routes: ClientRouteMap | undefined): StreamByFlow {
|
|
21
30
|
const out: Record<string, { readonly method: string; readonly path: string }> = {};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if ("live" in contract && typeof contract.live === "string") continue;
|
|
29
|
-
const method = "method" in contract ? contract.method : undefined;
|
|
30
|
-
const path = "path" in contract ? contract.path : undefined;
|
|
31
|
-
if (typeof method === "string" && typeof path === "string") {
|
|
32
|
-
out[`${unit}.${flow}`] = { method, path };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
31
|
+
walkContracts($routes, (unit, flow, contract) => {
|
|
32
|
+
if (!("stream" in contract) || contract.stream !== true) return;
|
|
33
|
+
if ("live" in contract && typeof contract.live === "string") return;
|
|
34
|
+
const route = methodAndPath(contract);
|
|
35
|
+
if (route) out[`${unit}.${flow}`] = route;
|
|
36
|
+
});
|
|
36
37
|
return out;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -51,36 +52,21 @@ export async function* openStream(
|
|
|
51
52
|
opts: ClientOptions,
|
|
52
53
|
): AsyncGenerator<unknown> {
|
|
53
54
|
const fetchFn: ClientFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
55
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
54
56
|
let refreshed = false;
|
|
55
57
|
for (;;) {
|
|
56
|
-
const ctrl = new AbortController();
|
|
57
|
-
const parent = opts.signal;
|
|
58
|
-
if (parent) {
|
|
59
|
-
if (parent.aborted) {
|
|
60
|
-
ctrl.abort();
|
|
61
|
-
} else {
|
|
62
|
-
parent.addEventListener("abort", () => ctrl.abort(), { once: true });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
58
|
const { url, method, body } = streamRequest(base, route.method, route.path, input);
|
|
66
59
|
const headers = new Headers({ accept: "text/event-stream" });
|
|
67
|
-
|
|
68
|
-
applyHeaders(headers, extra);
|
|
60
|
+
applyHeaderBag(headers, await resolveHeaders(opts));
|
|
69
61
|
if (body !== undefined && !headers.has("content-type")) {
|
|
70
62
|
headers.set("content-type", "application/json");
|
|
71
63
|
}
|
|
72
|
-
|
|
73
|
-
opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
|
|
74
|
-
? await opts.auth.getToken()
|
|
75
|
-
: undefined;
|
|
76
|
-
if (token && !headers.has("authorization")) {
|
|
77
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
78
|
-
}
|
|
64
|
+
await applyAuthHeader(headers, opts);
|
|
79
65
|
const res = await fetchFn(url, {
|
|
80
66
|
method,
|
|
81
67
|
headers,
|
|
82
68
|
body,
|
|
83
|
-
signal
|
|
69
|
+
signal,
|
|
84
70
|
...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
|
|
85
71
|
});
|
|
86
72
|
if (
|
|
@@ -98,7 +84,7 @@ export async function* openStream(
|
|
|
98
84
|
const text = await res.text().catch(() => "");
|
|
99
85
|
throw sseError(res.status, text);
|
|
100
86
|
}
|
|
101
|
-
|
|
87
|
+
for await (const frame of iterateSseFrames(res, signal)) yield frame.event;
|
|
102
88
|
return;
|
|
103
89
|
}
|
|
104
90
|
}
|
|
@@ -109,39 +95,11 @@ function streamRequest(
|
|
|
109
95
|
path: string,
|
|
110
96
|
input: unknown,
|
|
111
97
|
): { url: string; method: string; body: string | undefined } {
|
|
112
|
-
const
|
|
113
|
-
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
114
|
-
let pathOut = path;
|
|
115
|
-
const rest: Record<string, unknown> = {};
|
|
116
|
-
for (const [k, v] of Object.entries(params)) {
|
|
117
|
-
const token = `:${k}`;
|
|
118
|
-
if (pathOut.includes(token)) {
|
|
119
|
-
pathOut = pathOut.replaceAll(token, encodeURIComponent(String(v)));
|
|
120
|
-
} else {
|
|
121
|
-
rest[k] = v;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
98
|
+
const { path: pathOut, rest } = interpolatePath(path, input);
|
|
124
99
|
const upper = method.toUpperCase();
|
|
125
|
-
let body: string | undefined;
|
|
126
100
|
if (upper === "GET" || upper === "HEAD") {
|
|
127
|
-
|
|
128
|
-
for (const [k, v] of Object.entries(rest)) {
|
|
129
|
-
if (v !== undefined) {
|
|
130
|
-
query.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const qs = query.length ? `?${query.join("&")}` : "";
|
|
134
|
-
return { url: `${base}${pathOut}${qs}`, method: upper, body: undefined };
|
|
101
|
+
return { url: `${base}${pathOut}${toQuery(rest)}`, method: upper, body: undefined };
|
|
135
102
|
}
|
|
136
|
-
body = JSON.stringify(Object.keys(rest).length > 0 ? rest : (input ?? {}));
|
|
103
|
+
const body = JSON.stringify(Object.keys(rest).length > 0 ? rest : (input ?? {}));
|
|
137
104
|
return { url: `${base}${pathOut}`, method: upper, body };
|
|
138
105
|
}
|
|
139
|
-
|
|
140
|
-
function applyHeaders(headers: Headers, extra: ClientHeaders | undefined): void {
|
|
141
|
-
if (!extra) return;
|
|
142
|
-
if (Array.isArray(extra)) {
|
|
143
|
-
for (const [k, v] of extra) headers.set(k, v);
|
|
144
|
-
} else {
|
|
145
|
-
for (const [k, v] of Object.entries(extra)) headers.set(k, v);
|
|
146
|
-
}
|
|
147
|
-
}
|
package/src/client/transport.ts
CHANGED
|
@@ -12,6 +12,13 @@ import type {
|
|
|
12
12
|
ClientHeaders,
|
|
13
13
|
ClientOptions,
|
|
14
14
|
} from "./types.ts";
|
|
15
|
+
import {
|
|
16
|
+
applyAuthHeader,
|
|
17
|
+
applyHeaderBag,
|
|
18
|
+
interpolatePath,
|
|
19
|
+
resolveHeaders,
|
|
20
|
+
toQuery,
|
|
21
|
+
} from "./wire.ts";
|
|
15
22
|
|
|
16
23
|
/** Per-call transport options (binary decode, abort). */
|
|
17
24
|
export interface TransportCallOptions {
|
|
@@ -102,7 +109,7 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
102
109
|
* @param message - Human-readable failure text
|
|
103
110
|
* @param status - Optional HTTP status
|
|
104
111
|
*/
|
|
105
|
-
function transportEnvelope(message: string, status?: number): ClientEnvelope {
|
|
112
|
+
export function transportEnvelope(message: string, status?: number): ClientEnvelope {
|
|
106
113
|
return {
|
|
107
114
|
data: null,
|
|
108
115
|
error: {
|
|
@@ -154,37 +161,16 @@ async function once(
|
|
|
154
161
|
: rpcRequest(base, key, input);
|
|
155
162
|
|
|
156
163
|
const headers = new Headers();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (const [k, v] of extra) headers.set(k, v);
|
|
160
|
-
} else if (extra) {
|
|
161
|
-
for (const [k, v] of Object.entries(extra)) headers.set(k, v);
|
|
162
|
-
}
|
|
163
|
-
if (Array.isArray(callHeaders)) {
|
|
164
|
-
for (const [k, v] of callHeaders) headers.set(k, v);
|
|
165
|
-
} else if (callHeaders) {
|
|
166
|
-
for (const [k, v] of Object.entries(callHeaders)) headers.set(k, v);
|
|
167
|
-
}
|
|
164
|
+
applyHeaderBag(headers, await resolveHeaders(opts));
|
|
165
|
+
applyHeaderBag(headers, callHeaders);
|
|
168
166
|
if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
|
|
169
167
|
headers.set("content-type", "application/json");
|
|
170
168
|
}
|
|
169
|
+
await applyAuthHeader(headers, opts);
|
|
171
170
|
|
|
172
|
-
const
|
|
173
|
-
opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
|
|
174
|
-
? await opts.auth.getToken()
|
|
175
|
-
: undefined;
|
|
176
|
-
if (token && !headers.has("authorization")) {
|
|
177
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const signals: AbortSignal[] = [];
|
|
181
|
-
if (opts.signal) signals.push(opts.signal);
|
|
182
|
-
if (opts.timeout !== undefined) {
|
|
183
|
-
const t = AbortSignal.timeout(opts.timeout);
|
|
184
|
-
signals.push(t);
|
|
185
|
-
}
|
|
171
|
+
const timeout = opts.timeout !== undefined ? AbortSignal.timeout(opts.timeout) : undefined;
|
|
186
172
|
const signal =
|
|
187
|
-
|
|
173
|
+
opts.signal && timeout ? AbortSignal.any([opts.signal, timeout]) : (opts.signal ?? timeout);
|
|
188
174
|
|
|
189
175
|
return await fetchFn(url, {
|
|
190
176
|
method,
|
|
@@ -236,47 +222,32 @@ function restRequest(
|
|
|
236
222
|
if (isRawBody(input)) {
|
|
237
223
|
return { url: `${base}${path}`, method: method.toUpperCase(), body: input };
|
|
238
224
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const query: string[] = [];
|
|
243
|
-
const rest: Record<string, unknown> = {};
|
|
244
|
-
|
|
245
|
-
for (const [k, v] of Object.entries(params)) {
|
|
246
|
-
const token = `:${k}`;
|
|
247
|
-
if (pathOut.includes(token)) {
|
|
248
|
-
pathOut = pathOut.replaceAll(token, encodeURIComponent(String(v)));
|
|
249
|
-
} else {
|
|
250
|
-
rest[k] = v;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
225
|
+
const interpolated = interpolatePath(path, input);
|
|
226
|
+
const pathOut = interpolated.path;
|
|
227
|
+
const rest = interpolated.rest;
|
|
253
228
|
|
|
254
229
|
const upper = method.toUpperCase();
|
|
230
|
+
const hasRest = Object.keys(rest).length > 0;
|
|
255
231
|
let body: ClientBodyInit | undefined;
|
|
232
|
+
let qs = "";
|
|
256
233
|
if (upper === "GET" || upper === "HEAD") {
|
|
257
|
-
|
|
258
|
-
if (v !== undefined) {
|
|
259
|
-
query.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
234
|
+
qs = toQuery(rest);
|
|
262
235
|
} else if (upper === "QUERY") {
|
|
263
236
|
// RFC 10008 QUERY always carries JSON content (empty object when only path params).
|
|
264
|
-
body = JSON.stringify(
|
|
265
|
-
} else if (
|
|
266
|
-
body = JSON.stringify(
|
|
237
|
+
body = JSON.stringify(hasRest ? rest : {});
|
|
238
|
+
} else if (hasRest || path === pathOut) {
|
|
239
|
+
body = JSON.stringify(hasRest ? rest : (input ?? {}));
|
|
267
240
|
}
|
|
268
241
|
|
|
269
|
-
const qs = query.length ? `?${query.join("&")}` : "";
|
|
270
242
|
return { url: `${base}${pathOut}${qs}`, method: upper, body };
|
|
271
243
|
}
|
|
272
244
|
|
|
273
245
|
function isRawBody(input: unknown): input is ClientBodyInit {
|
|
274
|
-
if (input
|
|
275
|
-
if (
|
|
276
|
-
if (
|
|
277
|
-
if (
|
|
278
|
-
if (
|
|
279
|
-
if (typeof ReadableStream !== "undefined" && input instanceof ReadableStream) return true;
|
|
246
|
+
if (input instanceof Blob) return true;
|
|
247
|
+
if (input instanceof FormData) return true;
|
|
248
|
+
if (input instanceof ArrayBuffer) return true;
|
|
249
|
+
if (ArrayBuffer.isView(input)) return true;
|
|
250
|
+
if (input instanceof ReadableStream) return true;
|
|
280
251
|
return false;
|
|
281
252
|
}
|
|
282
253
|
|
|
@@ -311,7 +282,7 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
311
282
|
|
|
312
283
|
async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promise<ClientEnvelope> {
|
|
313
284
|
if (!res.ok) {
|
|
314
|
-
const structured = await
|
|
285
|
+
const structured = await decodeIfEnvelope(res);
|
|
315
286
|
if (structured) return structured;
|
|
316
287
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
317
288
|
}
|
|
@@ -319,22 +290,6 @@ async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promis
|
|
|
319
290
|
return { data, error: null };
|
|
320
291
|
}
|
|
321
292
|
|
|
322
|
-
/** Try JSON envelope from an error response without assuming the body is reusable. */
|
|
323
|
-
async function decodeIfEnvelopeClone(res: Response): Promise<ClientEnvelope | null> {
|
|
324
|
-
const text = await res.text();
|
|
325
|
-
if (!text) return null;
|
|
326
|
-
let json: unknown;
|
|
327
|
-
try {
|
|
328
|
-
json = JSON.parse(text);
|
|
329
|
-
} catch {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
333
|
-
return json as ClientEnvelope;
|
|
334
|
-
}
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
293
|
function isTransient(err: unknown): boolean {
|
|
339
294
|
if (!(err instanceof Error)) return false;
|
|
340
295
|
if (err.name === "AbortError") return false;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared REST wire helpers for transport, live subscribe, and streams.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ClientHeaders, ClientOptions, ClientRouteMap, FlowContract } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Visit every flow contract in `app.$routes`.
|
|
9
|
+
*
|
|
10
|
+
* @param $routes - Runtime route map
|
|
11
|
+
* @param visit - Called with unit, flow, and contract
|
|
12
|
+
*/
|
|
13
|
+
export function walkContracts(
|
|
14
|
+
$routes: ClientRouteMap | undefined,
|
|
15
|
+
visit: (unit: string, flow: string, contract: FlowContract) => void,
|
|
16
|
+
): void {
|
|
17
|
+
if (!$routes) return;
|
|
18
|
+
for (const [unit, flows] of Object.entries($routes)) {
|
|
19
|
+
if (!flows || typeof flows !== "object") continue;
|
|
20
|
+
for (const [flow, contract] of Object.entries(flows)) {
|
|
21
|
+
if (!contract || typeof contract !== "object") continue;
|
|
22
|
+
visit(unit, flow, contract);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read `method` and `path` when both are strings.
|
|
29
|
+
*
|
|
30
|
+
* @param contract - One flow contract
|
|
31
|
+
*/
|
|
32
|
+
export function methodAndPath(
|
|
33
|
+
contract: object,
|
|
34
|
+
): { readonly method: string; readonly path: string } | undefined {
|
|
35
|
+
const method = "method" in contract ? contract.method : undefined;
|
|
36
|
+
const path = "path" in contract ? contract.path : undefined;
|
|
37
|
+
if (typeof method === "string" && typeof path === "string") return { method, path };
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Replace `:param` tokens in `path`. Unmatched keys land in `rest`.
|
|
43
|
+
*
|
|
44
|
+
* @param path - Path template
|
|
45
|
+
* @param input - JSON object, or anything else (ignored)
|
|
46
|
+
*/
|
|
47
|
+
export function interpolatePath(
|
|
48
|
+
path: string,
|
|
49
|
+
input: unknown,
|
|
50
|
+
): { readonly path: string; readonly rest: Record<string, unknown> } {
|
|
51
|
+
const params =
|
|
52
|
+
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
53
|
+
let pathOut = path;
|
|
54
|
+
const rest: Record<string, unknown> = {};
|
|
55
|
+
for (const [k, v] of Object.entries(params)) {
|
|
56
|
+
const token = `:${k}`;
|
|
57
|
+
if (pathOut.includes(token)) {
|
|
58
|
+
pathOut = pathOut.replaceAll(token, encodeURIComponent(String(v)));
|
|
59
|
+
} else {
|
|
60
|
+
rest[k] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { path: pathOut, rest };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Encode a query string, skipping `undefined` values.
|
|
68
|
+
*
|
|
69
|
+
* @param rest - Keys that were not path params
|
|
70
|
+
*/
|
|
71
|
+
export function toQuery(rest: Record<string, unknown>): string {
|
|
72
|
+
const query: string[] = [];
|
|
73
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
74
|
+
if (v !== undefined) {
|
|
75
|
+
query.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return query.length > 0 ? `?${query.join("&")}` : "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Copy a header bag onto `headers` (later keys win within the bag).
|
|
83
|
+
*
|
|
84
|
+
* @param headers - Target
|
|
85
|
+
* @param extra - Record or tuple list
|
|
86
|
+
*/
|
|
87
|
+
export function applyHeaderBag(headers: Headers, extra: ClientHeaders | undefined): void {
|
|
88
|
+
if (!extra) return;
|
|
89
|
+
if (Array.isArray(extra)) {
|
|
90
|
+
for (const [k, v] of extra) headers.set(k, v);
|
|
91
|
+
} else {
|
|
92
|
+
for (const [k, v] of Object.entries(extra)) headers.set(k, v);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve `opts.headers` when it is a function.
|
|
98
|
+
*
|
|
99
|
+
* @param opts - Client options
|
|
100
|
+
*/
|
|
101
|
+
export async function resolveHeaders(opts: ClientOptions): Promise<ClientHeaders | undefined> {
|
|
102
|
+
return typeof opts.headers === "function" ? await opts.headers() : opts.headers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set `Authorization` from `opts.auth.getToken` when the caller did not.
|
|
107
|
+
*
|
|
108
|
+
* @param headers - Target
|
|
109
|
+
* @param opts - Client options
|
|
110
|
+
*/
|
|
111
|
+
export async function applyAuthHeader(headers: Headers, opts: ClientOptions): Promise<void> {
|
|
112
|
+
const token =
|
|
113
|
+
opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
|
|
114
|
+
? await opts.auth.getToken()
|
|
115
|
+
: undefined;
|
|
116
|
+
if (token && !headers.has("authorization")) {
|
|
117
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/compiler/aot.ts
CHANGED
|
@@ -6,30 +6,20 @@
|
|
|
6
6
|
* {@link compileDynamic} / `aot: false` on edge runtimes that ban `eval`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { FlowFailure } from "../kernel/errors.ts";
|
|
10
9
|
import { validate, type SchemaInput } from "../validation/standard-schema.ts";
|
|
11
10
|
import {
|
|
12
11
|
assembleInput,
|
|
13
|
-
extractParts,
|
|
14
12
|
parseBody,
|
|
15
13
|
parseCookie,
|
|
16
14
|
parseHeaders,
|
|
17
15
|
parseQuery,
|
|
18
16
|
type ContextInference,
|
|
19
|
-
type InputParts,
|
|
20
17
|
} from "./http-parse.ts";
|
|
18
|
+
import { createInterpretedParseValidate, type CompiledParseValidate } from "./interpret.ts";
|
|
21
19
|
import { sucrose } from "./sucrose.ts";
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
| { readonly ok: true; readonly input: unknown }
|
|
26
|
-
| { readonly ok: false; readonly failure: FlowFailure };
|
|
27
|
-
|
|
28
|
-
/** Compiled parse/validate function. */
|
|
29
|
-
export type CompiledParseValidate = (
|
|
30
|
-
request: Request,
|
|
31
|
-
params: Readonly<Record<string, string>>,
|
|
32
|
-
) => Promise<ParseValidateResult>;
|
|
21
|
+
export type { CompiledParseValidate, ParseValidateResult } from "./interpret.ts";
|
|
22
|
+
export { createInterpretedParseValidate };
|
|
33
23
|
|
|
34
24
|
/** Options for {@link compileAot}. */
|
|
35
25
|
export interface CompileRouteOptions {
|
|
@@ -152,25 +142,6 @@ function generateParseValidate(
|
|
|
152
142
|
return factory(helpers);
|
|
153
143
|
}
|
|
154
144
|
|
|
155
|
-
/**
|
|
156
|
-
* Interpreted parse/validate using the same helpers (AoT fallback).
|
|
157
|
-
*
|
|
158
|
-
* @param inference - Context flags
|
|
159
|
-
* @param schema - Input schema
|
|
160
|
-
*/
|
|
161
|
-
export function createInterpretedParseValidate(
|
|
162
|
-
inference: ContextInference,
|
|
163
|
-
schema: SchemaInput | undefined,
|
|
164
|
-
): CompiledParseValidate {
|
|
165
|
-
return async (request, params) => {
|
|
166
|
-
const parts: InputParts = await extractParts(request, params, inference);
|
|
167
|
-
const raw = assembleInput(parts);
|
|
168
|
-
const result = await validate(schema, raw);
|
|
169
|
-
if (!result.ok) return { ok: false, failure: result.failure };
|
|
170
|
-
return { ok: true, input: result.value };
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
145
|
/**
|
|
175
146
|
* Re-export sucrose for tests and tooling.
|
|
176
147
|
*/
|
package/src/compiler/dynamic.ts
CHANGED
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
* a requirement). Produces byte-identical responses to {@link compileAot}.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
10
11
|
import type { SchemaInput } from "../validation/standard-schema.ts";
|
|
11
|
-
import {
|
|
12
|
-
compileAot,
|
|
13
|
-
createInterpretedParseValidate,
|
|
14
|
-
type CompiledRoute,
|
|
15
|
-
type CompileRouteOptions,
|
|
16
|
-
} from "./aot.ts";
|
|
12
|
+
import type { CompiledRoute, CompileRouteOptions } from "./aot.ts";
|
|
17
13
|
import { FULL_INFERENCE } from "./http-parse.ts";
|
|
18
|
-
import {
|
|
14
|
+
import { createInterpretedParseValidate } from "./interpret.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load AoT codegen. Computed stem so the edge profile keeps the interpreted path only.
|
|
18
|
+
*/
|
|
19
|
+
function loadAot(): typeof import("./aot.ts") {
|
|
20
|
+
return lazyRequire(import.meta.dir, ["ao", "t"].join(""));
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Compile a route with the dynamic fallback (full context every request).
|
|
@@ -23,10 +26,9 @@ import { dynamicInference } from "./sucrose.ts";
|
|
|
23
26
|
* @param options - Route metadata
|
|
24
27
|
*/
|
|
25
28
|
export function compileDynamic(options: CompileRouteOptions): CompiledRoute {
|
|
26
|
-
const inference = dynamicInference();
|
|
27
29
|
return {
|
|
28
|
-
inference,
|
|
29
|
-
parseValidate: createInterpretedParseValidate(
|
|
30
|
+
inference: FULL_INFERENCE,
|
|
31
|
+
parseValidate: createInterpretedParseValidate(FULL_INFERENCE, options.schema),
|
|
30
32
|
aot: false,
|
|
31
33
|
};
|
|
32
34
|
}
|
|
@@ -39,7 +41,7 @@ export function compileDynamic(options: CompileRouteOptions): CompiledRoute {
|
|
|
39
41
|
*/
|
|
40
42
|
export function compileRoute(options: CompileRouteOptions, aot: boolean = true): CompiledRoute {
|
|
41
43
|
if (!aot) return compileDynamic(options);
|
|
42
|
-
return compileAot(options);
|
|
44
|
+
return loadAot().compileAot(options);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpreted parse/validate — the edge path.
|
|
3
|
+
*
|
|
4
|
+
* AoT (`new Function`) lives in {@link compileAot} and is loaded only when
|
|
5
|
+
* requested, so eval-restricted runtimes do not carry the codegen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FlowFailure } from "../kernel/errors.ts";
|
|
9
|
+
import { validate, type SchemaInput } from "../validation/standard-schema.ts";
|
|
10
|
+
import {
|
|
11
|
+
assembleInput,
|
|
12
|
+
extractParts,
|
|
13
|
+
type ContextInference,
|
|
14
|
+
type InputParts,
|
|
15
|
+
} from "./http-parse.ts";
|
|
16
|
+
|
|
17
|
+
/** Result of parse + validate for one request. */
|
|
18
|
+
export type ParseValidateResult =
|
|
19
|
+
| { readonly ok: true; readonly input: unknown }
|
|
20
|
+
| { readonly ok: false; readonly failure: FlowFailure };
|
|
21
|
+
|
|
22
|
+
/** Compiled parse/validate function. */
|
|
23
|
+
export type CompiledParseValidate = (
|
|
24
|
+
request: Request,
|
|
25
|
+
params: Readonly<Record<string, string>>,
|
|
26
|
+
) => Promise<ParseValidateResult>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Interpreted parse/validate using the shared HTTP helpers (AoT fallback).
|
|
30
|
+
*
|
|
31
|
+
* @param inference - Context flags
|
|
32
|
+
* @param schema - Input schema
|
|
33
|
+
*/
|
|
34
|
+
export function createInterpretedParseValidate(
|
|
35
|
+
inference: ContextInference,
|
|
36
|
+
schema: SchemaInput | undefined,
|
|
37
|
+
): CompiledParseValidate {
|
|
38
|
+
return async (request, params) => {
|
|
39
|
+
const parts: InputParts = await extractParts(request, params, inference);
|
|
40
|
+
const raw = assembleInput(parts);
|
|
41
|
+
const result = await validate(schema, raw);
|
|
42
|
+
if (!result.ok) return { ok: false, failure: result.failure };
|
|
43
|
+
return { ok: true, input: result.value };
|
|
44
|
+
};
|
|
45
|
+
}
|