atomirx 0.0.4 → 0.0.6

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.
package/README.md CHANGED
@@ -1543,7 +1543,7 @@ The `onCreateHook` receives different info objects based on what's being created
1543
1543
 
1544
1544
  ```typescript
1545
1545
  // Mutable atom
1546
- interface MutableAtomCreateInfo {
1546
+ interface MutableCreateInfo {
1547
1547
  type: "mutable";
1548
1548
  key: string | undefined;
1549
1549
  meta: AtomMeta | undefined;
@@ -1551,7 +1551,7 @@ interface MutableAtomCreateInfo {
1551
1551
  }
1552
1552
 
1553
1553
  // Derived atom
1554
- interface DerivedAtomCreateInfo {
1554
+ interface DerivedCreateInfo {
1555
1555
  type: "derived";
1556
1556
  key: string | undefined;
1557
1557
  meta: AtomMeta | undefined;
@@ -1,68 +1,64 @@
1
-
2
1
  <!doctype html>
3
2
  <html lang="en">
4
-
5
- <head>
3
+ <head>
6
4
  <title>Code coverage report for src/core/onCreateHook.ts</title>
7
5
  <meta charset="utf-8" />
8
6
  <link rel="stylesheet" href="../../prettify.css" />
9
7
  <link rel="stylesheet" href="../../base.css" />
10
8
  <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
11
9
  <meta name="viewport" content="width=device-width, initial-scale=1" />
12
- <style type='text/css'>
13
- .coverage-summary .sorter {
14
- background-image: url(../../sort-arrow-sprite.png);
15
- }
10
+ <style type="text/css">
11
+ .coverage-summary .sorter {
12
+ background-image: url(../../sort-arrow-sprite.png);
13
+ }
16
14
  </style>
17
- </head>
18
-
19
- <body>
20
- <div class='wrapper'>
21
- <div class='pad1'>
22
- <h1><a href="../../index.html">All files</a> / <a href="index.html">src/core</a> onCreateHook.ts</h1>
23
- <div class='clearfix'>
24
-
25
- <div class='fl pad1y space-right2'>
26
- <span class="strong">100% </span>
27
- <span class="quiet">Statements</span>
28
- <span class='fraction'>17/17</span>
29
- </div>
30
-
31
-
32
- <div class='fl pad1y space-right2'>
33
- <span class="strong">100% </span>
34
- <span class="quiet">Branches</span>
35
- <span class='fraction'>0/0</span>
36
- </div>
37
-
38
-
39
- <div class='fl pad1y space-right2'>
40
- <span class="strong">100% </span>
41
- <span class="quiet">Functions</span>
42
- <span class='fraction'>0/0</span>
43
- </div>
44
-
45
-
46
- <div class='fl pad1y space-right2'>
47
- <span class="strong">100% </span>
48
- <span class="quiet">Lines</span>
49
- <span class='fraction'>17/17</span>
50
- </div>
51
-
52
-
15
+ </head>
16
+
17
+ <body>
18
+ <div class="wrapper">
19
+ <div class="pad1">
20
+ <h1>
21
+ <a href="../../index.html">All files</a> /
22
+ <a href="index.html">src/core</a> onCreateHook.ts
23
+ </h1>
24
+ <div class="clearfix">
25
+ <div class="fl pad1y space-right2">
26
+ <span class="strong">100% </span>
27
+ <span class="quiet">Statements</span>
28
+ <span class="fraction">17/17</span>
29
+ </div>
30
+
31
+ <div class="fl pad1y space-right2">
32
+ <span class="strong">100% </span>
33
+ <span class="quiet">Branches</span>
34
+ <span class="fraction">0/0</span>
35
+ </div>
36
+
37
+ <div class="fl pad1y space-right2">
38
+ <span class="strong">100% </span>
39
+ <span class="quiet">Functions</span>
40
+ <span class="fraction">0/0</span>
41
+ </div>
42
+
43
+ <div class="fl pad1y space-right2">
44
+ <span class="strong">100% </span>
45
+ <span class="quiet">Lines</span>
46
+ <span class="fraction">17/17</span>
47
+ </div>
53
48
  </div>
54
49
  <p class="quiet">
55
- Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
50
+ Press <em>n</em> or <em>j</em> to go to the next uncovered block,
51
+ <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
56
52
  </p>
57
53
  <template id="filterTemplate">
58
- <div class="quiet">
59
- Filter:
60
- <input type="search" id="fileSearch">
61
- </div>
54
+ <div class="quiet">
55
+ Filter:
56
+ <input type="search" id="fileSearch" />
57
+ </div>
62
58
  </template>
63
- </div>
64
- <div class='status-line high'></div>
65
- <pre><table class="coverage">
59
+ </div>
60
+ <div class="status-line high"></div>
61
+ <pre><table class="coverage">
66
62
  <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
67
63
  <a name='L2'></a><a href='#L2'>2</a>
68
64
  <a name='L3'></a><a href='#L3'>3</a>
@@ -100,7 +96,7 @@
100
96
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { hook } from "./hook";
101
97
  import { MutableAtom } from "./types";
102
98
  &nbsp;
103
- export interface AtomCreateInfo {
99
+ export interface CreateInfo {
104
100
  type: "atom";
105
101
  key: string | undefined;
106
102
  atom: MutableAtom&lt;any&gt;;
@@ -113,24 +109,30 @@ export interface ModuleCreateInfo {
113
109
  }
114
110
  &nbsp;
115
111
  export const onCreateHook =
116
- hook&lt;(info: AtomCreateInfo | ModuleCreateInfo) =&gt; void&gt;();
112
+ hook&lt;(info: CreateInfo | ModuleCreateInfo) =&gt; void&gt;();
117
113
  &nbsp;</pre></td></tr></table></pre>
118
114
 
119
- <div class='push'></div><!-- for sticky footer -->
120
- </div><!-- /wrapper -->
121
- <div class='footer quiet pad2 space-top1 center small'>
122
- Code coverage generated by
123
- <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
124
- at 2026-01-16T14:35:38.788Z
125
- </div>
126
- <script src="../../prettify.js"></script>
127
- <script>
128
- window.onload = function () {
129
- prettyPrint();
130
- };
131
- </script>
132
- <script src="../../sorter.js"></script>
133
- <script src="../../block-navigation.js"></script>
134
- </body>
115
+ <div class="push"></div>
116
+ <!-- for sticky footer -->
117
+ </div>
118
+ <!-- /wrapper -->
119
+ <div class="footer quiet pad2 space-top1 center small">
120
+ Code coverage generated by
121
+ <a
122
+ href="https://istanbul.js.org/"
123
+ target="_blank"
124
+ rel="noopener noreferrer"
125
+ >istanbul</a
126
+ >
127
+ at 2026-01-16T14:35:38.788Z
128
+ </div>
129
+ <script src="../../prettify.js"></script>
130
+ <script>
131
+ window.onload = function () {
132
+ prettyPrint();
133
+ };
134
+ </script>
135
+ <script src="../../sorter.js"></script>
136
+ <script src="../../block-navigation.js"></script>
137
+ </body>
135
138
  </html>
136
-
@@ -1,6 +1,19 @@
1
+ import { CreateInfo } from './onCreateHook';
1
2
  import { ReactiveSelector, SelectContext } from './select';
2
3
  import { DerivedAtom, DerivedOptions } from './types';
3
4
  import { WithReadySelectContext } from './withReady';
5
+ /**
6
+ * Internal options for derived atoms.
7
+ * These are not part of the public API.
8
+ * @internal
9
+ */
10
+ export interface DerivedInternalOptions {
11
+ /**
12
+ * Override the error source for onErrorHook.
13
+ * Used by effect() to attribute errors to the effect instead of the internal derived.
14
+ */
15
+ _errorSource?: CreateInfo;
16
+ }
4
17
  /**
5
18
  * Context object passed to derived atom selector functions.
6
19
  * Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
@@ -130,7 +143,7 @@ export interface DerivedContext extends SelectContext, WithReadySelectContext {
130
143
  * data$.refresh(); // Re-run computation
131
144
  * ```
