diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 3684c00e65c..ad0c708c5e2 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -454,24 +454,35 @@ const buildStore = ( epochNumber: number, ])[] = [] const markedAtoms = new Set() - const visit = (a: AnyAtom, aState: AtomState) => { - if (markedAtoms.has(a)) { - return + // Visit the root atom. This is the only atom in the dependency graph + // without incoming edges, which is one reason we can simplify the algorithm + const stack: [a: AnyAtom, aState: AtomState, visited?: true][] = [ + [atom, atomState], + ] + while (stack.length > 0) { + const current = stack[stack.length - 1]! + const [a, aState, visited] = current + if (visited) { + // All dependents have been processed, now process this atom + stack.pop() + // The algorithm calls for pushing onto the front of the list. For + // performance, we will simply push onto the end, and then will iterate in + // reverse order later. + topsortedAtoms.push([a, aState, aState.n]) + continue } - markedAtoms.add(a) - for (const [d, s] of getDependents(pending, a, aState)) { - if (a !== d) { - visit(d, s) + if (!markedAtoms.has(a)) { + markedAtoms.add(a) + // Push unvisited dependents onto the stack + for (const [d, s] of getDependents(pending, a, aState)) { + if (a !== d && !markedAtoms.has(d)) { + stack.push([d, s]) + } } } - // The algorithm calls for pushing onto the front of the list. For - // performance, we will simply push onto the end, and then will iterate in - // reverse order later. - topsortedAtoms.push([a, aState, aState.n]) + // Atom has been visited but not yet processed + current[2] = true } - // Visit the root atom. This is the only atom in the dependency graph - // without incoming edges, which is one reason we can simplify the algorithm - visit(atom, atomState) // Step 2: use the topsorted atom list to recompute all affected atoms // Track what's changed, so that we can short circuit when possible const changedAtoms = new Set([atom]) diff --git a/tests/vanilla/store.test.tsx b/tests/vanilla/store.test.tsx index 8a765240a06..9de2b722ab5 100644 --- a/tests/vanilla/store.test.tsx +++ b/tests/vanilla/store.test.tsx @@ -876,3 +876,43 @@ describe('should mount and trigger listeners even when an error is thrown', () = expect(listener).toHaveBeenCalledOnce() }) }) + +it('processes deep atom a graph beyond maxDepth', () => { + function getMaxDepth() { + let depth = 0 + function d() { + ++depth + try { + return d() + } catch (error) { + return depth + } + } + return d() + } + const maxDepth = getMaxDepth() + 1 + const store = createStore() + const baseAtom = atom(0) + const atoms = [ + atom( + (get) => get(baseAtom), + (_, set) => set(baseAtom, (v) => ++v), + ), + ] + Array.from({ length: maxDepth }, (_, i) => { + const prevAtom = atoms[i]! + const a = atom( + (get) => get(prevAtom), + (get, set) => (get(prevAtom), set(prevAtom)), + ) + a.onMount = () => () => {} + atoms.push(a) + store.sub(a, () => {}) + }) + const lastAtom = atoms[maxDepth]! + expect(() => store.get(lastAtom)).not.toThrow() + expect(() => store.sub(lastAtom, () => {})).not.toThrow() + expect(() => store.set(baseAtom, 1)).not.toThrow() + expect(() => store.get(lastAtom)).not.toThrow() + // store.set(lastAtom) // FIXME: This is causing a stack overflow +})