Skip to content

Commit

Permalink
process all batch functions even on error
Browse files Browse the repository at this point in the history
  • Loading branch information
dmaskasky committed Dec 18, 2024
1 parent ec3ff5e commit fea7fa0
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 99 deletions.
17 changes: 7 additions & 10 deletions src/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
Expand All @@ -220,12 +220,6 @@ const addBatchAtomDependent = (
const getBatchAtomDependents = (batch: Batch, atom: AnyAtom) =>
batch.D.get(atom)

const copySetAndClear = <T>(origSet: Set<T>): Set<T> => {
const newSet = new Set(origSet)
origSet.clear()
return newSet
}

const flushBatch = (batch: Batch) => {
let error: AnyError
let hasError = false
Expand All @@ -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
Expand Down
57 changes: 18 additions & 39 deletions tests/vanilla/dependency.test.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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<void>((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()
Expand Down
75 changes: 25 additions & 50 deletions tests/vanilla/store.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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<void>((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)
Expand Down Expand Up @@ -1036,3 +986,28 @@ 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()
const listenerD = vi.fn()

store.sub(a, listenerA)
store.sub(a, listenerB)
store.sub(a, listenerC)
store.sub(a, listenerD)
try {
store.set(a, 1)
} catch {
// expect empty
}
expect(listenerA).toHaveBeenCalledTimes(1)
expect(listenerB).toHaveBeenCalledTimes(1)
expect(listenerC).toHaveBeenCalledTimes(1)
expect(listenerD).toHaveBeenCalledTimes(1)
})

0 comments on commit fea7fa0

Please sign in to comment.