@pumped-fn/lite 2.1.4 → 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/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: `atom({ factory, deps?, tags?, keepAlive? })
24
- Creates a managed effect. Factory receives (ctx, resolvedDeps) and returns a value.
25
- Cached per scope. Supports cleanup via ctx.onClose().
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
- Operation template executed per call. parse validates input, factory runs logic.
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. Attach to atoms/flows/contexts. Retrieve via tag.get/find/collect.
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 receives (ctx, ...args) for tracing/auth integration.`
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: `ctx = scope.createContext({ tags? })
86
- Execution boundary. Tags merge with scope tags. Cleanup runs LIFO on close.
87
-
88
- ctx.exec({ flow, input?, tags? }) → Promise<output>
89
- Execute a flow within this context. Creates a child context with merged tags.
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? }) → Promise<result>
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.onClose(cleanup) → void register cleanup (runs LIFO on ctx.close)
97
- ctx.close() void run all registered cleanups in LIFO order
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
- ctx.data
100
- Key-value store scoped to the context:
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: `controller(atom) → Controller
109
- Opt-in reactive handle for an atom.
110
-
111
- ctrl.get() current value (must be resolved first)
112
- ctrl.resolve() → Promise<value> (resolve if not yet)
113
- ctrl.set(value) → replace value, notify listeners
114
- ctrl.update(fn) → update value via function, notify listeners
115
- ctrl.invalidate() re-run factory, notify listeners
116
- ctrl.release() → release atom, run cleanups
117
- ctrl.on(event, listener) unsubscribe
118
- events: 'resolving' | 'resolved' | '*'
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 resolution events at scope level.
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: `tag<T>({ label, default?, parse? }) → Tag<T>
142
- Define an ambient context value type.
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
- tag(value) → Tagged<T>
145
- Create a tagged value to attach to atoms, flows, or contexts.
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 first match or throw
155
- tag.find(source) → T | undefined first match or undefined
156
- tag.collect(source) → T[] all matches
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 integration:
159
- ctx.data.setTag(tag, value)
160
- ctx.data.getTag(tag) T
161
- ctx.data.seekTag(tag) T (walks parent chain)
162
- ctx.data.hasTag(tag) boolean
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 tag or undefined
167
- tags.all(tag) → resolves all values for 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"}