atomirx 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +868 -161
- package/coverage/src/core/onCreateHook.ts.html +72 -70
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +69 -22
- package/dist/core/effect.d.ts +52 -52
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/hook.d.ts +1 -1
- package/dist/core/onCreateHook.d.ts +37 -23
- package/dist/core/onErrorHook.d.ts +49 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +107 -22
- package/dist/core/withReady.d.ts +115 -0
- package/dist/core/withReady.test.d.ts +1 -0
- package/dist/index-CBVj1kSj.js +1350 -0
- package/dist/index-Cxk9v0um.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +18 -15
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +422 -377
- package/dist/react/rx.d.ts +114 -25
- package/dist/react/useAction.d.ts +5 -4
- package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
- package/dist/react/useSelector.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/atom.test.ts +307 -43
- package/src/core/atom.ts +144 -22
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/define.test.ts +12 -11
- package/src/core/define.ts +1 -1
- package/src/core/derived.test.ts +906 -72
- package/src/core/derived.ts +192 -81
- package/src/core/effect.test.ts +651 -45
- package/src/core/effect.ts +102 -98
- package/src/core/getAtomState.ts +69 -0
- package/src/core/hook.test.ts +5 -5
- package/src/core/hook.ts +1 -1
- package/src/core/onCreateHook.ts +38 -23
- package/src/core/onErrorHook.test.ts +350 -0
- package/src/core/onErrorHook.ts +52 -0
- package/src/core/promiseCache.test.ts +5 -3
- package/src/core/promiseCache.ts +76 -71
- package/src/core/select.ts +405 -130
- package/src/core/selector.test.ts +574 -32
- package/src/core/types.ts +107 -29
- package/src/core/withReady.test.ts +534 -0
- package/src/core/withReady.ts +191 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +21 -7
- package/src/react/index.ts +2 -1
- package/src/react/rx.test.tsx +173 -18
- package/src/react/rx.tsx +274 -43
- package/src/react/useAction.test.ts +12 -14
- package/src/react/useAction.ts +11 -9
- package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
- package/src/react/{useValue.ts → useSelector.ts} +64 -33
- package/v2.md +44 -44
- package/dist/index-2ok7ilik.js +0 -1217
- package/dist/index-B_5SFzfl.cjs +0 -1
- /package/dist/{react/useValue.test.d.ts → core/onErrorHook.test.d.ts} +0 -0
package/src/core/effect.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { batch } from "./batch";
|
|
2
2
|
import { derived } from "./derived";
|
|
3
3
|
import { emitter } from "./emitter";
|
|
4
|
-
import {
|
|
5
|
-
import { SelectContext } from "./select";
|
|
6
|
-
import { EffectOptions } from "./types";
|
|
4
|
+
import { EffectInfo, onCreateHook } from "./onCreateHook";
|
|
5
|
+
import { ReactiveSelector, SelectContext } from "./select";
|
|
6
|
+
import { EffectMeta, EffectOptions } from "./types";
|
|
7
|
+
import { WithReadySelectContext } from "./withReady";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Context object passed to effect functions.
|
|
10
|
-
* Extends `SelectContext` with cleanup
|
|
11
|
+
* Extends `SelectContext` with cleanup utilities.
|
|
11
12
|
*/
|
|
12
|
-
export interface EffectContext extends SelectContext {
|
|
13
|
+
export interface EffectContext extends SelectContext, WithReadySelectContext {
|
|
13
14
|
/**
|
|
14
15
|
* Register a cleanup function that runs before the next execution or on dispose.
|
|
15
16
|
* Multiple cleanup functions can be registered; they run in FIFO order.
|
|
@@ -18,36 +19,19 @@ export interface EffectContext extends SelectContext {
|
|
|
18
19
|
*
|
|
19
20
|
* @example
|
|
20
21
|
* ```ts
|
|
21
|
-
* effect(({
|
|
22
|
+
* effect(({ read, onCleanup }) => {
|
|
22
23
|
* const id = setInterval(() => console.log('tick'), 1000);
|
|
23
24
|
* onCleanup(() => clearInterval(id));
|
|
24
25
|
* });
|
|
25
26
|
* ```
|
|
26
27
|
*/
|
|
27
28
|
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
29
|
}
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export type EffectFn = (context: EffectContext) => void;
|
|
31
|
+
export interface Effect {
|
|
32
|
+
dispose: VoidFunction;
|
|
33
|
+
meta?: EffectMeta;
|
|
34
|
+
}
|
|
51
35
|
|
|
52
36
|
/**
|
|
53
37
|
* Creates a side-effect that runs when accessed atom(s) change.
|
|
@@ -55,7 +39,7 @@ export type EffectFn = (context: EffectContext) => void;
|
|
|
55
39
|
* Effects are similar to derived atoms but for side-effects rather than computed values.
|
|
56
40
|
* They inherit derived's behavior:
|
|
57
41
|
* - **Suspense-like async**: Waits for async atoms to resolve before running
|
|
58
|
-
* - **Conditional dependencies**: Only tracks atoms actually accessed via `
|
|
42
|
+
* - **Conditional dependencies**: Only tracks atoms actually accessed via `read()`
|
|
59
43
|
* - **Automatic cleanup**: Previous cleanup runs before next execution
|
|
60
44
|
* - **Batched updates**: Atom updates within the effect are batched
|
|
61
45
|
*
|
|
@@ -65,23 +49,23 @@ export type EffectFn = (context: EffectContext) => void;
|
|
|
65
49
|
*
|
|
66
50
|
* ```ts
|
|
67
51
|
* // ❌ WRONG - Don't use async function
|
|
68
|
-
* effect(async ({
|
|
52
|
+
* effect(async ({ read }) => {
|
|
69
53
|
* const data = await fetch('/api');
|
|
70
54
|
* console.log(data);
|
|
71
55
|
* });
|
|
72
56
|
*
|
|
73
|
-
* // ✅ CORRECT - Create async atom and read with
|
|
57
|
+
* // ✅ CORRECT - Create async atom and read with read()
|
|
74
58
|
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
75
|
-
* effect(({
|
|
76
|
-
* console.log(
|
|
59
|
+
* effect(({ read }) => {
|
|
60
|
+
* console.log(read(data$)); // Suspends until resolved
|
|
77
61
|
* });
|
|
78
62
|
* ```
|
|
79
63
|
*
|
|
80
64
|
* ## Basic Usage
|
|
81
65
|
*
|
|
82
66
|
* ```ts
|
|
83
|
-
* const dispose = effect(({
|
|
84
|
-
* localStorage.setItem('count', String(
|
|
67
|
+
* const dispose = effect(({ read }) => {
|
|
68
|
+
* localStorage.setItem('count', String(read(countAtom)));
|
|
85
69
|
* });
|
|
86
70
|
* ```
|
|
87
71
|
*
|
|
@@ -90,95 +74,115 @@ export type EffectFn = (context: EffectContext) => void;
|
|
|
90
74
|
* Use `onCleanup` to register cleanup functions that run before the next execution or on dispose:
|
|
91
75
|
*
|
|
92
76
|
* ```ts
|
|
93
|
-
* const dispose = effect(({
|
|
94
|
-
* const interval =
|
|
77
|
+
* const dispose = effect(({ read, onCleanup }) => {
|
|
78
|
+
* const interval = read(intervalAtom);
|
|
95
79
|
* const id = setInterval(() => console.log('tick'), interval);
|
|
96
80
|
* onCleanup(() => clearInterval(id));
|
|
97
81
|
* });
|
|
98
82
|
* ```
|
|
99
83
|
*
|
|
100
|
-
* ##
|
|
84
|
+
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
101
85
|
*
|
|
102
|
-
*
|
|
86
|
+
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
87
|
+
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
88
|
+
* these Promises and break the Suspense mechanism.
|
|
103
89
|
*
|
|
104
90
|
* ```ts
|
|
105
|
-
* //
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
91
|
+
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
92
|
+
* effect(({ read }) => {
|
|
93
|
+
* try {
|
|
94
|
+
* const data = read(asyncAtom$);
|
|
95
|
+
* riskyOperation(data);
|
|
96
|
+
* } catch (e) {
|
|
97
|
+
* console.error(e); // Catches BOTH errors AND loading promises!
|
|
98
|
+
* }
|
|
110
99
|
* });
|
|
111
100
|
*
|
|
112
|
-
* //
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* const
|
|
116
|
-
* riskyOperation(
|
|
117
|
-
* }
|
|
118
|
-
*
|
|
119
|
-
* )
|
|
101
|
+
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
102
|
+
* effect(({ read, safe }) => {
|
|
103
|
+
* const [err, data] = safe(() => {
|
|
104
|
+
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
105
|
+
* return riskyOperation(raw); // Can throw Error
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* if (err) {
|
|
109
|
+
* console.error('Operation failed:', err);
|
|
110
|
+
* return;
|
|
111
|
+
* }
|
|
112
|
+
* // Use data safely
|
|
113
|
+
* });
|
|
120
114
|
* ```
|
|
121
115
|
*
|
|
122
|
-
*
|
|
116
|
+
* The `safe()` utility:
|
|
117
|
+
* - **Catches errors** and returns `[error, undefined]`
|
|
118
|
+
* - **Re-throws Promises** to preserve Suspense behavior
|
|
119
|
+
* - Returns `[undefined, result]` on success
|
|
120
|
+
*
|
|
121
|
+
* @param fn - Effect callback receiving context with `{ read, all, any, race, settled, safe, onCleanup }`.
|
|
123
122
|
* Must be synchronous (not async).
|
|
124
|
-
* @param options - Optional configuration (key
|
|
123
|
+
* @param options - Optional configuration (key)
|
|
125
124
|
* @returns Dispose function to stop the effect and run final cleanup
|
|
126
125
|
* @throws Error if effect function returns a Promise
|
|
127
126
|
*/
|
|
128
|
-
export function effect(
|
|
127
|
+
export function effect(
|
|
128
|
+
fn: ReactiveSelector<void, EffectContext>,
|
|
129
|
+
options?: EffectOptions
|
|
130
|
+
): Effect {
|
|
129
131
|
let disposed = false;
|
|
130
132
|
const cleanupEmitter = emitter();
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
// Create the Effect object early so we can build EffectInfo
|
|
135
|
+
const e: Effect = {
|
|
136
|
+
meta: options?.meta,
|
|
137
|
+
dispose: () => {
|
|
138
|
+
// Guard against multiple dispose calls
|
|
139
|
+
if (disposed) return;
|
|
140
|
+
|
|
141
|
+
// Mark as disposed
|
|
142
|
+
disposed = true;
|
|
143
|
+
// Run final cleanup
|
|
144
|
+
cleanupEmitter.emitAndClear();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Create EffectInfo to pass to derived for error attribution
|
|
149
|
+
const effectInfo: EffectInfo = {
|
|
150
|
+
type: "effect",
|
|
151
|
+
key: options?.meta?.key,
|
|
152
|
+
meta: options?.meta,
|
|
153
|
+
instance: e,
|
|
154
|
+
};
|
|
132
155
|
|
|
133
156
|
// Create a derived atom that runs the effect on each recomputation.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
157
|
+
// Pass _errorSource so errors are attributed to the effect, not the internal derived
|
|
158
|
+
const derivedAtom = derived(
|
|
159
|
+
(context) => {
|
|
160
|
+
// Run previous cleanup before next execution
|
|
161
|
+
cleanupEmitter.emitAndClear();
|
|
138
162
|
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
// Skip effect execution if disposed
|
|
164
|
+
if (disposed) return;
|
|
141
165
|
|
|
142
|
-
try {
|
|
143
166
|
// Run effect in a batch - multiple atom updates will only notify once
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
}
|
|
167
|
+
// Cast to EffectContext since we're adding onCleanup to the DerivedContext
|
|
168
|
+
const effectContext = {
|
|
169
|
+
...context,
|
|
170
|
+
onCleanup: cleanupEmitter.on,
|
|
171
|
+
} as unknown as EffectContext;
|
|
172
|
+
batch(() => fn(effectContext));
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
onError: options?.onError,
|
|
176
|
+
_errorSource: effectInfo,
|
|
162
177
|
}
|
|
163
|
-
|
|
178
|
+
);
|
|
164
179
|
|
|
165
|
-
// Access .
|
|
166
|
-
//
|
|
167
|
-
derivedAtom.
|
|
168
|
-
if (options?.onError && error instanceof Error) {
|
|
169
|
-
options.onError(error);
|
|
170
|
-
}
|
|
171
|
-
// Silently ignore if no error handler
|
|
172
|
-
});
|
|
180
|
+
// Access .get() to trigger initial computation (derived is lazy)
|
|
181
|
+
// Errors are handled via onError callback or safe() in the effect function
|
|
182
|
+
derivedAtom.get();
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (disposed) return;
|
|
184
|
+
// Notify devtools/plugins of effect creation
|
|
185
|
+
onCreateHook.current?.(effectInfo);
|
|
177
186
|
|
|
178
|
-
|
|
179
|
-
disposed = true;
|
|
180
|
-
errorEmitter.clear();
|
|
181
|
-
// Run final cleanup
|
|
182
|
-
cleanupEmitter.emitAndClear();
|
|
183
|
-
};
|
|
187
|
+
return e;
|
|
184
188
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { isDerived } from "./isAtom";
|
|
2
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
3
|
+
import { trackPromise } from "./promiseCache";
|
|
4
|
+
import { Atom, AtomState, DerivedAtom } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the current state of an atom as a discriminated union.
|
|
8
|
+
*
|
|
9
|
+
* For any atom (mutable or derived):
|
|
10
|
+
* - If value is not a Promise: returns ready state
|
|
11
|
+
* - If value is a Promise: tracks and returns its state (ready/error/loading)
|
|
12
|
+
*
|
|
13
|
+
* @param atom - The atom to get state from
|
|
14
|
+
* @returns AtomState discriminated union (ready | error | loading)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const state = getAtomState(myAtom$);
|
|
19
|
+
*
|
|
20
|
+
* switch (state.status) {
|
|
21
|
+
* case "ready":
|
|
22
|
+
* console.log(state.value); // T
|
|
23
|
+
* break;
|
|
24
|
+
* case "error":
|
|
25
|
+
* console.log(state.error);
|
|
26
|
+
* break;
|
|
27
|
+
* case "loading":
|
|
28
|
+
* console.log(state.promise);
|
|
29
|
+
* break;
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function getAtomState<T>(atom: Atom<T>): AtomState<Awaited<T>> {
|
|
34
|
+
if (isDerived(atom)) {
|
|
35
|
+
return (atom as DerivedAtom<Awaited<T>>).state();
|
|
36
|
+
}
|
|
37
|
+
const value = atom.get();
|
|
38
|
+
|
|
39
|
+
// 1. Sync value - ready
|
|
40
|
+
if (!isPromiseLike(value)) {
|
|
41
|
+
return {
|
|
42
|
+
status: "ready",
|
|
43
|
+
value: value as Awaited<T>,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Promise value - check state via promiseCache
|
|
48
|
+
const state = trackPromise(value);
|
|
49
|
+
|
|
50
|
+
switch (state.status) {
|
|
51
|
+
case "fulfilled":
|
|
52
|
+
return {
|
|
53
|
+
status: "ready",
|
|
54
|
+
value: state.value as Awaited<T>,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
case "rejected":
|
|
58
|
+
return {
|
|
59
|
+
status: "error",
|
|
60
|
+
error: state.error,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
case "pending":
|
|
64
|
+
return {
|
|
65
|
+
status: "loading",
|
|
66
|
+
promise: state.promise as Promise<Awaited<T>>,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/core/hook.test.ts
CHANGED
|
@@ -176,20 +176,20 @@ describe("hook", () => {
|
|
|
176
176
|
const hookA = hook<string | undefined>();
|
|
177
177
|
const hookB = hook<string | undefined>();
|
|
178
178
|
|
|
179
|
-
// Create custom setups that track release order
|
|
179
|
+
// Create custom setups that track release order using override/reset
|
|
180
180
|
const setupA = () => {
|
|
181
|
-
hookA.
|
|
181
|
+
hookA.override(() => "A");
|
|
182
182
|
return () => {
|
|
183
183
|
order.push("release A");
|
|
184
|
-
hookA.
|
|
184
|
+
hookA.reset();
|
|
185
185
|
};
|
|
186
186
|
};
|
|
187
187
|
|
|
188
188
|
const setupB = () => {
|
|
189
|
-
hookB.
|
|
189
|
+
hookB.override(() => "B");
|
|
190
190
|
return () => {
|
|
191
191
|
order.push("release B");
|
|
192
|
-
hookB.
|
|
192
|
+
hookB.reset();
|
|
193
193
|
};
|
|
194
194
|
};
|
|
195
195
|
|
package/src/core/hook.ts
CHANGED
package/src/core/onCreateHook.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Effect } from "./effect";
|
|
1
2
|
import { hook } from "./hook";
|
|
2
3
|
import {
|
|
3
4
|
MutableAtomMeta,
|
|
@@ -5,12 +6,13 @@ import {
|
|
|
5
6
|
MutableAtom,
|
|
6
7
|
DerivedAtom,
|
|
7
8
|
ModuleMeta,
|
|
9
|
+
EffectMeta,
|
|
8
10
|
} from "./types";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Information provided when a mutable atom is created.
|
|
12
14
|
*/
|
|
13
|
-
export interface
|
|
15
|
+
export interface MutableInfo {
|
|
14
16
|
/** Discriminator for mutable atoms */
|
|
15
17
|
type: "mutable";
|
|
16
18
|
/** Optional key from atom options (for debugging/devtools) */
|
|
@@ -18,13 +20,13 @@ export interface MutableAtomCreateInfo {
|
|
|
18
20
|
/** Optional metadata from atom options */
|
|
19
21
|
meta: MutableAtomMeta | undefined;
|
|
20
22
|
/** The created mutable atom instance */
|
|
21
|
-
|
|
23
|
+
instance: MutableAtom<unknown>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Information provided when a derived atom is created.
|
|
26
28
|
*/
|
|
27
|
-
export interface
|
|
29
|
+
export interface DerivedInfo {
|
|
28
30
|
/** Discriminator for derived atoms */
|
|
29
31
|
type: "derived";
|
|
30
32
|
/** Optional key from derived options (for debugging/devtools) */
|
|
@@ -32,18 +34,32 @@ export interface DerivedAtomCreateInfo {
|
|
|
32
34
|
/** Optional metadata from derived options */
|
|
33
35
|
meta: DerivedAtomMeta | undefined;
|
|
34
36
|
/** The created derived atom instance */
|
|
35
|
-
|
|
37
|
+
instance: DerivedAtom<unknown, boolean>;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
41
|
+
* Information provided when an effect is created.
|
|
40
42
|
*/
|
|
41
|
-
export
|
|
43
|
+
export interface EffectInfo {
|
|
44
|
+
/** Discriminator for effects */
|
|
45
|
+
type: "effect";
|
|
46
|
+
/** Optional key from effect options (for debugging/devtools) */
|
|
47
|
+
key: string | undefined;
|
|
48
|
+
/** Optional metadata from effect options */
|
|
49
|
+
meta: EffectMeta | undefined;
|
|
50
|
+
/** The created effect instance */
|
|
51
|
+
instance: Effect;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Union type for atom/derived/effect creation info.
|
|
56
|
+
*/
|
|
57
|
+
export type CreateInfo = MutableInfo | DerivedInfo | EffectInfo;
|
|
42
58
|
|
|
43
59
|
/**
|
|
44
60
|
* Information provided when a module (via define()) is created.
|
|
45
61
|
*/
|
|
46
|
-
export interface
|
|
62
|
+
export interface ModuleInfo {
|
|
47
63
|
/** Discriminator for modules */
|
|
48
64
|
type: "module";
|
|
49
65
|
/** Optional key from define options (for debugging/devtools) */
|
|
@@ -51,7 +67,7 @@ export interface ModuleCreateInfo {
|
|
|
51
67
|
/** Optional metadata from define options */
|
|
52
68
|
meta: ModuleMeta | undefined;
|
|
53
69
|
/** The created module instance */
|
|
54
|
-
|
|
70
|
+
instance: unknown;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/**
|
|
@@ -62,31 +78,30 @@ export interface ModuleCreateInfo {
|
|
|
62
78
|
* - **Debugging** - log atom creation for troubleshooting
|
|
63
79
|
* - **Testing** - verify expected atoms are created
|
|
64
80
|
*
|
|
81
|
+
* **IMPORTANT**: Always use `.override()` to preserve the hook chain.
|
|
82
|
+
* Direct assignment to `.current` will break existing handlers.
|
|
83
|
+
*
|
|
65
84
|
* @example Basic logging
|
|
66
85
|
* ```ts
|
|
67
|
-
* onCreateHook.
|
|
86
|
+
* onCreateHook.override((prev) => (info) => {
|
|
87
|
+
* prev?.(info); // call existing handlers first
|
|
68
88
|
* console.log(`Created ${info.type}: ${info.key ?? "anonymous"}`);
|
|
69
|
-
* };
|
|
89
|
+
* });
|
|
70
90
|
* ```
|
|
71
91
|
*
|
|
72
92
|
* @example DevTools integration
|
|
73
93
|
* ```ts
|
|
74
|
-
* const
|
|
75
|
-
* const modules = new Map();
|
|
94
|
+
* const registry = new Map();
|
|
76
95
|
*
|
|
77
|
-
* onCreateHook.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* atoms.set(info.key, info.atom);
|
|
82
|
-
* }
|
|
83
|
-
* };
|
|
96
|
+
* onCreateHook.override((prev) => (info) => {
|
|
97
|
+
* prev?.(info); // preserve chain
|
|
98
|
+
* registry.set(info.key, info.instance);
|
|
99
|
+
* });
|
|
84
100
|
* ```
|
|
85
101
|
*
|
|
86
|
-
* @example
|
|
102
|
+
* @example Reset to default (disable all handlers)
|
|
87
103
|
* ```ts
|
|
88
|
-
* onCreateHook.
|
|
104
|
+
* onCreateHook.reset();
|
|
89
105
|
* ```
|
|
90
106
|
*/
|
|
91
|
-
export const onCreateHook =
|
|
92
|
-
hook<(info: AtomCreateInfo | ModuleCreateInfo) => void>();
|
|
107
|
+
export const onCreateHook = hook<(info: CreateInfo | ModuleInfo) => void>();
|