UNPKG

@reatom/core

Version:

The ultimate state manager

1,269 lines (1,268 loc) 315 kB
/** * 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 *