@pumped-fn/lite 1.9.2 → 1.11.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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 1.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 60604a2: Add automatic garbage collection for atoms
8
+
9
+ - Atoms are automatically released when they have no subscribers after a configurable grace period (default 3000ms)
10
+ - Cascading GC: dependencies are protected while dependents are mounted
11
+ - New `keepAlive: true` option on atoms to prevent auto-release
12
+ - New `gc: { enabled, graceMs }` option on `createScope()` to configure or disable GC
13
+ - React Strict Mode compatible via grace period (handles double-mount/unmount)
14
+ - Disable with `createScope({ gc: { enabled: false } })` to preserve pre-1.11 behavior
15
+
16
+ - 06d527f: Add utility types for better DX and boundary types for extensions
17
+
18
+ - Add `Lite.Utils` namespace with type extraction utilities:
19
+ - `AtomValue<A>`, `FlowOutput<F>`, `FlowInput<F>`, `TagValue<T>`, `ControllerValue<C>`
20
+ - `DepsOf<T>`, `Simplify<T>`, `AtomType<T, D>`, `FlowType<O, I, D>`
21
+ - Add boundary types for passthrough extension code:
22
+ - `AnyAtom`, `AnyFlow`, `AnyController`
23
+ - Add `ExecTarget` and `ExecTargetFn` type aliases for cleaner extension signatures
24
+
25
+ ### Patch Changes
26
+
27
+ - a017021: docs: add Flow Deps & Execution pattern and improve documentation
28
+
29
+ - Add "Flow Deps & Execution" section to PATTERNS.md covering:
30
+ - Deps resolution (atoms from Scope vs tags from context hierarchy)
31
+ - Service invocation via ctx.exec (observable by extensions)
32
+ - Cleanup pattern with ctx.onClose (pessimistic cleanup)
33
+ - Remove redundant patterns (Command, Interceptor) covered by composite patterns
34
+ - Remove verbose Error Boundary diagram, replaced with bullet point
35
+ - Add Documentation section to README linking PATTERNS.md and API reference
36
+
37
+ ## 1.10.0
38
+
39
+ ### Minor Changes
40
+
41
+ - d227191: Add tag and atom registries for automatic tracking
42
+
43
+ - Add `tag.atoms()` method to query all atoms that use a specific tag
44
+ - Add `getAllTags()` function to query all created tags
45
+ - Tagged values now include a `tag` reference to their parent Tag
46
+ - Uses WeakRef for memory-efficient tracking (tags and atoms can be GC'd)
47
+ - Automatic registration when `tag()` and `atom()` are called
48
+
3
49
  ## 1.9.2
4
50
 
5
51
  ### Patch Changes
package/PATTERNS.md CHANGED
@@ -44,7 +44,7 @@ sequenceDiagram
44
44
  Scope-->>Context: deps (cached in scope)
45
45
  Context->>Flow1: factory(childCtx, deps)
46
46
  Note over Flow1: childCtx.parent = ctx
47
- Flow1->>Flow1: tags.required(userId) from merged tags
47
+ Flow1->>Flow1: tags.required(userIdTag) from merged tags
48
48
  Flow1-->>Context: validated
49
49
 
50
50
  App->>Context: ctx.exec({ flow: processFlow, input: validated })
@@ -70,46 +70,103 @@ sequenceDiagram
70
70
  - Each `exec()` creates child context with isolated `data` Map
71
71
  - `seekTag()` traverses parent chain for shared data (e.g., transaction)
72
72
  - `ctx.close()` runs all `onClose` cleanups (LIFO order)
73
+ - On error: child context auto-closes, cleanups still run
73
74
 
74
- **Error Boundary (natural extension):**
75
+ **Primitives:** `createScope()`, `scope.createContext()`, `ctx.exec()`, `ctx.data.setTag/seekTag()`, `ctx.onClose()`, `ctx.close()`
76
+
77
+ ---
78
+
79
+ ### Flow Deps & Execution
80
+
81
+ **Combines:** Command + Composite + Resource Management
82
+
83
+ | Concern | Primitive |
84
+ |---------|-----------|
85
+ | Dependency injection | `deps` (atoms from Scope, tags from context hierarchy) |
86
+ | Service invocation | `ctx.exec({ fn, params })` (observable by extensions) |
87
+ | Resource cleanup | `ctx.onClose()` (LIFO, runs on success or failure) |
88
+
89
+ **Deps Resolution:**
90
+
91
+ ```mermaid
92
+ flowchart TB
93
+ subgraph Flow["flow({ deps, factory })"]
94
+ Deps["deps: { db: dbAtom, userId: tags.required(userIdTag) }"]
95
+ end
96
+
97
+ subgraph Resolution
98
+ Deps --> AtomPath["Atom deps"]
99
+ Deps --> TagPath["Tag deps (TagExecutor)"]
100
+
101
+ AtomPath --> Scope["Scope.resolve()"]
102
+ Scope --> |"cached in scope"| ResolvedAtom["db instance"]
103
+
104
+ TagPath --> CtxHierarchy["ctx.data.seekTag()"]
105
+ CtxHierarchy --> |"traverses parent chain"| ResolvedTag["userId value"]
106
+ end
107
+
108
+ subgraph Factory["factory(ctx, { db, userId })"]
109
+ ResolvedAtom --> DepsObj["deps object"]
110
+ ResolvedTag --> DepsObj
111
+ end
112
+ ```
113
+
114
+ **Service Invocation:**
75
115
 
76
116
  ```mermaid
77
117
  sequenceDiagram
78
- participant App
79
- participant Scope
80
- participant Context as ExecutionContext
81
118
  participant Flow
119
+ participant Ctx as ExecutionContext
120
+ participant Ext as Extension.wrapExec
121
+ participant Fn as service.method
122
+
123
+ Note over Flow: ❌ Direct call
124
+ Flow->>Fn: service.method(ctx, data)
125
+ Note over Fn: Extensions cannot observe
126
+
127
+ Note over Flow: ✅ Via ctx.exec
128
+ Flow->>Ctx: ctx.exec({ fn: service.method, params: [data] })
129
+ Ctx->>Ctx: create childCtx (parent = ctx)
130
+ Ctx->>Ext: wrapExec(next, fn, childCtx)
131
+ Ext->>Fn: next()
132
+ Fn-->>Ext: result
133
+ Ext-->>Ctx: result
134
+ Ctx->>Ctx: childCtx.close()
135
+ Ctx-->>Flow: result
136
+ ```
82
137
 
83
- App->>Scope: createContext({ tags: [requestId] })
84
- Scope-->>App: ctx
85
- App->>Context: ctx.onClose(() => releaseResources())
86
-
87
- alt Success path
88
- App->>Context: ctx.exec({ flow, input })
89
- Context->>Scope: resolve flow deps
90
- Scope-->>Context: deps (cached)
91
- Context->>Flow: factory(childCtx, deps)
92
- Flow-->>Context: result
93
- Note over Context: childCtx auto-closes
94
- Context-->>App: result
95
- App->>Context: ctx.close()
96
- Context->>Context: run onClose cleanups
97
- else Error path
98
- App->>Context: ctx.exec({ flow, input })
99
- Context->>Scope: resolve flow deps
100
- Scope-->>Context: deps (cached)
101
- Context->>Flow: factory(childCtx, deps)
102
- Flow-->>Flow: throws Error
103
- Note over Context: childCtx auto-closes
104
- Context-->>App: throws Error
105
- App->>App: catch(error)
106
- App->>Context: ctx.close()
107
- Context->>Context: run onClose cleanups
108
- App-->>App: return error response
138
+ **Cleanup Pattern:**
139
+
140
+ ```mermaid
141
+ sequenceDiagram
142
+ participant Flow
143
+ participant Ctx as ExecutionContext
144
+ participant Tx as Transaction
145
+
146
+ Flow->>Tx: beginTransaction()
147
+ Tx-->>Flow: tx
148
+ Flow->>Ctx: ctx.onClose(() => tx.rollback())
149
+
150
+ alt Success
151
+ Flow->>Flow: do work
152
+ Flow->>Tx: tx.commit()
153
+ Flow->>Ctx: return result
154
+ Ctx->>Ctx: close() rollback() is no-op
155
+ else Error
156
+ Flow->>Flow: do work (throws)
157
+ Ctx->>Ctx: close() → rollback() executes
158
+ Ctx->>Tx: tx.rollback()
109
159
  end
110
160
  ```
111
161
 
112
- **Primitives:** `createScope()`, `scope.createContext()`, `ctx.exec()`, `ctx.data.setTag/seekTag()`, `ctx.onClose()`, `ctx.close()`
162
+ **Characteristics:**
163
+ - Atoms resolve via `Scope.resolve()` (cached, long-lived)
164
+ - Tag deps resolve via `ctx.data.seekTag()` (traverses parent → grandparent → scope tags)
165
+ - `ctx.exec({ fn, params })` creates child context with isolated `data` Map
166
+ - Extensions intercept via `wrapExec(next, target, ctx)`
167
+ - Register pessimistic cleanup via `ctx.onClose(fn)`, neutralize on success
168
+
169
+ **Primitives:** `flow({ deps })`, `tags.required()`, `tags.optional()`, `tags.all()`, `ctx.exec()`, `ctx.onClose()`
113
170
 
114
171
  ---
115
172
 
@@ -272,45 +329,6 @@ stateDiagram-v2
272
329
 
273
330
  ---
274
331
 
275
- ### Command
276
-
277
- **GoF:** Command Pattern
278
-
279
- ```mermaid
280
- graph LR
281
- Client -->|exec| Context[ExecutionContext]
282
- Context -->|invoke| Flow
283
- Flow -->|input| Factory
284
- Factory -->|output| Context
285
- Context -->|result| Client
286
- ```
287
-
288
- **Primitives:** `flow()`, `ctx.exec()`, `ctx.input`, `parse`
289
-
290
- **Characteristics:** Encapsulated request/response, input validation via `parse`, nestable execution, auto-closing child contexts.
291
-
292
- ---
293
-
294
- ### Interceptor
295
-
296
- **GoF:** Interceptor / Chain of Responsibility
297
-
298
- ```mermaid
299
- graph LR
300
- Request --> Ext1[Extension 1]
301
- Ext1 -->|next| Ext2[Extension 2]
302
- Ext2 -->|next| Target[Atom/Flow]
303
- Target --> Ext2
304
- Ext2 --> Ext1
305
- Ext1 --> Response
306
- ```
307
-
308
- **Primitives:** `Extension`, `wrapResolve()`, `wrapExec()`, `init()`, `dispose()`
309
-
310
- **Characteristics:** Wraps both atom resolution and flow execution, registration order determines nesting, access to scope and context.
311
-
312
- ---
313
-
314
332
  ### Context Object
315
333
 
316
334
  **GoF:** Context Object / Ambient Context
package/README.md CHANGED
@@ -4,6 +4,13 @@ A lightweight effect system for TypeScript with managed lifecycles and minimal r
4
4
 
5
5
  **Zero dependencies** · **<17KB bundle** · **Full TypeScript support**
6
6
 
7
+ ## Documentation
8
+
9
+ | Resource | Purpose |
10
+ |----------|---------|
11
+ | [PATTERNS.md](./PATTERNS.md) | Architecture patterns, flow design, deps resolution, cleanup strategies |
12
+ | [dist/index.d.mts](./dist/index.d.mts) | API reference with TSDoc |
13
+
7
14
  ## How It Works
8
15
 
9
16
  ```mermaid
@@ -74,20 +81,26 @@ sequenceDiagram
74
81
  Context->>Context: run onClose cleanups (LIFO)
75
82
  ```
76
83
 
77
- ## Tag Inheritance
84
+ ## Tag Inheritance (ADR-023)
85
+
86
+ Tags are auto-populated into `ctx.data` and resolved via `seekTag()`:
78
87
 
79
88
  ```mermaid
80
89
  flowchart TD
81
- subgraph Scope["Scope: tags=[tenantId('t1')]"]
82
- subgraph Context["Context: tags=[requestId('r1')]"]
83
- subgraph Flow["Flow deps:"]
84
- T1["tags.required(tenantId) → 't1'"]
85
- T2["tags.required(requestId)'r1'"]
90
+ subgraph Root["Root Context (ctx.data)"]
91
+ S["scope.tags → auto-populated"]
92
+ C["context.tags → auto-populated"]
93
+ subgraph Child["Child Context (exec)"]
94
+ E["exec.tags → auto-populated"]
95
+ F["flow.tags → auto-populated"]
96
+ D["ctx.data.setTag() → runtime"]
97
+ subgraph Deps["tags.required(tag)"]
98
+ R["seekTag() traverses: Child → Root"]
86
99
  end
87
100
  end
88
101
  end
89
102
 
90
- Note["Inner inherits from outer. Override by passing same tag at inner level."]
103
+ Note["Nearest value wins. Propagates to all descendants."]
91
104
  ```
92
105
 
93
106
  ## Controller Reactivity
@@ -155,11 +168,29 @@ Short-lived operation with input/output.
155
168
 
156
169
  Contextual value passed through execution without explicit wiring.
157
170
 
158
- - `tag({ label, default?, parse? })` — define with optional default and validation
159
- - `tags.required(tag)` dependency that throws if missing
160
- - `tags.optional(tag)` dependency that returns undefined if missing
161
- - `tags.all(tag)` — collects all values from inheritance chain
162
- - Tags inherit: Scope → Context → exec call
171
+ - Hierarchical lookup via `seekTag()` (ADR-023)
172
+ - Auto-populates into `ctx.data`: scope context exec → flow
173
+ - Registry tracks atom↔tag relationships (ADR-026)
174
+
175
+ ```mermaid
176
+ flowchart TD
177
+ subgraph "Tag Registry (ADR-026)"
178
+ direction LR
179
+ A["atom({ tags: [...] })"] -->|auto-register| R["WeakMap⟨Tag, WeakRef⟨Atom⟩[]⟩"]
180
+ R -->|"tag.atoms()"| Q["query atoms by tag"]
181
+ R -->|"getAllTags()"| T["query all tags"]
182
+ end
183
+
184
+ subgraph "Tag Inheritance (ADR-023)"
185
+ S[scope.tags] --> D[ctx.data]
186
+ C[context.tags] --> D
187
+ E[exec.tags] --> D
188
+ F[flow.tags] --> D
189
+ D -->|"seekTag()"| V["nearest value wins"]
190
+ end
191
+ ```
192
+
193
+ Memory: `WeakRef` allows GC of unused atoms/tags. Cleanup on query.
163
194
 
164
195
  ### Controller
165
196
 
@@ -194,9 +225,43 @@ AOP-style middleware for cross-cutting concerns.
194
225
  - `dispose(scope)` — cleanup when scope disposed
195
226
  - Pass via `createScope({ extensions: [...] })`
196
227
 
197
- ## Full API
228
+ ## Patterns
229
+
230
+ ### Eager Resolution via Tag Registry
231
+
232
+ Use tags to mark atoms for eager resolution without hardcoding atom references:
233
+
234
+ ```mermaid
235
+ flowchart LR
236
+ subgraph "Define"
237
+ T[eagerTag] --> A1[atomA]
238
+ T --> A2[atomB]
239
+ T --> A3[atomC]
240
+ end
241
+
242
+ subgraph "Extension init()"
243
+ E["eagerTag.atoms()"] --> R["resolve all marked atoms"]
244
+ end
245
+
246
+ A1 & A2 & A3 -.->|"auto-tracked"| E
247
+ ```
248
+
249
+ ### Extension Discovery via getAllTags()
250
+
251
+ Extensions can discover and process all tags at runtime:
252
+
253
+ ```mermaid
254
+ flowchart LR
255
+ subgraph "Runtime"
256
+ G["getAllTags()"] --> F{"filter by criteria"}
257
+ F --> P["process matching tags"]
258
+ P --> A["tag.atoms() for each"]
259
+ end
260
+ ```
261
+
262
+ Use cases: metrics collection, debugging, documentation generation.
198
263
 
199
- See [`dist/index.d.mts`](./dist/index.d.mts) for complete type definitions.
264
+ ## Types
200
265
 
201
266
  All types available under the `Lite` namespace:
202
267
 
@@ -229,9 +294,9 @@ ctx.data.getOrSetTag(countTag) // 0 (uses default, now stored)
229
294
  ctx.data.getTag(countTag) // 0 (now stored)
230
295
  ```
231
296
 
232
- ### Hierarchical Data Lookup with seek()
297
+ ### Hierarchical Data Lookup with seekTag() (ADR-023)
233
298
 
234
- Each execution context has isolated data, but `seekTag()` traverses the parent chain:
299
+ Tag dependencies (`tags.required()`, `tags.optional()`, `tags.all()`) use `seekTag()` internally to traverse the ExecutionContext parent chain. Tags from all sources are auto-populated into `ctx.data`:
235
300
 
236
301
  ```typescript
237
302
  const requestIdTag = tag<string>({ label: 'requestId' })
@@ -244,9 +309,9 @@ const middleware = flow({
244
309
  })
245
310
 
246
311
  const handler = flow({
247
- factory: (ctx) => {
248
- // seekTag() finds value from parent context
249
- const reqId = ctx.data.seekTag(requestIdTag) // 'req-123'
312
+ deps: { reqId: tags.required(requestIdTag) },
313
+ factory: (ctx, { reqId }) => {
314
+ // reqId === 'req-123' (found via seekTag from parent)
250
315
  }
251
316
  })
252
317
  ```
@@ -256,6 +321,122 @@ const handler = flow({
256
321
  | `getTag(tag)` | Local only | Per-exec isolated data |
257
322
  | `seekTag(tag)` | Local → parent → root | Cross-cutting concerns |
258
323
  | `setTag(tag, v)` | Local only | Always writes to current context |
324
+ | `tags.required(tag)` | Uses `seekTag()` | Dependency injection |
325
+
326
+ ### Resolution Timing
327
+
328
+ Tag dependencies resolve **once** at factory start. Direct `seekTag()` calls reflect runtime changes:
329
+
330
+ ```typescript
331
+ const handler = flow({
332
+ deps: { userId: tags.required(userIdTag) },
333
+ factory: (ctx, { userId }) => {
334
+ ctx.data.setTag(userIdTag, 'changed')
335
+
336
+ console.log(userId) // Original (stable)
337
+ console.log(ctx.data.seekTag(userIdTag)) // 'changed' (dynamic)
338
+ }
339
+ })
340
+ ```
341
+
342
+ | Access | Resolution | Runtime Changes |
343
+ |--------|------------|-----------------|
344
+ | `deps: { x: tags.required(tag) }` | Once at start | Stable snapshot |
345
+ | `ctx.data.seekTag(tag)` | Each call | Sees changes |
346
+
347
+ ## Automatic Garbage Collection
348
+
349
+ Atoms are automatically released when they have no subscribers, preventing memory leaks in long-running applications.
350
+
351
+ ### How It Works
352
+
353
+ ```mermaid
354
+ sequenceDiagram
355
+ participant Component
356
+ participant Controller
357
+ participant Scope
358
+ participant Timer
359
+
360
+ Component->>Controller: ctrl.on('resolved', callback)
361
+ Note over Controller: subscriberCount = 1
362
+
363
+ Component->>Controller: unsubscribe()
364
+ Note over Controller: subscriberCount = 0
365
+ Controller->>Timer: schedule GC (3000ms)
366
+
367
+ alt Resubscribe before timeout
368
+ Component->>Controller: ctrl.on('resolved', callback)
369
+ Controller->>Timer: cancel GC
370
+ Note over Controller: Atom stays alive
371
+ else Timeout fires
372
+ Timer->>Scope: release(atom)
373
+ Note over Scope: Cleanups run, cache cleared
374
+ Scope->>Scope: Check dependencies for cascading GC
375
+ end
376
+ ```
377
+
378
+ ### Configuration
379
+
380
+ ```typescript
381
+ // Default: GC enabled with 3000ms grace period
382
+ const scope = createScope()
383
+
384
+ // Custom grace period (useful for tests)
385
+ const scope = createScope({
386
+ gc: { graceMs: 100 }
387
+ })
388
+
389
+ // Disable GC entirely (preserves pre-1.11 behavior)
390
+ const scope = createScope({
391
+ gc: { enabled: false }
392
+ })
393
+ ```
394
+
395
+ ### Opt-Out with keepAlive
396
+
397
+ Mark atoms that should never be automatically released:
398
+
399
+ ```typescript
400
+ const configAtom = atom({
401
+ factory: () => loadConfig(),
402
+ keepAlive: true // Never auto-released
403
+ })
404
+ ```
405
+
406
+ ### Cascading Dependency Protection
407
+
408
+ Dependencies are protected while dependents are mounted:
409
+
410
+ ```
411
+ configAtom (keepAlive: true)
412
+
413
+ dbAtom ←── userServiceAtom ←── [Component subscribes]
414
+ ```
415
+
416
+ - `dbAtom` won't be GC'd while `userServiceAtom` is mounted
417
+ - When component unmounts, `userServiceAtom` is GC'd after grace period
418
+ - Then `dbAtom` becomes eligible for GC (no dependents)
419
+ - `configAtom` stays alive due to `keepAlive: true`
420
+
421
+ ### React Strict Mode Compatibility
422
+
423
+ The 3000ms default grace period handles React's double-mount behavior:
424
+
425
+ ```
426
+ Mount (render 1): subscribe → count=1
427
+ Unmount (cleanup 1): unsubscribe → count=0 → schedule GC
428
+ Mount (render 2): subscribe → count=1 → CANCEL GC
429
+ ```
430
+
431
+ The second mount always happens before the GC timer fires.
432
+
433
+ ### API Summary
434
+
435
+ | Option | Default | Description |
436
+ |--------|---------|-------------|
437
+ | `gc.enabled` | `true` | Enable/disable automatic GC |
438
+ | `gc.graceMs` | `3000` | Delay before releasing (ms) |
439
+ | `atom.keepAlive` | `false` | Prevent auto-release for specific atoms |
259
440
 
260
441
  ## License
261
442