@pumped-fn/lite 1.11.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs ADDED
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_url = require("node:url");
5
+
6
+ //#region src/cli.ts
7
+ const readme = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), ".."), "README.md"), "utf-8");
8
+ function extractOverview() {
9
+ const idx = readme.indexOf("## How It Works");
10
+ return (idx === -1 ? readme : readme.slice(0, idx)).replace(/^#[^\n]*\n+/, "").trim();
11
+ }
12
+ function extractDiagram() {
13
+ const match = readme.match(/```mermaid\n([\s\S]*?)```/);
14
+ return match ? `Full system sequence (unified):\n\n\`\`\`mermaid\n${match[1].trim()}\n\`\`\`` : "No diagram found in README.md";
15
+ }
16
+ const categories = {
17
+ overview: {
18
+ title: "What is @pumped-fn/lite",
19
+ content: extractOverview
20
+ },
21
+ primitives: {
22
+ 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().
26
+
27
+ import { atom } from "@pumped-fn/lite"
28
+ const dbAtom = atom({ factory: () => createDbPool() })
29
+ const userAtom = atom({
30
+ deps: { db: dbAtom },
31
+ factory: (ctx, { db }) => db.query("SELECT ..."),
32
+ })
33
+
34
+ flow({ factory, parse?, deps?, tags? })
35
+ Operation template executed per call. parse validates input, factory runs logic.
36
+
37
+ import { flow, typed } from "@pumped-fn/lite"
38
+ const getUser = flow({
39
+ parse: typed<{ id: string }>(),
40
+ deps: { db: dbAtom },
41
+ factory: (ctx, { db }) => db.findUser(ctx.input.id),
42
+ })
43
+
44
+ tag({ label, default?, parse? })
45
+ Ambient context value. Attach to atoms/flows/contexts. Retrieve via tag.get/find/collect.
46
+
47
+ import { tag } from "@pumped-fn/lite"
48
+ const tenantTag = tag<string>({ label: "tenant" })
49
+
50
+ preset(target, value)
51
+ Override an atom's resolved value. Used for testing and multi-tenant isolation.
52
+
53
+ import { preset } from "@pumped-fn/lite"
54
+ const mockDb = preset(dbAtom, fakeDatabaseInstance)
55
+
56
+ service({ factory, deps? })
57
+ Convenience wrapper for atom whose value is an object of methods.
58
+ Each method receives (ctx, ...args) for tracing/auth integration.`
59
+ },
60
+ scope: {
61
+ title: "Scope Management",
62
+ content: `createScope({ extensions?, presets?, tags?, gc? })
63
+ Creates a scope that manages atom resolution, caching, extensions, and GC.
64
+
65
+ import { createScope } from "@pumped-fn/lite"
66
+ const scope = createScope({
67
+ extensions: [loggingExt],
68
+ presets: [preset(dbAtom, mockDb)],
69
+ tags: [tenantTag("acme")],
70
+ gc: { enabled: true, graceMs: 3000 },
71
+ })
72
+ await scope.ready
73
+
74
+ scope.resolve(atom) → Promise<value> resolve and cache an atom
75
+ scope.controller(atom) → Controller get reactive handle
76
+ scope.select(atom, fn, opts?) → SelectHandle derived slice with equality check
77
+ scope.on(event, atom, fn) → unsubscribe listen to atom events
78
+ scope.release(atom) → void release atom, run cleanups
79
+ scope.createContext(opts?) → ExecutionContext create execution boundary
80
+ scope.flush() → Promise<void> wait all pending operations
81
+ scope.dispose() → void release everything, run all cleanups`
82
+ },
83
+ context: {
84
+ 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.
90
+ Child context closes automatically after execution.
91
+
92
+ ctx.exec({ fn, params?, tags? }) → Promise<result>
93
+ Execute an inline function: fn(childCtx, ...params).
94
+ Same child-context lifecycle as flow execution.
95
+
96
+ ctx.onClose(cleanup) → void register cleanup (runs LIFO on ctx.close)
97
+ ctx.close() → void run all registered cleanups in LIFO order
98
+
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.`
105
+ },
106
+ reactivity: {
107
+ 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' | '*'
119
+
120
+ select(atom, selector, { eq? }) → SelectHandle
121
+ Derived state slice. Only notifies when selected value changes per eq function.
122
+
123
+ handle.get() → current selected value
124
+ handle.subscribe(fn) → unsubscribe
125
+
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
+ })`
138
+ },
139
+ tags: {
140
+ title: "Tag System",
141
+ content: `tag<T>({ label, default?, parse? }) → Tag<T>
142
+ Define an ambient context value type.
143
+
144
+ tag(value) → Tagged<T>
145
+ Create a tagged value to attach to atoms, flows, or contexts.
146
+
147
+ 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")] })
152
+
153
+ 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
157
+
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
163
+
164
+ 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
168
+
169
+ Introspection:
170
+ tag.atoms() → Atom[] with this tag attached
171
+ getAllTags() → Tag[] all registered tags`
172
+ },
173
+ extensions: {
174
+ title: "Extensions Pipeline",
175
+ content: `Extensions wrap atom resolution and flow execution (middleware pattern).
176
+
177
+ interface Extension {
178
+ init?(scope): void | Promise<void>
179
+ dispose?(scope): void
180
+ wrapResolve?(next, event: ResolveEvent): Promise<value>
181
+ wrapExec?(next, flow, ctx): Promise<output>
182
+ }
183
+
184
+ createScope({ extensions: [ext1, ext2] })
185
+
186
+ Lifecycle:
187
+ 1. scope creation → ext.init(scope) called for each extension
188
+ 2. await scope.ready → all init() resolved
189
+ 3. resolve(atom) → ext.wrapResolve(next, { kind: "atom", target, scope })
190
+ resolve(resource) → ext.wrapResolve(next, { kind: "resource", target, ctx })
191
+ - call next() to proceed to actual resolution
192
+ - dispatch on event.kind for atom vs resource
193
+ 4. ctx.exec(flow) → ext.wrapExec(next, flow, ctx)
194
+ - call next() to proceed to actual execution
195
+ 5. scope.dispose() → ext.dispose(scope) called for each extension
196
+
197
+ Example:
198
+ const timingExt: Extension = {
199
+ wrapResolve: async (next, event) => {
200
+ const start = Date.now()
201
+ const value = await next()
202
+ console.log(event.target, Date.now() - start, "ms")
203
+ return value
204
+ },
205
+ }`
206
+ },
207
+ testing: {
208
+ title: "Testing & Isolation",
209
+ content: `Use presets to swap implementations without changing production code.
210
+
211
+ import { createScope, preset } from "@pumped-fn/lite"
212
+
213
+ const scope = createScope({
214
+ presets: [
215
+ preset(dbAtom, mockDatabase),
216
+ preset(cacheAtom, inMemoryCache),
217
+ ],
218
+ tags: [tenantTag("test-tenant")],
219
+ })
220
+
221
+ const db = await scope.resolve(dbAtom) // → mockDatabase (not real db)
222
+
223
+ Multi-tenant isolation:
224
+ Each scope is fully isolated. Create one scope per tenant/test.
225
+
226
+ const tenantScope = createScope({
227
+ tags: [tenantTag(tenantId)],
228
+ presets: tenantOverrides,
229
+ })
230
+
231
+ Cleanup:
232
+ scope.dispose() releases all atoms and runs all cleanup functions.
233
+ In tests: call scope.dispose() in afterEach.`
234
+ },
235
+ patterns: {
236
+ title: "Common Patterns",
237
+ content: `Request lifecycle:
238
+ const scope = createScope()
239
+ const ctx = scope.createContext({ tags: [requestTag(req)] })
240
+ const result = await ctx.exec({ flow: handleRequest, input: req.body })
241
+ ctx.close() // cleanup LIFO
242
+
243
+ Service pattern:
244
+ const userService = service({
245
+ deps: { db: dbAtom },
246
+ factory: (ctx, { db }) => ({
247
+ getUser: (ctx, id) => db.findUser(id),
248
+ updateUser: (ctx, id, data) => db.updateUser(id, data),
249
+ }),
250
+ })
251
+
252
+ Typed flow input:
253
+ const getUser = flow({
254
+ parse: typed<{ id: string }>(),
255
+ factory: (ctx) => findUser(ctx.input.id),
256
+ })
257
+
258
+ Inline execution:
259
+ const result = await ctx.exec({
260
+ fn: (ctx, a, b) => a + b,
261
+ params: [1, 2],
262
+ })
263
+
264
+ Atom with cleanup:
265
+ const serverAtom = atom({
266
+ factory: (ctx) => {
267
+ const server = createServer()
268
+ ctx.onClose(() => server.close())
269
+ return server
270
+ },
271
+ })
272
+
273
+ Atom retention / GC:
274
+ createScope({ gc: { enabled: true, graceMs: 3000 } })
275
+ atom({ keepAlive: true }) // never GC'd`
276
+ },
277
+ diagrams: {
278
+ title: "Visual Diagrams (mermaid)",
279
+ content: extractDiagram
280
+ },
281
+ types: {
282
+ title: "Type Utilities & Guards",
283
+ content: `Type extractors (Lite.Utils namespace):
284
+ AtomValue<A> extract resolved type from atom
285
+ FlowOutput<F> extract output type from flow
286
+ FlowInput<F> extract input type from flow
287
+ TagValue<T> extract value type from tag
288
+ DepsOf<A | F> extract deps record type
289
+ ControllerValue<C> extract value from controller
290
+ Simplify<T> flatten intersection types
291
+ AtomType<T, D> construct atom type
292
+ FlowType<O, I, D> construct flow type
293
+
294
+ Type guards:
295
+ isAtom(v) → v is Atom
296
+ isFlow(v) → v is Flow
297
+ isTag(v) → v is Tag
298
+ isTagged(v) → v is Tagged
299
+ isPreset(v) → v is Preset
300
+ isControllerDep(v) → v is ControllerDep
301
+ isTagExecutor(v) → v is TagExecutor
302
+
303
+ Convenience types:
304
+ AnyAtom any atom regardless of value/deps
305
+ AnyFlow any flow regardless of output/input/deps
306
+ AnyController any controller regardless of value
307
+
308
+ Symbols (advanced, for library authors):
309
+ atomSymbol, flowSymbol, tagSymbol, taggedSymbol,
310
+ presetSymbol, controllerSymbol, controllerDepSymbol,
311
+ tagExecutorSymbol, typedSymbol`
312
+ }
313
+ };
314
+ const category = process.argv.slice(2)[0];
315
+ if (!category || category === "help" || category === "--help") {
316
+ console.log("@pumped-fn/lite — Scoped Ambient State for TypeScript\n");
317
+ console.log("Usage: pumped-lite <category>\n");
318
+ console.log("Categories:");
319
+ for (const [key, { title }] of Object.entries(categories)) console.log(` ${key.padEnd(14)} ${title}`);
320
+ console.log("\nExamples:");
321
+ console.log(" npx @pumped-fn/lite primitives # API reference");
322
+ console.log(" npx @pumped-fn/lite diagrams # mermaid diagrams");
323
+ process.exit(0);
324
+ }
325
+ if (!(category in categories)) {
326
+ console.error(`Unknown category: "${category}"\n`);
327
+ console.error("Available categories: " + Object.keys(categories).join(", "));
328
+ process.exit(1);
329
+ }
330
+ const entry = categories[category];
331
+ const output = typeof entry.content === "function" ? entry.content() : entry.content;
332
+ console.log(`# ${entry.title}\n`);
333
+ console.log(output);
334
+
335
+ //#endregion
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ //#region src/cli.ts
7
+ const readme = readFileSync(join(join(dirname(fileURLToPath(import.meta.url)), ".."), "README.md"), "utf-8");
8
+ function extractOverview() {
9
+ const idx = readme.indexOf("## How It Works");
10
+ return (idx === -1 ? readme : readme.slice(0, idx)).replace(/^#[^\n]*\n+/, "").trim();
11
+ }
12
+ function extractDiagram() {
13
+ const match = readme.match(/```mermaid\n([\s\S]*?)```/);
14
+ return match ? `Full system sequence (unified):\n\n\`\`\`mermaid\n${match[1].trim()}\n\`\`\`` : "No diagram found in README.md";
15
+ }
16
+ const categories = {
17
+ overview: {
18
+ title: "What is @pumped-fn/lite",
19
+ content: extractOverview
20
+ },
21
+ primitives: {
22
+ 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().
26
+
27
+ import { atom } from "@pumped-fn/lite"
28
+ const dbAtom = atom({ factory: () => createDbPool() })
29
+ const userAtom = atom({
30
+ deps: { db: dbAtom },
31
+ factory: (ctx, { db }) => db.query("SELECT ..."),
32
+ })
33
+
34
+ flow({ factory, parse?, deps?, tags? })
35
+ Operation template executed per call. parse validates input, factory runs logic.
36
+
37
+ import { flow, typed } from "@pumped-fn/lite"
38
+ const getUser = flow({
39
+ parse: typed<{ id: string }>(),
40
+ deps: { db: dbAtom },
41
+ factory: (ctx, { db }) => db.findUser(ctx.input.id),
42
+ })
43
+
44
+ tag({ label, default?, parse? })
45
+ Ambient context value. Attach to atoms/flows/contexts. Retrieve via tag.get/find/collect.
46
+
47
+ import { tag } from "@pumped-fn/lite"
48
+ const tenantTag = tag<string>({ label: "tenant" })
49
+
50
+ preset(target, value)
51
+ Override an atom's resolved value. Used for testing and multi-tenant isolation.
52
+
53
+ import { preset } from "@pumped-fn/lite"
54
+ const mockDb = preset(dbAtom, fakeDatabaseInstance)
55
+
56
+ service({ factory, deps? })
57
+ Convenience wrapper for atom whose value is an object of methods.
58
+ Each method receives (ctx, ...args) for tracing/auth integration.`
59
+ },
60
+ scope: {
61
+ title: "Scope Management",
62
+ content: `createScope({ extensions?, presets?, tags?, gc? })
63
+ Creates a scope that manages atom resolution, caching, extensions, and GC.
64
+
65
+ import { createScope } from "@pumped-fn/lite"
66
+ const scope = createScope({
67
+ extensions: [loggingExt],
68
+ presets: [preset(dbAtom, mockDb)],
69
+ tags: [tenantTag("acme")],
70
+ gc: { enabled: true, graceMs: 3000 },
71
+ })
72
+ await scope.ready
73
+
74
+ scope.resolve(atom) → Promise<value> resolve and cache an atom
75
+ scope.controller(atom) → Controller get reactive handle
76
+ scope.select(atom, fn, opts?) → SelectHandle derived slice with equality check
77
+ scope.on(event, atom, fn) → unsubscribe listen to atom events
78
+ scope.release(atom) → void release atom, run cleanups
79
+ scope.createContext(opts?) → ExecutionContext create execution boundary
80
+ scope.flush() → Promise<void> wait all pending operations
81
+ scope.dispose() → void release everything, run all cleanups`
82
+ },
83
+ context: {
84
+ 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.
90
+ Child context closes automatically after execution.
91
+
92
+ ctx.exec({ fn, params?, tags? }) → Promise<result>
93
+ Execute an inline function: fn(childCtx, ...params).
94
+ Same child-context lifecycle as flow execution.
95
+
96
+ ctx.onClose(cleanup) → void register cleanup (runs LIFO on ctx.close)
97
+ ctx.close() → void run all registered cleanups in LIFO order
98
+
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.`
105
+ },
106
+ reactivity: {
107
+ 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' | '*'
119
+
120
+ select(atom, selector, { eq? }) → SelectHandle
121
+ Derived state slice. Only notifies when selected value changes per eq function.
122
+
123
+ handle.get() → current selected value
124
+ handle.subscribe(fn) → unsubscribe
125
+
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
+ })`
138
+ },
139
+ tags: {
140
+ title: "Tag System",
141
+ content: `tag<T>({ label, default?, parse? }) → Tag<T>
142
+ Define an ambient context value type.
143
+
144
+ tag(value) → Tagged<T>
145
+ Create a tagged value to attach to atoms, flows, or contexts.
146
+
147
+ 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")] })
152
+
153
+ 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
157
+
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
163
+
164
+ 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
168
+
169
+ Introspection:
170
+ tag.atoms() → Atom[] with this tag attached
171
+ getAllTags() → Tag[] all registered tags`
172
+ },
173
+ extensions: {
174
+ title: "Extensions Pipeline",
175
+ content: `Extensions wrap atom resolution and flow execution (middleware pattern).
176
+
177
+ interface Extension {
178
+ init?(scope): void | Promise<void>
179
+ dispose?(scope): void
180
+ wrapResolve?(next, event: ResolveEvent): Promise<value>
181
+ wrapExec?(next, flow, ctx): Promise<output>
182
+ }
183
+
184
+ createScope({ extensions: [ext1, ext2] })
185
+
186
+ Lifecycle:
187
+ 1. scope creation → ext.init(scope) called for each extension
188
+ 2. await scope.ready → all init() resolved
189
+ 3. resolve(atom) → ext.wrapResolve(next, { kind: "atom", target, scope })
190
+ resolve(resource) → ext.wrapResolve(next, { kind: "resource", target, ctx })
191
+ - call next() to proceed to actual resolution
192
+ - dispatch on event.kind for atom vs resource
193
+ 4. ctx.exec(flow) → ext.wrapExec(next, flow, ctx)
194
+ - call next() to proceed to actual execution
195
+ 5. scope.dispose() → ext.dispose(scope) called for each extension
196
+
197
+ Example:
198
+ const timingExt: Extension = {
199
+ wrapResolve: async (next, event) => {
200
+ const start = Date.now()
201
+ const value = await next()
202
+ console.log(event.target, Date.now() - start, "ms")
203
+ return value
204
+ },
205
+ }`
206
+ },
207
+ testing: {
208
+ title: "Testing & Isolation",
209
+ content: `Use presets to swap implementations without changing production code.
210
+
211
+ import { createScope, preset } from "@pumped-fn/lite"
212
+
213
+ const scope = createScope({
214
+ presets: [
215
+ preset(dbAtom, mockDatabase),
216
+ preset(cacheAtom, inMemoryCache),
217
+ ],
218
+ tags: [tenantTag("test-tenant")],
219
+ })
220
+
221
+ const db = await scope.resolve(dbAtom) // → mockDatabase (not real db)
222
+
223
+ Multi-tenant isolation:
224
+ Each scope is fully isolated. Create one scope per tenant/test.
225
+
226
+ const tenantScope = createScope({
227
+ tags: [tenantTag(tenantId)],
228
+ presets: tenantOverrides,
229
+ })
230
+
231
+ Cleanup:
232
+ scope.dispose() releases all atoms and runs all cleanup functions.
233
+ In tests: call scope.dispose() in afterEach.`
234
+ },
235
+ patterns: {
236
+ title: "Common Patterns",
237
+ content: `Request lifecycle:
238
+ const scope = createScope()
239
+ const ctx = scope.createContext({ tags: [requestTag(req)] })
240
+ const result = await ctx.exec({ flow: handleRequest, input: req.body })
241
+ ctx.close() // cleanup LIFO
242
+
243
+ Service pattern:
244
+ const userService = service({
245
+ deps: { db: dbAtom },
246
+ factory: (ctx, { db }) => ({
247
+ getUser: (ctx, id) => db.findUser(id),
248
+ updateUser: (ctx, id, data) => db.updateUser(id, data),
249
+ }),
250
+ })
251
+
252
+ Typed flow input:
253
+ const getUser = flow({
254
+ parse: typed<{ id: string }>(),
255
+ factory: (ctx) => findUser(ctx.input.id),
256
+ })
257
+
258
+ Inline execution:
259
+ const result = await ctx.exec({
260
+ fn: (ctx, a, b) => a + b,
261
+ params: [1, 2],
262
+ })
263
+
264
+ Atom with cleanup:
265
+ const serverAtom = atom({
266
+ factory: (ctx) => {
267
+ const server = createServer()
268
+ ctx.onClose(() => server.close())
269
+ return server
270
+ },
271
+ })
272
+
273
+ Atom retention / GC:
274
+ createScope({ gc: { enabled: true, graceMs: 3000 } })
275
+ atom({ keepAlive: true }) // never GC'd`
276
+ },
277
+ diagrams: {
278
+ title: "Visual Diagrams (mermaid)",
279
+ content: extractDiagram
280
+ },
281
+ types: {
282
+ title: "Type Utilities & Guards",
283
+ content: `Type extractors (Lite.Utils namespace):
284
+ AtomValue<A> extract resolved type from atom
285
+ FlowOutput<F> extract output type from flow
286
+ FlowInput<F> extract input type from flow
287
+ TagValue<T> extract value type from tag
288
+ DepsOf<A | F> extract deps record type
289
+ ControllerValue<C> extract value from controller
290
+ Simplify<T> flatten intersection types
291
+ AtomType<T, D> construct atom type
292
+ FlowType<O, I, D> construct flow type
293
+
294
+ Type guards:
295
+ isAtom(v) → v is Atom
296
+ isFlow(v) → v is Flow
297
+ isTag(v) → v is Tag
298
+ isTagged(v) → v is Tagged
299
+ isPreset(v) → v is Preset
300
+ isControllerDep(v) → v is ControllerDep
301
+ isTagExecutor(v) → v is TagExecutor
302
+
303
+ Convenience types:
304
+ AnyAtom any atom regardless of value/deps
305
+ AnyFlow any flow regardless of output/input/deps
306
+ AnyController any controller regardless of value
307
+
308
+ Symbols (advanced, for library authors):
309
+ atomSymbol, flowSymbol, tagSymbol, taggedSymbol,
310
+ presetSymbol, controllerSymbol, controllerDepSymbol,
311
+ tagExecutorSymbol, typedSymbol`
312
+ }
313
+ };
314
+ const category = process.argv.slice(2)[0];
315
+ if (!category || category === "help" || category === "--help") {
316
+ console.log("@pumped-fn/lite — Scoped Ambient State for TypeScript\n");
317
+ console.log("Usage: pumped-lite <category>\n");
318
+ console.log("Categories:");
319
+ for (const [key, { title }] of Object.entries(categories)) console.log(` ${key.padEnd(14)} ${title}`);
320
+ console.log("\nExamples:");
321
+ console.log(" npx @pumped-fn/lite primitives # API reference");
322
+ console.log(" npx @pumped-fn/lite diagrams # mermaid diagrams");
323
+ process.exit(0);
324
+ }
325
+ if (!(category in categories)) {
326
+ console.error(`Unknown category: "${category}"\n`);
327
+ console.error("Available categories: " + Object.keys(categories).join(", "));
328
+ process.exit(1);
329
+ }
330
+ const entry = categories[category];
331
+ const output = typeof entry.content === "function" ? entry.content() : entry.content;
332
+ console.log(`# ${entry.title}\n`);
333
+ console.log(output);
334
+
335
+ //#endregion
336
+ export { };
337
+ //# sourceMappingURL=cli.mjs.map