@pumped-fn/lite 1.0.0 → 1.1.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
@@ -2,6 +2,8 @@
2
2
 
3
3
  A lightweight effect system for TypeScript with managed lifecycles and minimal reactivity.
4
4
 
5
+ **Zero dependencies** · **<17KB bundle** · **Full TypeScript support**
6
+
5
7
  ## What is an Effect System?
6
8
 
7
9
  An effect system manages **how** and **when** computations run, handling:
@@ -10,6 +12,56 @@ An effect system manages **how** and **when** computations run, handling:
10
12
  - **Side effect isolation** - controlled execution boundaries
11
13
  - **State transitions** - idle → resolving → resolved → failed
12
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
+
13
65
  ## Core Concepts
14
66
 
15
67
  ```
@@ -35,60 +87,445 @@ An effect system manages **how** and **when** computations run, handling:
35
87
  |---------|---------|
36
88
  | **Scope** | Long-lived boundary that manages atom lifecycles |
37
89
  | **Atom** | A managed effect with lifecycle (create, cache, cleanup, recreate) |
38
- | **ExecutionContext** | Short-lived context for running operations with input and tags |
90
+ | **Flow** | Template for short-lived operations with input/output |
91
+ | **ExecutionContext** | Short-lived context for running flows with input and tags |
39
92
  | **Controller** | Handle for observing and controlling an atom's state |
40
93
  | **Tag** | Contextual value passed through execution |
41
94
 
42
- ## Install
95
+ ## Atoms
43
96
 
44
- ```bash
45
- npm install @pumped-fn/lite
46
- ```
97
+ Atoms are long-lived dependencies that are cached within a scope.
47
98
 
48
- ## Quick Example
99
+ ### Basic Atom
49
100
 
50
101
  ```typescript
51
- import { atom, flow, createScope } from '@pumped-fn/lite'
52
-
53
- // Define effects (atoms) - long-lived, cached
54
102
  const dbAtom = atom({
55
103
  factory: async (ctx) => {
56
- const conn = await createConnection()
57
- ctx.cleanup(() => conn.close())
58
- return conn
104
+ const connection = await createConnection()
105
+ ctx.cleanup(() => connection.close())
106
+ return connection
59
107
  }
60
108
  })
109
+ ```
110
+
111
+ ### Atom with Dependencies
61
112
 
62
- const repoAtom = atom({
113
+ ```typescript
114
+ const userRepoAtom = atom({
63
115
  deps: { db: dbAtom },
64
116
  factory: (ctx, { db }) => new UserRepository(db)
65
117
  })
