@jagreehal/workflow 1.11.0 → 1.13.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/README.md +1197 -20
- package/dist/batch.cjs +1 -1
- package/dist/batch.cjs.map +1 -1
- package/dist/batch.js +1 -1
- package/dist/batch.js.map +1 -1
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +31 -17
- package/dist/core.d.ts +31 -17
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/duration.cjs +2 -0
- package/dist/duration.cjs.map +1 -0
- package/dist/duration.d.cts +246 -0
- package/dist/duration.d.ts +246 -0
- package/dist/duration.js +2 -0
- package/dist/duration.js.map +1 -0
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/match.cjs +2 -0
- package/dist/match.cjs.map +1 -0
- package/dist/match.d.cts +216 -0
- package/dist/match.d.ts +216 -0
- package/dist/match.js +2 -0
- package/dist/match.js.map +1 -0
- package/dist/resource.cjs +1 -1
- package/dist/resource.cjs.map +1 -1
- package/dist/resource.js +1 -1
- package/dist/resource.js.map +1 -1
- package/dist/schedule.cjs +2 -0
- package/dist/schedule.cjs.map +1 -0
- package/dist/schedule.d.cts +387 -0
- package/dist/schedule.d.ts +387 -0
- package/dist/schedule.js +2 -0
- package/dist/schedule.js.map +1 -0
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.js +1 -1
- package/dist/workflow.js.map +1 -1
- package/docs/api.md +30 -0
- package/docs/coming-from-neverthrow.md +103 -10
- package/docs/effect-features-to-port.md +210 -0
- package/docs/match-examples.test.ts +558 -0
- package/docs/match.md +417 -0
- package/docs/policies-examples.test.ts +750 -0
- package/docs/policies.md +508 -0
- package/docs/resource-management-examples.test.ts +729 -0
- package/docs/resource-management.md +509 -0
- package/docs/schedule-examples.test.ts +736 -0
- package/docs/schedule.md +467 -0
- package/docs/tagged-error-examples.test.ts +494 -0
- package/docs/tagged-error.md +730 -0
- package/docs/visualization-examples.test.ts +663 -0
- package/docs/visualization.md +395 -0
- package/docs/visualize-examples.md +1 -1
- package/package.json +17 -2
package/docs/api.md
CHANGED
|
@@ -33,6 +33,36 @@ step.try(fn, { onError }) // Dynamic error from caught value
|
|
|
33
33
|
step.try(fn, { error, name, key }) // With tracing options
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### step.fromResult
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
step.fromResult(fn, { error }) // Map any Result error to static type
|
|
40
|
+
step.fromResult(fn, { onError }) // Map Result error dynamically
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### step.retry
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
step.retry(fn, { attempts, backoff }) // Basic retry
|
|
47
|
+
step.retry(fn, { attempts, timeout }) // Retry with per-attempt timeout
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### step.withTimeout
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
step.withTimeout(fn, { ms }) // Simple timeout
|
|
54
|
+
step.withTimeout(fn, { ms, signal }) // With AbortSignal propagation
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### step.parallel / step.race / step.allSettled
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
step.parallel(name, () => ...) // Parallel group with scope events
|
|
61
|
+
step.parallel({ key: fn }, opts) // Named object form
|
|
62
|
+
step.race(name, () => ...) // Race group with scope events
|
|
63
|
+
step.allSettled(name, () => ...) // allSettled group with scope events
|
|
64
|
+
```
|
|
65
|
+
|
|
36
66
|
### Low-level run
|
|
37
67
|
|
|
38
68
|
```typescript
|
|
@@ -4,8 +4,11 @@ You already get it: **errors should be in the type system, not hidden behind `un
|
|
|
4
4
|
|
|
5
5
|
The difference? neverthrow gives you typed Results. This library gives you typed Results *plus* orchestration—retries, timeouts, caching, resume, and visualization built in.
|
|
6
6
|
|
|
7
|
+
This library **automatically infers error types** from your dependencies. No more manually tracking error unions—add a step, the union updates. Remove one? It updates. TypeScript enforces it at compile time.
|
|
8
|
+
|
|
7
9
|
**TL;DR:**
|
|
8
10
|
- `andThen` chains → `step()` calls with async/await
|
|
11
|
+
- **Automatic error inference** — no manual union tracking
|
|
9
12
|
- Same error-first mindset, different syntax
|
|
10
13
|
- Keep your existing neverthrow code—they interop cleanly
|
|
11
14
|
|
|
@@ -15,10 +18,10 @@ The difference? neverthrow gives you typed Results. This library gives you typed
|
|
|
15
18
|
|---|------------|---------------------|
|
|
16
19
|
| **Mental model** | "The Realist" — explicit about what can fail | "The Orchestrator" — explicit failures + execution control |
|
|
17
20
|
| **Syntax** | Functional chaining (`.andThen()`, `.map()`) | Imperative async/await with `step()` |
|
|
18
|
-
| **Error inference** | Manual union types | Automatic from dependencies |
|
|
21
|
+
| **Error inference** | ⚠️ **Manual union types** — you maintain them | ✅ **Automatic from dependencies** — always in sync |
|
|
19
22
|
| **Orchestration** | DIY (retries, caching, timeouts) | Built-in primitives |
|
|
20
23
|
|
|
21
|
-
Both make your functions *honest*—the signature says what can go wrong. The difference
|
|
24
|
+
Both make your functions *honest*—the signature says what can go wrong. The key difference: **workflow eliminates the manual union tracking burden** while adding orchestration features.
|
|
22
25
|
|
|
23
26
|
---
|
|
24
27
|
|
|
@@ -107,6 +110,65 @@ Both neverthrow and workflow fix this by making errors part of the return type.
|
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
113
|
+
## The manual union tracking problem
|
|
114
|
+
|
|
115
|
+
**neverthrow's pain point:** You must manually declare and maintain error unions. Every time you add or remove a step, you update the type annotation:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ❌ Manual union tracking - easy to get out of sync
|
|
119
|
+
type SignUpError = 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'DB_ERROR' | 'EMAIL_EXISTS';
|
|
120
|
+
|
|
121
|
+
const signUp = (
|
|
122
|
+
email: string,
|
|
123
|
+
password: string
|
|
124
|
+
): ResultAsync<User, SignUpError> =>
|
|
125
|
+
validateEmail(email)
|
|
126
|
+
.andThen(() => validatePassword(password))
|
|
127
|
+
.andThen(() => checkDuplicate(email)) // Oops! Forgot to add 'EMAIL_EXISTS' to SignUpError
|
|
128
|
+
.andThen(() => createUser(email, password));
|
|
129
|
+
|
|
130
|
+
// TypeScript won't catch this - the error union is manually declared
|
|
131
|
+
// You'll only find out at runtime when you try to handle 'EMAIL_EXISTS'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**What happens:**
|
|
135
|
+
- Add a new step? Update the union manually
|
|
136
|
+
- Remove a step? Update the union manually
|
|
137
|
+
- Forget to update? TypeScript won't catch it
|
|
138
|
+
- Switch on an error that can't happen? TypeScript won't warn you
|
|
139
|
+
- Miss handling a possible error? TypeScript won't warn you
|
|
140
|
+
|
|
141
|
+
**workflow's solution:** Error types are **automatically inferred** from your dependencies:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// ✅ Automatic inference - always in sync
|
|
145
|
+
const signUp = createWorkflow({
|
|
146
|
+
validateEmail, // returns AsyncResult<string, 'INVALID_EMAIL'>
|
|
147
|
+
validatePassword, // returns AsyncResult<string, 'WEAK_PASSWORD'>
|
|
148
|
+
checkDuplicate, // returns AsyncResult<void, 'EMAIL_EXISTS'>
|
|
149
|
+
createUser, // returns AsyncResult<User, 'DB_ERROR'>
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await signUp(async (step, deps) => {
|
|
153
|
+
const email = await step(deps.validateEmail('alice@example.com'));
|
|
154
|
+
const password = await step(deps.validatePassword('securepass123'));
|
|
155
|
+
await step(deps.checkDuplicate(email));
|
|
156
|
+
return await step(deps.createUser(email, password));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// TypeScript knows: Result<User, 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'EMAIL_EXISTS' | 'DB_ERROR' | UnexpectedError>
|
|
160
|
+
// Add a step? Union updates automatically. Remove one? Updates automatically.
|
|
161
|
+
// Switch on an impossible error? TypeScript error. Miss a possible error? TypeScript error.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**The difference:**
|
|
165
|
+
- neverthrow: You maintain the error union manually (error-prone)
|
|
166
|
+
- workflow: The error union is computed from your dependencies (always correct)
|
|
167
|
+
|
|
168
|
+
This is especially valuable in complex workflows with 5+ steps where manual tracking becomes a maintenance burden.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
110
172
|
## Pattern-by-pattern comparison
|
|
111
173
|
|
|
112
174
|
### Basic Result construction
|
|
@@ -426,10 +488,12 @@ const enrichedResult = mapError(userResult, error => ({
|
|
|
426
488
|
|
|
427
489
|
### Automatic error type inference
|
|
428
490
|
|
|
491
|
+
> Error unions are computed automatically—no manual tracking, no drift, no bugs.
|
|
492
|
+
|
|
429
493
|
**neverthrow** requires you to declare error unions explicitly:
|
|
430
494
|
|
|
431
495
|
```typescript
|
|
432
|
-
// You MUST manually track the error union
|
|
496
|
+
// ❌ You MUST manually track the error union
|
|
433
497
|
type SignUpError = 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'DB_ERROR';
|
|
434
498
|
|
|
435
499
|
const signUp = (
|
|
@@ -439,14 +503,27 @@ const signUp = (
|
|
|
439
503
|
validateEmail(email)
|
|
440
504
|
.andThen(() => validatePassword(password))
|
|
441
505
|
.andThen(() => createUser(email, password));
|
|
506
|
+
|
|
507
|
+
// What happens when you add a step?
|
|
508
|
+
// 1. Add checkDuplicate() that returns 'EMAIL_EXISTS'
|
|
509
|
+
// 2. Remember to update SignUpError type
|
|
510
|
+
// 3. If you forget? TypeScript won't catch it
|
|
511
|
+
// 4. Runtime error when you try to handle 'EMAIL_EXISTS'
|
|
442
512
|
```
|
|
443
513
|
|
|
514
|
+
**The pain:**
|
|
515
|
+
- Add a step? Update the union manually
|
|
516
|
+
- Remove a step? Update the union manually
|
|
517
|
+
- Forget to update? Silent type error
|
|
518
|
+
- Switch on impossible error? No warning
|
|
519
|
+
- Miss handling possible error? No warning
|
|
520
|
+
|
|
444
521
|
**workflow** with `createWorkflow` infers them automatically:
|
|
445
522
|
|
|
446
523
|
```typescript
|
|
447
524
|
import { createWorkflow } from '@jagreehal/workflow';
|
|
448
525
|
|
|
449
|
-
// NO manual type annotation needed!
|
|
526
|
+
// ✅ NO manual type annotation needed!
|
|
450
527
|
const signUp = createWorkflow({
|
|
451
528
|
validateEmail, // returns AsyncResult<string, 'INVALID_EMAIL'>
|
|
452
529
|
validatePassword, // returns AsyncResult<string, 'WEAK_PASSWORD'>
|
|
@@ -462,7 +539,23 @@ const result = await signUp(async (step, deps) => {
|
|
|
462
539
|
// TypeScript knows: Result<User, 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'DB_ERROR' | UnexpectedError>
|
|
463
540
|
```
|
|
464
541
|
|
|
465
|
-
|
|
542
|
+
**Add a step?** The union updates automatically:
|
|
543
|
+
```typescript
|
|
544
|
+
// Add checkDuplicate - union updates automatically
|
|
545
|
+
const signUp = createWorkflow({
|
|
546
|
+
validateEmail,
|
|
547
|
+
validatePassword,
|
|
548
|
+
checkDuplicate, // returns AsyncResult<void, 'EMAIL_EXISTS'>
|
|
549
|
+
createUser,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Now TypeScript knows: 'INVALID_EMAIL' | 'WEAK_PASSWORD' | 'EMAIL_EXISTS' | 'DB_ERROR' | UnexpectedError
|
|
553
|
+
// No manual type update needed!
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Remove a step?** The union updates automatically. **Switch on impossible error?** TypeScript error. **Miss handling possible error?** TypeScript error.
|
|
557
|
+
|
|
558
|
+
The error union **always** matches your actual dependencies. This becomes invaluable in complex workflows with 5+ steps where manual tracking becomes error-prone.
|
|
466
559
|
|
|
467
560
|
---
|
|
468
561
|
|
|
@@ -761,11 +854,11 @@ const data = await limiter.execute(async () => {
|
|
|
761
854
|
Render workflow execution:
|
|
762
855
|
|
|
763
856
|
```typescript
|
|
764
|
-
import {
|
|
857
|
+
import { createVisualizer } from '@jagreehal/workflow/visualize';
|
|
765
858
|
|
|
766
|
-
const
|
|
859
|
+
const viz = createVisualizer({ workflowName: 'User posts flow' });
|
|
767
860
|
const workflow = createWorkflow({ fetchUser, fetchPosts }, {
|
|
768
|
-
onEvent:
|
|
861
|
+
onEvent: viz.handleEvent,
|
|
769
862
|
});
|
|
770
863
|
|
|
771
864
|
await workflow(async (step, deps) => {
|
|
@@ -774,8 +867,8 @@ await workflow(async (step, deps) => {
|
|
|
774
867
|
return { user, posts };
|
|
775
868
|
});
|
|
776
869
|
|
|
777
|
-
console.log(
|
|
778
|
-
console.log(
|
|
870
|
+
console.log(viz.render()); // ASCII for terminal
|
|
871
|
+
console.log(viz.renderAs('mermaid')); // Mermaid for docs
|
|
779
872
|
```
|
|
780
873
|
|
|
781
874
|
---
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# High-Value Effect Features to Port
|
|
2
|
+
|
|
3
|
+
## What Makes People *Love* Effect?
|
|
4
|
+
|
|
5
|
+
The "aha moments" in Effect are:
|
|
6
|
+
1. **Everything composes** - schedules, errors, resources combine naturally
|
|
7
|
+
2. **The type system catches mistakes** - exhaustive checks, no runtime surprises
|
|
8
|
+
3. **Declarative over imperative** - describe what you want, not how
|
|
9
|
+
|
|
10
|
+
Your library already delivers on #2 and #3. The biggest gap is **composability** for schedules.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features That People Would **Miss** If Gone
|
|
15
|
+
|
|
16
|
+
### 1. **Schedule Combinators** ⭐⭐⭐⭐⭐ (The "Aha Moment")
|
|
17
|
+
**What you have now** (from `src/policies.ts`):
|
|
18
|
+
```typescript
|
|
19
|
+
// Static presets - pick one
|
|
20
|
+
retryPolicies.transient // 3 attempts, exponential
|
|
21
|
+
retryPolicies.aggressive // 5 attempts, exponential
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**What Effect has** - composable at runtime:
|
|
25
|
+
```typescript
|
|
26
|
+
// "Exponential with jitter, but if that fails, poll every minute forever"
|
|
27
|
+
const schedule = Schedule.exponential("100ms")
|
|
28
|
+
.pipe(Schedule.jittered)
|
|
29
|
+
.pipe(Schedule.upTo(5))
|
|
30
|
+
.pipe(Schedule.andThen(Schedule.spaced("1 minute")))
|
|
31
|
+
|
|
32
|
+
// "Retry until the output satisfies a condition"
|
|
33
|
+
const untilHealthy = Schedule.spaced("5s")
|
|
34
|
+
.pipe(Schedule.whileOutput(resp => resp.status !== "healthy"))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Why people love it**:
|
|
38
|
+
- **Real-world scenarios**: "Retry 3x fast, then back off to polling" is common but hard to express
|
|
39
|
+
- **Type-safe composition**: Each combinator is typed, IDE autocompletes the next options
|
|
40
|
+
- **Declarative**: You describe the *shape* of retries, not the loop mechanics
|
|
41
|
+
|
|
42
|
+
**Key combinators to port**:
|
|
43
|
+
| Combinator | What it does |
|
|
44
|
+
|------------|--------------|
|
|
45
|
+
| `andThen` | Chain schedules: first exhaust A, then use B |
|
|
46
|
+
| `union` | Take the shorter delay of two schedules |
|
|
47
|
+
| `intersect` | Take the longer delay of two schedules |
|
|
48
|
+
| `jittered` | Add randomness (thundering herd prevention) |
|
|
49
|
+
| `whileInput/Output` | Continue while condition holds |
|
|
50
|
+
| `upTo(n)` | Limit to n repetitions |
|
|
51
|
+
| `elapsed` | Track cumulative time for timeouts |
|
|
52
|
+
|
|
53
|
+
**Effort**: Medium | **Impact**: Very High (transforms retry experience)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### 2. **Schema (Validation that Returns Results)** ⭐⭐⭐⭐
|
|
58
|
+
**What exists**: Zod, Yup, etc. - they all throw on failure
|
|
59
|
+
**What Effect has**: Validation that returns `Result<T, ParseError>`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const UserSchema = Schema.Struct({
|
|
63
|
+
id: Schema.NonEmptyString,
|
|
64
|
+
email: Schema.Email,
|
|
65
|
+
age: Schema.Int.pipe(Schema.between(0, 150)),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Integrates with your workflow!
|
|
69
|
+
const result = await workflow(async (step) => {
|
|
70
|
+
const input = await step(Schema.decode(UserSchema, rawInput)) // early-exit on invalid
|
|
71
|
+
return processUser(input)
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Why people love it**:
|
|
76
|
+
- **No try/catch** - validation failures are just Results
|
|
77
|
+
- **Encode + Decode** - bidirectional transforms (API responses, database rows)
|
|
78
|
+
- **Composition** - extend schemas, union them, transform them
|
|
79
|
+
|
|
80
|
+
**However**: Zod + Result wrapper might be "good enough" - this is a big undertaking.
|
|
81
|
+
|
|
82
|
+
**Effort**: Very High | **Impact**: High (but alternatives exist)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### 3. **Match (Exhaustive Pattern Matching)** ⭐⭐⭐⭐
|
|
87
|
+
**What you have**: `TaggedError.match()` - but only for errors
|
|
88
|
+
**What Effect has**: Pattern matching on **any** discriminated union
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Works on any { _tag: string } union
|
|
92
|
+
type Event =
|
|
93
|
+
| { _tag: 'UserCreated'; user: User }
|
|
94
|
+
| { _tag: 'UserUpdated'; userId: string; changes: Partial<User> }
|
|
95
|
+
| { _tag: 'UserDeleted'; userId: string }
|
|
96
|
+
|
|
97
|
+
const result = Match.value(event)
|
|
98
|
+
.pipe(Match.tag("UserCreated", e => `Created: ${e.user.name}`))
|
|
99
|
+
.pipe(Match.tag("UserUpdated", e => `Updated: ${e.userId}`))
|
|
100
|
+
.pipe(Match.tag("UserDeleted", e => `Deleted: ${e.userId}`))
|
|
101
|
+
.pipe(Match.exhaustive) // ← Compile error if you miss a case!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why people love it**:
|
|
105
|
+
- **Beyond errors** - use for events, commands, state machines
|
|
106
|
+
- **Exhaustive checking** - TypeScript catches missing cases
|
|
107
|
+
- **Natural with your tagged error pattern** - same `_tag` convention
|
|
108
|
+
|
|
109
|
+
**Effort**: Low | **Impact**: High (natural extension of TaggedError)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 4. **Option Type** ⭐⭐⭐
|
|
114
|
+
**What**: Explicit optional value handling (vs null/undefined)
|
|
115
|
+
**Trade-off**: JavaScript has `?.` and `??` - Option is less essential than in other languages
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Instead of: user.address?.city ?? "Unknown"
|
|
119
|
+
Option.fromNullable(user.address)
|
|
120
|
+
.pipe(Option.map(a => a.city))
|
|
121
|
+
.pipe(Option.getOrElse(() => "Unknown"))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Why some love it**:
|
|
125
|
+
- **Chainable** - long transformation pipelines are cleaner
|
|
126
|
+
- **toResult()** - converts to your Result type
|
|
127
|
+
|
|
128
|
+
**Effort**: Low | **Impact**: Medium (nice-to-have, not essential)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### 5. **Duration Type** ⭐⭐⭐
|
|
133
|
+
**What**: Type-safe duration handling instead of raw milliseconds
|
|
134
|
+
**Your current API**: `{ timeout: { ms: 5000 } }` - easy to make unit mistakes
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Before (easy to mess up)
|
|
138
|
+
step(fn, { timeout: { ms: 5000 } }) // is this 5 seconds or 5000 seconds?
|
|
139
|
+
|
|
140
|
+
// After (self-documenting)
|
|
141
|
+
step(fn, { timeout: Duration.seconds(5) })
|
|
142
|
+
step(fn, { timeout: Duration.minutes(2) })
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Bonus**: Integrates beautifully with Schedule combinators:
|
|
146
|
+
```typescript
|
|
147
|
+
Schedule.exponential(Duration.millis(100))
|
|
148
|
+
.pipe(Schedule.upTo(Duration.seconds(30)))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Effort**: Low | **Impact**: Medium (clarity + Schedule integration)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### 6. **Deferred (External Promise Resolution)** ⭐⭐⭐
|
|
156
|
+
**What**: A promise you can complete from outside
|
|
157
|
+
**Why relevant**: Your HITL patterns could use this internally
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const deferred = Deferred.make<string, Error>()
|
|
161
|
+
|
|
162
|
+
// In one place
|
|
163
|
+
Deferred.succeed(deferred, "approved")
|
|
164
|
+
|
|
165
|
+
// In another place (awaiting)
|
|
166
|
+
const result = await Deferred.await(deferred) // Result<string, Error>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Effort**: Low | **Impact**: Medium (coordination primitive)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### 7. **Stream** ⭐⭐⭐
|
|
174
|
+
**What**: Lazy, backpressure-aware data pipelines
|
|
175
|
+
**Trade-off**: RxJS exists, your batch processing covers many use cases
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
Stream.fromIterable(largeDataset)
|
|
179
|
+
.pipe(Stream.mapPar(4, processItem))
|
|
180
|
+
.pipe(Stream.filter(isValid))
|
|
181
|
+
.pipe(Stream.take(1000))
|
|
182
|
+
.pipe(Stream.runCollect)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Why some love it**:
|
|
186
|
+
- **Lazy** - doesn't load everything into memory
|
|
187
|
+
- **Backpressure** - slow consumers don't overflow
|
|
188
|
+
|
|
189
|
+
**Effort**: Very High | **Impact**: Medium (alternatives exist)
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Top Recommendations (What People Would Love/Miss)
|
|
194
|
+
|
|
195
|
+
| Rank | Feature | Why It's Lovable | Effort |
|
|
196
|
+
|------|---------|-----------------|--------|
|
|
197
|
+
| **1** | **Schedule Combinators** | "Aha moment" - retry strategies that compose | Medium |
|
|
198
|
+
| **2** | **Match** | Extends your TaggedError pattern to everything | Low |
|
|
199
|
+
| **3** | **Duration** | Small but delightful, enables Schedule cleanly | Low |
|
|
200
|
+
| **4** | **Schema** | Validation + Result integration (if not Zod) | Very High |
|
|
201
|
+
| **5** | **Option** | Nice chains, but JS has `?.` and `??` | Low |
|
|
202
|
+
|
|
203
|
+
### Recommendation
|
|
204
|
+
|
|
205
|
+
**Start with Schedule + Duration + Match** - they're complementary:
|
|
206
|
+
- Duration gives you type-safe time units
|
|
207
|
+
- Schedule uses Duration for composable retry logic
|
|
208
|
+
- Match extends your TaggedError philosophy to all discriminated unions
|
|
209
|
+
|
|
210
|
+
This creates a cohesive "composable primitives" layer that feels distinctly Effect-inspired while being uniquely yours.
|