atomirx 0.1.0 → 0.1.2

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.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Options for configuring an abortable promise.
3
+ */
4
+ export interface AbortableOptions {
5
+ /**
6
+ * External signal(s) to link. The abortable promise will abort when any
7
+ * of these signals abort. Useful for hierarchical cancellation.
8
+ *
9
+ * @example Single signal
10
+ * ```ts
11
+ * const controller = new AbortController();
12
+ * const req = abortable((signal) => fetch('/api', { signal }), {
13
+ * signal: controller.signal
14
+ * });
15
+ * controller.abort(); // Aborts the request
16
+ * ```
17
+ *
18
+ * @example Multiple signals (e.g., timeout + user cancel)
19
+ * ```ts
20
+ * const req = abortable((signal) => fetch('/api', { signal }), {
21
+ * signal: [AbortSignal.timeout(5000), userCancelSignal]
22
+ * });
23
+ * ```
24
+ */
25
+ signal?: AbortSignal | AbortSignal[];
26
+ /**
27
+ * Callback invoked when the operation is aborted. Called once, regardless
28
+ * of abort source (manual abort, linked signal, or inner AbortError).
29
+ *
30
+ * @param reason - The abort reason (string, Error, or any value)
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const req = abortable((signal) => fetch('/api', { signal }), {
35
+ * onAbort: (reason) => console.log('Aborted:', reason)
36
+ * });
37
+ * ```
38
+ */
39
+ onAbort?: (reason: unknown) => void;
40
+ }
41
+ /**
42
+ * Promise with abort control capabilities.
43
+ *
44
+ * Extends the standard Promise interface with:
45
+ * - `abort(reason?)` - Cancel the operation
46
+ * - `aborted()` - Check if operation was aborted
47
+ */
48
+ export interface AbortablePromise<T> extends Promise<T> {
49
+ /**
50
+ * Abort the operation with an optional reason.
51
+ *
52
+ * - Immediately rejects the promise with an AbortError
53
+ * - Calls the `onAbort` callback if provided
54
+ * - Signals abort to the inner function via its AbortSignal
55
+ * - Safe to call multiple times (subsequent calls are no-ops)
56
+ *
57
+ * @param reason - Optional abort reason (passed to onAbort and error message)
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const req = abortable((signal) => fetch('/api', { signal }));
62
+ * req.abort('User cancelled');
63
+ * ```
64
+ */
65
+ abort(reason?: unknown): void;
66
+ /**
67
+ * Returns true if the operation was aborted.
68
+ *
69
+ * Aborted state is set when:
70
+ * - `abort()` is called explicitly
71
+ * - A linked signal aborts
72
+ * - The inner function throws an AbortError
73
+ *
74
+ * @returns boolean indicating abort state
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const req = abortable((signal) => fetch('/api', { signal }));
79
+ * req.abort();
80
+ * console.log(req.aborted()); // true
81
+ * ```
82
+ */
83
+ aborted(): boolean;
84
+ }
85
+ /**
86
+ * Creates an AbortError (DOMException with name 'AbortError').
87
+ *
88
+ * @param reason - Optional reason for the abort (string, Error, or any value)
89
+ * @returns DOMException with name 'AbortError'
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * throw createAbortError('User cancelled');
94
+ * throw createAbortError({ message: 'Timeout exceeded' });
95
+ * ```
96
+ */
97
+ export declare function createAbortError(reason?: unknown): DOMException;
98
+ /**
99
+ * Checks if an error is an AbortError.
100
+ *
101
+ * Returns true for:
102
+ * - DOMException with name 'AbortError'
103
+ * - Error with name 'AbortError'
104
+ *
105
+ * @param error - The error to check
106
+ * @returns true if the error is an AbortError
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * try {
111
+ * await abortableRequest;
112
+ * } catch (e) {
113
+ * if (isAbortError(e)) {
114
+ * console.log('Request was cancelled');
115
+ * } else {
116
+ * throw e;
117
+ * }
118
+ * }
119
+ * ```
120
+ */
121
+ export declare function isAbortError(error: unknown): boolean;
122
+ /**
123
+ * Wraps an async function with abort control.
124
+ *
125
+ * Returns a Promise that can be aborted, with automatic cleanup and
126
+ * hierarchical cancellation support.
127
+ *
128
+ * ## Features
129
+ *
130
+ * - **Guaranteed rejection on abort**: Immediate rejection when abort is called
131
+ * - **Hierarchical cancellation**: Link to parent signals for nested abort
132
+ * - **Bidirectional abort detection**: Detects abort from both external and internal sources
133
+ * - **Safe abort**: Multiple abort() calls are no-ops
134
+ *
135
+ * ## Behavior Table
136
+ *
137
+ * | Trigger | Promise | aborted() | onAbort |
138
+ * |----------------------------|-----------------|-----------|---------|
139
+ * | `abort()` called | Rejects | `true` | ✓ |
140
+ * | Linked signal aborts | Rejects | `true` | ✓ |
141
+ * | Inner fn throws AbortError | Rejects | `true` | ✓ |
142
+ * | Inner fn throws other | Rejects | `false` | ✗ |
143
+ * | Inner fn resolves | Resolves | `false` | ✗ |
144
+ *
145
+ * @param fn - Function that receives an AbortSignal. Can return sync value or Promise.
146
+ * @param options - Optional configuration (linked signals, onAbort callback)
147
+ * @returns AbortablePromise with abort() and aborted() methods
148
+ *
149
+ * @example Basic usage with async function
150
+ * ```ts
151
+ * const req = abortable((signal) => fetch('/api', { signal }));
152
+ * req.abort('User cancelled');
153
+ * console.log(req.aborted()); // true
154
+ * ```
155
+ *
156
+ * @example Sync function (always returns a Promise)
157
+ * ```ts
158
+ * const result = await abortable(() => computeExpensiveValue());
159
+ * ```
160
+ *
161
+ * @example With timeout and cleanup
162
+ * ```ts
163
+ * const req = abortable(
164
+ * (signal) => fetch('/api/slow', { signal }),
165
+ * {
166
+ * signal: AbortSignal.timeout(5000),
167
+ * onAbort: () => console.log('Request timed out or cancelled')
168
+ * }
169
+ * );
170
+ * ```
171
+ *
172
+ * @example Composing nested abortable operations
173
+ * ```ts
174
+ * const parent = abortable(async (signal) => {
175
+ * const a = await abortable((s) => fetchA(s), { signal });
176
+ * const b = await abortable((s) => fetchB(s), { signal });
177
+ * return { a, b };
178
+ * });
179
+ * parent.abort(); // Cancels entire chain
180
+ * ```
181
+ *
182
+ * @example Component unmount pattern
183
+ * ```ts
184
+ * const unmountController = new AbortController();
185
+ *
186
+ * // In component effect
187
+ * const req = abortable(
188
+ * (signal) => fetchUserData(signal),
189
+ * { signal: unmountController.signal }
190
+ * );
191
+ *
192
+ * // On unmount
193
+ * unmountController.abort();
194
+ * ```
195
+ */
196
+ export declare function abortable<T>(fn: (signal: AbortSignal) => T | Promise<T>, options?: AbortableOptions): AbortablePromise<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -16,10 +16,13 @@ export interface DerivedInternalOptions {
16
16
  }
