fp-pack 0.9.1 → 0.9.2

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