118
+ ```
119
+
120
+ ### Self-Invalidating Atom
121
+
122
+ ```typescript
123
+ const configAtom = atom({
124
+ factory: async (ctx) => {
125
+ const config = await fetchConfig()
126
+
127
+ // Re-fetch every 60 seconds
128
+ const interval = setInterval(() => ctx.invalidate(), 60_000)
129
+ ctx.cleanup(() => clearInterval(interval))
130
+
131
+ return config
132
+ }
133
+ })
134
+ ```
135
+
136
+ ### Per-Atom Private Storage
137
+
138
+ Use `ctx.data` to store data that survives invalidation:
139
+
140
+ ```typescript
141
+ const prevDataTag = tag<Data>({ label: 'prevData' })
142
+
143
+ const pollingAtom = atom({
144
+ factory: async (ctx) => {
145
+ const prev = ctx.data.get(prevDataTag) // Data | undefined
146
+ const current = await fetchData()
147
+
148
+ if (prev && hasChanged(prev, current)) {
149
+ notifyChanges(prev, current)
150
+ }
151
+
152
+ ctx.data.set(prevDataTag, current)
153
+ setTimeout(() => ctx.invalidate(), 5000)
154
+ return current
155
+ }
156
+ })
157
+ ```
158
+
159
+ With default values:
160
+
161
+ ```typescript
162
+ const countTag = tag<number>({ label: 'count', default: 0 })
163
+
164
+ const counterAtom = atom({
165
+ factory: (ctx) => {
166
+ const count = ctx.data.get(countTag) // number (guaranteed!)
167
+ ctx.data.set(countTag, count + 1)
168
+ return count
169
+ }
170
+ })
171
+ ```
172
+
173
+ ## Flows
174
+
175
+ Flows are templates for short-lived operations.
176
+
177
+ ### Basic Flow
178
+
179
+ ```typescript
180
+ const createUserFlow = flow({
181
+ deps: { repo: userRepoAtom },
182
+ factory: async (ctx, { repo }) => {
183
+ const input = ctx.input as CreateUserInput
184
+ return repo.create(input)
185
+ }
186
+ })
187
+ ```
66
188
 
67
- // Define operation template
68
- const getUser = flow({
69
- deps: { repo: repoAtom },
189
+ ### Flow with Parse Validation
190
+
191
+ ```typescript
192
+ const createUserFlow = flow({
193
+ name: 'createUser',
194
+ parse: (raw) => {
195
+ const obj = raw as Record<string, unknown>
196
+ if (typeof obj.name !== 'string') throw new Error('name required')
197
+ if (typeof obj.email !== 'string') throw new Error('email required')
198
+ return { name: obj.name, email: obj.email }
199
+ },
200
+ deps: { repo: userRepoAtom },
70
201
  factory: async (ctx, { repo }) => {
71
- return repo.findById(ctx.input as string)
202
+ // ctx.input is typed as { name: string; email: string }
203
+ return repo.create(ctx.input)
72
204
  }
73
205
  })
206
+ ```
207
+
208
+ 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'`.
74
209
 
75
- // Create scope (long-lived boundary)
76
- const scope = await createScope()
210
+ ### Executing Flows
77
211
 
78
- // Create ExecutionContext (short-lived, per-operation)
79
- const ctx = scope.createContext()
212
+ ```typescript
213
+ const scope = createScope()
214
+ await scope.ready
80
215
 
81
- // Execute - resolves atoms, runs operation, provides input
82
- const user = await ctx.exec({ flow: getUser, input: 'user-123' })
216
+ const context = scope.createContext()
83
217
 
84
- // ExecutionContext cleanup (operation complete)
85
- await ctx.close()
218
+ // Execute with input
219
+ const user = await context.exec({
220
+ flow: createUserFlow,
221
+ input: { name: 'Alice', email: 'alice@example.com' }
222
+ })
86
223
 
87
- // Scope cleanup (application shutdown)
88
- await scope.dispose()
224
+ // Always close the context when done
225
+ await context.close()
226
+ ```
227
+
228
+ ### Flow with Tags
229
+
230
+ ```typescript
231
+ const requestIdTag = tag<string>({ label: 'requestId' })
232
+
233
+ const loggingFlow = flow({
234
+ deps: { requestId: tags.required(requestIdTag) },
235
+ factory: (ctx, { requestId }) => {
236
+ console.log(`[${requestId}] Processing request`)
237
+ return processRequest(ctx.input)
238
+ }
239
+ })
240
+
241
+ // Pass tags at execution time
242
+ const context = scope.createContext()
243
+ await context.exec({
244
+ flow: loggingFlow,
245
+ input: data,
246
+ tags: [requestIdTag('req-abc-123')]
247
+ })
248
+ ```
249
+
250
+ ## Controllers
251
+
252
+ Controllers provide reactive access to atom state.
253
+
254
+ ### Basic Usage
255
+
256
+ ```typescript
257
+ const ctrl = scope.controller(configAtom)
258
+
259
+ // Access state
260
+ console.log(ctrl.state) // 'idle' | 'resolving' | 'resolved' | 'failed'
261
+
262
+ // Get resolved value (throws if not resolved)
263
+ const config = ctrl.get()
264
+
265
+ // Resolve and wait
266
+ const config = await ctrl.resolve()
267
+
268
+ // Manual invalidation
269
+ ctrl.invalidate()
89
270
  ```
