@pumped-fn/lite 1.3.0 → 1.4.0

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/README.md CHANGED
@@ -4,655 +4,228 @@ A lightweight effect system for TypeScript with managed lifecycles and minimal r
4
4
 
5
5
  **Zero dependencies** · **<17KB bundle** · **Full TypeScript support**
6
6
 
7
- ## What is an Effect System?
8
-
9
- An effect system manages **how** and **when** computations run, handling:
10
- - **Resource lifecycle** - acquire, use, release
11
- - **Computation ordering** - what depends on what
12
- - **Side effect isolation** - controlled execution boundaries
13
- - **State transitions** - idle → resolving → resolved → failed
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @pumped-fn/lite
19
- # or
20
- pnpm add @pumped-fn/lite
21
- # or
22
- yarn add @pumped-fn/lite
23
- ```
24
-
25
- ## Quick Start
26
-
27
- ```typescript
28
- import { atom, flow, createScope, tag, tags, controller } from '@pumped-fn/lite'
29
-
30
- // 1. Define atoms (long-lived, cached dependencies)
31
- const configAtom = atom({
32
- factory: () => ({ apiUrl: 'https://api.example.com', timeout: 5000 })
33
- })
34
-
35
- const apiClientAtom = atom({
36
- deps: { config: configAtom },
37
- factory: (ctx, { config }) => {
38
- const client = new ApiClient(config.apiUrl, config.timeout)
39
- ctx.cleanup(() => client.disconnect())
40
- return client
41
- }
42
- })
43
-
44
- // 2. Define flows (short-lived request handlers)
45
- const fetchUserFlow = flow({
46
- deps: { api: apiClientAtom },
47
- factory: async (ctx, { api }) => {
48
- const userId = ctx.input as string
49
- return api.getUser(userId)
50
- }
51
- })
52
-
53
- // 3. Create scope and execute
54
- const scope = createScope()
55
- await scope.ready
56
-
57
- const context = scope.createContext()
58
- const user = await context.exec({ flow: fetchUserFlow, input: 'user-123' })
59
- await context.close()
60
-
61
- // 4. Cleanup when done
62
- await scope.dispose()
63
- ```
64
-
65
- ## Core Concepts
7
+ ## How It Works
66
8
 
67
9
  ```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
- | Concept | Purpose |
79
- |---------|---------|
80
- | **Scope** | Long-lived boundary that manages atom lifecycles |
81
- | **Atom** | A managed effect with lifecycle (create, cache, cleanup, recreate) |
82
- | **Flow** | Template for short-lived operations with input/output |
83
- | **ExecutionContext** | Short-lived context for running flows with input and tags |
84
- | **Controller** | Handle for observing and controlling an atom's state |
85
- | **Tag** | Contextual value passed through execution |
86
-
87
- ## Atoms
88
-
89
- Atoms are long-lived dependencies that are cached within a scope.
10
+ sequenceDiagram
11
+ participant User
12
+ participant Scope
13
+ participant Atom
90
14
 
91
- ```mermaid
92
- flowchart TB
93
- subgraph Scope
94
- Config["configAtom"] --> API["apiClientAtom"]
95
- DB["dbAtom"] --> Repo["userRepoAtom"]
96
- API --> Service["userServiceAtom"]
97
- Repo --> Service
15
+ User->>Scope: createScope(options?)
16
+ Scope-->>User: scope
17
+ User->>Scope: await scope.ready
18
+
19
+ User->>Scope: scope.resolve(atom)
20
+ alt preset exists
21
+ Scope-->>User: preset value (factory skipped)
22
+ else no preset
23
+ Scope->>Atom: factory(ctx, deps)
24
+ Atom-->>Scope: value (cached)
25
+ Scope-->>User: value
98
26
  end
99
- ```
100
-
101
- ### Basic Atom
102
27
 
103
- ```typescript
104
- const dbAtom = atom({
105
- factory: async (ctx) => {
106
- const connection = await createConnection()
107
- ctx.cleanup(() => connection.close())
108
- return connection
109
- }
110
- })
111
- ```
112
-
113
- ### Atom with Dependencies
114
-
115
- ```typescript
116
- const userRepoAtom = atom({
117
- deps: { db: dbAtom },
118
- factory: (ctx, { db }) => new UserRepository(db)
119
- })
120
- ```
121
-
122
- ### Self-Invalidating Atom
123
-
124
- ```typescript
125
- const configAtom = atom({
126
- factory: async (ctx) => {
127
- const config = await fetchConfig()
128
-
129
- // Re-fetch every 60 seconds
130
- const interval = setInterval(() => ctx.invalidate(), 60_000)
131
- ctx.cleanup(() => clearInterval(interval))
132
-
133
- return config
134
- }
135
- })
28
+ User->>Scope: scope.dispose()
29
+ Scope->>Atom: run cleanups, release all
136
30
  ```
