@pumped-fn/lite 1.3.0 → 1.3.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +49 -173
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 1.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3208cfe: Improve README documentation clarity and reduce size by 19%
8
+
9
+ **Enhanced API behavior documentation:**
10
+
11
+ - `ctx.cleanup()`: Clarified lifecycle - runs on every invalidation (before re-resolution) and release, LIFO order
12
+ - `ctx.data`: Clarified lifecycle - persists across invalidations, cleared on release, per-atom isolation
13
+ - `controller(atom)` as dep: Explained key difference - receives unresolved controller vs auto-resolved value
14
+ - `ctx.invalidate()`: Explained scheduling behavior - runs after factory completes, not interrupting
15
+ - `ctrl.get()`: Documented stale reads during resolving state
16
+ - `scope.flush()`: Added to API Reference (was undocumented)
17
+
18
+ **Trimmed content:**
19
+
20
+ - Removed duplicate Core Concepts diagram
21
+ - Condensed Flow section
22
+ - Condensed Extensions section
23
+ - Consolidated Lifecycle diagrams
24
+ - Removed rarely-used Direct Tag Methods section
25
+
3
26
  ## 1.3.0
4
27
 
5
28
  ### Minor Changes
package/README.md CHANGED
@@ -64,17 +64,6 @@ await scope.dispose()
64
64
 
65
65
  ## Core Concepts
66
66
 
67
- ```mermaid
68
- graph TB
69
- subgraph Scope["Scope (long-lived execution boundary)"]
70
- A1["Atom (effect)"] --- A2["Atom (effect)"]
71
- A2 --- A3["Atom (effect)"]
72
- A1 --> EC
73
- A3 --> EC
74
- EC["ExecutionContext<br/>(short-lived operation with input, tags, cleanup)"]
75
- end
76
- ```
77
-
78
67
  | Concept | Purpose |
79
68
  |---------|---------|
80
69
  | **Scope** | Long-lived boundary that manages atom lifecycles |
@@ -110,6 +99,8 @@ const dbAtom = atom({
110
99
  })
111
100
  ```
112
101
 
102
+ **`ctx.cleanup()` lifecycle:** Runs on every `invalidate()` (before re-resolution) and on `release()`. Cleanups execute in LIFO order. For resources that should survive invalidation, use `ctx.data` instead.
103
+
113
104
  ### Atom with Dependencies
114
105
 
115
106
  ```typescript
@@ -135,6 +126,8 @@ const configAtom = atom({
135
126
  })
136
127
  ```
137
128
 
129
+ **`ctx.invalidate()` behavior:** Schedules re-resolution after the current factory completes — does not interrupt execution. Use `scope.flush()` in tests to wait for pending invalidations.
130
+
138
131
  ### Per-Atom Private Storage
139
132
 
140
133
  Use `ctx.data` to store data that survives invalidation:
@@ -172,39 +165,27 @@ const counterAtom = atom({
172
165
  })
173
166
  ```
174
167
 
175
- ## Flows
176
-
177
- Flows are templates for short-lived operations.
178
-
179
- ```mermaid
180
- sequenceDiagram
181
- participant Client
182
- participant Context as ExecutionContext
183
- participant Flow
184
- participant Atoms
185
-
186
- Client->>Context: exec({ flow, input })
187
- Context->>Atoms: resolve dependencies
188
- Atoms-->>Context: deps
189
- Context->>Flow: factory(ctx, deps)
190
- Flow-->>Context: result
191
- Context-->>Client: result
192
- Client->>Context: close()
193
- ```
194
-
195
- ### Basic Flow
168
+ Use `getOrSet()` to initialize and retrieve in one call:
196
169
 
197
170
  ```typescript
198
- const createUserFlow = flow({
199
- deps: { repo: userRepoAtom },
200
- factory: async (ctx, { repo }) => {
201
- const input = ctx.input as CreateUserInput
202
- return repo.create(input)
171
+ const cacheTag = tag<Map<string, Result>>({ label: 'cache' })
172
+
173
+ const cachedAtom = atom({
174
+ factory: (ctx) => {
175
+ const cache = ctx.data.getOrSet(cacheTag, new Map())
176
+ return fetchWithCache(cache)
203
177
  }
204
178
  })
205
179
  ```
206
180
 
207
- ### Flow with Parse Validation
181
+ **`ctx.data` lifecycle:**
182
+ - **Persists** across `invalidate()` cycles
183
+ - **Cleared** on `release()` or `scope.dispose()`
184
+ - Each atom has independent storage (same tag, different atoms = separate data)
185
+
186
+ ## Flows
187
+
188
+ Flows are templates for short-lived operations with input validation.
208
189
 
