@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/CHANGELOG.md +20 -0
- package/MIGRATION.md +2 -2
- package/PATTERNS.md +58 -39
- package/README.md +145 -187
- package/dist/cli.cjs +335 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +337 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +169 -22
- package/dist/index.d.cts +126 -18
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +126 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +167 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -4
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
|