@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.
- package/CHANGELOG.md +11 -0
- package/README.md +132 -154
- 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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
//
|
|
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
|
-
|
|
276
|
-
ctrl.on('
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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:
|
|
487
|
+
### Example: Timing Extension
|
|
458
488
|
|
|
459
489
|
```typescript
|
|
460
|
-
const
|
|
461
|
-
name: '
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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: [
|
|
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
|