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.
Files changed (65) hide show
  1. package/README.md +868 -161
  2. package/coverage/src/core/onCreateHook.ts.html +72 -70
  3. package/dist/core/atom.d.ts +83 -6
  4. package/dist/core/batch.d.ts +3 -3
  5. package/dist/core/derived.d.ts +69 -22
  6. package/dist/core/effect.d.ts +52 -52
  7. package/dist/core/getAtomState.d.ts +29 -0
  8. package/dist/core/hook.d.ts +1 -1
  9. package/dist/core/onCreateHook.d.ts +37 -23
  10. package/dist/core/onErrorHook.d.ts +49 -0
  11. package/dist/core/promiseCache.d.ts +23 -32
  12. package/dist/core/select.d.ts +208 -29
  13. package/dist/core/types.d.ts +107 -22
  14. package/dist/core/withReady.d.ts +115 -0
  15. package/dist/core/withReady.test.d.ts +1 -0
  16. package/dist/index-CBVj1kSj.js +1350 -0
  17. package/dist/index-Cxk9v0um.cjs +1 -0
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.d.ts +12 -8
  20. package/dist/index.js +18 -15
  21. package/dist/react/index.cjs +10 -10
  22. package/dist/react/index.d.ts +2 -1
  23. package/dist/react/index.js +422 -377
  24. package/dist/react/rx.d.ts +114 -25
  25. package/dist/react/useAction.d.ts +5 -4
  26. package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
  27. package/dist/react/useSelector.test.d.ts +1 -0
  28. package/package.json +1 -1
  29. package/src/core/atom.test.ts +307 -43
  30. package/src/core/atom.ts +144 -22
  31. package/src/core/batch.test.ts +10 -10
  32. package/src/core/batch.ts +3 -3
  33. package/src/core/define.test.ts +12 -11
  34. package/src/core/define.ts +1 -1
  35. package/src/core/derived.test.ts +906 -72
  36. package/src/core/derived.ts +192 -81
  37. package/src/core/effect.test.ts +651 -45
  38. package/src/core/effect.ts +102 -98
  39. package/src/core/getAtomState.ts +69 -0
  40. package/src/core/hook.test.ts +5 -5
  41. package/src/core/hook.ts +1 -1
  42. package/src/core/onCreateHook.ts +38 -23
  43. package/src/core/onErrorHook.test.ts +350 -0
  44. package/src/core/onErrorHook.ts +52 -0
  45. package/src/core/promiseCache.test.ts +5 -3
  46. package/src/core/promiseCache.ts +76 -71
  47. package/src/core/select.ts +405 -130
  48. package/src/core/selector.test.ts +574 -32
  49. package/src/core/types.ts +107 -29
  50. package/src/core/withReady.test.ts +534 -0
  51. package/src/core/withReady.ts +191 -0
  52. package/src/core/withUse.ts +1 -1
  53. package/src/index.test.ts +4 -4
  54. package/src/index.ts +21 -7
  55. package/src/react/index.ts +2 -1
  56. package/src/react/rx.test.tsx +173 -18
  57. package/src/react/rx.tsx +274 -43
  58. package/src/react/useAction.test.ts +12 -14
  59. package/src/react/useAction.ts +11 -9
  60. package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
  61. package/src/react/{useValue.ts → useSelector.ts} +64 -33
  62. package/v2.md +44 -44
  63. package/dist/index-2ok7ilik.js +0 -1217
  64. package/dist/index-B_5SFzfl.cjs +0 -1
  65. /package/dist/{react/useValue.test.d.ts → core/onErrorHook.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 { AtomOptions, MutableAtom, SYMBOL_ATOM, Equality } from "./types";
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, `.value` returns the Promise object itself.
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 value, set/reset methods
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.value); // 2
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.value; // Promise<Post[]>
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
- // Resolve initial value (supports lazy initialization)
79
- const initialValue: T =
80
- typeof valueOrInit === "function"
81
- ? (valueOrInit as () => T)()
82
- : valueOrInit;
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
- // Re-run initializer if function, otherwise use initial value
132
- const nextValue: T =
133
- typeof valueOrInit === "function"
134
- ? (valueOrInit as () => T)()
135
- : valueOrInit;
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,29 +220,92 @@ export function atom<T>(
161
220
  meta: options.meta,
162
221
 
163
222
  /**
164
- * Current value (raw, including Promises).
223
+ * Get the current value (raw, including Promises).
165
224
  */
166
- get value(): T {
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?.({
182
240
  type: "mutable",
183
241
  key: options.meta?.key,
184
242
  meta: options.meta,
185
- atom: a as MutableAtom<unknown>,
243
+ instance: a as MutableAtom<unknown>,
186
244
  });
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
+ }
@@ -16,7 +16,7 @@ describe("batch", () => {
16
16
  });
17
17
 
18
18
  // All updates batched - listener called once at the end
19
- expect(count.value).toBe(3);
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.value).toBe(4);
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.value).toBe(2);
91
- expect(b.value).toBe(2);
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.value).toBe(3);
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.value !== undefined && a.value > 0) {
142
- b.set(a.value * 2);
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.value).toBe(5);
154
- expect(b.value).toBe(10);
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.value).toBe(3);
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.value));
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.value, b.value);
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.value * 2;
106
+ * return counter.get() * 2;
107
107
  * });