90
271
 
91
- ## Effect Lifecycle
272
+ ### Subscribing to Changes
273
+
274
+ ```typescript
275
+ // Subscribe to specific events
276
+ ctrl.on('resolved', () => {
277
+ console.log('Config updated:', ctrl.get())
278
+ })
279
+
280
+ ctrl.on('resolving', () => {
281
+ console.log('Config is refreshing...')
282
+ })
283
+
284
+ // Subscribe to all state changes
285
+ ctrl.on('*', () => {
286
+ console.log('State changed:', ctrl.state)
287
+ })
288
+ ```
289
+
290
+ ### Controller as Dependency
291
+
292
+ Use `controller()` to get a Controller instead of the resolved value:
293
+
294
+ ```typescript
295
+ const appAtom = atom({
296
+ deps: { config: controller(configAtom) },
297
+ factory: (ctx, { config }) => {
298
+ // Subscribe to config changes
299
+ config.on('resolved', () => {
300
+ console.log('Config updated, reinitializing...')
301
+ ctx.invalidate()
302
+ })
303
+
304
+ return new App(config.get())
305
+ }
306
+ })
307
+ ```
308
+
309
+ ### Fine-Grained Reactivity with select()
310
+
311
+ ```typescript
312
+ const portSelect = scope.select(
313
+ configAtom,
314
+ (config) => config.port,
315
+ { eq: (a, b) => a === b } // Optional custom equality
316
+ )
317
+
318
+ portSelect.subscribe(() => {
319
+ console.log('Port changed:', portSelect.get())
320
+ })
321
+ ```
322
+
323
+ ## Tags
324
+
325
+ Tags pass contextual values through execution without explicit wiring.
326
+
327
+ ### Creating Tags
328
+
329
+ ```typescript
330
+ const tenantIdTag = tag<string>({ label: 'tenantId' })
331
+ const userRolesTag = tag<string[]>({ label: 'userRoles', default: [] })
332
+
333
+ // With parse validation
334
+ const userId = tag({
335
+ label: 'userId',
336
+ parse: (raw) => {
337
+ if (typeof raw !== 'string') throw new Error('Must be string')
338
+ return raw
339
+ }
340
+ })
341
+
342
+ userId('abc-123') // OK
343
+ userId(123) // Throws ParseError
344
+ ```
345
+
346
+ ### Using Tags as Dependencies
347
+
348
+ ```typescript
349
+ // Required - throws if not found
350
+ const tenantAtom = atom({
351
+ deps: { tenantId: tags.required(tenantIdTag) },
352
+ factory: (ctx, { tenantId }) => loadTenantData(tenantId)
353
+ })
354
+
355
+ // Optional - undefined if not found
356
+ const optionalAtom = atom({
357
+ deps: { tenantId: tags.optional(tenantIdTag) },
358
+ factory: (ctx, { tenantId }) => {
359
+ if (tenantId) {
360
+ return loadTenantData(tenantId)
361
+ }
362
+ return loadDefaultData()
363
+ }
364
+ })
365
+
366
+ // Collect all - returns array of all matching tags
367
+ const multiAtom = atom({
368
+ deps: { roles: tags.all(userRolesTag) },
369
+ factory: (ctx, { roles }) => roles.flat() // string[][]
370
+ })
371
+ ```
372
+
373
+ ### Passing Tags
374
+
375
+ ```typescript
376
+ // At scope creation
377
+ const scope = createScope({
378
+ tags: [tenantIdTag('tenant-123')]
379
+ })
380
+
381
+ // At context creation
382
+ const context = scope.createContext({
383
+ tags: [userRolesTag(['admin', 'user'])]
384
+ })
385
+
386
+ // At execution time
387
+ await context.exec({
388
+ flow: myFlow,
389
+ input: data,
390
+ tags: [requestIdTag('req-456')]
391
+ })
392
+ ```
393
+
394
+ ### Direct Tag Methods
395
+
396
+ ```typescript
397
+ const tags = [tenantIdTag('tenant-123'), userRolesTag(['admin'])]
398
+
399
+ // Get (throws if not found for tags without default)
400
+ const tenantId = tenantIdTag.get(tags)
401
+
402
+ // Find (returns undefined if not found)
403
+ const roles = userRolesTag.find(tags)
404
+
405
+ // Collect all matching values
406
+ const allRoles = userRolesTag.collect(tags)
407
+ ```
408
+
409
+ ## Presets
410
+
411
+ Presets inject or redirect atom values, useful for testing.
412
+
413
+ ### Value Injection
414
+
415
+ ```typescript
416
+ const mockDb = { query: jest.fn() }
417
+
418
+ const scope = createScope({
419
+ presets: [preset(dbAtom, mockDb)]
420
+ })
421
+
422
+ // resolves to mockDb without calling factory
423
+ const db = await scope.resolve(dbAtom)
424
+ ```
425
+
426
+ ### Atom Redirection
427
+
428
+ ```typescript
429
+ const testConfigAtom = atom({
430
+ factory: () => ({ apiUrl: 'http://localhost:3000' })
431
+ })
432
+
433
+ const scope = createScope({
434
+ presets: [preset(configAtom, testConfigAtom)]
435
+ })
436
+
437
+ // resolves testConfigAtom instead of configAtom
438
+ const config = await scope.resolve(configAtom)
439
+ ```
440
+
441
+ ## Extensions
442
+
443
+ Extensions provide cross-cutting behavior via AOP-style hooks.
444
+
445
+ ### Extension Interface
446
+
447
+ ```typescript
448
+ interface Extension {
449
+ readonly name: string
450
+ init?(scope: Scope): MaybePromise<void>
451
+ wrapResolve?<T>(next: () => Promise<T>, atom: Atom<T>, scope: Scope): Promise<T>
452
+ wrapExec?<T>(next: () => Promise<T>, target: Flow | Function, ctx: ExecutionContext): Promise<T>
453
+ dispose?(scope: Scope): MaybePromise<void>
454
+ }
455
+ ```
456
+
457
+ ### Example: Logging Extension
458
+
459
+ ```typescript
460
+ const loggingExtension: Lite.Extension = {
461
+ name: 'logging',
462
+
463
+ init: (scope) => {
464
+ console.log('Scope initialized')
465
+ },
466
+
467
+ wrapResolve: async (next, atom, scope) => {
468
+ const start = performance.now()
469
+ console.log('Resolving atom...')
470
+
471
+ try {
472
+ const result = await next()
473
+ console.log(`Resolved in ${performance.now() - start}ms`)
474
+ return result
475
+ } catch (error) {
476
+ console.error('Resolution failed:', error)
477
+ throw error
478
+ }
479
+ },
480
+
481
+ wrapExec: async (next, target, ctx) => {
482
+ console.log('Executing:', 'flow' in target ? 'flow' : 'function')
483
+ return next()
484
+ },
485
+
486
+ dispose: (scope) => {
487
+ console.log('Scope disposed')
488
+ }
489
+ }
490
+
491
+ const scope = createScope({ extensions: [loggingExtension] })
492
+ ```
493
+
494
+ ### Example: Metrics Extension
495
+
496
+ ```typescript
497
+ const metricsExtension: Lite.Extension = {
498
+ name: 'metrics',
499
+
500
+ wrapResolve: async (next, atom, scope) => {
501
+ const start = performance.now()
502
+ try {
503
+ const result = await next()
504
+ metrics.recordResolution(atom, performance.now() - start, 'success')
505
+ return result
506
+ } catch (error) {
507
+ metrics.recordResolution(atom, performance.now() - start, 'error')
508
+ throw error
509
+ }
510
+ },
511
+
512
+ wrapExec: async (next, target, ctx) => {
513
+ const start = performance.now()
514
+ try {
515
+ const result = await next()
516
+ metrics.recordExecution(target, performance.now() - start, 'success')
517
+ return result
518
+ } catch (error) {
519
+ metrics.recordExecution(target, performance.now() - start, 'error')
520
+ throw error
521
+ }
522
+ }
523
+ }
524
+ ```
525
+
526
+ ## Lifecycle
527
+
528
+ ### Effect Lifecycle
92
529
 
