jotai
Version:
👻 Primitive and flexible state management for React
627 lines (624 loc) • 20.5 kB
JavaScript
let keyCount = 0;
function atom(read, write) {
const key = `atom${++keyCount}`;
const config = {
toString: () => key
};
if (typeof read === "function") {
config.read = read;
} else {
config.init = read;
config.read = defaultRead;
config.write = defaultWrite;
}
if (write) {
config.write = write;
}
return config;
}
function defaultRead(get) {
return get(this);
}
function defaultWrite(get, set, arg) {
return set(
this,
typeof arg === "function" ? arg(get(this)) : arg
);
}
const isSelfAtom = (atom, a) => atom.unstable_is ? atom.unstable_is(a) : a === atom;
const hasInitialValue = (atom) => "init" in atom;
const isActuallyWritableAtom = (atom) => !!atom.write;
const cancelPromiseMap = /* @__PURE__ */ new WeakMap();
const registerCancelPromise = (promise, cancel) => {
cancelPromiseMap.set(promise, cancel);
promise.catch(() => {
}).finally(() => cancelPromiseMap.delete(promise));
};
const cancelPromise = (promise, next) => {
const cancel = cancelPromiseMap.get(promise);
if (cancel) {
cancelPromiseMap.delete(promise);
cancel(next);
}
};
const resolvePromise = (promise, value) => {
promise.status = "fulfilled";
promise.value = value;
};
const rejectPromise = (promise, e) => {
promise.status = "rejected";
promise.reason = e;
};
const isPromiseLike = (x) => typeof (x == null ? void 0 : x.then) === "function";
const isEqualAtomValue = (a, b) => !!a && "v" in a && "v" in b && Object.is(a.v, b.v);
const isEqualAtomError = (a, b) => !!a && "e" in a && "e" in b && Object.is(a.e, b.e);
const hasPromiseAtomValue = (a) => !!a && "v" in a && a.v instanceof Promise;
const isEqualPromiseAtomValue = (a, b) => "v" in a && "v" in b && a.v.orig && a.v.orig === b.v.orig;
const returnAtomValue = (atomState) => {
if ("e" in atomState) {
throw atomState.e;
}
return atomState.v;
};
const createStore = () => {
const atomStateMap = /* @__PURE__ */ new WeakMap();
const mountedMap = /* @__PURE__ */ new WeakMap();
const pendingStack = [];
const pendingMap = /* @__PURE__ */ new WeakMap();
let storeListenersRev2;
let mountedAtoms;
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
storeListenersRev2 = /* @__PURE__ */ new Set();
mountedAtoms = /* @__PURE__ */ new Set();
}
const getAtomState = (atom) => atomStateMap.get(atom);
const addPendingDependent = (atom, atomState) => {
atomState.d.forEach((_, a) => {
var _a;
if (!pendingMap.has(a)) {
const aState = getAtomState(a);
(_a = pendingStack[pendingStack.length - 1]) == null ? void 0 : _a.add(a);
pendingMap.set(a, [aState, /* @__PURE__ */ new Set()]);
if (aState) {
addPendingDependent(a, aState);
}
}
pendingMap.get(a)[1].add(atom);
});
};
const setAtomState = (atom, atomState) => {
var _a;
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
Object.freeze(atomState);
}
const prevAtomState = getAtomState(atom);
atomStateMap.set(atom, atomState);
if (!pendingMap.has(atom)) {
(_a = pendingStack[pendingStack.length - 1]) == null ? void 0 : _a.add(atom);
pendingMap.set(atom, [prevAtomState, /* @__PURE__ */ new Set()]);
addPendingDependent(atom, atomState);
}
if (hasPromiseAtomValue(prevAtomState)) {
const next = "v" in atomState ? atomState.v instanceof Promise ? atomState.v : Promise.resolve(atomState.v) : Promise.reject(atomState.e);
if (prevAtomState.v !== next) {
cancelPromise(prevAtomState.v, next);
}
}
};
const updateDependencies = (atom, nextAtomState, nextDependencies, keepPreviousDependencies) => {
const dependencies = new Map(
keepPreviousDependencies ? nextAtomState.d : null
);
let changed = false;
nextDependencies.forEach((aState, a) => {
if (!aState && isSelfAtom(atom, a)) {
aState = nextAtomState;
}
if (aState) {
dependencies.set(a, aState);
if (nextAtomState.d.get(a) !== aState) {
changed = true;
}
} else if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
console.warn("[Bug] atom state not found");
}
});
if (changed || nextAtomState.d.size !== dependencies.size) {
nextAtomState.d = dependencies;
}
};
const setAtomValue = (atom, value, nextDependencies, keepPreviousDependencies) => {
const prevAtomState = getAtomState(atom);
const nextAtomState = {
d: (prevAtomState == null ? void 0 : prevAtomState.d) || /* @__PURE__ */ new Map(),
v: value
};
if (nextDependencies) {
updateDependencies(
atom,
nextAtomState,
nextDependencies,
keepPreviousDependencies
);
}
if (isEqualAtomValue(prevAtomState, nextAtomState) && prevAtomState.d === nextAtomState.d) {
return prevAtomState;
}
if (hasPromiseAtomValue(prevAtomState) && hasPromiseAtomValue(nextAtomState) && isEqualPromiseAtomValue(prevAtomState, nextAtomState)) {
if (prevAtomState.d === nextAtomState.d) {
return prevAtomState;
} else {
nextAtomState.v = prevAtomState.v;
}
}
setAtomState(atom, nextAtomState);
return nextAtomState;
};
const setAtomValueOrPromise = (atom, valueOrPromise, nextDependencies, abortPromise) => {
if (isPromiseLike(valueOrPromise)) {
let continuePromise;
const updatePromiseDependencies = () => {
const prevAtomState = getAtomState(atom);
if (!hasPromiseAtomValue(prevAtomState) || prevAtomState.v !== promise) {
return;
}
const nextAtomState = setAtomValue(
atom,
promise,
nextDependencies
);
if (mountedMap.has(atom) && prevAtomState.d !== nextAtomState.d) {
mountDependencies(atom, nextAtomState, prevAtomState.d);
}
};
const promise = new Promise((resolve, reject) => {
let settled = false;
valueOrPromise.then(
(v) => {
if (!settled) {
settled = true;
resolvePromise(promise, v);
resolve(v);
updatePromiseDependencies();
}
},
(e) => {
if (!settled) {
settled = true;
rejectPromise(promise, e);
reject(e);
updatePromiseDependencies();
}
}
);
continuePromise = (next) => {
if (!settled) {
settled = true;
next.then(
(v) => resolvePromise(promise, v),
(e) => rejectPromise(promise, e)
);
resolve(next);
}
};
});
promise.orig = valueOrPromise;
promise.status = "pending";
registerCancelPromise(promise, (next) => {
if (next) {
continuePromise(next);
}
abortPromise == null ? void 0 : abortPromise();
});
return setAtomValue(atom, promise, nextDependencies, true);
}
return setAtomValue(atom, valueOrPromise, nextDependencies);
};
const setAtomError = (atom, error, nextDependencies) => {
const prevAtomState = getAtomState(atom);
const nextAtomState = {
d: (prevAtomState == null ? void 0 : prevAtomState.d) || /* @__PURE__ */ new Map(),
e: error
};
if (nextDependencies) {
updateDependencies(atom, nextAtomState, nextDependencies);
}
if (isEqualAtomError(prevAtomState, nextAtomState) && prevAtomState.d === nextAtomState.d) {
return prevAtomState;
}
setAtomState(atom, nextAtomState);
return nextAtomState;
};
const readAtomState = (atom, force) => {
const atomState = getAtomState(atom);
if (!force && atomState) {
if (mountedMap.has(atom)) {
return atomState;
}
if (Array.from(atomState.d).every(([a, s]) => {
if (a === atom) {
return true;
}
const aState = readAtomState(a);
return aState === s || isEqualAtomValue(aState, s);
})) {
return atomState;
}
}
const nextDependencies = /* @__PURE__ */ new Map();
let isSync = true;
const getter = (a) => {
if (isSelfAtom(atom, a)) {
const aState2 = getAtomState(a);
if (aState2) {
nextDependencies.set(a, aState2);
return returnAtomValue(aState2);
}
if (hasInitialValue(a)) {
nextDependencies.set(a, void 0);
return a.init;
}
throw new Error("no atom init");
}
const aState = readAtomState(a);
nextDependencies.set(a, aState);
return returnAtomValue(aState);
};
let controller;
let setSelf;
const options = {
get signal() {
if (!controller) {
controller = new AbortController();
}
return controller.signal;
},
get setSelf() {
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && !isActuallyWritableAtom(atom)) {
console.warn("setSelf function cannot be used with read-only atom");
}
if (!setSelf && isActuallyWritableAtom(atom)) {
setSelf = (...args) => {
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && isSync) {
console.warn("setSelf function cannot be called in sync");
}
if (!isSync) {
return writeAtom(atom, ...args);
}
};
}
return setSelf;
}
};
try {
const valueOrPromise = atom.read(getter, options);
return setAtomValueOrPromise(
atom,
valueOrPromise,
nextDependencies,
() => controller == null ? void 0 : controller.abort()
);
} catch (error) {
return setAtomError(atom, error, nextDependencies);
} finally {
isSync = false;
}
};
const readAtom = (atom) => returnAtomValue(readAtomState(atom));
const addAtom = (atom) => {
let mounted = mountedMap.get(atom);
if (!mounted) {
mounted = mountAtom(atom);
}
return mounted;
};
const canUnmountAtom = (atom, mounted) => !mounted.l.size && (!mounted.t.size || mounted.t.size === 1 && mounted.t.has(atom));
const delAtom = (atom) => {
const mounted = mountedMap.get(atom);
if (mounted && canUnmountAtom(atom, mounted)) {
unmountAtom(atom);
}
};
const recomputeDependents = (atom) => {
const getDependents = (a) => {
var _a, _b;
const dependents = new Set((_a = mountedMap.get(a)) == null ? void 0 : _a.t);
(_b = pendingMap.get(a)) == null ? void 0 : _b[1].forEach((dependent) => {
dependents.add(dependent);
});
return dependents;
};
const topsortedAtoms = new Array();
const markedAtoms = /* @__PURE__ */ new Set();
const visit = (n) => {
if (markedAtoms.has(n)) {
return;
}
markedAtoms.add(n);
for (const m of getDependents(n)) {
if (n !== m) {
visit(m);
}
}
topsortedAtoms.push(n);
};
visit(atom);
const changedAtoms = /* @__PURE__ */ new Set([atom]);
for (let i = topsortedAtoms.length - 1; i >= 0; --i) {
const a = topsortedAtoms[i];
const prevAtomState = getAtomState(a);
if (!prevAtomState) {
continue;
}
let hasChangedDeps = false;
for (const dep of prevAtomState.d.keys()) {
if (dep !== a && changedAtoms.has(dep)) {
hasChangedDeps = true;
break;
}
}
if (hasChangedDeps) {
const nextAtomState = readAtomState(a, true);
if (!isEqualAtomValue(prevAtomState, nextAtomState)) {
changedAtoms.add(a);
}
}
}
};
const writeAtomState = (atom, ...args) => {
let isSync = true;
const getter = (a) => returnAtomValue(readAtomState(a));
const setter = (a, ...args2) => {
let r;
if (isSelfAtom(atom, a)) {
if (!hasInitialValue(a)) {
throw new Error("atom not writable");
}
const prevAtomState = getAtomState(a);
const nextAtomState = setAtomValueOrPromise(a, args2[0]);
if (!isEqualAtomValue(prevAtomState, nextAtomState)) {
recomputeDependents(a);
}
} else {
r = writeAtomState(a, ...args2);
}
if (!isSync) {
const flushed = flushPending([a]);
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
storeListenersRev2.forEach(
(l) => l({ type: "async-write", flushed })
);
}
}
return r;
};
const result = atom.write(getter, setter, ...args);
isSync = false;
return result;
};
const writeAtom = (atom, ...args) => {
pendingStack.push(/* @__PURE__ */ new Set([atom]));
const result = writeAtomState(atom, ...args);
const flushed = flushPending(pendingStack.pop());
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
storeListenersRev2.forEach((l) => l({ type: "write", flushed }));
}
return result;
};
const mountAtom = (atom, initialDependent, onMountQueue) => {
var _a;
const queue = onMountQueue || [];
(_a = getAtomState(atom)) == null ? void 0 : _a.d.forEach((_, a) => {
const aMounted = mountedMap.get(a);
if (aMounted) {
aMounted.t.add(atom);
} else {
if (a !== atom) {
mountAtom(a, atom, queue);
}
}
});
readAtomState(atom);
const mounted = {
t: new Set(initialDependent && [initialDependent]),
l: /* @__PURE__ */ new Set()
};
mountedMap.set(atom, mounted);
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
mountedAtoms.add(atom);
}
if (isActuallyWritableAtom(atom) && atom.onMount) {
const { onMount } = atom;
queue.push(() => {
const onUnmount = onMount((...args) => writeAtom(atom, ...args));
if (onUnmount) {
mounted.u = onUnmount;
}
});
}
if (!onMountQueue) {
queue.forEach((f) => f());
}
return mounted;
};
const unmountAtom = (atom) => {
var _a;
const onUnmount = (_a = mountedMap.get(atom)) == null ? void 0 : _a.u;
if (onUnmount) {
onUnmount();
}
mountedMap.delete(atom);
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
mountedAtoms.delete(atom);
}
const atomState = getAtomState(atom);
if (atomState) {
if (hasPromiseAtomValue(atomState)) {
cancelPromise(atomState.v);
}
atomState.d.forEach((_, a) => {
if (a !== atom) {
const mounted = mountedMap.get(a);
if (mounted) {
mounted.t.delete(atom);
if (canUnmountAtom(a, mounted)) {
unmountAtom(a);
}
}
}
});
} else if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
console.warn("[Bug] could not find atom state to unmount", atom);
}
};
const mountDependencies = (atom, atomState, prevDependencies) => {
const depSet = new Set(atomState.d.keys());
const maybeUnmountAtomSet = /* @__PURE__ */ new Set();
prevDependencies == null ? void 0 : prevDependencies.forEach((_, a) => {
if (depSet.has(a)) {
depSet.delete(a);
return;
}
maybeUnmountAtomSet.add(a);
const mounted = mountedMap.get(a);
if (mounted) {
mounted.t.delete(atom);
}
});
depSet.forEach((a) => {
const mounted = mountedMap.get(a);
if (mounted) {
mounted.t.add(atom);
} else if (mountedMap.has(atom)) {
mountAtom(a, atom);
}
});
maybeUnmountAtomSet.forEach((a) => {
const mounted = mountedMap.get(a);
if (mounted && canUnmountAtom(a, mounted)) {
unmountAtom(a);
}
});
};
const flushPending = (pendingAtoms) => {
let flushed;
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
flushed = /* @__PURE__ */ new Set();
}
const pending = [];
const collectPending = (pendingAtom) => {
var _a;
if (!pendingMap.has(pendingAtom)) {
return;
}
const [prevAtomState, dependents] = pendingMap.get(pendingAtom);
pendingMap.delete(pendingAtom);
pending.push([pendingAtom, prevAtomState]);
dependents.forEach(collectPending);
(_a = getAtomState(pendingAtom)) == null ? void 0 : _a.d.forEach((_, a) => collectPending(a));
};
pendingAtoms.forEach(collectPending);
pending.forEach(([atom, prevAtomState]) => {
const atomState = getAtomState(atom);
if (!atomState) {
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
console.warn("[Bug] no atom state to flush");
}
return;
}
if (atomState !== prevAtomState) {
const mounted = mountedMap.get(atom);
if (mounted && atomState.d !== (prevAtomState == null ? void 0 : prevAtomState.d)) {
mountDependencies(atom, atomState, prevAtomState == null ? void 0 : prevAtomState.d);
}
if (mounted && !// TODO This seems pretty hacky. Hope to fix it.
// Maybe we could `mountDependencies` in `setAtomState`?
(!hasPromiseAtomValue(prevAtomState) && (isEqualAtomValue(prevAtomState, atomState) || isEqualAtomError(prevAtomState, atomState)))) {
mounted.l.forEach((listener) => listener());
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
flushed.add(atom);
}
}
}
});
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
return flushed;
}
};
const subscribeAtom = (atom, listener) => {
const mounted = addAtom(atom);
const flushed = flushPending([atom]);
const listeners = mounted.l;
listeners.add(listener);
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
storeListenersRev2.forEach(
(l) => l({ type: "sub", flushed })
);
}
return () => {
listeners.delete(listener);
delAtom(atom);
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
storeListenersRev2.forEach((l) => l({ type: "unsub" }));
}
};
};
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
return {
get: readAtom,
set: writeAtom,
sub: subscribeAtom,
// store dev methods (these are tentative and subject to change without notice)
dev_subscribe_store: (l, rev) => {
if (rev !== 2) {
throw new Error("The current StoreListener revision is 2.");
}
storeListenersRev2.add(l);
return () => {
storeListenersRev2.delete(l);
};
},
dev_get_mounted_atoms: () => mountedAtoms.values(),
dev_get_atom_state: (a) => atomStateMap.get(a),
dev_get_mounted: (a) => mountedMap.get(a),
dev_restore_atoms: (values) => {
pendingStack.push(/* @__PURE__ */ new Set());
for (const [atom, valueOrPromise] of values) {
if (hasInitialValue(atom)) {
setAtomValueOrPromise(atom, valueOrPromise);
recomputeDependents(atom);
}
}
const flushed = flushPending(pendingStack.pop());
storeListenersRev2.forEach(
(l) => l({ type: "restore", flushed })
);
}
};
}
return {
get: readAtom,
set: writeAtom,
sub: subscribeAtom
};
};
let defaultStore;
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production") {
if (typeof globalThis.__NUMBER_OF_JOTAI_INSTANCES__ === "number") {
++globalThis.__NUMBER_OF_JOTAI_INSTANCES__;
} else {
globalThis.__NUMBER_OF_JOTAI_INSTANCES__ = 1;
}
}
const getDefaultStore = () => {
if (!defaultStore) {
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && globalThis.__NUMBER_OF_JOTAI_INSTANCES__ !== 1) {
console.warn(
"Detected multiple Jotai instances. It may cause unexpected behavior with the default store. https://github.com/pmndrs/jotai/discussions/2044"
);
}
defaultStore = createStore();
}
return defaultStore;
};
export { atom, createStore, getDefaultStore };