atomirx 0.0.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 +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
package/v2.md
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
# atomirx v2 - Design Specification
|
|
2
|
+
|
|
3
|
+
## Philosophy
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Atom = Base interface (value + subscribe)
|
|
7
|
+
MutableAtom = Atom + set/reset (raw storage)
|
|
8
|
+
DerivedAtom = Atom<Promise<T>> + refresh + state() (computed, always async)
|
|
9
|
+
= DerivedAtom<T, false> without fallback (staleValue: T | undefined)
|
|
10
|
+
= DerivedAtom<T, true> with fallback (staleValue: T, guaranteed)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Type Hierarchy
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Atom<T>
|
|
19
|
+
│
|
|
20
|
+
├─► MutableAtom<T> = Atom<T> & { set(), reset() }
|
|
21
|
+
│ Created by: atom(value)
|
|
22
|
+
│
|
|
23
|
+
└─► DerivedAtom<T, F> = Atom<Promise<T>> & { refresh(), state(), staleValue }
|
|
24
|
+
│
|
|
25
|
+
├─► DerivedAtom<T, false> (staleValue: T | undefined)
|
|
26
|
+
│ Created by: derived(compute)
|
|
27
|
+
│
|
|
28
|
+
└─► DerivedAtom<T, true> (staleValue: T, guaranteed)
|
|
29
|
+
Created by: derived(compute, { fallback })
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Base Interface
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
interface AtomMeta {
|
|
38
|
+
key: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Atom<T> {
|
|
42
|
+
readonly value: T;
|
|
43
|
+
readonly meta?: AtomMeta;
|
|
44
|
+
on(listener: () => void): () => void;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## MutableAtom
|
|
51
|
+
|
|
52
|
+
Simple, raw value container. Can store anything including Promises.
|
|
53
|
+
|
|
54
|
+
### API
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
function atom<T>(initialValue: T, options?: AtomOptions): MutableAtom<T>;
|
|
58
|
+
|
|
59
|
+
interface AtomOptions<T> {
|
|
60
|
+
meta?: AtomMeta;
|
|
61
|
+
equals?: EqualsFn<T>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface MutableAtom<T> extends Atom<T> {
|
|
65
|
+
set(value: T | ((prev: T) => T)): void; // throws sync errors
|
|
66
|
+
reset(): void; // restore initial value
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Behavior
|
|
71
|
+
|
|
72
|
+
| Operation | Behavior |
|
|
73
|
+
| --------------- | ----------------------------------------- |
|
|
74
|
+
| `.value` | Returns raw value (T, including Promise) |
|
|
75
|
+
| `.set(value)` | Stores value, notifies |
|
|
76
|
+
| `.set(reducer)` | Applies reducer, throws if reducer throws |
|
|
77
|
+
| `.reset()` | Restores initial value |
|
|
78
|
+
|
|
79
|
+
### Examples
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Sync
|
|
83
|
+
const count$ = atom(0);
|
|
84
|
+
count$.value; // 0
|
|
85
|
+
count$.set(1); // stores 1
|
|
86
|
+
count$.set((n) => n + 1); // stores 2
|
|
87
|
+
|
|
88
|
+
// Async (stores raw Promise)
|
|
89
|
+
const posts$ = atom(fetchPosts());
|
|
90
|
+
posts$.value; // Promise<Post[]>
|
|
91
|
+
|
|
92
|
+
// Refetch - must call set() with new Promise
|
|
93
|
+
posts$.set(fetchPosts()); // stores new Promise
|
|
94
|
+
|
|
95
|
+
// Reset - restores initial Promise (does NOT refetch)
|
|
96
|
+
posts$.reset(); // restores original Promise object
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## DerivedAtom
|
|
102
|
+
|
|
103
|
+
Computed value. Always returns `Promise<T>` for `.value`.
|
|
104
|
+
|
|
105
|
+
### API
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Without fallback - staleValue is T | undefined
|
|
109
|
+
function derived<T>(
|
|
110
|
+
compute: (ctx: SelectContext) => T,
|
|
111
|
+
options?: DerivedOptions,
|
|
112
|
+
): DerivedAtom<T, false>;
|
|
113
|
+
|
|
114
|
+
// With fallback - staleValue is guaranteed T
|
|
115
|
+
function derived<T>(
|
|
116
|
+
compute: (ctx: SelectContext) => T,
|
|
117
|
+
options: DerivedOptions & { fallback: T },
|
|
118
|
+
): DerivedAtom<T, true>;
|
|
119
|
+
|
|
120
|
+
interface DerivedOptions<T> {
|
|
121
|
+
meta?: AtomMeta;
|
|
122
|
+
equals?: EqualsFn<T>;
|
|
123
|
+
fallback?: T; // When provided, staleValue is guaranteed T
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface DerivedAtom<T, F extends boolean = false> extends Atom<Promise<T>> {
|
|
127
|
+
refresh(): void; // re-run computation
|
|
128
|
+
state(): AtomState<T>; // get current state (ready/error/loading)
|
|
129
|
+
readonly staleValue: F extends true ? T : T | undefined; // fallback or last resolved value
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### SelectContext
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
type AnyAtom<T> = MutableAtom<T> | DerivedAtom<T>;
|
|
137
|
+
type AtomValue<A> = A extends AnyAtom<infer V> ? Awaited<V> : never;
|
|
138
|
+
|
|
139
|
+
type SettledResult<T> =
|
|
140
|
+
| { status: "ready"; value: T }
|
|
141
|
+
| { status: "error"; error: unknown };
|
|
142
|
+
|
|
143
|
+
interface SelectContext {
|
|
144
|
+
// Single atom
|
|
145
|
+
get<V>(atom: AnyAtom<V>): Awaited<V>;
|
|
146
|
+
|
|
147
|
+
// Multiple atoms - parallel
|
|
148
|
+
all<A extends AnyAtom<unknown>[]>(
|
|
149
|
+
...atoms: A
|
|
150
|
+
): { [K in keyof A]: AtomValue<A[K]> };
|
|
151
|
+
race<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
|
|
152
|
+
any<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]>;
|
|
153
|
+
settled<A extends AnyAtom<unknown>[]>(
|
|
154
|
+
...atoms: A
|
|
155
|
+
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### getAtomState()
|
|
160
|
+
|
|
161
|
+
Returns the current state of an atom as a discriminated union.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
type AtomState<T> =
|
|
165
|
+
| { status: "ready"; value: T }
|
|
166
|
+
| { status: "error"; error: unknown }
|
|
167
|
+
| { status: "loading"; promise: Promise<T> };
|
|
168
|
+
|
|
169
|
+
function getAtomState<T>(atom: Atom<T>): AtomState<T> {
|
|
170
|
+
// For derived atoms, use their own state method
|
|
171
|
+
if (isDerived(atom)) {
|
|
172
|
+
return atom.state();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const value = atom.value;
|
|
176
|
+
|
|
177
|
+
// 1. Sync value - ready
|
|
178
|
+
if (!isPromiseLike(value)) {
|
|
179
|
+
return {
|
|
180
|
+
status: "ready",
|
|
181
|
+
value: value,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Promise value - check state via promiseCache
|
|
186
|
+
const state = trackPromise(value);
|
|
187
|
+
|
|
188
|
+
switch (state.status) {
|
|
189
|
+
case "fulfilled":
|
|
190
|
+
return {
|
|
191
|
+
status: "ready",
|
|
192
|
+
value: state.value,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
case "rejected":
|
|
196
|
+
return {
|
|
197
|
+
status: "error",
|
|
198
|
+
error: state.error,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
case "pending":
|
|
202
|
+
return {
|
|
203
|
+
status: "loading",
|
|
204
|
+
promise: state.promise,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Usage:**
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const state = getAtomState(myAtom$);
|
|
214
|
+
|
|
215
|
+
switch (state.status) {
|
|
216
|
+
case "ready":
|
|
217
|
+
console.log(state.value); // T
|
|
218
|
+
break;
|
|
219
|
+
case "error":
|
|
220
|
+
console.log(state.error); // unknown
|
|
221
|
+
break;
|
|
222
|
+
case "loading":
|
|
223
|
+
console.log(state.promise); // Promise<T>
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// For derived atoms with fallback, use staleValue during loading:
|
|
228
|
+
if (state.status === "loading") {
|
|
229
|
+
console.log("Using stale/fallback:", derived$.staleValue);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### get() Behavior
|
|
234
|
+
|
|
235
|
+
The `get()` function resolves atom values using `getAtomState()`:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
function get<T>(atom: AnyAtom<T>): Awaited<T> {
|
|
239
|
+
const state = getAtomState(atom);
|
|
240
|
+
|
|
241
|
+
switch (state.status) {
|
|
242
|
+
case "ready":
|
|
243
|
+
return state.value;
|
|
244
|
+
case "error":
|
|
245
|
+
throw state.error;
|
|
246
|
+
case "loading":
|
|
247
|
+
throw state.promise; // Suspense pattern
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Summary:**
|
|
253
|
+
|
|
254
|
+
| State | Result |
|
|
255
|
+
| ------- | ------------------------ |
|
|
256
|
+
| ready | return value |
|
|
257
|
+
| error | throw error |
|
|
258
|
+
| loading | throw Promise (Suspense) |
|
|
259
|
+
|
|
260
|
+
> **Note:** For derived atoms with fallback, use `staleValue` directly to access
|
|
261
|
+
> the fallback/cached value during loading without triggering Suspense.
|
|
262
|
+
|
|
263
|
+
### all() Behavior
|
|
264
|
+
|
|
265
|
+
Like `Promise.all` - waits for all atoms, returns array of values.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
function all<A extends AnyAtom<unknown>[]>(...atoms: A): AwaitedAll<A> {
|
|
269
|
+
const results: unknown[] = [];
|
|
270
|
+
let loadingPromise: Promise<unknown> | null = null;
|
|
271
|
+
|
|
272
|
+
for (const atom of atoms) {
|
|
273
|
+
const state = getAtomState(atom);
|
|
274
|
+
|
|
275
|
+
switch (state.status) {
|
|
276
|
+
case "ready":
|
|
277
|
+
results.push(state.value);
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case "error":
|
|
281
|
+
// Any error → throw immediately
|
|
282
|
+
throw state.error;
|
|
283
|
+
|
|
284
|
+
case "loading":
|
|
285
|
+
// First loading → will throw
|
|
286
|
+
if (!loadingPromise) {
|
|
287
|
+
loadingPromise = state.promise;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If any loading → throw Promise
|
|
294
|
+
if (loadingPromise) {
|
|
295
|
+
throw loadingPromise;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return results as AwaitedAll<A>;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Scenario | Result |
|
|
303
|
+
| ----------- | ------------------------------ |
|
|
304
|
+
| All ready | return `[value1, value2, ...]` |
|
|
305
|
+
| Any error | throw first error |
|
|
306
|
+
| Any loading | throw Promise |
|
|
307
|
+
|
|
308
|
+
### race() Behavior
|
|
309
|
+
|
|
310
|
+
Like `Promise.race` - returns first settled value (ready or error).
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
function race<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]> {
|
|
314
|
+
let firstLoadingPromise: Promise<unknown> | null = null;
|
|
315
|
+
|
|
316
|
+
for (const atom of atoms) {
|
|
317
|
+
const state = getAtomState(atom);
|
|
318
|
+
|
|
319
|
+
switch (state.status) {
|
|
320
|
+
case "ready":
|
|
321
|
+
return state.value as AtomValue<A[number]>;
|
|
322
|
+
|
|
323
|
+
case "error":
|
|
324
|
+
throw state.error;
|
|
325
|
+
|
|
326
|
+
case "loading":
|
|
327
|
+
if (!firstLoadingPromise) {
|
|
328
|
+
firstLoadingPromise = state.promise;
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// All loading → throw first Promise
|
|
335
|
+
if (firstLoadingPromise) {
|
|
336
|
+
throw firstLoadingPromise;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
throw new Error("race() called with no atoms");
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
| Scenario | Result |
|
|
344
|
+
| -------------- | ----------------------- |
|
|
345
|
+
| Any sync value | return first sync value |
|
|
346
|
+
| Any ready | return first value |
|
|
347
|
+
| Any error | throw first error |
|
|
348
|
+
| All loading | throw first Promise |
|
|
349
|
+
|
|
350
|
+
> **Note:** `race()` does NOT use fallback - it's meant to return the first "real" settled value.
|
|
351
|
+
|
|
352
|
+
### any() Behavior
|
|
353
|
+
|
|
354
|
+
Like `Promise.any` - returns first ready, ignores errors unless all error.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
function any<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]> {
|
|
358
|
+
const errors: unknown[] = [];
|
|
359
|
+
let firstLoadingPromise: Promise<unknown> | null = null;
|
|
360
|
+
|
|
361
|
+
for (const atom of atoms) {
|
|
362
|
+
const state = getAtomState(atom);
|
|
363
|
+
|
|
364
|
+
switch (state.status) {
|
|
365
|
+
case "ready":
|
|
366
|
+
return state.value as AtomValue<A[number]>;
|
|
367
|
+
|
|
368
|
+
case "error":
|
|
369
|
+
errors.push(state.error);
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case "loading":
|
|
373
|
+
if (!firstLoadingPromise) {
|
|
374
|
+
firstLoadingPromise = state.promise;
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// If any loading → throw Promise (might still succeed)
|
|
381
|
+
if (firstLoadingPromise) {
|
|
382
|
+
throw firstLoadingPromise;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// All errored → throw AggregateError
|
|
386
|
+
throw new AggregateError(errors, "All atoms rejected");
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Scenario | Result |
|
|
391
|
+
| -------------- | ----------------------------------- |
|
|
392
|
+
| Any sync value | return first sync value |
|
|
393
|
+
| Any ready | return first value |
|
|
394
|
+
| Any loading | throw Promise (wait for potential) |
|
|
395
|
+
| All errored | throw `AggregateError` |
|
|
396
|
+
|
|
397
|
+
> **Note:** `any()` does NOT use fallback - it waits for a real ready value.
|
|
398
|
+
|
|
399
|
+
### settled() Behavior
|
|
400
|
+
|
|
401
|
+
Like `Promise.allSettled` - returns status of all atoms.
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
function settled<A extends AnyAtom<unknown>[]>(
|
|
405
|
+
...atoms: A
|
|
406
|
+
): { [K in keyof A]: SettledResult<AtomValue<A[K]>> } {
|
|
407
|
+
const results: SettledResult<unknown>[] = [];
|
|
408
|
+
let loadingPromise: Promise<unknown> | null = null;
|
|
409
|
+
|
|
410
|
+
for (const atom of atoms) {
|
|
411
|
+
const state = getAtomState(atom);
|
|
412
|
+
|
|
413
|
+
switch (state.status) {
|
|
414
|
+
case "ready":
|
|
415
|
+
results.push({ status: "ready", value: state.value });
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case "error":
|
|
419
|
+
results.push({ status: "error", error: state.error });
|
|
420
|
+
break;
|
|
421
|
+
|
|
422
|
+
case "loading":
|
|
423
|
+
// Loading → will throw
|
|
424
|
+
if (!loadingPromise) {
|
|
425
|
+
loadingPromise = state.promise;
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// If any loading → throw Promise
|
|
432
|
+
if (loadingPromise) {
|
|
433
|
+
throw loadingPromise;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return results as { [K in keyof A]: SettledResult<AtomValue<A[K]>> };
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
| Scenario | Result |
|
|
441
|
+
| ----------- | ---------------------------------------- |
|
|
442
|
+
| All settled | return `[{ status, value/error }, ...]` |
|
|
443
|
+
| Any loading | throw Promise |
|
|
444
|
+
|
|
445
|
+
### SelectContext Summary
|
|
446
|
+
|
|
447
|
+
All methods use `getAtomState()` internally.
|
|
448
|
+
|
|
449
|
+
| Method | Returns | Throws on Error? |
|
|
450
|
+
| ------------------- | --------------- | ------------------ |
|
|
451
|
+
| `get(atom)` | Single value | Yes |
|
|
452
|
+
| `all(...atoms)` | Array of values | Yes (first error) |
|
|
453
|
+
| `race(...atoms)` | First settled | Yes (if first) |
|
|
454
|
+
| `any(...atoms)` | First ready | Only if all error |
|
|
455
|
+
| `settled(...atoms)` | Array of status | No |
|
|
456
|
+
|
|
457
|
+
> **Note:** `race()` and `any()` are typically used with `MutableAtom<Promise<T>>` for racing data sources.
|
|
458
|
+
|
|
459
|
+
### Behavior
|
|
460
|
+
|
|
461
|
+
**Without fallback (`DerivedAtom<T, false>`):**
|
|
462
|
+
|
|
463
|
+
| State | `.value` | `.staleValue` | `.state().status` |
|
|
464
|
+
| -------- | -------------------- | ------------------------ | ----------------- |
|
|
465
|
+
| Loading | `Promise` (pending) | `undefined` | `"loading"` |
|
|
466
|
+
| Resolved | `Promise` (resolved) | resolved value | `"ready"` |
|
|
467
|
+
| Error | `Promise` (rejected) | last value / `undefined` | `"error"` |
|
|
468
|
+
|
|
469
|
+
**With fallback (`DerivedAtom<T, true>`):**
|
|
470
|
+
|
|
471
|
+
| State | `.value` | `.staleValue` | `.state().status` |
|
|
472
|
+
| -------- | -------------------- | ------------------- | ----------------- |
|
|
473
|
+
| Loading | `Promise` (pending) | fallback | `"loading"` |
|
|
474
|
+
| Resolved | `Promise` (resolved) | resolved value | `"ready"` |
|
|
475
|
+
| Error | `Promise` (rejected) | last value/fallback | `"error"` |
|
|
476
|
+
|
|
477
|
+
> **Note:** Use `state().status === "loading"` or `isPending(derived$.value)` to check loading state.
|
|
478
|
+
|
|
479
|
+
### Examples
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
// Basic derived (no fallback) - DerivedAtom<number, false>
|
|
483
|
+
const count$ = atom(0);
|
|
484
|
+
const double$ = derived(({ get }) => get(count$) * 2);
|
|
485
|
+
await double$.value; // 0
|
|
486
|
+
double$.staleValue; // undefined (during loading) → 0 (after)
|
|
487
|
+
double$.state(); // { status: "ready", value: 0 }
|
|
488
|
+
|
|
489
|
+
// Async derived (no fallback) - DerivedAtom<number, false>
|
|
490
|
+
const posts$ = atom(fetchPosts());
|
|
491
|
+
const postCount$ = derived(({ get }) => get(posts$).length);
|
|
492
|
+
await postCount$.value; // 42 (after loading)
|
|
493
|
+
postCount$.staleValue; // undefined (during loading) → 42 (after)
|
|
494
|
+
postCount$.state(); // { status: "loading", promise } → { status: "ready", value: 42 }
|
|
495
|
+
|
|
496
|
+
// With fallback - DerivedAtom<number, true>
|
|
497
|
+
const postCountWithFallback$ = derived(({ get }) => get(posts$).length, {
|
|
498
|
+
fallback: 0,
|
|
499
|
+
});
|
|
500
|
+
await postCountWithFallback$.value; // 42
|
|
501
|
+
postCountWithFallback$.staleValue; // 0 (during loading, guaranteed) → 42 (after)
|
|
502
|
+
postCountWithFallback$.state(); // { status: "loading", promise } → { status: "ready", value: 42 }
|
|
503
|
+
|
|
504
|
+
// Check loading state
|
|
505
|
+
postCount$.state().status === "loading"; // true while loading
|
|
506
|
+
isPending(postCount$.value); // alternative: true while loading
|
|
507
|
+
|
|
508
|
+
// Multiple deps
|
|
509
|
+
const combined$ = derived(({ get, all }) => {
|
|
510
|
+
const [posts, user] = all(posts$, user$);
|
|
511
|
+
return posts.filter((p) => p.authorId === user.id);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Refresh - re-run computation
|
|
515
|
+
postCount$.refresh();
|
|
516
|
+
|
|
517
|
+
// Error handling
|
|
518
|
+
try {
|
|
519
|
+
const count = await postCount$.value;
|
|
520
|
+
} catch (e) {
|
|
521
|
+
console.error("Failed:", e);
|
|
522
|
+
}
|
|
523
|
+
// Or check state
|
|
524
|
+
const state = postCount$.state();
|
|
525
|
+
if (state.status === "error") {
|
|
526
|
+
console.error("Failed:", state.error);
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Effect
|
|
533
|
+
|
|
534
|
+
Side effect runner with same get() semantics.
|
|
535
|
+
|
|
536
|
+
### API
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
interface EffectContext extends SelectContext {
|
|
540
|
+
onCleanup: (cleanup: VoidFunction) => void;
|
|
541
|
+
onError: (handler: (error: unknown) => void) => void;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function effect(
|
|
545
|
+
fn: (ctx: EffectContext) => void,
|
|
546
|
+
options?: EffectOptions,
|
|
547
|
+
): () => void;
|
|
548
|
+
|
|
549
|
+
interface EffectOptions {
|
|
550
|
+
key?: string;
|
|
551
|
+
onError?: (error: Error) => void; // For unhandled errors
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Behavior
|
|
556
|
+
|
|
557
|
+
- Runs when dependencies change
|
|
558
|
+
- `get()` works same as in derived (throws Promise/Error)
|
|
559
|
+
- If throws Promise → re-runs when Promise resolves
|
|
560
|
+
- If throws Error → calls registered `onError` handlers, or `options.onError` if none registered
|
|
561
|
+
- Use `onCleanup()` to register cleanup functions
|
|
562
|
+
|
|
563
|
+
### Example
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// With cleanup
|
|
567
|
+
const dispose = effect(({ get, onCleanup }) => {
|
|
568
|
+
const interval = get(intervalAtom);
|
|
569
|
+
const id = setInterval(() => console.log("tick"), interval);
|
|
570
|
+
onCleanup(() => clearInterval(id));
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// With error handling (callback-based)
|
|
574
|
+
const dispose = effect(({ get, onError }) => {
|
|
575
|
+
onError((e) => console.error("Effect failed:", e));
|
|
576
|
+
const posts = get(posts$);
|
|
577
|
+
console.log("Posts loaded:", posts.length);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// With error handling (option-based for unhandled errors)
|
|
581
|
+
const dispose = effect(
|
|
582
|
+
({ get }) => {
|
|
583
|
+
const posts = get(posts$);
|
|
584
|
+
riskyOperation(posts);
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
onError: (e) => console.error("Unhandled error:", e),
|
|
588
|
+
},
|
|
589
|
+
);
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## React Hooks
|
|
595
|
+
|
|
596
|
+
### useValue
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
function useValue<T>(atom: Atom<T>): Awaited<T>;
|
|
600
|
+
function useValue<T>(
|
|
601
|
+
selector: (ctx: SelectContext) => T,
|
|
602
|
+
equals?: Equality<Awaited<T>>,
|
|
603
|
+
): Awaited<T>;
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Example
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
// With DerivedAtom (Suspense handles loading)
|
|
610
|
+
function PostCount() {
|
|
611
|
+
const count = useValue(postCount$); // number (awaited)
|
|
612
|
+
return <div>Count: {count}</div>;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Usage with Suspense + ErrorBoundary
|
|
616
|
+
<ErrorBoundary fallback={<Error />}>
|
|
617
|
+
<Suspense fallback={<Loading />}>
|
|
618
|
+
<PostCount />
|
|
619
|
+
</Suspense>
|
|
620
|
+
</ErrorBoundary>
|
|
621
|
+
|
|
622
|
+
// With fallback - staleValue is guaranteed T
|
|
623
|
+
function PostCountWithFallback() {
|
|
624
|
+
const count = postCountWithFallback$.staleValue; // always number
|
|
625
|
+
const isLoading = isPending(postCountWithFallback$.value);
|
|
626
|
+
|
|
627
|
+
return (
|
|
628
|
+
<div>
|
|
629
|
+
{isLoading && <Spinner />}
|
|
630
|
+
Count: {count}
|
|
631
|
+
</div>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Without fallback - staleValue is T | undefined
|
|
636
|
+
function PostCountNoFallback() {
|
|
637
|
+
const count = postCount$.staleValue; // number | undefined
|
|
638
|
+
const isLoading = isPending(postCount$.value);
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<div>
|
|
642
|
+
{isLoading && <Spinner />}
|
|
643
|
+
Count: {count ?? "Loading..."}
|
|
644
|
+
</div>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Promise State Cache
|
|
652
|
+
|
|
653
|
+
Internal mechanism for tracking Promise states.
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
// Internal - not exposed
|
|
657
|
+
type PromiseState<T> =
|
|
658
|
+
| { status: "pending"; promise: Promise<T> }
|
|
659
|
+
| { status: "fulfilled"; value: T }
|
|
660
|
+
| { status: "rejected"; error: unknown };
|
|
661
|
+
|
|
662
|
+
const promiseCache = new WeakMap<Promise<any>, PromiseState<any>>();
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Type Utilities
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
type Awaited<T> = T extends Promise<infer U> ? U : T;
|
|
671
|
+
|
|
672
|
+
type AwaitedAll<T extends unknown[]> = {
|
|
673
|
+
[K in keyof T]: Awaited<T[K]>;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
type EqualsFn<T> = (a: T, b: T) => boolean;
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Complete API Surface
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
// Core
|
|
685
|
+
atom<T>(initial, options?): MutableAtom<T>
|
|
686
|
+
derived<T>(compute, options?): DerivedAtom<T, false>
|
|
687
|
+
derived<T>(compute, { fallback }): DerivedAtom<T, true>
|
|
688
|
+
effect(fn, options?): () => void
|
|
689
|
+
|
|
690
|
+
// React
|
|
691
|
+
useValue(source): T | Awaited<T>
|
|
692
|
+
|
|
693
|
+
// Types
|
|
694
|
+
Atom<T>
|
|
695
|
+
MutableAtom<T>
|
|
696
|
+
DerivedAtom<T, F> // F = false (no fallback) | true (with fallback)
|
|
697
|
+
SelectContext
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Summary Table
|
|
703
|
+
|
|
704
|
+
| | MutableAtom | DerivedAtom | DerivedAtom with fallback |
|
|
705
|
+
| ---------------- | ----------- | ------------------- | ------------------------- |
|
|
706
|
+
| Purpose | Raw storage | Computed (async) | Computed + sync access |
|
|
707
|
+
| `.value` | `T` (raw) | `Promise<T>` | `Promise<T>` |
|
|
708
|
+
| `.staleValue` | ❌ | ✅ `T \| undefined` | ✅ `T` |
|
|
709
|
+
| `.state()` | ❌ | ✅ `AtomState<T>` | ✅ `AtomState<T>` |
|
|
710
|
+
| `.set()` | ✅ | ❌ | ❌ |
|
|
711
|
+
| `.reset()` | ✅ | ❌ | ❌ |
|
|
712
|
+
| `.refresh()` | ❌ | ✅ | ✅ |
|
|
713
|
+
| `.on()` | ✅ | ✅ | ✅ |
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
## Migration from v1
|
|
718
|
+
|
|
719
|
+
| v1 | v2 |
|
|
720
|
+
| ------------------------------ | --------------------------------------------- |
|
|
721
|
+
| `atom(promise, { fallback })` | `atom(promise)` (no fallback on atom) |
|
|
722
|
+
| `atom.loading` | `isPending(derived$.value)` from promiseCache |
|
|
723
|
+
| `atom.error` | `await derived.value` throws |
|
|
724
|
+
| `atom.stale()` | `isPending(derived$.value)` from promiseCache |
|
|
725
|
+
| `useValue(atom)` with Suspense | `useValue(derived)` with Suspense |
|