atomirx 0.0.2 → 0.0.5
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 +868 -161
- package/coverage/src/core/onCreateHook.ts.html +72 -70
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +69 -22
- package/dist/core/effect.d.ts +52 -52
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/hook.d.ts +1 -1
- package/dist/core/onCreateHook.d.ts +37 -23
- package/dist/core/onErrorHook.d.ts +49 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +107 -22
- package/dist/core/withReady.d.ts +115 -0
- package/dist/core/withReady.test.d.ts +1 -0
- package/dist/index-CBVj1kSj.js +1350 -0
- package/dist/index-Cxk9v0um.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +18 -15
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +422 -377
- package/dist/react/rx.d.ts +114 -25
- package/dist/react/useAction.d.ts +5 -4
- package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
- package/dist/react/useSelector.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/atom.test.ts +307 -43
- package/src/core/atom.ts +144 -22
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/define.test.ts +12 -11
- package/src/core/define.ts +1 -1
- package/src/core/derived.test.ts +906 -72
- package/src/core/derived.ts +192 -81
- package/src/core/effect.test.ts +651 -45
- package/src/core/effect.ts +102 -98
- package/src/core/getAtomState.ts +69 -0
- package/src/core/hook.test.ts +5 -5
- package/src/core/hook.ts +1 -1
- package/src/core/onCreateHook.ts +38 -23
- package/src/core/onErrorHook.test.ts +350 -0
- package/src/core/onErrorHook.ts +52 -0
- package/src/core/promiseCache.test.ts +5 -3
- package/src/core/promiseCache.ts +76 -71
- package/src/core/select.ts +405 -130
- package/src/core/selector.test.ts +574 -32
- package/src/core/types.ts +107 -29
- package/src/core/withReady.test.ts +534 -0
- package/src/core/withReady.ts +191 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +21 -7
- package/src/react/index.ts +2 -1
- package/src/react/rx.test.tsx +173 -18
- package/src/react/rx.tsx +274 -43
- package/src/react/useAction.test.ts +12 -14
- package/src/react/useAction.ts +11 -9
- package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
- package/src/react/{useValue.ts → useSelector.ts} +64 -33
- package/v2.md +44 -44
- package/dist/index-2ok7ilik.js +0 -1217
- package/dist/index-B_5SFzfl.cjs +0 -1
- /package/dist/{react/useValue.test.d.ts → core/onErrorHook.test.d.ts} +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { atom } from "./atom";
|
|
3
3
|
import { select } from "./select";
|
|
4
|
+
import { promisesEqual } from "./promiseCache";
|
|
4
5
|
|
|
5
6
|
describe("select", () => {
|
|
6
|
-
describe("
|
|
7
|
+
describe("read()", () => {
|
|
7
8
|
it("should read value from sync atom", () => {
|
|
8
9
|
const count$ = atom(5);
|
|
9
|
-
const result = select(({
|
|
10
|
+
const result = select(({ read }) => read(count$));
|
|
10
11
|
|
|
11
12
|
expect(result.value).toBe(5);
|
|
12
13
|
expect(result.error).toBe(undefined);
|
|
@@ -17,7 +18,7 @@ describe("select", () => {
|
|
|
17
18
|
const a$ = atom(1);
|
|
18
19
|
const b$ = atom(2);
|
|
19
20
|
|
|
20
|
-
const result = select(({
|
|
21
|
+
const result = select(({ read }) => read(a$) + read(b$));
|
|
21
22
|
|
|
22
23
|
expect(result.dependencies.size).toBe(2);
|
|
23
24
|
expect(result.dependencies.has(a$)).toBe(true);
|
|
@@ -28,8 +29,8 @@ describe("select", () => {
|
|
|
28
29
|
const count$ = atom(5);
|
|
29
30
|
const error = new Error("Test error");
|
|
30
31
|
|
|
31
|
-
const result = select(({
|
|
32
|
-
|
|
32
|
+
const result = select(({ read }) => {
|
|
33
|
+
read(count$);
|
|
33
34
|
throw error;
|
|
34
35
|
});
|
|
35
36
|
|
|
@@ -45,7 +46,7 @@ describe("select", () => {
|
|
|
45
46
|
const b$ = atom(2);
|
|
46
47
|
const c$ = atom(3);
|
|
47
48
|
|
|
48
|
-
const result = select(({ all }) => all(a$, b$, c$));
|
|
49
|
+
const result = select(({ all }) => all([a$, b$, c$]));
|
|
49
50
|
|
|
50
51
|
expect(result.value).toEqual([1, 2, 3]);
|
|
51
52
|
});
|
|
@@ -54,7 +55,7 @@ describe("select", () => {
|
|
|
54
55
|
const a$ = atom(1);
|
|
55
56
|
const b$ = atom(new Promise<number>(() => {}));
|
|
56
57
|
|
|
57
|
-
const result = select(({ all }) => all(a$, b$));
|
|
58
|
+
const result = select(({ all }) => all([a$, b$]));
|
|
58
59
|
|
|
59
60
|
expect(result.promise).toBeDefined();
|
|
60
61
|
expect(result.value).toBe(undefined);
|
|
@@ -63,32 +64,37 @@ describe("select", () => {
|
|
|
63
64
|
it("should throw error if any atom has rejected promise", async () => {
|
|
64
65
|
const error = new Error("Test error");
|
|
65
66
|
const a$ = atom(1);
|
|
66
|
-
|
|
67
|
+
// Create rejected promise with immediate catch to prevent unhandled rejection warning
|
|
68
|
+
let rejectFn: (e: Error) => void;
|
|
69
|
+
const rejectedPromise = new Promise<number>((_, reject) => {
|
|
70
|
+
rejectFn = reject;
|
|
71
|
+
});
|
|
67
72
|
rejectedPromise.catch(() => {}); // Prevent unhandled rejection
|
|
73
|
+
rejectFn!(error);
|
|
68
74
|
const b$ = atom(rejectedPromise);
|
|
69
75
|
|
|
70
76
|
// First call to select tracks the promise but returns pending
|
|
71
|
-
select(({ all }) => all(a$, b$));
|
|
77
|
+
select(({ all }) => all([a$, b$]));
|
|
72
78
|
|
|
73
79
|
// Wait for promise handlers to run
|
|
74
80
|
await Promise.resolve();
|
|
75
81
|
await Promise.resolve();
|
|
76
82
|
|
|
77
83
|
// Now the promise state should be updated
|
|
78
|
-
const result = select(({ all }) => all(a$, b$));
|
|
84
|
+
const result = select(({ all }) => all([a$, b$]));
|
|
79
85
|
|
|
80
86
|
expect(result.error).toBe(error);
|
|
81
87
|
});
|
|
82
88
|
});
|
|
83
89
|
|
|
84
90
|
describe("race()", () => {
|
|
85
|
-
it("should return first fulfilled value", () => {
|
|
91
|
+
it("should return first fulfilled value with key", () => {
|
|
86
92
|
const a$ = atom(1);
|
|
87
93
|
const b$ = atom(2);
|
|
88
94
|
|
|
89
|
-
const result = select(({ race }) => race(a$, b$));
|
|
95
|
+
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
90
96
|
|
|
91
|
-
expect(result.value).
|
|
97
|
+
expect(result.value).toEqual({ key: "a", value: 1 });
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
it("should throw first error if first atom is rejected", async () => {
|
|
@@ -99,11 +105,11 @@ describe("select", () => {
|
|
|
99
105
|
const b$ = atom(2);
|
|
100
106
|
|
|
101
107
|
// Track the promise first
|
|
102
|
-
select(({ race }) => race(a$, b$));
|
|
108
|
+
select(({ race }) => race({ a: a$, b: b$ }));
|
|
103
109
|
await Promise.resolve();
|
|
104
110
|
await Promise.resolve();
|
|
105
111
|
|
|
106
|
-
const result = select(({ race }) => race(a$, b$));
|
|
112
|
+
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
107
113
|
|
|
108
114
|
expect(result.error).toBe(error);
|
|
109
115
|
});
|
|
@@ -112,23 +118,23 @@ describe("select", () => {
|
|
|
112
118
|
const a$ = atom(new Promise<number>(() => {}));
|
|
113
119
|
const b$ = atom(new Promise<number>(() => {}));
|
|
114
120
|
|
|
115
|
-
const result = select(({ race }) => race(a$, b$));
|
|
121
|
+
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
116
122
|
|
|
117
123
|
expect(result.promise).toBeDefined();
|
|
118
124
|
});
|
|
119
125
|
});
|
|
120
126
|
|
|
121
127
|
describe("any()", () => {
|
|
122
|
-
it("should return first fulfilled value", () => {
|
|
128
|
+
it("should return first fulfilled value with key", () => {
|
|
123
129
|
const a$ = atom(1);
|
|
124
130
|
const b$ = atom(2);
|
|
125
131
|
|
|
126
|
-
const result = select(({ any }) => any(a$, b$));
|
|
132
|
+
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
127
133
|
|
|
128
|
-
expect(result.value).
|
|
134
|
+
expect(result.value).toEqual({ key: "a", value: 1 });
|
|
129
135
|
});
|
|
130
136
|
|
|
131
|
-
it("should skip rejected and return next fulfilled", async () => {
|
|
137
|
+
it("should skip rejected and return next fulfilled with key", async () => {
|
|
132
138
|
const error = new Error("Test error");
|
|
133
139
|
const rejectedPromise = Promise.reject(error);
|
|
134
140
|
rejectedPromise.catch(() => {});
|
|
@@ -136,32 +142,41 @@ describe("select", () => {
|
|
|
136
142
|
const b$ = atom(2);
|
|
137
143
|
|
|
138
144
|
// Track first, then wait for microtasks
|
|
139
|
-
select(({ any }) => any(a$, b$));
|
|
145
|
+
select(({ any }) => any({ a: a$, b: b$ }));
|
|
140
146
|
await Promise.resolve();
|
|
141
147
|
await Promise.resolve();
|
|
142
148
|
|
|
143
|
-
const result = select(({ any }) => any(a$, b$));
|
|
149
|
+
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
144
150
|
|
|
145
|
-
expect(result.value).
|
|
151
|
+
expect(result.value).toEqual({ key: "b", value: 2 });
|
|
146
152
|
});
|
|
147
153
|
|
|
148
154
|
it("should throw AggregateError if all rejected", async () => {
|
|
149
155
|
const error1 = new Error("Error 1");
|
|
150
156
|
const error2 = new Error("Error 2");
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
// Create rejected promises with immediate catch to prevent unhandled rejection warning
|
|
158
|
+
let reject1: (e: Error) => void;
|
|
159
|
+
let reject2: (e: Error) => void;
|
|
160
|
+
const p1 = new Promise<number>((_, reject) => {
|
|
161
|
+
reject1 = reject;
|
|
162
|
+
});
|
|
163
|
+
const p2 = new Promise<number>((_, reject) => {
|
|
164
|
+
reject2 = reject;
|
|
165
|
+
});
|
|
153
166
|
p1.catch(() => {});
|
|
154
167
|
p2.catch(() => {});
|
|
168
|
+
reject1!(error1);
|
|
169
|
+
reject2!(error2);
|
|
155
170
|
|
|
156
171
|
const a$ = atom(p1);
|
|
157
172
|
const b$ = atom(p2);
|
|
158
173
|
|
|
159
174
|
// Track first, then wait for microtasks
|
|
160
|
-
select(({ any }) => any(a$, b$));
|
|
175
|
+
select(({ any }) => any({ a: a$, b: b$ }));
|
|
161
176
|
await Promise.resolve();
|
|
162
177
|
await Promise.resolve();
|
|
163
178
|
|
|
164
|
-
const result = select(({ any }) => any(a$, b$));
|
|
179
|
+
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
165
180
|
|
|
166
181
|
expect(result.error).toBeDefined();
|
|
167
182
|
expect((result.error as Error).name).toBe("AggregateError");
|
|
@@ -172,16 +187,21 @@ describe("select", () => {
|
|
|
172
187
|
it("should return array of settled results", async () => {
|
|
173
188
|
const a$ = atom(1);
|
|
174
189
|
const error = new Error("Test error");
|
|
175
|
-
|
|
190
|
+
// Create rejected promise with immediate catch to prevent unhandled rejection warning
|
|
191
|
+
let rejectFn: (e: Error) => void;
|
|
192
|
+
const rejectedPromise = new Promise<number>((_, reject) => {
|
|
193
|
+
rejectFn = reject;
|
|
194
|
+
});
|
|
176
195
|
rejectedPromise.catch(() => {});
|
|
196
|
+
rejectFn!(error);
|
|
177
197
|
const b$ = atom(rejectedPromise);
|
|
178
198
|
|
|
179
199
|
// Track first, wait for microtasks
|
|
180
|
-
select(({ settled }) => settled(a$, b$));
|
|
200
|
+
select(({ settled }) => settled([a$, b$]));
|
|
181
201
|
await Promise.resolve();
|
|
182
202
|
await Promise.resolve();
|
|
183
203
|
|
|
184
|
-
const result = select(({ settled }) => settled(a$, b$));
|
|
204
|
+
const result = select(({ settled }) => settled([a$, b$]));
|
|
185
205
|
|
|
186
206
|
expect(result.value).toEqual([
|
|
187
207
|
{ status: "ready", value: 1 },
|
|
@@ -193,7 +213,7 @@ describe("select", () => {
|
|
|
193
213
|
const a$ = atom(1);
|
|
194
214
|
const b$ = atom(new Promise<number>(() => {}));
|
|
195
215
|
|
|
196
|
-
const result = select(({ settled }) => settled(a$, b$));
|
|
216
|
+
const result = select(({ settled }) => settled([a$, b$]));
|
|
197
217
|
|
|
198
218
|
expect(result.promise).toBeDefined();
|
|
199
219
|
});
|
|
@@ -205,7 +225,9 @@ describe("select", () => {
|
|
|
205
225
|
const a$ = atom(1);
|
|
206
226
|
const b$ = atom(2);
|
|
207
227
|
|
|
208
|
-
const result = select(({
|
|
228
|
+
const result = select(({ read }) =>
|
|
229
|
+
read(condition$) ? read(a$) : read(b$)
|
|
230
|
+
);
|
|
209
231
|
|
|
210
232
|
expect(result.dependencies.size).toBe(2);
|
|
211
233
|
expect(result.dependencies.has(condition$)).toBe(true);
|
|
@@ -254,4 +276,524 @@ describe("select", () => {
|
|
|
254
276
|
expect(result.promise).toBe(undefined);
|
|
255
277
|
});
|
|
256
278
|
});
|
|
279
|
+
|
|
280
|
+
describe("safe()", () => {
|
|
281
|
+
it("should return [undefined, result] on success", () => {
|
|
282
|
+
const count$ = atom(5);
|
|
283
|
+
|
|
284
|
+
const result = select(({ read, safe }) => {
|
|
285
|
+
const [err, value] = safe(() => read(count$) * 2);
|
|
286
|
+
return { err, value };
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(result.value).toEqual({ err: undefined, value: 10 });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should return [error, undefined] on sync error", () => {
|
|
293
|
+
const result = select(({ safe }) => {
|
|
294
|
+
const [err, value] = safe(() => {
|
|
295
|
+
throw new Error("Test error");
|
|
296
|
+
});
|
|
297
|
+
return { err, value };
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result.value?.err).toBeInstanceOf(Error);
|
|
301
|
+
expect((result.value?.err as Error).message).toBe("Test error");
|
|
302
|
+
expect(result.value?.value).toBe(undefined);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should re-throw Promise to preserve Suspense", () => {
|
|
306
|
+
const pending$ = atom(new Promise(() => {})); // Never resolves
|
|
307
|
+
|
|
308
|
+
const result = select(({ read, safe }) => {
|
|
309
|
+
const [err, value] = safe(() => read(pending$));
|
|
310
|
+
return { err, value };
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Promise should be thrown, not caught
|
|
314
|
+
expect(result.promise).toBeDefined();
|
|
315
|
+
expect(result.value).toBe(undefined);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should catch JSON.parse errors", () => {
|
|
319
|
+
const raw$ = atom("invalid json");
|
|
320
|
+
|
|
321
|
+
const result = select(({ read, safe }) => {
|
|
322
|
+
const [err, data] = safe(() => {
|
|
323
|
+
const raw = read(raw$);
|
|
324
|
+
return JSON.parse(raw);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (err) {
|
|
328
|
+
return { error: "Parse failed" };
|
|
329
|
+
}
|
|
330
|
+
return { data };
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(result.value).toEqual({ error: "Parse failed" });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should allow graceful degradation", () => {
|
|
337
|
+
const user$ = atom({ name: "John" });
|
|
338
|
+
|
|
339
|
+
const result = select(({ read, safe }) => {
|
|
340
|
+
const [err1, user] = safe(() => read(user$));
|
|
341
|
+
if (err1) return { user: null, posts: [] };
|
|
342
|
+
|
|
343
|
+
const [err2] = safe(() => {
|
|
344
|
+
throw new Error("Posts failed");
|
|
345
|
+
});
|
|
346
|
+
if (err2) return { user, posts: [] }; // Graceful degradation
|
|
347
|
+
|
|
348
|
+
return { user, posts: ["post1"] };
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(result.value).toEqual({
|
|
352
|
+
user: { name: "John" },
|
|
353
|
+
posts: [], // Gracefully degraded
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should preserve error type information", () => {
|
|
358
|
+
class CustomError extends Error {
|
|
359
|
+
code = "CUSTOM";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const result = select(({ safe }) => {
|
|
363
|
+
const [err] = safe(() => {
|
|
364
|
+
throw new CustomError("Custom error");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (err instanceof CustomError) {
|
|
368
|
+
return { code: err.code };
|
|
369
|
+
}
|
|
370
|
+
return { code: "UNKNOWN" };
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(result.value).toEqual({ code: "CUSTOM" });
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("async context detection", () => {
|
|
378
|
+
it("should throw error when read() is called outside selection context", async () => {
|
|
379
|
+
const count$ = atom(5);
|
|
380
|
+
let capturedRead: ((atom: typeof count$) => number) | null = null;
|
|
381
|
+
|
|
382
|
+
select(({ read }) => {
|
|
383
|
+
capturedRead = read;
|
|
384
|
+
return read(count$);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Calling read() after select() has finished should throw
|
|
388
|
+
expect(() => capturedRead!(count$)).toThrow(
|
|
389
|
+
"read() was called outside of the selection context"
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should throw error when all() is called outside selection context", async () => {
|
|
394
|
+
const a$ = atom(1);
|
|
395
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
396
|
+
let capturedAll: any = null;
|
|
397
|
+
|
|
398
|
+
select(({ all }) => {
|
|
399
|
+
capturedAll = all;
|
|
400
|
+
return all([a$]);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(() => capturedAll([a$])).toThrow(
|
|
404
|
+
"all() was called outside of the selection context"
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should throw error when race() is called outside selection context", async () => {
|
|
409
|
+
const a$ = atom(1);
|
|
410
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
411
|
+
let capturedRace: any = null;
|
|
412
|
+
|
|
413
|
+
select(({ race }) => {
|
|
414
|
+
capturedRace = race;
|
|
415
|
+
return race({ a: a$ });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(() => capturedRace({ a: a$ })).toThrow(
|
|
419
|
+
"race() was called outside of the selection context"
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should throw error when any() is called outside selection context", async () => {
|
|
424
|
+
const a$ = atom(1);
|
|
425
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
|
+
let capturedAny: any = null;
|
|
427
|
+
|
|
428
|
+
select(({ any }) => {
|
|
429
|
+
capturedAny = any;
|
|
430
|
+
return any({ a: a$ });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(() => capturedAny({ a: a$ })).toThrow(
|
|
434
|
+
"any() was called outside of the selection context"
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("should throw error when settled() is called outside selection context", async () => {
|
|
439
|
+
const a$ = atom(1);
|
|
440
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
441
|
+
let capturedSettled: any = null;
|
|
442
|
+
|
|
443
|
+
select(({ settled }) => {
|
|
444
|
+
capturedSettled = settled;
|
|
445
|
+
return settled([a$]);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(() => capturedSettled([a$])).toThrow(
|
|
449
|
+
"settled() was called outside of the selection context"
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should throw error when safe() is called outside selection context", async () => {
|
|
454
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
455
|
+
let capturedSafe: any = null;
|
|
456
|
+
|
|
457
|
+
select(({ safe }) => {
|
|
458
|
+
capturedSafe = safe;
|
|
459
|
+
return safe(() => 42);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(() => capturedSafe(() => 42)).toThrow(
|
|
463
|
+
"safe() was called outside of the selection context"
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("state()", () => {
|
|
469
|
+
it("should return ready state for sync atom", () => {
|
|
470
|
+
const count$ = atom(5);
|
|
471
|
+
|
|
472
|
+
const result = select(({ state }) => state(count$));
|
|
473
|
+
|
|
474
|
+
expect(result.value).toEqual({ status: "ready", value: 5 });
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should return loading state for pending promise atom", () => {
|
|
478
|
+
const promise = new Promise<number>(() => {});
|
|
479
|
+
const async$ = atom(promise);
|
|
480
|
+
|
|
481
|
+
const result = select(({ state }) => state(async$));
|
|
482
|
+
|
|
483
|
+
expect(result.value).toEqual({
|
|
484
|
+
status: "loading",
|
|
485
|
+
value: undefined,
|
|
486
|
+
error: undefined,
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should return error state for rejected promise atom", async () => {
|
|
491
|
+
const error = new Error("Test error");
|
|
492
|
+
const rejectedPromise = Promise.reject(error);
|
|
493
|
+
rejectedPromise.catch(() => {});
|
|
494
|
+
const async$ = atom(rejectedPromise);
|
|
495
|
+
|
|
496
|
+
// Track first
|
|
497
|
+
select(({ state }) => state(async$));
|
|
498
|
+
await Promise.resolve();
|
|
499
|
+
await Promise.resolve();
|
|
500
|
+
|
|
501
|
+
const result = select(({ state }) => state(async$));
|
|
502
|
+
|
|
503
|
+
expect(result.value).toEqual({ status: "error", error });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("should track dependencies when using state(atom)", () => {
|
|
507
|
+
const a$ = atom(1);
|
|
508
|
+
const b$ = atom(2);
|
|
509
|
+
|
|
510
|
+
const result = select(({ state }) => {
|
|
511
|
+
state(a$);
|
|
512
|
+
state(b$);
|
|
513
|
+
return "done";
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(result.dependencies.size).toBe(2);
|
|
517
|
+
expect(result.dependencies.has(a$)).toBe(true);
|
|
518
|
+
expect(result.dependencies.has(b$)).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("should wrap selector function with try/catch and return ready state", () => {
|
|
522
|
+
const a$ = atom(10);
|
|
523
|
+
const b$ = atom(20);
|
|
524
|
+
|
|
525
|
+
const result = select(({ read, state }) =>
|
|
526
|
+
state(() => read(a$) + read(b$))
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(result.value).toEqual({ status: "ready", value: 30 });
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("should wrap selector function and return loading state when promise thrown", () => {
|
|
533
|
+
const async$ = atom(new Promise<number>(() => {}));
|
|
534
|
+
|
|
535
|
+
const result = select(({ read, state }) => state(() => read(async$)));
|
|
536
|
+
|
|
537
|
+
expect(result.value).toEqual({
|
|
538
|
+
status: "loading",
|
|
539
|
+
value: undefined,
|
|
540
|
+
error: undefined,
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("should wrap selector function and return error state when error thrown", () => {
|
|
545
|
+
const result = select(({ state }) =>
|
|
546
|
+
state(() => {
|
|
547
|
+
throw new Error("Test error");
|
|
548
|
+
})
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(result.value?.status).toBe("error");
|
|
552
|
+
expect(
|
|
553
|
+
(result.value as { status: "error"; error: unknown }).error
|
|
554
|
+
).toBeInstanceOf(Error);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("should work with all() inside state()", () => {
|
|
558
|
+
const a$ = atom(1);
|
|
559
|
+
const b$ = atom(2);
|
|
560
|
+
|
|
561
|
+
const result = select(({ all, state }) => state(() => all([a$, b$])));
|
|
562
|
+
|
|
563
|
+
expect(result.value).toEqual({
|
|
564
|
+
status: "ready",
|
|
565
|
+
value: [1, 2],
|
|
566
|
+
error: undefined,
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should return loading state when all() has pending atoms", () => {
|
|
571
|
+
const a$ = atom(1);
|
|
572
|
+
const b$ = atom(new Promise<number>(() => {}));
|
|
573
|
+
|
|
574
|
+
const result = select(({ all, state }) => state(() => all([a$, b$])));
|
|
575
|
+
|
|
576
|
+
expect(result.value?.status).toBe("loading");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("should allow building dashboard-style derived atoms", () => {
|
|
580
|
+
const user$ = atom({ name: "John" });
|
|
581
|
+
const posts$ = atom(new Promise<string[]>(() => {})); // Loading
|
|
582
|
+
|
|
583
|
+
const result = select(({ state }) => {
|
|
584
|
+
const userState = state(user$);
|
|
585
|
+
const postsState = state(posts$);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
user: userState.status === "ready" ? userState.value : null,
|
|
589
|
+
posts: postsState.status === "ready" ? postsState.value : [],
|
|
590
|
+
isLoading:
|
|
591
|
+
userState.status === "loading" || postsState.status === "loading",
|
|
592
|
+
};
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(result.value).toEqual({
|
|
596
|
+
user: { name: "John" },
|
|
597
|
+
posts: [],
|
|
598
|
+
isLoading: true,
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("should throw error when called outside selection context", () => {
|
|
603
|
+
const a$ = atom(1);
|
|
604
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
605
|
+
let capturedState: any = null;
|
|
606
|
+
|
|
607
|
+
select(({ state }) => {
|
|
608
|
+
capturedState = state;
|
|
609
|
+
return state(a$);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
expect(() => capturedState(a$)).toThrow(
|
|
613
|
+
"state() was called outside of the selection context"
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe("parallel waiting behavior", () => {
|
|
619
|
+
it("all() should throw combined Promise.all for parallel waiting", async () => {
|
|
620
|
+
let resolve1: (value: number) => void;
|
|
621
|
+
let resolve2: (value: number) => void;
|
|
622
|
+
const p1 = new Promise<number>((r) => {
|
|
623
|
+
resolve1 = r;
|
|
624
|
+
});
|
|
625
|
+
const p2 = new Promise<number>((r) => {
|
|
626
|
+
resolve2 = r;
|
|
627
|
+
});
|
|
628
|
+
const a$ = atom(p1);
|
|
629
|
+
const b$ = atom(p2);
|
|
630
|
+
|
|
631
|
+
// First call should throw a combined promise
|
|
632
|
+
const result1 = select(({ all }) => all([a$, b$]));
|
|
633
|
+
expect(result1.promise).toBeDefined();
|
|
634
|
+
|
|
635
|
+
// Resolve promises in reverse order
|
|
636
|
+
resolve2!(20);
|
|
637
|
+
await Promise.resolve();
|
|
638
|
+
|
|
639
|
+
// Still loading (a$ not resolved yet)
|
|
640
|
+
const result2 = select(({ all }) => all([a$, b$]));
|
|
641
|
+
expect(result2.promise).toBeDefined();
|
|
642
|
+
|
|
643
|
+
// Resolve first promise
|
|
644
|
+
resolve1!(10);
|
|
645
|
+
await Promise.resolve();
|
|
646
|
+
await Promise.resolve();
|
|
647
|
+
|
|
648
|
+
// Now should be ready with both values
|
|
649
|
+
const result3 = select(({ all }) => all([a$, b$]));
|
|
650
|
+
expect(result3.value).toEqual([10, 20]);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("all() should return equivalent promises when atoms unchanged", async () => {
|
|
654
|
+
const p1 = new Promise<number>(() => {});
|
|
655
|
+
const p2 = new Promise<number>(() => {});
|
|
656
|
+
const a$ = atom(p1);
|
|
657
|
+
const b$ = atom(p2);
|
|
658
|
+
|
|
659
|
+
// Get promise from first call
|
|
660
|
+
const result1 = select(({ all }) => all([a$, b$]));
|
|
661
|
+
const promise1 = result1.promise;
|
|
662
|
+
|
|
663
|
+
// Second call should return equivalent promise (same source promises)
|
|
664
|
+
const result2 = select(({ all }) => all([a$, b$]));
|
|
665
|
+
const promise2 = result2.promise;
|
|
666
|
+
|
|
667
|
+
// Promises are equivalent via metadata comparison
|
|
668
|
+
expect(promisesEqual(promise1, promise2)).toBe(true);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("race() should throw combined Promise.race for parallel racing", async () => {
|
|
672
|
+
let resolve1: (value: number) => void;
|
|
673
|
+
let resolve2: (value: number) => void;
|
|
674
|
+
const p1 = new Promise<number>((r) => {
|
|
675
|
+
resolve1 = r;
|
|
676
|
+
});
|
|
677
|
+
const p2 = new Promise<number>((r) => {
|
|
678
|
+
resolve2 = r;
|
|
679
|
+
});
|
|
680
|
+
const a$ = atom(p1);
|
|
681
|
+
const b$ = atom(p2);
|
|
682
|
+
|
|
683
|
+
// First call should throw a combined promise
|
|
684
|
+
const result1 = select(({ race }) => race({ a: a$, b: b$ }));
|
|
685
|
+
expect(result1.promise).toBeDefined();
|
|
686
|
+
|
|
687
|
+
// Resolve second promise first (it should win the race)
|
|
688
|
+
resolve2!(20);
|
|
689
|
+
await Promise.resolve();
|
|
690
|
+
await Promise.resolve();
|
|
691
|
+
|
|
692
|
+
// Race should return second value (first to resolve) with key
|
|
693
|
+
const result2 = select(({ race }) => race({ a: a$, b: b$ }));
|
|
694
|
+
expect(result2.value).toEqual({ key: "b", value: 20 });
|
|
695
|
+
|
|
696
|
+
// Clean up: resolve first promise
|
|
697
|
+
resolve1!(10);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("race() should return equivalent promises when atoms unchanged", async () => {
|
|
701
|
+
const p1 = new Promise<number>(() => {});
|
|
702
|
+
const p2 = new Promise<number>(() => {});
|
|
703
|
+
const a$ = atom(p1);
|
|
704
|
+
const b$ = atom(p2);
|
|
705
|
+
|
|
706
|
+
const result1 = select(({ race }) => race({ a: a$, b: b$ }));
|
|
707
|
+
const promise1 = result1.promise;
|
|
708
|
+
|
|
709
|
+
const result2 = select(({ race }) => race({ a: a$, b: b$ }));
|
|
710
|
+
const promise2 = result2.promise;
|
|
711
|
+
|
|
712
|
+
// Promises are equivalent via metadata comparison
|
|
713
|
+
expect(promisesEqual(promise1, promise2)).toBe(true);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("any() should race all loading promises in parallel", async () => {
|
|
717
|
+
let resolve1: (value: number) => void;
|
|
718
|
+
let resolve2: (value: number) => void;
|
|
719
|
+
const p1 = new Promise<number>((r) => {
|
|
720
|
+
resolve1 = r;
|
|
721
|
+
});
|
|
722
|
+
const p2 = new Promise<number>((r) => {
|
|
723
|
+
resolve2 = r;
|
|
724
|
+
});
|
|
725
|
+
const a$ = atom(p1);
|
|
726
|
+
const b$ = atom(p2);
|
|
727
|
+
|
|
728
|
+
// First call should throw combined race promise
|
|
729
|
+
const result1 = select(({ any }) => any({ a: a$, b: b$ }));
|
|
730
|
+
expect(result1.promise).toBeDefined();
|
|
731
|
+
|
|
732
|
+
// Resolve second promise first
|
|
733
|
+
resolve2!(20);
|
|
734
|
+
await Promise.resolve();
|
|
735
|
+
await Promise.resolve();
|
|
736
|
+
|
|
737
|
+
// any() should return second value (first to resolve) with key
|
|
738
|
+
const result2 = select(({ any }) => any({ a: a$, b: b$ }));
|
|
739
|
+
expect(result2.value).toEqual({ key: "b", value: 20 });
|
|
740
|
+
|
|
741
|
+
// Clean up
|
|
742
|
+
resolve1!(10);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("settled() should wait for all in parallel", async () => {
|
|
746
|
+
let resolve1: (value: number) => void;
|
|
747
|
+
let reject2: (error: Error) => void;
|
|
748
|
+
const p1 = new Promise<number>((r) => {
|
|
749
|
+
resolve1 = r;
|
|
750
|
+
});
|
|
751
|
+
const p2 = new Promise<number>((_, reject) => {
|
|
752
|
+
reject2 = reject;
|
|
753
|
+
});
|
|
754
|
+
p2.catch(() => {}); // Prevent unhandled rejection
|
|
755
|
+
const a$ = atom(p1);
|
|
756
|
+
const b$ = atom(p2);
|
|
757
|
+
|
|
758
|
+
// First call should throw combined promise
|
|
759
|
+
const result1 = select(({ settled }) => settled([a$, b$]));
|
|
760
|
+
expect(result1.promise).toBeDefined();
|
|
761
|
+
|
|
762
|
+
// Settle promises in any order
|
|
763
|
+
reject2!(new Error("fail"));
|
|
764
|
+
await Promise.resolve();
|
|
765
|
+
|
|
766
|
+
// Still loading (a$ not settled yet)
|
|
767
|
+
const result2 = select(({ settled }) => settled([a$, b$]));
|
|
768
|
+
expect(result2.promise).toBeDefined();
|
|
769
|
+
|
|
770
|
+
// Settle first
|
|
771
|
+
resolve1!(10);
|
|
772
|
+
await Promise.resolve();
|
|
773
|
+
await Promise.resolve();
|
|
774
|
+
|
|
775
|
+
// Now should have settled results
|
|
776
|
+
const result3 = select(({ settled }) => settled([a$, b$]));
|
|
777
|
+
expect(result3.value).toEqual([
|
|
778
|
+
{ status: "ready", value: 10 },
|
|
779
|
+
{ status: "error", error: expect.any(Error) },
|
|
780
|
+
]);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("settled() should return equivalent promises when atoms unchanged", async () => {
|
|
784
|
+
const p1 = new Promise<number>(() => {});
|
|
785
|
+
const p2 = new Promise<number>(() => {});
|
|
786
|
+
const a$ = atom(p1);
|
|
787
|
+
const b$ = atom(p2);
|
|
788
|
+
|
|
789
|
+
const result1 = select(({ settled }) => settled([a$, b$]));
|
|
790
|
+
const promise1 = result1.promise;
|
|
791
|
+
|
|
792
|
+
const result2 = select(({ settled }) => settled([a$, b$]));
|
|
793
|
+
const promise2 = result2.promise;
|
|
794
|
+
|
|
795
|
+
// Promises are equivalent via metadata comparison
|
|
796
|
+
expect(promisesEqual(promise1, promise2)).toBe(true);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
257
799
|
});
|