@jagreehal/workflow 1.4.0 → 1.6.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,330 @@
1
+ # Visualization Examples
2
+
3
+ This document provides comprehensive examples of using the `@jagreehal/workflow/visualize` module.
4
+
5
+ ## Basic Usage
6
+
7
+ ### Simple Sequential Workflow
8
+
9
+ ```typescript
10
+ import { createVisualizer } from '@jagreehal/workflow/visualize';
11
+ import { createWorkflow } from '@jagreehal/workflow';
12
+
13
+ const viz = createVisualizer({ workflowName: 'User Data Flow' });
14
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
15
+ onEvent: viz.handleEvent,
16
+ });
17
+
18
+ await workflow(async (step) => {
19
+ const user = await step(() => fetchUser('1'), { name: 'Fetch user' });
20
+ const posts = await step(() => fetchPosts(user.id), { name: 'Fetch posts' });
21
+ return { user, posts };
22
+ });
23
+
24
+ console.log(viz.render());
25
+ ```
26
+
27
+ Output:
28
+ ```
29
+ ┌── User Data Flow ───────────────────────────────────────────┐
30
+ │ │
31
+ │ ✓ Fetch user [10ms] │
32
+ │ ✓ Fetch posts [15ms] │
33
+ │ │
34
+ │ Completed in 25ms │
35
+ │ │
36
+ └─────────────────────────────────────────────────────────────┘
37
+ ```
38
+
39
+ ## Parallel Operations
40
+
41
+ ### Visualizing Individual Parallel Steps
42
+
43
+ To see individual steps in parallel operations, wrap each operation with `step()`:
44
+
45
+ ```typescript
46
+ const viz = createVisualizer({ workflowName: 'Parallel Fetch' });
47
+ const workflow = createWorkflow({ fetchUser, fetchPosts }, {
48
+ onEvent: viz.handleEvent,
49
+ });
50
+
51
+ await workflow(async (step) => {
52
+ // Wrap each operation with step() to track individually
53
+ const [user, posts] = await step.parallel('Fetch all data', async () => {
54
+ const userResult = await step(() => fetchUser('1'), { name: 'Fetch user' });
55
+ const postsResult = await step(() => fetchPosts('1'), { name: 'Fetch posts' });
56
+ return [userResult, postsResult];
57
+ });
58
+ return { user, posts };
59
+ });
60
+
61
+ console.log(viz.render());
62
+ ```
63
+
64
+ Output:
65
+ ```
66
+ ┌── Parallel Fetch ───────────────────────────────────────────┐
67
+ │ │
68
+ │ ├┬─ ✓ Fetch all data │
69
+ │ │ ├ ✓ Fetch user [10ms] │
70
+ │ │ └ ✓ Fetch posts [15ms] │
71
+ │ └── [15ms] │
72
+ │ │
73
+ │ Completed in 15ms │
74
+ │ │
75
+ └─────────────────────────────────────────────────────────────┘
76
+ ```
77
+
78
+ ### Without Individual Step Tracking
79
+
80
+ If you use `allAsync()` directly without wrapping operations:
81
+
82
+ ```typescript
83
+ await workflow(async (step) => {
84
+ const [user, posts] = await step.parallel('Fetch all data', () =>
85
+ allAsync([fetchUser('1'), fetchPosts('1')])
86
+ );
87
+ return { user, posts };
88
+ });
89
+ ```
90
+
91
+ The visualization will show:
92
+ ```
93
+ ┌── Parallel Fetch ───────────────────────────────────────────┐
94
+ │ │
95
+ │ ├┬─ ✓ Fetch all data │
96
+ │ │ (operations not individually tracked) │
97
+ │ │ (wrap each operation with step() to see individual steps)
98
+ │ └── [15ms] │
99
+ │ │
100
+ └─────────────────────────────────────────────────────────────┘
101
+ ```
102
+
103
+ ## Decision Tracking
104
+
105
+ ### If/Else Decisions
106
+
107
+ ```typescript
108
+ import { trackIf } from '@jagreehal/workflow/visualize';
109
+
110
+ const collector = createEventCollector({ workflowName: 'Role Check' });
111
+ const workflow = createWorkflow({ fetchUser, processAdmin, processUser }, {
112
+ onEvent: collector.handleEvent,
113
+ });
114
+
115
+ await workflow(async (step, deps) => {
116
+ const user = await step(() => deps.fetchUser('1'), { name: 'Fetch user' });
117
+
118
+ const decision = trackIf('check-role', user.role === 'admin', {
119
+ condition: "user.role === 'admin'",
120
+ value: user.role,
121
+ emit: collector.handleDecisionEvent,
122
+ });
123
+
124
+ if (decision.condition) {
125
+ decision.then();
126
+ await step(() => deps.processAdmin(user), { name: 'Process admin' });
127
+ } else {
128
+ decision.else();
129
+ await step(() => deps.processUser(user), { name: 'Process user' });
130
+ }
131
+ decision.end();
132
+
133
+ return user;
134
+ });
135
+
136
+ console.log(collector.visualize());
137
+ ```
138
+
139
+ ### Switch Decisions
140
+
141
+ ```typescript
142
+ import { trackSwitch } from '@jagreehal/workflow/visualize';
143
+
144
+ const decision = trackSwitch('process-by-role', user.role, {
145
+ condition: 'switch(user.role)',
146
+ value: user.role,
147
+ emit: collector.handleDecisionEvent,
148
+ });
149
+
150
+ switch (user.role) {
151
+ case 'admin':
152
+ decision.case('admin', true);
153
+ await step(() => processAdmin(user), { name: 'Process admin' });
154
+ break;
155
+ case 'user':
156
+ decision.case('user', true);
157
+ await step(() => processUser(user), { name: 'Process user' });
158
+ break;
159
+ default:
160
+ decision.default(false);
161
+ }
162
+ decision.end();
163
+ ```
164
+
165
+ ## Output Formats
166
+
167
+ ### ASCII (Default)
168
+
169
+ ```typescript
170
+ const output = viz.render(); // or viz.renderAs('ascii')
171
+ ```
172
+
173
+ Best for:
174
+ - Terminal output
175
+ - CLI tools
176
+ - Logs
177
+ - Incident runbooks
178
+
179
+ ### Mermaid
180
+
181
+ ```typescript
182
+ const mermaid = viz.renderAs('mermaid');
183
+ console.log(mermaid);
184
+ ```
185
+
186
+ Best for:
187
+ - Markdown documentation
188
+ - GitHub README files
189
+ - Documentation sites
190
+ - PR descriptions
191
+
192
+ Example Mermaid output:
193
+ ```mermaid
194
+ flowchart TD
195
+ start((Start))
196
+ step_user[Fetch user 10ms]:::success
197
+ start --> step_user
198
+ step_posts[Fetch posts 15ms]:::success
199
+ step_user --> step_posts
200
+ finish((Done)):::success
201
+ step_posts --> finish
202
+ ```
203
+
204
+ ### JSON
205
+
206
+ ```typescript
207
+ const json = viz.renderAs('json');
208
+ const ir = JSON.parse(json);
209
+ // Programmatically inspect the IR
210
+ ```
211
+
212
+ Best for:
213
+ - Programmatic analysis
214
+ - Custom renderers
215
+ - Debugging
216
+ - Integration with other tools
217
+
218
+ ## Post-Execution Visualization
219
+
220
+ Collect events during execution and visualize later:
221
+
222
+ ```typescript
223
+ import { createEventCollector } from '@jagreehal/workflow/visualize';
224
+
225
+ const collector = createEventCollector({ workflowName: 'My Workflow' });
226
+ const workflow = createWorkflow(deps, {
227
+ onEvent: collector.handleEvent,
228
+ });
229
+
230
+ await workflow(async (step) => {
231
+ // ... workflow execution
232
+ });
233
+
234
+ // Later, visualize collected events
235
+ console.log(collector.visualize());
236
+ console.log(collector.visualizeAs('mermaid'));
237
+
238
+ // Or access raw events
239
+ const events = collector.getEvents();
240
+ ```
241
+
242
+ ## Options
243
+
244
+ ### Customize Display
245
+
246
+ ```typescript
247
+ const viz = createVisualizer({
248
+ workflowName: 'My Workflow',
249
+ showTimings: true, // Show duration for each step
250
+ showKeys: true, // Show cache keys
251
+ detectParallel: true, // Auto-detect parallel execution
252
+ colors: { // Customize colors
253
+ success: '\x1b[32m',
254
+ error: '\x1b[31m',
255
+ // ...
256
+ },
257
+ });
258
+ ```
259
+
260
+ ### Disable Parallel Detection
261
+
262
+ If you want to see steps in their original order without grouping:
263
+
264
+ ```typescript
265
+ const viz = createVisualizer({
266
+ workflowName: 'Sequential View',
267
+ detectParallel: false, // Disable heuristic parallel detection
268
+ });
269
+ ```
270
+
271
+ ## Best Practices
272
+
273
+ 1. **Always provide step names** for better visualization:
274
+ ```typescript
275
+ await step(() => fetchUser(id), { name: 'Fetch user' });
276
+ ```
277
+
278
+ 2. **Use keys for cacheable steps** to enable visualization of cache hits:
279
+ ```typescript
280
+ await step(() => fetchUser(id), {
281
+ name: 'Fetch user',
282
+ key: `user:${id}`
283
+ });
284
+ ```
285
+
286
+ 3. **Track decisions explicitly** for conditional logic:
287
+ ```typescript
288
+ const decision = trackIf('check-condition', condition, {
289
+ emit: collector.handleDecisionEvent,
290
+ });
291
+ ```
292
+
293
+ 4. **Wrap parallel operations** with `step()` to see individual steps:
294
+ ```typescript
295
+ await step.parallel('Parallel fetch', async () => {
296
+ const a = await step(() => fetchA(), { name: 'Fetch A' });
297
+ const b = await step(() => fetchB(), { name: 'Fetch B' });
298
+ return [a, b];
299
+ });
300
+ ```
301
+
302
+ ## Troubleshooting
303
+
304
+ ### Empty Parallel/Race Scopes
305
+
306
+ If you see "(operations not individually tracked)" in parallel/race scopes, wrap each operation with `step()` to track them individually.
307
+
308
+ ### Sequential Steps Grouped as Parallel
309
+
310
+ If sequential steps are incorrectly grouped as parallel, disable parallel detection:
311
+ ```typescript
312
+ const viz = createVisualizer({ detectParallel: false });
313
+ ```
314
+
315
+ Or adjust the parallel detection options:
316
+ ```typescript
317
+ const viz = createVisualizer({
318
+ detectParallel: true,
319
+ parallelDetection: {
320
+ minOverlapMs: 5, // Require 5ms overlap
321
+ maxGapMs: 0, // No gap tolerance
322
+ },
323
+ });
324
+ ```
325
+
326
+ ### Mermaid Parse Errors
327
+
328
+ The Mermaid renderer automatically escapes special characters. If you encounter parse errors, ensure step names and conditions don't contain unescaped brackets, parentheses, or quotes.
329
+
330
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagreehal/workflow",
3
- "version": "1.4.0",
3
+ "version": "1.6.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",
@@ -75,10 +75,10 @@
75
75
  "@total-typescript/tsconfig": "^1.0.4",
