@pumped-fn/lite 1.3.1 → 1.4.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/README.md CHANGED
@@ -4,531 +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
66
-
67
- | Concept | Purpose |
68
- |---------|---------|
69
- | **Scope** | Long-lived boundary that manages atom lifecycles |
70
- | **Atom** | A managed effect with lifecycle (create, cache, cleanup, recreate) |
71
- | **Flow** | Template for short-lived operations with input/output |
72
- | **ExecutionContext** | Short-lived context for running flows with input and tags |
73
- | **Controller** | Handle for observing and controlling an atom's state |
74
- | **Tag** | Contextual value passed through execution |
75
-
76
- ## Atoms
77
-
78
- Atoms are long-lived dependencies that are cached within a scope.
7
+ ## How It Works
79
8
 
80
9
  ```mermaid
81
- flowchart TB
82
- subgraph Scope
83
- Config["configAtom"] --> API["apiClientAtom"]
84
- DB["dbAtom"] --> Repo["userRepoAtom"]
85
- API --> Service["userServiceAtom"]
86
- Repo --> Service
10
+ sequenceDiagram
11
+ participant User
12
+ participant Scope
13
+ participant Atom
14
+
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
87
26
  end
88
- ```
89
27
 
90
- ### Basic Atom
91
-
92
- ```typescript
93
- const dbAtom = atom({
94
- factory: async (ctx) => {
95
- const connection = await createConnection()
96
- ctx.cleanup(() => connection.close())
97
- return connection
98
- }
99
- })
100
- ```
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
-
104
- ### Atom with Dependencies
105
-
106
- ```typescript
107
- const userRepoAtom = atom({
108
- deps: { db: dbAtom },
109
- factory: (ctx, { db }) => new UserRepository(db)
110
- })
28
+ User->>Scope: scope.dispose()
29
+ Scope->>Atom: run cleanups, release all
111
30
  ```
112
31
 
113
- ### Self-Invalidating Atom
114
-
115
- ```typescript
116
- const configAtom = atom({
117
- factory: async (ctx) => {
118
- const config = await fetchConfig()
119
-
120
- // Re-fetch every 60 seconds
121
- const interval = setInterval(() => ctx.invalidate(), 60_000)
122
- ctx.cleanup(() => clearInterval(interval))
32
+ ## Invalidation & Data Retention
123
33
 
124
- return config
125
- }
126
- })
127
- ```
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
-
131
- ### Per-Atom Private Storage
34
+ ```mermaid
35
+ sequenceDiagram
36
+ participant User
37
+ participant Controller
38
+ participant Atom
39
+ participant DataStore as ctx.data
132
40
 
133
- Use `ctx.data` to store data that survives invalidation:
41
+ Note over DataStore: persists across invalidations
134
42
 
135
- ```typescript
136
- const prevDataTag = tag<Data>({ label: 'prevData' })
137
-
138
- const pollingAtom = atom({
139
- factory: async (ctx) => {
140
- const prev = ctx.data.get(prevDataTag) // Data | undefined
141
- const current = await fetchData()
142
-
143
- if (prev && hasChanged(prev, current)) {
144
- notifyChanges(prev, current)
145
- }
146
-
147
- ctx.data.set(prevDataTag, current)
148
- setTimeout(() => ctx.invalidate(), 5000)
149
- return current
150
- }
151
- })
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
152
52
  ```
153
53
 
154
- With default values:
155
-
156
- ```typescript
157
- const countTag = tag<number>({ label: 'count', default: 0 })
158
-
159
- const counterAtom = atom({
160
- factory: (ctx) => {
161
- const count = ctx.data.get(countTag) // number (guaranteed!)
162
- ctx.data.set(countTag, count + 1)
163
- return count
164
- }
165
- })
166
- ```
54
+ ## Flow Execution
167
55
 
168
- Use `getOrSet()` to initialize and retrieve in one call:
169
-
170
- ```typescript
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)
177
- }
178
- })
179
- ```
180
-
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)
56
+ ```mermaid
57
+ sequenceDiagram
58
+ participant User
59
+ participant Scope
60
+ participant Context as ExecutionContext
61
+ participant Flow
185
62
 
186
- ## Flows
63
+ User->>Scope: scope.createContext(options?)
64
+ Scope-->>User: context
187
65
 
188
- Flows are templates for short-lived operations with input validation.
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
189
72
 
