atomirx 0.0.7 → 0.1.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/README.md +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -30
- package/dist/react/index.js +206 -791
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -39
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Hooks — Creation Tracking & Error Handling
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Hook | Purpose | Fires When |
|
|
6
|
+
| -------------- | -------------------------------- | ---------------------- |
|
|
7
|
+
| `onCreateHook` | Track atom/derived/effect/module | Primitive created |
|
|
8
|
+
| `onErrorHook` | Global error handling | Error in derived/effect|
|
|
9
|
+
| `hook()` | Create custom hooks | N/A (factory) |
|
|
10
|
+
|
|
11
|
+
## CRITICAL: MUST Use .override()
|
|
12
|
+
|
|
13
|
+
**NEVER** assign `.current` directly — breaks hook chain.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// ❌ FORBIDDEN
|
|
17
|
+
onCreateHook.current = (info) => { ... };
|
|
18
|
+
|
|
19
|
+
// ✅ REQUIRED
|
|
20
|
+
onCreateHook.override((prev) => (info) => {
|
|
21
|
+
prev?.(info);
|
|
22
|
+
// your code
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
onErrorHook.override((prev) => (info) => {
|
|
26
|
+
prev?.(info);
|
|
27
|
+
// your code
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Hook API
|
|
32
|
+
|
|
33
|
+
| Method | Signature | Description |
|
|
34
|
+
| ------------ | ----------------------- | ---------------------- |
|
|
35
|
+
| `.current` | `T \| undefined` | Read-only value |
|
|
36
|
+
| `.override()`| `(reducer) => void` | Set via reducer |
|
|
37
|
+
| `.reset()` | `() => void` | Reset to initial |
|
|
38
|
+
| `hook.use()` | `(setups[], fn) => T` | Temporary hooks in fn |
|
|
39
|
+
|
|
40
|
+
## onCreateHook
|
|
41
|
+
|
|
42
|
+
Fires on atom, derived, effect, module creation.
|
|
43
|
+
|
|
44
|
+
### CreateInfo Types
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface MutableCreateInfo {
|
|
48
|
+
type: "mutable";
|
|
49
|
+
key: string | undefined;
|
|
50
|
+
meta: MutableAtomMeta | undefined;
|
|
51
|
+
atom: MutableAtom<unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DerivedCreateInfo {
|
|
55
|
+
type: "derived";
|
|
56
|
+
key: string | undefined;
|
|
57
|
+
meta: DerivedAtomMeta | undefined;
|
|
58
|
+
atom: DerivedAtom<unknown, boolean>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface EffectCreateInfo {
|
|
62
|
+
type: "effect";
|
|
63
|
+
key: string | undefined;
|
|
64
|
+
meta: EffectMeta | undefined;
|
|
65
|
+
effect: Effect;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ModuleCreateInfo {
|
|
69
|
+
type: "module";
|
|
70
|
+
key: string | undefined;
|
|
71
|
+
meta: ModuleMeta | undefined;
|
|
72
|
+
module: unknown;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Use Cases
|
|
77
|
+
|
|
78
|
+
#### DevTools Registry
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const registry = {
|
|
82
|
+
atoms: new Map(),
|
|
83
|
+
derived: new Map(),
|
|
84
|
+
effects: new Map(),
|
|
85
|
+
modules: new Map(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
onCreateHook.override((prev) => (info) => {
|
|
89
|
+
prev?.(info);
|
|
90
|
+
const key = info.key ?? `anon-${Date.now()}`;
|
|
91
|
+
switch (info.type) {
|
|
92
|
+
case "mutable": registry.atoms.set(key, info.atom); break;
|
|
93
|
+
case "derived": registry.derived.set(key, info.atom); break;
|
|
94
|
+
case "effect": registry.effects.set(key, info.effect); break;
|
|
95
|
+
case "module": registry.modules.set(key, info.module); break;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
window.__ATOMIRX_DEVTOOLS__ = registry;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Persistence
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
declare module "atomirx" {
|
|
106
|
+
interface MutableAtomMeta { persisted?: boolean; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onCreateHook.override((prev) => (info) => {
|
|
110
|
+
prev?.(info);
|
|
111
|
+
if (info.type !== "mutable" || !info.meta?.persisted || !info.key) return;
|
|
112
|
+
|
|
113
|
+
const storageKey = `app:${info.key}`;
|
|
114
|
+
|
|
115
|
+
if (!info.atom.dirty()) {
|
|
116
|
+
const stored = localStorage.getItem(storageKey);
|
|
117
|
+
if (stored) try { info.atom.set(JSON.parse(stored)); } catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
info.atom.on(() => localStorage.setItem(storageKey, JSON.stringify(info.atom.get())));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Usage
|
|
124
|
+
const settings$ = atom({ theme: "dark" }, { meta: { key: "user.settings", persisted: true } });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Validation
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
declare module "atomirx" {
|
|
131
|
+
interface MutableAtomMeta { validate?: (v: unknown) => boolean; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onCreateHook.override((prev) => (info) => {
|
|
135
|
+
prev?.(info);
|
|
136
|
+
if (info.type !== "mutable" || !info.meta?.validate) return;
|
|
137
|
+
|
|
138
|
+
const validate = info.meta.validate;
|
|
139
|
+
const originalSet = info.atom.set.bind(info.atom);
|
|
140
|
+
|
|
141
|
+
info.atom.set = (valueOrReducer) => {
|
|
142
|
+
const next = typeof valueOrReducer === "function"
|
|
143
|
+
? (valueOrReducer as Function)(info.atom.get())
|
|
144
|
+
: valueOrReducer;
|
|
145
|
+
|
|
146
|
+
if (!validate(next)) {
|
|
147
|
+
console.warn(`Validation failed for ${info.key}:`, next);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
originalSet(valueOrReducer);
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Debug Logging
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
if (process.env.NODE_ENV === "development") {
|
|
159
|
+
onCreateHook.override((prev) => (info) => {
|
|
160
|
+
prev?.(info);
|
|
161
|
+
console.log(`[atomirx] Created ${info.type}: ${info.key ?? "anon"}`);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## onErrorHook
|
|
167
|
+
|
|
168
|
+
Fires on derived/effect errors.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
interface ErrorInfo {
|
|
172
|
+
source: CreateInfo;
|
|
173
|
+
error: unknown;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Use Cases
|
|
178
|
+
|
|
179
|
+
#### Sentry
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
onErrorHook.override((prev) => (info) => {
|
|
183
|
+
prev?.(info);
|
|
184
|
+
Sentry.captureException(info.error, {
|
|
185
|
+
tags: { source_type: info.source.type, source_key: info.source.key ?? "anon" },
|
|
186
|
+
extra: { meta: info.source.meta },
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Console
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
onErrorHook.override((prev) => (info) => {
|
|
195
|
+
prev?.(info);
|
|
196
|
+
console.error(`[atomirx] Error in ${info.source.type}: ${info.source.key ?? "anon"}`, info.error);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Toast
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
onErrorHook.override((prev) => (info) => {
|
|
204
|
+
prev?.(info);
|
|
205
|
+
if (info.error instanceof UserFacingError) toast.error(info.error.message);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Custom Hooks
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const debugHook = hook(false);
|
|
213
|
+
debugHook.current; // false
|
|
214
|
+
debugHook.override(() => true);
|
|
215
|
+
debugHook.current; // true
|
|
216
|
+
debugHook.reset();
|
|
217
|
+
debugHook.current; // false
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Temporary Hooks
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const loggerHook = hook<(msg: string) => void>();
|
|
224
|
+
|
|
225
|
+
const result = hook.use(
|
|
226
|
+
[loggerHook(() => (msg) => console.log("[TEST]", msg))],
|
|
227
|
+
() => {
|
|
228
|
+
loggerHook.current?.("Inside hook.use()");
|
|
229
|
+
return "result";
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
loggerHook.current?.("Outside"); // Does nothing
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Testing (IMPORTANT)
|
|
237
|
+
|
|
238
|
+
### Isolate Tests
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
describe("MyStore", () => {
|
|
242
|
+
beforeEach(() => {
|
|
243
|
+
onCreateHook.reset();
|
|
244
|
+
onErrorHook.reset();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
afterEach(() => {
|
|
248
|
+
onCreateHook.reset();
|
|
249
|
+
onErrorHook.reset();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should track created atoms", () => {
|
|
253
|
+
const created: string[] = [];
|
|
254
|
+
onCreateHook.override((prev) => (info) => {
|
|
255
|
+
prev?.(info);
|
|
256
|
+
if (info.key) created.push(info.key);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
myStore();
|
|
260
|
+
expect(created).toContain("myStore.counter");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Verify Error Hook
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
it("should call error hook", async () => {
|
|
269
|
+
const errors: ErrorInfo[] = [];
|
|
270
|
+
onErrorHook.override((prev) => (info) => { prev?.(info); errors.push(info); });
|
|
271
|
+
|
|
272
|
+
const buggy$ = derived(({ read }) => { throw new Error("test"); }, { meta: { key: "buggy" } });
|
|
273
|
+
|
|
274
|
+
try { await buggy$.get(); } catch {}
|
|
275
|
+
|
|
276
|
+
expect(errors).toHaveLength(1);
|
|
277
|
+
expect(errors[0].source.key).toBe("buggy");
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Initialization Order (CRITICAL)
|
|
282
|
+
|
|
283
|
+
**MUST** set up hooks BEFORE atoms are created:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// src/app/init.ts
|
|
287
|
+
import { onCreateHook, onErrorHook } from "atomirx";
|
|
288
|
+
|
|
289
|
+
// 1. DevTools
|
|
290
|
+
onCreateHook.override((prev) => (info) => {
|
|
291
|
+
prev?.(info);
|
|
292
|
+
window.__ATOMIRX_REGISTRY__?.add(info);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 2. Error monitoring
|
|
296
|
+
onErrorHook.override((prev) => (info) => {
|
|
297
|
+
prev?.(info);
|
|
298
|
+
Sentry.captureException(info.error);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// 3. Persistence
|
|
302
|
+
onCreateHook.override((prev) => (info) => {
|
|
303
|
+
prev?.(info);
|
|
304
|
+
if (info.type === "mutable" && info.meta?.persisted) setupPersistence(info);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// src/app/main.tsx
|
|
308
|
+
import "./init"; // Run first
|
|
309
|
+
import { App } from "./App";
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Summary
|
|
313
|
+
|
|
314
|
+
| Task | Hook | Pattern |
|
|
315
|
+
| --------------------- | -------------- | ----------------------------------- |
|
|
316
|
+
| Track creation | `onCreateHook` | `.override((prev) => (info) => {})` |
|
|
317
|
+
| Global error logging | `onErrorHook` | `.override((prev) => (info) => {})` |
|
|
318
|
+
| Persistence | `onCreateHook` | Check `info.meta?.persisted` |
|
|
319
|
+
| Validation | `onCreateHook` | Wrap `info.atom.set()` |
|
|
320
|
+
| DevTools | `onCreateHook` | Register in global registry |
|
|
321
|
+
| Reset all | Both | `.reset()` in tests |
|
|
322
|
+
| Temporary (tests) | `hook.use()` | `hook.use([setup], fn)` |
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Pool Patterns
|
|
2
|
+
|
|
3
|
+
Pool = collection of atoms indexed by params with automatic GC. Like `atomFamily` but with built-in GC and ScopedAtom safety.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
| Feature | Description |
|
|
8
|
+
| --------------- | ------------------------------------ |
|
|
9
|
+
| Auto GC | Removed after `gcTime` of inactivity |
|
|
10
|
+
| Promise-aware | GC pauses while Promise pending |
|
|
11
|
+
| ScopedAtom | Prevents stale reference leaks |
|
|
12
|
+
| Value API | Public API uses values, not atoms |
|
|
13
|
+
| Reactive API | `from(pool, params)` in selectors |
|
|
14
|
+
|
|
15
|
+
## Creating
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Object params
|
|
19
|
+
const userPool = pool((params: { id: string }) => fetchUser(params.id), {
|
|
20
|
+
gcTime: 60_000,
|
|
21
|
+
meta: { key: "users" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Primitive params
|
|
25
|
+
const articlePool = pool((id: string) => fetchArticle(id), {
|
|
26
|
+
gcTime: 300_000,
|
|
27
|
+
meta: { key: "articles" },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// With context
|
|
31
|
+
const dataPool = pool((params: { id: string }, ctx) => {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
ctx.onCleanup(() => controller.abort());
|
|
34
|
+
return fetchData(params.id, { signal: controller.signal });
|
|
35
|
+
}, { gcTime: 60_000 });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface PoolOptions<P> {
|
|
42
|
+
gcTime: number; // Required
|
|
43
|
+
equals?: Equality<P>; // Default: "shallow"
|
|
44
|
+
meta?: { key?: string };
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Public API (Value-based)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const userPool = pool((id: string) => ({ name: "", email: "" }), { gcTime: 60_000 });
|
|
52
|
+
|
|
53
|
+
userPool.get("user-1"); // Get/create
|
|
54
|
+
userPool.set("user-1", { name: "John", email: "j@e.com" });
|
|
55
|
+
userPool.set("user-1", (p) => ({ ...p, name: "Jane" })); // Reducer
|
|
56
|
+
userPool.has("user-1"); // Check existence
|
|
57
|
+
userPool.remove("user-1"); // Remove
|
|
58
|
+
userPool.clear(); // Clear all
|
|
59
|
+
userPool.forEach((val, params) => console.log(params, val));
|
|
60
|
+
|
|
61
|
+
// Subscribe
|
|
62
|
+
const unsub = userPool.onChange((params, value) => console.log("Changed:", params));
|
|
63
|
+
const unsub2 = userPool.onRemove((params, value) => console.log("Removed:", params));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Reactive API (from())
|
|
67
|
+
|
|
68
|
+
In `derived`, `effect`, `useSelector`:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// derived
|
|
72
|
+
const userPosts$ = derived(({ read, from }) => {
|
|
73
|
+
const user$ = from(userPool, "user-1");
|
|
74
|
+
return read(user$).posts;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// effect
|
|
78
|
+
effect(({ read, from }) => {
|
|
79
|
+
const user$ = from(userPool, currentUserId);
|
|
80
|
+
console.log("User:", read(user$));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// useSelector
|
|
84
|
+
const user = useSelector(({ read, from }) => {
|
|
85
|
+
const user$ = from(userPool, "user-1");
|
|
86
|
+
return read(user$);
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## ScopedAtom (CRITICAL)
|
|
91
|
+
|
|
92
|
+
ScopedAtom is a temporary wrapper:
|
|
93
|
+
- **ONLY** exists during select context
|
|
94
|
+
- **THROWS** if accessed outside
|
|
95
|
+
- **MUST** use with `read()`, **NEVER** with `.get()`
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// ❌ FORBIDDEN
|
|
99
|
+
derived(({ from }) => {
|
|
100
|
+
const user$ = from(userPool, "user-1");
|
|
101
|
+
return user$.get(); // THROWS
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ❌ FORBIDDEN
|
|
105
|
+
let cached: ScopedAtom<User>;
|
|
106
|
+
derived(({ from }) => { cached = from(userPool, "user-1"); });
|
|
107
|
+
cached._getAtom(); // THROWS after context ends
|
|
108
|
+
|
|
109
|
+
// ✅ REQUIRED
|
|
110
|
+
derived(({ read, from }) => {
|
|
111
|
+
const user$ = from(userPool, "user-1");
|
|
112
|
+
return read(user$);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## GC Behavior
|
|
117
|
+
|
|
118
|
+
Timer resets on: creation, value change, access.
|
|
119
|
+
|
|
120
|
+
GC pauses while Promise pending:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const asyncPool = pool((id: string) => fetchData(id), { gcTime: 5000 });
|
|
124
|
+
asyncPool.get("1"); // Timer starts AFTER Promise resolves
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Params Equality
|
|
128
|
+
|
|
129
|
+
Default `"shallow"` — order doesn't matter:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const pool1 = pool((p: { a: number; b: number }) => p.a + p.b, { gcTime: 60_000 });
|
|
133
|
+
pool1.get({ a: 1, b: 2 }); // Creates
|
|
134
|
+
pool1.get({ b: 2, a: 1 }); // Same entry
|
|
135
|
+
|
|
136
|
+
// Custom
|
|
137
|
+
const pool2 = pool(
|
|
138
|
+
(p: { id: string; version?: number }) => fetchData(p.id, p.version),
|
|
139
|
+
{ gcTime: 60_000, equals: (a, b) => a.id === b.id }
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Common Patterns
|
|
144
|
+
|
|
145
|
+
### Entity Cache
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const userCache = pool(
|
|
149
|
+
async (id: string) => (await fetch(`/api/users/${id}`)).json(),
|
|
150
|
+
{ gcTime: 300_000, meta: { key: "userCache" } }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const user = useSelector(({ read, from }) => read(from(userCache, userId)));
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Form State per Entity
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const formPool = pool(
|
|
160
|
+
(entityId: string): FormState => ({ values: {}, errors: {}, dirty: false }),
|
|
161
|
+
{ gcTime: 600_000, meta: { key: "forms" } }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
formPool.set(entityId, (p) => ({
|
|
165
|
+
...p,
|
|
166
|
+
values: { ...p.values, [field]: value },
|
|
167
|
+
dirty: true,
|
|
168
|
+
}));
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Optimistic Updates
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
async function updateUserName(id: string, name: string) {
|
|
175
|
+
userPool.set(id, (p) => ({ ...p, name })); // Optimistic
|
|
176
|
+
try {
|
|
177
|
+
await api.updateUser(id, { name });
|
|
178
|
+
} catch {
|
|
179
|
+
userPool.set(id, await fetchUser(id)); // Rollback
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Derived from Pool
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const currentUser$ = derived(({ read, ready, from }) => {
|
|
189
|
+
const userId = ready(currentUserId$);
|
|
190
|
+
const user$ = from(userPool, userId);
|
|
191
|
+
return read(user$);
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Multiple Pools
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const userDashboard$ = derived(({ read, from, all }) => {
|
|
199
|
+
const userId = "user-1";
|
|
200
|
+
const user$ = from(userPool, userId);
|
|
201
|
+
const posts$ = from(postsPool, userId);
|
|
202
|
+
const [user, posts] = all([user$, posts$]);
|
|
203
|
+
return { user, posts };
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Pool vs Manual Map
|
|
208
|
+
|
|
209
|
+
| Feature | pool() | Manual Map |
|
|
210
|
+
| -------------- | ------------------------ | ----------------------- |
|
|
211
|
+
| GC | Auto with `gcTime` | Manual cleanup |
|
|
212
|
+
| Memory safety | ScopedAtom prevents leaks| Easy to leak |
|
|
213
|
+
| Promise-aware | GC waits for pending | Manual handling |
|
|
214
|
+
| Events | `onChange`, `onRemove` | Implement manually |
|
|
215
|
+
| Reactive | Works with `from()` | Manual subscriptions |
|
|
216
|
+
| Testing | Easy mock via `define()` | Harder isolation |
|
|
217
|
+
|
|
218
|
+
## When to Use
|
|
219
|
+
|
|
220
|
+
✅ **Use pool:**
|
|
221
|
+
- Parameterized state (users, articles, forms)
|
|
222
|
+
- Entries with natural TTL (cache, session)
|
|
223
|
+
- Reactive subscriptions per entry
|
|
224
|
+
- Memory management matters
|
|
225
|
+
|
|
226
|
+
❌ **NEVER use pool:**
|
|
227
|
+
- Single global atom → use `atom`
|
|
228
|
+
- State doesn't vary by key
|
|
229
|
+
- Entries should NEVER be GC'd → use Map
|