137
31
 
138
- ### Per-Atom Private Storage
139
-
140
- Use `ctx.data` to store data that survives invalidation:
32
+ ## Invalidation & Data Retention
141
33
 
142
- ```typescript
143
- const prevDataTag = tag<Data>({ label: 'prevData' })
144
-
145
- const pollingAtom = atom({
146
- factory: async (ctx) => {
147
- const prev = ctx.data.get(prevDataTag) // Data | undefined
148
- const current = await fetchData()
149
-
150
- if (prev && hasChanged(prev, current)) {
151
- notifyChanges(prev, current)
152
- }
153
-
154
- ctx.data.set(prevDataTag, current)
155
- setTimeout(() => ctx.invalidate(), 5000)
156
- return current
157
- }
158
- })
159
- ```
160
-
161
- With default values:
34
+ ```mermaid
35
+ sequenceDiagram
36
+ participant User
37
+ participant Controller
38
+ participant Atom
39
+ participant DataStore as ctx.data
162
40
 
163
- ```typescript
164
- const countTag = tag<number>({ label: 'count', default: 0 })
41
+ Note over DataStore: persists across invalidations
165
42
 
166
- const counterAtom = atom({
167
- factory: (ctx) => {
168
- const count = ctx.data.get(countTag) // number (guaranteed!)
169
- ctx.data.set(countTag, count + 1)
170
- return count
171
- }
172
- })
43
+ User->>Controller: ctrl.invalidate()
44
+ Controller->>Atom: run cleanups (LIFO)
45
+ Note over DataStore: retained
46
+ Controller->>Atom: state = resolving
47
+ Controller->>Atom: factory(ctx, deps)
48
+ Note right of Atom: ctx.data still has previous values
49
+ Atom-->>Controller: new value
50
+ Controller->>Atom: state = resolved
51
+ Controller-->>User: listeners notified
173
52
  ```
174
53
 
175
- ## Flows
176
-
177
- Flows are templates for short-lived operations.
54
+ ## Flow Execution
178
55
 
179
56
  ```mermaid
180
57
  sequenceDiagram
181
- participant Client
58
+ participant User
59
+ participant Scope
182
60
  participant Context as ExecutionContext
183
61
  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
196
-
197
- ```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)
203
- }
204
- })
205
- ```
206
-
207
- ### Flow with Parse Validation
208
-
209
- ```typescript
210
- const createUserFlow = flow({
211
- name: 'createUser',
212
- parse: (raw) => {
213
- const obj = raw as Record<string, unknown>
214
- 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 }
217
- },
218
- deps: { repo: userRepoAtom },
219
- factory: async (ctx, { repo }) => {
220
- // ctx.input is typed as { name: string; email: string }
221
- return repo.create(ctx.input)
222
- }
223
- })
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
-
228
- ### Executing Flows
229
62
 
230
- ```typescript
231
- const context = scope.createContext()
232
- const user = await context.exec({
233
- flow: createUserFlow,
234
- input: { name: 'Alice', email: 'alice@example.com' }
235
- })
236
- await context.close()
237
- ```
63
+ User->>Scope: scope.createContext(options?)
64
+ Scope-->>User: context
238
65
 
239
- ### Flow with Tags
66
+ User->>Context: ctx.exec({ flow, input, tags? })
67
+ Context->>Flow: parse(input)
68
+ Context->>Context: resolve flow deps
69
+ Context->>Flow: factory(ctx, deps)
70
+ Flow-->>Context: output
71
+ Context-->>User: output
240
72
 
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
- })
73
+ User->>Context: ctx.close()
74
+ Context->>Context: run onClose cleanups (LIFO)
259
75
  ```
260
76
 
261
- ## Controllers
262
-
263
- Controllers provide reactive access to atom state.
77
+ ## Tag Inheritance
264
78
 
