Skip to content

Commit

Permalink
refactor(store): Replace runWithTransactions to flushCallbacks (#2946)
Browse files Browse the repository at this point in the history
* simplify store

* prevent recursive flushCallbacks

* short-circuit invalidateDependents an already invalidated

---------

Co-authored-by: Daishi Kato <[email protected]>
  • Loading branch information
dmaskasky and dai-shi authored Jan 19, 2025
1 parent e98b58a commit 81131dc
Showing 1 changed file with 74 additions and 48 deletions.
122 changes: 74 additions & 48 deletions src/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
const changedAtoms = new Map<AnyAtom, AtomState>()
const unmountCallbacks = new Set<() => void>()
const mountCallbacks = new Set<() => void>()

let inTransaction = 0
const runWithTransaction = <T>(fn: () => T): T => {

const flushCallbacks = () => {
const errors: unknown[] = []
const call = (fn: () => void) => {
try {
Expand All @@ -258,36 +258,28 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
errors.push(e)
}
}
let result: T
++inTransaction
try {
result = fn()
} finally {
if (inTransaction === 1) {
while (
changedAtoms.size ||
unmountCallbacks.size ||
mountCallbacks.size
) {
recomputeInvalidatedAtoms()
;(store as any)[INTERNAL_flushStoreHook]?.()
const callbacks = new Set<() => void>()
const add = callbacks.add.bind(callbacks)
changedAtoms.forEach((atomState) => atomState.m?.l.forEach(add))
changedAtoms.clear()
unmountCallbacks.forEach(add)
unmountCallbacks.clear()
mountCallbacks.forEach(add)
mountCallbacks.clear()
callbacks.forEach(call)
}
while (changedAtoms.size || unmountCallbacks.size || mountCallbacks.size) {
recomputeInvalidatedAtoms()
if (inTransaction > 1) {
--inTransaction
return
}
--inTransaction
;(store as any)[INTERNAL_flushStoreHook]?.()
const callbacks = new Set<() => void>()
const add = callbacks.add.bind(callbacks)
changedAtoms.forEach((atomState) => atomState.m?.l.forEach(add))
changedAtoms.clear()
unmountCallbacks.forEach(add)
unmountCallbacks.clear()
mountCallbacks.forEach(add)
mountCallbacks.clear()
callbacks.forEach(call)
}
--inTransaction
if (errors.length) {
throw errors[0]
}
return result
}

const setAtomStateValueOrPromise = (
Expand Down Expand Up @@ -344,7 +336,8 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
let isSync = true
const mountDependenciesIfAsync = () => {
if (atomState.m) {
runWithTransaction(() => mountDependencies(atom, atomState))
mountDependencies(atom, atomState)
flushCallbacks()
}
}
const getter: Getter = <V>(a: Atom<V>) => {
Expand Down Expand Up @@ -440,16 +433,18 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
return dependents
}

const invalidateDependents = <Value>(atomState: AtomState<Value>) => {
const invalidateDependents = (atomState: AtomState) => {
const visited = new WeakSet<AtomState>()
const stack: AtomState[] = [atomState]
while (stack.length) {
const aState = stack.pop()!
if (!visited.has(aState)) {
visited.add(aState)
for (const [d, s] of getMountedOrPendingDependents(aState)) {
invalidatedAtoms.set(d, s.n)
stack.push(s)
if (!invalidatedAtoms.has(d)) {
invalidatedAtoms.set(d, s.n)
stack.push(s)
}
}
}
}
Expand Down Expand Up @@ -529,13 +524,14 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
atom: WritableAtom<Value, Args, Result>,
...args: Args
): Result => {
let isSync = true
const getter: Getter = <V>(a: Atom<V>) => returnAtomValue(readAtomState(a))
const setter: Setter = <V, As extends unknown[], R>(
a: WritableAtom<V, As, R>,
...args: As
) => {
const aState = ensureAtomState(a)
return runWithTransaction(() => {
try {
if (isSelfAtom(atom, a)) {
if (!hasInitialValue(a)) {
// NOTE technically possible but restricted as it may cause bugs
Expand All @@ -554,15 +550,29 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
} else {
return writeAtomState(a, ...args)
}
})
} finally {
if (!isSync) {
flushCallbacks()
}
}
}
try {
return atomWrite(atom, getter, setter, ...args)
} finally {
isSync = false
}
return atomWrite(atom, getter, setter, ...args)
}

const writeAtom = <Value, Args extends unknown[], Result>(
atom: WritableAtom<Value, Args, Result>,
...args: Args
): Result => runWithTransaction(() => writeAtomState(atom, ...args))
): Result => {
try {
return writeAtomState(atom, ...args)
} finally {
flushCallbacks()
}
}

const mountDependencies = (atom: AnyAtom, atomState: AtomState) => {
if (atomState.m && !isPendingPromise(atomState.v)) {
Expand Down Expand Up @@ -604,12 +614,30 @@ const buildStore = (...storeArgs: StoreArgs): Store => {
atomState.h?.()
if (isActuallyWritableAtom(atom)) {
const mounted = atomState.m
let setAtom: (...args: unknown[]) => unknown
const createInvocationContext = <T>(fn: () => T) => {
let isSync = true
setAtom = (...args: unknown[]) => {
try {
return writeAtomState(atom, ...args)
} finally {
if (!isSync) {
flushCallbacks()
}
}
}
try {
return fn()
} finally {
isSync = false
}
}
const processOnMount = () => {
const onUnmount = atomOnMount(atom, (...args) =>
runWithTransaction(() => writeAtomState(atom, ...args)),
const onUnmount = createInvocationContext(() =>
atomOnMount(atom, (...args) => setAtom(...args)),
)
if (onUnmount) {
mounted.u = onUnmount
mounted.u = () => createInvocationContext(onUnmount)
}
}
mountCallbacks.add(processOnMount)
Expand Down Expand Up @@ -646,17 +674,15 @@ const buildStore = (...storeArgs: StoreArgs): Store => {

const subscribeAtom = (atom: AnyAtom, listener: () => void) => {
const atomState = ensureAtomState(atom)
return runWithTransaction(() => {
const mounted = mountAtom(atom, atomState)
const listeners = mounted.l
listeners.add(listener)
return () => {
runWithTransaction(() => {
listeners.delete(listener)
unmountAtom(atom, atomState)
})
}
})
const mounted = mountAtom(atom, atomState)
const listeners = mounted.l
listeners.add(listener)
flushCallbacks()
return () => {
listeners.delete(listener)
unmountAtom(atom, atomState)
flushCallbacks()
}
}

const unstable_derive: Store['unstable_derive'] = (fn) =>
Expand Down

0 comments on commit 81131dc

Please sign in to comment.