@pumped-fn/lite 1.11.3 → 1.11.4

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/PATTERNS.md +182 -299
  3. package/README.md +166 -378
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 1.11.4
4
+
5
+ ### Patch Changes
6
+
7
+ - a3ae2b7: Replace text glossary with mermaid sequence diagrams in documentation
8
+
9
+ - README.md now uses visual diagrams for composition, atom lifecycle, tag resolution, type utilities, and API surface
10
+ - PATTERNS.md converted all usage patterns to sequence diagrams for clarity
11
+
3
12
  ## 1.11.3
4
13
 
5
14
  ### Patch Changes
package/PATTERNS.md CHANGED
@@ -1,380 +1,263 @@
1
- # Architectural Patterns
1
+ # Patterns
2
2
 
3
- Design patterns implemented by `@pumped-fn/lite` and how to compose them for application architecture.
3
+ Usage patterns as sequences. For API details, see `packages/lite/dist/index.d.mts`.
4
4
 
5
- ## Composite Patterns
5
+ ## A. Fundamental Usage
6
6
 
7
7
  ### Request Lifecycle
8
8
 
9
- **Combines:** IoC Container + Command + Composite
10
-
11
- | GoF Pattern | Primitive |
12
- |-------------|-----------|
13
- | IoC Container | `Scope` (long-lived, caches atoms) |
14
- | Command | `Flow` (operations within request) |
15
- | Composite | `ExecutionContext` (parent-child with isolated data) |
16
-
17
- **Key Insight:**
18
- - `Scope` = application container (atoms cached here)
19
- - `ExecutionContext` = request boundary (data lives here, closed at request end)
20
- - `Flow` / `ctx.exec` = operations within the request (share context via `seekTag`)
9
+ Model a request boundary with cleanup and shared context.
21
10
 
22
11
  ```mermaid
23
12
  sequenceDiagram
24
13
  participant App
25
14
  participant Scope
26
- participant Context as ExecutionContext
27
- participant ServiceAtom as Service Atom
28
- participant Flow1 as Flow (validate)
29
- participant Flow2 as Flow (process)
30
-
31
- Note over Scope: Long-lived (app lifetime)
32
- App->>Scope: resolve(serviceAtom)
33
- Scope-->>App: service (cached)
15
+ participant Ctx as ExecutionContext
16
+ participant Flow
34
17
 
35
- Note over Context: Per-request boundary
36
- App->>Scope: createContext({ tags: [requestId, userId] })
18
+ App->>Scope: createScope()
19
+ App->>Scope: scope.createContext({ tags })
37
20
  Scope-->>App: ctx
38
21
 
39
- App->>Context: ctx.data.setTag(TX_TAG, beginTransaction())
40
- App->>Context: ctx.onClose(() => tx.rollback())
41
-
42
- App->>Context: ctx.exec({ flow: validateFlow, input })
43
- Context->>Scope: resolve flow deps (atoms)
44
- Scope-->>Context: deps (cached in scope)
45
- Context->>Flow1: factory(childCtx, deps)
46
- Note over Flow1: childCtx.parent = ctx
47
- Flow1->>Flow1: tags.required(userIdTag) from merged tags
48
- Flow1-->>Context: validated
49
-
50
- App->>Context: ctx.exec({ flow: processFlow, input: validated })
51
- Context->>Scope: resolve flow deps (atoms)
52
- Scope-->>Context: deps (from cache)
53
- Context->>Flow2: factory(childCtx, deps)
54
- Flow2->>Flow2: childCtx.exec({ fn: service.save, params: [data] })
55
- Note over Flow2: Creates grandchildCtx
56
- Flow2->>ServiceAtom: service.save(grandchildCtx, data)
57
- ServiceAtom->>ServiceAtom: grandchildCtx.data.seekTag(TX_TAG)
58
- Note over ServiceAtom: seekTag traverses parent chain
59
- ServiceAtom-->>Flow2: saved
60
- Flow2-->>Context: result
61
-
62
- App->>Context: ctx.data.getTag(TX_TAG).commit()
63
- App->>Context: ctx.close()
64
- Context->>Context: onClose cleanups run (rollback skipped)
65
- ```
22
+ App->>Ctx: ctx.exec({ flow, input, tags })
23
+ Ctx->>Flow: factory(childCtx, deps)
24
+ Flow-->>Ctx: output
25
+ Ctx->>Ctx: childCtx.close()
26
+ Ctx-->>App: output
66
27
 
67
- **Characteristics:**
68
- - Scope caches atoms across requests (resolve once, use many)
69
- - ExecutionContext bounds request lifecycle (`onClose` for cleanup)
70
- - Each `exec()` creates child context with isolated `data` Map
71
- - `seekTag()` traverses parent chain for shared data (e.g., transaction)
72
- - `ctx.close()` runs all `onClose` cleanups (LIFO order)
73
- - On error: child context auto-closes, cleanups still run
28
+ App->>Ctx: ctx.onClose(cleanup)
29
+ App->>Ctx: ctx.close()
30
+ Ctx->>Ctx: run cleanups (LIFO)
31
+ ```
74
32
 
75
- **Primitives:** `createScope()`, `scope.createContext()`, `ctx.exec()`, `ctx.data.setTag/seekTag()`, `ctx.onClose()`, `ctx.close()`
33
+ ### Extensions Pipeline
76
34
 
77
- ---
35
+ Observe and wrap timing for atoms/flows (logging, auth, tracing).
78
36
 
79
- ### Flow Deps & Execution
37
+ ```mermaid
38
+ sequenceDiagram
39
+ participant App
40
+ participant Scope
41
+ participant Ext as Extension
42
+ participant Atom
43
+ participant Flow
80
44
 