108
108
  * console.log(result); // 20
109
109
  * ```
@@ -3,14 +3,12 @@ import { define } from "./define";
3
3
  import { onCreateHook } from "./onCreateHook";
4
4
 
5
5
  describe("define", () => {
6
- const originalOnCreateHook = onCreateHook.current;
7
-
8
6
  beforeEach(() => {
9
- onCreateHook.current = undefined;
7
+ onCreateHook.reset();
10
8
  });
11
9
 
12
10
  afterEach(() => {
13
- onCreateHook.current = originalOnCreateHook;
11
+ onCreateHook.reset();
14
12
  });
15
13
 
16
14
  describe("basic functionality", () => {
@@ -238,7 +236,7 @@ describe("define", () => {
238
236
  describe("onCreateHook", () => {
239
237
  it("should call onCreateHook when module is created", () => {
240
238
  const hookFn = vi.fn();
241
- onCreateHook.current = hookFn;
239
+ onCreateHook.override(() => hookFn);
242
240
 
243
241
  const store = define(() => ({ value: 42 }), { key: "testModule" });
244
242
  const instance = store();
@@ -247,13 +245,14 @@ describe("define", () => {
247
245
  expect(hookFn).toHaveBeenCalledWith({
248
246
  type: "module",
249
247
  key: "testModule",
250
- module: instance,
248
+ meta: undefined,
249
+ instance,
251
250
  });
252
251
  });
253
252
 
254
253
  it("should call onCreateHook with undefined key when not provided", () => {
255
254
  const hookFn = vi.fn();
256
- onCreateHook.current = hookFn;
255
+ onCreateHook.override(() => hookFn);
257
256
 
258
257
  const store = define(() => ({ value: 42 }));
259
258
  store();
@@ -261,12 +260,13 @@ describe("define", () => {
261
260
  expect(hookFn).toHaveBeenCalledWith({
262
261
  type: "module",
263
262
  key: undefined,
264
- module: expect.any(Object),
263
+ meta: undefined,
264
+ instance: expect.any(Object),
265
265
  });
266
266
  });
267
267
 
268
268
  it("should not throw when onCreateHook is undefined", () => {
269
- onCreateHook.current = undefined;
269
+ onCreateHook.reset();
270
270
 
271
271
  const store = define(() => ({ value: 42 }));
272
272
  expect(() => store()).not.toThrow();
@@ -274,7 +274,7 @@ describe("define", () => {
274
274
 
275
275
  it("should call onCreateHook for overridden module", () => {
276
276
  const hookFn = vi.fn();
277
- onCreateHook.current = hookFn;
277
+ onCreateHook.override(() => hookFn);
278
278
 
279
279
  const store = define(() => ({ value: "original" }));
280
280
  store.override(() => ({ value: "overridden" }));
@@ -283,7 +283,8 @@ describe("define", () => {
283
283
  expect(hookFn).toHaveBeenCalledWith({
284
284
  type: "module",
285
285
  key: undefined,
286
- module: instance,
286
+ meta: undefined,
287
+ instance,
287
288
  });
288
289
  });
289
290
  });
@@ -213,7 +213,7 @@ export function define<T>(
213
213
  type: "module",
214
214
  key: options?.key,
215
215
  meta: options?.meta,
216
- module: instance,
216
+ instance,
217
217
  });
218
218
  }
219
219
  return instance;