workflow 5.0.0-beta.4 → 5.0.0-beta.6
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/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +14 -2
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -2
- package/docs/ai/index.mdx +6 -5
- package/docs/api-reference/vitest/index.mdx +28 -1
- package/docs/api-reference/workflow-api/start.mdx +5 -4
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
- package/docs/api-reference/workflow-next/with-workflow.mdx +32 -0
- package/docs/changelog/eager-processing.mdx +595 -0
- package/docs/changelog/index.mdx +2 -1
- package/docs/cookbook/advanced/child-workflows.mdx +13 -24
- package/docs/cookbook/advanced/meta.json +1 -6
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +29 -78
- package/docs/cookbook/agent-patterns/durable-agent.mdx +4 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +4 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +1 -1
- package/docs/cookbook/common-patterns/workflow-composition.mdx +7 -14
- package/docs/cookbook/index.mdx +0 -1
- package/docs/cookbook/integrations/ai-sdk.mdx +4 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +4 -0
- package/docs/deploying/building-a-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +5 -3
- package/docs/deploying/world/vercel-world.mdx +2 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
- package/docs/errors/hook-conflict.mdx +56 -4
- package/docs/foundations/cancellation.mdx +460 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/index.mdx +3 -0
- package/docs/foundations/meta.json +3 -1
- package/docs/foundations/serialization.mdx +77 -41
- package/docs/foundations/starting-workflows.mdx +5 -1
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +6 -0
- package/docs/getting-started/index.mdx +6 -7
- package/docs/getting-started/meta.json +1 -0
- package/docs/getting-started/nestjs.mdx +8 -0
- package/docs/getting-started/next.mdx +5 -3
- package/docs/getting-started/nitro.mdx +22 -0
- package/docs/getting-started/sveltekit.mdx +6 -0
- package/docs/getting-started/tanstack-start.mdx +241 -0
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/docs/how-it-works/meta.json +2 -1
- package/docs/internal/index.mdx +19 -0
- package/docs/internal/meta.json +5 -0
- package/docs/internal/serializable-abort-controller.mdx +148 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
- package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
- package/docs/migration-guides/migrating-from-temporal.mdx +5 -10
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
- package/package.json +13 -12
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
|
@@ -50,7 +50,7 @@ Migration removes infrastructure and collapses indirection. Business logic stays
|
|
|
50
50
|
| Signal | `createHook()` or `createWebhook()` | Use hooks for typed resume signals; webhooks for HTTP callbacks. |
|
|
51
51
|
| Query | `getWritable({ namespace: 'status' })` stream | Durably stream status updates from the workflow. Clients read from the stream instead of polling a database. |
|
|
52
52
|
| Update | `createHook()` + `resumeHook()` (one-way) | Temporal Updates return a value to the caller; hooks do not. If the Update returns data, either write the result to a named stream via `getWritable()` and have the caller read from it, or keep an HTTP read route that fetches the workflow's current state. |
|
|
53
|
-
| Child Workflow | `"use step"`
|
|
53
|
+
| Child Workflow | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run and return the `Run` object so observability can deep-link into child runs. |
|
|
54
54
|
| Activity retry policy | Step retries, `RetryableError`, `FatalError`, `maxRetries` | Retries live at the step boundary. |
|
|
55
55
|
| Event History | Workflow event log / run timeline | Same durable replay; built-in observability UI replaces Temporal Web. Search attributes and visibility APIs have no direct equivalent — filter by run status and timestamps instead. |
|
|
56
56
|
|
|
@@ -173,19 +173,14 @@ Temporal Queries expose in-memory workflow state on demand. In the Workflow SDK,
|
|
|
173
173
|
|
|
174
174
|
### Minimal translation
|
|
175
175
|
|
|
176
|
-
`start()`
|
|
176
|
+
In v5, call `start()` directly from the workflow to spawn a child run. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
|
|
177
177
|
|
|
178
178
|
```typescript title="workflow/workflows/parent.ts"
|
|
179
179
|
import { start } from 'workflow/api';
|
|
180
180
|
|
|
181
|
-
async function spawnChild(item: string) {
|
|
182
|
-
'use step'; // [!code highlight]
|
|
183
|
-
return start(childWorkflow, [item]); // [!code highlight]
|
|
184
|
-
}
|
|
185
|
-
|
|
186
181
|
export async function parentWorkflow(item: string) {
|
|
187
182
|
'use workflow';
|
|
188
|
-
const child = await
|
|
183
|
+
const child = await start(childWorkflow, [item]); // [!code highlight]
|
|
189
184
|
return { childRunId: child.runId };
|
|
190
185
|
}
|
|
191
186
|
```
|
|
@@ -204,7 +199,7 @@ async function collectResult(runId: string) {
|
|
|
204
199
|
}
|
|
205
200
|
```
|
|
206
201
|
|
|
207
|
-
Call
|
|
202
|
+
Call `start()` and then `collectResult()` from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls.
|
|
208
203
|
|
|
209
204
|
<Callout type="warn">
|
|
210
205
|
Activity retry policy moves to the step boundary. Use `maxRetries`, `RetryableError`, and `FatalError` on each step instead of a single workflow-wide retry block.
|
|
@@ -308,7 +303,7 @@ Remove the Worker process, `@temporalio/*` dependencies, and the Temporal Server
|
|
|
308
303
|
- Convert each Activity into a `"use step"` function.
|
|
309
304
|
- Remove Worker and Task Queue code. Start workflows from the app with `start()`.
|
|
310
305
|
- Replace Signals with `createHook()` or `createWebhook()` for HTTP callers.
|
|
311
|
-
-
|
|
306
|
+
- Use `start()` directly for child workflows, and wrap `getRun()` in a `"use step"` function when collecting results. Return the `Run` object from `start()` so observability can deep-link into child runs.
|
|
312
307
|
- Set retry policy per step with `maxRetries`, `RetryableError`, and `FatalError`.
|
|
313
308
|
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
314
309
|
- Stream status and progress from steps with `getWritable({ namespace: 'status' })`, and have clients read from the stream instead of polling.
|
|
@@ -48,7 +48,7 @@ Migration collapses the task abstraction into plain async functions. Business lo
|
|
|
48
48
|
| `logger` / `metadata.set` | `console` + `getWritable({ namespace: 'status' })` | Logs flow through the run timeline. Status writes go on a named stream. |
|
|
49
49
|
| `wait.for({ seconds \| minutes \| hours \| days })` / `wait.until({ date })` | `sleep()` | Import from `workflow`. |
|
|
50
50
|
| `wait.forToken({ timeout })` | `createHook()` + `Promise.race` with `sleep()` | Hooks carry a typed token. |
|
|
51
|
-
| `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` |
|
|
51
|
+
| `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` | Call `start()` directly; wrap `getRun()` collection in a `"use step"` function. |
|
|
52
52
|
| `batch.triggerAndWait()` | `Promise.all(runIds.map(collectResult))` | Fan out via standard concurrency. |
|
|
53
53
|
| `AbortTaskRunError` | `FatalError` | Stops retries immediately. |
|
|
54
54
|
| `retry.onThrow` / `retry.fetch` | `RetryableError`, `FatalError`, `maxRetries` | Retry count lives on the step via `myStep.maxRetries = N` (default 3). Control delay between attempts by throwing `new RetryableError(msg, { retryAfter: '5s' })` — there is no built-in exponential helper; compute the delay yourself based on `getStepMetadata().attempt` if you need one. |
|
|
@@ -180,23 +180,14 @@ A hook is an inbound write channel. The caller that knows the token resumes the
|
|
|
180
180
|
|
|
181
181
|
## Spawn a child workflow
|
|
182
182
|
|
|
183
|
-
`triggerAndWait()` splits into
|
|
183
|
+
`triggerAndWait()` splits into spawn and collect. In v5, call `start()` directly from the workflow to spawn the child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to collect the result.
|
|
184
184
|
|
|
185
185
|
You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler). The runtime serializes `Run` to its `runId` in the event log either way.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
import { start } from 'workflow/api';
|
|
189
|
-
|
|
190
|
-
async function spawnChild(item: string) {
|
|
191
|
-
'use step';
|
|
192
|
-
return start(childWorkflow, [item]); // [!code highlight]
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Await the result in a second step, then orchestrate both from the parent:
|
|
187
|
+
Await the result in a step, then orchestrate both from the parent:
|
|
197
188
|
|
|
198
189
|
```typescript title="workflow/workflows/parent.ts"
|
|
199
|
-
import { getRun } from 'workflow/api';
|
|
190
|
+
import { getRun, start } from 'workflow/api';
|
|
200
191
|
|
|
201
192
|
async function collectResult(runId: string) {
|
|
202
193
|
'use step';
|
|
@@ -206,12 +197,12 @@ async function collectResult(runId: string) {
|
|
|
206
197
|
|
|
207
198
|
export async function parentWorkflow(item: string) {
|
|
208
199
|
'use workflow';
|
|
209
|
-
const child = await
|
|
200
|
+
const child = await start(childWorkflow, [item]); // [!code highlight]
|
|
210
201
|
return await collectResult(child.runId);
|
|
211
202
|
}
|
|
212
203
|
```
|
|
213
204
|
|
|
214
|
-
To fan out, call `
|
|
205
|
+
To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
|
|
215
206
|
|
|
216
207
|
`Promise.all` rejects on first failure; use `Promise.allSettled` if you need batch-mode error tolerance similar to trigger.dev's `{ ok, output, error }` per-run result.
|
|
217
208
|
|
|
@@ -273,7 +264,7 @@ async function loadOrder(id: string) {
|
|
|
273
264
|
- `wait.for({ seconds | minutes | hours | days })` / `wait.until({ date })` → `sleep('5m')` or `sleep(date)` from `workflow`.
|
|
274
265
|
- `wait.forToken(token)` → `createHook({ token })` + `await`. Complete it with `resumeHook(token, payload)` from an API route.
|
|
275
266
|
- `wait.forToken({ timeout })` → `Promise.race([hook, sleep(timeout)])`.
|
|
276
|
-
- `triggerAndWait(payload)` →
|
|
267
|
+
- `triggerAndWait(payload)` → call `start(child, [payload])` from the workflow and return the `Run` object, then read the result with a step that calls `getRun(runId).returnValue`.
|
|
277
268
|
|
|
278
269
|
### Step 5: Start runs from the app
|
|
279
270
|
|
|
@@ -324,7 +315,7 @@ Throw `new RetryableError(msg, { retryAfter: '5s' })` to control delay between a
|
|
|
324
315
|
- Swap `wait.for` / `wait.until` for `sleep()` from `workflow`.
|
|
325
316
|
- Swap `wait.forToken` for `createHook()` (internal) or `createWebhook()` (HTTP).
|
|
326
317
|
- Model `wait.forToken` timeouts as `Promise.race()` between the hook and `sleep()`.
|
|
327
|
-
- Replace `triggerAndWait()` with `"use step"`
|
|
318
|
+
- Replace `triggerAndWait()` with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
|
|
328
319
|
- Replace `batch.triggerAndWait()` with `Promise.all` over the collected child `Run` handles.
|
|
329
320
|
- Move `schemaTask` validation to the call site; pass typed arguments into the workflow.
|
|
330
321
|
- Replace `AbortTaskRunError` with `FatalError`; model retries per step with `RetryableError` and `maxRetries`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.6",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
".": {
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
31
31
|
"workflow": "./dist/workflow.js",
|
|
32
|
+
"node": "./dist/index.js",
|
|
32
33
|
"require": "./dist/typescript-plugin.cjs",
|
|
33
34
|
"default": "./dist/index.js"
|
|
34
35
|
},
|
|
@@ -56,18 +57,18 @@
|
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"ms": "2.1.3",
|
|
59
|
-
"@workflow/astro": "5.0.0-beta.
|
|
60
|
-
"@workflow/cli": "5.0.0-beta.
|
|
61
|
-
"@workflow/core": "5.0.0-beta.
|
|
62
|
-
"@workflow/errors": "5.0.0-beta.
|
|
60
|
+
"@workflow/astro": "5.0.0-beta.6",
|
|
61
|
+
"@workflow/cli": "5.0.0-beta.6",
|
|
62
|
+
"@workflow/core": "5.0.0-beta.6",
|
|
63
|
+
"@workflow/errors": "5.0.0-beta.3",
|
|
63
64
|
"@workflow/typescript-plugin": "5.0.0-beta.3",
|
|
64
|
-
"@workflow/utils": "5.0.0-beta.
|
|
65
|
-
"@workflow/next": "5.0.0-beta.
|
|
66
|
-
"@workflow/nest": "5.0.0-beta.
|
|
67
|
-
"@workflow/
|
|
68
|
-
"@workflow/
|
|
69
|
-
"@workflow/
|
|
70
|
-
"@workflow/
|
|
65
|
+
"@workflow/utils": "5.0.0-beta.2",
|
|
66
|
+
"@workflow/next": "5.0.0-beta.6",
|
|
67
|
+
"@workflow/nest": "5.0.0-beta.6",
|
|
68
|
+
"@workflow/nitro": "5.0.0-beta.6",
|
|
69
|
+
"@workflow/nuxt": "5.0.0-beta.6",
|
|
70
|
+
"@workflow/sveltekit": "5.0.0-beta.6",
|
|
71
|
+
"@workflow/rollup": "5.0.0-beta.6"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@types/ms": "2.1.0",
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Distributed Abort Controller
|
|
3
|
-
description: A distributed AbortController that uses durable workflows for cross-process cancellation signaling.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Build a distributed abort controller that uses workflow streams and hooks to propagate cancellation signals across process boundaries.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
Use this pattern when you need an `AbortController`-like interface that works across distributed systems. The controller uses a durable workflow to coordinate cancellation — calling `.abort()` on one machine triggers the `.signal` on any other machine.
|
|
9
|
-
|
|
10
|
-
## When to use this
|
|
11
|
-
|
|
12
|
-
- **Cross-process cancellation** — Cancel a long-running operation from a different server, worker, or edge function
|
|
13
|
-
- **Durable cancellation** — The abort signal persists even if the process that created it crashes
|
|
14
|
-
- **UI stop buttons** — Let users cancel operations running on the server from the browser
|
|
15
|
-
- **Timeout coordination** — The built-in TTL auto-expires stale controllers
|
|
16
|
-
|
|
17
|
-
## Pattern
|
|
18
|
-
|
|
19
|
-
The `DistributedAbortController` class encapsulates a workflow that:
|
|
20
|
-
1. Accepts a user-provided unique ID (like a chat ID or task ID)
|
|
21
|
-
2. Creates or reconnects to an existing workflow using that ID
|
|
22
|
-
3. Waits for a hook signal OR TTL expiration
|
|
23
|
-
4. Writes a cancellation message to the run's stream when triggered
|
|
24
|
-
|
|
25
|
-
### Core Implementation
|
|
26
|
-
|
|
27
|
-
```typescript lineNumbers
|
|
28
|
-
import { defineHook, getWritable, sleep } from "workflow";
|
|
29
|
-
import { start, getRun, getHookByToken } from "workflow/api";
|
|
30
|
-
|
|
31
|
-
// Default TTL: 24 hours
|
|
32
|
-
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
33
|
-
// Default grace period: 1 hour (keeps hook alive after abort for late subscribers)
|
|
34
|
-
const DEFAULT_GRACE_MS = 60 * 60 * 1000;
|
|
35
|
-
|
|
36
|
-
// Hook to trigger the abort signal
|
|
37
|
-
export const abortHook = defineHook<{ reason?: string }>();
|
|
38
|
-
|
|
39
|
-
// The abort message written to the stream
|
|
40
|
-
export type AbortMessage = {
|
|
41
|
-
type: "abort";
|
|
42
|
-
reason?: string;
|
|
43
|
-
expired?: boolean;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Helper to create a consistent hook token from the user ID
|
|
47
|
-
function getAbortToken(id: string): string {
|
|
48
|
-
return `abort:${id}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Step function that writes the abort message to the stream
|
|
52
|
-
async function writeAbortSignal(reason?: string, expired?: boolean) {
|
|
53
|
-
"use step";
|
|
54
|
-
|
|
55
|
-
const writable = getWritable<AbortMessage>();
|
|
56
|
-
const writer = writable.getWriter();
|
|
57
|
-
try {
|
|
58
|
-
await writer.write({ type: "abort", reason, expired });
|
|
59
|
-
} finally {
|
|
60
|
-
writer.releaseLock();
|
|
61
|
-
}
|
|
62
|
-
await writable.close();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Workflow that waits for abort or TTL expiration
|
|
66
|
-
export async function abortControllerWorkflow(
|
|
67
|
-
id: string,
|
|
68
|
-
ttlMs: number,
|
|
69
|
-
graceMs: number
|
|
70
|
-
) {
|
|
71
|
-
"use workflow";
|
|
72
|
-
|
|
73
|
-
const startTime = Date.now();
|
|
74
|
-
const hook = abortHook.create({ token: getAbortToken(id) });
|
|
75
|
-
|
|
76
|
-
// Race: manual abort OR TTL expiration // [!code highlight]
|
|
77
|
-
const result = await Promise.race([
|
|
78
|
-
hook.then((payload) => ({
|
|
79
|
-
reason: payload.reason,
|
|
80
|
-
expired: false,
|
|
81
|
-
})),
|
|
82
|
-
sleep(`${ttlMs}ms`).then(() => ({
|
|
83
|
-
reason: "Controller expired",
|
|
84
|
-
expired: true,
|
|
85
|
-
})),
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
await writeAbortSignal(result.reason, result.expired);
|
|
89
|
-
|
|
90
|
-
// Only sleep through grace period on TTL expiration (keeps hook alive for late subscribers). // [!code highlight]
|
|
91
|
-
// Manual aborts complete immediately.
|
|
92
|
-
if (result.expired) {
|
|
93
|
-
const elapsed = Date.now() - startTime;
|
|
94
|
-
const remainingTime = graceMs - (elapsed - ttlMs);
|
|
95
|
-
if (remainingTime > 0) {
|
|
96
|
-
await sleep(`${remainingTime}ms`); // [!code highlight]
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return { aborted: true, reason: result.reason, expired: result.expired };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* A distributed abort controller that works across process boundaries.
|
|
105
|
-
* Uses a semantically meaningful ID (like a chat ID or task ID) to coordinate.
|
|
106
|
-
*/
|
|
107
|
-
export class DistributedAbortController {
|
|
108
|
-
private id: string;
|
|
109
|
-
readonly runId: string;
|
|
110
|
-
|
|
111
|
-
private constructor(id: string, runId: string) {
|
|
112
|
-
this.id = id;
|
|
113
|
-
this.runId = runId;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Creates or reconnects to a distributed abort controller.
|
|
118
|
-
* If a controller with this ID already exists, reconnects to it.
|
|
119
|
-
* Otherwise, starts a new workflow.
|
|
120
|
-
*
|
|
121
|
-
* @param id - A unique, semantically meaningful ID (e.g., "chat:123")
|
|
122
|
-
* @param options.ttlMs - Time-to-live in ms (default: 24 hours)
|
|
123
|
-
* @param options.graceMs - Grace period after abort (default: 1 hour)
|
|
124
|
-
*/
|
|
125
|
-
static async create( // [!code highlight]
|
|
126
|
-
id: string,
|
|
127
|
-
options: { ttlMs?: number; graceMs?: number } = {}
|
|
128
|
-
): Promise<DistributedAbortController> {
|
|
129
|
-
const { ttlMs = DEFAULT_TTL_MS, graceMs = DEFAULT_GRACE_MS } = options;
|
|
130
|
-
const token = getAbortToken(id);
|
|
131
|
-
|
|
132
|
-
// Try to find an existing run with this hook token
|
|
133
|
-
const existingHook = await getHookByToken(token).catch(() => null); // [!code highlight]
|
|
134
|
-
|
|
135
|
-
if (existingHook) {
|
|
136
|
-
// Reconnect to existing controller
|
|
137
|
-
return new DistributedAbortController(id, existingHook.runId);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Create a new workflow
|
|
141
|
-
const run = await start(abortControllerWorkflow, [id, ttlMs, graceMs]); // [!code highlight]
|
|
142
|
-
return new DistributedAbortController(id, run.runId);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Triggers the abort signal.
|
|
147
|
-
* Idempotent: safe to call multiple times or after the workflow has completed.
|
|
148
|
-
*/
|
|
149
|
-
async abort(reason?: string): Promise<void> { // [!code highlight]
|
|
150
|
-
try {
|
|
151
|
-
await abortHook.resume(getAbortToken(this.id), { reason });
|
|
152
|
-
} catch (error) {
|
|
153
|
-
const msg = error instanceof Error ? error.message.toLowerCase() : '';
|
|
154
|
-
if (msg.includes('not found') || msg.includes('expired')) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Returns an AbortSignal that fires when abort() is called or TTL expires.
|
|
163
|
-
* The signal fires with a reason indicating what triggered it.
|
|
164
|
-
*/
|
|
165
|
-
get signal(): AbortSignal { // [!code highlight]
|
|
166
|
-
const run = getRun<{ aborted: boolean; reason?: string; expired?: boolean }>(this.runId);
|
|
167
|
-
const controller = new AbortController();
|
|
168
|
-
const readable = run.getReadable<AbortMessage>();
|
|
169
|
-
|
|
170
|
-
(async () => {
|
|
171
|
-
const reader = readable.getReader();
|
|
172
|
-
try {
|
|
173
|
-
while (true) {
|
|
174
|
-
const { done, value } = await reader.read();
|
|
175
|
-
if (done) break;
|
|
176
|
-
if (value.type === "abort") {
|
|
177
|
-
const reason = value.expired
|
|
178
|
-
? `${value.reason} (expired)`
|
|
179
|
-
: value.reason;
|
|
180
|
-
controller.abort(reason);
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (!controller.signal.aborted) {
|
|
186
|
-
controller.abort(
|
|
187
|
-
error instanceof Error ? error.message : "Stream read failed"
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
} finally {
|
|
191
|
-
reader.releaseLock();
|
|
192
|
-
}
|
|
193
|
-
})();
|
|
194
|
-
|
|
195
|
-
return controller.signal;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Usage: Single Process
|
|
201
|
-
|
|
202
|
-
```typescript lineNumbers
|
|
203
|
-
import { DistributedAbortController } from "./distributed-abort-controller";
|
|
204
|
-
|
|
205
|
-
// Create a controller with a meaningful ID
|
|
206
|
-
const controller = await DistributedAbortController.create("chat:user-123");
|
|
207
|
-
|
|
208
|
-
// Get the signal and use it with fetch
|
|
209
|
-
const signal = controller.signal;
|
|
210
|
-
const response = await fetch("https://api.example.com/long-operation", {
|
|
211
|
-
signal,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Later: abort the operation
|
|
215
|
-
await controller.abort("User cancelled");
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### Usage: Cross-Process Coordination
|
|
219
|
-
|
|
220
|
-
```typescript lineNumbers
|
|
221
|
-
import { DistributedAbortController } from "./distributed-abort-controller";
|
|
222
|
-
|
|
223
|
-
// Process A: Create the controller
|
|
224
|
-
const controller = await DistributedAbortController.create("task:build-123");
|
|
225
|
-
// start long operation using controller.signal...
|
|
226
|
-
|
|
227
|
-
// Process B: Reconnect and abort (no run ID sharing needed!)
|
|
228
|
-
const sameController = await DistributedAbortController.create("task:build-123"); // [!code highlight]
|
|
229
|
-
await sameController.abort("Cancelled by admin");
|
|
230
|
-
|
|
231
|
-
// Process C: Reconnect and listen
|
|
232
|
-
const anotherRef = await DistributedAbortController.create("task:build-123");
|
|
233
|
-
anotherRef.signal.addEventListener("abort", (e) => {
|
|
234
|
-
console.log("Task was cancelled:", (e.target as AbortSignal).reason);
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Custom TTL
|
|
239
|
-
|
|
240
|
-
```typescript lineNumbers
|
|
241
|
-
import { DistributedAbortController } from "./distributed-abort-controller";
|
|
242
|
-
|
|
243
|
-
// Short-lived controller for a quick operation (5 minutes)
|
|
244
|
-
const shortLived = await DistributedAbortController.create("quick-task", {
|
|
245
|
-
ttlMs: 5 * 60 * 1000,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Long-lived controller for batch jobs (7 days)
|
|
249
|
-
const longLived = await DistributedAbortController.create("batch-job", {
|
|
250
|
-
ttlMs: 7 * 24 * 60 * 60 * 1000,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// When TTL expires, the signal fires with expired reason
|
|
254
|
-
shortLived.signal.addEventListener("abort", (e) => {
|
|
255
|
-
const reason = (e.target as AbortSignal).reason;
|
|
256
|
-
if (reason?.includes("expired")) {
|
|
257
|
-
console.log("Controller expired, cleaning up...");
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### API Route for Remote Abort
|
|
263
|
-
|
|
264
|
-
```typescript lineNumbers
|
|
265
|
-
import { DistributedAbortController } from "@/lib/distributed-abort-controller";
|
|
266
|
-
|
|
267
|
-
export async function POST(
|
|
268
|
-
request: Request,
|
|
269
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
270
|
-
) {
|
|
271
|
-
const { id } = await params;
|
|
272
|
-
const { reason } = await request.json();
|
|
273
|
-
|
|
274
|
-
const controller = await DistributedAbortController.create(id);
|
|
275
|
-
await controller.abort(reason || "Cancelled via API");
|
|
276
|
-
|
|
277
|
-
return Response.json({ success: true });
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Client Cancel Button
|
|
282
|
-
|
|
283
|
-
```tsx lineNumbers
|
|
284
|
-
"use client";
|
|
285
|
-
|
|
286
|
-
export function CancelButton({ taskId }: { taskId: string }) {
|
|
287
|
-
const handleCancel = async () => {
|
|
288
|
-
await fetch(`/api/abort/${taskId}`, {
|
|
289
|
-
method: "POST",
|
|
290
|
-
headers: { "Content-Type": "application/json" },
|
|
291
|
-
body: JSON.stringify({ reason: "User clicked cancel" }),
|
|
292
|
-
});
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
return (
|
|
296
|
-
<button type="button" onClick={handleCancel}>
|
|
297
|
-
Cancel Operation
|
|
298
|
-
</button>
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## Tips
|
|
304
|
-
|
|
305
|
-
- **Use semantic IDs** — Use meaningful IDs like `chat:123` or `task:abc` instead of random UUIDs
|
|
306
|
-
- **Create is idempotent** — Calling `create()` with the same ID reconnects to the existing controller
|
|
307
|
-
- **TTL auto-cleanup** — Workflows self-terminate after TTL expires; no manual cleanup needed
|
|
308
|
-
- **Signal is a getter** — Each access to `.signal` creates a new listener; cache it if needed
|
|
309
|
-
- **One-shot** — Once aborted or expired, the workflow completes; create a new controller for new operations
|
|
310
|
-
|
|
311
|
-
## Key APIs
|
|
312
|
-
|
|
313
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the abort trigger
|
|
314
|
-
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — write abort messages to the stream
|
|
315
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep) — TTL timer for auto-expiration
|
|
316
|
-
- [`start()`](/docs/api-reference/workflow-api/start) — start the abort controller workflow
|
|
317
|
-
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) — find existing run by hook token
|
|
318
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — reconnect to the workflow's readable stream
|