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.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
@@ -0,0 +1,261 @@
1
+ import { useSyncExternalStore, useCallback, useRef } from "react";
2
+ import { select, ContextSelectorFn } from "../core/select";
3
+ import { resolveEquality } from "../core/equality";
4
+ import { Atom, Equality } from "../core/types";
5
+ import { isAtom } from "../core/isAtom";
6
+
7
+ /**
8
+ * React hook that selects/derives a value from atom(s) with automatic subscriptions.
9
+ *
10
+ * Uses `useSyncExternalStore` for proper React 18+ concurrent mode support.
11
+ * Only subscribes to atoms that are actually accessed during selection.
12
+ *
13
+ * ## IMPORTANT: Selector Must Return Synchronous Value
14
+ *
15
+ * **The selector function MUST NOT be async or return a Promise.**
16
+ *
17
+ * ```tsx
18
+ * // ❌ WRONG - Don't use async function
19
+ * useValue(async ({ get }) => {
20
+ * const data = await fetch('/api');
21
+ * return data;
22
+ * });
23
+ *
24
+ * // ❌ WRONG - Don't return a Promise
25
+ * useValue(({ get }) => fetch('/api').then(r => r.json()));
26
+ *
27
+ * // ✅ CORRECT - Create async atom and read with get()
28
+ * const data$ = atom(fetch('/api').then(r => r.json()));
29
+ * useValue(({ get }) => get(data$)); // Suspends until resolved
30
+ * ```
31
+ *
32
+ * ## IMPORTANT: Suspense-Style API
33
+ *
34
+ * This hook uses a **Suspense-style API** for async atoms:
35
+ * - When an atom is **loading**, the getter throws a Promise (suspends)
36
+ * - When an atom has an **error**, the getter throws the error
37
+ * - When an atom is **resolved**, the getter returns the value
38
+ *
39
+ * This means:
40
+ * - **You MUST wrap components with `<Suspense>`** to handle loading states
41
+ * - **You MUST wrap components with `<ErrorBoundary>`** to handle errors
42
+ *
43
+ * ## Alternative: Using staleValue for Non-Suspense
44
+ *
45
+ * If you want to show loading states without Suspense:
46
+ *
47
+ * ```tsx
48
+ * function MyComponent() {
49
+ * // Access staleValue directly - always has a value (with fallback)
50
+ * const count = myDerivedAtom$.staleValue;
51
+ * const isLoading = isPending(myDerivedAtom$.value);
52
+ *
53
+ * return (
54
+ * <div>
55
+ * {isLoading && <Spinner />}
56
+ * Count: {count}
57
+ * </div>
58
+ * );
59
+ * }
60
+ * ```
61
+ *
62
+ * @template T - The type of the selected value
63
+ * @param selectorOrAtom - Atom or context-based selector function (must return sync value)
64
+ * @param equals - Equality function or shorthand. Defaults to "shallow"
65
+ * @returns The selected value (Awaited<T>)
66
+ * @throws Error if selector returns a Promise or PromiseLike
67
+ *
68
+ * @example Single atom (shorthand)
69
+ * ```tsx
70
+ * const count = atom(5);
71
+ *
72
+ * function Counter() {
73
+ * const value = useValue(count);
74
+ * return <div>{value}</div>;
75
+ * }
76
+ * ```
77
+ *
78
+ * @example With selector
79
+ * ```tsx
80
+ * const count = atom(5);
81
+ *
82
+ * function Counter() {
83
+ * const doubled = useValue(({ get }) => get(count) * 2);
84
+ * return <div>{doubled}</div>;
85
+ * }
86
+ * ```
87
+ *
88
+ * @example Multiple atoms
89
+ * ```tsx
90
+ * const firstName = atom("John");
91
+ * const lastName = atom("Doe");
92
+ *
93
+ * function FullName() {
94
+ * const fullName = useValue(({ get }) =>
95
+ * `${get(firstName)} ${get(lastName)}`
96
+ * );
97
+ * return <div>{fullName}</div>;
98
+ * }
99
+ * ```
100
+ *
101
+ * @example Async atom with Suspense
102
+ * ```tsx
103
+ * const userAtom = atom(fetchUser());
104
+ *
105
+ * function UserProfile() {
106
+ * const user = useValue(({ get }) => get(userAtom));
107
+ * return <div>{user.name}</div>;
108
+ * }
109
+ *
110
+ * // MUST wrap with Suspense and ErrorBoundary
111
+ * function App() {
112
+ * return (
113
+ * <ErrorBoundary fallback={<div>Error!</div>}>
114
+ * <Suspense fallback={<div>Loading...</div>}>
115
+ * <UserProfile />
116
+ * </Suspense>
117
+ * </ErrorBoundary>
118
+ * );
119
+ * }
120
+ * ```
121
+ *
122
+ * @example Using all() for multiple async atoms
123
+ * ```tsx
124
+ * const userAtom = atom(fetchUser());
125
+ * const postsAtom = atom(fetchPosts());
126
+ *
127
+ * function Dashboard() {
128
+ * const data = useValue(({ all }) => {
129
+ * const [user, posts] = all(userAtom, postsAtom);
130
+ * return { user, posts };
131
+ * });
132
+ *
133
+ * return <DashboardContent user={data.user} posts={data.posts} />;
134
+ * }
135
+ * ```
136
+ */
137
+ // Overload: Pass atom directly
138
+ export function useValue<T>(
139
+ atom: Atom<T>,
140
+ equals?: Equality<Awaited<T>>
141
+ ): Awaited<T>;
142
+
143
+ // Overload: Context-based selector function
144
+ export function useValue<T>(
145
+ selector: ContextSelectorFn<T>,
146
+ equals?: Equality<T>
147
+ ): T;
148
+
149
+ export function useValue<T>(
150
+ selectorOrAtom: ContextSelectorFn<T> | Atom<T>,
151
+ equals?: Equality<T>
152
+ ): T {
153
+ // 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>);
157
+
158
+ // Default to shallow equality
159
+ const eq = resolveEquality((equals as Equality<unknown>) ?? "shallow");
160
+
161
+ // Store selector in ref to avoid recreating callbacks
162
+ const selectorRef = useRef(selector);
163
+ const eqRef = useRef(eq);
164
+
165
+ // Update refs on each render
166
+ selectorRef.current = selector;
167
+ eqRef.current = eq;
168
+
169
+ // Track current dependencies and their unsubscribe functions
170
+ const subscriptionsRef = useRef<Map<Atom<unknown>, VoidFunction>>(new Map());
171
+ const dependenciesRef = useRef<Set<Atom<unknown>>>(new Set());
172
+
173
+ // Cache the last snapshot
174
+ const snapshotRef = useRef<{ value: T; initialized: boolean }>({
175
+ value: undefined as T,
176
+ initialized: false,
177
+ });
178
+
179
+ /**
180
+ * Get the current snapshot by running the selector.
181
+ */
182
+ const getSnapshot = useCallback(() => {
183
+ const result = select(selectorRef.current);
184
+
185
+ // Update dependencies
186
+ dependenciesRef.current = result.dependencies;
187
+
188
+ // Handle Suspense-style states
189
+ if (result.promise !== undefined) {
190
+ // Loading state - throw Promise
191
+ throw result.promise;
192
+ }
193
+
194
+ if (result.error !== undefined) {
195
+ // Error state - throw error
196
+ throw result.error;
197
+ }
198
+
199
+ // Success - check equality and update cache
200
+ const newValue = result.value as T;
201
+
202
+ if (
203
+ !snapshotRef.current.initialized ||
204
+ !eqRef.current(newValue, snapshotRef.current.value)
205
+ ) {
206
+ snapshotRef.current = { value: newValue, initialized: true };
207
+ }
208
+
209
+ return snapshotRef.current.value;
210
+ }, []);
211
+
212
+ /**
213
+ * Subscribe to atom changes.
214
+ */
215
+ const subscribe = useCallback((onStoreChange: () => void) => {
216
+ const subscriptions = subscriptionsRef.current;
217
+
218
+ const updateSubscriptions = () => {
219
+ const currentDeps = dependenciesRef.current;
220
+
221
+ // Unsubscribe from atoms no longer dependencies
222
+ for (const [atom, unsubscribe] of subscriptions) {
223
+ if (!currentDeps.has(atom)) {
224
+ unsubscribe();
225
+ subscriptions.delete(atom);
226
+ }
227
+ }
228
+
229
+ // Subscribe to new dependencies
230
+ for (const atom of currentDeps) {
231
+ if (!subscriptions.has(atom)) {
232
+ const unsubscribe = atom.on(() => {
233
+ // Re-run selector to update dependencies
234
+ const result = select(selectorRef.current);
235
+ dependenciesRef.current = result.dependencies;
236
+
237
+ // Update subscriptions if dependencies changed
238
+ updateSubscriptions();
239
+
240
+ // Notify React
241
+ onStoreChange();
242
+ });
243
+ subscriptions.set(atom, unsubscribe);
244
+ }
245
+ }
246
+ };
247
+
248
+ // Initial subscription setup
249
+ updateSubscriptions();
250
+
251
+ // Cleanup function
252
+ return () => {
253
+ for (const unsubscribe of subscriptions.values()) {
254
+ unsubscribe();
255
+ }
256
+ subscriptions.clear();
257
+ };
258
+ }, []);
259
+
260
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
261
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "moduleResolution": "bundler",
5
+ "outDir": "dist",
6
+ "declaration": true
7
+ },
8
+ "include": ["src"]
9
+ }