209
190
  ```typescript
210
191
  const createUserFlow = flow({
@@ -212,51 +193,24 @@ const createUserFlow = flow({
212
193
  parse: (raw) => {
213
194
  const obj = raw as Record<string, unknown>
214
195
  if (typeof obj.name !== 'string') throw new Error('name required')
215
- if (typeof obj.email !== 'string') throw new Error('email required')
216
- return { name: obj.name, email: obj.email }
196
+ return { name: obj.name }
217
197
  },
218
198
  deps: { repo: userRepoAtom },
219
199
  factory: async (ctx, { repo }) => {
220
- // ctx.input is typed as { name: string; email: string }
221
- return repo.create(ctx.input)
200
+ return repo.create(ctx.input) // ctx.input typed from parse
222
201
  }
223
202
  })
224
- ```
225
-
226
- Parse runs before the factory, and `ctx.input` type is inferred from the parse return type. On validation failure, throws `ParseError` with phase `'flow-input'`.
227
203
 
228
- ### Executing Flows
229
-
230
- ```typescript
204
+ // Execute
231
205
  const context = scope.createContext()
232
206
  const user = await context.exec({
233
207
  flow: createUserFlow,
234
- input: { name: 'Alice', email: 'alice@example.com' }
208
+ input: { name: 'Alice' }
235
209
  })
236
210
  await context.close()
237
211
  ```
238
212
 
239
- ### Flow with Tags
240
-
241
- ```typescript
242
- const requestIdTag = tag<string>({ label: 'requestId' })
243
-
244
- const loggingFlow = flow({
245
- deps: { requestId: tags.required(requestIdTag) },
246
- factory: (ctx, { requestId }) => {
247
- console.log(`[${requestId}] Processing request`)
248
- return processRequest(ctx.input)
249
- }
250
- })
251
-
252
- // Pass tags at execution time
253
- const context = scope.createContext()
254
- await context.exec({
255
- flow: loggingFlow,
256
- input: data,
257
- tags: [requestIdTag('req-abc-123')]
258
- })
259
- ```
213
+ Parse runs before factory. On failure, throws `ParseError`.
260
214
 
261
215
  ## Controllers
262
216
 
@@ -286,6 +240,8 @@ await ctrl.resolve() // resolve and wait
286
240
  ctrl.invalidate() // trigger re-resolution
287
241
  ```
288
242
 
243
+ **Stale reads:** During `'resolving'` state, `ctrl.get()` returns the previous value. This enables optimistic UI patterns.
244
+
289
245
  ### Subscribing to Changes
290
246
 
291
247
  ```typescript
@@ -296,18 +252,29 @@ ctrl.on('*', () => console.log('State:', ctrl.state))
296
252
 
297
253
  ### Controller as Dependency
298
254
 
299
- Use `controller()` to receive a Controller instead of the resolved value:
255
+ Use `controller()` when you need reactive access to an atom's state, not just its value.
256
+
257
+ **Key difference:**
258
+ - Regular dep `{ x: atom }` — auto-resolved, you receive the value
259
+ - Controller dep `{ x: controller(atom) }` — **unresolved**, you receive a reactive handle
300
260
 
301
261
  ```typescript
302
262
  const appAtom = atom({
303
263
  deps: { config: controller(configAtom) },
304
- factory: (ctx, { config }) => {
305
- config.on('resolved', () => ctx.invalidate())
264
+ factory: async (ctx, { config }) => {
265
+ await config.resolve() // Must resolve manually
266
+
267
+ // Subscribe to upstream changes
268
+ const unsub = config.on('resolved', () => ctx.invalidate())
269
+ ctx.cleanup(unsub)
270
+
306
271
  return new App(config.get())
307
272
  }
308
273
  })
309
274
  ```
310
275
 
276
+ **When to use:** React to upstream invalidations, conditional/lazy resolution, access atom state.
277
+
311
278
  ### Fine-Grained Reactivity
312
279
 
313
280
  Use `select()` to subscribe only when a derived value changes:
@@ -402,21 +369,6 @@ flowchart LR
402
369
 
403
370
  All three tag levels are available during flow execution.
404
371
 
405
- ### Direct Tag Methods
406
-
407
- ```typescript
408
- const tags = [tenantIdTag('tenant-123'), userRolesTag(['admin'])]
409
-
410
- // Get (throws if not found for tags without default)
411
- const tenantId = tenantIdTag.get(tags)
412
-
413
- // Find (returns undefined if not found)
414
- const roles = userRolesTag.find(tags)
415
-
416
- // Collect all matching values
417
- const allRoles = userRolesTag.collect(tags)
418
- ```
419
-
420
372
  ## Presets
421
373
 
