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.
Files changed (53) hide show
  1. package/README.md +866 -159
  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 +1 -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
@@ -1,19 +1,19 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
  import { renderHook, act, waitFor } from "@testing-library/react";
3
3
  import { atom } from "../core/atom";
4
- import { useValue } from "./useValue";
4
+ import { useSelector } from "./useSelector";
5
5
 
6
- describe("useValue", () => {
6
+ describe("useSelector", () => {
7
7
  describe("basic functionality", () => {
8
8
  it("should read value from sync atom", () => {
9
9
  const count$ = atom(5);
10
- const { result } = renderHook(() => useValue(count$));
10
+ const { result } = renderHook(() => useSelector(count$));
11
11
  expect(result.current).toBe(5);
12
12
  });
13
13
 
14
14
  it("should update when atom value changes", async () => {
15
15
  const count$ = atom(0);
16
- const { result } = renderHook(() => useValue(count$));
16
+ const { result } = renderHook(() => useSelector(count$));
17
17
 
18
18
  expect(result.current).toBe(0);
19
19
 
@@ -28,7 +28,7 @@ describe("useValue", () => {
28
28
 
29
29
  it("should work with object values", () => {
30
30
  const user$ = atom({ name: "John", age: 30 });
31
- const { result } = renderHook(() => useValue(user$));
31
+ const { result } = renderHook(() => useSelector(user$));
32
32
 
33
33
  expect(result.current).toEqual({ name: "John", age: 30 });
34
34
  });
@@ -38,7 +38,7 @@ describe("useValue", () => {
38
38
  it("should support selector function", () => {
39
39
  const count$ = atom(5);
40
40
  const { result } = renderHook(() =>
41
- useValue(({ get }) => get(count$) * 2)
41
+ useSelector(({ read }) => read(count$) * 2)
42
42
  );
43
43
 
44
44
  expect(result.current).toBe(10);
@@ -48,7 +48,7 @@ describe("useValue", () => {
48
48
  const a$ = atom(2);
49
49
  const b$ = atom(3);
50
50
  const { result } = renderHook(() =>
51
- useValue(({ get }) => get(a$) + get(b$))
51
+ useSelector(({ read }) => read(a$) + read(b$))
52
52
  );
53
53
 
54
54
  expect(result.current).toBe(5);
@@ -58,7 +58,7 @@ describe("useValue", () => {
58
58
  const a$ = atom(2);
59
59
  const b$ = atom(3);
60
60
  const { result } = renderHook(() =>
61
- useValue(({ get }) => get(a$) * get(b$))
61
+ useSelector(({ read }) => read(a$) * read(b$))
62
62
  );
63
63
 
64
64
  expect(result.current).toBe(6);
@@ -80,8 +80,8 @@ describe("useValue", () => {
80
80
  const details$ = atom("Detailed");
81
81
 
82
82
  const { result } = renderHook(() =>
83
- useValue(({ get }) =>
84
- get(showDetails$) ? get(details$) : get(summary$)
83
+ useSelector(({ read }) =>
84
+ read(showDetails$) ? read(details$) : read(summary$)
85
85
  )
86
86
  );
87
87
 
@@ -104,7 +104,7 @@ describe("useValue", () => {
104
104
 
105
105
  const { result } = renderHook(() => {
106
106
  renderCount();
107
- return useValue(source$);
107
+ return useSelector(source$);
108
108
  });
109
109
 
110
110
  expect(result.current).toEqual({ a: 1 });
@@ -120,7 +120,7 @@ describe("useValue", () => {
120
120
  it("should support custom equality", async () => {
121
121
  const source$ = atom({ id: 1, name: "John" });
122
122
  const { result } = renderHook(() =>
123
- useValue(source$, (a, b) => a.id === b.id)
123
+ useSelector(source$, (a, b) => a.id === b.id)
124
124
  );
125
125
 
126
126
  expect(result.current.name).toBe("John");
@@ -140,8 +140,8 @@ describe("useValue", () => {
140
140
  const c$ = atom(3);
141
141
 
142
142
  const { result } = renderHook(() =>
143
- useValue(({ all }) => {
144
- const [a, b, c] = all(a$, b$, c$);
143
+ useSelector(({ all }) => {
144
+ const [a, b, c] = all([a$, b$, c$]);
145
145
  return a + b + c;
146
146
  })
147
147
  );
@@ -153,7 +153,7 @@ describe("useValue", () => {
153
153
  describe("cleanup", () => {
154
154
  it("should unsubscribe on unmount", async () => {
155
155
  const count$ = atom(0);
156
- const { unmount } = renderHook(() => useValue(count$));
156
+ const { unmount } = renderHook(() => useSelector(count$));
157
157
 
158
158
  unmount();
159
159
 
@@ -171,7 +171,7 @@ describe("useValue", () => {
171
171
 
172
172
  describe("async atoms", () => {
173
173
  it("should throw promise for pending atom (Suspense)", () => {
174
- // When an atom's value is a pending Promise, useValue should throw
174
+ // When an atom's value is a pending Promise, useSelector should throw
175
175
  // the Promise to trigger Suspense. This is hard to test without
176
176
  // proper Suspense boundary setup.
177
177
  // The hook will throw the Promise which is caught by Suspense
@@ -1,7 +1,7 @@
1
- import { useSyncExternalStore, useCallback, useRef } from "react";
2
- import { select, ContextSelectorFn } from "../core/select";
3
- import { resolveEquality } from "../core/equality";
1
+ import { useCallback, useRef, useSyncExternalStore } from "react";
2
+ import { ReactiveSelector, select } from "../core/select";
4
3
  import { Atom, Equality } from "../core/types";
4
+ import { resolveEquality } from "../core/equality";
5
5
  import { isAtom } from "../core/isAtom";
6
6
 
7
7
  /**
@@ -16,19 +16,52 @@ import { isAtom } from "../core/isAtom";
16
16
  *
17
17
  * ```tsx
18
18
  * // ❌ WRONG - Don't use async function
19
- * useValue(async ({ get }) => {
19
+ * useSelector(async ({ read }) => {
20
20
  * const data = await fetch('/api');
21
21
  * return data;
22
22
  * });
23
23
  *
24
24
  * // ❌ WRONG - Don't return a Promise
25
- * useValue(({ get }) => fetch('/api').then(r => r.json()));
25
+ * useSelector(({ read }) => fetch('/api').then(r => r.json()));
26
26
  *
27
- * // ✅ CORRECT - Create async atom and read with get()
27
+ * // ✅ CORRECT - Create async atom and read with read()
28
28
  * const data$ = atom(fetch('/api').then(r => r.json()));
29
- * useValue(({ get }) => get(data$)); // Suspends until resolved
29
+ * useSelector(({ read }) => read(data$)); // Suspends until resolved
30
+ * ```
31
+ *
32
+ * ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
33
+ *
34
+ * **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
35
+ * Promises when atoms are loading (Suspense pattern). A try/catch will catch
36
+ * these Promises and break the Suspense mechanism.
37
+ *
38
+ * ```tsx
39
+ * // ❌ WRONG - Catches Suspense Promise, breaks loading state
40
+ * const data = useSelector(({ read }) => {
41
+ * try {
42
+ * return read(asyncAtom$);
43
+ * } catch (e) {
44
+ * return null; // This catches BOTH errors AND loading promises!
45
+ * }
46
+ * });
47
+ *
48
+ * // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
49
+ * const result = useSelector(({ read, safe }) => {
50
+ * const [err, data] = safe(() => {
51
+ * const raw = read(asyncAtom$); // Can throw Promise (Suspense)
52
+ * return JSON.parse(raw); // Can throw Error
53
+ * });
54
+ *
55
+ * if (err) return { error: err.message };
56
+ * return { data };
57
+ * });
30
58
  * ```
31
59
  *
60
+ * The `safe()` utility:
61
+ * - **Catches errors** and returns `[error, undefined]`
62
+ * - **Re-throws Promises** to preserve Suspense behavior
63
+ * - Returns `[undefined, result]` on success
64
+ *
32
65
  * ## IMPORTANT: Suspense-Style API
33
66
  *
34
67
  * This hook uses a **Suspense-style API** for async atoms:
@@ -40,22 +73,19 @@ import { isAtom } from "../core/isAtom";
40
73
  * - **You MUST wrap components with `<Suspense>`** to handle loading states
41
74
  * - **You MUST wrap components with `<ErrorBoundary>`** to handle errors
42
75
  *
43
- * ## Alternative: Using staleValue for Non-Suspense
76
+ * ## Alternative: useAsyncState for Non-Suspense
44
77
  *
45
- * If you want to show loading states without Suspense:
78
+ * If you want to handle loading/error states imperatively without Suspense:
46
79
  *
47
80
  * ```tsx
81
+ * import { useAsyncState } from 'atomirx/react';
82
+ *
48
83
  * function MyComponent() {
49
- * // Access staleValue directly - always has a value (with fallback)
50
- * const count = myDerivedAtom$.staleValue;
51
- * const isLoading = isPending(myDerivedAtom$.value);
84
+ * const state = useAsyncState(myAtom$);
52
85
  *
53
- * return (
54
- * <div>
55
- * {isLoading && <Spinner />}
56
- * Count: {count}
57
- * </div>
58
- * );
86
+ * if (state.status === "loading") return <Spinner />;
87
+ * if (state.status === "error") return <Error error={state.error} />;
88
+ * return <div>{state.value}</div>;
59
89
  * }
60
90
  * ```
61
91
  *
@@ -63,14 +93,15 @@ import { isAtom } from "../core/isAtom";
63
93
  * @param selectorOrAtom - Atom or context-based selector function (must return sync value)
64
94
  * @param equals - Equality function or shorthand. Defaults to "shallow"
65
95
  * @returns The selected value (Awaited<T>)
66
- * @throws Error if selector returns a Promise or PromiseLike
96
+ * @throws Promise when loading (caught by Suspense)
97
+ * @throws Error when failed (caught by ErrorBoundary)
67
98
  *
68
99
  * @example Single atom (shorthand)
69
100
  * ```tsx
70
101
  * const count = atom(5);
71
102
  *
72
103
  * function Counter() {
73
- * const value = useValue(count);
104
+ * const value = useSelector(count);
74
105
  * return <div>{value}</div>;
75
106
  * }
76
107
  * ```
@@ -80,7 +111,7 @@ import { isAtom } from "../core/isAtom";
80
111
  * const count = atom(5);
81
112
  *
82
113
  * function Counter() {
83
- * const doubled = useValue(({ get }) => get(count) * 2);
114
+ * const doubled = useSelector(({ read }) => read(count) * 2);
84
115
  * return <div>{doubled}</div>;
85
116
  * }
86
117
  * ```
@@ -91,8 +122,8 @@ import { isAtom } from "../core/isAtom";
91
122
  * const lastName = atom("Doe");
92
123
  *
93
124
  * function FullName() {
94
- * const fullName = useValue(({ get }) =>
95
- * `${get(firstName)} ${get(lastName)}`
125
+ * const fullName = useSelector(({ read }) =>
126
+ * `${read(firstName)} ${read(lastName)}`
96
127
  * );
97
128
  * return <div>{fullName}</div>;
98
129
  * }
@@ -103,7 +134,7 @@ import { isAtom } from "../core/isAtom";
103
134
  * const userAtom = atom(fetchUser());
104
135
  *
105
136
  * function UserProfile() {
106
- * const user = useValue(({ get }) => get(userAtom));
137
+ * const user = useSelector(({ read }) => read(userAtom));
107
138
  * return <div>{user.name}</div>;
108
139
  * }
109
140
  *
@@ -125,7 +156,7 @@ import { isAtom } from "../core/isAtom";
125
156
  * const postsAtom = atom(fetchPosts());
126
157
  *
127
158
  * function Dashboard() {
128
- * const data = useValue(({ all }) => {
159
+ * const data = useSelector(({ all }) => {
129
160
  * const [user, posts] = all(userAtom, postsAtom);
130
161
  * return { user, posts };
131
162
  * });
@@ -135,25 +166,25 @@ import { isAtom } from "../core/isAtom";
135
166
  * ```
136
167
  */
137
168
  // Overload: Pass atom directly
138
- export function useValue<T>(
169
+ export function useSelector<T>(
139
170
  atom: Atom<T>,
140
171
  equals?: Equality<Awaited<T>>
141
172
  ): Awaited<T>;
142
173
 
143
174
  // Overload: Context-based selector function
144
- export function useValue<T>(
145
- selector: ContextSelectorFn<T>,
175
+ export function useSelector<T>(
176
+ selector: ReactiveSelector<T>,
146
177
  equals?: Equality<T>
147
178
  ): T;
148
179
 
149
- export function useValue<T>(
150
- selectorOrAtom: ContextSelectorFn<T> | Atom<T>,
180
+ export function useSelector<T>(
181
+ selectorOrAtom: ReactiveSelector<T> | Atom<T>,
151
182
  equals?: Equality<T>
152
183
  ): T {
153
184
  // Convert atom shorthand to context selector
154
- const selector: ContextSelectorFn<T> = isAtom(selectorOrAtom)
155
- ? ({ get }) => get(selectorOrAtom as Atom<T>) as T
156
- : (selectorOrAtom as ContextSelectorFn<T>);
185
+ const selector: ReactiveSelector<T> = isAtom(selectorOrAtom)
186
+ ? ({ read }) => read(selectorOrAtom as Atom<T>) as T
187
+ : (selectorOrAtom as ReactiveSelector<T>);
157
188
 
158
189
  // Default to shallow equality
159
190
  const eq = resolveEquality((equals as Equality<unknown>) ?? "shallow");
package/v2.md CHANGED
@@ -108,13 +108,13 @@ Computed value. Always returns `Promise<T>` for `.value`.
108
108
  // Without fallback - staleValue is T | undefined
109
109
  function derived<T>(
110
110
  compute: (ctx: SelectContext) => T,
111
- options?: DerivedOptions,
111
+ options?: DerivedOptions
112
112
  ): DerivedAtom<T, false>;
113
113
 
114
114
  // With fallback - staleValue is guaranteed T
115
115
  function derived<T>(
116
116
  compute: (ctx: SelectContext) => T,
117
- options: DerivedOptions & { fallback: T },
117
+ options: DerivedOptions & { fallback: T }
118
118
  ): DerivedAtom<T, true>;
119
119
 
120
120
  interface DerivedOptions<T> {
@@ -387,12 +387,12 @@ function any<A extends AnyAtom<unknown>[]>(...atoms: A): AtomValue<A[number]> {
387
387
  }
388
388
  ```
389
389
 
390
- | Scenario | Result |
391
- | -------------- | ----------------------------------- |
392
- | Any sync value | return first sync value |
393
- | Any ready | return first value |
394
- | Any loading | throw Promise (wait for potential) |
395
- | All errored | throw `AggregateError` |
390
+ | Scenario | Result |
391
+ | -------------- | ---------------------------------- |
392
+ | Any sync value | return first sync value |
393
+ | Any ready | return first value |
394
+ | Any loading | throw Promise (wait for potential) |
395
+ | All errored | throw `AggregateError` |
396
396
 
397
397
  > **Note:** `any()` does NOT use fallback - it waits for a real ready value.
398
398
 
@@ -437,22 +437,22 @@ function settled<A extends AnyAtom<unknown>[]>(
437
437
  }
438
438
  ```
439
439
 
440
- | Scenario | Result |
441
- | ----------- | ---------------------------------------- |
442
- | All settled | return `[{ status, value/error }, ...]` |
443
- | Any loading | throw Promise |
440
+ | Scenario | Result |
441
+ | ----------- | --------------------------------------- |
442
+ | All settled | return `[{ status, value/error }, ...]` |
443
+ | Any loading | throw Promise |
444
444
 
445
445
  ### SelectContext Summary
446
446
 
447
447
  All methods use `getAtomState()` internally.
448
448
 
449
- | Method | Returns | Throws on Error? |
450
- | ------------------- | --------------- | ------------------ |
451
- | `get(atom)` | Single value | Yes |
452
- | `all(...atoms)` | Array of values | Yes (first error) |
453
- | `race(...atoms)` | First settled | Yes (if first) |
454
- | `any(...atoms)` | First ready | Only if all error |
455
- | `settled(...atoms)` | Array of status | No |
449
+ | Method | Returns | Throws on Error? |
450
+ | ------------------- | --------------- | ----------------- |
451
+ | `get(atom)` | Single value | Yes |
452
+ | `all(...atoms)` | Array of values | Yes (first error) |
453
+ | `race(...atoms)` | First settled | Yes (if first) |
454
+ | `any(...atoms)` | First ready | Only if all error |
455
+ | `settled(...atoms)` | Array of status | No |
456
456
 
457
457
  > **Note:** `race()` and `any()` are typically used with `MutableAtom<Promise<T>>` for racing data sources.
458
458
 
@@ -543,7 +543,7 @@ interface EffectContext extends SelectContext {
543
543
 
544
544
  function effect(
545
545
  fn: (ctx: EffectContext) => void,
546
- options?: EffectOptions,
546
+ options?: EffectOptions
547
547
  ): () => void;
548
548
 
549
549
  interface EffectOptions {
@@ -585,7 +585,7 @@ const dispose = effect(
585
585
  },
586
586
  {
587
587
  onError: (e) => console.error("Unhandled error:", e),
588
- },
588
+ }
589
589
  );
590
590
  ```
591
591
 
@@ -593,13 +593,13 @@ const dispose = effect(
593
593
 
594
594
  ## React Hooks
595
595
 
596
- ### useValue
596
+ ### useSelector
597
597
 
598
598
  ```typescript
599
- function useValue<T>(atom: Atom<T>): Awaited<T>;
600
- function useValue<T>(
599
+ function useSelector<T>(atom: Atom<T>): Awaited<T>;
600
+ function useSelector<T>(
601
601
  selector: (ctx: SelectContext) => T,
602
- equals?: Equality<Awaited<T>>,
602
+ equals?: Equality<Awaited<T>>
603
603
  ): Awaited<T>;
604
604
  ```
605
605
 
@@ -608,7 +608,7 @@ function useValue<T>(
608
608
  ```typescript
609
609
  // With DerivedAtom (Suspense handles loading)
610
610
  function PostCount() {
611
- const count = useValue(postCount$); // number (awaited)
611
+ const count = useSelector(postCount$); // number (awaited)
612
612
  return <div>Count: {count}</div>;
613
613
  }
614
614
 
@@ -688,7 +688,7 @@ derived<T>(compute, { fallback }): DerivedAtom<T, true>
688
688
  effect(fn, options?): () => void
689
689
 
690
690
  // React
691
- useValue(source): T | Awaited<T>
691
+ useSelector(source): T | Awaited<T>
692
692
 
693
693
  // Types
694
694
  Atom<T>
@@ -701,25 +701,25 @@ SelectContext
701
701
 
702
702
  ## Summary Table
703
703
 
704
- | | MutableAtom | DerivedAtom | DerivedAtom with fallback |
705
- | ---------------- | ----------- | ------------------- | ------------------------- |
706
- | Purpose | Raw storage | Computed (async) | Computed + sync access |
707
- | `.value` | `T` (raw) | `Promise<T>` | `Promise<T>` |
708
- | `.staleValue` | ❌ | ✅ `T \| undefined` | ✅ `T` |
709
- | `.state()` | ❌ | ✅ `AtomState<T>` | ✅ `AtomState<T>` |
710
- | `.set()` | ✅ | ❌ | ❌ |
711
- | `.reset()` | ✅ | ❌ | ❌ |
712
- | `.refresh()` | ❌ | ✅ | ✅ |
713
- | `.on()` | ✅ | ✅ | ✅ |
704
+ | | MutableAtom | DerivedAtom | DerivedAtom with fallback |
705
+ | ------------- | ----------- | ------------------- | ------------------------- |
706
+ | Purpose | Raw storage | Computed (async) | Computed + sync access |
707
+ | `.value` | `T` (raw) | `Promise<T>` | `Promise<T>` |
708
+ | `.staleValue` | ❌ | ✅ `T \| undefined` | ✅ `T` |
709
+ | `.state()` | ❌ | ✅ `AtomState<T>` | ✅ `AtomState<T>` |
710
+ | `.set()` | ✅ | ❌ | ❌ |
711
+ | `.reset()` | ✅ | ❌ | ❌ |
712
+ | `.refresh()` | ❌ | ✅ | ✅ |
713
+ | `.on()` | ✅ | ✅ | ✅ |
714
714
 
715
715
  ---
716
716
 
717
717
  ## Migration from v1
718
718
 
719
- | v1 | v2 |
720
- | ------------------------------ | --------------------------------------------- |
721
- | `atom(promise, { fallback })` | `atom(promise)` (no fallback on atom) |
722
- | `atom.loading` | `isPending(derived$.value)` from promiseCache |
723
- | `atom.error` | `await derived.value` throws |
724
- | `atom.stale()` | `isPending(derived$.value)` from promiseCache |
725
- | `useValue(atom)` with Suspense | `useValue(derived)` with Suspense |
719
+ | v1 | v2 |
720
+ | --------------------------------- | --------------------------------------------- |
721
+ | `atom(promise, { fallback })` | `atom(promise)` (no fallback on atom) |
722
+ | `atom.loading` | `isPending(derived$.value)` from promiseCache |
723
+ | `atom.error` | `await derived.value` throws |
724
+ | `atom.stale()` | `isPending(derived$.value)` from promiseCache |
725
+ | `useSelector(atom)` with Suspense | `useSelector(derived)` with Suspense |