190
- ```typescript
191
- const createUserFlow = flow({
192
- name: 'createUser',
193
- parse: (raw) => {
194
- const obj = raw as Record<string, unknown>
195
- if (typeof obj.name !== 'string') throw new Error('name required')
196
- return { name: obj.name }
197
- },
198
- deps: { repo: userRepoAtom },
199
- factory: async (ctx, { repo }) => {
200
- return repo.create(ctx.input) // ctx.input typed from parse
201
- }
202
- })
203
-
204
- // Execute
205
- const context = scope.createContext()
206
- const user = await context.exec({
207
- flow: createUserFlow,
208
- input: { name: 'Alice' }
209
- })
210
- await context.close()
73
+ User->>Context: ctx.close()
74
+ Context->>Context: run onClose cleanups (LIFO)
211
75
  ```
212
76
 
213
- Parse runs before factory. On failure, throws `ParseError`.
214
-
215
- ## Controllers
216
-
217
- Controllers provide reactive access to atom state.
77
+ ## Tag Inheritance
218
78
 
219
79
  ```mermaid
220
- flowchart LR
221
- subgraph Controller
222
- State["state: idle|resolving|resolved|failed"]
223
- Get["get()"]
224
- 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
225
88
  end
226
89
 
227
- App -->|subscribe| Controller
228
- Controller -->|on 'resolved'| App
229
- Controller -->|on 'resolving'| App
90
+ Note["Inner inherits from outer. Override by passing same tag at inner level."]
230
91
  ```
231
92
 
232
- ### Basic Usage
93
+ ## Controller Reactivity
233
94
 
234
- ```typescript
235
- const ctrl = scope.controller(configAtom)
95
+ ```mermaid
96
+ sequenceDiagram
97
+ participant User
98
+ participant Controller
99
+ participant Atom
236
100
 
237
- ctrl.state // 'idle' | 'resolving' | 'resolved' | 'failed'
238
- ctrl.get() // value (throws if not resolved)
239
- await ctrl.resolve() // resolve and wait
240
- ctrl.invalidate() // trigger re-resolution
241
- ```
101
+ User->>Controller: scope.controller(atom)
102
+ User->>Controller: ctrl.on('resolved', listener)
103
+ Controller-->>User: unsubscribe fn
242
104
 
243
- **Stale reads:** During `'resolving'` state, `ctrl.get()` returns the previous value. This enables optimistic UI patterns.
105
+ Note over Controller: atom invalidated elsewhere
244
106
 
245
- ### Subscribing to Changes
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
246
112
 
247
- ```typescript
248
- ctrl.on('resolved', () => console.log('Updated:', ctrl.get()))
249
- ctrl.on('resolving', () => console.log('Refreshing...'))
250
- ctrl.on('*', () => console.log('State:', ctrl.state))
113
+ User->>Controller: ctrl.get()
114
+ Controller-->>User: current value
251
115
  ```
252
116
 
253
- ### Controller as Dependency
117
+ ## Primitives
254
118
 
255
- Use `controller()` when you need reactive access to an atom's state, not just its value.
119
+ ### Scope
256
120
 
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
121
+ Entry point. Manages atom lifecycles, caching, and cleanup orchestration.
260
122
 
261
- ```typescript
262
- const appAtom = atom({
263
- deps: { config: controller(configAtom) },
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
-
271
- return new App(config.get())
272
- }
273
- })
274
- ```
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
275
132
 
276
- **When to use:** React to upstream invalidations, conditional/lazy resolution, access atom state.
133
+ ### Atom
277
134
 
278
- ### Fine-Grained Reactivity
135
+ Long-lived cached dependency with lifecycle.
279
136
 
280
- Use `select()` to subscribe only when a derived value changes:
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
281
142
 
282
- ```typescript
283
- const portSelect = scope.select(configAtom, (c) => c.port)
284
- portSelect.subscribe(() => console.log('Port changed:', portSelect.get()))
285
- ```
143
+ ### Flow
286
144
 
287
- ## Tags
145
+ Short-lived operation with input/output.
288
146
 
289
- Tags pass contextual values through execution without explicit wiring.
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
290
152
 
291
- ```mermaid
292
- flowchart LR
293
- subgraph Definition
294
- T["tag({ label: 'tenantId' })"]
295
- end
153
+ ### Tag
296
154
 