265
79
  ```mermaid
266
- flowchart LR
267
- subgraph Controller
268
- State["state: idle|resolving|resolved|failed"]
269
- Get["get()"]
270
- Inv["invalidate()"]
80
+ flowchart TD
81
+ subgraph Scope["Scope: tags=[tenantId('t1')]"]
82
+ subgraph Context["Context: tags=[requestId('r1')]"]
83
+ subgraph Flow["Flow deps:"]
84
+ T1["tags.required(tenantId) → 't1'"]
85
+ T2["tags.required(requestId) → 'r1'"]
86
+ end
87
+ end
271
88
  end
272
89
 
273
- App -->|subscribe| Controller
274
- Controller -->|on 'resolved'| App
275
- Controller -->|on 'resolving'| App
276
- ```
277
-
278
- ### Basic Usage
279
-
280
- ```typescript
281
- const ctrl = scope.controller(configAtom)
282
-
283
- ctrl.state // 'idle' | 'resolving' | 'resolved' | 'failed'
284
- ctrl.get() // value (throws if not resolved)
285
- await ctrl.resolve() // resolve and wait
286
- ctrl.invalidate() // trigger re-resolution
90
+ Note["Inner inherits from outer. Override by passing same tag at inner level."]
287
91
  ```
288
92
 
289
- ### Subscribing to Changes
290
-
291
- ```typescript
292
- ctrl.on('resolved', () => console.log('Updated:', ctrl.get()))
293
- ctrl.on('resolving', () => console.log('Refreshing...'))
294
- ctrl.on('*', () => console.log('State:', ctrl.state))
295
- ```
296
-
297
- ### Controller as Dependency
298
-
299
- Use `controller()` to receive a Controller instead of the resolved value:
300
-
301
- ```typescript
302
- const appAtom = atom({
303
- deps: { config: controller(configAtom) },
304
- factory: (ctx, { config }) => {
305
- config.on('resolved', () => ctx.invalidate())
306
- return new App(config.get())
307
- }
308
- })
309
- ```
310
-
311
- ### Fine-Grained Reactivity
312
-
313
- Use `select()` to subscribe only when a derived value changes:
314
-
315
- ```typescript
316
- const portSelect = scope.select(configAtom, (c) => c.port)
317
- portSelect.subscribe(() => console.log('Port changed:', portSelect.get()))
318
- ```
319
-
320
- ## Tags
321
-
322
- Tags pass contextual values through execution without explicit wiring.
93
+ ## Controller Reactivity
323
94
 
324
95
  ```mermaid
325
- flowchart LR
326
- subgraph Definition
327
- T["tag({ label: 'tenantId' })"]
328
- end
329
-
330
- subgraph Usage
331
- R["tags.required(tag)"] --> |"throws if missing"| V1["string"]
332
- O["tags.optional(tag)"] --> |"undefined if missing"| V2["string | undefined"]
333
- A["tags.all(tag)"] --> |"collects all"| V3["string[]"]
334
- end
335
- ```
96
+ sequenceDiagram
97
+ participant User
98
+ participant Controller
99
+ participant Atom
336
100
 
337
- ### Creating Tags
101
+ User->>Controller: scope.controller(atom)
102
+ User->>Controller: ctrl.on('resolved', listener)
103
+ Controller-->>User: unsubscribe fn
338
104
 
339
- ```typescript
340
- const tenantIdTag = tag<string>({ label: 'tenantId' })
341
- const userRolesTag = tag<string[]>({ label: 'userRoles', default: [] })
342
-
343
- // With parse validation
344
- const userId = tag({
345
- label: 'userId',
346
- parse: (raw) => {
347
- if (typeof raw !== 'string') throw new Error('Must be string')
348
- return raw
349
- }
350
- })
351
-
352
- userId('abc-123') // OK
353
- userId(123) // Throws ParseError
354
- ```
105
+ Note over Controller: atom invalidated elsewhere
355
106
 
356
- ### Using Tags as Dependencies
107
+ Controller->>Atom: state = resolving
108
+ Controller-->>User: 'resolving' listeners fire
109
+ Atom-->>Controller: new value
110
+ Controller->>Atom: state = resolved
111
+ Controller-->>User: 'resolved' listeners fire
357
112
 