93
530
  ```mermaid
94
531
  stateDiagram-v2
@@ -102,7 +539,7 @@ stateDiagram-v2
102
539
  failed --> idle: release()
103
540
  ```
104
541
 
105
- ## Resolution Flow
542
+ ### Resolution Flow
106
543
 
107
544
  ```mermaid
108
545
  sequenceDiagram
@@ -126,9 +563,7 @@ sequenceDiagram
126
563
  Atom-->>App: value
127
564
  ```
128
565
 
129
- ## Invalidation (Re-execution)
130
-
131
- Atoms can be invalidated to re-run their effect:
566
+ ### Invalidation Flow
132
567
 
133
568
  ```mermaid
134
569
  sequenceDiagram
@@ -145,7 +580,7 @@ sequenceDiagram
145
580
  Note over Queue: queueMicrotask (batched)
146
581
 
147
582
  Queue->>Scope: flush
148
- Scope->>Scope: run cleanups
583
+ Scope->>Scope: run cleanups (LIFO)
149
584
  Scope->>Scope: state = resolving
150
585
  Scope->>Factory: factory(ctx, deps)
151
586
  Factory-->>Scope: new value
@@ -153,117 +588,115 @@ sequenceDiagram
153
588
  Scope->>Scope: notify listeners
154
589
  ```
