@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.
- package/CHANGELOG.md +9 -0
- package/PATTERNS.md +182 -299
- package/README.md +166 -378
- 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
|
-
#
|
|
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()
|
|
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(cleanup)
|
|
29
|
+
App->>Ctx: ctx.close()
|
|
30
|
+
Ctx->>Ctx: run cleanups (LIFO)
|
|
31
|
+
```
|
|
74
32
|
|
|
75
|
-
|
|
33
|
+
### Extensions Pipeline
|
|
76
34
|
|
|
77
|
-
|
|
35
|
+
Observe and wrap timing for atoms/flows (logging, auth, tracing).
|
|
78
36
|
|
|
79
|
-
|
|
37
|
+
```mermaid
|
|
38
|
+
sequenceDiagram
|
|
39
|
+
participant App
|
|
40
|
+
participant Scope
|
|
41
|
+
participant Ext as Extension
|
|
42
|
+
participant Atom
|
|
43
|
+
participant Flow
|
|
80
44
|
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
Swap implementations and isolate tenants/tests.
|
|
90
70
|
|
|
91
71
|
```mermaid
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
72
|
+
sequenceDiagram
|
|
73
|
+
participant Test
|
|
74
|
+
participant Scope
|
|
75
|
+
participant Atom
|
|
96
76
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
81
|
+
Test->>Scope: createContext()
|
|
82
|
+
Scope-->>Test: ctx with tenantTag
|
|
83
|
+
```
|
|
103
84
|
|
|
104
|
-
|
|
105
|
-
CtxHierarchy --> |"traverses parent chain"| ResolvedTag["userId value"]
|
|
106
|
-
end
|
|
85
|
+
## B. Advanced Client/State Usage
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
ResolvedAtom --> DepsObj["deps object"]
|
|
110
|
-
ResolvedTag --> DepsObj
|
|
111
|
-
end
|
|
112
|
-
```
|
|
87
|
+
### Controller Reactivity
|
|
113
88
|
|
|
114
|
-
|
|
89
|
+
Client-side state with lifecycle hooks and invalidation.
|
|
115
90
|
|
|
116
91
|
```mermaid
|
|
117
92
|
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
|
-
```
|
|
93
|
+
participant App
|
|
94
|
+
participant Scope
|
|
95
|
+
participant Ctrl as Controller
|
|
96
|
+
participant Atom
|
|
137
97
|
|
|
138
|
-
|
|
98
|
+
App->>Scope: controller(atom)
|
|
99
|
+
Scope-->>App: ctrl
|
|
139
100
|
|
|
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
|
-
```
|
|
101
|
+
App->>Ctrl: ctrl.on('resolving' | 'resolved' | '*', listener)
|
|
102
|
+
Ctrl-->>App: unsubscribe
|
|
161
103
|
|
|
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
|
|
104
|
+
App->>Ctrl: ctrl.get()
|
|
105
|
+
Ctrl-->>App: current value
|
|
168
106
|
|
|
169
|
-
|
|
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
|
-
|
|
113
|
+
App->>Ctrl: ctrl.invalidate()
|
|
114
|
+
Ctrl->>Atom: re-run factory
|
|
115
|
+
Ctrl->>Ctrl: notify listeners
|
|
116
|
+
```
|
|
174
117
|
|
|
175
|
-
|
|
118
|
+
### Ambient Context (Tags)
|
|
176
119
|
|
|
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) |
|
|
120
|
+
Propagate state without wiring parameters (app shell, user, locale).
|
|
182
121
|
|
|
183
122
|
```mermaid
|
|
184
123
|
sequenceDiagram
|
|
185
|
-
participant
|
|
186
|
-
participant
|
|
187
|
-
participant
|
|
188
|
-
participant
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
ChildCtx->>Data: ctx.data.getTag(localeTag)
|
|
137
|
+
Data-->>ChildCtx: 'en'
|
|
138
|
+
```
|
|
225
139
|
|
|
226
|
-
|
|
140
|
+
### Derived State (Select)
|
|
227
141
|
|
|
228
|
-
|
|
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
|
|
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
|
|
147
|
+
participant Scope
|
|
148
|
+
participant Handle as SelectHandle
|
|
149
|
+
participant Atom
|
|
271
150
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
Note over Atom,Handle: atom changes
|
|
161
|
+
Handle->>Handle: eq(prev, next)?
|
|
162
|
+
Handle->>App: notify if changed
|
|
163
|
+
```
|
|
282
164
|
|
|
283
|
-
###
|
|
165
|
+
### Service Pattern
|
|
284
166
|
|
|
285
|
-
|
|
167
|
+
Constrain atom methods to ExecutionContext-first signature for tracing/auth.
|
|
286
168
|
|
|
287
169
|
```mermaid
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
185
|
+
### Typed Flow Input
|
|
310
186
|
|
|
311
|
-
|
|
187
|
+
Type flow input without runtime parsing overhead.
|
|
312
188
|
|
|
313
189
|
```mermaid
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
202
|
+
### Controller as Dependency
|
|
327
203
|
|
|
328
|
-
|
|
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
|
-
###
|
|
222
|
+
### Inline Function Execution
|
|
333
223
|
|
|
334
|
-
|
|
224
|
+
Execute ad-hoc logic within context without defining a flow.
|
|
335
225
|
|
|
336
226
|
```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
|
|
227
|
+
sequenceDiagram
|
|
228
|
+
participant App
|
|
229
|
+
participant Ctx as ExecutionContext
|
|
351
230
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
238
|
+
### Atom Retention (GC)
|
|
360
239
|
|
|
361
|
-
|
|
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
|
-
|
|
248
|
+
App->>Scope: createScope({ gc: { enabled: true, graceMs: 3000 } })
|
|
366
249
|
|
|
367
|
-
|
|
250
|
+
App->>Scope: resolve(atom)
|
|
251
|
+
Scope-->>App: value
|
|
252
|
+
Note over Scope: no refs → start grace timer
|
|
368
253
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
+
Lightweight effect system for TypeScript: scoped lifecycles, tagged context, and opt‑in reactivity.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
Scope
|
|
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
|
-
##
|
|
74
|
+
## Composition
|
|
40
75
|
|
|
41
76
|
```mermaid
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Context-->>User: output
|
|
86
|
+
subgraph Wrappers
|
|
87
|
+
typed["typed<T>()"]
|
|
88
|
+
ctrlDep["controller(atom, { resolve? })"]
|
|
89
|
+
tagExec["tags.required/optional/all(tag)"]
|
|
90
|
+
end
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
flow --> typed
|
|
93
|
+
atom --> ctrlDep
|
|
94
|
+
tag --> tagExec
|
|
82
95
|
```
|
|
83
96
|
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
Tags are auto-populated into `ctx.data` and resolved via `seekTag()`:
|
|
97
|
+
## Context Data
|
|
87
98
|
|
|
88
99
|
```mermaid
|
|
89
|
-
|
|
90
|
-
subgraph
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
##
|
|
108
|
+
## Atom Lifecycle (AtomState)
|
|
107
109
|
|
|
108
110
|
```mermaid
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
130
|
+
App->>Tag: tag({ label, default? })
|
|
131
|
+
Tag-->>App: Tag<T>
|
|
213
132
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- Pass via `createScope({ presets: [...] })`
|
|
133
|
+
App->>Tag: tag(value)
|
|
134
|
+
Tag-->>App: Tagged<T>
|
|
217
135
|
|
|
218
|
-
|
|
136
|
+
App->>Source: attach Tagged[] to atom/flow/ctx
|
|
219
137
|
|
|
220
|
-
|
|
138
|
+
App->>Tag: tag.get(source)
|
|
139
|
+
Tag->>Source: find first match
|
|
140
|
+
Source-->>Tag: value or throw
|
|
221
141
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
146
|
+
App->>Tag: tag.collect(source)
|
|
147
|
+
Tag->>Source: gather all matches
|
|
148
|
+
Source-->>Tag: T[]
|
|
229
149
|
|
|
230
|
-
|
|
150
|
+
App->>Tag: tag.atoms()
|
|
151
|
+
Tag-->>App: Atom[] with this tag
|
|
152
|
+
```
|
|
231
153
|
|
|
232
|
-
|
|
154
|
+
## Type Utilities
|
|
233
155
|
|
|
234
156
|
```mermaid
|
|
235
|
-
|
|
236
|
-
subgraph "
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
157
|
+
graph LR
|
|
158
|
+
subgraph "Lite.Utils"
|
|
159
|
+
AtomValue["AtomValue<A>"]
|
|
160
|
+
FlowOutput["FlowOutput<F>"]
|
|
161
|
+
FlowInput["FlowInput<F>"]
|
|
162
|
+
TagValue["TagValue<T>"]
|
|
163
|
+
DepsOf["DepsOf<A|F>"]
|
|
164
|
+
ControllerValue["ControllerValue<C>"]
|
|
165
|
+
Simplify["Simplify<T>"]
|
|
166
|
+
AtomType["AtomType<T, D>"]
|
|
167
|
+
FlowType["FlowType<O, I, D>"]
|
|
240
168
|
end
|
|
241
169
|
|
|
242
|
-
subgraph "
|
|
243
|
-
|
|
170
|
+
subgraph "Type Guards"
|
|
171
|
+
isAtom
|
|
172
|
+
isFlow
|
|
173
|
+
isTag
|
|
174
|
+
isTagged
|
|
175
|
+
isPreset
|
|
176
|
+
isControllerDep
|
|
177
|
+
isTagExecutor
|
|
244
178
|
end
|
|
245
179
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
356
|
-
participant
|
|
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
|
-
|
|
194
|
+
App->>Registry: getAllTags()
|
|
195
|
+
Registry-->>App: Tag[] (all live tags)
|
|
398
196
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
202
|
+
## Additional Exports
|
|
407
203
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|