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
|
@@ -1,799 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { atom } from "./atom";
|
|
3
|
-
import { select } from "./select";
|
|
4
|
-
import { promisesEqual } from "./promiseCache";
|
|
5
|
-
|
|
6
|
-
describe("select", () => {
|
|
7
|
-
describe("read()", () => {
|
|
8
|
-
it("should read value from sync atom", () => {
|
|
9
|
-
const count$ = atom(5);
|
|
10
|
-
const result = select(({ read }) => read(count$));
|
|
11
|
-
|
|
12
|
-
expect(result.value).toBe(5);
|
|
13
|
-
expect(result.error).toBe(undefined);
|
|
14
|
-
expect(result.promise).toBe(undefined);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should track dependencies", () => {
|
|
18
|
-
const a$ = atom(1);
|
|
19
|
-
const b$ = atom(2);
|
|
20
|
-
|
|
21
|
-
const result = select(({ read }) => read(a$) + read(b$));
|
|
22
|
-
|
|
23
|
-
expect(result.dependencies.size).toBe(2);
|
|
24
|
-
expect(result.dependencies.has(a$)).toBe(true);
|
|
25
|
-
expect(result.dependencies.has(b$)).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should throw error if computation throws", () => {
|
|
29
|
-
const count$ = atom(5);
|
|
30
|
-
const error = new Error("Test error");
|
|
31
|
-
|
|
32
|
-
const result = select(({ read }) => {
|
|
33
|
-
read(count$);
|
|
34
|
-
throw error;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(result.value).toBe(undefined);
|
|
38
|
-
expect(result.error).toBe(error);
|
|
39
|
-
expect(result.promise).toBe(undefined);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("all()", () => {
|
|
44
|
-
it("should return array of values for all sync atoms", () => {
|
|
45
|
-
const a$ = atom(1);
|
|
46
|
-
const b$ = atom(2);
|
|
47
|
-
const c$ = atom(3);
|
|
48
|
-
|
|
49
|
-
const result = select(({ all }) => all([a$, b$, c$]));
|
|
50
|
-
|
|
51
|
-
expect(result.value).toEqual([1, 2, 3]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("should throw promise if any atom is pending", () => {
|
|
55
|
-
const a$ = atom(1);
|
|
56
|
-
const b$ = atom(new Promise<number>(() => {}));
|
|
57
|
-
|
|
58
|
-
const result = select(({ all }) => all([a$, b$]));
|
|
59
|
-
|
|
60
|
-
expect(result.promise).toBeDefined();
|
|
61
|
-
expect(result.value).toBe(undefined);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("should throw error if any atom has rejected promise", async () => {
|
|
65
|
-
const error = new Error("Test error");
|
|
66
|
-
const a$ = atom(1);
|
|
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
|
-
});
|
|
72
|
-
rejectedPromise.catch(() => {}); // Prevent unhandled rejection
|
|
73
|
-
rejectFn!(error);
|
|
74
|
-
const b$ = atom(rejectedPromise);
|
|
75
|
-
|
|
76
|
-
// First call to select tracks the promise but returns pending
|
|
77
|
-
select(({ all }) => all([a$, b$]));
|
|
78
|
-
|
|
79
|
-
// Wait for promise handlers to run
|
|
80
|
-
await Promise.resolve();
|
|
81
|
-
await Promise.resolve();
|
|
82
|
-
|
|
83
|
-
// Now the promise state should be updated
|
|
84
|
-
const result = select(({ all }) => all([a$, b$]));
|
|
85
|
-
|
|
86
|
-
expect(result.error).toBe(error);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe("race()", () => {
|
|
91
|
-
it("should return first fulfilled value with key", () => {
|
|
92
|
-
const a$ = atom(1);
|
|
93
|
-
const b$ = atom(2);
|
|
94
|
-
|
|
95
|
-
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
96
|
-
|
|
97
|
-
expect(result.value).toEqual({ key: "a", value: 1 });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("should throw first error if first atom is rejected", async () => {
|
|
101
|
-
const error = new Error("Test error");
|
|
102
|
-
const rejectedPromise = Promise.reject(error);
|
|
103
|
-
rejectedPromise.catch(() => {});
|
|
104
|
-
const a$ = atom(rejectedPromise);
|
|
105
|
-
const b$ = atom(2);
|
|
106
|
-
|
|
107
|
-
// Track the promise first
|
|
108
|
-
select(({ race }) => race({ a: a$, b: b$ }));
|
|
109
|
-
await Promise.resolve();
|
|
110
|
-
await Promise.resolve();
|
|
111
|
-
|
|
112
|
-
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
113
|
-
|
|
114
|
-
expect(result.error).toBe(error);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should throw promise if all are pending", () => {
|
|
118
|
-
const a$ = atom(new Promise<number>(() => {}));
|
|
119
|
-
const b$ = atom(new Promise<number>(() => {}));
|
|
120
|
-
|
|
121
|
-
const result = select(({ race }) => race({ a: a$, b: b$ }));
|
|
122
|
-
|
|
123
|
-
expect(result.promise).toBeDefined();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("any()", () => {
|
|
128
|
-
it("should return first fulfilled value with key", () => {
|
|
129
|
-
const a$ = atom(1);
|
|
130
|
-
const b$ = atom(2);
|
|
131
|
-
|
|
132
|
-
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
133
|
-
|
|
134
|
-
expect(result.value).toEqual({ key: "a", value: 1 });
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("should skip rejected and return next fulfilled with key", async () => {
|
|
138
|
-
const error = new Error("Test error");
|
|
139
|
-
const rejectedPromise = Promise.reject(error);
|
|
140
|
-
rejectedPromise.catch(() => {});
|
|
141
|
-
const a$ = atom(rejectedPromise);
|
|
142
|
-
const b$ = atom(2);
|
|
143
|
-
|
|
144
|
-
// Track first, then wait for microtasks
|
|
145
|
-
select(({ any }) => any({ a: a$, b: b$ }));
|
|
146
|
-
await Promise.resolve();
|
|
147
|
-
await Promise.resolve();
|
|
148
|
-
|
|
149
|
-
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
150
|
-
|
|
151
|
-
expect(result.value).toEqual({ key: "b", value: 2 });
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("should throw AggregateError if all rejected", async () => {
|
|
155
|
-
const error1 = new Error("Error 1");
|
|
156
|
-
const error2 = new Error("Error 2");
|
|
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
|
-
});
|
|
166
|
-
p1.catch(() => {});
|
|
167
|
-
p2.catch(() => {});
|
|
168
|
-
reject1!(error1);
|
|
169
|
-
reject2!(error2);
|
|
170
|
-
|
|
171
|
-
const a$ = atom(p1);
|
|
172
|
-
const b$ = atom(p2);
|
|
173
|
-
|
|
174
|
-
// Track first, then wait for microtasks
|
|
175
|
-
select(({ any }) => any({ a: a$, b: b$ }));
|
|
176
|
-
await Promise.resolve();
|
|
177
|
-
await Promise.resolve();
|
|
178
|
-
|
|
179
|
-
const result = select(({ any }) => any({ a: a$, b: b$ }));
|
|
180
|
-
|
|
181
|
-
expect(result.error).toBeDefined();
|
|
182
|
-
expect((result.error as Error).name).toBe("AggregateError");
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe("settled()", () => {
|
|
187
|
-
it("should return array of settled results", async () => {
|
|
188
|
-
const a$ = atom(1);
|
|
189
|
-
const error = new Error("Test error");
|
|
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
|
-
});
|
|
195
|
-
rejectedPromise.catch(() => {});
|
|
196
|
-
rejectFn!(error);
|
|
197
|
-
const b$ = atom(rejectedPromise);
|
|
198
|
-
|
|
199
|
-
// Track first, wait for microtasks
|
|
200
|
-
select(({ settled }) => settled([a$, b$]));
|
|
201
|
-
await Promise.resolve();
|
|
202
|
-
await Promise.resolve();
|
|
203
|
-
|
|
204
|
-
const result = select(({ settled }) => settled([a$, b$]));
|
|
205
|
-
|
|
206
|
-
expect(result.value).toEqual([
|
|
207
|
-
{ status: "ready", value: 1 },
|
|
208
|
-
{ status: "error", error },
|
|
209
|
-
]);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("should throw promise if any atom is pending", () => {
|
|
213
|
-
const a$ = atom(1);
|
|
214
|
-
const b$ = atom(new Promise<number>(() => {}));
|
|
215
|
-
|
|
216
|
-
const result = select(({ settled }) => settled([a$, b$]));
|
|
217
|
-
|
|
218
|
-
expect(result.promise).toBeDefined();
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("conditional dependencies", () => {
|
|
223
|
-
it("should only track accessed atoms", () => {
|
|
224
|
-
const condition$ = atom(false);
|
|
225
|
-
const a$ = atom(1);
|
|
226
|
-
const b$ = atom(2);
|
|
227
|
-
|
|
228
|
-
const result = select(({ read }) =>
|
|
229
|
-
read(condition$) ? read(a$) : read(b$)
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
expect(result.dependencies.size).toBe(2);
|
|
233
|
-
expect(result.dependencies.has(condition$)).toBe(true);
|
|
234
|
-
expect(result.dependencies.has(b$)).toBe(true);
|
|
235
|
-
// a$ was not accessed because condition was false
|
|
236
|
-
expect(result.dependencies.has(a$)).toBe(false);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe("error handling", () => {
|
|
241
|
-
it("should throw error if selector returns a Promise", () => {
|
|
242
|
-
const result = select(() => Promise.resolve(42));
|
|
243
|
-
|
|
244
|
-
expect(result.error).toBeDefined();
|
|
245
|
-
expect(result.error).toBeInstanceOf(Error);
|
|
246
|
-
expect((result.error as Error).message).toContain(
|
|
247
|
-
"select() selector must return a synchronous value"
|
|
248
|
-
);
|
|
249
|
-
expect(result.value).toBe(undefined);
|
|
250
|
-
expect(result.promise).toBe(undefined);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("should throw error if selector returns a PromiseLike", () => {
|
|
254
|
-
// Custom PromiseLike object
|
|
255
|
-
const promiseLike = {
|
|
256
|
-
then: (resolve: (value: number) => void) => {
|
|
257
|
-
resolve(42);
|
|
258
|
-
return promiseLike;
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const result = select(() => promiseLike);
|
|
263
|
-
|
|
264
|
-
expect(result.error).toBeDefined();
|
|
265
|
-
expect(result.error).toBeInstanceOf(Error);
|
|
266
|
-
expect((result.error as Error).message).toContain(
|
|
267
|
-
"select() selector must return a synchronous value"
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("should work fine with sync values", () => {
|
|
272
|
-
const result = select(() => 42);
|
|
273
|
-
|
|
274
|
-
expect(result.value).toBe(42);
|
|
275
|
-
expect(result.error).toBe(undefined);
|
|
276
|
-
expect(result.promise).toBe(undefined);
|
|
277
|
-
});
|
|
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
|
-
});
|
|
799
|
-
});
|