@jagreehal/workflow 1.0.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,234 @@
1
+ # Advanced Usage
2
+
3
+ For core concepts (`createWorkflow`, `step`, `step.try`), see the [README](../README.md).
4
+
5
+ ## Batch operations
6
+
7
+ ```typescript
8
+ import { all, allSettled, any, partition } from '@jagreehal/workflow';
9
+
10
+ // All must succeed (short-circuits on first error)
11
+ const combined = all([ok(1), ok(2), ok(3)]); // ok([1, 2, 3])
12
+
13
+ // Collect ALL errors (great for form validation)
14
+ const validated = allSettled([
15
+ validateEmail(email),
16
+ validatePassword(password),
17
+ ]);
18
+ // If any fail: err(['INVALID_EMAIL', 'WEAK_PASSWORD'])
19
+
20
+ // First success wins
21
+ const first = any([err('A'), ok('success'), err('B')]); // ok('success')
22
+
23
+ // Split successes and failures
24
+ const { values, errors } = partition(results);
25
+ ```
26
+
27
+ Async versions: `allAsync`, `allSettledAsync`, `anyAsync`.
28
+
29
+ ## Dynamic error mapping
30
+
31
+ Use `{ onError }` instead of `{ error }` to create errors from the caught value:
32
+
33
+ ```typescript
34
+ const result = await workflow(async (step) => {
35
+ const data = await step.try(
36
+ () => fetchExternalApi(),
37
+ { onError: (e) => ({ type: 'API_ERROR' as const, message: String(e) }) }
38
+ );
39
+
40
+ // Or extract specific error info
41
+ const parsed = await step.try(
42
+ () => schema.parse(data),
43
+ { onError: (e) => ({ type: 'VALIDATION_ERROR' as const, issues: e.issues }) }
44
+ );
45
+
46
+ return parsed;
47
+ });
48
+ ```
49
+
50
+ ## Type utilities
51
+
52
+ ```typescript
53
+ import { type ErrorOf, type Errors, type ErrorsOfDeps } from '@jagreehal/workflow';
54
+
55
+ // Extract error type from a function
56
+ type UserError = ErrorOf<typeof fetchUser>; // 'NOT_FOUND'
57
+
58
+ // Combine errors from multiple functions
59
+ type AppError = Errors<[typeof fetchUser, typeof fetchPosts]>;
60
+ // 'NOT_FOUND' | 'FETCH_ERROR'
61
+
62
+ // Extract from a deps object (same as createWorkflow uses)
63
+ type WorkflowErrors = ErrorsOfDeps<{ fetchUser: typeof fetchUser }>;
64
+ ```
65
+
66
+ ## Wrapping existing code
67
+
68
+ ```typescript
69
+ import { from, fromPromise, tryAsync, fromNullable } from '@jagreehal/workflow';
70
+
71
+ // Sync throwing function
72
+ const parsed = from(
73
+ () => JSON.parse(input),
74
+ (cause) => ({ type: 'PARSE_ERROR' as const, cause })
75
+ );
76
+
77
+ // Existing promise (remember: fetch needs r.ok check!)
78
+ const result = await fromPromise(
79
+ fetch('/api').then(r => {
80
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
81
+ return r.json();
82
+ }),
83
+ () => 'FETCH_FAILED' as const
84
+ );
85
+
86
+ // Nullable → Result
87
+ const element = fromNullable(
88
+ document.getElementById('app'),
89
+ () => 'NOT_FOUND' as const
90
+ );
91
+ ```
92
+
93
+ ## Transformers
94
+
95
+ ```typescript
96
+ import { map, mapError, match, andThen, tap } from '@jagreehal/workflow';
97
+
98
+ const doubled = map(ok(21), n => n * 2); // ok(42)
99
+
100
+ const mapped = mapError(err('not_found'), e => e.toUpperCase()); // err('NOT_FOUND')
101
+
102
+ const message = match(result, {
103
+ ok: (user) => `Hello ${user.name}`,
104
+ err: (error) => `Error: ${error}`,
105
+ });
106
+
107
+ // Chain results (flatMap)
108
+ const userPosts = andThen(fetchUser('1'), user => fetchPosts(user.id));
109
+
110
+ // Side effects without changing result
111
+ const logged = tap(result, user => console.log('Got user:', user.name));
112
+ ```
113
+
114
+ ## Human-in-the-loop (HITL)
115
+
116
+ Build workflows that pause for human approval:
117
+
118
+ ```typescript
119
+ import {
120
+ createWorkflow,
121
+ createApprovalStep,
122
+ createHITLCollector,
123
+ isPendingApproval,
124
+ injectApproval,
125
+ } from '@jagreehal/workflow';
126
+
127
+ // 1. Create an approval step
128
+ const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
129
+ key: 'manager-approval',
130
+ checkApproval: async () => {
131
+ const approval = await db.getApproval('manager-approval');
132
+ if (!approval) return { status: 'pending' };
133
+ if (approval.rejected) return { status: 'rejected', reason: approval.reason };
134
+ return { status: 'approved', value: { approvedBy: approval.manager } };
135
+ },
136
+ pendingReason: 'Waiting for manager approval',
137
+ });
138
+
139
+ // 2. Use in workflow with collector
140
+ const collector = createHITLCollector();
141
+ const workflow = createWorkflow(
142
+ { fetchData, requireManagerApproval },
143
+ { onEvent: collector.handleEvent }
144
+ );
145
+
146
+ const result = await workflow(async (step) => {
147
+ const data = await step(() => fetchData('123'), { key: 'data' });
148
+ const approval = await step(requireManagerApproval, { key: 'manager-approval' });
149
+ return { data, approvedBy: approval.approvedBy };
150
+ });
151
+
152
+ // 3. Handle pending state
153
+ if (!result.ok && isPendingApproval(result.error)) {
154
+ // Save state for later resume
155
+ await saveToDatabase(collector.getState());
156
+ console.log(`Workflow paused: ${result.error.reason}`);
157
+ }
158
+
159
+ // 4. Resume after approval granted
160
+ const savedState = await loadFromDatabase();
161
+ const resumeState = injectApproval(savedState, {
162
+ stepKey: 'manager-approval',
163
+ value: { approvedBy: 'alice@example.com' }
164
+ });
165
+
166
+ const workflow2 = createWorkflow(
167
+ { fetchData, requireManagerApproval },
168
+ { resumeState }
169
+ );
170
+ // Re-run same workflow body — cached steps skip, approval injected
171
+ ```
172
+
173
+ ### HITL utilities
174
+
175
+ ```typescript
176
+ // Check approval state
177
+ hasPendingApproval(state, 'approval-key') // boolean
178
+ getPendingApprovals(state) // string[]
179
+
180
+ // Modify state
181
+ clearStep(state, 'step-key') // Remove step from state
182
+ injectApproval(state, { stepKey, value }) // Add approval result
183
+ ```
184
+
185
+ ## Interop with neverthrow
186
+
187
+ ```typescript
188
+ import { Result as NTResult } from 'neverthrow';
189
+ import { ok, err, type Result } from '@jagreehal/workflow';
190
+
191
+ function fromNeverthrow<T, E>(ntResult: NTResult<T, E>): Result<T, E> {
192
+ return ntResult.isOk() ? ok(ntResult.value) : err(ntResult.error);
193
+ }
194
+
195
+ // Use in workflow
196
+ const result = await workflow(async (step) => {
197
+ const validated = await step(fromNeverthrow(validateInput(data)));
198
+ return validated;
199
+ });
200
+ ```
201
+
202
+ ## Low-level: run()
203
+
204
+ `createWorkflow` is built on `run()`. Use it for one-off workflows:
205
+
206
+ ```typescript
207
+ import { run } from '@jagreehal/workflow';
208
+
209
+ const result = await run(async (step) => {
210
+ const user = await step(fetchUser(id));
211
+ return user;
212
+ });
213
+ ```
214
+
215
+ ### run.strict()
216
+
217
+ For closed error unions without `UnexpectedError`:
218
+
219
+ ```typescript
220
+ import { run } from '@jagreehal/workflow';
221
+
222
+ type AppError = 'NOT_FOUND' | 'UNAUTHORIZED' | 'UNEXPECTED';
223
+
224
+ const result = await run.strict<User, AppError>(
225
+ async (step) => {
226
+ return await step(fetchUser(id));
227
+ },
228
+ { catchUnexpected: () => 'UNEXPECTED' as const }
229
+ );
230
+
231
+ // result.error: 'NOT_FOUND' | 'UNAUTHORIZED' | 'UNEXPECTED' (exactly)
232
+ ```
233
+
234
+ Prefer `createWorkflow` for automatic error type inference.
package/docs/api.md ADDED
@@ -0,0 +1,195 @@
1
+ # API Reference
2
+
3
+ ## Workflows
4
+
5
+ ### createWorkflow
6
+
7
+ ```typescript
8
+ createWorkflow(deps) // Auto-inferred error types
9
+ createWorkflow(deps, { strict: true, catchUnexpected }) // Closed error union
10
+ createWorkflow(deps, { onEvent, createContext }) // Event stream + context
11
+ createWorkflow(deps, { cache }) // Step caching
12
+ createWorkflow(deps, { resumeState }) // Resume from saved state
13
+ ```
14
+
15
+ ### step
16
+
17
+ ```typescript
18
+ step(result) // Unwrap Result or exit early
19
+ step(result, { name, key }) // With tracing/caching options
20
+ step(() => result) // Lazy form (for caching/resume)
21
+ step(() => result, { name, key }) // Lazy with options
22
+ ```
23
+
24
+ ### step.try
25
+
26
+ ```typescript
27
+ step.try(fn, { error }) // Static error type
28
+ step.try(fn, { onError }) // Dynamic error from caught value
29
+ step.try(fn, { error, name, key }) // With tracing options
30
+ ```
31
+
32
+ ### Low-level run
33
+
34
+ ```typescript
35
+ run(fn) // One-off workflow
36
+ run(fn, { onError }) // With error callback
37
+ run.strict(fn, { catchUnexpected }) // Closed error union
38
+ ```
39
+
40
+ ### Event helpers
41
+
42
+ ```typescript
43
+ isStepComplete(event) // Type guard for step_complete events
44
+ ```
45
+
46
+ ## Results
47
+
48
+ ### Constructors
49
+
50
+ ```typescript
51
+ ok(value) // Create success
52
+ err(error) // Create error
53
+ err(error, { cause }) // Create error with cause
54
+ ```
55
+
56
+ ### Type guards
57
+
58
+ ```typescript
59
+ isOk(result) // result is { ok: true, value }
60
+ isErr(result) // result is { ok: false, error }
61
+ isUnexpectedError(error) // error is UnexpectedError
62
+ ```
63
+
64
+ ## Unwrap
65
+
66
+ ```typescript
67
+ unwrap(result) // Value or throw UnwrapError
68
+ unwrapOr(result, defaultValue) // Value or default
69
+ unwrapOrElse(result, fn) // Value or compute from error
70
+ ```
71
+
72
+ ## Wrap
73
+
74
+ ```typescript
75
+ from(fn) // Sync throwing → Result
76
+ from(fn, onError) // With error mapper
77
+ fromPromise(promise) // Promise → Result
78
+ fromPromise(promise, onError) // With error mapper
79
+ tryAsync(fn) // Async fn → Result
80
+ tryAsync(fn, onError) // With error mapper
81
+ fromNullable(value, onNull) // Nullable → Result
82
+ ```
83
+
84
+ ## Transform
85
+
86
+ ```typescript
87
+ map(result, fn) // Transform value
88
+ mapError(result, fn) // Transform error
89
+ mapTry(result, fn, onError) // Transform value, catch throws
90
+ mapErrorTry(result, fn, onError) // Transform error, catch throws
91
+ andThen(result, fn) // Chain (flatMap)
92
+ match(result, { ok, err }) // Pattern match
93
+ tap(result, fn) // Side effect on success
94
+ tapError(result, fn) // Side effect on error
95
+ ```
96
+
97
+ ## Batch
98
+
99
+ ```typescript
100
+ all(results) // All succeed (sync, short-circuits)
101
+ allAsync(results) // All succeed (async, short-circuits)
102
+ any(results) // First success (sync)
103
+ anyAsync(results) // First success (async)
104
+ allSettled(results) // Collect all; error array if any fail (sync)
105
+ allSettledAsync(results) // Collect all; error array if any fail (async)
106
+ partition(results) // Split into { values, errors }
107
+ ```
108
+
109
+ ## Human-in-the-Loop (HITL)
110
+
111
+ ### Creating approval steps
112
+
113
+ ```typescript
114
+ createApprovalStep<T>(options) // Create approval-gated step function
115
+ // options: { key, checkApproval, pendingReason?, rejectedReason? }
116
+ ```
117
+
118
+ ### Checking approval status
119
+
120
+ ```typescript
121
+ isPendingApproval(error) // error is PendingApproval
122
+ isApprovalRejected(error) // error is ApprovalRejected
123
+ pendingApproval(stepKey, options?) // Create PendingApproval error
124
+ ```
125
+
126
+ ### Managing approval state
127
+
128
+ ```typescript
129
+ createHITLCollector() // Collect step events for resume
130
+ injectApproval(state, { stepKey, value }) // Add approval to resume state
131
+ clearStep(state, stepKey) // Remove step from resume state
132
+ hasPendingApproval(state, stepKey) // Check if step is pending
133
+ getPendingApprovals(state) // Get all pending step keys
134
+ ```
135
+
136
+ ## Types
137
+
138
+ ### Core Result types
139
+
140
+ ```typescript
141
+ Result<T, E, C> // { ok: true, value: T } | { ok: false, error: E, cause?: C }
142
+ AsyncResult<T, E, C> // Promise<Result<T, E, C>>
143
+ UnexpectedError // { type: 'UNEXPECTED_ERROR', cause: unknown }
144
+ ```
145
+
146
+ ### Workflow types
147
+
148
+ ```typescript
149
+ Workflow<E, Deps> // Non-strict workflow return type
150
+ WorkflowStrict<E, U, Deps> // Strict workflow return type
151
+ WorkflowOptions<E, C> // Options for createWorkflow
152
+ WorkflowOptionsStrict<E, U, C> // Options for strict createWorkflow
153
+ AnyResultFn // Constraint for Result-returning functions
154
+ ```
155
+
156
+ ### Event types
157
+
158
+ ```typescript
159
+ WorkflowEvent<E> // Union of all event types
160
+ StepOptions // { name?: string, key?: string }
161
+ ```
162
+
163
+ ### Cache & Resume types
164
+
165
+ ```typescript
166
+ StepCache // Cache interface (get/set/has/delete/clear)
167
+ ResumeState // { steps: Map<string, ResumeStateEntry> }
168
+ ResumeStateEntry // { result: Result, meta?: StepFailureMeta }
169
+ ```
170
+
171
+ ### HITL types
172
+
173
+ ```typescript
174
+ PendingApproval // { type: 'PENDING_APPROVAL', stepKey, reason? }
175
+ ApprovalRejected // { type: 'APPROVAL_REJECTED', stepKey, reason? }
176
+ ```
177
+
178
+ ### Type extraction utilities
179
+
180
+ ```typescript
181
+ ErrorOf<Fn> // Extract error type from function
182
+ CauseOf<Fn> // Extract cause type from function
183
+ Errors<[Fn1, Fn2, ...]> // Union of error types from functions
184
+ ErrorsOfDeps<Deps> // Extract errors from deps object
185
+ CausesOfDeps<Deps> // Extract causes from deps object
186
+ ExtractValue<Result> // Extract value type from Result
187
+ ExtractError<Result> // Extract error type from Result
188
+ ExtractCause<Result> // Extract cause type from Result
189
+ ```
190
+
191
+ ### Error types
192
+
193
+ ```typescript
194
+ UnwrapError<E, C> // Error thrown by unwrap()
195
+ ```
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "@jagreehal/workflow",
3
+ "version": "1.0.0",
4
+ "type": "module",
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
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./core": {
16
+ "types": "./dist/core.d.ts",
17
+ "import": "./dist/core.js",
18
+ "require": "./dist/core.cjs"
19
+ },
20
+ "./workflow": {
21
+ "types": "./dist/workflow.d.ts",
22
+ "import": "./dist/workflow.js",
23
+ "require": "./dist/workflow.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "docs"
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
+ "keywords": [
47
+ "workflow",
48
+ "workflows",
49
+ "result",
50
+ "result-type",
51
+ "error-handling",
52
+ "typescript",
53
+ "async",
54
+ "async-await",
55
+ "type-safe",
56
+ "type-inference",
57
+ "railway-oriented-programming",
58
+ "functional-programming",
59
+ "either",
60
+ "monad",
61
+ "step",
62
+ "orchestration",
63
+ "pipeline",
64
+ "early-exit",
65
+ "resume",
66
+ "caching",
67
+ "hitl",
68
+ "human-in-the-loop"
69
+ ],
70
+ "author": "Jag Reehal <jag@jagreehal.com>",
71
+ "homepage": "https://github.com/jagreehal/workflow#readme",
72
+ "bugs": {
73
+ "url": "https://github.com/jagreehal/workflow/issues"
74
+ },
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+https://github.com/jagreehal/workflow.git"
78
+ },
79
+ "license": "MIT",
80
+ "devDependencies": {
81
+ "@changesets/cli": "^2.29.8",
82
+ "@eslint/js": "^9.39.2",
83
+ "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
84
+ "@total-typescript/ts-reset": "^0.6.1",
85
+ "@total-typescript/tsconfig": "^1.0.4",
86
+ "@types/node": "^25.0.3",
87
+ "@types/picomatch": "^4.0.2",
88
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
89
+ "@typescript-eslint/parser": "^8.50.0",
90
+ "@typescript-eslint/rule-tester": "^8.50.0",
91
+ "@typescript-eslint/utils": "^8.50.0",
92
+ "@vitest/coverage-v8": "^4.0.16",
93
+ "eslint": "9.39.2",
94
+ "eslint-config-prettier": "^10.1.8",
95
+ "eslint-plugin-unicorn": "^62.0.0",
96
+ "tsd": "^0.33.0",
97
+ "tsup": "^8.5.1",
98
+ "typescript": "^5.9.3",
99
+ "typescript-eslint": "^8.50.0",
100
+ "vitest": "^4.0.16"
101
+ },
102
+ "engines": {
103
+ "node": ">=18.0.0"
104
+ },
105
+ "tsd": {
106
+ "directory": "src",
107
+ "testFilePattern": "**/index.test-d.ts",
108
+ "compilerOptions": {
109
+ "strict": true,
110
+ "noUnusedLocals": false,
111
+ "noUnusedParameters": false,
112
+ "skipLibCheck": true
113
+ }
114
+ },
115
+ "publishConfig": {
116
+ "access": "public",
117
+ "registry": "https://registry.npmjs.org/"
118
+ }
119
+ }