Skip to content

Commit

Permalink
More async API type fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed May 21, 2024
1 parent 4d7e3f5 commit 31e1775
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 21 deletions.
6 changes: 6 additions & 0 deletions .changeset/modern-jokes-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@quilted/preact-async": patch
"@quilted/async": patch
---

More async API type fixes
28 changes: 13 additions & 15 deletions packages/async/source/AsyncFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface AsyncFetchFunction<Data = unknown, Input = unknown> {
(
input: Input,
options: {
signal?: AbortSignal;
signal: AbortSignal;
},
): PromiseLike<Data>;
}
Expand Down Expand Up @@ -82,14 +82,9 @@ export class AsyncFetch<Data = unknown, Input = unknown> {
): AsyncFetchPromise<Data, Input> => {
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});

Expand Down Expand Up @@ -168,8 +163,7 @@ export class AsyncFetchCall<Data = unknown, Input = unknown> {
this.promise = new AsyncFetchPromise((res, rej) => {
resolve = res;
reject = rej;
});
Object.assign(this.promise, {source: this});
}, this);

if (onFinally) {
this.promise.then(
Expand Down Expand Up @@ -225,12 +219,12 @@ export class AsyncFetchCall<Data = unknown, Input = unknown> {
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 {
Expand Down Expand Up @@ -262,9 +256,12 @@ export class AsyncFetchPromise<
readonly status: 'pending' | 'fulfilled' | 'rejected' = 'pending';
readonly value?: Data;
readonly reason?: unknown;
readonly source?: AsyncFetchCall<Data, Input>;
readonly source: AsyncFetchCall<Data, Input>;

constructor(executor: ConstructorParameters<typeof Promise<Data>>[0]) {
constructor(
executor: ConstructorParameters<typeof Promise<Data>>[0],
source: AsyncFetchCall<Data, Input>,
) {
super((resolve, reject) => {
executor(
(value) => {
Expand All @@ -279,6 +276,7 @@ export class AsyncFetchPromise<
},
);
});
this.source = source;
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/async/source/AsyncFetchCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class AsyncFetchCache {
get = <Data = unknown, Input = unknown>(
fetchFunction: AsyncFetchFunction<Data, Input>,
{key, tags = EMPTY_ARRAY}: AsyncFetchCacheGetOptions<Data, Input> = {},
) => {
): AsyncFetchCacheEntry<Data, Input> => {
let resolvedKey = key;

if (resolvedKey == null) {
Expand Down Expand Up @@ -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<any, any>) => {
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;
Expand Down
69 changes: 69 additions & 0 deletions packages/async/source/tests/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
3 changes: 2 additions & 1 deletion packages/preact-async/source/hooks/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const useAsyncFetchCache = AsyncFetchCacheContext.use;

export interface UseAsyncFetchOptions<Data = unknown, Input = unknown>
extends Pick<AsyncFetchCacheGetOptions<Data, Input>, 'tags'> {
readonly input?: Input;
readonly key?: unknown | Signal<unknown>;
readonly defer?: boolean;
readonly cache?: boolean | AsyncFetchCache;
Expand Down Expand Up @@ -85,7 +86,7 @@ export function useAsync<Data = unknown, Input = unknown>(

if (shouldFetch) {
if (fetch.isRunning) throw fetch.promise;
throw fetch.call();
throw fetch.call(options?.input);
}

return fetch;
Expand Down

0 comments on commit 31e1775

Please sign in to comment.