297
- subgraph Usage
298
- R["tags.required(tag)"] --> |"throws if missing"| V1["string"]
299
- O["tags.optional(tag)"] --> |"undefined if missing"| V2["string | undefined"]
300
- A["tags.all(tag)"] --> |"collects all"| V3["string[]"]
301
- end
302
- ```
155
+ Contextual value passed through execution without explicit wiring.
303
156
 
304
- ### Creating Tags
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
305
162
 
306
- ```typescript
307
- const tenantIdTag = tag<string>({ label: 'tenantId' })
308
- const userRolesTag = tag<string[]>({ label: 'userRoles', default: [] })
309
-
310
- // With parse validation
311
- const userId = tag({
312
- label: 'userId',
313
- parse: (raw) => {
314
- if (typeof raw !== 'string') throw new Error('Must be string')
315
- return raw
316
- }
317
- })
318
-
319
- userId('abc-123') // OK
320
- userId(123) // Throws ParseError
321
- ```
163
+ ### Controller
322
164
 
323
- ### Using Tags as Dependencies
165
+ Reactive handle for observing and controlling atom state.
324
166
 
325
- ```typescript
326
- // Required - throws if not found
327
- const tenantAtom = atom({
328
- deps: { tenantId: tags.required(tenantIdTag) },
329
- factory: (ctx, { tenantId }) => loadTenantData(tenantId)
330
- })
331
-
332
- // Optional - undefined if not found
333
- const optionalAtom = atom({
334
- deps: { tenantId: tags.optional(tenantIdTag) },
335
- factory: (ctx, { tenantId }) => {
336
- if (tenantId) {
337
- return loadTenantData(tenantId)
338
- }
339
- return loadDefaultData()
340
- }
341
- })
342
-
343
- // Collect all - returns array of all matching tags
344
- const multiAtom = atom({
345
- deps: { roles: tags.all(userRolesTag) },
346
- factory: (ctx, { roles }) => roles.flat() // string[][]
347
- })
348
- ```
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)
349
175
 
350
- ### Passing Tags
176
+ ### Preset
351
177
 
352
- Tags can be passed at different levels, with inner levels inheriting from outer:
178
+ Value injection for testing. Bypasses factory entirely.
353
179
 
354
- ```mermaid
355
- flowchart LR
356
- subgraph Scope["createScope()"]
357
- ST["tenantId: 'tenant-123'"]
358
- subgraph Context["createContext()"]
359
- CT["userRoles: ['admin']"]
360
- subgraph Exec["exec()"]
361
- ET["requestId: 'req-456'"]
362
- end
363
- end
364
- end
180
+ - `preset(atom, value)` — inject direct value
181
+ - `preset(atom, otherAtom)` — redirect to another atom's factory
182
+ - Pass via `createScope({ presets: [...] })`
365
183
 
366
- ST -.->|inherited| CT
367
- CT -.->|inherited| ET
368
- ```
184
+ ### Extension
369
185
 
370
- All three tag levels are available during flow execution.
186
+ AOP-style middleware for cross-cutting concerns.
371
187
 
372
- ## Presets
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: [...] })`
373
193
 
374
- Presets inject or redirect atom values, useful for testing.
194
+ ## Full API
375
195
 
376
- ```mermaid
377
- flowchart LR
378
- subgraph Normal["Normal Resolution"]
379
- A1[resolve dbAtom] --> F1[factory]
380
- F1 --> V1[real connection]
381
- end
196
+ See [`dist/index.d.mts`](./dist/index.d.mts) for complete type definitions.
382
197
 
383
- subgraph Preset["With Preset"]
384
- A2[resolve dbAtom] --> P[preset check]
385
- P -->|value| V2[mockDb]
386
- P -->|atom| F2[testAtom.factory]
387
- end
388
- ```
198
+ All types available under the `Lite` namespace:
389
199
 
390
- **Value injection** bypasses the factory entirely:
391
200
  ```typescript
392
- const scope = createScope({
393
- presets: [preset(dbAtom, mockDb)]
394
- })
201
+ import type { Lite } from '@pumped-fn/lite'
395
202
  ```
396
203
 
397
- **Atom redirection** uses another atom's factory:
398
- ```typescript
399
- const scope = createScope({
400
- presets: [preset(configAtom, testConfigAtom)]
401
- })
402
- ```
204
+ ## Edge Cases
403
205
 
404
- ## Extensions
206
+ ### Controller.set() / update()
405
207
 
406
- Extensions wrap atom resolution and flow execution (AOP-style middleware).
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 |
407
214
 
408
- ```typescript
409
- const timingExtension: Lite.Extension = {
410
- name: 'timing',
411
- wrapResolve: async (next, atom, scope) => {
412
- const start = performance.now()
413
- const result = await next()
414
- console.log(`Resolved in ${performance.now() - start}ms`)
415
- return result
416
- }
417
- }
418
-
419
- const scope = createScope({ extensions: [timingExtension] })
420
- ```
421
-
422
- Interface: `{ name, init?, wrapResolve?, wrapExec?, dispose? }`
423
-
424
- ## Lifecycle
215
+ Both run cleanups before applying the new value.
425
216
 
