jotai-effect
Version:
127 lines (122 loc) • 4.24 kB
text/typescript
import type { Atom, WritableAtom } from 'jotai/vanilla'
import {
INTERNAL_getBuildingBlocksRev2 as getBuildingBlocks,
INTERNAL_initializeStoreHooksRev2 as initializeStoreHooks,
} from 'jotai/vanilla/internals'
import type { Effect, GetterWithPeek, SetterWithRecurse } from './atomEffect'
import { atomEffect } from './atomEffect'
import { isDev } from './env'
export function withAtomEffect<T extends Atom<unknown>>(
targetAtom: T,
effect: Effect
): T & { effect: Effect } {
const proto = Object.getPrototypeOf(targetAtom)
const desc = Object.getOwnPropertyDescriptors(targetAtom)
let depth = 0
desc.read.value = function read(get, options) {
try {
++depth
// handles case when withAtomEffect is nested
const context = depth === 1 ? targetAtom : this
return targetAtom.read.call(context, get, options)
} finally {
--depth
}
}
if (isWritableAtom(targetAtom)) {
desc.write!.value = function write(this: T, get, set, ...args) {
try {
++depth
const context = depth === 1 ? targetAtom : this
return targetAtom.write.call(context, get, set, ...args)
} finally {
--depth
}
} as (typeof targetAtom)['write']
}
const targetWithEffect: T & { effect: Effect } = Object.create(proto, desc)
targetWithEffect.INTERNAL_onInit = (store) => {
const buildingBlocks = getBuildingBlocks(store)
const invalidatedAtoms = buildingBlocks[2]
const storeHooks = initializeStoreHooks(buildingBlocks[6])
const ensureAtomState = buildingBlocks[11]
const flushCallbacks = buildingBlocks[12]
const readAtomState = buildingBlocks[14]
const invalidateDependents = buildingBlocks[15]
const mountDependencies = buildingBlocks[17]
const mountAtom = buildingBlocks[18]
const unmountAtom = buildingBlocks[19]
let inProgress = false
let isSubscribed = false
const effectAtom = atomEffect((get, set) => {
if (inProgress) {
return
}
isSubscribed = false
const getter: GetterWithPeek = (a) => {
if (a === targetWithEffect) {
isSubscribed = true
return get.peek(a)
}
return get(a)
}
getter.peek = get.peek
const setter: SetterWithRecurse = (a, ...args) => {
if (a === (targetWithEffect as any)) {
inProgress = true
return set(a, ...args)
}
return set(a, ...args)
}
setter.recurse = (...args) => {
inProgress = false
return set.recurse(...args)
}
return targetWithEffect.effect.call(targetAtom, getter, setter)
})
if (isDev()) {
Object.defineProperty(effectAtom, 'debugLabel', {
get: () => `${targetWithEffect.debugLabel ?? 'atom'}:effect`,
})
effectAtom.debugPrivate = true
}
const effectAtomState = ensureAtomState(store, effectAtom)
const targetWithEffectAtomState = ensureAtomState(store, targetWithEffect)
storeHooks.c.add(targetWithEffect, function atomChanged() {
if (isSubscribed) {
invalidatedAtoms.set(effectAtom, effectAtomState.n)
effectAtomState.d.set(targetWithEffect, targetWithEffectAtomState.n - 1)
readAtomState(store, effectAtom)
mountDependencies(store, effectAtom)
invalidatedAtoms.delete(effectAtom)
effectAtomState.d.delete(targetWithEffect)
}
})
storeHooks.m.add(targetWithEffect, function mountEffect() {
const atomState = ensureAtomState(store, targetWithEffect)
const { n } = atomState
mountAtom(store, effectAtom)
flushCallbacks(store)
if (n !== atomState.n) {
const unsub = storeHooks.f.add(() => {
invalidateDependents(store, targetWithEffect)
unsub()
})
}
})
storeHooks.u.add(targetWithEffect, function unmountEffect() {
unmountAtom(store, effectAtom)
flushCallbacks(store)
})
storeHooks.f.add(function flushEffect() {
inProgress = false
})
}
targetWithEffect.effect = effect
return targetWithEffect
}
function isWritableAtom(
atom: Atom<unknown>
): atom is WritableAtom<unknown, any[], any> {
return 'write' in atom && typeof atom.write === 'function'
}