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