426
- ```mermaid
427
- stateDiagram-v2
428
- [*] --> idle
429
- idle --> resolving: resolve()
430
- resolving --> resolved: success
431
- resolving --> failed: error
432
- resolved --> resolving: invalidate()
433
- failed --> resolving: invalidate()
434
- resolved --> idle: release()
435
- failed --> idle: release()
436
- ```
217
+ ### DataStore.get()
437
218
 
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
443
-
444
- ## API Reference
445
-
446
- ### Factory Functions
447
-
448
- | Function | Description |
449
- |----------|-------------|
450
- | `createScope(options?)` | Create DI container (returns Scope with `ready` promise) |
451
- | `atom(config)` | Define long-lived cached dependency |
452
- | `flow(config)` | Define short-lived operation template (optional `name`, `parse`) |
453
- | `tag(config)` | Define contextual value (optional `parse` for validation) |
454
- | `controller(atom)` | Create controller dependency helper |
455
- | `preset(atom, value)` | Create value injection preset |
456
-
457
- ### Scope Methods
458
-
459
- | Method | Description |
460
- |--------|-------------|
461
- | `scope.ready` | Promise that resolves when extensions are initialized |
462
- | `scope.resolve(atom)` | Resolve atom and return cached value |
463
- | `scope.controller(atom)` | Get Controller for atom |
464
- | `scope.select(atom, selector, options?)` | Create fine-grained subscription |
465
- | `scope.release(atom)` | Release atom (run cleanups, remove from cache) |
466
- | `scope.dispose()` | Dispose scope (release all atoms, cleanup extensions) |
467
- | `scope.createContext(options?)` | Create ExecutionContext for flows |
468
- | `scope.on(event, atom, listener)` | Subscribe to atom state changes |
469
- | `scope.flush()` | Wait for pending invalidation queue to process |
470
-
471
- ### Controller Methods
472
-
473
- | Method | Description |
474
- |--------|-------------|
475
- | `ctrl.state` | Current state: `'idle'` \| `'resolving'` \| `'resolved'` \| `'failed'` |
476
- | `ctrl.get()` | Get resolved value (throws if not resolved) |
477
- | `ctrl.resolve()` | Resolve and return value |
478
- | `ctrl.release()` | Release atom |
479
- | `ctrl.invalidate()` | Trigger re-resolution |
480
- | `ctrl.on(event, listener)` | Subscribe: `'resolved'` \| `'resolving'` \| `'*'` |
481
-
482
- ### ExecutionContext Methods
483
-
484
- | Method | Description |
485
- |--------|-------------|
486
- | `ctx.input` | Current execution input |
487
- | `ctx.scope` | Parent scope |
488
- | `ctx.exec(options)` | Execute flow or function |
489
- | `ctx.onClose(fn)` | Register cleanup for context close |
490
- | `ctx.close()` | Close context and run cleanups |
491
-
492
- ### ResolveContext Methods
493
-
494
- | Method | Description |
495
- |--------|-------------|
496
- | `ctx.cleanup(fn)` | Register cleanup for atom invalidation/release |
497
- | `ctx.invalidate()` | Schedule self-invalidation |
498
- | `ctx.scope` | Parent scope |
499
- | `ctx.data` | Per-atom DataStore (survives invalidation) |
500
-
501
- ### Type Guards
502
-
503
- | Function | Description |
504
- |----------|-------------|
505
- | `isAtom(value)` | Check if value is Atom |
506
- | `isFlow(value)` | Check if value is Flow |
507
- | `isTag(value)` | Check if value is Tag |
508
- | `isTagged(value)` | Check if value is Tagged |
509
- | `isPreset(value)` | Check if value is Preset |
510
- | `isControllerDep(value)` | Check if value is ControllerDep |
511
-
512
- ### Types
513
-
514
- 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.
515
220
 
516
221
  ```typescript
517
- import type { Lite } from '@pumped-fn/lite'
222
+ const countTag = tag<number>({ label: 'count', default: 0 })
518
223
 
519
- const myAtom: Lite.Atom<Config> = atom({ factory: () => loadConfig() })
520
- const myController: Lite.Controller<Config> = scope.controller(myAtom)
521
- 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)
522
227
  ```
523
228
 
524
- ## Design Principles
525
-
526
- 1. **Minimal API** - Every export is expensive to learn
527
- 2. **Zero dependencies** - No runtime dependencies
528
- 3. **Explicit lifecycle** - No magic, clear state transitions
529
- 4. **Composable** - Effects compose through deps
530
- 5. **Type-safe** - Full TypeScript inference
531
-
532
229
  ## License
533
230
 
534
231
  MIT