358
- ```typescript
359
- // Required - throws if not found
360
- const tenantAtom = atom({
361
- deps: { tenantId: tags.required(tenantIdTag) },
362
- factory: (ctx, { tenantId }) => loadTenantData(tenantId)
363
- })
364
-
365
- // Optional - undefined if not found
366
- const optionalAtom = atom({
367
- deps: { tenantId: tags.optional(tenantIdTag) },
368
- factory: (ctx, { tenantId }) => {
369
- if (tenantId) {
370
- return loadTenantData(tenantId)
371
- }
372
- return loadDefaultData()
373
- }
374
- })
375
-
376
- // Collect all - returns array of all matching tags
377
- const multiAtom = atom({
378
- deps: { roles: tags.all(userRolesTag) },
379
- factory: (ctx, { roles }) => roles.flat() // string[][]
380
- })
113
+ User->>Controller: ctrl.get()
114
+ Controller-->>User: current value
381
115
  ```
382
116
 
383
- ### Passing Tags
117
+ ## Primitives
384
118
 
385
- Tags can be passed at different levels, with inner levels inheriting from outer:
119
+ ### Scope
386
120
 
387
- ```mermaid
388
- flowchart LR
389
- subgraph Scope["createScope()"]
390
- ST["tenantId: 'tenant-123'"]
391
- subgraph Context["createContext()"]
392
- CT["userRoles: ['admin']"]
393
- subgraph Exec["exec()"]
394
- ET["requestId: 'req-456'"]
395
- end
396
- end
397
- end
121
+ Entry point. Manages atom lifecycles, caching, and cleanup orchestration.
398
122
 
399
- ST -.->|inherited| CT
400
- CT -.->|inherited| ET
401
- ```
123
+ - `createScope(options?)` — create with optional extensions, presets, tags
124
+ - `scope.ready` — wait for extension initialization
125
+ - `scope.resolve(atom)` — resolve and cache
126
+ - `scope.controller(atom)` — get reactive handle
127
+ - `scope.release(atom)` — run cleanups, remove from cache
128
+ - `scope.dispose()` — release all, cleanup extensions
129
+ - `scope.createContext(options?)` — create execution context for flows
130
+ - `scope.select(atom, selector)` — fine-grained reactivity
131
+ - `scope.flush()` — wait for pending invalidations
402
132
 
403
- All three tag levels are available during flow execution.
133
+ ### Atom
404
134
 
405
- ### Direct Tag Methods
135
+ Long-lived cached dependency with lifecycle.
406
136
 
407
- ```typescript
408
- const tags = [tenantIdTag('tenant-123'), userRolesTag(['admin'])]
137
+ - Dependencies on other atoms via `deps`
138
+ - `ctx.cleanup(fn)` runs on invalidate and release (LIFO order)
139
+ - `ctx.invalidate()` — schedule re-resolution
140
+ - `ctx.data` — storage that survives invalidation (cleared on release)
141
+ - `ctx.data.getOrSet(tag, defaultValue)` — initialize and retrieve in one call
409
142
 
410
- // Get (throws if not found for tags without default)
411
- const tenantId = tenantIdTag.get(tags)
143
+ ### Flow
412
144
 
413
- // Find (returns undefined if not found)
414
- const roles = userRolesTag.find(tags)
145
+ Short-lived operation with input/output.
415
146
 
416
- // Collect all matching values
417
- const allRoles = userRolesTag.collect(tags)
418
- ```
147
+ - `parse` validate/transform input before factory (throws `ParseError` on failure)
148
+ - `typed<T>()` type marker without runtime parsing
149
+ - Dependencies on atoms via `deps`
150
+ - `ctx.input` — typed input access
151
+ - `ctx.onClose(fn)` — cleanup when context closes
419
152
 
420
- ## Presets
153
+ ### Tag
421
154
 
422
- Presets inject or redirect atom values, useful for testing.
155
+ Contextual value passed through execution without explicit wiring.
423
156
 
424
- ```mermaid
425
- flowchart LR
426
- subgraph Normal["Normal Resolution"]
427
- A1[resolve dbAtom] --> F1[factory]
428
- F1 --> V1[real connection]
429
- end
157
+ - `tag({ label, default?, parse? })` — define with optional default and validation
158
+ - `tags.required(tag)` — dependency that throws if missing
159
+ - `tags.optional(tag)` — dependency that returns undefined if missing
160
+ - `tags.all(tag)` collects all values from inheritance chain
161
+ - Tags inherit: Scope → Context → exec call
430
162
 
431
- subgraph Preset["With Preset"]
432
- A2[resolve dbAtom] --> P[preset check]
433
- P -->|value| V2[mockDb]
434
- P -->|atom| F2[testAtom.factory]
435
- end
436
- ```
163
+ ### Controller
437
164
 
