@jagreehal/workflow 1.13.0 → 1.15.0

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.
@@ -0,0 +1,396 @@
1
+ # Logging Workflow Events to Pino
2
+
3
+ Workflow events are structured JSON objects that work perfectly with Pino's structured logging.
4
+
5
+ ## Using `renderAs('logger')` (Recommended)
6
+
7
+ The easiest way to log workflow data is using the built-in logger renderer:
8
+
9
+ ```typescript
10
+ import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
11
+ import pino from 'pino';
12
+
13
+ const logger = pino();
14
+ const viz = createVisualizer({ workflowName: 'checkout' });
15
+
16
+ const workflow = createWorkflow({ fetchUser, processPayment }, {
17
+ onEvent: viz.handleEvent,
18
+ });
19
+
20
+ const result = await workflow(async (step) => {
21
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
22
+ await step(() => processPayment(user.id), { name: 'Process payment' });
23
+ });
24
+
25
+ // Log structured workflow data
26
+ // Note: renderAs() returns a JSON string for API consistency across renderers
27
+ const logData = JSON.parse(viz.renderAs('logger'));
28
+ logger.info(logData, 'Workflow completed');
29
+ ```
30
+
31
+ ### Logger Output
32
+
33
+ The renderer returns this structure (Pino will add `level`, `time`, and `msg` when logging):
34
+
35
+ ```json
36
+ {
37
+ "workflow": {
38
+ "id": "8757b92c-d415-4555-93cd-37076cbec9ee",
39
+ "name": "checkout",
40
+ "state": "success",
41
+ "durationMs": 33,
42
+ "startedAt": 1768506411038
43
+ },
44
+ "steps": [
45
+ {
46
+ "id": "user:1",
47
+ "name": "Fetch user",
48
+ "state": "success",
49
+ "durationMs": 11
50
+ },
51
+ {
52
+ "id": "payment:1",
53
+ "name": "Process payment",
54
+ "state": "success",
55
+ "durationMs": 21,
56
+ "retryCount": 1
57
+ }
58
+ ],
59
+ "summary": {
60
+ "totalSteps": 2,
61
+ "successCount": 2,
62
+ "errorCount": 0,
63
+ "cacheHits": 0,
64
+ "totalRetries": 1,
65
+ "slowestStep": { "name": "Process payment", "durationMs": 21 }
66
+ },
67
+ "diagram": "┌── checkout ─────────────────────┐\n│ ✓ Fetch user [11ms] │\n│ ✓ Process payment [21ms] │\n└─────────────────────────────────┘"
68
+ }
69
+ ```
70
+
71
+ ### Works With Any Logger
72
+
73
+ The logger renderer is logger-agnostic:
74
+
75
+ ```typescript
76
+ // Pino
77
+ logger.info(JSON.parse(viz.renderAs('logger')), 'Workflow completed');
78
+
79
+ // Winston
80
+ logger.info('Workflow completed', JSON.parse(viz.renderAs('logger')));
81
+
82
+ // Console
83
+ console.log('Workflow completed:', JSON.parse(viz.renderAs('logger')));
84
+
85
+ // OpenTelemetry
86
+ span.addEvent('workflow_completed', JSON.parse(viz.renderAs('logger')));
87
+ ```
88
+
89
+ ### Why Use `renderAs('logger')`?
90
+
91
+ - **Structured data**: Workflow state, step details, and summary stats as queryable fields
92
+ - **Summary calculations**: Slowest step, retry counts, error counts computed for you
93
+ - **ASCII diagram included**: ANSI colors stripped for clean JSON
94
+ - **One call**: No need to parse events manually
95
+
96
+ ### When to Use Each Approach
97
+
98
+ | Use Case | Recommended Approach |
99
+ |----------|---------------------|
100
+ | Log workflow summary after completion | `renderAs('logger')` |
101
+ | Need custom filtering of steps/events | Manual approach |
102
+ | Real-time logging during execution | Manual approach with `onEvent` |
103
+ | Just want structured output quickly | `renderAs('logger')` |
104
+ | Need to transform events before logging | Manual approach |
105
+
106
+ ---
107
+
108
+ ## Manual Approach (More Control)
109
+
110
+ For more control over what gets logged, you can collect events manually.
111
+
112
+ ## Basic Example
113
+
114
+ ```typescript
115
+ import { createWorkflow, createEventCollector } from '@jagreehal/workflow';
116
+ import pino from 'pino';
117
+
118
+ const logger = pino();
119
+
120
+ const collector = createEventCollector();
121
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
122
+ onEvent: collector.handleEvent,
123
+ });
124
+
125
+ const result = await workflow(async (step) => {
126
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
127
+ const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
128
+ return { user, posts };
129
+ });
130
+
131
+ // After workflow completes, log all events
132
+ const events = collector.getWorkflowEvents();
133
+
134
+ // Log summary
135
+ if (result.ok) {
136
+ logger.info({
137
+ workflowId: events[0]?.workflowId,
138
+ duration: events.find(e => e.type === 'workflow_success')?.durationMs,
139
+ stepCount: events.filter(e => e.type === 'step_success').length,
140
+ }, 'Workflow completed successfully');
141
+ } else {
142
+ logger.error({
143
+ workflowId: events[0]?.workflowId,
144
+ error: result.error,
145
+ duration: events.find(e => e.type === 'workflow_error')?.durationMs,
146
+ }, 'Workflow failed');
147
+ }
148
+
149
+ // Log all events (optional - can be verbose)
150
+ events.forEach(event => {
151
+ logger.debug({ event }, 'Workflow event');
152
+ });
153
+ ```
154
+
155
+ ## Pino Output Format
156
+
157
+ Pino will serialize workflow events as clean JSON. Here's what the output looks like:
158
+
159
+ ```json
160
+ {
161
+ "level": 30,
162
+ "time": 1704067200000,
163
+ "workflowId": "abc-123",
164
+ "duration": 245,
165
+ "stepCount": 2,
166
+ "msg": "Workflow completed successfully"
167
+ }
168
+ ```
169
+
170
+ For individual events:
171
+
172
+ ```json
173
+ {
174
+ "level": 20,
175
+ "time": 1704067200000,
176
+ "event": {
177
+ "type": "step_success",
178
+ "workflowId": "abc-123",
179
+ "stepId": "step-1",
180
+ "name": "Fetch user",
181
+ "stepKey": "user:1",
182
+ "ts": 1704067200100,
183
+ "durationMs": 45,
184
+ "context": {
185
+ "requestId": "req-456",
186
+ "userId": "user-789"
187
+ }
188
+ },
189
+ "msg": "Workflow event"
190
+ }
191
+ ```
192
+
193
+ ## Helper Function
194
+
195
+ Create a reusable Pino logger helper:
196
+
197
+ ```typescript
198
+ import { createEventCollector, type WorkflowEvent } from '@jagreehal/workflow';
199
+ import pino from 'pino';
200
+
201
+ export function createPinoLogger(logger: pino.Logger) {
202
+ const collector = createEventCollector();
203
+
204
+ return {
205
+ handleEvent: collector.handleEvent,
206
+
207
+ logAfterCompletion: (result: { ok: boolean; error?: unknown }) => {
208
+ const events = collector.getWorkflowEvents();
209
+ const workflowId = events[0]?.workflowId;
210
+
211
+ // Find workflow completion event
212
+ const completion = events.find(
213
+ e => e.type === 'workflow_success' || e.type === 'workflow_error'
214
+ );
215
+
216
+ if (result.ok && completion?.type === 'workflow_success') {
217
+ logger.info({
218
+ workflowId,
219
+ duration: completion.durationMs,
220
+ stepCount: events.filter(e => e.type === 'step_success').length,
221
+ cacheHits: events.filter(e => e.type === 'step_cache_hit').length,
222
+ context: completion.context,
223
+ }, 'Workflow completed');
224
+ } else if (!result.ok && completion?.type === 'workflow_error') {
225
+ logger.error({
226
+ workflowId,
227
+ error: result.error,
228
+ duration: completion.durationMs,
229
+ failedStep: events.find(e => e.type === 'step_error')?.name,
230
+ context: completion.context,
231
+ }, 'Workflow failed');
232
+ }
233
+
234
+ // Log slow steps
235
+ events
236
+ .filter((e): e is WorkflowEvent<unknown> & { durationMs: number } =>
237
+ e.type === 'step_success' && e.durationMs > 1000
238
+ )
239
+ .forEach(step => {
240
+ logger.warn({
241
+ workflowId,
242
+ stepName: step.name,
243
+ stepKey: step.stepKey,
244
+ duration: step.durationMs,
245
+ }, 'Slow step detected');
246
+ });
247
+
248
+ // Log retries
249
+ events
250
+ .filter(e => e.type === 'step_retry')
251
+ .forEach(retry => {
252
+ logger.warn({
253
+ workflowId,
254
+ stepName: retry.name,
255
+ attempt: retry.attempt,
256
+ maxAttempts: retry.maxAttempts,
257
+ error: retry.error,
258
+ }, 'Step retry');
259
+ });
260
+ },
261
+ };
262
+ }
263
+
264
+ // Usage
265
+ const pinoLogger = createPinoLogger(pino());
266
+ const workflow = createWorkflow(deps, {
267
+ onEvent: pinoLogger.handleEvent,
268
+ });
269
+
270
+ const result = await workflow(async (step) => {
271
+ // ... workflow logic
272
+ });
273
+
274
+ pinoLogger.logAfterCompletion(result);
275
+ ```
276
+
277
+ ## Why This Works Well
278
+
279
+ 1. **Structured Events**: All workflow events are plain objects with consistent properties
280
+ 2. **JSON-Serializable**: Events contain only JSON-safe values (strings, numbers, booleans, objects)
281
+ 3. **Context Support**: Events include `context` field for correlation IDs, user IDs, etc.
282
+ 4. **Type Safety**: TypeScript knows all event properties
283
+ 5. **Pino-Friendly**: Pino automatically serializes objects - no custom formatting needed
284
+
285
+ ## Logging ASCII Visualization
286
+
287
+ You can log the ASCII diagram directly to Pino:
288
+
289
+ ```typescript
290
+ import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
291
+ import pino from 'pino';
292
+
293
+ const logger = pino();
294
+ const viz = createVisualizer({ workflowName: 'checkout' });
295
+
296
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
297
+ onEvent: viz.handleEvent,
298
+ });
299
+
300
+ const result = await workflow(async (step) => {
301
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
302
+ const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
303
+ return { user, posts };
304
+ });
305
+
306
+ // Log ASCII visualization
307
+ const asciiDiagram = viz.render();
308
+ logger.info({
309
+ workflowId: result.ok ? 'success' : 'failed',
310
+ visualization: asciiDiagram,
311
+ }, 'Workflow completed');
312
+ ```
313
+
314
+ ### Pino Output with ASCII
315
+
316
+ In JSON format, the ASCII will be escaped with `\n`:
317
+
318
+ ```json
319
+ {
320
+ "level": 30,
321
+ "time": 1704067200000,
322
+ "workflowId": "success",
323
+ "visualization": "┌── checkout ───────────────────────────────┐\n│ │\n│ ✓ Fetch user [45ms] │\n│ ✓ Fetch posts [67ms] │\n│ │\n│ Completed in 112ms │\n│ │\n└──────────────────────────────────────────┘",
324
+ "msg": "Workflow completed"
325
+ }
326
+ ```
327
+
328
+ ### With pino-pretty
329
+
330
+ When using `pino-pretty` or viewing in a terminal, the ASCII renders correctly:
331
+
332
+ ```
333
+ [2024-01-01 12:00:00] INFO: Workflow completed
334
+ workflowId: "success"
335
+ visualization: "
336
+ ┌── checkout ───────────────────────────────┐
337
+ │ │
338
+ │ ✓ Fetch user [45ms] │
339
+ │ ✓ Fetch posts [67ms] │
340
+ │ │
341
+ │ Completed in 112ms │
342
+ │ │
343
+ └──────────────────────────────────────────┘
344
+ "
345
+ ```
346
+
347
+ ### ANSI Colors
348
+
349
+ The ASCII output includes ANSI color codes. They work in:
350
+ - ✅ Terminal output (pino-pretty, console)
351
+ - ✅ Log viewers that support ANSI (most modern ones)
352
+ - ⚠️ Raw JSON logs (colors are included as escape sequences)
353
+
354
+ To strip colors for cleaner JSON logs:
355
+
356
+ ```typescript
357
+ // Simple regex to strip ANSI codes
358
+ function stripAnsi(str: string): string {
359
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
360
+ }
361
+
362
+ const asciiDiagram = viz.render();
363
+ const asciiWithoutColors = stripAnsi(asciiDiagram);
364
+
365
+ logger.info({
366
+ visualization: asciiWithoutColors, // Clean box-drawing, no ANSI codes
367
+ }, 'Workflow completed');
368
+ ```
369
+
370
+ **Note:** The ASCII output includes ANSI color codes. They render correctly in terminals and log viewers that support ANSI (like `pino-pretty`), but appear as escape sequences in raw JSON logs. Use `stripAnsi()` if you want clean box-drawing characters without color codes.
371
+
372
+ ## Example Output
373
+
374
+ ```json
375
+ {
376
+ "level": 30,
377
+ "time": 1704067200000,
378
+ "workflowId": "checkout-abc-123",
379
+ "duration": 1234,
380
+ "stepCount": 5,
381
+ "cacheHits": 2,
382
+ "context": {
383
+ "requestId": "req-456",
384
+ "userId": "user-789",
385
+ "orderId": "order-123"
386
+ },
387
+ "msg": "Workflow completed"
388
+ }
389
+ ```
390
+
391
+ The format looks great in Pino because:
392
+ - All fields are at the top level (easy to query/filter)
393
+ - Context is nested but accessible
394
+ - Duration and counts are numeric (good for metrics)
395
+ - Error objects serialize cleanly
396
+ - ASCII visualization can be included as a multi-line string field
@@ -95,6 +95,41 @@ flowchart TD
95
95
 
