okengine 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +76 -10
- package/site/content/docs/deployment/docker-swarm.mdx +48 -111
- package/site/content/docs/deployment/docker.mdx +43 -36
- package/site/content/docs/deployment/index.mdx +9 -9
- package/site/content/docs/deployment/kubernetes.mdx +4 -4
- package/site/content/docs/elements/ai.mdx +7 -7
- package/site/content/docs/elements/channel.mdx +9 -7
- package/site/content/docs/elements/clock.mdx +8 -8
- package/site/content/docs/elements/flow.mdx +46 -47
- package/site/content/docs/elements/gate.mdx +6 -5
- package/site/content/docs/elements/signal.mdx +2 -2
- package/site/content/docs/elements/store.mdx +71 -78
- package/site/content/docs/elements/vault.mdx +89 -70
- package/site/content/docs/get-started/basic-usage.mdx +8 -8
- package/site/content/docs/get-started/installation.mdx +36 -36
- package/site/content/docs/get-started/introduction.mdx +11 -0
- package/site/content/docs/get-started/why.mdx +2 -2
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +11 -11
- package/site/content/docs/plugins/otp.mdx +2 -2
- package/site/content/docs/plugins/passkey.mdx +1 -1
- package/site/content/docs/plugins/two-factor.mdx +1 -1
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +1 -1
- package/site/content/docs/recipes/cockroachdb.mdx +2 -2
- package/site/content/docs/recipes/dragonfly.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -2
- package/site/content/docs/recipes/llama-cpp.mdx +21 -23
- package/site/content/docs/recipes/mailpit.mdx +11 -11
- package/site/content/docs/recipes/meilisearch.mdx +10 -10
- package/site/content/docs/recipes/meta.json +0 -1
- package/site/content/docs/recipes/ollama.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +12 -9
- package/site/content/docs/recipes/postgres.mdx +3 -3
- package/site/content/docs/recipes/redis.mdx +2 -2
- package/site/content/docs/recipes/rustfs.mdx +10 -7
- package/site/content/docs/recipes/sglang.mdx +1 -1
- package/site/content/docs/recipes/supabase-docker.mdx +1 -1
- package/site/content/docs/recipes/timescale.mdx +2 -2
- package/site/content/docs/recipes/valkey.mdx +1 -1
- package/site/content/docs/recipes/vllm.mdx +1 -1
- package/site/content/docs/recipes/yugabytedb.mdx +2 -2
- package/site/content/docs/reference/cli.md +61 -38
- package/site/content/docs/reference/configuration.mdx +145 -70
- package/site/content/docs/reference/environment-variables.mdx +36 -28
- package/site/content/docs/reference/errors.mdx +28 -29
- package/site/content/docs/reference/fx.mdx +11 -4
- package/site/content/docs/reference/i18n.mdx +17 -6
- package/site/content/docs/reference/index.mdx +5 -0
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/migrating-environments.mdx +158 -0
- package/site/content/docs/reference/plugins.mdx +2 -2
- package/src/auth/gate-auth.test.ts +12 -10
- package/src/cli/ai-setup/ai-setup.test.ts +27 -15
- package/src/cli/ai-setup/apply.ts +111 -15
- package/src/cli/ai-setup/catalog.ts +3 -3
- package/src/cli/ai-setup/index.ts +1 -1
- package/src/cli/ai-setup/prompts.ts +1 -1
- package/src/cli/ai-setup/recommend.test.ts +1 -1
- package/src/cli/ask-seed.test.ts +3 -3
- package/src/cli/build.ts +1 -1
- package/src/cli/db-auto-push.test.ts +6 -3
- package/src/cli/db-auto-push.ts +7 -5
- package/src/cli/db-seed.ts +7 -8
- package/src/cli/db.test.ts +70 -71
- package/src/cli/db.ts +86 -12
- package/src/cli/dev-controller.test.ts +35 -0
- package/src/cli/dev-controller.ts +298 -0
- package/src/cli/dev-controls.ts +11 -65
- package/src/cli/dev-db-push.test.ts +47 -15
- package/src/cli/dev-schema-sync.test.ts +11 -11
- package/src/cli/dev-schema-sync.ts +10 -10
- package/src/cli/dev-session-lock.test.ts +106 -0
- package/src/cli/dev-session-lock.ts +209 -0
- package/src/cli/dev.test.ts +166 -121
- package/src/cli/dev.ts +138 -192
- package/src/cli/doc-staleness.test.ts +3 -6
- package/src/cli/docker-clean.test.ts +33 -1
- package/src/cli/docker-clean.ts +28 -33
- package/src/cli/docker-cli.test.ts +12 -11
- package/src/cli/docker.ts +95 -20
- package/src/cli/doctor.test.ts +40 -0
- package/src/cli/doctor.ts +55 -15
- package/src/cli/drizzle-env.test.ts +11 -13
- package/src/cli/drizzle-env.ts +18 -11
- package/src/cli/ensure-drizzle-config.ts +11 -14
- package/src/cli/hero-meta.test.ts +15 -15
- package/src/cli/hero-meta.ts +10 -9
- package/src/cli/index.ts +36 -6
- package/src/cli/load-config.images.test.ts +16 -12
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +34 -28
- package/src/cli/meilisearch-local.ts +1 -2
- package/src/cli/mode.ts +7 -58
- package/src/cli/registry.ts +175 -19
- package/src/cli/resolve-dev-sql-env.test.ts +8 -16
- package/src/cli/resolve-dev-sql-env.ts +13 -15
- package/src/cli/safe-defaults.test.ts +28 -10
- package/src/cli/schema.ts +65 -16
- package/src/cli/test.ts +58 -0
- package/src/cli/tui/App.tsx +252 -0
- package/src/cli/tui/DevLive.tsx +116 -0
- package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
- package/src/cli/tui/components/HeaderBar.tsx +67 -0
- package/src/cli/tui/components/LogStream.tsx +47 -0
- package/src/cli/tui/components/PanelCard.tsx +39 -0
- package/src/cli/tui/components/SelectRow.tsx +37 -0
- package/src/cli/tui/components/SlashPalette.tsx +172 -0
- package/src/cli/tui/components/Spinner.tsx +32 -0
- package/src/cli/tui/components/StatusBar.tsx +51 -0
- package/src/cli/tui/components/StatusDot.tsx +37 -0
- package/src/cli/tui/components/TabBar.tsx +49 -0
- package/src/cli/tui/components/Wordmark.tsx +43 -0
- package/src/cli/tui/format.ts +38 -0
- package/src/cli/tui/keys.test.ts +18 -0
- package/src/cli/tui/keys.ts +26 -0
- package/src/cli/tui/launch.test.ts +18 -0
- package/src/cli/tui/launch.ts +57 -0
- package/src/cli/tui/panels/Dashboard.tsx +201 -0
- package/src/cli/tui/panels/Database.tsx +198 -0
- package/src/cli/tui/panels/Dev.tsx +257 -0
- package/src/cli/tui/panels/Docker.tsx +357 -0
- package/src/cli/tui/panels/Navigator.test.ts +22 -0
- package/src/cli/tui/panels/Navigator.tsx +213 -0
- package/src/cli/tui/slash-catalog.test.ts +31 -0
- package/src/cli/tui/slash-catalog.ts +184 -0
- package/src/cli/tui/slash-run.ts +54 -0
- package/src/cli/tui/state/ports.ts +95 -0
- package/src/cli/tui/theme.ts +24 -0
- package/src/cli/vault-cmd.test.ts +402 -0
- package/src/cli/vault-cmd.ts +727 -69
- package/src/cli/vault-secure-input.test.ts +56 -0
- package/src/cli/vault-secure-input.ts +124 -0
- package/src/compiler/effects-infer.ts +7 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +16 -23
- package/src/config/driver-defaults.ts +33 -63
- package/src/config/index.ts +169 -144
- package/src/config/resolve-driver.test.ts +14 -19
- package/src/console/server/app.ts +4 -2
- package/src/console/server/console.test.ts +58 -1
- package/src/console/server/flows.ts +22 -2
- package/src/console/server/index.ts +3 -0
- package/src/console/server/operator-db.test.ts +55 -13
- package/src/console/server/operator-db.ts +305 -263
- package/src/console/server/serve.ts +16 -7
- package/src/console/server/state.ts +31 -9
- package/src/console/server/vault.test.ts +120 -5
- package/src/console/server/vault.ts +208 -12
- package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
- package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
- package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/shell/client.ts +16 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
- package/src/console/ui/vault/VaultA11yView.tsx +33 -1
- package/src/console/ui/vault/backend.test.ts +75 -0
- package/src/console/ui/vault/backend.ts +117 -0
- package/src/console/ui/vault/fixture.ts +21 -2
- package/src/console/ui/vault/index.ts +11 -1
- package/src/console/ui/vault/types.ts +26 -0
- package/src/docker/compose.ts +164 -61
- package/src/docker/derive.ts +106 -12
- package/src/docker/docker.test.ts +156 -110
- package/src/docker/helpers.ts +98 -0
- package/src/docker/images-config.test.ts +3 -8
- package/src/docker/index.ts +30 -5
- package/src/docker/recipes/index.ts +0 -3
- package/src/docker/recipes/llama-cpp.ts +4 -4
- package/src/docker/recipes/pgdog.ts +9 -2
- package/src/docker/resources.test.ts +71 -0
- package/src/docker/resources.ts +176 -0
- package/src/docker/stack-id.test.ts +11 -4
- package/src/docker/stack-id.ts +42 -32
- package/src/docker/stack.integration.test.ts +4 -15
- package/src/docker/types.ts +35 -3
- package/src/drivers/ai-ollama.integration.test.ts +2 -2
- package/src/drivers/conformance.test.ts +11 -12
- package/src/drivers/drizzle-dialect.test.ts +0 -4
- package/src/drivers/drizzle-dialect.ts +7 -13
- package/src/drivers/index.ts +24 -7
- package/src/drivers/journal-postgres.test.ts +3 -1
- package/src/drivers/journal-postgres.ts +4 -2
- package/src/drivers/meilisearch.ts +1 -1
- package/src/drivers/pglite.ts +28 -5
- package/src/drivers/types.ts +9 -11
- package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
- package/src/drivers/vault-aws-secrets-manager.ts +333 -0
- package/src/drivers/vault-builtin.test.ts +110 -0
- package/src/drivers/vault-builtin.ts +184 -0
- package/src/drivers/vault-driver-removal.test.ts +13 -0
- package/src/drivers/vault-managed.test.ts +95 -0
- package/src/drivers/vault-managed.ts +101 -25
- package/src/drivers/vault-types.ts +25 -5
- package/src/elements/ai/runtime.ts +1 -1
- package/src/elements/clock/chaos.test.ts +4 -1
- package/src/elements/clock/durable.ts +57 -4
- package/src/elements/clock.test.ts +13 -14
- package/src/elements/gate/config.ts +13 -3
- package/src/elements/gate/constants.ts +18 -0
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- package/src/elements/gate.test.ts +1 -1
- package/src/elements/gate.ts +2 -2
- package/src/elements/store/domain-ddl.test.ts +5 -12
- package/src/elements/store/emit-drizzle.ts +44 -19
- package/src/elements/store/index-boot.test.ts +15 -116
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/resource-list-docs.fixture.ts +12 -6
- package/src/elements/store/resource-list-docs.test.ts +64 -44
- package/src/elements/store/resource.test.ts +17 -10
- package/src/elements/store/runtime.ts +9 -12
- package/src/elements/store/schema-decl.test.ts +16 -32
- package/src/elements/store/seed.test.ts +1 -2
- package/src/elements/store/seed.ts +7 -8
- package/src/elements/store/sql-condition.test.ts +2 -2
- package/src/elements/store/sql-session.test.ts +8 -10
- package/src/elements/store/sql-session.ts +10 -2
- package/src/elements/store.test.ts +13 -13
- package/src/elements/vault/audit.ts +233 -0
- package/src/elements/vault/boot-chain.test.ts +78 -0
- package/src/elements/vault/boot-chain.ts +43 -39
- package/src/elements/vault/builtin-adapter.test.ts +479 -0
- package/src/elements/vault/builtin-adapter.ts +1184 -0
- package/src/elements/vault/crypto.test.ts +237 -0
- package/src/elements/vault/crypto.ts +431 -0
- package/src/elements/vault/declare.ts +164 -3
- package/src/elements/vault/errors.ts +56 -0
- package/src/elements/vault/kms-unseal.test.ts +76 -0
- package/src/elements/vault/kms-unseal.ts +181 -0
- package/src/elements/vault/path.ts +90 -0
- package/src/elements/vault/resilience.test.ts +102 -0
- package/src/elements/vault/resilience.ts +107 -0
- package/src/elements/vault/runtime.ts +36 -4
- package/src/elements/vault/security-checklist.test.ts +130 -0
- package/src/elements/vault/storage.ts +448 -0
- package/src/elements/vault/types.ts +288 -0
- package/src/elements/vault/unseal.ts +150 -0
- package/src/elements/vault.test.ts +72 -4
- package/src/elements/vault.ts +91 -2
- package/src/full.ts +248 -0
- package/src/http.ts +41 -0
- package/src/i18n/interpolate-simple.ts +37 -0
- package/src/i18n/messages.ts +51 -4
- package/src/i18n-entry.ts +7 -0
- package/src/index.ts +35 -145
- package/src/journal-entry.ts +20 -0
- package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
- package/src/kernel/app-auth.ts +98 -0
- package/src/kernel/app.ts +77 -110
- package/src/kernel/auto-registry.test.ts +2 -2
- package/src/kernel/boot-bind/ai.test.ts +5 -5
- package/src/kernel/boot-bind/ai.ts +2 -1
- package/src/kernel/boot-bind/channel.test.ts +6 -6
- package/src/kernel/boot-bind/channel.ts +3 -10
- package/src/kernel/boot-bind/clock.ts +2 -2
- package/src/kernel/boot-bind/gate.ts +5 -9
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +2 -2
- package/src/kernel/boot-bind/store.test.ts +43 -44
- package/src/kernel/boot-bind/store.ts +16 -41
- package/src/kernel/boot-bind/vault.ts +16 -1
- package/src/kernel/boot.test.ts +13 -19
- package/src/kernel/boot.ts +31 -19
- package/src/kernel/budget-entry.ts +1 -1
- package/src/kernel/compensate.test.ts +288 -1
- package/src/kernel/compensate.ts +126 -0
- package/src/kernel/concurrency.test.ts +10 -9
- package/src/kernel/effects-stamping.test.ts +27 -2
- package/src/kernel/element-registries.ts +5 -3
- package/src/kernel/flow.ts +4 -2
- package/src/kernel/fx.test.ts +119 -3
- package/src/kernel/fx.ts +215 -40
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +130 -7
- package/src/kernel/pipeline.test.ts +20 -6
- package/src/kernel/redacted.test.ts +35 -0
- package/src/kernel/redacted.ts +24 -2
- package/src/kernel/router/create-default.ts +16 -0
- package/src/kernel/router/create-edge.ts +18 -0
- package/src/kernel/router/create.ts +23 -0
- package/src/kernel/router/index.ts +23 -0
- package/src/kernel/router/linear.ts +88 -0
- package/src/kernel/router/regexp.ts +188 -0
- package/src/kernel/router/smart.ts +120 -0
- package/src/kernel/router/trie.ts +108 -0
- package/src/kernel/router/types.ts +56 -0
- package/src/kernel/router.ts +18 -532
- package/src/kernel-entry.ts +43 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/plugins/config-source.test.ts +7 -7
- package/src/release/build-lib.ts +61 -0
- package/src/release/exports.test.ts +4 -2
- package/src/release/exports.ts +30 -4
- package/src/release/measure.ts +101 -22
- package/src/runs/duckdb.ts +7 -2
- package/src/shared/lazy-src.ts +79 -0
- package/src/shared/optional-peer.ts +41 -0
- package/src/term.test.ts +3 -44
- package/src/test/create-test-app.ts +12 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/test/reset-element-registries.ts +2 -0
- package/src/upgrade/codemods.test.ts +7 -20
- package/src/upgrade/codemods.ts +93 -60
- package/site/content/docs/recipes/openbao.mdx +0 -141
- package/src/cli/ask-dev-mode.ts +0 -40
- package/src/cli/dev-mode.test.ts +0 -45
- package/src/cli/dev-mode.ts +0 -78
- package/src/cli/openbao-bootstrap.test.ts +0 -150
- package/src/cli/openbao-bootstrap.ts +0 -321
- package/src/cli/openbao-restart.integration.test.ts +0 -145
- package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
- package/src/docker/recipes/openbao.ts +0 -47
- package/src/drivers/libsql.ts +0 -179
- package/src/drivers/sqlite.ts +0 -44
- package/src/drivers/vault-openbao.test.ts +0 -97
- package/src/drivers/vault-openbao.ts +0 -143
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
|
-
import { createMemoryJournalStore } from "./journal.ts";
|
|
7
|
+
import { createMemoryJournalStore, createJournal } from "./journal.ts";
|
|
8
8
|
import type { JournalRuntime } from "./boot-bind/journal.ts";
|
|
9
9
|
import { flow } from "./flow.ts";
|
|
10
10
|
import { oke } from "./app.ts";
|
|
11
11
|
import { createRunsRuntime } from "../runs/runtime.ts";
|
|
12
|
+
import { runDurable } from "../elements/clock/durable.ts";
|
|
13
|
+
import { fail } from "./errors.ts";
|
|
12
14
|
|
|
13
15
|
function memoryJournal(store = createMemoryJournalStore()): JournalRuntime {
|
|
14
16
|
return {
|
|
@@ -125,4 +127,289 @@ describe("durable compensate", () => {
|
|
|
125
127
|
expect((await store.list())[0]!.status).toBe("failed");
|
|
126
128
|
await runs.close();
|
|
127
129
|
});
|
|
130
|
+
|
|
131
|
+
test("per-step { undo } runs LIFO and skips the failed step", async () => {
|
|
132
|
+
const store = createMemoryJournalStore();
|
|
133
|
+
const undos: string[] = [];
|
|
134
|
+
|
|
135
|
+
const pay = flow("pay.perStep", {
|
|
136
|
+
durable: true,
|
|
137
|
+
do: async (_input: { orderId: string }, fx) => {
|
|
138
|
+
await fx.step("reserve", () => "reserved", {
|
|
139
|
+
undo: () => {
|
|
140
|
+
undos.push("undo:reserve");
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await fx.step(
|
|
144
|
+
"charge",
|
|
145
|
+
() => {
|
|
146
|
+
throw new Error("card_declined");
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
undo: () => {
|
|
150
|
+
undos.push("undo:charge");
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const app = oke({
|
|
159
|
+
name: "compensate-per-step",
|
|
160
|
+
env: "test",
|
|
161
|
+
startScheduler: false,
|
|
162
|
+
gate: { unguardedHttp: "allow" },
|
|
163
|
+
elements: { journal: memoryJournal(store) },
|
|
164
|
+
}).adopt(pay);
|
|
165
|
+
await app.boot();
|
|
166
|
+
await app.execute(pay, { orderId: "o1" }, { kind: "internal" });
|
|
167
|
+
|
|
168
|
+
expect(undos).toEqual(["undo:reserve"]);
|
|
169
|
+
const stepNames = (await store.list())[0]!.entries
|
|
170
|
+
.filter((e) => e.kind === "step")
|
|
171
|
+
.map((e) => (e as { name: string }).name);
|
|
172
|
+
expect(stepNames).toEqual(["reserve", "undo:reserve"]);
|
|
173
|
+
expect((await store.list())[0]!.status).toBe("failed");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("per-step undo receives the journaled step value", async () => {
|
|
177
|
+
const store = createMemoryJournalStore();
|
|
178
|
+
let refunded: unknown;
|
|
179
|
+
|
|
180
|
+
const pay = flow("pay.value", {
|
|
181
|
+
durable: true,
|
|
182
|
+
do: async (_input, fx) => {
|
|
183
|
+
await fx.step("charge", () => ({ id: "ch_1" }), {
|
|
184
|
+
undo: (intent) => {
|
|
185
|
+
refunded = intent;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
throw new Error("after_charge");
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const app = oke({
|
|
193
|
+
name: "compensate-value",
|
|
194
|
+
env: "test",
|
|
195
|
+
startScheduler: false,
|
|
196
|
+
gate: { unguardedHttp: "allow" },
|
|
197
|
+
elements: { journal: memoryJournal(store) },
|
|
198
|
+
}).adopt(pay);
|
|
199
|
+
await app.boot();
|
|
200
|
+
await app.execute(pay, {}, { kind: "internal" });
|
|
201
|
+
expect(refunded).toEqual({ id: "ch_1" });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("auto undos then flow.compensate", async () => {
|
|
205
|
+
const store = createMemoryJournalStore();
|
|
206
|
+
const order: string[] = [];
|
|
207
|
+
|
|
208
|
+
const pay = flow("pay.both", {
|
|
209
|
+
durable: true,
|
|
210
|
+
do: async (_input, fx) => {
|
|
211
|
+
await fx.step("reserve", () => "ok", {
|
|
212
|
+
undo: () => {
|
|
213
|
+
order.push("auto:reserve");
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
throw new Error("boom");
|
|
217
|
+
},
|
|
218
|
+
compensate: async (_ctx, fx) => {
|
|
219
|
+
await fx.step("undo:extra", async () => {
|
|
220
|
+
order.push("hook:extra");
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const app = oke({
|
|
226
|
+
name: "compensate-both",
|
|
227
|
+
env: "test",
|
|
228
|
+
startScheduler: false,
|
|
229
|
+
gate: { unguardedHttp: "allow" },
|
|
230
|
+
elements: { journal: memoryJournal(store) },
|
|
231
|
+
}).adopt(pay);
|
|
232
|
+
await app.boot();
|
|
233
|
+
await app.execute(pay, {}, { kind: "internal" });
|
|
234
|
+
expect(order).toEqual(["auto:reserve", "hook:extra"]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("fx.fail triggers compensate on execute", async () => {
|
|
238
|
+
const store = createMemoryJournalStore();
|
|
239
|
+
const undos: string[] = [];
|
|
240
|
+
|
|
241
|
+
const pay = flow("pay.fail", {
|
|
242
|
+
durable: true,
|
|
243
|
+
do: async (_input, fx) => {
|
|
244
|
+
await fx.step("reserve", () => "ok", {
|
|
245
|
+
undo: () => {
|
|
246
|
+
undos.push("undo:reserve");
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return fail("Declined", {});
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const app = oke({
|
|
254
|
+
name: "compensate-fx-fail",
|
|
255
|
+
env: "test",
|
|
256
|
+
startScheduler: false,
|
|
257
|
+
gate: { unguardedHttp: "allow" },
|
|
258
|
+
elements: { journal: memoryJournal(store) },
|
|
259
|
+
}).adopt(pay);
|
|
260
|
+
await app.boot();
|
|
261
|
+
const result = await app.execute(pay, {}, { kind: "internal" });
|
|
262
|
+
expect(result.failure?.error.code).toBe("Declined");
|
|
263
|
+
expect(undos).toEqual(["undo:reserve"]);
|
|
264
|
+
expect((await store.list())[0]!.status).toBe("failed");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("runDurable: fx.fail triggers compensate", async () => {
|
|
268
|
+
const store = createMemoryJournalStore();
|
|
269
|
+
const undos: string[] = [];
|
|
270
|
+
const pay = flow("pay.durableFail", {
|
|
271
|
+
durable: true,
|
|
272
|
+
do: async (_input, fx) => {
|
|
273
|
+
await fx.step("reserve", () => "ok", {
|
|
274
|
+
undo: () => {
|
|
275
|
+
undos.push("undo:reserve");
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
return fail("Declined", {});
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const result = await runDurable({ flow: pay, journalStore: store });
|
|
283
|
+
expect(result.status).toBe("failed");
|
|
284
|
+
expect(undos).toEqual(["undo:reserve"]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("runDurable refuses resume of failed runs", async () => {
|
|
288
|
+
const store = createMemoryJournalStore();
|
|
289
|
+
let runs = 0;
|
|
290
|
+
const pay = flow("pay.terminal", {
|
|
291
|
+
durable: true,
|
|
292
|
+
do: async () => {
|
|
293
|
+
runs += 1;
|
|
294
|
+
throw new Error("boom");
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const first = await runDurable({ flow: pay, journalStore: store });
|
|
298
|
+
expect(first.status).toBe("failed");
|
|
299
|
+
expect(runs).toBe(1);
|
|
300
|
+
const second = await runDurable({
|
|
301
|
+
flow: pay,
|
|
302
|
+
journalStore: store,
|
|
303
|
+
runId: first.status === "failed" ? first.runId : "",
|
|
304
|
+
});
|
|
305
|
+
expect(second.status).toBe("failed");
|
|
306
|
+
expect(runs).toBe(1); // do must not re-enter
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("orphan mid-undo resumes compensating without forward re-entry", async () => {
|
|
310
|
+
const store = createMemoryJournalStore();
|
|
311
|
+
const forward: string[] = [];
|
|
312
|
+
const undos: string[] = [];
|
|
313
|
+
const now = () => 1_000;
|
|
314
|
+
|
|
315
|
+
const pay = flow("pay.midUndo", {
|
|
316
|
+
durable: true,
|
|
317
|
+
do: async (_input, fx) => {
|
|
318
|
+
await fx.step(
|
|
319
|
+
"reserve",
|
|
320
|
+
() => {
|
|
321
|
+
forward.push("reserve");
|
|
322
|
+
return "r";
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
undo: () => {
|
|
326
|
+
undos.push("undo:reserve");
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
await fx.step(
|
|
331
|
+
"charge",
|
|
332
|
+
() => {
|
|
333
|
+
forward.push("charge");
|
|
334
|
+
return "c";
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
undo: () => {
|
|
338
|
+
undos.push("undo:charge");
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
return true;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Seed: forwards done, first undo journaled, status compensating (crash mid-phase).
|
|
347
|
+
await store.put({
|
|
348
|
+
id: "mid-undo",
|
|
349
|
+
flow: "pay.midUndo",
|
|
350
|
+
input: {},
|
|
351
|
+
status: "compensating",
|
|
352
|
+
error: "after_steps",
|
|
353
|
+
entries: [
|
|
354
|
+
{ kind: "step", name: "reserve", value: "r", at: now() },
|
|
355
|
+
{ kind: "step", name: "charge", value: "c", at: now() },
|
|
356
|
+
{ kind: "step", name: "undo:charge", value: undefined, at: now() },
|
|
357
|
+
],
|
|
358
|
+
createdAt: now(),
|
|
359
|
+
updatedAt: now(),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const second = await runDurable({
|
|
363
|
+
flow: pay,
|
|
364
|
+
journalStore: store,
|
|
365
|
+
runId: "mid-undo",
|
|
366
|
+
now,
|
|
367
|
+
});
|
|
368
|
+
expect(second.status).toBe("failed");
|
|
369
|
+
expect(forward).toEqual([]); // registration pass only — no new forward work
|
|
370
|
+
expect(undos).toEqual(["undo:reserve"]);
|
|
371
|
+
const names = (await store.get("mid-undo"))!.entries
|
|
372
|
+
.filter((e) => e.kind === "step")
|
|
373
|
+
.map((e) => (e as { name: string }).name);
|
|
374
|
+
expect(names).toEqual(["reserve", "charge", "undo:charge", "undo:reserve"]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("flow.retry does not undo between attempts", async () => {
|
|
378
|
+
const store = createMemoryJournalStore();
|
|
379
|
+
const undos: string[] = [];
|
|
380
|
+
let attempts = 0;
|
|
381
|
+
|
|
382
|
+
const pay = flow("pay.retryUndo", {
|
|
383
|
+
durable: true,
|
|
384
|
+
retry: { retries: 2, delay: 0, jitter: false },
|
|
385
|
+
do: async (_input, fx) => {
|
|
386
|
+
await fx.step("reserve", () => "ok", {
|
|
387
|
+
undo: () => {
|
|
388
|
+
undos.push("undo:reserve");
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
attempts += 1;
|
|
392
|
+
if (attempts < 3) throw new Error("transient");
|
|
393
|
+
return true;
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const result = await runDurable({ flow: pay, journalStore: store });
|
|
398
|
+
expect(result.status).toBe("completed");
|
|
399
|
+
expect(undos).toEqual([]);
|
|
400
|
+
expect(attempts).toBe(3);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("duplicate forward step name throws", async () => {
|
|
404
|
+
const store = createMemoryJournalStore();
|
|
405
|
+
const journal = createJournal({ store });
|
|
406
|
+
const session = await journal.start("dup");
|
|
407
|
+
await session.step("a", () => 1);
|
|
408
|
+
try {
|
|
409
|
+
await session.step("a", () => 2);
|
|
410
|
+
expect.unreachable();
|
|
411
|
+
} catch (err) {
|
|
412
|
+
expect(String(err)).toMatch(/duplicate step name/);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
128
415
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable compensation phase — auto per-step undos then optional flow.compensate.
|
|
3
|
+
*
|
|
4
|
+
* Runs under the same journal session after terminal failure. Uses status
|
|
5
|
+
* `compensating` so orphan reclaim resumes undos instead of forward `do`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnyFlowDef } from "./flow.ts";
|
|
9
|
+
import type { Fx } from "./fx.ts";
|
|
10
|
+
import { isFlowFailure } from "./hooks.ts";
|
|
11
|
+
import { JOURNAL_UNDO_PREFIX, type JournalSession, type JournalStepEntry } from "./journal.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Machine-readable code for journal / Runs failure fields.
|
|
15
|
+
*
|
|
16
|
+
* @param err - Thrown value or {@link FlowFailure}
|
|
17
|
+
*/
|
|
18
|
+
export function failureCodeOf(err: unknown): string {
|
|
19
|
+
if (isFlowFailure(err)) return err.error.code;
|
|
20
|
+
if (err instanceof Error && err.name && err.name !== "Error") return err.name;
|
|
21
|
+
if (err instanceof Error && err.message) return err.message.slice(0, 120);
|
|
22
|
+
return "Error";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Forward step names only (excludes journaled `undo:*` entries). */
|
|
26
|
+
export function forwardCompletedSteps(session: JournalSession): readonly string[] {
|
|
27
|
+
return session.run.entries
|
|
28
|
+
.filter(
|
|
29
|
+
(e): e is JournalStepEntry => e.kind === "step" && !e.name.startsWith(JOURNAL_UNDO_PREFIX),
|
|
30
|
+
)
|
|
31
|
+
.map((e) => e.name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options for {@link runCompensationPhase}. */
|
|
35
|
+
export interface RunCompensationPhaseOptions {
|
|
36
|
+
/** Flow definition (optional `compensate` hook). */
|
|
37
|
+
readonly flow: AnyFlowDef;
|
|
38
|
+
/** Original flow input. */
|
|
39
|
+
readonly input: unknown;
|
|
40
|
+
/** Active journal session. */
|
|
41
|
+
readonly session: JournalSession;
|
|
42
|
+
/** Fx bound to the same journal. */
|
|
43
|
+
readonly fx: Fx;
|
|
44
|
+
/** Terminal failure that triggered compensation. */
|
|
45
|
+
readonly error: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Enter compensating status, run LIFO per-step undos, then `flow.compensate`,
|
|
50
|
+
* and commit `failed`.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Session, fx, terminal error
|
|
53
|
+
*/
|
|
54
|
+
export async function runCompensationPhase(options: RunCompensationPhaseOptions): Promise<void> {
|
|
55
|
+
const { flow, input, session, fx, error } = options;
|
|
56
|
+
const terminalCode = failureCodeOf(error);
|
|
57
|
+
|
|
58
|
+
if (session.run.status !== "compensating") {
|
|
59
|
+
await session.commit("compensating", { error: terminalCode });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
session.setUndoExecution(true);
|
|
63
|
+
try {
|
|
64
|
+
const stack = [...session.undoStack()].reverse();
|
|
65
|
+
for (const frame of stack) {
|
|
66
|
+
const undoName = `${JOURNAL_UNDO_PREFIX}${frame.name}`;
|
|
67
|
+
await session.step(undoName, () => frame.undo(frame.value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (flow.compensate) {
|
|
71
|
+
try {
|
|
72
|
+
await flow.compensate(
|
|
73
|
+
{
|
|
74
|
+
input: input as never,
|
|
75
|
+
error,
|
|
76
|
+
completedSteps: forwardCompletedSteps(session),
|
|
77
|
+
},
|
|
78
|
+
fx,
|
|
79
|
+
);
|
|
80
|
+
} catch (compErr) {
|
|
81
|
+
await session.commit("failed", {
|
|
82
|
+
error: `compensate:${failureCodeOf(compErr)}`,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (session.run.status === "compensating" || session.run.status === "running") {
|
|
89
|
+
await session.commit("failed", { error: terminalCode });
|
|
90
|
+
}
|
|
91
|
+
} catch (undoErr) {
|
|
92
|
+
if (session.run.status !== "failed") {
|
|
93
|
+
await session.commit("failed", {
|
|
94
|
+
error: `compensate:${failureCodeOf(undoErr)}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
session.setUndoExecution(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Re-bind per-step undos by re-entering `do` without new forward work.
|
|
104
|
+
*
|
|
105
|
+
* @param flow - Flow definition
|
|
106
|
+
* @param input - Original input
|
|
107
|
+
* @param fx - Fx bound to the journal
|
|
108
|
+
* @param session - Compensating session
|
|
109
|
+
*/
|
|
110
|
+
export async function rebindUndosFromDo(
|
|
111
|
+
flow: AnyFlowDef,
|
|
112
|
+
input: unknown,
|
|
113
|
+
fx: Fx,
|
|
114
|
+
session: JournalSession,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
session.rewind();
|
|
117
|
+
session.beginRegistrationPass();
|
|
118
|
+
try {
|
|
119
|
+
await flow.do(input as never, fx);
|
|
120
|
+
} catch {
|
|
121
|
+
// JournalRegistrationComplete = caught up with journaled forwards.
|
|
122
|
+
// Other throws while replaying between steps: keep whatever undos bound.
|
|
123
|
+
} finally {
|
|
124
|
+
session.endRegistrationPass();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { currentAbortSignal, isAbortError, withAbortSignal } from "./abort-scope.ts";
|
|
4
|
-
import { createMemoryJournalStore } from "./journal.ts";
|
|
4
|
+
import { createJournal, createMemoryJournalStore } from "./journal.ts";
|
|
5
5
|
import { createFx, createFxContext } from "./fx.ts";
|
|
6
6
|
import { flow } from "./flow.ts";
|
|
7
7
|
import { runDurable } from "../elements/clock/durable.ts";
|
|
@@ -216,12 +216,11 @@ describe("retry + durable journal", () => {
|
|
|
216
216
|
test("fx.retry inside fx.step does not re-run completed step on resume", async () => {
|
|
217
217
|
const journalStore = createMemoryJournalStore();
|
|
218
218
|
let attempts = 0;
|
|
219
|
-
let crash = true;
|
|
220
219
|
|
|
221
220
|
const f = flow("pay.retry", {
|
|
222
221
|
durable: true,
|
|
223
222
|
do: async (_input, fx) => {
|
|
224
|
-
|
|
223
|
+
return fx.step("charge", () =>
|
|
225
224
|
fx.retry(
|
|
226
225
|
async () => {
|
|
227
226
|
attempts += 1;
|
|
@@ -230,20 +229,22 @@ describe("retry + durable journal", () => {
|
|
|
230
229
|
{ retries: 2, delay: 0 },
|
|
231
230
|
),
|
|
232
231
|
);
|
|
233
|
-
if (crash) throw new Error("KILLED");
|
|
234
|
-
return charged;
|
|
235
232
|
},
|
|
236
233
|
});
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
// Simulate crash after the journaled step: leave status `running`.
|
|
236
|
+
const journal = createJournal({ store: journalStore });
|
|
237
|
+
const crashed = await journal.start("pay.retry");
|
|
238
|
+
await crashed.step("charge", () => {
|
|
239
|
+
attempts += 1;
|
|
240
|
+
return { id: "ch_1" };
|
|
241
|
+
});
|
|
240
242
|
expect(attempts).toBe(1);
|
|
241
243
|
|
|
242
|
-
crash = false;
|
|
243
244
|
const second = await runDurable({
|
|
244
245
|
flow: f,
|
|
245
246
|
journalStore,
|
|
246
|
-
runId:
|
|
247
|
+
runId: crashed.runId,
|
|
247
248
|
});
|
|
248
249
|
expect(second.status).toBe("completed");
|
|
249
250
|
expect(attempts).toBe(1);
|
|
@@ -91,8 +91,22 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
|
|
|
91
91
|
const create = buildUnannotatedCreateFlow(db);
|
|
92
92
|
const app = oke({ name: "stamp-docker", gate: { policies: [gate.public] } }).adopt({ create });
|
|
93
93
|
|
|
94
|
+
const memoryDrivers = {
|
|
95
|
+
store: {
|
|
96
|
+
sql: { dev: "memory", prod: "memory" },
|
|
97
|
+
kv: { dev: "memory", prod: "memory" },
|
|
98
|
+
},
|
|
99
|
+
channel: { email: { dev: "console", prod: "console" } },
|
|
100
|
+
} as const;
|
|
94
101
|
await expect(
|
|
95
|
-
app.boot({
|
|
102
|
+
app.boot({
|
|
103
|
+
env: "dev",
|
|
104
|
+
docker: true,
|
|
105
|
+
stores: [db],
|
|
106
|
+
unguardedHttp: "allow",
|
|
107
|
+
startScheduler: false,
|
|
108
|
+
config: { drivers: memoryDrivers },
|
|
109
|
+
}),
|
|
96
110
|
).rejects.toThrow(/OKE1008/);
|
|
97
111
|
});
|
|
98
112
|
|
|
@@ -104,7 +118,18 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
|
|
|
104
118
|
const app = oke({ name: "stamp-prod", gate: { policies: [gate.public] } }).adopt({ create });
|
|
105
119
|
|
|
106
120
|
await expect(
|
|
107
|
-
app.boot({
|
|
121
|
+
app.boot({
|
|
122
|
+
env: "prod",
|
|
123
|
+
stores: [db],
|
|
124
|
+
unguardedHttp: "allow",
|
|
125
|
+
startScheduler: false,
|
|
126
|
+
config: {
|
|
127
|
+
drivers: {
|
|
128
|
+
store: { sql: { prod: "memory" }, kv: { prod: "memory" } },
|
|
129
|
+
channel: { email: { prod: "console" } },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
108
133
|
).rejects.toThrow(/OKE1008/);
|
|
109
134
|
});
|
|
110
135
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
|
|
3
|
-
* `signal()`, and `channel.<medium>().template()` — the
|
|
4
|
-
* each declare module's `listX()` / `resetX()` pair
|
|
5
|
-
* trigger-drain `bindings` array).
|
|
3
|
+
* `vault.env.required`, `signal()`, and `channel.<medium>().template()` — the
|
|
4
|
+
* plain arrays behind each declare module's `listX()` / `resetX()` pair
|
|
5
|
+
* (mirrors `on.ts`'s trigger-drain `bindings` array).
|
|
6
6
|
*
|
|
7
7
|
* Lives here, not in the declare modules themselves, so {@link oke} can read
|
|
8
8
|
* every registry with one lightweight import instead of statically pulling
|
|
@@ -21,6 +21,8 @@ import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
|
|
|
21
21
|
export const storeRegistry: StoreDecl[] = [];
|
|
22
22
|
/** `vault.secret` declarations since the last reset. */
|
|
23
23
|
export const secretRegistry: VaultSecretDecl[] = [];
|
|
24
|
+
/** `vault.env.required(name)` environment variables since the last reset. */
|
|
25
|
+
export const requiredEnvRegistry: string[] = [];
|
|
24
26
|
/** `signal()` declarations since the last reset. */
|
|
25
27
|
export const signalRegistry: SignalDecl[] = [];
|
|
26
28
|
/** Medium-binder `.template()` declarations since the last reset. */
|
package/src/kernel/flow.ts
CHANGED
|
@@ -71,8 +71,10 @@ export interface FlowOptions<I = unknown, O = unknown, E extends FlowErrorMap =
|
|
|
71
71
|
readonly breaking?: boolean;
|
|
72
72
|
/**
|
|
73
73
|
* Compensation phase after terminal failure on a durable flow.
|
|
74
|
-
* Runs under the same journal session
|
|
75
|
-
*
|
|
74
|
+
* Runs under the same journal session after auto per-step `{ undo }`
|
|
75
|
+
* handlers and before `commit("failed")`. Prefer `fx.step(name, fn, { undo })`
|
|
76
|
+
* for step-local reverse work; use this hook for cross-cutting cleanup.
|
|
77
|
+
* Manual bodies must use distinct `fx.step("undo:…")` names — never reuse
|
|
76
78
|
* forward step names. Never called on retryable attempts or sleep park.
|
|
77
79
|
*/
|
|
78
80
|
readonly compensate?: (
|