Skip to content

Commit

Permalink
fix(defer): recover from error (#3933)
Browse files Browse the repository at this point in the history
* feat(defer): implement waitable callback

* fix(defer): recover from callback error

* test(defer): remove useless wait test
  • Loading branch information
samouss authored and Haroenv committed Oct 23, 2019
1 parent 1b9b5f4 commit f22b9e2
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 15 deletions.
57 changes: 49 additions & 8 deletions src/lib/utils/__tests__/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(1);
});
Expand All @@ -24,7 +24,7 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(1);
});
Expand All @@ -39,15 +39,15 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(1);

deferred();
deferred();
deferred();

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(2);
});
Expand All @@ -64,7 +64,7 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(0);
});
Expand All @@ -81,13 +81,13 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(0);

deferred();

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(1);
});
Expand All @@ -104,8 +104,49 @@ describe('defer', () => {

expect(fn).toHaveBeenCalledTimes(0);

await Promise.resolve();
await deferred.wait();

expect(fn).toHaveBeenCalledTimes(1);
});

it('throws an error when `wait` is called before the deferred function', () => {
const fn = jest.fn();
const deferred = defer(fn);

expect(() => deferred.wait()).toThrowErrorMatchingInlineSnapshot(
`"The deferred function should be called before calling \`wait()\`"`
);
});

it('recovers a deferred function that throws an error', async () => {
const fn = jest.fn();
const deferred = defer(fn);

fn.mockImplementation(() => {
throw new Error('FAIL');
});

deferred();

expect(fn).toHaveBeenCalledTimes(0);

try {
await deferred.wait();
} catch {
// The test verifies that the function is able to recover. We don't want
// to terminate the test on this expected error.
}

expect(fn).toHaveBeenCalledTimes(1);

fn.mockImplementation();

deferred();

expect(fn).toHaveBeenCalledTimes(1);

await deferred.wait();

expect(fn).toHaveBeenCalledTimes(2);
});
});
27 changes: 20 additions & 7 deletions src/lib/utils/defer.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
const nextMicroTask = Promise.resolve();

type Callback = (...args: any[]) => void;
type Cancellable = Callback & {
type Defer = Callback & {
wait(): Promise<void>;
cancel(): void;
};

const defer = (callback: Callback): Cancellable => {
const defer = (callback: Callback): Defer => {
let progress: Promise<void> | null = null;
let cancelled = false;

const fn: Cancellable = (...args) => {
const fn: Defer = (...args) => {
if (progress !== null) {
return;
}

progress = nextMicroTask.then(() => {
progress = null;

if (cancelled) {
progress = null;
cancelled = false;
return;
}

callback(...args);
progress = null;
});
};

fn.wait = () => {
if (progress === null) {
throw new Error(
'The deferred function should be called before calling `wait()`'
);
}

return progress;
};

fn.cancel = () => {
if (progress !== null) {
cancelled = true;
if (progress === null) {
return;
}

cancelled = true;
};

return fn;
Expand Down

0 comments on commit f22b9e2

Please sign in to comment.