96
96
  Paste into GitHub, Notion, or any Mermaid-compatible tool.
97
97
 
98
+ #### Enhanced Mermaid Edges
99
+
100
+ Mermaid diagrams now show retry loops, error paths, and timeouts as visual edges (not just labels):
101
+
102
+ ```mermaid
103
+ flowchart TD
104
+ A["✓ Fetch user 45ms"] --> B["✗ Process payment 201ms"]
105
+ A -.->|"↻ 2 retries"| A
106
+ B -->|error| ERR{{PAYMENT_FAILED}}
107
+ B -.->|timeout| TO{{⏱ Timeout 5000ms}}
108
+ style ERR fill:#fee2e2,stroke:#dc2626
109
+ style TO fill:#fef3c7,stroke:#f59e0b
110
+ ```
111
+
112
+ **What the edges show:**
113
+
114
+ - **Retry loops** (`-.->`) - Self-loop showing how many retries occurred
115
+ - **Error paths** (`-->|error|`) - Flow to error node showing the error value
116
+ - **Timeout paths** (`-.->|timeout|`) - Dashed edge to timeout node
117
+
118
+ These are enabled by default. To disable, pass options when rendering:
119
+
120
+ ```typescript
121
+ import type { MermaidRenderOptions } from '@jagreehal/workflow/visualize';
122
+
123
+ const options: MermaidRenderOptions = {
124
+ showTimings: true,
125
+ showKeys: false,
126
+ colors: defaultColorScheme,
127
+ showRetryEdges: false, // Hide retry self-loops
128
+ showErrorEdges: false, // Hide error path edges
129
+ showTimeoutEdges: false, // Hide timeout path edges
130
+ };
131
+ ```
132
+
98
133
  ### JSON (IR)