81
- **Combines:** Command + Composite + Resource Management
45
+ App->>Scope: createScope({ extensions: [ext] })
46
+ Scope->>Ext: ext.init(scope)
47
+ App->>Scope: await scope.ready
48
+
49
+ App->>Scope: resolve(atom)
50
+ Scope->>Ext: wrapResolve(next, atom, scope)
51
+ Ext->>Ext: before logic
52
+ Ext->>Atom: next()
53
+ Atom-->>Ext: value
54
+ Ext->>Ext: after logic
55
+ Ext-->>Scope: value
56
+
57
+ App->>Scope: ctx.exec({ flow })
58
+ Scope->>Ext: wrapExec(next, flow, ctx)
59
+ Ext->>Flow: next()
60
+ Flow-->>Ext: output
61
+ Ext-->>Scope: output
62
+
63
+ App->>Scope: dispose()
64
+ Scope->>Ext: ext.dispose(scope)
65
+ ```
82
66
 
83
- | Concern | Primitive |
84
- |---------|-----------|
85
- | Dependency injection | `deps` (atoms from Scope, tags from context hierarchy) |
86
- | Service invocation | `ctx.exec({ fn, params })` (observable by extensions) |
87
- | Resource cleanup | `ctx.onClose()` (LIFO, runs on success or failure) |
67
+ ### Scoped Isolation + Testing
88
68
 
89
- **Deps Resolution:**
69
+ Swap implementations and isolate tenants/tests.
90
70
 
91
71
  ```mermaid
92
- flowchart TB
93
- subgraph Flow["flow({ deps, factory })"]
94
- Deps["deps: { db: dbAtom, userId: tags.required(userIdTag) }"]
95
- end
72
+ sequenceDiagram
73
+ participant Test
74
+ participant Scope
75
+ participant Atom
96
76
 
97
- subgraph Resolution
98
- Deps --> AtomPath["Atom deps"]
99
- Deps --> TagPath["Tag deps (TagExecutor)"]
77
+ Test->>Scope: createScope({ presets: [preset(dbAtom, mockDb)], tags: [tenantTag(id)] })
78
+ Test->>Scope: resolve(dbAtom)
79
+ Scope-->>Test: mockDb (not real db)
100
80
 
101
- AtomPath --> Scope["Scope.resolve()"]
102
- Scope --> |"cached in scope"| ResolvedAtom["db instance"]
81
+ Test->>Scope: createContext()
82
+ Scope-->>Test: ctx with tenantTag
83
+ ```
103
84
 
104
- TagPath --> CtxHierarchy["ctx.data.seekTag()"]
105
- CtxHierarchy --> |"traverses parent chain"| ResolvedTag["userId value"]
106
- end
85
+ ## B. Advanced Client/State Usage
107
86
 
108
- subgraph Factory["factory(ctx, { db, userId })"]
109
- ResolvedAtom --> DepsObj["deps object"]
110
- ResolvedTag --> DepsObj
111
- end
112
- ```
87
+ ### Controller Reactivity
113
88
 
114
- **Service Invocation:**
89
+ Client-side state with lifecycle hooks and invalidation.
115
90
 
116
91
  ```mermaid
117
92
  sequenceDiagram
118
- participant Flow
119
- participant Ctx as ExecutionContext
120
- participant Ext as Extension.wrapExec
121
- participant Fn as service.method
122
-
123
- Note over Flow: ❌ Direct call
124
- Flow->>Fn: service.method(ctx, data)
125
- Note over Fn: Extensions cannot observe
126
-
127
- Note over Flow: ✅ Via ctx.exec
128
- Flow->>Ctx: ctx.exec({ fn: service.method, params: [data] })
129
- Ctx->>Ctx: create childCtx (parent = ctx)
130
- Ctx->>Ext: wrapExec(next, fn, childCtx)
131
- Ext->>Fn: next()
132
- Fn-->>Ext: result
133
- Ext-->>Ctx: result
134
- Ctx->>Ctx: childCtx.close()
135
- Ctx-->>Flow: result
136
- ```
93
+ participant App
94
+ participant Scope
95
+ participant Ctrl as Controller
96
+ participant Atom
137
97
 
138
- **Cleanup Pattern:**
98
+ App->>Scope: controller(atom)
99
+ Scope-->>App: ctrl
139
100
 
140
- ```mermaid
141
- sequenceDiagram
142
- participant Flow
143
- participant Ctx as ExecutionContext
144
- participant Tx as Transaction
145
-
146
- Flow->>Tx: beginTransaction()
147
- Tx-->>Flow: tx
148
- Flow->>Ctx: ctx.onClose(() => tx.rollback())
149
-
150
- alt Success
151
- Flow->>Flow: do work
152
- Flow->>Tx: tx.commit()
153
- Flow->>Ctx: return result
154
- Ctx->>Ctx: close() → rollback() is no-op
155
- else Error
156
- Flow->>Flow: do work (throws)
157
- Ctx->>Ctx: close() → rollback() executes
158
- Ctx->>Tx: tx.rollback()
159
- end
160
- ```
101
+ App->>Ctrl: ctrl.on('resolving' | 'resolved' | '*', listener)
102
+ Ctrl-->>App: unsubscribe
161
103
 
162
- **Characteristics:**
163
- - Atoms resolve via `Scope.resolve()` (cached, long-lived)
164
- - Tag deps resolve via `ctx.data.seekTag()` (traverses parent → grandparent → scope tags)
165
- - `ctx.exec({ fn, params })` creates child context with isolated `data` Map
166
- - Extensions intercept via `wrapExec(next, target, ctx)`
167
- - Register pessimistic cleanup via `ctx.onClose(fn)`, neutralize on success
104
+ App->>Ctrl: ctrl.get()
105
+ Ctrl-->>App: current value
168
106
 
169
- **Primitives:** `flow({ deps })`, `tags.required()`, `tags.optional()`, `tags.all()`, `ctx.exec()`, `ctx.onClose()`
107
+ App->>Ctrl: ctrl.set(newValue)
108
+ Ctrl->>Ctrl: notify listeners
170
109
 
171
- ---
110
+ App->>Ctrl: ctrl.update(v => v + 1)
111
+ Ctrl->>Ctrl: notify listeners
172
112
 
173
- ### Request Pipeline
113
+ App->>Ctrl: ctrl.invalidate()
114
+ Ctrl->>Atom: re-run factory
115
+ Ctrl->>Ctrl: notify listeners
116
+ ```
174
117
 
175
- **Combines:** Command + Interceptor + Context Object
118
+ ### Ambient Context (Tags)
176
119
 
177
- | GoF Pattern | Primitive |
178
- |-------------|-----------|
179
- | Command | `Flow` (encapsulates request with input/output) |
180
- | Interceptor | `Extension.wrapExec()` (wraps execution) |
181
- | Context Object | `Tag` (propagates metadata without explicit passing) |
120
+ Propagate state without wiring parameters (app shell, user, locale).
182
121
 
183
122
  ```mermaid
