@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/CHANGELOG.md +53 -0
- package/README.md +154 -457
- package/dist/index.cjs +47 -6
- package/dist/index.d.cts +88 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +88 -21
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +47 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
##
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
41
|
+
Note over DataStore: persists across invalidations
|
|
134
42
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
63
|
+
User->>Scope: scope.createContext(options?)
|
|
64
|
+
Scope-->>User: context
|
|
187
65
|
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
## Controllers
|
|
216
|
-
|
|
217
|
-
Controllers provide reactive access to atom state.
|
|
77
|
+
## Tag Inheritance
|
|
218
78
|
|
|
219
79
|
```mermaid
|
|
220
|
-
flowchart
|
|
221
|
-
subgraph
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
## Controller Reactivity
|
|
233
94
|
|
|
234
|
-
```
|
|
235
|
-
|
|
95
|
+
```mermaid
|
|
96
|
+
sequenceDiagram
|
|
97
|
+
participant User
|
|
98
|
+
participant Controller
|
|
99
|
+
participant Atom
|
|
236
100
|
|
|
237
|
-
|
|
238
|
-
ctrl.
|
|
239
|
-
|
|
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
|
-
|
|
105
|
+
Note over Controller: atom invalidated elsewhere
|
|
244
106
|
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
117
|
+
## Primitives
|
|
254
118
|
|
|
255
|
-
|
|
119
|
+
### Scope
|
|
256
120
|
|
|
257
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
133
|
+
### Atom
|
|
277
134
|
|
|
278
|
-
|
|
135
|
+
Long-lived cached dependency with lifecycle.
|
|
279
136
|
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
const portSelect = scope.select(configAtom, (c) => c.port)
|
|
284
|
-
portSelect.subscribe(() => console.log('Port changed:', portSelect.get()))
|
|
285
|
-
```
|
|
143
|
+
### Flow
|
|
286
144
|
|
|
287
|
-
|
|
145
|
+
Short-lived operation with input/output.
|
|
288
146
|
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
flowchart LR
|
|
293
|
-
subgraph Definition
|
|
294
|
-
T["tag({ label: 'tenantId' })"]
|
|
295
|
-
end
|
|
153
|
+
### Tag
|
|
296
154
|
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
Reactive handle for observing and controlling atom state.
|
|
324
166
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
###
|
|
176
|
+
### Preset
|
|
351
177
|
|
|
352
|
-
|
|
178
|
+
Value injection for testing. Bypasses factory entirely.
|
|
353
179
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
367
|
-
CT -.->|inherited| ET
|
|
368
|
-
```
|
|
184
|
+
### Extension
|
|
369
185
|
|
|
370
|
-
|
|
186
|
+
AOP-style middleware for cross-cutting concerns.
|
|
371
187
|
|
|
372
|
-
|
|
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
|
-
|
|
194
|
+
## Full API
|
|
375
195
|
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
presets: [preset(dbAtom, mockDb)]
|
|
394
|
-
})
|
|
201
|
+
import type { Lite } from '@pumped-fn/lite'
|
|
395
202
|
```
|
|
396
203
|
|
|
397
|
-
|
|
398
|
-
```typescript
|
|
399
|
-
const scope = createScope({
|
|
400
|
-
presets: [preset(configAtom, testConfigAtom)]
|
|
401
|
-
})
|
|
402
|
-
```
|
|
204
|
+
## Edge Cases
|
|
403
205
|
|
|
404
|
-
|
|
206
|
+
### Controller.set() / update()
|
|
405
207
|
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
const countTag = tag<number>({ label: 'count', default: 0 })
|
|
518
223
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|