atomirx 0.0.7 → 0.1.0

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 (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -30
  33. package/dist/react/index.js +206 -791
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. package/vite.config.ts +0 -39
@@ -1,450 +0,0 @@
1
- import { CreateInfo, DerivedInfo, onCreateHook } from "./onCreateHook";
2
- import { emitter } from "./emitter";
3
- import { resolveEquality } from "./equality";
4
- import { onErrorHook } from "./onErrorHook";
5
- import { scheduleNotifyHook } from "./scheduleNotifyHook";
6
- import { ReactiveSelector, select, SelectContext } from "./select";
7
- import {
8
- Atom,
9
- AtomState,
10
- DerivedAtom,
11
- DerivedOptions,
12
- Equality,
13
- SYMBOL_ATOM,
14
- SYMBOL_DERIVED,
15
- } from "./types";
16
- import { withReady, WithReadySelectContext } from "./withReady";
17
-
18
- /**
19
- * Internal options for derived atoms.
20
- * These are not part of the public API.
21
- * @internal
22
- */
23
- export interface DerivedInternalOptions {
24
- /**
25
- * Override the error source for onErrorHook.
26
- * Used by effect() to attribute errors to the effect instead of the internal derived.
27
- */
28
- _errorSource?: CreateInfo;
29
- }
30
-
31
- /**
32
- * Context object passed to derived atom selector functions.
33
- * Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
34
- *
35
- * Currently identical to `SelectContext`, but defined separately to allow
36
- * future derived-specific extensions without breaking changes.
37
- */
38
- export interface DerivedContext extends SelectContext, WithReadySelectContext {}
39
-
40
- /**
41
- * Creates a derived (computed) atom from source atom(s).
42
- *
43
- * Derived atoms are **read-only** and automatically recompute when their
44
- * source atoms change. The `.get()` method always returns a `Promise<T>`,
45
- * even for synchronous computations.
46
- *
47
- * ## IMPORTANT: Selector Must Return Synchronous Value
48
- *
49
- * **The selector function MUST NOT be async or return a Promise.**
50
- *
51
- * ```ts
52
- * // ❌ WRONG - Don't use async function
53
- * derived(async ({ read }) => {
54
- * const data = await fetch('/api');
55
- * return data;
56
- * });
57
- *
58
- * // ❌ WRONG - Don't return a Promise
59
- * derived(({ read }) => fetch('/api').then(r => r.json()));
60
- *
61
- * // ✅ CORRECT - Create async atom and read with read()
62
- * const data$ = atom(fetch('/api').then(r => r.json()));
63
- * derived(({ read }) => read(data$)); // Suspends until resolved
64
- * ```
65
- *
66
- * ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
67
- *
68
- * **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
69
- * Promises when atoms are loading (Suspense pattern). A try/catch will catch
70
- * these Promises and break the Suspense mechanism.
71
- *
72
- * ```ts
73
- * // ❌ WRONG - Catches Suspense Promise, breaks loading state
74
- * derived(({ read }) => {
75
- * try {
76
- * return read(asyncAtom$);
77
- * } catch (e) {
78
- * return 'fallback'; // This catches BOTH errors AND loading promises!
79
- * }
80
- * });
81
- *
82
- * // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
83
- * derived(({ read, safe }) => {
84
- * const [err, data] = safe(() => {
85
- * const raw = read(asyncAtom$); // Can throw Promise (Suspense)
86
- * return JSON.parse(raw); // Can throw Error
87
- * });
88
- *
89
- * if (err) return { error: err.message };
90
- * return { data };
91
- * });
92
- * ```
93
- *
94
- * The `safe()` utility:
95
- * - **Catches errors** and returns `[error, undefined]`
96
- * - **Re-throws Promises** to preserve Suspense behavior
97
- * - Returns `[undefined, result]` on success
98
- *
99
- * ## Key Features
100
- *
101
- * 1. **Always async**: `.get()` returns `Promise<T>`
102
- * 2. **Lazy computation**: Value is computed on first access
103
- * 3. **Automatic updates**: Recomputes when any source atom changes
104
- * 4. **Equality checking**: Only notifies if derived value changed
105
- * 5. **Fallback support**: Optional fallback for loading/error states
106
- * 6. **Suspense-like async**: `read()` throws promise if loading
107
- * 7. **Conditional dependencies**: Only subscribes to atoms accessed
108
- *
109
- * ## Suspense-Style read()
110
- *
111
- * The `read()` function behaves like React Suspense:
112
- * - If source atom is **loading**: `read()` throws the promise
113
- * - If source atom has **error**: `read()` throws the error
114
- * - If source atom has **value**: `read()` returns the value
115
- *
116
- * @template T - Derived value type
117
- * @template F - Whether fallback is provided
118
- * @param fn - Context-based derivation function (must return sync value, not Promise)
119
- * @param options - Optional configuration (meta, equals, fallback)
120
- * @returns A read-only derived atom
121
- * @throws Error if selector returns a Promise or PromiseLike
122
- *
123
- * @example Basic derived (no fallback)
124
- * ```ts
125
- * const count$ = atom(5);
126
- * const doubled$ = derived(({ read }) => read(count$) * 2);
127
- *
128
- * await doubled$.get(); // 10
129
- * doubled$.staleValue; // undefined (until first resolve) -> 10
130
- * doubled$.state(); // { status: "ready", value: 10 }
131
- * ```
132
- *
133
- * @example With fallback
134
- * ```ts
135
- * const posts$ = atom(fetchPosts());
136
- * const count$ = derived(({ read }) => read(posts$).length, { fallback: 0 });
137
- *
138
- * count$.staleValue; // 0 (during loading) -> 42 (after resolve)
139
- * count$.state(); // { status: "loading", promise } during loading
140
- * // { status: "ready", value: 42 } after resolve
141
- * ```
142
- *
143
- * @example Async dependencies
144
- * ```ts
145
- * const user$ = atom(fetchUser());
146
- * const posts$ = atom(fetchPosts());
147
- *
148
- * const dashboard$ = derived(({ all }) => {
149
- * const [user, posts] = all(user$, posts$);
150
- * return { user, posts };
151
- * });
152
- * ```
153
- *
154
- * @example Refresh
155
- * ```ts
156
- * const data$ = derived(({ read }) => read(source$));
157
- * data$.refresh(); // Re-run computation
158
- * ```
159
- */
160
-
161
- // Overload: Without fallback - staleValue is T | undefined
162
- export function derived<T>(
163
- fn: ReactiveSelector<T, DerivedContext>,
164
- options?: DerivedOptions<T> & DerivedInternalOptions
165
- ): DerivedAtom<T, false>;
166
-
167
- // Overload: With fallback - staleValue is guaranteed T
168
- export function derived<T>(
169
- fn: ReactiveSelector<T, DerivedContext>,
170
- options: DerivedOptions<T> & { fallback: T } & DerivedInternalOptions
171
- ): DerivedAtom<T, true>;
172
-
173
- // Implementation
174
- export function derived<T>(
175
- fn: ReactiveSelector<T, DerivedContext>,
176
- options: DerivedOptions<T> & { fallback?: T } & DerivedInternalOptions = {}
177
- ): DerivedAtom<T, boolean> {
178
- const changeEmitter = emitter();
179
- const eq = resolveEquality(options.equals as Equality<unknown>);
180
-
181
- // Fallback configuration
182
- const hasFallback = "fallback" in options;
183
- const fallbackValue = options.fallback as T;
184
-
185
- // State
186
- let lastResolved: { value: T } | undefined;
187
- let lastError: unknown = undefined;
188
- let currentPromise: Promise<T> | null = null;
189
- let isInitialized = false;
190
- let isLoading = false;
191
- let version = 0;
192
-
193
- // Store resolve/reject to allow reusing the same promise across recomputations
194
- let resolvePromise: ((value: T) => void) | null = null;
195
- let rejectPromise: ((error: unknown) => void) | null = null;
196
-
197
- // Track current subscriptions (atom -> unsubscribe function)
198
- const subscriptions = new Map<Atom<unknown>, VoidFunction>();
199
-
200
- // CreateInfo for this derived - stored for onErrorHook
201
- // Will be set after derivedAtom is created
202
- let createInfo: DerivedInfo;
203
-
204
- /**
205
- * Handles errors by calling both the user's onError callback and the global onErrorHook.
206
- */
207
- const handleError = (error: unknown) => {
208
- // Invoke user's error callback if provided
209
- options.onError?.(error);
210
-
211
- // Invoke global error hook
212
- // Use _errorSource if provided (for effect), otherwise use this derived's createInfo
213
- const source = options._errorSource ?? createInfo;
214
- onErrorHook.current?.({ source, error });
215
- };
216
-
217
- /**
218
- * Schedules notification to all subscribers.
219
- */
220
- const notify = () => {
221
- changeEmitter.forEach((listener) => {
222
- scheduleNotifyHook.current(listener);
223
- });
224
- };
225
-
226
- /**
227
- * Updates subscriptions based on new dependencies.
228
- */
229
- const updateSubscriptions = (newDeps: Set<Atom<unknown>>) => {
230
- // Unsubscribe from atoms that are no longer accessed
231
- for (const [atom, unsubscribe] of subscriptions) {
232
- if (!newDeps.has(atom)) {
233
- unsubscribe();
234
- subscriptions.delete(atom);
235
- }
236
- }
237
-
238
- // Subscribe to newly accessed atoms
239
- for (const atom of newDeps) {
240
- if (!subscriptions.has(atom)) {
241
- const unsubscribe = atom.on(() => {
242
- compute();
243
- });
244
- subscriptions.set(atom, unsubscribe);
245
- }
246
- }
247
- };
248
-
249
- /**
250
- * Computes the derived value.
251
- * Reuses the existing Promise if loading (to prevent orphaned promises
252
- * that React Suspense might be waiting on).
253
- */
254
- const compute = (silent = false) => {
255
- const computeVersion = ++version;
256
- isLoading = true;
257
- lastError = undefined; // Clear error when starting new computation
258
-
259
- // Create a new promise if:
260
- // 1. We don't have one yet, OR
261
- // 2. The previous computation completed (resolved/rejected) and we need a new one
262
- // This ensures we reuse promises while loading (for Suspense) but create fresh
263
- // promises for new computations after completion
264
- if (!resolvePromise) {
265
- currentPromise = new Promise<T>((resolve, reject) => {
266
- resolvePromise = resolve;
267
- rejectPromise = reject;
268
- });
269
- // Prevent unhandled rejection warnings - errors are accessible via:
270
- // 1. onError callback (if provided)
271
- // 2. state() returning { status: "error", error }
272
- // 3. .get().catch() by consumers
273
- currentPromise.catch(() => {});
274
- }
275
-
276
- // Run select to compute value and track dependencies
277
- const attemptCompute = () => {
278
- const result = select((context) => fn(context.use(withReady())));
279
-
280
- // Update subscriptions based on accessed deps
281
- updateSubscriptions(result.dependencies);
282
-
283
- if (result.promise) {
284
- // Notify subscribers that we're now in loading state
285
- // This allows downstream derived atoms and useSelector to suspend
286
- if (!silent) notify();
287
- // Promise thrown - wait for it and retry
288
- // Note: For never-resolving promises (from ready()), this .then() will never fire.
289
- // But when a dependency changes, compute() is called again via subscription,
290
- // and the new computation will run (with a new version).
291
- result.promise.then(
292
- () => {
293
- // Check if we're still the current computation
294
- if (version !== computeVersion) return;
295
- attemptCompute();
296
- },
297
- (error) => {
298
- // Check if we're still the current computation
299
- if (version !== computeVersion) return;
300
- isLoading = false;
301
- lastError = error;
302
- rejectPromise?.(error);
303
- // Clear resolve/reject so next computation creates new promise
304
- resolvePromise = null;
305
- rejectPromise = null;
306
- // Invoke error handlers
307
- handleError(error);
308
- // Always notify when promise rejects - subscribers need to know
309
- // state changed from loading to error
310
- notify();
311
- }
312
- );
313
- } else if (result.error !== undefined) {
314
- // Error thrown
315
- isLoading = false;
316
- lastError = result.error;
317
- rejectPromise?.(result.error);
318
- // Clear resolve/reject so next computation creates new promise
319
- resolvePromise = null;
320
- rejectPromise = null;
321
- // Invoke error handlers
322
- handleError(result.error);
323
- if (!silent) notify();
324
- } else {
325
- // Success - update lastResolved and resolve
326
- const newValue = result.value as T;
327
- const wasFirstResolve = !lastResolved;
328
- isLoading = false;
329
- lastError = undefined;
330
-
331
- // Only update and notify if value changed
332
- if (!lastResolved || !eq(newValue, lastResolved.value)) {
333
- lastResolved = { value: newValue };
334
- // Always notify on first resolve (loading → ready transition)
335
- // even if silent, because subscribers need to know state changed
336
- if (wasFirstResolve || !silent) notify();
337
- }
338
-
339
- resolvePromise?.(newValue);
340
- // Clear resolve/reject so next computation creates new promise
341
- resolvePromise = null;
342
- rejectPromise = null;
343
- }
344
- };
345
-
346
- attemptCompute();
347
-
348
- return currentPromise!;
349
- };
350
-
351
- /**
352
- * Initializes the derived atom.
353
- * Called lazily on first access.
354
- */
355
- const init = () => {
356
- if (isInitialized) return;
357
- isInitialized = true;
358
-
359
- // Initial computation (silent - don't notify on init)
360
- compute(true);
361
- };
362
-
363
- const derivedAtom: DerivedAtom<T, boolean> = {
364
- [SYMBOL_ATOM]: true as const,
365
- [SYMBOL_DERIVED]: true as const,
366
- meta: options.meta,
367
-
368
- /**
369
- * Get the computed value as a Promise.
370
- * Always returns Promise<T>, even for sync computations.
371
- */
372
- get(): Promise<T> {
373
- init();
374
- return currentPromise!;
375
- },
376
-
377
- /**
378
- * The stale value - fallback or last resolved value.
379
- * - Without fallback: T | undefined
380
- * - With fallback: T (guaranteed)
381
- */
382
- get staleValue(): T | undefined {
383
- init();
384
- // Return lastResolvedValue if available, otherwise fallback (if configured)
385
- if (lastResolved) {
386
- return lastResolved.value;
387
- }
388
- if (hasFallback) {
389
- return fallbackValue;
390
- }
391
- return undefined;
392
- },
393
-
394
- /**
395
- * Get the current state of the derived atom.
396
- * Returns the actual underlying state (loading/ready/error).
397
- * Use staleValue if you need fallback/cached value during loading.
398
- */
399
- state(): AtomState<T> {
400
- init();
401
-
402
- if (isLoading) {
403
- return { status: "loading", promise: currentPromise! };
404
- }
405
-
406
- if (lastError !== undefined) {
407
- return { status: "error", error: lastError };
408
- }
409
-
410
- if (lastResolved) {
411
- return { status: "ready", value: lastResolved.value };
412
- }
413
-
414
- // Initial state before first computation completes
415
- return { status: "loading", promise: currentPromise! };
416
- },
417
-
418
- /**
419
- * Re-run the computation.
420
- */
421
- refresh(): void {
422
- if (!isInitialized) {
423
- init();
424
- } else {
425
- compute();
426
- }
427
- },
428
-
429
- /**
430
- * Subscribe to value changes.
431
- */
432
- on(listener: VoidFunction): VoidFunction {
433
- init();
434
- return changeEmitter.on(listener);
435
- },
436
- };
437
-
438
- // Store createInfo for use in onErrorHook
439
- createInfo = {
440
- type: "derived",
441
- key: options.meta?.key,
442
- meta: options.meta,
443
- instance: derivedAtom,
444
- };
445
-
446
- // Notify devtools/plugins of derived atom creation
447
- onCreateHook.current?.(createInfo);
448
-
449
- return derivedAtom;
450
- }