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.
Files changed (45) hide show
  1. package/package.json +2 -7
  2. package/src/cli/dev.test.ts +105 -2
  3. package/src/cli/dev.ts +37 -2
  4. package/src/client/create.ts +13 -26
  5. package/src/client/live.ts +44 -101
  6. package/src/client/sse.ts +20 -66
  7. package/src/client/stream.ts +25 -67
  8. package/src/client/transport.ts +28 -73
  9. package/src/client/wire.ts +119 -0
  10. package/src/compiler/aot.ts +3 -32
  11. package/src/compiler/dynamic.ts +13 -11
  12. package/src/compiler/interpret.ts +45 -0
  13. package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Bgt9bq2r.js} +1 -1
  14. package/src/console/ui-next/dist/assets/{agent-disclosure-CSKumwS2.js → agent-disclosure-B43CXZZR.js} +1 -1
  15. package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-92uM5MO7.js} +1 -1
  16. package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-DtGVPtcs.js} +1 -1
  17. package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-DN7l6zmC.js} +1 -1
  18. package/src/console/ui-next/dist/assets/{duration-tone-CwoV56jn.js → duration-tone-BmIR9FV8.js} +1 -1
  19. package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-BMHs-IzK.js} +1 -1
  20. package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-BlAEVgNW.js} +1 -1
  21. package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-BDf7OAHv.js} +1 -1
  22. package/src/console/ui-next/dist/assets/{index-Cul17AcV.js → index-BKpaes3n.js} +3 -3
  23. package/src/console/ui-next/dist/assets/{observability-page-oY9vdYBk.js → observability-page-WnVLI-0j.js} +1 -1
  24. package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-B3GLNVfF.js} +1 -1
  25. package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-DMbnAe3f.js} +1 -1
  26. package/src/console/ui-next/dist/assets/{store-page-CL0D9dOq.js → store-page-KvFDingJ.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
  29. package/src/console/ui-next/dist/assets/{units-page-1PlT19ft.js → units-page-C4NdNuxP.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-CBYl0LW_.js} +1 -1
  31. package/src/console/ui-next/dist/index.html +1 -1
  32. package/src/elements/store/sql-session.ts +20 -4
  33. package/src/kernel/app.ts +9 -2
  34. package/src/kernel/client-descriptor.test.ts +78 -0
  35. package/src/kernel/client-descriptor.ts +23 -0
  36. package/src/kernel/errors-text.ts +99 -0
  37. package/src/kernel/errors.ts +90 -138
  38. package/src/kernel/fx-sql-handle.ts +305 -0
  39. package/src/kernel/fx.ts +37 -329
  40. package/src/kernel/json-result.ts +59 -0
  41. package/src/kernel/project-out.ts +6 -1
  42. package/src/okid-extended.ts +175 -0
  43. package/src/okid-shared.ts +103 -0
  44. package/src/okid.ts +30 -213
  45. package/src/release/build-lib.ts +9 -0
@@ -4,8 +4,17 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { iterateSse, sseError } from "./sse.ts";
8
- import type { ClientFetch, ClientHeaders, ClientOptions, ClientRouteMap } from "./types.ts";
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
- if (!$routes) return out;
23
- for (const [unit, flows] of Object.entries($routes)) {
24
- if (!flows || typeof flows !== "object") continue;
25
- for (const [flow, contract] of Object.entries(flows)) {
26
- if (!contract || typeof contract !== "object") continue;
27
- if (!("stream" in contract) || contract.stream !== true) continue;
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
- const extra = typeof opts.headers === "function" ? await opts.headers() : opts.headers;
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
- const token =
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: ctrl.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
- yield* iterateSse(res, ctrl.signal);
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 params =
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
- const query: string[] = [];
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
- }
@@ -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
- const extra = typeof opts.headers === "function" ? await opts.headers() : opts.headers;
158
- if (Array.isArray(extra)) {
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 token =
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
- signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
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 params =
240
- input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
241
- let pathOut = path;
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
- for (const [k, v] of Object.entries(rest)) {
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(Object.keys(rest).length > 0 ? rest : {});
265
- } else if (Object.keys(rest).length > 0 || path === pathOut) {
266
- body = JSON.stringify(Object.keys(rest).length > 0 ? rest : (input ?? {}));
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 === null || input === undefined) return false;
275
- if (typeof Blob !== "undefined" && input instanceof Blob) return true;
276
- if (typeof FormData !== "undefined" && input instanceof FormData) return true;
277
- if (typeof ArrayBuffer !== "undefined" && input instanceof ArrayBuffer) return true;
278
- if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(input)) return true;
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 decodeIfEnvelopeClone(res);
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
+ }
@@ -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
- /** Result of parse + validate for one request. */
24
- export type ParseValidateResult =
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
  */
@@ -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 { dynamicInference } from "./sucrose.ts";
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(inference, options.schema),
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
+ }