184
123
  sequenceDiagram
185
- participant Client
186
- participant Scope
187
- participant Extension1 as Extension (Auth)
188
- participant Extension2 as Extension (Tracing)
189
- participant Flow
190
- participant Context as ExecutionContext
191
-
192
- Client->>Scope: createContext({ tags: [requestId] })
193
- Scope-->>Client: ctx
194
- Client->>Context: exec({ flow, input, tags: [userId] })
195
-
196
- Context->>Context: merge tags (flow → scope → context → exec)
197
- Context->>Context: create child context
198
-
199
- Context->>Extension1: wrapExec(next, flow, childCtx)
200
- Extension1->>Extension1: extract userId tag, validate
201
- Extension1->>Extension2: next()
202
- Extension2->>Extension2: read parent span from ctx.parent?.data
203
- Extension2->>Extension2: create child span, store in ctx.data
204
- Extension2->>Flow: next()
205
- Flow->>Flow: factory(ctx, deps) with tags.required(userId)
206
- Flow-->>Extension2: result
207
- Extension2->>Extension2: end span
208
- Extension2-->>Extension1: result
209
- Extension1-->>Context: result
210
- Context->>Context: auto-close child (run onClose cleanups)
211
- Context-->>Client: result
212
- ```
213
-
214
- **Characteristics:**
215
- - Extensions wrap in registration order (outer → inner)
216
- - Each `exec()` creates isolated child context with own `data` Map
217
- - Tags merge with later sources winning (exec tags override flow tags)
218
- - Parent chain enables span correlation without AsyncLocalStorage
124
+ participant App
125
+ participant Ctx as ExecutionContext
126
+ participant ChildCtx
127
+ participant Data as ctx.data
219
128
 
220
- **Primitives:** `flow()`, `Extension.wrapExec`, `tag()`, `ctx.exec()`, `ctx.parent`, `ctx.data`
129
+ App->>Data: ctx.data.setTag(userTag, user)
130
+ App->>Ctx: ctx.exec({ flow, tags: [localeTag('en')] })
131
+ Ctx->>ChildCtx: create with merged tags
221
132
 
222
- ---
133
+ ChildCtx->>Data: ctx.data.seekTag(userTag)
134
+ Data-->>ChildCtx: user (from parent)
223
135
 
224
- ### Scoped Isolation
136
+ ChildCtx->>Data: ctx.data.getTag(localeTag)
137
+ Data-->>ChildCtx: 'en'
138
+ ```
225
139
 
226
- **Combines:** IoC Container + Strategy + Composite
140
+ ### Derived State (Select)
227
141
 
228
- | GoF Pattern | Primitive |
229
- |-------------|-----------|
230
- | IoC Container | `Scope` (manages atom lifecycles and resolution) |
231
- | Strategy | `Preset` (swap implementations at scope creation) |
232
- | Composite | `ExecutionContext` (parent-child isolation) |
142
+ Subscribe to a slice of atom state with custom equality.
233
143
 
234
144
  ```mermaid
235
145
  sequenceDiagram
236
146
  participant App
237
- participant TenantScope as Scope (Tenant A)
238
- participant TestScope as Scope (Test)
239
- participant DbAtom as dbAtom
240
- participant MockDb as mockDbAtom
241
-
242
- Note over App: Production - Tenant A
243
- App->>TenantScope: createScope({ tags: [tenantId('A')] })
244
- App->>TenantScope: resolve(dbAtom)
245
- TenantScope->>DbAtom: factory(ctx, deps)
246
- DbAtom->>DbAtom: tags.required(tenantId) → 'A'
247
- DbAtom-->>TenantScope: TenantA DB connection
248
-
249
- Note over App: Test - Mocked DB
250
- App->>TestScope: createScope({ presets: [preset(dbAtom, mockDbAtom)] })
251
- App->>TestScope: resolve(dbAtom)
252
- TestScope->>TestScope: check presets → found
253
- TestScope->>MockDb: resolve mockDbAtom instead
254
- MockDb-->>TestScope: Mock DB instance
255
-
256
- Note over App: Parallel tenant contexts
257
- par Tenant A request
258
- TenantScope->>TenantScope: createContext({ tags: [requestId('r1')] })
259
- and Tenant B request
260
- TenantScope->>TenantScope: createContext({ tags: [requestId('r2')] })
261
- end
262
- Note over TenantScope: Each context isolated, same scope
263
- ```
264
-
265
- **Characteristics:**
266
- - Each Scope is an isolated DI container with own cache
267
- - Presets swap atom implementations without changing definitions
268
- - Tags at scope level apply to all resolutions
269
- - Multiple ExecutionContexts share scope but isolate request data
270
- - Child contexts inherit parent tags, can override
147
+ participant Scope
148
+ participant Handle as SelectHandle
149
+ participant Atom
271
150
 
272
- **Use Cases:**
273
- - Multi-tenancy: scope-level tenant tag, context-level request isolation
274
- - Testing: preset mocks without touching production atom definitions
275
- - Feature flags: preset alternative implementations per environment
151
+ App->>Scope: select(atom, v => v.count, { eq: shallowEqual })
152
+ Scope-->>App: handle
276
153
 
277
- **Primitives:** `createScope()`, `preset()`, `tag()`, `createContext()`, scope `tags` option
154
+ App->>Handle: handle.get()
155
+ Handle-->>App: selected value
278
156
 
279
- ---
157
+ App->>Handle: handle.subscribe(listener)
158
+ Handle-->>App: unsubscribe
280
159
 
281
- ## Foundational Patterns
160
+ Note over Atom,Handle: atom changes
161
+ Handle->>Handle: eq(prev, next)?
162
+ Handle->>App: notify if changed
163
+ ```
282
164
 
283
- ### IoC Container
165
+ ### Service Pattern
284
166
 
285
- **GoF:** Inversion of Control / Dependency Injection Container
167
+ Constrain atom methods to ExecutionContext-first signature for tracing/auth.
286
168
 
287
169
  ```mermaid
