atomirx 0.0.8 → 0.1.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 +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 -1
- package/dist/react/index.js +191 -151
- 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 -42
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { atom } from "./atom";
|
|
3
|
-
import { derived } from "./derived";
|
|
4
|
-
import { effect } from "./effect";
|
|
5
|
-
import { onErrorHook, ErrorInfo } from "./onErrorHook";
|
|
6
|
-
|
|
7
|
-
describe("onErrorHook", () => {
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
onErrorHook.reset();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
onErrorHook.reset();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe("with derived", () => {
|
|
17
|
-
it("should call onErrorHook when derived throws synchronously", async () => {
|
|
18
|
-
const hookFn = vi.fn();
|
|
19
|
-
onErrorHook.override(() => hookFn);
|
|
20
|
-
|
|
21
|
-
const source$ = atom(0);
|
|
22
|
-
const derived$ = derived(
|
|
23
|
-
({ read }) => {
|
|
24
|
-
const val = read(source$);
|
|
25
|
-
if (val > 0) {
|
|
26
|
-
throw new Error("Derived error");
|
|
27
|
-
}
|
|
28
|
-
return val;
|
|
29
|
-
},
|
|
30
|
-
{ meta: { key: "testDerived" } }
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
await derived$.get();
|
|
34
|
-
expect(hookFn).not.toHaveBeenCalled();
|
|
35
|
-
|
|
36
|
-
// Trigger error
|
|
37
|
-
source$.set(5);
|
|
38
|
-
derived$.get().catch(() => {});
|
|
39
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
40
|
-
|
|
41
|
-
expect(hookFn).toHaveBeenCalledTimes(1);
|
|
42
|
-
const info: ErrorInfo = hookFn.mock.calls[0][0];
|
|
43
|
-
expect(info.source.type).toBe("derived");
|
|
44
|
-
expect(info.source.key).toBe("testDerived");
|
|
45
|
-
expect((info.error as Error).message).toBe("Derived error");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should call onErrorHook when async dependency rejects", async () => {
|
|
49
|
-
const hookFn = vi.fn();
|
|
50
|
-
onErrorHook.override(() => hookFn);
|
|
51
|
-
|
|
52
|
-
const asyncSource$ = atom(Promise.reject(new Error("Async error")));
|
|
53
|
-
|
|
54
|
-
const derived$ = derived(({ read }) => read(asyncSource$), {
|
|
55
|
-
meta: { key: "asyncDerived" },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
derived$.get().catch(() => {});
|
|
59
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
60
|
-
|
|
61
|
-
expect(hookFn).toHaveBeenCalledTimes(1);
|
|
62
|
-
const info: ErrorInfo = hookFn.mock.calls[0][0];
|
|
63
|
-
expect(info.source.type).toBe("derived");
|
|
64
|
-
expect(info.source.key).toBe("asyncDerived");
|
|
65
|
-
expect((info.error as Error).message).toBe("Async error");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("should include derived atom in source", async () => {
|
|
69
|
-
const hookFn = vi.fn();
|
|
70
|
-
onErrorHook.override(() => hookFn);
|
|
71
|
-
|
|
72
|
-
const source$ = atom(1);
|
|
73
|
-
const derived$ = derived(({ read }) => {
|
|
74
|
-
throw new Error("Test");
|
|
75
|
-
return read(source$);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
derived$.get().catch(() => {});
|
|
79
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
80
|
-
|
|
81
|
-
const info: ErrorInfo = hookFn.mock.calls[0][0];
|
|
82
|
-
expect(info.source.type).toBe("derived");
|
|
83
|
-
if (info.source.type === "derived") {
|
|
84
|
-
expect(info.source.instance).toBe(derived$);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("should call both onError option and onErrorHook", async () => {
|
|
89
|
-
const hookFn = vi.fn();
|
|
90
|
-
const onErrorFn = vi.fn();
|
|
91
|
-
onErrorHook.override(() => hookFn);
|
|
92
|
-
|
|
93
|
-
const source$ = atom(0);
|
|
94
|
-
const derived$ = derived(
|
|
95
|
-
({ read }) => {
|
|
96
|
-
const val = read(source$);
|
|
97
|
-
if (val > 0) throw new Error("Error");
|
|
98
|
-
return val;
|
|
99
|
-
},
|
|
100
|
-
{ onError: onErrorFn }
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
await derived$.get();
|
|
104
|
-
source$.set(1);
|
|
105
|
-
derived$.get().catch(() => {});
|
|
106
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
107
|
-
|
|
108
|
-
expect(onErrorFn).toHaveBeenCalledTimes(1);
|
|
109
|
-
expect(hookFn).toHaveBeenCalledTimes(1);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("with effect", () => {
|
|
114
|
-
it("should call onErrorHook with effect source when effect throws", async () => {
|
|
115
|
-
const hookFn = vi.fn();
|
|
116
|
-
onErrorHook.override(() => hookFn);
|
|
117
|
-
|
|
118
|
-
const source$ = atom(0);
|
|
119
|
-
|
|
120
|
-
effect(
|
|
121
|
-
({ read }) => {
|
|
122
|
-
const val = read(source$);
|
|
123
|
-
if (val > 0) {
|
|
124
|
-
throw new Error("Effect error");
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
{ meta: { key: "testEffect" } }
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
131
|
-
expect(hookFn).not.toHaveBeenCalled();
|
|
132
|
-
|
|
133
|
-
// Trigger error
|
|
134
|
-
source$.set(5);
|
|
135
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
136
|
-
|
|
137
|
-
expect(hookFn).toHaveBeenCalledTimes(1);
|
|
138
|
-
const info: ErrorInfo = hookFn.mock.calls[0][0];
|
|
139
|
-
expect(info.source.type).toBe("effect"); // Should be effect, not derived!
|
|
140
|
-
expect(info.source.key).toBe("testEffect");
|
|
141
|
-
expect((info.error as Error).message).toBe("Effect error");
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("should include effect instance in source", async () => {
|
|
145
|
-
const hookFn = vi.fn();
|
|
146
|
-
onErrorHook.override(() => hookFn);
|
|
147
|
-
|
|
148
|
-
const source$ = atom(1);
|
|
149
|
-
const e = effect(
|
|
150
|
-
({ read }) => {
|
|
151
|
-
read(source$);
|
|
152
|
-
throw new Error("Test");
|
|
153
|
-
},
|
|
154
|
-
{ meta: { key: "myEffect" } }
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
158
|
-
|
|
159
|
-
const info: ErrorInfo = hookFn.mock.calls[0][0];
|
|
160
|
-
expect(info.source.type).toBe("effect");
|
|
161
|
-
if (info.source.type === "effect") {
|
|
162
|
-
expect(info.source.instance).toBe(e);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("should call both onError option and onErrorHook for effect", async () => {
|
|
167
|
-
const hookFn = vi.fn();
|
|
168
|
-
const onErrorFn = vi.fn();
|
|
169
|
-
onErrorHook.override(() => hookFn);
|
|
170
|
-
|
|
171
|
-
const source$ = atom(0);
|
|
172
|
-
|
|
173
|
-
effect(
|
|
174
|
-
({ read }) => {
|
|
175
|
-
const val = read(source$);
|
|
176
|
-
if (val > 0) throw new Error("Error");
|
|
177
|
-
},
|
|
178
|
-
{ onError: onErrorFn }
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
182
|
-
source$.set(1);
|
|
183
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
184
|
-
|
|
185
|
-
expect(onErrorFn).toHaveBeenCalledTimes(1);
|
|
186
|
-
expect(hookFn).toHaveBeenCalledTimes(1);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("hook behavior", () => {
|
|
191
|
-
it("should not throw when onErrorHook is not set", async () => {
|
|
192
|
-
// Hook is reset in beforeEach, so no handler is set
|
|
193
|
-
|
|
194
|
-
const source$ = atom(1);
|
|
195
|
-
const derived$ = derived(({ read }) => {
|
|
196
|
-
throw new Error("Test");
|
|
197
|
-
return read(source$);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Should not throw
|
|
201
|
-
derived$.get().catch(() => {});
|
|
202
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("should support middleware pattern with override", async () => {
|
|
206
|
-
const errors: ErrorInfo[] = [];
|
|
207
|
-
|
|
208
|
-
// First handler
|
|
209
|
-
onErrorHook.override(() => (info) => {
|
|
210
|
-
errors.push({ ...info, error: "first" });
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Add middleware - chains with previous
|
|
214
|
-
onErrorHook.override((prev) => (info) => {
|
|
215
|
-
prev?.(info);
|
|
216
|
-
errors.push({ ...info, error: "second" });
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const source$ = atom(1);
|
|
220
|
-
derived(({ read }) => {
|
|
221
|
-
throw new Error("Test");
|
|
222
|
-
return read(source$);
|
|
223
|
-
})
|
|
224
|
-
.get()
|
|
225
|
-
.catch(() => {});
|
|
226
|
-
|
|
227
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
228
|
-
|
|
229
|
-
expect(errors.length).toBe(2);
|
|
230
|
-
expect(errors[0].error).toBe("first");
|
|
231
|
-
expect(errors[1].error).toBe("second");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("should support reset", async () => {
|
|
235
|
-
const hookFn = vi.fn();
|
|
236
|
-
onErrorHook.override(() => hookFn);
|
|
237
|
-
|
|
238
|
-
onErrorHook.reset();
|
|
239
|
-
|
|
240
|
-
const source$ = atom(1);
|
|
241
|
-
derived(({ read }) => {
|
|
242
|
-
throw new Error("Test");
|
|
243
|
-
return read(source$);
|
|
244
|
-
})
|
|
245
|
-
.get()
|
|
246
|
-
.catch(() => {});
|
|
247
|
-
|
|
248
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
249
|
-
|
|
250
|
-
expect(hookFn).not.toHaveBeenCalled();
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
describe("real-world scenarios", () => {
|
|
255
|
-
it("should enable global error logging", async () => {
|
|
256
|
-
const errorLog: Array<{ type: string; key?: string; error: string }> = [];
|
|
257
|
-
|
|
258
|
-
onErrorHook.override(() => (info) => {
|
|
259
|
-
errorLog.push({
|
|
260
|
-
type: info.source.type,
|
|
261
|
-
key: info.source.key,
|
|
262
|
-
error: String(info.error),
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const source$ = atom(0);
|
|
267
|
-
|
|
268
|
-
// Create multiple derived/effects that will error
|
|
269
|
-
const derived1$ = derived(
|
|
270
|
-
({ read }) => {
|
|
271
|
-
if (read(source$) > 0) throw new Error("Derived 1 failed");
|
|
272
|
-
return read(source$);
|
|
273
|
-
},
|
|
274
|
-
{ meta: { key: "derived1" } }
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const derived2$ = derived(
|
|
278
|
-
({ read }) => {
|
|
279
|
-
if (read(source$) > 0) throw new Error("Derived 2 failed");
|
|
280
|
-
return read(source$);
|
|
281
|
-
},
|
|
282
|
-
{ meta: { key: "derived2" } }
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
effect(
|
|
286
|
-
({ read }) => {
|
|
287
|
-
if (read(source$) > 0) throw new Error("Effect 1 failed");
|
|
288
|
-
},
|
|
289
|
-
{ meta: { key: "effect1" } }
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// Trigger initial computation for derived atoms (they're lazy)
|
|
293
|
-
await derived1$.get();
|
|
294
|
-
await derived2$.get();
|
|
295
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
296
|
-
expect(errorLog.length).toBe(0);
|
|
297
|
-
|
|
298
|
-
// Trigger all errors
|
|
299
|
-
source$.set(1);
|
|
300
|
-
derived1$.get().catch(() => {});
|
|
301
|
-
derived2$.get().catch(() => {});
|
|
302
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
303
|
-
|
|
304
|
-
expect(errorLog.length).toBe(3);
|
|
305
|
-
expect(errorLog.map((e) => e.key).sort()).toEqual([
|
|
306
|
-
"derived1",
|
|
307
|
-
"derived2",
|
|
308
|
-
"effect1",
|
|
309
|
-
]);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("should enable error monitoring service integration", async () => {
|
|
313
|
-
const sentryMock = {
|
|
314
|
-
captureException: vi.fn(),
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
onErrorHook.override(() => (info) => {
|
|
318
|
-
sentryMock.captureException(info.error, {
|
|
319
|
-
tags: {
|
|
320
|
-
source_type: info.source.type,
|
|
321
|
-
source_key: info.source.key,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const source$ = atom(1);
|
|
327
|
-
derived(
|
|
328
|
-
({ read }) => {
|
|
329
|
-
throw new Error("Critical error");
|
|
330
|
-
return read(source$);
|
|
331
|
-
},
|
|
332
|
-
{ meta: { key: "criticalDerived" } }
|
|
333
|
-
)
|
|
334
|
-
.get()
|
|
335
|
-
.catch(() => {});
|
|
336
|
-
|
|
337
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
338
|
-
|
|
339
|
-
expect(sentryMock.captureException).toHaveBeenCalledWith(
|
|
340
|
-
expect.any(Error),
|
|
341
|
-
{
|
|
342
|
-
tags: {
|
|
343
|
-
source_type: "derived",
|
|
344
|
-
source_key: "criticalDerived",
|
|
345
|
-
},
|
|
346
|
-
}
|
|
347
|
-
);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
});
|
package/src/core/onErrorHook.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { hook } from "./hook";
|
|
2
|
-
import { CreateInfo } from "./onCreateHook";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Information provided when an error occurs in an atom, derived, or effect.
|
|
6
|
-
*/
|
|
7
|
-
export interface ErrorInfo {
|
|
8
|
-
/** The source that produced the error (atom, derived, or effect) */
|
|
9
|
-
source: CreateInfo;
|
|
10
|
-
/** The error that was thrown */
|
|
11
|
-
error: unknown;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Global hook that fires whenever an error occurs in a derived atom or effect.
|
|
16
|
-
*
|
|
17
|
-
* This is useful for:
|
|
18
|
-
* - **Global error logging** - capture all errors in one place
|
|
19
|
-
* - **Error monitoring** - send errors to monitoring services (Sentry, etc.)
|
|
20
|
-
* - **DevTools integration** - show errors in developer tools
|
|
21
|
-
* - **Debugging** - track which atoms/effects are failing
|
|
22
|
-
*
|
|
23
|
-
* **IMPORTANT**: Always use `.override()` to preserve the hook chain.
|
|
24
|
-
* Direct assignment to `.current` will break existing handlers.
|
|
25
|
-
*
|
|
26
|
-
* @example Basic logging
|
|
27
|
-
* ```ts
|
|
28
|
-
* onErrorHook.override((prev) => (info) => {
|
|
29
|
-
* prev?.(info); // call existing handlers first
|
|
30
|
-
* console.error(`Error in ${info.source.type}: ${info.source.key ?? "anonymous"}`, info.error);
|
|
31
|
-
* });
|
|
32
|
-
* ```
|
|
33
|
-
*
|
|
34
|
-
* @example Send to monitoring service
|
|
35
|
-
* ```ts
|
|
36
|
-
* onErrorHook.override((prev) => (info) => {
|
|
37
|
-
* prev?.(info); // preserve chain
|
|
38
|
-
* Sentry.captureException(info.error, {
|
|
39
|
-
* tags: {
|
|
40
|
-
* source_type: info.source.type,
|
|
41
|
-
* source_key: info.source.key,
|
|
42
|
-
* },
|
|
43
|
-
* });
|
|
44
|
-
* });
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* @example Reset to default (disable all handlers)
|
|
48
|
-
* ```ts
|
|
49
|
-
* onErrorHook.reset();
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export const onErrorHook = hook<(info: ErrorInfo) => void>();
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
trackPromise,
|
|
4
|
-
getPromiseState,
|
|
5
|
-
isTracked,
|
|
6
|
-
isPending,
|
|
7
|
-
isFulfilled,
|
|
8
|
-
isRejected,
|
|
9
|
-
unwrap,
|
|
10
|
-
isDerived,
|
|
11
|
-
} from "./promiseCache";
|
|
12
|
-
import { getAtomState } from "./getAtomState";
|
|
13
|
-
import { atom } from "./atom";
|
|
14
|
-
import { derived } from "./derived";
|
|
15
|
-
|
|
16
|
-
describe("promiseCache", () => {
|
|
17
|
-
describe("trackPromise", () => {
|
|
18
|
-
it("should return pending state for unresolved promise", () => {
|
|
19
|
-
const promise = new Promise<number>(() => {});
|
|
20
|
-
const state = trackPromise(promise);
|
|
21
|
-
expect(state.status).toBe("pending");
|
|
22
|
-
expect((state as { promise: PromiseLike<number> }).promise).toBe(promise);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("should return fulfilled state after promise resolves", async () => {
|
|
26
|
-
const promise = Promise.resolve(42);
|
|
27
|
-
await promise;
|
|
28
|
-
trackPromise(promise); // Start tracking
|
|
29
|
-
// May still be pending if checked immediately, wait a tick
|
|
30
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
31
|
-
const state = trackPromise(promise);
|
|
32
|
-
expect(state.status).toBe("fulfilled");
|
|
33
|
-
expect((state as { value: number }).value).toBe(42);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("should return rejected state after promise rejects", async () => {
|
|
37
|
-
const error = new Error("Test error");
|
|
38
|
-
const promise = Promise.reject(error);
|
|
39
|
-
promise.catch(() => {}); // Prevent unhandled rejection
|
|
40
|
-
trackPromise(promise); // Start tracking
|
|
41
|
-
await Promise.resolve();
|
|
42
|
-
await Promise.resolve();
|
|
43
|
-
const state = trackPromise(promise);
|
|
44
|
-
expect(state.status).toBe("rejected");
|
|
45
|
-
expect((state as { error: unknown }).error).toBe(error);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("should return same state for same promise", () => {
|
|
49
|
-
const promise = new Promise<number>(() => {});
|
|
50
|
-
const state1 = trackPromise(promise);
|
|
51
|
-
const state2 = trackPromise(promise);
|
|
52
|
-
expect(state1).toBe(state2);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("getPromiseState", () => {
|
|
57
|
-
it("should return undefined for untracked promise", () => {
|
|
58
|
-
const promise = new Promise<number>(() => {});
|
|
59
|
-
expect(getPromiseState(promise)).toBe(undefined);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("should return state for tracked promise", () => {
|
|
63
|
-
const promise = new Promise<number>(() => {});
|
|
64
|
-
trackPromise(promise);
|
|
65
|
-
expect(getPromiseState(promise)).toBeDefined();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("isTracked", () => {
|
|
70
|
-
it("should return false for untracked promise", () => {
|
|
71
|
-
const promise = new Promise<number>(() => {});
|
|
72
|
-
expect(isTracked(promise)).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("should return true for tracked promise", () => {
|
|
76
|
-
const promise = new Promise<number>(() => {});
|
|
77
|
-
trackPromise(promise);
|
|
78
|
-
expect(isTracked(promise)).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("isPending", () => {
|
|
83
|
-
it("should return false for non-promise", () => {
|
|
84
|
-
expect(isPending(42)).toBe(false);
|
|
85
|
-
expect(isPending("hello")).toBe(false);
|
|
86
|
-
expect(isPending(null)).toBe(false);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("should return true for pending promise", () => {
|
|
90
|
-
const promise = new Promise<number>(() => {});
|
|
91
|
-
expect(isPending(promise)).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("should return false for fulfilled promise", async () => {
|
|
95
|
-
const promise = Promise.resolve(42);
|
|
96
|
-
trackPromise(promise); // Start tracking
|
|
97
|
-
await Promise.resolve();
|
|
98
|
-
await Promise.resolve();
|
|
99
|
-
expect(isPending(promise)).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("isFulfilled", () => {
|
|
104
|
-
it("should return false for non-promise", () => {
|
|
105
|
-
expect(isFulfilled(42)).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("should return true for fulfilled promise", async () => {
|
|
109
|
-
const promise = Promise.resolve(42);
|
|
110
|
-
trackPromise(promise);
|
|
111
|
-
await Promise.resolve();
|
|
112
|
-
await Promise.resolve();
|
|
113
|
-
expect(isFulfilled(promise)).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("should return false for pending promise", () => {
|
|
117
|
-
const promise = new Promise<number>(() => {});
|
|
118
|
-
expect(isFulfilled(promise)).toBe(false);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe("isRejected", () => {
|
|
123
|
-
it("should return false for non-promise", () => {
|
|
124
|
-
expect(isRejected(42)).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("should return true for rejected promise", async () => {
|
|
128
|
-
const promise = Promise.reject(new Error("test"));
|
|
129
|
-
promise.catch(() => {});
|
|
130
|
-
trackPromise(promise);
|
|
131
|
-
await Promise.resolve();
|
|
132
|
-
await Promise.resolve();
|
|
133
|
-
expect(isRejected(promise)).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("should return false for fulfilled promise", async () => {
|
|
137
|
-
const promise = Promise.resolve(42);
|
|
138
|
-
trackPromise(promise);
|
|
139
|
-
await Promise.resolve();
|
|
140
|
-
await Promise.resolve();
|
|
141
|
-
expect(isRejected(promise)).toBe(false);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("unwrap", () => {
|
|
146
|
-
it("should return value for non-promise", () => {
|
|
147
|
-
expect(unwrap(42)).toBe(42);
|
|
148
|
-
expect(unwrap("hello")).toBe("hello");
|
|
149
|
-
expect(unwrap(null)).toBe(null);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("should return value for fulfilled promise", async () => {
|
|
153
|
-
const promise = Promise.resolve(42);
|
|
154
|
-
trackPromise(promise);
|
|
155
|
-
await Promise.resolve();
|
|
156
|
-
await Promise.resolve();
|
|
157
|
-
expect(unwrap(promise)).toBe(42);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("should throw promise for pending promise", () => {
|
|
161
|
-
const promise = new Promise<number>(() => {});
|
|
162
|
-
let thrown: unknown;
|
|
163
|
-
try {
|
|
164
|
-
unwrap(promise);
|
|
165
|
-
} catch (e) {
|
|
166
|
-
thrown = e;
|
|
167
|
-
}
|
|
168
|
-
expect(thrown).toBe(promise);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("should throw error for rejected promise", async () => {
|
|
172
|
-
const error = new Error("Test error");
|
|
173
|
-
const promise = Promise.reject(error);
|
|
174
|
-
promise.catch(() => {});
|
|
175
|
-
trackPromise(promise);
|
|
176
|
-
await Promise.resolve();
|
|
177
|
-
await Promise.resolve();
|
|
178
|
-
expect(() => unwrap(promise)).toThrow(error);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("isDerived", () => {
|
|
183
|
-
it("should return false for mutable atom", () => {
|
|
184
|
-
const a$ = atom(0);
|
|
185
|
-
expect(isDerived(a$)).toBe(false);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("should return true for derived atom", () => {
|
|
189
|
-
const a$ = atom(0);
|
|
190
|
-
const d$ = derived(({ read }) => read(a$) * 2);
|
|
191
|
-
expect(isDerived(d$)).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("should return false for non-atom values", () => {
|
|
195
|
-
expect(isDerived(null)).toBe(false);
|
|
196
|
-
expect(isDerived(undefined)).toBe(false);
|
|
197
|
-
expect(isDerived(42)).toBe(false);
|
|
198
|
-
expect(isDerived({})).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("getAtomState", () => {
|
|
203
|
-
it("should return ready state for sync mutable atom", () => {
|
|
204
|
-
const a$ = atom(42);
|
|
205
|
-
const state = getAtomState(a$);
|
|
206
|
-
expect(state.status).toBe("ready");
|
|
207
|
-
expect((state as { value: number }).value).toBe(42);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("should return loading state for atom with pending Promise", () => {
|
|
211
|
-
const promise = new Promise<number>(() => {});
|
|
212
|
-
const a$ = atom(promise);
|
|
213
|
-
const state = getAtomState(a$);
|
|
214
|
-
expect(state.status).toBe("loading");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("should return ready state for atom with resolved Promise", async () => {
|
|
218
|
-
const promise = Promise.resolve(42);
|
|
219
|
-
const a$ = atom(promise);
|
|
220
|
-
trackPromise(promise);
|
|
221
|
-
await Promise.resolve();
|
|
222
|
-
await Promise.resolve();
|
|
223
|
-
const state = getAtomState(a$);
|
|
224
|
-
expect(state.status).toBe("ready");
|
|
225
|
-
expect((state as { value: number }).value).toBe(42);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("should return loading state for derived with fallback during loading", async () => {
|
|
229
|
-
const asyncValue$ = atom(new Promise<number>(() => {}));
|
|
230
|
-
const derived$ = derived(({ read }) => read(asyncValue$), {
|
|
231
|
-
fallback: 0,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Derived atoms return their state directly via state()
|
|
235
|
-
// State is loading, but staleValue provides the fallback
|
|
236
|
-
const state = getAtomState(derived$);
|
|
237
|
-
expect(state.status).toBe("loading");
|
|
238
|
-
expect(derived$.staleValue).toBe(0);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|