fp-pack 0.9.1 → 0.9.2
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 +1 -1
- package/dist/ai-addons/fp-pack-agent-addon.md +1 -1
- package/dist/skills/fp-pack/SKILL.md +266 -1586
- package/dist/skills/fp-pack.md +266 -1586
- package/package.json +1 -1
package/dist/skills/fp-pack.md
CHANGED
|
@@ -1,1752 +1,432 @@
|
|
|
1
1
|
# fp-pack AI Agent Skills
|
|
2
2
|
|
|
3
|
-
Document Version: 0.9.
|
|
3
|
+
Document Version: 0.9.2
|
|
4
4
|
|
|
5
|
-
## ⚠️ Activation Condition
|
|
5
|
+
## ⚠️ Activation Condition (Read First)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
These guidelines apply **only when `fp-pack` is installed** in the current project.
|
|
8
8
|
|
|
9
|
-
Before following
|
|
10
|
-
- Check `package.json` for fp-pack in dependencies
|
|
11
|
-
- Check
|
|
12
|
-
- Check
|
|
9
|
+
Before following this document:
|
|
10
|
+
- Check `package.json` for `fp-pack` in dependencies/devDependencies
|
|
11
|
+
- Check `node_modules/fp-pack` exists
|
|
12
|
+
- Check existing code imports from `fp-pack` or `fp-pack/stream`
|
|
13
13
|
|
|
14
|
-
If fp-pack is
|
|
14
|
+
If `fp-pack` is **not** installed, use the project’s existing conventions. Do **not** suggest adding fp-pack unless the user asks.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## Agent Checklist (TL;DR)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
When writing or editing code in an fp-pack project:
|
|
21
|
+
- Prefer `pipe` / `pipeAsync` for composition; keep helpers **unary** in the pipeline.
|
|
22
|
+
- Prefer **data-last** utilities (curried) so they compose: `map(fn)`, `prop('k')`, `assoc('k', v)`, etc.
|
|
23
|
+
- Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
|
|
24
|
+
- Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
|
|
25
|
+
- **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
|
|
26
|
+
- If TypeScript inference gets stuck with a data-last generic, use a small wrapper, an explicit type hint, or `pipeHint`.
|
|
21
27
|
|
|
22
|
-
fp-pack
|
|
28
|
+
## Philosophy (What fp-pack Optimizes For)
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
fp-pack is a TypeScript FP utility library focused on:
|
|
31
|
+
1) Function composition via `pipe` / `pipeAsync`
|
|
32
|
+
2) Declarative, immutable transforms (avoid loops and mutations)
|
|
33
|
+
3) Practical TypeScript inference (data-last + currying for most helpers)
|
|
34
|
+
4) Optional early-exit handling via `SideEffect` (only when needed)
|
|
35
|
+
5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Core Composition
|
|
31
40
|
|
|
32
|
-
### pipe
|
|
41
|
+
### `pipe` (sync)
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
Use `pipe` to build a unary function:
|
|
35
44
|
|
|
36
|
-
```
|
|
37
|
-
import { pipe,
|
|
45
|
+
```ts
|
|
46
|
+
import { pipe, filter, map, take } from 'fp-pack';
|
|
38
47
|
|
|
39
|
-
// GOOD: Declarative pipe composition
|
|
40
48
|
const processUsers = pipe(
|
|
41
|
-
filter((
|
|
42
|
-
map(
|
|
49
|
+
filter((u: User) => u.active),
|
|
50
|
+
map((u) => u.name),
|
|
43
51
|
take(10)
|
|
44
52
|
);
|
|
45
|
-
|
|
46
|
-
// BAD: Imperative approach
|
|
47
|
-
const processUsers = (users: User[]) => {
|
|
48
|
-
const result = [];
|
|
49
|
-
for (const user of users) {
|
|
50
|
-
if (user.age >= 18) {
|
|
51
|
-
result.push(user.name.toUpperCase());
|
|
52
|
-
if (result.length >= 10) break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
};
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
### `pipeAsync` (async)
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
Use `pipeAsync` when any step is async:
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
59
|
+
```ts
|
|
66
60
|
import { pipeAsync } from 'fp-pack';
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
async (
|
|
71
|
-
async (response) => response.json(),
|
|
62
|
+
const fetchUser = pipeAsync(
|
|
63
|
+
async (id: string) => fetch(`/api/users/${id}`),
|
|
64
|
+
async (res) => res.json(),
|
|
72
65
|
(data) => data.user
|
|
73
66
|
);
|
|
74
|
-
|
|
75
|
-
// BAD: Manual async handling
|
|
76
|
-
const fetchUserData = async (userId: string) => {
|
|
77
|
-
const response = await fetch(`/api/users/${userId}`);
|
|
78
|
-
const data = await response.json();
|
|
79
|
-
return data.user;
|
|
80
|
-
};
|
|
81
67
|
```
|
|
82
68
|
|
|
83
|
-
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Currying & Data-Last Conventions
|
|
84
72
|
|
|
85
|
-
|
|
73
|
+
Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
|
|
74
|
+
|
|
75
|
+
- Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
|
|
76
|
+
- Avoid in pipelines: helpers that require you to pass the data first, or that return a function whose type depends on the final data arg (see next section)
|
|
77
|
+
|
|
78
|
+
Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
|
|
79
|
+
|
|
80
|
+
---
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
`pipe`/`pipeAsync`, TypeScript cannot infer that type, so you may need `pipeHint`, a small type hint, or a data-first wrapper.
|
|
82
|
+
## TypeScript: Data-last Generic Inference Caveats (Important)
|
|
89
83
|
|
|
90
|
-
|
|
84
|
+
Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
|
|
85
|
+
Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
|
|
86
|
+
|
|
87
|
+
### Recommended fixes (choose 1)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
91
90
|
import { pipe, pipeHint, zip, some } from 'fp-pack';
|
|
92
91
|
|
|
93
|
-
//
|
|
92
|
+
// 1) data-first wrapper (most explicit)
|
|
94
93
|
const withWrapper = pipe(
|
|
95
94
|
(values: number[]) => zip([1, 2, 3], values),
|
|
96
95
|
some(([a, b]) => a > b)
|
|
97
96
|
);
|
|
98
97
|
|
|
99
|
-
//
|
|
98
|
+
// 2) explicit function type hint (short, but uses an assertion)
|
|
100
99
|
const withHint = pipe(
|
|
101
100
|
zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
|
|
102
101
|
some(([a, b]) => a > b)
|
|
103
102
|
);
|
|
104
103
|
|
|
105
|
-
//
|
|
104
|
+
// 3) pipeHint helper (keeps pipeline style)
|
|
106
105
|
const withPipeHint = pipe(
|
|
107
106
|
pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
|
|
108
107
|
some(([a, b]) => a > b)
|
|
109
108
|
);
|
|
110
109
|
```
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
- **Array**: `chunk`, `drop`, `take`, `zip`
|
|
114
|
-
- **Object**: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
|
|
115
|
-
- **Async**: `timeout`
|
|
116
|
-
- **Stream**: `chunk`, `drop`, `take`, `zip`
|
|
111
|
+
### Utilities that may need a hint in data-last pipelines
|
|
117
112
|
|
|
118
|
-
|
|
113
|
+
- Array: `chunk`, `drop`, `take`, `zip`
|
|
114
|
+
- Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
|
|
115
|
+
- Async: `timeout`
|
|
116
|
+
- Stream: `chunk`, `drop`, `take`, `zip`
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
- Early termination based on validation
|
|
124
|
-
- Error handling with side effects (logging, toasts, etc.)
|
|
125
|
-
- Optional chaining patterns
|
|
126
|
-
|
|
127
|
-
For regular error handling, standard try-catch or error propagation is perfectly fine.
|
|
128
|
-
If you want precise SideEffect unions across branches, use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`.
|
|
120
|
+
---
|
|
129
121
|
|
|
130
|
-
|
|
131
|
-
// MOST CASES: Just use pipe with regular error handling
|
|
132
|
-
import { pipe, map, filter } from 'fp-pack';
|
|
122
|
+
## SideEffect Pattern (Use Only When Needed)
|
|
133
123
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
124
|
+
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you truly need **early termination**:
|
|
125
|
+
- validation pipelines that should stop early
|
|
126
|
+
- recoverable errors you want to model as data
|
|
127
|
+
- branching flows where you want to short-circuit
|
|
139
128
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.error('Processing failed:', error);
|
|
144
|
-
}
|
|
129
|
+
### SideEffect-aware pipes
|
|
130
|
+
- `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
|
|
131
|
+
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended when you care about types)
|
|
145
132
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
validateInput,
|
|
151
|
-
(data) => {
|
|
152
|
-
if (!data.isValid) {
|
|
153
|
-
return SideEffect.of(() => {
|
|
154
|
-
showToast('Invalid data'); // Side effect
|
|
155
|
-
logError('validation_failed'); // Side effect
|
|
156
|
-
return null;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return data;
|
|
160
|
-
},
|
|
161
|
-
transformData
|
|
162
|
-
);
|
|
133
|
+
### Rules
|
|
134
|
+
- Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
|
|
135
|
+
- Prefer `isSideEffect` for precise runtime narrowing in both branches.
|
|
136
|
+
- Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
|
|
163
137
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
138
|
+
### Key functions
|
|
139
|
+
- `SideEffect.of(effectFn, label?)`
|
|
140
|
+
- `isSideEffect(value)` (type guard)
|
|
141
|
+
- `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
|
|
142
|
+
- `matchSideEffect(result, { value, effect })`
|
|
167
143
|
|
|
168
|
-
|
|
169
|
-
- `SideEffect.of(fn, label?)` - Create a side effect container
|
|
170
|
-
- `isSideEffect(value)` - Type guard for **runtime checking** whether a value is a SideEffect
|
|
171
|
-
- `runPipeResult<T, R>(result)` - Execute SideEffect or return value (call **OUTSIDE** pipelines). If the input is narrowed to `SideEffect<R>` (e.g. after `isSideEffect`), it returns `R`. If the input is widened to `SideEffect<any>` or `any`, the result becomes `any` unless you provide generics.
|
|
172
|
-
- `matchSideEffect(result, { value, effect })` - Pattern match on result
|
|
144
|
+
### `runPipeResult` type behavior (important)
|
|
173
145
|
|
|
174
|
-
|
|
146
|
+
- If the input is **narrowed** to `SideEffect<R>` (e.g. inside `if (isSideEffect(x))`), `runPipeResult(x)` returns `R`.
|
|
147
|
+
- If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
|
|
175
148
|
|
|
176
|
-
```
|
|
177
|
-
import {
|
|
149
|
+
```ts
|
|
150
|
+
import {
|
|
151
|
+
pipeSideEffect,
|
|
152
|
+
pipeSideEffectStrict,
|
|
153
|
+
SideEffect,
|
|
154
|
+
isSideEffect,
|
|
155
|
+
runPipeResult,
|
|
156
|
+
} from 'fp-pack';
|
|
178
157
|
|
|
179
|
-
const
|
|
180
|
-
(
|
|
181
|
-
(odds) => odds.length > 0
|
|
182
|
-
? odds
|
|
183
|
-
: SideEffect.of(() => 'No odd numbers'),
|
|
184
|
-
(odds) => odds.map(n => n * 2)
|
|
158
|
+
const nonStrict = pipeSideEffect(
|
|
159
|
+
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
185
160
|
);
|
|
161
|
+
const widened: number | SideEffect<any> = nonStrict(-1);
|
|
162
|
+
const unsafe = runPipeResult(widened); // any
|
|
186
163
|
|
|
187
|
-
const
|
|
164
|
+
const strict = pipeSideEffectStrict(
|
|
165
|
+
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
166
|
+
);
|
|
167
|
+
const precise = strict(-1); // number | SideEffect<'NEG'>
|
|
188
168
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// TypeScript knows: result is number[]
|
|
192
|
-
const sum: number = result.reduce((a, b) => a + b, 0);
|
|
193
|
-
} else {
|
|
194
|
-
// TypeScript knows: result is SideEffect<'No odd numbers'>
|
|
195
|
-
const error = runPipeResult(result); // 'No odd numbers'
|
|
169
|
+
if (isSideEffect(precise)) {
|
|
170
|
+
const err = runPipeResult(precise); // 'NEG'
|
|
196
171
|
}
|
|
197
|
-
|
|
198
|
-
// ⚠️ Non-strict pipeSideEffect widens SideEffect to any
|
|
199
|
-
const widened: number[] | SideEffect<any> = pipeSideEffect(
|
|
200
|
-
(nums: number[]) => nums,
|
|
201
|
-
(nums) => nums.length > 0 ? nums : SideEffect.of(() => 'EMPTY')
|
|
202
|
-
)([]);
|
|
203
|
-
const unsafeValue = runPipeResult(widened); // any
|
|
204
|
-
|
|
205
|
-
// ✅ CORRECT: Provide generics to recover a safe union
|
|
206
|
-
const safeValue = runPipeResult<number[], string>(result); // result: number[] | string (union type - safe but not narrowed)
|
|
207
172
|
```
|
|
208
173
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`runPipeResult<T, R=any>` has a default type parameter `R=any`. This means:
|
|
212
|
-
|
|
213
|
-
- ✅ **Precise input types**: `T | SideEffect<'E'>` preserves `T | 'E'` without extra annotations.
|
|
214
|
-
- ⚠️ **Widened inputs**: `T | SideEffect<any>` (or `any`) collapses to `any`.
|
|
215
|
-
- ✅ **With generics**: `runPipeResult<SuccessType, ErrorType>(result)` restores a safe union when inference is lost.
|
|
216
|
-
- ✅ **After narrowing**: If the input is `SideEffect<'E'>`, `runPipeResult` returns `'E'`.
|
|
217
|
-
- ✅ **With isSideEffect**: Prefer for runtime narrowing when you need branch-specific types.
|
|
174
|
+
---
|
|
218
175
|
|
|
219
|
-
|
|
176
|
+
## Stream Functions (`fp-pack/stream`) — Lazy Iterables
|
|
220
177
|
|
|
221
|
-
|
|
178
|
+
Use stream utilities when:
|
|
179
|
+
- data is large or unbounded (`range`, streams, generators)
|
|
180
|
+
- you want lazy evaluation (avoid allocating intermediate arrays)
|
|
181
|
+
- you want to support `Iterable` and `AsyncIterable`
|
|
222
182
|
|
|
223
|
-
|
|
183
|
+
Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
|
|
224
184
|
|
|
225
|
-
```
|
|
185
|
+
```ts
|
|
226
186
|
import { pipe } from 'fp-pack';
|
|
227
|
-
import {
|
|
187
|
+
import { range, filter, map, take, toArray } from 'fp-pack/stream';
|
|
228
188
|
|
|
229
|
-
|
|
230
|
-
const processLargeDataset = pipe(
|
|
189
|
+
const first100SquaresOfEvens = pipe(
|
|
231
190
|
filter((n: number) => n % 2 === 0),
|
|
232
|
-
map(n => n * n),
|
|
191
|
+
map((n) => n * n),
|
|
233
192
|
take(100),
|
|
234
193
|
toArray
|
|
235
194
|
);
|
|
236
195
|
|
|
237
|
-
|
|
238
|
-
const result = processLargeDataset(range(1, 1000000));
|
|
239
|
-
|
|
240
|
-
// BAD: Eager array processing
|
|
241
|
-
const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
|
|
242
|
-
.filter(n => n % 2 === 0)
|
|
243
|
-
.map(n => n * n)
|
|
244
|
-
.slice(0, 100); // Processed entire dataset!
|
|
196
|
+
const result = first100SquaresOfEvens(range(Infinity));
|
|
245
197
|
```
|
|
246
198
|
|
|
247
|
-
|
|
248
|
-
- Sync: `Iterable<T>` → `IterableIterator<R>`
|
|
249
|
-
- Async: `AsyncIterable<T>` → `AsyncIterableIterator<R>`
|
|
199
|
+
---
|
|
250
200
|
|
|
251
|
-
## Available Functions
|
|
201
|
+
## Available Functions (Practical Index)
|
|
252
202
|
|
|
253
|
-
|
|
254
|
-
- `pipe` - Left-to-right function composition (sync)
|
|
255
|
-
- `pipeSideEffect` - Left-to-right composition with SideEffect short-circuiting
|
|
256
|
-
- `pipeSideEffectStrict` - SideEffect composition with strict effect unions
|
|
257
|
-
- `compose` - Right-to-left function composition
|
|
258
|
-
- `curry` - Curry a function
|
|
259
|
-
- `partial` - Partial application
|
|
260
|
-
- `flip` - Flip function argument order
|
|
261
|
-
- `complement` - Logical negation
|
|
262
|
-
- `identity` - Return input unchanged
|
|
263
|
-
- `constant` - Always return the same value
|
|
264
|
-
- `from` - Ignore input and return a fixed value
|
|
265
|
-
- `tap` - Execute side effect and return original value
|
|
266
|
-
- `tap0` - Execute side effect without input
|
|
267
|
-
- `once` - Execute function only once
|
|
268
|
-
- `memoize` - Cache function results
|
|
269
|
-
- `SideEffect` - Side effect container
|
|
270
|
-
- `isSideEffect` - Type guard for SideEffect
|
|
271
|
-
- `matchSideEffect` - Pattern match on value/SideEffect
|
|
272
|
-
- `runPipeResult` - Execute SideEffect or return value
|
|
203
|
+
This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
|
|
273
204
|
|
|
274
|
-
###
|
|
275
|
-
- `pipeAsync`
|
|
276
|
-
-
|
|
277
|
-
- `
|
|
278
|
-
- `
|
|
279
|
-
- `timeout` - Add timeout to promise
|
|
280
|
-
- `retry` - Retry failed operations
|
|
281
|
-
- `debounce` - Debounce function calls
|
|
282
|
-
- `debounceLeading` - Debounce with leading edge
|
|
283
|
-
- `debounceLeadingTrailing` - Debounce with both edges
|
|
284
|
-
- `throttle` - Throttle function calls
|
|
205
|
+
### Composition
|
|
206
|
+
- `pipe`, `pipeAsync`
|
|
207
|
+
- SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
|
|
208
|
+
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `flip`, `partial`, `curry`, `compose`, `complement`
|
|
209
|
+
- SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
|
|
285
210
|
|
|
286
211
|
### Array
|
|
287
|
-
- `map`, `filter`, `reduce`, `
|
|
288
|
-
- `find`, `some`, `every`
|
|
289
|
-
- `take`, `drop`, `takeWhile`, `dropWhile`
|
|
290
|
-
-
|
|
291
|
-
-
|
|
292
|
-
- `concat`, `append`, `prepend`, `flatten
|
|
293
|
-
- `head`, `tail`, `last`, `init`
|
|
294
|
-
- `range`, `partition`, `scan`
|
|
212
|
+
- Core transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
|
|
213
|
+
- Queries: `find`, `some`, `every`
|
|
214
|
+
- Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
|
|
215
|
+
- Ordering/grouping: `sort`, `sortBy`, `groupBy`, `uniqBy`
|
|
216
|
+
- Pairing: `zip`, `zipWith`
|
|
217
|
+
- Combining: `concat`, `append`, `prepend`, `flatten`
|
|
295
218
|
|
|
296
219
|
### Object
|
|
297
|
-
- `prop`, `propOr`, `path`, `pathOr`
|
|
298
|
-
- `pick`, `omit`
|
|
299
|
-
- `assoc`, `assocPath`, `
|
|
300
|
-
- `merge`, `mergeDeep
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
- `has`, `hasPath`
|
|
220
|
+
- Access: `prop`, `propOr`, `propStrict`, `path`, `pathOr`
|
|
221
|
+
- Pick/drop: `pick`, `omit`
|
|
222
|
+
- Updates: `assoc`, `assocPath`, `dissocPath`
|
|
223
|
+
- Merge: `merge`, `mergeDeep`
|
|
224
|
+
- Transforms: `mapValues`, `evolve`
|
|
225
|
+
- Predicates: `has`
|
|
304
226
|
|
|
305
227
|
### Control Flow
|
|
306
|
-
- `ifElse`
|
|
307
|
-
- `when`, `unless` - Conditional execution
|
|
308
|
-
- `cond` - Multi-branch conditional
|
|
309
|
-
- `tryCatch` - Safe function execution
|
|
310
|
-
- `guard` - Validation guard
|
|
311
|
-
|
|
312
|
-
### Stream (Lazy Iterables)
|
|
313
|
-
- `append`, `concat`, `prepend`
|
|
314
|
-
- `map`, `filter`, `flatMap`, `flatten`, `flattenDeep`
|
|
315
|
-
- `take`, `takeWhile`, `drop`, `dropWhile`, `chunk`
|
|
316
|
-
- `zip`, `zipWith`, `find`, `some`, `every`
|
|
317
|
-
- `reduce`, `scan`
|
|
318
|
-
- `range`
|
|
319
|
-
- `toArray` - Materialize stream to array
|
|
320
|
-
- `toAsync` - Convert to async iterable
|
|
321
|
-
|
|
322
|
-
### Math
|
|
323
|
-
- `add`, `sub`, `mul`, `div`
|
|
324
|
-
- `sum`, `mean`, `min`, `max`
|
|
325
|
-
- `round`, `floor`, `ceil`, `randomInt`
|
|
326
|
-
|
|
327
|
-
### String
|
|
328
|
-
- `trim`, `split`, `join`, `replace`
|
|
329
|
-
- `toUpper`, `toLower`
|
|
330
|
-
- `startsWith`, `endsWith`, `match`
|
|
331
|
-
|
|
332
|
-
### Equality
|
|
333
|
-
- `equals`, `includes`
|
|
334
|
-
- `isNil`, `isEmpty`, `isType`
|
|
335
|
-
- `gt`, `gte`, `lt`, `lte`
|
|
336
|
-
- `clamp`
|
|
337
|
-
|
|
338
|
-
### Nullable
|
|
339
|
-
- `maybe`, `mapMaybe`, `getOrElse`, `fold`, `result`
|
|
340
|
-
|
|
341
|
-
### Debug
|
|
342
|
-
- `assert`, `invariant`, `log`
|
|
343
|
-
|
|
344
|
-
## Coding Guidelines for AI Agents
|
|
345
|
-
|
|
346
|
-
### 0. Detect Project Language (JS vs TS)
|
|
347
|
-
|
|
348
|
-
Before writing code, check whether the project is JavaScript or TypeScript:
|
|
349
|
-
|
|
350
|
-
- **TypeScript projects**: use explicit types, leverage generics, and keep type-safe signatures in examples.
|
|
351
|
-
- **JavaScript projects**: avoid TypeScript-only syntax and prefer JSDoc only when it adds clarity or is already used.
|
|
352
|
-
|
|
353
|
-
### 0.1 Quick Signature Lookup
|
|
228
|
+
- `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
|
|
354
229
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
- Stream exports: `dist/stream/index.d.ts`
|
|
359
|
-
- Main utilities (fallback): `src/implement/**`
|
|
360
|
-
- Stream utilities (fallback): `src/stream/**`
|
|
361
|
-
- Installed package:
|
|
362
|
-
- `node_modules/fp-pack/dist/index.d.ts`
|
|
363
|
-
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
364
|
-
- `node_modules/fp-pack/src/implement/**`
|
|
365
|
-
- `node_modules/fp-pack/src/stream/**`
|
|
366
|
-
|
|
367
|
-
### 1. Always Prefer pipe/pipeAsync
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
// GOOD
|
|
371
|
-
const result = pipe(
|
|
372
|
-
trim,
|
|
373
|
-
split(','),
|
|
374
|
-
map(toNumber),
|
|
375
|
-
filter(isPositive)
|
|
376
|
-
)(input);
|
|
377
|
-
|
|
378
|
-
// BAD
|
|
379
|
-
const trimmed = trim(input);
|
|
380
|
-
const parts = split(',')(trimmed);
|
|
381
|
-
const numbers = map(toNumber)(parts);
|
|
382
|
-
const result = filter(isPositive)(numbers);
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### 2. Use Curried Functions (Where Available)
|
|
386
|
-
|
|
387
|
-
Most multi-arg functions are curried. Single-arg utilities are already unary, so currying adds no benefit; use them directly (e.g. `uniq`, `flatten`, `flattenDeep`, `head`, `tail`, `last`, `init`, `range`, `sum`, `mean`, `min`, `max`, `round`, `floor`, `ceil`, `trim`, `toLower`, `toUpper`, `isNil`, `isEmpty`, `isType`). These are already pipe-friendly without a curried variant.
|
|
230
|
+
### Async
|
|
231
|
+
- `retry`, `timeout`, `delay`
|
|
232
|
+
- Function-returning helpers: `debounce*`, `throttle`
|
|
388
233
|
|
|
389
|
-
|
|
390
|
-
|
|
234
|
+
### Stream (Lazy Iterables)
|
|
235
|
+
- Building: `range`
|
|
236
|
+
- Transforms: `map`, `filter`, `flatMap`, `flatten`
|
|
237
|
+
- Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
|
|
238
|
+
- Queries/reductions: `find`, `some`, `every`, `reduce`, `scan`
|
|
239
|
+
- Pairing/combining: `zip`, `zipWith`, `concat`, `append`, `prepend`
|
|
240
|
+
- Utilities: `toArray`, `toAsync`
|
|
241
|
+
|
|
242
|
+
### Math / String / Equality / Debug
|
|
243
|
+
- Math: `add`, `sub`, `mul`, `div`, `randomInt`, `clamp`
|
|
244
|
+
- String: `split`, `join`, `replace`, `match`, `trim`
|
|
245
|
+
- Equality: `equals`, `isNil`
|
|
246
|
+
- Debug: `assert`, `invariant`
|
|
391
247
|
|
|
392
|
-
|
|
393
|
-
const processUsers = pipe(
|
|
394
|
-
filter(user => user.active),
|
|
395
|
-
map(user => user.name)
|
|
396
|
-
);
|
|
248
|
+
---
|
|
397
249
|
|
|
398
|
-
|
|
399
|
-
const filterActive = filter((user: User) => user.active);
|
|
400
|
-
const getNames = map((user: User) => user.name);
|
|
401
|
-
const processUsers = pipe(filterActive, getNames);
|
|
402
|
-
```
|
|
250
|
+
## Micro-Patterns (Keep It Short)
|
|
403
251
|
|
|
404
|
-
###
|
|
252
|
+
### Boundary handling (UI/event/service)
|
|
405
253
|
|
|
406
|
-
|
|
254
|
+
Do the work in a pipeline, unwrap at the boundary:
|
|
407
255
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
- **Fixed signatures** can use `curry(fn)` directly.
|
|
411
|
-
- **Generic or overloaded signatures** should use an explicit type alias + cast to preserve inference.
|
|
256
|
+
```ts
|
|
257
|
+
import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
|
|
412
258
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
function split(separator: string, str: string): string[] {
|
|
416
|
-
return str.split(separator);
|
|
417
|
-
}
|
|
418
|
-
export default curry(split);
|
|
259
|
+
const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
|
|
260
|
+
const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
|
|
419
261
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
(
|
|
423
|
-
|
|
262
|
+
export const handler = (n: number) => {
|
|
263
|
+
const result = pipeline(n);
|
|
264
|
+
if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
|
|
265
|
+
return result; // number
|
|
424
266
|
};
|
|
425
|
-
|
|
426
|
-
function chunk<T>(size: number, arr: T[]): T[][] {
|
|
427
|
-
// ...
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const curriedChunk = curry(chunk) as Chunk;
|
|
432
|
-
export default curriedChunk;
|
|
433
267
|
```
|
|
434
268
|
|
|
435
|
-
###
|
|
436
|
-
|
|
437
|
-
**Default choice: Start with `pipe` / `pipeAsync`**
|
|
438
|
-
|
|
439
|
-
Most data transformations are pure and don't need SideEffect handling. Use `pipe` for sync operations and `pipeAsync` for async operations. **Only switch to SideEffect-aware pipes when you actually need** early termination or error handling with side effects.
|
|
269
|
+
### Data-first style with `from()`
|
|
440
270
|
|
|
441
|
-
-
|
|
442
|
-
- **`pipeAsync`** - Async, **pure** transformations (99% of cases)
|
|
443
|
-
- **`pipeSideEffect`** - **Only when you need** SideEffect short-circuiting (sync)
|
|
444
|
-
- **`pipeAsyncSideEffect`** - **Only when you need** SideEffect short-circuiting (async)
|
|
445
|
-
- **`pipeSideEffectStrict`** - Sync SideEffect pipelines with strict effect unions
|
|
446
|
-
- **`pipeAsyncSideEffectStrict`** - Async SideEffect pipelines with strict effect unions
|
|
271
|
+
Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
|
|
447
272
|
|
|
448
|
-
|
|
273
|
+
```ts
|
|
274
|
+
import { pipe, from, map, filter, take } from 'fp-pack';
|
|
449
275
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
)
|
|
276
|
+
// data-first feeling: start with data, then compose transforms
|
|
277
|
+
const result = pipe(
|
|
278
|
+
from([1, 2, 3, 4, 5]),
|
|
279
|
+
filter((n: number) => n % 2 === 0),
|
|
280
|
+
map((n) => n * 10),
|
|
281
|
+
take(2)
|
|
282
|
+
)(); // [20, 40]
|
|
456
283
|
|
|
457
|
-
//
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
map(
|
|
461
|
-
filter(profile => profile.verified)
|
|
284
|
+
// same idea when you already have an input (normal usage)
|
|
285
|
+
const process = pipe(
|
|
286
|
+
filter((n: number) => n % 2 === 0),
|
|
287
|
+
map((n) => n * 10)
|
|
462
288
|
);
|
|
289
|
+
const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
|
|
463
290
|
```
|
|
464
291
|
|
|
465
|
-
###
|
|
466
|
-
|
|
467
|
-
**🔄 Critical Rule: SideEffect Contagion**
|
|
468
|
-
|
|
469
|
-
Once you use `pipeSideEffect` or `pipeAsyncSideEffect`, the result is **always `T | SideEffect`** (or `Promise<T | SideEffect>` for async). The same rule applies to strict variants.
|
|
470
|
-
|
|
471
|
-
If you want to continue composing this result, you **MUST** keep using SideEffect-aware pipes. You **CANNOT** switch back to `pipe` or `pipeAsync` because they don't handle `SideEffect`.
|
|
292
|
+
### `ifElse` uses functions (not values)
|
|
472
293
|
|
|
473
|
-
```
|
|
474
|
-
import {
|
|
475
|
-
|
|
476
|
-
const validateUserPipeline = pipeSideEffect(
|
|
477
|
-
findUser,
|
|
478
|
-
validateAge
|
|
479
|
-
);
|
|
480
|
-
// Result type: User | SideEffect
|
|
481
|
-
|
|
482
|
-
// ❌ WRONG - pipe cannot handle SideEffect
|
|
483
|
-
const wrongPipeline = pipe(
|
|
484
|
-
validateUserPipeline, // Returns User | SideEffect
|
|
485
|
-
(user) => user.email // Type error! SideEffect has no 'email' property
|
|
486
|
-
);
|
|
294
|
+
```ts
|
|
295
|
+
import { ifElse, from } from 'fp-pack';
|
|
487
296
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
(
|
|
492
|
-
sendEmail
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
// The same rule applies to async pipes
|
|
496
|
-
const asyncPipeline = pipeAsyncSideEffect(
|
|
497
|
-
fetchUser,
|
|
498
|
-
validateUser
|
|
499
|
-
);
|
|
500
|
-
// Result type: Promise<User | SideEffect>
|
|
501
|
-
|
|
502
|
-
// You must continue with pipeAsyncSideEffect, not pipeAsync
|
|
503
|
-
const extendedAsyncPipeline = pipeAsyncSideEffect(
|
|
504
|
-
asyncPipeline,
|
|
505
|
-
processUser,
|
|
506
|
-
saveToDatabase
|
|
297
|
+
const label = ifElse(
|
|
298
|
+
(n: number) => n >= 60,
|
|
299
|
+
from('pass'),
|
|
300
|
+
from('fail')
|
|
507
301
|
);
|
|
508
302
|
```
|
|
509
303
|
|
|
510
|
-
###
|
|
304
|
+
### Stream pipeline to array
|
|
511
305
|
|
|
512
|
-
```
|
|
306
|
+
```ts
|
|
513
307
|
import { pipe } from 'fp-pack';
|
|
514
|
-
import {
|
|
308
|
+
import { map, filter, toArray } from 'fp-pack/stream';
|
|
515
309
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
take(100),
|
|
310
|
+
const toIds = pipe(
|
|
311
|
+
filter((u: User) => u.active),
|
|
312
|
+
map((u) => u.id),
|
|
520
313
|
toArray
|
|
521
314
|
);
|
|
522
|
-
|
|
523
|
-
// Stops after finding 100 items (only processes 100, not 1 million)
|
|
524
|
-
const result = getFirst100Even(range(1, 1000000));
|
|
525
315
|
```
|
|
526
316
|
|
|
527
|
-
###
|
|
528
|
-
|
|
529
|
-
```typescript
|
|
530
|
-
import { pipeSideEffect, SideEffect, runPipeResult } from 'fp-pack';
|
|
531
|
-
|
|
532
|
-
const safeDividePipeline = pipeSideEffect(
|
|
533
|
-
(input: { a: number; b: number }) => {
|
|
534
|
-
if (input.b === 0) {
|
|
535
|
-
return SideEffect.of(() => {
|
|
536
|
-
throw new Error('Division by zero');
|
|
537
|
-
}, 'DIVISION_ERROR');
|
|
538
|
-
}
|
|
539
|
-
return input;
|
|
540
|
-
},
|
|
541
|
-
({ a, b }) => a / b
|
|
542
|
-
);
|
|
317
|
+
### Data-last inference workaround (when needed)
|
|
543
318
|
|
|
544
|
-
|
|
545
|
-
const result = runPipeResult(safeDividePipeline({ a: 10, b: 2 })); // 5
|
|
546
|
-
```
|
|
319
|
+
Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
|
|
547
320
|
|
|
548
|
-
|
|
321
|
+
```ts
|
|
322
|
+
import { pipe, pipeHint, zip } from 'fp-pack';
|
|
549
323
|
|
|
550
|
-
|
|
551
|
-
|
|
324
|
+
// wrapper
|
|
325
|
+
const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
|
|
552
326
|
|
|
553
|
-
//
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
(age: number) => age >= 18,
|
|
557
|
-
age => ({ age, status: 'adult' }),
|
|
558
|
-
age => ({ age, status: 'minor' })
|
|
559
|
-
)
|
|
327
|
+
// pipeHint
|
|
328
|
+
const zipUsers2 = pipe(
|
|
329
|
+
pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
|
|
560
330
|
);
|
|
561
|
-
|
|
562
|
-
// GOOD: Multi-branch with cond
|
|
563
|
-
const gradeToLetter = cond([
|
|
564
|
-
[(n: number) => n >= 90, () => 'A'],
|
|
565
|
-
[(n: number) => n >= 80, () => 'B'],
|
|
566
|
-
[(n: number) => n >= 70, () => 'C'],
|
|
567
|
-
[() => true, () => 'F']
|
|
568
|
-
]);
|
|
569
331
|
```
|
|
570
332
|
|
|
571
|
-
###
|
|
572
|
-
|
|
573
|
-
- `prop` returns `T[K] | undefined`. Use `propOr` (or guard) before array operations.
|
|
574
|
-
- `ifElse` expects **functions** for both branches. If you already have a value, wrap it: `() => value` or use `from(value)` for cleaner constant branches.
|
|
575
|
-
- Use `from(value)` when you need a unary function that ignores input (handy for `ifElse`/`cond` branches and data-first patterns). Pipelines that start with `from(...)` can be called without an initial input value.
|
|
576
|
-
- `cond` returns `R | undefined`. Add a default branch and coalesce when you need a strict result.
|
|
577
|
-
- In `pipeSideEffect`, keep step return types aligned to avoid wide unions.
|
|
578
|
-
|
|
579
|
-
```typescript
|
|
580
|
-
import { pipe, propOr, append, assoc, ifElse, cond, from, filter, map } from 'fp-pack';
|
|
581
|
-
|
|
582
|
-
// propOr keeps the type strict for array ops
|
|
583
|
-
const addTodo = (text: string, state: AppState) =>
|
|
584
|
-
pipe(
|
|
585
|
-
propOr([], 'todos'),
|
|
586
|
-
append(createTodo(text)),
|
|
587
|
-
(todos) => assoc('todos', todos, state)
|
|
588
|
-
)(state);
|
|
589
|
-
|
|
590
|
-
// ifElse expects functions, not values
|
|
591
|
-
const toggleTodo = (id: string) => ifElse(
|
|
592
|
-
(todo: Todo) => todo.id === id,
|
|
593
|
-
assoc('completed', true),
|
|
594
|
-
(todo) => todo
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
// from is useful for constant branches - cleaner than () => value
|
|
598
|
-
const statusLabel = ifElse(
|
|
599
|
-
(score: number) => score >= 60,
|
|
600
|
-
from('pass'), // Constant value
|
|
601
|
-
from('fail')
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
// Data-first pattern with from: inject data into pipeline
|
|
605
|
-
const processData = pipe(
|
|
606
|
-
from([1, 2, 3, 4, 5]),
|
|
607
|
-
filter((n: number) => n % 2 === 0),
|
|
608
|
-
map(n => n * 2)
|
|
609
|
-
);
|
|
610
|
-
const result = processData(); // [4, 8]
|
|
611
|
-
|
|
612
|
-
// cond still returns R | undefined, so coalesce if needed
|
|
613
|
-
const grade = (score: number) =>
|
|
614
|
-
cond([
|
|
615
|
-
[(n: number) => n >= 90, () => 'A'],
|
|
616
|
-
[() => true, () => 'F']
|
|
617
|
-
])(score) ?? 'F';
|
|
618
|
-
```
|
|
333
|
+
### Async pipelines (retry/timeout)
|
|
619
334
|
|
|
620
|
-
|
|
335
|
+
Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
|
|
621
336
|
|
|
622
|
-
```
|
|
623
|
-
import {
|
|
337
|
+
```ts
|
|
338
|
+
import { pipeAsync, retry, timeout } from 'fp-pack';
|
|
624
339
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
mapValues((value) => typeof value === 'string' ? value.trim() : value),
|
|
629
|
-
merge({ verified: false })
|
|
340
|
+
const fetchJson = pipeAsync(
|
|
341
|
+
(url: string) => fetch(url),
|
|
342
|
+
(res) => res.json()
|
|
630
343
|
);
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
## Anti-Patterns to Avoid
|
|
634
344
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
for (const item of items) {
|
|
641
|
-
if (item.active) {
|
|
642
|
-
result.push(item.name);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// GOOD
|
|
647
|
-
const result = pipe(
|
|
648
|
-
filter((item: Item) => item.active),
|
|
649
|
-
map(item => item.name)
|
|
650
|
-
)(items);
|
|
345
|
+
const fetchJsonWithGuards = pipeAsync(
|
|
346
|
+
fetchJson,
|
|
347
|
+
timeout(5_000),
|
|
348
|
+
retry(3)
|
|
349
|
+
);
|
|
651
350
|
```
|
|
652
351
|
|
|
653
|
-
###
|
|
352
|
+
### Object updates (avoid mutation)
|
|
654
353
|
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
const result = users
|
|
658
|
-
.filter(u => u.active)
|
|
659
|
-
.map(u => u.name)
|
|
660
|
-
.slice(0, 10);
|
|
354
|
+
```ts
|
|
355
|
+
import { pipe, assocPath, merge } from 'fp-pack';
|
|
661
356
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
map(u => u.name),
|
|
666
|
-
take(10)
|
|
667
|
-
)(users);
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
### ❌ Don't use traditional monads (Option, Either, Maybe)
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
// BAD - Don't implement this pattern
|
|
674
|
-
const maybeUser = Option.of(user)
|
|
675
|
-
.map(u => u.profile)
|
|
676
|
-
.flatMap(p => p.email);
|
|
677
|
-
|
|
678
|
-
// GOOD - Use SideEffect with pipeSideEffect
|
|
679
|
-
const getUserEmail = pipeSideEffect(
|
|
680
|
-
(user: User) => {
|
|
681
|
-
if (!user.profile) {
|
|
682
|
-
return SideEffect.of(() => null, 'NO_PROFILE');
|
|
683
|
-
}
|
|
684
|
-
return user.profile;
|
|
685
|
-
},
|
|
686
|
-
(profile) => {
|
|
687
|
-
if (!profile.email) {
|
|
688
|
-
return SideEffect.of(() => null, 'NO_EMAIL');
|
|
689
|
-
}
|
|
690
|
-
return profile.email;
|
|
691
|
-
}
|
|
357
|
+
const updateAccount = pipe(
|
|
358
|
+
assocPath(['profile', 'role'], 'member'),
|
|
359
|
+
merge({ updatedAt: Date.now() })
|
|
692
360
|
);
|
|
693
361
|
```
|
|
694
362
|
|
|
695
|
-
|
|
363
|
+
---
|
|
696
364
|
|
|
697
|
-
|
|
698
|
-
// BAD
|
|
699
|
-
const updateUser = (user: User) => {
|
|
700
|
-
user.lastLogin = new Date();
|
|
701
|
-
return user;
|
|
702
|
-
};
|
|
365
|
+
## Decision Guide (What Should the Agent Pick?)
|
|
703
366
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
367
|
+
- Is everything sync and pure? → `pipe`
|
|
368
|
+
- Any step async? → `pipeAsync`
|
|
369
|
+
- Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
370
|
+
- Need early-exit but type precision doesn’t matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
|
|
371
|
+
- Handling result at boundary?
|
|
372
|
+
- Need exact branch types? → `isSideEffect` + separate branches
|
|
373
|
+
- Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
|
|
374
|
+
- Large/unbounded/iterable data? → `fp-pack/stream`
|
|
709
375
|
|
|
710
|
-
|
|
711
|
-
import { assoc } from 'fp-pack';
|
|
712
|
-
const updateUser = assoc('lastLogin', new Date());
|
|
713
|
-
```
|
|
376
|
+
---
|
|
714
377
|
|
|
715
378
|
## Quick Reference
|
|
716
379
|
|
|
717
380
|
### Import Paths
|
|
718
|
-
|
|
719
|
-
-
|
|
720
|
-
- SideEffect: `import { pipeSideEffect, pipeSideEffectStrict,
|
|
381
|
+
|
|
382
|
+
- Main: `import { pipe, map, filter } from 'fp-pack'`
|
|
383
|
+
- SideEffect pipes: `import { pipeSideEffect, pipeSideEffectStrict, SideEffect } from 'fp-pack'`
|
|
384
|
+
- Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
|
|
721
385
|
- Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
|
|
722
386
|
|
|
723
387
|
### When to Use What
|
|
724
|
-
- **Pure sync transformations**: `pipe` + array/object functions
|
|
725
|
-
- **Pure async operations**: `pipeAsync`
|
|
726
|
-
- **Error handling with SideEffect**: `pipeSideEffect` (sync) / `pipeAsyncSideEffect` (async)
|
|
727
|
-
- **Strict SideEffect unions**: `pipeSideEffectStrict` (sync) / `pipeAsyncSideEffectStrict` (async)
|
|
728
|
-
- **Type-safe result handling**: `isSideEffect` for precise type narrowing (prefer this when you need branch-specific types)
|
|
729
|
-
- **Execute SideEffect**: `runPipeResult` (call OUTSIDE pipelines). If the input is narrowed to `SideEffect<R>`, it returns `R`. If the input is widened to `SideEffect<any>`/`any`, the result becomes `any`; provide generics to recover
|
|
730
|
-
- **Large datasets**: `stream/*` functions
|
|
731
|
-
- **Conditionals**: `ifElse`, `when`, `unless`, `cond`
|
|
732
|
-
- **Object access**: `prop`, `propStrict`, `path`, `pick`, `omit`
|
|
733
|
-
- **Object updates**: `assoc`, `merge`, `evolve`
|
|
734
|
-
|
|
735
|
-
## UI Framework Integration Patterns
|
|
736
|
-
|
|
737
|
-
fp-pack works seamlessly with UI frameworks. Here are common patterns organized by **use case**, not framework.
|
|
738
|
-
|
|
739
|
-
### Pattern 1: Handling User Input
|
|
740
|
-
|
|
741
|
-
**When**: Form inputs, button clicks, drag & drop, any user interaction
|
|
742
|
-
**Where to use**: Event handlers (onChange, @input, on:click, etc.)
|
|
743
|
-
|
|
744
|
-
```typescript
|
|
745
|
-
import { pipe, pipeAsyncSideEffect, trim, prop, assoc, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
746
|
-
|
|
747
|
-
// GOOD: Process form input declaratively
|
|
748
|
-
const handleNameChange = pipe(
|
|
749
|
-
prop('currentTarget'), // Safer than target in most UI libs
|
|
750
|
-
(el) => (el as HTMLInputElement).value,
|
|
751
|
-
trim,
|
|
752
|
-
tap((value) => {
|
|
753
|
-
// Prefer updater form to avoid stale state in React-like frameworks
|
|
754
|
-
setFormState(prev => assoc('name', value, prev));
|
|
755
|
-
})
|
|
756
|
-
);
|
|
757
|
-
|
|
758
|
-
// Use in any framework:
|
|
759
|
-
// React: <input onChange={handleNameChange} />
|
|
760
|
-
// Vue: <input @input="handleNameChange" />
|
|
761
|
-
// Svelte: <input on:input={handleNameChange} />
|
|
762
|
-
|
|
763
|
-
// GOOD: Complex form validation
|
|
764
|
-
const validateFieldsOrStop = (data: any) => {
|
|
765
|
-
const errors = validateFields(data);
|
|
766
|
-
if (!errors) return data;
|
|
767
|
-
return SideEffect.of(() => {
|
|
768
|
-
setErrors(errors);
|
|
769
|
-
return null;
|
|
770
|
-
}, 'VALIDATION_ERROR');
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
const handleSubmitPipeline = pipeAsyncSideEffect(
|
|
774
|
-
tap((e: Event) => e.preventDefault()),
|
|
775
|
-
prop('currentTarget'),
|
|
776
|
-
(form) => getFormData(form as HTMLFormElement),
|
|
777
|
-
validateFieldsOrStop, // Returns data or SideEffect
|
|
778
|
-
sanitizeInput,
|
|
779
|
-
submitToAPI
|
|
780
|
-
);
|
|
781
|
-
|
|
782
|
-
const handleSubmit = (e: Event) => runPipeResult(handleSubmitPipeline(e));
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Pattern 2: Computing Derived/Reactive Values
|
|
786
388
|
|
|
787
|
-
|
|
788
|
-
|
|
389
|
+
- Pure sync transforms: `pipe`
|
|
390
|
+
- Pure async transforms: `pipeAsync`
|
|
391
|
+
- Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
|
|
392
|
+
- Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
393
|
+
- Runtime branching: prefer `isSideEffect`
|
|
394
|
+
- Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
|
|
395
|
+
- Large/unbounded data: `fp-pack/stream`
|
|
789
396
|
|
|
790
|
-
|
|
791
|
-
import { pipe, filter, sortBy, map, take } from 'fp-pack';
|
|
792
|
-
|
|
793
|
-
// GOOD: Create reusable data transformation
|
|
794
|
-
const processUsers = pipe(
|
|
795
|
-
filter((u: User) => u.status === 'active'),
|
|
796
|
-
sortBy(u => u.lastLogin),
|
|
797
|
-
map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` })),
|
|
798
|
-
take(50)
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
// Use in any framework:
|
|
802
|
-
// React: const processed = useMemo(() => processUsers(users), [users]);
|
|
803
|
-
// Vue: const processed = computed(() => processUsers(users.value));
|
|
804
|
-
// Svelte: $: processed = processUsers($users);
|
|
805
|
-
// Solid: const processed = createMemo(() => processUsers(users()));
|
|
806
|
-
|
|
807
|
-
// GOOD: Search + filter + pagination
|
|
808
|
-
const searchUsers = (query: string, page: number) =>
|
|
809
|
-
pipe(
|
|
810
|
-
filter((u: User) =>
|
|
811
|
-
u.name.toLowerCase().includes(query.toLowerCase())
|
|
812
|
-
),
|
|
813
|
-
sortBy(u => u.name),
|
|
814
|
-
chunk(20), // Paginate
|
|
815
|
-
(pages) => pages[page] || []
|
|
816
|
-
);
|
|
817
|
-
|
|
818
|
-
// React example:
|
|
819
|
-
// const results = useMemo(
|
|
820
|
-
// () => searchUsers(searchQuery, currentPage)(allUsers),
|
|
821
|
-
// [searchQuery, currentPage, allUsers]
|
|
822
|
-
// );
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
### Pattern 3: Async Data Fetching and Processing
|
|
826
|
-
|
|
827
|
-
**When**: API calls, database queries, file operations
|
|
828
|
-
**Where to use**: Lifecycle hooks, effects, async event handlers
|
|
829
|
-
|
|
830
|
-
```typescript
|
|
831
|
-
import { pipeAsync, pipeAsyncSideEffect, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
832
|
-
import { filter, map } from 'fp-pack';
|
|
833
|
-
|
|
834
|
-
// GOOD: Fetch + transform + update state
|
|
835
|
-
const fetchAndProcessUsers = pipeAsync(
|
|
836
|
-
async (userId: string) => fetch(`/api/users/${userId}/friends`),
|
|
837
|
-
async (res) => res.json(),
|
|
838
|
-
filter((u: User) => u.isActive),
|
|
839
|
-
map(u => ({ id: u.id, name: u.name, avatar: u.avatar })),
|
|
840
|
-
(processed) => {
|
|
841
|
-
setUsers(processed); // Framework-specific state update
|
|
842
|
-
return processed;
|
|
843
|
-
}
|
|
844
|
-
);
|
|
845
|
-
|
|
846
|
-
// Use in any framework:
|
|
847
|
-
// React: useEffect(() => { fetchAndProcessUsers(id); }, [id]);
|
|
848
|
-
// Vue: watchEffect(() => fetchAndProcessUsers(userId.value));
|
|
849
|
-
// Svelte: $: fetchAndProcessUsers($userId);
|
|
850
|
-
|
|
851
|
-
// GOOD: Error handling with SideEffect
|
|
852
|
-
const validateResponseOrStop = (users: unknown) => {
|
|
853
|
-
if (!Array.isArray(users)) {
|
|
854
|
-
return SideEffect.of(() => {
|
|
855
|
-
setError('Invalid response');
|
|
856
|
-
return [];
|
|
857
|
-
}, 'INVALID_RESPONSE');
|
|
858
|
-
}
|
|
859
|
-
return users as User[];
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const safeFetchUsersPipeline = pipeAsyncSideEffect(
|
|
863
|
-
fetchUsers,
|
|
864
|
-
validateResponseOrStop,
|
|
865
|
-
filter((u: User) => u.verified),
|
|
866
|
-
tap((users) => setUsers(users))
|
|
867
|
-
);
|
|
868
|
-
|
|
869
|
-
const safeFetchUsers = () => runPipeResult(safeFetchUsersPipeline());
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
### Pattern 4: List/Table Data Processing
|
|
873
|
-
|
|
874
|
-
**When**: Displaying lists, tables, grids with search/filter/sort
|
|
875
|
-
**Where to use**: Component render logic, computed values
|
|
876
|
-
|
|
877
|
-
```typescript
|
|
878
|
-
import { pipe, filter, sortBy, groupBy, map } from 'fp-pack';
|
|
879
|
-
|
|
880
|
-
// GOOD: Complete table data pipeline
|
|
881
|
-
const processTableData = (
|
|
882
|
-
data: Product[],
|
|
883
|
-
filters: Filters,
|
|
884
|
-
sortConfig: SortConfig
|
|
885
|
-
) => pipe(
|
|
886
|
-
// Apply filters
|
|
887
|
-
filter((p: Product) => {
|
|
888
|
-
if (filters.category && p.category !== filters.category) return false;
|
|
889
|
-
if (filters.minPrice && p.price < filters.minPrice) return false;
|
|
890
|
-
if (filters.maxPrice && p.price > filters.maxPrice) return false;
|
|
891
|
-
return true;
|
|
892
|
-
}),
|
|
893
|
-
// Apply sorting
|
|
894
|
-
sortBy(sortConfig.direction === 'asc'
|
|
895
|
-
? (p) => p[sortConfig.key]
|
|
896
|
-
: (p) => -p[sortConfig.key]
|
|
897
|
-
),
|
|
898
|
-
// Add row metadata
|
|
899
|
-
map((product, index) => ({
|
|
900
|
-
...product,
|
|
901
|
-
rowId: `row-${index}`,
|
|
902
|
-
isEven: index % 2 === 0
|
|
903
|
-
}))
|
|
904
|
-
)(data);
|
|
905
|
-
|
|
906
|
-
// GOOD: Group for categorized display
|
|
907
|
-
const groupProductsByCategory = pipe(
|
|
908
|
-
groupBy((p: Product) => p.category),
|
|
909
|
-
(grouped) => Object.entries(grouped).map(([category, products]) => ({
|
|
910
|
-
category,
|
|
911
|
-
products,
|
|
912
|
-
count: products.length,
|
|
913
|
-
totalValue: products.reduce((sum, p) => sum + p.price, 0)
|
|
914
|
-
}))
|
|
915
|
-
);
|
|
916
|
-
```
|
|
917
|
-
|
|
918
|
-
### Pattern 5: Form State Management
|
|
919
|
-
|
|
920
|
-
**When**: Complex forms with validation and state
|
|
921
|
-
**Where to use**: Form submission, field updates, validation
|
|
922
|
-
|
|
923
|
-
```typescript
|
|
924
|
-
import { pipe, pipeSideEffect, assoc, pick, mapValues, SideEffect, runPipeResult } from 'fp-pack';
|
|
925
|
-
|
|
926
|
-
// GOOD: Update nested form state immutably
|
|
927
|
-
const updateField = (fieldName: string, value: any) =>
|
|
928
|
-
pipe(
|
|
929
|
-
assoc(fieldName, value),
|
|
930
|
-
(state) => assoc('touched', { ...state.touched, [fieldName]: true }, state)
|
|
931
|
-
);
|
|
932
|
-
|
|
933
|
-
// GOOD: Form submission with validation
|
|
934
|
-
const validateFormOrStop = (data: any) => {
|
|
935
|
-
const errors = validateFormData(data);
|
|
936
|
-
return Object.keys(errors).length > 0
|
|
937
|
-
? SideEffect.of(() => {
|
|
938
|
-
setFormErrors(errors);
|
|
939
|
-
return null;
|
|
940
|
-
}, 'VALIDATION_ERROR')
|
|
941
|
-
: data;
|
|
942
|
-
};
|
|
943
|
-
|
|
944
|
-
const submitFormPipeline = pipeSideEffect(
|
|
945
|
-
pick(['email', 'password', 'name']), // Only include relevant fields
|
|
946
|
-
mapValues((v) => typeof v === 'string' ? v.trim() : v), // Sanitize
|
|
947
|
-
validateFormOrStop,
|
|
948
|
-
submitToAPI
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
const submitForm = (data: any) => runPipeResult(submitFormPipeline(data));
|
|
952
|
-
|
|
953
|
-
// GOOD: Multi-step form state
|
|
954
|
-
const validateCurrentStepOrStop = (state: any) => {
|
|
955
|
-
const errors = validateCurrentStep(state);
|
|
956
|
-
if (!errors) return state;
|
|
957
|
-
return SideEffect.of(() => {
|
|
958
|
-
setStepErrors(errors);
|
|
959
|
-
return state;
|
|
960
|
-
}, 'STEP_VALIDATION_ERROR');
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
const goToNextStepPipeline = pipeSideEffect(
|
|
964
|
-
validateCurrentStepOrStop,
|
|
965
|
-
(state) => assoc('currentStep', state.currentStep + 1, state)
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
const goToNextStep = (state: any) => runPipeResult(goToNextStepPipeline(state));
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
### Pattern 6: Real-time Data Streams
|
|
972
|
-
|
|
973
|
-
**When**: WebSocket updates, SSE, real-time data
|
|
974
|
-
**Where to use**: WebSocket handlers, event listeners
|
|
975
|
-
|
|
976
|
-
```typescript
|
|
977
|
-
import { pipe, filter, map, take } from 'fp-pack';
|
|
978
|
-
|
|
979
|
-
// GOOD: Process incoming WebSocket messages
|
|
980
|
-
const handleWebSocketMessage = pipe(
|
|
981
|
-
(event: MessageEvent) => JSON.parse(event.data),
|
|
982
|
-
filter((msg: Message) => msg.type === 'USER_UPDATE'),
|
|
983
|
-
map(msg => msg.payload),
|
|
984
|
-
(update) => {
|
|
985
|
-
// Update state with new data
|
|
986
|
-
setUsers(prevUsers =>
|
|
987
|
-
prevUsers.map(u => u.id === update.id ? { ...u, ...update } : u)
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
);
|
|
991
|
-
|
|
992
|
-
// websocket.onmessage = handleWebSocketMessage;
|
|
993
|
-
|
|
994
|
-
// GOOD: Batch updates with stream
|
|
995
|
-
import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
|
|
996
|
-
import { pipeAsync, runPipeResult } from 'fp-pack';
|
|
997
|
-
|
|
998
|
-
const processBatchUpdates = async (updates: AsyncIterable<Update>) => {
|
|
999
|
-
const processed = await streamPipe(
|
|
1000
|
-
streamFilter((u: Update) => u.priority === 'high'),
|
|
1001
|
-
streamTake(100),
|
|
1002
|
-
toArray
|
|
1003
|
-
)(updates);
|
|
1004
|
-
|
|
1005
|
-
batchUpdateUI(processed);
|
|
1006
|
-
};
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
### Pattern 7: Component Props Transformation
|
|
1010
|
-
|
|
1011
|
-
**When**: Passing data to child components
|
|
1012
|
-
**Where to use**: Component composition, prop drilling
|
|
1013
|
-
|
|
1014
|
-
```typescript
|
|
1015
|
-
import { pipe, pick, map, merge } from 'fp-pack';
|
|
1016
|
-
|
|
1017
|
-
// GOOD: Transform data for child component
|
|
1018
|
-
const prepareUserCardProps = pipe(
|
|
1019
|
-
pick(['id', 'name', 'avatar', 'status']),
|
|
1020
|
-
merge({
|
|
1021
|
-
onClick: handleUserClick,
|
|
1022
|
-
className: 'user-card'
|
|
1023
|
-
})
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
// Usage:
|
|
1027
|
-
// const userProps = prepareUserCardProps(user);
|
|
1028
|
-
// <UserCard {...userProps} />
|
|
1029
|
-
|
|
1030
|
-
// GOOD: Prepare list of component props
|
|
1031
|
-
const prepareListItems = pipe(
|
|
1032
|
-
filter((item: Item) => item.visible),
|
|
1033
|
-
map(item => ({
|
|
1034
|
-
key: item.id,
|
|
1035
|
-
...pick(['title', 'description', 'icon'], item),
|
|
1036
|
-
onClick: () => handleClick(item.id),
|
|
1037
|
-
isActive: item.id === activeId
|
|
1038
|
-
}))
|
|
1039
|
-
);
|
|
1040
|
-
|
|
1041
|
-
// Usage:
|
|
1042
|
-
// {prepareListItems(items).map(props => <ListItem {...props} />)}
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
### Pattern 8: State Update Reducers
|
|
1046
|
-
|
|
1047
|
-
**When**: Complex state updates, global state management
|
|
1048
|
-
**Where to use**: Redux/Zustand/Pinia reducers, state update functions
|
|
1049
|
-
|
|
1050
|
-
```typescript
|
|
1051
|
-
import { pipe, assoc, dissoc, merge, evolve } from 'fp-pack';
|
|
1052
|
-
|
|
1053
|
-
// GOOD: Redux-style reducer with fp-pack
|
|
1054
|
-
const userReducer = (state: State, action: Action) => {
|
|
1055
|
-
switch (action.type) {
|
|
1056
|
-
case 'ADD_USER':
|
|
1057
|
-
return pipe(
|
|
1058
|
-
prop('users'),
|
|
1059
|
-
append(action.payload),
|
|
1060
|
-
(users) => assoc('users', users, state)
|
|
1061
|
-
)(state);
|
|
1062
|
-
|
|
1063
|
-
case 'UPDATE_USER':
|
|
1064
|
-
return pipe(
|
|
1065
|
-
prop('users'),
|
|
1066
|
-
map((u: User) => u.id === action.payload.id
|
|
1067
|
-
? merge(u, action.payload.updates)
|
|
1068
|
-
: u
|
|
1069
|
-
),
|
|
1070
|
-
(users) => assoc('users', users, state)
|
|
1071
|
-
)(state);
|
|
1072
|
-
|
|
1073
|
-
case 'DELETE_USER':
|
|
1074
|
-
return pipe(
|
|
1075
|
-
prop('users'),
|
|
1076
|
-
filter((u: User) => u.id !== action.payload),
|
|
1077
|
-
(users) => assoc('users', users, state)
|
|
1078
|
-
)(state);
|
|
1079
|
-
|
|
1080
|
-
default:
|
|
1081
|
-
return state;
|
|
1082
|
-
}
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
|
-
// GOOD: Using evolve for nested updates
|
|
1086
|
-
const updateNestedState = evolve({
|
|
1087
|
-
user: evolve({
|
|
1088
|
-
profile: merge({ verified: true }),
|
|
1089
|
-
settings: assoc('notifications', false)
|
|
1090
|
-
}),
|
|
1091
|
-
lastUpdated: () => new Date()
|
|
1092
|
-
});
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
### Pattern 9: Optimistic Updates
|
|
1096
|
-
|
|
1097
|
-
**When**: UI updates before server confirmation
|
|
1098
|
-
**Where to use**: Create/update/delete operations
|
|
1099
|
-
|
|
1100
|
-
```typescript
|
|
1101
|
-
import { pipe, pipeAsync, append, filter } from 'fp-pack';
|
|
1102
|
-
|
|
1103
|
-
// GOOD: Optimistic create with rollback
|
|
1104
|
-
const createItemOptimistically = (newItem: Item) => {
|
|
1105
|
-
const tempId = `temp-${Date.now()}`;
|
|
1106
|
-
const optimisticItem = { ...newItem, id: tempId, pending: true };
|
|
1107
|
-
|
|
1108
|
-
// Immediately update UI
|
|
1109
|
-
setItems(pipe(append(optimisticItem)));
|
|
1110
|
-
|
|
1111
|
-
// Then persist
|
|
1112
|
-
return pipeAsync(
|
|
1113
|
-
async () => api.createItem(newItem),
|
|
1114
|
-
(savedItem) => {
|
|
1115
|
-
// Replace temp with real item
|
|
1116
|
-
setItems(
|
|
1117
|
-
pipe(
|
|
1118
|
-
filter((item: Item) => item.id !== tempId),
|
|
1119
|
-
append(savedItem)
|
|
1120
|
-
)
|
|
1121
|
-
);
|
|
1122
|
-
return savedItem;
|
|
1123
|
-
}
|
|
1124
|
-
)().catch((error) => {
|
|
1125
|
-
// Rollback on error
|
|
1126
|
-
setItems(pipe(filter((item: Item) => item.id !== tempId)));
|
|
1127
|
-
throw error;
|
|
1128
|
-
});
|
|
1129
|
-
};
|
|
1130
|
-
```
|
|
1131
|
-
|
|
1132
|
-
### Pattern 10: URL/Query Parameter Handling
|
|
1133
|
-
|
|
1134
|
-
**When**: Syncing UI state with URL
|
|
1135
|
-
**Where to use**: Routing, search parameters, filters
|
|
1136
|
-
|
|
1137
|
-
```typescript
|
|
1138
|
-
import { pipe, pick, mapValues, merge } from 'fp-pack';
|
|
1139
|
-
|
|
1140
|
-
// GOOD: Parse query params to state
|
|
1141
|
-
const parseQueryParams = pipe(
|
|
1142
|
-
(search: string) => new URLSearchParams(search),
|
|
1143
|
-
(params) => ({
|
|
1144
|
-
page: Number(params.get('page')) || 1,
|
|
1145
|
-
query: params.get('q') || '',
|
|
1146
|
-
category: params.get('category') || 'all',
|
|
1147
|
-
sort: params.get('sort') || 'date'
|
|
1148
|
-
})
|
|
1149
|
-
);
|
|
1150
|
-
|
|
1151
|
-
// GOOD: Convert state to query params
|
|
1152
|
-
const stateToQueryParams = pipe(
|
|
1153
|
-
pick(['page', 'query', 'category', 'sort']),
|
|
1154
|
-
(state) => {
|
|
1155
|
-
const params = new URLSearchParams();
|
|
1156
|
-
Object.entries(state).forEach(([key, value]) => {
|
|
1157
|
-
if (value) params.set(key, String(value));
|
|
1158
|
-
});
|
|
1159
|
-
return params.toString();
|
|
1160
|
-
}
|
|
1161
|
-
);
|
|
1162
|
-
|
|
1163
|
-
// Usage in framework router:
|
|
1164
|
-
// const filters = parseQueryParams(location.search);
|
|
1165
|
-
// navigate(`/products?${stateToQueryParams(currentState)}`);
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
### Pattern 11: Infinite Scroll / Virtual Lists
|
|
1169
|
-
|
|
1170
|
-
**When**: Large lists with lazy loading, infinite scroll, virtual rendering
|
|
1171
|
-
**Where to use**: Scroll handlers, pagination, large dataset rendering
|
|
1172
|
-
|
|
1173
|
-
```typescript
|
|
1174
|
-
import { pipe, pipeAsyncSideEffect, when, tap, runPipeResult } from 'fp-pack';
|
|
1175
|
-
import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
|
|
1176
|
-
|
|
1177
|
-
// GOOD: Infinite scroll with pipe - all logic inside
|
|
1178
|
-
const handleScroll = pipe(
|
|
1179
|
-
(e: Event) => e.target as HTMLElement,
|
|
1180
|
-
(el) => ({
|
|
1181
|
-
scrollTop: el.scrollTop,
|
|
1182
|
-
scrollHeight: el.scrollHeight,
|
|
1183
|
-
clientHeight: el.clientHeight,
|
|
1184
|
-
hasMore,
|
|
1185
|
-
isLoading
|
|
1186
|
-
}),
|
|
1187
|
-
// Only load if near bottom, has more data, and not currently loading
|
|
1188
|
-
when(
|
|
1189
|
-
({ scrollTop, scrollHeight, clientHeight, hasMore, isLoading }) =>
|
|
1190
|
-
scrollTop + clientHeight >= scrollHeight - 100 && hasMore && !isLoading,
|
|
1191
|
-
tap(() => loadNextPage())
|
|
1192
|
-
)
|
|
1193
|
-
);
|
|
1194
|
-
|
|
1195
|
-
// GOOD: Load next page with stream processing
|
|
1196
|
-
const loadNextPagePipeline = pipeAsyncSideEffect(
|
|
1197
|
-
async () => {
|
|
1198
|
-
setIsLoading(true);
|
|
1199
|
-
return fetchItemsFromAPI(currentPage);
|
|
1200
|
-
},
|
|
1201
|
-
// Use stream for lazy processing
|
|
1202
|
-
(items) => streamPipe(
|
|
1203
|
-
streamFilter((item: Item) => item.visible),
|
|
1204
|
-
streamTake(pageSize),
|
|
1205
|
-
toArray
|
|
1206
|
-
)(items),
|
|
1207
|
-
tap((newItems) => setItems(prev => [...prev, ...newItems])),
|
|
1208
|
-
tap(() => setCurrentPage(prev => prev + 1)),
|
|
1209
|
-
tap(() => setIsLoading(false))
|
|
1210
|
-
);
|
|
1211
|
-
|
|
1212
|
-
const loadNextPage = () => runPipeResult(loadNextPagePipeline());
|
|
1213
|
-
|
|
1214
|
-
// GOOD: Virtual scroll - calculate visible range in pipe
|
|
1215
|
-
const getVisibleItems = pipe(
|
|
1216
|
-
(scrollTop: number) => ({
|
|
1217
|
-
itemHeight: 50,
|
|
1218
|
-
viewportHeight: 600,
|
|
1219
|
-
bufferSize: 5,
|
|
1220
|
-
scrollTop
|
|
1221
|
-
}),
|
|
1222
|
-
({ itemHeight, viewportHeight, bufferSize, scrollTop }) => ({
|
|
1223
|
-
startIndex: Math.floor(scrollTop / itemHeight),
|
|
1224
|
-
endIndex: Math.ceil((scrollTop + viewportHeight) / itemHeight),
|
|
1225
|
-
bufferSize
|
|
1226
|
-
}),
|
|
1227
|
-
({ startIndex, endIndex, bufferSize }) => ({
|
|
1228
|
-
start: Math.max(0, startIndex - bufferSize),
|
|
1229
|
-
end: endIndex + bufferSize
|
|
1230
|
-
}),
|
|
1231
|
-
({ start, end }) => allItems.slice(start, end)
|
|
1232
|
-
);
|
|
1233
|
-
|
|
1234
|
-
// GOOD: Lazy load with intersection observer
|
|
1235
|
-
const handleIntersection = pipe(
|
|
1236
|
-
(entries: IntersectionObserverEntry[]) => entries[0],
|
|
1237
|
-
when(
|
|
1238
|
-
(entry) => entry.isIntersecting && hasMoreItems && !isLoadingMore,
|
|
1239
|
-
tap(() => loadMoreItems())
|
|
1240
|
-
)
|
|
1241
|
-
);
|
|
1242
|
-
```
|
|
1243
|
-
|
|
1244
|
-
### Pattern 12: Conditional State Updates
|
|
1245
|
-
|
|
1246
|
-
**When**: State updates that depend on conditions
|
|
1247
|
-
**Where to use**: Any state update with business logic
|
|
1248
|
-
|
|
1249
|
-
```typescript
|
|
1250
|
-
import { pipe, ifElse, when, cond, tap, assoc, prop, append, map, filter, merge } from 'fp-pack';
|
|
1251
|
-
|
|
1252
|
-
// GOOD: Use ifElse instead of if/else
|
|
1253
|
-
const toggleUserStatus = pipe(
|
|
1254
|
-
ifElse(
|
|
1255
|
-
(user: User) => user.status === 'active',
|
|
1256
|
-
assoc('status', 'inactive'),
|
|
1257
|
-
assoc('status', 'active')
|
|
1258
|
-
),
|
|
1259
|
-
tap((updatedUser) => setUser(updatedUser))
|
|
1260
|
-
);
|
|
1261
|
-
|
|
1262
|
-
// GOOD: Use ifElse for conditional side effects
|
|
1263
|
-
const saveIfValid = pipe(
|
|
1264
|
-
validateForm,
|
|
1265
|
-
ifElse(
|
|
1266
|
-
(result) => result.isValid,
|
|
1267
|
-
pipe(
|
|
1268
|
-
tap((data) => saveToAPI(data)),
|
|
1269
|
-
tap(() => showSuccessMessage())
|
|
1270
|
-
),
|
|
1271
|
-
tap((result) => setErrors(result.errors))
|
|
1272
|
-
)
|
|
1273
|
-
);
|
|
1274
|
-
|
|
1275
|
-
// GOOD: Use cond for multi-branch logic (instead of switch/if-else chain)
|
|
1276
|
-
const processUserAction = pipe(
|
|
1277
|
-
prop('action'),
|
|
1278
|
-
cond([
|
|
1279
|
-
[
|
|
1280
|
-
(action) => action.type === 'CREATE',
|
|
1281
|
-
pipe(
|
|
1282
|
-
prop('payload'),
|
|
1283
|
-
(user) => append(user),
|
|
1284
|
-
tap((users) => setUsers(users))
|
|
1285
|
-
)
|
|
1286
|
-
],
|
|
1287
|
-
[
|
|
1288
|
-
(action) => action.type === 'UPDATE',
|
|
1289
|
-
pipe(
|
|
1290
|
-
prop('payload'),
|
|
1291
|
-
({ id, updates }) => map((u: User) => u.id === id ? merge(u, updates) : u),
|
|
1292
|
-
tap((users) => setUsers(users))
|
|
1293
|
-
)
|
|
1294
|
-
],
|
|
1295
|
-
[
|
|
1296
|
-
(action) => action.type === 'DELETE',
|
|
1297
|
-
pipe(
|
|
1298
|
-
prop('payload'),
|
|
1299
|
-
(id) => filter((u: User) => u.id !== id),
|
|
1300
|
-
tap((users) => setUsers(users))
|
|
1301
|
-
)
|
|
1302
|
-
],
|
|
1303
|
-
[
|
|
1304
|
-
() => true, // default case
|
|
1305
|
-
tap(() => console.warn('Unknown action'))
|
|
1306
|
-
]
|
|
1307
|
-
])
|
|
1308
|
-
);
|
|
1309
|
-
|
|
1310
|
-
// GOOD: Complex state update with all logic in pipe
|
|
1311
|
-
const updateCartItem = (itemId: string, quantity: number) => pipe(
|
|
1312
|
-
// Get current cart
|
|
1313
|
-
(cart) => cart.items,
|
|
1314
|
-
// Find and update item
|
|
1315
|
-
map((item: CartItem) =>
|
|
1316
|
-
ifElse(
|
|
1317
|
-
() => item.id === itemId,
|
|
1318
|
-
pipe(
|
|
1319
|
-
assoc('quantity', quantity),
|
|
1320
|
-
when(
|
|
1321
|
-
(updated) => updated.quantity <= 0,
|
|
1322
|
-
() => null // Mark for removal
|
|
1323
|
-
)
|
|
1324
|
-
),
|
|
1325
|
-
() => item
|
|
1326
|
-
)(item)
|
|
1327
|
-
),
|
|
1328
|
-
// Remove null items (quantity <= 0)
|
|
1329
|
-
filter((item) => item !== null),
|
|
1330
|
-
// Update state
|
|
1331
|
-
tap((items) => setCart({ items })),
|
|
1332
|
-
// Show notification
|
|
1333
|
-
tap((items) => {
|
|
1334
|
-
const item = items.find(i => i.id === itemId);
|
|
1335
|
-
if (item) showNotification(`Updated ${item.name}`);
|
|
1336
|
-
else showNotification('Item removed from cart');
|
|
1337
|
-
})
|
|
1338
|
-
);
|
|
1339
|
-
```
|
|
1340
|
-
|
|
1341
|
-
## Library Integration Quick Reference
|
|
1342
|
-
|
|
1343
|
-
This section shows how to integrate fp-pack with popular UI libraries. All examples keep logic **inside pipe chains** using fp-pack control flow functions.
|
|
1344
|
-
|
|
1345
|
-
### React Ecosystem
|
|
1346
|
-
|
|
1347
|
-
#### React Query / TanStack Query
|
|
1348
|
-
|
|
1349
|
-
```typescript
|
|
1350
|
-
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
1351
|
-
import { pipe, filter, sortBy, map, tap, when } from 'fp-pack';
|
|
1352
|
-
|
|
1353
|
-
// GOOD: Transform data in select using pipe
|
|
1354
|
-
const { data: activeUsers } = useQuery({
|
|
1355
|
-
queryKey: ['users'],
|
|
1356
|
-
queryFn: fetchUsers,
|
|
1357
|
-
select: pipe(
|
|
1358
|
-
filter((u: User) => u.status === 'active'),
|
|
1359
|
-
sortBy((u) => u.name),
|
|
1360
|
-
map((u) => ({ id: u.id, name: u.name, email: u.email }))
|
|
1361
|
-
)
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
// GOOD: Handle mutations with pipe
|
|
1365
|
-
const mutation = useMutation({
|
|
1366
|
-
mutationFn: createUser,
|
|
1367
|
-
onSuccess: pipe(
|
|
1368
|
-
tap((newUser) => queryClient.invalidateQueries(['users'])),
|
|
1369
|
-
when(
|
|
1370
|
-
(user) => user.isPremium,
|
|
1371
|
-
tap(() => showPremiumWelcome())
|
|
1372
|
-
),
|
|
1373
|
-
tap(() => navigate('/dashboard'))
|
|
1374
|
-
)
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
// GOOD: Optimistic updates in pipe
|
|
1378
|
-
const updateMutation = useMutation({
|
|
1379
|
-
mutationFn: updateUser,
|
|
1380
|
-
onMutate: pipe(
|
|
1381
|
-
tap(async (newUser) => {
|
|
1382
|
-
await queryClient.cancelQueries(['users']);
|
|
1383
|
-
const previous = queryClient.getQueryData(['users']);
|
|
1384
|
-
queryClient.setQueryData(['users'], pipe(
|
|
1385
|
-
map((u: User) => u.id === newUser.id ? merge(u, newUser) : u)
|
|
1386
|
-
));
|
|
1387
|
-
return { previous };
|
|
1388
|
-
})
|
|
1389
|
-
),
|
|
1390
|
-
onError: pipe(
|
|
1391
|
-
tap((err, variables, context) => {
|
|
1392
|
-
if (context?.previous) {
|
|
1393
|
-
queryClient.setQueryData(['users'], context.previous);
|
|
1394
|
-
}
|
|
1395
|
-
})
|
|
1396
|
-
)
|
|
1397
|
-
});
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
#### Zustand
|
|
1401
|
-
|
|
1402
|
-
```typescript
|
|
1403
|
-
import create from 'zustand';
|
|
1404
|
-
import { pipe, append, filter, map, merge, ifElse, when, tap, prop, sortBy, assoc } from 'fp-pack';
|
|
1405
|
-
|
|
1406
|
-
// GOOD: All actions use pipe
|
|
1407
|
-
const useStore = create((set, get) => ({
|
|
1408
|
-
users: [],
|
|
1409
|
-
|
|
1410
|
-
addUser: pipe(
|
|
1411
|
-
(user: User) => user,
|
|
1412
|
-
when(
|
|
1413
|
-
(user) => !get().users.some(u => u.id === user.id),
|
|
1414
|
-
tap((user) => set(pipe(
|
|
1415
|
-
prop('users'),
|
|
1416
|
-
append(user),
|
|
1417
|
-
sortBy((u: User) => u.name),
|
|
1418
|
-
(users) => ({ users })
|
|
1419
|
-
)(get())))
|
|
1420
|
-
)
|
|
1421
|
-
),
|
|
1422
|
-
|
|
1423
|
-
updateUser: (id: string, updates: Partial<User>) => set(pipe(
|
|
1424
|
-
prop('users'),
|
|
1425
|
-
map((u: User) =>
|
|
1426
|
-
ifElse(
|
|
1427
|
-
() => u.id === id,
|
|
1428
|
-
merge(u, updates),
|
|
1429
|
-
() => u
|
|
1430
|
-
)(u)
|
|
1431
|
-
),
|
|
1432
|
-
(users) => ({ users })
|
|
1433
|
-
)(get())),
|
|
1434
|
-
|
|
1435
|
-
deleteUser: (id: string) => set(pipe(
|
|
1436
|
-
prop('users'),
|
|
1437
|
-
filter((u: User) => u.id !== id),
|
|
1438
|
-
(users) => ({ users })
|
|
1439
|
-
)(get())),
|
|
1440
|
-
|
|
1441
|
-
toggleUserStatus: (id: string) => set(pipe(
|
|
1442
|
-
prop('users'),
|
|
1443
|
-
map((u: User) => u.id === id
|
|
1444
|
-
? pipe(
|
|
1445
|
-
ifElse(
|
|
1446
|
-
(user) => user.status === 'active',
|
|
1447
|
-
assoc('status', 'inactive'),
|
|
1448
|
-
assoc('status', 'active')
|
|
1449
|
-
)
|
|
1450
|
-
)(u)
|
|
1451
|
-
: u
|
|
1452
|
-
),
|
|
1453
|
-
(users) => ({ users })
|
|
1454
|
-
)(get()))
|
|
1455
|
-
}));
|
|
1456
|
-
```
|
|
1457
|
-
|
|
1458
|
-
#### Redux Toolkit
|
|
1459
|
-
|
|
1460
|
-
```typescript
|
|
1461
|
-
import { createSlice } from '@reduxjs/toolkit';
|
|
1462
|
-
import { pipe, append, filter, map, merge, sortBy, cond, assoc } from 'fp-pack';
|
|
1463
|
-
|
|
1464
|
-
// GOOD: Reducers with pipe - no manual mutations
|
|
1465
|
-
const userSlice = createSlice({
|
|
1466
|
-
name: 'users',
|
|
1467
|
-
initialState: { list: [], loading: false },
|
|
1468
|
-
reducers: {
|
|
1469
|
-
addUser: (state, action) => {
|
|
1470
|
-
state.list = pipe(
|
|
1471
|
-
append(action.payload),
|
|
1472
|
-
sortBy((u: User) => u.createdAt)
|
|
1473
|
-
)(state.list);
|
|
1474
|
-
},
|
|
1475
|
-
|
|
1476
|
-
updateUser: (state, action) => {
|
|
1477
|
-
state.list = pipe(
|
|
1478
|
-
map((u: User) =>
|
|
1479
|
-
u.id === action.payload.id
|
|
1480
|
-
? merge(u, action.payload.updates)
|
|
1481
|
-
: u
|
|
1482
|
-
)
|
|
1483
|
-
)(state.list);
|
|
1484
|
-
},
|
|
1485
|
-
|
|
1486
|
-
deleteUser: (state, action) => {
|
|
1487
|
-
state.list = pipe(
|
|
1488
|
-
filter((u: User) => u.id !== action.payload)
|
|
1489
|
-
)(state.list);
|
|
1490
|
-
},
|
|
1491
|
-
|
|
1492
|
-
// Complex update with cond
|
|
1493
|
-
processAction: (state, action) => {
|
|
1494
|
-
state.list = pipe(
|
|
1495
|
-
cond([
|
|
1496
|
-
[
|
|
1497
|
-
() => action.type === 'BULK_ACTIVATE',
|
|
1498
|
-
map((u: User) => assoc('status', 'active', u))
|
|
1499
|
-
],
|
|
1500
|
-
[
|
|
1501
|
-
() => action.type === 'BULK_DELETE',
|
|
1502
|
-
filter((u: User) => !action.payload.ids.includes(u.id))
|
|
1503
|
-
],
|
|
1504
|
-
[
|
|
1505
|
-
() => true,
|
|
1506
|
-
(users) => users // no change
|
|
1507
|
-
]
|
|
1508
|
-
])
|
|
1509
|
-
)(state.list);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
});
|
|
1513
|
-
```
|
|
1514
|
-
|
|
1515
|
-
#### React Hook Form
|
|
1516
|
-
|
|
1517
|
-
```typescript
|
|
1518
|
-
import { useForm } from 'react-hook-form';
|
|
1519
|
-
import { pipe, pipeSideEffect, pipeAsyncSideEffect, pick, mapValues, trim, when, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
1520
|
-
|
|
1521
|
-
// GOOD: Validation with pipeSideEffect
|
|
1522
|
-
const validateFormDataPipeline = pipeSideEffect(
|
|
1523
|
-
pick(['email', 'password', 'name']),
|
|
1524
|
-
mapValues((v) => typeof v === 'string' ? trim(v) : v),
|
|
1525
|
-
(data) => {
|
|
1526
|
-
const errors: any = {};
|
|
1527
|
-
if (!data.email?.includes('@')) errors.email = 'Invalid email';
|
|
1528
|
-
if ((data.password?.length || 0) < 8) errors.password = 'Too short';
|
|
1529
|
-
return Object.keys(errors).length > 0
|
|
1530
|
-
? SideEffect.of(() => ({ values: {}, errors }), 'VALIDATION_ERROR')
|
|
1531
|
-
: { values: data, errors: {} };
|
|
1532
|
-
}
|
|
1533
|
-
);
|
|
1534
|
-
|
|
1535
|
-
const validateFormData = (values: any) => runPipeResult(validateFormDataPipeline(values));
|
|
1536
|
-
|
|
1537
|
-
const { register, handleSubmit } = useForm({
|
|
1538
|
-
resolver: (values) => validateFormData(values)
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
// GOOD: Submit handler with pipeAsyncSideEffect
|
|
1542
|
-
const onSubmitPipeline = pipeAsyncSideEffect(
|
|
1543
|
-
validateFormData,
|
|
1544
|
-
when(
|
|
1545
|
-
(result) => Object.keys(result.errors).length === 0,
|
|
1546
|
-
pipe(
|
|
1547
|
-
prop('values'),
|
|
1548
|
-
submitToAPI,
|
|
1549
|
-
tap(() => navigate('/success'))
|
|
1550
|
-
)
|
|
1551
|
-
)
|
|
1552
|
-
);
|
|
1553
|
-
|
|
1554
|
-
const onSubmit = (data: any) => runPipeResult(onSubmitPipeline(data));
|
|
1555
|
-
```
|
|
397
|
+
---
|
|
1556
398
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
#### Pinia
|
|
1560
|
-
|
|
1561
|
-
```typescript
|
|
1562
|
-
import { defineStore } from 'pinia';
|
|
1563
|
-
import { pipe, append, filter, map, merge, sortBy, when, tap } from 'fp-pack';
|
|
1564
|
-
|
|
1565
|
-
// GOOD: All actions use pipe
|
|
1566
|
-
export const useUserStore = defineStore('user', {
|
|
1567
|
-
state: () => ({ users: [], loading: false }),
|
|
1568
|
-
|
|
1569
|
-
actions: {
|
|
1570
|
-
addUser(user: User) {
|
|
1571
|
-
this.users = pipe(
|
|
1572
|
-
append(user),
|
|
1573
|
-
sortBy((u: User) => u.name),
|
|
1574
|
-
when(
|
|
1575
|
-
(users) => users.length > 100,
|
|
1576
|
-
tap(() => this.showWarning('Many users'))
|
|
1577
|
-
)
|
|
1578
|
-
)(this.users);
|
|
1579
|
-
},
|
|
1580
|
-
|
|
1581
|
-
updateUser(id: string, updates: Partial<User>) {
|
|
1582
|
-
this.users = pipe(
|
|
1583
|
-
map((u: User) => u.id === id ? merge(u, updates) : u)
|
|
1584
|
-
)(this.users);
|
|
1585
|
-
},
|
|
1586
|
-
|
|
1587
|
-
deleteUser(id: string) {
|
|
1588
|
-
this.users = pipe(
|
|
1589
|
-
filter((u: User) => u.id !== id),
|
|
1590
|
-
tap((users) => {
|
|
1591
|
-
if (users.length === 0) this.showEmptyState = true;
|
|
1592
|
-
})
|
|
1593
|
-
)(this.users);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
});
|
|
1597
|
-
```
|
|
399
|
+
## Anti-Patterns (Avoid)
|
|
1598
400
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
// GOOD: Transform response with pipe
|
|
1606
|
-
const { data } = useFetch('/api/users', {
|
|
1607
|
-
afterFetch: pipe(
|
|
1608
|
-
prop('data'),
|
|
1609
|
-
filter((u: User) => u.verified),
|
|
1610
|
-
sortBy((u) => u.name),
|
|
1611
|
-
map((u) => ({ id: u.id, name: u.name })),
|
|
1612
|
-
tap((users) => console.log(`Loaded ${users.length} users`))
|
|
1613
|
-
)
|
|
1614
|
-
}).json();
|
|
1615
|
-
|
|
1616
|
-
// GOOD: Refetch with condition in pipe
|
|
1617
|
-
const { execute } = useFetch('/api/users');
|
|
1618
|
-
|
|
1619
|
-
const refreshIfNeeded = pipe(
|
|
1620
|
-
(lastUpdate: Date) => Date.now() - lastUpdate.getTime(),
|
|
1621
|
-
when(
|
|
1622
|
-
(diff) => diff > 5 * 60 * 1000, // 5 minutes
|
|
1623
|
-
tap(() => execute())
|
|
1624
|
-
)
|
|
1625
|
-
);
|
|
1626
|
-
```
|
|
401
|
+
- Imperative loops when a pipeline is clearer
|
|
402
|
+
- Mutating inputs (prefer immutable transforms)
|
|
403
|
+
- Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
|
|
404
|
+
- Calling `runPipeResult` inside a SideEffect-aware pipeline
|
|
405
|
+
- Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
|
|
1627
406
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
All state management libraries benefit from fp-pack's immutable update patterns:
|
|
1631
|
-
|
|
1632
|
-
```typescript
|
|
1633
|
-
// GOOD: Generic state update pattern (works with any library)
|
|
1634
|
-
const updateState = <T>(
|
|
1635
|
-
state: T,
|
|
1636
|
-
path: string[],
|
|
1637
|
-
updater: (value: any) => any
|
|
1638
|
-
) => pipe(
|
|
1639
|
-
pathOr(null, path),
|
|
1640
|
-
updater,
|
|
1641
|
-
(newValue) => assocPath(path, newValue, state)
|
|
1642
|
-
)(state);
|
|
1643
|
-
|
|
1644
|
-
// Usage in any framework:
|
|
1645
|
-
// Redux: return updateState(state, ['users', 0, 'name'], toUpper);
|
|
1646
|
-
// Zustand: set(updateState(get(), ['users', 0, 'name'], toUpper));
|
|
1647
|
-
// Pinia: this.$state = updateState(this.$state, ['users', 0, 'name'], toUpper);
|
|
1648
|
-
```
|
|
407
|
+
---
|
|
1649
408
|
|
|
1650
|
-
##
|
|
409
|
+
## Quick Signature Lookup (When Unsure)
|
|
1651
410
|
|
|
1652
|
-
|
|
411
|
+
Prefer local types first:
|
|
412
|
+
- Main types: `dist/index.d.ts`
|
|
413
|
+
- Stream types: `dist/stream/index.d.ts`
|
|
1653
414
|
|
|
1654
|
-
|
|
1655
|
-
-
|
|
1656
|
-
-
|
|
1657
|
-
- **Svelte**: `$: result = pipe(...)(data)`
|
|
1658
|
-
- **Solid**: `createMemo(() => pipe(...)(data()))`
|
|
415
|
+
If you’re in a consumer project:
|
|
416
|
+
- `node_modules/fp-pack/dist/index.d.ts`
|
|
417
|
+
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
1659
418
|
|
|
1660
|
-
|
|
1661
|
-
- **React**: `<button onClick={pipe(...)}>` or `const handler = pipe(...)`
|
|
1662
|
-
- **Vue**: `<button @click="pipe(...)">` or `const handler = pipe(...)`
|
|
1663
|
-
- **Svelte**: `<button on:click={pipe(...)}>` or `const handler = pipe(...)`
|
|
419
|
+
---
|
|
1664
420
|
|
|
1665
|
-
|
|
1666
|
-
- **React**: `useEffect(() => { pipeAsync(...)() }, [deps])`
|
|
1667
|
-
- **Vue**: `watchEffect(() => pipeAsync(...)())`
|
|
1668
|
-
- **Svelte**: `onMount(() => pipeAsync(...)())`
|
|
421
|
+
## Writing New Helpers (If Needed)
|
|
1669
422
|
|
|
1670
|
-
|
|
1671
|
-
- **
|
|
1672
|
-
- **
|
|
1673
|
-
-
|
|
423
|
+
If you add your own utilities that should compose well:
|
|
424
|
+
- Keep them **unary** for pipelines (or return a unary function via currying).
|
|
425
|
+
- Prefer **data-last** argument order.
|
|
426
|
+
- For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
|
|
1674
427
|
|
|
1675
|
-
|
|
428
|
+
---
|
|
1676
429
|
|
|
1677
430
|
## Summary
|
|
1678
431
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
1. **Default to `pipe`** for all data transformations
|
|
1682
|
-
2. **Switch to `pipeAsync`** when async operations are involved (use `pipeAsyncSideEffect` if SideEffect is in the flow)
|
|
1683
|
-
3. **Use `stream/*`** for lazy, memory-efficient processing
|
|
1684
|
-
4. **Handle errors with `SideEffect`** in `pipeSideEffect`/`pipeAsyncSideEffect`, not try-catch
|
|
1685
|
-
5. **Avoid imperative loops** - use fp-pack's declarative functions
|
|
1686
|
-
6. **Never suggest monads** - use SideEffect pattern instead
|
|
1687
|
-
7. **Keep code declarative** - describe what, not how
|
|
1688
|
-
8. **All logic inside pipe** - never break out of pipe chains for conditionals or loops
|
|
1689
|
-
9. **Use control flow functions** - `when`, `unless`, `ifElse`, `cond` instead of if/else/switch
|
|
1690
|
-
10. **Call `runPipeResult` OUTSIDE pipelines** - `runPipeResult` / `matchSideEffect` must be called outside `pipeSideEffect`/`pipeAsyncSideEffect` for proper type safety
|
|
1691
|
-
11. **Use `isSideEffect` for type narrowing** - get precise types in both success and error branches
|
|
1692
|
-
12. **Apply use-case patterns** - recognize scenarios (form handling, list processing, etc.) and apply appropriate fp-pack patterns
|
|
1693
|
-
13. **Framework-agnostic core** - write fp-pack logic independent of UI framework, only wrap at the boundaries
|
|
1694
|
-
14. **Library integration** - use pipe in select/resolver/action functions of popular libraries (React Query, Zustand, Pinia, etc.)
|
|
1695
|
-
|
|
1696
|
-
### Key Principles
|
|
1697
|
-
|
|
1698
|
-
**✅ DO: Keep everything in pipe**
|
|
1699
|
-
```typescript
|
|
1700
|
-
// GOOD: All logic inside pipe
|
|
1701
|
-
const handleSubmitPipeline = pipeAsyncSideEffect(
|
|
1702
|
-
getFormData,
|
|
1703
|
-
validateFields,
|
|
1704
|
-
when(isValid, submitToAPI),
|
|
1705
|
-
unless(isValid, showErrors)
|
|
1706
|
-
);
|
|
1707
|
-
|
|
1708
|
-
const handleSubmit = (form: HTMLFormElement) => runPipeResult(handleSubmitPipeline(form));
|
|
1709
|
-
```
|
|
1710
|
-
|
|
1711
|
-
**❌ DON'T: Break out of pipe for conditionals**
|
|
1712
|
-
```typescript
|
|
1713
|
-
// BAD: Breaking pipe for if/else
|
|
1714
|
-
const handleSubmit = pipe(
|
|
1715
|
-
getFormData,
|
|
1716
|
-
validateFields
|
|
1717
|
-
);
|
|
1718
|
-
const result = handleSubmit(form);
|
|
1719
|
-
if (result.isValid) { // ❌ Outside pipe
|
|
1720
|
-
submitToAPI(result);
|
|
1721
|
-
} else {
|
|
1722
|
-
showErrors(result.errors);
|
|
1723
|
-
}
|
|
1724
|
-
```
|
|
1725
|
-
|
|
1726
|
-
**✅ DO: Use when/cond/ifElse for branching**
|
|
1727
|
-
```typescript
|
|
1728
|
-
// GOOD: Branching inside pipe
|
|
1729
|
-
const processAction = pipe(
|
|
1730
|
-
cond([
|
|
1731
|
-
[(action) => action.type === 'CREATE', handleCreate],
|
|
1732
|
-
[(action) => action.type === 'UPDATE', handleUpdate],
|
|
1733
|
-
[(action) => action.type === 'DELETE', handleDelete],
|
|
1734
|
-
[() => true, handleDefault]
|
|
1735
|
-
])
|
|
1736
|
-
);
|
|
1737
|
-
```
|
|
1738
|
-
|
|
1739
|
-
**❌ DON'T: Use switch/if-else chains**
|
|
1740
|
-
```typescript
|
|
1741
|
-
// BAD: Imperative branching
|
|
1742
|
-
const processAction = (action) => {
|
|
1743
|
-
switch (action.type) { // ❌ Imperative
|
|
1744
|
-
case 'CREATE': return handleCreate(action);
|
|
1745
|
-
case 'UPDATE': return handleUpdate(action);
|
|
1746
|
-
case 'DELETE': return handleDelete(action);
|
|
1747
|
-
default: return handleDefault(action);
|
|
1748
|
-
}
|
|
1749
|
-
};
|
|
1750
|
-
```
|
|
1751
|
-
|
|
1752
|
-
Your goal is to write clean, readable, functional code that leverages fp-pack's full potential in real-world UI applications.
|
|
432
|
+
Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary (with generics if inference widens to `any`).
|