atomirx 0.0.8 → 0.1.1
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 -1
- package/dist/react/index.js +191 -151
- 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 -42
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Rules & Best Practices
|
|
2
|
+
|
|
3
|
+
## Service vs Store (CRITICAL)
|
|
4
|
+
|
|
5
|
+
**All state/logic MUST use `define()`. Services = stateless. Stores = atoms.**
|
|
6
|
+
|
|
7
|
+
| Type | Purpose | Variable | File | Contains |
|
|
8
|
+
| ----------- | -------------- | ------------- | ----------------- | ----------------------- |
|
|
9
|
+
| **Service** | Stateless I/O | `authService` | `auth.service.ts` | Pure functions |
|
|
10
|
+
| **Store** | Reactive state | `authStore` | `auth.store.ts` | Atoms, derived, effects |
|
|
11
|
+
|
|
12
|
+
### Service
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
export const authService = define(
|
|
16
|
+
(): AuthService => ({
|
|
17
|
+
checkSupport: async () => {
|
|
18
|
+
/* WebAuthn API */
|
|
19
|
+
},
|
|
20
|
+
register: async (opts) => {
|
|
21
|
+
/* credential creation */
|
|
22
|
+
},
|
|
23
|
+
authenticate: async (opts) => {
|
|
24
|
+
/* credential assertion */
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Store
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { authService } from "@/services/auth/auth.service";
|
|
34
|
+
|
|
35
|
+
export const authStore = define(() => {
|
|
36
|
+
const auth = authService(); // Inject via invocation
|
|
37
|
+
|
|
38
|
+
const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
|
|
39
|
+
const isAuthenticated$ = derived(({ read }) => read(user$) !== null, {
|
|
40
|
+
meta: { key: "auth.isAuthenticated" },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...readonly({ user$, isAuthenticated$ }),
|
|
45
|
+
login: async () => {
|
|
46
|
+
const result = await auth.authenticate({});
|
|
47
|
+
if (result.success) user$.set(result.user);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### FORBIDDEN: Factory Pattern
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// ❌ FORBIDDEN
|
|
57
|
+
let instance: AuthService | null = null;
|
|
58
|
+
export function getAuthService(): AuthService {
|
|
59
|
+
if (!instance) instance = createAuthService();
|
|
60
|
+
return instance;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
import { getAuthService } from "@/services/auth";
|
|
64
|
+
const auth = getAuthService(); // WRONG
|
|
65
|
+
|
|
66
|
+
// ✅ REQUIRED
|
|
67
|
+
import { authService } from "@/services/auth/auth.service";
|
|
68
|
+
const auth = authService(); // Module invocation
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Detection:** `get*Service()`, `create*Service()`, `*Factory()` → STOP, refactor to `define()`.
|
|
72
|
+
|
|
73
|
+
| Factory Pattern | Module Pattern (`define()`) |
|
|
74
|
+
| --------------- | --------------------------- |
|
|
75
|
+
| Not mockable | `service.override(mock)` |
|
|
76
|
+
| Hidden deps | Explicit dependencies |
|
|
77
|
+
| No lazy control | Lazy singleton default |
|
|
78
|
+
| Breaks DI | Uses atomirx DI |
|
|
79
|
+
|
|
80
|
+
## useSelector Grouping (CRITICAL)
|
|
81
|
+
|
|
82
|
+
**MUST group multiple reads into single `useSelector`.**
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// ✅ DO
|
|
86
|
+
const { count, user, settings } = useSelector(({ read }) => ({
|
|
87
|
+
count: read(count$),
|
|
88
|
+
user: read(user$),
|
|
89
|
+
settings: read(settings$),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// ❌ DON'T
|
|
93
|
+
const count = useSelector(count$);
|
|
94
|
+
const user = useSelector(user$);
|
|
95
|
+
const settings = useSelector(settings$);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Multiple Calls | Single Grouped |
|
|
99
|
+
| ------------------- | ---------------- |
|
|
100
|
+
| N subscriptions | 1 subscription |
|
|
101
|
+
| N checks per change | 1 check |
|
|
102
|
+
| Scattered values | Related together |
|
|
103
|
+
|
|
104
|
+
**Single `useSelector(atom$)` OK when:** only one atom needed.
|
|
105
|
+
|
|
106
|
+
## useAction with Atom Deps
|
|
107
|
+
|
|
108
|
+
**Pass atoms to `deps`, use `.get()` inside for auto re-dispatch.**
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// ✅ DO
|
|
112
|
+
const load = useAction(
|
|
113
|
+
async () => {
|
|
114
|
+
const val1 = atom1$.get();
|
|
115
|
+
const val2 = await atom2$.get();
|
|
116
|
+
return val1 + val2;
|
|
117
|
+
},
|
|
118
|
+
{ deps: [atom1$, atom2$], lazy: false }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ❌ DON'T
|
|
122
|
+
const { val1, val2 } = useSelector(({ read }) => ({
|
|
123
|
+
val1: read(atom1$), // Suspends before useAction
|
|
124
|
+
val2: read(atom2$),
|
|
125
|
+
}));
|
|
126
|
+
const load = useAction(async () => val1 + val2, {
|
|
127
|
+
deps: [val1, val2],
|
|
128
|
+
lazy: false,
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## define() Isolation (CRITICAL)
|
|
133
|
+
|
|
134
|
+
**MUST use `define()` for all state/logic.**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// ✅ DO
|
|
138
|
+
export const counterStore = define(() => {
|
|
139
|
+
const storage = storageService();
|
|
140
|
+
const count$ = atom(0, { meta: { key: "counter.count" } });
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
...readonly({ count$ }),
|
|
144
|
+
increment: () => count$.set((x) => x + 1),
|
|
145
|
+
save: () => storage.set("count", count$.get()),
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ❌ DON'T
|
|
150
|
+
const count$ = atom(0); // Global, not testable
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Benefit | Description |
|
|
154
|
+
| --------------- | ------------------------------- |
|
|
155
|
+
| Testing/Mocking | Override for unit tests |
|
|
156
|
+
| Lazy init | Only when first accessed |
|
|
157
|
+
| DI | Depend on services/stores |
|
|
158
|
+
| Environment | Override per platform |
|
|
159
|
+
| Encapsulation | `readonly()` prevents mutations |
|
|
160
|
+
|
|
161
|
+
### Override Pattern
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const storageService = define((): StorageService => {
|
|
165
|
+
throw new Error("Not implemented");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Platform implementations
|
|
169
|
+
const webStorage = define(
|
|
170
|
+
(): StorageService => ({
|
|
171
|
+
get: (key) => localStorage.getItem(key),
|
|
172
|
+
set: (key, val) => localStorage.setItem(key, val),
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Override based on environment
|
|
177
|
+
if (isWeb) storageService.override(webStorage);
|
|
178
|
+
|
|
179
|
+
// In tests
|
|
180
|
+
storageService.override(() => ({
|
|
181
|
+
get: jest.fn(),
|
|
182
|
+
set: jest.fn(),
|
|
183
|
+
}));
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## batch() for Multiple Updates
|
|
187
|
+
|
|
188
|
+
**MUST wrap multiple updates in `batch()`.**
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// ✅ DO
|
|
192
|
+
batch(() => {
|
|
193
|
+
user$.set(newUser);
|
|
194
|
+
settings$.set(newSettings);
|
|
195
|
+
lastUpdated$.set(Date.now());
|
|
196
|
+
}); // Single notification
|
|
197
|
+
|
|
198
|
+
// ❌ DON'T
|
|
199
|
+
user$.set(newUser); // Notification 1
|
|
200
|
+
settings$.set(newSettings); // Notification 2
|
|
201
|
+
lastUpdated$.set(Date.now()); // Notification 3
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Without batch | With batch |
|
|
205
|
+
| ------------------- | ---------------- |
|
|
206
|
+
| N notifications | 1 notification |
|
|
207
|
+
| Intermediate states | Only final state |
|
|
208
|
+
| UI flicker | Clean update |
|
|
209
|
+
|
|
210
|
+
## Single Effect, Single Workflow (CRITICAL)
|
|
211
|
+
|
|
212
|
+
**Each effect = ONE workflow. Split multiple workflows.**
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// ❌ WRONG
|
|
216
|
+
effect(({ read }) => {
|
|
217
|
+
const id = read(currentId$);
|
|
218
|
+
const filter = read(filter$);
|
|
219
|
+
fetchEntity(id); // Workflow 1
|
|
220
|
+
localStorage.setItem("filter", filter); // Workflow 2
|
|
221
|
+
trackPageView(id); // Workflow 3
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ✅ CORRECT
|
|
225
|
+
effect(
|
|
226
|
+
({ read }) => {
|
|
227
|
+
const id = read(currentId$);
|
|
228
|
+
if (id) fetchEntity(id);
|
|
229
|
+
},
|
|
230
|
+
{ meta: { key: "fetch.entity" } }
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
effect(
|
|
234
|
+
({ read }) => {
|
|
235
|
+
localStorage.setItem("filter", read(filter$));
|
|
236
|
+
},
|
|
237
|
+
{ meta: { key: "persist.filter" } }
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
effect(
|
|
241
|
+
({ read }) => {
|
|
242
|
+
const id = read(currentId$);
|
|
243
|
+
if (id) trackPageView(id);
|
|
244
|
+
},
|
|
245
|
+
{ meta: { key: "analytics.pageView" } }
|
|
246
|
+
);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
| Multiple Workflows | Single Workflow |
|
|
250
|
+
| ------------------ | -------------------- |
|
|
251
|
+
| Hard to trace | Clear cause → effect |
|
|
252
|
+
| Combined triggers | Independent |
|
|
253
|
+
| Hard to test | Test in isolation |
|
|
254
|
+
| Hard to disable | Comment one effect |
|
|
255
|
+
|
|
256
|
+
## meta.key (CRITICAL)
|
|
257
|
+
|
|
258
|
+
**MUST define for ALL atoms, derived, effects.**
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// ✅ CORRECT
|
|
262
|
+
const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
|
|
263
|
+
const isAuth$ = derived(({ read }) => read(user$) !== null, {
|
|
264
|
+
meta: { key: "auth.isAuthenticated" },
|
|
265
|
+
});
|
|
266
|
+
effect(({ read }) => analytics.identify(read(user$)?.id), {
|
|
267
|
+
meta: { key: "auth.identifyUser" },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ❌ WRONG
|
|
271
|
+
const user$ = atom<User | null>(null);
|
|
272
|
+
const isAuth$ = derived(({ read }) => read(user$) !== null);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Pattern | Example |
|
|
276
|
+
| ------------------- | ---------------------- |
|
|
277
|
+
| `store.atomName` | `auth.user` |
|
|
278
|
+
| `store.derivedName` | `auth.isAuthenticated` |
|
|
279
|
+
| `store.effectName` | `sync.autoSave` |
|
|
280
|
+
|
|
281
|
+
## Atom Storage
|
|
282
|
+
|
|
283
|
+
**NEVER store atoms in component scope.**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// ❌ BAD - memory leak
|
|
287
|
+
function Component() {
|
|
288
|
+
const data$ = useRef(atom(0)).current;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ✅ GOOD
|
|
292
|
+
const dataStore = define(() => {
|
|
293
|
+
const data$ = atom(0, { meta: { key: "data" } });
|
|
294
|
+
return { ...readonly({ data$ }), update: (v) => data$.set(v) };
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Mutation Co-location
|
|
299
|
+
|
|
300
|
+
**All mutations MUST be in the store that owns the atom.**
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// ✅ CORRECT
|
|
304
|
+
const counterStore = define(() => {
|
|
305
|
+
const count$ = atom(0, { meta: { key: "counter.count" } });
|
|
306
|
+
return {
|
|
307
|
+
...readonly({ count$ }),
|
|
308
|
+
increment: () => count$.set((p) => p + 1),
|
|
309
|
+
decrement: () => count$.set((p) => p - 1),
|
|
310
|
+
reset: () => count$.reset(),
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ❌ WRONG
|
|
315
|
+
const { count$ } = counterStore();
|
|
316
|
+
count$.set(10); // External mutation
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## SelectContext: Sync Only
|
|
320
|
+
|
|
321
|
+
**All context methods MUST be called synchronously.**
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// ❌ WRONG
|
|
325
|
+
derived(({ read }) => {
|
|
326
|
+
setTimeout(() => read(atom$), 100); // Error
|
|
327
|
+
return "value";
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ✅ CORRECT
|
|
331
|
+
effect(({ read }) => {
|
|
332
|
+
const config = read(config$);
|
|
333
|
+
setTimeout(() => {
|
|
334
|
+
const data = myAtom$.get(); // Use .get() for async
|
|
335
|
+
console.log(data);
|
|
336
|
+
}, 100);
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Error Handling: safe() Not try/catch
|
|
341
|
+
|
|
342
|
+
**NEVER try/catch with read() — breaks Suspense.**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// ❌ WRONG
|
|
346
|
+
derived(({ read }) => {
|
|
347
|
+
try {
|
|
348
|
+
return read(asyncAtom$);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return null; // Catches Promise!
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ✅ CORRECT
|
|
355
|
+
derived(({ read, safe }) => {
|
|
356
|
+
const [err, value] = safe(() => read(asyncAtom$));
|
|
357
|
+
if (err) return { error: err.message };
|
|
358
|
+
return { value };
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Naming Conventions
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// All atoms: $ suffix
|
|
366
|
+
const count$ = atom(0);
|
|
367
|
+
const user$ = atom<User | null>(null);
|
|
368
|
+
const productList$ = atom(fetchProducts()); // Async — still just $
|
|
369
|
+
const config$ = atom(loadConfig());
|
|
370
|
+
|
|
371
|
+
// Derived: $ suffix
|
|
372
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
373
|
+
const userName$ = derived(({ read }) => read(user$).name);
|
|
374
|
+
|
|
375
|
+
// Services: *Service (NO atoms)
|
|
376
|
+
const authService = define((): AuthService => ...);
|
|
377
|
+
|
|
378
|
+
// Stores: *Store (HAS atoms)
|
|
379
|
+
const authStore = define(() => ...);
|
|
380
|
+
|
|
381
|
+
// Actions: verb-led
|
|
382
|
+
navigateTo, invalidate, refresh, fetchUser, logout
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
| Type | Suffix | Example |
|
|
386
|
+
| -------------------- | --------- | ------------------------ |
|
|
387
|
+
| Atom (sync or async) | `$` | `count$`, `productList$` |
|
|
388
|
+
| Derived | `$` | `doubled$`, `userName$` |
|
|
389
|
+
| Pool | `Pool` | `userPool` |
|
|
390
|
+
| Service | `Service` | `authService` |
|
|
391
|
+
| Store | `Store` | `authStore` |
|
|
392
|
+
|
|
393
|
+
**Why no `Async$`?** Atomirx abstracts async/sync — you don't care in SelectContext (Suspense handles it), and services receive values as parameters.
|
|
394
|
+
|
|
395
|
+
### File Structure
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
src/
|
|
399
|
+
├── services/ # Stateless
|
|
400
|
+
│ ├── auth/
|
|
401
|
+
│ │ └── auth.service.ts
|
|
402
|
+
│ └── crypto/
|
|
403
|
+
│ └── crypto.service.ts
|
|
404
|
+
└── stores/ # Stateful
|
|
405
|
+
├── auth.store.ts
|
|
406
|
+
└── todos.store.ts
|
|
407
|
+
```
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# SelectContext API
|
|
2
|
+
|
|
3
|
+
SelectContext provides methods for `derived`, `effect`, `useSelector`, and `rx`.
|
|
4
|
+
|
|
5
|
+
## Methods
|
|
6
|
+
|
|
7
|
+
| Method | Returns | Description |
|
|
8
|
+
| --------------- | -------------------------- | ------------------------------ |
|
|
9
|
+
| `read(atom)` | `T` | Get value, suspends if loading |
|
|
10
|
+
| `ready(atom)` | `T` (non-nullable) | Block until truthy |
|
|
11
|
+
| `state(atom)` | `AtomState<T>` | Get state without suspending |
|
|
12
|
+
| `untrack(atom)` | `T` | Read without tracking dep |
|
|
13
|
+
| `untrack(fn)` | `T` | Execute fn without tracking |
|
|
14
|
+
| `safe(fn)` | `[error?, value?]` | Catch errors, rethrow Promise |
|
|
15
|
+
| `all(atoms)` | `T[]` | Wait for all |
|
|
16
|
+
| `any(record)` | `{ key, value }` | First to resolve |
|
|
17
|
+
| `race(record)` | `{ key, value }` | First to settle |
|
|
18
|
+
| `settled()` | `SettledResult<T>[]` | All with status |
|
|
19
|
+
| `from(pool)` | `ScopedAtom<T>` | Get atom from pool |
|
|
20
|
+
| `track(atom)` | `void` | Track without reading |
|
|
21
|
+
| `and(conds)` | `boolean` | Logical AND |
|
|
22
|
+
| `or(conds)` | `boolean` | Logical OR |
|
|
23
|
+
|
|
24
|
+
## CRITICAL Rules
|
|
25
|
+
|
|
26
|
+
### Sync Access Only
|
|
27
|
+
|
|
28
|
+
**All methods MUST be called synchronously:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ❌ FORBIDDEN
|
|
32
|
+
derived(({ read }) => {
|
|
33
|
+
setTimeout(() => read(atom$), 100); // Throws
|
|
34
|
+
return "value";
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ✅ REQUIRED
|
|
38
|
+
derived(({ read }) => {
|
|
39
|
+
const value = read(atom$); // Sync call
|
|
40
|
+
return value;
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### read() + Suspense
|
|
45
|
+
|
|
46
|
+
`read()` suspends (throws Promise) when atom loading:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Wrap with Suspense
|
|
50
|
+
<Suspense fallback={<Loading />}>
|
|
51
|
+
<Component />
|
|
52
|
+
</Suspense>
|
|
53
|
+
|
|
54
|
+
function Component() {
|
|
55
|
+
const data = useSelector(({ read }) => read(asyncAtom$)); // May suspend
|
|
56
|
+
return <div>{data}</div>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### from() Returns ScopedAtom
|
|
61
|
+
|
|
62
|
+
**MUST use with `read()`, NEVER `.get()`:**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// ❌ FORBIDDEN
|
|
66
|
+
derived(({ from }) => {
|
|
67
|
+
const user$ = from(userPool, "user-1");
|
|
68
|
+
return user$.get(); // THROWS
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ✅ REQUIRED
|
|
72
|
+
derived(({ read, from }) => {
|
|
73
|
+
const user$ = from(userPool, "user-1");
|
|
74
|
+
return read(user$);
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### untrack() — Read Without Tracking
|
|
79
|
+
|
|
80
|
+
**Read atoms or execute functions without creating dependencies.**
|
|
81
|
+
|
|
82
|
+
Use `untrack()` when you need a value but don't want re-computation when it changes.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Form 1: Pass atom directly
|
|
86
|
+
const combined$ = derived(({ read, untrack }) => {
|
|
87
|
+
const count = read(count$); // ✅ Tracked — re-computes on change
|
|
88
|
+
const config = untrack(config$); // ❌ NOT tracked — no re-compute
|
|
89
|
+
return count * config.multiplier;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Form 2: Pass function for multiple reads
|
|
93
|
+
const snapshot$ = derived(({ read, untrack }) => {
|
|
94
|
+
const liveData = read(liveData$); // Tracked
|
|
95
|
+
const snapshot = untrack(() => {
|
|
96
|
+
// None of these create dependencies
|
|
97
|
+
return { a: read(a$), b: read(b$), c: read(c$) };
|
|
98
|
+
});
|
|
99
|
+
return { liveData, snapshot };
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**When to use:**
|
|
104
|
+
|
|
105
|
+
| Scenario | Use |
|
|
106
|
+
| -------- | --- |
|
|
107
|
+
| Need latest value, re-compute on change | `read()` |
|
|
108
|
+
| Need value once, ignore future changes | `untrack()` |
|
|
109
|
+
| Initial value / config that rarely changes | `untrack()` |
|
|
110
|
+
| Snapshot of multiple atoms | `untrack(() => ...)` |
|
|
111
|
+
|
|
112
|
+
**Flow Diagram:**
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
untrack(input)
|
|
116
|
+
│
|
|
117
|
+
▼
|
|
118
|
+
Is input an Atom?
|
|
119
|
+
│
|
|
120
|
+
┌───┴───┐
|
|
121
|
+
│Yes │No (function)
|
|
122
|
+
▼ ▼
|
|
123
|
+
Read Disable tracking
|
|
124
|
+
value │
|
|
125
|
+
(no ▼
|
|
126
|
+
track) Execute fn()
|
|
127
|
+
│ │
|
|
128
|
+
│ ▼
|
|
129
|
+
│ Re-enable tracking
|
|
130
|
+
│ │
|
|
131
|
+
└───────┴──→ Return value
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Type Definitions
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
interface SelectContext {
|
|
138
|
+
read<T>(atom: ReadableAtom<T>): T;
|
|
139
|
+
ready<T>(atom: ReadableAtom<T | undefined | null>): T;
|
|
140
|
+
state<T>(atom: ReadableAtom<T>): AtomState<T>;
|
|
141
|
+
untrack<T>(atom: ReadableAtom<T>): T;
|
|
142
|
+
untrack<T>(fn: () => T): T;
|
|
143
|
+
safe<T>(fn: () => T): [unknown, undefined] | [undefined, T];
|
|
144
|
+
all<T extends readonly ReadableAtom<unknown>[]>(atoms: T): MapAtomValues<T>;
|
|
145
|
+
any<T extends Record<string, ReadableAtom<unknown>>>(atoms: T): { key: keyof T; value: T[keyof T] };
|
|
146
|
+
race<T extends Record<string, ReadableAtom<unknown>>>(atoms: T): { key: keyof T; value: T[keyof T] };
|
|
147
|
+
settled<T extends readonly ReadableAtom<unknown>[]>(atoms: T): MapSettledResults<T>;
|
|
148
|
+
from<P, T>(pool: Pool<P, T>, params: P): ScopedAtom<T>;
|
|
149
|
+
track(atom: ReadableAtom<unknown>): void;
|
|
150
|
+
and(conditions: Condition[]): boolean;
|
|
151
|
+
or(conditions: Condition[]): boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type AtomState<T> =
|
|
155
|
+
| { status: "ready"; value: T }
|
|
156
|
+
| { status: "error"; error: unknown }
|
|
157
|
+
| { status: "loading"; promise: Promise<T> };
|
|
158
|
+
|
|
159
|
+
type SettledResult<T> =
|
|
160
|
+
| { status: "ready"; value: T }
|
|
161
|
+
| { status: "error"; error: unknown };
|
|
162
|
+
|
|
163
|
+
type Condition = boolean | ReadableAtom<unknown> | (() => boolean | ReadableAtom<unknown>);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## EffectContext
|
|
167
|
+
|
|
168
|
+
Effects get additional methods:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
interface EffectContext extends SelectContext {
|
|
172
|
+
onCleanup: (fn: VoidFunction) => void;
|
|
173
|
+
signal: AbortSignal;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Usage in Primitives
|
|
178
|
+
|
|
179
|
+
### derived
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
183
|
+
|
|
184
|
+
const combined$ = derived(({ all }) => {
|
|
185
|
+
const [a, b, c] = all([atom1$, atom2$, atom3$]);
|
|
186
|
+
return a + b + c;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const guarded$ = derived(({ ready, read, from }) => {
|
|
190
|
+
const id = ready(currentId$);
|
|
191
|
+
return read(from(userPool, id));
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### effect
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
effect(({ read, onCleanup, signal }) => {
|
|
199
|
+
const id = read(userId$);
|
|
200
|
+
|
|
201
|
+
fetch(`/api/user/${id}`, { signal })
|
|
202
|
+
.then(r => r.json())
|
|
203
|
+
.then(data => userDetails$.set(data))
|
|
204
|
+
.catch(err => {
|
|
205
|
+
if (err.name !== "AbortError") console.error(err);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const sub = eventBus.subscribe("update", handler);
|
|
209
|
+
onCleanup(() => sub.unsubscribe());
|
|
210
|
+
}, { meta: { key: "fetch.user" } });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### useSelector
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const data = useSelector(({ read, safe }) => {
|
|
217
|
+
const [err, value] = safe(() => JSON.parse(read(rawJson$)));
|
|
218
|
+
return err ? { error: err.message } : { data: value };
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### rx
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
{rx(({ read, state }) => {
|
|
226
|
+
const userState = state(user$);
|
|
227
|
+
if (userState.status === "loading") return <Skeleton />;
|
|
228
|
+
if (userState.status === "error") return <ErrorMsg />;
|
|
229
|
+
return <UserCard user={userState.value} />;
|
|
230
|
+
})}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## use() — Composable Selectors
|
|
234
|
+
|
|
235
|
+
Split and reuse selection logic with `use()`:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Reusable selector functions
|
|
239
|
+
const selectProduct = ({ read }: SelectContext) => read(product$);
|
|
240
|
+
const selectUser = ({ read }: SelectContext) => read(user$);
|
|
241
|
+
const selectCart = ({ read, from }: SelectContext) => {
|
|
242
|
+
const userId = read(userId$);
|
|
243
|
+
return read(from(cartPool, userId));
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Compose in derived
|
|
247
|
+
const checkout$ = derived(({ use }) => {
|
|
248
|
+
const product = use(selectProduct);
|
|
249
|
+
const user = use(selectUser);
|
|
250
|
+
const cart = use(selectCart);
|
|
251
|
+
return { product, user, cart };
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Reuse in effect
|
|
255
|
+
effect(({ use }) => {
|
|
256
|
+
const user = use(selectUser);
|
|
257
|
+
analytics.identify(user.id);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Reuse in useSelector
|
|
261
|
+
const { product, user } = useSelector(({ use }) => ({
|
|
262
|
+
product: use(selectProduct),
|
|
263
|
+
user: use(selectUser),
|
|
264
|
+
}));
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Benefits:**
|
|
268
|
+
- Reusable selection logic across derived/effect/useSelector
|
|
269
|
+
- Cleaner, more readable selectors
|
|
270
|
+
- Easy to test individual selectors
|
|
271
|
+
- Single source of truth for data access patterns
|
|
272
|
+
|
|
273
|
+
## .get() vs read()
|
|
274
|
+
|
|
275
|
+
| Context | Use | Why |
|
|
276
|
+
| --------------------------- | ---------- | ---------------------------- |
|
|
277
|
+
| Inside selector callback | `read()` | Tracks dependencies |
|
|
278
|
+
| setTimeout/setInterval | `.get()` | Outside reactive context |
|
|
279
|
+
| Event handlers | `.get()` | Outside reactive context |
|
|
280
|
+
| After await | `.get()` | Context no longer valid |
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
effect(({ read }) => {
|
|
284
|
+
const config = read(config$); // ✅ read() — tracked
|
|
285
|
+
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
const value = count$.get(); // ✅ .get() — outside context
|
|
288
|
+
console.log(value);
|
|
289
|
+
}, 1000);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// In event handler
|
|
293
|
+
const handleClick = () => {
|
|
294
|
+
const current = count$.get(); // ✅ .get() — not in selector
|
|
295
|
+
console.log(current);
|
|
296
|
+
};
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Best Practices
|
|
300
|
+
|
|
301
|
+
1. **Group reads** in useSelector
|
|
302
|
+
2. **Use `safe()`** for error handling (not try/catch)
|
|
303
|
+
3. **Use `ready()`** for optional params
|
|
304
|
+
4. **Use `state()`** when you need loading/error states without Suspense
|
|
305
|
+
5. **Use `all()`** for parallel async atoms
|
|
306
|
+
6. **Use `settled()`** for graceful degradation
|
|
307
|
+
7. **Use `from()`** only with `read()` — never `.get()`
|
|
308
|
+
8. **Use `read()`** inside selectors, `.get()` outside
|
|
309
|
+
9. **Use `untrack()`** when you need a value but don't want re-computation on change
|