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

lib: initial experimental AbortController implementation #33527

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -280,6 +280,7 @@ module.exports = {
'node-core/no-duplicate-requires': 'error',
},
globals: {
AbortController: 'readable',
Atomics: 'readable',
jasnell marked this conversation as resolved.
Show resolved Hide resolved
BigInt: 'readable',
BigInt64Array: 'readable',
Expand Down
13 changes: 13 additions & 0 deletions doc/api/cli.md
Expand Up @@ -167,6 +167,18 @@ Enable experimental Source Map V3 support for stack traces.
Currently, overriding `Error.prepareStackTrace` is ignored when the
`--enable-source-maps` flag is set.

### `--experimental-abortcontroller`
<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/33527
description: --experimental-abortcontroller is no longer required.
-->

Experimental `AbortController` and `AbortSignal` support is enabled by default.
Use of this command line flag is no longer required.
jasnell marked this conversation as resolved.
Show resolved Hide resolved

### `--experimental-import-meta-resolve`
<!-- YAML
added:
Expand Down Expand Up @@ -1209,6 +1221,7 @@ Node.js options that are allowed are:
* `--disable-proto`
* `--enable-fips`
* `--enable-source-maps`
* `--experimental-abortcontroller`
* `--experimental-import-meta-resolve`
* `--experimental-json-modules`
* `--experimental-loader`
Expand Down
94 changes: 94 additions & 0 deletions doc/api/globals.md
Expand Up @@ -17,6 +17,99 @@ The objects listed here are specific to Node.js. There are [built-in objects][]
that are part of the JavaScript language itself, which are also globally
accessible.

## Class: `AbortController`
<!--YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

<!-- type=global -->

A utility class used to signal cancelation in selected `Promise`-based APIs.
The API is based on the Web API [`AbortController`][].

```js
const ac = new AbortController();

ac.signal.addEventListener('abort', () => console.log('Aborted!'),
{ once: true });

ac.abort();

console.log(ac.signal.aborted); // Prints True
```

### `abortController.abort()`
<!-- YAML
added: REPLACEME
-->

Triggers the abort signal, causing the `abortController.signal` to emit
the `'abort'` event.

### `abortController.signal`
<!-- YAML
added: REPLACEME
-->

* Type: {AbortSignal}

### Class: `AbortSignal extends EventTarget`
<!-- YAML
added: REPLACEME
-->

The `AbortSignal` is used to notify observers when the
`abortController.abort()` method is called.

#### Event: `'abort'`
<!-- YAML
added: REPLACEME
-->

The `'abort'` event is emitted when the `abortController.abort()` method
is called. The callback is invoked with a single object argument with a
single `type` propety set to `'abort'`:

```js
const ac = new AbortController();

// Use either the onabort property...
ac.signal.onabort = () => console.log('aborted!');
jasnell marked this conversation as resolved.
Show resolved Hide resolved

// Or the EventTarget API...
ac.signal.addEventListener('abort', (event) => {
console.log(event.type); // Prints 'abort'
}, { once: true });

ac.abort();
```

The `AbortController` with which the `AbortSignal` is associated will only
ever trigger the `'abort'` event once. Any event listeners attached to the
`AbortSignal` *should* use the `{ once: true }` option (or, if using the
`EventEmitter` APIs to attach a listener, use the `once()` method) to ensure
that the event listener is removed as soon as the `'abort'` event is handled.
Failure to do so may result in memory leaks.

#### `abortSignal.aborted`
<!-- YAML
added: REPLACEME
-->

* Type: {boolean} True after the `AbortController` has been aborted.

#### `abortSignal.onabort`
<!-- YAML
added: REPLACEME
-->

* Type: {Function}

An optional callback function that may be set by user code to be notified
when the `abortController.abort()` function has been called.

## Class: `Buffer`
<!-- YAML
added: v0.1.103
Expand Down Expand Up @@ -226,6 +319,7 @@ The object that acts as the namespace for all W3C
[WebAssembly][webassembly-org] related functionality. See the
[Mozilla Developer Network][webassembly-mdn] for usage and compatibility.

[`AbortController`]: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
[`TextDecoder`]: util.html#util_class_util_textdecoder
[`TextEncoder`]: util.html#util_class_util_textencoder
[`URLSearchParams`]: url.html#url_class_urlsearchparams
Expand Down
83 changes: 83 additions & 0 deletions lib/internal/abort_controller.js
@@ -0,0 +1,83 @@
'use strict';

// Modeled very closely on the AbortController implementation
// in https://github.com/mysticatea/abort-controller (MIT license)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @mysticatea <3


const {
Object,
Symbol,
} = primordials;

const {
EventTarget,
Event
} = require('internal/event_target');
const {
customInspectSymbol,
emitExperimentalWarning
} = require('internal/util');
const { inspect } = require('internal/util/inspect');

const kAborted = Symbol('kAborted');

function customInspect(self, obj, depth, options) {
if (depth < 0)
return self;

const opts = Object.assign({}, options, {
depth: options.depth === null ? null : options.depth - 1
});

return `${self.constructor.name} ${inspect(obj, opts)}`;
}

class AbortSignal extends EventTarget {
get aborted() { return !!this[kAborted]; }

[customInspectSymbol](depth, options) {
return customInspect(this, {
aborted: this.aborted
}, depth, options);
}
}

Object.defineProperties(AbortSignal.prototype, {
aborted: { enumerable: true }
});

function abortSignal(signal) {
if (signal[kAborted]) return;
signal[kAborted] = true;
const event = new Event('abort');
if (typeof signal.onabort === 'function') {
signal.onabort(event);
}
signal.dispatchEvent(event);
}
jasnell marked this conversation as resolved.
Show resolved Hide resolved

class AbortController {
#signal = new AbortSignal();

constructor() {
emitExperimentalWarning('AbortController');
}

get signal() { return this.#signal; }
abort() { abortSignal(this.#signal); }

[customInspectSymbol](depth, options) {
return customInspect(this, {
signal: this.signal
}, depth, options);
}
jasnell marked this conversation as resolved.
Show resolved Hide resolved
}

Object.defineProperties(AbortController.prototype, {
signal: { enumerable: true },
abort: { enumerable: true }
});

module.exports = {
AbortController,
AbortSignal,
};
7 changes: 7 additions & 0 deletions lib/internal/bootstrap/node.js
Expand Up @@ -133,6 +133,13 @@ if (!config.noBrowserGlobals) {
// https://encoding.spec.whatwg.org/#textdecoder
exposeInterface(global, 'TextDecoder', TextDecoder);

const {
AbortController,
AbortSignal,
} = require('internal/abort_controller');
exposeInterface(global, 'AbortController', AbortController);
jasnell marked this conversation as resolved.
Show resolved Hide resolved
exposeInterface(global, 'AbortSignal', AbortSignal);

// https://html.spec.whatwg.org/multipage/webappapis.html#windoworworkerglobalscope
const timers = require('timers');
defineOperation(global, 'clearInterval', timers.clearInterval);
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Expand Up @@ -95,6 +95,7 @@
'lib/wasi.js',
'lib/worker_threads.js',
'lib/zlib.js',
'lib/internal/abort_controller.js',
'lib/internal/assert.js',
'lib/internal/assert/assertion_error.js',
'lib/internal/assert/calltracker.js',
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.cc
Expand Up @@ -275,6 +275,8 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental Source Map V3 support",
&EnvironmentOptions::enable_source_maps,
kAllowedInEnvironment);
AddOption("--experimental-abortcontroller", "",
NoOp{}, kAllowedInEnvironment);
AddOption("--experimental-json-modules",
"experimental JSON interop support for the ES Module loader",
&EnvironmentOptions::experimental_json_modules,
Expand Down
1 change: 1 addition & 0 deletions test/common/index.js
Expand Up @@ -256,6 +256,7 @@ function platformTimeout(ms) {
}

let knownGlobals = [
AbortController,
clearImmediate,
clearInterval,
clearTimeout,
Expand Down
22 changes: 22 additions & 0 deletions test/parallel/test-abortcontroller.js
@@ -0,0 +1,22 @@
// Flags: --no-warnings
'use strict';

const common = require('../common');

const { ok, strictEqual } = require('assert');

{
const ac = new AbortController();
ok(ac.signal);
ac.signal.onabort = common.mustCall((event) => {
ok(event);
strictEqual(event.type, 'abort');
});
ac.signal.addEventListener('abort', common.mustCall((event) => {
ok(event);
strictEqual(event.type, 'abort');
}), { once: true });
ac.abort();
ac.abort();
ok(ac.signal.aborted);
}
7 changes: 7 additions & 0 deletions test/parallel/test-bootstrap-modules.js
Expand Up @@ -20,6 +20,7 @@ const expectedModules = new Set([
'Internal Binding module_wrap',
'Internal Binding native_module',
'Internal Binding options',
'Internal Binding performance',
'Internal Binding process_methods',
'Internal Binding report',
'Internal Binding string_decoder',
Expand All @@ -30,9 +31,11 @@ const expectedModules = new Set([
'Internal Binding types',
'Internal Binding url',
'Internal Binding util',
'NativeModule async_hooks',
'NativeModule buffer',
'NativeModule events',
'NativeModule fs',
'NativeModule internal/abort_controller',
'NativeModule internal/assert',
'NativeModule internal/async_hooks',
'NativeModule internal/bootstrap/pre_execution',
Expand All @@ -42,9 +45,11 @@ const expectedModules = new Set([
'NativeModule internal/constants',
'NativeModule internal/encoding',
'NativeModule internal/errors',
'NativeModule internal/event_target',
'NativeModule internal/fixed_queue',
'NativeModule internal/fs/dir',
'NativeModule internal/fs/utils',
'NativeModule internal/histogram',
jasnell marked this conversation as resolved.
Show resolved Hide resolved
'NativeModule internal/idna',
'NativeModule internal/linkedlist',
'NativeModule internal/modules/run_main',
Expand Down Expand Up @@ -81,8 +86,10 @@ const expectedModules = new Set([
'NativeModule internal/validators',
'NativeModule internal/vm/module',
'NativeModule path',
'NativeModule perf_hooks',
'NativeModule timers',
'NativeModule url',
'NativeModule util',
'NativeModule vm',
]);

Expand Down
4 changes: 4 additions & 0 deletions tools/doc/type-parser.js
Expand Up @@ -26,6 +26,10 @@ const customTypesMap = {

'this': `${jsDocPrefix}Reference/Operators/this`,

'AbortController': 'globals.html#globals_class_abortcontroller',
'AbortSignal':
'globals.html#globals_class_abortsignal_extends_eventtarget',

'ArrayBufferView':
'https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView',

Expand Down