atomirx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { atom } from "./atom";
|
|
3
|
+
import { effect } from "./effect";
|
|
4
|
+
|
|
5
|
+
describe("effect", () => {
|
|
6
|
+
describe("basic functionality", () => {
|
|
7
|
+
it("should run effect immediately", async () => {
|
|
8
|
+
const effectFn = vi.fn();
|
|
9
|
+
const count$ = atom(0);
|
|
10
|
+
|
|
11
|
+
effect(({ get }) => {
|
|
12
|
+
effectFn(get(count$));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Wait for async execution
|
|
16
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
17
|
+
expect(effectFn).toHaveBeenCalledWith(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should run effect when dependency changes", async () => {
|
|
21
|
+
const effectFn = vi.fn();
|
|
22
|
+
const count$ = atom(0);
|
|
23
|
+
|
|
24
|
+
effect(({ get }) => {
|
|
25
|
+
effectFn(get(count$));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
29
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
30
|
+
|
|
31
|
+
count$.set(5);
|
|
32
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
33
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
34
|
+
expect(effectFn).toHaveBeenLastCalledWith(5);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should track multiple dependencies", async () => {
|
|
38
|
+
const effectFn = vi.fn();
|
|
39
|
+
const a$ = atom(1);
|
|
40
|
+
const b$ = atom(2);
|
|
41
|
+
|
|
42
|
+
effect(({ get }) => {
|
|
43
|
+
effectFn(get(a$) + get(b$));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
47
|
+
expect(effectFn).toHaveBeenCalledWith(3);
|
|
48
|
+
|
|
49
|
+
a$.set(10);
|
|
50
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
51
|
+
expect(effectFn).toHaveBeenCalledWith(12);
|
|
52
|
+
|
|
53
|
+
b$.set(5);
|
|
54
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
55
|
+
expect(effectFn).toHaveBeenCalledWith(15);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("cleanup", () => {
|
|
60
|
+
it("should run cleanup before next execution", async () => {
|
|
61
|
+
const cleanupFn = vi.fn();
|
|
62
|
+
const effectFn = vi.fn();
|
|
63
|
+
const count$ = atom(0);
|
|
64
|
+
|
|
65
|
+
effect(({ get, onCleanup }) => {
|
|
66
|
+
effectFn(get(count$));
|
|
67
|
+
onCleanup(cleanupFn);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
71
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(cleanupFn).not.toHaveBeenCalled();
|
|
73
|
+
|
|
74
|
+
count$.set(1);
|
|
75
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
76
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
77
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should run cleanup on dispose", async () => {
|
|
81
|
+
const cleanupFn = vi.fn();
|
|
82
|
+
const count$ = atom(0);
|
|
83
|
+
|
|
84
|
+
const dispose = effect(({ get, onCleanup }) => {
|
|
85
|
+
get(count$);
|
|
86
|
+
onCleanup(cleanupFn);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
90
|
+
expect(cleanupFn).not.toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
dispose();
|
|
93
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("dispose", () => {
|
|
98
|
+
it("should stop effect execution after dispose", async () => {
|
|
99
|
+
const effectFn = vi.fn();
|
|
100
|
+
const count$ = atom(0);
|
|
101
|
+
|
|
102
|
+
const dispose = effect(({ get }) => {
|
|
103
|
+
effectFn(get(count$));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
107
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
dispose();
|
|
110
|
+
|
|
111
|
+
count$.set(5);
|
|
112
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
113
|
+
// Should not have been called again
|
|
114
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should be safe to call dispose multiple times", async () => {
|
|
118
|
+
const cleanupFn = vi.fn();
|
|
119
|
+
const count$ = atom(0);
|
|
120
|
+
|
|
121
|
+
const dispose = effect(({ get, onCleanup }) => {
|
|
122
|
+
get(count$);
|
|
123
|
+
onCleanup(cleanupFn);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
127
|
+
|
|
128
|
+
dispose();
|
|
129
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
130
|
+
|
|
131
|
+
dispose(); // Second call should be no-op
|
|
132
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("error handling", () => {
|
|
137
|
+
it("should call onError callback when effect throws", async () => {
|
|
138
|
+
const errorHandler = vi.fn();
|
|
139
|
+
const count$ = atom(0);
|
|
140
|
+
const error = new Error("Effect error");
|
|
141
|
+
|
|
142
|
+
effect(({ get, onError }) => {
|
|
143
|
+
onError(errorHandler);
|
|
144
|
+
if (get(count$) > 0) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
150
|
+
expect(errorHandler).not.toHaveBeenCalled();
|
|
151
|
+
|
|
152
|
+
count$.set(5);
|
|
153
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
154
|
+
expect(errorHandler).toHaveBeenCalledWith(error);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should call options.onError for unhandled errors", async () => {
|
|
158
|
+
const onError = vi.fn();
|
|
159
|
+
const count$ = atom(0);
|
|
160
|
+
const error = new Error("Effect error");
|
|
161
|
+
|
|
162
|
+
effect(
|
|
163
|
+
({ get }) => {
|
|
164
|
+
if (get(count$) > 0) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{ onError }
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
172
|
+
expect(onError).not.toHaveBeenCalled();
|
|
173
|
+
|
|
174
|
+
count$.set(5);
|
|
175
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
176
|
+
// options.onError should be called for unhandled sync errors
|
|
177
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("context utilities", () => {
|
|
182
|
+
it("should support all() for multiple atoms", async () => {
|
|
183
|
+
const effectFn = vi.fn();
|
|
184
|
+
const a$ = atom(1);
|
|
185
|
+
const b$ = atom(2);
|
|
186
|
+
|
|
187
|
+
effect(({ all }) => {
|
|
188
|
+
const [a, b] = all(a$, b$);
|
|
189
|
+
effectFn(a + b);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
193
|
+
expect(effectFn).toHaveBeenCalledWith(3);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { batch } from "./batch";
|
|
2
|
+
import { derived } from "./derived";
|
|
3
|
+
import { emitter } from "./emitter";
|
|
4
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
5
|
+
import { SelectContext } from "./select";
|
|
6
|
+
import { EffectOptions } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Context object passed to effect functions.
|
|
10
|
+
* Extends `SelectContext` with cleanup and error handling utilities.
|
|
11
|
+
*/
|
|
12
|
+
export interface EffectContext extends SelectContext {
|
|
13
|
+
/**
|
|
14
|
+
* Register a cleanup function that runs before the next execution or on dispose.
|
|
15
|
+
* Multiple cleanup functions can be registered; they run in FIFO order.
|
|
16
|
+
*
|
|
17
|
+
* @param cleanup - Function to run during cleanup
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* effect(({ get, onCleanup }) => {
|
|
22
|
+
* const id = setInterval(() => console.log('tick'), 1000);
|
|
23
|
+
* onCleanup(() => clearInterval(id));
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
onCleanup: (cleanup: VoidFunction) => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register an error handler for synchronous errors thrown in the effect.
|
|
31
|
+
* If registered, prevents errors from propagating to `options.onError`.
|
|
32
|
+
*
|
|
33
|
+
* @param handler - Function to handle errors
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* effect(({ get, onError }) => {
|
|
38
|
+
* onError((e) => console.error('Effect failed:', e));
|
|
39
|
+
* riskyOperation();
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
onError: (handler: (error: unknown) => void) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Callback function for effects.
|
|
48
|
+
* Receives the effect context with `{ get, all, any, race, settled, onCleanup, onError }` utilities.
|
|
49
|
+
*/
|
|
50
|
+
export type EffectFn = (context: EffectContext) => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a side-effect that runs when accessed atom(s) change.
|
|
54
|
+
*
|
|
55
|
+
* Effects are similar to derived atoms but for side-effects rather than computed values.
|
|
56
|
+
* They inherit derived's behavior:
|
|
57
|
+
* - **Suspense-like async**: Waits for async atoms to resolve before running
|
|
58
|
+
* - **Conditional dependencies**: Only tracks atoms actually accessed via `get()`
|
|
59
|
+
* - **Automatic cleanup**: Previous cleanup runs before next execution
|
|
60
|
+
* - **Batched updates**: Atom updates within the effect are batched
|
|
61
|
+
*
|
|
62
|
+
* ## IMPORTANT: Effect Function Must Be Synchronous
|
|
63
|
+
*
|
|
64
|
+
* **The effect function MUST NOT be async or return a Promise.**
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* // ❌ WRONG - Don't use async function
|
|
68
|
+
* effect(async ({ get }) => {
|
|
69
|
+
* const data = await fetch('/api');
|
|
70
|
+
* console.log(data);
|
|
71
|
+
* });
|
|
72
|
+
*
|
|
73
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
74
|
+
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
75
|
+
* effect(({ get }) => {
|
|
76
|
+
* console.log(get(data$)); // Suspends until resolved
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ## Basic Usage
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* const dispose = effect(({ get }) => {
|
|
84
|
+
* localStorage.setItem('count', String(get(countAtom)));
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* ## With Cleanup
|
|
89
|
+
*
|
|
90
|
+
* Use `onCleanup` to register cleanup functions that run before the next execution or on dispose:
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const dispose = effect(({ get, onCleanup }) => {
|
|
94
|
+
* const interval = get(intervalAtom);
|
|
95
|
+
* const id = setInterval(() => console.log('tick'), interval);
|
|
96
|
+
* onCleanup(() => clearInterval(id));
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* ## Error Handling
|
|
101
|
+
*
|
|
102
|
+
* Use `onError` callback to handle errors within the effect, or `options.onError` for unhandled errors:
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* // Callback-based error handling
|
|
106
|
+
* const dispose = effect(({ get, onError }) => {
|
|
107
|
+
* onError((e) => console.error('Effect failed:', e));
|
|
108
|
+
* const data = get(dataAtom);
|
|
109
|
+
* riskyOperation(data);
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* // Option-based error handling (for unhandled errors)
|
|
113
|
+
* const dispose = effect(
|
|
114
|
+
* ({ get }) => {
|
|
115
|
+
* const data = get(dataAtom);
|
|
116
|
+
* riskyOperation(data);
|
|
117
|
+
* },
|
|
118
|
+
* { onError: (e) => console.error('Effect failed:', e) }
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @param fn - Effect callback receiving context with `{ get, all, any, race, settled, onCleanup, onError }`.
|
|
123
|
+
* Must be synchronous (not async).
|
|
124
|
+
* @param options - Optional configuration (key, onError for unhandled errors)
|
|
125
|
+
* @returns Dispose function to stop the effect and run final cleanup
|
|
126
|
+
* @throws Error if effect function returns a Promise
|
|
127
|
+
*/
|
|
128
|
+
export function effect(fn: EffectFn, options?: EffectOptions): VoidFunction {
|
|
129
|
+
let disposed = false;
|
|
130
|
+
const cleanupEmitter = emitter();
|
|
131
|
+
const errorEmitter = emitter<unknown>();
|
|
132
|
+
|
|
133
|
+
// Create a derived atom that runs the effect on each recomputation.
|
|
134
|
+
const derivedAtom = derived((context) => {
|
|
135
|
+
// Run previous cleanup before next execution
|
|
136
|
+
errorEmitter.clear();
|
|
137
|
+
cleanupEmitter.emitAndClear();
|
|
138
|
+
|
|
139
|
+
// Skip effect execution if disposed
|
|
140
|
+
if (disposed) return;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Run effect in a batch - multiple atom updates will only notify once
|
|
144
|
+
batch(() =>
|
|
145
|
+
fn({
|
|
146
|
+
...context,
|
|
147
|
+
onCleanup: cleanupEmitter.on,
|
|
148
|
+
onError: errorEmitter.on,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (isPromiseLike(error)) {
|
|
153
|
+
// let derived atom handle the promise
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
// Emit to registered handlers, or fall back to options.onError
|
|
157
|
+
if (errorEmitter.size() > 0) {
|
|
158
|
+
errorEmitter.emitAndClear(error);
|
|
159
|
+
} else if (options?.onError && error instanceof Error) {
|
|
160
|
+
options.onError(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Access .value to trigger initial computation (derived is lazy)
|
|
166
|
+
// Handle the promise
|
|
167
|
+
derivedAtom.value.catch((error) => {
|
|
168
|
+
if (options?.onError && error instanceof Error) {
|
|
169
|
+
options.onError(error);
|
|
170
|
+
}
|
|
171
|
+
// Silently ignore if no error handler
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
// Guard against multiple dispose calls
|
|
176
|
+
if (disposed) return;
|
|
177
|
+
|
|
178
|
+
// Mark as disposed
|
|
179
|
+
disposed = true;
|
|
180
|
+
errorEmitter.clear();
|
|
181
|
+
// Run final cleanup
|
|
182
|
+
cleanupEmitter.emitAndClear();
|
|
183
|
+
};
|
|
184
|
+
}
|