atomirx 0.0.4 → 0.0.6
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 +2 -2
- package/coverage/src/core/onCreateHook.ts.html +72 -70
- package/dist/core/derived.d.ts +15 -2
- package/dist/core/effect.d.ts +6 -2
- 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/onErrorHook.test.d.ts +1 -0
- package/dist/core/types.d.ts +52 -3
- package/dist/core/withReady.d.ts +46 -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 +3 -1
- package/dist/index.js +12 -11
- package/dist/react/index.cjs +9 -9
- package/dist/react/index.js +76 -75
- package/package.json +5 -5
- package/src/core/atom.ts +1 -1
- package/src/core/define.test.ts +12 -11
- package/src/core/define.ts +1 -1
- package/src/core/derived.test.ts +179 -0
- package/src/core/derived.ts +51 -8
- package/src/core/effect.test.ts +395 -9
- package/src/core/effect.ts +56 -29
- 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/types.ts +53 -3
- package/src/core/withReady.test.ts +174 -0
- package/src/core/withReady.ts +91 -27
- package/src/index.ts +10 -1
- package/dist/index-CqO6BDwj.cjs +0 -1
- package/dist/index-D8RDOTB_.js +0 -1319
package/src/core/derived.test.ts
CHANGED
|
@@ -1033,4 +1033,183 @@ describe("derived", () => {
|
|
|
1033
1033
|
});
|
|
1034
1034
|
});
|
|
1035
1035
|
});
|
|
1036
|
+
|
|
1037
|
+
describe("onError callback", () => {
|
|
1038
|
+
it("should call onError when computation throws synchronously", async () => {
|
|
1039
|
+
const onError = vi.fn();
|
|
1040
|
+
const source$ = atom(0);
|
|
1041
|
+
|
|
1042
|
+
const derived$ = derived(
|
|
1043
|
+
({ read }) => {
|
|
1044
|
+
const val = read(source$);
|
|
1045
|
+
if (val > 0) {
|
|
1046
|
+
throw new Error("Value too high");
|
|
1047
|
+
}
|
|
1048
|
+
return val;
|
|
1049
|
+
},
|
|
1050
|
+
{ onError }
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
// Initial value - no error
|
|
1054
|
+
await derived$.get();
|
|
1055
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1056
|
+
|
|
1057
|
+
// Trigger error - catch the rejection to avoid unhandled rejection warning
|
|
1058
|
+
source$.set(5);
|
|
1059
|
+
derived$.get().catch(() => {}); // Catch expected rejection
|
|
1060
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1061
|
+
|
|
1062
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
1063
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
1064
|
+
expect((onError.mock.calls[0][0] as Error).message).toBe("Value too high");
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it("should call onError when async atom dependency rejects", async () => {
|
|
1068
|
+
const onError = vi.fn();
|
|
1069
|
+
|
|
1070
|
+
// Create an atom with a rejecting Promise
|
|
1071
|
+
const asyncSource$ = atom(Promise.reject(new Error("Async error")));
|
|
1072
|
+
|
|
1073
|
+
const derived$ = derived(
|
|
1074
|
+
({ read }) => {
|
|
1075
|
+
return read(asyncSource$);
|
|
1076
|
+
},
|
|
1077
|
+
{ onError }
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
// Access to trigger computation
|
|
1081
|
+
derived$.get().catch(() => {}); // Catch to avoid unhandled rejection
|
|
1082
|
+
|
|
1083
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
1084
|
+
|
|
1085
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
1086
|
+
expect((onError.mock.calls[0][0] as Error).message).toBe("Async error");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("should call onError on each recomputation that throws", async () => {
|
|
1090
|
+
const onError = vi.fn();
|
|
1091
|
+
const source$ = atom(0);
|
|
1092
|
+
|
|
1093
|
+
const derived$ = derived(
|
|
1094
|
+
({ read }) => {
|
|
1095
|
+
const val = read(source$);
|
|
1096
|
+
if (val > 0) {
|
|
1097
|
+
throw new Error(`Error for ${val}`);
|
|
1098
|
+
}
|
|
1099
|
+
return val;
|
|
1100
|
+
},
|
|
1101
|
+
{ onError }
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
await derived$.get();
|
|
1105
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1106
|
+
|
|
1107
|
+
// First error - catch to avoid unhandled rejection
|
|
1108
|
+
source$.set(1);
|
|
1109
|
+
derived$.get().catch(() => {});
|
|
1110
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1111
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
1112
|
+
|
|
1113
|
+
// Second error - catch to avoid unhandled rejection
|
|
1114
|
+
source$.set(2);
|
|
1115
|
+
derived$.get().catch(() => {});
|
|
1116
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1117
|
+
expect(onError).toHaveBeenCalledTimes(2);
|
|
1118
|
+
expect((onError.mock.calls[1][0] as Error).message).toBe("Error for 2");
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it("should not call onError when computation succeeds", async () => {
|
|
1122
|
+
const onError = vi.fn();
|
|
1123
|
+
const source$ = atom(5);
|
|
1124
|
+
|
|
1125
|
+
const derived$ = derived(({ read }) => read(source$) * 2, { onError });
|
|
1126
|
+
|
|
1127
|
+
await derived$.get();
|
|
1128
|
+
source$.set(10);
|
|
1129
|
+
await derived$.get();
|
|
1130
|
+
source$.set(15);
|
|
1131
|
+
await derived$.get();
|
|
1132
|
+
|
|
1133
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("should not call onError for Promise throws (Suspense)", async () => {
|
|
1137
|
+
const onError = vi.fn();
|
|
1138
|
+
let resolvePromise: (value: number) => void;
|
|
1139
|
+
const asyncSource$ = atom(
|
|
1140
|
+
new Promise<number>((resolve) => {
|
|
1141
|
+
resolvePromise = resolve;
|
|
1142
|
+
})
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
const derived$ = derived(({ read }) => read(asyncSource$) * 2, {
|
|
1146
|
+
onError,
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Still loading - onError should NOT be called
|
|
1150
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1151
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1152
|
+
|
|
1153
|
+
// Resolve successfully
|
|
1154
|
+
resolvePromise!(5);
|
|
1155
|
+
expect(await derived$.get()).toBe(10);
|
|
1156
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it("should work without onError callback", async () => {
|
|
1160
|
+
const source$ = atom(0);
|
|
1161
|
+
|
|
1162
|
+
const derived$ = derived(({ read }) => {
|
|
1163
|
+
const val = read(source$);
|
|
1164
|
+
if (val > 0) {
|
|
1165
|
+
throw new Error("Error");
|
|
1166
|
+
}
|
|
1167
|
+
return val;
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Should not throw even without onError
|
|
1171
|
+
await derived$.get();
|
|
1172
|
+
source$.set(5);
|
|
1173
|
+
derived$.get().catch(() => {}); // Catch expected rejection
|
|
1174
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1175
|
+
|
|
1176
|
+
expect(derived$.state().status).toBe("error");
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it("should allow error recovery and call onError again on subsequent errors", async () => {
|
|
1180
|
+
const onError = vi.fn();
|
|
1181
|
+
const source$ = atom(0);
|
|
1182
|
+
|
|
1183
|
+
const derived$ = derived(
|
|
1184
|
+
({ read }) => {
|
|
1185
|
+
const val = read(source$);
|
|
1186
|
+
if (val === 1) {
|
|
1187
|
+
throw new Error("First error");
|
|
1188
|
+
}
|
|
1189
|
+
if (val === 3) {
|
|
1190
|
+
throw new Error("Second error");
|
|
1191
|
+
}
|
|
1192
|
+
return val * 2;
|
|
1193
|
+
},
|
|
1194
|
+
{ onError }
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
await derived$.get(); // 0 -> success
|
|
1198
|
+
expect(onError).not.toHaveBeenCalled();
|
|
1199
|
+
|
|
1200
|
+
source$.set(1); // error
|
|
1201
|
+
derived$.get().catch(() => {}); // Catch expected rejection
|
|
1202
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1203
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
1204
|
+
|
|
1205
|
+
source$.set(2); // recover
|
|
1206
|
+
expect(await derived$.get()).toBe(4);
|
|
1207
|
+
expect(onError).toHaveBeenCalledTimes(1); // still 1
|
|
1208
|
+
|
|
1209
|
+
source$.set(3); // error again
|
|
1210
|
+
derived$.get().catch(() => {}); // Catch expected rejection
|
|
1211
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1212
|
+
expect(onError).toHaveBeenCalledTimes(2);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1036
1215
|
});
|
package/src/core/derived.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { onCreateHook } from "./onCreateHook";
|
|
1
|
+
import { CreateInfo, DerivedInfo, onCreateHook } from "./onCreateHook";
|
|
2
2
|
import { emitter } from "./emitter";
|
|
3
3
|
import { resolveEquality } from "./equality";
|
|
4
|
+
import { onErrorHook } from "./onErrorHook";
|
|
4
5
|
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
5
6
|
import { ReactiveSelector, select, SelectContext } from "./select";
|
|
6
7
|
import {
|
|
@@ -14,6 +15,19 @@ import {
|
|
|
14
15
|
} from "./types";
|
|
15
16
|
import { withReady, WithReadySelectContext } from "./withReady";
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Internal options for derived atoms.
|
|
20
|
+
* These are not part of the public API.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface DerivedInternalOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Override the error source for onErrorHook.
|
|
26
|
+
* Used by effect() to attribute errors to the effect instead of the internal derived.
|
|
27
|
+
*/
|
|
28
|
+
_errorSource?: CreateInfo;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/**
|
|
18
32
|
* Context object passed to derived atom selector functions.
|
|
19
33
|
* Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
|
|
@@ -147,19 +161,19 @@ export interface DerivedContext extends SelectContext, WithReadySelectContext {}
|
|
|
147
161
|
// Overload: Without fallback - staleValue is T | undefined
|
|
148
162
|
export function derived<T>(
|
|
149
163
|
fn: ReactiveSelector<T, DerivedContext>,
|
|
150
|
-
options?: DerivedOptions<T>
|
|
164
|
+
options?: DerivedOptions<T> & DerivedInternalOptions
|
|
151
165
|
): DerivedAtom<T, false>;
|
|
152
166
|
|
|
153
167
|
// Overload: With fallback - staleValue is guaranteed T
|
|
154
168
|
export function derived<T>(
|
|
155
169
|
fn: ReactiveSelector<T, DerivedContext>,
|
|
156
|
-
options: DerivedOptions<T> & { fallback: T }
|
|
170
|
+
options: DerivedOptions<T> & { fallback: T } & DerivedInternalOptions
|
|
157
171
|
): DerivedAtom<T, true>;
|
|
158
172
|
|
|
159
173
|
// Implementation
|
|
160
174
|
export function derived<T>(
|
|
161
175
|
fn: ReactiveSelector<T, DerivedContext>,
|
|
162
|
-
options: DerivedOptions<T> & { fallback?: T } = {}
|
|
176
|
+
options: DerivedOptions<T> & { fallback?: T } & DerivedInternalOptions = {}
|
|
163
177
|
): DerivedAtom<T, boolean> {
|
|
164
178
|
const changeEmitter = emitter();
|
|
165
179
|
const eq = resolveEquality(options.equals as Equality<unknown>);
|
|
@@ -183,6 +197,23 @@ export function derived<T>(
|
|
|
183
197
|
// Track current subscriptions (atom -> unsubscribe function)
|
|
184
198
|
const subscriptions = new Map<Atom<unknown>, VoidFunction>();
|
|
185
199
|
|
|
200
|
+
// CreateInfo for this derived - stored for onErrorHook
|
|
201
|
+
// Will be set after derivedAtom is created
|
|
202
|
+
let createInfo: DerivedInfo;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handles errors by calling both the user's onError callback and the global onErrorHook.
|
|
206
|
+
*/
|
|
207
|
+
const handleError = (error: unknown) => {
|
|
208
|
+
// Invoke user's error callback if provided
|
|
209
|
+
options.onError?.(error);
|
|
210
|
+
|
|
211
|
+
// Invoke global error hook
|
|
212
|
+
// Use _errorSource if provided (for effect), otherwise use this derived's createInfo
|
|
213
|
+
const source = options._errorSource ?? createInfo;
|
|
214
|
+
onErrorHook.current?.({ source, error });
|
|
215
|
+
};
|
|
216
|
+
|
|
186
217
|
/**
|
|
187
218
|
* Schedules notification to all subscribers.
|
|
188
219
|
*/
|
|
@@ -235,6 +266,11 @@ export function derived<T>(
|
|
|
235
266
|
resolvePromise = resolve;
|
|
236
267
|
rejectPromise = reject;
|
|
237
268
|
});
|
|
269
|
+
// Prevent unhandled rejection warnings - errors are accessible via:
|
|
270
|
+
// 1. onError callback (if provided)
|
|
271
|
+
// 2. state() returning { status: "error", error }
|
|
272
|
+
// 3. .get().catch() by consumers
|
|
273
|
+
currentPromise.catch(() => {});
|
|
238
274
|
}
|
|
239
275
|
|
|
240
276
|
// Run select to compute value and track dependencies
|
|
@@ -267,6 +303,8 @@ export function derived<T>(
|
|
|
267
303
|
// Clear resolve/reject so next computation creates new promise
|
|
268
304
|
resolvePromise = null;
|
|
269
305
|
rejectPromise = null;
|
|
306
|
+
// Invoke error handlers
|
|
307
|
+
handleError(error);
|
|
270
308
|
// Always notify when promise rejects - subscribers need to know
|
|
271
309
|
// state changed from loading to error
|
|
272
310
|
notify();
|
|
@@ -280,6 +318,8 @@ export function derived<T>(
|
|
|
280
318
|
// Clear resolve/reject so next computation creates new promise
|
|
281
319
|
resolvePromise = null;
|
|
282
320
|
rejectPromise = null;
|
|
321
|
+
// Invoke error handlers
|
|
322
|
+
handleError(result.error);
|
|
283
323
|
if (!silent) notify();
|
|
284
324
|
} else {
|
|
285
325
|
// Success - update lastResolved and resolve
|
|
@@ -395,13 +435,16 @@ export function derived<T>(
|
|
|
395
435
|
},
|
|
396
436
|
};
|
|
397
437
|
|
|
398
|
-
//
|
|
399
|
-
|
|
438
|
+
// Store createInfo for use in onErrorHook
|
|
439
|
+
createInfo = {
|
|
400
440
|
type: "derived",
|
|
401
441
|
key: options.meta?.key,
|
|
402
442
|
meta: options.meta,
|
|
403
|
-
|
|
404
|
-
}
|
|
443
|
+
instance: derivedAtom,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Notify devtools/plugins of derived atom creation
|
|
447
|
+
onCreateHook.current?.(createInfo);
|
|
405
448
|
|
|
406
449
|
return derivedAtom;
|
|
407
450
|
}
|