155
590
 
156
- Invalidations are **batched** via microtask queue - multiple invalidations in one tick become a single re-execution.
157
-
158
- ## Controller Pattern
159
-
160
- Controllers provide a handle to observe and control atom state:
161
-
162
- ```typescript
163
- const configAtom = atom({
164
- factory: async (ctx) => {
165
- const config = await fetchConfig()
166
- setTimeout(() => ctx.invalidate(), 60000) // refresh every minute
167
- return config
168
- }
169
- })
170
-
171
- const appAtom = atom({
172
- deps: { config: controller(configAtom) }, // controller, not direct
173
- factory: (ctx, { config }) => {
174
- // Subscribe to config changes (must specify event type)
175
- config.on('resolved', () => {
176
- console.log('config updated:', config.get())
177
- ctx.invalidate() // re-run this atom too
178
- })
179
-
180
- return new App(config.get())
181
- }
182
- })
183
- ```
184
-
185
- ## Per-Atom Private Storage
186
-
187
- The `ctx.data` Map provides private storage that survives invalidation but clears on release. Useful for internal state that shouldn't be exposed:
188
-
189
- ```typescript
190
- const pollingAtom = atom({
191
- factory: async (ctx) => {
192
- const prev = ctx.data.get('prev') as Data | undefined
193
- const current = await fetchData()
194
-
195
- if (prev && current !== prev) {
196
- console.log('Data changed!')
197
- }
198
- ctx.data.set('prev', current)
199
-
200
- setTimeout(() => ctx.invalidate(), 5000)
201
- return current
202
- }
203
- })
204
- ```
205
-
206
- | Event | `ctx.data` Behavior |
207
- |-------|---------------------|
208
- | First access | Map created lazily |
209
- | `invalidate()` | Map preserved |
210
- | `release()` | Map cleared |
211
-
212
- ## Tags (Contextual Values)
591
+ ## API Reference
213
592
 
214
- Tags pass contextual values through execution without explicit wiring:
593
+ ### Factory Functions
215
594
 
