@pumped-fn/lite 1.10.0 → 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,39 @@
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
+
3
37
  ## 1.10.0
4
38
 
5
39
  ### Minor 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
@@ -254,9 +261,7 @@ flowchart LR
254
261
 
255
262
  Use cases: metrics collection, debugging, documentation generation.
256
263
 
257
- ## Full API
258
-
259
- See [`dist/index.d.mts`](./dist/index.d.mts) for complete type definitions.
264
+ ## Types
260
265
 
261
266
  All types available under the `Lite` namespace:
262
267
 
@@ -339,6 +344,100 @@ const handler = flow({
339
344
  | `deps: { x: tags.required(tag) }` | Once at start | Stable snapshot |
340
345
  | `ctx.data.seekTag(tag)` | Each call | Sees changes |
341
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 |
440
+
342
441
  ## License
343
442
 
344
443
  MIT
package/dist/index.cjs CHANGED
@@ -235,7 +235,8 @@ function atom(config) {
235
235
  [atomSymbol]: true,
236
236
  factory: config.factory,
237
237
  deps: config.deps,
238
- tags: config.tags
238
+ tags: config.tags,
239
+ keepAlive: config.keepAlive
239
240
  };
240
241
  if (config.tags?.length) registerAtomToTags(atomInstance, config.tags);
241
242
  return atomInstance;
@@ -535,6 +536,7 @@ var ScopeImpl = class {
535
536
  chainPromise = null;
536
537
  initialized = false;
537
538
  controllers = /* @__PURE__ */ new Map();
539
+ gcOptions;
538
540
  extensions;
539
541
  tags;
540
542
  ready;
@@ -580,6 +582,10 @@ var ScopeImpl = class {
580
582
  this.extensions = options?.extensions ?? [];
581
583
  this.tags = options?.tags ?? [];
582
584
  for (const p of options?.presets ?? []) this.presets.set(p.atom, p.value);
585
+ this.gcOptions = {
586
+ enabled: options?.gc?.enabled ?? true,
587
+ graceMs: options?.gc?.graceMs ?? 3e3
588
+ };
583
589
  this.ready = this.init();
584
590
  }
585
591
  async init() {
@@ -601,19 +607,68 @@ var ScopeImpl = class {
601
607
  ["resolved", /* @__PURE__ */ new Set()],
602
608
  ["*", /* @__PURE__ */ new Set()]
603
609
  ]),
604
- pendingInvalidate: false
610
+ pendingInvalidate: false,
611
+ dependents: /* @__PURE__ */ new Set(),
612
+ gcScheduled: null
605
613
  };
606
614
  this.cache.set(atom$1, entry);
607
615
  }
608
616
  return entry;
609
617
  }
610
618
  addListener(atom$1, event, listener) {
619
+ this.cancelScheduledGC(atom$1);
611
620
  const listeners = this.getOrCreateEntry(atom$1).listeners.get(event);
612
621
  listeners.add(listener);
613
622
  return () => {
614
623
  listeners.delete(listener);
624
+ this.maybeScheduleGC(atom$1);
615
625
  };
616
626
  }
