@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 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
  ![NPM Version](https://img.shields.io/npm/v/@bobtail.software/b-durable.svg)
5
4
  ![License](https://img.shields.io/npm/l/@bobtail.software/eslint-plugin-b-durable.svg)
6
5
 
7
- `b-durable` is a powerful 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, external events, and crash recovery, allowing you to focus on your business logic.
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. It would hold a process hostage, consume resources, and wouldn't survive a single deployment.
15
- 3. **Orchestration Complexity**: Coordinating processes that involve multiple services, human-in-the-loop steps (like approvals), or external system webhooks often leads to a tangled mess of state machines, queues, and database flags.
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, ensuring it can resume from the exact point of interruption.
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
- Imagine orchestrating an e-commerce order. With `b-durable`, the code is as clear as the business process itself:
21
+ ### Key Capabilities
22
22
 
23
- ```typescript
24
- // Define a reusable sub-workflow for handling payments
25
- export const paymentWorkflow = bDurable({
26
- workflow: async (input: { orderId: string; amount: number }, context) => {
27
- // 1. Call a service to process the payment
28
- const result = await processPayment({ orderId: input.orderId, amount: input.amount });
29
- if (!result.success) {
30
- throw new Error('Payment failed!');
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
- // Define the main order processing workflow
39
- export const orderProcessingWorkflow = bDurable({
40
- workflow: async (input: { orderId: string; items: Item[] }, context) => {
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. Call another workflow and await its result
43
- const payment = await context.bExecute(paymentWorkflow, { orderId: input.orderId, amount: 99.99 });
44
- context.log(`Payment successful: ${payment.transactionId}`);
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
- // 2. Pause and wait for an external event (e.g., from a UI or webhook)
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
- // 3. Call the final service function
59
+ // 4. Schedule shipping
51
60
  await shipOrder(input.orderId);
52
-
61
+
53
62
  return { status: 'completed' };
54
63
  } catch (error) {
55
- // 4. Handle errors durably
56
- await notifyCustomerOfFailure(input.orderId, error.message);
57
- await cancelOrder(input.orderId);
58
- return { status: 'failed', reason: error.message };
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. Set Up ESLint (Highly Recommended)
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 file ending in `.workflow.ts`. The `(input, context)` signature gives you access to durable functions.
87
+ Create a `.workflow.ts` file. Note the mandatory `version` field.
118
88
 
119
89
  ```typescript
120
- // src/workflows/onboarding.workflow.ts
121
- import { bDurable, DurableContext } from '@bobtail.software/b-durable';
122
- import { createUser, sendWelcomeEmail } from '../services';
123
-
124
- interface OnboardingInput {
125
- userId: string;
126
- email: string;
127
- }
128
-
129
- export const userOnboardingWorkflow = bDurable({
130
- workflow: async (input: OnboardingInput, context: DurableContext) => {
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
- ### 4. Compile Workflows
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`. This generates the durable definitions in `src/generated`.
113
+ Run `pnpm compile-workflows`.
153
114
 
154
- ### 5. Initialize the Runtime
115
+ ### 4. Initialize the Runtime
155
116
 
156
- In your application's entry point, initialize the system and start a workflow.
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, { userOnboardingWorkflow } from './generated';
163
-
164
- async function main() {
165
- const redis = new Redis();
166
- const blockingRedis = new Redis(); // Required for reliable queue operations
167
-
168
- const durableSystem = bDurableInitialize({
169
- durableFunctions,
170
- sourceRoot: process.cwd(),
171
- redisClient: redis,
172
- blockingRedisClient: blockingRedis,
173
- });
174
-
175
- console.log('Durable system ready. Starting workflows...');
176
-
177
- // --- Start a workflow with a system-generated ID ---
178
- const workflowId1 = await durableSystem.start(userOnboardingWorkflow, {
179
- input: {
180
- userId: `user-${Date.now()}`,
181
- email: 'test.user@example.com',
182
- }
183
- });
184
- console.log(`Workflow ${workflowId1} started.`);
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
- main().catch(console.error);
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
- ### 6. Run Your Application
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
- Run your app (`node src/main.ts`). You'll see the workflow execute, pause, and resume, with all its state managed by `b-durable`.
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
- ## Development Setup (for contributors)
163
+ ### Reliability & The Reaper
208
164
 
209
- The project is a `pnpm` monorepo.
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
- 1. **Clone & Install**:
212
- ```bash
213
- git clone <repository-url>
214
- cd b-durable-monorepo
215
- pnpm install
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
- 2. **Run in Development Mode**:
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
- 3. **Run Tests**:
225
- Tests use Vitest and a real Redis instance.
226
- ```bash
227
- pnpm --filter @bobtail.software/b-durable test
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
- This project is licensed under the GPL-3.0 License. See the [LICENSE](LICENSE) file for details.
207
+ GPL-3.0