diff --git a/.changeset/calm-world-singleton.md b/.changeset/calm-world-singleton.md new file mode 100644 index 0000000000..f3b2aab0fa --- /dev/null +++ b/.changeset/calm-world-singleton.md @@ -0,0 +1,6 @@ +--- +'workflow': patch +'@workflow/core': patch +--- + +Reuse the runtime World singleton when the workflow entrypoint initializes its queue handler, avoiding duplicate resources for stateful async World factories. diff --git a/docs/content/docs/v5/api-reference/workflow-runtime/get-world-handlers.mdx b/docs/content/docs/v5/api-reference/workflow-runtime/get-world-handlers.mdx index 4c2bfad0a4..8473a6680e 100644 --- a/docs/content/docs/v5/api-reference/workflow-runtime/get-world-handlers.mdx +++ b/docs/content/docs/v5/api-reference/workflow-runtime/get-world-handlers.mdx @@ -35,10 +35,10 @@ type WorldHandlers = Pick; ``` - This is SDK infrastructure used by framework adapters and the workflow entrypoint. Application code should use [`getWorld()`](/docs/api-reference/workflow-runtime/get-world) instead. + This is SDK infrastructure used by framework adapters at build time. Runtime routes and application code should use [`getWorld()`](/docs/api-reference/workflow-runtime/get-world) instead. ## Related Functions - [`getWorld()`](/docs/api-reference/workflow-runtime/get-world) - Resolve the full World instance at runtime. -- [`workflowEntrypoint()`](/docs/api-reference/workflow-runtime/workflow-entrypoint) - The route handler factory built on these handlers. +- [`workflowEntrypoint()`](/docs/api-reference/workflow-runtime/workflow-entrypoint) - Create the runtime route handler that shares the full World instance. diff --git a/docs/content/docs/v5/api-reference/workflow-runtime/workflow-entrypoint.mdx b/docs/content/docs/v5/api-reference/workflow-runtime/workflow-entrypoint.mdx index efce612990..c4e0cd6626 100644 --- a/docs/content/docs/v5/api-reference/workflow-runtime/workflow-entrypoint.mdx +++ b/docs/content/docs/v5/api-reference/workflow-runtime/workflow-entrypoint.mdx @@ -38,5 +38,6 @@ Returns a fetch-style request handler: `(req: Request) => Promise`. ## Related Functions -- [`getWorldHandlers()`](/docs/api-reference/workflow-runtime/get-world-handlers) - The build-time World access this handler is built on. +- [`getWorld()`](/docs/api-reference/workflow-runtime/get-world) - Resolve the runtime World instance this handler shares with workflow execution. +- [`getWorldHandlers()`](/docs/api-reference/workflow-runtime/get-world-handlers) - Access build-time-safe World handlers for framework tooling. - [`healthCheck()`](/docs/api-reference/workflow-runtime/health-check) - Verify the entrypoint processes queue messages end-to-end. diff --git a/packages/core/src/runtime-world-singleton.test.ts b/packages/core/src/runtime-world-singleton.test.ts new file mode 100644 index 0000000000..d789bafbbf --- /dev/null +++ b/packages/core/src/runtime-world-singleton.test.ts @@ -0,0 +1,40 @@ +import { SPEC_VERSION_CURRENT, type World } from '@workflow/world'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getWorld, setWorld } from './runtime/world.js'; +import { workflowEntrypoint } from './runtime.js'; + +const createLocalWorld = vi.hoisted(() => vi.fn()); + +vi.mock('@workflow/world-local', () => ({ + createWorld: createLocalWorld, +})); + +describe('workflowEntrypoint world initialization', () => { + const world = { + specVersion: SPEC_VERSION_CURRENT, + createQueueHandler: vi.fn( + () => async () => new Response(null, { status: 204 }) + ), + } as unknown as World; + + beforeEach(() => { + setWorld(undefined); + createLocalWorld.mockReset(); + createLocalWorld.mockResolvedValue(world); + }); + + afterEach(() => { + setWorld(undefined); + vi.clearAllMocks(); + }); + + it('reuses the runtime World after initializing the route handler', async () => { + const handler = workflowEntrypoint(''); + + const response = await handler(new Request('https://example.test')); + + expect(response.status).toBe(204); + await expect(getWorld()).resolves.toBe(world); + expect(createLocalWorld).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 7841f9c0c3..c3ba73246b 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -112,11 +112,7 @@ import { runStepSingleFlight } from './runtime/step-single-flight.js'; import { handleSuspension } from './runtime/suspension-handler.js'; import { useQuickJSVm } from './runtime/vm-mode.js'; import { getWaitContinuationDispatch } from './runtime/wait-continuation.js'; -import { - getWorld, - getWorldHandlers, - type WorldHandlers, -} from './runtime/world.js'; +import { getWorld, type WorldHandlers } from './runtime/world.js'; import { dehydrateRunError } from './serialization.js'; import { remapErrorStack } from './source-map.js'; import * as Attribute from './telemetry/semantic-conventions.js'; @@ -4504,7 +4500,7 @@ export function workflowEntrypoint( cachedHandler = await trace('workflow.route.init', async () => { const worldHandlers = await trace( 'workflow.route.get_world_handlers', - async () => getWorldHandlers() + async () => getWorld() ); return handler(worldHandlers); });