UNPKG

@reatom/core

Version:

The ultimate state manager

1,435 lines 284 kB
//#region src/core/action.ts function actionMiddleware(next, ...params) { let frame = STACK[STACK.length - 1]; frame.pubs = [STACK[STACK.length - 2]]; _enqueue(() => frame.state = [], "cleanup"); return frame.state = [...frame.state, { params, payload: next(...params) }]; } /** * 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 */ let isAction = (target) => isAtom(target) && !target.__reatom.reactive; /** * 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() */ let withActionMiddleware = (cb) => (target) => { const actionMiddlewareIdx = target.__reatom.middlewares.indexOf(actionMiddleware); if (!isAction(target) || actionMiddlewareIdx === -1) throw new ReatomError("withActionMiddleware can only be applied to actions"); target.__reatom.middlewares.splice(actionMiddlewareIdx, 0, cb(target)); _recompile(target); return target; }; function action(cb, name = named("action", cb.name)) { let target = createAtom({ initState: [], computed: cb, middlewares: [ cb, actionMiddleware, cacheMiddleware ] }, name); Object.assign(target.__reatom, { reactive: false }); if (EXTENSIONS.length !== 0) target.extend(...EXTENSIONS); return target; } //#endregion //#region src/core/actions.ts /** * 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 */ function withActions(options) { return (target) => { let methods = typeof options === "function" ? options(target) : options; let result = {}; for (let key in methods) result[key] = action(methods[key], `${target.name}.${key}`); return result; }; } //#endregion //#region src/utils.ts /** * 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 */ function assert(value, message, ErrorConstructor = Error) { if (!value) throw new ErrorConstructor(message); } /** * No-operation function that accepts any parameters and returns undefined. * Useful as a default callback or for stubbing functionality. */ const noop = () => {}; /** * 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 */ const identity = (value, ...a) => value; /** * 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 */ const sleep = (ms = 0) => new Promise((r) => setTimeout$1(r, ms)); /** * 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 */ const isObject = (thing) => typeof thing === "object" && thing !== null; /** * 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 */ const isRec = (thing) => { if (!isObject(thing)) return false; const proto = Reflect.getPrototypeOf(thing); return !proto || !Reflect.getPrototypeOf(proto); }; /** * 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 */ const isShallowEqual = (a, b, is = Object.is) => { if (Object.is(a, b)) return true; if (!isObject(a) || !isObject(b) || a.__proto__ !== b.__proto__ || a instanceof Error) return false; if (Symbol.iterator in a) { let equal = a instanceof Map ? (a, b) => is(a[0], b[0]) && is(a[1], b[1]) : is; let aIter = a[Symbol.iterator](); let bIter = b[Symbol.iterator](); while (true) { let aNext = aIter.next(); let bNext = bIter.next(); if (aNext.done || bNext.done || !equal(aNext.value, bNext.value)) return aNext.done && bNext.done; } } if (a instanceof Date) return a.getTime() === b.getTime(); if (a instanceof RegExp) return String(a) === String(b); for (let k in a) if (k in b === false || !is(a[k], b[k])) return false; return Object.keys(a).length === Object.keys(b).length; }; /** * 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 */ const isDeepEqual = (a, b) => { const visited = /* @__PURE__ */ new WeakMap(); const is = (a, b) => { if (isObject(a)) { if (visited.has(a)) return visited.get(a) === b; visited.set(a, b); } return isShallowEqual(a, b, is); }; return isShallowEqual(a, b, is); }; /** * 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 */ const assign = Object.assign; /** * 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 */ const merge = (...params) => Object.assign({}, ...params); /** * 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 */ const keys = Object.keys; /** * 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 */ const entries = Object.entries; /** * 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 */ const fromEntries = Object.fromEntries; /** * 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 */ const pick = (target, keys) => { const result = {}; for (const key of keys) result[key] = target[key]; return result; }; /** * 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 */ const omit = (target, keys) => { const result = {}; for (const key in target) if (!keys.includes(key)) result[key] = target[key]; return result; }; /** * 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 */ const jsonClone = (value) => JSON.parse(JSON.stringify(value)); let _random = (min = 0, max = Number.MAX_SAFE_INTEGER - 1) => Math.floor(Math.random() * (max - min + 1)) + min; /** * 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 */ const random = (min, max) => _random(min, max); /** * 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 */ const mockRandom = (fn) => { const origin = _random; _random = fn; return () => { _random = origin; }; }; /** * 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 */ const nonNullable = (value, message) => { if (value == null) throw new TypeError(message || "Value is null or undefined"); return value; }; const toString = Object.prototype.toString; const toStringArray = [].toString; const visited = /* @__PURE__ */ new WeakMap(); /** * 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 */ const toStringKey = (thing, immutable = true) => { let tag = typeof thing; if (tag === "symbol") return `[reatom Symbol]${thing.description || "symbol"}`; if (tag !== "function" && (tag !== "object" || thing === null || thing instanceof Date || thing instanceof RegExp)) return `[reatom ${tag}]` + thing; if (visited.has(thing)) return visited.get(thing); let result = `[reatom ${Reflect.getPrototypeOf(thing)?.constructor.name || toString.call(thing).slice(8, -1)}#${random()}]`; if (tag === "function") { visited.set(thing, result += thing.name); return result; } visited.set(thing, result); let proto = Reflect.getPrototypeOf(thing); if (proto && Reflect.getPrototypeOf(proto) && thing.toString !== toStringArray && Symbol.iterator in thing === false) return result; let iterator = Symbol.iterator in thing ? thing : Object.entries(thing).sort(([a], [b]) => a.localeCompare(b)); for (let item of iterator) result += toStringKey(item, immutable); if (immutable) visited.set(thing, result); else visited.delete(thing); return result; }; let i$2 = 0; /** * 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 */ const toAbortError = (reason) => { let options; if (reason instanceof Error === false || reason.name !== "AbortError") { if (reason instanceof Error) { options = { cause: reason }; reason = reason.message; } else reason = isObject(reason) ? toString.call(reason) : String(reason); reason += ` [#${++i$2}]`; if (typeof DOMException === "undefined") { reason = new Error(reason, options); reason.name = "AbortError"; } else reason = assign(new DOMException(reason, "AbortError"), options); } return reason; }; /** * 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 */ const throwIfAborted = (controller) => { if (controller?.signal.aborted) throw toAbortError(controller.signal.reason); }; /** * 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 */ const isAbort = (thing) => thing instanceof Error && thing.name === "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 */ const throwAbort = (message = "", controller) => { const error = toAbortError(message); controller?.abort(error); throw error; }; /** * 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 */ const setTimeout$1 = Object.assign((...params) => { const intervalId = globalThis.setTimeout(...params); return typeof intervalId === "number" ? intervalId : Object.assign(intervalId, { toJSON() { return -1; } }); }, globalThis.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 */ const MAX_SAFE_TIMEOUT = 2 ** 31 - 1; /** * 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 */ const isBrowser = () => typeof window === "object" && typeof document === "object"; /** * 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. */ const withResolvers = () => { let resolve; let reject; return { promise: new Promise((res, rej) => { resolve = res; reject = rej; }), resolve, reject }; }; /** * 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 */ const removeItem = (array, item) => { let found = false; for (let i = 0; i < array.length; i++) if (found) array[i - 1] = array[i]; else if (array[i] === item) found = true; if (found) array.pop(); return found; }; //#endregion //#region src/core/atom.ts let identity$1 = (value) => value; var ReatomError = class extends Error {}; function run(fn, ...params) { try { STACK.push(this); return fn(...params); } finally { STACK.pop(); } } /** @private */ let _copy = (frame) => { let pubs = frame.pubs.slice(); pubs[0] = null; frame = { error: frame.error, state: frame.state, "var#abort": void 0, atom: frame.atom, pubs, subs: frame.subs, run, root: frame.root }; frame.root.store.set(frame.atom, frame); return frame; }; let isAtom = (value) => { return typeof value === "function" && "__reatom" in value; }; let isWritableAtom = (value) => { return isAtom(value) && value.set !== void 0; }; let _mark = (frame) => { for (let i = 0; i < frame.subs.length; i++) { let sub = frame.subs[i]; if ("__reatom" in sub) { let subFrame = frame.root.store.get(sub); if (sub.__reatom.processing > 0) { if (subFrame.subs.length > 0) { _enqueue(() => { _copy(frame.root.store.get(sub)); }, "compute"); sub.__reatom.processing++; } } if (subFrame.pubs[0] !== null) _mark(_copy(subFrame)); else if (sub.__reatom.processing > 0) _mark(subFrame); } else _enqueue(sub, "compute"); } }; let frameDependsOn = (frame, changedAtom, visited) => { if (visited.has(frame)) return false; visited.add(frame); for (let i = 1; i < frame.pubs.length; i++) { let pub = frame.pubs[i]; if (pub.subs.length !== 0) continue; if (pub.atom === changedAtom || frameDependsOn(pub, changedAtom, visited)) return true; } return false; }; let markComputingReaders = (changedAtom) => { for (let i = STACK.length - 2; i >= 0; i--) { let activeFrame = STACK[i]; if (activeFrame.atom.__reatom.reactive && !activeFrame.atom.__reatom.processing) break; if (!activeFrame.atom.__reatom.linking) continue; if (frameDependsOn(activeFrame, changedAtom, /* @__PURE__ */ new Set())) activeFrame.atom.__reatom.processing++; } }; let link = (frame) => { let { pubs, atom } = frame; for (let i = 1; i < pubs.length; i++) { let pub = pubs[i]; if (pub.subs.push(atom) === 1) { if (pub.atom.__reatom.onConnect !== void 0) _enqueue(pub.atom.__reatom.onConnect, "effect"); link(pub); } } }; let unlink = (sub, oldPubs) => { for (let i = oldPubs.length - 1; i > 0; i--) { let pub = oldPubs[i]; let idx = pub.subs.lastIndexOf(sub); if (idx === -1) continue; if (pub.subs.length === 1) { pub.subs.pop(); if (pub.atom.__reatom.onConnect !== void 0) _enqueue(pub.atom.__reatom.onConnect.abort, "effect"); unlink(pub.atom, pub.pubs); } else if (idx === pub.subs.length - 1) pub.subs.pop(); else { let shiftIdx = pub.subs.findLastIndex((el) => el !== sub); if (shiftIdx === -1) shiftIdx = idx; pub.subs[idx] = pub.subs[shiftIdx]; pub.subs.splice(shiftIdx, 1); } } }; let relink = (frame, oldPubs) => { if (oldPubs.length !== frame.pubs.length) { link(frame); unlink(frame.atom, oldPubs); } else for (let i = 1; i < oldPubs.length; i++) if (oldPubs[i].atom !== frame.pubs[i].atom) { link(frame); unlink(frame.atom, oldPubs); break; } }; /** * Checks if an atom has active subscriptions. * * This function determines if an atom is currently connected to any * subscribers, which indicates that the atom is being actively used somewhere * in the application. This is useful for optimizations or conditional logic * based on whether an atom's changes are being observed. * * @param anAtom - The atom to check for subscriptions * @returns `true` if the atom has subscribers, `false` otherwise */ let isConnected = (anAtom) => !!top().root.store.get(anAtom)?.subs.length; function assertFn(fn) { if (typeof fn !== "function") throw new ReatomError("function expected"); } let _trackAction = (target, parentFrame) => { let targetFrame = parentFrame.root.store.get(target); if (targetFrame === void 0) { targetFrame = { error: null, state: [], "var#abort": void 0, atom: target, pubs: [parentFrame.root.frame], subs: [], run, root: parentFrame.root }; parentFrame.root.store.set(target, targetFrame); } if (parentFrame.atom.__reatom.linking) parentFrame.pubs.push(targetFrame); return targetFrame; }; function subscribe(userCb) { let isActionSubscription = isAction(this); let parentFrame = top(); try { parentFrame.root.frame.run(() => { if (isActionSubscription) _trackAction(this, parentFrame); else this(); }); } catch (error) { if (!(error instanceof Promise) && !isAbort(error)) throw error; } let frame = parentFrame.root.store.get(this); let listener = () => { if (frame.subs.length === 0) return; if ((isActionSubscription || !Object.is(frame.state, this())) && userCb) { let frameSnapshot = frame = parentFrame.root.store.get(this); let state = frame.state; _enqueue(() => { if (frameSnapshot === frame) if (isActionSubscription) state.forEach(({ payload, params }) => frame.run(userCb, payload, params)); else frame.run(userCb, state); }, "effect"); } }; if (frame.subs.push(listener) === 1) { if (frame.atom.__reatom.onConnect !== void 0) _enqueue(frame.atom.__reatom.onConnect, "effect"); relink(frame, [null]); } if (userCb && !isActionSubscription) userCb(frame.state); return bind(() => { let idx = frame.subs.lastIndexOf(listener); if (idx === -1) return; frame.subs.splice(idx, 1); if (frame.subs.length === 0) { if (frame.atom.__reatom.onConnect !== void 0) _enqueue(frame.atom.__reatom.onConnect.abort, "effect"); unlink(this, parentFrame.root.store.get(this).pubs); } }, parentFrame.root.frame); } let i$1 = 0; let named = (name, suffix) => { return `${suffix || name}#${++i$1}`; }; if (globalThis.__REATOM) throw new ReatomError("package duplication"); let EXTENSIONS = globalThis.__REATOM = []; /** @private */ let __GLOBAL_ATOMS = []; /** * Registers a global extension that will be automatically applied to all atoms * and actions created after registration. * * This function allows you to add behavior to all Reatom entities in your * application, such as tracking, logging, analytics, or debugging capabilities. * Extensions registered with this function will be applied before any local * extensions defined on individual atoms. * * @example * import { addGlobalExtension, isAction, withCallHook } from '@reatom/core' * * // Track all action calls for analytics * addGlobalExtension((target) => { * if (isAction(target)) { * target.extend(withCallHook(console.log)) * } * return target * }) * * @param extension - Extension function that receives an atom or action and * returns it (optionally modified) */ let addGlobalExtension = (extension) => { EXTENSIONS.push(extension); __GLOBAL_ATOMS.forEach(extension); }; /** This MUTATES frame.pubs */ function _isPubsChanged(frame, pubs, from) { let hasCycleDep = false; pubLoop: for (let i = from; i < pubs.length; i++) { let { error: pubError, state: pubState, atom: pubAtom } = pubs[i]; let pubFreshState = pubState; let pubFreshError = pubError; let pubFreshFrame = frame.root.store.get(pubAtom); if (pubFreshFrame.atom.__reatom.processing > 0 && Object.is(pubFreshFrame.state, pubState)) { hasCycleDep = true; frame.pubs.push(pubs[i]); continue; } else if (pubFreshFrame.pubs.length === 1 || pubFreshFrame.pubs[0] !== null && pubFreshFrame.subs.length !== 0) { pubFreshState = pubFreshFrame.state; pubFreshError = pubFreshFrame.error; } else { try { pubFreshState = pubAtom(); } catch (error) { pubFreshError = error; } pubFreshFrame = frame.root.store.get(pubAtom); } if (!Object.is(pubState, pubFreshState) || !Object.is(pubError, pubFreshError)) { if (hasCycleDep) { frame.pubs.push(pubFreshFrame); continue; } else for (let j = i + 1; j < pubs.length; j++) { let pubFrameJ = pubs[j]; let pubFreshFrameJ = frame.root.store.get(pubFrameJ.atom); if (pubFreshFrameJ.atom.__reatom.processing > 0 && Object.is(pubFreshFrameJ.state, pubFrameJ.state)) { hasCycleDep = true; frame.pubs.push(pubFreshFrame); continue pubLoop; } } if (from === 1) frame.pubs = [null]; else frame.pubs.length = from; return true; } else frame.pubs.push(pubFreshFrame); } return false; } /** The hurt of atom internal logic */ function computedMiddleware(next, ...args) { let frame = STACK[STACK.length - 1]; let push = args.length > 0; let { state, pubs } = frame; let dirty = pubs[0] === null; let dependent = pubs.length !== 1; let subscribed = frame.subs.length !== 0; let computed = next !== identity$1; let emptyComputed = computed && !dependent; let newState = state; let invalid = computed && (dirty || dependent && !subscribed) && (!dependent || (frame.pubs = [null], _isPubsChanged(frame, pubs, 1))); while (push || invalid) { if (invalid) { invalid = false; frame.pubs = [null]; try { frame.atom.__reatom.linking = true; frame.state = newState = next(newState); frame.error = null; } finally { frame.atom.__reatom.linking = false; frame.pubs[0] ??= frame.root.frame; if (frame.subs.length) relink(frame, pubs); } } if (push) { push = false; let update = args[0]; newState = frame.state = typeof update === "function" ? update(newState) : update; frame.error = null; frame.pubs[0] = STACK[STACK.length - 2]; invalid = emptyComputed && !Object.is(state, frame.state); } } if (frame.error != null) throw frame.error; return newState; } /** @internal apply new middlewares to the atom */ let _recompile = (target) => { let { middlewares } = target.__reatom; let fn = middlewares[0]; for (let i = 1; i < middlewares.length; i++) fn = middlewares[i].bind(null, fn); target.__reatom.pipeline = fn; }; /** Cache checking middleware, placed in every atom's middlewares */ function cacheMiddleware(next, ...args) { let topFrame = STACK[STACK.length - 2]; let frame = STACK[STACK.length - 1]; let target = frame.atom; let { reactive } = target.__reatom; let push = !reactive || args.length > 0; let { error, state } = frame; let dirty = frame.pubs[0] === null; let dependent = frame.pubs.length !== 1; let subscribed = frame.subs.length !== 0; let isInit = frame.state instanceof AtomInitState; if (target.__reatom.processing === 0 && (push || dirty || dependent && !subscribed)) { let recursionTries = 10; recursion: while (recursionTries--) { if (!dirty) STACK[STACK.length - 1] = frame = _copy(frame); if (reactive) target.__reatom.processing++; try { if (isInit) frame.state = frame.state.initState(); isInit = false; frame.state = next(...args); frame.error = null; } catch (error) { frame.error = error ?? new ReatomError("Unknown error"); if (isInit) frame.state = void 0; } frame.pubs[0] ??= topFrame.root.frame; if (!push && topFrame.atom.__reatom.linking) topFrame.pubs.push(frame); let changed = !Object.is(state, frame.state) || !Object.is(error, frame.error); if ((push || !dirty) && subscribed && changed) _mark(frame); if (push && changed) markComputingReaders(target); if (reactive) { target.__reatom.processing--; if (target.__reatom.processing > 0) { target.__reatom.processing = 0; if (!push) { if (recursionTries === 0) throw new ReatomError("Stuck in recursion"); frame.pubs[0] = null; dirty = true; if (topFrame.atom.__reatom.linking) topFrame.pubs.pop(); continue recursion; } } } break; } } else if (topFrame.atom.__reatom.linking) topFrame.pubs.push(frame); if (frame.error != null) throw frame.error; return frame.state; } /** @internal default pipeline for atoms */ let _defaultPipeline = cacheMiddleware.bind(null, computedMiddleware.bind(null, identity$1)); let SET_PARAMS = null; let castAtom = (target, meta) => Object.assign(target, { extend, set(...params) { if (params.length === 0) throw new ReatomError("Missing payload"); SET_PARAMS = params; return target(); }, subscribe: subscribe.bind(target), __reatom: { reactive: meta.reactive, middlewares: meta.middlewares, pipeline: meta.pipeline, processing: 0, linking: false, onConnect: void 0 }, toString: () => `[Atom ${target.name}]`, toJSON: target }); let ANONYMOUS = false; /** * Useful for security reasons, if you need to increase your runtime complexity. * It's important to call this function before creating any atoms. */ let anonymizeNames = () => { ANONYMOUS = true; }; let _set = (target, ...params) => { if (params.length === 0) throw new ReatomError("Missing payload"); SET_PARAMS = params; return target(); }; var AtomInitState = class { constructor(initState) { this.initState = initState; this.initState = initState; } }; let createAtom = (setup, name = named("atom", setup?.computed?.name)) => { if (ANONYMOUS) name = "anonymous"; let target = castAtom(function() { let { reactive, pipeline } = target.__reatom; if (reactive && !SET_PARAMS && arguments.length) throw new ReatomError(`Can't call atom "${name}" with arguments, use .set instead`); let args = reactive ? SET_PARAMS : arguments; let write = args != void 0; SET_PARAMS = null; let topFrame = top(); let frame = topFrame.root.store.get(target); if (frame === void 0) { if (reactive && target.__reatom.processing > 0) throw new ReatomError("Cyclic initialization"); frame = { error: null, state: setup.initState, "var#abort": void 0, atom: target, pubs: [null], subs: [], run, root: topFrame.root }; if (typeof frame.state === "function") frame.state = new AtomInitState(frame.state); topFrame.root.store.set(target, frame); } try { STACK.push(frame); let state; if (!write) state = pipeline(); else if (args.length === 1) state = pipeline(args[0]); else state = pipeline.apply(null, args); return reactive ? state : state.at(-1).payload; } finally { STACK.pop(); } }, { reactive: true, middlewares: [ setup.computed ?? identity$1, computedMiddleware, cacheMiddleware ], pipeline: setup.computed ? cacheMiddleware.bind(null, computedMiddleware.bind(null, setup.computed)) : _defaultPipeline }); Object.defineProperty(target, "name", { value: name, writable: false, enumerable: false, configurable: true }); if (setup.middlewares) { target.__reatom.middlewares = setup.middlewares; _recompile(target); } return EXTENSIONS.length === 0 ? target : target.extend(...EXTENSIONS); }; /** * Creates a mutable state container. * * The atom is the core primitive for storing and updating mutable state in * Reatom. Atoms can be called as functions to read their current value or to * update the value. * * @example * // Create with initial value * const counter = atom(0, 'counter') * * // Read current value * const value = counter() // -> 0 * * // Update with new value * counter.set(5) // Sets value to 5 * * // Update with a function * counter.set((prev) => prev + 1) // Sets value to 6 * * @template T - The type of state stored in the atom * @param createState - A function that returns the initial state, or the * initial state value directly * @param name - Optional name for the atom (useful for debugging) * @returns An atom instance containing the state */ let atom = (initState, name) => createAtom({ initState }, name); function computedParamsMiddleware(next, ...args) { if (args.length > 0) throw new ReatomError("Computed can't accept parameters"); return next(); } /** * Creates a derived state container that lazily recalculates only when read. * * Computed atoms automatically track their dependencies (other atoms or * computed values that are called during computation) and only recalculate when * those dependencies change. The computation is lazy - it only runs when the * computed value is read AND subscribed to. * * @example * const counter = atom(5, 'counter') * const doubled = computed(() => counter() * 2, 'doubledCounter') * * // Reading triggers computation only if subscribed * const value = doubled() // -> 10 * * @template State - The type of state derived by the computation * @param computed - A function that computes the derived state * @param name - Optional name for debugging purposes * @returns A computed atom instance */ let computed = (computed, name) => { assertFn(computed); return createAtom({ computed }, name).extend((target) => { target.__reatom.middlewares.push(computedParamsMiddleware); _recompile(target); target.set = void 0; return target; }); }; /** * Checks if the provided target is a READONLY computed atom * * @param target - The atom to check * @returns Boolean */ let isComputed = (target) => target.__reatom.middlewares.includes(computedParamsMiddleware); /** * Core context object that manages the reactive state context in Reatom. * * The context is responsible for tracking dependencies between atoms, managing * computation stacks, and ensuring proper reactivity. It serves as the * foundation for Reatom's reactivity system and provides access to the current * context frame. * * @returns The current context frame * @throws {ReatomError} If called outside a valid context (broken async stack) */ let context = castAtom(function context() { return top().root.frame; }, { reactive: false, middlewares: [identity$1], pipeline: identity$1 }); context.start = (cb = top) => { let frame = { error: null, state: { store: /* @__PURE__ */ new WeakMap(), frames: /* @__PURE__ */ new WeakMap(), inits: /* @__PURE__ */ new WeakMap(), memoKey: /* @__PURE__ */ new WeakMap(), hook: [], compute: [], cleanup: [], effect: [], pushQueue(cb, queue) { this[queue].push(cb); }, frame: void 0 }, "var#abort": void 0, atom: context, pubs: [null], subs: [], run, root: void 0 }; frame.root = frame.state; frame.state.frame = frame; return frame.run(cb); }; context.reset = () => { let rootFrame = context(); (rootFrame.root = rootFrame.state = context.start().state).frame = rootFrame; }; /** * Reads the current frame for an atom from the context store. * * This internal utility function retrieves the frame associated with an atom * from the current context. It's used to access an atom's state and * dependencies without triggering reactivity or creating new dependencies. * * @private * @template State - The state type of the atom * @template Params - The parameter types the atom accepts * @template Payload - The return type when the atom is called * @param target - The atom to read the frame for * @returns The frame for the atom if it exists in the current context, or * undefined otherwise */ let _read = (target) => top().root.store.get(target); /** * Gets the current top frame in the Reatom context stack. * * Returns the currently active frame in the execution stack, which contains the * current atom being processed and its state. * * @returns The current top frame from the context stack * @throws {ReatomError} If the context stack is empty (missing async stack) */ let top = () => { if (STACK.length === 0) throw new ReatomError("missing async stack"); return STACK[STACK.length - 1]; }; let STACK = []; STACK.push(context.start()); /** * Clears the current Reatom context stack. * * This is primarily used to force explicit context preservation via `wrap()`. * By clearing the stack, any atom operations outside of a properly wrapped * function will throw "missing async stack" errors, ensuring proper context * handling. */ let clearStack = () => { STACK = []; }; /** * Light version of `wrap` that binds a function to the current reactive * context. * * Unlike the full `wrap` function, `bind` does not follow abort context, making * it more lightweight but less safe for certain async operations. Use this when * you need to preserve context but don't need the abort handling capabilities * of `wrap`. * * @template Params - The parameter types of the target function * @template Payload - The return type of the target function * @param target - The function to bind to the reactive context * @param frame - The frame to bind to (defaults to the current top frame) * @returns A function that will run in the specified context when called */ let bind = (target, frame = top()) => frame.run.bind(frame, target); /** * Mocks an atom or action for testing purposes. * * This function replaces the original behavior of an atom or action with a * custom callback function for the duration of the mock. This is useful for * isolating units of code during testing and controlling their behavior. * * @template Params - The parameter types of the target atom/action * @template Payload - The return type of the target atom/action * @param target - The atom or action to mock * @param cb - The callback function to use as the mock implementation. It * receives the parameters passed to the mocked atom/action and should return * the desired payload. * @returns A function that, when called, removes the mock and restores the * original behavior. */ let mock = (target, cb) => { let { root } = top(); let mockMiddleware = (next, ...params) => { if (root !== top().root) return next(...params); return cb(...params); }; let cacheMiddlewareIdx = target.__reatom.middlewares.indexOf(cacheMiddleware); if (cacheMiddlewareIdx !== -1) target.__reatom.middlewares.splice(cacheMiddlewareIdx, 0, mockMiddleware); else target.__reatom.middlewares.push(mockMiddleware); _recompile(target); return () => { let idx = target.__reatom.middlewares.indexOf(mockMiddleware); if (idx !== -1) target.__reatom.middlewares.splice(idx, 1); _recompile(target); }; }; //#endregion //#region src/core/extend.ts /** * Applies extensions to atoms or actions. * * This is the core extension mechanism in Reatom that allows adding * functionality to atoms and actions. Extensions can add properties, methods, * or modify behavior. Extended atoms maintain their original reference * identity. * * @example * // Extending an atom with reset capability * const counter = atom(0, 'counter').extend( * withReset(0), // Adds counter.reset() method * withLogger('COUNTER'), // Adds logging middleware * ) * * @template This - The type of atom or action being extended * @param extensions - Array of extensions to apply to the atom/action * @returns The original atom/action with extensions applied */ function extend(...extensions) { for (let ext of extensions) { let result = ext(this); if (this !== result) { if (isAtom(result)) throw new ReatomError("extension can not change the atom reference, use middleware instead"); if (!result) throw new ReatomError("extension can not return nothing"); for (let key in result) { if (key in this && this[key] !== result[key]) throw new ReatomError(`extension can not override existing methods: ${key}`); this[key] = result[key]; } } } return this; } let withMiddleware = (cb, place = "invalidation") => (target) => { let middleware = cb(target); if (typeof middleware !== "function") throw new ReatomError("function expected"); if (place === "read") target.__reatom.middlewares.push(middleware); else if (place === "computed") { const computedMiddlewareIdx = target.__reatom.middlewares.indexOf(computedMiddleware); target.__reatom.middlewares.splice(computedMiddlewareIdx, 0, middleware); } else { const cacheMiddlewareIdx = target.__reatom.middlewares.indexOf(cacheMiddleware); if (cacheMiddlewareIdx !== -1) target.__reatom.middlewares.splice(cacheMiddlewareIdx, 0, middleware); else target.__reatom.middlewares.push(middleware); } _recompile(target); return target; }; /** * Creates an extension that allows observing state changes without modifying * them. * * This extension adds a middleware that calls the provided callback function * whenever the atom's state changes, passing the target atom, new state, and * previous state. This is useful for side effects like logging, analytics, or * debugging. * * @example * const counter = atom(0, 'counter').extend( * withTap((target, state, prevState) => { * console.log(`${target.name} changed from ${prevState} to ${state}`) * }), * ) * * @param cb - Callback function that receives the target, new state, and * previous state * @returns An extension that can be applied to atoms or actions */ let withTap = (cb) => { if (typeof cb !== "function") throw new ReatomError("function expected"); return withMiddleware((target) => function withTap(next, ...params) { let { state } = top(); let nextState = next(...params); cb(target, state, nextState); return nextState; }); }; /** * Extension that transforms parameters before they reach the atom or action. * Useful as the `.set` atom method can't be reassigned and changed. * * This utility lets you change how parameters are processed when an atom or * action is called, enabling custom parameter handling, validation, or * transformation. * * @example * // Convert from any unit to meters * const length = atom(0, 'length').extend( * withParams((value: number, unit: 'cm' | 'm' | 'km') => { * switch (unit) { * case 'cm': * return value / 100 * case 'm': * return value * case 'km': * return value * 1000 * default: * return value * } * }), * ) * * length(5, 'km') // Sets value to 5000 meters * * @template Target - The type of atom or action being extended * @template Params - The parameter types that will be accepted by the extended * atom/action * @param parse - Function that transforms the new parameters into what the * atom/action expects * @returns An extension that applies the parameter transformation */ let withParams = (parse) => { if (typeof parse !== "function") throw new ReatomError("function expected"); return withMiddleware((target) => { let idx = target.__reatom.middlewares.indexOf(computedParamsMiddleware); if (idx !== -1) target.__reatom.middlewares.splice(idx, 1); return (next, ...params) => target.__reatom.reactive && params.length === 0 ? next() : next(parse(...params)); }); }; //#endregion //#region src/core/queues.ts /** * Schedules a function to be executed in a specific queue of the current * context. * * This is the core mechanism for scheduling reactive updates in Reatom. When an * atom's state changes, tasks are queued to be executed afterwards in the * appropriate order. If this is the first task being scheduled, a microtask is * created to process the queues asynchronously. * * @param fn - The function to schedule for execution * @param queue - The queue to add the function to ('hook', 'compute', * 'cleanup', or 'effect') */ let _enqueue = (fn, queue) => { let contextFrame = context(); if (contextFrame.state.hook.length === 0 && contextFrame.state.compute.length === 0 && contextFrame.state.cleanup.length === 0 && contextFrame.state.effect.length === 0) Promise.resolve().then(bind(notify, contextFrame)); contextFrame.state.pushQueue(fn, queue); }; /** * Creates an iterator function for a queue that returns items sequentially. * * @param queue - The queue to iterate over * @param i - The starting index * @returns A function that returns the next item in the queue or undefined when * empty */ let QueueIterator = (queue, i) => () => i < queue.length ? queue[i++] : void 0; let batchNestDepth = 0; /** * Runs a callback as a nested batch and optionally flushes the queue after the * outermost batch completes. * * Use `shouldNotify: true` for user-facing write batches that must notify * synchronously after all nested writes finish. Leave it `false` when wrapping * reads such as computed values or effects. * * @example * import { atom, batch } from '@reatom/core' * * const count = atom(0, 'count') * * batch(() => { * count.set(1) * count.set(2) * }, true) * * @param cb - The callback to run inside the batch * @param shouldNotify - Whether to call `notify` after the outermost batch * @returns The callback result */ let batch = (cb, shouldNotify = false) => { try { batchNestDepth++; return cb(); } finally { batchNestDepth--; if (shouldNotify && batchNestDepth === 0) notify(); } }; /** * Processes a