jotai-effect
Version:
219 lines • 8.57 kB
JavaScript
import { atom } from 'jotai/vanilla';
import { INTERNAL_getBuildingBlocksRev1 as INTERNAL_getBuildingBlocks, INTERNAL_hasInitialValue as hasInitialValue, INTERNAL_initializeStoreHooks as initializeStoreHooks, INTERNAL_isAtomStateInitialized as isAtomStateInitialized, INTERNAL_isSelfAtom as isSelfAtom, INTERNAL_returnAtomValue as returnAtomValue, INTERNAL_setAtomStateValueOrPromise as setAtomStateValueOrPromise, } from 'jotai/vanilla/internals';
import { isDev } from './env.js';
function getBuildingBlocks(store) {
const buildingBlocks = INTERNAL_getBuildingBlocks(store);
return [
buildingBlocks[1], // mountedAtoms
buildingBlocks[3], // changedAtoms
initializeStoreHooks(buildingBlocks[6]), // storeHooks
buildingBlocks[11], // ensureAtomState
buildingBlocks[14], // readAtomState
buildingBlocks[16], // writeAtomState
buildingBlocks[17], // mountDependencies
buildingBlocks[15], // invalidateDependents
buildingBlocks[13], // recomputeInvalidatedAtoms
buildingBlocks[12], // flushCallbacks
];
}
export function atomEffect(effect) {
const refAtom = atom(() => []);
const effectAtom = atom(function effectAtomRead(get) {
const [dependencies, atomState, mountedAtoms] = get(refAtom);
if (mountedAtoms.has(effectAtom)) {
dependencies.forEach(get);
++atomState.n;
}
});
effectAtom.effect = effect;
effectAtom.unstable_onInit = (store) => {
const deps = new Set();
let inProgress = 0;
let isRecursing = false;
let hasChanged = false;
let fromCleanup = false;
let runCleanup;
function runEffect() {
if (inProgress) {
return;
}
deps.clear();
let isSync = true;
const getter = (a) => {
if (fromCleanup) {
return store.get(a);
}
if (isSelfAtom(effectAtom, a)) {
const aState = ensureAtomState(a);
if (!isAtomStateInitialized(aState)) {
if (hasInitialValue(a)) {
setAtomStateValueOrPromise(a, a.init, ensureAtomState);
}
else {
// NOTE invalid derived atoms can reach here
throw new Error('no atom init');
}
}
return returnAtomValue(aState);
}
// a !== atom
const aState = readAtomState(a);
try {
return returnAtomValue(aState);
}
finally {
atomState.d.set(a, aState.n);
mountedAtoms.get(a)?.t.add(effectAtom);
if (isSync) {
deps.add(a);
}
else {
if (mountedAtoms.has(a)) {
mountDependencies(effectAtom);
recomputeInvalidatedAtoms();
flushCallbacks();
}
}
}
};
getter.peek = store.get;
const setter = (a, ...args) => {
const aState = ensureAtomState(a);
try {
++inProgress;
if (isSelfAtom(effectAtom, a)) {
if (!hasInitialValue(a)) {
// NOTE technically possible but restricted as it may cause bugs
throw new Error('atom not writable');
}
const prevEpochNumber = aState.n;
const v = args[0];
setAtomStateValueOrPromise(a, v, ensureAtomState);
mountDependencies(a);
if (prevEpochNumber !== aState.n) {
changedAtoms.add(a);
storeHooks.c?.(a);
invalidateDependents(a);
}
return undefined;
}
else {
return writeAtomState(a, ...args);
}
}
finally {
if (!isSync) {
recomputeInvalidatedAtoms();
flushCallbacks();
}
--inProgress;
}
};
setter.recurse = (a, ...args) => {
if (fromCleanup) {
if (isDev()) {
throw new Error('set.recurse is not allowed in cleanup');
}
return undefined;
}
try {
isRecursing = true;
mountDependencies(effectAtom);
return setter(a, ...args);
}
finally {
recomputeInvalidatedAtoms();
isRecursing = false;
if (hasChanged) {
hasChanged = false;
runEffect();
}
}
};
try {
runCleanup?.();
const cleanup = effectAtom.effect(getter, setter);
if (typeof cleanup !== 'function') {
return;
}
runCleanup = () => {
if (inProgress) {
return;
}
try {
isSync = true;
fromCleanup = true;
return cleanup();
}
finally {
isSync = false;
fromCleanup = false;
runCleanup = undefined;
}
};
}
finally {
isSync = false;
deps.forEach((depAtom) => {
atomState.d.set(depAtom, ensureAtomState(depAtom).n);
});
mountDependencies(effectAtom);
recomputeInvalidatedAtoms();
}
}
const [mountedAtoms, changedAtoms, storeHooks, ensureAtomState, readAtomState, writeAtomState, mountDependencies, invalidateDependents, recomputeInvalidatedAtoms, flushCallbacks,] = getBuildingBlocks(store);
const atomEffectChannel = ensureAtomEffectChannel(store);
const atomState = ensureAtomState(effectAtom);
// initialize atomState
atomState.v = undefined;
Object.assign(store.get(refAtom), [deps, atomState, mountedAtoms]);
storeHooks.m.add(effectAtom, function atomOnMount() {
// mounted
atomEffectChannel.add(runEffect);
if (runCleanup) {
atomEffectChannel.delete(runCleanup);
}
});
storeHooks.u.add(effectAtom, function atomOnUnmount() {
// unmounted
atomEffectChannel.delete(runEffect);
if (runCleanup) {
atomEffectChannel.add(runCleanup);
}
});
storeHooks.c.add(effectAtom, function atomOnUpdate() {
// changed
if (isRecursing) {
hasChanged = true;
}
else {
atomEffectChannel.add(runEffect);
}
});
};
if (isDev()) {
Object.defineProperty(refAtom, 'debugLabel', {
get: () => effectAtom.debugLabel ? `${effectAtom.debugLabel}:ref` : undefined,
});
refAtom.debugPrivate = true;
}
return effectAtom;
}
const atomEffectChannelStoreMap = new WeakMap();
function ensureAtomEffectChannel(store) {
const storeHooks = getBuildingBlocks(store)[2];
let atomEffectChannel = atomEffectChannelStoreMap.get(store);
if (!atomEffectChannel) {
atomEffectChannel = new Set();
atomEffectChannelStoreMap.set(store, atomEffectChannel);
storeHooks.f.add(function storeOnFlush() {
// flush
for (const fn of atomEffectChannel) {
atomEffectChannel.delete(fn);
fn();
}
});
}
return atomEffectChannel;
}
//# sourceMappingURL=atomEffect.js.map