From 2ae642b6b57455bcf2091a3b8e9ba68bdc35b5d1 Mon Sep 17 00:00:00 2001 From: Pranay Prakash Date: Sat, 20 Jun 2026 14:55:35 -0700 Subject: [PATCH] feat(nitro): auto-start the World at server boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-introduce the dropped auto startup plugin so self-hosted Nitro apps (Nitro v2/v3, Nuxt, Express/Hono/Fastify on Nitro) recover in-flight runs after a restart with no manual wiring. The previously-reverted version imported the runtime via a build-time `file://` URL, which collided with the bundled flow handler's copy of the same file (CJS/ESM dual-load -> ERR_INTERNAL_ASSERTION, 500ing the flow route). This version instead emits a real plugin file in the build dir that imports `workflow/runtime` via a *bare* dynamic import — mirroring a hand-written Nitro plugin — so the bundler resolves and dedupes it with the flow handler's runtime. `ensureWorldStarted()` caches its start promise on `globalThis`, so the World starts exactly once. Gated off Vercel deploys (the Vercel World's start() is a no-op). Removes the manual `start-pg-world.ts` workbench workaround and updates the recovering-in-flight-runs docs to note Nitro starts the World automatically. Co-Authored-By: Claude Opus 4.8 --- .changeset/nitro-auto-world-start.md | 5 ++ .../deploying/recovering-in-flight-runs.mdx | 11 +---- .../deploying/recovering-in-flight-runs.mdx | 11 +---- packages/nitro/src/index.ts | 48 +++++++++++++++++++ workbench/nitro-v3/nitro.config.ts | 1 - workbench/nitro-v3/plugins/start-pg-world.ts | 15 ------ 6 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 .changeset/nitro-auto-world-start.md delete mode 100644 workbench/nitro-v3/plugins/start-pg-world.ts diff --git a/.changeset/nitro-auto-world-start.md b/.changeset/nitro-auto-world-start.md new file mode 100644 index 0000000000..0cc0fd12d4 --- /dev/null +++ b/.changeset/nitro-auto-world-start.md @@ -0,0 +1,5 @@ +--- +'@workflow/nitro': minor +--- + +Start the workflow World automatically at server boot via a generated Nitro plugin, so self-hosted Nitro apps (Nitro v2/v3, Nuxt, Express/Hono/Fastify on Nitro) recover in-flight runs after a restart with no manual wiring. Skipped on Vercel deploys. diff --git a/docs/content/docs/v4/deploying/recovering-in-flight-runs.mdx b/docs/content/docs/v4/deploying/recovering-in-flight-runs.mdx index 02c88f5c68..2ef555aea7 100644 --- a/docs/content/docs/v4/deploying/recovering-in-flight-runs.mdx +++ b/docs/content/docs/v4/deploying/recovering-in-flight-runs.mdx @@ -46,16 +46,7 @@ export async function register() { ### Nitro, Nuxt, Express, Hono, Fastify (Nitro) -Add a [Nitro server plugin](https://nitro.build/guide/plugins) (Nuxt: `server/plugins/`) that starts the World at boot: - -```ts title="server/plugins/workflow.ts" -import { defineNitroPlugin } from 'nitro/~internal/runtime/plugin'; -import { ensureWorldStarted } from 'workflow/runtime'; - -export default defineNitroPlugin(() => { - void ensureWorldStarted(); -}); -``` +No action required — the `@workflow/nitro` integration registers a Nitro server plugin that starts the World at boot for you. (Not on Vercel deploys, where the push-based Vercel World needs no boot recovery.) ### SvelteKit diff --git a/docs/content/docs/v5/deploying/recovering-in-flight-runs.mdx b/docs/content/docs/v5/deploying/recovering-in-flight-runs.mdx index 02c88f5c68..2ef555aea7 100644 --- a/docs/content/docs/v5/deploying/recovering-in-flight-runs.mdx +++ b/docs/content/docs/v5/deploying/recovering-in-flight-runs.mdx @@ -46,16 +46,7 @@ export async function register() { ### Nitro, Nuxt, Express, Hono, Fastify (Nitro) -Add a [Nitro server plugin](https://nitro.build/guide/plugins) (Nuxt: `server/plugins/`) that starts the World at boot: - -```ts title="server/plugins/workflow.ts" -import { defineNitroPlugin } from 'nitro/~internal/runtime/plugin'; -import { ensureWorldStarted } from 'workflow/runtime'; - -export default defineNitroPlugin(() => { - void ensureWorldStarted(); -}); -``` +No action required — the `@workflow/nitro` integration registers a Nitro server plugin that starts the World at boot for you. (Not on Vercel deploys, where the push-based Vercel World needs no boot recovery.) ### SvelteKit diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index aff5c9f445..6f46124c4d 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -229,6 +229,14 @@ export default { 'workflow/workflows.mjs' ); + // Start the World once at server boot (Nitro server plugin) so in-flight + // runs recover after a restart without needing a workflow operation. + // Covers self-hosted Nitro apps (Nitro v2/v3, Nuxt). Skipped on Vercel: + // the Vercel World's start() is a no-op (push-based — VQS redelivers). + if (!isVercelDeploy) { + addStartupPlugin(nitro); + } + // Nitro v3+ Vercel deploy: configure function rules for the combined // flow handler so it gets the queue triggers + max duration that the // workflow runtime needs. Workflow-required fields (`maxDuration`, @@ -292,6 +300,46 @@ export default { }, } satisfies NitroModule; +/** + * Auto-register a Nitro server plugin that starts the World once at app boot, + * so boot-time recovery (`reenqueueActiveRuns` for queue-backed self-hosted + * Worlds) runs after a restart without requiring a workflow operation to wake + * the process. + * + * The plugin is emitted as a real file in the build dir and imports + * `workflow/runtime` via a *bare* dynamic import (resolved by the bundler) — + * mirroring a hand-written Nitro plugin. This shares the same runtime module + * the flow handler loads, avoiding the CJS/ESM dual-load that a build-time + * `file://` import would trigger against the bundled flow handler. + * `ensureWorldStarted()` caches its start promise on `globalThis`, so the World + * is started exactly once even though the flow handler also reaches the runtime. + * + * Not registered for Vercel deploys (the Vercel World's start() is a no-op, and + * there is nothing to recover at boot for a push-based world). + */ +function addStartupPlugin(nitro: Nitro) { + const dir = join(nitro.options.buildDir, 'workflow'); + mkdirSync(dir, { recursive: true }); + const pluginPath = join(dir, 'start-world-plugin.mjs'); + writeFileSync( + pluginPath, + /* js */ `// Auto-generated by @workflow/nitro — starts the workflow World at server boot. +export default () => { + import('workflow/runtime') + .then(({ ensureWorldStarted }) => ensureWorldStarted()) + .catch((error) => { + console.error('[workflow] Failed to start World on server startup:', error); + }); +}; +` + ); + + nitro.options.plugins ||= []; + if (!nitro.options.plugins.includes(pluginPath)) { + nitro.options.plugins.push(pluginPath); + } +} + const DASHBOARD_VIRTUAL_ID = '#workflow/dashboard-handler'; function addDashboardHandler(nitro: Nitro) { diff --git a/workbench/nitro-v3/nitro.config.ts b/workbench/nitro-v3/nitro.config.ts index 946bbbb53c..5e800f3a10 100644 --- a/workbench/nitro-v3/nitro.config.ts +++ b/workbench/nitro-v3/nitro.config.ts @@ -3,5 +3,4 @@ import { defineConfig } from 'nitro'; export default defineConfig({ modules: ['workflow/nitro'], serverDir: './', - plugins: ['plugins/start-pg-world.ts'], }); diff --git a/workbench/nitro-v3/plugins/start-pg-world.ts b/workbench/nitro-v3/plugins/start-pg-world.ts deleted file mode 100644 index 88f5ef3b7c..0000000000 --- a/workbench/nitro-v3/plugins/start-pg-world.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { definePlugin } from 'nitro'; - -// Start the Postgres World -// Needed since we test this in CI -export default definePlugin(async () => { - if (process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres') { - import('workflow/runtime').then(async ({ getWorld }) => { - const world = await getWorld(); - if (world.start) { - console.log('Starting World workers...'); - await world.start(); - } - }); - } -});