422
374
  Presets inject or redirect atom values, useful for testing.
@@ -451,45 +403,11 @@ const scope = createScope({
451
403
 
452
404
  ## Extensions
453
405
 
454
- Extensions provide cross-cutting behavior via AOP-style hooks.
455
-
456
- ```mermaid
457
- sequenceDiagram
458
- participant App
459
- participant Ext1 as Extension 1
460
- participant Ext2 as Extension 2
461
- participant Core as Core Logic
462
-
463
- App->>Ext1: resolve(atom)
464
- Ext1->>Ext1: before logic
465
- Ext1->>Ext2: next()
466
- Ext2->>Ext2: before logic
467
- Ext2->>Core: next()
468
- Core-->>Ext2: value
469
- Ext2->>Ext2: after logic
470
- Ext2-->>Ext1: value
471
- Ext1->>Ext1: after logic
472
- Ext1-->>App: value
473
- ```
474
-
475
- ### Extension Interface
476
-
477
- ```typescript
478
- interface Extension {
479
- readonly name: string
480
- init?(scope: Scope): MaybePromise<void>
481
- wrapResolve?<T>(next: () => Promise<T>, atom: Atom<T>, scope: Scope): Promise<T>
482
- wrapExec?<T>(next: () => Promise<T>, target: Flow | Function, ctx: ExecutionContext): Promise<T>
483
- dispose?(scope: Scope): MaybePromise<void>
484
- }
485
- ```
486
-
487
- ### Example: Timing Extension
406
+ Extensions wrap atom resolution and flow execution (AOP-style middleware).
488
407
 
489
408
  ```typescript
490
409
  const timingExtension: Lite.Extension = {
491
410
  name: 'timing',
492
-
493
411
  wrapResolve: async (next, atom, scope) => {
494
412
  const start = performance.now()
495
413
  const result = await next()
@@ -501,9 +419,9 @@ const timingExtension: Lite.Extension = {
501
419
  const scope = createScope({ extensions: [timingExtension] })
502
420
  ```
503
421
 
504
- ## Lifecycle
422
+ Interface: `{ name, init?, wrapResolve?, wrapExec?, dispose? }`
505
423
 
506
- ### Effect Lifecycle
424
+ ## Lifecycle
507
425
 
508
426
  ```mermaid
509
427
  stateDiagram-v2
@@ -517,54 +435,11 @@ stateDiagram-v2
517
435
  failed --> idle: release()
518
436
  ```
519
437
 
520
- ### Resolution Flow
521
-
522
- ```mermaid
523
- sequenceDiagram
524
- participant App
525
- participant Scope
526
- participant Atom
527
- participant Factory
528
-
529
- App->>Scope: resolve(atom)
530
- Scope->>Atom: check state
531
-
532
- alt idle
533
- Scope->>Atom: state = resolving
534
- Scope->>Factory: factory(ctx, deps)
535
- Factory-->>Scope: value
536
- Scope->>Atom: state = resolved
537
- else resolved
538
- Atom-->>App: cached value
539
- end
540
-
541
- Atom-->>App: value
542
- ```
543
-
544
- ### Invalidation Flow
545
-
546
- ```mermaid
547
- sequenceDiagram
548
- participant App
549
- participant Controller
550
- participant Scope
551
- participant Queue
552
- participant Factory
553
-
554
- App->>Controller: invalidate()
555
- Controller->>Scope: queue invalidation
556
- Scope->>Queue: add atom
557
-
558
- Note over Queue: queueMicrotask (batched)
559
-
560
- Queue->>Scope: flush
561
- Scope->>Scope: run cleanups (LIFO)
562
- Scope->>Scope: state = resolving
563
- Scope->>Factory: factory(ctx, deps)
564
- Factory-->>Scope: new value
565
- Scope->>Scope: state = resolved
566
- Scope->>Scope: notify listeners
567
- ```
438
+ **Invalidation sequence:**
439
+ 1. `invalidate()` → queued (microtask batched)
440
+ 2. Cleanups run (LIFO)
441
+ 3. State = resolving → factory()
442
+ 4. State = resolved → listeners notified
568
443
 
569
444
  ## API Reference
570
445
 
@@ -591,6 +466,7 @@ sequenceDiagram
591
466
  | `scope.dispose()` | Dispose scope (release all atoms, cleanup extensions) |
592
467
  | `scope.createContext(options?)` | Create ExecutionContext for flows |
593
468
  | `scope.on(event, atom, listener)` | Subscribe to atom state changes |
469
+ | `scope.flush()` | Wait for pending invalidation queue to process |
594
470
 
595
471
  ### Controller Methods
596
472
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pumped-fn/lite",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Lightweight dependency injection with minimal reactivity",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",