132
145
  */
133
- export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options?: DerivedOptions<T>): DerivedAtom<T, false>;
146
+ export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options?: DerivedOptions<T> & DerivedInternalOptions): DerivedAtom<T, false>;
134
147
  export declare function derived<T>(fn: ReactiveSelector<T, DerivedContext>, options: DerivedOptions<T> & {
135
148
  fallback: T;
136
- }): DerivedAtom<T, true>;
149
+ } & DerivedInternalOptions): DerivedAtom<T, true>;
@@ -1,5 +1,5 @@
1
1
  import { ReactiveSelector, SelectContext } from './select';
2
- import { EffectOptions } from './types';
2
+ import { EffectMeta, EffectOptions } from './types';
3
3
  import { WithReadySelectContext } from './withReady';
4
4
  /**
5
5
  * Context object passed to effect functions.
@@ -22,6 +22,10 @@ export interface EffectContext extends SelectContext, WithReadySelectContext {
22
22
  */
23
23
  onCleanup: (cleanup: VoidFunction) => void;
24
24
  }
25
+ export interface Effect {
26
+ dispose: VoidFunction;
27
+ meta?: EffectMeta;
28
+ }
25
29
  /**
26
30
  * Creates a side-effect that runs when accessed atom(s) change.
27
31
  *
@@ -113,4 +117,4 @@ export interface EffectContext extends SelectContext, WithReadySelectContext {
113
117
  * @returns Dispose function to stop the effect and run final cleanup
114
118
  * @throws Error if effect function returns a Promise
115
119
  */
