@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.
- package/CHANGELOG.md +23 -0
- package/README.md +49 -173
- 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
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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'
|
|
208
|
+
input: { name: 'Alice' }
|
|
235
209
|
})
|
|
236
210
|
await context.close()
|
|
237
211
|
```
|
|
238
212
|
|
|
239
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
422
|
+
Interface: `{ name, init?, wrapResolve?, wrapExec?, dispose? }`
|
|
505
423
|
|
|
506
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|