438
- **Value injection** bypasses the factory entirely:
439
- ```typescript
440
- const scope = createScope({
441
- presets: [preset(dbAtom, mockDb)]
442
- })
443
- ```
165
+ Reactive handle for observing and controlling atom state.
444
166
 
445
- **Atom redirection** uses another atom's factory:
446
- ```typescript
447
- const scope = createScope({
448
- presets: [preset(configAtom, testConfigAtom)]
449
- })
450
- ```
167
+ - `ctrl.state` sync access: `'idle' | 'resolving' | 'resolved' | 'failed'`
168
+ - `ctrl.get()` — sync value access (throws if not resolved, returns stale during resolving)
169
+ - `ctrl.resolve()` async resolution
170
+ - `ctrl.invalidate()` — trigger re-resolution (runs factory)
171
+ - `ctrl.set(value)` — replace value directly (skips factory)
172
+ - `ctrl.update(fn)` — transform value: `fn(currentValue) → newValue` (skips factory)
173
+ - `ctrl.on(event, listener)` — subscribe to `'resolved' | 'resolving' | '*'`
174
+ - Use `controller(atom)` in deps for reactive dependency (unresolved, you control timing)
451
175
 
452
- ## Extensions
176
+ ### Preset
453
177
 
454
- Extensions provide cross-cutting behavior via AOP-style hooks.
178
+ Value injection for testing. Bypasses factory entirely.
455
179
 
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
- ```
180
+ - `preset(atom, value)` — inject direct value
181
+ - `preset(atom, otherAtom)` — redirect to another atom's factory
182
+ - Pass via `createScope({ presets: [...] })`
474
183
 
475
- ### Extension Interface
184
+ ### Extension
476
185
 
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
- ```
186
+ AOP-style middleware for cross-cutting concerns.
486
187
 
487
- ### Example: Timing Extension
188
+ - `init(scope)` setup when scope created
189
+ - `wrapResolve(next, atom, scope)` — intercept atom resolution
190
+ - `wrapExec(next, target, ctx)` — intercept flow execution
191
+ - `dispose(scope)` — cleanup when scope disposed
192
+ - Pass via `createScope({ extensions: [...] })`
488
193
 
489
- ```typescript
490
- const timingExtension: Lite.Extension = {
491
- name: 'timing',
492
-
493
- wrapResolve: async (next, atom, scope) => {
494
- const start = performance.now()
495
- const result = await next()
496
- console.log(`Resolved in ${performance.now() - start}ms`)
497
- return result
498
- }
499
- }
500
-
501
- const scope = createScope({ extensions: [timingExtension] })
502
- ```
194
+ ## Full API
503
195
 
504
- ## Lifecycle
196
+ See [`dist/index.d.mts`](./dist/index.d.mts) for complete type definitions.
505
197
 
506
- ### Effect Lifecycle
198
+ All types available under the `Lite` namespace:
507
199
 
508
- ```mermaid
509
- stateDiagram-v2
510
- [*] --> idle
511
- idle --> resolving: resolve()
512
- resolving --> resolved: success
513
- resolving --> failed: error
514
- resolved --> resolving: invalidate()
515
- failed --> resolving: invalidate()
516
- resolved --> idle: release()
517
- failed --> idle: release()
200
+ ```typescript
201
+ import type { Lite } from '@pumped-fn/lite'
518
202
  ```
519
203
 
520
- ### Resolution Flow
204
+ ## Edge Cases
521
205
 
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
206
+ ### Controller.set() / update()
540
207
 
541
- Atom-->>App: value
542
- ```
208
+ | State | Behavior |
209
+ |-------|----------|
210
+ | `idle` | Throws "Atom not resolved" |
211
+ | `resolving` | Queues, applies after resolution completes |
212
+ | `resolved` | Queues normally |
213
+ | `failed` | Throws the stored error |
543
214
 
