@pumped-fn/lite 1.2.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +132 -154
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b524371: docs: replace ASCII diagrams with Mermaid and streamline code examples
8
+
9
+ - Convert Core Concepts ASCII chart to Mermaid graph
10
+ - Add Mermaid diagrams for Atoms, Flows, Controllers, Tags, Presets, and Extensions sections
11
+ - Replace verbose code examples with concise versions where diagrams communicate the concept
12
+ - Reduce README from ~710 lines to ~690 lines while improving visual clarity
13
+
3
14
  ## 1.2.0
4
15
 
5
16
  ### Minor Changes
package/README.md CHANGED
@@ -64,23 +64,15 @@ await scope.dispose()
64
64
 
65
65
  ## Core Concepts
66
66
 
67
- ```
68
- ┌─────────────────────────────────────────────────────────────┐
69
- Scope
70
- (long-lived execution boundary)
71
- │ │
72
- │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
73
- │ │ Atom │ ──── │ Atom │ ──── │ Atom │ │
74
- │ │ (effect)│ │ (effect)│ │ (effect)│ │
75
- │ └─────────┘ └─────────┘ └─────────┘ │
76
- │ │ │ │
77
- │ └──────────────┬───────────────────┘ │
78
- │ ▼ │
79
- │ ┌─────────────────────────────────────────────────────┐ │
80
- │ │ ExecutionContext │ │
81
- │ │ (short-lived operation with input, tags, cleanup) │ │
82
- │ └─────────────────────────────────────────────────────┘ │
83
- └─────────────────────────────────────────────────────────────┘
67
+ ```mermaid
68
+ graph TB
69
+ subgraph Scope["Scope (long-lived execution boundary)"]
70
+ A1["Atom (effect)"] --- A2["Atom (effect)"]
71
+ A2 --- A3["Atom (effect)"]
72
+ A1 --> EC
73
+ A3 --> EC
74
+ EC["ExecutionContext<br/>(short-lived operation with input, tags, cleanup)"]
75
+ end
84
76
  ```
85
77
 
86
78
  | Concept | Purpose |
@@ -96,6 +88,16 @@ await scope.dispose()
96
88
 
97
89
  Atoms are long-lived dependencies that are cached within a scope.
98
90
 
91
+ ```mermaid
92
+ flowchart TB
93
+ subgraph Scope
94
+ Config["configAtom"] --> API["apiClientAtom"]
95
+ DB["dbAtom"] --> Repo["userRepoAtom"]
96
+ API --> Service["userServiceAtom"]
97
+ Repo --> Service
98
+ end
99
+ ```
100
+
99
101
  ### Basic Atom
100
102
 
101
103
  ```typescript
