From e777f469586d685d864d9120274e83db4fbad4c8 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Fri, 20 Sep 2024 09:17:31 +0900 Subject: [PATCH] breaking(core): avoid continuable promise in store api (#2695) * add failing test for #2682 * wip: continuable promise in useAtomValue * wip: abortable promise * wip fix store test * wip: fix dependency test * wip: fix continuable promise * fix unwrap test * fix loadable tes * fix with attachPromiseMeta * refactor * eliminate abort controller for promise * small refactor * refactor * minor refactor * improvement: cancel handler receives next value --- src/react/useAtomValue.ts | 88 ++++++++++-- src/vanilla/store.ts | 196 ++++++++------------------- src/vanilla/utils/loadable.ts | 39 ++---- src/vanilla/utils/unwrap.ts | 26 +--- tests/react/async2.test.tsx | 100 ++++++++++++++ tests/vanilla/dependency.test.tsx | 24 +--- tests/vanilla/store.test.tsx | 73 ++++------ tests/vanilla/utils/loadable.test.ts | 4 + tests/vanilla/utils/unwrap.test.ts | 2 + 9 files changed, 293 insertions(+), 259 deletions(-) diff --git a/src/react/useAtomValue.ts b/src/react/useAtomValue.ts index 8c28b62ebd..72c9d6498e 100644 --- a/src/react/useAtomValue.ts +++ b/src/react/useAtomValue.ts @@ -10,6 +10,26 @@ type Store = ReturnType const isPromiseLike = (x: unknown): x is PromiseLike => typeof (x as any)?.then === 'function' +const attachPromiseMeta = ( + promise: PromiseLike & { + status?: 'pending' | 'fulfilled' | 'rejected' + value?: T + reason?: unknown + }, +) => { + promise.status = 'pending' + promise.then( + (v) => { + promise.status = 'fulfilled' + promise.value = v + }, + (e) => { + promise.status = 'rejected' + promise.reason = e + }, + ) +} + const use = ReactExports.use || (( @@ -26,21 +46,56 @@ const use = } else if (promise.status === 'rejected') { throw promise.reason } else { - promise.status = 'pending' - promise.then( - (v) => { - promise.status = 'fulfilled' - promise.value = v - }, - (e) => { - promise.status = 'rejected' - promise.reason = e - }, - ) + attachPromiseMeta(promise) throw promise } }) +const continuablePromiseMap = new WeakMap< + PromiseLike, + Promise +>() + +const createContinuablePromise = (promise: PromiseLike) => { + let continuablePromise = continuablePromiseMap.get(promise) + if (!continuablePromise) { + continuablePromise = new Promise((resolve, reject) => { + let curr = promise + const onFulfilled = (me: PromiseLike) => (v: T) => { + if (curr === me) { + resolve(v) + } + } + const onRejected = (me: PromiseLike) => (e: unknown) => { + if (curr === me) { + reject(e) + } + } + const registerCancelHandler = (p: PromiseLike) => { + if ('onCancel' in p && typeof p.onCancel === 'function') { + p.onCancel((nextValue: PromiseLike | T) => { + if (import.meta.env?.MODE !== 'production' && nextValue === p) { + throw new Error('[Bug] p is not updated even after cancelation') + } + if (isPromiseLike(nextValue)) { + continuablePromiseMap.set(nextValue, continuablePromise!) + curr = nextValue + nextValue.then(onFulfilled(nextValue), onRejected(nextValue)) + registerCancelHandler(nextValue) + } else { + resolve(nextValue) + } + }) + } + } + promise.then(onFulfilled(promise), onRejected(promise)) + registerCancelHandler(promise) + }) + continuablePromiseMap.set(promise, continuablePromise) + } + return continuablePromise +} + type Options = Parameters[0] & { delay?: number } @@ -88,6 +143,10 @@ export function useAtomValue(atom: Atom, options?: Options) { useEffect(() => { const unsub = store.sub(atom, () => { if (typeof delay === 'number') { + const value = store.get(atom) + if (isPromiseLike(value)) { + attachPromiseMeta(createContinuablePromise(value)) + } // delay rerendering to wait a promise possibly to resolve setTimeout(rerender, delay) return @@ -99,8 +158,11 @@ export function useAtomValue(atom: Atom, options?: Options) { }, [store, atom, delay]) useDebugValue(value) - // TS doesn't allow using `use` always. // The use of isPromiseLike is to be consistent with `use` type. // `instanceof Promise` actually works fine in this case. - return isPromiseLike(value) ? use(value) : (value as Awaited) + if (isPromiseLike(value)) { + const promise = createContinuablePromise(value) + return use(promise) + } + return value as Awaited } diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 7b04acdbae..07bbad31f4 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -20,92 +20,46 @@ const isActuallyWritableAtom = (atom: AnyAtom): atom is AnyWritableAtom => !!(atom as AnyWritableAtom).write // -// Continuable Promise +// Cancelable Promise // -const CONTINUE_PROMISE = Symbol( - import.meta.env?.MODE !== 'production' ? 'CONTINUE_PROMISE' : '', -) - -const PENDING = 'pending' -const FULFILLED = 'fulfilled' -const REJECTED = 'rejected' - -type ContinuePromise = ( - nextPromise: PromiseLike | undefined, - nextAbort: () => void, -) => void - -type ContinuablePromise = Promise & - ( - | { status: typeof PENDING } - | { status: typeof FULFILLED; value?: T } - | { status: typeof REJECTED; reason?: AnyError } - ) & { - [CONTINUE_PROMISE]: ContinuePromise - } +type CancelHandler = (nextValue: unknown) => void +type PromiseState = [cancelHandlers: Set, settled: boolean] -const isContinuablePromise = ( - promise: unknown, -): promise is ContinuablePromise => - typeof promise === 'object' && promise !== null && CONTINUE_PROMISE in promise +const cancelablePromiseMap = new WeakMap, PromiseState>() -const continuablePromiseMap: WeakMap< - PromiseLike, - ContinuablePromise -> = new WeakMap() +const isPendingPromise = (value: unknown): value is PromiseLike => + isPromiseLike(value) && !cancelablePromiseMap.get(value)?.[1] -/** - * Create a continuable promise from a regular promise. - */ -const createContinuablePromise = ( - promise: PromiseLike, - abort: () => void, - complete: () => void, -): ContinuablePromise => { - if (!continuablePromiseMap.has(promise)) { - let continuePromise: ContinuePromise - const p: any = new Promise((resolve, reject) => { - let curr = promise - const onFulfilled = (me: PromiseLike) => (v: T) => { - if (curr === me) { - p.status = FULFILLED - p.value = v - resolve(v) - complete() - } - } - const onRejected = (me: PromiseLike) => (e: AnyError) => { - if (curr === me) { - p.status = REJECTED - p.reason = e - reject(e) - complete() - } - } - promise.then(onFulfilled(promise), onRejected(promise)) - continuePromise = (nextPromise, nextAbort) => { - if (nextPromise) { - continuablePromiseMap.set(nextPromise, p) - curr = nextPromise - nextPromise.then(onFulfilled(nextPromise), onRejected(nextPromise)) - - // Only abort promises that aren't user-facing. When nextPromise is set, - // we can replace the current promise with the next one, so we don't - // see any abort-related errors. - abort() - abort = nextAbort - } - } - }) - p.status = PENDING - p[CONTINUE_PROMISE] = continuePromise! - continuablePromiseMap.set(promise, p) +const cancelPromise = (promise: PromiseLike, nextValue: unknown) => { + const promiseState = cancelablePromiseMap.get(promise) + if (promiseState) { + promiseState[1] = true + promiseState[0].forEach((fn) => fn(nextValue)) + } else if (import.meta.env?.MODE !== 'production') { + throw new Error('[Bug] cancelable promise not found') } - return continuablePromiseMap.get(promise) as ContinuablePromise } -const isPromiseLike = (x: unknown): x is PromiseLike => +const patchPromiseForCancelability = (promise: PromiseLike) => { + if (cancelablePromiseMap.has(promise)) { + // already patched + return + } + const promiseState: PromiseState = [new Set(), false] + cancelablePromiseMap.set(promise, promiseState) + const settle = () => { + promiseState[1] = true + } + promise.then(settle, settle) + ;(promise as { onCancel?: (fn: CancelHandler) => void }).onCancel = (fn) => { + promiseState[0].add(fn) + } +} + +const isPromiseLike = ( + x: unknown, +): x is PromiseLike & { onCancel?: (fn: CancelHandler) => void } => typeof (x as any)?.then === 'function' /** @@ -165,17 +119,9 @@ const returnAtomValue = (atomState: AtomState): Value => { return atomState.v! } -const getPendingContinuablePromise = (atomState: AtomState) => { - const value: unknown = atomState.v - if (isContinuablePromise(value) && value.status === PENDING) { - return value - } - return null -} - -const addPendingContinuablePromiseToDependency = ( +const addPendingPromiseToDependency = ( atom: AnyAtom, - promise: ContinuablePromise & { status: typeof PENDING }, + promise: PromiseLike, dependencyAtomState: AtomState, ) => { if (!dependencyAtomState.p.has(atom)) { @@ -202,9 +148,8 @@ const addDependency = ( throw new Error('[Bug] atom cannot depend on itself') } atomState.d.set(a, aState.n) - const continuablePromise = getPendingContinuablePromise(atomState) - if (continuablePromise) { - addPendingContinuablePromiseToDependency(atom, continuablePromise, aState) + if (isPendingPromise(atomState.v)) { + addPendingPromiseToDependency(atom, atomState.v, aState) } aState.m?.t.add(atom) if (pending) { @@ -312,48 +257,30 @@ const buildStore = (getAtomState: StoreArgs[0]): Store => { atom: AnyAtom, atomState: AtomState, valueOrPromise: unknown, - abortPromise = () => {}, - completePromise = () => {}, ) => { const hasPrevValue = 'v' in atomState const prevValue = atomState.v - const pendingPromise = getPendingContinuablePromise(atomState) + const pendingPromise = isPendingPromise(atomState.v) ? atomState.v : null if (isPromiseLike(valueOrPromise)) { - if (pendingPromise) { - if (pendingPromise !== valueOrPromise) { - pendingPromise[CONTINUE_PROMISE](valueOrPromise, abortPromise) - ++atomState.n - } - } else { - const continuablePromise = createContinuablePromise( + patchPromiseForCancelability(valueOrPromise) + for (const a of atomState.d.keys()) { + addPendingPromiseToDependency( + atom, valueOrPromise, - abortPromise, - completePromise, + getAtomState(a, atomState), ) - if (continuablePromise.status === PENDING) { - for (const a of atomState.d.keys()) { - addPendingContinuablePromiseToDependency( - atom, - continuablePromise, - getAtomState(a, atomState), - ) - } - } - atomState.v = continuablePromise - delete atomState.e } + atomState.v = valueOrPromise + delete atomState.e } else { - if (pendingPromise) { - pendingPromise[CONTINUE_PROMISE]( - Promise.resolve(valueOrPromise), - abortPromise, - ) - } atomState.v = valueOrPromise delete atomState.e } if (!hasPrevValue || !Object.is(prevValue, atomState.v)) { ++atomState.n + if (pendingPromise) { + cancelPromise(pendingPromise, valueOrPromise) + } } } @@ -448,19 +375,18 @@ const buildStore = (getAtomState: StoreArgs[0]): Store => { } try { const valueOrPromise = atom.read(getter, options as never) - setAtomStateValueOrPromise( - atom, - atomState, - valueOrPromise, - () => controller?.abort(), - () => { + setAtomStateValueOrPromise(atom, atomState, valueOrPromise) + if (isPromiseLike(valueOrPromise)) { + valueOrPromise.onCancel?.(() => controller?.abort()) + const complete = () => { if (atomState.m) { const pending = createPending() mountDependencies(pending, atom, atomState) flushPending(pending) } - }, - ) + } + valueOrPromise.then(complete, complete) + } return atomState } catch (error) { delete atomState.v @@ -484,10 +410,10 @@ const buildStore = (getAtomState: StoreArgs[0]): Store => { for (const a of atomState.m?.t || []) { dependents.set(a, getAtomState(a, atomState)) } - for (const atomWithPendingContinuablePromise of atomState.p) { + for (const atomWithPendingPromise of atomState.p) { dependents.set( - atomWithPendingContinuablePromise, - getAtomState(atomWithPendingContinuablePromise, atomState), + atomWithPendingPromise, + getAtomState(atomWithPendingPromise, atomState), ) } getPendingDependents(pending, atom)?.forEach((dependent) => { @@ -609,7 +535,7 @@ const buildStore = (getAtomState: StoreArgs[0]): Store => { atom: AnyAtom, atomState: AtomState, ) => { - if (atomState.m && !getPendingContinuablePromise(atomState)) { + if (atomState.m && !isPendingPromise(atomState.v)) { for (const a of atomState.d.keys()) { if (!atomState.m.d.has(a)) { const aMounted = mountAtom(pending, a, getAtomState(a, atomState)) @@ -691,12 +617,6 @@ const buildStore = (getAtomState: StoreArgs[0]): Store => { const aMounted = unmountAtom(pending, a, getAtomState(a, atomState)) aMounted?.t.delete(atom) } - // abort pending promise - const pendingPromise = getPendingContinuablePromise(atomState) - if (pendingPromise) { - // FIXME using `undefined` is kind of a hack. - pendingPromise[CONTINUE_PROMISE](undefined, () => {}) - } return undefined } return atomState.m diff --git a/src/vanilla/utils/loadable.ts b/src/vanilla/utils/loadable.ts index e2c7401600..135bfccd56 100644 --- a/src/vanilla/utils/loadable.ts +++ b/src/vanilla/utils/loadable.ts @@ -5,14 +5,8 @@ const cache1 = new WeakMap() const memo1 = (create: () => T, dep1: object): T => (cache1.has(dep1) ? cache1 : cache1.set(dep1, create())).get(dep1) -type PromiseMeta = - | { status?: 'pending' } - | { status: 'fulfilled'; value: Awaited } - | { status: 'rejected'; reason: unknown } - -const isPromise = ( - x: unknown, -): x is Promise> & PromiseMeta => x instanceof Promise +const isPromise = (x: unknown): x is Promise> => + x instanceof Promise export type Loadable = | { state: 'loading' } @@ -50,25 +44,16 @@ export function loadable(anAtom: Atom): Atom> { if (cached1) { return cached1 } - if (promise.status === 'fulfilled') { - loadableCache.set(promise, { state: 'hasData', data: promise.value }) - } else if (promise.status === 'rejected') { - loadableCache.set(promise, { - state: 'hasError', - error: promise.reason, - }) - } else { - promise - .then( - (data) => { - loadableCache.set(promise, { state: 'hasData', data }) - }, - (error) => { - loadableCache.set(promise, { state: 'hasError', error }) - }, - ) - .finally(setSelf) - } + promise + .then( + (data) => { + loadableCache.set(promise, { state: 'hasData', data }) + }, + (error) => { + loadableCache.set(promise, { state: 'hasError', error }) + }, + ) + .finally(setSelf) const cached2 = loadableCache.get(promise) if (cached2) { return cached2 diff --git a/src/vanilla/utils/unwrap.ts b/src/vanilla/utils/unwrap.ts index fe6269636d..3313980171 100644 --- a/src/vanilla/utils/unwrap.ts +++ b/src/vanilla/utils/unwrap.ts @@ -9,13 +9,7 @@ const memo2 = (create: () => T, dep1: object, dep2: object): T => { return getCached(create, cache2, dep2) } -type PromiseMeta = - | { status?: 'pending' } - | { status: 'fulfilled'; value: unknown } - | { status: 'rejected'; reason: unknown } - -const isPromise = (x: unknown): x is Promise & PromiseMeta => - x instanceof Promise +const isPromise = (x: unknown): x is Promise => x instanceof Promise const defaultFallback = () => undefined @@ -66,18 +60,12 @@ export function unwrap( return { v: promise as Awaited } } if (promise !== prev?.p) { - if (promise.status === 'fulfilled') { - promiseResultCache.set(promise, promise.value as Awaited) - } else if (promise.status === 'rejected') { - promiseErrorCache.set(promise, promise.reason) - } else { - promise - .then( - (v) => promiseResultCache.set(promise, v as Awaited), - (e) => promiseErrorCache.set(promise, e), - ) - .finally(setSelf) - } + promise + .then( + (v) => promiseResultCache.set(promise, v as Awaited), + (e) => promiseErrorCache.set(promise, e), + ) + .finally(setSelf) } if (promiseErrorCache.has(promise)) { throw promiseErrorCache.get(promise) diff --git a/tests/react/async2.test.tsx b/tests/react/async2.test.tsx index 0578f713d0..4bfcede5e0 100644 --- a/tests/react/async2.test.tsx +++ b/tests/react/async2.test.tsx @@ -267,3 +267,103 @@ describe('infinite pending', () => { await findByText('count: 3') }) }) + +describe('write to async atom twice', async () => { + it('no wait', async () => { + const asyncAtom = atom(Promise.resolve(2)) + const writer = atom(null, async (get, set) => { + set(asyncAtom, async (c) => (await c) + 1) + set(asyncAtom, async (c) => (await c) + 1) + return get(asyncAtom) + }) + + const Component = () => { + const count = useAtomValue(asyncAtom) + const write = useSetAtom(writer) + return ( + <> +
count: {count}
+ + + ) + } + + const { findByText, getByText } = render( + + + + + , + ) + + await findByText('count: 2') + await userEvent.click(getByText('button')) + await findByText('count: 4') + }) + + it('wait Promise.resolve()', async () => { + const asyncAtom = atom(Promise.resolve(2)) + const writer = atom(null, async (get, set) => { + set(asyncAtom, async (c) => (await c) + 1) + await Promise.resolve() + set(asyncAtom, async (c) => (await c) + 1) + return get(asyncAtom) + }) + + const Component = () => { + const count = useAtomValue(asyncAtom) + const write = useSetAtom(writer) + return ( + <> +
count: {count}
+ + + ) + } + + const { findByText, getByText } = render( + + + + + , + ) + + await findByText('count: 2') + await userEvent.click(getByText('button')) + await findByText('count: 4') + }) + + it('wait setTimeout()', async () => { + const asyncAtom = atom(Promise.resolve(2)) + const writer = atom(null, async (get, set) => { + set(asyncAtom, async (c) => (await c) + 1) + await new Promise((r) => setTimeout(r)) + set(asyncAtom, async (c) => (await c) + 1) + return get(asyncAtom) + }) + + const Component = () => { + const count = useAtomValue(asyncAtom) + const write = useSetAtom(writer) + return ( + <> +
count: {count}
+ + + ) + } + + const { findByText, getByText } = render( + + + + + , + ) + + await findByText('count: 2') + await userEvent.click(getByText('button')) + await findByText('count: 4') + }) +}) diff --git a/tests/vanilla/dependency.test.tsx b/tests/vanilla/dependency.test.tsx index ec368e1f8c..a021c3c80e 100644 --- a/tests/vanilla/dependency.test.tsx +++ b/tests/vanilla/dependency.test.tsx @@ -201,25 +201,18 @@ it('settles never resolving async derivations with deps picked up sync', async ( let sub = 0 const values: unknown[] = [] store.get(asyncAtom).then((value) => values.push(value)) - store.sub(asyncAtom, () => { sub++ store.get(asyncAtom).then((value) => values.push(value)) }) - await new Promise((r) => setTimeout(r)) - store.set(syncAtom, { promise: new Promise((r) => resolve.push(r)), }) - - await new Promise((r) => setTimeout(r)) - resolve[1]?.(1) - await new Promise((r) => setTimeout(r)) - - expect(values).toEqual([1, 1]) + await new Promise((r) => setTimeout(r)) // wait for a tick + expect(values).toEqual([1]) expect(sub).toBe(1) }) @@ -233,7 +226,6 @@ it('settles never resolving async derivations with deps picked up async', async const asyncAtom = atom(async (get) => { // we want to pick up `syncAtom` as an async dep await Promise.resolve() - return await get(syncAtom).promise }) @@ -242,25 +234,19 @@ it('settles never resolving async derivations with deps picked up async', async let sub = 0 const values: unknown[] = [] store.get(asyncAtom).then((value) => values.push(value)) - store.sub(asyncAtom, () => { sub++ store.get(asyncAtom).then((value) => values.push(value)) }) - await new Promise((r) => setTimeout(r)) - + await new Promise((r) => setTimeout(r)) // wait for a tick store.set(syncAtom, { promise: new Promise((r) => resolve.push(r)), }) - - await new Promise((r) => setTimeout(r)) - resolve[1]?.(1) - await new Promise((r) => setTimeout(r)) - - expect(values).toEqual([1, 1]) + await new Promise((r) => setTimeout(r)) // wait for a tick + expect(values).toEqual([1]) expect(sub).toBe(1) }) diff --git a/tests/vanilla/store.test.tsx b/tests/vanilla/store.test.tsx index 4514cc8a3c..59ccb0672a 100644 --- a/tests/vanilla/store.test.tsx +++ b/tests/vanilla/store.test.tsx @@ -87,9 +87,11 @@ it('should override a promise by setting', async () => { const countAtom = atom(Promise.resolve(0)) const infinitePending = new Promise(() => {}) store.set(countAtom, infinitePending) - const promise = store.get(countAtom) + const promise1 = store.get(countAtom) + expect(promise1).toBe(infinitePending) store.set(countAtom, Promise.resolve(1)) - expect(await promise).toBe(1) + const promise2 = store.get(countAtom) + expect(await promise2).toBe(1) }) it('should update async atom with deps after await (#1905)', async () => { @@ -417,31 +419,37 @@ it('should flush pending write triggered asynchronously and indirectly (#2451)', describe('async atom with subtle timing', () => { it('case 1', async () => { const store = createStore() - let resolve = () => {} + const resolve: (() => void)[] = [] const a = atom(1) const b = atom(async (get) => { - await new Promise((r) => (resolve = r)) + await new Promise((r) => resolve.push(r)) return get(a) }) const bValue = store.get(b) store.set(a, 2) - resolve() + resolve.splice(0).forEach((fn) => fn()) + const bValue2 = store.get(b) + resolve.splice(0).forEach((fn) => fn()) expect(await bValue).toBe(2) + expect(await bValue2).toBe(2) }) it('case 2', async () => { const store = createStore() - let resolve = () => {} + const resolve: (() => void)[] = [] const a = atom(1) const b = atom(async (get) => { const aValue = get(a) - await new Promise((r) => (resolve = r)) + await new Promise((r) => resolve.push(r)) return aValue }) const bValue = store.get(b) store.set(a, 2) - resolve() - expect(await bValue).toBe(2) + resolve.splice(0).forEach((fn) => fn()) + const bValue2 = store.get(b) + resolve.splice(0).forEach((fn) => fn()) + expect(await bValue).toBe(1) // returns old value + expect(await bValue2).toBe(2) }) }) @@ -458,32 +466,26 @@ describe('aborting atoms', () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() - let resolve = () => {} + const resolve: (() => void)[] = [] const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) - - await new Promise((r) => (resolve = r)) - + await new Promise((r) => resolve.push(r)) callBeforeAbort() - throwIfAborted(signal) - callAfterAbort() - return aVal + 1 }) const promise = store.get(derivedAtom) - const firstResolve = resolve store.set(a, 3) + const promise2 = store.get(derivedAtom) - firstResolve() - resolve() - expect(await promise).toEqual(4) - + resolve.splice(0).forEach((fn) => fn()) + expect(promise).rejects.toThrow('aborted') + expect(await promise2).toEqual(4) expect(callBeforeAbort).toHaveBeenCalledTimes(2) expect(callAfterAbort).toHaveBeenCalledTimes(1) }) @@ -492,33 +494,24 @@ describe('aborting atoms', () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() - let resolve = () => {} + const resolve: (() => void)[] = [] const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) - - await new Promise((r) => (resolve = r)) - + await new Promise((r) => resolve.push(r)) callBeforeAbort() - throwIfAborted(signal) - callAfterAbort() - return aVal + 1 }) store.sub(derivedAtom, () => {}) - const firstResolve = resolve store.set(a, 3) - firstResolve() - resolve() - - await new Promise(setImmediate) - + resolve.splice(0).forEach((fn) => fn()) + await new Promise((r) => setTimeout(r)) // wait for a tick expect(callBeforeAbort).toHaveBeenCalledTimes(2) expect(callAfterAbort).toHaveBeenCalledTimes(1) }) @@ -527,28 +520,22 @@ describe('aborting atoms', () => { const a = atom(1) const callBeforeAbort = vi.fn() const callAfterAbort = vi.fn() - let resolve = () => {} + const resolve: (() => void)[] = [] const store = createStore() const derivedAtom = atom(async (get, { signal }) => { const aVal = get(a) - - await new Promise((r) => (resolve = r)) - + await new Promise((r) => resolve.push(r)) callBeforeAbort() - throwIfAborted(signal) - callAfterAbort() - return aVal + 1 }) const unsub = store.sub(derivedAtom, () => {}) - unsub() - resolve() + resolve.splice(0).forEach((fn) => fn()) expect(await store.get(derivedAtom)).toEqual(2) expect(callBeforeAbort).toHaveBeenCalledTimes(1) diff --git a/tests/vanilla/utils/loadable.test.ts b/tests/vanilla/utils/loadable.test.ts index b0072e385e..484343c8b4 100644 --- a/tests/vanilla/utils/loadable.test.ts +++ b/tests/vanilla/utils/loadable.test.ts @@ -8,6 +8,10 @@ describe('loadable', () => { const asyncAtom = atom(Promise.resolve('concrete')) expect(await store.get(asyncAtom)).toEqual('concrete') + expect(store.get(loadable(asyncAtom))).toEqual({ + state: 'loading', + }) + await new Promise((r) => setTimeout(r)) // wait for a tick expect(store.get(loadable(asyncAtom))).toEqual({ state: 'hasData', data: 'concrete', diff --git a/tests/vanilla/utils/unwrap.test.ts b/tests/vanilla/utils/unwrap.test.ts index 31af69abf6..a0ca0ed8f5 100644 --- a/tests/vanilla/utils/unwrap.test.ts +++ b/tests/vanilla/utils/unwrap.test.ts @@ -133,6 +133,8 @@ describe('unwrap', () => { const asyncAtom = atom(Promise.resolve('concrete')) expect(await store.get(asyncAtom)).toEqual('concrete') + expect(store.get(unwrap(asyncAtom))).toEqual(undefined) + await new Promise((r) => setTimeout(r)) // wait for a tick expect(store.get(unwrap(asyncAtom))).toEqual('concrete') }) })