okengine 0.19.9 → 0.21.0
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/AGENTS.md +1 -1
- package/package.json +2 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +125 -37
- package/site/content/docs/client/index.mdx +7 -7
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +42 -23
- package/site/content/docs/elements/flow/index.mdx +41 -30
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +13 -6
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +100 -39
- package/site/content/docs/elements/vault/index.mdx +14 -13
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +7 -4
- package/site/content/docs/reference/configuration.mdx +8 -8
- package/site/content/docs/reference/errors.mdx +229 -55
- package/site/content/docs/reference/fx.mdx +22 -9
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +759 -24
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +2 -1
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +13 -11
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +3 -3
- package/src/client/explain.test.ts +252 -0
- package/src/client/explain.ts +272 -0
- package/src/client/live.test.ts +44 -0
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +6 -2
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +24 -40
- package/src/client/types.ts +17 -6
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/live-resource.ts +6 -2
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/compiler/response.ts +17 -27
- package/src/config/index.ts +6 -4
- package/src/console/server/invoke-user-flow.test.ts +8 -2
- package/src/console/server/invoke-user-flow.ts +12 -18
- package/src/console/server/security.gate.test.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/docker/docker.test.ts +3 -3
- package/src/docker/images-config.test.ts +4 -4
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/files-errors.test.ts +149 -0
- package/src/elements/store/files-errors.ts +189 -0
- package/src/elements/store/kv-errors.test.ts +98 -0
- package/src/elements/store/kv-errors.ts +139 -0
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-errors.test.ts +197 -0
- package/src/elements/store/sql-errors.ts +294 -0
- package/src/elements/store/sql-session.test.ts +52 -0
- package/src/elements/store/sql-session.ts +50 -2
- package/src/elements/store/store-errors.ts +47 -0
- package/src/elements/store/table.ts +8 -6
- package/src/http.ts +9 -1
- package/src/i18n/catalogs/ar.ts +18 -0
- package/src/i18n/catalogs/en.ts +18 -0
- package/src/index.ts +9 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +40 -34
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/builtin-errors.test.ts +117 -0
- package/src/kernel/builtin-errors.ts +129 -0
- package/src/kernel/call.test.ts +182 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +97 -24
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +24 -8
- package/src/kernel/index.ts +12 -1
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- package/src/runs/duckdb.test.ts +2 -2
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
package/src/client/transport.ts
CHANGED
|
@@ -83,15 +83,7 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
83
83
|
} catch (err) {
|
|
84
84
|
const transient = isTransient(err);
|
|
85
85
|
if (!transient || attempt >= retries) {
|
|
86
|
-
return
|
|
87
|
-
data: null,
|
|
88
|
-
error: {
|
|
89
|
-
code: "TransportError",
|
|
90
|
-
data: {
|
|
91
|
-
message: err instanceof Error ? err.message : String(err),
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
};
|
|
86
|
+
return transportEnvelope(err instanceof Error ? err.message : String(err));
|
|
95
87
|
}
|
|
96
88
|
await sleep(delay);
|
|
97
89
|
delay *= backoff;
|
|
@@ -102,6 +94,25 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
102
94
|
};
|
|
103
95
|
}
|
|
104
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Build a {@link ClientEnvelope} for a transport / protocol failure.
|
|
99
|
+
*
|
|
100
|
+
* Guarantees `error.message === error.data.message`.
|
|
101
|
+
*
|
|
102
|
+
* @param message - Human-readable failure text
|
|
103
|
+
* @param status - Optional HTTP status
|
|
104
|
+
*/
|
|
105
|
+
function transportEnvelope(message: string, status?: number): ClientEnvelope {
|
|
106
|
+
return {
|
|
107
|
+
data: null,
|
|
108
|
+
error: {
|
|
109
|
+
code: "TransportError",
|
|
110
|
+
message,
|
|
111
|
+
data: status !== undefined ? { message, status } : { message },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
105
116
|
function normalizeCallOpts(
|
|
106
117
|
headersOrOpts: ClientHeaders | TransportCallOptions | undefined,
|
|
107
118
|
): TransportCallOptions {
|
|
@@ -277,29 +288,14 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
277
288
|
const text = await res.text();
|
|
278
289
|
if (!text) {
|
|
279
290
|
if (res.ok) return { data: undefined, error: null };
|
|
280
|
-
return {
|
|
281
|
-
data: null,
|
|
282
|
-
error: {
|
|
283
|
-
code: "TransportError",
|
|
284
|
-
data: { message: `HTTP ${res.status}`, status: res.status },
|
|
285
|
-
},
|
|
286
|
-
};
|
|
291
|
+
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
let json: unknown;
|
|
290
295
|
try {
|
|
291
296
|
json = JSON.parse(text);
|
|
292
297
|
} catch {
|
|
293
|
-
return {
|
|
294
|
-
data: null,
|
|
295
|
-
error: {
|
|
296
|
-
code: "TransportError",
|
|
297
|
-
data: {
|
|
298
|
-
message: `Invalid JSON (${res.status})`,
|
|
299
|
-
status: res.status,
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
};
|
|
298
|
+
return transportEnvelope(`Invalid JSON (${res.status})`, res.status);
|
|
303
299
|
}
|
|
304
300
|
|
|
305
301
|
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
@@ -310,26 +306,14 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
310
306
|
return { data: json, error: null };
|
|
311
307
|
}
|
|
312
308
|
|
|
313
|
-
return {
|
|
314
|
-
data: null,
|
|
315
|
-
error: {
|
|
316
|
-
code: "TransportError",
|
|
317
|
-
data: { message: `HTTP ${res.status}`, status: res.status },
|
|
318
|
-
},
|
|
319
|
-
};
|
|
309
|
+
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
320
310
|
}
|
|
321
311
|
|
|
322
312
|
async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promise<ClientEnvelope> {
|
|
323
313
|
if (!res.ok) {
|
|
324
314
|
const structured = await decodeIfEnvelopeClone(res);
|
|
325
315
|
if (structured) return structured;
|
|
326
|
-
return {
|
|
327
|
-
data: null,
|
|
328
|
-
error: {
|
|
329
|
-
code: "TransportError",
|
|
330
|
-
data: { message: `HTTP ${res.status}`, status: res.status },
|
|
331
|
-
},
|
|
332
|
-
};
|
|
316
|
+
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
333
317
|
}
|
|
334
318
|
const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
|
|
335
319
|
return { data, error: null };
|
package/src/client/types.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Local / separate-repo: augment {@link Register} so `createClient(url)` needs no import.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { BuiltinErrorMap } from "../kernel/builtin-errors.ts";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* One flow's client contract. `in` / `out` / `errors` are phantom (type-only).
|
|
10
12
|
*
|
|
@@ -125,7 +127,8 @@ export interface TransportError {
|
|
|
125
127
|
readonly message: string;
|
|
126
128
|
readonly status?: number;
|
|
127
129
|
};
|
|
128
|
-
|
|
130
|
+
/** Same string as {@link TransportError.data.message}. */
|
|
131
|
+
readonly message: string;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/** Next / previous list request — TanStack `pageParam` / URL bag. */
|
|
@@ -431,12 +434,20 @@ type ContractIn<C> = "in" extends keyof C
|
|
|
431
434
|
/** Pull output from a contract shape. */
|
|
432
435
|
type ContractOut<C> = "out" extends keyof C ? NonNullable<C["out"]> : unknown;
|
|
433
436
|
|
|
434
|
-
/**
|
|
437
|
+
/**
|
|
438
|
+
* Pull error map from a contract shape. Built-in codes are always on; declared
|
|
439
|
+
* keys win. A missing `errors` bag (phantom `Record<string, unknown>`) is
|
|
440
|
+
* treated as unspecified — not as an index signature that wipes the builtins.
|
|
441
|
+
*/
|
|
435
442
|
type ContractErrors<C> = "errors" extends keyof C
|
|
436
|
-
? NonNullable<C["errors"]> extends
|
|
437
|
-
?
|
|
438
|
-
|
|
439
|
-
|
|
443
|
+
? NonNullable<C["errors"]> extends infer E
|
|
444
|
+
? E extends Record<string, unknown>
|
|
445
|
+
? string extends keyof E
|
|
446
|
+
? BuiltinErrorMap
|
|
447
|
+
: Omit<BuiltinErrorMap, keyof E> & E
|
|
448
|
+
: BuiltinErrorMap
|
|
449
|
+
: BuiltinErrorMap
|
|
450
|
+
: BuiltinErrorMap;
|
|
440
451
|
|
|
441
452
|
/**
|
|
442
453
|
* Typed client proxy derived from a route map.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser graph for `okengine/client-react` — Vite SPAs import `Can` from the
|
|
3
|
+
* barrel, which also loads `useLiveQuery`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
describe("client-react browser graph", () => {
|
|
10
|
+
test("browser bundle does not pull node:async_hooks", async () => {
|
|
11
|
+
const result = await Bun.build({
|
|
12
|
+
entrypoints: [join(import.meta.dir, "index.ts")],
|
|
13
|
+
target: "browser",
|
|
14
|
+
format: "esm",
|
|
15
|
+
minify: false,
|
|
16
|
+
external: ["react", "react/jsx-runtime", "react-dom"],
|
|
17
|
+
});
|
|
18
|
+
expect(result.success).toBe(true);
|
|
19
|
+
const text = (await Promise.all(result.outputs.map((output) => output.text()))).join("\n");
|
|
20
|
+
expect(text).not.toContain("node:async_hooks");
|
|
21
|
+
expect(text).not.toContain("realtime-bind");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -219,8 +219,12 @@ function sseError(status: number, body: string): Error {
|
|
|
219
219
|
try {
|
|
220
220
|
const json: unknown = JSON.parse(body);
|
|
221
221
|
if (json !== null && typeof json === "object" && "error" in json) {
|
|
222
|
-
const err = (
|
|
223
|
-
|
|
222
|
+
const err = (
|
|
223
|
+
json as {
|
|
224
|
+
error?: { code?: string; message?: string; data?: { message?: string } };
|
|
225
|
+
}
|
|
226
|
+
).error;
|
|
227
|
+
message = err?.message ?? err?.data?.message ?? err?.code ?? message;
|
|
224
228
|
}
|
|
225
229
|
} catch {
|
|
226
230
|
message = body.slice(0, 200);
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
24
24
|
import { transportOf } from "../client/create.ts";
|
|
25
25
|
import type { ClientCall, ClientResult } from "../client/types.ts";
|
|
26
|
-
import { MUTATION_ID_HEADER } from "../kernel/
|
|
26
|
+
import { MUTATION_ID_HEADER } from "../kernel/mutation-id.ts";
|
|
27
27
|
import {
|
|
28
28
|
applyOptimisticPatch,
|
|
29
29
|
clearOptimisticPatch,
|
|
@@ -67,6 +67,7 @@ describe("pathFromFlowFile", () => {
|
|
|
67
67
|
expect(pathFromFlowFile("notes/list.test.ts")).toBeUndefined();
|
|
68
68
|
expect(pathFromFlowFile("notes/_lib/util.ts")).toBeUndefined();
|
|
69
69
|
expect(pathFromFlowFile("src/flows/generated.ts")).toBeUndefined();
|
|
70
|
+
expect(pathFromFlowFile("src/flows/index.ts")).toBeUndefined();
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
test("accepts a source path under src/flows/", () => {
|
|
@@ -17,7 +17,7 @@ export const RESERVED_LEAVES: ReadonlySet<string> = new Set([
|
|
|
17
17
|
"route",
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
/** Filenames that are never routes. */
|
|
20
|
+
/** Filenames that are never HTTP routes (`signals.ts` may still export `on()` consumers). */
|
|
21
21
|
export const SKIP_FLOW_FILENAMES: ReadonlySet<string> = new Set([
|
|
22
22
|
"generated.ts",
|
|
23
23
|
"shapes.ts",
|
|
@@ -69,7 +69,7 @@ describe("generateAdoptBarrel", () => {
|
|
|
69
69
|
'export const create = "notes-flow";\n',
|
|
70
70
|
);
|
|
71
71
|
const result = await generateAdoptBarrel({ rootDir: root });
|
|
72
|
-
const generatedPath = join(root, "src/flows/
|
|
72
|
+
const generatedPath = join(root, "src/flows/index.ts");
|
|
73
73
|
await mkdir(join(root, "node_modules/okengine"), { recursive: true });
|
|
74
74
|
await writeFile(
|
|
75
75
|
join(root, "node_modules/okengine/package.json"),
|
|
@@ -173,12 +173,66 @@ describe("generateAdoptBarrel", () => {
|
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
test("writeAdoptBarrel unlinks leftover generated.ts", async () => {
|
|
177
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-prev-"));
|
|
178
|
+
try {
|
|
179
|
+
await mkdir(join(root, "src/flows"), { recursive: true });
|
|
180
|
+
const prev = join(root, "src/flows/generated.ts");
|
|
181
|
+
await writeFile(prev, "// old\n");
|
|
182
|
+
expect((await writeAdoptBarrel(root, "// new\n")).written).toBe(true);
|
|
183
|
+
expect(await Bun.file(join(root, "src/flows/index.ts")).text()).toBe("// new\n");
|
|
184
|
+
expect(await Bun.file(prev).exists()).toBe(false);
|
|
185
|
+
await writeFile(prev, "// leftover\n");
|
|
186
|
+
expect((await writeAdoptBarrel(root, "// new\n")).written).toBe(false);
|
|
187
|
+
expect(await Bun.file(prev).exists()).toBe(false);
|
|
188
|
+
} finally {
|
|
189
|
+
await rm(root, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
176
193
|
test("import specifiers stay POSIX when the walked path used backslash", () => {
|
|
177
194
|
const spec = importSpecifierFromWalked("notes\\[id]\\get.ts");
|
|
178
195
|
expect(spec).toBe("./notes/[id]/get.ts");
|
|
179
196
|
expect(spec.includes("\\")).toBe(false);
|
|
180
197
|
});
|
|
181
198
|
|
|
199
|
+
test("signals.ts on() consumers join the unit; signal.once decls do not", async () => {
|
|
200
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-signals-"));
|
|
201
|
+
try {
|
|
202
|
+
await mkdir(join(root, "src/flows/links"), { recursive: true });
|
|
203
|
+
await writeFile(
|
|
204
|
+
join(root, "src/flows/links/create.ts"),
|
|
205
|
+
"export const create = on(http.post(), flow({ do: () => 1 }));\n",
|
|
206
|
+
);
|
|
207
|
+
await writeFile(
|
|
208
|
+
join(root, "src/flows/links/signals.ts"),
|
|
209
|
+
[
|
|
210
|
+
'export const linkCreated = signal.once("link-created");',
|
|
211
|
+
"export const onCreated = on(",
|
|
212
|
+
" linkCreated,",
|
|
213
|
+
' flow("links.onCreated", { do: () => 1 }),',
|
|
214
|
+
");",
|
|
215
|
+
"export const onClicked = on(",
|
|
216
|
+
" linkClicked,",
|
|
217
|
+
' flow("links.onClicked", { do: () => 1 }),',
|
|
218
|
+
");",
|
|
219
|
+
].join("\n"),
|
|
220
|
+
);
|
|
221
|
+
await writeFile(join(root, "src/flows/links/shapes.ts"), "export const LinkOut = 1;\n");
|
|
222
|
+
const result = await generateAdoptBarrel({ rootDir: root });
|
|
223
|
+
expect(result.source).toContain('from "./links/signals.ts"');
|
|
224
|
+
expect(result.source).toContain("links_signals.onCreated");
|
|
225
|
+
expect(result.source).toContain("links_signals.onClicked");
|
|
226
|
+
expect(result.source).toContain('"/links/on-created"');
|
|
227
|
+
expect(result.source).toContain('"/links/on-clicked"');
|
|
228
|
+
expect(result.source).not.toContain("linkCreated");
|
|
229
|
+
expect(result.source).not.toContain("LinkOut");
|
|
230
|
+
expect(result.source).not.toContain("shapes.ts");
|
|
231
|
+
} finally {
|
|
232
|
+
await rm(root, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
182
236
|
test("[...slug] unit adds the * client-key comment", async () => {
|
|
183
237
|
const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
|
|
184
238
|
try {
|
|
@@ -51,15 +51,24 @@ const CATCHALL_COMMENT =
|
|
|
51
51
|
'// Catch-all param is "*" — call api.docs.get({ "*": "a/b/c" }), not { slug }.\n';
|
|
52
52
|
|
|
53
53
|
/** Filename of the on-disk `.adopt()` barrel under the flows directory. */
|
|
54
|
-
export const ADOPT_BARREL_FILE = "
|
|
54
|
+
export const ADOPT_BARREL_FILE = "index.ts";
|
|
55
|
+
|
|
56
|
+
/** Previous barrel filename — unlinked after writing {@link ADOPT_BARREL_FILE}. */
|
|
57
|
+
export const PREV_ADOPT_BARREL_FILE = "generated.ts";
|
|
55
58
|
|
|
56
59
|
/** A folder name usable as an `export * as <name>` binding. */
|
|
57
60
|
const VALID_UNIT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
58
61
|
|
|
59
62
|
const EXPORT_CONST = /^export const ([A-Za-z_$][A-Za-z0-9_$]*)\b/gm;
|
|
60
63
|
|
|
64
|
+
/** `export const onCreated = on(` — consumers colocated with `signal.once` in `signals.ts`. */
|
|
65
|
+
const EXPORT_ON = /^export const ([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*on\s*\(/gm;
|
|
66
|
+
|
|
61
67
|
const EXPLICIT_FLOW_UNIT = /flow\(\s*["']([A-Za-z_$][A-Za-z0-9_$]*)\./g;
|
|
62
68
|
|
|
69
|
+
/** Skip-list file that may still export `on()` consumers (not `signal.once` decls). */
|
|
70
|
+
const SIGNALS_FILE = "signals.ts";
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* Generate-time failure (mixed barrel + tree, export collision, unit drift).
|
|
65
74
|
*/
|
|
@@ -86,9 +95,28 @@ interface WalkedFlowFile {
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
98
|
+
* True when `rel` is the adopt barrel (or its `.tmp`) at the flows root —
|
|
99
|
+
* not a unit `index.ts`.
|
|
100
|
+
*
|
|
101
|
+
* @param rel - Watcher or source path (POSIX or Windows)
|
|
102
|
+
*/
|
|
103
|
+
export function isAdoptBarrelRelPath(rel: string): boolean {
|
|
104
|
+
const posix = toPosixPath(rel);
|
|
105
|
+
const marker = "flows/";
|
|
106
|
+
const idx = posix.lastIndexOf(marker);
|
|
107
|
+
const after = idx >= 0 ? posix.slice(idx + marker.length) : posix;
|
|
108
|
+
return (
|
|
109
|
+
after === ADOPT_BARREL_FILE ||
|
|
110
|
+
after === `${ADOPT_BARREL_FILE}.tmp` ||
|
|
111
|
+
after === PREV_ADOPT_BARREL_FILE ||
|
|
112
|
+
after === `${PREV_ADOPT_BARREL_FILE}.tmp`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write the adopt barrel atomically: `index.ts.tmp` → rename to
|
|
118
|
+
* `index.ts`. Skips the write when bytes are unchanged so the `src/`
|
|
119
|
+
* watcher + bun `--hot` do not loop. Unlinks leftover `generated.ts`.
|
|
92
120
|
*
|
|
93
121
|
* @param rootDir - Project root
|
|
94
122
|
* @param source - Full barrel source from {@link generateAdoptBarrel}
|
|
@@ -103,25 +131,44 @@ export async function writeAdoptBarrel(
|
|
|
103
131
|
const target = join(dir, ADOPT_BARREL_FILE);
|
|
104
132
|
const tmp = `${target}.tmp`;
|
|
105
133
|
await mkdir(dir, { recursive: true });
|
|
134
|
+
let written = true;
|
|
106
135
|
try {
|
|
107
136
|
const existing = await readFile(target, "utf8");
|
|
108
|
-
if (existing === source)
|
|
137
|
+
if (existing === source) written = false;
|
|
109
138
|
} catch {
|
|
110
139
|
// missing — write
|
|
111
140
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await rename(tmp, target);
|
|
115
|
-
} catch {
|
|
116
|
-
// Windows cannot rename onto an existing path (EEXIST / EPERM).
|
|
117
|
-
await writeFile(target, source);
|
|
141
|
+
if (written) {
|
|
142
|
+
await writeFile(tmp, source);
|
|
118
143
|
try {
|
|
119
|
-
await
|
|
144
|
+
await rename(tmp, target);
|
|
120
145
|
} catch {
|
|
121
|
-
//
|
|
146
|
+
// Windows cannot rename onto an existing path (EEXIST / EPERM).
|
|
147
|
+
await writeFile(target, source);
|
|
148
|
+
try {
|
|
149
|
+
await unlink(tmp);
|
|
150
|
+
} catch {
|
|
151
|
+
// tmp already replaced or removed
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
await unlinkPrevAdoptBarrel(dir);
|
|
156
|
+
return { written };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove leftover `generated.ts` after the barrel moved to `index.ts`.
|
|
161
|
+
*
|
|
162
|
+
* @param dir - Absolute flows directory
|
|
163
|
+
*/
|
|
164
|
+
async function unlinkPrevAdoptBarrel(dir: string): Promise<void> {
|
|
165
|
+
for (const name of [PREV_ADOPT_BARREL_FILE, `${PREV_ADOPT_BARREL_FILE}.tmp`]) {
|
|
166
|
+
try {
|
|
167
|
+
await unlink(join(dir, name));
|
|
168
|
+
} catch {
|
|
169
|
+
// absent
|
|
122
170
|
}
|
|
123
171
|
}
|
|
124
|
-
return { written: true };
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
/**
|
|
@@ -225,6 +272,7 @@ export async function generateAdoptBarrel(
|
|
|
225
272
|
|
|
226
273
|
/**
|
|
227
274
|
* Walk a unit folder for route files (skip-list applied).
|
|
275
|
+
* `signals.ts` is included only for `export const … = on(` consumers.
|
|
228
276
|
*
|
|
229
277
|
* @param unitAbs - Absolute unit directory
|
|
230
278
|
*/
|
|
@@ -246,10 +294,11 @@ async function listFlowFiles(unitAbs: string): Promise<WalkedFlowFile[]> {
|
|
|
246
294
|
continue;
|
|
247
295
|
}
|
|
248
296
|
if (!posixName.endsWith(".ts") && !posixName.endsWith(".tsx")) continue;
|
|
249
|
-
if (isSkipFlowFile(posixName)) continue;
|
|
297
|
+
if (isSkipFlowFile(posixName) && posixName !== SIGNALS_FILE) continue;
|
|
250
298
|
const relFile = rel ? `${rel}/${posixName}` : posixName;
|
|
251
299
|
const source = await readFile(join(dir, d.name), "utf8");
|
|
252
|
-
const exports =
|
|
300
|
+
const exports =
|
|
301
|
+
posixName === SIGNALS_FILE ? listExportedOnConsts(source) : listExportedConsts(source);
|
|
253
302
|
const relPosix = toPosixPath(relFile);
|
|
254
303
|
// Barrel `index.ts` counts even with no `export const` (`export {}` / re-exports).
|
|
255
304
|
if (exports.length === 0 && relPosix !== "index.ts") continue;
|
|
@@ -282,6 +331,31 @@ export function listExportedConsts(source: string): string[] {
|
|
|
282
331
|
return names;
|
|
283
332
|
}
|
|
284
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Collect `export const name = on(` bindings (skip `signal.once` decls).
|
|
336
|
+
*
|
|
337
|
+
* @param source - TypeScript source
|
|
338
|
+
*/
|
|
339
|
+
export function listExportedOnConsts(source: string): string[] {
|
|
340
|
+
const names: string[] = [];
|
|
341
|
+
EXPORT_ON.lastIndex = 0;
|
|
342
|
+
let match: RegExpExecArray | null;
|
|
343
|
+
while ((match = EXPORT_ON.exec(source))) {
|
|
344
|
+
const name = match[1];
|
|
345
|
+
if (name) names.push(name);
|
|
346
|
+
}
|
|
347
|
+
return names;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* camelCase export → kebab filename (`onCreated` → `on-created`).
|
|
352
|
+
*
|
|
353
|
+
* @param name - `export const` binding
|
|
354
|
+
*/
|
|
355
|
+
function camelToKebab(name: string): string {
|
|
356
|
+
return name.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`).replace(/^-/, "");
|
|
357
|
+
}
|
|
358
|
+
|
|
285
359
|
/**
|
|
286
360
|
* Emit import + stamped namespace for a tree unit.
|
|
287
361
|
*
|
|
@@ -309,10 +383,6 @@ function emitTreeUnit(
|
|
|
309
383
|
const alias = importAlias(unit, file.relFromUnit);
|
|
310
384
|
importLines.push(`import * as ${alias} from ${JSON.stringify(spec)};`);
|
|
311
385
|
|
|
312
|
-
const inferred =
|
|
313
|
-
pathFromFlowFile(`${unit}/${file.relFromUnit}`) ??
|
|
314
|
-
pathFromFlowFile(`src/flows/${unit}/${file.relFromUnit}`);
|
|
315
|
-
|
|
316
386
|
for (const exp of file.exports) {
|
|
317
387
|
const prev = used.get(exp);
|
|
318
388
|
if (prev !== undefined) {
|
|
@@ -322,7 +392,7 @@ function emitTreeUnit(
|
|
|
322
392
|
}
|
|
323
393
|
used.set(exp, file.relFromUnit);
|
|
324
394
|
const flowName = nameFromFlowFile(`${unit}/${file.relFromUnit}`, exp) ?? `${unit}.${exp}`;
|
|
325
|
-
const pathLit = JSON.stringify(
|
|
395
|
+
const pathLit = JSON.stringify(httpPathForExport(unit, file.relFromUnit, exp));
|
|
326
396
|
fieldLines.push(
|
|
327
397
|
` ${exp}: stampHttpPath(stampFlowName(${alias}.${exp}, ${JSON.stringify(flowName)}), ${pathLit}),`,
|
|
328
398
|
);
|
|
@@ -340,6 +410,26 @@ function emitTreeUnit(
|
|
|
340
410
|
return { lines, catchAll };
|
|
341
411
|
}
|
|
342
412
|
|
|
413
|
+
/**
|
|
414
|
+
* HTTP path stamp for one export.
|
|
415
|
+
*
|
|
416
|
+
* Skip-list files (`signals.ts`) have no file-tree path — fall back to
|
|
417
|
+
* `unit` + kebab export so `onCreated` in `links/signals.ts` stamps
|
|
418
|
+
* `/links/on-created` like a dedicated `on-created.ts` would.
|
|
419
|
+
*
|
|
420
|
+
* @param unit - Folder name
|
|
421
|
+
* @param relFromUnit - POSIX relative path
|
|
422
|
+
* @param exp - `export const` binding
|
|
423
|
+
*/
|
|
424
|
+
function httpPathForExport(unit: string, relFromUnit: string, exp: string): string {
|
|
425
|
+
return (
|
|
426
|
+
pathFromFlowFile(`${unit}/${relFromUnit}`) ??
|
|
427
|
+
pathFromFlowFile(`src/flows/${unit}/${relFromUnit}`) ??
|
|
428
|
+
pathFromFlowFile(`${unit}/${camelToKebab(exp)}.ts`) ??
|
|
429
|
+
"/"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
343
433
|
/**
|
|
344
434
|
* Fail when `flow("other.x")` lives under a different unit folder.
|
|
345
435
|
*
|
package/src/compiler/response.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* Shared by AoT and dynamic so responses stay byte-identical.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { fail, type FlowFailure } from "../kernel/errors.ts";
|
|
8
8
|
import type { JsonResult, JsonStreamResult, SseFrame } from "../kernel/fx.ts";
|
|
9
9
|
import { isFlowFailure } from "../kernel/hooks.ts";
|
|
10
10
|
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
11
|
-
import { VALIDATION_ERROR_CODE } from "../validation/standard-schema.ts";
|
|
12
11
|
|
|
13
12
|
/** Successful envelope. */
|
|
14
13
|
export interface SuccessEnvelope {
|
|
@@ -26,24 +25,12 @@ export interface FailureEnvelope {
|
|
|
26
25
|
/**
|
|
27
26
|
* HTTP status for a flow-boundary failure.
|
|
28
27
|
*
|
|
29
|
-
*
|
|
30
|
-
* `Unauthorized` → 401 · `Forbidden` → 403 · `RateLimited` → 429.
|
|
28
|
+
* Built-in codes use {@link httpStatusForFailure}. Domain codes default to 400.
|
|
31
29
|
*
|
|
32
30
|
* @param failure - Typed failure
|
|
33
31
|
*/
|
|
34
32
|
export function statusForFailure(failure: FlowFailure): number {
|
|
35
|
-
|
|
36
|
-
case VALIDATION_ERROR_CODE:
|
|
37
|
-
return 422;
|
|
38
|
-
case "Unauthorized":
|
|
39
|
-
return 401;
|
|
40
|
-
case "Forbidden":
|
|
41
|
-
return 403;
|
|
42
|
-
case "RateLimited":
|
|
43
|
-
return 429;
|
|
44
|
-
default:
|
|
45
|
-
return 400;
|
|
46
|
-
}
|
|
33
|
+
return loadBuiltinErrors().httpStatusForFailure(failure.error.code, failure.error.data);
|
|
47
34
|
}
|
|
48
35
|
|
|
49
36
|
/**
|
|
@@ -87,6 +74,14 @@ function loadFxLiveStream(): typeof import("../kernel/fx-live-stream.ts") {
|
|
|
87
74
|
return lazyRequire(`${import.meta.dir}/../kernel`, ["fx", "live", "stream"].join("-"));
|
|
88
75
|
}
|
|
89
76
|
|
|
77
|
+
function loadBuiltinErrors(): typeof import("../kernel/builtin-errors.ts") {
|
|
78
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["builtin", "errors"].join("-"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadStoreErrors(): typeof import("../elements/store/store-errors.ts") {
|
|
82
|
+
return lazyRequire(`${import.meta.dir}/../elements/store`, ["store", "errors"].join("-"));
|
|
83
|
+
}
|
|
84
|
+
|
|
90
85
|
function loadFxJson(): {
|
|
91
86
|
isJsonResult: (value: unknown) => value is JsonResult;
|
|
92
87
|
isJsonStreamResult: (value: unknown) => value is JsonStreamResult;
|
|
@@ -123,18 +118,13 @@ export async function encodeExecuteResult(result: {
|
|
|
123
118
|
? loadFxLiveStream().encodeGap(result.error)
|
|
124
119
|
: undefined;
|
|
125
120
|
if (gap) return gap;
|
|
121
|
+
const storeMapped = loadStoreErrors().storeErrorToFailure(result.error, {
|
|
122
|
+
retryable: "unavailable",
|
|
123
|
+
});
|
|
124
|
+
if (storeMapped) return encodeFailure(storeMapped);
|
|
126
125
|
// Unhandled throws must never look like success (`undefined` → 204).
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
data: null,
|
|
130
|
-
error: {
|
|
131
|
-
code: "InternalError",
|
|
132
|
-
data: {},
|
|
133
|
-
message: result.error instanceof Error ? result.error.message : "internal error",
|
|
134
|
-
},
|
|
135
|
-
} satisfies FailureEnvelope,
|
|
136
|
-
{ status: 500 },
|
|
137
|
-
);
|
|
126
|
+
// Catalog message only — never copy the thrown `Error.message`.
|
|
127
|
+
return encodeFailure(fail("InternalError", {}));
|
|
138
128
|
}
|
|
139
129
|
if (loadFxJson().isJsonStreamResult(result.output) && result.output.ready) {
|
|
140
130
|
const early = await loadFxLiveStream().awaitLiveReady(result.output);
|
package/src/config/index.ts
CHANGED
|
@@ -214,14 +214,16 @@ export interface DbConfig {
|
|
|
214
214
|
/**
|
|
215
215
|
* Abstract schema declare module (`store.schema.table` exports).
|
|
216
216
|
* When present, `oke db` emits Drizzle into {@link generated} before sync.
|
|
217
|
-
* Default `
|
|
217
|
+
* Default: `src/db/schema.ts` if that file exists, else
|
|
218
|
+
* `src/db/schema.decl.ts`, else `src/db/schema/index.ts`, else legacy
|
|
219
|
+
* `src/schema.decl.ts`. Set this to override discovery.
|
|
218
220
|
*/
|
|
219
221
|
readonly declare?: string;
|
|
220
222
|
/**
|
|
221
223
|
* Generated Drizzle schema path written from abstract decls.
|
|
222
|
-
* Default `"src/db/
|
|
223
|
-
*
|
|
224
|
-
*
|
|
224
|
+
* Default `"src/db/drizzle/index.ts"`. Older single files still resolve
|
|
225
|
+
* when `generated` is set. Point `drizzle.config.ts` `schema` at this
|
|
226
|
+
* barrel when using the abstract path.
|
|
225
227
|
*/
|
|
226
228
|
readonly generated?: string;
|
|
227
229
|
/**
|
|
@@ -34,17 +34,23 @@ const MANIFEST = {
|
|
|
34
34
|
} as Manifest;
|
|
35
35
|
|
|
36
36
|
function execute(over: Partial<ExecuteResult>): ExecuteResult {
|
|
37
|
-
|
|
37
|
+
const result = {
|
|
38
38
|
output: undefined,
|
|
39
39
|
failure: undefined,
|
|
40
40
|
response: undefined,
|
|
41
41
|
ctx: {} as ExecuteResult["ctx"],
|
|
42
42
|
fx: {} as ExecuteResult["fx"],
|
|
43
|
-
cache: "none",
|
|
43
|
+
cache: "none" as const,
|
|
44
44
|
durationMs: 0,
|
|
45
45
|
runId: "run_test",
|
|
46
46
|
...over,
|
|
47
47
|
};
|
|
48
|
+
return {
|
|
49
|
+
...result,
|
|
50
|
+
get error() {
|
|
51
|
+
return result.ctx.error;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
describe("invokeResultFromExecute", () => {
|