@reatom/core
Version:
The ultimate state manager
1,269 lines (1,268 loc) • 315 kB
TypeScript
/**
* TypeScript type representation of the standard `setTimeout` function.
*
* This type is isolated in a separate file to avoid referencing the global
* `setTimeout` directly in the main bundle, which would automatically add a
* Node.js reference (`/// <reference types="node" />`) to the bundle.
*
* @remarks
* The separation is necessary because using `typeof setTimeout` in the same
* file where `setTimeout` is referenced as a variable in the module scope
* would cause TypeScript to add Node.js type references, potentially creating
* issues in non-Node environments.
* @example
* // How to use this type
* import { SetTimeout } from '@reatom/core'
*
* function setupTimer(customTimeout: SetTimeout) {
* return customTimeout(() => console.log('Timer fired'), 1000)
* }
*
* @public
* @see {@link https://github.com/reatom/reatom/issues/983} for the original issue and discussion
*/import { StandardSchemaV1 } from "@standard-schema/spec";
//#region src/setTimeout.d.ts
type SetTimeout = typeof setTimeout;
//#endregion
//#region src/utils.d.ts
/**
* Generic function type representing any function that takes any parameters and
* returns any value. Used throughout Reatom for typing function parameters and
* callbacks.
*/
interface Fn {
(...params: any[]): any;
}
/**
* Type alias for Record<string, T> for brevity. Represents an object with
* string keys and values of type T.
*
* @template T - The type of values in the record (defaults to any)
*/
type Rec<T = any> = Record<string, T>;
/**
* Function interface for unsubscribing from subscriptions. Used consistently
* throughout Reatom for cleanup functions.
*/
interface Unsubscribe {
(): void;
}
/**
* Type representing different possible return values from observable
* subscription methods. Supports both function-based unsubscribers and objects
* with unsubscribe methods.
*/
type MaybeUnsubscribe = void | Exclude<{}, Fn> | Unsubscribe | {
unsubscribe: Unsubscribe;
};
/**
* Utility type that converts properties with undefined values to optional
* properties. Makes properties with object or null values required, while
* making other properties optional.
*
* @template T - The object type to transform
*/
type UndefinedToOptional<T extends object> = Partial<T> & PickValues<T, {} | null>;
/**
* Union type of all JavaScript falsy values except for NaN. Includes: false, 0,
* empty string, null, and undefined.
*
* @see https://stackoverflow.com/a/51390763
*/
type Falsy = false | 0 | '' | null | undefined;
/**
* Removes named generics to produce a plain type representation. Preserves
* function signatures and object structure while eliminating generic parameter
* names.
*
* This is useful for presenting cleaner types in documentation and error
* messages.
*
* @template Intersection - The type to convert to a plain representation
*/
type Plain<Intersection> = Intersection extends ((...params: infer I) => infer O) ? ((...params: I) => O) & { [Key in keyof Intersection]: Intersection[Key] } : Intersection extends (new (...params: any[]) => any) ? Intersection : Intersection extends object ? { [Key in keyof Intersection]: Intersection[Key] } : Intersection;
/**
* Creates a shallow clone type of T. Useful for creating a new type that has
* the same shape but is a distinct type.
*
* @template T - The type to create a shallow clone of
*/
type Shallow<T> = { [K in keyof T]: T[K] } & {};
/**
* Represents a constructor function that can be instantiated with the new
* operator.
*
* @template ReturnType - The type of object that will be created when
* instantiated
*/
interface Newable<ReturnType> {
new (...params: any[]): ReturnType;
}
/**
* Extracts the union type of all values in an object type.
*
* @template T - The object type to extract values from
*/
type Values<T> = T[keyof T];
/**
* Extracts keys from type T where the corresponding value does not extend type
* V.
*
* @template T - The object type to extract keys from
* @template V - The value type to exclude
*/
type OmitValuesKeys<T, V> = Values<{ [K in keyof T]: T[K] extends V ? never : K }>;
/**
* Creates a type with all properties from T except those with values extending
* V.
*
* @template T - The object type to filter properties from
* @template V - The value type to exclude
*/
type OmitValues<T, V> = { [K in OmitValuesKeys<T, V>]: T[K] };
/**
* Extracts keys from type T where the corresponding value extends type V.
*
* @template T - The object type to extract keys from
* @template V - The value type to include
*/
type PickValuesKeys<T, V> = Values<{ [K in keyof T]: T[K] extends V ? K : never }>;
/**
* Creates a type with only properties from T with values extending V.
*
* @template T - The object type to filter properties from
* @template V - The value type to include
*/
type PickValues<T, V> = { [K in PickValuesKeys<T, V>]: T[K] };
/**
* Flattens a function type with up to 5 overloads into a single function
* signature. This creates a union of the parameter types and return types.
*
* Useful for generic type handling of overloaded functions.
*
* @template T - The overloaded function type to flatten
*/
type Overloads<T> = T extends {
(...params: infer Overload1Params): infer Return1;
(...params: infer Overload2Params): infer Return2;
(...params: infer Overload3Params): infer Return3;
(...params: infer Overload4Params): infer Return4;
(...params: infer Overload5Params): infer Return5;
} ? (...params: Overload1Params | Overload2Params | Overload3Params | Overload4Params | Overload5Params) => Return1 | Return2 | Return3 | Return4 | Return5 : never;
/**
* Extracts the parameters type from an overloaded function. Returns a union of
* all possible parameter tuples.
*
* @template T - The overloaded function type to extract parameters from
*/
type OverloadParameters<T> = Parameters<Overloads<T>>;
/**
* Asserts that a value is truthy, throwing an error if it's falsy. This is a
* TypeScript type assertion function that helps with type narrowing.
*
* @param value - The value to check
* @param message - The error message to use if the assertion fails
* @param ErrorConstructor - Optional custom error constructor to use (defaults
* to Error)
* @throws {Error} Throws an error with the provided message if value is falsy
*/
declare function assert(value: unknown, message: string, ErrorConstructor?: Newable<Error>): asserts value;
/**
* No-operation function that accepts any parameters and returns undefined.
* Useful as a default callback or for stubbing functionality.
*/
declare const noop: (...params: any[]) => any;
/**
* Identity function that returns the first argument unchanged. Can accept
* additional parameters but ignores them.
*
* @template T - The type of value being passed through
* @param value - The value to return
* @returns The same value that was passed in
*/
declare const identity: <T>(value: T, ...a: any[]) => T;
/**
* Creates a promise that resolves after the specified number of milliseconds.
* Useful for creating delays in async functions.
*
* @param ms - The number of milliseconds to sleep (defaults to 0)
* @returns A promise that resolves after the specified delay
*/
declare const sleep: (ms?: number) => Promise<unknown>;
/**
* Type guard that checks if a value is an object (non-null and typeof
* 'object'). Provides advanced type narrowing to either the original object
* type or a generic object type.
*
* @template T - The type of value being checked
* @param thing - The value to check
* @returns True if the value is a non-null object, false otherwise
*/
declare const isObject: <T>(thing: T) => thing is T extends Record<string | number | symbol, unknown> ? T : Record<string | number | symbol, unknown>;
/**
* Type guard that checks if a value is a plain object (a simple object literal
* or created with Object.create(null)). Verifies that the object either has no
* prototype or its prototype is Object.prototype.
*
* @param thing - The value to check
* @returns True if the value is a plain object, false otherwise
*/
declare const isRec: (thing: unknown) => thing is Record<string, unknown>;
/**
* Performs a shallow equality comparison between two values. Handles
* primitives, objects, dates, regular expressions, arrays, maps, and sets.
*
* For iterables, compares each item in sequence for equality. For objects,
* compares direct property values but not nested objects deeply.
*
* @param a - First value to compare
* @param b - Second value to compare
* @param is - Optional comparison function to use for individual values
* (defaults to Object.is)
* @returns True if the values are shallowly equal, false otherwise
*/
declare const isShallowEqual: (a: any, b: any, is?: (value1: any, value2: any) => boolean) => any;
/**
* Performs a deep equality comparison between two values. Recursively compares
* nested objects and arrays while properly handling cyclic references.
*
* Handles primitives, objects, dates, regular expressions, arrays, maps, and
* sets. Uses a WeakMap to track visited objects to avoid infinite recursion
* with circular references.
*
* @param a - First value to compare
* @param b - Second value to compare
* @returns True if the values are deeply equal, false otherwise
*/
declare const isDeepEqual: (a: any, b: any) => any;
/**
* Type utility for merging up to four types with proper type safety. Properties
* from later types override properties from earlier types. Preserves function
* signatures from T1 if it's a function type.
*
* @template T1 - First type to merge
* @template T2 - Second type to merge, overrides T1 properties
* @template T3 - Optional third type to merge, overrides T1 and T2 properties
* @template T4 - Optional fourth type to merge, overrides T1, T2, and T3
* properties
*/
type Assign<T1, T2, T3 = {}, T4 = {}> = Plain<(T1 extends ((...params: infer I) => infer O) ? (...params: I) => O : {}) & Omit<T1, keyof T2 | keyof T3 | keyof T4> & Omit<T2, keyof T3 | keyof T4> & Omit<T3, keyof T4> & T4>;
/**
* Type-safe version of Object.assign that properly handles type merging. Unlike
* standard Object.assign typing, properties with the same name are replaced
* rather than becoming a union type.
*
* @template T1 - Type of the target object
* @template T2 - Type of the first source object
* @template T3 - Type of the optional second source object
* @template T4 - Type of the optional third source object
* @returns A new object with merged properties
*/
declare const assign: {
<T1, T2>(a1: T1, a2: T2): Assign<T1, T2>;
<T1, T2, T3 = {}>(a1: T1, a2: T2, a3?: T3): Assign<T1, T2, T3>;
<T1, T2, T3 = {}, T4 = {}>(a1: T1, a2: T2, a3?: T3, a4?: T4): Assign<T1, T2, T3, T4>;
};
/**
* Creates a new object with merged properties from all provided objects.
* Similar to Object.assign but always creates a new object rather than mutating
* the first argument.
*
* @example
* // Creates a new object: { a: 1, b: 2, c: 3 }
* const obj = merge({ a: 1 }, { b: 2 }, { c: 3 })
*
* @returns A new object with all properties from the provided objects
*/
declare const merge: typeof assign;
/**
* Type-safe version of Object.keys that preserves the key type information.
* Returns an array of keys with the correct type for the object.
*
* @template T - The object type
* @param thing - The object to get keys from
* @returns An array of the object's keys with proper typing
*/
declare const keys: {
<T extends object>(thing: T): Array<keyof T>;
};
/**
* Type-safe version of Object.entries that preserves key and value type
* information. Returns an array of key-value pairs with correct types.
*
* @template T - The object type
* @param thing - The object to get entries from
* @returns An array of [key, value] pairs with proper typing
*/
declare const entries: {
<T extends object>(thing: T): Array<[keyof T, T[keyof T]]>;
};
/**
* Type-safe version of Object.fromEntries that preserves key and value type
* information. Creates an object from an iterable of key-value pairs.
*
* @template K - The key type
* @template V - The value type
* @param entries - An iterable of [key, value] pairs
* @returns An object with the specified keys and values
*/
declare const fromEntries: {
<K extends PropertyKey, V>(entries: Iterable<readonly [K, V]>): Record<K, V>;
};
/**
* Creates a new object with only the specified keys from the original object.
*
* @example
* const user = { id: 1, name: 'Alice', email: 'alice@example.com' }
* const userInfo = pick(user, ['name', 'email'])
* // Result: { name: 'Alice', email: 'alice@example.com' }
*
* @template T - The source object type
* @template K - The keys to pick from the object
* @param target - The source object
* @param keys - Array of keys to include in the result
* @returns A new object containing only the specified keys and their values
*/
declare const pick: <T, K extends keyof T>(target: T, keys: Array<K>) => Plain<Pick<T, K>>;
/**
* Creates a new object excluding the specified keys from the original object.
*
* @example
* const user = { id: 1, name: 'Alice', password: 'secret' }
* const safeUser = omit(user, ['password'])
* // Result: { id: 1, name: 'Alice' }
*
* @template T - The source object type
* @template K - The keys to omit from the object
* @param target - The source object
* @param keys - Array of keys to exclude from the result
* @returns A new object containing all keys except the specified ones
*/
declare const omit: <T, K extends keyof T>(target: T, keys: Array<K>) => Plain<Omit<T, K>>;
/**
* Creates a deep clone of a value using JSON serialization/deserialization.
* This is a type-safe shortcut to `JSON.parse(JSON.stringify(value))`.
*
* Note: This has limitations with circular references, functions, symbols, and
* special objects like Date (converts to string). Consider using the native
* structuredClone when available.
*
* @template T - The type of value being cloned
* @param value - The value to clone
* @returns A deep clone of the input value
* @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
*/
declare const jsonClone: <T>(value: T) => T;
declare let _random: (min?: number, max?: number) => number;
/**
* Generates a random integer between min and max (inclusive).
*
* @param min - The minimum integer value (defaults to 0)
* @param max - The maximum integer value (defaults to Number.MAX_SAFE_INTEGER -
* 1)
* @returns A random integer between min and max
*/
declare const random: typeof _random;
/**
* Replaces the default random number generator with a custom implementation.
* Useful for testing to provide deterministic "random" values.
*
* @example
* // Set up deterministic random values for testing
* const restore = mockRandom(() => 42)
* console.log(random()) // Always returns 42
* restore() // Back to normal random behavior
*
* @param fn - The custom random function to use
* @returns A restore function that reverts to the original random
* implementation when called
*/
declare const mockRandom: (fn: typeof random) => () => void;
/**
* Asserts that a value is not null or undefined. Throws a TypeError if the
* value is null or undefined. Also serves as a type guard to narrow the type to
* non-nullable.
*
* @example
* const name = nonNullable(user.name) // TypeScript knows name is not null or undefined
*
* @template T - The type of value to check
* @param value - The value to check
* @param message - Optional custom error message
* @returns The input value if it's not null or undefined
* @throws {TypeError} If the value is null or undefined
*/
declare const nonNullable: <T>(value: T, message?: string) => NonNullable<T>;
/**
* Converts any JavaScript value to a stable string representation. Handles
* complex data structures and edge cases that JSON.stringify cannot manage.
*
* Provides special handling for:
*
* - Circular references
* - Maps and Sets
* - Symbols
* - Functions
* - Custom class instances
* - Regular objects (with sorted keys for stability)
*
* @example
* // Handles circular references
* const obj = { name: 'test' }
* obj.self = obj
* const key = toStringKey(obj) // No infinite recursion!
*
* // Stable representation of objects (key order doesn't matter)
* toStringKey({ a: 1, b: 2 }) === toStringKey({ b: 2, a: 1 }) // true
*
* @param thing - The value to convert to a string
* @param immutable - Whether to memoize results for complex objects (defaults
* to true)
* @returns A string representation of the value
*/
declare const toStringKey: (thing: any, immutable?: boolean) => string;
/**
* Interface extending DOMException for abort-specific error handling. Used to
* represent errors triggered by AbortController signal aborts.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
interface AbortError extends DOMException {
name: 'AbortError';
}
/**
* Converts any value to an AbortError. If the value is already an AbortError,
* it will be returned as is. Otherwise, creates a new AbortError with
* appropriate information.
*
* Handles different environments by using DOMException when available or
* falling back to regular Error with name set to 'AbortError'.
*
* @param reason - The value to convert to an AbortError
* @returns An AbortError instance
*/
declare const toAbortError: (reason: any) => AbortError;
/**
* Checks if an AbortController is aborted and throws an AbortError if it is.
* Useful for quick abort checks at the beginning of async operations.
*
* @param controller - The AbortController to check (can be undefined, null or
* void)
* @throws {AbortError} If the controller's signal is aborted
*/
declare const throwIfAborted: (controller?: void | AbortController | null | undefined) => void;
/**
* Type guard that checks if a value is an AbortError.
*
* @param thing - The value to check
* @returns True if the value is an AbortError, false otherwise
*/
declare const isAbort: (thing: any) => thing is AbortError;
/**
* Creates and throws an AbortError with the provided message. Optionally aborts
* the provided controller with the same error.
*
* @param message - The error message
* @param controller - Optional AbortController to abort
* @throws {AbortError} Always throws the created AbortError
*/
declare const throwAbort: (message?: string, controller?: AbortController) => never;
/**
* Enhanced version of the global setTimeout function. Ensures consistent
* behavior across different environments by handling both numeric and object
* timeout IDs. Adds a toJSON method to object timeout IDs for serialization.
*
* @param handler - The function to call after the timeout
* @param timeout - The time in milliseconds to wait before calling the handler
* @param args - Optional arguments to pass to the handler function
* @returns A timeout ID that can be used with clearTimeout
*/
declare const setTimeout$1: SetTimeout;
/**
* Maximum safe integer value for setTimeout delay. Any timeout value larger
* than this may cause overflow issues in some browsers.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
*/
declare const MAX_SAFE_TIMEOUT: number;
/**
* Represents a constructor function that can be instantiated with the new
* operator.
*
* @template T - The type of object that will be created when instantiated
*/
type Constructor<T> = new (...args: any[]) => T;
/**
* Detects whether the code is running in a browser environment. Checks for the
* existence of window and document objects.
*
* @returns True if running in a browser environment, false otherwise
*/
declare const isBrowser: () => boolean;
/**
* Creates a Promise and returns it along with its resolve and reject functions.
* This utility is similar to the upcoming `Promise.withResolvers()` static
* method. It allows for manual control over a Promise's settlement from outside
* its constructor.
*
* @example
* const { promise, resolve, reject } = withResolvers<string>()
*
* promise
* .then((value) => console.log('Resolved:', value))
* .catch((error) => console.error('Rejected:', error))
*
* // Sometime later, or in a different part of the code:
* if (Math.random() > 0.5) {
* resolve('Success!')
* } else {
* reject(new Error('Failed!'))
* }
*
* @template T - The type of the value the promise will resolve with.
* @property {Promise<T>} promise - The created Promise.
* @property {(value: T) => void} resolve - A function to resolve the promise
* with a value of type T.
* @property {(reason?: any) => void} reject - A function to reject the promise
* with an optional reason.
* @returns An object containing the `promise`, and its `resolve` and `reject`
* functions.
*/
declare const withResolvers: <T>() => {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (reason?: any) => void;
};
/**
* Removes the first occurrence of an item from an array in a single iteration.
* Mutates the array in place by splicing out the found element.
*
* More efficient than using `indexOf` + `splice` as it only walks the array
* once.
*
* @example
* const items = [1, 2, 3, 4]
* removeItem(items, 3) // returns true, items is now [1, 2, 4]
* removeItem(items, 5) // returns false, items unchanged
*
* @template T - The type of elements in the array
* @param array - The array to remove the item from
* @param item - The item to remove (compared using strict equality)
* @returns True if the item was found and removed, false otherwise
*/
declare const removeItem: <T>(array: T[], item: T) => boolean;
//#endregion
//#region src/core/action.d.ts
interface ActionCall<Params extends any[] = any[], Payload = any> {
params: Params;
payload: Payload;
}
/** Autoclearable array of processed events */
interface ActionState<Params extends any[] = any[], Payload = any> extends Array<ActionCall<Params, Payload>> {}
/** Logic container with atom features */
interface Action<Params extends any[] = any[], Payload = any> extends AtomLike<ActionState<Params, Payload>, Params, Payload> {
subscribe: (cb?: (payload: Payload, params: Params) => any) => Unsubscribe;
}
/** Action type that supports all overloads of the original function */
type GAction<T extends Fn> = T & Action<Parameters<T>, ReturnType<T>>;
/**
* Type guard to check if a value is a Reatom action.
*
* This function determines whether the given value is an action by checking if
* it's an atom with non-reactive behavior (actions are non-reactive atoms).
*
* @param target - The value to check
* @returns `true` if the value is a Reatom action, `false` otherwise
*/
declare let isAction: (target: unknown) => target is Action;
/**
* Creates an extension that adds middleware to an action, wrapping only the
* call function, not the state.
*
* Unlike `withMiddleware`, which wraps the entire middleware chain (including
* the state update), `withActionMiddleware` wraps only the call function that
* gets passed to `actionMiddleware`. This allows you to decorate the action's
* execution logic without interfering with the action's state management.
*
* @example
* // Creating a retry middleware for actions
* const withRetry = (maxAttempts: number) =>
* withActionMiddleware((target) => {
* return (next, ...params) => {
* let attempts = 0
* while (attempts < maxAttempts) {
* try {
* return next(...params)
* } catch (error) {
* attempts++
* if (attempts >= maxAttempts) throw error
* }
* }
* }
* })
*
* // Using the middleware
* const fetchData = action(async () => {
* const response = await fetch('/api/data')
* return response.json()
* }).extend(withRetry(3))
*
* @template Target - The type of action the middleware will be applied to
* @param cb - A function that receives the target action and returns a
* middleware function that wraps the call
* @returns An extension that applies the middleware when used with .extend()
*/
declare let withActionMiddleware: {
<Params extends any[] = any[], Payload = any>(cb: (target: Action<Params, Payload>) => (next: Fn, ...params: Params) => Payload): Ext<Action<Params, Payload>>;
};
/**
* Creates a logic and side effect container.
*
* Actions are used to encapsulate complex logic, perform side effects (like API
* calls), and orchestrate multiple state updates. Unlike atoms, actions are
* meant to be called with parameters and can return values.
*
* Actions also have atom-like features (subscribe, extend) and track their call
* history.
*
* @example
* // Create an action that fetches data and updates state
* const fetchUserData = action(async (userId: string) => {
* const response = await wrap(fetch(`/api/users/${userId}`))
* const data = await wrap(response.json())
*
* // Update state atoms with the fetched data
* userName(data.name)
* userEmail(data.email)
*
* return data // Actions can return values
* }, 'fetchUserData')
*
* // Call the action
* fetchUserData('user123')
*
* @template Params - The parameter types the action accepts
* @template Payload - The return type of the action
* @param cb - The function containing the action's logic
* @param name - Optional name for debugging purposes
* @returns An action instance that can be called with the specified parameters
*/
declare function action<T extends (...a1: never[]) => any>(cb: T, name?: string): GAction<T>;
declare function action<Params extends any[] = any[], Payload = any>(cb: (...params: Params) => Payload, name?: string): Action<Params, Payload>;
//#endregion
//#region src/core/actions.d.ts
/**
* Type representing a set of methods converted to Reatom actions.
*
* This type maps each method in the original record to a corresponding Reatom
* action with the same parameter and return types.
*
* @template Methods - Record of functions to be converted to actions
*/
type ActionsExt<Methods extends Rec<Fn>> = { [K in keyof Methods]: Methods[K] extends ((...params: infer Params) => infer Payload) ? Action<Params, Payload> : never };
/**
* Extension that binds actions to an atom or action as methods.
*
* This extension adds methods to an atom or action by converting them to Reatom
* actions. Each method is converted to an action with the same name and bound
* to the target. The name of each action will be prefixed with the target's
* name for better debugging.
*
* @example
* const counter = atom(0, 'counter').extend(
* withActions({
* increment: (amount = 1) => counter((prev) => prev + amount),
* decrement: (amount = 1) => counter((prev) => prev - amount),
* reset: () => counter(0),
* }),
* )
*
* counter.increment(5) // Can now call these methods directly
* counter.reset()
*
* @example
* const counter = atom(0, 'counter').extend(
* withActions((target) => ({
* increment: (amount = 1) => target.set((prev) => prev + amount),
* decrement: (amount = 1) => target.set((prev) => prev - amount),
* reset: () => target.set(0),
* })),
* )
*
* @template Target - The atom or action being extended
* @template Methods - Record of functions to convert to actions
* @param options - Either a record of methods or a function that creates
* methods given the target
* @returns An extension that adds the methods as actions
* @throws {ReatomError} If a method name collides with an existing property on
* the target
*/
declare function withActions<Target extends AtomLike, Methods extends Rec<Fn>>(options: Methods | ((target: Target) => Methods)): (target: Target) => ActionsExt<Methods>;
//#endregion
//#region src/extensions/withAbort.d.ts
interface AbortExt {
abort: Action<[reason?: any]>;
}
/**
* Extension to add abort handling to actions and computed atoms.
*
* @example
* // last-in-win: only the last request matters
* const fetchUser = action(async (id: number) => {
* const response = await wrap(fetch(`/api/user/${id}`))
* return response.json()
* }).extend(withAbort())
*
* fetchUser(1) // will be aborted
* fetchUser(2) // will be aborted
* fetchUser(3) // this one wins
*
* @example
* // first-in-win: ignore subsequent calls until the first completes
* const fetchOnce = action(async () => {
* await wrap(fetch('/api/data'))
* }).extend(withAbort('first-in-win'))
*
* fetchOnce() // runs
* fetchOnce() // ignored, returns previous promise
* fetchOnce() // ignored, returns previous promise
*
* @example
* // manual with manual abort (useful for polling/long-running tasks)
* const poll = action(async () => {
* while (true) {
* await wrap(sleep(1000))
* doSome()
* }
* }).extend(withAbort('manual'))
*
* // start
* poll()
*
* // stop
* poll.abort()
*
* @param strategy - The abort strategy to use:
*
* - `'last-in-win'` (default): Aborts previous concurrent calls when a new one
* starts
* - `'first-in-win'`: Ignores new calls while a previous one is still running
* - `'manual'`: No automatic abort, just adds the `abort` action for manual
* control
*/
declare let withAbort: (strategy?: "last-in-win" | "first-in-win" | "finally" | "manual") => AssignerExt<AbortExt>;
//#endregion
//#region src/extensions/withChangeHook.d.ts
/**
* Executes a callback whenever the target atom's state changes.
*
* This extension is essential for creating stable, declarative connections
* between independent modules or features. The hook fires in the "Hooks" phase
* of Reatom's lifecycle (after Updates, before Computations), making it perfect
* for triggering side effects or synchronizing state across module boundaries.
*
* **When to use:**
*
* - Creating stable connections between features that shouldn't depend on each
* other directly
* - Triggering validation when a field's value or state changes
* - Syncing derived state in response to source state changes
* - Managing side effects like DOM updates or analytics based on state changes
* - Coordinating behavior across module boundaries without coupling them
*
* **When NOT to use:**
*
* - In dynamic features, like from computed factories (use `take` or `effect` and
* `ifChanged` instead)
* - When a regular computed dependency would suffice
* - For connection/disconnection events (use `withConnectHook` instead)
*
* The callback receives the new state and previous state. It only fires when
* the state actually changes (referential inequality check via `Object.is`).
* The callback executes within the same reactive context, so you can safely
* call other atoms and actions.
*
* @example
* // Basic usage: React to atom state changes
* const theme = reatomEnum(['light', 'dark', 'system']).extend(
* withChangeHook((state, prevState) => {
* document.body.classList.remove(prevState)
* document.body.classList.add(state)
* }),
* )
*
* @example
* // Stable feature connection: Analytics tracking
* // In userModule.ts
* export const userAtom = atom({ id: null, name: '' }, 'user')
*
* // In analyticsModule.ts
* import { userAtom } from './userModule'
* userAtom.extend(
* withChangeHook((user, prevUser) => {
* if (user.id !== prevUser?.id) {
* analytics.identify(user.id, { name: user.name })
* }
* }),
* )
*
* @template Target - The atom type being extended
* @param cb - Callback fired when state changes. Receives:
*
* - `state` - The new state value
* - `prevState` - The previous state value (undefined on first change)
*
* @returns Extension function to be used with `.extend()`
* @throws {ReatomError} If callback is not a function
* @see {@link addChangeHook} For dynamically adding/removing change hooks
* @see {@link withCallHook} For reacting to action calls instead of state changes
* @see {@link withConnectHook} For reacting to connection lifecycle events
*/
declare let withChangeHook: <Target extends AtomLike>(cb: (state: AtomState<Target>, prevState: undefined | AtomState<Target>) => void) => Ext<Target>;
/**
* Dynamically adds a change hook to an existing atom and returns a function to
* remove it.
*
* Unlike `withChangeHook` which is applied at atom definition time,
* `addChangeHook` allows you to add and remove hooks at runtime. This is useful
* for temporary subscriptions or when you need conditional hook behavior that
* can be enabled/disabled dynamically.
*
* This feature is rarely needed, you should prefer using `effect` with
* `ifChanged` or `take` instead.
*
* @template T - The atom type
* @param target - The atom to attach the hook to
* @param cb - Callback fired when state changes
* @returns Unsubscribe function to remove this specific hook
* @see {@link withChangeHook} For adding hooks at atom definition time
*/
declare let addChangeHook: <T extends AtomLike>(target: T, cb: (state: AtomState<T>, prevState?: AtomState<T>) => void) => Unsubscribe;
/**
* Executes a callback whenever the target action is called.
*
* This extension enables you to react to action invocations, making it
* invaluable for creating stable connections between independent features. The
* hook fires in the "Hooks" phase (after Updates, before Computations) and
* receives both the action's return value and its parameters.
*
* **When to use:**
*
* - Creating stable cross-module connections that react to specific actions
* - Tracking action calls for analytics, logging, or debugging
* - Triggering side effects in response to action completions
* - Coordinating behavior between independent features without coupling them
* - Implementing event-driven communication patterns
*
* **When NOT to use:**
*
* - In dynamic features, like from computed factories (`take` or `effect` and
* `getCalls` instead)
* - When you can achieve the same goal with direct action composition
*
* For actions extended with `withAsync`, you can also hook into `.onFulfill`,
* `.onReject`, or `.onSettle` to react to async completion events.
*
* @example
* // Cross-module coordination: Analytics tracking
* // In checkoutModule.ts
* export const submitOrder = action(async (order) => {
* const result = await api.submitOrder(order)
* return result
* }, 'submitOrder')
*
* // In analyticsModule.ts
* import { submitOrder } from './checkoutModule'
* submitOrder.extend(
* withCallHook((promise, params) => {
* const [order] = params
* analytics.track('new_order', {
* orderId: order.id,
* total: order.total,
* })
* }),
* )
*
* @example
* // Stable feature connection: Form submission tracking
* const fetch = action(async (param: number) => {
* const data = await api.fetch(param)
* return data
* }, 'fetch').extend(withAsync())
*
* fetch.onFulfill.extend(
* withCallHook((call) => {
* console.log('Fetch completed', call.payload)
* }),
* )
*
* @template Target - The action type being extended
* @param cb - Callback fired when action is called. Receives:
*
* - `payload` - The return value of the action
* - `params` - The parameters passed to the action as an array
*
* @returns Extension function to be used with `.extend()`
* @throws {ReatomError} If callback is not a function
* @throws {ReatomError} If applied to an atom instead of an action
* @see {@link addCallHook} For dynamically adding/removing call hooks
* @see {@link withChangeHook} For reacting to atom state changes instead
* @see {@link withAsync} For async action lifecycle hooks (onFulfill, onReject, onSettle)
*/
declare let withCallHook: <Target extends Action>(cb: (payload: ReturnType<Target>, params: OverloadParameters<Target>) => void) => Ext<Target>;
/**
* Dynamically adds a call hook to an existing action and returns a function to
* remove it.
*
* Unlike `withCallHook` which is applied at action definition time,
* `addCallHook` allows you to add and remove hooks at runtime. This is useful
* for temporary subscriptions, conditional hook behavior, or when integrating
* with external systems that need to be connected and disconnected
* dynamically.
*
* This feature is rarely needed, you should prefer using `effect` with
* `getCalls` or `take` instead.
*
* @template Target - The action type
* @param target - The action to attach the hook to
* @param cb - Callback fired when the action is called
* @returns Unsubscribe function to remove this specific hook
* @see {@link withCallHook} For adding hooks at action definition time
*/
declare let addCallHook: <Target extends Action>(target: Target, cb: (payload: ReturnType<Target>, params: OverloadParameters<Target>) => void) => Unsubscribe;
//#endregion
//#region src/extensions/withComputed.d.ts
/**
* A middleware extension that enhances an atom with computed capabilities.
*
* @template Target - The target atom or action type to be extended with
* computed functionality.
* @param {function} computed - A function that computes the new state based on
* the current state.
* @param {Object} [options={}] - Configuration options. Default is `{}`
* @param {boolean} [options.tail=true] - Determines the order of the passed
* computed calling. ATTENTION: use `false` only for computed with fixed size
* of dependencies. Default is `true`
* @returns {Ext<Target>} The extended atom or action with computed
* functionality.
*/
declare let withComputed: <Target extends AtomLike>(computed: (state: AtomState<Target>) => AtomState<Target>, {
tail
}?: {
tail?: boolean;
}) => Ext<Target>;
//#endregion
//#region src/extensions/withConnectHook.d.ts
declare let withConnectHook: <Target extends AtomLike>(cb: (target: Target) => MaybeUnsubscribe) => Ext<Target>;
declare let withDisconnectHook: <Target extends AtomLike>(cb: (target: Target) => void) => Ext<Target>;
//#endregion
//#region src/extensions/withDynamicSubscription.d.ts
/**
* This interface improve `.subscribe` method behavior by relying it on
* `abortVar`. It performs unsubscribe automatically, when abort will occur.
*/
interface DynamicSubscriptionExt {}
declare let withDynamicSubscription: <Target extends AtomLike>() => (target: Target) => Target & DynamicSubscriptionExt;
//#endregion
//#region src/extensions/withInit.d.ts
/**
* Checks if the current execution context is within the initialization of the
* current atom.
*
* @example
* const search = atom('', 'search').extend(withSearchParams('search'))
* const page = atom(1, 'page').extend(
* withSearchParams('page'),
* withComputed((state) => {
* search() // subscribe to the search changes
* // do NOT drop the persisted state on init
* return isInit() ? state : 1
* }),
* )
*
* @returns {boolean} True if currently in the initialization phase, false
* otherwise
*/
declare let isInit: () => boolean;
/**
* Define dynamically computed initial value for an atom.
*
* Typically, you can use just an init callback in `atom` first argument:
* `atom(() => new Date())`. But if you need to add initial callback after the
* atom creation, so there this extensions is useful.
*
* @example
* const something = reatomSomething().extend(
* withInit((initState) => ({ ...initState, ...additions })),
* )
*
* @example
* const myData = atom(null, 'myData')
* if (meta.env.TEST) {
* myData.extend(withInit(mockData))
* }
*
* @template Target - The atom type that extends AtomLike
* @param {AtomState<Target>
* | ((state: AtomState<Target>) => AtomState<Target>)} init
* The initial value or a function that returns the initial value based on
* current state
* @returns {Ext<Target>} An extension that can be applied to an atom
*/
declare let withInit: <Target extends AtomLike>(init: AtomState<Target> | ((state: AtomState<Target>, ...params: any[]) => AtomState<Target>)) => Ext<Target>;
/**
* Extension that runs the passed hook when the atom is initialized.
*
* @example
* const userAtom = atom({ id: 1, name: 'John' }).extend(
* withInitHook((initState) => {
* // Perform any setup logic here
* analytics.track('user_loaded', initState)
* }),
* )
*
* @template Target - The atom type that extends AtomLike
* @param {(initState: AtomState<Target>) => any} hook A function to be called
* with the initial state during initialization
* @returns {Ext<Target>} An extension that can be applied to an atom
*/
declare let withInitHook: <Target extends AtomLike>(hook: (initState: AtomState<Target>) => any, queue?: QueueKind) => Ext<Target>;
//#endregion
//#region src/extensions/withMemo.d.ts
declare let withMemo: <Target extends AtomLike>(isEqual?: (prevState: AtomState<Target>, nextState: AtomState<Target>) => boolean) => Ext<Target>;
//#endregion
//#region src/extensions/withSuspense.d.ts
/**
* Internal suspense cache record tracking promise state. Do not use it
* directly, only for libraries!
*/
interface SuspenseRecord {
kind: 'pending' | 'fulfilled' | 'rejected';
value: any;
}
/**
* Internal suspense cache mapping promises to their settlement state. Do not
* use it directly, only for libraries!
*/
declare let SUSPENSE: WeakMap<Promise<any>, SuspenseRecord>;
/**
* Checks if a promise is settled and returns its value or fallback. If the
* promise is fulfilled, returns the resolved value. If the promise is rejected,
* throws the error. If the promise is pending, returns the fallback value
* (defaults to undefined).
*
* Uses an internal WeakMap cache to track promise states across calls.
*
* @example
* const promise = Promise.resolve(42)
* await promise
* const value = settled(promise) // 42
*
* @param promise - The promise or synchronous value to check
* @param fallback - The value to return if the promise is still pending
* @returns The resolved value if fulfilled, throws if rejected, or fallback if
* pending
*/
declare let settled: <Result, Fallback = undefined>(promise: Result | Promise<Result>, fallback?: Fallback) => Result | Fallback;
/**
* Extension type that adds a `suspended` computed atom to track resolved values
* from async atoms.
*/
type SuspenseExt<State> = {
suspended: Computed<Awaited<State>>;
};
/**
* Extension that adds suspense support to async atoms. Creates a `suspended`
* computed atom that tracks the resolved value of promises and throws the
* promise when pending (for React Suspense compatibility).
*
* The `suspended` atom will:
*
* - Return the resolved value immediately if the promise is already fulfilled
* - Throw the promise if it's still pending (allowing Suspense boundaries to
* catch it)
* - Propagate errors if the promise is rejected
* - Automatically update when the promise resolves
*
* @example
* const data = computed(async () => {
* const response = await fetch('/api/data')
* return response.json()
* }, 'data').extend(withSuspense())
*
* // Subscribe to resolved values
* subscribe(data.suspended, (value) => {
* console.log('Resolved:', value)
* })
*
* // Use in React component with Suspense
* function Component() {
* const value = useAtom(data.suspended) // throws promise if pending
* return <div>{value}</div>
* }
*
* @param options - Configuration options
* @param options.preserve - If true, preserves the previous state when
* suspending instead of throwing immediately. Useful for preventing
* flickering in UI.
* @returns An extension that adds the `suspended` computed atom
*/
declare let withSuspense: <Target extends AtomLike & Partial<SuspenseExt<AtomState<Target>>>>({
preserve
}?: {
preserve?: boolean;
}) => Ext<Target, SuspenseExt<AtomState<Target>>>;
/**
* Helper function to access the suspended value of an atom. Automatically
* applies `withSuspense()` extension if the atom doesn't already have it.
*
* This function:
*
* - Returns the resolved value if the promise is fulfilled
* - Throws the promise if it's still pending (for Suspense boundaries)
* - Throws the error if the promise is rejected
*
* @remarks
* If `withSuspense` is already applied with different `preserve` options, the
* behavior may be inconsistent. Consider applying `withSuspense()` explicitly
* to control options.
* @example
* const data = computed(async () => {
* const response = await fetch('/api/data')
* return response.json()
* }, 'data')
*
* // Automatically applies withSuspense() and returns suspended value
* const result = computed(() => {
* try {
* return suspense(data) // throws promise if pending
* } catch (promise) {
* if (promise instanceof Promise) {
* // Handle pending state
* return undefined
* }
* throw promise // Re-throw errors
* }
* }, 'result')
*
* @param target - The atom to get the suspended value from
* @returns The resolved value (Awaited<State>), or throws a promise/error
*/
declare let suspense: <State>(target: AtomLike<State>) => Awaited<State>;
/**
* Extension that enables asynchronous initialization for synchronous atoms.
* This feature bridges async data loading with sync atom semantics.
*
* During initialization, if the result is a Promise, it throws the promise
* (suspense pattern) and schedules setting the atom's state when resolved.
* After initialization completes, the atom operates fully synchronously.
*
* This is perfect for local-first architectures: load data asynchronously on
* init, then work with it synchronously. Combine with `withChangeHook` to sync
* changes back to a server or database.
*
* **Without callback**: Transforms `Atom<Promise<State>>` into `Atom<State>`.
* The atom's async initializer is unwrapped, and consumers receive the resolved
* value.
*
* @example
* const userSettings = atom(async () => {
* const response = await fetch('/api/settings')
* return response.json()
* }).extend(withSuspenseInit())
* // Type: Atom<Settings> (not Atom<Promise<Settings>>)
*
* effect(() => {
* // After init completes, reads are synchronous
* const settings = userSettings()
* console.log(settings.theme)
* })
*
* @example
* // With callback: Provides an async initializer for any atom type, keeping the original state type.
* // Local-first pattern: async init + sync operations + sync-back
* const todos = atom<Todo[]>([]).extend(
* withSuspenseInit(async () => {
* const cached = await indexedDB.get('todos')
* return cached ?? []
* }),
* withChangeHook((newState) => {
* // Sync changes back to storage
* indexedDB.set('todos', newState)
* }),
* )
*
* @example
* // Typed async init with custom default
* const profile = atom<{ username: string; age: number }>(
* throwAbort,
* ).extend(
* withSuspenseInit(async () => {
* const data = await fetchProfile()
* return data ?? { username: 'guest', age: 0 }
* }),
* )
*
* @overload
* @overload
* @param cb - Async or sync initializer function. Receives the current init
* state and returns the new state (or Promise of it).
* @returns Extension that unwraps `Atom<Promise<State>>` to `Atom<State>`
* @returns Extension that initializes the atom with the callback result
*/
declare let withSuspenseInit: {
<State>(): Ext<Atom<Promise<State>>, Atom<State>>;
<Target extends AtomLike>(cb: (state?: AtomState<Target>) => AtomState<Target> | Promise<AtomState<Target>>): Ext<Target>;
};
//#endregion
//#region src/extensions/withSuspenseRetry.d.ts
/**
* Creates a mixin that retries an async action when it fails coz of a
* suspension
*
* This mixin wraps an async action to automatically retry it when a Promise is
* thrown, which indicates a suspension. It will keep retrying until the action
* completes successfully or throws a non-Promise error.
*
* ⚠️ Be careful with non-idempotent operations inside the action body, as they
* may be executed multiple times during retries. It's recommended to carefully
* plan the execution logic to handle potential retries safely.
*
* @example
* const fetchUserBooks = action(async () => {
* const id = user().id // `user` is a suspended atom
* const response = await fetch(`/api/users/${id}/books`)
* return response.json()
* }).extend(withSuspenseRetry())
*
* @returns The same passed action
*/
declare let withSuspenseRetry: <T extends Action<unknown[], Promise<unknown>>>() => Ext<T>;
//#endregion
//#region src/methods/variable.d.ts
type NonUndefined = NonNullable<unknown> | null;
interface AsyncVariableOptions<T extends NonUndefined, Params extends any[] = any[]> {
name?: string;
defaultValue?: T;
create?: (...params: Params) => T;
}
/**
* Interface for context variables in Reatom
*
* Variables maintain values within the context of a computation tree, allowing
* for context-aware state similar to React's Context API but with more granular
* control and integration with Reatom's reactive system.
*
* @template T - Type of the stored value
* @see {@link https://github.com/tc39/proposal-async-context?tab=readme-ov-file#asynccontextvariable}
*/
declare class Variable<T extends NonUndefined, Params extends any[] = any[]> {
protected _findReactiveStartIndex: number;
protected create: (...params: Params) => T;
readonly name: `var#${string}`;
constructor(options?: AsyncVariableOptions<T, Params>);
/**
* Gets the frame value of the variable. Traverse the whole stack to find it.
*
* @param {Frame} [frame] - Optional frame to check (defaults to current top
*