288
- graph TB
289
- Scope["Scope (Container)"]
290
- AtomA["Atom A"]
291
- AtomB["Atom B"]
292
- AtomC["Atom C"]
293
- Cache["Resolution Cache"]
294
-
295
- Scope -->|resolve| AtomA
296
- Scope -->|resolve| AtomB
297
- AtomB -->|deps| AtomA
298
- AtomC -->|deps| AtomA
299
- AtomC -->|deps| AtomB
300
- Scope --- Cache
301
- ```
302
-
303
- **Primitives:** `createScope()`, `atom()`, `deps`, `scope.resolve()`
170
+ sequenceDiagram
171
+ participant App
172
+ participant Scope
173
+ participant Ctx as ExecutionContext
174
+ participant Svc as Service Atom
304
175
 
305
- **Characteristics:** Lazy resolution, automatic caching, dependency graph traversal, circular dependency detection.
176
+ App->>Scope: resolve(userService)
177
+ Scope-->>App: { getUser, updateUser }
306
178
 
307
- ---
179
+ App->>Ctx: svc.getUser(ctx, userId)
180
+ Ctx->>Svc: traced execution
181
+ Svc-->>Ctx: user
182
+ Ctx-->>App: user
183
+ ```
308
184
 
309
- ### Observer
185
+ ### Typed Flow Input
310
186
 
311
- **GoF:** Observer Pattern with State Machine
187
+ Type flow input without runtime parsing overhead.
312
188
 
313
189
  ```mermaid
314
- stateDiagram-v2
315
- [*] --> idle
316
- idle --> resolving: resolve()
317
- resolving --> resolved: success
318
- resolving --> failed: error
319
- resolved --> resolving: invalidate()
320
- failed --> resolving: invalidate()
321
-
322
- note right of resolving: listeners notified
323
- note right of resolved: listeners notified
190
+ sequenceDiagram
191
+ participant App
192
+ participant Flow
193
+ participant Ctx
194
+
195
+ Note over Flow: flow({ parse: typed<T>(), factory })
196
+ App->>Flow: ctx.exec({ flow, input: typedInput })
197
+ Flow->>Flow: skip parse (type-only)
198
+ Flow->>Ctx: factory(ctx) with ctx.input: T
199
+ Ctx-->>App: output
324
200
  ```
325
201
 
326
- **Primitives:** `controller()`, `ctrl.on('resolved' | 'resolving' | '*')`, `ctrl.invalidate()`
202
+ ### Controller as Dependency
327
203
 
328
- **Characteristics:** State-filtered subscriptions, LIFO cleanup before re-resolution, sequential invalidation chains with loop detection.
204
+ Receive reactive handle instead of resolved value in atom/flow deps.
329
205
 
330
- ---
206
+ ```mermaid
207
+ sequenceDiagram
208
+ participant Scope
209
+ participant AtomA as serverAtom
210
+ participant Ctrl as Controller
211
+ participant AtomB as configAtom
212
+
213
+ Scope->>AtomA: resolve(serverAtom)
214
+ Note over AtomA: deps: { cfg: controller(configAtom, { resolve: true }) }
215
+ AtomA->>Scope: resolve configAtom first
216
+ Scope-->>Ctrl: ctrl (already resolved)
217
+ AtomA->>AtomA: factory(ctx, { cfg: ctrl })
218
+ AtomA->>Ctrl: ctrl.on('resolved', () => ctx.invalidate())
219
+ Note over AtomA: react to config changes
220
+ ```
331
221
 
332
- ### Context Object
222
+ ### Inline Function Execution
333
223
 
334
- **GoF:** Context Object / Ambient Context
224
+ Execute ad-hoc logic within context without defining a flow.
335
225
 
336
226
  ```mermaid
337
- graph TB
338
- subgraph Sources
339
- FlowTags[Flow tags]
340
- ScopeTags[Scope tags]
341
- CtxTags[Context tags]
342
- ExecTags[Exec tags]
343
- end
344
-
345
- subgraph Merge[Tag Merge - later wins]
346
- FlowTags --> Merged
347
- ScopeTags --> Merged
348
- CtxTags --> Merged
349
- ExecTags --> Merged
350
- end
227
+ sequenceDiagram
228
+ participant App
229
+ participant Ctx as ExecutionContext
351
230
 
352
- subgraph Extract
353
- Merged --> Required[tags.required]
354
- Merged --> Optional[tags.optional]
355
- Merged --> All[tags.all]
356
- end
231
+ App->>Ctx: ctx.exec({ fn: (ctx, a, b) => a + b, params: [1, 2], tags })
232
+ Ctx->>Ctx: create childCtx with tags
233
+ Ctx->>Ctx: fn(childCtx, 1, 2)
234
+ Ctx->>Ctx: childCtx.close()
235
+ Ctx-->>App: 3
357
236
  ```
358
237
 
359
- **Primitives:** `tag()`, `tags.required()`, `tags.optional()`, `tags.all()`, `Tagged`
238
+ ### Atom Retention (GC)
360
239
 
361
- **Characteristics:** Implicit propagation through execution layers, type-safe extraction, merge precedence (exec > context > scope > flow).
240
+ Control when atoms are garbage collected or kept alive indefinitely.
362
241
 
363
- ---
242
+ ```mermaid
243
+ sequenceDiagram
244
+ participant App
245
+ participant Scope
246
+ participant Atom
364
247
 
365
- ### Strategy
248
+ App->>Scope: createScope({ gc: { enabled: true, graceMs: 3000 } })
366
249
 
367
- **GoF:** Strategy Pattern
250
+ App->>Scope: resolve(atom)
251
+ Scope-->>App: value
252
+ Note over Scope: no refs → start grace timer
368
253
 
369
- ```mermaid
370
- graph TB
371
- Scope -->|resolve| Atom
372
- Atom -->|check| Presets{Preset?}
373
- Presets -->|value| Direct[Return value]
374
- Presets -->|atom| Redirect[Resolve other atom]
375
- Presets -->|none| Factory[Run factory]
376
- ```
377
-
378
- **Primitives:** `preset()`, `createScope({ presets })`, `isPreset()`
254
+ alt keepAlive: true
255
+ Note over Atom: never GC'd
256
+ else graceMs expires
257
+ Scope->>Atom: release()
258
+ Atom->>Atom: run cleanups
259
+ end
379
260
 
380
- **Characteristics:** Swap implementations at scope creation, value injection bypasses factory, atom redirection for mock substitution.
261
+ App->>Scope: flush()
262
+ Note over Scope: wait all pending
263
+ ```
package/README.md CHANGED
@@ -1,442 +1,230 @@
1
1
  # @pumped-fn/lite
