From 1404ff275dcfc6c1392411895c4e085d2d274faf Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Thu, 18 Jun 2026 10:06:40 +0300 Subject: [PATCH 1/2] feat: eager-start QStash dev server on serve() across all adapters - call startDevServer() in all 10 platform adapters - also in nextjs servePagesRouter and express serveMany - derive devMode onto the serve-side Client in multi-region handlers - add dev-server-eager-start unit tests; update multi-region + integration tests - pin @upstash/qstash to CI snapshot for integration testing --- bun.lock | 4 +- package.json | 2 +- platforms/astro.ts | 3 + platforms/cloudflare.ts | 3 + platforms/express.ts | 7 ++ platforms/h3.ts | 3 + platforms/hono.ts | 3 + platforms/nextjs.ts | 5 + platforms/react-router.ts | 3 + platforms/solidjs.ts | 3 + platforms/svelte.ts | 3 + platforms/tanstack.ts | 3 + src/serve/dev-server-eager-start.test.ts | 110 ++++++++++++++++++ .../dev-server-integration.test.ts | 29 ++++- src/serve/multi-region/handlers.ts | 7 ++ src/serve/multi-region/multi-region.test.ts | 45 +++++++ 16 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 src/serve/dev-server-eager-start.test.ts diff --git a/bun.lock b/bun.lock index a35dc2b4..12433197 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@upstash/workflow", "dependencies": { - "@upstash/qstash": "^2.11.0", + "@upstash/qstash": "0.0.0-ci.8c658c13532090cf00f778d2216eacbad68f85f9-20260618065901", }, "devDependencies": { "@commitlint/cli": "^19.5.0", @@ -521,7 +521,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@upstash/qstash": ["@upstash/qstash@2.11.0", "", { "dependencies": { "crypto-js": ">=4.2.0", "jose": "^5.2.3", "neverthrow": "^7.0.1" } }, "sha512-AfPPxsUeOJCrxMQ9dkh1RZL40wgxCsUNFkxrbBSomC3U4j4qKFRawU8sDK+dqqH+sZFUS1biVM4GK46d5Tg2Vg=="], + "@upstash/qstash": ["@upstash/qstash@0.0.0-ci.8c658c13532090cf00f778d2216eacbad68f85f9-20260618065901", "", { "dependencies": { "crypto-js": ">=4.2.0", "jose": "^5.2.3", "neverthrow": "^7.0.1" } }, "sha512-Tezvd3lUcZzbf5PaUCxuCTFZTw+t6Zb2xjuZsx5WzhRCvlJ+FZdc79tNTnye4HUbMjbPMiS2c589kzd10eRmXg=="], "@vercel/nft": ["@vercel/nft@1.5.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA=="], diff --git a/package.json b/package.json index 8d39ec19..0bfd089c 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "typescript-eslint": "^8.18.0" }, "dependencies": { - "@upstash/qstash": "^2.11.0" + "@upstash/qstash": "0.0.0-ci.8c658c13532090cf00f778d2216eacbad68f85f9-20260618065901" }, "directories": { "example": "examples" diff --git a/platforms/astro.ts b/platforms/astro.ts index 51708adf..37e84c6b 100644 --- a/platforms/astro.ts +++ b/platforms/astro.ts @@ -4,6 +4,7 @@ import { InvokableWorkflow, WorkflowServeOptions, Telemetry, WorkflowContext } f import { serveBase } from "../src/serve"; import { SDK_TELEMETRY } from "../src/constants"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const telemetry: Telemetry = { sdk: SDK_TELEMETRY, @@ -20,6 +21,8 @@ export function serve( ) => Promise, options?: WorkflowServeOptions ) { + void startDevServer(); + const POST: APIRoute = (apiContext) => { const { handler } = serveBase( (workflowContext) => routeFunction(workflowContext, apiContext), diff --git a/platforms/cloudflare.ts b/platforms/cloudflare.ts index 5f5d7dd5..9b1786f6 100644 --- a/platforms/cloudflare.ts +++ b/platforms/cloudflare.ts @@ -2,6 +2,7 @@ import type { InvokableWorkflow, WorkflowServeOptions, RouteFunction, Telemetry import { SDK_TELEMETRY } from "../src/constants"; import { serveBase } from "../src/serve"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; export type WorkflowBindings = { QSTASH_TOKEN: string; @@ -69,6 +70,8 @@ export const serve = ( ): { fetch: (...args: PagesHandlerArgs | WorkersHandlerArgs) => Promise; } => { + void startDevServer(); + const fetch = async (...args: PagesHandlerArgs | WorkersHandlerArgs) => { const { request, env } = getArgs(args); const { handler: serveHandler } = serveBase(routeFunction, telemetry, { diff --git a/platforms/express.ts b/platforms/express.ts index ebc60c92..917cb784 100644 --- a/platforms/express.ts +++ b/platforms/express.ts @@ -8,6 +8,7 @@ import { RequestHandler, } from "express"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const isEmptyRequest = (req: ExpressRequest) => { return ( @@ -83,6 +84,8 @@ export function serve( routeFunction: RouteFunction, options?: WorkflowServeOptions ): Router { + void startDevServer(); + const router = Router(); const handler: RequestHandler = createExpressHandler([routeFunction, options]); @@ -108,6 +111,10 @@ export const serveMany = ( workflows: Parameters[0]["workflows"], options?: Parameters[0]["options"] ) => { + // serveMany's serveMethod uses createExpressHandler (not serve), so start the + // dev server here too. No-op unless QSTASH_DEV is set. + void startDevServer(); + const router = Router(); const { handler } = serveManyBase>({ diff --git a/platforms/h3.ts b/platforms/h3.ts index a778619a..11a851f3 100644 --- a/platforms/h3.ts +++ b/platforms/h3.ts @@ -5,6 +5,7 @@ import { serveBase } from "../src/serve"; import type { IncomingHttpHeaders } from "node:http"; import { SDK_TELEMETRY } from "../src/constants"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; function transformHeaders(headers: IncomingHttpHeaders): [string, string][] { const formattedHeaders = Object.entries(headers).map(([key, value]) => [ @@ -34,6 +35,8 @@ export const serve = ( routeFunction: RouteFunction, options?: WorkflowServeOptions ) => { + void startDevServer(); + const handler = defineEventHandler(async (event) => { const method = event.node.req.method; if (method?.toUpperCase() !== "POST") { diff --git a/platforms/hono.ts b/platforms/hono.ts index fd73abae..7ccabe64 100644 --- a/platforms/hono.ts +++ b/platforms/hono.ts @@ -4,6 +4,7 @@ import { serveBase } from "../src/serve"; import { Variables } from "hono/types"; import { SDK_TELEMETRY } from "../src/constants"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; export type WorkflowBindings = { QSTASH_TOKEN: string; @@ -36,6 +37,8 @@ export const serve = < routeFunction: RouteFunction, options?: WorkflowServeOptions ): ((context: Context<{ Bindings: TBindings; Variables: TVariables }>) => Promise) => { + void startDevServer(); + const handler = async (context: Context<{ Bindings: TBindings; Variables: TVariables }>) => { const environment = context.env ? context.env diff --git a/platforms/nextjs.ts b/platforms/nextjs.ts index c266642c..9f3f8dcb 100644 --- a/platforms/nextjs.ts +++ b/platforms/nextjs.ts @@ -6,6 +6,7 @@ import type { RouteFunction, WorkflowServeOptions, Telemetry, InvokableWorkflow import { serveBase } from "../src/serve"; import { SDK_TELEMETRY } from "../src/constants"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const appTelemetry: Telemetry = { sdk: SDK_TELEMETRY, @@ -34,6 +35,8 @@ export const serve = ( routeFunction: RouteFunction, options?: WorkflowServeOptions ) => { + void startDevServer(); + const { handler: serveHandler } = serveBase( routeFunction, appTelemetry, @@ -80,6 +83,8 @@ export const servePagesRouter = ( ): { handler: NextApiHandler; } => { + void startDevServer(); + const { handler: serveHandler } = serveBase(routeFunction, pagesTelemetry, options, undefined); const handler = async (request_: NextApiRequest, res: NextApiResponse) => { diff --git a/platforms/react-router.ts b/platforms/react-router.ts index fed81e45..6372882b 100644 --- a/platforms/react-router.ts +++ b/platforms/react-router.ts @@ -2,6 +2,7 @@ import type { WorkflowServeOptions, RouteFunction, Telemetry, InvokableWorkflow import { SDK_TELEMETRY } from "../src/constants"; import { serveBase } from "../src/serve"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const telemetry: Telemetry = { sdk: SDK_TELEMETRY, @@ -39,6 +40,8 @@ export const serve = ( routeFunction: RouteFunction, options?: WorkflowServeOptions ) => { + void startDevServer(); + const { handler: serveHandler } = serveBase( routeFunction, telemetry, diff --git a/platforms/solidjs.ts b/platforms/solidjs.ts index 46832a63..6a98eb26 100644 --- a/platforms/solidjs.ts +++ b/platforms/solidjs.ts @@ -3,6 +3,7 @@ import type { APIEvent } from "@solidjs/start/server"; import type { WorkflowServeOptions, RouteFunction, Telemetry } from "../src"; import { serveBase } from "../src/serve"; import { SDK_TELEMETRY } from "../src/constants"; +import { startDevServer } from "@upstash/qstash"; /** * Serve method to serve a Upstash Workflow in a SolidJS project @@ -17,6 +18,8 @@ export const serve = ( routeFunction: RouteFunction, options?: WorkflowServeOptions ) => { + void startDevServer(); + const telemetry: Telemetry = { sdk: SDK_TELEMETRY, framework: "solidjs", diff --git a/platforms/svelte.ts b/platforms/svelte.ts index 2ab464c2..0d2afbb1 100644 --- a/platforms/svelte.ts +++ b/platforms/svelte.ts @@ -4,6 +4,7 @@ import type { InvokableWorkflow, WorkflowServeOptions, RouteFunction, Telemetry import { serveBase } from "../src/serve"; import { SDK_TELEMETRY } from "../src/constants"; import { OmitOptionsInServeMany, serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const telemetry: Telemetry = { sdk: SDK_TELEMETRY, @@ -31,6 +32,8 @@ export const serve = ( ): { POST: RequestHandler; } => { + void startDevServer(); + const handler: RequestHandler = async ({ request }) => { const { handler: serveHandler } = serveBase( routeFunction, diff --git a/platforms/tanstack.ts b/platforms/tanstack.ts index a95fd81a..96a7b6d9 100644 --- a/platforms/tanstack.ts +++ b/platforms/tanstack.ts @@ -8,6 +8,7 @@ import type { import { serveBase } from "../src/serve"; import { SDK_TELEMETRY } from "../src/constants"; import { serveManyBase } from "../src/serve/serve-many"; +import { startDevServer } from "@upstash/qstash"; const telemetry: Telemetry = { sdk: SDK_TELEMETRY, @@ -32,6 +33,8 @@ export function serve( > & ExclusiveValidationOptions ) { + void startDevServer(); + const POST = (tanstackContext: { request: Request }) => { // Create a Next.js compatible handler that passes the route context const { handler } = serveBase( diff --git a/src/serve/dev-server-eager-start.test.ts b/src/serve/dev-server-eager-start.test.ts new file mode 100644 index 00000000..ed554f17 --- /dev/null +++ b/src/serve/dev-server-eager-start.test.ts @@ -0,0 +1,110 @@ +/** + * Pins the eager-start regression (DX-2705): defining a workflow route by + * calling an adapter's `serve()` / `serveMany()` factory must start the QStash + * dev server *synchronously at factory-call time* — NOT deferred to the first + * inbound request, which is how every lazy adapter behaved before this change. + * + * We assert at the `@upstash/qstash` boundary by mocking `startDevServer`, the + * single call each adapter makes. (Prior art for the "drive the public entry, + * assert at the qstash boundary" approach: multi-region/multi-region.test.ts.) + */ +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import * as realQstash from "@upstash/qstash"; + +const startDevServer = mock(() => Promise.resolve()); + +// Keep the real Client/Receiver/etc.; only intercept startDevServer. Registered +// before the adapters are imported (below) so they pick up the mocked export. +void mock.module("@upstash/qstash", () => ({ + ...realQstash, + startDevServer, +})); + +// Import the adapters dynamically AFTER the mock is registered. +const hono = await import("../../platforms/hono"); +const cloudflare = await import("../../platforms/cloudflare"); +const express = await import("../../platforms/express"); +const svelte = await import("../../platforms/svelte"); +const h3 = await import("../../platforms/h3"); +const solidjs = await import("../../platforms/solidjs"); +const astro = await import("../../platforms/astro"); +const tanstack = await import("../../platforms/tanstack"); +const nextjs = await import("../../platforms/nextjs"); +const reactRouter = await import("../../platforms/react-router"); + +const routeFunction = async () => {}; + +// Every public serve-like factory should start the dev server when the route is +// defined. `defineRoute` calls the factory but never invokes the returned +// request handler — so a passing assertion proves the start was NOT deferred. +const factories: { name: string; defineRoute: () => unknown }[] = [ + { name: "hono.serve", defineRoute: () => hono.serve(routeFunction) }, + { name: "cloudflare.serve", defineRoute: () => cloudflare.serve(routeFunction) }, + { name: "express.serve", defineRoute: () => express.serve(routeFunction) }, + { name: "svelte.serve", defineRoute: () => svelte.serve(routeFunction, { env: {} }) }, + { name: "h3.serve", defineRoute: () => h3.serve(routeFunction) }, + { name: "solidjs.serve", defineRoute: () => solidjs.serve(routeFunction) }, + { name: "astro.serve", defineRoute: () => astro.serve(routeFunction) }, + { name: "tanstack.serve", defineRoute: () => tanstack.serve(routeFunction) }, + { name: "nextjs.serve", defineRoute: () => nextjs.serve(routeFunction) }, + { name: "nextjs.servePagesRouter", defineRoute: () => nextjs.servePagesRouter(routeFunction) }, + { name: "reactRouter.serve", defineRoute: () => reactRouter.serve(routeFunction) }, +]; + +// serveMany factories. For most adapters serveMethod is the adapter `serve`, so +// they are covered transitively; express is the exception (its serveMethod is an +// internal handler, not `serve`) and so starts the dev server in serveMany itself. +const serveManyFactories: { name: string; defineRoutes: () => unknown }[] = [ + { + name: "hono.serveMany", + defineRoutes: () => hono.serveMany({ wf: hono.createWorkflow(routeFunction) }), + }, + { + name: "express.serveMany", + defineRoutes: () => express.serveMany({ wf: express.createWorkflow(routeFunction) }), + }, + { + name: "nextjs.serveMany", + defineRoutes: () => nextjs.serveMany({ wf: nextjs.createWorkflow(routeFunction) }), + }, + { + name: "nextjs.serveManyPagesRouter", + defineRoutes: () => + nextjs.serveManyPagesRouter({ wf: nextjs.createWorkflowPagesRouter(routeFunction) }), + }, +]; + +describe("adapters eagerly start the QStash dev server on serve()", () => { + let prevQstashDev: string | undefined; + + beforeEach(() => { + // Eager adapters (nextjs, react-router) construct a real Client in serve(); + // make sure a machine-level QSTASH_DEV=true doesn't make that real Client + // spawn an actual dev server during this unit test. + prevQstashDev = process.env.QSTASH_DEV; + delete process.env.QSTASH_DEV; + startDevServer.mockClear(); + }); + + afterEach(() => { + if (prevQstashDev === undefined) delete process.env.QSTASH_DEV; + else process.env.QSTASH_DEV = prevQstashDev; + }); + + for (const { name, defineRoute } of factories) { + test(`${name} starts the dev server synchronously at factory-call time`, () => { + expect(startDevServer).not.toHaveBeenCalled(); + defineRoute(); + // Synchronously after defining the route and before any inbound request. + expect(startDevServer).toHaveBeenCalledTimes(1); + }); + } + + for (const { name, defineRoutes } of serveManyFactories) { + test(`${name} starts the dev server at setup time`, () => { + expect(startDevServer).not.toHaveBeenCalled(); + defineRoutes(); + expect(startDevServer).toHaveBeenCalled(); + }); + } +}); diff --git a/src/serve/multi-region/dev-server-integration.test.ts b/src/serve/multi-region/dev-server-integration.test.ts index 5f5c71dd..ce41da7e 100644 --- a/src/serve/multi-region/dev-server-integration.test.ts +++ b/src/serve/multi-region/dev-server-integration.test.ts @@ -44,8 +44,6 @@ describe("workflow dev server integration", () => { }, { url: workflowUrl } ).handler; - - client = new Client({ devMode: true }); }); afterAll(() => { @@ -59,10 +57,37 @@ describe("workflow dev server integration", () => { while (Date.now() < deadline && !predicate()) await Bun.sleep(100); }; + const isDevServerReachable = async () => { + try { + // Any HTTP response (even an error status) means the dev server is up. + await fetch(`http://127.0.0.1:${INTEGRATION_PORT}`); + return true; + } catch { + return false; + } + }; + + const waitForAsync = async (predicate: () => Promise, timeoutMs = 60_000) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline && !(await predicate())) await Bun.sleep(100); + }; + + test( + "dev server is reachable after serve() and before any inbound request", + async () => { + // The only thing that has run is serve() in beforeAll — no workflow request + // has been published yet. This is the new eager-start guarantee. + await waitForAsync(isDevServerReachable); + expect(await isDevServerReachable()).toBe(true); + }, + { timeout: 60_000 } + ); + test( "multi-step workflow: every dev-server delivery verifies and steps run in order", async () => { ranSteps.length = 0; + client = new Client({ devMode: true }); const { messageId } = await client.publishJSON({ url: workflowUrl, body: "world" }); expect(messageId).toBeTruthy(); diff --git a/src/serve/multi-region/handlers.ts b/src/serve/multi-region/handlers.ts index cb8c7c5d..7d3bab90 100644 --- a/src/serve/multi-region/handlers.ts +++ b/src/serve/multi-region/handlers.ts @@ -66,6 +66,11 @@ const createRegionalHandler = ( ...clientOptions, baseUrl: clientEnv.QSTASH_URL!, token: clientEnv.QSTASH_TOKEN!, + // Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points + // the client at the local dev server too. Without this the QStash Client + // re-derives dev mode from its own process.env, which is blind to the + // Cloudflare worker `env` binding that actually carries QSTASH_DEV there. + devMode: isQStashDevModeEnabled(environment), }); const receiver = getReceiver(environment, receiverConfig, region); @@ -151,6 +156,8 @@ const getQStashHandlers = ({ ...qstashClientOption, baseUrl: environment.QSTASH_URL!, token: environment.QSTASH_TOKEN!, + // Mirror the receiver's dev-mode decision (see createRegionalHandler). + devMode: isQStashDevModeEnabled(environment), }), receiver: getReceiver(environment, receiverConfig), }, diff --git a/src/serve/multi-region/multi-region.test.ts b/src/serve/multi-region/multi-region.test.ts index 1046b627..ff6a480d 100644 --- a/src/serve/multi-region/multi-region.test.ts +++ b/src/serve/multi-region/multi-region.test.ts @@ -597,4 +597,49 @@ describe("QStash Handler Options - Multi-Region Mode Detection", () => { expect(result.defaultClient.http).toBeDefined(); }); }); + + describe("Client Dev Mode (derived from environment)", () => { + // Pins the fix for the receiver-dev / client-prod split: a single + // QSTASH_DEV=true (e.g. a Cloudflare `.dev.vars` binding, where the QStash + // Client's own process.env lookup is blind) must put the *client* in dev + // mode too, matching the receiver. Asserted on the constructed serve Client. + const getServeClientHttp = (environment: Record) => { + // Entering dev mode makes the Client fire a fire-and-forget dev-server + // spawn. NODE_ENV=production short-circuits the spawn while leaving + // credential resolution in dev mode, keeping this a pure unit test. + const processEnv = process.env as Record; + const previousNodeEnv = processEnv.NODE_ENV; + processEnv.NODE_ENV = "production"; + try { + const result = getQStashHandlerOptions({ environment, receiverConfig: "not-set" }); + return result.defaultClient.http as unknown as { baseUrl: string; devMode?: boolean }; + } finally { + if (previousNodeEnv === undefined) delete processEnv.NODE_ENV; + else processEnv.NODE_ENV = previousNodeEnv; + } + }; + + test("should put the serve client in dev mode when QSTASH_DEV=true", () => { + const http = getServeClientHttp(createEnvironment({ QSTASH_DEV: "true" })); + expect(http.devMode).toBe(true); + expect(http.baseUrl).toContain("127.0.0.1"); + }); + + test("should put the serve client in dev mode when QSTASH_DEV=1", () => { + const http = getServeClientHttp(createEnvironment({ QSTASH_DEV: "1" })); + expect(http.devMode).toBe(true); + expect(http.baseUrl).toContain("127.0.0.1"); + }); + + test("should keep the serve client pointed at production without QSTASH_DEV", () => { + const http = getServeClientHttp( + createEnvironment({ + QSTASH_URL: "https://qstash.upstash.io", + QSTASH_TOKEN: "test-token", + }) + ); + expect(http.devMode).toBe(false); + expect(http.baseUrl).toBe("https://qstash.upstash.io"); + }); + }); }); From 692e393d9185a2644eda0dd733dcf7533d255772 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Thu, 18 Jun 2026 11:13:21 +0300 Subject: [PATCH 2/2] feat: eager-start QStash dev server in the framework-agnostic serve too --- src/serve/dev-server-eager-start.test.ts | 2 ++ src/serve/index.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/serve/dev-server-eager-start.test.ts b/src/serve/dev-server-eager-start.test.ts index ed554f17..d00d71c1 100644 --- a/src/serve/dev-server-eager-start.test.ts +++ b/src/serve/dev-server-eager-start.test.ts @@ -31,6 +31,7 @@ const astro = await import("../../platforms/astro"); const tanstack = await import("../../platforms/tanstack"); const nextjs = await import("../../platforms/nextjs"); const reactRouter = await import("../../platforms/react-router"); +const baseServe = await import("./index"); const routeFunction = async () => {}; @@ -49,6 +50,7 @@ const factories: { name: string; defineRoute: () => unknown }[] = [ { name: "nextjs.serve", defineRoute: () => nextjs.serve(routeFunction) }, { name: "nextjs.servePagesRouter", defineRoute: () => nextjs.servePagesRouter(routeFunction) }, { name: "reactRouter.serve", defineRoute: () => reactRouter.serve(routeFunction) }, + { name: "serve (framework-agnostic root)", defineRoute: () => baseServe.serve(routeFunction) }, ]; // serveMany factories. For most adapters serveMethod is the adapter `serve`, so diff --git a/src/serve/index.ts b/src/serve/index.ts index 8bbeee45..436ee2e6 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -1,3 +1,4 @@ +import { startDevServer } from "@upstash/qstash"; import { makeCancelRequest } from "../client/utils"; import { SDK_TELEMETRY, @@ -375,6 +376,7 @@ export const serve = < routeFunction: RouteFunction, options?: WorkflowServeOptions ): ReturnType> => { + void startDevServer(); return serveBase( routeFunction, {