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,188 @@
|
|
|
1
|
+
# Async Patterns
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
| Utility | Input | Output | Behavior |
|
|
6
|
+
| ----------- | --------------- | ---------------------- | -------------------------- |
|
|
7
|
+
| `all()` | Array of atoms | Array of values | Waits for all |
|
|
8
|
+
| `any()` | Record of atoms | `{ key, value }` | First to resolve |
|
|
9
|
+
| `race()` | Record of atoms | `{ key, value }` | First to settle |
|
|
10
|
+
| `settled()` | Array of atoms | Array of SettledResult | Waits for all settled |
|
|
11
|
+
| `and()` | Array of conds | boolean | AND with short-circuit |
|
|
12
|
+
| `or()` | Array of conds | boolean | OR with short-circuit |
|
|
13
|
+
|
|
14
|
+
## all() — Promise.all
|
|
15
|
+
|
|
16
|
+
Waits for ALL atoms. Returns array.
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
const dashboard$ = derived(({ all }) => {
|
|
20
|
+
const [user, posts, comments] = all([user$, posts$, comments$]);
|
|
21
|
+
return { user, posts, comments };
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Use when:** Need all data before rendering.
|
|
26
|
+
|
|
27
|
+
## any() — Promise.any
|
|
28
|
+
|
|
29
|
+
Returns first resolved. Uses object for key identification.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
const data$ = derived(({ any }) => {
|
|
33
|
+
const result = any({ primary: primaryApi$, fallback: fallbackApi$ });
|
|
34
|
+
// result: { key: "primary" | "fallback", value: T }
|
|
35
|
+
return result.value;
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Use when:** Multiple redundant sources, want fastest.
|
|
40
|
+
|
|
41
|
+
## race() — Promise.race
|
|
42
|
+
|
|
43
|
+
Returns first settled (ready OR error). Uses object.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const data$ = derived(({ race }) => {
|
|
47
|
+
const result = race({ cache: cache$, api: api$ });
|
|
48
|
+
return result.value;
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Use when:** Show whatever resolves first.
|
|
53
|
+
|
|
54
|
+
## settled() — Promise.allSettled
|
|
55
|
+
|
|
56
|
+
Returns status for each. Waits until all settled.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const results$ = derived(({ settled }) => {
|
|
60
|
+
const [userResult, postsResult] = settled([user$, posts$]);
|
|
61
|
+
return {
|
|
62
|
+
user: userResult.status === "ready" ? userResult.value : null,
|
|
63
|
+
posts: postsResult.status === "ready" ? postsResult.value : [],
|
|
64
|
+
hasErrors: userResult.status === "error" || postsResult.status === "error",
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Use when:** Handle partial failures gracefully.
|
|
70
|
+
|
|
71
|
+
### SettledResult Type
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
type SettledResult<T> =
|
|
75
|
+
| { status: "ready"; value: T }
|
|
76
|
+
| { status: "error"; error: unknown };
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## state() — No Throwing
|
|
80
|
+
|
|
81
|
+
Get state without Suspense.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const data$ = derived(({ state }) => {
|
|
85
|
+
const userState = state(user$);
|
|
86
|
+
if (userState.status === "loading") return { loading: true };
|
|
87
|
+
if (userState.status === "error") return { error: userState.error };
|
|
88
|
+
return { user: userState.value };
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### AtomState Type
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
type AtomState<T> =
|
|
96
|
+
| { status: "ready"; value: T }
|
|
97
|
+
| { status: "error"; error: unknown }
|
|
98
|
+
| { status: "loading"; promise: Promise<T> };
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Combining Patterns
|
|
102
|
+
|
|
103
|
+
### Graceful Degradation
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const dashboard$ = derived(({ read, settled }) => {
|
|
107
|
+
const user = read(user$); // Required
|
|
108
|
+
|
|
109
|
+
const [analyticsResult, notificationsResult] = settled([analytics$, notifications$]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
user,
|
|
113
|
+
analytics: analyticsResult.status === "ready" ? analyticsResult.value : null,
|
|
114
|
+
notifications: notificationsResult.status === "ready" ? notificationsResult.value : [],
|
|
115
|
+
warnings: [analyticsResult, notificationsResult]
|
|
116
|
+
.filter((r) => r.status === "error")
|
|
117
|
+
.map((r) => r.error),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Cache-First
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const article$ = derived(({ race }) => {
|
|
126
|
+
const result = race({ cache: cachedArticle$, network: fetchedArticle$ });
|
|
127
|
+
console.log(`From: ${result.key}`);
|
|
128
|
+
return result.value;
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Parallel Loading
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const [user, posts, comments] = useSelector(({ all }) =>
|
|
136
|
+
all([user$, posts$, comments$])
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## and() — Logical AND
|
|
141
|
+
|
|
142
|
+
Short-circuit. Returns true if ALL truthy.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const canEdit$ = derived(({ and }) => and([isLoggedIn$, hasPermission$]));
|
|
146
|
+
|
|
147
|
+
// Lazy evaluation
|
|
148
|
+
const canDelete$ = derived(({ and }) => and([
|
|
149
|
+
isLoggedIn$,
|
|
150
|
+
() => hasDeletePermission$, // Only if logged in
|
|
151
|
+
]));
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Condition Types
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
type Condition =
|
|
158
|
+
| boolean // Static
|
|
159
|
+
| Atom<unknown> // Always read
|
|
160
|
+
| (() => boolean | Atom<unknown>); // Lazy
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## or() — Logical OR
|
|
164
|
+
|
|
165
|
+
Short-circuit. Returns true if ANY truthy.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const hasData$ = derived(({ or }) => or([cacheData$, apiData$]));
|
|
169
|
+
|
|
170
|
+
// Lazy fallback
|
|
171
|
+
const data$ = derived(({ or }) => or([
|
|
172
|
+
() => primaryData$,
|
|
173
|
+
() => fallbackData$,
|
|
174
|
+
]));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Boolean + Async
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// (A && B) || C
|
|
181
|
+
const result$ = derived(({ or, and }) => or([and([a$, b$]), c$]));
|
|
182
|
+
|
|
183
|
+
// Guard expensive ops
|
|
184
|
+
const data$ = derived(({ and, read }) => {
|
|
185
|
+
if (!and([isLoggedIn$, hasPermission$])) return null;
|
|
186
|
+
return read(expensiveData$);
|
|
187
|
+
});
|
|
188
|
+
```
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Atom Patterns
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
const count$ = atom(0);
|
|
7
|
+
const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
|
|
8
|
+
const data$ = atom(() => expensiveInit());
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core API
|
|
12
|
+
|
|
13
|
+
| Method | Signature | Description |
|
|
14
|
+
| ------------ | -------------------------- | ------------------------ |
|
|
15
|
+
| `get()` | `() => T` | Get current value |
|
|
16
|
+
| `set()` | `(value \| reducer)` | Update value |
|
|
17
|
+
| `reset()` | `() => void` | Reset to initial |
|
|
18
|
+
| `dirty()` | `() => boolean` | Changed since init/reset |
|
|
19
|
+
| `on()` | `(listener) => unsub` | Subscribe to changes |
|
|
20
|
+
| `_dispose()` | `() => void` | Cleanup (used by pool) |
|
|
21
|
+
|
|
22
|
+
## Lazy Initialization
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
const config$ = atom(() => parseExpensiveConfig());
|
|
26
|
+
|
|
27
|
+
// reset() re-runs initializer
|
|
28
|
+
const timestamp$ = atom(() => Date.now());
|
|
29
|
+
timestamp$.reset(); // New timestamp
|
|
30
|
+
|
|
31
|
+
// Store function as value
|
|
32
|
+
const callback$ = atom(() => () => console.log("hello"));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Dirty Tracking
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const form$ = atom({ name: "", email: "" }, { meta: { key: "form" } });
|
|
39
|
+
|
|
40
|
+
form$.dirty(); // false
|
|
41
|
+
form$.set({ name: "John", email: "" });
|
|
42
|
+
form$.dirty(); // true
|
|
43
|
+
form$.reset();
|
|
44
|
+
form$.dirty(); // false
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
function FormButtons() {
|
|
49
|
+
const isDirty = useSelector(() => form$.dirty());
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<button disabled={!isDirty}>Save</button>
|
|
53
|
+
<button onClick={() => form$.reset()}>Reset</button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## AtomContext — Signal & Cleanup
|
|
60
|
+
|
|
61
|
+
Lazy initializer receives context:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
interface AtomContext {
|
|
65
|
+
signal: AbortSignal; // Aborted on set()/reset()
|
|
66
|
+
onCleanup(fn: VoidFunction): void; // Runs on value change
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Abort Signal
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const data$ = atom((ctx) => {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
ctx.signal.addEventListener("abort", () => controller.abort());
|
|
76
|
+
return fetch("/api/data", { signal: controller.signal });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
data$.set(fetch("/api/data/new")); // Previous fetch aborted
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Cleanup
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const subscription$ = atom((ctx) => {
|
|
86
|
+
const sub = websocket.subscribe("channel");
|
|
87
|
+
ctx.onCleanup(() => sub.unsubscribe());
|
|
88
|
+
return sub;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
subscription$.reset(); // Unsubscribes, creates new
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Combined
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const realtime$ = atom((ctx) => {
|
|
98
|
+
const socket = new WebSocket("wss://api.example.com");
|
|
99
|
+
ctx.onCleanup(() => socket.close());
|
|
100
|
+
fetchInitialData({ signal: ctx.signal });
|
|
101
|
+
return socket;
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Equality Options
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Default: strict (Object.is)
|
|
109
|
+
const count$ = atom(0);
|
|
110
|
+
|
|
111
|
+
// Shallow
|
|
112
|
+
const user$ = atom({ name: "", email: "" }, { equals: "shallow" });
|
|
113
|
+
user$.set((prev) => ({ ...prev })); // No notification
|
|
114
|
+
|
|
115
|
+
// Deep
|
|
116
|
+
const config$ = atom({ nested: { value: 1 } }, { equals: "deep" });
|
|
117
|
+
|
|
118
|
+
// Custom
|
|
119
|
+
const data$ = atom(
|
|
120
|
+
{ id: 1, timestamp: Date.now() },
|
|
121
|
+
{ equals: (a, b) => a.id === b.id }
|
|
122
|
+
);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Shorthand | Description |
|
|
126
|
+
| ------------ | ---------------------------- |
|
|
127
|
+
| `"strict"` | Object.is (default, fastest) |
|
|
128
|
+
| `"shallow"` | Compare keys with Object.is |
|
|
129
|
+
| `"shallow2"` | 2 levels deep |
|
|
130
|
+
| `"shallow3"` | 3 levels deep |
|
|
131
|
+
| `"deep"` | Full recursive (slowest) |
|
|
132
|
+
|
|
133
|
+
## readonly() (REQUIRED)
|
|
134
|
+
|
|
135
|
+
**MUST** expose atoms as read-only:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const myModule = define(() => {
|
|
139
|
+
const count$ = atom(0, { meta: { key: "counter" } });
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
count$: readonly(count$), // Consumers can't set()
|
|
143
|
+
increment: () => count$.set((p) => p + 1),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Usage
|
|
148
|
+
const { count$, increment } = myModule();
|
|
149
|
+
count$.get(); // ✅ OK
|
|
150
|
+
count$.set(5); // ❌ TypeScript error
|
|
151
|
+
increment(); // ✅ Use action
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Multiple atoms:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
return { ...readonly({ count$, name$ }), setName: (n) => name$.set(n) };
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Async Values
|
|
161
|
+
|
|
162
|
+
Atom stores Promises as-is:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const posts$ = atom(fetchPosts());
|
|
166
|
+
posts$.get(); // Promise<Post[]>
|
|
167
|
+
posts$.set(fetchPosts()); // Store new Promise
|
|
168
|
+
|
|
169
|
+
// With lazy init
|
|
170
|
+
const lazyPosts$ = atom(() => fetchPosts());
|
|
171
|
+
lazyPosts$.reset(); // Refetches
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Note:** Use `derived()` with `read()` for automatic Promise unwrapping and Suspense.
|
|
175
|
+
|
|
176
|
+
## Plugin System (.use())
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const count$ = atom(0)
|
|
180
|
+
.use((src) => ({ ...src, double: () => src.get() * 2 }))
|
|
181
|
+
.use((src) => ({ ...src, triple: () => src.get() * 3 }));
|
|
182
|
+
|
|
183
|
+
count$.double(); // 0
|
|
184
|
+
count$.triple(); // 0
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Common Patterns
|
|
188
|
+
|
|
189
|
+
### Form State
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const form$ = atom<FormState>(
|
|
193
|
+
{ values: {}, errors: {}, touched: {} },
|
|
194
|
+
{ meta: { key: "contactForm" }, equals: "shallow" }
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const setField = (name: string, value: string) => {
|
|
198
|
+
form$.set((p) => ({
|
|
199
|
+
...p,
|
|
200
|
+
values: { ...p.values, [name]: value },
|
|
201
|
+
touched: { ...p.touched, [name]: true },
|
|
202
|
+
}));
|
|
203
|
+
};
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Cache with Expiration
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const cache$ = atom((ctx) => {
|
|
210
|
+
const data = new Map<string, unknown>();
|
|
211
|
+
const timeout = setTimeout(() => data.clear(), 300_000);
|
|
212
|
+
ctx.onCleanup(() => clearTimeout(timeout));
|
|
213
|
+
return data;
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### WebSocket
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const ws$ = atom((ctx) => {
|
|
221
|
+
const socket = new WebSocket("wss://api.example.com");
|
|
222
|
+
socket.onopen = () => console.log("Connected");
|
|
223
|
+
ctx.onCleanup(() => socket.close());
|
|
224
|
+
return socket;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
ws$.reset(); // Closes old, creates new
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## When to Use
|
|
231
|
+
|
|
232
|
+
| Use Case | MutableAtom | Derived |
|
|
233
|
+
| ----------------------- | ----------- | ------- |
|
|
234
|
+
| User input/form state | ✅ | ❌ |
|
|
235
|
+
| API response storage | ✅ | ❌ |
|
|
236
|
+
| Computed from atoms | ❌ | ✅ |
|
|
237
|
+
| Transformed async data | ❌ | ✅ |
|
|
238
|
+
| Cached calculations | ❌ | ✅ |
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Deferred Loading with ready()
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
When atom value is `undefined`/`null` during initialization:
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// ❌ Problem: id starts undefined
|
|
9
|
+
const currentUserId$ = atom<string | undefined>(undefined);
|
|
10
|
+
|
|
11
|
+
const userProfile$ = derived(({ read, from }) => {
|
|
12
|
+
const userId = read(currentUserId$); // undefined initially
|
|
13
|
+
const user$ = from(userPool, userId); // Error: can't create entry
|
|
14
|
+
return read(user$);
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Solution: ready()
|
|
19
|
+
|
|
20
|
+
`ready()` returns `never` when value is `undefined`/`null`, blocking computation until value exists:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// ✅ Correct
|
|
24
|
+
const userProfile$ = derived(({ read, ready, from }) => {
|
|
25
|
+
const userId = ready(currentUserId$); // Blocks until truthy
|
|
26
|
+
const user$ = from(userPool, userId);
|
|
27
|
+
return read(user$);
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
| Input | Output |
|
|
34
|
+
| ----------------------------- | --------------- |
|
|
35
|
+
| `ready(atom<T \| undefined>)` | `T` when truthy |
|
|
36
|
+
| Value is `undefined`/`null` | Returns `never` |
|
|
37
|
+
| Value is truthy | Returns `T` |
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const id$ = atom<string | undefined>(undefined);
|
|
41
|
+
|
|
42
|
+
derived(({ ready }) => {
|
|
43
|
+
const id = ready(id$);
|
|
44
|
+
// ^? string (not string | undefined)
|
|
45
|
+
return fetchUser(id);
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Behavior
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const userId$ = atom<string | undefined>(undefined);
|
|
53
|
+
|
|
54
|
+
const profile$ = derived(({ read, ready, from }) => {
|
|
55
|
+
const userId = ready(userId$);
|
|
56
|
+
const user$ = from(userPool, userId);
|
|
57
|
+
return read(user$);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Initially:
|
|
61
|
+
profile$.get(); // Promise never resolves (blocked)
|
|
62
|
+
profile$.staleValue; // fallback if set, else undefined
|
|
63
|
+
|
|
64
|
+
// After:
|
|
65
|
+
userId$.set("user-123");
|
|
66
|
+
await profile$.get(); // { name: "John", ... }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Use Cases
|
|
70
|
+
|
|
71
|
+
### Pool with Optional Params
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const currentEntityId$ = atom<string | undefined>(undefined);
|
|
75
|
+
|
|
76
|
+
const entityDetails$ = derived(({ read, ready, from }) => {
|
|
77
|
+
const id = ready(currentEntityId$);
|
|
78
|
+
const entity$ = from(entityPool, id);
|
|
79
|
+
return read(entity$);
|
|
80
|
+
}, { fallback: null });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Dependent Computation
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const config$ = atom<Config | null>(null);
|
|
87
|
+
const apiUrl$ = atom<string | undefined>(undefined);
|
|
88
|
+
|
|
89
|
+
const data$ = derived(({ read, ready }) => {
|
|
90
|
+
const config = ready(config$);
|
|
91
|
+
const url = ready(apiUrl$);
|
|
92
|
+
return read(atom(() => fetch(`${url}/data?version=${config.version}`)));
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Conditional UI
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
function UserDashboard() {
|
|
100
|
+
const user = useSelector(({ read, ready }) => {
|
|
101
|
+
return ready(currentUser$);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return <Dashboard user={user} />;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// With state() fallback
|
|
108
|
+
function UserCard() {
|
|
109
|
+
const result = useSelector(({ state }) => state(currentUser$));
|
|
110
|
+
|
|
111
|
+
if (result.status === "loading") return <Skeleton />;
|
|
112
|
+
if (result.status === "error") return <Error />;
|
|
113
|
+
if (!result.value) return <LoginPrompt />;
|
|
114
|
+
return <Card user={result.value} />;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Effect with Guard
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
effect(({ read, ready }) => {
|
|
122
|
+
const userId = ready(currentUserId$);
|
|
123
|
+
analytics.identify(userId);
|
|
124
|
+
}, { meta: { key: "analytics.identify" } });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Multi-ready
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const report$ = derived(({ read, ready, from }) => {
|
|
131
|
+
const userId = ready(currentUserId$);
|
|
132
|
+
const projectId = ready(currentProjectId$);
|
|
133
|
+
const teamId = ready(currentTeamId$);
|
|
134
|
+
|
|
135
|
+
const user$ = from(userPool, userId);
|
|
136
|
+
const project$ = from(projectPool, projectId);
|
|
137
|
+
const team$ = from(teamPool, teamId);
|
|
138
|
+
|
|
139
|
+
const [user, project, team] = [read(user$), read(project$), read(team$)];
|
|
140
|
+
return { user, project, team };
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## ready() vs Manual Check
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// ❌ Verbose, TypeScript still sees string | undefined
|
|
148
|
+
const profile$ = derived(({ read }) => {
|
|
149
|
+
const id = read(currentUserId$);
|
|
150
|
+
if (!id) return null;
|
|
151
|
+
return read(from(userPool, id)); // Type: still string | undefined
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ✅ Concise, TypeScript narrows to string
|
|
155
|
+
const profile$ = derived(({ ready, read, from }) => {
|
|
156
|
+
const id = ready(currentUserId$);
|
|
157
|
+
// ^? string
|
|
158
|
+
return read(from(userPool, id));
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## With Suspense
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
function EntityPage() {
|
|
166
|
+
return (
|
|
167
|
+
<Suspense fallback={<Skeleton />}>
|
|
168
|
+
<EntityDetails />
|
|
169
|
+
</Suspense>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function EntityDetails() {
|
|
174
|
+
const entity = useSelector(({ read, ready, from }) => {
|
|
175
|
+
const id = ready(currentEntityId$); // Suspends until ID set
|
|
176
|
+
return read(from(entityPool, id));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return <Details entity={entity} />;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Summary
|
|
184
|
+
|
|
185
|
+
| Scenario | Solution |
|
|
186
|
+
| ------------------------------- | --------------------------- |
|
|
187
|
+
| Pool with optional params | `ready(params$)` + `from()` |
|
|
188
|
+
| Dependent chains | `ready()` per dependency |
|
|
189
|
+
| Wait for auth state | `ready(currentUser$)` |
|
|
190
|
+
| Multi-value gate | Multiple `ready()` calls |
|
|
191
|
+
| Type narrowing | `ready()` removes `null` |
|