2
2
 
3
- A lightweight effect system for TypeScript with managed lifecycles and minimal reactivity.
3
+ Lightweight effect system for TypeScript: scoped lifecycles, tagged context, and opt‑in reactivity.
4
4
 
5
- **Zero dependencies** · **<17KB bundle** · **Full TypeScript support**
6
-
7
- ## Documentation
8
-
9
- | Resource | Purpose |
10
- |----------|---------|
11
- | [PATTERNS.md](./PATTERNS.md) | Architecture patterns, flow design, deps resolution, cleanup strategies |
12
- | [dist/index.d.mts](./dist/index.d.mts) | API reference with TSDoc |
5
+ Docs: `packages/lite/PATTERNS.md` for usage patterns, `packages/lite/dist/index.d.mts` for API details.
13
6
 
14
7
  ## How It Works
15
8
 
16
9
  ```mermaid
17
10
  sequenceDiagram
18
- participant User
11
+ participant App
19
12
  participant Scope
13
+ participant Ext as Extension
14
+ participant Ctx as ExecutionContext
20
15
  participant Atom
16
+ participant Flow
17
+ participant Ctrl as Controller
18
+
19
+ App->>Scope: createScope({ extensions, presets, tags })
20
+ Scope-->>App: scope (ready)
21
+
22
+ App->>Scope: resolve(atom)
23
+ Scope->>Ext: wrapResolve(next, atom)
24
+ Ext->>Atom: factory(ctx, deps)
25
+ Atom-->>Scope: value (cached)
26
+
27
+ App->>Scope: createContext({ tags })
28
+ Scope-->>App: ctx
29
+
30
+ App->>Ctx: ctx.exec({ flow, input, tags })
31
+ Ctx->>Ctx: merge tags, create childCtx
32
+ Ctx->>Ext: wrapExec(next, flow, childCtx)
33
+ Ext->>Flow: parse(input) + factory(childCtx, deps)
34
+ Flow-->>Ext: output
35
+ Ext-->>Ctx: output
36
+ Ctx->>Ctx: childCtx.close() (onClose LIFO)
37
+ Ctx-->>App: output
38
+
39
+ rect rgb(240, 248, 255)
40
+ Note over App,Ctrl: Reactivity (opt‑in)
41
+ App->>Scope: controller(atom)
42
+ Scope-->>Ctrl: ctrl
43
+ App->>Ctrl: ctrl.get() / ctrl.resolve()
44
+ Ctrl-->>App: value
45
+ App->>Ctrl: ctrl.set(v) / ctrl.update(fn)
46
+ App->>Ctrl: ctrl.on('resolved', listener)
47
+ Ctrl-->>App: unsubscribe
48
+ App->>Ctrl: ctrl.invalidate()
49
+ Ctrl->>Atom: re‑run factory
50
+ App->>Ctrl: ctrl.release()
51
+ App->>Scope: release(atom)
52
+ Scope->>Scope: run cleanups, remove cache
53
+ App->>Scope: select(atom, selector, { eq })
54
+ Scope-->>App: { get, subscribe }
55
+ App->>Scope: on('resolved', atom, listener)
56
+ Scope-->>App: unsubscribe
57
+ end
21
58
 
22
- User->>Scope: createScope(options?)
23
- Scope-->>User: scope
24
- User->>Scope: await scope.ready
25
-
26
- User->>Scope: scope.resolve(atom)
27
- alt preset exists
28
- Scope-->>User: preset value (factory skipped)
29
- else no preset
30
- Scope->>Atom: factory(ctx, deps)
31
- Atom-->>Scope: value (cached)
32
- Scope-->>User: value
59
+ rect rgb(255, 250, 240)
60
+ Note over App,Scope: Introspection
61
+ App->>App: isAtom(v), isFlow(v), isTag(v), isTagged(v)
62
+ App->>App: isPreset(v), isControllerDep(v), isTagExecutor(v)
63
+ App->>App: getAllTags() → Tag[]
33
64
  end
34
65
 
35
- User->>Scope: scope.dispose()
36
- Scope->>Atom: run cleanups, release all
66
+ App->>Scope: flush()
67
+ Note right of Scope: wait pending ops
68
+ App->>Ctx: ctx.close()
69
+ Ctx->>Ctx: run onClose (LIFO)
70
+ App->>Scope: dispose()
71
+ Scope->>Scope: release atoms, run cleanups
37
72
  ```
38
73
 
39
- ## Invalidation & Data Retention
74
+ ## Composition
40
75
 
41
76
  ```mermaid
42
- sequenceDiagram
43
- participant User
44
- participant Controller
45
- participant Atom
46
- participant DataStore as ctx.data
47
-
48
- Note over DataStore: persists across invalidations
49
-
50
- User->>Controller: ctrl.invalidate()
51
- Controller->>Atom: run cleanups (LIFO)
52
- Note over DataStore: retained
53
- Controller->>Atom: state = resolving
54
- Controller->>Atom: factory(ctx, deps)
55
- Note right of Atom: ctx.data still has previous values
56
- Atom-->>Controller: new value
57
- Controller->>Atom: state = resolved
58
- Controller-->>User: listeners notified
59
- ```
60
-
61
- ## Flow Execution
62
-
63
- ```mermaid
64
- sequenceDiagram
65
- participant User
66
- participant Scope
67
- participant Context as ExecutionContext
68
- participant Flow
69
-
70
- User->>Scope: scope.createContext(options?)
71
- Scope-->>User: context
77
+ graph LR
78
+ subgraph Primitives
79
+ atom["atom({ factory, deps?, tags?, keepAlive? })"]
80
+ flow["flow({ factory, parse?, deps?, tags? })"]
81
+ service["service({ factory, deps? })"]
82
+ tag["tag({ label, default?, parse? })"]
83
+ preset["preset(target, value)"]
84
+ end
72
85
 
73
- User->>Context: ctx.exec({ flow, input, tags? })
74
- Context->>Flow: parse(input)
75
- Context->>Context: resolve flow deps
76
- Context->>Flow: factory(ctx, deps)
77
- Flow-->>Context: output
78
- Context-->>User: output
86
+ subgraph Wrappers
87
+ typed["typed&lt;T&gt;()"]
88
+ ctrlDep["controller(atom, { resolve? })"]
89
+ tagExec["tags.required/optional/all(tag)"]
90
+ end
79
91
 
80
- User->>Context: ctx.close()
81
- Context->>Context: run onClose cleanups (LIFO)
92
+ flow --> typed
93
+ atom --> ctrlDep
94
+ tag --> tagExec
82
95
  ```
