@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 +21 -0
- package/MIGRATION.md +2 -2
- package/PATTERNS.md +199 -298
- package/README.md +143 -404
- package/dist/cli.cjs +335 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +337 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +124 -12
- package/dist/index.d.cts +97 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +97 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +122 -13
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
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,
|
|
240
|
-
console.log('Resolving
|
|
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
|
-
#
|
|
1
|
+
# Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Usage patterns as sequences. For API details, see `packages/lite/dist/index.d.mts`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## A. Fundamental Usage
|
|
6
6
|
|
|
7
7
|
### Request Lifecycle
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
27
|
-
participant
|
|
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
|
-
|
|
36
|
-
App->>Scope: createContext({ tags
|
|
18
|
+
App->>Scope: createScope()
|
|
19
|
+
App->>Scope: scope.createContext({ tags })
|
|
37
20
|
Scope-->>App: ctx
|
|
38
21
|
|
|
39
|
-
App->>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Swap implementations and isolate tenants/tests.
|
|
90
72
|
|
|
91
73
|
```mermaid
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
sequenceDiagram
|
|
75
|
+
participant Test
|
|
76
|
+
participant Scope
|
|
77
|
+
participant Atom
|
|
96
78
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
83
|
+
Test->>Scope: createContext()
|
|
84
|
+
Scope-->>Test: ctx with tenantTag
|
|
85
|
+
```
|
|
103
86
|
|
|
104
|
-
|
|
105
|
-
CtxHierarchy --> |"traverses parent chain"| ResolvedTag["userId value"]
|
|
106
|
-
end
|
|
87
|
+
## B. Advanced Client/State Usage
|
|
107
88
|
|
|
108
|
-
|
|
109
|
-
ResolvedAtom --> DepsObj["deps object"]
|
|
110
|
-
ResolvedTag --> DepsObj
|
|
111
|
-
end
|
|
112
|
-
```
|
|
89
|
+
### Controller Reactivity
|
|
113
90
|
|
|
114
|
-
|
|
91
|
+
Client-side state with lifecycle hooks and invalidation.
|
|
115
92
|
|
|
116
93
|
```mermaid
|
|
117
94
|
sequenceDiagram
|
|
118
|
-
participant
|
|
119
|
-
participant
|
|
120
|
-
participant
|
|
121
|
-
participant
|
|
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
|
-
|
|
100
|
+
App->>Scope: controller(atom)
|
|
101
|
+
Scope-->>App: ctrl
|
|
139
102
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
App->>Ctrl: ctrl.invalidate()
|
|
116
|
+
Ctrl->>Atom: re-run factory
|
|
117
|
+
Ctrl->>Ctrl: notify listeners
|
|
118
|
+
```
|
|
174
119
|
|
|
175
|
-
|
|
120
|
+
### Ambient Context (Tags)
|
|
176
121
|
|
|
177
|
-
|
|
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
|
|
126
|
+
participant App
|
|
186
127
|
participant Scope
|
|
187
|
-
participant
|
|
188
|
-
participant
|
|
189
|
-
participant
|
|
190
|
-
participant
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
ChildCtx->>Data: ctx.data.seekTag(requestIdTag)
|
|
145
|
+
Data-->>ChildCtx: rid (from parent)
|
|
225
146
|
|
|
226
|
-
|
|
147
|
+
ChildCtx->>Data: ctx.data.getTag(localeTag)
|
|
148
|
+
Data-->>ChildCtx: 'en'
|
|
149
|
+
```
|
|
227
150
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
238
|
-
participant
|
|
239
|
-
participant
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
+
Note over Atom,Handle: atom changes
|
|
172
|
+
Handle->>Handle: eq(prev, next)?
|
|
173
|
+
Handle->>App: notify if changed
|
|
174
|
+
```
|
|
282
175
|
|
|
283
|
-
###
|
|
176
|
+
### Service Pattern
|
|
284
177
|
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
Scope
|
|
296
|
-
Scope
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
199
|
+
### Typed Flow Input
|
|
304
200
|
|
|
305
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
219
|
+
### Controller as Dependency
|
|
327
220
|
|
|
328
|
-
|
|
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
|
-
###
|
|
239
|
+
### Inline Function Execution
|
|
333
240
|
|
|
334
|
-
|
|
241
|
+
Execute ad-hoc logic within context without defining a flow.
|
|
335
242
|
|
|
336
243
|
```mermaid
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
256
|
+
### Atom Retention (GC)
|
|
360
257
|
|
|
361
|
-
|
|
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
|
-
|
|
266
|
+
App->>Scope: createScope({ gc: { enabled: true, graceMs: 3000 } })
|
|
366
267
|
|
|
367
|
-
|
|
268
|
+
App->>Scope: resolve(atom)
|
|
269
|
+
Scope-->>App: value
|
|
270
|
+
Note over Scope: no refs → start grace timer
|
|
368
271
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
279
|
+
App->>Scope: flush()
|
|
280
|
+
Note over Scope: wait all pending
|
|
281
|
+
```
|