@bobtail.software/b-durable 1.0.5 → 1.0.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/README.md +138 -163
- package/dist/compiler/cli.mjs +41 -703
- package/dist/index.d.mts +136 -18
- package/dist/index.mjs +1 -551
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,232 +1,207 @@
|
|
|
1
|
-
|
|
2
|
-
# `@bobtail.software/b-durable`: Composable, Type-Safe, Durable Workflows for TypeScript
|
|
1
|
+
# `b-durable`: Composable, Type-Safe, Durable Workflows for TypeScript
|
|
3
2
|
|
|
4
3
|

|
|
5
4
|

|
|
6
5
|
|
|
7
|
-
`b-durable` is a
|
|
6
|
+
`b-durable` is a production-ready system that transforms standard `async` functions into **composable, interactive, durable, and resilient workflows**. It lets you write long-running business logic—spanning hours, days, or months—as simple, linear `async/await` code. The system handles state persistence, orchestration, crash recovery, strict versioning, and observability.
|
|
8
7
|
|
|
9
8
|
## The Problem
|
|
10
9
|
|
|
11
10
|
Standard `async/await` is great for short-lived operations, but it breaks down for complex, long-running processes:
|
|
12
11
|
|
|
13
12
|
1. **Fragility**: If your server restarts mid-execution, all in-memory state is lost.
|
|
14
|
-
2. **Inefficiency**: An operation like `await bSleep('7 days')` is impossible
|
|
15
|
-
3. **
|
|
13
|
+
2. **Inefficiency**: An operation like `await bSleep('7 days')` is impossible standard Node.js.
|
|
14
|
+
3. **Operational Blindness**: It's hard to know the state of a multi-step process running across distributed services.
|
|
15
|
+
4. **Deployment Risks**: Deploying new code while old processes are running can corrupt memory/state.
|
|
16
16
|
|
|
17
17
|
## The `b-durable` Solution
|
|
18
18
|
|
|
19
|
-
`b-durable` allows you to express this complexity as a single, readable `async` function. The system automatically persists the workflow's state after each `await` step
|
|
19
|
+
`b-durable` allows you to express this complexity as a single, readable `async` function. The system automatically persists the workflow's state after each `await` step in Redis.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Key Capabilities
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
// 2. Pause durably for fraud checks
|
|
33
|
-
await context.bSleep('30m');
|
|
34
|
-
return { transactionId: result.transactionId };
|
|
35
|
-
},
|
|
36
|
-
});
|
|
23
|
+
- **🛡️ Strict Versioning**: Prevents state corruption by ensuring running workflows only execute code compatible with their version.
|
|
24
|
+
- **💀 The Reaper (Reliability)**: Automatically detects crashed workers and recovers "lost" tasks, ensuring zero data loss.
|
|
25
|
+
- **👁️ Observability First**: Injectable Loggers, `getState` inspection API, and detailed tracking.
|
|
26
|
+
- **♻️ Dead Letter Queue (DLQ)**: Automatic retries for failed tasks; moves persistently failing tasks to a DLQ for manual inspection.
|
|
27
|
+
- **🛑 Cancellation**: Gracefully cancel running workflows with support for cleanup logic (`try/catch/finally`).
|
|
28
|
+
- **🧹 Auto-Retention**: Automatically expire completed/failed workflows from Redis to manage storage costs.
|
|
29
|
+
|
|
30
|
+
## Example: E-Commerce Order
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
```typescript
|
|
33
|
+
import { bDurable } from '@bobtail.software/b-durable';
|
|
34
|
+
|
|
35
|
+
// Define contracts
|
|
36
|
+
interface OrderEvents { 'order.approved': { approverId: string }; }
|
|
37
|
+
interface OrderSignals { 'status.update': { status: string }; }
|
|
38
|
+
|
|
39
|
+
export const orderProcessingWorkflow = bDurable<
|
|
40
|
+
{ orderId: string; amount: number },
|
|
41
|
+
{ status: 'completed' | 'failed' },
|
|
42
|
+
OrderEvents,
|
|
43
|
+
OrderSignals
|
|
44
|
+
>({
|
|
45
|
+
// VERSIONING IS MANDATORY
|
|
46
|
+
version: '1.0',
|
|
47
|
+
workflow: async (input, context) => {
|
|
41
48
|
try {
|
|
42
|
-
// 1.
|
|
43
|
-
const payment = await context.bExecute(paymentWorkflow, {
|
|
44
|
-
|
|
49
|
+
// 1. Execute sub-workflow
|
|
50
|
+
const payment = await context.bExecute(paymentWorkflow, { amount: input.amount });
|
|
51
|
+
|
|
52
|
+
// 2. Emit non-blocking signal
|
|
53
|
+
await context.bSignal('status.update', { status: 'paid' });
|
|
45
54
|
|
|
46
|
-
//
|
|
55
|
+
// 3. Wait for external event (human approval)
|
|
47
56
|
const approval = await context.bWaitForEvent('order.approved');
|
|
48
57
|
context.log(`Order approved by ${approval.approverId}`);
|
|
49
58
|
|
|
50
|
-
//
|
|
59
|
+
// 4. Schedule shipping
|
|
51
60
|
await shipOrder(input.orderId);
|
|
52
|
-
|
|
61
|
+
|
|
53
62
|
return { status: 'completed' };
|
|
54
63
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// 5. Handle errors (and cancellations!) durably
|
|
65
|
+
if (error.isCancellation) {
|
|
66
|
+
await releaseInventory(input.orderId); // Cleanup
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
await notifyFailure(input.orderId);
|
|
70
|
+
return { status: 'failed' };
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
73
|
});
|
|
62
74
|
```
|
|
63
75
|
|
|
64
|
-
## Core Features
|
|
65
|
-
|
|
66
|
-
- **Composable Orchestration**: Workflows can call other workflows using `await context.bExecute()`, allowing you to build complex processes from smaller, reusable parts. Results and errors are propagated automatically.
|
|
67
|
-
- **Interactive & Event-Driven**: Pause a workflow indefinitely with `await context.bWaitForEvent()` until an external event is received, enabling human-in-the-loop patterns and webhook integrations.
|
|
68
|
-
- **Durable & Resilient**: Workflows survive server restarts, crashes, and deployments, resuming exactly where they left off.
|
|
69
|
-
- **Built-in Error Handling**: Use standard `try/catch` blocks to handle errors from tasks or sub-workflows. Your `catch` block will execute reliably, even if the failure occurs hours after the `try` block started.
|
|
70
|
-
- **Durable Timers**: Use `await context.bSleep('30 days')` to pause workflows for extended periods without consuming server resources.
|
|
71
|
-
- **Type Safety End-to-End**: Leverages TypeScript for type safety across steps, I/O, events, and workflow composition.
|
|
72
|
-
- **Compiler-Powered**: A smart CLI compiler transforms your workflows into a step-by-step executable format, preserving types and ensuring runtime correctness.
|
|
73
|
-
|
|
74
|
-
## How It Works: Compiler + Runtime
|
|
75
|
-
|
|
76
|
-
1. **The Smart Compiler (`b-durable-compiler`)**:
|
|
77
|
-
Analyzes your workflow files (`*.workflow.ts`). For each function wrapped in `bDurable(...)`, it:
|
|
78
|
-
- **Maps Control Flow**: Breaks the function into steps at each `await`, analyzing `if/else` and `try/catch` blocks to build a complete state machine.
|
|
79
|
-
- **Identifies Durable Calls**: Differentiates between a durable instruction (`context.bSleep`, `context.bExecute`) and a standard service task call.
|
|
80
|
-
- **Generates Durable Artifacts**: Produces compiled `.mts` files that the runtime can execute step-by-step.
|
|
81
|
-
|
|
82
|
-
2. **The Durable Runtime**:
|
|
83
|
-
The engine that executes the compiled workflows.
|
|
84
|
-
- **State Persistence**: Uses Redis to store the state, step, and context of every workflow instance.
|
|
85
|
-
- **Orchestration Logic**: Manages parent/child workflow relationships, passing results and errors up the chain.
|
|
86
|
-
- **Event System**: Tracks which workflows are waiting for which events.
|
|
87
|
-
- **Task Queue & Scheduler**: Reliably executes service function calls and manages long-running timers.
|
|
88
|
-
|
|
89
76
|
## Getting Started
|
|
90
77
|
|
|
91
78
|
### 1. Installation
|
|
92
79
|
|
|
93
|
-
Install the core library and its peer dependencies. We also highly recommend the ESLint plugin for the best developer experience.
|
|
94
|
-
|
|
95
80
|
```bash
|
|
96
|
-
pnpm add @bobtail.software/b-durable ioredis
|
|
81
|
+
pnpm add @bobtail.software/b-durable ioredis ms
|
|
97
82
|
pnpm add -D @bobtail.software/eslint-plugin-b-durable
|
|
98
83
|
```
|
|
99
84
|
|
|
100
|
-
### 2.
|
|
101
|
-
|
|
102
|
-
Our ESLint plugin prevents common errors by flagging unsupported code constructs (like loops) inside your workflows.
|
|
103
|
-
|
|
104
|
-
**In `eslint.config.js` (Flat Config):**
|
|
105
|
-
```javascript
|
|
106
|
-
import bDurablePlugin from '@bobtail.software/eslint-plugin-b-durable';
|
|
107
|
-
|
|
108
|
-
export default [
|
|
109
|
-
// ... your other configs
|
|
110
|
-
bDurablePlugin.configs.recommended,
|
|
111
|
-
];
|
|
112
|
-
```
|
|
113
|
-
For legacy `.eslintrc.js` setup, see the [plugin's documentation](link-to-your-eslint-plugin-readme).
|
|
114
|
-
|
|
115
|
-
### 3. Define a Workflow
|
|
85
|
+
### 2. Define a Workflow
|
|
116
86
|
|
|
117
|
-
Create a
|
|
87
|
+
Create a `.workflow.ts` file. Note the mandatory `version` field.
|
|
118
88
|
|
|
119
89
|
```typescript
|
|
120
|
-
// src/workflows/
|
|
121
|
-
import { bDurable
|
|
122
|
-
import {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const user = await createUser({ id: input.userId, email: input.email });
|
|
132
|
-
|
|
133
|
-
await context.bSleep('10s');
|
|
134
|
-
|
|
135
|
-
await sendWelcomeEmail(user.email);
|
|
136
|
-
|
|
137
|
-
return { status: 'completed', userId: user.id };
|
|
90
|
+
// src/workflows/user.workflow.ts
|
|
91
|
+
import { bDurable } from '@bobtail.software/b-durable';
|
|
92
|
+
import { sendEmail } from '../services';
|
|
93
|
+
|
|
94
|
+
export const userOnboarding = bDurable({
|
|
95
|
+
name: 'userOnboarding',
|
|
96
|
+
version: '1.0', // Required for safety
|
|
97
|
+
workflow: async (input: { email: string }, context) => {
|
|
98
|
+
await context.bSleep('1 day');
|
|
99
|
+
await sendEmail(input.email, 'Welcome!');
|
|
100
|
+
return 'sent';
|
|
138
101
|
},
|
|
139
102
|
});
|
|
140
103
|
```
|
|
141
104
|
|
|
142
|
-
###
|
|
143
|
-
|
|
144
|
-
Add a script to your `package.json` to run the compiler.
|
|
105
|
+
### 3. Compile
|
|
145
106
|
|
|
107
|
+
Add to `package.json`:
|
|
146
108
|
```json
|
|
147
|
-
// package.json
|
|
148
109
|
"scripts": {
|
|
149
110
|
"compile-workflows": "b-durable-compiler --in src/workflows --out src/generated"
|
|
150
111
|
}
|
|
151
112
|
```
|
|
152
|
-
Run `pnpm compile-workflows`.
|
|
113
|
+
Run `pnpm compile-workflows`.
|
|
153
114
|
|
|
154
|
-
###
|
|
115
|
+
### 4. Initialize the Runtime
|
|
155
116
|
|
|
156
|
-
|
|
117
|
+
Initialize the system with Redis connections and configuration options.
|
|
157
118
|
|
|
158
119
|
```typescript
|
|
159
120
|
// src/main.ts
|
|
160
121
|
import { bDurableInitialize } from '@bobtail.software/b-durable';
|
|
161
122
|
import Redis from 'ioredis';
|
|
162
|
-
import durableFunctions
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// --- Start a workflow with a predictable, user-provided ID ---
|
|
187
|
-
const orderId = `order-abc-123`;
|
|
188
|
-
const workflowId2 = await durableSystem.start(userOnboardingWorkflow, {
|
|
189
|
-
workflowId: orderId,
|
|
190
|
-
input: {
|
|
191
|
-
userId: 'user-from-order-123',
|
|
192
|
-
email: 'customer@example.com',
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
console.log(`Workflow started with predictable ID: ${workflowId2}`);
|
|
196
|
-
// Now you can easily send events using the 'orderId'
|
|
197
|
-
// durableSystem.sendEvent(..., orderId, ...);
|
|
198
|
-
}
|
|
123
|
+
import durableFunctions from './generated';
|
|
124
|
+
import { myLogger } from './logger'; // Your Winston/Pino logger
|
|
125
|
+
|
|
126
|
+
const durableSystem = bDurableInitialize({
|
|
127
|
+
durableFunctions,
|
|
128
|
+
sourceRoot: process.cwd(),
|
|
129
|
+
redisClient: new Redis(),
|
|
130
|
+
blockingRedisClient: new Redis(), // Dedicated connection for queues
|
|
131
|
+
|
|
132
|
+
// --- Production Configuration ---
|
|
133
|
+
retention: '7 days', // Auto-delete finished workflows after 7 days
|
|
134
|
+
pollingInterval: 5000, // Scheduler/Heartbeat frequency (default: 5000ms)
|
|
135
|
+
logger: { // Inject your logger for better observability
|
|
136
|
+
info: (msg, meta) => myLogger.info(msg, meta),
|
|
137
|
+
error: (msg, meta) => myLogger.error(msg, meta),
|
|
138
|
+
warn: (msg, meta) => myLogger.warn(msg, meta),
|
|
139
|
+
debug: (msg, meta) => myLogger.debug(msg, meta),
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Start a workflow
|
|
144
|
+
const { workflowId } = await durableSystem.start(userOnboarding, {
|
|
145
|
+
input: { email: 'test@example.com' }
|
|
146
|
+
});
|
|
199
147
|
|
|
200
|
-
|
|
148
|
+
// Inspect state in real-time
|
|
149
|
+
const state = await durableSystem.getState(workflowId);
|
|
150
|
+
console.log(`Current step: ${state.step}, Status: ${state.status}`);
|
|
201
151
|
```
|
|
202
152
|
|
|
203
|
-
|
|
153
|
+
## Advanced Features
|
|
154
|
+
|
|
155
|
+
### Strict Versioning & Deployment
|
|
156
|
+
|
|
157
|
+
When you modify a workflow, you **must** increment the `version` string (e.g., `'1.0'` -> `'1.1'`).
|
|
204
158
|
|
|
205
|
-
|
|
159
|
+
* **Runtime Check**: Before executing a step, the worker checks if the database version matches the code version.
|
|
160
|
+
* **Mismatch**: If versions differ, the workflow halts with status `VERSION_MISMATCH`. This prevents "Frankenstein" workflows where step 2 of version 1 tries to run step 3 of version 2.
|
|
161
|
+
* **Strategy**: Run new workers alongside old workers (Blue/Green) or drain queues before deploying breaking changes.
|
|
206
162
|
|
|
207
|
-
|
|
163
|
+
### Reliability & The Reaper
|
|
208
164
|
|
|
209
|
-
|
|
165
|
+
* **Heartbeats**: Every worker sends a heartbeat to Redis every few seconds.
|
|
166
|
+
* **The Reaper**: If a worker crashes (OOM, power failure) while holding a task, the Reaper detects the missing heartbeat and automatically re-queues the task for another worker. No manual intervention required.
|
|
210
167
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
168
|
+
### Error Handling & Dead Letter Queue (DLQ)
|
|
169
|
+
|
|
170
|
+
* **Retries**: Tasks are automatically retried 3 times on failure with backoff.
|
|
171
|
+
* **DLQ**: After 3 failures, the task payload and error stack are moved to the Redis list `queue:dead`.
|
|
172
|
+
* **Sub-workflows**: Failures in sub-workflows bubble up to the parent as standard JavaScript exceptions, catchable with `try/catch`.
|
|
173
|
+
|
|
174
|
+
### Cancellation
|
|
175
|
+
|
|
176
|
+
You can cancel a running workflow at any time.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
await durableSystem.cancel(workflowId, 'User requested cancellation');
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Inside the workflow, this throws a `WorkflowCancellationError`. You can catch this to perform cleanup (e.g., reverting a payment) before re-throwing or returning.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
try {
|
|
186
|
+
await context.bWaitForEvent('approval');
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (e.isCancellation) {
|
|
189
|
+
await refundPayment();
|
|
190
|
+
// Workflow ends here as CANCELLED
|
|
191
|
+
}
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
217
195
|
|
|
218
|
-
|
|
219
|
-
This command builds the library, compiles example workflows, and starts the example app with hot-reloading.
|
|
220
|
-
```bash
|
|
221
|
-
pnpm dev
|
|
222
|
-
```
|
|
196
|
+
## Architecture
|
|
223
197
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
198
|
+
1. **Compiler**: Analyzes `await` points and transforms code into a deterministic state machine.
|
|
199
|
+
2. **Redis**: Stores state, task queues, locks, and signals.
|
|
200
|
+
3. **Runtime**:
|
|
201
|
+
* **Dispatcher**: Routes tasks to service functions.
|
|
202
|
+
* **Scheduler**: Manages timers (`bSleep`) and the Reaper.
|
|
203
|
+
* **Signal Bus**: Uses Redis Pub/Sub for real-time communication.
|
|
229
204
|
|
|
230
205
|
## License
|
|
231
206
|
|
|
232
|
-
|
|
207
|
+
GPL-3.0
|