83
96
 
84
- ## Tag Inheritance (ADR-023)
85
-
86
- Tags are auto-populated into `ctx.data` and resolved via `seekTag()`:
97
+ ## Context Data
87
98
 
88
99
  ```mermaid
89
- flowchart TD
90
- subgraph Root["Root Context (ctx.data)"]
91
- S["scope.tags → auto-populated"]
92
- C["context.tags → auto-populated"]
93
- subgraph Child["Child Context (exec)"]
94
- E["exec.tags → auto-populated"]
95
- F["flow.tags → auto-populated"]
96
- D["ctx.data.setTag() → runtime"]
97
- subgraph Deps["tags.required(tag)"]
98
- R["seekTag() traverses: Child → Root"]
99
- end
100
- end
100
+ graph TD
101
+ subgraph "ctx.data"
102
+ raw["Raw: get/set/has/delete/clear/seek"]
103
+ typed["Typed: getTag/setTag/hasTag/deleteTag/seekTag/getOrSetTag"]
101
104
  end
102
-
103
- Note["Nearest value wins. Propagates to all descendants."]
105
+ raw --> typed
104
106
  ```
105
107
 
106
- ## Controller Reactivity
108
+ ## Atom Lifecycle (AtomState)
107
109
 
108
110
  ```mermaid
109
- sequenceDiagram
110
- participant User
111
- participant Controller
112
- participant Atom
113
-
114
- User->>Controller: scope.controller(atom)
115
- User->>Controller: ctrl.on('resolved', listener)
116
- Controller-->>User: unsubscribe fn
117
-
118
- Note over Controller: atom invalidated elsewhere
119
-
120
- Controller->>Atom: state = resolving
121
- Controller-->>User: 'resolving' listeners fire
122
- Atom-->>Controller: new value
123
- Controller->>Atom: state = resolved
124
- Controller-->>User: 'resolved' listeners fire
125
-
126
- User->>Controller: ctrl.get()
127
- Controller-->>User: current value
111
+ stateDiagram-v2
112
+ [*] --> idle
113
+ idle --> resolving: resolve() / controller()
114
+ resolving --> resolved: factory completes
115
+ resolving --> failed: factory throws
116
+ resolved --> resolving: invalidate()
117
+ resolved --> idle: release()
118
+ failed --> resolving: invalidate()
119
+ failed --> idle: release()
128
120
  ```
129
121
 
130
- ## Primitives
131
-
132
- ### Scope
133
-
134
- Entry point. Manages atom lifecycles, caching, and cleanup orchestration.
135
-
136
- - `createScope(options?)` — create with optional extensions, presets, tags
137
- - `scope.ready` — wait for extension initialization
138
- - `scope.resolve(atom)` — resolve and cache
139
- - `scope.controller(atom)` — get reactive handle
140
- - `scope.release(atom)` — run cleanups, remove from cache
141
- - `scope.dispose()` — release all, cleanup extensions
142
- - `scope.createContext(options?)` — create execution context for flows
143
- - `scope.select(atom, selector)` — fine-grained reactivity
144
- - `scope.flush()` — wait for pending invalidations
145
-
146
- ### Atom
147
-
148
- Long-lived cached dependency with lifecycle.
149
-
150
- - Dependencies on other atoms via `deps`
151
- - `ctx.cleanup(fn)` — runs on invalidate and release (LIFO order)
152
- - `ctx.invalidate()` — schedule re-resolution
153
- - `ctx.data` — storage that survives invalidation (cleared on release)
154
- - `ctx.data.getOrSetTag(tag, defaultValue)` — initialize and retrieve in one call
155
-
156
- ### Flow
157
-
158
- Short-lived operation with input/output.
159
-
160
- - `parse` — validate/transform input before factory (throws `ParseError` on failure)
161
- - `typed<T>()` — type marker without runtime parsing
162
- - Dependencies on atoms via `deps`
163
- - `ctx.input` — typed input access
164
- - `ctx.onClose(fn)` — cleanup when context closes
165
- - `ctx.exec({ flow, rawInput })` — pass unknown input when flow has `parse`
166
-
167
- ### Tag
168
-
169
- Contextual value passed through execution without explicit wiring.
170
-
171
- - Hierarchical lookup via `seekTag()` (ADR-023)
172
- - Auto-populates into `ctx.data`: scope → context → exec → flow
173
- - Registry tracks atom↔tag relationships (ADR-026)
122
+ ## Tag Resolution
174
123
 
175
124
  ```mermaid
176
- flowchart TD
177
- subgraph "Tag Registry (ADR-026)"
178
- direction LR
179
- A["atom({ tags: [...] })"] -->|auto-register| R["WeakMap⟨Tag, WeakRef⟨Atom⟩[]⟩"]
180
- R -->|"tag.atoms()"| Q["query atoms by tag"]
181
- R -->|"getAllTags()"| T["query all tags"]
182
- end
183
-
184
- subgraph "Tag Inheritance (ADR-023)"
185
- S[scope.tags] --> D[ctx.data]
186
- C[context.tags] --> D
187
- E[exec.tags] --> D
188
- F[flow.tags] --> D
189
- D -->|"seekTag()"| V["nearest value wins"]
190
- end
191
- ```
192
-
193
- Memory: `WeakRef` allows GC of unused atoms/tags. Cleanup on query.
194
-
195
- ### Controller
196
-
197
- Reactive handle for observing and controlling atom state.
198
-
199
- - `ctrl.state` — sync access: `'idle' | 'resolving' | 'resolved' | 'failed'`
200
- - `ctrl.get()` — sync value access (throws if not resolved, returns stale during resolving)
201
- - `ctrl.resolve()` — async resolution
202
- - `ctrl.invalidate()` — trigger re-resolution (runs factory)
203
- - `ctrl.set(value)` — replace value directly (skips factory)
204
- - `ctrl.update(fn)` — transform value: `fn(currentValue) → newValue` (skips factory)
205
- - `ctrl.on(event, listener)` — subscribe to `'resolved' | 'resolving' | '*'`
206
- - Use `controller(atom)` in deps for reactive dependency (unresolved, you control timing)
207
- - Use `controller(atom, { resolve: true })` to auto-resolve before passing to factory
208
- - Use `scope.controller(atom, { resolve: true })` for same behavior outside deps
209
-
210
- ### Preset
125
+ sequenceDiagram
126
+ participant App
127
+ participant Tag
128
+ participant Source as Atom/Flow/Ctx
211
129
 