216
- ```typescript
217
- const requestIdTag = tag<string>({ label: 'requestId' })
218
-
219
- const loggingAtom = atom({
220
- deps: { requestId: tags.required(requestIdTag) },
221
- factory: (ctx, { requestId }) => new Logger(requestId)
222
- })
223
-
224
- // Pass tags when creating ExecutionContext or at exec time
225
- const ctx = scope.createContext({ tags: [requestIdTag('req-abc-123')] })
226
- await ctx.exec({ flow: myFlow, input: data })
595
+ | Function | Description |
596
+ |----------|-------------|
597
+ | `createScope(options?)` | Create DI container (returns Scope with `ready` promise) |
598
+ | `atom(config)` | Define long-lived cached dependency |
599
+ | `flow(config)` | Define short-lived operation template (optional `name`, `parse`) |
600
+ | `tag(config)` | Define contextual value (optional `parse` for validation) |
601
+ | `controller(atom)` | Create controller dependency helper |
602
+ | `preset(atom, value)` | Create value injection preset |
603
+
604
+ ### Scope Methods
605
+
606
+ | Method | Description |
607
+ |--------|-------------|
608
+ | `scope.ready` | Promise that resolves when extensions are initialized |
609
+ | `scope.resolve(atom)` | Resolve atom and return cached value |
610
+ | `scope.controller(atom)` | Get Controller for atom |
611
+ | `scope.select(atom, selector, options?)` | Create fine-grained subscription |
612
+ | `scope.release(atom)` | Release atom (run cleanups, remove from cache) |
613
+ | `scope.dispose()` | Dispose scope (release all atoms, cleanup extensions) |
614
+ | `scope.createContext(options?)` | Create ExecutionContext for flows |
615
+ | `scope.on(event, atom, listener)` | Subscribe to atom state changes |
616
+
617
+ ### Controller Methods
618
+
619
+ | Method | Description |
620
+ |--------|-------------|
621
+ | `ctrl.state` | Current state: `'idle'` \| `'resolving'` \| `'resolved'` \| `'failed'` |
622
+ | `ctrl.get()` | Get resolved value (throws if not resolved) |
623
+ | `ctrl.resolve()` | Resolve and return value |
624
+ | `ctrl.release()` | Release atom |
625
+ | `ctrl.invalidate()` | Trigger re-resolution |
626
+ | `ctrl.on(event, listener)` | Subscribe: `'resolved'` \| `'resolving'` \| `'*'` |
627
+
628
+ ### ExecutionContext Methods
629
+
630
+ | Method | Description |
631
+ |--------|-------------|
632
+ | `ctx.input` | Current execution input |
633
+ | `ctx.scope` | Parent scope |
634
+ | `ctx.exec(options)` | Execute flow or function |
635
+ | `ctx.onClose(fn)` | Register cleanup for context close |
636
+ | `ctx.close()` | Close context and run cleanups |
637
+
638
+ ### ResolveContext Methods
639
+
640
+ | Method | Description |
641
+ |--------|-------------|
642
+ | `ctx.cleanup(fn)` | Register cleanup for atom invalidation/release |
643
+ | `ctx.invalidate()` | Schedule self-invalidation |
644
+ | `ctx.scope` | Parent scope |
645
+ | `ctx.data` | Per-atom DataStore (survives invalidation) |
646
+
647
+ ### Type Guards
227
648
 
228
- // Or per-execution
229
- await ctx.exec({
230
- flow: myFlow,
231
- input: data,
232
- tags: [requestIdTag('req-xyz-456')]
233
- })
234
- ```
649
+ | Function | Description |
650
+ |----------|-------------|
651
+ | `isAtom(value)` | Check if value is Atom |
652
+ | `isFlow(value)` | Check if value is Flow |
653
+ | `isTag(value)` | Check if value is Tag |
654
+ | `isTagged(value)` | Check if value is Tagged |
655
+ | `isPreset(value)` | Check if value is Preset |
656
+ | `isControllerDep(value)` | Check if value is ControllerDep |
235
657
 
