atomirx 0.0.2 → 0.0.4
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 +866 -159
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +55 -21
- package/dist/core/effect.d.ts +47 -51
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +55 -19
- package/dist/core/withReady.d.ts +69 -0
- package/dist/index-CqO6BDwj.cjs +1 -0
- package/dist/index-D8RDOTB_.js +1319 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +12 -10
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +423 -379
- 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 +143 -21
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/derived.test.ts +727 -72
- package/src/core/derived.ts +141 -73
- package/src/core/effect.test.ts +259 -39
- package/src/core/effect.ts +62 -85
- package/src/core/getAtomState.ts +69 -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 +54 -26
- package/src/core/withReady.test.ts +360 -0
- package/src/core/withReady.ts +127 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +11 -6
- 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/withReady.test.d.ts} +0 -0
package/src/core/atom.ts
CHANGED
|
@@ -2,16 +2,42 @@ import { onCreateHook } from "./onCreateHook";
|
|
|
2
2
|
import { emitter } from "./emitter";
|
|
3
3
|
import { resolveEquality } from "./equality";
|
|
4
4
|
import { scheduleNotifyHook } from "./scheduleNotifyHook";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AtomOptions,
|
|
7
|
+
MutableAtom,
|
|
8
|
+
SYMBOL_ATOM,
|
|
9
|
+
Equality,
|
|
10
|
+
Pipeable,
|
|
11
|
+
Atom,
|
|
12
|
+
} from "./types";
|
|
6
13
|
import { withUse } from "./withUse";
|
|
7
14
|
import { isPromiseLike } from "./isPromiseLike";
|
|
8
15
|
import { trackPromise } from "./promiseCache";
|
|
9
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Context object passed to atom initializer functions.
|
|
19
|
+
* Provides utilities for cleanup and cancellation.
|
|
20
|
+
*/
|
|
21
|
+
export interface AtomContext extends Pipeable {
|
|
22
|
+
/**
|
|
23
|
+
* AbortSignal that is aborted when the atom value changes (via set or reset).
|
|
24
|
+
* Use this to cancel pending async operations.
|
|
25
|
+
*/
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
/**
|
|
28
|
+
* Register a cleanup function that runs when the atom value changes or resets.
|
|
29
|
+
* Multiple cleanup functions can be registered; they run in FIFO order.
|
|
30
|
+
*
|
|
31
|
+
* @param cleanup - Function to run during cleanup
|
|
32
|
+
*/
|
|
33
|
+
onCleanup(cleanup: VoidFunction): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
/**
|
|
11
37
|
* Creates a mutable atom - a reactive state container that holds a single value.
|
|
12
38
|
*
|
|
13
39
|
* MutableAtom is a raw storage container. It stores values as-is, including Promises.
|
|
14
|
-
* If you store a Promise, `.
|
|
40
|
+
* If you store a Promise, `.get()` returns the Promise object itself.
|
|
15
41
|
*
|
|
16
42
|
* Features:
|
|
17
43
|
* - Raw storage: stores any value including Promises
|
|
@@ -25,14 +51,14 @@ import { trackPromise } from "./promiseCache";
|
|
|
25
51
|
* @param options - Configuration options
|
|
26
52
|
* @param options.meta - Optional metadata for debugging/devtools
|
|
27
53
|
* @param options.equals - Equality strategy for change detection (default: strict)
|
|
28
|
-
* @returns A mutable atom with
|
|
54
|
+
* @returns A mutable atom with get, set/reset methods
|
|
29
55
|
*
|
|
30
56
|
* @example Synchronous value
|
|
31
57
|
* ```ts
|
|
32
58
|
* const count = atom(0);
|
|
33
59
|
* count.set(1);
|
|
34
60
|
* count.set(prev => prev + 1);
|
|
35
|
-
* console.log(count.
|
|
61
|
+
* console.log(count.get()); // 2
|
|
36
62
|
* ```
|
|
37
63
|
*
|
|
38
64
|
* @example Lazy initialization
|
|
@@ -51,7 +77,7 @@ import { trackPromise } from "./promiseCache";
|
|
|
51
77
|
* @example Async value (stores Promise as-is)
|
|
52
78
|
* ```ts
|
|
53
79
|
* const posts = atom(fetchPosts());
|
|
54
|
-
* posts.
|
|
80
|
+
* posts.get(); // Promise<Post[]>
|
|
55
81
|
*
|
|
56
82
|
* // Refetch - set a new Promise
|
|
57
83
|
* posts.set(fetchPosts());
|
|
@@ -69,17 +95,45 @@ import { trackPromise } from "./promiseCache";
|
|
|
69
95
|
* ```
|
|
70
96
|
*/
|
|
71
97
|
export function atom<T>(
|
|
72
|
-
valueOrInit: T | (() => T),
|
|
98
|
+
valueOrInit: T | ((context: AtomContext) => T),
|
|
73
99
|
options: AtomOptions<T> = {}
|
|
74
100
|
): MutableAtom<T> {
|
|
75
101
|
const changeEmitter = emitter();
|
|
76
102
|
const eq = resolveEquality(options.equals as Equality<unknown>);
|
|
77
103
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
// Track current AbortController and cleanup emitter for init context
|
|
105
|
+
let abortController: AbortController | null = null;
|
|
106
|
+
const cleanupEmitter = emitter();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Aborts the current signal and calls all registered cleanup functions.
|
|
110
|
+
*/
|
|
111
|
+
const abortAndCleanup = (reason: string) => {
|
|
112
|
+
// Abort the signal first
|
|
113
|
+
if (abortController) {
|
|
114
|
+
abortController.abort(reason);
|
|
115
|
+
abortController = null;
|
|
116
|
+
}
|
|
117
|
+
// Then call all registered cleanups
|
|
118
|
+
cleanupEmitter.emitAndClear();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a fresh AtomContext for initializer functions.
|
|
123
|
+
*/
|
|
124
|
+
const createContext = (): AtomContext => {
|
|
125
|
+
abortController = new AbortController();
|
|
126
|
+
return withUse({
|
|
127
|
+
signal: abortController.signal,
|
|
128
|
+
onCleanup: cleanupEmitter.on,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Resolve initial value (supports lazy initialization with context)
|
|
133
|
+
const isInitFunction = typeof valueOrInit === "function";
|
|
134
|
+
const initialValue: T = isInitFunction
|
|
135
|
+
? (valueOrInit as (context: AtomContext) => T)(createContext())
|
|
136
|
+
: valueOrInit;
|
|
83
137
|
|
|
84
138
|
// Current value
|
|
85
139
|
let value: T = initialValue;
|
|
@@ -118,6 +172,9 @@ export function atom<T>(
|
|
|
118
172
|
return;
|
|
119
173
|
}
|
|
120
174
|
|
|
175
|
+
// Abort previous signal and run cleanups before changing value
|
|
176
|
+
abortAndCleanup("value changed");
|
|
177
|
+
|
|
121
178
|
value = nextValue;
|
|
122
179
|
isDirty = true;
|
|
123
180
|
isPromiseLike(value) && trackPromise(value);
|
|
@@ -128,11 +185,13 @@ export function atom<T>(
|
|
|
128
185
|
* Resets the atom to its initial value and clears dirty flag.
|
|
129
186
|
*/
|
|
130
187
|
const reset = () => {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
188
|
+
// Abort previous signal and run cleanups before resetting
|
|
189
|
+
abortAndCleanup("reset");
|
|
190
|
+
|
|
191
|
+
// Re-run initializer if function (with fresh context), otherwise use initial value
|
|
192
|
+
const nextValue: T = isInitFunction
|
|
193
|
+
? (valueOrInit as (context: AtomContext) => T)(createContext())
|
|
194
|
+
: valueOrInit;
|
|
136
195
|
|
|
137
196
|
// Track promise if needed
|
|
138
197
|
isPromiseLike(nextValue) && trackPromise(nextValue);
|
|
@@ -161,21 +220,20 @@ export function atom<T>(
|
|
|
161
220
|
meta: options.meta,
|
|
162
221
|
|
|
163
222
|
/**
|
|
164
|
-
*
|
|
223
|
+
* Get the current value (raw, including Promises).
|
|
165
224
|
*/
|
|
166
|
-
get
|
|
225
|
+
get(): any {
|
|
167
226
|
return value;
|
|
168
227
|
},
|
|
169
|
-
|
|
228
|
+
use: undefined as any,
|
|
170
229
|
set,
|
|
171
230
|
reset,
|
|
172
231
|
dirty,
|
|
173
|
-
|
|
174
232
|
/**
|
|
175
233
|
* Subscribe to value changes.
|
|
176
234
|
*/
|
|
177
235
|
on: changeEmitter.on,
|
|
178
|
-
}) as MutableAtom<T>;
|
|
236
|
+
}) as Pipeable & MutableAtom<T>;
|
|
179
237
|
|
|
180
238
|
// Notify devtools/plugins of atom creation
|
|
181
239
|
onCreateHook.current?.({
|
|
@@ -187,3 +245,67 @@ export function atom<T>(
|
|
|
187
245
|
|
|
188
246
|
return a;
|
|
189
247
|
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Type utility to expose an atom as read-only when exporting from a module.
|
|
251
|
+
*
|
|
252
|
+
* This function returns the same atom instance but with a narrowed type (`Atom<T>`)
|
|
253
|
+
* that hides mutable methods like `set()` and `reset()`. Use this to encapsulate
|
|
254
|
+
* state mutations within a module while allowing external consumers to only read
|
|
255
|
+
* and subscribe to changes.
|
|
256
|
+
*
|
|
257
|
+
* **Note:** This is a compile-time restriction only. At runtime, the atom is unchanged.
|
|
258
|
+
* Consumers with access to the original reference can still mutate it.
|
|
259
|
+
*
|
|
260
|
+
* @param atom - The atom (or record of atoms) to expose as read-only
|
|
261
|
+
* @returns The same atom(s) with a read-only type signature
|
|
262
|
+
*
|
|
263
|
+
* @example Single atom
|
|
264
|
+
* ```ts
|
|
265
|
+
* const myModule = define(() => {
|
|
266
|
+
* const count$ = atom(0); // Internal mutable atom
|
|
267
|
+
*
|
|
268
|
+
* return {
|
|
269
|
+
* // Expose as read-only - consumers can't call set() or reset()
|
|
270
|
+
* count$: readonly(count$),
|
|
271
|
+
* // Mutations only possible through explicit actions
|
|
272
|
+
* increment: () => count$.set(prev => prev + 1),
|
|
273
|
+
* decrement: () => count$.set(prev => prev - 1),
|
|
274
|
+
* };
|
|
275
|
+
* });
|
|
276
|
+
*
|
|
277
|
+
* // Usage:
|
|
278
|
+
* const { count$, increment } = myModule();
|
|
279
|
+
* count$.get(); // ✅ OK - reading is allowed
|
|
280
|
+
* count$.on(console.log); // ✅ OK - subscribing is allowed
|
|
281
|
+
* count$.set(5); // ❌ TypeScript error - set() not available on Atom<T>
|
|
282
|
+
* increment(); // ✅ OK - use exposed action instead
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @example Record of atoms
|
|
286
|
+
* ```ts
|
|
287
|
+
* const myModule = define(() => {
|
|
288
|
+
* const count$ = atom(0);
|
|
289
|
+
* const name$ = atom('');
|
|
290
|
+
*
|
|
291
|
+
* return {
|
|
292
|
+
* // Expose multiple atoms as read-only at once
|
|
293
|
+
* ...readonly({ count$, name$ }),
|
|
294
|
+
* setName: (name: string) => name$.set(name),
|
|
295
|
+
* };
|
|
296
|
+
* });
|
|
297
|
+
*
|
|
298
|
+
* // Usage:
|
|
299
|
+
* const { count$, name$, setName } = myModule();
|
|
300
|
+
* count$.get(); // ✅ Atom<number>
|
|
301
|
+
* name$.get(); // ✅ Atom<string>
|
|
302
|
+
* name$.set(''); // ❌ TypeScript error
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export function readonly<T extends Atom<any> | Record<string, Atom<any>>>(
|
|
306
|
+
atom: T
|
|
307
|
+
): T extends Atom<infer V>
|
|
308
|
+
? Atom<V>
|
|
309
|
+
: { [K in keyof T]: T[K] extends Atom<infer V> ? Atom<V> : never } {
|
|
310
|
+
return atom as any;
|
|
311
|
+
}
|
package/src/core/batch.test.ts
CHANGED
|
@@ -16,7 +16,7 @@ describe("batch", () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
// All updates batched - listener called once at the end
|
|
19
|
-
expect(count.
|
|
19
|
+
expect(count.get()).toBe(3);
|
|
20
20
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -54,7 +54,7 @@ describe("batch", () => {
|
|
|
54
54
|
count.set(4);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
expect(count.
|
|
57
|
+
expect(count.get()).toBe(4);
|
|
58
58
|
// All updates batched together
|
|
59
59
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
60
60
|
});
|
|
@@ -87,8 +87,8 @@ describe("batch", () => {
|
|
|
87
87
|
b.set(2);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
expect(a.
|
|
91
|
-
expect(b.
|
|
90
|
+
expect(a.get()).toBe(2);
|
|
91
|
+
expect(b.get()).toBe(2);
|
|
92
92
|
expect(listenerA).toHaveBeenCalledTimes(1);
|
|
93
93
|
expect(listenerB).toHaveBeenCalledTimes(1);
|
|
94
94
|
});
|
|
@@ -125,7 +125,7 @@ describe("batch", () => {
|
|
|
125
125
|
});
|
|
126
126
|
}).not.toThrow();
|
|
127
127
|
|
|
128
|
-
expect(count.
|
|
128
|
+
expect(count.get()).toBe(3);
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
|
|
@@ -138,8 +138,8 @@ describe("batch", () => {
|
|
|
138
138
|
|
|
139
139
|
// When a changes, update b
|
|
140
140
|
a.on(() => {
|
|
141
|
-
if (a.
|
|
142
|
-
b.set(a.
|
|
141
|
+
if (a.get() !== undefined && a.get() > 0) {
|
|
142
|
+
b.set(a.get() * 2);
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
@@ -150,8 +150,8 @@ describe("batch", () => {
|
|
|
150
150
|
a.set(5);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
expect(a.
|
|
154
|
-
expect(b.
|
|
153
|
+
expect(a.get()).toBe(5);
|
|
154
|
+
expect(b.get()).toBe(10);
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
@@ -226,7 +226,7 @@ describe("batch", () => {
|
|
|
226
226
|
|
|
227
227
|
// Listener called once at the end with final value
|
|
228
228
|
expect(listener).toHaveBeenCalledTimes(1);
|
|
229
|
-
expect(count.
|
|
229
|
+
expect(count.get()).toBe(3);
|
|
230
230
|
});
|
|
231
231
|
|
|
232
232
|
it("should handle mixed scenario with shared and unique listeners", () => {
|
package/src/core/batch.ts
CHANGED
|
@@ -59,7 +59,7 @@ let batchDepth = 0;
|
|
|
59
59
|
* ```ts
|
|
60
60
|
* const counter = atom(0);
|
|
61
61
|
*
|
|
62
|
-
* counter.on(() => console.log("Counter:", counter.
|
|
62
|
+
* counter.on(() => console.log("Counter:", counter.get()));
|
|
63
63
|
*
|
|
64
64
|
* batch(() => {
|
|
65
65
|
* counter.set(1);
|
|
@@ -75,7 +75,7 @@ let batchDepth = 0;
|
|
|
75
75
|
* const b = atom(0);
|
|
76
76
|
*
|
|
77
77
|
* // Same listener subscribed to both atoms
|
|
78
|
-
* const listener = () => console.log("Changed!", a.
|
|
78
|
+
* const listener = () => console.log("Changed!", a.get(), b.get());
|
|
79
79
|
* a.on(listener);
|
|
80
80
|
* b.on(listener);
|
|
81
81
|
*
|
|
@@ -103,7 +103,7 @@ let batchDepth = 0;
|
|
|
103
103
|
* ```ts
|
|
104
104
|
* const result = batch(() => {
|
|
105
105
|
* counter.set(10);
|
|
106
|
-
* return counter.
|
|
106
|
+
* return counter.get() * 2;
|
|
107
107
|
* });
|
|
108
108
|
* console.log(result); // 20
|
|
109
109
|
* ```
|