@reatom/core
Version:
The ultimate state manager
1,435 lines • 284 kB
JavaScript
//#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