Skip to content

Commit

Permalink
feat(abortableAsync): alwaysPendingWhenAborted
Browse files Browse the repository at this point in the history
  • Loading branch information
bowencool committed Mar 25, 2022
1 parent 61dc54e commit c5fcb52
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 12 deletions.
46 changes: 42 additions & 4 deletions packages/abortableAsync/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { abortableAsync, AbortError, TimeoutError } from './index';

jest.useFakeTimers();

function someAsyncTask(delay = 1000, fail?: boolean): Promise<string> {
function resolveInNms(delay = 1000, fail?: boolean): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail) {
Expand All @@ -16,7 +16,7 @@ function someAsyncTask(delay = 1000, fail?: boolean): Promise<string> {

describe('abortableAsync', () => {
test('basic', async () => {
const fn = abortableAsync(someAsyncTask);
const fn = abortableAsync(resolveInNms);
const p = fn(1000);
// “快进”时间使得定时器回调被执行
jest.advanceTimersByTime(1000);
Expand All @@ -27,7 +27,7 @@ describe('abortableAsync', () => {
await expect(p2).rejects.toBe('error');
});
test('timeout', async () => {
const fn = abortableAsync(someAsyncTask, { timeout: 2000 });
const fn = abortableAsync(resolveInNms, { timeout: 2000 });
const p = fn(1000);
jest.advanceTimersByTime(1000);
await expect(p).resolves.toBe('The result in 1000ms');
Expand All @@ -40,7 +40,7 @@ describe('abortableAsync', () => {
test('signal', async () => {
// controller.abort() 只能触发一次 abort 事件
const controller = new AbortController();
const fn = abortableAsync(someAsyncTask, { signal: controller.signal });
const fn = abortableAsync(resolveInNms, { signal: controller.signal });
const p = fn(1000);
jest.advanceTimersByTime(1000);
await expect(p).resolves.toBe('The result in 1000ms');
Expand All @@ -51,4 +51,42 @@ describe('abortableAsync', () => {
await expect(p2).rejects.toBeInstanceOf(AbortError);
await expect(p2).rejects.toMatchObject({ name: 'AbortError' });
});
test('alwaysPendingWhenAborted', async () => {
const controller = new AbortController();
const fn = abortableAsync(resolveInNms, {
signal: controller.signal,
timeout: 2000,
alwaysPendingWhenAborted: true,
});
const p = fn(1000);
jest.advanceTimersByTime(1000);
await expect(p).resolves.toBe('The result in 1000ms');

const cb = jest.fn();
fn(2000).then(cb).catch(cb);
jest.advanceTimersByTime(1000);
controller.abort();
expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(1500);
expect(cb).not.toHaveBeenCalled();
});
test('alwaysPendingWhenTimeout', async () => {
const controller = new AbortController();
const fn = abortableAsync(resolveInNms, {
signal: controller.signal,
timeout: 2000,
alwaysPendingWhenAborted: true,
});
const p = fn(1000);
jest.advanceTimersByTime(1000);
await expect(p).resolves.toBe('The result in 1000ms');

const cb = jest.fn();
fn(2000).then(cb, cb);
jest.advanceTimersByTime(1000);
expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(1500);
controller.abort();
expect(cb).not.toHaveBeenCalled();
});
});
30 changes: 23 additions & 7 deletions packages/abortableAsync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ AbortError.prototype.name = 'AbortError';
export class TimeoutError extends Error {}
TimeoutError.prototype.name = 'TimeoutError';

export function abortableAsync<T, P extends any[], R>(
fn: (this: T, ...p: P) => Promise<R>,
opt: { timeout?: number; signal?: AbortSignal } = {},
) {
export type AbortableOption = {
timeout?: number;
signal?: AbortSignal;
alwaysPendingWhenAborted?: boolean;
};

export function abortableAsync<T, P extends any[], R>(fn: (this: T, ...p: P) => Promise<R>, opt: AbortableOption = {}) {
return function abortabledAsync(this: T, ...args: P): Promise<R> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout>;
let aborted = false; // avoid resolve when opt.reject is false
function doAbort() {
reject(new AbortError('aborted'));
if (aborted) return;
if (!opt.alwaysPendingWhenAborted) {
reject(new AbortError('aborted'));
}
aborted = true;
}
function doTimeout() {
reject(new TimeoutError(`timeout of ${opt.timeout}ms`));
if (aborted) return;
if (!opt.alwaysPendingWhenAborted) {
reject(new TimeoutError(`timeout of ${opt.timeout}ms`));
}
aborted = true;
}
if (typeof opt.timeout === 'number' && opt.timeout > 0) {
timer = setTimeout(doTimeout, opt.timeout);
Expand All @@ -32,11 +44,15 @@ export function abortableAsync<T, P extends any[], R>(

fn.call(this, ...args)
.then((...r) => {
resolve(...r);
if (!aborted) {
resolve(...r);
}
clearEffect();
})
.catch((...e) => {
// if (!stoped) {
reject(...e);
// }
clearEffect();
});
});
Expand Down
1 change: 0 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ import { throttleAsyncResult } from 'high-order-async-utilities';

- ~~cacheAsync~~ see [memoizee](https://github.com/medikoo/memoizee#memoizing-asynchronous-functions) or [lru-pcache](https://github.com/jmendiara/lru-pcache)
- [ ] abort fetch & xhr
- [ ] always pending when canceled

0 comments on commit c5fcb52

Please sign in to comment.