544
- ### Invalidation Flow
215
+ Both run cleanups before applying the new value.
545
216
 
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
- ```
217
+ ### DataStore.get()
568
218
 
569
- ## API Reference
570
-
571
- ### Factory Functions
572
-
573
- | Function | Description |
574
- |----------|-------------|
575
- | `createScope(options?)` | Create DI container (returns Scope with `ready` promise) |
576
- | `atom(config)` | Define long-lived cached dependency |
577
- | `flow(config)` | Define short-lived operation template (optional `name`, `parse`) |
578
- | `tag(config)` | Define contextual value (optional `parse` for validation) |
579
- | `controller(atom)` | Create controller dependency helper |
580
- | `preset(atom, value)` | Create value injection preset |
581
-
582
- ### Scope Methods
583
-
584
- | Method | Description |
585
- |--------|-------------|
586
- | `scope.ready` | Promise that resolves when extensions are initialized |
587
- | `scope.resolve(atom)` | Resolve atom and return cached value |
588
- | `scope.controller(atom)` | Get Controller for atom |
589
- | `scope.select(atom, selector, options?)` | Create fine-grained subscription |
590
- | `scope.release(atom)` | Release atom (run cleanups, remove from cache) |
591
- | `scope.dispose()` | Dispose scope (release all atoms, cleanup extensions) |
592
- | `scope.createContext(options?)` | Create ExecutionContext for flows |
593
- | `scope.on(event, atom, listener)` | Subscribe to atom state changes |
594
-
595
- ### Controller Methods
596
-
597
- | Method | Description |
598
- |--------|-------------|
599
- | `ctrl.state` | Current state: `'idle'` \| `'resolving'` \| `'resolved'` \| `'failed'` |
600
- | `ctrl.get()` | Get resolved value (throws if not resolved) |
601
- | `ctrl.resolve()` | Resolve and return value |
602
- | `ctrl.release()` | Release atom |
603
- | `ctrl.invalidate()` | Trigger re-resolution |
604
- | `ctrl.on(event, listener)` | Subscribe: `'resolved'` \| `'resolving'` \| `'*'` |
605
-
606
- ### ExecutionContext Methods
607
-
608
- | Method | Description |
609
- |--------|-------------|
610
- | `ctx.input` | Current execution input |
611
- | `ctx.scope` | Parent scope |
612
- | `ctx.exec(options)` | Execute flow or function |
613
- | `ctx.onClose(fn)` | Register cleanup for context close |
614
- | `ctx.close()` | Close context and run cleanups |
615
-
616
- ### ResolveContext Methods
617
-
618
- | Method | Description |
619
- |--------|-------------|
620
- | `ctx.cleanup(fn)` | Register cleanup for atom invalidation/release |
621
- | `ctx.invalidate()` | Schedule self-invalidation |
622
- | `ctx.scope` | Parent scope |
623
- | `ctx.data` | Per-atom DataStore (survives invalidation) |
624
-
625
- ### Type Guards
626
-
627
- | Function | Description |
628
- |----------|-------------|
629
- | `isAtom(value)` | Check if value is Atom |
630
- | `isFlow(value)` | Check if value is Flow |
631
- | `isTag(value)` | Check if value is Tag |
632
- | `isTagged(value)` | Check if value is Tagged |
633
- | `isPreset(value)` | Check if value is Preset |
634
- | `isControllerDep(value)` | Check if value is ControllerDep |
635
-
636
- ### Types
637
-
638
- All types are available under the `Lite` namespace:
219
+ `ctx.data.get(tag)` always returns `T | undefined` (Map-like semantics). Use `getOrSet(tag)` when you need the tag's default value.
639
220
 
640
221
  ```typescript
641
- import type { Lite } from '@pumped-fn/lite'
222
+ const countTag = tag<number>({ label: 'count', default: 0 })
642
223
 
643
- const myAtom: Lite.Atom<Config> = atom({ factory: () => loadConfig() })
644
- const myController: Lite.Controller<Config> = scope.controller(myAtom)
645
- const myTag: Lite.Tag<string> = tag({ label: 'myTag' })
224
+ ctx.data.get(countTag) // undefined (not stored)
225
+ ctx.data.getOrSet(countTag) // 0 (uses default, now stored)
226
+ ctx.data.get(countTag) // 0 (now stored)
646
227
  ```
647
228
 
648
- ## Design Principles
649
-
650
- 1. **Minimal API** - Every export is expensive to learn
651
- 2. **Zero dependencies** - No runtime dependencies
652
- 3. **Explicit lifecycle** - No magic, clear state transitions
653
- 4. **Composable** - Effects compose through deps
654
- 5. **Type-safe** - Full TypeScript inference
655
-
656
229
  ## License
657
230
 
658
231
  MIT