99
134
 
100
135
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagreehal/workflow",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "description": "Typed async workflows with automatic error inference. Build type-safe workflows with Result types, step caching, resume state, and human-in-the-loop support.",
6
6
  "main": "./dist/index.cjs",
@@ -100,6 +100,7 @@
100
100
  "@total-typescript/tsconfig": "^1.0.4",
101
101
  "@types/node": "^25.0.9",
102
102
  "@types/picomatch": "^4.0.2",
103
+ "@types/ws": "^8.18.1",
103
104
  "@typescript-eslint/eslint-plugin": "^8.53.0",
104
105
  "@typescript-eslint/parser": "^8.53.0",
105
106
  "@typescript-eslint/rule-tester": "^8.53.0",
@@ -108,11 +109,15 @@
108
109
  "eslint": "9.39.2",
109
110
  "eslint-config-prettier": "^10.1.8",
110
111
  "eslint-plugin-unicorn": "^62.0.0",
112
+ "jsdom": "^27.4.0",
113
+ "mermaid": "^11.12.2",
114
+ "pino": "^10.2.0",
111
115
  "tsd": "^0.33.0",
112
116
  "tsup": "^8.5.1",
113
117
  "typescript": "^5.9.3",
114
118
  "typescript-eslint": "^8.53.0",
115
- "vitest": "^4.0.17"
119
+ "vitest": "^4.0.17",
120
+ "ws": "^8.19.0"
116
121
  },
117
122
  "engines": {
118
123
  "node": ">=18.0.0"