17
17
  /**
18
18
  * Context object passed to derived atom selector functions.
19
- * Provides utilities for reading atoms: `{ read, all, any, race, settled }`.
19
+ * Provides utilities for reading atoms.
20
20
  *
21
- * Currently identical to `SelectContext`, but defined separately to allow
22
- * future derived-specific extensions without breaking changes.
21
+ * Extends SelectContext with:
22
+ * - `ready()` - Wait for non-null values (from WithReadyContext)
23
+ *
24
+ * Note: Events now implement Atom<Promise<T>> and work directly with
25
+ * `read()`, `race()`, `all()` - no special `wait()` method needed.
23
26
  */
24
27
  export interface DerivedContext extends SelectContext, WithReadyContext {
25
28
  }
@@ -5,6 +5,9 @@ import { WithReadyContext } from './withReady';
5
5
  /**
6
6
  * Context object passed to effect functions.
7
7
  * Extends `SelectContext` with cleanup utilities.
8
+ *
9
+ * Note: Events now implement Atom<Promise<T>> and work directly with
10
+ * `read()`, `race()`, `all()` - no special `wait()` method needed.
8
11
  */
9
12
  export interface EffectContext extends SelectContext, WithReadyContext, WithAbortContext {
10
13
  /**
@@ -0,0 +1,152 @@
1
+ import { Equality, Event, EventMeta } from './types';
2
+ /**
3
+ * Options for creating an event.
4
+ *
5
+ * @template T - The type of payload
6
+ */
7
+ export interface EventOptions<T = void> {
8
+ /** Optional metadata for debugging/devtools */
9
+ meta?: EventMeta;
10
+ /**
11
+ * Equality function to compare payloads.
12
+ *
13
+ * When `fire(payload)` is called after the first fire:
14
+ * - If `equals(lastPayload, payload)` returns true → no-op (no new promise)
15
+ * - If `equals(lastPayload, payload)` returns false → create new resolved promise
16
+ *
17
+ * **Default: `() => false`** - every fire creates a new promise.
18
+ * This is important for void events where `fire()` is called multiple times.
19
+ *
20
+ * @example Dedupe identical payloads
21
+ * ```ts
22
+ * const searchEvent = event<string>({ equals: "shallow" });
23
+ * searchEvent.fire("hello"); // Promise1 resolves
24
+ * searchEvent.fire("hello"); // No-op (same value)
25
+ * searchEvent.fire("world"); // Promise2 created
26
+ * ```
27
+ */
28
+ equals?: Equality<T>;
29
+ /**
30
+ * If true, the event can only fire once.
31
+ * After first fire, subsequent `fire()` calls are no-op.
32
+ * `next()` returns the same resolved promise forever.
33
+ *
34
+ * Useful for one-time events like initialization, login, or data load.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const initEvent = event<Config>({ once: true });
39
+ * initEvent.fire(config); // Promise resolved
40
+ * initEvent.fire(config2); // No-op (already fired)
41
+ * initEvent.get(); // Same resolved promise
42
+ * initEvent.next(); // Same resolved promise
43
+ * ```
44
+ */
45
+ once?: boolean;
46
+ }
47
+ /**
48
+ * Creates an event - a reactive signal that can be fired with a payload.
49
+ *
50
+ * Events implement `Atom<Promise<T>>`, so they work directly with
51
+ * `read()`, `race()`, `all()` in derived/effect - no wrapper needed.
52
+ *
53
+ * ## Behavior
54
+ *
55
+ * ```
56
+ * event<T>() created
57
+ * │
58
+ * ▼
59
+ * atom.value = Promise1 (pending)
60
+ * │
61
+ * fire(A) ────────────────────────────────┐
62
+ * │ │
63
+ * First fire? │
64
+ * │Yes │
65
+ * ▼ │
66
+ * Promise1.resolve(A) │
67
+ * last = { data: A } │
68
+ * │ │
69
+ * fire(B) ────────────────────────────────┤
70
+ * │ │
71
+ * !equals(A, B)? │
72
+ * │Yes │
73
+ * ▼ │
74
+ * atom.value = Promise2 (resolved B) │
75
+ * → subscribers notified │
76
+ * last = { data: B } │
77
+ * ```
78
+ *
79
+ * ## Key Features
80
+ *
81
+ * 1. **Atom-compatible**: Works with `read()`, `race()`, `all()` directly
82
+ * 2. **Suspend until fire**: `read(event)` suspends if promise is pending
83
+ * 3. **Reactive**: `fire()` triggers derived recomputation via subscription
84
+ * 4. **Deduplication**: Use `equals` option to skip identical payloads
85
+ *
86
+ * @template T - The type of payload (void for no payload)
87
+ * @param options - Optional configuration (meta, equals)
88
+ * @returns An Event instance that implements Atom<Promise<T>>
89
+ *
90
+ * @example Basic usage
91
+ * ```ts
92
+ * const clickEvent = event<{ x: number; y: number }>();
93
+ *
94
+ * // Fire the event
95
+ * clickEvent.fire({ x: 100, y: 200 });
96
+ *
97
+ * // Get last fired payload
98
+ * clickEvent.last(); // { x: 100, y: 200 }
99
+ *
100
+ * // Get current promise
101
+ * const promise = clickEvent.get();
102
+ * ```
103
+ *
104
+ * @example Void event (no payload)
105
+ * ```ts
106
+ * const cancelEvent = event();
107
+ * cancelEvent.fire(); // No payload needed
108
+ * cancelEvent.fire(); // Each fire creates new promise (default equals)
109
+ * ```
110
+ *
111
+ * @example In derived - works directly with read()
112
+ * ```ts
113
+ * const result$ = derived(({ read }) => {
114
+ * const data = read(submitEvent); // suspends until fire
115
+ * return processSubmit(data);
116
+ * });
117
+ * ```
118
+ *
119
+ * @example Race multiple events
120
+ * ```ts
121
+ * const result$ = derived(({ race }) => {
122
+ * const { key, value } = race({
123
+ * submit: submitEvent,
124
+ * cancel: cancelEvent,
125
+ * });
126
+ * return key === 'cancel' ? null : processSubmit(value);
127
+ * });
128
+ * ```
129
+ *
130
+ * @example With equals (dedupe identical payloads)
131
+ * ```ts
132
+ * const searchEvent = event<string>({ equals: "shallow" });
133
+ * searchEvent.fire("hello"); // Promise1 resolves
134
+ * searchEvent.fire("hello"); // No-op (same value)
135
+ * searchEvent.fire("world"); // Promise2 created, subscribers notified
136
+ * ```
137
+ */
138
+ export declare function event<T = void>(options?: EventOptions<T>): Event<T>;
139
+ /**
140
+ * Type guard to check if a value is an Event.
141
+ *
142
+ * @param value - The value to check
143
+ * @returns true if the value is an Event
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * if (isEvent(value)) {
148
+ * value.fire(payload);
149
+ * }
150
+ * ```
151
+ */
152
+ export declare function isEvent<T = unknown>(value: unknown): value is Event<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { Effect } from './effect';
2
- import { MutableAtomMeta, DerivedAtomMeta, MutableAtom, DerivedAtom, ModuleMeta, EffectMeta, PoolMeta, Pool } from './types';
2
+ import { MutableAtomMeta, DerivedAtomMeta, MutableAtom, DerivedAtom, ModuleMeta, EffectMeta, PoolMeta, Pool, EventMeta, Event } from './types';
3
3
  /**
4
4
  * Information provided when a mutable atom is created.
5
5
  */
@@ -40,9 +40,22 @@ export interface EffectInfo {
40
40
  instance: Effect;
41
41
  }
42
42
  /**
43
- * Union type for atom/derived/effect creation info.
43
+ * Information provided when an event is created.
44
44
  */
45
- export type CreateInfo = MutableInfo | DerivedInfo | EffectInfo | PoolInfo;
45
+ export interface EventInfo {
46
+ /** Discriminator for events */
47
+ type: "event";
48
+ /** Optional key from event options (for debugging/devtools) */
49
+ key: string | undefined;
50
+ /** Optional metadata from event options */
51
+ meta: EventMeta | undefined;
52
+ /** The created event instance */
53
+ instance: Event<unknown>;
54
+ }
55
+ /**
56
+ * Union type for atom/derived/effect/event creation info.
57
+ */
58
+ export type CreateInfo = MutableInfo | DerivedInfo | EffectInfo | PoolInfo | EventInfo;
46
59
  /**
47
60
  * Information provided when a module (via define()) is created.
48
61
  */
@@ -109,6 +109,41 @@ export declare function isFulfilled<T>(value: T | PromiseLike<T>): boolean;
109
109
  * @returns true if value is a Promise in rejected state
110
110
  */
111
111
  export declare function isRejected<T>(value: T | PromiseLike<T>): boolean;
112
+ /**
113
+ * Creates a Promise that is immediately resolved AND pre-cached as fulfilled.
114
+ * This avoids the loading→resolved flicker when trackPromise() is called.
115
+ *
116
+ * Use this instead of `Promise.resolve(value)` when you need the promise
117
+ * to be recognized as already-resolved by the promise tracking system.
118
+ *
119
+ * @param value - The resolved value
120
+ * @returns A resolved Promise with pre-populated cache state
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const promise = createResolvedPromise(42);
125
+ * const state = trackPromise(promise);
126
+ * // state.status === 'fulfilled' immediately (no loading flicker)
127
+ * ```
128
+ */
129
+ export declare function createResolvedPromise<T>(value: T): Promise<T>;
130
+ /**
131
+ * Creates a Promise that is immediately rejected AND pre-cached as rejected.
132
+ * This avoids the loading→rejected flicker when trackPromise() is called.
133
+ *
134
+ * Note: Attaches a no-op catch to prevent unhandled rejection warnings.
135
+ *
136
+ * @param error - The rejection error
137
+ * @returns A rejected Promise with pre-populated cache state
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const promise = createRejectedPromise(new Error('Failed'));
142
+ * const state = trackPromise(promise);
143
+ * // state.status === 'rejected' immediately (no loading flicker)
144
+ * ```
145
+ */
146
+ export declare function createRejectedPromise<T = never>(error: unknown): Promise<T>;
112
147
  /**
113
148
  * Gets the resolved value of a Promise if fulfilled, otherwise undefined.
114
149
  *
@@ -103,7 +103,7 @@ export interface SelectContext extends Pipeable {
103
103
  * const [user, posts] = all([user$, posts$]);
104
104
  * ```
105
105
  */
106
- all<A extends Atom<unknown>[]>(atoms: A): {
106
+ all<const A extends readonly Atom<unknown>[]>(atoms: A): {
107
107
  [K in keyof A]: AtomValue<A[K]>;
108
108
  };
109
109
  /**
@@ -115,20 +115,24 @@ export interface SelectContext extends Pipeable {
115
115
  * - If all atoms are loading → throws first Promise
116
116
  *
117
117
  * The `key` in the result identifies which atom won the race.
118
+ * Returns a discriminated union - checking `key` narrows `value` type.
118
119
  *
119
120
  * Note: race() does NOT use fallback - it's meant for first "real" settled value.
120
121
  *
121
122
  * @param atoms - Record of atoms to race
122
- * @returns KeyedResult with winning key and value
123
+ * @returns KeyedResult discriminated union with winning key and value
123
124
  *
124
125
  * @example
125
126
  * ```ts
126
127
  * const result = race({ cache: cache$, api: api$ });
127
- * console.log(result.key); // "cache" or "api"
128
- * console.log(result.value); // The winning value
128
+ * if (result.key === "cache") {
129
+ * result.value; // narrowed to cache atom's type
130
+ * }
129
131
  * ```
130
132
  */
131
- race<T extends Record<string, Atom<unknown>>>(atoms: T): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
133
+ race<T extends Record<string, Atom<unknown>>>(atoms: T): KeyedResult<{
134
+ [K in keyof T & string]: AtomValue<T[K]>;
135
+ }>;
132
136
  /**
133
137
  * Return the first ready value (like Promise.any).
134
138
  * Object-based - pass atoms as a record with keys.
@@ -138,20 +142,24 @@ export interface SelectContext extends Pipeable {
138
142
  * - If any loading (not all errored) → throws Promise
139
143
  *
140
144
  * The `key` in the result identifies which atom resolved first.
145
+ * Returns a discriminated union - checking `key` narrows `value` type.
141
146
  *
142
147
  * Note: any() does NOT use fallback - it waits for a real ready value.
143
148
  *
144
149
  * @param atoms - Record of atoms to check
145
- * @returns KeyedResult with winning key and value
150
+ * @returns KeyedResult discriminated union with winning key and value
146
151
  *
147
152
  * @example
148
153
  * ```ts
149
154
  * const result = any({ primary: primaryApi$, fallback: fallbackApi$ });
150
- * console.log(result.key); // "primary" or "fallback"
151
- * console.log(result.value); // The winning value
155
+ * if (result.key === "primary") {
156
+ * result.value; // narrowed to primary atom's type
157
+ * }
152
158
  * ```
153
159
  */
154
- any<T extends Record<string, Atom<unknown>>>(atoms: T): KeyedResult<keyof T & string, AtomValue<T[keyof T]>>;
160
+ any<T extends Record<string, Atom<unknown>>>(atoms: T): KeyedResult<{
161
+ [K in keyof T & string]: AtomValue<T[K]>;
162
+ }>;
155
163
  /**
156
164
  * Get all atom statuses when all are settled (like Promise.allSettled).
157
165
  * Array-based - pass atoms as an array.
@@ -168,7 +176,7 @@ export interface SelectContext extends Pipeable {
168
176
  * const [userResult, postsResult] = settled([user$, posts$]);
169
177
  * ```
170
178
  */
171
- settled<A extends Atom<unknown>[]>(atoms: A): {
179
+ settled<const A extends readonly Atom<unknown>[]>(atoms: A): {
172
180
  [K in keyof A]: SettledResult<AtomValue<A[K]>>;
173
181
  };
174
182
  /**