Skip to content

Commit

Permalink
lib: initial experimental AbortController implementation
Browse files Browse the repository at this point in the history
AbortController impl based very closely on:
 https://github.com/mysticatea/abort-controller

Marked experimental.
Not currently used by any of the existing promise apis.

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: #33527
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
  • Loading branch information
jasnell committed Jun 5, 2020
1 parent 3e2a300 commit 74ca960
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 0 deletions.
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',
BigInt: 'readable',
BigInt64Array: 'readable',
Expand Down
8 changes: 8 additions & 0 deletions doc/api/cli.md
Expand Up @@ -167,6 +167,13 @@ 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
-->

Enable experimental `AbortController` and `AbortSignal` support.

### `--experimental-import-meta-resolve`
<!-- YAML
added:
Expand Down Expand Up @@ -1209,6 +1216,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
96 changes: 96 additions & 0 deletions doc/api/globals.md
Expand Up @@ -17,6 +17,101 @@ 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`][].

To use, launch Node.js using the `--experimental-abortcontroller` flag.

```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!');

// 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 +321,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
3 changes: 3 additions & 0 deletions doc/node.1
Expand Up @@ -118,6 +118,9 @@ Enable FIPS-compliant crypto at startup.
Requires Node.js to be built with
.Sy ./configure --openssl-fips .
.
.It Fl -experimental-abortcontroller
Enable experimental AbortController and AbortSignal support.
.
.It Fl -enable-source-maps
Enable experimental Source Map V3 support for stack traces.
.
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)

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);
}

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);
}
}

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

module.exports = {
AbortController,
AbortSignal,
};
27 changes: 27 additions & 0 deletions lib/internal/bootstrap/pre_execution.js
Expand Up @@ -3,6 +3,7 @@
const {
Map,
ObjectDefineProperty,
ObjectDefineProperties,
SafeWeakMap,
} = primordials;

Expand Down Expand Up @@ -53,6 +54,7 @@ function prepareMainThreadExecution(expandArgv1 = false) {
// (including preload modules).
initializeClusterIPC();

initializeAbortController();
initializeDeprecations();
initializeWASI();
initializeCJSLoader();
Expand Down Expand Up @@ -304,6 +306,30 @@ function initializeDeprecations() {
});
}

function initializeAbortController() {
const abortController = getOptionValue('--experimental-abortcontroller');
if (abortController) {
const {
AbortController,
AbortSignal
} = require('internal/abort_controller');
ObjectDefineProperties(global, {
AbortController: {
writable: true,
enumerable: false,
configurable: true,
value: AbortController
},
AbortSignal: {
writable: true,
enumerable: false,
configurable: true,
value: AbortSignal
}
});
}
}

function setupChildProcessIpcChannel() {
if (process.env.NODE_CHANNEL_FD) {
const assert = require('internal/assert');
Expand Down Expand Up @@ -438,6 +464,7 @@ module.exports = {
setupWarningHandler,
setupDebugEnv,
prepareMainThreadExecution,
initializeAbortController,
initializeDeprecations,
initializeESMLoader,
initializeFrozenIntrinsics,
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/main/worker_thread.js
Expand Up @@ -13,6 +13,7 @@ const {
setupInspectorHooks,
setupWarningHandler,
setupDebugEnv,
initializeAbortController,
initializeDeprecations,
initializeWASI,
initializeCJSLoader,
Expand Down Expand Up @@ -112,6 +113,7 @@ port.on('message', (message) => {
if (manifestSrc) {
require('internal/process/policy').setup(manifestSrc, manifestURL);
}
initializeAbortController();
initializeDeprecations();
initializeWASI();
initializeCJSLoader();
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
4 changes: 4 additions & 0 deletions src/node_options.cc
Expand Up @@ -274,6 +274,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental Source Map V3 support",
&EnvironmentOptions::enable_source_maps,
kAllowedInEnvironment);
AddOption("--experimental-abortcontroller",
"experimental AbortController support",
&EnvironmentOptions::experimental_abortcontroller,
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 src/node_options.h
Expand Up @@ -101,6 +101,7 @@ class EnvironmentOptions : public Options {
public:
bool abort_on_uncaught_exception = false;
bool enable_source_maps = false;
bool experimental_abortcontroller = false;
bool experimental_json_modules = false;
bool experimental_modules = false;
std::string experimental_specifier_resolution;
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 --experimental-abortcontroller
'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);
}

0 comments on commit 74ca960

Please sign in to comment.