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
@@ -25,8 +25,8 @@ export declare const SYMBOL_DERIVED: unique symbol;
25
25
  * @example
26
26
  * ```ts
27
27
  * const enhanced = atom(0)
28
- * .use(source => ({ ...source, double: () => source.value * 2 }))
29
- * .use(source => ({ ...source, triple: () => source.value * 3 }));
28
+ * .use(source => ({ ...source, double: () => source.get() * 2 }))
29
+ * .use(source => ({ ...source, triple: () => source.get() * 3 }));
30
30
  * ```
31
31
  */
32
32
  export interface Pipeable {
@@ -37,9 +37,8 @@ export interface Pipeable {
37
37
  /**
38
38
  * Optional metadata for atoms.
39
39
  */
40
- export interface AtomMeta {
40
+ export interface AtomMeta extends AtomirxMeta {
41
41
  key?: string;
42
- [key: string]: unknown;
43
42
  }
44
43
  /**
45
44
  * Base interface for all atoms.
@@ -50,10 +49,10 @@ export interface AtomMeta {
50
49
  export interface Atom<T> {
51
50
  /** Symbol marker to identify atom instances */
52
51
  readonly [SYMBOL_ATOM]: true;
53
- /** The current value */
54
- readonly value: T;
55
52
  /** Optional metadata for the atom */
56
53
  readonly meta?: AtomMeta;
54
+ /** Get the current value */
55
+ get(): T;
57
56
  /**
58
57
  * Subscribe to value changes.
59
58
  * @param listener - Callback invoked when value changes
@@ -79,7 +78,7 @@ export interface Atom<T> {
79
78
  *
80
79
  * // Async value (stores Promise as-is)
81
80
  * const posts = atom(fetchPosts());
82
- * posts.value; // Promise<Post[]>
81
+ * posts.get(); // Promise<Post[]>
83
82
  * posts.set(fetchPosts()); // Store new Promise
84
83
  * ```
85
84
  */
@@ -119,7 +118,7 @@ export interface MutableAtom<T> extends Atom<T>, Pipeable {
119
118
  * A derived (computed) atom that always returns Promise<T> for its value.
120
119
  *
121
120
  * DerivedAtom computes its value from other atoms. The computation is
122
- * re-run whenever dependencies change. The `.value` always returns a Promise,
121
+ * re-run whenever dependencies change. The `.get()` always returns a Promise,
123
122
  * even for synchronous computations.
124
123
  *
125
124
  * @template T - The resolved type of the computed value
@@ -128,13 +127,13 @@ export interface MutableAtom<T> extends Atom<T>, Pipeable {
128
127
  * @example
129
128
  * ```ts
130
129
  * // Without fallback
131
- * const double$ = derived(({ get }) => get(count$) * 2);
132
- * await double$.value; // number
130
+ * const double$ = derived(({ read }) => read(count$) * 2);
131
+ * await double$.get(); // number
133
132
  * double$.staleValue; // number | undefined
134
133
  * double$.state(); // { status: "ready", value: 10 }
135
134
  *
136
135
  * // With fallback - during loading
137
- * const double$ = derived(({ get }) => get(count$) * 2, { fallback: 0 });
136
+ * const double$ = derived(({ read }) => read(count$) * 2, { fallback: 0 });
138
137
  * double$.staleValue; // number (guaranteed)
139
138
  * double$.state(); // { status: "loading", promise } during loading
140
139
  * ```
@@ -178,13 +177,59 @@ export type AtomValue<A> = A extends DerivedAtom<infer V, boolean> ? V : A exten
178
177
  export type AtomState<T> = {
179
178
  status: "ready";
180
179
  value: T;
180
+ error?: undefined;
181
+ promise?: undefined;
181
182
  } | {
182
183
  status: "error";
183
184
  error: unknown;
185
+ value?: undefined;
186
+ promise?: undefined;
184
187
  } | {
185
188
  status: "loading";
186
189
  promise: Promise<T>;
190
+ value?: undefined;
191
+ error?: undefined;
187
192
  };
193
+ /**
194
+ * Result type for SelectContext.state() - simplified AtomState without promise.
195
+ *
196
+ * All properties (`status`, `value`, `error`) are always present:
197
+ * - `value` is `T` when ready, `undefined` otherwise
198
+ * - `error` is the error when errored, `undefined` otherwise
199
+ *
200
+ * This enables easy destructuring without type narrowing:
201
+ * ```ts
202
+ * const { status, value, error } = state(atom$);
203
+ * ```
204
+ *
205
+ * Equality comparisons work correctly (no promise reference issues).
206
+ */
207
+ export type SelectStateResult<T> = {
208
+ status: "ready";
209
+ value: T;
210
+ error: undefined;
211
+ } | {
212
+ status: "error";
213
+ value: undefined;
214
+ error: unknown;
215
+ } | {
216
+ status: "loading";
217
+ value: undefined;
218
+ error: undefined;
219
+ };
220
+ /**
221
+ * Result type for race() and any() - includes winning key.
222
+ *
223
+ * @template K - The key type (string literal union)
224
+ * @template V - The value type
225
+ */
226
+ export type KeyedResult<K extends string, V> = {
227
+ /** The key that won the race/any */
228
+ key: K;
229
+ /** The resolved value */
230
+ value: V;
231
+ };
232
+ export type AtomPlugin = <T extends Atom<any>>(atom: T) => T | void;
188
233
  /**
189
234
  * Result type for settled operations.
190
235
  */
@@ -220,15 +265,63 @@ export interface DerivedOptions<T> {
220
265
  meta?: DerivedAtomMeta;
221
266
  /** Equality strategy for change detection (default: "strict") */
222
267
  equals?: Equality<T>;
268
+ /**
269
+ * Callback invoked when the derived computation throws an error.
270
+ * This is called for actual errors, NOT for Promise throws (Suspense).
271
+ *
272
+ * @param error - The error thrown during computation
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const data$ = derived(
277
+ * ({ read }) => {
278
+ * const raw = read(source$);
279
+ * return JSON.parse(raw); // May throw SyntaxError
280
+ * },
281
+ * {
282
+ * onError: (error) => {
283
+ * console.error('Derived computation failed:', error);
284
+ * reportToSentry(error);
285
+ * }
286
+ * }
287
+ * );
288
+ * ```
289
+ */
290
+ onError?: (error: unknown) => void;
223
291
  }
224
292
  /**
225
293
  * Configuration options for effects.
226
294
  */
227
295
  export interface EffectOptions {
228
- /** Optional key for debugging */
296
+ meta?: EffectMeta;
297
+ /**
298
+ * Callback invoked when the effect computation throws an error.
299
+ * This is called for actual errors, NOT for Promise throws (Suspense).
300
+ *
301
+ * @param error - The error thrown during effect execution
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * effect(
306
+ * ({ read }) => {
307
+ * const data = read(source$);
308
+ * riskyOperation(data); // May throw
309
+ * },
310
+ * {
311
+ * onError: (error) => {
312
+ * console.error('Effect failed:', error);
313
+ * showErrorNotification(error);
314
+ * }
315
+ * }
316
+ * );
317
+ * ```
318
+ */
319
+ onError?: (error: unknown) => void;
320
+ }
321
+ export interface AtomirxMeta {
322
+ }
323
+ export interface EffectMeta extends AtomirxMeta {
229
324
  key?: string;
230
- /** Error handler for uncaught errors in the effect */
231
- onError?: (error: Error) => void;
232
325
  }
233
326
  /**
234
327
  * A function that returns a value when called.
@@ -269,11 +362,3 @@ export interface ModuleMeta {
269
362
  }
270
363
  export type Listener<T> = (value: T) => void;
271
364
  export type SingleOrMultipleListeners<T> = Listener<T> | Listener<T>[];
272
- /**
273
- * Type guard to check if a value is an Atom.
274
- */
275
- export declare function isAtom<T>(value: unknown): value is Atom<T>;
276
- /**
277
- * Type guard to check if a value is a DerivedAtom.
278
- */
279
- export declare function isDerived<T>(value: unknown): value is DerivedAtom<T, boolean>;
@@ -0,0 +1,115 @@
1
+ import { SelectContext } from './select';
2
+ import { Atom } from './types';
3
+ /**
4
+ * Extension interface that adds `ready()` method to SelectContext.
5
+ * Used in derived atoms and effects to wait for non-null values.
6
+ */
7
+ export interface WithReadySelectContext {
8
+ /**
9
+ * Wait for an atom to have a non-null/non-undefined value.
10
+ *
11
+ * If the value is null/undefined, the computation suspends until the atom
12
+ * changes to a non-null value, then automatically resumes.
13
+ *
14
+ * **IMPORTANT: Only use in `derived()` or `effect()` context**
15
+ *
16
+ * @param atom - The atom to read and wait for
17
+ * @returns The non-null value (type excludes null | undefined)
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Wait for currentArticleId to be set before computing
22
+ * const currentArticle$ = derived(({ ready, read }) => {
23
+ * const id = ready(currentArticleId$); // Suspends if null
24
+ * const cache = read(articleCache$);
25
+ * return cache[id];
26
+ * });
27
+ * ```
28
+ */
29
+ ready<T>(atom: Atom<T>): T extends PromiseLike<any> ? never : Exclude<T, null | undefined>;
30
+ /**
31
+ * Wait for a selected value from an atom to be non-null/non-undefined.
32
+ *
33
+ * If the selected value is null/undefined, the computation suspends until the
34
+ * selected value changes to a non-null value, then automatically resumes.
35
+ *
36
+ * **IMPORTANT: Only use in `derived()` or `effect()` context**
37
+ *
38
+ * @param atom - The atom to read
39
+ * @param selector - Function to extract/transform the value
40
+ * @returns The non-null selected value
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Wait for user's email to be set
45
+ * const emailDerived$ = derived(({ ready }) => {
46
+ * const email = ready(user$, u => u.email); // Suspends if email is null
47
+ * return `Contact: ${email}`;
48
+ * });
49
+ * ```
50
+ */
51
+ ready<T, R>(atom: Atom<T>, selector: (current: Awaited<T>) => R): R extends PromiseLike<any> ? never : Exclude<R, null | undefined>;
52
+ /**
53
+ * Execute a function and wait for its result to be non-null/non-undefined.
54
+ *
55
+ * If the function returns null/undefined, the computation suspends until
56
+ * re-executed with a non-null result.
57
+ *
58
+ * **IMPORTANT: Only use in `derived()` or `effect()` context**
59
+ *
60
+ * **NOTE:** This overload is designed for use with async combinators like
61
+ * `all()`, `race()`, `any()`, `settled()` where promises come from stable
62
+ * atom sources. It does NOT support dynamic promise creation (returning a
63
+ * new Promise from the callback). For async selectors that return promises,
64
+ * use `ready(atom$, selector?)` instead.
65
+ *
66
+ * @param fn - Synchronous function to execute and wait for
67
+ * @returns The non-null result (excludes null | undefined)
68
+ * @throws {Error} If the callback returns a Promise
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * // Wait for a computed value to be ready
73
+ * const result$ = derived(({ ready, read }) => {
74
+ * const value = ready(() => computeExpensiveValue(read(input$)));
75
+ * return `Result: ${value}`;
76
+ * });
77
+ * ```
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Use with async combinators (all, race, any, settled)
82
+ * const combined$ = derived(({ ready, all }) => {
83
+ * const [user, posts] = ready(() => all(user$, posts$));
84
+ * return { user, posts };
85
+ * });
86
+ * ```
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // For async selectors, use ready(atom$, selector?) instead:
91
+ * const data$ = derived(({ ready }) => {
92
+ * const data = ready(source$, (val) => fetchData(val.id));
93
+ * return data;
94
+ * });
95
+ * ```
96
+ */
97
+ ready<T>(fn: () => T): T extends PromiseLike<any> ? never : Exclude<Awaited<T>, null | undefined>;
98
+ }
99
+ /**
100
+ * Plugin that adds `ready()` method to a SelectContext.
101
+ *
102
+ * `ready()` enables a "reactive suspension" pattern where derived atoms
103
+ * wait for required values before computing. This is useful for:
104
+ *
105
+ * - Route-based entity loading (`/article/:id` - wait for ID to be set)
106
+ * - Authentication-gated content (wait for user to be logged in)
107
+ * - Conditional data dependencies (wait for prerequisite data)
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // Used internally by derived() - you don't need to call this directly
112
+ * const result = select((context) => fn(context.use(withReady())));
113
+ * ```
114
+ */
115
+ export declare function withReady(): <TContext extends SelectContext>(context: TContext) => TContext & WithReadySelectContext;
@@ -0,0 +1 @@
1
+ export {};