@@ -174,6 +176,22 @@ const counterAtom = atom({
174
176
 
175
177
  Flows are templates for short-lived operations.
176
178
 
179
+ ```mermaid
180
+ sequenceDiagram
181
+ participant Client
182
+ participant Context as ExecutionContext
183
+ participant Flow
184
+ participant Atoms
185
+
186
+ Client->>Context: exec({ flow, input })
187
+ Context->>Atoms: resolve dependencies
188
+ Atoms-->>Context: deps
189
+ Context->>Flow: factory(ctx, deps)
190
+ Flow-->>Context: result
191
+ Context-->>Client: result
192
+ Client->>Context: close()
193
+ ```
194
+
177
195
  ### Basic Flow
178
196
 
179
197
  ```typescript
@@ -210,18 +228,11 @@ Parse runs before the factory, and `ctx.input` type is inferred from the parse r
210
228
  ### Executing Flows
211
229
 
212
230
  ```typescript
213
- const scope = createScope()
214
- await scope.ready
215
-
216
231
  const context = scope.createContext()
217
-
218
- // Execute with input
219
232
  const user = await context.exec({
220
233
  flow: createUserFlow,
221
234
  input: { name: 'Alice', email: 'alice@example.com' }
222
235
  })
223
-
224
- // Always close the context when done
225
236
  await context.close()
226
237
  ```
227
238
 
@@ -251,79 +262,78 @@ await context.exec({
251
262
 
252
263
  Controllers provide reactive access to atom state.
253
264
 
265
+ ```mermaid
266
+ flowchart LR
267
+ subgraph Controller
268
+ State["state: idle|resolving|resolved|failed"]
269
+ Get["get()"]
270
+ Inv["invalidate()"]
271
+ end
272
+
273
+ App -->|subscribe| Controller
274
+ Controller -->|on 'resolved'| App
275
+ Controller -->|on 'resolving'| App
276
+ ```
277
+
254
278
  ### Basic Usage
255
279
 
256
280
  ```typescript
257
281
  const ctrl = scope.controller(configAtom)
258
282
 
259
- // Access state
260
- console.log(ctrl.state) // 'idle' | 'resolving' | 'resolved' | 'failed'
261
-
262
- // Get resolved value (throws if not resolved)
263
- const config = ctrl.get()
264
-
265
- // Resolve and wait
266
- const config = await ctrl.resolve()
267
-
268
- // Manual invalidation
269
- ctrl.invalidate()
283
+ ctrl.state // 'idle' | 'resolving' | 'resolved' | 'failed'
284
+ ctrl.get() // value (throws if not resolved)
285
+ await ctrl.resolve() // resolve and wait
286
+ ctrl.invalidate() // trigger re-resolution
270
287
  ```
271
288
 
272
289
  ### Subscribing to Changes
273
290
 
274
291
  ```typescript
275
- // Subscribe to specific events
276
- ctrl.on('resolved', () => {
277
- console.log('Config updated:', ctrl.get())
278
- })
279
-
280
- ctrl.on('resolving', () => {
281
- console.log('Config is refreshing...')
282
- })
283
-
284
- // Subscribe to all state changes
285
- ctrl.on('*', () => {
286
- console.log('State changed:', ctrl.state)
287
- })
292
+ ctrl.on('resolved', () => console.log('Updated:', ctrl.get()))
293
+ ctrl.on('resolving', () => console.log('Refreshing...'))
294
+ ctrl.on('*', () => console.log('State:', ctrl.state))
288
295
  ```
289
296
 
290
297
  ### Controller as Dependency
291
298
 
292
- Use `controller()` to get a Controller instead of the resolved value:
299
+ Use `controller()` to receive a Controller instead of the resolved value:
293
300
 
294
301
  ```typescript
295
302
  const appAtom = atom({
296
303
  deps: { config: controller(configAtom) },
297
304
  factory: (ctx, { config }) => {
298
- // Subscribe to config changes
299
- config.on('resolved', () => {
300
- console.log('Config updated, reinitializing...')
301
- ctx.invalidate()
302
- })
303
-
305
+ config.on('resolved', () => ctx.invalidate())
304
306
  return new App(config.get())
305
307
  }
306
308
  })
307
309
  ```
308
310
 
309
- ### Fine-Grained Reactivity with select()
311
+ ### Fine-Grained Reactivity
312
+
313
+ Use `select()` to subscribe only when a derived value changes:
310
314
 
311
315
  ```typescript
312
- const portSelect = scope.select(
313
- configAtom,
314
- (config) => config.port,
315
- { eq: (a, b) => a === b } // Optional custom equality
316
- )
317
-
318
- portSelect.subscribe(() => {
319
- console.log('Port changed:', portSelect.get())
320
- })
316
+ const portSelect = scope.select(configAtom, (c) => c.port)
317
+ portSelect.subscribe(() => console.log('Port changed:', portSelect.get()))
321
318
  ```
322
319
 
323
320
  ## Tags
324
321
 
325
322
  Tags pass contextual values through execution without explicit wiring.
326
323
 
324
+ ```mermaid
325
+ flowchart LR
326
+ subgraph Definition
327
+ T["tag({ label: 'tenantId' })"]
328
+ end
329
+
330
+ subgraph Usage
331
+ R["tags.required(tag)"] --> |"throws if missing"| V1["string"]
332
+ O["tags.optional(tag)"] --> |"undefined if missing"| V2["string | undefined"]
333
+ A["tags.all(tag)"] --> |"collects all"| V3["string[]"]
334
+ end
335
+ ```
336
+
327
337
  ### Creating Tags
328
338
 
329
339
  ```typescript
@@ -372,25 +382,26 @@ const multiAtom = atom({
372
382
 
373
383
  ### Passing Tags
374
384
 
375
- ```typescript
376
- // At scope creation
377
- const scope = createScope({
378
- tags: [tenantIdTag('tenant-123')]
379
- })
385
+ Tags can be passed at different levels, with inner levels inheriting from outer:
380
386
 
381
- // At context creation
382
- const context = scope.createContext({
383
- tags: [userRolesTag(['admin', 'user'])]
384
- })
387
+ ```mermaid
388
+ flowchart LR
389
+ subgraph Scope["createScope()"]
390
+ ST["tenantId: 'tenant-123'"]
391
+ subgraph Context["createContext()"]
392
+ CT["userRoles: ['admin']"]
393
+ subgraph Exec["exec()"]
394
+ ET["requestId: 'req-456'"]
395
+ end
396
+ end
397
+ end
385
398
 
386
- // At execution time
387
- await context.exec({
388
- flow: myFlow,
389
- input: data,
390
- tags: [requestIdTag('req-456')]
391
- })
399
+ ST -.->|inherited| CT
400
+ CT -.->|inherited| ET
392
401
  ```
393
402
 
403
+ All three tag levels are available during flow execution.
404
+
394
405
  ### Direct Tag Methods
395
406
 
396
407
  ```typescript
@@ -410,38 +421,57 @@ const allRoles = userRolesTag.collect(tags)
410
421
 
411
422
  Presets inject or redirect atom values, useful for testing.
412
423
 
413
- ### Value Injection
424
+ ```mermaid
425
+ flowchart LR
426
+ subgraph Normal["Normal Resolution"]
427
+ A1[resolve dbAtom] --> F1[factory]
428
+ F1 --> V1[real connection]
429
+ end
414
430
 
415
- ```typescript
416
- const mockDb = { query: jest.fn() }
431
+ subgraph Preset["With Preset"]
432
+ A2[resolve dbAtom] --> P[preset check]
433
+ P -->|value| V2[mockDb]
434
+ P -->|atom| F2[testAtom.factory]
435
+ end
436
+ ```
417
437
 
438
+ **Value injection** bypasses the factory entirely:
439
+ ```typescript
418
440
  const scope = createScope({
419
441
  presets: [preset(dbAtom, mockDb)]
420
442
  })
421
-
422
- // resolves to mockDb without calling factory
423
- const db = await scope.resolve(dbAtom)
424
443
  ```
425
444
 
426
- ### Atom Redirection
427
-
445
+ **Atom redirection** uses another atom's factory:
428
446
  ```typescript
429
- const testConfigAtom = atom({
430
- factory: () => ({ apiUrl: 'http://localhost:3000' })
431
- })
432
-
433
447
  const scope = createScope({
434
448
  presets: [preset(configAtom, testConfigAtom)]
435
449
  })
436
-
437
- // resolves testConfigAtom instead of configAtom
438
- const config = await scope.resolve(configAtom)
439
450
  ```
440
451
 
441
452
  ## Extensions
442
453
 
443
454
  Extensions provide cross-cutting behavior via AOP-style hooks.
444
455
 
456
+ ```mermaid
457
+ sequenceDiagram
458
+ participant App
459
+ participant Ext1 as Extension 1
460
+ participant Ext2 as Extension 2
461
+ participant Core as Core Logic
462
+
463
+ App->>Ext1: resolve(atom)
464
+ Ext1->>Ext1: before logic
465
+ Ext1->>Ext2: next()
466
+ Ext2->>Ext2: before logic
467
+ Ext2->>Core: next()
468
+ Core-->>Ext2: value
469
+ Ext2->>Ext2: after logic
470
+ Ext2-->>Ext1: value
471
+ Ext1->>Ext1: after logic
472
+ Ext1-->>App: value
473
+ ```
474
+
445
475
  ### Extension Interface
446
476
 
447
477
  ```typescript
@@ -454,73 +484,21 @@ interface Extension {
454
484
  }
455
485
  ```
456
486
 
457
- ### Example: Logging Extension
487
+ ### Example: Timing Extension
458
488
 
459
489
  ```typescript
460
- const loggingExtension: Lite.Extension = {
461
- name: 'logging',
462
-
463
- init: (scope) => {
464
- console.log('Scope initialized')
465
- },
490
+ const timingExtension: Lite.Extension = {
491
+ name: 'timing',
466
492
 
467
493
  wrapResolve: async (next, atom, scope) => {
468
494
  const start = performance.now()
469
- console.log('Resolving atom...')
470
-
471
- try {
472
- const result = await next()
473
- console.log(`Resolved in ${performance.now() - start}ms`)
474
- return result
475
- } catch (error) {
476
- console.error('Resolution failed:', error)
477
- throw error
478
- }
479
- },
480
-
481
- wrapExec: async (next, target, ctx) => {
482
- console.log('Executing:', 'flow' in target ? 'flow' : 'function')
483
- return next()
484
- },
485
-
486
- dispose: (scope) => {
487
- console.log('Scope disposed')
495
+ const result = await next()
496
+ console.log(`Resolved in ${performance.now() - start}ms`)
497
+ return result
488
498
  }
489
499
  }
490
500
 
491
- const scope = createScope({ extensions: [loggingExtension] })
492
- ```
493
-
494
- ### Example: Metrics Extension
495
-
496
- ```typescript
497
- const metricsExtension: Lite.Extension = {
498
- name: 'metrics',
499
-
500
- wrapResolve: async (next, atom, scope) => {
501
- const start = performance.now()
502
- try {
503
- const result = await next()
504
- metrics.recordResolution(atom, performance.now() - start, 'success')
505
- return result
506
- } catch (error) {
507
- metrics.recordResolution(atom, performance.now() - start, 'error')
508
- throw error
509
- }
510
- },
511
-
512
- wrapExec: async (next, target, ctx) => {
513
- const start = performance.now()
514
- try {
515
- const result = await next()
516
- metrics.recordExecution(target, performance.now() - start, 'success')
517
- return result
518
- } catch (error) {
519
- metrics.recordExecution(target, performance.now() - start, 'error')
520
- throw error
521
- }
522
- }
523
- }
501
+ const scope = createScope({ extensions: [timingExtension] })
524
502
  ```
525
503
 
526
504
  ## Lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pumped-fn/lite",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Lightweight dependency injection with minimal reactivity",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",