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,342 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { atomState } from "./atomState";
|
|
3
|
+
|
|
4
|
+
describe("atomState", () => {
|
|
5
|
+
describe("initial state", () => {
|
|
6
|
+
it("should start with undefined value", () => {
|
|
7
|
+
const state = atomState<number>();
|
|
8
|
+
expect(state.getValue()).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should start with loading false", () => {
|
|
12
|
+
const state = atomState<number>();
|
|
13
|
+
expect(state.getLoading()).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should start with undefined error", () => {
|
|
17
|
+
const state = atomState<number>();
|
|
18
|
+
expect(state.getError()).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should start with version 0", () => {
|
|
22
|
+
const state = atomState<number>();
|
|
23
|
+
expect(state.getVersion()).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("setValue", () => {
|
|
28
|
+
it("should set the value", () => {
|
|
29
|
+
const state = atomState<number>();
|
|
30
|
+
state.setValue(42);
|
|
31
|
+
expect(state.getValue()).toBe(42);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should clear loading state", () => {
|
|
35
|
+
const state = atomState<number>();
|
|
36
|
+
state.setLoading(Promise.resolve(1));
|
|
37
|
+
expect(state.getLoading()).toBe(true);
|
|
38
|
+
|
|
39
|
+
state.setValue(42);
|
|
40
|
+
expect(state.getLoading()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should clear error state", () => {
|
|
44
|
+
const state = atomState<number>();
|
|
45
|
+
state.setError(new Error("test"));
|
|
46
|
+
expect(state.getError()).toBeDefined();
|
|
47
|
+
|
|
48
|
+
state.setValue(42);
|
|
49
|
+
expect(state.getError()).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should bump version", () => {
|
|
53
|
+
const state = atomState<number>();
|
|
54
|
+
const v1 = state.getVersion();
|
|
55
|
+
state.setValue(42);
|
|
56
|
+
expect(state.getVersion()).toBe(v1 + 1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should notify listeners", () => {
|
|
60
|
+
const state = atomState<number>();
|
|
61
|
+
const listener = vi.fn();
|
|
62
|
+
state.on(listener);
|
|
63
|
+
|
|
64
|
+
state.setValue(42);
|
|
65
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should not notify if value is equal (strict equality)", () => {
|
|
69
|
+
const state = atomState<number>();
|
|
70
|
+
state.setValue(42);
|
|
71
|
+
|
|
72
|
+
const listener = vi.fn();
|
|
73
|
+
state.on(listener);
|
|
74
|
+
|
|
75
|
+
state.setValue(42);
|
|
76
|
+
expect(listener).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should use custom equality function", () => {
|
|
80
|
+
const state = atomState<{ id: number; name: string }>({
|
|
81
|
+
equals: (a, b) => a?.id === b?.id,
|
|
82
|
+
});
|
|
83
|
+
state.setValue({ id: 1, name: "John" });
|
|
84
|
+
|
|
85
|
+
const listener = vi.fn();
|
|
86
|
+
state.on(listener);
|
|
87
|
+
|
|
88
|
+
// Same id, different name - should not notify
|
|
89
|
+
state.setValue({ id: 1, name: "Jane" });
|
|
90
|
+
expect(listener).not.toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
// Different id - should notify
|
|
93
|
+
state.setValue({ id: 2, name: "Jane" });
|
|
94
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should create a resolved promise", async () => {
|
|
98
|
+
const state = atomState<number>();
|
|
99
|
+
state.setValue(42);
|
|
100
|
+
|
|
101
|
+
const value = await state.getPromise();
|
|
102
|
+
expect(value).toBe(42);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("setLoading", () => {
|
|
107
|
+
it("should set loading to true", () => {
|
|
108
|
+
const state = atomState<number>();
|
|
109
|
+
state.setLoading(Promise.resolve(1));
|
|
110
|
+
expect(state.getLoading()).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should clear value", () => {
|
|
114
|
+
const state = atomState<number>();
|
|
115
|
+
state.setValue(42);
|
|
116
|
+
state.setLoading(Promise.resolve(1));
|
|
117
|
+
expect(state.getValue()).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should clear error", () => {
|
|
121
|
+
const state = atomState<number>();
|
|
122
|
+
state.setError(new Error("test"));
|
|
123
|
+
state.setLoading(Promise.resolve(1));
|
|
124
|
+
expect(state.getError()).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should bump version", () => {
|
|
128
|
+
const state = atomState<number>();
|
|
129
|
+
const v1 = state.getVersion();
|
|
130
|
+
state.setLoading(Promise.resolve(1));
|
|
131
|
+
expect(state.getVersion()).toBe(v1 + 1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should notify listeners", () => {
|
|
135
|
+
const state = atomState<number>();
|
|
136
|
+
const listener = vi.fn();
|
|
137
|
+
state.on(listener);
|
|
138
|
+
|
|
139
|
+
state.setLoading(Promise.resolve(1));
|
|
140
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should store the promise", () => {
|
|
144
|
+
const state = atomState<number>();
|
|
145
|
+
const promise = Promise.resolve(42);
|
|
146
|
+
state.setLoading(promise);
|
|
147
|
+
expect(state.getPromise()).toBe(promise);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("setError", () => {
|
|
152
|
+
it("should set the error", () => {
|
|
153
|
+
const state = atomState<number>();
|
|
154
|
+
const error = new Error("test");
|
|
155
|
+
state.setError(error);
|
|
156
|
+
expect(state.getError()).toBe(error);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should set loading to false", () => {
|
|
160
|
+
const state = atomState<number>();
|
|
161
|
+
state.setLoading(Promise.resolve(1));
|
|
162
|
+
state.setError(new Error("test"));
|
|
163
|
+
expect(state.getLoading()).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should clear value", () => {
|
|
167
|
+
const state = atomState<number>();
|
|
168
|
+
state.setValue(42);
|
|
169
|
+
state.setError(new Error("test"));
|
|
170
|
+
expect(state.getValue()).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should bump version", () => {
|
|
174
|
+
const state = atomState<number>();
|
|
175
|
+
const v1 = state.getVersion();
|
|
176
|
+
state.setError(new Error("test"));
|
|
177
|
+
expect(state.getVersion()).toBe(v1 + 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should notify listeners", () => {
|
|
181
|
+
const state = atomState<number>();
|
|
182
|
+
const listener = vi.fn();
|
|
183
|
+
state.on(listener);
|
|
184
|
+
|
|
185
|
+
state.setError(new Error("test"));
|
|
186
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should not notify if same error", () => {
|
|
190
|
+
const state = atomState<number>();
|
|
191
|
+
const error = new Error("test");
|
|
192
|
+
state.setError(error);
|
|
193
|
+
|
|
194
|
+
const listener = vi.fn();
|
|
195
|
+
state.on(listener);
|
|
196
|
+
|
|
197
|
+
state.setError(error);
|
|
198
|
+
expect(listener).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("race condition handling", () => {
|
|
203
|
+
it("should detect stale versions", () => {
|
|
204
|
+
const state = atomState<number>();
|
|
205
|
+
const v1 = state.getVersion();
|
|
206
|
+
|
|
207
|
+
state.setValue(1);
|
|
208
|
+
expect(state.isVersionStale(v1)).toBe(true);
|
|
209
|
+
expect(state.isVersionStale(state.getVersion())).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("should ignore stale promise resolution", async () => {
|
|
213
|
+
const state = atomState<number>();
|
|
214
|
+
|
|
215
|
+
let resolve1: (value: number) => void;
|
|
216
|
+
const promise1 = new Promise<number>((r) => {
|
|
217
|
+
resolve1 = r;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
state.setLoading(promise1);
|
|
221
|
+
const v1 = state.getVersion();
|
|
222
|
+
|
|
223
|
+
// Set a new value before promise resolves
|
|
224
|
+
state.setValue(100);
|
|
225
|
+
|
|
226
|
+
// Now resolve the old promise
|
|
227
|
+
resolve1!(42);
|
|
228
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
229
|
+
|
|
230
|
+
// Value should still be 100, not 42
|
|
231
|
+
expect(state.getValue()).toBe(100);
|
|
232
|
+
expect(state.isVersionStale(v1)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("subscriptions", () => {
|
|
237
|
+
it("should return unsubscribe function", () => {
|
|
238
|
+
const state = atomState<number>();
|
|
239
|
+
const listener = vi.fn();
|
|
240
|
+
|
|
241
|
+
const unsubscribe = state.on(listener);
|
|
242
|
+
state.setValue(1);
|
|
243
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
244
|
+
|
|
245
|
+
unsubscribe();
|
|
246
|
+
state.setValue(2);
|
|
247
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should support multiple listeners", () => {
|
|
251
|
+
const state = atomState<number>();
|
|
252
|
+
const listener1 = vi.fn();
|
|
253
|
+
const listener2 = vi.fn();
|
|
254
|
+
|
|
255
|
+
state.on(listener1);
|
|
256
|
+
state.on(listener2);
|
|
257
|
+
|
|
258
|
+
state.setValue(1);
|
|
259
|
+
|
|
260
|
+
expect(listener1).toHaveBeenCalledTimes(1);
|
|
261
|
+
expect(listener2).toHaveBeenCalledTimes(1);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("reset", () => {
|
|
266
|
+
it("should reset to initial state", () => {
|
|
267
|
+
const state = atomState<number>();
|
|
268
|
+
state.setValue(42);
|
|
269
|
+
|
|
270
|
+
state.reset();
|
|
271
|
+
|
|
272
|
+
expect(state.getValue()).toBeUndefined();
|
|
273
|
+
expect(state.getLoading()).toBe(false);
|
|
274
|
+
expect(state.getError()).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should bump version on reset", () => {
|
|
278
|
+
const state = atomState<number>();
|
|
279
|
+
state.setValue(42);
|
|
280
|
+
const v1 = state.getVersion();
|
|
281
|
+
|
|
282
|
+
state.reset();
|
|
283
|
+
expect(state.getVersion()).toBe(v1 + 1);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should notify listeners on reset", () => {
|
|
287
|
+
const state = atomState<number>();
|
|
288
|
+
state.setValue(42);
|
|
289
|
+
|
|
290
|
+
const listener = vi.fn();
|
|
291
|
+
state.on(listener);
|
|
292
|
+
|
|
293
|
+
state.reset();
|
|
294
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should not notify if already in initial state", () => {
|
|
298
|
+
const state = atomState<number>();
|
|
299
|
+
const listener = vi.fn();
|
|
300
|
+
state.on(listener);
|
|
301
|
+
|
|
302
|
+
state.reset();
|
|
303
|
+
expect(listener).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("equals options", () => {
|
|
308
|
+
it("should use shallow equality when specified", () => {
|
|
309
|
+
const state = atomState<{ a: number }>({ equals: "shallow" });
|
|
310
|
+
state.setValue({ a: 1 });
|
|
311
|
+
|
|
312
|
+
const listener = vi.fn();
|
|
313
|
+
state.on(listener);
|
|
314
|
+
|
|
315
|
+
// Same content - no notification
|
|
316
|
+
state.setValue({ a: 1 });
|
|
317
|
+
expect(listener).not.toHaveBeenCalled();
|
|
318
|
+
|
|
319
|
+
// Different content - should notify
|
|
320
|
+
state.setValue({ a: 2 });
|
|
321
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should use deep equality when specified", () => {
|
|
325
|
+
const state = atomState<{ nested: { value: number } }>({
|
|
326
|
+
equals: "deep",
|
|
327
|
+
});
|
|
328
|
+
state.setValue({ nested: { value: 1 } });
|
|
329
|
+
|
|
330
|
+
const listener = vi.fn();
|
|
331
|
+
state.on(listener);
|
|
332
|
+
|
|
333
|
+
// Same deep content - no notification
|
|
334
|
+
state.setValue({ nested: { value: 1 } });
|
|
335
|
+
expect(listener).not.toHaveBeenCalled();
|
|
336
|
+
|
|
337
|
+
// Different deep content - should notify
|
|
338
|
+
state.setValue({ nested: { value: 2 } });
|
|
339
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { emitter, Emitter } from "./emitter";
|
|
2
|
+
import { resolveEquality } from "./equality";
|
|
3
|
+
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
4
|
+
import { Equality } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for creating an atomState.
|
|
8
|
+
*/
|
|
9
|
+
export interface AtomStateOptions<T, TFallback = undefined> {
|
|
10
|
+
/** Equality strategy for change detection (default: "strict") */
|
|
11
|
+
equals?: Equality<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Fallback value to use during loading or error states.
|
|
14
|
+
* When set, enables "stale" mode where value is never undefined.
|
|
15
|
+
*/
|
|
16
|
+
fallback?: TFallback;
|
|
17
|
+
/**
|
|
18
|
+
* Whether fallback mode is enabled.
|
|
19
|
+
* When true, getValue() returns fallback/lastResolved during loading/error.
|
|
20
|
+
*/
|
|
21
|
+
hasFallback?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* API for managing atom state with async support.
|
|
26
|
+
*/
|
|
27
|
+
export interface AtomStateAPI<T, TFallback = undefined> {
|
|
28
|
+
/** Get the current value (undefined if loading or error, unless fallback mode) */
|
|
29
|
+
getValue(): TFallback extends undefined ? T | undefined : T | TFallback;
|
|
30
|
+
/** Get the loading state */
|
|
31
|
+
getLoading(): boolean;
|
|
32
|
+
/** Get the error (undefined if no error) */
|
|
33
|
+
getError(): any;
|
|
34
|
+
/** Get the current promise */
|
|
35
|
+
getPromise(): PromiseLike<T>;
|
|
36
|
+
|
|
37
|
+
/** Set the value (clears loading and error, notifies if changed) */
|
|
38
|
+
setValue(value: T, silent?: boolean): void;
|
|
39
|
+
/** Set loading state with a promise (clears value and error, notifies) */
|
|
40
|
+
setLoading(promise: PromiseLike<T>, silent?: boolean): void;
|
|
41
|
+
/** Set error state (clears value and loading, notifies if changed) */
|
|
42
|
+
setError(error: any, silent?: boolean): void;
|
|
43
|
+
/** Reset to initial state (notifies if was not already initial) */
|
|
44
|
+
reset(): void;
|
|
45
|
+
|
|
46
|
+
/** Get current version (for race condition handling) */
|
|
47
|
+
getVersion(): number;
|
|
48
|
+
/** Check if a version is stale (older than current) */
|
|
49
|
+
isVersionStale(version: number): boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if fallback mode is enabled AND (loading OR error).
|
|
53
|
+
* When true, getValue() returns fallback or last resolved value.
|
|
54
|
+
*/
|
|
55
|
+
stale(): boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns true if value has been changed by setValue() (not during init).
|
|
59
|
+
*/
|
|
60
|
+
isDirty(): boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Marks the state as dirty (called when set() is used).
|
|
64
|
+
*/
|
|
65
|
+
markDirty(): void;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clears the dirty flag (called on reset).
|
|
69
|
+
*/
|
|
70
|
+
clearDirty(): void;
|
|
71
|
+
|
|
72
|
+
/** Subscribe to state changes */
|
|
73
|
+
on: Emitter["on"];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a state container for atoms with async support.
|
|
78
|
+
*
|
|
79
|
+
* Handles:
|
|
80
|
+
* - Value, loading, and error states
|
|
81
|
+
* - Version tracking for race condition handling
|
|
82
|
+
* - Equality checking for change detection
|
|
83
|
+
* - Notification scheduling
|
|
84
|
+
* - Fallback mode for stale-while-revalidate pattern
|
|
85
|
+
*
|
|
86
|
+
* @template T - The type of value stored
|
|
87
|
+
* @template TFallback - The type of fallback value (default: undefined)
|
|
88
|
+
* @param options - Configuration options
|
|
89
|
+
* @returns AtomStateAPI for managing the state
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* const state = atomState<number>();
|
|
94
|
+
*
|
|
95
|
+
* state.setValue(42);
|
|
96
|
+
* console.log(state.getValue()); // 42
|
|
97
|
+
*
|
|
98
|
+
* state.setLoading(fetchData());
|
|
99
|
+
* console.log(state.getLoading()); // true
|
|
100
|
+
*
|
|
101
|
+
* state.on(() => console.log('State changed!'));
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example With fallback
|
|
105
|
+
* ```ts
|
|
106
|
+
* const state = atomState<User, User>({
|
|
107
|
+
* fallback: { name: 'Guest' },
|
|
108
|
+
* hasFallback: true
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* state.setLoading(fetchUser());
|
|
112
|
+
* console.log(state.getValue()); // { name: 'Guest' }
|
|
113
|
+
* console.log(state.isStale()); // true
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function atomState<T, TFallback = undefined>(
|
|
117
|
+
options: AtomStateOptions<T, TFallback> = {}
|
|
118
|
+
): AtomStateAPI<T, TFallback> {
|
|
119
|
+
const changeEmitter = emitter();
|
|
120
|
+
const eq = resolveEquality(options.equals as Equality<unknown>);
|
|
121
|
+
|
|
122
|
+
// Fallback configuration
|
|
123
|
+
const hasFallback = options.hasFallback ?? false;
|
|
124
|
+
const fallbackValue = options.fallback as TFallback;
|
|
125
|
+
|
|
126
|
+
// Internal state
|
|
127
|
+
let value: T | undefined = undefined;
|
|
128
|
+
let loading = false;
|
|
129
|
+
let error: any = undefined;
|
|
130
|
+
let promise: PromiseLike<T> = Promise.resolve(undefined as T);
|
|
131
|
+
let version = 0;
|
|
132
|
+
|
|
133
|
+
// Track last resolved value for stale-while-revalidate
|
|
134
|
+
let lastResolvedValue: T | undefined = undefined;
|
|
135
|
+
|
|
136
|
+
// Track if value has been modified by set()
|
|
137
|
+
let dirty = false;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Schedules notification to all subscribers.
|
|
141
|
+
* Each listener is scheduled individually to enable deduping in batch().
|
|
142
|
+
*/
|
|
143
|
+
const notify = () => {
|
|
144
|
+
changeEmitter.forEach((listener) => {
|
|
145
|
+
scheduleNotifyHook.current(listener);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Checks if state is in initial state (no value, not loading, no error).
|
|
151
|
+
*/
|
|
152
|
+
const isInitialState = () => {
|
|
153
|
+
return value === undefined && !loading && error === undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const api: AtomStateAPI<T, TFallback> = {
|
|
157
|
+
getValue: () => {
|
|
158
|
+
// If fallback mode enabled and in loading/error state
|
|
159
|
+
if (hasFallback && (loading || error !== undefined)) {
|
|
160
|
+
// Return last resolved value if available, otherwise fallback
|
|
161
|
+
return (lastResolvedValue ?? fallbackValue) as any;
|
|
162
|
+
}
|
|
163
|
+
return value as any;
|
|
164
|
+
},
|
|
165
|
+
getLoading: () => loading,
|
|
166
|
+
getError: () => error,
|
|
167
|
+
getPromise: () => promise,
|
|
168
|
+
getVersion: () => version,
|
|
169
|
+
|
|
170
|
+
isVersionStale: (v: number) => v !== version,
|
|
171
|
+
|
|
172
|
+
stale: () => {
|
|
173
|
+
// Returns true if fallback mode enabled AND (loading OR error)
|
|
174
|
+
return hasFallback && (loading || error !== undefined);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
isDirty: () => dirty,
|
|
178
|
+
|
|
179
|
+
markDirty: () => {
|
|
180
|
+
dirty = true;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
clearDirty: () => {
|
|
184
|
+
dirty = false;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
setValue: (newValue: T, silent = false) => {
|
|
188
|
+
// Check equality before updating (only if we have a previous value)
|
|
189
|
+
if (
|
|
190
|
+
value !== undefined &&
|
|
191
|
+
eq(newValue, value) &&
|
|
192
|
+
!loading &&
|
|
193
|
+
error === undefined
|
|
194
|
+
) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
value = newValue;
|
|
199
|
+
loading = false;
|
|
200
|
+
error = undefined;
|
|
201
|
+
promise = Promise.resolve(newValue);
|
|
202
|
+
version++;
|
|
203
|
+
|
|
204
|
+
// Track last resolved value for fallback mode
|
|
205
|
+
lastResolvedValue = newValue;
|
|
206
|
+
|
|
207
|
+
if (!silent) notify();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
setLoading: (newPromise: PromiseLike<T>, silent = false) => {
|
|
211
|
+
// In fallback mode, don't clear value - it will be returned via getValue()
|
|
212
|
+
// But internally we track that we're loading
|
|
213
|
+
value = undefined;
|
|
214
|
+
loading = true;
|
|
215
|
+
error = undefined;
|
|
216
|
+
promise = newPromise;
|
|
217
|
+
version++;
|
|
218
|
+
if (!silent) notify();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
setError: (newError: any, silent = false) => {
|
|
222
|
+
// Check if error is the same
|
|
223
|
+
if (Object.is(error, newError) && !loading && value === undefined) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
value = undefined;
|
|
228
|
+
loading = false;
|
|
229
|
+
error = newError;
|
|
230
|
+
version++;
|
|
231
|
+
if (!silent) notify();
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
reset: () => {
|
|
235
|
+
// Don't notify if already in initial state
|
|
236
|
+
if (isInitialState() && lastResolvedValue === undefined && !dirty) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
value = undefined;
|
|
241
|
+
loading = false;
|
|
242
|
+
error = undefined;
|
|
243
|
+
promise = Promise.resolve(undefined as T);
|
|
244
|
+
version++;
|
|
245
|
+
// Clear last resolved value on reset
|
|
246
|
+
lastResolvedValue = undefined;
|
|
247
|
+
// Clear dirty flag on reset
|
|
248
|
+
dirty = false;
|
|
249
|
+
notify();
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
on: changeEmitter.on,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return api;
|
|
256
|
+
}
|