@pumped-fn/lite 2.1.3 → 2.1.5
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 +70 -0
- package/dist/cli.cjs +249 -64
- package/dist/cli.mjs +249 -64
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +176 -157
- package/dist/index.d.cts +15 -15
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +15 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +176 -157
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/cli.mjs
CHANGED
|
@@ -18,11 +18,48 @@ const categories = {
|
|
|
18
18
|
title: "What is @pumped-fn/lite",
|
|
19
19
|
content: extractOverview
|
|
20
20
|
},
|
|
21
|
+
"mental-model": {
|
|
22
|
+
title: "Mental Model",
|
|
23
|
+
content: `@pumped-fn/lite is a scoped dependency graph with three primitives:
|
|
24
|
+
|
|
25
|
+
ATOM = singleton (cached per scope)
|
|
26
|
+
Created once. Lives as long as the scope. Think: db pool, config, auth service.
|
|
27
|
+
Resolved via scope.resolve(atom). Second call returns cached value.
|
|
28
|
+
Factory receives ResolveContext: ctx.cleanup(), ctx.invalidate(), ctx.scope, ctx.data.
|
|
29
|
+
|
|
30
|
+
FLOW = transient operation (new instance per exec)
|
|
31
|
+
Runs once per ctx.exec() call. Think: HTTP handler, mutation, query.
|
|
32
|
+
Factory receives ExecutionContext: ctx.exec(), ctx.onClose(), ctx.input, ctx.parent, ctx.data.
|
|
33
|
+
|
|
34
|
+
RESOURCE = execution-scoped singleton (shared within an exec chain)
|
|
35
|
+
Created fresh per root ctx.exec(). Shared across nested exec() calls via seek-up.
|
|
36
|
+
Think: per-request logger, transaction, trace span.
|
|
37
|
+
Declared as a flow dep, NOT called directly.
|
|
38
|
+
|
|
39
|
+
Scope = the container. Owns all atom caches. One per process (server) or per component tree (React).
|
|
40
|
+
ExecutionContext = the request boundary. Created per request/operation. Carries tags. Closes with cleanup.
|
|
41
|
+
Controller = opt-in reactive handle for an atom. Enables set/update/invalidate/subscribe.
|
|
42
|
+
Tag = ambient typed value. Propagates through scope → context → nested exec. No parameter drilling.
|
|
43
|
+
Preset = test/environment override. Replaces any atom or flow without touching production code.
|
|
44
|
+
Extension = middleware for resolve and exec. Wraps every atom resolution and flow execution.
|
|
45
|
+
|
|
46
|
+
Key invariant: atoms are resolved from scope, flows are executed from context.
|
|
47
|
+
scope.resolve(atom) ✓ correct
|
|
48
|
+
ctx.exec({ flow, input }) ✓ correct
|
|
49
|
+
scope.resolve(flow) ✗ wrong — flows are not cached
|
|
50
|
+
ctx.exec({ atom }) ✗ wrong — atoms are not executed`
|
|
51
|
+
},
|
|
21
52
|
primitives: {
|
|
22
53
|
title: "Primitives API",
|
|
23
|
-
content: `
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
content: `There are three primitives with distinct lifetimes:
|
|
55
|
+
atom — SINGLETON per scope. Created once, cached, reused everywhere. Think: db pool, config, service instance.
|
|
56
|
+
flow — EPHEMERAL per call. New execution each time ctx.exec() is called. Think: HTTP handler, mutation, query.
|
|
57
|
+
resource — EPHEMERAL per execution chain. Created once per ctx.exec() tree, shared across nested execs. Think: logger, transaction, trace span.
|
|
58
|
+
|
|
59
|
+
atom({ factory, deps?, tags?, keepAlive? })
|
|
60
|
+
Factory receives (resolveCtx, resolvedDeps) → value.
|
|
61
|
+
resolveCtx has: cleanup(fn), invalidate(), scope, data.
|
|
62
|
+
Resolved via scope.resolve(atom). Cached — second resolve() returns same value.
|
|
26
63
|
|
|
27
64
|
import { atom } from "@pumped-fn/lite"
|
|
28
65
|
const dbAtom = atom({ factory: () => createDbPool() })
|
|
@@ -32,7 +69,9 @@ const categories = {
|
|
|
32
69
|
})
|
|
33
70
|
|
|
34
71
|
flow({ factory, parse?, deps?, tags? })
|
|
35
|
-
|
|
72
|
+
Factory receives (executionCtx, resolvedDeps) → output.
|
|
73
|
+
executionCtx has: exec(), onClose(fn), input, parent, data, scope, name.
|
|
74
|
+
Executed via ctx.exec({ flow, input }). Never cached — each call runs the factory.
|
|
36
75
|
|
|
37
76
|
import { flow, typed } from "@pumped-fn/lite"
|
|
38
77
|
const getUser = flow({
|
|
@@ -41,21 +80,45 @@ flow({ factory, parse?, deps?, tags? })
|
|
|
41
80
|
factory: (ctx, { db }) => db.findUser(ctx.input.id),
|
|
42
81
|
})
|
|
43
82
|
|
|
83
|
+
resource({ factory, deps?, name? })
|
|
84
|
+
Like a flow factory but resolved as a DEPENDENCY of flows, not called directly.
|
|
85
|
+
Created fresh per execution chain. Shared via seek-up: nested ctx.exec() reuses parent's instance.
|
|
86
|
+
Factory receives (executionCtx, resolvedDeps) → instance.
|
|
87
|
+
Cleanup via ctx.onClose(fn).
|
|
88
|
+
|
|
89
|
+
import { resource } from "@pumped-fn/lite"
|
|
90
|
+
const txResource = resource({
|
|
91
|
+
deps: { db: dbAtom },
|
|
92
|
+
factory: (ctx, { db }) => {
|
|
93
|
+
const tx = db.beginTransaction()
|
|
94
|
+
ctx.onClose(result => result.ok ? tx.commit() : tx.rollback())
|
|
95
|
+
return tx
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
// Used as a flow dep — NOT called directly
|
|
99
|
+
const saveUser = flow({
|
|
100
|
+
deps: { tx: txResource },
|
|
101
|
+
factory: (ctx, { tx }) => tx.insert("users", ctx.input),
|
|
102
|
+
})
|
|
103
|
+
|
|
44
104
|
tag({ label, default?, parse? })
|
|
45
|
-
Ambient context value.
|
|
105
|
+
Ambient context value. Propagates through scope → context → exec hierarchy.
|
|
106
|
+
Resolution order: exec tags > context tags > scope tags (nearest wins).
|
|
46
107
|
|
|
47
108
|
import { tag } from "@pumped-fn/lite"
|
|
48
109
|
const tenantTag = tag<string>({ label: "tenant" })
|
|
49
110
|
|
|
50
111
|
preset(target, value)
|
|
51
|
-
Override an atom's resolved value. Used for testing and multi-tenant isolation.
|
|
112
|
+
Override an atom or flow's resolved value. Used for testing and multi-tenant isolation.
|
|
113
|
+
value can be: a literal, another atom (redirect), or a function (flow only).
|
|
52
114
|
|
|
53
115
|
import { preset } from "@pumped-fn/lite"
|
|
54
116
|
const mockDb = preset(dbAtom, fakeDatabaseInstance)
|
|
55
117
|
|
|
56
118
|
service({ factory, deps? })
|
|
57
119
|
Convenience wrapper for atom whose value is an object of methods.
|
|
58
|
-
Each method
|
|
120
|
+
Each method MUST have (ctx: ExecutionContext, ...args) as signature.
|
|
121
|
+
Called via ctx.exec({ fn: svc.method, params: [args] }) for lifecycle/tracing.`
|
|
59
122
|
},
|
|
60
123
|
scope: {
|
|
61
124
|
title: "Scope Management",
|
|
@@ -82,89 +145,131 @@ scope.dispose() → void release everything, run all c
|
|
|
82
145
|
},
|
|
83
146
|
context: {
|
|
84
147
|
title: "ExecutionContext",
|
|
85
|
-
content: `
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ctx.
|
|
89
|
-
|
|
148
|
+
content: `IMPORTANT: There are two context types. Don't confuse them.
|
|
149
|
+
|
|
150
|
+
ResolveContext (received by atom factories):
|
|
151
|
+
ctx.cleanup(fn) register cleanup (runs LIFO on release/invalidate)
|
|
152
|
+
ctx.invalidate() schedule re-resolution after current factory completes
|
|
153
|
+
ctx.scope the owning Scope
|
|
154
|
+
ctx.data per-atom key-value store (persists across invalidations)
|
|
155
|
+
|
|
156
|
+
ExecutionContext (received by flow factories, resource factories, and inline fns):
|
|
157
|
+
ctx.exec(...) execute a nested flow or function (creates child context)
|
|
158
|
+
ctx.onClose(fn) register cleanup (runs LIFO on close, receives CloseResult)
|
|
159
|
+
ctx.close(result?) close this context, run all cleanups
|
|
160
|
+
ctx.input parsed input (flows only)
|
|
161
|
+
ctx.parent parent ExecutionContext (undefined for root)
|
|
162
|
+
ctx.name exec name or flow name
|
|
163
|
+
ctx.scope the owning Scope
|
|
164
|
+
ctx.data per-context key-value store with tag support
|
|
165
|
+
|
|
166
|
+
ctx = scope.createContext({ tags? })
|
|
167
|
+
Creates a root ExecutionContext. Tags merge: exec tags > context tags > scope tags.
|
|
168
|
+
|
|
169
|
+
ctx.exec({ flow, input?, rawInput?, tags? }) → Promise<output>
|
|
170
|
+
Execute a flow. Creates a child context with merged tags.
|
|
171
|
+
If flow has parse: rawInput goes through parse first, input skips parse.
|
|
90
172
|
Child context closes automatically after execution.
|
|
91
173
|
|
|
92
|
-
ctx.exec({ fn, params?, tags? })
|
|
174
|
+
ctx.exec({ fn, params?, name?, tags? }) → Promise<result>
|
|
93
175
|
Execute an inline function: fn(childCtx, ...params).
|
|
94
176
|
Same child-context lifecycle as flow execution.
|
|
95
177
|
|
|
96
|
-
ctx.
|
|
97
|
-
|
|
178
|
+
ctx.data (both context types)
|
|
179
|
+
Raw: get(key) / set(key, val) / has(key) / delete(key) / clear() / seek(key)
|
|
180
|
+
Typed: getTag(tag) / setTag(tag, val) / hasTag(tag) / deleteTag(tag) / seekTag(tag) / getOrSetTag(tag, default?)
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Raw: get(key) / set(key, val) / has(key) / delete(key) / clear() / seek(key)
|
|
102
|
-
Typed: getTag(tag) / setTag(tag, val) / hasTag(tag) / deleteTag(tag) / seekTag(tag) / getOrSetTag(tag, factory)
|
|
103
|
-
|
|
104
|
-
seek/seekTag walks up the context chain to find values in parent contexts.`
|
|
182
|
+
seek/seekTag walks up the parent chain to find values set in ancestor contexts.
|
|
183
|
+
This is how tags propagate: middleware sets a tag, nested flows read it via seekTag.`
|
|
105
184
|
},
|
|
106
185
|
reactivity: {
|
|
107
186
|
title: "Reactivity (opt-in)",
|
|
108
|
-
content: `
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
187
|
+
content: `Atoms are STATIC by default — resolved once, value never changes.
|
|
188
|
+
Reactivity is opt-in via controllers. Two ways to get a controller:
|
|
189
|
+
|
|
190
|
+
1. scope.controller(atom) → Controller
|
|
191
|
+
Retrieve the reactive handle for an atom. Same instance per atom per scope.
|
|
192
|
+
Used externally (app code, React hooks, middleware).
|
|
193
|
+
|
|
194
|
+
2. controller(atom, opts?) → ControllerDep (dep marker)
|
|
195
|
+
Wrap an atom dep so the factory receives a Controller instead of the resolved value.
|
|
196
|
+
Used inside deps: { cfg: controller(configAtom, { resolve: true }) }
|
|
197
|
+
This is NOT the same as scope.controller() — it's a dep declaration.
|
|
198
|
+
|
|
199
|
+
Controller API:
|
|
200
|
+
ctrl.state → 'idle' | 'resolving' | 'resolved' | 'failed'
|
|
201
|
+
ctrl.get() → current value (throws if not resolved)
|
|
202
|
+
ctrl.resolve() → Promise<value> (resolve if not yet)
|
|
203
|
+
ctrl.set(value) → replace value, notify listeners, skip factory
|
|
204
|
+
ctrl.update(fn) → transform value via function, notify listeners
|
|
205
|
+
ctrl.invalidate() → re-run factory, notify listeners
|
|
206
|
+
ctrl.release() → release atom, run cleanups
|
|
207
|
+
ctrl.on(event, listener) → unsubscribe
|
|
208
|
+
events: 'resolving' | 'resolved' | 'failed' | '*'
|
|
209
|
+
|
|
210
|
+
Controller as dependency (opts):
|
|
211
|
+
controller(atom) → dep receives Controller (idle, must manually resolve)
|
|
212
|
+
controller(atom, { resolve: true }) → dep receives Controller (pre-resolved before factory runs)
|
|
213
|
+
controller(atom, { resolve: true, watch: true }) → ALSO auto-invalidates parent when dep value changes
|
|
214
|
+
controller(atom, { resolve: true, watch: true, eq }) → custom equality gate (default: structural deep equal for plain objects, Object.is otherwise)
|
|
215
|
+
|
|
216
|
+
watch:true replaces the manual pattern:
|
|
217
|
+
ctx.cleanup(ctx.scope.on('resolved', dep, () => ctx.invalidate()))
|
|
218
|
+
With the declarative:
|
|
219
|
+
deps: { src: controller(srcAtom, { resolve: true, watch: true }) }
|
|
220
|
+
The watch listener is auto-cleaned on re-resolve, release, and dispose.
|
|
119
221
|
|
|
120
222
|
select(atom, selector, { eq? }) → SelectHandle
|
|
121
223
|
Derived state slice. Only notifies when selected value changes per eq function.
|
|
122
|
-
|
|
123
224
|
handle.get() → current selected value
|
|
124
225
|
handle.subscribe(fn) → unsubscribe
|
|
226
|
+
handle.dispose() → clean up internal subscription
|
|
125
227
|
|
|
126
|
-
scope.on('resolved', atom, listener) → unsubscribe
|
|
127
|
-
Listen to atom
|
|
128
|
-
|
|
129
|
-
Controller as dependency:
|
|
130
|
-
import { controller } from "@pumped-fn/lite"
|
|
131
|
-
const serverAtom = atom({
|
|
132
|
-
deps: { cfg: controller(configAtom, { resolve: true }) },
|
|
133
|
-
factory: (ctx, { cfg }) => {
|
|
134
|
-
cfg.on('resolved', () => ctx.invalidate())
|
|
135
|
-
return createServer(cfg.get())
|
|
136
|
-
},
|
|
137
|
-
})`
|
|
228
|
+
scope.on('resolving' | 'resolved' | 'failed', atom, listener) → unsubscribe
|
|
229
|
+
Listen to atom state transitions at scope level.`
|
|
138
230
|
},
|
|
139
231
|
tags: {
|
|
140
232
|
title: "Tag System",
|
|
141
|
-
content: `
|
|
142
|
-
|
|
233
|
+
content: `Tags are typed ambient values that propagate without parameter drilling.
|
|
234
|
+
|
|
235
|
+
tag<T>({ label, default?, parse? }) → Tag<T>
|
|
236
|
+
Define a tag type. The tag object is both a type definition and a factory:
|
|
237
|
+
const tenantTag = tag<string>({ label: "tenant" })
|
|
238
|
+
const tagged = tenantTag("acme") // creates Tagged<string>
|
|
239
|
+
|
|
240
|
+
Resolution hierarchy (nearest wins):
|
|
241
|
+
1. exec tags: ctx.exec({ flow, tags: [tenantTag("exec")] })
|
|
242
|
+
2. flow tags: flow({ tags: [tenantTag("flow")] })
|
|
243
|
+
3. context tags: scope.createContext({ tags: [tenantTag("ctx")] })
|
|
244
|
+
4. ctx.data: parent ctx.data.setTag(tenantTag, "middleware") ← seekTag walks up
|
|
245
|
+
5. scope tags: createScope({ tags: [tenantTag("scope")] })
|
|
246
|
+
6. tag default: tag({ label: "tenant", default: "default" })
|
|
143
247
|
|
|
144
|
-
|
|
145
|
-
|
|
248
|
+
In atom deps: tags resolve from scope tags (atoms live at scope level).
|
|
249
|
+
In flow deps: tags resolve from exec/context/scope hierarchy + ctx.data seek-up.
|
|
146
250
|
|
|
147
251
|
Attaching tags:
|
|
148
|
-
atom({ tags: [tenantTag("acme")] })
|
|
149
|
-
flow({ tags: [roleTag("admin")] })
|
|
150
|
-
scope.createContext({ tags: [userTag(currentUser)] })
|
|
151
|
-
ctx.exec({ flow, tags: [localeTag("en")] })
|
|
252
|
+
atom({ tags: [tenantTag("acme")] }) metadata on atom definition
|
|
253
|
+
flow({ tags: [roleTag("admin")] }) applied to child context
|
|
254
|
+
scope.createContext({ tags: [userTag(currentUser)] }) on context creation
|
|
255
|
+
ctx.exec({ flow, tags: [localeTag("en")] }) on specific execution
|
|
256
|
+
ctx.data.setTag(tenantTag, "middleware-set") programmatic, propagates to children
|
|
152
257
|
|
|
153
258
|
Reading tags:
|
|
154
|
-
tag.get(source) → T
|
|
155
|
-
tag.find(source) → T | undefined
|
|
156
|
-
tag.collect(source) → T[]
|
|
259
|
+
tag.get(source) → T first match or throw
|
|
260
|
+
tag.find(source) → T | undefined first match or undefined
|
|
261
|
+
tag.collect(source) → T[] all matches
|
|
157
262
|
|
|
158
|
-
Context data
|
|
159
|
-
ctx.data.setTag(tag, value)
|
|
160
|
-
ctx.data.getTag(tag)
|
|
161
|
-
ctx.data.seekTag(tag)
|
|
162
|
-
ctx.data.hasTag(tag)
|
|
263
|
+
Context data:
|
|
264
|
+
ctx.data.setTag(tag, value) set on current context
|
|
265
|
+
ctx.data.getTag(tag) read from current context only
|
|
266
|
+
ctx.data.seekTag(tag) walk up parent chain until found
|
|
267
|
+
ctx.data.hasTag(tag) check current context only
|
|
163
268
|
|
|
164
269
|
Tag executor (dependency wiring):
|
|
165
|
-
tags.required(tag) → resolves tag or throws
|
|
166
|
-
tags.optional(tag) → resolves
|
|
167
|
-
tags.all(tag) →
|
|
270
|
+
tags.required(tag) → T resolves tag or throws (atom deps: scope, flow deps: hierarchy)
|
|
271
|
+
tags.optional(tag) → T | undefined resolves or undefined
|
|
272
|
+
tags.all(tag) → T[] collects from all levels of hierarchy
|
|
168
273
|
|
|
169
274
|
Introspection:
|
|
170
275
|
tag.atoms() → Atom[] with this tag attached
|
|
@@ -273,6 +378,86 @@ Atom with cleanup:
|
|
|
273
378
|
Atom retention / GC:
|
|
274
379
|
createScope({ gc: { enabled: true, graceMs: 3000 } })
|
|
275
380
|
atom({ keepAlive: true }) // never GC'd`
|
|
381
|
+
},
|
|
382
|
+
"tanstack-start": {
|
|
383
|
+
title: "TanStack Start Integration",
|
|
384
|
+
content: `Singleton scope at server entry, per-request ExecutionContext via middleware.
|
|
385
|
+
|
|
386
|
+
Server entry — one scope per process:
|
|
387
|
+
const scope = createScope({ extensions: [otel()], tags: [envTag(env)] })
|
|
388
|
+
export default createServerEntry({
|
|
389
|
+
async fetch(request) {
|
|
390
|
+
return handler.fetch(request, { context: { scope } })
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
Execution context middleware — per-request lifecycle:
|
|
395
|
+
export const execCtxMiddleware = createMiddleware()
|
|
396
|
+
.server(async ({ next, context: { scope } }) => {
|
|
397
|
+
const execContext = scope.createContext({})
|
|
398
|
+
try {
|
|
399
|
+
return await next({ context: { execContext } })
|
|
400
|
+
} finally {
|
|
401
|
+
await execContext.close()
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
Tag-seeding middleware — ambient data for downstream:
|
|
406
|
+
export const authMiddleware = createMiddleware()
|
|
407
|
+
.middleware([execCtxMiddleware])
|
|
408
|
+
.server(async ({ next, context: { execContext } }) => {
|
|
409
|
+
const user = await resolveCurrentUser()
|
|
410
|
+
execContext.data.setTag(currentUserTag, user)
|
|
411
|
+
return next({ context: { user } })
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
export const transactionMiddleware = createMiddleware()
|
|
415
|
+
.middleware([authMiddleware])
|
|
416
|
+
.server(async ({ next, context: { execContext } }) => {
|
|
417
|
+
const tx = await beginTransaction()
|
|
418
|
+
execContext.data.setTag(transactionTag, tx)
|
|
419
|
+
try {
|
|
420
|
+
const result = await next()
|
|
421
|
+
await tx.commit()
|
|
422
|
+
return result
|
|
423
|
+
} catch (e) {
|
|
424
|
+
await tx.rollback()
|
|
425
|
+
throw e
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
Server functions — execute flows via context:
|
|
430
|
+
export const listInvoices = createServerFn({ method: 'POST' })
|
|
431
|
+
.middleware([transactionMiddleware])
|
|
432
|
+
.handler(async ({ data, context: { execContext } }) => {
|
|
433
|
+
return execContext.exec({ flow: invoiceFlows.list, rawInput: data })
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
Client hydration — preset loader data into client scope:
|
|
437
|
+
const loaderData = Route.useLoaderData()
|
|
438
|
+
const scope = createScope({
|
|
439
|
+
presets: [
|
|
440
|
+
preset(invoicesAtom, loaderData.invoices),
|
|
441
|
+
preset(userAtom, loaderData.user),
|
|
442
|
+
],
|
|
443
|
+
})
|
|
444
|
+
return <ScopeProvider scope={scope}><Outlet /></ScopeProvider>
|
|
445
|
+
|
|
446
|
+
Rules:
|
|
447
|
+
One scope per server process Atoms cache singletons (connections, services)
|
|
448
|
+
One execContext per request Tag isolation (user, tx, tracing)
|
|
449
|
+
Middleware creates+closes ctx Guarantees cleanup even on error
|
|
450
|
+
Tags over function params Flows read ambient tags, no signature coupling
|
|
451
|
+
execContext.exec({ flow }) Flows get lifecycle, tracing, cleanup
|
|
452
|
+
scope.resolve(atom) for deps Atoms are long-lived, cached in scope
|
|
453
|
+
Preset server data on client No re-fetch; atoms hydrate from loader
|
|
454
|
+
|
|
455
|
+
Don't:
|
|
456
|
+
createScope() in a server fn New scope per request — atoms re-resolve, connections leak
|
|
457
|
+
flow.factory(ctx, deps) direct Bypasses context lifecycle, tags, extensions, cleanup
|
|
458
|
+
User/tx as flow input Couples signatures to transport; use tags instead
|
|
459
|
+
scope.resolve(flow) Flows are ephemeral — exec(), don't resolve()
|
|
460
|
+
ScopeProvider without presets Client re-fetches everything server already loaded`
|
|
276
461
|
},
|
|
277
462
|
diagrams: {
|
|
278
463
|
title: "Visual Diagrams (mermaid)",
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["categories: Record<string, { title: string; content: string | (() => string) }>"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst pkgDir = join(dirname(fileURLToPath(import.meta.url)), \"..\")\nconst readme = readFileSync(join(pkgDir, \"README.md\"), \"utf-8\")\n\nfunction extractOverview(): string {\n const idx = readme.indexOf(\"## How It Works\")\n const raw = idx === -1 ? readme : readme.slice(0, idx)\n return raw.replace(/^#[^\\n]*\\n+/, \"\").trim()\n}\n\nfunction extractDiagram(): string {\n const match = readme.match(/```mermaid\\n([\\s\\S]*?)```/)\n return match ? `Full system sequence (unified):\\n\\n\\`\\`\\`mermaid\\n${match[1]!.trim()}\\n\\`\\`\\`` : \"No diagram found in README.md\"\n}\n\nconst categories: Record<string, { title: string; content: string | (() => string) }> = {\n overview: {\n title: \"What is @pumped-fn/lite\",\n content: extractOverview,\n },\n\n primitives: {\n title: \"Primitives API\",\n content: `atom({ factory, deps?, tags?, keepAlive? })\n Creates a managed effect. Factory receives (ctx, resolvedDeps) and returns a value.\n Cached per scope. Supports cleanup via ctx.onClose().\n\n import { atom } from \"@pumped-fn/lite\"\n const dbAtom = atom({ factory: () => createDbPool() })\n const userAtom = atom({\n deps: { db: dbAtom },\n factory: (ctx, { db }) => db.query(\"SELECT ...\"),\n })\n\nflow({ factory, parse?, deps?, tags? })\n Operation template executed per call. parse validates input, factory runs logic.\n\n import { flow, typed } from \"@pumped-fn/lite\"\n const getUser = flow({\n parse: typed<{ id: string }>(),\n deps: { db: dbAtom },\n factory: (ctx, { db }) => db.findUser(ctx.input.id),\n })\n\ntag({ label, default?, parse? })\n Ambient context value. Attach to atoms/flows/contexts. Retrieve via tag.get/find/collect.\n\n import { tag } from \"@pumped-fn/lite\"\n const tenantTag = tag<string>({ label: \"tenant\" })\n\npreset(target, value)\n Override an atom's resolved value. Used for testing and multi-tenant isolation.\n\n import { preset } from \"@pumped-fn/lite\"\n const mockDb = preset(dbAtom, fakeDatabaseInstance)\n\nservice({ factory, deps? })\n Convenience wrapper for atom whose value is an object of methods.\n Each method receives (ctx, ...args) for tracing/auth integration.`,\n },\n\n scope: {\n title: \"Scope Management\",\n content: `createScope({ extensions?, presets?, tags?, gc? })\n Creates a scope that manages atom resolution, caching, extensions, and GC.\n\n import { createScope } from \"@pumped-fn/lite\"\n const scope = createScope({\n extensions: [loggingExt],\n presets: [preset(dbAtom, mockDb)],\n tags: [tenantTag(\"acme\")],\n gc: { enabled: true, graceMs: 3000 },\n })\n await scope.ready\n\nscope.resolve(atom) → Promise<value> resolve and cache an atom\nscope.controller(atom) → Controller get reactive handle\nscope.select(atom, fn, opts?) → SelectHandle derived slice with equality check\nscope.on(event, atom, fn) → unsubscribe listen to atom events\nscope.release(atom) → void release atom, run cleanups\nscope.createContext(opts?) → ExecutionContext create execution boundary\nscope.flush() → Promise<void> wait all pending operations\nscope.dispose() → void release everything, run all cleanups`,\n },\n\n context: {\n title: \"ExecutionContext\",\n content: `ctx = scope.createContext({ tags? })\n Execution boundary. Tags merge with scope tags. Cleanup runs LIFO on close.\n\nctx.exec({ flow, input?, tags? }) → Promise<output>\n Execute a flow within this context. Creates a child context with merged tags.\n Child context closes automatically after execution.\n\nctx.exec({ fn, params?, tags? }) → Promise<result>\n Execute an inline function: fn(childCtx, ...params).\n Same child-context lifecycle as flow execution.\n\nctx.onClose(cleanup) → void register cleanup (runs LIFO on ctx.close)\nctx.close() → void run all registered cleanups in LIFO order\n\nctx.data\n Key-value store scoped to the context:\n Raw: get(key) / set(key, val) / has(key) / delete(key) / clear() / seek(key)\n Typed: getTag(tag) / setTag(tag, val) / hasTag(tag) / deleteTag(tag) / seekTag(tag) / getOrSetTag(tag, factory)\n\n seek/seekTag walks up the context chain to find values in parent contexts.`,\n },\n\n reactivity: {\n title: \"Reactivity (opt-in)\",\n content: `controller(atom) → Controller\n Opt-in reactive handle for an atom.\n\n ctrl.get() → current value (must be resolved first)\n ctrl.resolve() → Promise<value> (resolve if not yet)\n ctrl.set(value) → replace value, notify listeners\n ctrl.update(fn) → update value via function, notify listeners\n ctrl.invalidate() → re-run factory, notify listeners\n ctrl.release() → release atom, run cleanups\n ctrl.on(event, listener) → unsubscribe\n events: 'resolving' | 'resolved' | '*'\n\nselect(atom, selector, { eq? }) → SelectHandle\n Derived state slice. Only notifies when selected value changes per eq function.\n\n handle.get() → current selected value\n handle.subscribe(fn) → unsubscribe\n\nscope.on('resolved', atom, listener) → unsubscribe\n Listen to atom resolution events at scope level.\n\nController as dependency:\n import { controller } from \"@pumped-fn/lite\"\n const serverAtom = atom({\n deps: { cfg: controller(configAtom, { resolve: true }) },\n factory: (ctx, { cfg }) => {\n cfg.on('resolved', () => ctx.invalidate())\n return createServer(cfg.get())\n },\n })`,\n },\n\n tags: {\n title: \"Tag System\",\n content: `tag<T>({ label, default?, parse? }) → Tag<T>\n Define an ambient context value type.\n\ntag(value) → Tagged<T>\n Create a tagged value to attach to atoms, flows, or contexts.\n\nAttaching tags:\n atom({ tags: [tenantTag(\"acme\")] })\n flow({ tags: [roleTag(\"admin\")] })\n scope.createContext({ tags: [userTag(currentUser)] })\n ctx.exec({ flow, tags: [localeTag(\"en\")] })\n\nReading tags:\n tag.get(source) → T first match or throw\n tag.find(source) → T | undefined first match or undefined\n tag.collect(source) → T[] all matches\n\nContext data integration:\n ctx.data.setTag(tag, value)\n ctx.data.getTag(tag) → T\n ctx.data.seekTag(tag) → T (walks parent chain)\n ctx.data.hasTag(tag) → boolean\n\nTag executor (dependency wiring):\n tags.required(tag) → resolves tag or throws\n tags.optional(tag) → resolves tag or undefined\n tags.all(tag) → resolves all values for tag\n\nIntrospection:\n tag.atoms() → Atom[] with this tag attached\n getAllTags() → Tag[] all registered tags`,\n },\n\n extensions: {\n title: \"Extensions Pipeline\",\n content: `Extensions wrap atom resolution and flow execution (middleware pattern).\n\ninterface Extension {\n init?(scope): void | Promise<void>\n dispose?(scope): void\n wrapResolve?(next, event: ResolveEvent): Promise<value>\n wrapExec?(next, flow, ctx): Promise<output>\n}\n\ncreateScope({ extensions: [ext1, ext2] })\n\nLifecycle:\n 1. scope creation → ext.init(scope) called for each extension\n 2. await scope.ready → all init() resolved\n 3. resolve(atom) → ext.wrapResolve(next, { kind: \"atom\", target, scope })\n resolve(resource) → ext.wrapResolve(next, { kind: \"resource\", target, ctx })\n - call next() to proceed to actual resolution\n - dispatch on event.kind for atom vs resource\n 4. ctx.exec(flow) → ext.wrapExec(next, flow, ctx)\n - call next() to proceed to actual execution\n 5. scope.dispose() → ext.dispose(scope) called for each extension\n\nExample:\n const timingExt: Extension = {\n wrapResolve: async (next, event) => {\n const start = Date.now()\n const value = await next()\n console.log(event.target, Date.now() - start, \"ms\")\n return value\n },\n }`,\n },\n\n testing: {\n title: \"Testing & Isolation\",\n content: `Use presets to swap implementations without changing production code.\n\nimport { createScope, preset } from \"@pumped-fn/lite\"\n\nconst scope = createScope({\n presets: [\n preset(dbAtom, mockDatabase),\n preset(cacheAtom, inMemoryCache),\n ],\n tags: [tenantTag(\"test-tenant\")],\n})\n\nconst db = await scope.resolve(dbAtom) // → mockDatabase (not real db)\n\nMulti-tenant isolation:\n Each scope is fully isolated. Create one scope per tenant/test.\n\n const tenantScope = createScope({\n tags: [tenantTag(tenantId)],\n presets: tenantOverrides,\n })\n\nCleanup:\n scope.dispose() releases all atoms and runs all cleanup functions.\n In tests: call scope.dispose() in afterEach.`,\n },\n\n patterns: {\n title: \"Common Patterns\",\n content: `Request lifecycle:\n const scope = createScope()\n const ctx = scope.createContext({ tags: [requestTag(req)] })\n const result = await ctx.exec({ flow: handleRequest, input: req.body })\n ctx.close() // cleanup LIFO\n\nService pattern:\n const userService = service({\n deps: { db: dbAtom },\n factory: (ctx, { db }) => ({\n getUser: (ctx, id) => db.findUser(id),\n updateUser: (ctx, id, data) => db.updateUser(id, data),\n }),\n })\n\nTyped flow input:\n const getUser = flow({\n parse: typed<{ id: string }>(),\n factory: (ctx) => findUser(ctx.input.id),\n })\n\nInline execution:\n const result = await ctx.exec({\n fn: (ctx, a, b) => a + b,\n params: [1, 2],\n })\n\nAtom with cleanup:\n const serverAtom = atom({\n factory: (ctx) => {\n const server = createServer()\n ctx.onClose(() => server.close())\n return server\n },\n })\n\nAtom retention / GC:\n createScope({ gc: { enabled: true, graceMs: 3000 } })\n atom({ keepAlive: true }) // never GC'd`,\n },\n\n diagrams: {\n title: \"Visual Diagrams (mermaid)\",\n content: extractDiagram,\n },\n\n types: {\n title: \"Type Utilities & Guards\",\n content: `Type extractors (Lite.Utils namespace):\n AtomValue<A> extract resolved type from atom\n FlowOutput<F> extract output type from flow\n FlowInput<F> extract input type from flow\n TagValue<T> extract value type from tag\n DepsOf<A | F> extract deps record type\n ControllerValue<C> extract value from controller\n Simplify<T> flatten intersection types\n AtomType<T, D> construct atom type\n FlowType<O, I, D> construct flow type\n\nType guards:\n isAtom(v) → v is Atom\n isFlow(v) → v is Flow\n isTag(v) → v is Tag\n isTagged(v) → v is Tagged\n isPreset(v) → v is Preset\n isControllerDep(v) → v is ControllerDep\n isTagExecutor(v) → v is TagExecutor\n\nConvenience types:\n AnyAtom any atom regardless of value/deps\n AnyFlow any flow regardless of output/input/deps\n AnyController any controller regardless of value\n\nSymbols (advanced, for library authors):\n atomSymbol, flowSymbol, tagSymbol, taggedSymbol,\n presetSymbol, controllerSymbol, controllerDepSymbol,\n tagExecutorSymbol, typedSymbol`,\n },\n}\n\nconst args = process.argv.slice(2)\nconst category = args[0]\n\nif (!category || category === \"help\" || category === \"--help\") {\n console.log(\"@pumped-fn/lite — Scoped Ambient State for TypeScript\\n\")\n console.log(\"Usage: pumped-lite <category>\\n\")\n console.log(\"Categories:\")\n for (const [key, { title }] of Object.entries(categories)) {\n console.log(` ${key.padEnd(14)} ${title}`)\n }\n console.log(\"\\nExamples:\")\n console.log(\" npx @pumped-fn/lite primitives # API reference\")\n console.log(\" npx @pumped-fn/lite diagrams # mermaid diagrams\")\n process.exit(0)\n}\n\nif (!(category in categories)) {\n console.error(`Unknown category: \"${category}\"\\n`)\n console.error(\"Available categories: \" + Object.keys(categories).join(\", \"))\n process.exit(1)\n}\n\nconst entry = categories[category]!\nconst output = typeof entry.content === \"function\" ? entry.content() : entry.content\nconsole.log(`# ${entry.title}\\n`)\nconsole.log(output)\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,aAAa,KADb,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK,EACzB,YAAY,EAAE,QAAQ;AAE/D,SAAS,kBAA0B;CACjC,MAAM,MAAM,OAAO,QAAQ,kBAAkB;AAE7C,SADY,QAAQ,KAAK,SAAS,OAAO,MAAM,GAAG,IAAI,EAC3C,QAAQ,eAAe,GAAG,CAAC,MAAM;;AAG9C,SAAS,iBAAyB;CAChC,MAAM,QAAQ,OAAO,MAAM,4BAA4B;AACvD,QAAO,QAAQ,qDAAqD,MAAM,GAAI,MAAM,CAAC,YAAY;;AAGnG,MAAMA,aAAkF;CACtF,UAAU;EACR,OAAO;EACP,SAAS;EACV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCV;CAED,OAAO;EACL,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;EAoBV;CAED,SAAS;EACP,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;EAoBV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BV;CAED,MAAM;EACJ,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BV;CAED,SAAS;EACP,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;EAyBV;CAED,UAAU;EACR,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCV;CAED,UAAU;EACR,OAAO;EACP,SAAS;EACV;CAED,OAAO;EACL,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6BV;CACF;AAGD,MAAM,WADO,QAAQ,KAAK,MAAM,EAAE,CACZ;AAEtB,IAAI,CAAC,YAAY,aAAa,UAAU,aAAa,UAAU;AAC7D,SAAQ,IAAI,0DAA0D;AACtE,SAAQ,IAAI,kCAAkC;AAC9C,SAAQ,IAAI,cAAc;AAC1B,MAAK,MAAM,CAAC,KAAK,EAAE,YAAY,OAAO,QAAQ,WAAW,CACvD,SAAQ,IAAI,KAAK,IAAI,OAAO,GAAG,CAAC,GAAG,QAAQ;AAE7C,SAAQ,IAAI,cAAc;AAC1B,SAAQ,IAAI,qDAAqD;AACjE,SAAQ,IAAI,wDAAwD;AACpE,SAAQ,KAAK,EAAE;;AAGjB,IAAI,EAAE,YAAY,aAAa;AAC7B,SAAQ,MAAM,sBAAsB,SAAS,KAAK;AAClD,SAAQ,MAAM,2BAA2B,OAAO,KAAK,WAAW,CAAC,KAAK,KAAK,CAAC;AAC5E,SAAQ,KAAK,EAAE;;AAGjB,MAAM,QAAQ,WAAW;AACzB,MAAM,SAAS,OAAO,MAAM,YAAY,aAAa,MAAM,SAAS,GAAG,MAAM;AAC7E,QAAQ,IAAI,KAAK,MAAM,MAAM,IAAI;AACjC,QAAQ,IAAI,OAAO"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["categories: Record<string, { title: string; content: string | (() => string) }>"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nconst pkgDir = join(dirname(fileURLToPath(import.meta.url)), \"..\")\nconst readme = readFileSync(join(pkgDir, \"README.md\"), \"utf-8\")\n\nfunction extractOverview(): string {\n const idx = readme.indexOf(\"## How It Works\")\n const raw = idx === -1 ? readme : readme.slice(0, idx)\n return raw.replace(/^#[^\\n]*\\n+/, \"\").trim()\n}\n\nfunction extractDiagram(): string {\n const match = readme.match(/```mermaid\\n([\\s\\S]*?)```/)\n return match ? `Full system sequence (unified):\\n\\n\\`\\`\\`mermaid\\n${match[1]!.trim()}\\n\\`\\`\\`` : \"No diagram found in README.md\"\n}\n\nconst categories: Record<string, { title: string; content: string | (() => string) }> = {\n overview: {\n title: \"What is @pumped-fn/lite\",\n content: extractOverview,\n },\n\n \"mental-model\": {\n title: \"Mental Model\",\n content: `@pumped-fn/lite is a scoped dependency graph with three primitives:\n\n ATOM = singleton (cached per scope)\n Created once. Lives as long as the scope. Think: db pool, config, auth service.\n Resolved via scope.resolve(atom). Second call returns cached value.\n Factory receives ResolveContext: ctx.cleanup(), ctx.invalidate(), ctx.scope, ctx.data.\n\n FLOW = transient operation (new instance per exec)\n Runs once per ctx.exec() call. Think: HTTP handler, mutation, query.\n Factory receives ExecutionContext: ctx.exec(), ctx.onClose(), ctx.input, ctx.parent, ctx.data.\n\n RESOURCE = execution-scoped singleton (shared within an exec chain)\n Created fresh per root ctx.exec(). Shared across nested exec() calls via seek-up.\n Think: per-request logger, transaction, trace span.\n Declared as a flow dep, NOT called directly.\n\nScope = the container. Owns all atom caches. One per process (server) or per component tree (React).\nExecutionContext = the request boundary. Created per request/operation. Carries tags. Closes with cleanup.\nController = opt-in reactive handle for an atom. Enables set/update/invalidate/subscribe.\nTag = ambient typed value. Propagates through scope → context → nested exec. No parameter drilling.\nPreset = test/environment override. Replaces any atom or flow without touching production code.\nExtension = middleware for resolve and exec. Wraps every atom resolution and flow execution.\n\nKey invariant: atoms are resolved from scope, flows are executed from context.\n scope.resolve(atom) ✓ correct\n ctx.exec({ flow, input }) ✓ correct\n scope.resolve(flow) ✗ wrong — flows are not cached\n ctx.exec({ atom }) ✗ wrong — atoms are not executed`,\n },\n\n primitives: {\n title: \"Primitives API\",\n content: `There are three primitives with distinct lifetimes:\n atom — SINGLETON per scope. Created once, cached, reused everywhere. Think: db pool, config, service instance.\n flow — EPHEMERAL per call. New execution each time ctx.exec() is called. Think: HTTP handler, mutation, query.\n resource — EPHEMERAL per execution chain. Created once per ctx.exec() tree, shared across nested execs. Think: logger, transaction, trace span.\n\natom({ factory, deps?, tags?, keepAlive? })\n Factory receives (resolveCtx, resolvedDeps) → value.\n resolveCtx has: cleanup(fn), invalidate(), scope, data.\n Resolved via scope.resolve(atom). Cached — second resolve() returns same value.\n\n import { atom } from \"@pumped-fn/lite\"\n const dbAtom = atom({ factory: () => createDbPool() })\n const userAtom = atom({\n deps: { db: dbAtom },\n factory: (ctx, { db }) => db.query(\"SELECT ...\"),\n })\n\nflow({ factory, parse?, deps?, tags? })\n Factory receives (executionCtx, resolvedDeps) → output.\n executionCtx has: exec(), onClose(fn), input, parent, data, scope, name.\n Executed via ctx.exec({ flow, input }). Never cached — each call runs the factory.\n\n import { flow, typed } from \"@pumped-fn/lite\"\n const getUser = flow({\n parse: typed<{ id: string }>(),\n deps: { db: dbAtom },\n factory: (ctx, { db }) => db.findUser(ctx.input.id),\n })\n\nresource({ factory, deps?, name? })\n Like a flow factory but resolved as a DEPENDENCY of flows, not called directly.\n Created fresh per execution chain. Shared via seek-up: nested ctx.exec() reuses parent's instance.\n Factory receives (executionCtx, resolvedDeps) → instance.\n Cleanup via ctx.onClose(fn).\n\n import { resource } from \"@pumped-fn/lite\"\n const txResource = resource({\n deps: { db: dbAtom },\n factory: (ctx, { db }) => {\n const tx = db.beginTransaction()\n ctx.onClose(result => result.ok ? tx.commit() : tx.rollback())\n return tx\n },\n })\n // Used as a flow dep — NOT called directly\n const saveUser = flow({\n deps: { tx: txResource },\n factory: (ctx, { tx }) => tx.insert(\"users\", ctx.input),\n })\n\ntag({ label, default?, parse? })\n Ambient context value. Propagates through scope → context → exec hierarchy.\n Resolution order: exec tags > context tags > scope tags (nearest wins).\n\n import { tag } from \"@pumped-fn/lite\"\n const tenantTag = tag<string>({ label: \"tenant\" })\n\npreset(target, value)\n Override an atom or flow's resolved value. Used for testing and multi-tenant isolation.\n value can be: a literal, another atom (redirect), or a function (flow only).\n\n import { preset } from \"@pumped-fn/lite\"\n const mockDb = preset(dbAtom, fakeDatabaseInstance)\n\nservice({ factory, deps? })\n Convenience wrapper for atom whose value is an object of methods.\n Each method MUST have (ctx: ExecutionContext, ...args) as signature.\n Called via ctx.exec({ fn: svc.method, params: [args] }) for lifecycle/tracing.`,\n },\n\n scope: {\n title: \"Scope Management\",\n content: `createScope({ extensions?, presets?, tags?, gc? })\n Creates a scope that manages atom resolution, caching, extensions, and GC.\n\n import { createScope } from \"@pumped-fn/lite\"\n const scope = createScope({\n extensions: [loggingExt],\n presets: [preset(dbAtom, mockDb)],\n tags: [tenantTag(\"acme\")],\n gc: { enabled: true, graceMs: 3000 },\n })\n await scope.ready\n\nscope.resolve(atom) → Promise<value> resolve and cache an atom\nscope.controller(atom) → Controller get reactive handle\nscope.select(atom, fn, opts?) → SelectHandle derived slice with equality check\nscope.on(event, atom, fn) → unsubscribe listen to atom events\nscope.release(atom) → void release atom, run cleanups\nscope.createContext(opts?) → ExecutionContext create execution boundary\nscope.flush() → Promise<void> wait all pending operations\nscope.dispose() → void release everything, run all cleanups`,\n },\n\n context: {\n title: \"ExecutionContext\",\n content: `IMPORTANT: There are two context types. Don't confuse them.\n\nResolveContext (received by atom factories):\n ctx.cleanup(fn) register cleanup (runs LIFO on release/invalidate)\n ctx.invalidate() schedule re-resolution after current factory completes\n ctx.scope the owning Scope\n ctx.data per-atom key-value store (persists across invalidations)\n\nExecutionContext (received by flow factories, resource factories, and inline fns):\n ctx.exec(...) execute a nested flow or function (creates child context)\n ctx.onClose(fn) register cleanup (runs LIFO on close, receives CloseResult)\n ctx.close(result?) close this context, run all cleanups\n ctx.input parsed input (flows only)\n ctx.parent parent ExecutionContext (undefined for root)\n ctx.name exec name or flow name\n ctx.scope the owning Scope\n ctx.data per-context key-value store with tag support\n\nctx = scope.createContext({ tags? })\n Creates a root ExecutionContext. Tags merge: exec tags > context tags > scope tags.\n\nctx.exec({ flow, input?, rawInput?, tags? }) → Promise<output>\n Execute a flow. Creates a child context with merged tags.\n If flow has parse: rawInput goes through parse first, input skips parse.\n Child context closes automatically after execution.\n\nctx.exec({ fn, params?, name?, tags? }) → Promise<result>\n Execute an inline function: fn(childCtx, ...params).\n Same child-context lifecycle as flow execution.\n\nctx.data (both context types)\n Raw: get(key) / set(key, val) / has(key) / delete(key) / clear() / seek(key)\n Typed: getTag(tag) / setTag(tag, val) / hasTag(tag) / deleteTag(tag) / seekTag(tag) / getOrSetTag(tag, default?)\n\n seek/seekTag walks up the parent chain to find values set in ancestor contexts.\n This is how tags propagate: middleware sets a tag, nested flows read it via seekTag.`,\n },\n\n reactivity: {\n title: \"Reactivity (opt-in)\",\n content: `Atoms are STATIC by default — resolved once, value never changes.\nReactivity is opt-in via controllers. Two ways to get a controller:\n\n1. scope.controller(atom) → Controller\n Retrieve the reactive handle for an atom. Same instance per atom per scope.\n Used externally (app code, React hooks, middleware).\n\n2. controller(atom, opts?) → ControllerDep (dep marker)\n Wrap an atom dep so the factory receives a Controller instead of the resolved value.\n Used inside deps: { cfg: controller(configAtom, { resolve: true }) }\n This is NOT the same as scope.controller() — it's a dep declaration.\n\nController API:\n ctrl.state → 'idle' | 'resolving' | 'resolved' | 'failed'\n ctrl.get() → current value (throws if not resolved)\n ctrl.resolve() → Promise<value> (resolve if not yet)\n ctrl.set(value) → replace value, notify listeners, skip factory\n ctrl.update(fn) → transform value via function, notify listeners\n ctrl.invalidate() → re-run factory, notify listeners\n ctrl.release() → release atom, run cleanups\n ctrl.on(event, listener) → unsubscribe\n events: 'resolving' | 'resolved' | 'failed' | '*'\n\nController as dependency (opts):\n controller(atom) → dep receives Controller (idle, must manually resolve)\n controller(atom, { resolve: true }) → dep receives Controller (pre-resolved before factory runs)\n controller(atom, { resolve: true, watch: true }) → ALSO auto-invalidates parent when dep value changes\n controller(atom, { resolve: true, watch: true, eq }) → custom equality gate (default: structural deep equal for plain objects, Object.is otherwise)\n\n watch:true replaces the manual pattern:\n ctx.cleanup(ctx.scope.on('resolved', dep, () => ctx.invalidate()))\n With the declarative:\n deps: { src: controller(srcAtom, { resolve: true, watch: true }) }\n The watch listener is auto-cleaned on re-resolve, release, and dispose.\n\nselect(atom, selector, { eq? }) → SelectHandle\n Derived state slice. Only notifies when selected value changes per eq function.\n handle.get() → current selected value\n handle.subscribe(fn) → unsubscribe\n handle.dispose() → clean up internal subscription\n\nscope.on('resolving' | 'resolved' | 'failed', atom, listener) → unsubscribe\n Listen to atom state transitions at scope level.`,\n },\n\n tags: {\n title: \"Tag System\",\n content: `Tags are typed ambient values that propagate without parameter drilling.\n\ntag<T>({ label, default?, parse? }) → Tag<T>\n Define a tag type. The tag object is both a type definition and a factory:\n const tenantTag = tag<string>({ label: \"tenant\" })\n const tagged = tenantTag(\"acme\") // creates Tagged<string>\n\nResolution hierarchy (nearest wins):\n 1. exec tags: ctx.exec({ flow, tags: [tenantTag(\"exec\")] })\n 2. flow tags: flow({ tags: [tenantTag(\"flow\")] })\n 3. context tags: scope.createContext({ tags: [tenantTag(\"ctx\")] })\n 4. ctx.data: parent ctx.data.setTag(tenantTag, \"middleware\") ← seekTag walks up\n 5. scope tags: createScope({ tags: [tenantTag(\"scope\")] })\n 6. tag default: tag({ label: \"tenant\", default: \"default\" })\n\nIn atom deps: tags resolve from scope tags (atoms live at scope level).\nIn flow deps: tags resolve from exec/context/scope hierarchy + ctx.data seek-up.\n\nAttaching tags:\n atom({ tags: [tenantTag(\"acme\")] }) metadata on atom definition\n flow({ tags: [roleTag(\"admin\")] }) applied to child context\n scope.createContext({ tags: [userTag(currentUser)] }) on context creation\n ctx.exec({ flow, tags: [localeTag(\"en\")] }) on specific execution\n ctx.data.setTag(tenantTag, \"middleware-set\") programmatic, propagates to children\n\nReading tags:\n tag.get(source) → T first match or throw\n tag.find(source) → T | undefined first match or undefined\n tag.collect(source) → T[] all matches\n\nContext data:\n ctx.data.setTag(tag, value) set on current context\n ctx.data.getTag(tag) read from current context only\n ctx.data.seekTag(tag) walk up parent chain until found\n ctx.data.hasTag(tag) check current context only\n\nTag executor (dependency wiring):\n tags.required(tag) → T resolves tag or throws (atom deps: scope, flow deps: hierarchy)\n tags.optional(tag) → T | undefined resolves or undefined\n tags.all(tag) → T[] collects from all levels of hierarchy\n\nIntrospection:\n tag.atoms() → Atom[] with this tag attached\n getAllTags() → Tag[] all registered tags`,\n },\n\n extensions: {\n title: \"Extensions Pipeline\",\n content: `Extensions wrap atom resolution and flow execution (middleware pattern).\n\ninterface Extension {\n init?(scope): void | Promise<void>\n dispose?(scope): void\n wrapResolve?(next, event: ResolveEvent): Promise<value>\n wrapExec?(next, flow, ctx): Promise<output>\n}\n\ncreateScope({ extensions: [ext1, ext2] })\n\nLifecycle:\n 1. scope creation → ext.init(scope) called for each extension\n 2. await scope.ready → all init() resolved\n 3. resolve(atom) → ext.wrapResolve(next, { kind: \"atom\", target, scope })\n resolve(resource) → ext.wrapResolve(next, { kind: \"resource\", target, ctx })\n - call next() to proceed to actual resolution\n - dispatch on event.kind for atom vs resource\n 4. ctx.exec(flow) → ext.wrapExec(next, flow, ctx)\n - call next() to proceed to actual execution\n 5. scope.dispose() → ext.dispose(scope) called for each extension\n\nExample:\n const timingExt: Extension = {\n wrapResolve: async (next, event) => {\n const start = Date.now()\n const value = await next()\n console.log(event.target, Date.now() - start, \"ms\")\n return value\n },\n }`,\n },\n\n testing: {\n title: \"Testing & Isolation\",\n content: `Use presets to swap implementations without changing production code.\n\nimport { createScope, preset } from \"@pumped-fn/lite\"\n\nconst scope = createScope({\n presets: [\n preset(dbAtom, mockDatabase),\n preset(cacheAtom, inMemoryCache),\n ],\n tags: [tenantTag(\"test-tenant\")],\n})\n\nconst db = await scope.resolve(dbAtom) // → mockDatabase (not real db)\n\nMulti-tenant isolation:\n Each scope is fully isolated. Create one scope per tenant/test.\n\n const tenantScope = createScope({\n tags: [tenantTag(tenantId)],\n presets: tenantOverrides,\n })\n\nCleanup:\n scope.dispose() releases all atoms and runs all cleanup functions.\n In tests: call scope.dispose() in afterEach.`,\n },\n\n patterns: {\n title: \"Common Patterns\",\n content: `Request lifecycle:\n const scope = createScope()\n const ctx = scope.createContext({ tags: [requestTag(req)] })\n const result = await ctx.exec({ flow: handleRequest, input: req.body })\n ctx.close() // cleanup LIFO\n\nService pattern:\n const userService = service({\n deps: { db: dbAtom },\n factory: (ctx, { db }) => ({\n getUser: (ctx, id) => db.findUser(id),\n updateUser: (ctx, id, data) => db.updateUser(id, data),\n }),\n })\n\nTyped flow input:\n const getUser = flow({\n parse: typed<{ id: string }>(),\n factory: (ctx) => findUser(ctx.input.id),\n })\n\nInline execution:\n const result = await ctx.exec({\n fn: (ctx, a, b) => a + b,\n params: [1, 2],\n })\n\nAtom with cleanup:\n const serverAtom = atom({\n factory: (ctx) => {\n const server = createServer()\n ctx.onClose(() => server.close())\n return server\n },\n })\n\nAtom retention / GC:\n createScope({ gc: { enabled: true, graceMs: 3000 } })\n atom({ keepAlive: true }) // never GC'd`,\n },\n\n \"tanstack-start\": {\n title: \"TanStack Start Integration\",\n content: `Singleton scope at server entry, per-request ExecutionContext via middleware.\n\nServer entry — one scope per process:\n const scope = createScope({ extensions: [otel()], tags: [envTag(env)] })\n export default createServerEntry({\n async fetch(request) {\n return handler.fetch(request, { context: { scope } })\n },\n })\n\nExecution context middleware — per-request lifecycle:\n export const execCtxMiddleware = createMiddleware()\n .server(async ({ next, context: { scope } }) => {\n const execContext = scope.createContext({})\n try {\n return await next({ context: { execContext } })\n } finally {\n await execContext.close()\n }\n })\n\nTag-seeding middleware — ambient data for downstream:\n export const authMiddleware = createMiddleware()\n .middleware([execCtxMiddleware])\n .server(async ({ next, context: { execContext } }) => {\n const user = await resolveCurrentUser()\n execContext.data.setTag(currentUserTag, user)\n return next({ context: { user } })\n })\n\n export const transactionMiddleware = createMiddleware()\n .middleware([authMiddleware])\n .server(async ({ next, context: { execContext } }) => {\n const tx = await beginTransaction()\n execContext.data.setTag(transactionTag, tx)\n try {\n const result = await next()\n await tx.commit()\n return result\n } catch (e) {\n await tx.rollback()\n throw e\n }\n })\n\nServer functions — execute flows via context:\n export const listInvoices = createServerFn({ method: 'POST' })\n .middleware([transactionMiddleware])\n .handler(async ({ data, context: { execContext } }) => {\n return execContext.exec({ flow: invoiceFlows.list, rawInput: data })\n })\n\nClient hydration — preset loader data into client scope:\n const loaderData = Route.useLoaderData()\n const scope = createScope({\n presets: [\n preset(invoicesAtom, loaderData.invoices),\n preset(userAtom, loaderData.user),\n ],\n })\n return <ScopeProvider scope={scope}><Outlet /></ScopeProvider>\n\nRules:\n One scope per server process Atoms cache singletons (connections, services)\n One execContext per request Tag isolation (user, tx, tracing)\n Middleware creates+closes ctx Guarantees cleanup even on error\n Tags over function params Flows read ambient tags, no signature coupling\n execContext.exec({ flow }) Flows get lifecycle, tracing, cleanup\n scope.resolve(atom) for deps Atoms are long-lived, cached in scope\n Preset server data on client No re-fetch; atoms hydrate from loader\n\nDon't:\n createScope() in a server fn New scope per request — atoms re-resolve, connections leak\n flow.factory(ctx, deps) direct Bypasses context lifecycle, tags, extensions, cleanup\n User/tx as flow input Couples signatures to transport; use tags instead\n scope.resolve(flow) Flows are ephemeral — exec(), don't resolve()\n ScopeProvider without presets Client re-fetches everything server already loaded`,\n },\n\n diagrams: {\n title: \"Visual Diagrams (mermaid)\",\n content: extractDiagram,\n },\n\n types: {\n title: \"Type Utilities & Guards\",\n content: `Type extractors (Lite.Utils namespace):\n AtomValue<A> extract resolved type from atom\n FlowOutput<F> extract output type from flow\n FlowInput<F> extract input type from flow\n TagValue<T> extract value type from tag\n DepsOf<A | F> extract deps record type\n ControllerValue<C> extract value from controller\n Simplify<T> flatten intersection types\n AtomType<T, D> construct atom type\n FlowType<O, I, D> construct flow type\n\nType guards:\n isAtom(v) → v is Atom\n isFlow(v) → v is Flow\n isTag(v) → v is Tag\n isTagged(v) → v is Tagged\n isPreset(v) → v is Preset\n isControllerDep(v) → v is ControllerDep\n isTagExecutor(v) → v is TagExecutor\n\nConvenience types:\n AnyAtom any atom regardless of value/deps\n AnyFlow any flow regardless of output/input/deps\n AnyController any controller regardless of value\n\nSymbols (advanced, for library authors):\n atomSymbol, flowSymbol, tagSymbol, taggedSymbol,\n presetSymbol, controllerSymbol, controllerDepSymbol,\n tagExecutorSymbol, typedSymbol`,\n },\n}\n\nconst args = process.argv.slice(2)\nconst category = args[0]\n\nif (!category || category === \"help\" || category === \"--help\") {\n console.log(\"@pumped-fn/lite — Scoped Ambient State for TypeScript\\n\")\n console.log(\"Usage: pumped-lite <category>\\n\")\n console.log(\"Categories:\")\n for (const [key, { title }] of Object.entries(categories)) {\n console.log(` ${key.padEnd(14)} ${title}`)\n }\n console.log(\"\\nExamples:\")\n console.log(\" npx @pumped-fn/lite primitives # API reference\")\n console.log(\" npx @pumped-fn/lite diagrams # mermaid diagrams\")\n process.exit(0)\n}\n\nif (!(category in categories)) {\n console.error(`Unknown category: \"${category}\"\\n`)\n console.error(\"Available categories: \" + Object.keys(categories).join(\", \"))\n process.exit(1)\n}\n\nconst entry = categories[category]!\nconst output = typeof entry.content === \"function\" ? entry.content() : entry.content\nconsole.log(`# ${entry.title}\\n`)\nconsole.log(output)\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,aAAa,KADb,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK,EACzB,YAAY,EAAE,QAAQ;AAE/D,SAAS,kBAA0B;CACjC,MAAM,MAAM,OAAO,QAAQ,kBAAkB;AAE7C,SADY,QAAQ,KAAK,SAAS,OAAO,MAAM,GAAG,IAAI,EAC3C,QAAQ,eAAe,GAAG,CAAC,MAAM;;AAG9C,SAAS,iBAAyB;CAChC,MAAM,QAAQ,OAAO,MAAM,4BAA4B;AACvD,QAAO,QAAQ,qDAAqD,MAAM,GAAI,MAAM,CAAC,YAAY;;AAGnG,MAAMA,aAAkF;CACtF,UAAU;EACR,OAAO;EACP,SAAS;EACV;CAED,gBAAgB;EACd,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4BV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoEV;CAED,OAAO;EACL,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;EAoBV;CAED,SAAS;EACP,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CV;CAED,MAAM;EACJ,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4CV;CAED,YAAY;EACV,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BV;CAED,SAAS;EACP,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;EAyBV;CAED,UAAU;EACR,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCV;CAED,kBAAkB;EAChB,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6EV;CAED,UAAU;EACR,OAAO;EACP,SAAS;EACV;CAED,OAAO;EACL,OAAO;EACP,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6BV;CACF;AAGD,MAAM,WADO,QAAQ,KAAK,MAAM,EAAE,CACZ;AAEtB,IAAI,CAAC,YAAY,aAAa,UAAU,aAAa,UAAU;AAC7D,SAAQ,IAAI,0DAA0D;AACtE,SAAQ,IAAI,kCAAkC;AAC9C,SAAQ,IAAI,cAAc;AAC1B,MAAK,MAAM,CAAC,KAAK,EAAE,YAAY,OAAO,QAAQ,WAAW,CACvD,SAAQ,IAAI,KAAK,IAAI,OAAO,GAAG,CAAC,GAAG,QAAQ;AAE7C,SAAQ,IAAI,cAAc;AAC1B,SAAQ,IAAI,qDAAqD;AACjE,SAAQ,IAAI,wDAAwD;AACpE,SAAQ,KAAK,EAAE;;AAGjB,IAAI,EAAE,YAAY,aAAa;AAC7B,SAAQ,MAAM,sBAAsB,SAAS,KAAK;AAClD,SAAQ,MAAM,2BAA2B,OAAO,KAAK,WAAW,CAAC,KAAK,KAAK,CAAC;AAC5E,SAAQ,KAAK,EAAE;;AAGjB,MAAM,QAAQ,WAAW;AACzB,MAAM,SAAS,OAAO,MAAM,YAAY,aAAa,MAAM,SAAS,GAAG,MAAM;AAC7E,QAAQ,IAAI,KAAK,MAAM,MAAM,IAAI;AACjC,QAAQ,IAAI,OAAO"}
|