76
76
  "@types/node": "^25.0.3",
77
77
  "@types/picomatch": "^4.0.2",
78
- "@typescript-eslint/eslint-plugin": "^8.50.0",
79
- "@typescript-eslint/parser": "^8.50.0",
80
- "@typescript-eslint/rule-tester": "^8.50.0",
81
- "@typescript-eslint/utils": "^8.50.0",
78
+ "@typescript-eslint/eslint-plugin": "^8.51.0",
79
+ "@typescript-eslint/parser": "^8.51.0",
80
+ "@typescript-eslint/rule-tester": "^8.51.0",
81
+ "@typescript-eslint/utils": "^8.51.0",
82
82
  "@vitest/coverage-v8": "^4.0.16",
83
83
  "eslint": "9.39.2",
84
84
  "eslint-config-prettier": "^10.1.8",
@@ -86,7 +86,7 @@
86
86
  "tsd": "^0.33.0",
87
87
  "tsup": "^8.5.1",
88
88
  "typescript": "^5.9.3",
89
- "typescript-eslint": "^8.50.0",
89
+ "typescript-eslint": "^8.51.0",
90
90
  "vitest": "^4.0.16"
91
91
  },
92
92
  "engines": {
@@ -116,6 +116,7 @@
116
116
  "tsd": "tsd --files 'src/**/*.test-d.ts'",
117
117
  "clean": "rm -rf dist lib",
118
118
  "prebuild": "pnpm clean",
119
+ "quality": "pnpm build:tsc && pnpm test && pnpm lint",
119
120
  "changeset": "changeset",
120
121
  "version-packages": "changeset version",
121
122
  "release": "pnpm build && changeset publish"