@pumped-fn/lite 1.11.3 → 2.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - e87f8c9: feat(lite): add `resource()` execution-scoped dependency primitive
8
+
9
+ BREAKING CHANGE: `wrapResolve` extension hook signature changed from `(next, atom, scope)` to `(next, event: ResolveEvent)` where `ResolveEvent` is a discriminated union (`{ kind: "atom" }` or `{ kind: "resource" }`).
10
+
11
+ New `resource({ deps, factory })` primitive for execution-level dependencies (logger, transaction, trace span). Resources are resolved fresh per execution chain, shared via seek-up within nested execs, and cleaned up with `ctx.onClose()`.
12
+
13
+ Migration: update `wrapResolve(next, atom, scope)` → `wrapResolve(next, event)`, dispatch on `event.kind`.
14
+
15
+ ## 1.11.4
16
+
17
+ ### Patch Changes
18
+
19
+ - a3ae2b7: Replace text glossary with mermaid sequence diagrams in documentation
20
+
21
+ - README.md now uses visual diagrams for composition, atom lifecycle, tag resolution, type utilities, and API surface
22
+ - PATTERNS.md converted all usage patterns to sequence diagrams for clarity
23
+
3
24
  ## 1.11.3
4
25
 
5
26
  ### Patch Changes
package/MIGRATION.md CHANGED
@@ -236,8 +236,8 @@ const loggingExt = extension({
236
236
  // AFTER (lite) - Simplified 4-hook interface
237
237
  const loggingExt: Lite.Extension = {
238
238
  name: 'logging',
239
- wrapResolve: async (next, atom, scope) => {
240
- console.log('Resolving atom...')
239
+ wrapResolve: async (next, event) => {
240
+ console.log('Resolving:', event.kind, event.target)
241
241
  const result = await next()
242
242
  console.log('Resolved:', result)
243
243
  return result
package/PATTERNS.md CHANGED
@@ -1,380 +1,281 @@
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(result)
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(result => cleanup)
29
+ App->>Ctx: ctx.close(result?)
30
+ Ctx->>Ctx: run onClose(CloseResult) 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 atoms/flows — logging, auth, tracing, transaction boundaries. Extensions register `onClose(CloseResult)` to finalize based on success or failure.
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 Ctx as ExecutionContext
44
+ participant Flow
80
45
 
81
- **Combines:** Command + Composite + Resource Management
46
+ App->>Scope: createScope({ extensions: [ext] })
47
+ Scope->>Ext: ext.init(scope)
48
+ App->>Scope: await scope.ready
49
+
50
+ App->>Scope: resolve(atom)
51
+ Scope->>Ext: wrapResolve(next, { kind: "atom", target, scope })
52
+ Ext->>Ext: before logic
53
+ Ext->>Atom: next()
54
+ Atom-->>Ext: value
55
+ Ext->>Ext: after logic
56
+ Ext-->>Scope: value
57
+
58
+ App->>Ctx: ctx.exec({ flow })
59
+ Ctx->>Ext: wrapExec(next, flow, childCtx)
60
+ Ext->>Ext: ctx.onClose(result => result.ok ? commit : rollback)
61
+ Ext->>Flow: next()
62
+ Flow-->>Ext: output
63
+ Ext-->>Ctx: output
64
+
65
+ App->>Scope: dispose()
66
+ Scope->>Ext: ext.dispose(scope)
67
+ ```
82
68
 
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) |
69
+ ### Scoped Isolation + Testing
88
70
 
89
- **Deps Resolution:**
71
+ Swap implementations and isolate tenants/tests.
90
72
 
91
73
  ```mermaid
92
- flowchart TB
93
- subgraph Flow["flow({ deps, factory })"]
94
- Deps["deps: { db: dbAtom, userId: tags.required(userIdTag) }"]
95
- end
74
+ sequenceDiagram
75
+ participant Test
76
+ participant Scope
77
+ participant Atom
96
78
 
97
- subgraph Resolution
98
- Deps --> AtomPath["Atom deps"]
99
- Deps --> TagPath["Tag deps (TagExecutor)"]
79
+ Test->>Scope: createScope({ presets: [preset(dbAtom, mockDb)], tags: [tenantTag(id)] })
80
+ Test->>Scope: resolve(dbAtom)
81
+ Scope-->>Test: mockDb (not real db)
100
82
 
101
- AtomPath --> Scope["Scope.resolve()"]
102
- Scope --> |"cached in scope"| ResolvedAtom["db instance"]
83
+ Test->>Scope: createContext()
84
+ Scope-->>Test: ctx with tenantTag
85
+ ```
103
86
 
104
- TagPath --> CtxHierarchy["ctx.data.seekTag()"]
105
- CtxHierarchy --> |"traverses parent chain"| ResolvedTag["userId value"]
106
- end
87
+ ## B. Advanced Client/State Usage
107
88
 
108
- subgraph Factory["factory(ctx, { db, userId })"]
109
- ResolvedAtom --> DepsObj["deps object"]
110
- ResolvedTag --> DepsObj
111
- end
112
- ```
89
+ ### Controller Reactivity
113
90
 
114
- **Service Invocation:**
91
+ Client-side state with lifecycle hooks and invalidation.
115
92
 
116
93
  ```mermaid
117
94
  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
- ```
95
+ participant App
96
+ participant Scope
97
+ participant Ctrl as Controller
98
+ participant Atom
137
99
 
138
- **Cleanup Pattern:**
100
+ App->>Scope: controller(atom)
101
+ Scope-->>App: ctrl
139
102
 
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
- ```
103
+ App->>Ctrl: ctrl.on('resolving' | 'resolved' | '*', listener)
104
+ Ctrl-->>App: unsubscribe
161
105
 
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
106
+ App->>Ctrl: ctrl.get()
107
+ Ctrl-->>App: current value
168
108
 
169
- **Primitives:** `flow({ deps })`, `tags.required()`, `tags.optional()`, `tags.all()`, `ctx.exec()`, `ctx.onClose()`
109
+ App->>Ctrl: ctrl.set(newValue)
110
+ Ctrl->>Ctrl: notify listeners
170
111
 
171
- ---
112
+ App->>Ctrl: ctrl.update(v => v + 1)
113
+ Ctrl->>Ctrl: notify listeners
172
114
 
173
- ### Request Pipeline
115
+ App->>Ctrl: ctrl.invalidate()
116
+ Ctrl->>Atom: re-run factory
117
+ Ctrl->>Ctrl: notify listeners
118
+ ```
174
119
 
175
- **Combines:** Command + Interceptor + Context Object
120
+ ### Ambient Context (Tags)
176
121
 
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) |
122
+ Propagate values without wiring parameters. Tags serve two roles: scope-level config (consumed by atoms via `tags.required()`) and per-context ambient data (requestId, locale). Use `tags.required()` in deps to declare that an atom or flow needs an ambient value (e.g., a transacted connection) — extensions or context setup provide the value, the consumer just depends on it.
182
123
 
183
124
  ```mermaid
184
125
  sequenceDiagram
185
- participant Client
126
+ participant App
186
127
  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
- ```
128
+ participant Atom
129
+ participant Ctx as ExecutionContext
130
+ participant ChildCtx
131
+ participant Data as ctx.data
213
132
 
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
133
+ App->>Scope: createScope({ tags: [configTag(cfg)] })
134
+ App->>Scope: resolve(dbAtom)
135
+ Note right of Atom: deps: { config: tags.required(configTag) }
136
+ Scope->>Atom: factory(ctx, { config: cfg })
219
137
 
220
- **Primitives:** `flow()`, `Extension.wrapExec`, `tag()`, `ctx.exec()`, `ctx.parent`, `ctx.data`
138
+ App->>Scope: scope.createContext({ tags: [requestIdTag(rid)] })
139
+ Scope-->>App: ctx
221
140
 
222
- ---
141
+ App->>Ctx: ctx.exec({ flow, tags: [localeTag('en')] })
142
+ Ctx->>ChildCtx: create with merged tags
223
143
 
224
- ### Scoped Isolation
144
+ ChildCtx->>Data: ctx.data.seekTag(requestIdTag)
145
+ Data-->>ChildCtx: rid (from parent)
225
146
 
226
- **Combines:** IoC Container + Strategy + Composite
147
+ ChildCtx->>Data: ctx.data.getTag(localeTag)
148
+ Data-->>ChildCtx: 'en'
149
+ ```
227
150
 
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) |
151
+ ### Derived State (Select)
152
+
153
+ Subscribe to a slice of atom state with custom equality.
233
154
 
234
155
  ```mermaid
235
156
  sequenceDiagram
236
157
  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
158
+ participant Scope
159
+ participant Handle as SelectHandle
160
+ participant Atom
271
161
 
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
162
+ App->>Scope: select(atom, v => v.count, { eq: shallowEqual })
163
+ Scope-->>App: handle
276
164
 
277
- **Primitives:** `createScope()`, `preset()`, `tag()`, `createContext()`, scope `tags` option
165
+ App->>Handle: handle.get()
166
+ Handle-->>App: selected value
278
167
 
279
- ---
168
+ App->>Handle: handle.subscribe(listener)
169
+ Handle-->>App: unsubscribe
280
170
 
281
- ## Foundational Patterns
171
+ Note over Atom,Handle: atom changes
172
+ Handle->>Handle: eq(prev, next)?
173
+ Handle->>App: notify if changed
174
+ ```
282
175
 
283
- ### IoC Container
176
+ ### Service Pattern
284
177
 
285
- **GoF:** Inversion of Control / Dependency Injection Container
178
+ Constrain atom methods to ExecutionContext-first signature. Always invoke via `ctx.exec` so a child context is created — extensions can observe the call, and cleanup is scoped.
286
179
 
287
180
  ```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
181
+ sequenceDiagram
182
+ participant App
183
+ participant Scope
184
+ participant Ctx as ExecutionContext
185
+ participant Child as ChildContext
186
+ participant Svc as Service Atom
187
+
188
+ App->>Scope: resolve(userService)
189
+ Scope-->>App: { getUser, updateUser }
190
+
191
+ App->>Ctx: ctx.exec({ fn: svc.getUser, params: [userId] })
192
+ Ctx->>Child: create child context
193
+ Child->>Svc: getUser(childCtx, userId)
194
+ Svc-->>Child: user
195
+ Child->>Child: close(result)
196
+ Child-->>App: user
301
197
  ```
302
198
 
303
- **Primitives:** `createScope()`, `atom()`, `deps`, `scope.resolve()`
199
+ ### Typed Flow Input
304
200
 
305
- **Characteristics:** Lazy resolution, automatic caching, dependency graph traversal, circular dependency detection.
306
-
307
- ---
308
-
309
- ### Observer
310
-
311
- **GoF:** Observer Pattern with State Machine
201
+ Type flow input without runtime parsing overhead.
312
202
 
313
203
  ```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
204
+ sequenceDiagram
205
+ participant App
206
+ participant Ctx as ExecutionContext
207
+ participant Child as ChildContext
208
+ participant Flow
209
+
210
+ Note over Flow: flow({ parse: typed<T>(), factory })
211
+ App->>Ctx: ctx.exec({ flow, input: typedInput })
212
+ Ctx->>Child: create child (input passed through, no parse)
213
+ Child->>Flow: factory(childCtx, deps) with ctx.input: T
214
+ Flow-->>Child: output
215
+ Child->>Child: close(result)
216
+ Child-->>App: output
324
217
  ```
325
218
 
326
- **Primitives:** `controller()`, `ctrl.on('resolved' | 'resolving' | '*')`, `ctrl.invalidate()`
219
+ ### Controller as Dependency
327
220
 
328
- **Characteristics:** State-filtered subscriptions, LIFO cleanup before re-resolution, sequential invalidation chains with loop detection.
221
+ Receive reactive handle instead of resolved value in atom/flow deps.
329
222
 
330
- ---
223
+ ```mermaid
224
+ sequenceDiagram
225
+ participant Scope
226
+ participant AtomA as serverAtom
227
+ participant Ctrl as Controller
228
+ participant AtomB as configAtom
229
+
230
+ Scope->>AtomA: resolve(serverAtom)
231
+ Note over AtomA: deps: { cfg: controller(configAtom, { resolve: true }) }
232
+ AtomA->>Scope: resolve configAtom first
233
+ Scope-->>Ctrl: ctrl (already resolved)
234
+ AtomA->>AtomA: factory(ctx, { cfg: ctrl })
235
+ AtomA->>Ctrl: ctrl.on('resolved', () => ctx.invalidate())
236
+ Note over AtomA: react to config changes
237
+ ```
331
238
 
332
- ### Context Object
239
+ ### Inline Function Execution
333
240
 
334
- **GoF:** Context Object / Ambient Context
241
+ Execute ad-hoc logic within context without defining a flow.
335
242
 
336
243
  ```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
244
+ sequenceDiagram
245
+ participant App
246
+ participant Ctx as ExecutionContext
351
247
 
352
- subgraph Extract
353
- Merged --> Required[tags.required]
354
- Merged --> Optional[tags.optional]
355
- Merged --> All[tags.all]
356
- end
248
+ App->>Ctx: ctx.exec({ name, fn, params, tags })
249
+ Ctx->>Ctx: create childCtx (name + tags)
250
+ Ctx->>Ctx: fn(childCtx, ...params)
251
+ Ctx->>Ctx: childCtx.close(result)
252
+ Ctx-->>App: output
253
+ Note right of Ctx: name makes sub-executions observable by extensions
357
254
  ```
358
255
 
359
- **Primitives:** `tag()`, `tags.required()`, `tags.optional()`, `tags.all()`, `Tagged`
256
+ ### Atom Retention (GC)
360
257
 
361
- **Characteristics:** Implicit propagation through execution layers, type-safe extraction, merge precedence (exec > context > scope > flow).
258
+ Control when atoms are garbage collected or kept alive indefinitely.
362
259
 
363
- ---
260
+ ```mermaid
261
+ sequenceDiagram
262
+ participant App
263
+ participant Scope
264
+ participant Atom
364
265
 
365
- ### Strategy
266
+ App->>Scope: createScope({ gc: { enabled: true, graceMs: 3000 } })
366
267
 
367
- **GoF:** Strategy Pattern
268
+ App->>Scope: resolve(atom)
269
+ Scope-->>App: value
270
+ Note over Scope: no refs → start grace timer
368
271
 
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()`
272
+ alt keepAlive: true
273
+ Note over Atom: never GC'd
274
+ else graceMs expires
275
+ Scope->>Atom: release()
276
+ Atom->>Atom: run cleanups
277
+ end
379
278
 
380
- **Characteristics:** Swap implementations at scope creation, value injection bypasses factory, atom redirection for mock substitution.
279
+ App->>Scope: flush()
280
+ Note over Scope: wait all pending
281
+ ```