212
- Value injection for testing. Bypasses factory entirely.
130
+ App->>Tag: tag({ label, default? })
131
+ Tag-->>App: Tag<T>
213
132
 
214
- - `preset(atom, value)` — inject direct value
215
- - `preset(atom, otherAtom)` — redirect to another atom's factory
216
- - Pass via `createScope({ presets: [...] })`
133
+ App->>Tag: tag(value)
134
+ Tag-->>App: Tagged<T>
217
135
 
218
- ### Extension
136
+ App->>Source: attach Tagged[] to atom/flow/ctx
219
137
 
220
- AOP-style middleware for cross-cutting concerns.
138
+ App->>Tag: tag.get(source)
139
+ Tag->>Source: find first match
140
+ Source-->>Tag: value or throw
221
141
 
222
- - `init(scope)` — setup when scope created
223
- - `wrapResolve(next, atom, scope)` — intercept atom resolution
224
- - `wrapExec(next, target, ctx)` — intercept flow execution
225
- - `dispose(scope)` — cleanup when scope disposed
226
- - Pass via `createScope({ extensions: [...] })`
142
+ App->>Tag: tag.find(source)
143
+ Tag->>Source: find first match
144
+ Source-->>Tag: value or undefined
227
145
 
228
- ## Patterns
146
+ App->>Tag: tag.collect(source)
147
+ Tag->>Source: gather all matches
148
+ Source-->>Tag: T[]
229
149
 
230
- ### Eager Resolution via Tag Registry
150
+ App->>Tag: tag.atoms()
151
+ Tag-->>App: Atom[] with this tag
152
+ ```
231
153
 
232
- Use tags to mark atoms for eager resolution without hardcoding atom references:
154
+ ## Type Utilities
233
155
 
234
156
  ```mermaid
235
- flowchart LR
236
- subgraph "Define"
237
- T[eagerTag] --> A1[atomA]
238
- T --> A2[atomB]
239
- T --> A3[atomC]
157
+ graph LR
158
+ subgraph "Lite.Utils"
159
+ AtomValue["AtomValue&lt;A&gt;"]
160
+ FlowOutput["FlowOutput&lt;F&gt;"]
161
+ FlowInput["FlowInput&lt;F&gt;"]
162
+ TagValue["TagValue&lt;T&gt;"]
163
+ DepsOf["DepsOf&lt;A|F&gt;"]
164
+ ControllerValue["ControllerValue&lt;C&gt;"]
165
+ Simplify["Simplify&lt;T&gt;"]
166
+ AtomType["AtomType&lt;T, D&gt;"]
167
+ FlowType["FlowType&lt;O, I, D&gt;"]
240
168
  end
241
169
 
242
- subgraph "Extension init()"
243
- E["eagerTag.atoms()"] --> R["resolve all marked atoms"]
170
+ subgraph "Type Guards"
171
+ isAtom
172
+ isFlow
173
+ isTag
174
+ isTagged
175
+ isPreset
176
+ isControllerDep
177
+ isTagExecutor
244
178
  end
245
179
 
246
- A1 & A2 & A3 -.->|"auto-tracked"| E
247
- ```
248
-
249
- ### Extension Discovery via getAllTags()
250
-
251
- Extensions can discover and process all tags at runtime:
252
-
253
- ```mermaid
254
- flowchart LR
255
- subgraph "Runtime"
256
- G["getAllTags()"] --> F{"filter by criteria"}
257
- F --> P["process matching tags"]
258
- P --> A["tag.atoms() for each"]
180
+ subgraph "Convenience"
181
+ AnyAtom
182
+ AnyFlow
183
+ AnyController
259
184
  end
