@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 +46 -0
- package/PATTERNS.md +89 -71
- package/README.md +200 -19
- package/dist/index.cjs +195 -31
- package/dist/index.d.cts +120 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +120 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +195 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
|
@@ -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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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["
|
|
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
|
-
-
|
|
159
|
-
- `
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
297
|
+
### Hierarchical Data Lookup with seekTag() (ADR-023)
|
|
233
298
|
|
|
234
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|