okengine 0.13.0 → 0.15.2
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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +10 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke({...})` auto-populates `stores` / `secrets` / `signals` /
|
|
3
|
-
* `channel.templates` from the same kind of
|
|
4
|
-
* already proven for `on()`'s trigger drain
|
|
2
|
+
* `oke({...})` auto-populates `stores` / `secrets` / `signals` / `clocks` /
|
|
3
|
+
* `gate.policies` / `channel.templates` from the same kind of
|
|
4
|
+
* module-evaluation registry already proven for `on()`'s trigger drain
|
|
5
|
+
* (`registry-isolation.test.ts`).
|
|
5
6
|
*
|
|
6
|
-
* `store
|
|
7
|
-
* `.template()` push into a shared registry
|
|
8
|
-
* at call time — zero change to how
|
|
9
|
-
*
|
|
10
|
-
* switch that already governs
|
|
7
|
+
* `store.*`, `vault.secret`, `signal()`, `clock()`, `gate.policy` / `.scope`
|
|
8
|
+
* / `.rate`, and medium-binder `.template()` push into a shared registry
|
|
9
|
+
* (`src/kernel/element-registries.ts`) at call time — zero change to how
|
|
10
|
+
* developers call them. `oke()` drains it at construction under the same
|
|
11
|
+
* `registry: "consume" | "keep" | "ignore"` switch that already governs
|
|
12
|
+
* bindings.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { describe, expect, test } from "bun:test";
|
|
14
16
|
import { channel } from "../elements/channel/declare.ts";
|
|
17
|
+
import { clock } from "../elements/clock/declare.ts";
|
|
18
|
+
import { gate } from "../elements/gate/declare.ts";
|
|
15
19
|
import { signal } from "../elements/signal/declare.ts";
|
|
16
20
|
import { store } from "../elements/store/declare.ts";
|
|
17
21
|
import { vault } from "../elements/vault/declare.ts";
|
|
@@ -20,7 +24,7 @@ import { flow } from "./flow.ts";
|
|
|
20
24
|
import { on, resetBindings } from "./on.ts";
|
|
21
25
|
import { http } from "./triggers.ts";
|
|
22
26
|
|
|
23
|
-
describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai", () => {
|
|
27
|
+
describe("oke() auto-registry — stores/secrets/signals/clocks/gates/channel.templates/ai", () => {
|
|
24
28
|
test("before/after: zero explicit arrays boots identically to the explicit-array form", async () => {
|
|
25
29
|
resetBindings();
|
|
26
30
|
|
|
@@ -161,6 +165,42 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
161
165
|
expect(app.bootResult?.vault?.contracts.has("IGNORED_SECRET")).toBeFalsy();
|
|
162
166
|
});
|
|
163
167
|
|
|
168
|
+
test("store.kv / store.index / clock() / gate.policy auto-drain — no explicit oke() arrays", async () => {
|
|
169
|
+
resetBindings();
|
|
170
|
+
store.kv("drafts", { description: "Compose drafts" });
|
|
171
|
+
store.index("tasks", { description: "Task search" });
|
|
172
|
+
clock("daily-digest", { cron: "0 8 * * *", every: "1d", timezone: "UTC" });
|
|
173
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
174
|
+
gate.scope("task:write");
|
|
175
|
+
gate.rate({ max: 60, per: "1m", keyBy: "user" });
|
|
176
|
+
|
|
177
|
+
const ping = flow("auto.ping", {
|
|
178
|
+
do: () => ({ ok: true as const }),
|
|
179
|
+
});
|
|
180
|
+
on(http.get("/auto-ping").gate(member), ping);
|
|
181
|
+
|
|
182
|
+
const app = oke({
|
|
183
|
+
name: "auto-kv-clock-gate",
|
|
184
|
+
autoBoot: false,
|
|
185
|
+
startScheduler: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(app.$options.stores?.some((s) => s.ref === "kv:drafts")).toBe(true);
|
|
189
|
+
expect(app.$options.stores?.some((s) => s.ref === "index:tasks")).toBe(true);
|
|
190
|
+
expect(app.$options.clocks?.some((c) => c.name === "daily-digest")).toBe(true);
|
|
191
|
+
expect(app.$options.gate?.policies?.some((g) => "name" in g && g.name === "member")).toBe(true);
|
|
192
|
+
expect(app.$options.gate?.policies?.some((g) => "name" in g && g.name === "task:write")).toBe(
|
|
193
|
+
true,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await app.boot({ env: "test" });
|
|
197
|
+
expect(app.bootResult?.store?.declarations.has("kv:drafts")).toBe(true);
|
|
198
|
+
expect(app.bootResult?.store?.declarations.has("index:tasks")).toBe(true);
|
|
199
|
+
expect(app.bootResult?.clock?.declarations.has("daily-digest")).toBe(true);
|
|
200
|
+
expect(app.bootResult?.gate?.gates.has("member")).toBe(true);
|
|
201
|
+
expect(app.bootResult?.gate?.gates.has("task:write")).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
164
204
|
test('registry: "consume" (default) drains stores/secrets/signals/channel.templates — a later app does not inherit them', async () => {
|
|
165
205
|
store.sql("leak-app", { schema: {} });
|
|
166
206
|
vault.secret("LEAK_SECRET", { dev: "dev-leak" });
|
|
@@ -205,6 +245,10 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
205
245
|
const summarizeNote = smart.prompt("summarize-note", {
|
|
206
246
|
version: 1,
|
|
207
247
|
});
|
|
248
|
+
ai.mcpServer("github", {
|
|
249
|
+
url: "https://mcp.example/github",
|
|
250
|
+
tools: ["create_issue"],
|
|
251
|
+
});
|
|
208
252
|
|
|
209
253
|
const ping = flow("ai.ping", {
|
|
210
254
|
effects: { asks: ["summarize-note"] },
|
|
@@ -221,6 +265,7 @@ describe("oke() auto-registry — stores/secrets/signals/channel.templates/ai",
|
|
|
221
265
|
|
|
222
266
|
expect(app.$options.ai?.models?.some((m) => m.name === "smart")).toBe(true);
|
|
223
267
|
expect(app.$options.ai?.prompts?.some((p) => p.name === "summarize-note")).toBe(true);
|
|
268
|
+
expect(app.$options.ai?.mcpServers?.some((s) => s.name === "github")).toBe(true);
|
|
224
269
|
|
|
225
270
|
const runtime = createAiRuntime({
|
|
226
271
|
models: [smart],
|
|
@@ -18,6 +18,7 @@ import { openaiCompatibleAiDriver } from "../../drivers/ai-openai-compatible.ts"
|
|
|
18
18
|
import type { AiDriver, AiOpenOptions } from "../../drivers/ai-types.ts";
|
|
19
19
|
import { createAiRuntime, type AiRuntime } from "../../elements/ai.ts";
|
|
20
20
|
import type { GateRuntime } from "../../elements/gate.ts";
|
|
21
|
+
import type { VaultRuntime } from "../../elements/vault.ts";
|
|
21
22
|
import type { BootOptions } from "../boot.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -28,6 +29,7 @@ import type { BootOptions } from "../boot.ts";
|
|
|
28
29
|
* @param now - Clock
|
|
29
30
|
* @param env - Active environment
|
|
30
31
|
* @param docker - Prefer compose AI URL when active
|
|
32
|
+
* @param vault - Optional vault for MCP bearer secrets
|
|
31
33
|
*/
|
|
32
34
|
export function bindAi(
|
|
33
35
|
options: BootOptions,
|
|
@@ -35,13 +37,21 @@ export function bindAi(
|
|
|
35
37
|
now: () => number,
|
|
36
38
|
env: ConfigEnv = "test",
|
|
37
39
|
docker = false,
|
|
40
|
+
vault?: VaultRuntime,
|
|
38
41
|
): AiRuntime {
|
|
39
42
|
const id = resolveAiDriverId(options, env, docker);
|
|
40
43
|
const driver =
|
|
41
44
|
options.ai?.defaultDriver ?? withOpenDefaults(aiDriverFor(id), openDefaultsFor(id, docker));
|
|
42
45
|
return createAiRuntime({
|
|
43
46
|
...(options.ai ?? {}),
|
|
47
|
+
...(vault ? { resolveSecret: (name: string) => vault.read(name) } : undefined),
|
|
44
48
|
defaultDriver: driver,
|
|
49
|
+
drivers: {
|
|
50
|
+
mock: lazyDriver("mock", docker),
|
|
51
|
+
anthropic: lazyDriver("anthropic", docker),
|
|
52
|
+
"openai-compatible": lazyDriver("openai-compatible", docker),
|
|
53
|
+
ollama: lazyDriver("ollama", docker),
|
|
54
|
+
},
|
|
45
55
|
gates: options.ai?.gates ?? gate,
|
|
46
56
|
now,
|
|
47
57
|
});
|
|
@@ -156,6 +166,20 @@ export function aiUrlFor(docker = false): string {
|
|
|
156
166
|
* @param driver - Base driver
|
|
157
167
|
* @param defaults - Env-derived open options
|
|
158
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Open a protocol driver with env defaults only when a model actually uses it.
|
|
171
|
+
*
|
|
172
|
+
* @param id - Driver id
|
|
173
|
+
* @param docker - Docker mode
|
|
174
|
+
*/
|
|
175
|
+
function lazyDriver(id: string, docker: boolean): AiDriver {
|
|
176
|
+
const base = aiDriverFor(id);
|
|
177
|
+
return {
|
|
178
|
+
id: base.id,
|
|
179
|
+
open: (opts = {}) => base.open({ ...openDefaultsFor(id, docker), ...opts }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
function withOpenDefaults(driver: AiDriver, defaults: AiOpenOptions): AiDriver {
|
|
160
184
|
if (Object.keys(defaults).length === 0) return driver;
|
|
161
185
|
return {
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -116,9 +116,9 @@ describe("boot — lazy element needs", () => {
|
|
|
116
116
|
if (raw.byteLength === 0) continue;
|
|
117
117
|
total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
|
|
118
118
|
}
|
|
119
|
-
// Rebased after
|
|
120
|
-
// (~
|
|
121
|
-
expect(total).toBeLessThan(
|
|
119
|
+
// Rebased after clock / gate / kv / index auto-drain on `oke()`.
|
|
120
|
+
// (~48.3 kB gzip with export externals).
|
|
121
|
+
expect(total).toBeLessThan(49_000);
|
|
122
122
|
} finally {
|
|
123
123
|
await rm(dir, { recursive: true, force: true });
|
|
124
124
|
}
|
|
@@ -227,7 +227,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
|
|
|
227
227
|
resetBindings();
|
|
228
228
|
resetFlowSeq();
|
|
229
229
|
on(
|
|
230
|
-
http.get("/ping").
|
|
230
|
+
http.get("/ping").public(),
|
|
231
231
|
flow("ping-public", {
|
|
232
232
|
do: () => ({ ok: true }),
|
|
233
233
|
}),
|
package/src/kernel/boot.ts
CHANGED
|
@@ -101,10 +101,10 @@ export interface BootOptions {
|
|
|
101
101
|
readonly gates?: readonly GateDecl[];
|
|
102
102
|
/**
|
|
103
103
|
* HTTP auth-posture enforcement at boot.
|
|
104
|
-
* - `"deny"` (default) — every HTTP trigger must carry a gate or `.
|
|
104
|
+
* - `"deny"` (default) — every HTTP trigger must carry a gate or `.public()`
|
|
105
105
|
* - `"allow"` — skips the audit **only** when {@link env} is `"test"`;
|
|
106
106
|
* ignored in local/prod/docker (never a production-wide bypass).
|
|
107
|
-
* Migrate real apps with per-trigger `.
|
|
107
|
+
* Migrate real apps with per-trigger `.public()`.
|
|
108
108
|
*/
|
|
109
109
|
readonly unguardedHttp?: "deny" | "allow";
|
|
110
110
|
/** Signal declarations. */
|
|
@@ -550,7 +550,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
550
550
|
// 6. AI
|
|
551
551
|
let ai = pre.ai;
|
|
552
552
|
if (needs.ai && !ai) {
|
|
553
|
-
ai = aiBind!.bindAi(options, gate, now, env, docker);
|
|
553
|
+
ai = aiBind!.bindAi(options, gate, now, env, docker, vault);
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// 7. Runs
|
|
@@ -607,12 +607,14 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
607
607
|
clearInterval(schedulerTimer);
|
|
608
608
|
schedulerTimer = undefined;
|
|
609
609
|
}
|
|
610
|
+
clock?.stopWakes();
|
|
610
611
|
},
|
|
611
612
|
async close() {
|
|
612
613
|
if (schedulerTimer !== undefined) {
|
|
613
614
|
clearInterval(schedulerTimer);
|
|
614
615
|
schedulerTimer = undefined;
|
|
615
616
|
}
|
|
617
|
+
clock?.stopWakes();
|
|
616
618
|
await signal?.close();
|
|
617
619
|
await vault?.close();
|
|
618
620
|
await runs?.flush();
|
|
@@ -47,7 +47,7 @@ const CreateOut = z.object({ ok: z.boolean() });
|
|
|
47
47
|
|
|
48
48
|
function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
|
|
49
49
|
return on(
|
|
50
|
-
http.post("/notes").
|
|
50
|
+
http.post("/notes").public(),
|
|
51
51
|
flow("notes.create", {
|
|
52
52
|
in: CreateIn,
|
|
53
53
|
out: CreateOut,
|
|
@@ -140,7 +140,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
140
140
|
resetFlowSeq();
|
|
141
141
|
const db = store.sql("app", { schema: { notes } });
|
|
142
142
|
const create = on(
|
|
143
|
-
http.post("/notes").
|
|
143
|
+
http.post("/notes").public(),
|
|
144
144
|
flow("notes.create", {
|
|
145
145
|
in: CreateIn,
|
|
146
146
|
out: CreateOut,
|
|
@@ -193,7 +193,7 @@ import { on, flow, http, gate } from "okengine";
|
|
|
193
193
|
const db = {} as any;
|
|
194
194
|
const notes = {} as any;
|
|
195
195
|
export const create = on(
|
|
196
|
-
http.post("/notes").
|
|
196
|
+
http.post("/notes").public(),
|
|
197
197
|
flow("notes.create", {
|
|
198
198
|
do: async (input, fx) => {
|
|
199
199
|
await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-evaluation registries for `store
|
|
3
|
-
* `vault.env.required`, `signal()`, `
|
|
4
|
-
* `ai.model` / `.prompt` /
|
|
5
|
-
*
|
|
2
|
+
* Module-evaluation registries for `store.*`, `vault.secret`,
|
|
3
|
+
* `vault.env.required`, `signal()`, `clock()`, `gate.policy` / `.scope` /
|
|
4
|
+
* `.rate`, `channel.<medium>().template()`, and `ai.model` / `.prompt` /
|
|
5
|
+
* `ai.embed` / `ai.agent` / `ai.mcpServer` — the plain arrays behind each
|
|
6
|
+
* declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
|
|
6
7
|
* trigger-drain `bindings` array).
|
|
7
8
|
*
|
|
8
9
|
* Lives here, not in the declare modules themselves, so {@link oke} can read
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
import type {
|
|
17
18
|
AiAgentDecl,
|
|
18
19
|
AiEmbedDecl,
|
|
20
|
+
AiMcpServerDecl,
|
|
19
21
|
AiModelDecl,
|
|
20
22
|
AiPromptDecl,
|
|
21
23
|
} from "../elements/ai/declare.ts";
|
|
@@ -23,8 +25,10 @@ import type { StoreDecl } from "../elements/store/declare.ts";
|
|
|
23
25
|
import type { VaultSecretDecl } from "../elements/vault/declare.ts";
|
|
24
26
|
import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
25
27
|
import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
|
|
28
|
+
import type { ClockDecl } from "../elements/clock/declare.ts";
|
|
29
|
+
import type { GateDecl } from "../elements/gate/declare.ts";
|
|
26
30
|
|
|
27
|
-
/** `store.sql` / `store.files` declarations since the last reset. */
|
|
31
|
+
/** `store.sql` / `store.kv` / `store.files` / `store.index` declarations since the last reset. */
|
|
28
32
|
export const storeRegistry: StoreDecl[] = [];
|
|
29
33
|
/** `vault.secret` declarations since the last reset. */
|
|
30
34
|
export const secretRegistry: VaultSecretDecl[] = [];
|
|
@@ -32,6 +36,10 @@ export const secretRegistry: VaultSecretDecl[] = [];
|
|
|
32
36
|
export const requiredEnvRegistry: string[] = [];
|
|
33
37
|
/** `signal()` declarations since the last reset. */
|
|
34
38
|
export const signalRegistry: SignalDecl[] = [];
|
|
39
|
+
/** `clock()` declarations since the last reset. */
|
|
40
|
+
export const clockRegistry: ClockDecl[] = [];
|
|
41
|
+
/** `gate.policy` / `gate.scope` / `gate.rate` declarations since the last reset. */
|
|
42
|
+
export const gateRegistry: GateDecl[] = [];
|
|
35
43
|
/** Medium-binder `.template()` declarations since the last reset. */
|
|
36
44
|
export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
|
|
37
45
|
/** `ai.model` declarations since the last reset. */
|
|
@@ -42,3 +50,5 @@ export const aiPromptRegistry: AiPromptDecl[] = [];
|
|
|
42
50
|
export const aiEmbedRegistry: AiEmbedDecl[] = [];
|
|
43
51
|
/** `ai.agent` declarations since the last reset. */
|
|
44
52
|
export const aiAgentRegistry: AiAgentDecl[] = [];
|
|
53
|
+
/** `ai.mcpServer` declarations since the last reset. */
|
|
54
|
+
export const aiMcpServerRegistry: AiMcpServerDecl[] = [];
|
package/src/kernel/errors.ts
CHANGED
|
@@ -183,6 +183,30 @@ export const OKE_ERRORS = {
|
|
|
183
183
|
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
184
184
|
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
|
|
185
185
|
},
|
|
186
|
+
/**
|
|
187
|
+
* `http.get()` was never stamped from the file tree — refuse a silent `/`.
|
|
188
|
+
*/
|
|
189
|
+
HTTP_PATH_UNRESOLVED: {
|
|
190
|
+
code: 1010,
|
|
191
|
+
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
192
|
+
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows/generated`, or pass an explicit path to `http.{method}("/…")`.',
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
|
|
196
|
+
*/
|
|
197
|
+
HTTP_ROUTE_DUPLICATE: {
|
|
198
|
+
code: 1011,
|
|
199
|
+
cause: '{method} {path} is bound twice (flow "{flow}").',
|
|
200
|
+
fix: "Give each HTTP flow a unique method + path.",
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
|
|
204
|
+
*/
|
|
205
|
+
HTTP_FLOW_UNNAMED: {
|
|
206
|
+
code: 1012,
|
|
207
|
+
cause: "An HTTP flow on {method} {path} has no name.",
|
|
208
|
+
fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
|
|
209
|
+
},
|
|
186
210
|
/**
|
|
187
211
|
* Emit target has no subscriber (unified-theory §21).
|
|
188
212
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-evaluation registry for `src/flows/generated.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Same consume/ignore posture as `on()` / `listBindings()`: generated.ts
|
|
5
|
+
* calls {@link registerFlowUnits}; {@link oke} drains the bag into `$routes`
|
|
6
|
+
* unless `registry: "ignore"`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AnyFlowDef } from "./flow.ts";
|
|
10
|
+
import { isFlow } from "./flow.ts";
|
|
11
|
+
|
|
12
|
+
/** Unit name → export name → Flow. */
|
|
13
|
+
export type FlowUnitBag = {
|
|
14
|
+
readonly [exportName: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const pending: Record<string, FlowUnitBag> = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record generated flow units for the next {@link oke} construction.
|
|
21
|
+
*
|
|
22
|
+
* @param units - `{ notes, main, … }` from `generated.ts`
|
|
23
|
+
*/
|
|
24
|
+
export function registerFlowUnits(units: Record<string, FlowUnitBag>): void {
|
|
25
|
+
Object.assign(pending, units);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drain registered units (consume). Clears the registry.
|
|
30
|
+
*/
|
|
31
|
+
export function consumeRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
32
|
+
const snap: Record<string, FlowUnitBag> = { ...pending };
|
|
33
|
+
resetRegisteredFlowUnits();
|
|
34
|
+
return snap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot without clearing (keep / tests).
|
|
39
|
+
*/
|
|
40
|
+
export function peekRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
41
|
+
return { ...pending };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear the unit registry (tests / `registry: "consume"` after drain).
|
|
46
|
+
*/
|
|
47
|
+
export function resetRegisteredFlowUnits(): void {
|
|
48
|
+
for (const key of Object.keys(pending)) {
|
|
49
|
+
delete pending[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collect FlowDefs from a unit bag (skip non-flow exports).
|
|
55
|
+
*
|
|
56
|
+
* @param bag - Namespace object
|
|
57
|
+
*/
|
|
58
|
+
export function flowsInUnitBag(bag: FlowUnitBag): AnyFlowDef[] {
|
|
59
|
+
const found: AnyFlowDef[] = [];
|
|
60
|
+
for (const value of Object.values(bag)) {
|
|
61
|
+
if (isFlow(value)) found.push(value);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
package/src/kernel/flow.ts
CHANGED
|
@@ -239,13 +239,34 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
|
|
|
239
239
|
* Input / output types are inferred from Standard Schema `in` / `out` when
|
|
240
240
|
* present; otherwise from the `do` handler signature.
|
|
241
241
|
*
|
|
242
|
+
* A nameless `flow({ do })` is stamped `unit.export` by the file-tree
|
|
243
|
+
* generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
|
|
244
|
+
* still wins.
|
|
245
|
+
*
|
|
246
|
+
* @param options - Contract and handler (name stamped later)
|
|
247
|
+
*/
|
|
248
|
+
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
249
|
+
options: Opts,
|
|
250
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
251
|
+
/**
|
|
242
252
|
* @param name - Stable name (used by `fx.call` and the Manifest)
|
|
243
253
|
* @param options - Contract and handler
|
|
244
254
|
*/
|
|
245
255
|
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
246
256
|
name: string,
|
|
247
257
|
options: Opts & { readonly name?: never },
|
|
248
|
-
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts
|
|
258
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
259
|
+
export function flow(
|
|
260
|
+
nameOrOptions: string | FlowOptions<any, any, any>,
|
|
261
|
+
maybeOptions?: FlowOptions<any, any, any> & { readonly name?: never },
|
|
262
|
+
): FlowDef {
|
|
263
|
+
const named = typeof nameOrOptions === "string";
|
|
264
|
+
const name = named ? nameOrOptions : "";
|
|
265
|
+
const options = (named ? maybeOptions : nameOrOptions) as FlowOptions<any, any, any> | undefined;
|
|
266
|
+
if (!options || typeof options.do !== "function") {
|
|
267
|
+
throw new TypeError("flow() expected an options bag with a do handler");
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
const triggers: Trigger[] = [];
|
|
250
271
|
const hooks: Partial<Record<HookStage, HookFn[]>> = {};
|
|
251
272
|
const pendingPlugins: PluginDef[] = [];
|
|
@@ -253,13 +274,13 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
253
274
|
const dot = name.indexOf(".");
|
|
254
275
|
const unit = dot > 0 ? name.slice(0, dot) : undefined;
|
|
255
276
|
|
|
256
|
-
const def: FlowDef
|
|
277
|
+
const def: FlowDef = {
|
|
257
278
|
[flowBrand]: true,
|
|
258
279
|
name,
|
|
259
280
|
unit,
|
|
260
281
|
in: options.in,
|
|
261
282
|
out: options.out,
|
|
262
|
-
errors: (options.errors ?? undefined) as
|
|
283
|
+
errors: (options.errors ?? undefined) as FlowErrorMap | undefined,
|
|
263
284
|
effects: options.effects,
|
|
264
285
|
durable: options.durable ?? false,
|
|
265
286
|
retry: options.retry,
|
|
@@ -268,17 +289,8 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
268
289
|
slo: options.slo,
|
|
269
290
|
plane: options.plane,
|
|
270
291
|
breaking: options.breaking ?? false,
|
|
271
|
-
compensate: options.compensate as
|
|
272
|
-
|
|
273
|
-
ctx: {
|
|
274
|
-
readonly input: InferFlowIn<Opts>;
|
|
275
|
-
readonly error: unknown;
|
|
276
|
-
readonly completedSteps: readonly string[];
|
|
277
|
-
},
|
|
278
|
-
fx: import("./fx.ts").Fx,
|
|
279
|
-
) => unknown | Promise<unknown>)
|
|
280
|
-
| undefined,
|
|
281
|
-
do: options.do as FlowHandler<InferFlowIn<Opts>, InferFlowOut<Opts>>,
|
|
292
|
+
compensate: options.compensate as FlowDef["compensate"],
|
|
293
|
+
do: options.do as FlowHandler,
|
|
282
294
|
triggers,
|
|
283
295
|
$trigger: undefined,
|
|
284
296
|
hooks,
|
|
@@ -320,9 +332,9 @@ export function isFlow(value: unknown): value is AnyFlowDef {
|
|
|
320
332
|
}
|
|
321
333
|
|
|
322
334
|
/**
|
|
323
|
-
* No-op. `
|
|
324
|
-
*
|
|
325
|
-
*
|
|
335
|
+
* No-op. Nameless flows start as `""` and are stamped by unit / adopt /
|
|
336
|
+
* the file-tree generator — kept so existing test `beforeEach` blocks
|
|
337
|
+
* don't need touching.
|
|
326
338
|
*
|
|
327
339
|
* @internal
|
|
328
340
|
*/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { memorySignalDriver } from "../drivers/index.ts";
|
|
3
|
+
import { createSignalRuntime, signal, type SignalDecl } from "../elements/signal.ts";
|
|
4
|
+
import { OkeError } from "./errors.ts";
|
|
5
|
+
import { createFx, createFxContext, signalReadRef } from "./fx.ts";
|
|
6
|
+
|
|
7
|
+
describe("fx.deadLetters", () => {
|
|
8
|
+
test("returns dead-lettered messages for a declared signal read", async () => {
|
|
9
|
+
const notify = signal("notify", { delivery: "once", retries: 0, deadLetter: true });
|
|
10
|
+
const runtime = openRuntime(notify);
|
|
11
|
+
const bus = await runtime.start();
|
|
12
|
+
await bus.subscribe("notify", "c1", async () => {
|
|
13
|
+
throw new Error("smtp-down");
|
|
14
|
+
});
|
|
15
|
+
await runtime.emit("notify", { orderId: "ord_1" });
|
|
16
|
+
await bus.drain();
|
|
17
|
+
|
|
18
|
+
const { fx, ledger } = createFxContext({
|
|
19
|
+
flow: "notifications.failed",
|
|
20
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
21
|
+
signalRuntime: runtime,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const dead = await fx.deadLetters(notify);
|
|
25
|
+
expect(dead).toHaveLength(1);
|
|
26
|
+
expect(dead[0]!.payload).toEqual({ orderId: "ord_1" });
|
|
27
|
+
expect(dead[0]!.status).toBe("dead");
|
|
28
|
+
expect(dead[0]!.attempts).toBe(1);
|
|
29
|
+
expect(dead[0]!.failures[0]!.message).toBe("smtp-down");
|
|
30
|
+
expect(ledger.entries.map((e) => `${e.kind}:${e.resource}`)).toEqual(["read:signal:notify"]);
|
|
31
|
+
|
|
32
|
+
await runtime.close();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("undeclared and cross-signal reads throw OKE1001", async () => {
|
|
36
|
+
const notify = signal("notify", { delivery: "once", retries: 0, deadLetter: true });
|
|
37
|
+
const other = signal("other", { delivery: "once", retries: 0, deadLetter: true });
|
|
38
|
+
const runtime = openRuntime(notify);
|
|
39
|
+
const fx = createFx({
|
|
40
|
+
flow: "notifications.failed",
|
|
41
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
42
|
+
signalRuntime: runtime,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let err: unknown;
|
|
46
|
+
try {
|
|
47
|
+
await fx.deadLetters(other);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
err = e;
|
|
50
|
+
}
|
|
51
|
+
expect(err).toBeInstanceOf(OkeError);
|
|
52
|
+
const oke = err as OkeError;
|
|
53
|
+
expect(oke.code).toBe(1001);
|
|
54
|
+
expect(oke.causeText).toBe(
|
|
55
|
+
'Flow "notifications.failed" reads "signal:other" without declaring it.',
|
|
56
|
+
);
|
|
57
|
+
expect(oke.fix).toBe('Add "signal:other" to this flow\'s effects.reads.');
|
|
58
|
+
|
|
59
|
+
await runtime.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("throws when no signal runtime is bound", async () => {
|
|
63
|
+
const fx = createFx({
|
|
64
|
+
flow: "notifications.failed",
|
|
65
|
+
effects: { reads: [signalReadRef("notify")] },
|
|
66
|
+
});
|
|
67
|
+
await expect(fx.deadLetters("notify")).rejects.toThrow(
|
|
68
|
+
"fx.deadLetters requires a bound signal runtime",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function openRuntime(decl: SignalDecl) {
|
|
74
|
+
const runtime = createSignalRuntime({ driver: memorySignalDriver });
|
|
75
|
+
runtime.register(decl);
|
|
76
|
+
return runtime;
|
|
77
|
+
}
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -361,11 +361,17 @@ describe("fx — wholesale swap", () => {
|
|
|
361
361
|
async emit(signal) {
|
|
362
362
|
calls.push(`emit:${typeof signal === "string" ? signal : signal.name}`);
|
|
363
363
|
},
|
|
364
|
+
async deadLetters() {
|
|
365
|
+
return [];
|
|
366
|
+
},
|
|
364
367
|
async call() {
|
|
365
368
|
return null;
|
|
366
369
|
},
|
|
367
370
|
clock: {
|
|
368
371
|
now: () => 0,
|
|
372
|
+
ago: () => 0,
|
|
373
|
+
fromNow: () => 0,
|
|
374
|
+
duration: () => 0,
|
|
369
375
|
sleep: async () => undefined,
|
|
370
376
|
},
|
|
371
377
|
vault: {
|
|
@@ -451,6 +457,9 @@ describe("fx — wholesale swap", () => {
|
|
|
451
457
|
withQuery(rows) {
|
|
452
458
|
return { [jsonResultBrand]: true, status: 200, value: [...rows], meta: {} };
|
|
453
459
|
},
|
|
460
|
+
stream(chunks) {
|
|
461
|
+
return { [jsonResultBrand]: true, kind: "stream" as const, status: 200 as const, chunks };
|
|
462
|
+
},
|
|
454
463
|
},
|
|
455
464
|
async step(_name, fn) {
|
|
456
465
|
return fn();
|
|
@@ -514,6 +523,19 @@ describe("fx.t — catalogs", () => {
|
|
|
514
523
|
});
|
|
515
524
|
});
|
|
516
525
|
|
|
526
|
+
describe("fx.clock — relative instants", () => {
|
|
527
|
+
test("ago / fromNow / duration use the injected now and duration strings", () => {
|
|
528
|
+
const fx = createFx({ flow: "sessions.sweepExpired", effects: {}, now: () => 1_000_000 });
|
|
529
|
+
expect(fx.clock.now()).toBe(1_000_000);
|
|
530
|
+
expect(fx.clock.duration("30d")).toBe(30 * 86_400_000);
|
|
531
|
+
expect(fx.clock.ago("30d")).toBe(1_000_000 - 30 * 86_400_000);
|
|
532
|
+
expect(fx.clock.fromNow("14d")).toBe(1_000_000 + 14 * 86_400_000);
|
|
533
|
+
expect(fx.clock.duration("nope")).toBe(0);
|
|
534
|
+
expect(fx.clock.ago("nope")).toBe(1_000_000);
|
|
535
|
+
expect(fx.clock.fromNow("200ms")).toBe(1_000_200);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
517
539
|
describe("fx.json", () => {
|
|
518
540
|
test("with accepts a page or (data, meta)", () => {
|
|
519
541
|
const fx = createFx({ flow: "x", effects: {} });
|