260
185
  ```
261
186
 
262
- Use cases: metrics collection, debugging, documentation generation.
263
-
264
- ## Types
265
-
266
- All types available under the `Lite` namespace:
267
-
268
- ```typescript
269
- import type { Lite } from '@pumped-fn/lite'
270
- ```
271
-
272
- ## Edge Cases
273
-
274
- ### Controller.set() / update()
275
-
276
- | State | Behavior |
277
- |-------|----------|
278
- | `idle` | Throws "Atom not resolved" |
279
- | `resolving` | Queues, applies after resolution completes |
280
- | `resolved` | Queues normally |
281
- | `failed` | Throws the stored error |
282
-
283
- Both run cleanups before applying the new value.
284
-
285
- ### ContextData.getTag()
286
-
287
- `ctx.data.getTag(tag)` always returns `T | undefined` (Map-like semantics). Use `getOrSetTag(tag)` when you need the tag's default value.
288
-
289
- ```typescript
290
- const countTag = tag<number>({ label: 'count', default: 0 })
291
-
292
- ctx.data.getTag(countTag) // undefined (not stored)
293
- ctx.data.getOrSetTag(countTag) // 0 (uses default, now stored)
294
- ctx.data.getTag(countTag) // 0 (now stored)
295
- ```
296
-
297
- ### Hierarchical Data Lookup with seekTag() (ADR-023)
298
-
299
- Tag dependencies (`tags.required()`, `tags.optional()`, `tags.all()`) use `seekTag()` internally to traverse the ExecutionContext parent chain. Tags from all sources are auto-populated into `ctx.data`:
300
-
301
- ```typescript
302
- const requestIdTag = tag<string>({ label: 'requestId' })
303
-
304
- const middleware = flow({
305
- factory: async (ctx) => {
306
- ctx.data.setTag(requestIdTag, 'req-123')
307
- return ctx.exec({ flow: handler })
308
- }
309
- })
310
-
311
- const handler = flow({
312
- deps: { reqId: tags.required(requestIdTag) },
313
- factory: (ctx, { reqId }) => {
314
- // reqId === 'req-123' (found via seekTag from parent)
315
- }
316
- })
317
- ```
318
-
319
- | Method | Scope | Use Case |
320
- |--------|-------|----------|
321
- | `getTag(tag)` | Local only | Per-exec isolated data |
322
- | `seekTag(tag)` | Local → parent → root | Cross-cutting concerns |
323
- | `setTag(tag, v)` | Local only | Always writes to current context |
324
- | `tags.required(tag)` | Uses `seekTag()` | Dependency injection |
325
-
326
- ### Resolution Timing
327
-
328
- Tag dependencies resolve **once** at factory start. Direct `seekTag()` calls reflect runtime changes:
329
-
330
- ```typescript
331
- const handler = flow({
332
- deps: { userId: tags.required(userIdTag) },
333
- factory: (ctx, { userId }) => {
334
- ctx.data.setTag(userIdTag, 'changed')
335
-
336
- console.log(userId) // Original (stable)
337
- console.log(ctx.data.seekTag(userIdTag)) // 'changed' (dynamic)
338
- }
339
- })
340
- ```
341
-
342
- | Access | Resolution | Runtime Changes |
343
- |--------|------------|-----------------|
344
- | `deps: { x: tags.required(tag) }` | Once at start | Stable snapshot |
345
- | `ctx.data.seekTag(tag)` | Each call | Sees changes |
346
-
347
- ## Automatic Garbage Collection
348
-
349
- Atoms are automatically released when they have no subscribers, preventing memory leaks in long-running applications.
350
-
351
- ### How It Works
187
+ ## Introspection
352
188
 
353
189
  ```mermaid
354
190
  sequenceDiagram
355
- participant Component
356
- participant Controller
357
- participant Scope
358
- participant Timer
359
-
360
- Component->>Controller: ctrl.on('resolved', callback)
361
- Note over Controller: subscriberCount = 1
362
-
363
- Component->>Controller: unsubscribe()
364
- Note over Controller: subscriberCount = 0
365
- Controller->>Timer: schedule GC (3000ms)
366
-
367
- alt Resubscribe before timeout
368
- Component->>Controller: ctrl.on('resolved', callback)
369
- Controller->>Timer: cancel GC
370
- Note over Controller: Atom stays alive
371
- else Timeout fires
372
- Timer->>Scope: release(atom)
373
- Note over Scope: Cleanups run, cache cleared
374
- Scope->>Scope: Check dependencies for cascading GC
375
- end
376
- ```
377
-
378
- ### Configuration
379
-
380
- ```typescript
381
- // Default: GC enabled with 3000ms grace period
382
- const scope = createScope()
383
-
384
- // Custom grace period (useful for tests)
385
- const scope = createScope({
386
- gc: { graceMs: 100 }
387
- })
388
-
389
- // Disable GC entirely (preserves pre-1.11 behavior)
390
- const scope = createScope({
391
- gc: { enabled: false }
392
- })
393
- ```
394
-
395
- ### Opt-Out with keepAlive
191
+ participant App
192
+ participant Registry
396
193
 
397
- Mark atoms that should never be automatically released:
194
+ App->>Registry: getAllTags()
195
+ Registry-->>App: Tag[] (all live tags)
398
196
 
399
- ```typescript
400
- const configAtom = atom({
401
- factory: () => loadConfig(),
402
- keepAlive: true // Never auto-released
403
- })
197
+ App->>App: isAtom(v) / isFlow(v) / isTag(v)
198
+ App->>App: isTagged(v) / isPreset(v)
199
+ App->>App: isControllerDep(v) / isTagExecutor(v)
404
200
  ```
405
201
 
406
- ### Cascading Dependency Protection
202
+ ## Additional Exports
407
203
 
408
- Dependencies are protected while dependents are mounted:
409
-
410
- ```
411
- configAtom (keepAlive: true)
412
-
413
- dbAtom ←── userServiceAtom ←── [Component subscribes]
414
- ```
415
-
416
- - `dbAtom` won't be GC'd while `userServiceAtom` is mounted
417
- - When component unmounts, `userServiceAtom` is GC'd after grace period
418
- - Then `dbAtom` becomes eligible for GC (no dependents)
419
- - `configAtom` stays alive due to `keepAlive: true`
420
-
421
- ### React Strict Mode Compatibility
204
+ ```mermaid
205
+ graph LR
206
+ subgraph Errors
207
+ ParseError["ParseError (tag | flow-input)"]
208
+ end
422
209
 
423
- The 3000ms default grace period handles React's double-mount behavior:
210
+ subgraph Meta
211
+ VERSION
212
+ end
424
213
 
214
+ subgraph "Symbols (advanced)"
215
+ atomSymbol
216
+ flowSymbol
217
+ tagSymbol
218
+ taggedSymbol
219
+ presetSymbol
220
+ controllerSymbol
221
+ controllerDepSymbol
222
+ tagExecutorSymbol
223
+ typedSymbol
224
+ end
425
225
  ```
426
- Mount (render 1): subscribe → count=1
427
- Unmount (cleanup 1): unsubscribe → count=0 → schedule GC
428
- Mount (render 2): subscribe → count=1 → CANCEL GC
429
- ```
430
-
431
- The second mount always happens before the GC timer fires.
432
-
433
- ### API Summary
434
226
 
435
- | Option | Default | Description |
436
- |--------|---------|-------------|
437
- | `gc.enabled` | `true` | Enable/disable automatic GC |
438
- | `gc.graceMs` | `3000` | Delay before releasing (ms) |
439
- | `atom.keepAlive` | `false` | Prevent auto-release for specific atoms |
227
+ API reference: `packages/lite/dist/index.d.mts`.
440
228
 
441
229
  ## License
442
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pumped-fn/lite",
3
- "version": "1.11.3",
3
+ "version": "1.11.4",
4
4
  "description": "Lightweight dependency injection with minimal reactivity",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",