atomirx 0.0.1 → 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.
Files changed (53) hide show
  1. package/README.md +867 -160
  2. package/dist/core/atom.d.ts +83 -6
  3. package/dist/core/batch.d.ts +3 -3
  4. package/dist/core/derived.d.ts +55 -21
  5. package/dist/core/effect.d.ts +47 -51
  6. package/dist/core/getAtomState.d.ts +29 -0
  7. package/dist/core/promiseCache.d.ts +23 -32
  8. package/dist/core/select.d.ts +208 -29
  9. package/dist/core/types.d.ts +55 -19
  10. package/dist/core/withReady.d.ts +69 -0
  11. package/dist/index-CqO6BDwj.cjs +1 -0
  12. package/dist/index-D8RDOTB_.js +1319 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.ts +9 -7
  15. package/dist/index.js +12 -10
  16. package/dist/react/index.cjs +10 -10
  17. package/dist/react/index.d.ts +2 -1
  18. package/dist/react/index.js +423 -379
  19. package/dist/react/rx.d.ts +114 -25
  20. package/dist/react/useAction.d.ts +5 -4
  21. package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
  22. package/dist/react/useSelector.test.d.ts +1 -0
  23. package/package.json +17 -1
  24. package/src/core/atom.test.ts +307 -43
  25. package/src/core/atom.ts +143 -21
  26. package/src/core/batch.test.ts +10 -10
  27. package/src/core/batch.ts +3 -3
  28. package/src/core/derived.test.ts +727 -72
  29. package/src/core/derived.ts +141 -73
  30. package/src/core/effect.test.ts +259 -39
  31. package/src/core/effect.ts +62 -85
  32. package/src/core/getAtomState.ts +69 -0
  33. package/src/core/promiseCache.test.ts +5 -3
  34. package/src/core/promiseCache.ts +76 -71
  35. package/src/core/select.ts +405 -130
  36. package/src/core/selector.test.ts +574 -32
  37. package/src/core/types.ts +54 -26
  38. package/src/core/withReady.test.ts +360 -0
  39. package/src/core/withReady.ts +127 -0
  40. package/src/core/withUse.ts +1 -1
  41. package/src/index.test.ts +4 -4
  42. package/src/index.ts +11 -6
  43. package/src/react/index.ts +2 -1
  44. package/src/react/rx.test.tsx +173 -18
  45. package/src/react/rx.tsx +274 -43
  46. package/src/react/useAction.test.ts +12 -14
  47. package/src/react/useAction.ts +11 -9
  48. package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
  49. package/src/react/{useValue.ts → useSelector.ts} +64 -33
  50. package/v2.md +44 -44
  51. package/dist/index-2ok7ilik.js +0 -1217
  52. package/dist/index-B_5SFzfl.cjs +0 -1
  53. /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 { 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,21 +220,20 @@ 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?.({
@@ -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
+ }
@@ -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
  * ```