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
package/src/core/derived.ts
DELETED
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
import { CreateInfo, DerivedInfo, onCreateHook } from "./onCreateHook";
|
|
2
|
-
import { emitter } from "./emitter";
|
|
3
|
-
import { resolveEquality } from "./equality";
|
|
4
|
-
import { onErrorHook } from "./onErrorHook";
|
|
5
|
-
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
6
|
-
import { ReactiveSelector, select, SelectContext } from "./select";
|
|
7
|
-
import {
|
|
8
|
-
Atom,
|
|
9
|
-
AtomState,
|
|
10
|
-
DerivedAtom,
|
|
11
|
-
DerivedOptions,
|
|
12
|
-
Equality,
|
|
13
|
-
SYMBOL_ATOM,
|
|
14
|
-
SYMBOL_DERIVED,
|
|
15
|
-
} from "./types";
|
|
16
|
-
import { withReady, WithReadySelectContext } from "./withReady";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Internal options for derived atoms.
|
|
20
|
-
* These are not part of the public API.
|
|
21
|
-
* @internal
|
|
22
|
-
*/
|
|
23
|
-
export interface DerivedInternalOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Override the error source for onErrorHook.
|
|
26
|
-
* Used by effect() to attribute errors to the effect instead of the internal derived.
|
|
27
|
-
*/
|
|
28
|
-
_errorSource?: CreateInfo;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Context object passed to derived atom selector functions.
|
|
33
|
-
* Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
|
|
34
|
-
*
|
|
35
|
-
* Currently identical to `SelectContext`, but defined separately to allow
|
|
36
|
-
* future derived-specific extensions without breaking changes.
|
|
37
|
-
*/
|
|
38
|
-
export interface DerivedContext extends SelectContext, WithReadySelectContext {}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Creates a derived (computed) atom from source atom(s).
|
|
42
|
-
*
|
|
43
|
-
* Derived atoms are **read-only** and automatically recompute when their
|
|
44
|
-
* source atoms change. The `.get()` method always returns a `Promise<T>`,
|
|
45
|
-
* even for synchronous computations.
|
|
46
|
-
*
|
|
47
|
-
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
48
|
-
*
|
|
49
|
-
* **The selector function MUST NOT be async or return a Promise.**
|
|
50
|
-
*
|
|
51
|
-
* ```ts
|
|
52
|
-
* // ❌ WRONG - Don't use async function
|
|
53
|
-
* derived(async ({ read }) => {
|
|
54
|
-
* const data = await fetch('/api');
|
|
55
|
-
* return data;
|
|
56
|
-
* });
|
|
57
|
-
*
|
|
58
|
-
* // ❌ WRONG - Don't return a Promise
|
|
59
|
-
* derived(({ read }) => fetch('/api').then(r => r.json()));
|
|
60
|
-
*
|
|
61
|
-
* // ✅ CORRECT - Create async atom and read with read()
|
|
62
|
-
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
63
|
-
* derived(({ read }) => read(data$)); // Suspends until resolved
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
|
-
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
67
|
-
*
|
|
68
|
-
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
69
|
-
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
70
|
-
* these Promises and break the Suspense mechanism.
|
|
71
|
-
*
|
|
72
|
-
* ```ts
|
|
73
|
-
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
74
|
-
* derived(({ read }) => {
|
|
75
|
-
* try {
|
|
76
|
-
* return read(asyncAtom$);
|
|
77
|
-
* } catch (e) {
|
|
78
|
-
* return 'fallback'; // This catches BOTH errors AND loading promises!
|
|
79
|
-
* }
|
|
80
|
-
* });
|
|
81
|
-
*
|
|
82
|
-
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
83
|
-
* derived(({ read, safe }) => {
|
|
84
|
-
* const [err, data] = safe(() => {
|
|
85
|
-
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
86
|
-
* return JSON.parse(raw); // Can throw Error
|
|
87
|
-
* });
|
|
88
|
-
*
|
|
89
|
-
* if (err) return { error: err.message };
|
|
90
|
-
* return { data };
|
|
91
|
-
* });
|
|
92
|
-
* ```
|
|
93
|
-
*
|
|
94
|
-
* The `safe()` utility:
|
|
95
|
-
* - **Catches errors** and returns `[error, undefined]`
|
|
96
|
-
* - **Re-throws Promises** to preserve Suspense behavior
|
|
97
|
-
* - Returns `[undefined, result]` on success
|
|
98
|
-
*
|
|
99
|
-
* ## Key Features
|
|
100
|
-
*
|
|
101
|
-
* 1. **Always async**: `.get()` returns `Promise<T>`
|
|
102
|
-
* 2. **Lazy computation**: Value is computed on first access
|
|
103
|
-
* 3. **Automatic updates**: Recomputes when any source atom changes
|
|
104
|
-
* 4. **Equality checking**: Only notifies if derived value changed
|
|
105
|
-
* 5. **Fallback support**: Optional fallback for loading/error states
|
|
106
|
-
* 6. **Suspense-like async**: `read()` throws promise if loading
|
|
107
|
-
* 7. **Conditional dependencies**: Only subscribes to atoms accessed
|
|
108
|
-
*
|
|
109
|
-
* ## Suspense-Style read()
|
|
110
|
-
*
|
|
111
|
-
* The `read()` function behaves like React Suspense:
|
|
112
|
-
* - If source atom is **loading**: `read()` throws the promise
|
|
113
|
-
* - If source atom has **error**: `read()` throws the error
|
|
114
|
-
* - If source atom has **value**: `read()` returns the value
|
|
115
|
-
*
|
|
116
|
-
* @template T - Derived value type
|
|
117
|
-
* @template F - Whether fallback is provided
|
|
118
|
-
* @param fn - Context-based derivation function (must return sync value, not Promise)
|
|
119
|
-
* @param options - Optional configuration (meta, equals, fallback)
|
|
120
|
-
* @returns A read-only derived atom
|
|
121
|
-
* @throws Error if selector returns a Promise or PromiseLike
|
|
122
|
-
*
|
|
123
|
-
* @example Basic derived (no fallback)
|
|
124
|
-
* ```ts
|
|
125
|
-
* const count$ = atom(5);
|
|
126
|
-
* const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
127
|
-
*
|
|
128
|
-
* await doubled$.get(); // 10
|
|
129
|
-
* doubled$.staleValue; // undefined (until first resolve) -> 10
|
|
130
|
-
* doubled$.state(); // { status: "ready", value: 10 }
|
|
131
|
-
* ```
|
|
132
|
-
*
|
|
133
|
-
* @example With fallback
|
|
134
|
-
* ```ts
|
|
135
|
-
* const posts$ = atom(fetchPosts());
|
|
136
|
-
* const count$ = derived(({ read }) => read(posts$).length, { fallback: 0 });
|
|
137
|
-
*
|
|
138
|
-
* count$.staleValue; // 0 (during loading) -> 42 (after resolve)
|
|
139
|
-
* count$.state(); // { status: "loading", promise } during loading
|
|
140
|
-
* // { status: "ready", value: 42 } after resolve
|
|
141
|
-
* ```
|
|
142
|
-
*
|
|
143
|
-
* @example Async dependencies
|
|
144
|
-
* ```ts
|
|
145
|
-
* const user$ = atom(fetchUser());
|
|
146
|
-
* const posts$ = atom(fetchPosts());
|
|
147
|
-
*
|
|
148
|
-
* const dashboard$ = derived(({ all }) => {
|
|
149
|
-
* const [user, posts] = all(user$, posts$);
|
|
150
|
-
* return { user, posts };
|
|
151
|
-
* });
|
|
152
|
-
* ```
|
|
153
|
-
*
|
|
154
|
-
* @example Refresh
|
|
155
|
-
* ```ts
|
|
156
|
-
* const data$ = derived(({ read }) => read(source$));
|
|
157
|
-
* data$.refresh(); // Re-run computation
|
|
158
|
-
* ```
|
|
159
|
-
*/
|
|
160
|
-
|
|
161
|
-
// Overload: Without fallback - staleValue is T | undefined
|
|
162
|
-
export function derived<T>(
|
|
163
|
-
fn: ReactiveSelector<T, DerivedContext>,
|
|
164
|
-
options?: DerivedOptions<T> & DerivedInternalOptions
|
|
165
|
-
): DerivedAtom<T, false>;
|
|
166
|
-
|
|
167
|
-
// Overload: With fallback - staleValue is guaranteed T
|
|
168
|
-
export function derived<T>(
|
|
169
|
-
fn: ReactiveSelector<T, DerivedContext>,
|
|
170
|
-
options: DerivedOptions<T> & { fallback: T } & DerivedInternalOptions
|
|
171
|
-
): DerivedAtom<T, true>;
|
|
172
|
-
|
|
173
|
-
// Implementation
|
|
174
|
-
export function derived<T>(
|
|
175
|
-
fn: ReactiveSelector<T, DerivedContext>,
|
|
176
|
-
options: DerivedOptions<T> & { fallback?: T } & DerivedInternalOptions = {}
|
|
177
|
-
): DerivedAtom<T, boolean> {
|
|
178
|
-
const changeEmitter = emitter();
|
|
179
|
-
const eq = resolveEquality(options.equals as Equality<unknown>);
|
|
180
|
-
|
|
181
|
-
// Fallback configuration
|
|
182
|
-
const hasFallback = "fallback" in options;
|
|
183
|
-
const fallbackValue = options.fallback as T;
|
|
184
|
-
|
|
185
|
-
// State
|
|
186
|
-
let lastResolved: { value: T } | undefined;
|
|
187
|
-
let lastError: unknown = undefined;
|
|
188
|
-
let currentPromise: Promise<T> | null = null;
|
|
189
|
-
let isInitialized = false;
|
|
190
|
-
let isLoading = false;
|
|
191
|
-
let version = 0;
|
|
192
|
-
|
|
193
|
-
// Store resolve/reject to allow reusing the same promise across recomputations
|
|
194
|
-
let resolvePromise: ((value: T) => void) | null = null;
|
|
195
|
-
let rejectPromise: ((error: unknown) => void) | null = null;
|
|
196
|
-
|
|
197
|
-
// Track current subscriptions (atom -> unsubscribe function)
|
|
198
|
-
const subscriptions = new Map<Atom<unknown>, VoidFunction>();
|
|
199
|
-
|
|
200
|
-
// CreateInfo for this derived - stored for onErrorHook
|
|
201
|
-
// Will be set after derivedAtom is created
|
|
202
|
-
let createInfo: DerivedInfo;
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Handles errors by calling both the user's onError callback and the global onErrorHook.
|
|
206
|
-
*/
|
|
207
|
-
const handleError = (error: unknown) => {
|
|
208
|
-
// Invoke user's error callback if provided
|
|
209
|
-
options.onError?.(error);
|
|
210
|
-
|
|
211
|
-
// Invoke global error hook
|
|
212
|
-
// Use _errorSource if provided (for effect), otherwise use this derived's createInfo
|
|
213
|
-
const source = options._errorSource ?? createInfo;
|
|
214
|
-
onErrorHook.current?.({ source, error });
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Schedules notification to all subscribers.
|
|
219
|
-
*/
|
|
220
|
-
const notify = () => {
|
|
221
|
-
changeEmitter.forEach((listener) => {
|
|
222
|
-
scheduleNotifyHook.current(listener);
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Updates subscriptions based on new dependencies.
|
|
228
|
-
*/
|
|
229
|
-
const updateSubscriptions = (newDeps: Set<Atom<unknown>>) => {
|
|
230
|
-
// Unsubscribe from atoms that are no longer accessed
|
|
231
|
-
for (const [atom, unsubscribe] of subscriptions) {
|
|
232
|
-
if (!newDeps.has(atom)) {
|
|
233
|
-
unsubscribe();
|
|
234
|
-
subscriptions.delete(atom);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Subscribe to newly accessed atoms
|
|
239
|
-
for (const atom of newDeps) {
|
|
240
|
-
if (!subscriptions.has(atom)) {
|
|
241
|
-
const unsubscribe = atom.on(() => {
|
|
242
|
-
compute();
|
|
243
|
-
});
|
|
244
|
-
subscriptions.set(atom, unsubscribe);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Computes the derived value.
|
|
251
|
-
* Reuses the existing Promise if loading (to prevent orphaned promises
|
|
252
|
-
* that React Suspense might be waiting on).
|
|
253
|
-
*/
|
|
254
|
-
const compute = (silent = false) => {
|
|
255
|
-
const computeVersion = ++version;
|
|
256
|
-
isLoading = true;
|
|
257
|
-
lastError = undefined; // Clear error when starting new computation
|
|
258
|
-
|
|
259
|
-
// Create a new promise if:
|
|
260
|
-
// 1. We don't have one yet, OR
|
|
261
|
-
// 2. The previous computation completed (resolved/rejected) and we need a new one
|
|
262
|
-
// This ensures we reuse promises while loading (for Suspense) but create fresh
|
|
263
|
-
// promises for new computations after completion
|
|
264
|
-
if (!resolvePromise) {
|
|
265
|
-
currentPromise = new Promise<T>((resolve, reject) => {
|
|
266
|
-
resolvePromise = resolve;
|
|
267
|
-
rejectPromise = reject;
|
|
268
|
-
});
|
|
269
|
-
// Prevent unhandled rejection warnings - errors are accessible via:
|
|
270
|
-
// 1. onError callback (if provided)
|
|
271
|
-
// 2. state() returning { status: "error", error }
|
|
272
|
-
// 3. .get().catch() by consumers
|
|
273
|
-
currentPromise.catch(() => {});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Run select to compute value and track dependencies
|
|
277
|
-
const attemptCompute = () => {
|
|
278
|
-
const result = select((context) => fn(context.use(withReady())));
|
|
279
|
-
|
|
280
|
-
// Update subscriptions based on accessed deps
|
|
281
|
-
updateSubscriptions(result.dependencies);
|
|
282
|
-
|
|
283
|
-
if (result.promise) {
|
|
284
|
-
// Notify subscribers that we're now in loading state
|
|
285
|
-
// This allows downstream derived atoms and useSelector to suspend
|
|
286
|
-
if (!silent) notify();
|
|
287
|
-
// Promise thrown - wait for it and retry
|
|
288
|
-
// Note: For never-resolving promises (from ready()), this .then() will never fire.
|
|
289
|
-
// But when a dependency changes, compute() is called again via subscription,
|
|
290
|
-
// and the new computation will run (with a new version).
|
|
291
|
-
result.promise.then(
|
|
292
|
-
() => {
|
|
293
|
-
// Check if we're still the current computation
|
|
294
|
-
if (version !== computeVersion) return;
|
|
295
|
-
attemptCompute();
|
|
296
|
-
},
|
|
297
|
-
(error) => {
|
|
298
|
-
// Check if we're still the current computation
|
|
299
|
-
if (version !== computeVersion) return;
|
|
300
|
-
isLoading = false;
|
|
301
|
-
lastError = error;
|
|
302
|
-
rejectPromise?.(error);
|
|
303
|
-
// Clear resolve/reject so next computation creates new promise
|
|
304
|
-
resolvePromise = null;
|
|
305
|
-
rejectPromise = null;
|
|
306
|
-
// Invoke error handlers
|
|
307
|
-
handleError(error);
|
|
308
|
-
// Always notify when promise rejects - subscribers need to know
|
|
309
|
-
// state changed from loading to error
|
|
310
|
-
notify();
|
|
311
|
-
}
|
|
312
|
-
);
|
|
313
|
-
} else if (result.error !== undefined) {
|
|
314
|
-
// Error thrown
|
|
315
|
-
isLoading = false;
|
|
316
|
-
lastError = result.error;
|
|
317
|
-
rejectPromise?.(result.error);
|
|
318
|
-
// Clear resolve/reject so next computation creates new promise
|
|
319
|
-
resolvePromise = null;
|
|
320
|
-
rejectPromise = null;
|
|
321
|
-
// Invoke error handlers
|
|
322
|
-
handleError(result.error);
|
|
323
|
-
if (!silent) notify();
|
|
324
|
-
} else {
|
|
325
|
-
// Success - update lastResolved and resolve
|
|
326
|
-
const newValue = result.value as T;
|
|
327
|
-
const wasFirstResolve = !lastResolved;
|
|
328
|
-
isLoading = false;
|
|
329
|
-
lastError = undefined;
|
|
330
|
-
|
|
331
|
-
// Only update and notify if value changed
|
|
332
|
-
if (!lastResolved || !eq(newValue, lastResolved.value)) {
|
|
333
|
-
lastResolved = { value: newValue };
|
|
334
|
-
// Always notify on first resolve (loading → ready transition)
|
|
335
|
-
// even if silent, because subscribers need to know state changed
|
|
336
|
-
if (wasFirstResolve || !silent) notify();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
resolvePromise?.(newValue);
|
|
340
|
-
// Clear resolve/reject so next computation creates new promise
|
|
341
|
-
resolvePromise = null;
|
|
342
|
-
rejectPromise = null;
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
attemptCompute();
|
|
347
|
-
|
|
348
|
-
return currentPromise!;
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Initializes the derived atom.
|
|
353
|
-
* Called lazily on first access.
|
|
354
|
-
*/
|
|
355
|
-
const init = () => {
|
|
356
|
-
if (isInitialized) return;
|
|
357
|
-
isInitialized = true;
|
|
358
|
-
|
|
359
|
-
// Initial computation (silent - don't notify on init)
|
|
360
|
-
compute(true);
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const derivedAtom: DerivedAtom<T, boolean> = {
|
|
364
|
-
[SYMBOL_ATOM]: true as const,
|
|
365
|
-
[SYMBOL_DERIVED]: true as const,
|
|
366
|
-
meta: options.meta,
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Get the computed value as a Promise.
|
|
370
|
-
* Always returns Promise<T>, even for sync computations.
|
|
371
|
-
*/
|
|
372
|
-
get(): Promise<T> {
|
|
373
|
-
init();
|
|
374
|
-
return currentPromise!;
|
|
375
|
-
},
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* The stale value - fallback or last resolved value.
|
|
379
|
-
* - Without fallback: T | undefined
|
|
380
|
-
* - With fallback: T (guaranteed)
|
|
381
|
-
*/
|
|
382
|
-
get staleValue(): T | undefined {
|
|
383
|
-
init();
|
|
384
|
-
// Return lastResolvedValue if available, otherwise fallback (if configured)
|
|
385
|
-
if (lastResolved) {
|
|
386
|
-
return lastResolved.value;
|
|
387
|
-
}
|
|
388
|
-
if (hasFallback) {
|
|
389
|
-
return fallbackValue;
|
|
390
|
-
}
|
|
391
|
-
return undefined;
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Get the current state of the derived atom.
|
|
396
|
-
* Returns the actual underlying state (loading/ready/error).
|
|
397
|
-
* Use staleValue if you need fallback/cached value during loading.
|
|
398
|
-
*/
|
|
399
|
-
state(): AtomState<T> {
|
|
400
|
-
init();
|
|
401
|
-
|
|
402
|
-
if (isLoading) {
|
|
403
|
-
return { status: "loading", promise: currentPromise! };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (lastError !== undefined) {
|
|
407
|
-
return { status: "error", error: lastError };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (lastResolved) {
|
|
411
|
-
return { status: "ready", value: lastResolved.value };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Initial state before first computation completes
|
|
415
|
-
return { status: "loading", promise: currentPromise! };
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Re-run the computation.
|
|
420
|
-
*/
|
|
421
|
-
refresh(): void {
|
|
422
|
-
if (!isInitialized) {
|
|
423
|
-
init();
|
|
424
|
-
} else {
|
|
425
|
-
compute();
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Subscribe to value changes.
|
|
431
|
-
*/
|
|
432
|
-
on(listener: VoidFunction): VoidFunction {
|
|
433
|
-
init();
|
|
434
|
-
return changeEmitter.on(listener);
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// Store createInfo for use in onErrorHook
|
|
439
|
-
createInfo = {
|
|
440
|
-
type: "derived",
|
|
441
|
-
key: options.meta?.key,
|
|
442
|
-
meta: options.meta,
|
|
443
|
-
instance: derivedAtom,
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// Notify devtools/plugins of derived atom creation
|
|
447
|
-
onCreateHook.current?.(createInfo);
|
|
448
|
-
|
|
449
|
-
return derivedAtom;
|
|
450
|
-
}
|