diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 73eb9fb06c..d1616a24b5 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -201,7 +201,7 @@ const registerBatchAtom = ( if (!batch.D.has(atom)) { batch.D.set(atom, new Set()) addBatchFuncMedium(batch, () => { - atomState.m?.l.forEach((listener) => listener()) + atomState.m?.l.forEach((listener) => addBatchFuncMedium(batch, listener)) }) } } @@ -220,12 +220,6 @@ const addBatchAtomDependent = ( const getBatchAtomDependents = (batch: Batch, atom: AnyAtom) => batch.D.get(atom) -const copySetAndClear = (origSet: Set): Set => { - const newSet = new Set(origSet) - origSet.clear() - return newSet -} - const flushBatch = (batch: Batch) => { let error: AnyError let hasError = false @@ -241,9 +235,12 @@ const flushBatch = (batch: Batch) => { } while (batch.M.size || batch.L.size) { batch.D.clear() - copySetAndClear(batch.H).forEach(call) - copySetAndClear(batch.M).forEach(call) - copySetAndClear(batch.L).forEach(call) + batch.H.forEach(call) + batch.H.clear() + batch.M.forEach(call) + batch.M.clear() + batch.L.forEach(call) + batch.L.clear() } if (hasError) { throw error diff --git a/tests/vanilla/dependency.test.tsx b/tests/vanilla/dependency.test.tsx index 370054e4d9..90f0c114e7 100644 --- a/tests/vanilla/dependency.test.tsx +++ b/tests/vanilla/dependency.test.tsx @@ -1,9 +1,5 @@ import { expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' -import type { - INTERNAL_DevStoreRev4, - INTERNAL_PrdStore, -} from 'jotai/vanilla/store' it('can propagate updates with async atom chains', async () => { const store = createStore() @@ -254,47 +250,30 @@ it('settles never resolving async derivations with deps picked up async', async expect(sub).toBe(1) }) -it.only('refreshes deps for each async read', async () => { - const store = createStore().unstable_derive((getAtomState, ...rest) => [ - (a) => Object.assign(getAtomState(a), { label: a.debugLabel }), - ...rest, - ]) as INTERNAL_DevStoreRev4 & INTERNAL_PrdStore - const getAtomState = store.dev4_get_internal_weak_map().get - - const a = atom(0) - a.debugLabel = 'a' - const b = atom(false) - b.debugLabel = 'b' +it('refreshes deps for each async read', async () => { + const countAtom = atom(0) + const depAtom = atom(false) const resolve: (() => void)[] = [] const values: number[] = [] - const c = atom(async (get) => { - const v = get(a) - values.push(v) - if (v === 0) { - get(b) + const asyncAtom = atom(async (get) => { + const count = get(countAtom) + values.push(count) + if (count === 0) { + get(depAtom) } await new Promise((r) => resolve.push(r)) - return v + return count }) - c.debugLabel = 'c' - - await new Promise((r) => setTimeout(r)) - store.get(c) - store.set(a, (c) => c + 1) - resolve.pop()!() - await new Promise((r) => setTimeout(r)) - await new Promise((r) => setTimeout(r)) - await new Promise((r) => setTimeout(r)) - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() - const v = await store.get(c) // freezes - expect(v).toBe(1) - store.set(b, true) - store.get(c) - resolve.pop()!() + const store = createStore() + store.get(asyncAtom) + store.set(countAtom, (c) => c + 1) + resolve.splice(0).forEach((fn) => fn()) + expect(await store.get(asyncAtom)).toBe(1) + store.set(depAtom, true) + store.get(asyncAtom) + resolve.splice(0).forEach((fn) => fn()) expect(values).toEqual([0, 1]) -}, 500) +}) it('should not re-evaluate stable derived atom values in situations where dependencies are re-ordered (#2738)', () => { const callCounter = vi.fn() diff --git a/tests/vanilla/store.test.tsx b/tests/vanilla/store.test.tsx index 7774af7333..5a186b0a3f 100644 --- a/tests/vanilla/store.test.tsx +++ b/tests/vanilla/store.test.tsx @@ -2,10 +2,6 @@ import { waitFor } from '@testing-library/react' import { assert, describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' import type { Atom, Getter, PrimitiveAtom } from 'jotai/vanilla' -import type { - INTERNAL_DevStoreRev4, - INTERNAL_PrdStore, -} from 'jotai/vanilla/store' it('should not fire on subscribe', async () => { const store = createStore() @@ -316,52 +312,6 @@ it('should update derived atoms during write (#2107)', async () => { expect(store.get(countAtom)).toBe(2) }) -it.only('mounts dependencies in async edge case', async () => { - const store = createStore().unstable_derive((getAtomState, ...rest) => [ - (a) => Object.assign(getAtomState(a), { label: a.debugLabel }), - ...rest, - ]) as INTERNAL_DevStoreRev4 & INTERNAL_PrdStore - const getAtomState = store.dev4_get_internal_weak_map().get - - const a = atom(0) - a.debugLabel = 'a' - const resolve: (() => void)[] = [] - const b = atom((get) => { - get(a) - return new Promise((r) => { - resolve.push(() => { - r() - }) - }) - }) - b.debugLabel = 'b' - const c = atom(async (get) => { - await Promise.resolve() - await get(b) - }) - c.debugLabel = 'c' - - store.sub(c, () => {}) - - await Promise.resolve() - expect(resolve.length).toBe(1) - resolve[0]!() - // --- Need to wait two microtasks to make it work --- - await Promise.resolve() - await Promise.resolve() - const aState = getAtomState(a) - const bState = getAtomState(b) - const cState = getAtomState(c) - console.log('aState', aState) - console.log('bState', bState) - console.log('cState', cState) - - store.set(a, 20) - store.set(a, 30) - await Promise.resolve() - expect(resolve.length).toBe(3) -}) - it('resolves dependencies reliably after a delay (#2192)', async () => { expect.assertions(1) const countAtom = atom(0) @@ -390,9 +340,6 @@ it('resolves dependencies reliably after a delay (#2192)', async () => { await waitFor(() => assert(resolve.length === 1)) resolve[0]!() - await Promise.resolve() - await Promise.resolve() - await Promise.resolve() const increment = (c: number) => c + 1 store.set(countAtom, increment) store.set(countAtom, increment) @@ -1036,3 +983,25 @@ it('mounted atom should be recomputed eagerly', () => { store.set(a, 1) expect(result).toEqual(['bRead', 'aCallback', 'bCallback']) }) + +it('should process all atom listeners even if some of them throw errors', () => { + const store = createStore() + const a = atom(0) + const listenerA = vi.fn() + const listenerB = vi.fn(() => { + throw new Error('error') + }) + const listenerC = vi.fn() + + store.sub(a, listenerA) + store.sub(a, listenerB) + store.sub(a, listenerC) + try { + store.set(a, 1) + } catch { + // expect empty + } + expect(listenerA).toHaveBeenCalledTimes(1) + expect(listenerB).toHaveBeenCalledTimes(1) + expect(listenerC).toHaveBeenCalledTimes(1) +})