@jagreehal/workflow 1.0.0 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagreehal/workflow",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",
@@ -28,21 +28,6 @@
28
28
  "README.md",
29
29
  "docs"
30
30
  ],
31
- "scripts": {
32
- "build": "tsup",
33
- "build:tsc": "tsc --noEmit",
34
- "test": "vitest run",
35
- "test:watch": "vitest watch",
36
- "test:coverage": "vitest run --coverage",
37
- "lint": "eslint .",
38
- "clean": "rm -rf dist lib",
39
- "prebuild": "pnpm clean",
40
- "prepare": "pnpm build",
41
- "prepublishOnly": "pnpm build:tsc && pnpm run test && pnpm run lint",
42
- "changeset": "changeset",
43
- "version-packages": "changeset version",
44
- "release": "pnpm build && changeset publish"
45
- },
46
31
  "keywords": [
47
32
  "workflow",
48
33
  "workflows",
@@ -115,5 +100,18 @@
115
100
  "publishConfig": {
116
101
  "access": "public",
117
102
  "registry": "https://registry.npmjs.org/"
103
+ },
104
+ "scripts": {
105
+ "build": "tsup",
106
+ "build:tsc": "tsc --noEmit",
107
+ "test": "vitest run",
108
+ "test:watch": "vitest watch",
109
+ "test:coverage": "vitest run --coverage",
110
+ "lint": "eslint .",
111
+ "clean": "rm -rf dist lib",
112
+ "prebuild": "pnpm clean",
113
+ "changeset": "changeset",
114
+ "version-packages": "changeset version",
115
+ "release": "pnpm build && changeset publish"
118
116
  }
119
- }
117
+ }
@@ -1,565 +0,0 @@
1
- /**
2
- * Test file to verify all code examples in docs/advanced.md work as documented.
3
- * Run with: pnpm vitest run docs/advanced.test.ts
4
- */
5
- /* eslint-disable @typescript-eslint/no-unused-vars */
6
-
7
- import { describe, it, expect, vi } from 'vitest';
8
- import {
9
- all,
10
- allSettled,
11
- any,
12
- partition,
13
- allAsync,
14
- allSettledAsync,
15
- anyAsync,
16
- from,
17
- fromPromise,
18
- tryAsync,
19
- fromNullable,
20
- map,
21
- mapError,
22
- match,
23
- andThen,
24
- tap,
25
- ok,
26
- err,
27
- type AsyncResult,
28
- type Result,
29
- run,
30
- createWorkflow,
31
- createApprovalStep,
32
- createHITLCollector,
33
- isPendingApproval,
34
- injectApproval,
35
- hasPendingApproval,
36
- getPendingApprovals,
37
- clearStep,
38
- isStepComplete,
39
- } from '../src/index';
40
-
41
- // Types for examples
42
- type User = { id: string; name: string };
43
- type Post = { id: number; title: string };
44
-
45
- describe('Advanced Examples', () => {
46
- describe('Batch operations', () => {
47
- it('should work with all() - all must succeed', () => {
48
- const combined = all([ok(1), ok(2), ok(3)]);
49
- expect(combined.ok).toBe(true);
50
- if (combined.ok) {
51
- expect(combined.value).toEqual([1, 2, 3]);
52
- }
53
- });
54
-
55
- it('should work with all() - short-circuits on first error', () => {
56
- const combined = all([ok(1), err('ERROR'), ok(3)]);
57
- expect(combined.ok).toBe(false);
58
- if (!combined.ok) {
59
- expect(combined.error).toBe('ERROR');
60
- }
61
- });
62
-
63
- it('should work with allSettled() - collects all errors', async () => {
64
- const validateEmail = (email: string): Result<string, 'INVALID_EMAIL'> =>
65
- email.includes('@') ? ok(email) : err('INVALID_EMAIL');
66
-
67
- const validatePassword = (password: string): Result<string, 'WEAK_PASSWORD'> =>
68
- password.length >= 8 ? ok(password) : err('WEAK_PASSWORD');
69
-
70
- const email = 'test@example.com';
71
- const password = 'short';
72
- const validated = allSettled([validateEmail(email), validatePassword(password)]);
73
-
74
- expect(validated.ok).toBe(false);
75
- if (!validated.ok) {
76
- expect(validated.error).toHaveLength(1);
77
- expect(validated.error[0].error).toBe('WEAK_PASSWORD');
78
- }
79
- });
80
-
81
- it('should work with any() - first success wins', () => {
82
- const first = any([err('A'), ok('success'), err('B')]);
83
- expect(first.ok).toBe(true);
84
- if (first.ok) {
85
- expect(first.value).toBe('success');
86
- }
87
- });
88
-
89
- it('should work with any() - all errors returns first error', () => {
90
- const first = any([err('A'), err('B'), err('C')]);
91
- expect(first.ok).toBe(false);
92
- if (!first.ok) {
93
- expect(first.error).toBe('A');
94
- }
95
- });
96
-
97
- it('should work with partition() - split successes and failures', () => {
98
- const results: Result<number, string>[] = [
99
- ok(1),
100
- err('ERROR_1'),
101
- ok(3),
102
- err('ERROR_2'),
103
- ];
104
- const { values, errors } = partition(results);
105
-
106
- expect(values).toEqual([1, 3]);
107
- expect(errors).toEqual(['ERROR_1', 'ERROR_2']);
108
- });
109
-
110
- it('should work with allAsync()', async () => {
111
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
112
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
113
-
114
- const fetchPosts = async (_userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
115
- ok([{ id: 1, title: 'Hello' }]);
116
-
117
- const result = await allAsync([fetchUser('1'), fetchPosts('1')]);
118
- expect(result.ok).toBe(true);
119
- if (result.ok) {
120
- expect(result.value[0].name).toBe('Alice');
121
- expect(result.value[1]).toHaveLength(1);
122
- }
123
- });
124
-
125
- it('should work with allSettledAsync()', async () => {
126
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
127
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
128
-
129
- const fetchPosts = async (_userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
130
- ok([{ id: 1, title: 'Hello' }]);
131
-
132
- const result = await allSettledAsync([fetchUser('2'), fetchPosts('1')]);
133
- expect(result.ok).toBe(false);
134
- if (!result.ok) {
135
- expect(result.error).toHaveLength(1);
136
- expect(result.error[0].error).toBe('NOT_FOUND');
137
- }
138
- });
139
-
140
- it('should work with anyAsync()', async () => {
141
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
142
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
143
-
144
- const fetchPosts = async (_userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
145
- ok([{ id: 1, title: 'Hello' }]);
146
-
147
- const result = await anyAsync([fetchUser('2'), fetchPosts('1')]);
148
- expect(result.ok).toBe(true);
149
- if (result.ok) {
150
- expect(result.value).toHaveLength(1);
151
- }
152
- });
153
- });
154
-
155
- describe('Dynamic error mapping', () => {
156
- it('should work with onError option in step.try', async () => {
157
- const workflow = createWorkflow({});
158
-
159
- // Mock fetch
160
- global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
161
-
162
- const result = await workflow(async (step) => {
163
- const data = await step.try(
164
- () => fetch('/api/data'),
165
- { onError: (e) => ({ type: 'API_ERROR' as const, message: String(e) }) }
166
- );
167
-
168
- return data;
169
- });
170
-
171
- expect(result.ok).toBe(false);
172
- if (!result.ok) {
173
- expect(result.error).toHaveProperty('type', 'API_ERROR');
174
- expect(result.error).toHaveProperty('message');
175
- }
176
- });
177
-
178
- it('should work with onError for validation errors', async () => {
179
- // Simulate a schema validation library
180
- const schema = {
181
- parse: (data: unknown) => {
182
- if (typeof data !== 'object' || data === null) {
183
- throw { issues: ['Invalid data'] };
184
- }
185
- return data;
186
- },
187
- };
188
-
189
- const workflow = createWorkflow({});
190
-
191
- const result = await workflow(async (step) => {
192
- const parsed = await step.try(
193
- () => schema.parse(null),
194
- { onError: (e) => ({ type: 'VALIDATION_ERROR' as const, issues: (e as { issues: string[] }).issues }) }
195
- );
196
-
197
- return parsed;
198
- });
199
-
200
- expect(result.ok).toBe(false);
201
- if (!result.ok) {
202
- expect(result.error).toHaveProperty('type', 'VALIDATION_ERROR');
203
- expect(result.error).toHaveProperty('issues');
204
- }
205
- });
206
- });
207
-
208
- describe('Wrapping existing code', () => {
209
- it('should work with from() for sync throwing functions', () => {
210
- const parsed = from(
211
- () => JSON.parse('{"key": "value"}'),
212
- (cause) => ({ type: 'PARSE_ERROR' as const, cause })
213
- );
214
-
215
- expect(parsed.ok).toBe(true);
216
- if (parsed.ok) {
217
- expect(parsed.value).toEqual({ key: 'value' });
218
- }
219
- });
220
-
221
- it('should work with from() - handles parse errors', () => {
222
- const parsed = from(
223
- () => JSON.parse('invalid json'),
224
- (cause) => ({ type: 'PARSE_ERROR' as const, cause })
225
- );
226
-
227
- expect(parsed.ok).toBe(false);
228
- if (!parsed.ok) {
229
- expect(parsed.error.type).toBe('PARSE_ERROR');
230
- }
231
- });
232
-
233
- it('should work with fromPromise()', async () => {
234
- // Mock fetch
235
- global.fetch = vi.fn().mockResolvedValue({
236
- ok: true,
237
- json: async () => ({ data: 'test' }),
238
- });
239
-
240
- const result = await fromPromise(
241
- fetch('/api').then(async (r) => {
242
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
243
- return r.json();
244
- }),
245
- () => 'FETCH_FAILED' as const
246
- );
247
-
248
- expect(result.ok).toBe(true);
249
- if (result.ok) {
250
- expect(result.value).toEqual({ data: 'test' });
251
- }
252
- });
253
-
254
- it('should work with fromPromise() - handles fetch errors', async () => {
255
- global.fetch = vi.fn().mockResolvedValue({
256
- ok: false,
257
- status: 404,
258
- });
259
-
260
- const result = await fromPromise(
261
- fetch('/api').then(async (r) => {
262
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
263
- return r.json();
264
- }),
265
- () => 'FETCH_FAILED' as const
266
- );
267
-
268
- expect(result.ok).toBe(false);
269
- if (!result.ok) {
270
- expect(result.error).toBe('FETCH_FAILED');
271
- }
272
- });
273
-
274
- it('should work with tryAsync()', async () => {
275
- const result = await tryAsync(
276
- async () => {
277
- return { data: 'test' };
278
- },
279
- () => 'ASYNC_ERROR' as const
280
- );
281
-
282
- expect(result.ok).toBe(true);
283
- if (result.ok) {
284
- expect(result.value).toEqual({ data: 'test' });
285
- }
286
- });
287
-
288
- it('should work with tryAsync() - handles async errors', async () => {
289
- const result = await tryAsync(
290
- async () => {
291
- throw new Error('Async error');
292
- },
293
- () => 'ASYNC_ERROR' as const
294
- );
295
-
296
- expect(result.ok).toBe(false);
297
- if (!result.ok) {
298
- expect(result.error).toBe('ASYNC_ERROR');
299
- }
300
- });
301
-
302
- it('should work with fromNullable()', () => {
303
- // Simulate a DOM element (in real usage, this would be document.getElementById('app'))
304
- const element = fromNullable(
305
- { id: 'app', tagName: 'DIV' } as unknown as HTMLElement,
306
- () => 'NOT_FOUND' as const
307
- );
308
-
309
- expect(element.ok).toBe(true);
310
- });
311
-
312
- it('should work with fromNullable() - handles null', () => {
313
- const element = fromNullable(
314
- null,
315
- () => 'NOT_FOUND' as const
316
- );
317
-
318
- expect(element.ok).toBe(false);
319
- if (!element.ok) {
320
- expect(element.error).toBe('NOT_FOUND');
321
- }
322
- });
323
- });
324
-
325
- describe('Transformers', () => {
326
- it('should work with map()', () => {
327
- const doubled = map(ok(21), (n) => n * 2);
328
- expect(doubled.ok).toBe(true);
329
- if (doubled.ok) {
330
- expect(doubled.value).toBe(42);
331
- }
332
- });
333
-
334
- it('should work with mapError()', () => {
335
- const mapped = mapError(err('not_found'), (e) => e.toUpperCase());
336
- expect(mapped.ok).toBe(false);
337
- if (!mapped.ok) {
338
- expect(mapped.error).toBe('NOT_FOUND');
339
- }
340
- });
341
-
342
- it('should work with match()', () => {
343
- const result = ok({ name: 'Alice' });
344
- const message = match(result, {
345
- ok: (user) => `Hello ${user.name}`,
346
- err: (error) => `Error: ${error}`,
347
- });
348
-
349
- expect(message).toBe('Hello Alice');
350
- });
351
-
352
- it('should work with match() - error case', () => {
353
- const result = err('NOT_FOUND');
354
- const message = match(result, {
355
- ok: (user) => `Hello ${user.name}`,
356
- err: (error) => `Error: ${error}`,
357
- });
358
-
359
- expect(message).toBe('Error: NOT_FOUND');
360
- });
361
-
362
- it('should work with andThen()', async () => {
363
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
364
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
365
-
366
- const fetchPosts = async (userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
367
- ok([{ id: 1, title: 'Hello' }]);
368
-
369
- const userResult = await fetchUser('1');
370
- const userPosts = await andThen(userResult, (user) => fetchPosts(user.id));
371
-
372
- expect(userPosts.ok).toBe(true);
373
- if (userPosts.ok) {
374
- expect(userPosts.value).toHaveLength(1);
375
- }
376
- });
377
-
378
- it('should work with tap()', () => {
379
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
380
- const result = ok({ name: 'Alice' });
381
- const logged = tap(result, (user) => console.log('Got user:', user.name));
382
-
383
- expect(logged.ok).toBe(true);
384
- expect(consoleSpy).toHaveBeenCalledWith('Got user:', 'Alice');
385
- consoleSpy.mockRestore();
386
- });
387
- });
388
-
389
- describe('Human-in-the-loop (HITL)', () => {
390
- it('should work with createApprovalStep and createHITLCollector', async () => {
391
- const fetchData = async (id: string): AsyncResult<{ data: string }, 'NOT_FOUND'> =>
392
- ok({ data: 'test data' });
393
-
394
- const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
395
- key: 'manager-approval',
396
- checkApproval: async () => {
397
- // Simulate pending approval
398
- return { status: 'pending' as const };
399
- },
400
- pendingReason: 'Waiting for manager approval',
401
- });
402
-
403
- const collector = createHITLCollector();
404
- const workflow = createWorkflow(
405
- { fetchData, requireManagerApproval },
406
- { onEvent: collector.handleEvent }
407
- );
408
-
409
- const result = await workflow(async (step) => {
410
- const data = await step(() => fetchData('123'), { key: 'data' });
411
- const approval = await step(requireManagerApproval, { key: 'manager-approval' });
412
- return { data, approvedBy: approval.approvedBy };
413
- });
414
-
415
- expect(result.ok).toBe(false);
416
- expect(isPendingApproval(result.error)).toBe(true);
417
-
418
- if (!result.ok && isPendingApproval(result.error)) {
419
- expect(result.error.reason).toBe('Waiting for manager approval');
420
- expect(collector.hasPendingApprovals()).toBe(true);
421
- const pending = collector.getPendingApprovals();
422
- expect(pending).toHaveLength(1);
423
- expect(pending[0].stepKey).toBe('manager-approval');
424
- }
425
- });
426
-
427
- it('should work with injectApproval for resuming', async () => {
428
- const fetchData = async (id: string): AsyncResult<{ data: string }, 'NOT_FOUND'> =>
429
- ok({ data: 'test data' });
430
-
431
- const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
432
- key: 'manager-approval',
433
- checkApproval: async () => {
434
- return { status: 'pending' as const };
435
- },
436
- });
437
-
438
- const collector = createHITLCollector();
439
- const workflow1 = createWorkflow(
440
- { fetchData, requireManagerApproval },
441
- { onEvent: collector.handleEvent }
442
- );
443
-
444
- const result1 = await workflow1(async (step) => {
445
- const data = await step(() => fetchData('123'), { key: 'data' });
446
- const approval = await step(requireManagerApproval, { key: 'manager-approval' });
447
- return { data, approvedBy: approval.approvedBy };
448
- });
449
-
450
- expect(result1.ok).toBe(false);
451
- expect(isPendingApproval(result1.error)).toBe(true);
452
-
453
- // Save state
454
- const savedState = collector.getState();
455
-
456
- // Inject approval
457
- const resumeState = injectApproval(savedState, {
458
- stepKey: 'manager-approval',
459
- value: { approvedBy: 'alice@example.com' },
460
- });
461
-
462
- const workflow2 = createWorkflow(
463
- { fetchData, requireManagerApproval },
464
- { resumeState }
465
- );
466
-
467
- const result2 = await workflow2(async (step) => {
468
- const data = await step(() => fetchData('123'), { key: 'data' });
469
- const approval = await step(requireManagerApproval, { key: 'manager-approval' });
470
- return { data, approvedBy: approval.approvedBy };
471
- });
472
-
473
- expect(result2.ok).toBe(true);
474
- if (result2.ok) {
475
- expect(result2.value.approvedBy).toBe('alice@example.com');
476
- }
477
- });
478
-
479
- it('should work with HITL utilities', () => {
480
- const state = {
481
- steps: new Map([
482
- ['step1', { result: ok('value1') }],
483
- ['approval:deploy', { result: err({ type: 'PENDING_APPROVAL' as const, stepKey: 'approval:deploy' }) }],
484
- ['approval:staging', { result: err({ type: 'PENDING_APPROVAL' as const, stepKey: 'approval:staging' }) }],
485
- ]),
486
- };
487
-
488
- expect(hasPendingApproval(state, 'approval:deploy')).toBe(true);
489
- expect(hasPendingApproval(state, 'step1')).toBe(false);
490
-
491
- const pending = getPendingApprovals(state);
492
- expect(pending).toHaveLength(2);
493
- expect(pending).toContain('approval:deploy');
494
- expect(pending).toContain('approval:staging');
495
-
496
- const cleared = clearStep(state, 'approval:deploy');
497
- expect(cleared.steps.has('approval:deploy')).toBe(false);
498
- expect(cleared.steps.has('approval:staging')).toBe(true);
499
- });
500
- });
501
-
502
- describe('Interop with neverthrow', () => {
503
- it('should work with neverthrow Result conversion', async () => {
504
- // Simulate neverthrow Result
505
- type NTResult<T, E> = { isOk: () => boolean; value?: T; error?: E };
506
- const ntResult: NTResult<User, 'NOT_FOUND'> = {
507
- isOk: () => true,
508
- value: { id: '1', name: 'Alice' },
509
- };
510
-
511
- function fromNeverthrow<T, E>(ntResult: NTResult<T, E>): Result<T, E> {
512
- return ntResult.isOk() ? ok(ntResult.value!) : err(ntResult.error!);
513
- }
514
-
515
- const workflow = createWorkflow({});
516
-
517
- const result = await workflow(async (step) => {
518
- const validated = await step(fromNeverthrow(ntResult));
519
- return validated;
520
- });
521
-
522
- expect(result.ok).toBe(true);
523
- if (result.ok) {
524
- expect(result.value.name).toBe('Alice');
525
- }
526
- });
527
- });
528
-
529
- describe('Low-level: run()', () => {
530
- it('should work with run()', async () => {
531
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
532
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
533
-
534
- const result = await run(async (step) => {
535
- const user = await step(fetchUser('1'));
536
- return user;
537
- });
538
-
539
- expect(result.ok).toBe(true);
540
- if (result.ok) {
541
- expect(result.value.name).toBe('Alice');
542
- }
543
- });
544
-
545
- it('should work with run.strict()', async () => {
546
- const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
547
- id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
548
-
549
- type AppError = 'NOT_FOUND' | 'UNAUTHORIZED' | 'UNEXPECTED';
550
-
551
- const result = await run.strict<User, AppError>(
552
- async (step) => {
553
- return await step(fetchUser('1'));
554
- },
555
- { catchUnexpected: () => 'UNEXPECTED' as const }
556
- );
557
-
558
- expect(result.ok).toBe(true);
559
- if (!result.ok) {
560
- const error: AppError = result.error;
561
- expect(['NOT_FOUND', 'UNAUTHORIZED', 'UNEXPECTED']).toContain(error);
562
- }
563
- });
564
- });
565
- });
@@ -1,64 +0,0 @@
1
- # Advanced Documentation Code Verification Report
2
-
3
- ## Summary
4
- All code examples in docs/advanced.md have been verified. **31/31 tests pass** ✅
5
-
6
- ## Test Coverage
7
-
8
- ### ✅ Working Examples
9
-
10
- 1. **Batch operations**
11
- - `all()` - All must succeed (short-circuits on first error)
12
- - `allSettled()` - Collects all errors (great for form validation)
13
- - `any()` - First success wins
14
- - `partition()` - Split successes and failures
15
- - `allAsync()` - Async version of all()
16
- - `allSettledAsync()` - Async version of allSettled()
17
- - `anyAsync()` - Async version of any()
18
-
19
- 2. **Dynamic error mapping**
20
- - `step.try()` with `onError` option for creating errors from caught values
21
- - API error mapping example
22
- - Validation error mapping example
23
-
24
- 3. **Type utilities**
25
- - `ErrorOf<T>` - Extract error type from a function (compile-time only)
26
- - `Errors<T[]>` - Combine errors from multiple functions (compile-time only)
27
- - `ErrorsOfDeps<Deps>` - Extract from a deps object (compile-time only)
28
- - Note: Type utilities are verified via TypeScript compilation, not runtime tests
29
-
30
- 4. **Wrapping existing code**
31
- - `from()` - Sync throwing function wrapper
32
- - `fromPromise()` - Existing promise wrapper
33
- - `tryAsync()` - Async function wrapper
34
- - `fromNullable()` - Nullable value wrapper
35
-
36
- 5. **Transformers**
37
- - `map()` - Transform success values
38
- - `mapError()` - Transform error values
39
- - `match()` - Pattern matching on results
40
- - `andThen()` - Chain results (flatMap)
41
- - `tap()` - Side effects without changing result
42
-
43
- 6. **Human-in-the-loop (HITL)**
44
- - `createApprovalStep()` - Create approval-gated steps
45
- - `createHITLCollector()` - Track pending approvals
46
- - `isPendingApproval()` - Type guard for pending approvals
47
- - `injectApproval()` - Inject approval results for resuming
48
- - `hasPendingApproval()` - Check if step has pending approval
49
- - `getPendingApprovals()` - Get all pending approval step keys
50
- - `clearStep()` - Remove step from resume state
51
-
52
- 7. **Interop with neverthrow**
53
- - Conversion function from neverthrow Result to @jreehal/workflow Result
54
- - Using converted results in workflows
55
-
56
- 8. **Low-level: run()**
57
- - `run()` - One-off workflow execution
58
- - `run.strict()` - Closed error union without UnexpectedError
59
-
60
- ## All Tests Pass ✅
61
-
62
- All examples in docs/advanced.md are verified and working correctly.
63
-
64
- Run with: `pnpm vitest run docs/advanced.test.ts`