@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 +34 -0
- package/PATTERNS.md +89 -71
- package/README.md +102 -3
- package/dist/index.cjs +78 -6
- package/dist/index.d.cts +96 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +96 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +78 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
-
**
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
**
|
|
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
|
-
##
|
|
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))
|
|
759
|
-
|
|
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:
|
|
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.
|