okengine 0.10.1 → 0.10.3
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/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +2 -8
- package/site/content/docs/elements/flow.mdx +10 -17
- package/site/content/docs/elements/gate.mdx +4 -10
- package/site/content/docs/elements/signal.mdx +4 -13
- package/site/content/docs/elements/store.mdx +9 -16
- package/site/content/docs/elements/vault.mdx +1 -7
- package/site/content/docs/get-started/basic-usage.mdx +1 -9
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +6 -7
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +33 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +90 -5
- package/src/cli/dev.ts +39 -16
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +15 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +111 -0
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/images-config.test.ts +168 -0
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
package/src/kernel/boot.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
|
|
|
79
79
|
expect(needs.signal).toBe(false);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("oke() Store-only graph stays under the prior 54 kB baseline", async () => {
|
|
82
|
+
test("oke() Store-only graph stays under the prior 54.3 kB baseline", async () => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
|
|
84
84
|
const entry = join(dir, "entry.ts");
|
|
85
85
|
const appPath = join(import.meta.dir, "app.ts");
|
|
@@ -120,8 +120,18 @@ describe("boot — lazy element needs", () => {
|
|
|
120
120
|
// gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
|
|
121
121
|
// unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
|
|
122
122
|
// shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
|
|
123
|
-
// stay lazy-bound; far below eager bind.
|
|
124
|
-
|
|
123
|
+
// stay lazy-bound; far below eager bind. Rebased again for the
|
|
124
|
+
// store/vault/signal/channel auto-registry drain (`element-registries.ts`
|
|
125
|
+
// + the `oke()` merge/dedup logic) — every app now carries this
|
|
126
|
+
// regardless of which elements it uses, since the drain must run
|
|
127
|
+
// synchronously at construction (same isolation guarantee as the
|
|
128
|
+
// `on()` binding registry) and can't be deferred behind a lazy import
|
|
129
|
+
// (~54.0 kB → 54.3 kB cap). Rebased again for `assertAdoptBarrelFresh`
|
|
130
|
+
// + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
|
|
131
|
+
// check, OKE1009) — part of the already-lazily-loaded boot graph, not
|
|
132
|
+
// the `oke()` construction path, but `Bun.build` still traces and
|
|
133
|
+
// sums the dynamic-import chunk (~54.3 kB → 54.7 kB cap).
|
|
134
|
+
expect(total).toBeLessThan(54_700);
|
|
125
135
|
} finally {
|
|
126
136
|
await rm(dir, { recursive: true, force: true });
|
|
127
137
|
}
|
|
@@ -130,8 +140,7 @@ describe("boot — lazy element needs", () => {
|
|
|
130
140
|
|
|
131
141
|
describe("boot — capabilities from Manifest effects", () => {
|
|
132
142
|
test("tokens cover declared effects only", async () => {
|
|
133
|
-
const charge = flow({
|
|
134
|
-
name: "payments.charge",
|
|
143
|
+
const charge = flow("payments.charge", {
|
|
135
144
|
effects: { secrets: ["STRIPE_KEY"], writes: ["sql:orders"] },
|
|
136
145
|
do: () => ({ ok: true }),
|
|
137
146
|
});
|
|
@@ -158,8 +167,7 @@ describe("boot — cron fires without manual dispatch", () => {
|
|
|
158
167
|
let ran = 0;
|
|
159
168
|
on(
|
|
160
169
|
every("1h"),
|
|
161
|
-
flow({
|
|
162
|
-
name: "cleanup.expire",
|
|
170
|
+
flow("cleanup.expire", {
|
|
163
171
|
do: () => {
|
|
164
172
|
ran += 1;
|
|
165
173
|
},
|
|
@@ -200,8 +208,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
|
|
|
200
208
|
resetFlowSeq();
|
|
201
209
|
on(
|
|
202
210
|
http.get("/ping"),
|
|
203
|
-
flow({
|
|
204
|
-
name: "ping",
|
|
211
|
+
flow("ping", {
|
|
205
212
|
do: () => ({ ok: true }),
|
|
206
213
|
}),
|
|
207
214
|
);
|
|
@@ -217,8 +224,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
|
|
|
217
224
|
const { GateBootError } = await import("../elements/gate/boot.ts");
|
|
218
225
|
on(
|
|
219
226
|
http.get("/ping"),
|
|
220
|
-
flow({
|
|
221
|
-
name: "ping",
|
|
227
|
+
flow("ping", {
|
|
222
228
|
do: () => ({ ok: true }),
|
|
223
229
|
}),
|
|
224
230
|
);
|
|
@@ -235,8 +241,7 @@ describe("boot — HTTP gate wiring on the default path", () => {
|
|
|
235
241
|
const { gate } = await import("../elements/gate.ts");
|
|
236
242
|
on(
|
|
237
243
|
http.get("/ping").gate(gate.public),
|
|
238
|
-
flow({
|
|
239
|
-
name: "ping-public",
|
|
244
|
+
flow("ping-public", {
|
|
240
245
|
do: () => ({ ok: true }),
|
|
241
246
|
}),
|
|
242
247
|
);
|
|
@@ -261,8 +266,7 @@ describe("boot — cron autostart via real timer loop", () => {
|
|
|
261
266
|
let ran = 0;
|
|
262
267
|
on(
|
|
263
268
|
every("10m"),
|
|
264
|
-
flow({
|
|
265
|
-
name: "cleanup.autostart",
|
|
269
|
+
flow("cleanup.autostart", {
|
|
266
270
|
do: () => {
|
|
267
271
|
ran += 1;
|
|
268
272
|
},
|
package/src/kernel/boot.ts
CHANGED
|
@@ -285,7 +285,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
285
285
|
input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
|
|
286
286
|
// `-d` always selects the `docker` driver profile — not a mix of local/test +
|
|
287
287
|
// prod store overrides when `$options.env` is unset.
|
|
288
|
-
const env: ConfigEnv = docker ? "docker" :
|
|
288
|
+
const env: ConfigEnv = input.env ?? (docker ? "docker" : "local");
|
|
289
289
|
let config = input.config;
|
|
290
290
|
if (config === undefined) {
|
|
291
291
|
try {
|
|
@@ -484,6 +484,11 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
484
484
|
rootDir: options.rootDir,
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
+
// 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
|
|
488
|
+
// capability minting above; never a filesystem read the kernel performs
|
|
489
|
+
// on its own).
|
|
490
|
+
await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir);
|
|
491
|
+
|
|
487
492
|
return {
|
|
488
493
|
vault,
|
|
489
494
|
store,
|
|
@@ -603,6 +608,81 @@ export async function mintCapabilities(
|
|
|
603
608
|
return map;
|
|
604
609
|
}
|
|
605
610
|
|
|
611
|
+
/** Per-process guard so the stale-barrel boot warning fires once. */
|
|
612
|
+
let staleAdoptBarrelWarned = false;
|
|
613
|
+
|
|
614
|
+
/** Reset the once-per-process stale-barrel warn latch (tests only). */
|
|
615
|
+
export function resetStaleAdoptBarrelWarnForTests(): void {
|
|
616
|
+
staleAdoptBarrelWarned = false;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Best-effort disk scan for `<rootDir>/src/flows/*` unit folders — mirrors
|
|
621
|
+
* {@link tryAutoExtractManifest}'s lazy, defensive, never-throws posture and
|
|
622
|
+
* the same `new URL` trick so the generator never enters the bundler graph
|
|
623
|
+
* for apps that never opt into `rootDir`.
|
|
624
|
+
*
|
|
625
|
+
* @param rootDir - Project root to scan
|
|
626
|
+
*/
|
|
627
|
+
async function tryListFlowsUnits(rootDir: string): Promise<readonly string[] | undefined> {
|
|
628
|
+
try {
|
|
629
|
+
const url = new URL("../compiler/generate-adopt.ts", import.meta.url);
|
|
630
|
+
const { generateAdoptBarrel } = (await import(
|
|
631
|
+
url.href
|
|
632
|
+
)) as typeof import("../compiler/generate-adopt.ts");
|
|
633
|
+
return (await generateAdoptBarrel({ rootDir })).units;
|
|
634
|
+
} catch {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Confirm every `src/flows/<unit>` folder on disk actually reached this
|
|
641
|
+
* boot's adopted flows — the disk-file counterpart of a stale/missing
|
|
642
|
+
* generated `.adopt()` barrel (`src/flows/generated.ts`). A folder present
|
|
643
|
+
* on disk with zero adopted flows under that unit means the barrel wasn't
|
|
644
|
+
* regenerated (or was hand-edited) after the folder was added.
|
|
645
|
+
*
|
|
646
|
+
* Opt-in only via {@link BootOptions.rootDir} / `OKE_ROOT_DIR` (same gate as
|
|
647
|
+
* {@link mintCapabilities}'s Manifest fallback) — never a filesystem read
|
|
648
|
+
* the kernel performs on its own. `local` / `test` warn once per process;
|
|
649
|
+
* `docker` / `prod` fail loud (`OKE1009`) — same class as `NO_EFFECTS_DECLARED`,
|
|
650
|
+
* never a silently-incomplete route table in a deploy-shaped environment.
|
|
651
|
+
*
|
|
652
|
+
* @param flows - Adopted flows
|
|
653
|
+
* @param env - Resolved {@link ConfigEnv}
|
|
654
|
+
* @param rootDir - Explicit project root (falls back to `OKE_ROOT_DIR`)
|
|
655
|
+
*/
|
|
656
|
+
export async function assertAdoptBarrelFresh(
|
|
657
|
+
flows: readonly AnyFlowDef[],
|
|
658
|
+
env: ConfigEnv = "local",
|
|
659
|
+
rootDir?: string,
|
|
660
|
+
): Promise<void> {
|
|
661
|
+
const dir = rootDir ?? process.env["OKE_ROOT_DIR"];
|
|
662
|
+
if (!dir) return;
|
|
663
|
+
|
|
664
|
+
const diskUnits = await tryListFlowsUnits(dir);
|
|
665
|
+
if (diskUnits === undefined || diskUnits.length === 0) return;
|
|
666
|
+
|
|
667
|
+
const adoptedUnits = new Set(
|
|
668
|
+
flows.map((f) => f.unit).filter((u): u is string => u !== undefined),
|
|
669
|
+
);
|
|
670
|
+
const missing = diskUnits.filter((u) => !adoptedUnits.has(u));
|
|
671
|
+
if (missing.length === 0) return;
|
|
672
|
+
|
|
673
|
+
if (env === "docker" || env === "prod") {
|
|
674
|
+
throwOke("ADOPT_BARREL_STALE", { unit: missing[0]! });
|
|
675
|
+
}
|
|
676
|
+
if (!staleAdoptBarrelWarned) {
|
|
677
|
+
staleAdoptBarrelWarned = true;
|
|
678
|
+
emitBootWarn(
|
|
679
|
+
`oke boot: src/flows/${missing[0]} exists on disk but adopted no flows — the ` +
|
|
680
|
+
'.adopt() barrel ("src/flows/generated.ts") is stale. Run `oke dev` or `oke build` ' +
|
|
681
|
+
"to regenerate it — docker/prod refuse to boot this way.",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
606
686
|
function isRunsRuntime(value: RunsRuntime | CreateRunsRuntimeOptions): value is RunsRuntime {
|
|
607
687
|
return (
|
|
608
688
|
typeof value === "object" &&
|
package/src/kernel/call.test.ts
CHANGED
|
@@ -11,15 +11,13 @@ beforeEach(() => {
|
|
|
11
11
|
|
|
12
12
|
describe("fx.call — untriggered flows", () => {
|
|
13
13
|
test("untriggered flow is callable via app.call and fx.call", async () => {
|
|
14
|
-
const stats = flow({
|
|
15
|
-
name: "links.stats",
|
|
14
|
+
const stats = flow("links.stats", {
|
|
16
15
|
do: ({ code }: { code: string }) => ({ code, clicks: 7 }),
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
const parent = on(
|
|
20
19
|
http.post("/run"),
|
|
21
|
-
flow({
|
|
22
|
-
name: "links.run",
|
|
20
|
+
flow("links.run", {
|
|
23
21
|
effects: { calls: ["links.stats"] },
|
|
24
22
|
do: async ({ code }: { code: string }, fx) => {
|
|
25
23
|
const result = await fx.call("links.stats", { code });
|
|
@@ -54,8 +52,7 @@ describe("fx.call — untriggered flows", () => {
|
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
test("fx.call propagates fx.principal without filling fx.auth", async () => {
|
|
57
|
-
const audit = flow({
|
|
58
|
-
name: "audit.log",
|
|
55
|
+
const audit = flow("audit.log", {
|
|
59
56
|
do: (_input: { event: string }, fx) => ({
|
|
60
57
|
authUserId: fx.auth.userId,
|
|
61
58
|
principalUserId: fx.principal.userId,
|
|
@@ -65,8 +62,7 @@ describe("fx.call — untriggered flows", () => {
|
|
|
65
62
|
|
|
66
63
|
const act = on(
|
|
67
64
|
http.post("/act"),
|
|
68
|
-
flow({
|
|
69
|
-
name: "act",
|
|
65
|
+
flow("act", {
|
|
70
66
|
effects: { calls: ["audit.log"] },
|
|
71
67
|
do: async (_input: Record<string, never>, fx) => {
|
|
72
68
|
return fx.call("audit.log", { event: "act" });
|
|
@@ -99,8 +95,7 @@ describe("fx.call — untriggered flows", () => {
|
|
|
99
95
|
|
|
100
96
|
test("http and signal invocations of the same flow execute identically", async () => {
|
|
101
97
|
let runs = 0;
|
|
102
|
-
const work = flow({
|
|
103
|
-
name: "work",
|
|
98
|
+
const work = flow("work", {
|
|
104
99
|
do: (input: { v: number }) => {
|
|
105
100
|
runs += 1;
|
|
106
101
|
return { out: input.v + 1 };
|
|
@@ -24,8 +24,7 @@ describe("durable compensate", () => {
|
|
|
24
24
|
const store = createMemoryJournalStore();
|
|
25
25
|
const undos: string[] = [];
|
|
26
26
|
|
|
27
|
-
const pay = flow({
|
|
28
|
-
name: "pay.charge",
|
|
27
|
+
const pay = flow("pay.charge", {
|
|
29
28
|
durable: true,
|
|
30
29
|
do: async (_input: { amount: number }, fx) => {
|
|
31
30
|
await fx.step("reserve", async () => "reserved");
|
|
@@ -72,8 +71,7 @@ describe("durable compensate", () => {
|
|
|
72
71
|
test("compensate does not run on successful path", async () => {
|
|
73
72
|
let compensated = 0;
|
|
74
73
|
const store = createMemoryJournalStore();
|
|
75
|
-
const ok = flow({
|
|
76
|
-
name: "pay.ok",
|
|
74
|
+
const ok = flow("pay.ok", {
|
|
77
75
|
durable: true,
|
|
78
76
|
do: async (_input, fx) => {
|
|
79
77
|
await fx.step("a", () => "done");
|
|
@@ -103,8 +101,7 @@ describe("durable compensate", () => {
|
|
|
103
101
|
await runs.open();
|
|
104
102
|
const store = createMemoryJournalStore();
|
|
105
103
|
|
|
106
|
-
const boom = flow({
|
|
107
|
-
name: "pay.boom",
|
|
104
|
+
const boom = flow("pay.boom", {
|
|
108
105
|
durable: true,
|
|
109
106
|
do: async () => {
|
|
110
107
|
throw new Error("boom");
|
|
@@ -218,8 +218,7 @@ describe("retry + durable journal", () => {
|
|
|
218
218
|
let attempts = 0;
|
|
219
219
|
let crash = true;
|
|
220
220
|
|
|
221
|
-
const f = flow({
|
|
222
|
-
name: "pay.retry",
|
|
221
|
+
const f = flow("pay.retry", {
|
|
223
222
|
durable: true,
|
|
224
223
|
do: async (_input, fx) => {
|
|
225
224
|
const charged = await fx.step("charge", () =>
|
|
@@ -250,13 +249,12 @@ describe("retry + durable journal", () => {
|
|
|
250
249
|
expect(attempts).toBe(1);
|
|
251
250
|
});
|
|
252
251
|
|
|
253
|
-
test("flow({ retry }) reuses journal — completed steps do not re-run", async () => {
|
|
252
|
+
test("flow(name, { retry }) reuses journal — completed steps do not re-run", async () => {
|
|
254
253
|
const journalStore = createMemoryJournalStore();
|
|
255
254
|
const calls: string[] = [];
|
|
256
255
|
let failOnce = true;
|
|
257
256
|
|
|
258
|
-
const f = flow({
|
|
259
|
-
name: "flow.retry",
|
|
257
|
+
const f = flow("flow.retry", {
|
|
260
258
|
durable: true,
|
|
261
259
|
retry: { retries: 2, delay: 0, jitter: false },
|
|
262
260
|
do: async (_input, fx) => {
|
|
@@ -17,8 +17,7 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
|
|
|
17
17
|
const runs = createRunsRuntime({ driver: "memory" });
|
|
18
18
|
await runs.open();
|
|
19
19
|
|
|
20
|
-
const producer = flow({
|
|
21
|
-
name: "orders.create",
|
|
20
|
+
const producer = flow("orders.create", {
|
|
22
21
|
effects: { emits: ["corr-order-placed"] },
|
|
23
22
|
do: async (input: { id: string }, fx) => {
|
|
24
23
|
await fx.emit(orderPlaced, { id: input.id });
|
|
@@ -26,8 +25,7 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
|
|
|
26
25
|
},
|
|
27
26
|
});
|
|
28
27
|
|
|
29
|
-
const consumer = flow({
|
|
30
|
-
name: "orders.notify",
|
|
28
|
+
const consumer = flow("orders.notify", {
|
|
31
29
|
do: async (_input: { id: string }) => ({ notified: true as const }),
|
|
32
30
|
});
|
|
33
31
|
|
|
@@ -72,13 +70,11 @@ describe("correlation — parentId across Flow → Signal → Flow", () => {
|
|
|
72
70
|
const runs = createRunsRuntime({ driver: "memory" });
|
|
73
71
|
await runs.open();
|
|
74
72
|
|
|
75
|
-
const child = flow({
|
|
76
|
-
name: "inner.work",
|
|
73
|
+
const child = flow("inner.work", {
|
|
77
74
|
do: () => ({ done: true as const }),
|
|
78
75
|
});
|
|
79
76
|
|
|
80
|
-
const parent = flow({
|
|
81
|
-
name: "outer.work",
|
|
77
|
+
const parent = flow("outer.work", {
|
|
82
78
|
effects: { calls: ["inner.work"] },
|
|
83
79
|
do: async (_input, fx) => {
|
|
84
80
|
await fx.call(child, {});
|
package/src/kernel/edge.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ beforeEach(() => {
|
|
|
19
19
|
|
|
20
20
|
describe("plugin edge handlers", () => {
|
|
21
21
|
test("an edge handler answers an unmatched request; the flow still owns matched ones", async () => {
|
|
22
|
-
on(http.get("/x"), flow(
|
|
22
|
+
on(http.get("/x"), flow("x.get", { do: () => ({ ok: true }) }));
|
|
23
23
|
const edge = plugin("edge-test", { version: "0.0.1" }).edge((_request, info) => {
|
|
24
24
|
if (info.method === "OPTIONS") {
|
|
25
25
|
return new Response(null, { status: 204, headers: { "x-edge": "yes" } });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Boot-level proof: compiled/inferred effects reaching a REAL running app's
|
|
3
3
|
* capability token — not an isolated compiler-only or kernel-only test.
|
|
4
4
|
*
|
|
5
|
-
* Before this file existed, `flow({...})` with no hand-declared `effects`
|
|
5
|
+
* Before this file existed, `flow(name, {...})` with no hand-declared `effects`
|
|
6
6
|
* always minted an OPEN capability token (every access allowed, no gate at
|
|
7
7
|
* all) in every environment, including `docker` / `prod` — `extractManifest`
|
|
8
8
|
* inference only ever fed the static `manifest.oke.json` artifact (Console,
|
|
@@ -48,8 +48,7 @@ const CreateOut = z.object({ ok: z.boolean() });
|
|
|
48
48
|
function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
|
|
49
49
|
return on(
|
|
50
50
|
http.post("/notes").gate(gate.public),
|
|
51
|
-
flow({
|
|
52
|
-
name: "notes.create",
|
|
51
|
+
flow("notes.create", {
|
|
53
52
|
in: CreateIn,
|
|
54
53
|
out: CreateOut,
|
|
55
54
|
// Deliberately no `effects` — the "let the compiler infer it" case.
|
|
@@ -117,8 +116,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
117
116
|
const db = store.sql("app", { schema: { notes } });
|
|
118
117
|
const create = on(
|
|
119
118
|
http.post("/notes").gate(gate.public),
|
|
120
|
-
flow({
|
|
121
|
-
name: "notes.create",
|
|
119
|
+
flow("notes.create", {
|
|
122
120
|
in: CreateIn,
|
|
123
121
|
out: CreateOut,
|
|
124
122
|
// The convention every template/test predates Direction B with —
|
|
@@ -171,8 +169,7 @@ const db = {} as any;
|
|
|
171
169
|
const notes = {} as any;
|
|
172
170
|
export const create = on(
|
|
173
171
|
http.post("/notes").gate(gate.public),
|
|
174
|
-
flow({
|
|
175
|
-
name: "notes.create",
|
|
172
|
+
flow("notes.create", {
|
|
176
173
|
do: async (input, fx) => {
|
|
177
174
|
await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
|
|
178
175
|
return { ok: true };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
|
|
3
|
+
* `signal()`, and `channel.<medium>().template()` — the plain arrays behind
|
|
4
|
+
* each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
|
|
5
|
+
* trigger-drain `bindings` array).
|
|
6
|
+
*
|
|
7
|
+
* Lives here, not in the declare modules themselves, so {@link oke} can read
|
|
8
|
+
* every registry with one lightweight import instead of statically pulling
|
|
9
|
+
* in all four element `declare.ts` modules (and every unrelated export they
|
|
10
|
+
* carry) into every app's bundle — see the `oke()` Store-only bundle-size
|
|
11
|
+
* budget in `boot.test.ts`. Only type imports cross back to the declare
|
|
12
|
+
* modules, so this file has zero runtime dependencies of its own.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { StoreDecl } from "../elements/store/declare.ts";
|
|
16
|
+
import type { VaultSecretDecl } from "../elements/vault/declare.ts";
|
|
17
|
+
import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
18
|
+
import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
|
|
19
|
+
|
|
20
|
+
/** `store.sql` / `store.files` declarations since the last reset. */
|
|
21
|
+
export const storeRegistry: StoreDecl[] = [];
|
|
22
|
+
/** `vault.secret` declarations since the last reset. */
|
|
23
|
+
export const secretRegistry: VaultSecretDecl[] = [];
|
|
24
|
+
/** `signal()` declarations since the last reset. */
|
|
25
|
+
export const signalRegistry: SignalDecl[] = [];
|
|
26
|
+
/** Medium-binder `.template()` declarations since the last reset. */
|
|
27
|
+
export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
|
package/src/kernel/errors.ts
CHANGED
|
@@ -153,6 +153,17 @@ export const OKE_ERRORS = {
|
|
|
153
153
|
"Add explicit `effects` to this flow, or boot with a Manifest (`oke build`) / " +
|
|
154
154
|
"`rootDir` so effects can be derived. docker/prod refuse an open capability token.",
|
|
155
155
|
},
|
|
156
|
+
/**
|
|
157
|
+
* A `src/flows/<unit>` folder exists on disk but no adopted flow carries
|
|
158
|
+
* that unit — the generated `.adopt()` barrel (`src/flows/generated.ts`)
|
|
159
|
+
* is stale or was hand-edited. docker/prod — never a silently-incomplete
|
|
160
|
+
* route table in a deploy-shaped environment.
|
|
161
|
+
*/
|
|
162
|
+
ADOPT_BARREL_STALE: {
|
|
163
|
+
code: 1009,
|
|
164
|
+
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
165
|
+
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
|
|
166
|
+
},
|
|
156
167
|
/**
|
|
157
168
|
* Emit target has no subscriber (unified-theory §21).
|
|
158
169
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
package/src/kernel/flow.test.ts
CHANGED
|
@@ -11,8 +11,7 @@ beforeEach(() => {
|
|
|
11
11
|
|
|
12
12
|
describe("flow — one species", () => {
|
|
13
13
|
test("flow() returns a branded FlowDef", () => {
|
|
14
|
-
const f = flow({
|
|
15
|
-
name: "ping",
|
|
14
|
+
const f = flow("ping", {
|
|
16
15
|
do: () => ({ ok: true }),
|
|
17
16
|
});
|
|
18
17
|
expect(isFlow(f)).toBe(true);
|
|
@@ -22,7 +21,7 @@ describe("flow — one species", () => {
|
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
test("on() returns the same flow object and records the trigger", () => {
|
|
25
|
-
const f = flow(
|
|
24
|
+
const f = flow("create", { do: () => 1 });
|
|
26
25
|
const bound = on(http.post("/notes"), f);
|
|
27
26
|
// Same runtime object; trigger stamp is type-only so identities differ at the type level.
|
|
28
27
|
expect(bound as object).toBe(f);
|
|
@@ -33,8 +32,7 @@ describe("flow — one species", () => {
|
|
|
33
32
|
|
|
34
33
|
test("the same flow object is reachable from HTTP and signal triggers", async () => {
|
|
35
34
|
const calls: string[] = [];
|
|
36
|
-
const shared = flow({
|
|
37
|
-
name: "shared.work",
|
|
35
|
+
const shared = flow("shared.work", {
|
|
38
36
|
do: (input: { n: number }) => {
|
|
39
37
|
calls.push(`n=${input.n}`);
|
|
40
38
|
return { n: input.n * 2 };
|
|
@@ -65,8 +63,7 @@ describe("five trigger kinds", () => {
|
|
|
65
63
|
|
|
66
64
|
const httpFlow = on(
|
|
67
65
|
http.get("/:code"),
|
|
68
|
-
flow({
|
|
69
|
-
name: "t.http",
|
|
66
|
+
flow("t.http", {
|
|
70
67
|
do: ({ code }: { code: string }) => {
|
|
71
68
|
seen.push(`http:${code}`);
|
|
72
69
|
return { code };
|
|
@@ -76,8 +73,7 @@ describe("five trigger kinds", () => {
|
|
|
76
73
|
|
|
77
74
|
on(
|
|
78
75
|
every("1h"),
|
|
79
|
-
flow({
|
|
80
|
-
name: "t.every",
|
|
76
|
+
flow("t.every", {
|
|
81
77
|
do: () => {
|
|
82
78
|
seen.push("every");
|
|
83
79
|
},
|
|
@@ -86,8 +82,7 @@ describe("five trigger kinds", () => {
|
|
|
86
82
|
|
|
87
83
|
on(
|
|
88
84
|
{ name: "link-clicked", delivery: "once" },
|
|
89
|
-
flow({
|
|
90
|
-
name: "t.signal",
|
|
85
|
+
flow("t.signal", {
|
|
91
86
|
do: ({ code }: { code: string }) => {
|
|
92
87
|
seen.push(`signal:${code}`);
|
|
93
88
|
},
|
|
@@ -96,8 +91,7 @@ describe("five trigger kinds", () => {
|
|
|
96
91
|
|
|
97
92
|
on(
|
|
98
93
|
table("orders").changed("status"),
|
|
99
|
-
flow({
|
|
100
|
-
name: "t.cdc",
|
|
94
|
+
flow("t.cdc", {
|
|
101
95
|
do: (p: { before: { status: string }; after: { status: string } }) => {
|
|
102
96
|
seen.push(`cdc:${p.before.status}->${p.after.status}`);
|
|
103
97
|
},
|
|
@@ -106,8 +100,7 @@ describe("five trigger kinds", () => {
|
|
|
106
100
|
|
|
107
101
|
on(
|
|
108
102
|
internal,
|
|
109
|
-
flow({
|
|
110
|
-
name: "t.internal",
|
|
103
|
+
flow("t.internal", {
|
|
111
104
|
do: () => {
|
|
112
105
|
seen.push("internal");
|
|
113
106
|
return { ok: true };
|
package/src/kernel/flow.ts
CHANGED
|
@@ -38,10 +38,6 @@ export type FlowHandler<I = unknown, O = unknown> = (
|
|
|
38
38
|
|
|
39
39
|
/** Options for {@link flow}. */
|
|
40
40
|
export interface FlowOptions<I = unknown, O = unknown, E extends FlowErrorMap = FlowErrorMap> {
|
|
41
|
-
/** Optional stable name (used by `fx.call` and the Manifest). */
|
|
42
|
-
readonly name?: string;
|
|
43
|
-
/** Optional unit scope for unit-level hooks. */
|
|
44
|
-
readonly unit?: string;
|
|
45
41
|
/** Input schema (Standard Schema when present). */
|
|
46
42
|
readonly in?: SchemaInput;
|
|
47
43
|
/** Output schema (Standard Schema when present). */
|
|
@@ -155,7 +151,7 @@ export interface FlowDef<
|
|
|
155
151
|
readonly [triggerPhantom]?: T;
|
|
156
152
|
/** Stable name (auto-assigned when omitted). */
|
|
157
153
|
readonly name: string;
|
|
158
|
-
/**
|
|
154
|
+
/** Unit scope — derived from `name`'s first dot segment (e.g. `"auth.refresh"` → `"auth"`). */
|
|
159
155
|
readonly unit: string | undefined;
|
|
160
156
|
/** Input schema. */
|
|
161
157
|
readonly in: SchemaInput | undefined;
|
|
@@ -231,28 +227,30 @@ export interface FlowDef<
|
|
|
231
227
|
/** Unique brand symbol for {@link FlowDef}. */
|
|
232
228
|
export const flowBrand: unique symbol = Symbol("oke.flow");
|
|
233
229
|
|
|
234
|
-
let flowSeq = 0;
|
|
235
|
-
|
|
236
230
|
/**
|
|
237
231
|
* Define a Flow — the one species of backend behavior.
|
|
238
232
|
*
|
|
239
233
|
* Input / output types are inferred from Standard Schema `in` / `out` when
|
|
240
234
|
* present; otherwise from the `do` handler signature.
|
|
241
235
|
*
|
|
236
|
+
* @param name - Stable name (used by `fx.call` and the Manifest)
|
|
242
237
|
* @param options - Contract and handler
|
|
243
238
|
*/
|
|
244
239
|
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
245
|
-
|
|
240
|
+
name: string,
|
|
241
|
+
options: Opts & { readonly name?: never },
|
|
246
242
|
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>> {
|
|
247
|
-
const name = options.name ?? `flow_${++flowSeq}`;
|
|
248
243
|
const triggers: Trigger[] = [];
|
|
249
244
|
const hooks: Partial<Record<HookStage, HookFn[]>> = {};
|
|
250
245
|
const pendingPlugins: PluginDef[] = [];
|
|
251
246
|
|
|
247
|
+
const dot = name.indexOf(".");
|
|
248
|
+
const unit = dot > 0 ? name.slice(0, dot) : undefined;
|
|
249
|
+
|
|
252
250
|
const def: FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>> = {
|
|
253
251
|
[flowBrand]: true,
|
|
254
252
|
name,
|
|
255
|
-
unit
|
|
253
|
+
unit,
|
|
256
254
|
in: options.in,
|
|
257
255
|
out: options.out,
|
|
258
256
|
errors: (options.errors ?? undefined) as InferFlowErrors<Opts> | undefined,
|
|
@@ -316,10 +314,10 @@ export function isFlow(value: unknown): value is AnyFlowDef {
|
|
|
316
314
|
}
|
|
317
315
|
|
|
318
316
|
/**
|
|
319
|
-
*
|
|
317
|
+
* No-op. `name` is a required positional argument now, so there is no
|
|
318
|
+
* auto-name sequence left to reset — kept so existing test `beforeEach`
|
|
319
|
+
* blocks don't need touching.
|
|
320
320
|
*
|
|
321
321
|
* @internal
|
|
322
322
|
*/
|
|
323
|
-
export function resetFlowSeq(): void {
|
|
324
|
-
flowSeq = 0;
|
|
325
|
-
}
|
|
323
|
+
export function resetFlowSeq(): void {}
|
|
@@ -27,7 +27,7 @@ describe("hook timing", () => {
|
|
|
27
27
|
});
|
|
28
28
|
const ctx: InvocationContext = {
|
|
29
29
|
trigger: { kind: "internal" },
|
|
30
|
-
flow: flow(
|
|
30
|
+
flow: flow("t", { do: () => ({ ok: true }) }),
|
|
31
31
|
input: undefined,
|
|
32
32
|
params: {},
|
|
33
33
|
state: {},
|
|
@@ -56,7 +56,7 @@ describe("hook timing", () => {
|
|
|
56
56
|
const hooks = registry.hooksAt("app", undefined, "x");
|
|
57
57
|
const ctx: InvocationContext = {
|
|
58
58
|
trigger: { kind: "internal" },
|
|
59
|
-
flow: flow(
|
|
59
|
+
flow: flow("x", { do: () => undefined }),
|
|
60
60
|
input: undefined,
|
|
61
61
|
params: {},
|
|
62
62
|
state: {},
|
package/src/kernel/hooks.test.ts
CHANGED
|
@@ -40,7 +40,7 @@ describe("hooks — order and composition", () => {
|
|
|
40
40
|
void fn(
|
|
41
41
|
{
|
|
42
42
|
trigger: { kind: "internal" },
|
|
43
|
-
flow: flow(
|
|
43
|
+
flow: flow("x", { do: () => undefined }),
|
|
44
44
|
input: undefined,
|
|
45
45
|
params: {},
|
|
46
46
|
state: {},
|
|
@@ -57,9 +57,7 @@ describe("hooks — order and composition", () => {
|
|
|
57
57
|
test("pipeline fires stages in order around the handler", async () => {
|
|
58
58
|
const order: string[] = [];
|
|
59
59
|
|
|
60
|
-
const f = flow({
|
|
61
|
-
name: "ordered",
|
|
62
|
-
unit: "links",
|
|
60
|
+
const f = flow("links.ordered", {
|
|
63
61
|
do: () => {
|
|
64
62
|
order.push("handler");
|
|
65
63
|
return { ok: true };
|
|
@@ -119,8 +117,7 @@ describe("hooks — order and composition", () => {
|
|
|
119
117
|
|
|
120
118
|
on(
|
|
121
119
|
http.get("/sc"),
|
|
122
|
-
flow({
|
|
123
|
-
name: "sc",
|
|
120
|
+
flow("sc", {
|
|
124
121
|
do: () => {
|
|
125
122
|
order.push("handler");
|
|
126
123
|
return { ok: true };
|
|
@@ -151,8 +148,7 @@ describe("hooks — order and composition", () => {
|
|
|
151
148
|
|
|
152
149
|
on(
|
|
153
150
|
http.get("/data"),
|
|
154
|
-
flow({
|
|
155
|
-
name: "data",
|
|
151
|
+
flow("data", {
|
|
156
152
|
do: () => ({ ok: true }),
|
|
157
153
|
}),
|
|
158
154
|
);
|
|
@@ -184,8 +180,7 @@ describe("hooks — order and composition", () => {
|
|
|
184
180
|
|
|
185
181
|
on(
|
|
186
182
|
http.get("/fail"),
|
|
187
|
-
flow({
|
|
188
|
-
name: "fail-flow",
|
|
183
|
+
flow("fail-flow", {
|
|
189
184
|
do: () => {
|
|
190
185
|
order.push("handler");
|
|
191
186
|
},
|