From 31e1775f06e6be1ecdb9da53ba27f5528ba327d1 Mon Sep 17 00:00:00 2001 From: Chris Sauve Date: Mon, 20 May 2024 23:06:22 -0400 Subject: [PATCH] More async API type fixes --- .changeset/modern-jokes-sing.md | 6 ++ packages/async/source/AsyncFetch.ts | 28 ++++----- packages/async/source/AsyncFetchCache.ts | 10 +-- packages/async/source/tests/e2e.test.ts | 69 +++++++++++++++++++++ packages/preact-async/source/hooks/fetch.ts | 3 +- 5 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 .changeset/modern-jokes-sing.md create mode 100644 packages/async/source/tests/e2e.test.ts diff --git a/.changeset/modern-jokes-sing.md b/.changeset/modern-jokes-sing.md new file mode 100644 index 000000000..914b13b63 --- /dev/null +++ b/.changeset/modern-jokes-sing.md @@ -0,0 +1,6 @@ +--- +"@quilted/preact-async": patch +"@quilted/async": patch +--- + +More async API type fixes diff --git a/packages/async/source/AsyncFetch.ts b/packages/async/source/AsyncFetch.ts index ab1b0d90a..6132df9a2 100644 --- a/packages/async/source/AsyncFetch.ts +++ b/packages/async/source/AsyncFetch.ts @@ -4,7 +4,7 @@ export interface AsyncFetchFunction { ( input: Input, options: { - signal?: AbortSignal; + signal: AbortSignal; }, ): PromiseLike; } @@ -82,14 +82,9 @@ export class AsyncFetch { ): AsyncFetchPromise => { const wasRunning = this.runningSignal.peek(); - const fetchCall = - wasRunning == null && - this.finishedSignal.peek() == null && - !this.initial.signal.aborted - ? this.initial - : new AsyncFetchCall(this.function, { - finally: this.finalizeFetchCall, - }); + const fetchCall = new AsyncFetchCall(this.function, { + finally: this.finalizeFetchCall, + }); fetchCall.run(input, {signal}); @@ -168,8 +163,7 @@ export class AsyncFetchCall { this.promise = new AsyncFetchPromise((res, rej) => { resolve = res; reject = rej; - }); - Object.assign(this.promise, {source: this}); + }, this); if (onFinally) { this.promise.then( @@ -225,12 +219,12 @@ export class AsyncFetchCall { Object.assign(this, {startedAt: now(), input}); if (signal?.aborted) { - this.abortController.abort(); + this.abortController.abort(signal.reason); return this.promise; } signal?.addEventListener('abort', () => { - this.abortController.abort(); + this.abortController.abort(signal.reason); }); try { @@ -262,9 +256,12 @@ export class AsyncFetchPromise< readonly status: 'pending' | 'fulfilled' | 'rejected' = 'pending'; readonly value?: Data; readonly reason?: unknown; - readonly source?: AsyncFetchCall; + readonly source: AsyncFetchCall; - constructor(executor: ConstructorParameters>[0]) { + constructor( + executor: ConstructorParameters>[0], + source: AsyncFetchCall, + ) { super((resolve, reject) => { executor( (value) => { @@ -279,6 +276,7 @@ export class AsyncFetchPromise< }, ); }); + this.source = source; } } diff --git a/packages/async/source/AsyncFetchCache.ts b/packages/async/source/AsyncFetchCache.ts index 5fead2be9..7fc362485 100644 --- a/packages/async/source/AsyncFetchCache.ts +++ b/packages/async/source/AsyncFetchCache.ts @@ -39,7 +39,7 @@ export class AsyncFetchCache { get = ( fetchFunction: AsyncFetchFunction, {key, tags = EMPTY_ARRAY}: AsyncFetchCacheGetOptions = {}, - ) => { + ): AsyncFetchCacheEntry => { let resolvedKey = key; if (resolvedKey == null) { @@ -136,15 +136,15 @@ export class AsyncFetchCache { function createCachePredicate(options: AsyncFetchCacheFindOptions) { const {key, tags} = options; - const keyToCompare = key ? stringifyCacheKey(key) : undefined; + const id = key ? stringifyCacheKey(key) : undefined; return (entry: AsyncFetchCacheEntry) => { - if (keyToCompare != null && entry.id !== keyToCompare) { + if (id != null && entry.id !== id) { return false; } - if (tags?.length !== 0) { - return tags!.every((tag) => entry.tags.includes(tag)); + if (tags != null && tags.length !== 0) { + return tags.every((tag) => entry.tags.includes(tag)); } return true; diff --git a/packages/async/source/tests/e2e.test.ts b/packages/async/source/tests/e2e.test.ts new file mode 100644 index 000000000..a6768759a --- /dev/null +++ b/packages/async/source/tests/e2e.test.ts @@ -0,0 +1,69 @@ +import {describe, it, expect, vi} from 'vitest'; + +import {AsyncFetchCache} from '../AsyncFetchCache.ts'; + +describe('AsyncFetchCache', () => { + describe('get()', () => { + it('returns a wrapper around an async function', async () => { + const cache = new AsyncFetchCache(); + const greet = cache.get(async (name: string) => `Hello ${name}!`, { + key: ['greet'], + }); + + expect(greet).toHaveProperty('key', ['greet']); + expect(greet).toHaveProperty('id', expect.any(String)); + + const result = await greet.call('Winston'); + expect(result).toBe('Hello Winston!'); + }); + + it('returns the same promise for a given key', async () => { + const cache = new AsyncFetchCache(); + const entry1 = cache.get(async () => '', {key: 'key'}); + const entry2 = cache.get(async () => '', {key: 'key'}); + + expect(entry1).toBe(entry2); + }); + + it('passes through an abort signal to the fetch function', async () => { + const cache = new AsyncFetchCache(); + const abortController = new AbortController(); + const spy = vi.fn(); + const reason = new Error('Abort'); + + const waitUntilAbort = cache.get(async (_, {signal}) => { + signal.addEventListener('abort', () => { + spy(signal.reason); + }); + }); + + const promise = waitUntilAbort.call(undefined, { + signal: abortController.signal, + }); + + abortController.abort(reason); + + await expect(promise).rejects.toThrow(reason); + expect(spy).toHaveBeenCalledWith(reason); + }); + + it('provides an abort() method to manually abort the fetch function', async () => { + const cache = new AsyncFetchCache(); + const spy = vi.fn(); + const reason = new Error('Abort'); + + const waitUntilAbort = cache.get(async (_, {signal}) => { + signal.addEventListener('abort', () => { + spy(signal.reason); + }); + }); + + const promise = waitUntilAbort.call(); + + promise.source.abort(reason); + + await expect(promise).rejects.toThrow(reason); + expect(spy).toHaveBeenCalledWith(reason); + }); + }); +}); diff --git a/packages/preact-async/source/hooks/fetch.ts b/packages/preact-async/source/hooks/fetch.ts index ab2ae0410..d9073264a 100644 --- a/packages/preact-async/source/hooks/fetch.ts +++ b/packages/preact-async/source/hooks/fetch.ts @@ -15,6 +15,7 @@ export const useAsyncFetchCache = AsyncFetchCacheContext.use; export interface UseAsyncFetchOptions extends Pick, 'tags'> { + readonly input?: Input; readonly key?: unknown | Signal; readonly defer?: boolean; readonly cache?: boolean | AsyncFetchCache; @@ -85,7 +86,7 @@ export function useAsync( if (shouldFetch) { if (fetch.isRunning) throw fetch.promise; - throw fetch.call(); + throw fetch.call(options?.input); } return fetch;