627
+ getSubscriberCount(atom$1) {
628
+ const entry = this.cache.get(atom$1);
629
+ if (!entry) return 0;
630
+ let count = 0;
631
+ for (const listeners of entry.listeners.values()) count += listeners.size;
632
+ return count;
633
+ }
634
+ maybeScheduleGC(atom$1) {
635
+ if (!this.gcOptions.enabled) return;
636
+ if (atom$1.keepAlive) return;
637
+ const entry = this.cache.get(atom$1);
638
+ if (!entry) return;
639
+ if (entry.state === "idle") return;
640
+ if (this.getSubscriberCount(atom$1) > 0) return;
641
+ if (entry.dependents.size > 0) return;
642
+ if (entry.gcScheduled) return;
643
+ entry.gcScheduled = setTimeout(() => {
644
+ this.executeGC(atom$1);
645
+ }, this.gcOptions.graceMs);
646
+ }
647
+ cancelScheduledGC(atom$1) {
648
+ const entry = this.cache.get(atom$1);
649
+ if (entry?.gcScheduled) {
650
+ clearTimeout(entry.gcScheduled);
651
+ entry.gcScheduled = null;
652
+ }
653
+ }
654
+ async executeGC(atom$1) {
655
+ const entry = this.cache.get(atom$1);
656
+ if (!entry) return;
657
+ entry.gcScheduled = null;
658
+ if (this.getSubscriberCount(atom$1) > 0) return;
659
+ if (entry.dependents.size > 0) return;
660
+ if (atom$1.keepAlive) return;
661
+ await this.release(atom$1);
662
+ if (atom$1.deps) for (const dep of Object.values(atom$1.deps)) {
663
+ const depAtom = isAtom(dep) ? dep : isControllerDep(dep) ? dep.atom : null;
664
+ if (!depAtom) continue;
665
+ const depEntry = this.cache.get(depAtom);
666
+ if (depEntry) {
667
+ depEntry.dependents.delete(atom$1);
668
+ this.maybeScheduleGC(depAtom);
669
+ }
670
+ }
671
+ }
617
672
  notifyListeners(atom$1, event) {
618
673
  const entry = this.cache.get(atom$1);
619
674
  if (!entry) return;
@@ -692,7 +747,7 @@ var ScopeImpl = class {
692
747
  this.emitStateChange("resolving", atom$1);
693
748
  this.notifyListeners(atom$1, "resolving");
694
749
  }
695
- const resolvedDeps = await this.resolveDeps(atom$1.deps);
750
+ const resolvedDeps = await this.resolveDeps(atom$1.deps, void 0, atom$1);
696
751
  const ctx = {
697
752
  cleanup: (fn) => entry.cleanups.push(fn),
698
753
  invalidate: () => {
@@ -752,14 +807,23 @@ var ScopeImpl = class {
752
807
  }
753
808
  return next();
754
809
  }
755
- async resolveDeps(deps, ctx) {
810
+ async resolveDeps(deps, ctx, dependentAtom) {
756
811
  if (!deps) return {};
757
812
  const result = {};
758
- for (const [key, dep] of Object.entries(deps)) if (isAtom(dep)) result[key] = await this.resolve(dep);
759
- else if (isControllerDep(dep)) {
813
+ for (const [key, dep] of Object.entries(deps)) if (isAtom(dep)) {
814
+ result[key] = await this.resolve(dep);
815
+ if (dependentAtom) {
816
+ const depEntry = this.getEntry(dep);
817
+ if (depEntry) depEntry.dependents.add(dependentAtom);
818
+ }
819
+ } else if (isControllerDep(dep)) {
760
820
  const ctrl = new ControllerImpl(dep.atom, this);
761
821
  if (dep.resolve) await ctrl.resolve();
762
822
  result[key] = ctrl;
823
+ if (dependentAtom) {
824
+ const depEntry = this.getEntry(dep.atom);
825
+ if (depEntry) depEntry.dependents.add(dependentAtom);
826
+ }
763
827
  } else if (tagExecutorSymbol in dep) {
764
828
  const tagExecutor = dep;
765
829
  switch (tagExecutor.mode) {
@@ -868,6 +932,10 @@ var ScopeImpl = class {
868
932
  async release(atom$1) {
869
933
  const entry = this.cache.get(atom$1);
870
934
  if (!entry) return;
935
+ if (entry.gcScheduled) {
936
+ clearTimeout(entry.gcScheduled);
937
+ entry.gcScheduled = null;
938
+ }
871
939
  for (let i = entry.cleanups.length - 1; i >= 0; i--) {
872
940
  const cleanup = entry.cleanups[i];
873
941
  if (cleanup) await cleanup();
@@ -877,6 +945,10 @@ var ScopeImpl = class {
877
945
  }
878
946
  async dispose() {
879
947
  for (const ext of this.extensions) if (ext.dispose) await ext.dispose(this);
948
+ for (const entry of this.cache.values()) if (entry.gcScheduled) {
949
+ clearTimeout(entry.gcScheduled);
950
+ entry.gcScheduled = null;
951
+ }
880
952
  const atoms = Array.from(this.cache.keys());
881
953
  for (const atom$1 of atoms) await this.release(atom$1);
882
954
  }
package/dist/index.d.cts CHANGED
@@ -35,12 +35,20 @@ declare namespace Lite {
35
35
  extensions?: Extension[];
36
36
  tags?: Tagged<unknown>[];
37
37
  presets?: Preset<unknown>[];
38
+ gc?: GCOptions;
39
+ }
40
+ interface GCOptions {
41
+ /** Enable automatic garbage collection. Default: true */
42
+ enabled?: boolean;
43
+ /** Grace period before releasing (ms). Default: 3000 */
44
+ graceMs?: number;
38
45
  }
39
46
  interface Atom<T> {
40
47
  readonly [atomSymbol]: true;
41
48
  readonly factory: AtomFactory<T, Record<string, Dependency>>;
42
49
  readonly deps?: Record<string, Dependency>;
43
50
  readonly tags?: Tagged<unknown>[];
51
+ readonly keepAlive?: boolean;
44
52
  }
45
53
  interface Flow<TOutput, TInput = unknown> {
46
54
  readonly [flowSymbol]: true;
@@ -225,7 +233,7 @@ declare namespace Lite {
225
233
  readonly name: string;
226
234
  init?(scope: Scope): MaybePromise<void>;
227
235
  wrapResolve?(next: () => Promise<unknown>, atom: Atom<unknown>, scope: Scope): Promise<unknown>;
228
- wrapExec?(next: () => Promise<unknown>, target: Flow<unknown, unknown> | ((ctx: ExecutionContext, ...args: unknown[]) => MaybePromise<unknown>), ctx: ExecutionContext): Promise<unknown>;
236
+ wrapExec?(next: () => Promise<unknown>, target: ExecTarget, ctx: ExecutionContext): Promise<unknown>;
229
237
  dispose?(scope: Scope): MaybePromise<void>;
230
238
  }
231
239
  type Dependency = Atom<unknown> | ControllerDep<unknown> | TagExecutor<unknown>;
@@ -239,6 +247,91 @@ declare namespace Lite {
239
247
  }, deps: InferDeps<D>) => MaybePromise<Output>;
240
248
  type ServiceMethod = (ctx: ExecutionContext, ...args: any[]) => unknown;
241
249
  type ServiceMethods = Record<string, ServiceMethod>;
250
+ /**
251
+ * Any atom regardless of value type.
252
+ * Useful for APIs that don't need the value type.
253
+ */
254
+ type AnyAtom = Atom<any>;
255
+ /**
256
+ * Any flow regardless of input/output types.
257
+ * Useful for APIs that don't need the type parameters.
258
+ */
259
+ type AnyFlow = Flow<any, any>;
260
+ /**
261
+ * Any controller regardless of value type.
262
+ */
263
+ type AnyController = Controller<any>;
264
+ /**
265
+ * Target type for wrapExec extension hook.
266
+ * Either a Flow or an inline function.
267
+ */
268
+ type ExecTarget = Flow<unknown, unknown> | ExecTargetFn;
269
+ /**
270
+ * Inline function that can be executed via ctx.exec.
271
+ */
272
+ type ExecTargetFn = (ctx: ExecutionContext, ...args: any[]) => MaybePromise<unknown>;
273
+ /**
274
+ * Utility types for type extraction and manipulation.
275
+ * @example
276
+ * type Config = Lite.Utils.AtomValue<typeof configAtom>
277
+ * type Result = Lite.Utils.FlowOutput<typeof processFlow>
278
+ */
279
+ namespace Utils {
280
+ /**
281
+ * Extract value type from an Atom.
282
+ * @example
283
+ * type Config = Lite.Utils.AtomValue<typeof configAtom> // string
284
+ */
285
+ type AtomValue<A> = A extends Atom<infer T> ? T : never;
286
+ /**
287
+ * Extract output type from a Flow.
288
+ * @example
289
+ * type Result = Lite.Utils.FlowOutput<typeof processFlow> // ProcessResult
290
+ */
291
+ type FlowOutput<F> = F extends Flow<infer O, unknown> ? O : never;
292
+ /**
293
+ * Extract input type from a Flow.
294
+ * @example
295
+ * type Input = Lite.Utils.FlowInput<typeof processFlow> // ProcessRequest
296
+ */
297
+ type FlowInput<F> = F extends Flow<unknown, infer I> ? I : never;
298
+ /**
299
+ * Extract value type from a Tag.
300
+ * @example
301
+ * type UserId = Lite.Utils.TagValue<typeof userIdTag> // string
302
+ */
303
+ type TagValue<T> = T extends Tag<infer V, boolean> ? V : never;
304
+ /**
305
+ * Extract dependencies record from an Atom or Flow.
306
+ * @example
307
+ * type Deps = Lite.Utils.DepsOf<typeof myAtom> // { db: DbAtom, cache: CacheAtom }
308
+ */
309
+ type DepsOf<T> = T extends Atom<unknown> ? T['deps'] : T extends Flow<unknown, unknown> ? T['deps'] : never;
310
+ /**
311
+ * Flatten complex intersection types for better IDE display.
312
+ */
313
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
314
+ /**
315
+ * Create an atom type with inferred value.
316
+ * Useful for declaring atom types without defining the atom.
317
+ */
318
+ type AtomType<T, D extends Record<string, Dependency> = Record<string, never>> = Atom<T> & {
319
+ readonly deps: D;
320
+ };
321
+ /**
322
+ * Create a flow type with inferred input/output.
323
+ * Useful for declaring flow types without defining the flow.
324
+ */
325
+ type FlowType<O, I = void, D extends Record<string, Dependency> = Record<string, never>> = Flow<O, I> & {
326
+ readonly deps: D;
327
+ };
328
+ /**
329
+ * Extract value type from a Controller.
330
+ * @example
331
+ * type Value = Lite.Utils.ControllerValue<typeof ctrl> // string
332
+ */
333
+ type ControllerValue<C> = C extends Controller<infer T> ? T : never;
334
+ }
242
335
  }
243
336
  //#endregion
244
337
  //#region src/tag.d.ts
@@ -405,6 +498,7 @@ declare function atom<T>(config: {
405
498
  deps?: undefined;
406
499
  factory: (ctx: Lite.ResolveContext) => MaybePromise<T>;
407
500
  tags?: Lite.Tagged<unknown>[];
501
+ keepAlive?: boolean;
408
502
  }): Lite.Atom<T>;
409
503
  declare function atom<T, const D extends Record<string, Lite.Atom<unknown> | Lite.ControllerDep<unknown> | {
410
504
  mode: string;
@@ -412,6 +506,7 @@ declare function atom<T, const D extends Record<string, Lite.Atom<unknown> | Lit
412
506
  deps: D;
413
507
  factory: (ctx: Lite.ResolveContext, deps: Lite.InferDeps<D>) => MaybePromise<T>;
414
508
  tags?: Lite.Tagged<unknown>[];
509
+ keepAlive?: boolean;
415
510
  }): Lite.Atom<T>;
416
511
  /**
417
512
  * Type guard to check if a value is an Atom.