diff --git a/.changeset/expose-world-helpers.md b/.changeset/expose-world-helpers.md new file mode 100644 index 000000000..9eff81654 --- /dev/null +++ b/.changeset/expose-world-helpers.md @@ -0,0 +1,5 @@ +--- +"workflow": patch +--- + +Expose `getWorld` plus every `World` helper via `workflow/api`, add delegation tests, and document how to access the singleton programmatically. diff --git a/docs/content/docs/api-reference/workflow-api/index.mdx b/docs/content/docs/api-reference/workflow-api/index.mdx index 6f84c3998..a6a59a36e 100644 --- a/docs/content/docs/api-reference/workflow-api/index.mdx +++ b/docs/content/docs/api-reference/workflow-api/index.mdx @@ -25,4 +25,7 @@ Workflow DevKit provides runtime functions that are used outside of workflow and Get workflow run status and metadata without waiting for completion. + + Access the World singleton plus list/cancel APIs for runs, steps, events, and hooks. + diff --git a/docs/content/docs/api-reference/workflow-api/world.mdx b/docs/content/docs/api-reference/workflow-api/world.mdx new file mode 100644 index 000000000..dda3750c7 --- /dev/null +++ b/docs/content/docs/api-reference/workflow-api/world.mdx @@ -0,0 +1,149 @@ +--- +title: World helpers +--- + +# Accessing the World singleton + +> “Is there any public API to access the active default World? I'd like to be able to list active runs in my app, like I can via the CLI. I could store them myself, but if there is an API for this that works across testing and deployment, that'd be even better!” + +Yes. Import `getWorld` or any of the helper functions below directly from `workflow/api`. They proxy to the same singleton that powers the CLI, whether you're running locally with the embedded world, in tests (via `setWorld`), or on Vercel. + +```typescript +import { + getWorld, + listRuns, + listSteps, + cancelRun, + queue, +} from 'workflow/api'; + +const runs = await listRuns({ status: 'running', pagination: { limit: 10 } }); +await cancelRun(runs.data[0].runId); + +// You still have full access to the underlying World instance when needed. +const world = getWorld(); +await world.start?.(); +``` + +## Example: List and cancel runs in a Next.js route + +```typescript filename="app/api/workflows/route.ts" +import { listRuns, cancelRun } from 'workflow/api'; + +export async function GET() { + const runs = await listRuns({ status: 'running', pagination: { limit: 20 } }); + return Response.json({ runs: runs.data }); +} + +export async function POST(request: Request) { + const { runId } = await request.json(); + await cancelRun(runId); + return Response.json({ ok: true }); +} +``` + +This works the same way in development and production—just make sure the usual `WORKFLOW_*` environment variables are present so the correct world implementation can be chosen. + +## Helper catalog + +### Run helpers + +| Function | Description | +| --- | --- | +| `createRun(data)` | Calls `world.runs.create` directly. Useful for advanced tooling/tests. | +| `getWorkflowRun(id, params?)` | Fetches a workflow run record without wrapping it in the `Run` class. | +| `updateRun(id, data)` | Partially updates a run record. | +| `listRuns(params?)` | Lists runs with optional filters/pagination. | +| `cancelRun(id, params?)` | Cancels a run. | +| `pauseRun(id, params?)` / `resumeRun(id, params?)` | Pause/resume administrative helpers. | + +### Step helpers + +| Function | Description | +| --- | --- | +| `createStep(runId, data)` | Inserts a step record. | +| `getStep(runId, stepId, params?)` | Retrieves a single step. | +| `updateStep(runId, stepId, data)` | Updates status/attempt metadata. | +| `listSteps(params)` | Lists steps for a run (with pagination + `resolveData`). | + +### Events & hooks + +| Function | Description | +| --- | --- | +| `createEvent(runId, data, params?)` | Writes workflow/step events. | +| `listEvents(params)` | Lists events for a run. | +| `listEventsByCorrelationId(params)` | Lists events for a correlation ID across runs. | +| `createHook(runId, data, params?)` | Creates a hook. | +| `getHook(id, params?)` | Fetches hook metadata. | +| `listHooks(params)` | Lists hooks. | +| `disposeHook(id, params?)` | Disposes a hook. | +| `getHookByToken(token, params?)` | Continues to be exported for convenience. | + +### Queue, streams, and lifecycle helpers + +| Function | Description | +| --- | --- | +| `getDeploymentId()` | Returns the deployment ID that queue operations will use. | +| `queue(name, payload, opts?)` | Enqueue workflow/step invocations manually. | +| `createQueueHandler(prefix, handler)` | Builds queue HTTP handlers (the same API the runtime uses). | +| `writeToStream(name, chunk)` / `closeStream(name)` / `readFromStream(name, startIndex?)` | Direct streaming helpers. | +| `startWorld()` | Invokes `world.start?.()` if provided by your world implementation. | + +## Testing tips + +Use `setWorld` to stub custom worlds in tests so that the helpers continue to work without hitting real infrastructure: + +```typescript +import { setWorld } from 'workflow/runtime'; +import { listRuns } from 'workflow/api'; +import type { World } from '@workflow/world'; + +beforeEach(() => { + const mockWorld: World = { + // Provide a minimal mock World + runs: { + list: async () => ({ data: [], cursor: null, hasMore: false }), + create: async () => { throw new Error('not implemented'); }, + get: async () => { throw new Error('not implemented'); }, + update: async () => { throw new Error('not implemented'); }, + cancel: async () => { throw new Error('not implemented'); }, + pause: async () => { throw new Error('not implemented'); }, + resume: async () => { throw new Error('not implemented'); }, + }, + steps: { + create: async () => { throw new Error('not implemented'); }, + get: async () => { throw new Error('not implemented'); }, + update: async () => { throw new Error('not implemented'); }, + list: async () => ({ data: [], cursor: null, hasMore: false }), + }, + events: { + create: async () => { throw new Error('not implemented'); }, + list: async () => ({ data: [], cursor: null, hasMore: false }), + listByCorrelationId: async () => ({ data: [], cursor: null, hasMore: false }), + }, + hooks: { + create: async () => { throw new Error('not implemented'); }, + get: async () => { throw new Error('not implemented'); }, + getByToken: async () => { throw new Error('not implemented'); }, + list: async () => ({ data: [], cursor: null, hasMore: false }), + dispose: async () => { throw new Error('not implemented'); }, + }, + getDeploymentId: async () => 'test', + queue: async () => ({ messageId: 'msg_test' }), + createQueueHandler: () => async () => new Response(), + writeToStream: async () => {}, + closeStream: async () => {}, + readFromStream: async () => new ReadableStream(), + }; + setWorld(mockWorld); +}); + +afterEach(() => setWorld(undefined)); + +it('lists runs without contacting real services', async () => { + const runs = await listRuns(); + expect(runs.data).toHaveLength(0); +}); +``` + +This mirrors how the new automated tests stub the world singleton. diff --git a/docs/content/docs/foundations/starting-workflows.mdx b/docs/content/docs/foundations/starting-workflows.mdx index fb3d7aa31..0929a23c1 100644 --- a/docs/content/docs/foundations/starting-workflows.mdx +++ b/docs/content/docs/foundations/starting-workflows.mdx @@ -210,6 +210,27 @@ export async function GET(request: Request) { } ``` +### Programmatically list and control runs + +If you want to show workflow progress directly in your product UI—or let trusted operators retry or cancel work—you can now call the same administrative helpers the CLI uses, straight from `workflow/api`. + +```typescript lineNumbers +import { listRuns, cancelRun } from 'workflow/api'; + +export async function GET() { + const runs = await listRuns({ status: 'running', pagination: { limit: 20 } }); + return Response.json(runs.data); +} + +export async function POST(request: Request) { + const { runId } = await request.json(); + await cancelRun(runId); + return Response.json({ ok: true }); +} +``` + +Need lower-level access? Call `getWorld()` (also exported from `workflow/api`) to reach the singleton directly, or `setWorld()` from `workflow/runtime` in your tests to install a mock world. See the [World helpers reference](/docs/api-reference/workflow-api/world) for every available function. + --- ## Next Steps diff --git a/packages/workflow/package.json b/packages/workflow/package.json index dc441eb65..880c6efd4 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -52,6 +52,7 @@ "@workflow/core": "workspace:*", "@workflow/errors": "workspace:*", "@workflow/typescript-plugin": "workspace:*", + "@workflow/world": "workspace:*", "ms": "2.1.3", "@workflow/next": "workspace:*", "@workflow/nitro": "workspace:*", diff --git a/packages/workflow/src/api.test.ts b/packages/workflow/src/api.test.ts new file mode 100644 index 000000000..3d5a0f2ee --- /dev/null +++ b/packages/workflow/src/api.test.ts @@ -0,0 +1,426 @@ +import { setWorld } from '@workflow/core/runtime'; +import type { + CreateEventRequest, + CreateHookRequest, + CreateStepRequest, + CreateWorkflowRunRequest, + Event, + Hook, + ListEventsByCorrelationIdParams, + ListEventsParams, + ListHooksParams, + ListWorkflowRunStepsParams, + ListWorkflowRunsParams, + MessageId, + PaginatedResponse, + QueuePayload, + QueuePrefix, + Step, + UpdateStepRequest, + UpdateWorkflowRunRequest, + ValidQueueName, + WorkflowRun, + World, +} from '@workflow/world'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + type Mock, + vi, +} from 'vitest'; +import { + cancelRun, + closeStream, + createEvent, + createHook, + createQueueHandler, + createRun, + createStep, + disposeHook, + getDeploymentId, + getHook, + getStep, + getWorkflowRun, + listEvents, + listEventsByCorrelationId, + listHooks, + listRuns, + listSteps, + pauseRun, + queue, + readFromStream, + resumeRun, + startWorld, + updateRun, + updateStep, + writeToStream, +} from './api'; + +// Utility types to strongly type the mocked World instance +type FnMock = T extends (...args: infer A) => infer R ? Mock : never; +type RunsMock = { [K in keyof World['runs']]: FnMock }; +type StepsMock = { [K in keyof World['steps']]: FnMock }; +type EventsMock = { [K in keyof World['events']]: FnMock }; +type HooksMock = { [K in keyof World['hooks']]: FnMock }; + +interface InstrumentedWorld extends World { + runs: RunsMock; + steps: StepsMock; + events: EventsMock; + hooks: HooksMock; + getDeploymentId: FnMock; + queue: FnMock; + createQueueHandler: FnMock; + writeToStream: FnMock; + closeStream: FnMock; + readFromStream: FnMock; + start?: FnMock>; +} + +const sampleRun: WorkflowRun = { + runId: 'run_123', + deploymentId: 'dep_abc', + status: 'running', + workflowName: 'exampleWorkflow', + executionContext: {}, + input: [], + createdAt: new Date(), + updatedAt: new Date(), +}; + +const sampleStep: Step = { + runId: 'run_123', + stepId: 'step_1', + stepName: 'Example Step', + status: 'pending', + input: [], + attempt: 1, + createdAt: new Date(), + updatedAt: new Date(), +}; + +const sampleHook: Hook = { + runId: 'run_123', + hookId: 'hook_1', + token: 'token', + ownerId: 'owner', + projectId: 'project', + environment: 'development', + createdAt: new Date(), +}; + +const sampleEvent: Event = { + runId: 'run_123', + eventId: 'event_1', + eventType: 'workflow_started', + createdAt: new Date(), +}; + +const paginate = (data: T[]): PaginatedResponse => ({ + data, + cursor: null, + hasMore: false, +}); + +function createMockWorld(): InstrumentedWorld { + return { + runs: { + create: vi.fn(), + get: vi.fn(), + update: vi.fn(), + list: vi.fn(), + cancel: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + }, + steps: { + create: vi.fn(), + get: vi.fn(), + update: vi.fn(), + list: vi.fn(), + }, + events: { + create: vi.fn(), + list: vi.fn(), + listByCorrelationId: vi.fn(), + }, + hooks: { + create: vi.fn(), + get: vi.fn(), + getByToken: vi.fn(), + list: vi.fn(), + dispose: vi.fn(), + }, + getDeploymentId: vi.fn(), + queue: vi.fn(), + createQueueHandler: vi.fn(), + writeToStream: vi.fn(), + closeStream: vi.fn(), + readFromStream: vi.fn(), + start: vi.fn(), + } as InstrumentedWorld; +} + +describe('workflow/api world helpers', () => { + let mockWorld: InstrumentedWorld; + + beforeEach(() => { + mockWorld = createMockWorld(); + setWorld(mockWorld); + }); + + afterEach(() => { + setWorld(undefined); + vi.restoreAllMocks(); + }); + + it('delegates run helpers to the active world singleton', async () => { + const createInput: CreateWorkflowRunRequest = { + deploymentId: 'dep_abc', + workflowName: 'exampleWorkflow', + input: [], + }; + const updateInput: UpdateWorkflowRunRequest = { status: 'completed' }; + const listParams: ListWorkflowRunsParams = { + workflowName: 'exampleWorkflow', + }; + + mockWorld.runs.create.mockResolvedValue(sampleRun); + mockWorld.runs.get.mockResolvedValue(sampleRun); + mockWorld.runs.update.mockResolvedValue(sampleRun); + mockWorld.runs.list.mockResolvedValue(paginate([sampleRun])); + mockWorld.runs.cancel.mockResolvedValue(sampleRun); + mockWorld.runs.pause.mockResolvedValue(sampleRun); + mockWorld.runs.resume.mockResolvedValue(sampleRun); + + await expect(createRun(createInput)).resolves.toBe(sampleRun); + expect(mockWorld.runs.create).toHaveBeenCalledWith(createInput); + + await expect(getWorkflowRun(sampleRun.runId, undefined)).resolves.toBe( + sampleRun + ); + expect(mockWorld.runs.get).toHaveBeenCalledWith(sampleRun.runId, undefined); + + await expect(updateRun(sampleRun.runId, updateInput)).resolves.toBe( + sampleRun + ); + expect(mockWorld.runs.update).toHaveBeenCalledWith( + sampleRun.runId, + updateInput + ); + + await expect(listRuns(listParams)).resolves.toEqual(paginate([sampleRun])); + expect(mockWorld.runs.list).toHaveBeenCalledWith(listParams); + + await expect(cancelRun(sampleRun.runId)).resolves.toBe(sampleRun); + expect(mockWorld.runs.cancel).toHaveBeenCalledWith( + sampleRun.runId, + undefined + ); + + await expect(pauseRun(sampleRun.runId)).resolves.toBe(sampleRun); + expect(mockWorld.runs.pause).toHaveBeenCalledWith( + sampleRun.runId, + undefined + ); + + await expect(resumeRun(sampleRun.runId)).resolves.toBe(sampleRun); + expect(mockWorld.runs.resume).toHaveBeenCalledWith( + sampleRun.runId, + undefined + ); + }); + + it('supports the “list runs then cancel” programmatic use case', async () => { + mockWorld.runs.list.mockResolvedValue(paginate([sampleRun])); + mockWorld.runs.cancel.mockResolvedValue(sampleRun); + + const runs = await listRuns(); + expect(runs.data[0].runId).toBe(sampleRun.runId); + + await cancelRun(sampleRun.runId); + expect(mockWorld.runs.cancel).toHaveBeenCalledWith( + sampleRun.runId, + undefined + ); + }); + + it('delegates step helpers', async () => { + const createStepInput: CreateStepRequest = { + stepId: 'step_1', + stepName: 'Example Step', + input: [], + }; + const updateStepInput: UpdateStepRequest = { status: 'completed' }; + const listStepsParams: ListWorkflowRunStepsParams = { runId: 'run_123' }; + + mockWorld.steps.create.mockResolvedValue(sampleStep); + mockWorld.steps.get.mockResolvedValue(sampleStep); + mockWorld.steps.update.mockResolvedValue(sampleStep); + mockWorld.steps.list.mockResolvedValue(paginate([sampleStep])); + + await expect(createStep(sampleRun.runId, createStepInput)).resolves.toBe( + sampleStep + ); + expect(mockWorld.steps.create).toHaveBeenCalledWith( + sampleRun.runId, + createStepInput + ); + + await expect(getStep(sampleRun.runId, sampleStep.stepId)).resolves.toBe( + sampleStep + ); + expect(mockWorld.steps.get).toHaveBeenCalledWith( + sampleRun.runId, + sampleStep.stepId, + undefined + ); + + await expect( + updateStep(sampleRun.runId, sampleStep.stepId, updateStepInput) + ).resolves.toBe(sampleStep); + expect(mockWorld.steps.update).toHaveBeenCalledWith( + sampleRun.runId, + sampleStep.stepId, + updateStepInput + ); + + await expect(listSteps(listStepsParams)).resolves.toEqual( + paginate([sampleStep]) + ); + expect(mockWorld.steps.list).toHaveBeenCalledWith(listStepsParams); + }); + + it('delegates event and hook helpers', async () => { + const createEventInput: CreateEventRequest = { + eventType: 'workflow_started', + }; + const listEventParams: ListEventsParams = { runId: sampleRun.runId }; + const listByCorrelationParams: ListEventsByCorrelationIdParams = { + correlationId: 'corr_1', + }; + const createHookInput: CreateHookRequest = { + hookId: 'hook_1', + token: 'token', + }; + const listHookParams: ListHooksParams = {}; + + mockWorld.events.create.mockResolvedValue(sampleEvent); + mockWorld.events.list.mockResolvedValue(paginate([sampleEvent])); + mockWorld.events.listByCorrelationId.mockResolvedValue( + paginate([sampleEvent]) + ); + + mockWorld.hooks.create.mockResolvedValue(sampleHook); + mockWorld.hooks.get.mockResolvedValue(sampleHook); + mockWorld.hooks.list.mockResolvedValue(paginate([sampleHook])); + mockWorld.hooks.dispose.mockResolvedValue(sampleHook); + + await expect(createEvent(sampleRun.runId, createEventInput)).resolves.toBe( + sampleEvent + ); + expect(mockWorld.events.create).toHaveBeenCalledWith( + sampleRun.runId, + createEventInput, + undefined + ); + + await expect(listEvents(listEventParams)).resolves.toEqual( + paginate([sampleEvent]) + ); + expect(mockWorld.events.list).toHaveBeenCalledWith(listEventParams); + + await expect( + listEventsByCorrelationId(listByCorrelationParams) + ).resolves.toEqual(paginate([sampleEvent])); + expect(mockWorld.events.listByCorrelationId).toHaveBeenCalledWith( + listByCorrelationParams + ); + + await expect(createHook(sampleRun.runId, createHookInput)).resolves.toBe( + sampleHook + ); + expect(mockWorld.hooks.create).toHaveBeenCalledWith( + sampleRun.runId, + createHookInput, + undefined + ); + + await expect(getHook(sampleHook.hookId)).resolves.toBe(sampleHook); + expect(mockWorld.hooks.get).toHaveBeenCalledWith( + sampleHook.hookId, + undefined + ); + + await expect(listHooks(listHookParams)).resolves.toEqual( + paginate([sampleHook]) + ); + expect(mockWorld.hooks.list).toHaveBeenCalledWith(listHookParams); + + await expect(disposeHook(sampleHook.hookId)).resolves.toBe(sampleHook); + expect(mockWorld.hooks.dispose).toHaveBeenCalledWith( + sampleHook.hookId, + undefined + ); + }); + + it('delegates queue, stream, and lifecycle helpers', async () => { + const queueName = '__wkf_workflow_example' as ValidQueueName; + const queuePayload: QueuePayload = { runId: sampleRun.runId }; + const queuePrefix = '__wkf_step_' as QueuePrefix; + const messageHandler = vi.fn(async () => undefined); + const queueHandler = vi.fn( + async (_req: Request) => new Response(null, { status: 200 }) + ); + + mockWorld.getDeploymentId.mockResolvedValue('dep_abc'); + mockWorld.queue.mockResolvedValue({ messageId: 'msg_1' as MessageId }); + mockWorld.createQueueHandler.mockReturnValue(queueHandler as any); + mockWorld.writeToStream.mockResolvedValue(); + mockWorld.closeStream.mockResolvedValue(); + const stream = new ReadableStream({ + start(controller) { + controller.close(); + }, + }); + mockWorld.readFromStream.mockResolvedValue(stream); + mockWorld.start?.mockResolvedValue(undefined); + + await expect(getDeploymentId()).resolves.toBe('dep_abc'); + expect(mockWorld.getDeploymentId).toHaveBeenCalledTimes(1); + + await expect(queue(queueName, queuePayload)).resolves.toEqual({ + messageId: 'msg_1', + }); + expect(mockWorld.queue).toHaveBeenCalledWith( + queueName, + queuePayload, + undefined + ); + + const createdHandler = createQueueHandler(queuePrefix, messageHandler); + expect(mockWorld.createQueueHandler).toHaveBeenCalledWith( + queuePrefix, + messageHandler + ); + expect(createdHandler).toBe(queueHandler); + + await writeToStream('stream_1', 'payload'); + expect(mockWorld.writeToStream).toHaveBeenCalledWith('stream_1', 'payload'); + + await closeStream('stream_1'); + expect(mockWorld.closeStream).toHaveBeenCalledWith('stream_1'); + + await expect(readFromStream('stream_1')).resolves.toBe(stream); + expect(mockWorld.readFromStream).toHaveBeenCalledWith( + 'stream_1', + undefined + ); + + await startWorld(); + expect(mockWorld.start).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/workflow/src/api.ts b/packages/workflow/src/api.ts index 81baccd20..d9580f9ff 100644 --- a/packages/workflow/src/api.ts +++ b/packages/workflow/src/api.ts @@ -1,7 +1,40 @@ +import { getWorld } from '@workflow/core/runtime'; +import type { + CancelWorkflowRunParams, + CreateEventParams, + CreateEventRequest, + CreateHookRequest, + CreateStepRequest, + CreateWorkflowRunRequest, + Event, + GetHookParams, + GetStepParams, + GetWorkflowRunParams, + Hook, + ListEventsByCorrelationIdParams, + ListEventsParams, + ListHooksParams, + ListWorkflowRunStepsParams, + ListWorkflowRunsParams, + MessageId, + PaginatedResponse, + PauseWorkflowRunParams, + QueuePayload, + QueuePrefix, + ResumeWorkflowRunParams, + Step, + UpdateStepRequest, + UpdateWorkflowRunRequest, + ValidQueueName, + WorkflowRun, + World, +} from '@workflow/world'; + export { type Event, getHookByToken, getRun, + getWorld, Run, resumeHook, resumeWebhook, @@ -11,3 +44,268 @@ export { type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime'; + +export type { + CancelWorkflowRunParams, + CreateEventParams, + CreateEventRequest, + CreateHookRequest, + CreateStepRequest, + CreateWorkflowRunRequest, + GetHookParams, + GetStepParams, + GetWorkflowRunParams, + Hook, + ListEventsByCorrelationIdParams, + ListEventsParams, + ListHooksParams, + ListWorkflowRunStepsParams, + ListWorkflowRunsParams, + MessageId, + PaginatedResponse, + PauseWorkflowRunParams, + QueuePayload, + QueuePrefix, + ResumeWorkflowRunParams, + Step, + UpdateStepRequest, + UpdateWorkflowRunRequest, + ValidQueueName, +} from '@workflow/world'; + +/** + * Creates a workflow run using the configured World implementation. + */ +export function createRun( + data: CreateWorkflowRunRequest +): Promise { + return getWorld().runs.create(data); +} + +/** + * Retrieves a workflow run record without wrapping it in the `Run` helper. + */ +export function getWorkflowRun( + runId: string, + params?: GetWorkflowRunParams +): Promise { + return getWorld().runs.get(runId, params); +} + +/** + * Updates a workflow run with partial data. + */ +export function updateRun( + runId: string, + data: UpdateWorkflowRunRequest +): Promise { + return getWorld().runs.update(runId, data); +} + +/** + * Lists workflow runs with optional filtering/pagination. + */ +export function listRuns( + params?: ListWorkflowRunsParams +): Promise> { + return getWorld().runs.list(params); +} + +/** + * Cancels a workflow run. + */ +export function cancelRun( + runId: string, + params?: CancelWorkflowRunParams +): Promise { + return getWorld().runs.cancel(runId, params); +} + +/** + * Pauses a workflow run. + */ +export function pauseRun( + runId: string, + params?: PauseWorkflowRunParams +): Promise { + return getWorld().runs.pause(runId, params); +} + +/** + * Resumes a previously paused workflow run. + */ +export function resumeRun( + runId: string, + params?: ResumeWorkflowRunParams +): Promise { + return getWorld().runs.resume(runId, params); +} + +/** + * Creates a step record for a workflow run. + */ +export function createStep( + runId: string, + data: CreateStepRequest +): Promise { + return getWorld().steps.create(runId, data); +} + +/** + * Retrieves a workflow step. + */ +export function getStep( + runId: string | undefined, + stepId: string, + params?: GetStepParams +): Promise { + return getWorld().steps.get(runId, stepId, params); +} + +/** + * Updates a workflow step. + */ +export function updateStep( + runId: string, + stepId: string, + data: UpdateStepRequest +): Promise { + return getWorld().steps.update(runId, stepId, data); +} + +/** + * Lists steps for a workflow run. + */ +export function listSteps( + params: ListWorkflowRunStepsParams +): Promise> { + return getWorld().steps.list(params); +} + +/** + * Creates an event associated with a workflow run. + */ +export function createEvent( + runId: string, + data: CreateEventRequest, + params?: CreateEventParams +): Promise { + return getWorld().events.create(runId, data, params); +} + +/** + * Lists events for a workflow run. + */ +export function listEvents( + params: ListEventsParams +): Promise> { + return getWorld().events.list(params); +} + +/** + * Lists events filtered by correlation ID across runs. + */ +export function listEventsByCorrelationId( + params: ListEventsByCorrelationIdParams +): Promise> { + return getWorld().events.listByCorrelationId(params); +} + +/** + * Creates a hook for a workflow run. + */ +export function createHook( + runId: string, + data: CreateHookRequest, + params?: GetHookParams +): Promise { + return getWorld().hooks.create(runId, data, params); +} + +/** + * Retrieves a hook by ID. + */ +export function getHook(hookId: string, params?: GetHookParams): Promise { + return getWorld().hooks.get(hookId, params); +} + +/** + * Lists hooks with optional filters. + */ +export function listHooks( + params: ListHooksParams +): Promise> { + return getWorld().hooks.list(params); +} + +/** + * Disposes an existing hook. + */ +export function disposeHook( + hookId: string, + params?: GetHookParams +): Promise { + return getWorld().hooks.dispose(hookId, params); +} + +/** + * Returns the deployment ID used by the underlying queue implementation. + */ +export function getDeploymentId(): Promise { + return getWorld().getDeploymentId(); +} + +/** + * Enqueues a message for workflow or step execution. + */ +export function queue( + queueName: ValidQueueName, + message: QueuePayload, + opts?: { deploymentId?: string; idempotencyKey?: string } +): Promise<{ messageId: MessageId }> { + return getWorld().queue(queueName, message, opts); +} + +/** + * Creates an HTTP handler for the provided queue prefix. + */ +export function createQueueHandler( + queueNamePrefix: QueuePrefix, + handler: Parameters[1] +): ReturnType { + return getWorld().createQueueHandler(queueNamePrefix, handler); +} + +/** + * Writes chunked data to a named stream. + */ +export function writeToStream( + name: string, + chunk: string | Uint8Array +): Promise { + return getWorld().writeToStream(name, chunk); +} + +/** + * Closes a named stream. + */ +export function closeStream(name: string): Promise { + return getWorld().closeStream(name); +} + +/** + * Reads data from a named stream. + */ +export function readFromStream( + name: string, + startIndex?: number +): Promise> { + return getWorld().readFromStream(name, startIndex); +} + +/** + * Starts any background services provided by the configured World. + */ +export async function startWorld(): Promise { + await getWorld().start?.(); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01614c57d..ee47ed064 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -823,6 +823,9 @@ importers: '@workflow/typescript-plugin': specifier: workspace:* version: link:../typescript-plugin + '@workflow/world': + specifier: workspace:* + version: link:../world ms: specifier: 2.1.3 version: 2.1.3 @@ -14367,6 +14370,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(vite@7.1.11(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 7.1.11(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0) + '@vitest/mocker@3.2.4(vite@7.1.11(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 @@ -20141,7 +20152,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@7.1.11(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4