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