okengine 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/constants.ts +18 -0
- 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.ts +40 -42
- 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 +12 -4
- 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 +58 -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/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
package/src/kernel/boot.test.ts
CHANGED
|
@@ -130,8 +130,9 @@ describe("boot — lazy element needs", () => {
|
|
|
130
130
|
// + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
|
|
131
131
|
// check, OKE1009) — part of the already-lazily-loaded boot graph, not
|
|
132
132
|
// the `oke()` construction path, but `Bun.build` still traces and
|
|
133
|
-
// sums the dynamic-import chunk (~54.3 kB → 54.7 kB
|
|
134
|
-
|
|
133
|
+
// sums the dynamic-import chunk (~54.3 kB → 54.7 kB → 55.0 kB after
|
|
134
|
+
// ConfigEnv local/docker → dev/test/prod + sqlite removal).
|
|
135
|
+
expect(total).toBeLessThan(55_000);
|
|
135
136
|
} finally {
|
|
136
137
|
await rm(dir, { recursive: true, force: true });
|
|
137
138
|
}
|
|
@@ -273,11 +274,18 @@ describe("boot — cron autostart via real timer loop", () => {
|
|
|
273
274
|
}),
|
|
274
275
|
);
|
|
275
276
|
|
|
276
|
-
// env
|
|
277
|
+
// Non-test env → startScheduler defaults ON. No createTestApp / advance.
|
|
277
278
|
const app = oke({
|
|
278
279
|
name: "cron-autostart",
|
|
279
|
-
env: "
|
|
280
|
+
env: "dev",
|
|
280
281
|
schedulerIntervalMs: 1000,
|
|
282
|
+
config: {
|
|
283
|
+
drivers: {
|
|
284
|
+
clock: { dev: "memory" },
|
|
285
|
+
store: { sql: { dev: "memory" }, kv: { dev: "memory" } },
|
|
286
|
+
channel: { email: { dev: "console" } },
|
|
287
|
+
},
|
|
288
|
+
},
|
|
281
289
|
});
|
|
282
290
|
await app.boot();
|
|
283
291
|
|
package/src/kernel/boot.ts
CHANGED
|
@@ -216,7 +216,9 @@ export interface ElementNeeds {
|
|
|
216
216
|
export function resolveElementNeeds(options: BootOptions): ElementNeeds {
|
|
217
217
|
const pre = options.elements ?? {};
|
|
218
218
|
let vault =
|
|
219
|
-
pre.vault !== undefined ||
|
|
219
|
+
pre.vault !== undefined ||
|
|
220
|
+
(options.vault?.secrets ?? options.secrets ?? []).length > 0 ||
|
|
221
|
+
(options.vault?.requiredEnv?.length ?? 0) > 0;
|
|
220
222
|
let store = pre.store !== undefined || (options.stores?.length ?? 0) > 0;
|
|
221
223
|
let signal = pre.signal !== undefined || (options.signals?.length ?? 0) > 0;
|
|
222
224
|
let clock = pre.clock !== undefined || (options.clocks?.length ?? 0) > 0;
|
|
@@ -283,9 +285,10 @@ async function loadBind<T>(name: string): Promise<T> {
|
|
|
283
285
|
export async function bootApplication(input: BootOptions = {}): Promise<BootResult> {
|
|
284
286
|
const docker =
|
|
285
287
|
input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
const env: ConfigEnv =
|
|
288
|
+
// Three targets: `prod` when NODE_ENV=production; `dev` when compose infra
|
|
289
|
+
// is up (`oke dev` / `OKE_DOCKER=1`); otherwise `test` (PGLite / memory).
|
|
290
|
+
const env: ConfigEnv =
|
|
291
|
+
input.env ?? (process.env.NODE_ENV === "production" ? "prod" : docker ? "dev" : "test");
|
|
289
292
|
let config = input.config;
|
|
290
293
|
if (config === undefined) {
|
|
291
294
|
try {
|
|
@@ -482,12 +485,13 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
482
485
|
const capabilities = await mintCapabilities(options.flows ?? [], env, {
|
|
483
486
|
manifest: options.manifest,
|
|
484
487
|
rootDir: options.rootDir,
|
|
488
|
+
docker,
|
|
485
489
|
});
|
|
486
490
|
|
|
487
491
|
// 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
|
|
488
492
|
// capability minting above; never a filesystem read the kernel performs
|
|
489
493
|
// on its own).
|
|
490
|
-
await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir);
|
|
494
|
+
await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir, docker);
|
|
491
495
|
|
|
492
496
|
return {
|
|
493
497
|
vault,
|
|
@@ -556,10 +560,9 @@ async function tryAutoExtractManifest(rootDir: string): Promise<Manifest | undef
|
|
|
556
560
|
* an explicit {@link BootOptions.manifest}, or a lazy AoT extract from
|
|
557
561
|
* {@link BootOptions.rootDir} / `OKE_ROOT_DIR`.
|
|
558
562
|
*
|
|
559
|
-
* When neither is available: `
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* silent open door in a deploy-shaped environment.
|
|
563
|
+
* When neither is available: `test` stays open with a once-per-process
|
|
564
|
+
* `oke boot:` warning; `dev` with compose infra and `prod` fail loud
|
|
565
|
+
* (`OKE1008`) — never a silent open door in a deploy-shaped environment.
|
|
563
566
|
*
|
|
564
567
|
* @param flows - Adopted flows
|
|
565
568
|
* @param env - Resolved {@link ConfigEnv}
|
|
@@ -567,8 +570,12 @@ async function tryAutoExtractManifest(rootDir: string): Promise<Manifest | undef
|
|
|
567
570
|
*/
|
|
568
571
|
export async function mintCapabilities(
|
|
569
572
|
flows: readonly AnyFlowDef[],
|
|
570
|
-
env: ConfigEnv = "
|
|
571
|
-
options: {
|
|
573
|
+
env: ConfigEnv = "test",
|
|
574
|
+
options: {
|
|
575
|
+
readonly manifest?: Manifest;
|
|
576
|
+
readonly rootDir?: string;
|
|
577
|
+
readonly docker?: boolean;
|
|
578
|
+
} = {},
|
|
572
579
|
): Promise<Map<string, CapabilityToken>> {
|
|
573
580
|
const map = new Map<string, CapabilityToken>();
|
|
574
581
|
|
|
@@ -591,7 +598,8 @@ export async function mintCapabilities(
|
|
|
591
598
|
continue;
|
|
592
599
|
}
|
|
593
600
|
|
|
594
|
-
|
|
601
|
+
const strict = env === "prod" || (env === "dev" && options.docker === true);
|
|
602
|
+
if (strict) {
|
|
595
603
|
throwOke("NO_EFFECTS_DECLARED", { flow: f.name });
|
|
596
604
|
}
|
|
597
605
|
if (!noEffectsWarned) {
|
|
@@ -600,7 +608,7 @@ export async function mintCapabilities(
|
|
|
600
608
|
`oke boot: flow "${f.name}" (and possibly others) has no declared effects and no ` +
|
|
601
609
|
"Manifest-derived effects — running with an OPEN capability token (every access " +
|
|
602
610
|
"allowed, ledgered but not gated). Run `oke build`, or boot with `manifest` / " +
|
|
603
|
-
"`rootDir`, before deploying —
|
|
611
|
+
"`rootDir`, before deploying — dev+compose/prod refuse to boot this way.",
|
|
604
612
|
);
|
|
605
613
|
}
|
|
606
614
|
map.set(f.name, createCapabilityToken(f.name, undefined));
|
|
@@ -645,18 +653,21 @@ async function tryListFlowsUnits(rootDir: string): Promise<readonly string[] | u
|
|
|
645
653
|
*
|
|
646
654
|
* Opt-in only via {@link BootOptions.rootDir} / `OKE_ROOT_DIR` (same gate as
|
|
647
655
|
* {@link mintCapabilities}'s Manifest fallback) — never a filesystem read
|
|
648
|
-
* the kernel performs on its own. `
|
|
649
|
-
* `
|
|
650
|
-
* never a silently-incomplete route table in a
|
|
656
|
+
* the kernel performs on its own. `test` warns once per process; `prod`
|
|
657
|
+
* (and `dev` with compose) fail loud (`OKE1009`) — same class as
|
|
658
|
+
* `NO_EFFECTS_DECLARED`, never a silently-incomplete route table in a
|
|
659
|
+
* deploy-shaped environment.
|
|
651
660
|
*
|
|
652
661
|
* @param flows - Adopted flows
|
|
653
662
|
* @param env - Resolved {@link ConfigEnv}
|
|
654
663
|
* @param rootDir - Explicit project root (falls back to `OKE_ROOT_DIR`)
|
|
664
|
+
* @param docker - Compose infra active (`oke dev`)
|
|
655
665
|
*/
|
|
656
666
|
export async function assertAdoptBarrelFresh(
|
|
657
667
|
flows: readonly AnyFlowDef[],
|
|
658
|
-
env: ConfigEnv = "
|
|
668
|
+
env: ConfigEnv = "test",
|
|
659
669
|
rootDir?: string,
|
|
670
|
+
docker = false,
|
|
660
671
|
): Promise<void> {
|
|
661
672
|
const dir = rootDir ?? process.env["OKE_ROOT_DIR"];
|
|
662
673
|
if (!dir) return;
|
|
@@ -670,7 +681,8 @@ export async function assertAdoptBarrelFresh(
|
|
|
670
681
|
const missing = diskUnits.filter((u) => !adoptedUnits.has(u));
|
|
671
682
|
if (missing.length === 0) return;
|
|
672
683
|
|
|
673
|
-
|
|
684
|
+
const strict = env === "prod" || (env === "dev" && docker);
|
|
685
|
+
if (strict) {
|
|
674
686
|
throwOke("ADOPT_BARREL_STALE", { unit: missing[0]! });
|
|
675
687
|
}
|
|
676
688
|
if (!staleAdoptBarrelWarned) {
|
|
@@ -678,7 +690,7 @@ export async function assertAdoptBarrelFresh(
|
|
|
678
690
|
emitBootWarn(
|
|
679
691
|
`oke boot: src/flows/${missing[0]} exists on disk but adopted no flows — the ` +
|
|
680
692
|
'.adopt() barrel ("src/flows/generated.ts") is stale. Run `oke dev` or `oke build` ' +
|
|
681
|
-
"to regenerate it —
|
|
693
|
+
"to regenerate it — dev+compose/prod refuse to boot this way.",
|
|
682
694
|
);
|
|
683
695
|
}
|
|
684
696
|
}
|
|
@@ -14,7 +14,7 @@ import { flow } from "./flow.ts";
|
|
|
14
14
|
import { createFx } from "./fx.ts";
|
|
15
15
|
import { mergeHooks, runPipeline } from "./hooks.ts";
|
|
16
16
|
import { on, resetBindings } from "./on.ts";
|
|
17
|
-
import { createRouter } from "./router.ts";
|
|
17
|
+
import { createEdgeRouter as createRouter } from "./router/create-edge.ts";
|
|
18
18
|
import { http } from "./triggers.ts";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -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);
|