Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AbortSignal #37

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ async function register(address, name) {
Throws the error passed if it matches any of the specified rules where:
- `err` - the error.
- `type` - a single item or an array of items of:
- An error constructor (e.g. `SyntaxError`).
- An error constructor (e.g. `SyntaxError`) - matches error created from constructor or any subclass.
- `'system'` - matches any languange native error or node assertions.
- `'boom'` - matches [**boom**](https://github.com/hapijs/boom) errors.
- `'abort'` - matches an `AbortError`, as generated on an `AbortSignal` by `AbortController.abort()`.
- `'timeout'` - matches a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`.
- an object where each property is compared with the error and must match the error property
value. All the properties in the object must match the error but do not need to include all
the error properties.
Expand All @@ -90,6 +92,7 @@ Throws the error passed if it matches any of the specified rules where:
- `override` - an error used to override `err` when `err` matches. If used with `decorate`,
the `override` object is modified.
- `return` - if `true`, the error is returned instead of thrown. Defaults to `false`.
- `signal` - an `AbortSignal`. Throws `signal.reason` if it has already been aborted.

### `ignore(err, types, [options])`

Expand Down Expand Up @@ -124,3 +127,18 @@ Return `true` when `err` is one of:
- `TypeError`
- `URIError`
- Node's `AssertionError`
- Hoek's `AssertError`

### `isAbort(err)`

Returns `true` when `err` is an `AbortError`, as generated by `AbortSignal.abort()`.

Note that unlike other errors, `AbortError` cannot be considered a class in itself.
The best way to create a custom `AbortError` is with `new DOMException(message, 'AbortError')`.

### `isTimeout(err)`

Returns `true` when `err` is a `TimeoutError`, as generated by `AbortSignal.timeout(delay)`.

Note that unlike other errors, `TimeoutError` cannot be considered a class in itself.
The best way to create a custom `TimeoutError` is with `new DOMException(message, 'TimeoutError')`.
20 changes: 19 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ exports.ignore = function (err, types, options = {}) {

internals.catch = function (err, types, options, match) {

if (options.signal?.aborted) {
throw options.signal.reason ?? new DOMException('This operation was aborted', 'AbortError');
}

if (internals.match(err, types) !== match) {
return;
}
Expand Down Expand Up @@ -115,9 +119,23 @@ exports.isSystem = function (err) {
};


exports.isAbort = function (err) {

return err instanceof Error && err.name === 'AbortError';
};


exports.isTimeout = function (err) {

return err instanceof Error && err.name === 'TimeoutError';
};


internals.rules = {
system: exports.isSystem,
boom: exports.isBoom
boom: exports.isBoom,
abort: exports.isAbort,
timeout: exports.isTimeout
};


Expand Down
210 changes: 210 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,52 @@ describe('Bounce', () => {
expect(error3).to.be.an.error('Something', SyntaxError);
});

it('rethrows only abort errors', () => {

try {
Bounce.rethrow(new Error('Something'), 'abort');
}
catch (err) {
var error1 = err;
}

expect(error1).to.not.exist();

try {
Bounce.rethrow(AbortSignal.abort().reason, 'abort');
}
catch (err) {
var error2 = err;
}

expect(error2).to.be.an.error(DOMException);
expect(error2.name).to.equal('AbortError');
});

it('rethrows only timeout errors', async () => {

try {
Bounce.rethrow(new Error('Something'), 'timeout');
}
catch (err) {
var error1 = err;
}

expect(error1).to.not.exist();

try {
const signal = AbortSignal.timeout(0);
await Hoek.wait(1);
Bounce.rethrow(signal.reason, 'timeout');
}
catch (err) {
var error2 = err;
}

expect(error2).to.be.an.error(DOMException);
expect(error2.name).to.equal('TimeoutError');
});

it('rethrows only specified errors', () => {

try {
Expand Down Expand Up @@ -240,6 +286,55 @@ describe('Bounce', () => {
expect(error).to.shallow.equal(orig);
expect(error).to.be.an.error('Something');
});

it('rethrows already aborted signal reason', () => {

const orig = new Error('Something');
const signal = AbortSignal.abort(new Error('Fail'));

try {
Bounce.rethrow(orig, null, { signal });
}
catch (err) {
var error = err;
}

expect(error).to.shallow.equal(signal.reason);
expect(error).to.be.an.error('Fail');
});

it('rethrows already aborted signal with no reason', () => {

const orig = new Error('Something');
const signal = AbortSignal.abort();

Object.defineProperty(signal, 'reason', { value: undefined }); // Simulate older API without the reason property

try {
Bounce.rethrow(orig, null, { signal });
}
catch (err) {
var error = err;
}

expect(error).to.be.an.error(DOMException, 'This operation was aborted');
});

it('ignores non-aborted signal', () => {

const orig = new Error('Something');
const signal = new AbortController().signal;

try {
Bounce.rethrow(orig, null, { signal });
}
catch (err) {
var error = err;
}

expect(error).to.shallow.equal(orig);
expect(error).to.be.an.error('Something');
});
});

describe('ignore()', () => {
Expand Down Expand Up @@ -315,6 +410,38 @@ describe('Bounce', () => {

expect(error3).to.not.exist();
});

it('rethrows already aborted signal reason', () => {

const orig = new Error('Something');
const signal = AbortSignal.abort(new Error('Fail'));

try {
Bounce.ignore(orig, 'system', { signal });
}
catch (err) {
var error = err;
}

expect(error).to.shallow.equal(signal.reason);
expect(error).to.be.an.error('Fail');
});

it('ignores non-aborted signal', () => {

const orig = new Error('Something');
const signal = new AbortController().signal;

try {
Bounce.ignore(orig, 'system', { signal });
}
catch (err) {
var error = err;
}

expect(error).to.shallow.equal(orig);
expect(error).to.be.an.error('Something');
});
});

describe('background()', () => {
Expand Down Expand Up @@ -537,4 +664,87 @@ describe('Bounce', () => {
expect(Bounce.isSystem(Boom.boomify(new TypeError()))).to.be.false();
});
});

describe('isAbort()', () => {

it('identifies AbortSignal.abort() reason as abort', () => {

expect(Bounce.isAbort(AbortSignal.abort().reason)).to.be.true();
});

it('identifies DOMException AbortError as abort', () => {

expect(Bounce.isAbort(new DOMException('aborted', 'AbortError'))).to.be.true();
});

it('identifies Error with name "AbortError" as abort', () => {

class MyAbort extends Error {
name = 'AbortError';
}

expect(Bounce.isAbort(new MyAbort())).to.be.true();
});

it('identifies object as non-abort', () => {

expect(Bounce.isAbort({})).to.be.false();
});

it('identifies error as non-abort', () => {

expect(Bounce.isAbort(new Error('failed'))).to.be.false();
});

it('identifies object with name "AbortError" as non-abort', () => {

expect(Bounce.isAbort({ name: 'AbortError' })).to.be.false();
});

it('identifies AbortSignal.timeout() reason non-abort', async () => {

const signal = AbortSignal.timeout(0);
await Hoek.wait(1);
expect(Bounce.isAbort(signal.reason)).to.be.false();
});
});

describe('isTimeout()', () => {

it('identifies AbortSignal.timeout() reason as timeout', async () => {

const signal = AbortSignal.timeout(0);
await Hoek.wait(1);
expect(Bounce.isTimeout(signal.reason)).to.be.true();
});

it('identifies DOMException TimeoutError as timeout', () => {

expect(Bounce.isTimeout(new DOMException('timed out', 'TimeoutError'))).to.be.true();
});

it('identifies Error with name "TimeoutError" as timeout', () => {

class MyTimeout extends Error {
name = 'TimeoutError';
}

expect(Bounce.isTimeout(new MyTimeout())).to.be.true();
});

it('identifies object as non-timeout', () => {

expect(Bounce.isTimeout({})).to.be.false();
});

it('identifies error as non-timeout', () => {

expect(Bounce.isTimeout(new Error('failed'))).to.be.false();
});

it('identifies object with name "TimeoutError" as non-timeout', () => {

expect(Bounce.isTimeout({ name: 'TimeoutError' })).to.be.false();
});
});
});