Skip to content

Commit

Permalink
A utility you can use to combine an AbortSignal and a Promise int…
Browse files Browse the repository at this point in the history
…o an abortable promise (#2791)
  • Loading branch information
steveluscher committed Jun 11, 2024
1 parent 91f35ca commit 0a3f63b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 1 deletion.
74 changes: 74 additions & 0 deletions packages/react/src/__tests__/abortable-promise-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getAbortablePromise } from '../abortable-promise';

describe('getAbortablePromise()', () => {
let promise: Promise<unknown>;
let resolve: (value: unknown) => void;
let reject: (reason?: unknown) => void;
beforeEach(() => {
promise = new Promise<unknown>((res, rej) => {
resolve = res;
reject = rej;
});
});
it('returns the original promise when called with no `AbortSignal`', () => {
expect(getAbortablePromise(promise)).toBe(promise);
});
it('rejects with the `reason` when passed an already-aborted signal with a pending promise', async () => {
expect.assertions(1);
const signal = AbortSignal.abort('o no');
await expect(getAbortablePromise(promise, signal)).rejects.toBe('o no');
});
it('rejects with the `reason` when passed an already-aborted signal and an already-resolved promise', async () => {
expect.assertions(1);
const signal = AbortSignal.abort('o no');
resolve(123);
await expect(getAbortablePromise(promise, signal)).rejects.toBe('o no');
});
it('rejects with the `reason` when passed an already-aborted signal and an already-rejected promise', async () => {
expect.assertions(1);
const signal = AbortSignal.abort('o no');
reject('mais non');
await expect(getAbortablePromise(promise, signal)).rejects.toBe('o no');
});
it('rejects with the `reason` when the signal aborts before the promise settles', async () => {
expect.assertions(2);
const controller = new AbortController();
const abortablePromise = getAbortablePromise(promise, controller.signal);
await expect(Promise.race(['pending', abortablePromise])).resolves.toBe('pending');
controller.abort('o no');
await expect(abortablePromise).rejects.toBe('o no');
});
it('rejects with the promise rejection when passed an already-rejected promise and a not-yet-aborted signal', async () => {
expect.assertions(1);
const signal = new AbortController().signal;
reject('mais non');
await expect(getAbortablePromise(promise, signal)).rejects.toBe('mais non');
});
it('rejects with the promise rejection when the promise rejects before the signal aborts', async () => {
expect.assertions(2);
const signal = new AbortController().signal;
const abortablePromise = getAbortablePromise(promise, signal);
await expect(Promise.race(['pending', abortablePromise])).resolves.toBe('pending');
reject('mais non');
await expect(abortablePromise).rejects.toBe('mais non');
});
it('resolves with the promise value when passed an already-resolved promise and a not-yet-aborted signal', async () => {
expect.assertions(1);
const signal = new AbortController().signal;
resolve(123);
await expect(getAbortablePromise(promise, signal)).resolves.toBe(123);
});
it('resolves with the promise value when the promise resolves before the signal aborts', async () => {
expect.assertions(2);
const signal = new AbortController().signal;
const abortablePromise = getAbortablePromise(promise, signal);
await expect(Promise.race(['pending', abortablePromise])).resolves.toBe('pending');
resolve(123);
await expect(abortablePromise).resolves.toBe(123);
});
it('pends when neither the promise has resolved nor the signal aborted', async () => {
expect.assertions(1);
const signal = new AbortController().signal;
await expect(Promise.race(['pending', getAbortablePromise(promise, signal)])).resolves.toBe('pending');
});
});
21 changes: 21 additions & 0 deletions packages/react/src/abortable-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function getAbortablePromise<T>(promise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
if (!abortSignal) {
return promise;
} else {
return Promise.race([
// This promise only ever rejects if the signal is aborted. Otherwise it idles forever.
// It's important that this come before the input promise; in the event of an abort, we
// want to throw even if the input promise's result is ready
new Promise<never>((_, reject) => {
if (abortSignal.aborted) {
reject(abortSignal.reason);
} else {
abortSignal.addEventListener('abort', function () {
reject(this.reason);
});
}
}),
promise,
]);
}
}
2 changes: 1 addition & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"jsx": "react",
"lib": ["DOM", "ES2015"]
"lib": ["DOM", "ES2015", "ESNext.Promise"]
},
"display": "@solana/react",
"extends": "../tsconfig/base.json",
Expand Down

0 comments on commit 0a3f63b

Please sign in to comment.