116
- export declare function effect(fn: ReactiveSelector<void, EffectContext>, _options?: EffectOptions): VoidFunction;
120
+ export declare function effect(fn: ReactiveSelector<void, EffectContext>, options?: EffectOptions): Effect;
@@ -54,7 +54,7 @@ export interface Hook<T> {
54
54
  /**
55
55
  * Current value of the hook. Direct property access for fast reads.
56
56
  */
57
- current: T;
57
+ readonly current: T;
58
58
  /**
59
59
  * Override the current value using a reducer.
60
60
  * The reducer receives the previous value and returns the next value.
@@ -1,8 +1,9 @@
1
- import { MutableAtomMeta, DerivedAtomMeta, MutableAtom, DerivedAtom, ModuleMeta } from './types';
1
+ import { Effect } from './effect';
2
+ import { MutableAtomMeta, DerivedAtomMeta, MutableAtom, DerivedAtom, ModuleMeta, EffectMeta } from './types';
2
3
  /**
3
4
  * Information provided when a mutable atom is created.
4
5
  */
5
- export interface MutableAtomCreateInfo {
6
+ export interface MutableInfo {
6
7
  /** Discriminator for mutable atoms */
7
8
  type: "mutable";
8
9
  /** Optional key from atom options (for debugging/devtools) */
@@ -10,12 +11,12 @@ export interface MutableAtomCreateInfo {
10
11
  /** Optional metadata from atom options */
11
12
  meta: MutableAtomMeta | undefined;
12
13
  /** The created mutable atom instance */
13
- atom: MutableAtom<unknown>;
14
+ instance: MutableAtom<unknown>;
14
15
  }
15
16
  /**
16
17
  * Information provided when a derived atom is created.
17
18
  */
18
- export interface DerivedAtomCreateInfo {
19
+ export interface DerivedInfo {
19
20
  /** Discriminator for derived atoms */
20
21
  type: "derived";
21
22
  /** Optional key from derived options (for debugging/devtools) */
@@ -23,16 +24,29 @@ export interface DerivedAtomCreateInfo {
23
24
  /** Optional metadata from derived options */
24
25
  meta: DerivedAtomMeta | undefined;
25
26
  /** The created derived atom instance */
26
- atom: DerivedAtom<unknown, boolean>;
27
+ instance: DerivedAtom<unknown, boolean>;
27
28
  }
28
29
  /**
29
- * Union type for atom creation info (mutable or derived).
30
+ * Information provided when an effect is created.
30
31
  */
31
- export type AtomCreateInfo = MutableAtomCreateInfo | DerivedAtomCreateInfo;
32
+ export interface EffectInfo {
33
+ /** Discriminator for effects */
34
+ type: "effect";
35
+ /** Optional key from effect options (for debugging/devtools) */
36
+ key: string | undefined;
37
+ /** Optional metadata from effect options */
38
+ meta: EffectMeta | undefined;
39
+ /** The created effect instance */
40
+ instance: Effect;
41
+ }
42
+ /**
43
+ * Union type for atom/derived/effect creation info.
44
+ */
45
+ export type CreateInfo = MutableInfo | DerivedInfo | EffectInfo;
32
46
  /**
33
47
  * Information provided when a module (via define()) is created.
34
48
  */
35
- export interface ModuleCreateInfo {
49
+ export interface ModuleInfo {
36
50
  /** Discriminator for modules */
37
51
  type: "module";
38
52
  /** Optional key from define options (for debugging/devtools) */
@@ -40,7 +54,7 @@ export interface ModuleCreateInfo {
40
54
  /** Optional metadata from define options */
41
55
  meta: ModuleMeta | undefined;
42
56
  /** The created module instance */
43
- module: unknown;
57
+ instance: unknown;
44
58
  }
45
59
  /**
46
60
  * Global hook that fires whenever an atom or module is created.
@@ -50,30 +64,30 @@ export interface ModuleCreateInfo {
50
64
  * - **Debugging** - log atom creation for troubleshooting
51
65
  * - **Testing** - verify expected atoms are created
52
66
  *
67
+ * **IMPORTANT**: Always use `.override()` to preserve the hook chain.
68
+ * Direct assignment to `.current` will break existing handlers.
69
+ *
53
70
  * @example Basic logging
54
71
  * ```ts
55
- * onCreateHook.current = (info) => {
72
+ * onCreateHook.override((prev) => (info) => {
73
+ * prev?.(info); // call existing handlers first
56
74
  * console.log(`Created ${info.type}: ${info.key ?? "anonymous"}`);
57
- * };
75
+ * });
58
76
  * ```
59
77
  *
60
78
  * @example DevTools integration
61
79
  * ```ts
62
- * const atoms = new Map();
63
- * const modules = new Map();
80
+ * const registry = new Map();
64
81
  *
65
- * onCreateHook.current = (info) => {
66
- * if (info.type === "module") {
67
- * modules.set(info.key, info.module);
68
- * } else {
69
- * atoms.set(info.key, info.atom);
70
- * }
71
- * };
82
+ * onCreateHook.override((prev) => (info) => {
83
+ * prev?.(info); // preserve chain
84
+ * registry.set(info.key, info.instance);
85
+ * });
72
86
  * ```
73
87
  *
74
- * @example Cleanup (disable hook)
88
+ * @example Reset to default (disable all handlers)
75
89
  * ```ts
76
- * onCreateHook.current = undefined;
90
+ * onCreateHook.reset();
77
91
  * ```
78
92
  */
79
- export declare const onCreateHook: import('./hook').Hook<((info: AtomCreateInfo | ModuleCreateInfo) => void) | undefined>;
93
+ export declare const onCreateHook: import('./hook').Hook<((info: CreateInfo | ModuleInfo) => void) | undefined>;
@@ -0,0 +1,49 @@
1
+ import { CreateInfo } from './onCreateHook';
2
+ /**
3
+ * Information provided when an error occurs in an atom, derived, or effect.
4
+ */
5
+ export interface ErrorInfo {
6
+ /** The source that produced the error (atom, derived, or effect) */
7
+ source: CreateInfo;
8
+ /** The error that was thrown */
9
+ error: unknown;
10
+ }
11
+ /**
12
+ * Global hook that fires whenever an error occurs in a derived atom or effect.
13
+ *
14
+ * This is useful for:
15
+ * - **Global error logging** - capture all errors in one place
16
+ * - **Error monitoring** - send errors to monitoring services (Sentry, etc.)
17
+ * - **DevTools integration** - show errors in developer tools
18
+ * - **Debugging** - track which atoms/effects are failing
19
+ *
20
+ * **IMPORTANT**: Always use `.override()` to preserve the hook chain.
21
+ * Direct assignment to `.current` will break existing handlers.
22
+ *
23
+ * @example Basic logging
24
+ * ```ts
25
+ * onErrorHook.override((prev) => (info) => {
26
+ * prev?.(info); // call existing handlers first
27
+ * console.error(`Error in ${info.source.type}: ${info.source.key ?? "anonymous"}`, info.error);
28
+ * });
29
+ * ```
30
+ *
31
+ * @example Send to monitoring service
32
+ * ```ts
33
+ * onErrorHook.override((prev) => (info) => {
34
+ * prev?.(info); // preserve chain
35
+ * Sentry.captureException(info.error, {
36
+ * tags: {
37
+ * source_type: info.source.type,
38
+ * source_key: info.source.key,
39
+ * },
40
+ * });
41
+ * });
42
+ * ```
43
+ *
44
+ * @example Reset to default (disable all handlers)
45
+ * ```ts
46
+ * onErrorHook.reset();
47
+ * ```
48
+ */
49
+ export declare const onErrorHook: import('./hook').Hook<((info: ErrorInfo) => void) | undefined>;
@@ -0,0 +1 @@
1
+ export {};
@@ -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.
@@ -266,12 +265,62 @@ export interface DerivedOptions<T> {
266
265
  meta?: DerivedAtomMeta;
267
266
  /** Equality strategy for change detection (default: "strict") */
268
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;
269
291
  }
270
292
  /**
271
293
  * Configuration options for effects.
272
294
  */
273
295
  export interface EffectOptions {
274
- /** 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 {
275
324
  key?: string;
276
325
  }
277
326
  /**
@@ -49,6 +49,52 @@ export interface WithReadySelectContext {
49
49
  * ```
50
50
  */
51
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>;
52
98
  }
53
99
  /**
54
100
  * Plugin that adds `ready()` method to a SelectContext.