236
- ## Extensions (Cross-cutting Effects)
658
+ ### Types
237
659
 
238
- Wrap resolution and execution with cross-cutting behavior:
660
+ All types are available under the `Lite` namespace:
239
661
 
240
662
  ```typescript
241
- const timingExtension: Lite.Extension = {
242
- name: 'timing',
243
- wrapResolve: async (next, atom, scope) => {
244
- const start = performance.now()
245
- const result = await next()
246
- console.log(`resolved in ${performance.now() - start}ms`)
247
- return result
248
- }
249
- }
663
+ import type { Lite } from '@pumped-fn/lite'
250
664
 
251
- const scope = await createScope({ extensions: [timingExtension] })
665
+ const myAtom: Lite.Atom<Config> = atom({ factory: () => loadConfig() })
666
+ const myController: Lite.Controller<Config> = scope.controller(myAtom)
667
+ const myTag: Lite.Tag<string> = tag({ label: 'myTag' })
252
668
  ```
253
669
 
254
- ## API Reference
255
-
256
- | Function | Description |
257
- |----------|-------------|
258
- | `createScope(options?)` | Create execution boundary |
259
- | `atom(config)` | Define managed effect (long-lived) |
260
- | `flow(config)` | Define operation template (used by ExecutionContext) |
261
- | `tag(config)` | Define contextual value |
262
- | `controller(atom)` | Wrap atom for deferred resolution |
263
- | `preset(atom, value)` | Override atom value in scope |
264
- | `scope.createContext(options?)` | Create ExecutionContext for operations |
265
- | `ctx.exec(options)` | Execute operation with input and tags |
266
- | `ctx.close()` | Cleanup ExecutionContext resources |
670
+ ## Comparison with @pumped-fn/core-next
671
+
672
+ | Feature | @pumped-fn/lite | @pumped-fn/core-next |
673
+ |---------|-----------------|----------------------|
674
+ | Atoms/Executors | `atom()` | `provide()`, `derive()` |
675
+ | Flows | `flow()` | `flow()` |
676
+ | Tags | `tag()`, `tags.*` | `tag()`, `tags.*` |
677
+ | Extensions | Simple 4-hook interface | Full lifecycle hooks |
678
+ | Schema validation | No | StandardSchema |
679
+ | Journaling | No | Yes |
680
+ | Multi-executor pools | No | `multi()` |
681
+ | Enhanced Promise | No | `Promised` class |
682
+ | Error classes | Simple Error | Rich hierarchy |
683
+ | Controller reactivity | ✅ Built-in | No |
684
+ | Self-invalidation | ✅ Built-in | No |
685
+ | Fine-grained select() | ✅ Built-in | No |
686
+ | Tag/Flow parse functions | ✅ Built-in | No |
687
+ | Bundle size | <17KB | ~75KB |
688
+
689
+ **Choose `@pumped-fn/lite` when:**
690
+ - Bundle size matters
691
+ - You need built-in reactivity (Controller pattern)
692
+ - You want a minimal API surface
693
+ - Schema validation can be done manually
694
+
695
+ **Choose `@pumped-fn/core-next` when:**
696
+ - You need StandardSchema validation
697
+ - You need multi-executor pools
698
+ - You need journaling/debugging features
699
+ - You need rich error context
267
700
 
268
701
  ## Design Principles
269
702
 
@@ -271,6 +704,7 @@ const scope = await createScope({ extensions: [timingExtension] })
271
704
  2. **Zero dependencies** - No runtime dependencies
272
705
  3. **Explicit lifecycle** - No magic, clear state transitions
273
706
  4. **Composable** - Effects compose through deps
707
+ 5. **Type-safe** - Full TypeScript inference
274
708
 
275
709
  ## License
276
710