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,227 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hook } from "./hook";
|
|
3
|
+
|
|
4
|
+
describe("hook", () => {
|
|
5
|
+
describe("createHook", () => {
|
|
6
|
+
it("should create a hook with initial value", () => {
|
|
7
|
+
const myHook = hook(42);
|
|
8
|
+
expect(myHook.current).toBe(42);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should create a hook with undefined when no initial value", () => {
|
|
12
|
+
const myHook = hook<string>();
|
|
13
|
+
expect(myHook.current).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should create a hook with object value", () => {
|
|
17
|
+
const obj = { name: "test" };
|
|
18
|
+
const myHook = hook(obj);
|
|
19
|
+
expect(myHook.current).toBe(obj);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should create a hook with function value", () => {
|
|
23
|
+
const fn = () => 42;
|
|
24
|
+
const myHook = hook(fn);
|
|
25
|
+
expect(myHook.current).toBe(fn);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("hook.override", () => {
|
|
30
|
+
it("should override the current value using reducer", () => {
|
|
31
|
+
const myHook = hook(0);
|
|
32
|
+
expect(myHook.current).toBe(0);
|
|
33
|
+
|
|
34
|
+
myHook.override(() => 100);
|
|
35
|
+
expect(myHook.current).toBe(100);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should allow multiple overrides", () => {
|
|
39
|
+
const myHook = hook("initial");
|
|
40
|
+
myHook.override(() => "first");
|
|
41
|
+
expect(myHook.current).toBe("first");
|
|
42
|
+
|
|
43
|
+
myHook.override(() => "second");
|
|
44
|
+
expect(myHook.current).toBe("second");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should receive previous value in reducer", () => {
|
|
48
|
+
const myHook = hook(10);
|
|
49
|
+
myHook.override((prev) => prev + 5);
|
|
50
|
+
expect(myHook.current).toBe(15);
|
|
51
|
+
|
|
52
|
+
myHook.override((prev) => prev * 2);
|
|
53
|
+
expect(myHook.current).toBe(30);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should support composing handlers (middleware pattern)", () => {
|
|
57
|
+
const calls: string[] = [];
|
|
58
|
+
const myHook = hook<((msg: string) => void) | undefined>(undefined);
|
|
59
|
+
|
|
60
|
+
// First handler
|
|
61
|
+
myHook.override(() => (msg) => calls.push(`first: ${msg}`));
|
|
62
|
+
|
|
63
|
+
// Compose with second handler
|
|
64
|
+
myHook.override((prev) => (msg) => {
|
|
65
|
+
prev?.(msg);
|
|
66
|
+
calls.push(`second: ${msg}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
myHook.current?.("hello");
|
|
70
|
+
expect(calls).toEqual(["first: hello", "second: hello"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("hook setup/release pattern", () => {
|
|
75
|
+
it("should create a setup function that returns a release function", () => {
|
|
76
|
+
const myHook = hook(0);
|
|
77
|
+
const setup = myHook(() => 10);
|
|
78
|
+
|
|
79
|
+
expect(typeof setup).toBe("function");
|
|
80
|
+
|
|
81
|
+
const release = setup();
|
|
82
|
+
expect(typeof release).toBe("function");
|
|
83
|
+
expect(myHook.current).toBe(10);
|
|
84
|
+
|
|
85
|
+
release();
|
|
86
|
+
expect(myHook.current).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should support nested setup/release", () => {
|
|
90
|
+
const myHook = hook(0);
|
|
91
|
+
|
|
92
|
+
const setup1 = myHook(() => 1);
|
|
93
|
+
const release1 = setup1();
|
|
94
|
+
expect(myHook.current).toBe(1);
|
|
95
|
+
|
|
96
|
+
const setup2 = myHook(() => 2);
|
|
97
|
+
const release2 = setup2();
|
|
98
|
+
expect(myHook.current).toBe(2);
|
|
99
|
+
|
|
100
|
+
release2();
|
|
101
|
+
expect(myHook.current).toBe(1);
|
|
102
|
+
|
|
103
|
+
release1();
|
|
104
|
+
expect(myHook.current).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should receive previous value in setup reducer", () => {
|
|
108
|
+
const myHook = hook(10);
|
|
109
|
+
|
|
110
|
+
const setup = myHook((prev) => prev + 5);
|
|
111
|
+
const release = setup();
|
|
112
|
+
expect(myHook.current).toBe(15);
|
|
113
|
+
|
|
114
|
+
release();
|
|
115
|
+
expect(myHook.current).toBe(10);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("hook.use", () => {
|
|
120
|
+
it("should temporarily set hook value during function execution", () => {
|
|
121
|
+
const myHook = hook(0);
|
|
122
|
+
|
|
123
|
+
const result = hook.use([myHook(() => 42)], () => {
|
|
124
|
+
expect(myHook.current).toBe(42);
|
|
125
|
+
return "done";
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).toBe("done");
|
|
129
|
+
expect(myHook.current).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should support multiple hooks", () => {
|
|
133
|
+
const hookA = hook("a");
|
|
134
|
+
const hookB = hook("b");
|
|
135
|
+
|
|
136
|
+
hook.use([hookA(() => "A"), hookB(() => "B")], () => {
|
|
137
|
+
expect(hookA.current).toBe("A");
|
|
138
|
+
expect(hookB.current).toBe("B");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(hookA.current).toBe("a");
|
|
142
|
+
expect(hookB.current).toBe("b");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should restore values even if function throws", () => {
|
|
146
|
+
const myHook = hook(0);
|
|
147
|
+
|
|
148
|
+
expect(() => {
|
|
149
|
+
hook.use([myHook(() => 42)], () => {
|
|
150
|
+
expect(myHook.current).toBe(42);
|
|
151
|
+
throw new Error("test error");
|
|
152
|
+
});
|
|
153
|
+
}).toThrow("test error");
|
|
154
|
+
|
|
155
|
+
expect(myHook.current).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should support nested hook.use calls", () => {
|
|
159
|
+
const myHook = hook(0);
|
|
160
|
+
|
|
161
|
+
hook.use([myHook(() => 1)], () => {
|
|
162
|
+
expect(myHook.current).toBe(1);
|
|
163
|
+
|
|
164
|
+
hook.use([myHook(() => 2)], () => {
|
|
165
|
+
expect(myHook.current).toBe(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(myHook.current).toBe(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(myHook.current).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should release hooks in reverse order", () => {
|
|
175
|
+
const order: string[] = [];
|
|
176
|
+
const hookA = hook<string | undefined>();
|
|
177
|
+
const hookB = hook<string | undefined>();
|
|
178
|
+
|
|
179
|
+
// Create custom setups that track release order
|
|
180
|
+
const setupA = () => {
|
|
181
|
+
hookA.current = "A";
|
|
182
|
+
return () => {
|
|
183
|
+
order.push("release A");
|
|
184
|
+
hookA.current = undefined;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const setupB = () => {
|
|
189
|
+
hookB.current = "B";
|
|
190
|
+
return () => {
|
|
191
|
+
order.push("release B");
|
|
192
|
+
hookB.current = undefined;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
hook.use([setupA, setupB], () => {
|
|
197
|
+
expect(hookA.current).toBe("A");
|
|
198
|
+
expect(hookB.current).toBe("B");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(order).toEqual(["release B", "release A"]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should work with empty setups array", () => {
|
|
205
|
+
const result = hook.use([], () => "result");
|
|
206
|
+
expect(result).toBe("result");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should return the function result", () => {
|
|
210
|
+
const myHook = hook(0);
|
|
211
|
+
const result = hook.use([myHook(() => 1)], () => {
|
|
212
|
+
return { value: myHook.current };
|
|
213
|
+
});
|
|
214
|
+
expect(result).toEqual({ value: 1 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should support reducer composition in hook.use", () => {
|
|
218
|
+
const myHook = hook(10);
|
|
219
|
+
|
|
220
|
+
hook.use([myHook((prev) => prev + 5)], () => {
|
|
221
|
+
expect(myHook.current).toBe(15);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(myHook.current).toBe(10);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
package/src/core/hook.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A setup function that returns a release function.
|
|
3
|
+
* Called by hook.use() to activate a hook, release restores previous value.
|
|
4
|
+
*/
|
|
5
|
+
export type HookSetup = () => VoidFunction;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A hook is a callable factory that creates setup functions,
|
|
9
|
+
* with direct access to current value via `.current`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const myHook = hook<string>("default");
|
|
14
|
+
*
|
|
15
|
+
* // Read current value (fast - direct property access)
|
|
16
|
+
* console.log(myHook.current); // "default"
|
|
17
|
+
*
|
|
18
|
+
* // Create a setup function (reducer receives previous value)
|
|
19
|
+
* const setup = myHook(() => "new value");
|
|
20
|
+
*
|
|
21
|
+
* // Use with hook.use()
|
|
22
|
+
* hook.use([myHook(() => "temp")], () => {
|
|
23
|
+
* console.log(myHook.current); // "temp"
|
|
24
|
+
* });
|
|
25
|
+
* console.log(myHook.current); // "default" (restored)
|
|
26
|
+
*
|
|
27
|
+
* // Compose with previous value
|
|
28
|
+
* myHook.override(prev => {
|
|
29
|
+
* console.log("Previous:", prev);
|
|
30
|
+
* return "next";
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface Hook<T> {
|
|
35
|
+
/**
|
|
36
|
+
* Creates a HookSetup that will set this hook using a reducer.
|
|
37
|
+
* The reducer receives the previous value and returns the next value.
|
|
38
|
+
*
|
|
39
|
+
* @param reducer - Function that receives previous value and returns next value
|
|
40
|
+
*
|
|
41
|
+
* @example Set new value (ignore previous)
|
|
42
|
+
* ```ts
|
|
43
|
+
* myHook(() => "new value")
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example Compose with previous
|
|
47
|
+
* ```ts
|
|
48
|
+
* myHook(prev => {
|
|
49
|
+
* prev?.(); // call previous handler
|
|
50
|
+
* return newHandler;
|
|
51
|
+
* })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
(reducer: (prev: T) => T): HookSetup;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Current value of the hook. Direct property access for fast reads.
|
|
58
|
+
*/
|
|
59
|
+
current: T;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Override the current value using a reducer.
|
|
63
|
+
* The reducer receives the previous value and returns the next value.
|
|
64
|
+
* Unlike the setup/release pattern, this is an immediate mutation.
|
|
65
|
+
*
|
|
66
|
+
* @param reducer - Function that receives previous value and returns next value
|
|
67
|
+
*
|
|
68
|
+
* @example Set new value (ignore previous)
|
|
69
|
+
* ```ts
|
|
70
|
+
* myHook.override(() => "new value")
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @example Compose with previous (middleware pattern)
|
|
74
|
+
* ```ts
|
|
75
|
+
* onCreateHook.override(prev => (info) => {
|
|
76
|
+
* prev?.(info); // call existing handler
|
|
77
|
+
* console.log("Created:", info.key);
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
override(reducer: (prev: T) => T): void;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reset the hook to its initial value.
|
|
85
|
+
*/
|
|
86
|
+
reset(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates a new hook with an initial value.
|
|
91
|
+
*
|
|
92
|
+
* Hooks use the setup/release pattern for performance:
|
|
93
|
+
* - Reads are direct property access (fastest)
|
|
94
|
+
* - Writes use setup/release for proper nesting
|
|
95
|
+
*
|
|
96
|
+
* @param initial - Initial value for the hook
|
|
97
|
+
* @returns A Hook instance
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* // Create a hook
|
|
102
|
+
* const countHook = hook(0);
|
|
103
|
+
*
|
|
104
|
+
* // Read
|
|
105
|
+
* console.log(countHook.current); // 0
|
|
106
|
+
*
|
|
107
|
+
* // Use with hook.use() - reducer receives previous value
|
|
108
|
+
* hook.use([countHook(() => 5)], () => {
|
|
109
|
+
* console.log(countHook.current); // 5
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* // Compose with previous (middleware pattern)
|
|
113
|
+
* myHook.override(prev => (info) => {
|
|
114
|
+
* prev?.(info); // call existing
|
|
115
|
+
* newHandler(info);
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
function createHook<T>(initial: T): Hook<T>;
|
|
120
|
+
function createHook<T>(): Hook<T | undefined>;
|
|
121
|
+
function createHook<T>(initial?: T): Hook<T | undefined> {
|
|
122
|
+
// The hook function creates a setup that returns a release
|
|
123
|
+
const h = Object.assign(
|
|
124
|
+
(reducer: (prev: T | undefined) => T | undefined): HookSetup => {
|
|
125
|
+
return () => {
|
|
126
|
+
const prev = h.current;
|
|
127
|
+
h.current = reducer(prev);
|
|
128
|
+
return () => {
|
|
129
|
+
h.current = prev;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
current: initial,
|
|
135
|
+
// Override method for direct mutation using reducer
|
|
136
|
+
override: (reducer: (prev: T | undefined) => T | undefined) => {
|
|
137
|
+
h.current = reducer(h.current);
|
|
138
|
+
},
|
|
139
|
+
reset: () => {
|
|
140
|
+
h.current = initial;
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return h as Hook<T | undefined>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Executes a function with multiple hooks temporarily set.
|
|
150
|
+
*
|
|
151
|
+
* @param setups - Array of HookSetup functions (from hook factories)
|
|
152
|
+
* @param fn - Function to execute with hooks active
|
|
153
|
+
* @returns The return value of fn
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* hook.use([trackHook(myTracker), debugHook(true)], () => {
|
|
158
|
+
* // hooks active here
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
function use<T>(setups: HookSetup[], fn: () => T): T {
|
|
163
|
+
const releases: VoidFunction[] = [];
|
|
164
|
+
for (const setup of setups) {
|
|
165
|
+
releases.push(setup());
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
return fn();
|
|
169
|
+
} finally {
|
|
170
|
+
for (const release of releases.reverse()) {
|
|
171
|
+
release();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Combine into namespace
|
|
177
|
+
export const hook = Object.assign(createHook, { use });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Atom, DerivedAtom, SYMBOL_ATOM, SYMBOL_DERIVED } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard to check if a value is an Atom.
|
|
5
|
+
*/
|
|
6
|
+
export function isAtom<T>(value: unknown): value is Atom<T> {
|
|
7
|
+
return (
|
|
8
|
+
value !== null &&
|
|
9
|
+
typeof value === "object" &&
|
|
10
|
+
SYMBOL_ATOM in value &&
|
|
11
|
+
(value as Atom<T>)[SYMBOL_ATOM] === true
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type guard to check if a value is a DerivedAtom.
|
|
17
|
+
*/
|
|
18
|
+
export function isDerived<T>(
|
|
19
|
+
value: unknown
|
|
20
|
+
): value is DerivedAtom<T, boolean> {
|
|
21
|
+
return (
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof value === "object" &&
|
|
24
|
+
SYMBOL_DERIVED in value &&
|
|
25
|
+
(value as DerivedAtom<T, boolean>)[SYMBOL_DERIVED] === true
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
3
|
+
|
|
4
|
+
describe("isPromiseLike", () => {
|
|
5
|
+
describe("returns true for PromiseLike values", () => {
|
|
6
|
+
it("should return true for native Promise", () => {
|
|
7
|
+
expect(isPromiseLike(Promise.resolve(1))).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should return true for rejected Promise", () => {
|
|
11
|
+
const rejected = Promise.reject(new Error("test"));
|
|
12
|
+
rejected.catch(() => {}); // Prevent unhandled rejection
|
|
13
|
+
expect(isPromiseLike(rejected)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should return true for object with then method", () => {
|
|
17
|
+
const thenable = { then: () => {} };
|
|
18
|
+
expect(isPromiseLike(thenable)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return true for object with then method that takes callbacks", () => {
|
|
22
|
+
const thenable = {
|
|
23
|
+
then: (resolve: (v: number) => void, _reject: (e: Error) => void) => {
|
|
24
|
+
resolve(42);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
expect(isPromiseLike(thenable)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("returns false for non-PromiseLike values", () => {
|
|
32
|
+
it("should return false for null", () => {
|
|
33
|
+
expect(isPromiseLike(null)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return false for undefined", () => {
|
|
37
|
+
expect(isPromiseLike(undefined)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return false for number", () => {
|
|
41
|
+
expect(isPromiseLike(42)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return false for string", () => {
|
|
45
|
+
expect(isPromiseLike("hello")).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should return false for boolean", () => {
|
|
49
|
+
expect(isPromiseLike(true)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should return false for plain object without then", () => {
|
|
53
|
+
expect(isPromiseLike({ value: 1 })).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should return false for array", () => {
|
|
57
|
+
expect(isPromiseLike([1, 2, 3])).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should return false for function", () => {
|
|
61
|
+
expect(isPromiseLike(() => {})).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return false for object with non-function then property", () => {
|
|
65
|
+
expect(isPromiseLike({ then: "not a function" })).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return false for object with then as number", () => {
|
|
69
|
+
expect(isPromiseLike({ then: 123 })).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a value is a PromiseLike (has a .then method).
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* isPromiseLike(Promise.resolve(1)) // true
|
|
6
|
+
* isPromiseLike({ then: () => {} }) // true
|
|
7
|
+
* isPromiseLike(42) // false
|
|
8
|
+
*/
|
|
9
|
+
export function isPromiseLike<T>(value: unknown): value is PromiseLike<T> {
|
|
10
|
+
return (
|
|
11
|
+
value !== null &&
|
|
12
|
+
typeof value === "object" &&
|
|
13
|
+
"then" in value &&
|
|
14
|
+
typeof (value as any).then === "function"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { hook } from "./hook";
|
|
2
|
+
import {
|
|
3
|
+
MutableAtomMeta,
|
|
4
|
+
DerivedAtomMeta,
|
|
5
|
+
MutableAtom,
|
|
6
|
+
DerivedAtom,
|
|
7
|
+
ModuleMeta,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Information provided when a mutable atom is created.
|
|
12
|
+
*/
|
|
13
|
+
export interface MutableAtomCreateInfo {
|
|
14
|
+
/** Discriminator for mutable atoms */
|
|
15
|
+
type: "mutable";
|
|
16
|
+
/** Optional key from atom options (for debugging/devtools) */
|
|
17
|
+
key: string | undefined;
|
|
18
|
+
/** Optional metadata from atom options */
|
|
19
|
+
meta: MutableAtomMeta | undefined;
|
|
20
|
+
/** The created mutable atom instance */
|
|
21
|
+
atom: MutableAtom<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Information provided when a derived atom is created.
|
|
26
|
+
*/
|
|
27
|
+
export interface DerivedAtomCreateInfo {
|
|
28
|
+
/** Discriminator for derived atoms */
|
|
29
|
+
type: "derived";
|
|
30
|
+
/** Optional key from derived options (for debugging/devtools) */
|
|
31
|
+
key: string | undefined;
|
|
32
|
+
/** Optional metadata from derived options */
|
|
33
|
+
meta: DerivedAtomMeta | undefined;
|
|
34
|
+
/** The created derived atom instance */
|
|
35
|
+
atom: DerivedAtom<unknown, boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Union type for atom creation info (mutable or derived).
|
|
40
|
+
*/
|
|
41
|
+
export type AtomCreateInfo = MutableAtomCreateInfo | DerivedAtomCreateInfo;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Information provided when a module (via define()) is created.
|
|
45
|
+
*/
|
|
46
|
+
export interface ModuleCreateInfo {
|
|
47
|
+
/** Discriminator for modules */
|
|
48
|
+
type: "module";
|
|
49
|
+
/** Optional key from define options (for debugging/devtools) */
|
|
50
|
+
key: string | undefined;
|
|
51
|
+
/** Optional metadata from define options */
|
|
52
|
+
meta: ModuleMeta | undefined;
|
|
53
|
+
/** The created module instance */
|
|
54
|
+
module: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Global hook that fires whenever an atom or module is created.
|
|
59
|
+
*
|
|
60
|
+
* This is useful for:
|
|
61
|
+
* - **DevTools integration** - track all atoms/modules in the app
|
|
62
|
+
* - **Debugging** - log atom creation for troubleshooting
|
|
63
|
+
* - **Testing** - verify expected atoms are created
|
|
64
|
+
*
|
|
65
|
+
* @example Basic logging
|
|
66
|
+
* ```ts
|
|
67
|
+
* onCreateHook.current = (info) => {
|
|
68
|
+
* console.log(`Created ${info.type}: ${info.key ?? "anonymous"}`);
|
|
69
|
+
* };
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example DevTools integration
|
|
73
|
+
* ```ts
|
|
74
|
+
* const atoms = new Map();
|
|
75
|
+
* const modules = new Map();
|
|
76
|
+
*
|
|
77
|
+
* onCreateHook.current = (info) => {
|
|
78
|
+
* if (info.type === "module") {
|
|
79
|
+
* modules.set(info.key, info.module);
|
|
80
|
+
* } else {
|
|
81
|
+
* atoms.set(info.key, info.atom);
|
|
82
|
+
* }
|
|
83
|
+
* };
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example Cleanup (disable hook)
|
|
87
|
+
* ```ts
|
|
88
|
+
* onCreateHook.current = undefined;
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export const onCreateHook =
|
|
92
|
+
hook<(info: AtomCreateInfo | ModuleCreateInfo) => void>();
|