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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
trackPromise,
|
|
4
|
+
getPromiseState,
|
|
5
|
+
isTracked,
|
|
6
|
+
isPending,
|
|
7
|
+
isFulfilled,
|
|
8
|
+
isRejected,
|
|
9
|
+
unwrap,
|
|
10
|
+
getAtomState,
|
|
11
|
+
isDerived,
|
|
12
|
+
} from "./promiseCache";
|
|
13
|
+
import { atom } from "./atom";
|
|
14
|
+
import { derived } from "./derived";
|
|
15
|
+
|
|
16
|
+
describe("promiseCache", () => {
|
|
17
|
+
describe("trackPromise", () => {
|
|
18
|
+
it("should return pending state for unresolved promise", () => {
|
|
19
|
+
const promise = new Promise<number>(() => {});
|
|
20
|
+
const state = trackPromise(promise);
|
|
21
|
+
expect(state.status).toBe("pending");
|
|
22
|
+
expect((state as { promise: PromiseLike<number> }).promise).toBe(promise);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return fulfilled state after promise resolves", async () => {
|
|
26
|
+
const promise = Promise.resolve(42);
|
|
27
|
+
await promise;
|
|
28
|
+
trackPromise(promise); // Start tracking
|
|
29
|
+
// May still be pending if checked immediately, wait a tick
|
|
30
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
31
|
+
const state = trackPromise(promise);
|
|
32
|
+
expect(state.status).toBe("fulfilled");
|
|
33
|
+
expect((state as { value: number }).value).toBe(42);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return rejected state after promise rejects", async () => {
|
|
37
|
+
const error = new Error("Test error");
|
|
38
|
+
const promise = Promise.reject(error);
|
|
39
|
+
promise.catch(() => {}); // Prevent unhandled rejection
|
|
40
|
+
trackPromise(promise); // Start tracking
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
await Promise.resolve();
|
|
43
|
+
const state = trackPromise(promise);
|
|
44
|
+
expect(state.status).toBe("rejected");
|
|
45
|
+
expect((state as { error: unknown }).error).toBe(error);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return same state for same promise", () => {
|
|
49
|
+
const promise = new Promise<number>(() => {});
|
|
50
|
+
const state1 = trackPromise(promise);
|
|
51
|
+
const state2 = trackPromise(promise);
|
|
52
|
+
expect(state1).toBe(state2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("getPromiseState", () => {
|
|
57
|
+
it("should return undefined for untracked promise", () => {
|
|
58
|
+
const promise = new Promise<number>(() => {});
|
|
59
|
+
expect(getPromiseState(promise)).toBe(undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return state for tracked promise", () => {
|
|
63
|
+
const promise = new Promise<number>(() => {});
|
|
64
|
+
trackPromise(promise);
|
|
65
|
+
expect(getPromiseState(promise)).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("isTracked", () => {
|
|
70
|
+
it("should return false for untracked promise", () => {
|
|
71
|
+
const promise = new Promise<number>(() => {});
|
|
72
|
+
expect(isTracked(promise)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should return true for tracked promise", () => {
|
|
76
|
+
const promise = new Promise<number>(() => {});
|
|
77
|
+
trackPromise(promise);
|
|
78
|
+
expect(isTracked(promise)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("isPending", () => {
|
|
83
|
+
it("should return false for non-promise", () => {
|
|
84
|
+
expect(isPending(42)).toBe(false);
|
|
85
|
+
expect(isPending("hello")).toBe(false);
|
|
86
|
+
expect(isPending(null)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return true for pending promise", () => {
|
|
90
|
+
const promise = new Promise<number>(() => {});
|
|
91
|
+
expect(isPending(promise)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return false for fulfilled promise", async () => {
|
|
95
|
+
const promise = Promise.resolve(42);
|
|
96
|
+
trackPromise(promise); // Start tracking
|
|
97
|
+
await Promise.resolve();
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
expect(isPending(promise)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("isFulfilled", () => {
|
|
104
|
+
it("should return false for non-promise", () => {
|
|
105
|
+
expect(isFulfilled(42)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return true for fulfilled promise", async () => {
|
|
109
|
+
const promise = Promise.resolve(42);
|
|
110
|
+
trackPromise(promise);
|
|
111
|
+
await Promise.resolve();
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
expect(isFulfilled(promise)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should return false for pending promise", () => {
|
|
117
|
+
const promise = new Promise<number>(() => {});
|
|
118
|
+
expect(isFulfilled(promise)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("isRejected", () => {
|
|
123
|
+
it("should return false for non-promise", () => {
|
|
124
|
+
expect(isRejected(42)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should return true for rejected promise", async () => {
|
|
128
|
+
const promise = Promise.reject(new Error("test"));
|
|
129
|
+
promise.catch(() => {});
|
|
130
|
+
trackPromise(promise);
|
|
131
|
+
await Promise.resolve();
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
expect(isRejected(promise)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should return false for fulfilled promise", async () => {
|
|
137
|
+
const promise = Promise.resolve(42);
|
|
138
|
+
trackPromise(promise);
|
|
139
|
+
await Promise.resolve();
|
|
140
|
+
await Promise.resolve();
|
|
141
|
+
expect(isRejected(promise)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("unwrap", () => {
|
|
146
|
+
it("should return value for non-promise", () => {
|
|
147
|
+
expect(unwrap(42)).toBe(42);
|
|
148
|
+
expect(unwrap("hello")).toBe("hello");
|
|
149
|
+
expect(unwrap(null)).toBe(null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should return value for fulfilled promise", async () => {
|
|
153
|
+
const promise = Promise.resolve(42);
|
|
154
|
+
trackPromise(promise);
|
|
155
|
+
await Promise.resolve();
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
expect(unwrap(promise)).toBe(42);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should throw promise for pending promise", () => {
|
|
161
|
+
const promise = new Promise<number>(() => {});
|
|
162
|
+
let thrown: unknown;
|
|
163
|
+
try {
|
|
164
|
+
unwrap(promise);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
thrown = e;
|
|
167
|
+
}
|
|
168
|
+
expect(thrown).toBe(promise);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should throw error for rejected promise", async () => {
|
|
172
|
+
const error = new Error("Test error");
|
|
173
|
+
const promise = Promise.reject(error);
|
|
174
|
+
promise.catch(() => {});
|
|
175
|
+
trackPromise(promise);
|
|
176
|
+
await Promise.resolve();
|
|
177
|
+
await Promise.resolve();
|
|
178
|
+
expect(() => unwrap(promise)).toThrow(error);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("isDerived", () => {
|
|
183
|
+
it("should return false for mutable atom", () => {
|
|
184
|
+
const a$ = atom(0);
|
|
185
|
+
expect(isDerived(a$)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should return true for derived atom", () => {
|
|
189
|
+
const a$ = atom(0);
|
|
190
|
+
const d$ = derived(({ get }) => get(a$) * 2);
|
|
191
|
+
expect(isDerived(d$)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should return false for non-atom values", () => {
|
|
195
|
+
expect(isDerived(null)).toBe(false);
|
|
196
|
+
expect(isDerived(undefined)).toBe(false);
|
|
197
|
+
expect(isDerived(42)).toBe(false);
|
|
198
|
+
expect(isDerived({})).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("getAtomState", () => {
|
|
203
|
+
it("should return ready state for sync mutable atom", () => {
|
|
204
|
+
const a$ = atom(42);
|
|
205
|
+
const state = getAtomState(a$);
|
|
206
|
+
expect(state.status).toBe("ready");
|
|
207
|
+
expect((state as { value: number }).value).toBe(42);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should return loading state for atom with pending Promise", () => {
|
|
211
|
+
const promise = new Promise<number>(() => {});
|
|
212
|
+
const a$ = atom(promise);
|
|
213
|
+
const state = getAtomState(a$);
|
|
214
|
+
expect(state.status).toBe("loading");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should return ready state for atom with resolved Promise", async () => {
|
|
218
|
+
const promise = Promise.resolve(42);
|
|
219
|
+
const a$ = atom(promise);
|
|
220
|
+
trackPromise(promise);
|
|
221
|
+
await Promise.resolve();
|
|
222
|
+
await Promise.resolve();
|
|
223
|
+
const state = getAtomState(a$);
|
|
224
|
+
expect(state.status).toBe("ready");
|
|
225
|
+
expect((state as { value: number }).value).toBe(42);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should return loading state for derived with fallback during loading", async () => {
|
|
229
|
+
const asyncValue$ = atom(new Promise<number>(() => {}));
|
|
230
|
+
const derived$ = derived(({ get }) => get(asyncValue$), { fallback: 0 });
|
|
231
|
+
|
|
232
|
+
// Derived atoms return their state directly via state()
|
|
233
|
+
// State is loading, but staleValue provides the fallback
|
|
234
|
+
const state = getAtomState(derived$);
|
|
235
|
+
expect(state.status).toBe("loading");
|
|
236
|
+
expect(derived$.staleValue).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
2
|
+
import { Atom, AtomState, DerivedAtom, SYMBOL_DERIVED } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents the state of a tracked Promise.
|
|
6
|
+
*/
|
|
7
|
+
export type PromiseState<T> =
|
|
8
|
+
| { status: "pending"; promise: PromiseLike<T> }
|
|
9
|
+
| { status: "fulfilled"; value: T }
|
|
10
|
+
| { status: "rejected"; error: unknown };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WeakMap cache for Promise states.
|
|
14
|
+
* Using WeakMap allows Promises to be garbage collected when no longer referenced.
|
|
15
|
+
*/
|
|
16
|
+
const promiseCache = new WeakMap<PromiseLike<unknown>, PromiseState<unknown>>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks a Promise and caches its state.
|
|
20
|
+
* If the Promise is already tracked, returns the existing state.
|
|
21
|
+
* Otherwise, starts tracking and returns the initial pending state.
|
|
22
|
+
*
|
|
23
|
+
* @param promise - The Promise to track
|
|
24
|
+
* @returns The current state of the Promise
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const promise = fetchData();
|
|
29
|
+
* const state = trackPromise(promise);
|
|
30
|
+
* // state.status === 'pending'
|
|
31
|
+
*
|
|
32
|
+
* await promise;
|
|
33
|
+
* const state2 = trackPromise(promise);
|
|
34
|
+
* // state2.status === 'fulfilled'
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function trackPromise<T>(promise: PromiseLike<T>): PromiseState<T> {
|
|
38
|
+
const existing = promiseCache.get(promise);
|
|
39
|
+
if (existing) {
|
|
40
|
+
return existing as PromiseState<T>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const state: PromiseState<T> = { status: "pending", promise };
|
|
44
|
+
promiseCache.set(promise, state as PromiseState<unknown>);
|
|
45
|
+
|
|
46
|
+
promise.then(
|
|
47
|
+
(value) => {
|
|
48
|
+
// Only update if still pending (not replaced by another state)
|
|
49
|
+
const current = promiseCache.get(promise);
|
|
50
|
+
if (current?.status === "pending") {
|
|
51
|
+
promiseCache.set(promise, { status: "fulfilled", value });
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
(error) => {
|
|
55
|
+
// Only update if still pending
|
|
56
|
+
const current = promiseCache.get(promise);
|
|
57
|
+
if (current?.status === "pending") {
|
|
58
|
+
promiseCache.set(promise, { status: "rejected", error });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets the current state of a Promise without tracking it.
|
|
68
|
+
* Returns undefined if the Promise is not being tracked.
|
|
69
|
+
*
|
|
70
|
+
* @param promise - The Promise to check
|
|
71
|
+
* @returns The current state or undefined if not tracked
|
|
72
|
+
*/
|
|
73
|
+
export function getPromiseState<T>(
|
|
74
|
+
promise: PromiseLike<T>
|
|
75
|
+
): PromiseState<T> | undefined {
|
|
76
|
+
return promiseCache.get(promise) as PromiseState<T> | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Checks if a Promise is being tracked.
|
|
81
|
+
*
|
|
82
|
+
* @param promise - The Promise to check
|
|
83
|
+
* @returns true if the Promise is tracked
|
|
84
|
+
*/
|
|
85
|
+
export function isTracked(promise: PromiseLike<unknown>): boolean {
|
|
86
|
+
return promiseCache.has(promise);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Type guard to check if a value is a DerivedAtom.
|
|
91
|
+
*/
|
|
92
|
+
export function isDerived<T>(value: unknown): value is DerivedAtom<T, boolean> {
|
|
93
|
+
return (
|
|
94
|
+
value !== null &&
|
|
95
|
+
typeof value === "object" &&
|
|
96
|
+
SYMBOL_DERIVED in value &&
|
|
97
|
+
(value as DerivedAtom<T, boolean>)[SYMBOL_DERIVED] === true
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns the current state of an atom as a discriminated union.
|
|
103
|
+
*
|
|
104
|
+
* For DerivedAtom:
|
|
105
|
+
* - Returns atom.state() directly (derived atoms track their own state)
|
|
106
|
+
*
|
|
107
|
+
* For MutableAtom:
|
|
108
|
+
* - If value is not a Promise: returns ready state
|
|
109
|
+
* - If value is a Promise: tracks and returns its state (ready/error/loading)
|
|
110
|
+
*
|
|
111
|
+
* @param atom - The atom to get state from
|
|
112
|
+
* @returns AtomState discriminated union (ready | error | loading)
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const state = getAtomState(myAtom$);
|
|
117
|
+
*
|
|
118
|
+
* switch (state.status) {
|
|
119
|
+
* case "ready":
|
|
120
|
+
* console.log(state.value); // T
|
|
121
|
+
* break;
|
|
122
|
+
* case "error":
|
|
123
|
+
* console.log(state.error);
|
|
124
|
+
* break;
|
|
125
|
+
* case "loading":
|
|
126
|
+
* console.log(state.promise);
|
|
127
|
+
* break;
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function getAtomState<T>(atom: Atom<T>): AtomState<Awaited<T>> {
|
|
132
|
+
// For derived atoms, use their own state method
|
|
133
|
+
if (isDerived<T>(atom)) {
|
|
134
|
+
return atom.state() as AtomState<Awaited<T>>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const value = atom.value;
|
|
138
|
+
|
|
139
|
+
// 1. Sync value - ready
|
|
140
|
+
if (!isPromiseLike(value)) {
|
|
141
|
+
return {
|
|
142
|
+
status: "ready",
|
|
143
|
+
value: value as Awaited<T>,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 2. Promise value - check state via promiseCache
|
|
148
|
+
const state = trackPromise(value);
|
|
149
|
+
|
|
150
|
+
switch (state.status) {
|
|
151
|
+
case "fulfilled":
|
|
152
|
+
return {
|
|
153
|
+
status: "ready",
|
|
154
|
+
value: state.value as Awaited<T>,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
case "rejected":
|
|
158
|
+
return {
|
|
159
|
+
status: "error",
|
|
160
|
+
error: state.error,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
case "pending":
|
|
164
|
+
return {
|
|
165
|
+
status: "loading",
|
|
166
|
+
promise: state.promise as Promise<Awaited<T>>,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Unwraps a value that may be a Promise.
|
|
173
|
+
* - If not a Promise, returns the value directly.
|
|
174
|
+
* - If a resolved Promise, returns the resolved value.
|
|
175
|
+
* - If a loading Promise, throws the Promise (for Suspense).
|
|
176
|
+
* - If a rejected Promise, throws the error.
|
|
177
|
+
*
|
|
178
|
+
* This follows the React Suspense pattern where throwing a Promise
|
|
179
|
+
* signals that the component should suspend until the Promise resolves.
|
|
180
|
+
*
|
|
181
|
+
* @param value - The value to unwrap (may be a Promise)
|
|
182
|
+
* @returns The unwrapped value
|
|
183
|
+
* @throws Promise if loading, Error if rejected
|
|
184
|
+
*/
|
|
185
|
+
export function unwrap<T>(value: T | PromiseLike<T>): T {
|
|
186
|
+
if (!isPromiseLike(value)) {
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const promise = value as PromiseLike<T>;
|
|
191
|
+
const state = trackPromise(promise);
|
|
192
|
+
|
|
193
|
+
switch (state.status) {
|
|
194
|
+
case "pending":
|
|
195
|
+
throw state.promise;
|
|
196
|
+
case "rejected":
|
|
197
|
+
throw state.error;
|
|
198
|
+
case "fulfilled":
|
|
199
|
+
return state.value;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Checks if a value is a pending Promise.
|
|
205
|
+
*
|
|
206
|
+
* @param value - The value to check
|
|
207
|
+
* @returns true if value is a Promise in pending state
|
|
208
|
+
*/
|
|
209
|
+
export function isPending<T>(value: T | PromiseLike<T>): boolean {
|
|
210
|
+
if (!isPromiseLike(value)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const state = trackPromise(value as PromiseLike<T>);
|
|
214
|
+
return state.status === "pending";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Checks if a value is a fulfilled Promise.
|
|
219
|
+
*
|
|
220
|
+
* @param value - The value to check
|
|
221
|
+
* @returns true if value is a Promise in fulfilled state
|
|
222
|
+
*/
|
|
223
|
+
export function isFulfilled<T>(value: T | PromiseLike<T>): boolean {
|
|
224
|
+
if (!isPromiseLike(value)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const state = trackPromise(value as PromiseLike<T>);
|
|
228
|
+
return state.status === "fulfilled";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Checks if a value is a rejected Promise.
|
|
233
|
+
*
|
|
234
|
+
* @param value - The value to check
|
|
235
|
+
* @returns true if value is a Promise in rejected state
|
|
236
|
+
*/
|
|
237
|
+
export function isRejected<T>(value: T | PromiseLike<T>): boolean {
|
|
238
|
+
if (!isPromiseLike(value)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const state = trackPromise(value as PromiseLike<T>);
|
|
242
|
+
return state.status === "rejected";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Gets the resolved value of a Promise if fulfilled, otherwise undefined.
|
|
247
|
+
*
|
|
248
|
+
* @param value - The value to check
|
|
249
|
+
* @returns The resolved value or undefined
|
|
250
|
+
*/
|
|
251
|
+
export function getResolvedValue<T>(value: T | PromiseLike<T>): T | undefined {
|
|
252
|
+
if (!isPromiseLike(value)) {
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
const state = getPromiseState(value as PromiseLike<T>);
|
|
256
|
+
if (state?.status === "fulfilled") {
|
|
257
|
+
return state.value;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Gets the error of a Promise if rejected, otherwise undefined.
|
|
264
|
+
*
|
|
265
|
+
* @param value - The value to check
|
|
266
|
+
* @returns The error or undefined
|
|
267
|
+
*/
|
|
268
|
+
export function getRejectedError<T>(
|
|
269
|
+
value: T | PromiseLike<T>
|
|
270
|
+
): unknown | undefined {
|
|
271
|
+
if (!isPromiseLike(value)) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const state = getPromiseState(value as PromiseLike<T>);
|
|
275
|
+
if (state?.status === "rejected") {
|
|
276
|
+
return state.error;
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { hook } from "./hook";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook that controls how atom change notifications are scheduled.
|
|
5
|
+
*
|
|
6
|
+
* ## Default Behavior
|
|
7
|
+
*
|
|
8
|
+
* By default, notifications are **synchronous** - listeners are called immediately
|
|
9
|
+
* when an atom's value changes:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // Default: (fn) => fn() - immediate execution
|
|
13
|
+
* atom.set(newValue); // Listeners called immediately here
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## Used by `batch()`
|
|
17
|
+
*
|
|
18
|
+
* The `batch()` function temporarily overrides this hook to defer notifications
|
|
19
|
+
* until all updates complete:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* batch(() => {
|
|
23
|
+
* a.set(1); // Notification deferred
|
|
24
|
+
* b.set(2); // Notification deferred
|
|
25
|
+
* });
|
|
26
|
+
* // All listeners called here (deduped)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Custom Scheduling
|
|
30
|
+
*
|
|
31
|
+
* Can be overridden for custom scheduling strategies (e.g., microtask, RAF):
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* // Schedule notifications as microtasks
|
|
35
|
+
* scheduleNotifyHook.override(() => (fn) => queueMicrotask(fn));
|
|
36
|
+
*
|
|
37
|
+
* // Schedule notifications on next animation frame
|
|
38
|
+
* scheduleNotifyHook.override(() => (fn) => requestAnimationFrame(fn));
|
|
39
|
+
*
|
|
40
|
+
* // Reset to default synchronous behavior
|
|
41
|
+
* scheduleNotifyHook.reset();
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ## API
|
|
45
|
+
*
|
|
46
|
+
* - `scheduleNotifyHook.current` - Get/set the current scheduler function
|
|
47
|
+
* - `scheduleNotifyHook.override(reducer)` - Override with custom scheduler (reducer receives previous)
|
|
48
|
+
* - `scheduleNotifyHook.reset()` - Reset to default synchronous behavior
|
|
49
|
+
* - `scheduleNotifyHook(reducer)` - Create a HookSetup for use with `hook.use()`
|
|
50
|
+
*
|
|
51
|
+
* @internal Used internally by atomState and batch. Not typically needed by users.
|
|
52
|
+
*/
|
|
53
|
+
export const scheduleNotifyHook = hook((fn: VoidFunction) => fn());
|