@jagreehal/workflow 1.13.0 → 1.14.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,293 @@
1
+ # Logging Workflow Events to Pino
2
+
3
+ Workflow events are structured JSON objects that work perfectly with Pino's structured logging. You can collect events during execution and log them after completion.
4
+
5
+ ## Logging ASCII Visualization
6
+
7
+ You can also log the ASCII visualization output to Pino. The multi-line ASCII diagram will be included as a string field in the log entry.
8
+
9
+ ## Basic Example
10
+
11
+ ```typescript
12
+ import { createWorkflow, createEventCollector } from '@jagreehal/workflow';
13
+ import pino from 'pino';
14
+
15
+ const logger = pino();
16
+
17
+ const collector = createEventCollector();
18
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
19
+ onEvent: collector.handleEvent,
20
+ });
21
+
22
+ const result = await workflow(async (step) => {
23
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
24
+ const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
25
+ return { user, posts };
26
+ });
27
+
28
+ // After workflow completes, log all events
29
+ const events = collector.getWorkflowEvents();
30
+
31
+ // Log summary
32
+ if (result.ok) {
33
+ logger.info({
34
+ workflowId: events[0]?.workflowId,
35
+ duration: events.find(e => e.type === 'workflow_success')?.durationMs,
36
+ stepCount: events.filter(e => e.type === 'step_success').length,
37
+ }, 'Workflow completed successfully');
38
+ } else {
39
+ logger.error({
40
+ workflowId: events[0]?.workflowId,
41
+ error: result.error,
42
+ duration: events.find(e => e.type === 'workflow_error')?.durationMs,
43
+ }, 'Workflow failed');
44
+ }
45
+
46
+ // Log all events (optional - can be verbose)
47
+ events.forEach(event => {
48
+ logger.debug({ event }, 'Workflow event');
49
+ });
50
+ ```
51
+
52
+ ## Pino Output Format
53
+
54
+ Pino will serialize workflow events as clean JSON. Here's what the output looks like:
55
+
56
+ ```json
57
+ {
58
+ "level": 30,
59
+ "time": 1704067200000,
60
+ "workflowId": "abc-123",
61
+ "duration": 245,
62
+ "stepCount": 2,
63
+ "msg": "Workflow completed successfully"
64
+ }
65
+ ```
66
+
67
+ For individual events:
68
+
69
+ ```json
70
+ {
71
+ "level": 20,
72
+ "time": 1704067200000,
73
+ "event": {
74
+ "type": "step_success",
75
+ "workflowId": "abc-123",
76
+ "stepId": "step-1",
77
+ "name": "Fetch user",
78
+ "stepKey": "user:1",
79
+ "ts": 1704067200100,
80
+ "durationMs": 45,
81
+ "context": {
82
+ "requestId": "req-456",
83
+ "userId": "user-789"
84
+ }
85
+ },
86
+ "msg": "Workflow event"
87
+ }
88
+ ```
89
+
90
+ ## Helper Function
91
+
92
+ Create a reusable Pino logger helper:
93
+
94
+ ```typescript
95
+ import { createEventCollector, type WorkflowEvent } from '@jagreehal/workflow';
96
+ import pino from 'pino';
97
+
98
+ export function createPinoLogger(logger: pino.Logger) {
99
+ const collector = createEventCollector();
100
+
101
+ return {
102
+ handleEvent: collector.handleEvent,
103
+
104
+ logAfterCompletion: (result: { ok: boolean; error?: unknown }) => {
105
+ const events = collector.getWorkflowEvents();
106
+ const workflowId = events[0]?.workflowId;
107
+
108
+ // Find workflow completion event
109
+ const completion = events.find(
110
+ e => e.type === 'workflow_success' || e.type === 'workflow_error'
111
+ );
112
+
113
+ if (result.ok && completion?.type === 'workflow_success') {
114
+ logger.info({
115
+ workflowId,
116
+ duration: completion.durationMs,
117
+ stepCount: events.filter(e => e.type === 'step_success').length,
118
+ cacheHits: events.filter(e => e.type === 'step_cache_hit').length,
119
+ context: completion.context,
120
+ }, 'Workflow completed');
121
+ } else if (!result.ok && completion?.type === 'workflow_error') {
122
+ logger.error({
123
+ workflowId,
124
+ error: result.error,
125
+ duration: completion.durationMs,
126
+ failedStep: events.find(e => e.type === 'step_error')?.name,
127
+ context: completion.context,
128
+ }, 'Workflow failed');
129
+ }
130
+
131
+ // Log slow steps
132
+ events
133
+ .filter((e): e is WorkflowEvent<unknown> & { durationMs: number } =>
134
+ e.type === 'step_success' && e.durationMs > 1000
135
+ )
136
+ .forEach(step => {
137
+ logger.warn({
138
+ workflowId,
139
+ stepName: step.name,
140
+ stepKey: step.stepKey,
141
+ duration: step.durationMs,
142
+ }, 'Slow step detected');
143
+ });
144
+
145
+ // Log retries
146
+ events
147
+ .filter(e => e.type === 'step_retry')
148
+ .forEach(retry => {
149
+ logger.warn({
150
+ workflowId,
151
+ stepName: retry.name,
152
+ attempt: retry.attempt,
153
+ maxAttempts: retry.maxAttempts,
154
+ error: retry.error,
155
+ }, 'Step retry');
156
+ });
157
+ },
158
+ };
159
+ }
160
+
161
+ // Usage
162
+ const pinoLogger = createPinoLogger(pino());
163
+ const workflow = createWorkflow(deps, {
164
+ onEvent: pinoLogger.handleEvent,
165
+ });
166
+
167
+ const result = await workflow(async (step) => {
168
+ // ... workflow logic
169
+ });
170
+
171
+ pinoLogger.logAfterCompletion(result);
172
+ ```
173
+
174
+ ## Why This Works Well
175
+
176
+ 1. **Structured Events**: All workflow events are plain objects with consistent properties
177
+ 2. **JSON-Serializable**: Events contain only JSON-safe values (strings, numbers, booleans, objects)
178
+ 3. **Context Support**: Events include `context` field for correlation IDs, user IDs, etc.
179
+ 4. **Type Safety**: TypeScript knows all event properties
180
+ 5. **Pino-Friendly**: Pino automatically serializes objects - no custom formatting needed
181
+
182
+ ## Logging ASCII Visualization
183
+
184
+ You can log the ASCII diagram directly to Pino:
185
+
186
+ ```typescript
187
+ import { createWorkflow, createVisualizer } from '@jagreehal/workflow';
188
+ import pino from 'pino';
189
+
190
+ const logger = pino();
191
+ const viz = createVisualizer({ workflowName: 'checkout' });
192
+
193
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
194
+ onEvent: viz.handleEvent,
195
+ });
196
+
197
+ const result = await workflow(async (step) => {
198
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
199
+ const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
200
+ return { user, posts };
201
+ });
202
+
203
+ // Log ASCII visualization
204
+ const asciiDiagram = viz.render();
205
+ logger.info({
206
+ workflowId: result.ok ? 'success' : 'failed',
207
+ visualization: asciiDiagram,
208
+ }, 'Workflow completed');
209
+ ```
210
+
211
+ ### Pino Output with ASCII
212
+
213
+ In JSON format, the ASCII will be escaped with `\n`:
214
+
215
+ ```json
216
+ {
217
+ "level": 30,
218
+ "time": 1704067200000,
219
+ "workflowId": "success",
220
+ "visualization": "┌── checkout ───────────────────────────────┐\n│ │\n│ ✓ Fetch user [45ms] │\n│ ✓ Fetch posts [67ms] │\n│ │\n│ Completed in 112ms │\n│ │\n└──────────────────────────────────────────┘",
221
+ "msg": "Workflow completed"
222
+ }
223
+ ```
224
+
225
+ ### With pino-pretty
226
+
227
+ When using `pino-pretty` or viewing in a terminal, the ASCII renders correctly:
228
+
229
+ ```
230
+ [2024-01-01 12:00:00] INFO: Workflow completed
231
+ workflowId: "success"
232
+ visualization: "
233
+ ┌── checkout ───────────────────────────────┐
234
+ │ │
235
+ │ ✓ Fetch user [45ms] │
236
+ │ ✓ Fetch posts [67ms] │
237
+ │ │
238
+ │ Completed in 112ms │
239
+ │ │
240
+ └──────────────────────────────────────────┘
241
+ "
242
+ ```
243
+
244
+ ### ANSI Colors
245
+
246
+ The ASCII output includes ANSI color codes. They work in:
247
+ - ✅ Terminal output (pino-pretty, console)
248
+ - ✅ Log viewers that support ANSI (most modern ones)
249
+ - ⚠️ Raw JSON logs (colors are included as escape sequences)
250
+
251
+ To strip colors for cleaner JSON logs:
252
+
253
+ ```typescript
254
+ // Simple regex to strip ANSI codes
255
+ function stripAnsi(str: string): string {
256
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
257
+ }
258
+
259
+ const asciiDiagram = viz.render();
260
+ const asciiWithoutColors = stripAnsi(asciiDiagram);
261
+
262
+ logger.info({
263
+ visualization: asciiWithoutColors, // Clean box-drawing, no ANSI codes
264
+ }, 'Workflow completed');
265
+ ```
266
+
267
+ **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.
268
+
269
+ ## Example Output
270
+
271
+ ```json
272
+ {
273
+ "level": 30,
274
+ "time": 1704067200000,
275
+ "workflowId": "checkout-abc-123",
276
+ "duration": 1234,
277
+ "stepCount": 5,
278
+ "cacheHits": 2,
279
+ "context": {
280
+ "requestId": "req-456",
281
+ "userId": "user-789",
282
+ "orderId": "order-123"
283
+ },
284
+ "msg": "Workflow completed"
285
+ }
286
+ ```
287
+
288
+ The format looks great in Pino because:
289
+ - All fields are at the top level (easy to query/filter)
290
+ - Context is nested but accessible
291
+ - Duration and counts are numeric (good for metrics)
292
+ - Error objects serialize cleanly
293
+ - 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.14.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,14 @@
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",
111
114
  "tsd": "^0.33.0",
112
115
  "tsup": "^8.5.1",
113
116
  "typescript": "^5.9.3",
114
117
  "typescript-eslint": "^8.53.0",
115
- "vitest": "^4.0.17"
118
+ "vitest": "^4.0.17",
119
+ "ws": "^8.19.0"
116
120
  },
117
121
  "engines": {
118
122
  "node": ">=18.0.0"