@pumped-fn/lite 1.11.4 → 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 +12 -0
- package/MIGRATION.md +2 -2
- package/PATTERNS.md +46 -28
- package/README.md +138 -187
- 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,17 @@
|
|
|
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
|
+
|
|
3
15
|
## 1.11.4
|
|
4
16
|
|
|
5
17
|
### 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
|
@@ -22,17 +22,17 @@ sequenceDiagram
|
|
|
22
22
|
App->>Ctx: ctx.exec({ flow, input, tags })
|
|
23
23
|
Ctx->>Flow: factory(childCtx, deps)
|
|
24
24
|
Flow-->>Ctx: output
|
|
25
|
-
Ctx->>Ctx: childCtx.close()
|
|
25
|
+
Ctx->>Ctx: childCtx.close(result)
|
|
26
26
|
Ctx-->>App: output
|
|
27
27
|
|
|
28
|
-
App->>Ctx: ctx.onClose(cleanup)
|
|
29
|
-
App->>Ctx: ctx.close()
|
|
30
|
-
Ctx->>Ctx: run
|
|
28
|
+
App->>Ctx: ctx.onClose(result => cleanup)
|
|
29
|
+
App->>Ctx: ctx.close(result?)
|
|
30
|
+
Ctx->>Ctx: run onClose(CloseResult) LIFO
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### Extensions Pipeline
|
|
34
34
|
|
|
35
|
-
Observe and wrap
|
|
35
|
+
Observe and wrap atoms/flows — logging, auth, tracing, transaction boundaries. Extensions register `onClose(CloseResult)` to finalize based on success or failure.
|
|
36
36
|
|
|
37
37
|
```mermaid
|
|
38
38
|
sequenceDiagram
|
|
@@ -40,6 +40,7 @@ sequenceDiagram
|
|
|
40
40
|
participant Scope
|
|
41
41
|
participant Ext as Extension
|
|
42
42
|
participant Atom
|
|
43
|
+
participant Ctx as ExecutionContext
|
|
43
44
|
participant Flow
|
|
44
45
|
|
|
45
46
|
App->>Scope: createScope({ extensions: [ext] })
|
|
@@ -47,18 +48,19 @@ sequenceDiagram
|
|
|
47
48
|
App->>Scope: await scope.ready
|
|
48
49
|
|
|
49
50
|
App->>Scope: resolve(atom)
|
|
50
|
-
Scope->>Ext: wrapResolve(next, atom, scope)
|
|
51
|
+
Scope->>Ext: wrapResolve(next, { kind: "atom", target, scope })
|
|
51
52
|
Ext->>Ext: before logic
|
|
52
53
|
Ext->>Atom: next()
|
|
53
54
|
Atom-->>Ext: value
|
|
54
55
|
Ext->>Ext: after logic
|
|
55
56
|
Ext-->>Scope: value
|
|
56
57
|
|
|
57
|
-
App->>
|
|
58
|
-
|
|
58
|
+
App->>Ctx: ctx.exec({ flow })
|
|
59
|
+
Ctx->>Ext: wrapExec(next, flow, childCtx)
|
|
60
|
+
Ext->>Ext: ctx.onClose(result => result.ok ? commit : rollback)
|
|
59
61
|
Ext->>Flow: next()
|
|
60
62
|
Flow-->>Ext: output
|
|
61
|
-
Ext-->>
|
|
63
|
+
Ext-->>Ctx: output
|
|
62
64
|
|
|
63
65
|
App->>Scope: dispose()
|
|
64
66
|
Scope->>Ext: ext.dispose(scope)
|
|
@@ -117,21 +119,30 @@ sequenceDiagram
|
|
|
117
119
|
|
|
118
120
|
### Ambient Context (Tags)
|
|
119
121
|
|
|
120
|
-
Propagate
|
|
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.
|
|
121
123
|
|
|
122
124
|
```mermaid
|
|
123
125
|
sequenceDiagram
|
|
124
126
|
participant App
|
|
127
|
+
participant Scope
|
|
128
|
+
participant Atom
|
|
125
129
|
participant Ctx as ExecutionContext
|
|
126
130
|
participant ChildCtx
|
|
127
131
|
participant Data as ctx.data
|
|
128
132
|
|
|
129
|
-
App->>
|
|
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 })
|
|
137
|
+
|
|
138
|
+
App->>Scope: scope.createContext({ tags: [requestIdTag(rid)] })
|
|
139
|
+
Scope-->>App: ctx
|
|
140
|
+
|
|
130
141
|
App->>Ctx: ctx.exec({ flow, tags: [localeTag('en')] })
|
|
131
142
|
Ctx->>ChildCtx: create with merged tags
|
|
132
143
|
|
|
133
|
-
ChildCtx->>Data: ctx.data.seekTag(
|
|
134
|
-
Data-->>ChildCtx:
|
|
144
|
+
ChildCtx->>Data: ctx.data.seekTag(requestIdTag)
|
|
145
|
+
Data-->>ChildCtx: rid (from parent)
|
|
135
146
|
|
|
136
147
|
ChildCtx->>Data: ctx.data.getTag(localeTag)
|
|
137
148
|
Data-->>ChildCtx: 'en'
|
|
@@ -164,22 +175,25 @@ sequenceDiagram
|
|
|
164
175
|
|
|
165
176
|
### Service Pattern
|
|
166
177
|
|
|
167
|
-
Constrain atom methods to ExecutionContext-first signature
|
|
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.
|
|
168
179
|
|
|
169
180
|
```mermaid
|
|
170
181
|
sequenceDiagram
|
|
171
182
|
participant App
|
|
172
183
|
participant Scope
|
|
173
184
|
participant Ctx as ExecutionContext
|
|
185
|
+
participant Child as ChildContext
|
|
174
186
|
participant Svc as Service Atom
|
|
175
187
|
|
|
176
188
|
App->>Scope: resolve(userService)
|
|
177
189
|
Scope-->>App: { getUser, updateUser }
|
|
178
190
|
|
|
179
|
-
App->>Ctx: svc.getUser
|
|
180
|
-
Ctx->>
|
|
181
|
-
Svc
|
|
182
|
-
|
|
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
|
|
183
197
|
```
|
|
184
198
|
|
|
185
199
|
### Typed Flow Input
|
|
@@ -189,14 +203,17 @@ Type flow input without runtime parsing overhead.
|
|
|
189
203
|
```mermaid
|
|
190
204
|
sequenceDiagram
|
|
191
205
|
participant App
|
|
206
|
+
participant Ctx as ExecutionContext
|
|
207
|
+
participant Child as ChildContext
|
|
192
208
|
participant Flow
|
|
193
|
-
participant Ctx
|
|
194
209
|
|
|
195
210
|
Note over Flow: flow({ parse: typed<T>(), factory })
|
|
196
|
-
App->>
|
|
197
|
-
|
|
198
|
-
Flow
|
|
199
|
-
|
|
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
|
|
200
217
|
```
|
|
201
218
|
|
|
202
219
|
### Controller as Dependency
|
|
@@ -228,11 +245,12 @@ sequenceDiagram
|
|
|
228
245
|
participant App
|
|
229
246
|
participant Ctx as ExecutionContext
|
|
230
247
|
|
|
231
|
-
App->>Ctx: ctx.exec({
|
|
232
|
-
Ctx->>Ctx: create childCtx
|
|
233
|
-
Ctx->>Ctx: fn(childCtx,
|
|
234
|
-
Ctx->>Ctx: childCtx.close()
|
|
235
|
-
Ctx-->>App:
|
|
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
|
|
236
254
|
```
|
|
237
255
|
|
|
238
256
|
### Atom Retention (GC)
|
package/README.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# @pumped-fn/lite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
  
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Scoped Ambient State** for TypeScript — a scope-local atom graph with explicit dependencies and opt-in reactivity.
|
|
6
|
+
|
|
7
|
+
State lives in the scope, not in the component tree. Handlers and components observe — they don't own or construct dependencies. The same graph works across React, server handlers, background jobs, and tests.
|
|
8
|
+
|
|
9
|
+
**Frontend** — atoms form a reactive dependency graph (`homeData <- auth`). UI subscribes via controllers; auth changes cascade to dependents automatically. Components are projections of state, not owners.
|
|
10
|
+
|
|
11
|
+
**Backend** — atoms are infrastructure singletons (db pool, cache). Runtime config enters the scope as tags; atoms consume it via `tags.required()`. Contexts bound per request carry tags (tenantId, traceId) without parameter drilling. Extensions wrap every resolve/exec for logging, tracing, auth. Cleanup is guaranteed.
|
|
12
|
+
|
|
13
|
+
**Both** — presets swap any atom/flow for testing or multi-tenant isolation. Tags carry runtime config; presets replace implementations. No mocks, no test-only code paths.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npx @pumped-fn/lite # CLI reference
|
|
17
|
+
npx @pumped-fn/lite primitives # API
|
|
18
|
+
npx @pumped-fn/lite diagrams # mermaid source
|
|
19
|
+
```
|
|
6
20
|
|
|
7
21
|
## How It Works
|
|
8
22
|
|
|
@@ -11,220 +25,157 @@ sequenceDiagram
|
|
|
11
25
|
participant App
|
|
12
26
|
participant Scope
|
|
13
27
|
participant Ext as Extension
|
|
14
|
-
participant Ctx as ExecutionContext
|
|
15
28
|
participant Atom
|
|
29
|
+
participant Ctx as ExecutionContext
|
|
30
|
+
participant Child as ChildContext
|
|
16
31
|
participant Flow
|
|
17
32
|
participant Ctrl as Controller
|
|
18
33
|
|
|
19
|
-
App
|
|
20
|
-
Scope-->>App: scope (ready)
|
|
34
|
+
Note over App,Ctrl: (*) stable ref — same identity until released
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
Scope
|
|
24
|
-
|
|
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
|
|
36
|
+
%% ── Scope Creation & Extension Init ──
|
|
37
|
+
App->>Scope: createScope({ extensions, presets, tags, gc })
|
|
38
|
+
Scope-->>App: scope (sync return)
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
|
|
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[]
|
|
40
|
+
loop each extension (sequential)
|
|
41
|
+
Scope->>Ext: await ext.init(scope)
|
|
64
42
|
end
|
|
43
|
+
Note right of Scope: all init() done → scope.ready resolves
|
|
44
|
+
Note right of Scope: any init() throws → scope.ready rejects
|
|
45
|
+
Note right of Scope: resolve() auto‑awaits scope.ready
|
|
65
46
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
App
|
|
69
|
-
|
|
70
|
-
App->>Scope: dispose()
|
|
71
|
-
Scope->>Scope: release atoms, run cleanups
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Composition
|
|
47
|
+
%% ── Observers (register before or after resolve) ──
|
|
48
|
+
App->>Scope: scope.on('resolving' | 'resolved' | 'failed', atom, listener)
|
|
49
|
+
Scope-->>App: unsubscribe fn
|
|
50
|
+
Note right of Scope: scope.on listens to AtomState transitions
|
|
75
51
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
%% ── Atom Resolution ──
|
|
53
|
+
Note right of Scope: singletons — created once, reused across contexts. deps can include tags.required()
|
|
54
|
+
App->>Scope: resolve(atom)
|
|
55
|
+
Scope->>Scope: cache hit? → return cached
|
|
56
|
+
alt preset hit
|
|
57
|
+
Scope->>Scope: value → store directly, skip factory
|
|
58
|
+
Scope->>Scope: atom → resolve that atom instead
|
|
59
|
+
Scope->>Scope: ⚡ emit 'resolved' (no 'resolving')
|
|
84
60
|
end
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
Scope->>Scope: state → resolving
|
|
62
|
+
Scope->>Scope: ⚡ emit 'resolving' → scope.on listeners
|
|
63
|
+
Scope->>Ext: wrapResolve(next, { kind: "atom", target, scope })
|
|
64
|
+
Ext->>Atom: next() → factory(ctx, deps)
|
|
65
|
+
Note right of Atom: ctx.cleanup(fn) → stored per atom
|
|
66
|
+
Note right of Atom: cleanups run LIFO on release/invalidate
|
|
67
|
+
Atom-->>Ext: value
|
|
68
|
+
Note right of Ext: ext returns value — may transform or replace
|
|
69
|
+
alt factory succeeds
|
|
70
|
+
Ext-->>Scope: value (*) cached in entry
|
|
71
|
+
Scope->>Scope: state → resolved
|
|
72
|
+
Scope->>Scope: ⚡ emit 'resolved' → scope.on + ctrl.on listeners
|
|
73
|
+
else factory throws
|
|
74
|
+
Atom-->>Scope: error
|
|
75
|
+
Scope->>Scope: state → failed
|
|
76
|
+
Scope->>Scope: ⚡ emit 'failed' → scope.on listeners (ctrl.on '*' only)
|
|
90
77
|
end
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
## Context Data
|
|
79
|
+
%% ── Context Creation ──
|
|
80
|
+
Note right of Scope: HTTP request, job, transaction — groups exec calls with shared tags + guaranteed cleanup
|
|
81
|
+
App->>Scope: scope.createContext({ tags })
|
|
82
|
+
Scope-->>App: ctx
|
|
98
83
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
%% ── Execution ──
|
|
85
|
+
alt ctx.exec({ flow, input, tags })
|
|
86
|
+
Ctx->>Ctx: preset? → flow: re‑exec with replacement / fn: run as factory
|
|
87
|
+
Ctx->>Ctx: flow.parse(input) if defined
|
|
88
|
+
Ctx->>Child: create child (parent = ctx, merged tags)
|
|
89
|
+
Child->>Ext: wrapExec(next, flow, childCtx)
|
|
90
|
+
Ext->>Flow: next() → factory(childCtx, deps)
|
|
91
|
+
Note right of Flow: childCtx.onClose(result: CloseResult) → { ok: true } | { ok: false, error }
|
|
92
|
+
Flow-->>Ext: output
|
|
93
|
+
Note right of Ext: ext returns output — may transform or replace
|
|
94
|
+
Ext-->>Child: output
|
|
95
|
+
else ctx.exec({ name?, fn, params, tags })
|
|
96
|
+
Ctx->>Child: create child (parent = ctx)
|
|
97
|
+
Child->>Ext: wrapExec(next, fn, childCtx)
|
|
98
|
+
Ext->>Child: next() → fn(childCtx, ...params)
|
|
99
|
+
Child-->>Ext: result
|
|
104
100
|
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
## Atom Lifecycle (AtomState)
|
|
109
|
-
|
|
110
|
-
```mermaid
|
|
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()
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Tag Resolution
|
|
123
|
-
|
|
124
|
-
```mermaid
|
|
125
|
-
sequenceDiagram
|
|
126
|
-
participant App
|
|
127
|
-
participant Tag
|
|
128
|
-
participant Source as Atom/Flow/Ctx
|
|
129
|
-
|
|
130
|
-
App->>Tag: tag({ label, default? })
|
|
131
|
-
Tag-->>App: Tag<T>
|
|
132
|
-
|
|
133
|
-
App->>Tag: tag(value)
|
|
134
|
-
Tag-->>App: Tagged<T>
|
|
135
|
-
|
|
136
|
-
App->>Source: attach Tagged[] to atom/flow/ctx
|
|
137
|
-
|
|
138
|
-
App->>Tag: tag.get(source)
|
|
139
|
-
Tag->>Source: find first match
|
|
140
|
-
Source-->>Tag: value or throw
|
|
141
|
-
|
|
142
|
-
App->>Tag: tag.find(source)
|
|
143
|
-
Tag->>Source: find first match
|
|
144
|
-
Source-->>Tag: value or undefined
|
|
145
|
-
|
|
146
|
-
App->>Tag: tag.collect(source)
|
|
147
|
-
Tag->>Source: gather all matches
|
|
148
|
-
Source-->>Tag: T[]
|
|
149
|
-
|
|
150
|
-
App->>Tag: tag.atoms()
|
|
151
|
-
Tag-->>App: Atom[] with this tag
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Type Utilities
|
|
101
|
+
Ctx->>Child: [A] close(result) → run onClose(CloseResult) LIFO
|
|
102
|
+
Child-->>Ctx: output
|
|
103
|
+
Ctx-->>App: output
|
|
155
104
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
105
|
+
%% ── Resource (execution‑scoped) ──
|
|
106
|
+
rect rgb(245, 240, 255)
|
|
107
|
+
Note over App,Ctrl: Resource (per‑execution middleware)
|
|
108
|
+
Note right of Scope: reusable factory resolved fresh per execution chain — logger, transaction, trace span
|
|
109
|
+
|
|
110
|
+
App->>App: resource({ deps, factory })
|
|
111
|
+
App-->>App: Resource definition (inert)
|
|
112
|
+
|
|
113
|
+
Note right of Child: during dep resolution in ctx.exec():
|
|
114
|
+
Note right of Child: seek hierarchy for existing instance
|
|
115
|
+
alt cache hit (seek‑up)
|
|
116
|
+
Child->>Child: reuse instance from parent ✓
|
|
117
|
+
else cache miss
|
|
118
|
+
Child->>Ext: wrapResolve(next, { kind: "resource", target, ctx })
|
|
119
|
+
Ext->>Child: next() → factory(parentCtx, deps)
|
|
120
|
+
Note right of Child: parentCtx.onClose(result) → cleanup registered
|
|
121
|
+
Child-->>Ext: instance stored on parent context
|
|
122
|
+
end
|
|
168
123
|
end
|
|
169
124
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
isControllerDep
|
|
177
|
-
isTagExecutor
|
|
178
|
-
end
|
|
125
|
+
%% ── Reactivity (opt‑in) ──
|
|
126
|
+
rect rgb(240, 248, 255)
|
|
127
|
+
Note over App,Ctrl: Reactivity (opt‑in — atoms are static by default)
|
|
128
|
+
Note right of Scope: live config, UI state, cache invalidation — when values change after initial resolve
|
|
129
|
+
App->>Scope: controller(atom)
|
|
130
|
+
Scope-->>Ctrl: ctrl (*)
|
|
179
131
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
AnyController
|
|
184
|
-
end
|
|
185
|
-
```
|
|
132
|
+
App->>Ctrl: ctrl.on('resolving' | 'resolved' | '*', listener)
|
|
133
|
+
Ctrl-->>App: unsubscribe
|
|
134
|
+
Note right of Ctrl: ctrl.on listens to per‑atom entry events
|
|
186
135
|
|
|
187
|
-
|
|
136
|
+
App->>Ctrl: ctrl.set(v) / ctrl.update(fn)
|
|
137
|
+
Ctrl->>Scope: scheduleInvalidation
|
|
138
|
+
Scope->>Scope: run atom cleanups (LIFO)
|
|
139
|
+
Scope->>Scope: ⚡ emit 'resolving' → scope.on + ctrl.on
|
|
140
|
+
Scope->>Atom: apply new value (skip factory)
|
|
141
|
+
Scope->>Scope: state → resolved
|
|
142
|
+
Scope->>Scope: ⚡ emit 'resolved' → scope.on + ctrl.on
|
|
188
143
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
144
|
+
App->>Ctrl: ctrl.invalidate()
|
|
145
|
+
Ctrl->>Scope: scheduleInvalidation
|
|
146
|
+
Scope->>Scope: run atom cleanups (LIFO)
|
|
147
|
+
Scope->>Scope: ⚡ emit 'resolving' → scope.on + ctrl.on
|
|
148
|
+
Scope->>Atom: re‑run factory
|
|
149
|
+
Scope->>Scope: state → resolved
|
|
150
|
+
Scope->>Scope: ⚡ emit 'resolved' → scope.on + ctrl.on
|
|
193
151
|
|
|
194
|
-
|
|
195
|
-
|
|
152
|
+
App->>Scope: select(atom, selector, { eq })
|
|
153
|
+
Scope-->>App: handle { get, subscribe }
|
|
154
|
+
end
|
|
196
155
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
156
|
+
%% ── Cleanup & Teardown ──
|
|
157
|
+
rect rgb(255, 245, 238)
|
|
158
|
+
Note over App,Scope: Teardown
|
|
159
|
+
App->>Ctx: ctx.close(result?) — same as [A]
|
|
160
|
+
Ctx->>Ctx: run onClose(CloseResult) cleanups (LIFO, idempotent)
|
|
201
161
|
|
|
202
|
-
|
|
162
|
+
App->>Scope: release(atom)
|
|
163
|
+
Scope->>Scope: run atom cleanups (LIFO)
|
|
164
|
+
Scope->>Scope: remove from cache + controllers
|
|
203
165
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
subgraph Errors
|
|
207
|
-
ParseError["ParseError (tag | flow-input)"]
|
|
208
|
-
end
|
|
166
|
+
App->>Scope: flush()
|
|
167
|
+
Note right of Scope: await pending invalidation chain
|
|
209
168
|
|
|
210
|
-
|
|
211
|
-
|
|
169
|
+
App->>Scope: dispose()
|
|
170
|
+
loop each extension
|
|
171
|
+
Scope->>Ext: ext.dispose(scope)
|
|
172
|
+
end
|
|
173
|
+
Scope->>Scope: release all atoms, run all cleanups
|
|
212
174
|
end
|
|
213
175
|
|
|
214
|
-
subgraph "Symbols (advanced)"
|
|
215
|
-
atomSymbol
|
|
216
|
-
flowSymbol
|
|
217
|
-
tagSymbol
|
|
218
|
-
taggedSymbol
|
|
219
|
-
presetSymbol
|
|
220
|
-
controllerSymbol
|
|
221
|
-
controllerDepSymbol
|
|
222
|
-
tagExecutorSymbol
|
|
223
|
-
typedSymbol
|
|
224
|
-
end
|
|
225
176
|
```
|
|
226
177
|
|
|
227
|
-
API reference: `
|
|
178
|
+
API reference: `dist/index.d.mts` | Patterns: `PATTERNS.md` | CLI: `npx @pumped-fn/lite`
|
|
228
179
|
|
|
229
180
|
## License
|
|
230
181
|
|