Skip to content

Commit

Permalink
lib: add aborted() utility function
Browse files Browse the repository at this point in the history
Fixes: #37220
Refs: #36607
PR-URL: #46494
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
debadree25 authored Feb 7, 2023
1 parent 4d36ebd commit 6d584ae
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 5 deletions.
45 changes: 45 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1989,6 +1989,51 @@ const channel = new MessageChannel();
channel.port2.postMessage(signal, [signal]);
```
## `util.aborted(signal, resource)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `signal` {AbortSignal}
* `resource` {Object} Any non-null entity, reference to which is held weakly.
* Returns: {Promise}
Listens to abort event on the provided `signal` and
returns a promise that is fulfilled when the `signal` is
aborted. If the passed `resource` is garbage collected before the `signal` is
aborted, the returned promise shall remain pending indefinitely.
```cjs
const { aborted } = require('node:util');

const dependent = obtainSomethingAbortable();

aborted(dependent.signal, dependent).then(() => {
// Do something when dependent is aborted.
});

dependent.on('event', () => {
dependent.abort();
});
```
```mjs
import { aborted } from 'node:util';

const dependent = obtainSomethingAbortable();

aborted(dependent.signal, dependent).then(() => {
// Do something when dependent is aborted.
});

dependent.on('event', () => {
dependent.abort();
});
```
## `util.types`
<!-- YAML
Expand Down
34 changes: 29 additions & 5 deletions lib/internal/abort_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
ObjectDefineProperty,
PromiseResolve,
SafeFinalizationRegistry,
SafeSet,
Symbol,
Expand All @@ -22,11 +23,13 @@ const {
kTrustEvent,
kNewListener,
kRemoveListener,
kWeakHandler,
} = require('internal/event_target');
const {
createDeferredPromise,
customInspectSymbol,
kEnumerableProperty,
kEmptyObject,
kEnumerableProperty,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
const {
Expand All @@ -38,6 +41,8 @@ const {
} = require('internal/errors');

const {
validateAbortSignal,
validateObject,
validateUint32,
} = require('internal/validators');

Expand Down Expand Up @@ -94,7 +99,7 @@ function customInspect(self, obj, depth, options) {
return `${self.constructor.name} ${inspect(obj, opts)}`;
}

function validateAbortSignal(obj) {
function validateThisAbortSignal(obj) {
if (obj?.[kAborted] === undefined)
throw new ERR_INVALID_THIS('AbortSignal');
}
Expand Down Expand Up @@ -132,15 +137,15 @@ class AbortSignal extends EventTarget {
* @type {boolean}
*/
get aborted() {
validateAbortSignal(this);
validateThisAbortSignal(this);
return !!this[kAborted];
}

/**
* @type {any}
*/
get reason() {
validateAbortSignal(this);
validateThisAbortSignal(this);
return this[kReason];
}

Expand Down Expand Up @@ -202,7 +207,7 @@ class AbortSignal extends EventTarget {
}

[kTransfer]() {
validateAbortSignal(this);
validateThisAbortSignal(this);
const aborted = this.aborted;
if (aborted) {
const reason = this.reason;
Expand Down Expand Up @@ -357,6 +362,24 @@ function transferableAbortController() {
return AbortController[kMakeTransferable]();
}

/**
* @param {AbortSignal} signal
* @param {any} resource
* @returns {Promise<void>}
*/
async function aborted(signal, resource) {
if (signal === undefined) {
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
}
validateAbortSignal(signal, 'signal');
validateObject(resource, 'resource', { nullable: false, allowFunction: true, allowArray: true });
if (signal.aborted)
return PromiseResolve();
const abortPromise = createDeferredPromise();
signal.addEventListener('abort', abortPromise.resolve, { [kWeakHandler]: resource, once: true });
return abortPromise.promise;
}

ObjectDefineProperties(AbortController.prototype, {
signal: kEnumerableProperty,
abort: kEnumerableProperty,
Expand All @@ -375,6 +398,7 @@ module.exports = {
AbortController,
AbortSignal,
ClonedAbortSignal,
aborted,
transferableAbortSignal,
transferableAbortController,
};
3 changes: 3 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ module.exports = {
get transferableAbortController() {
return lazyAbortController().transferableAbortController;
},
get aborted() {
return lazyAbortController().aborted;
},
types
};

Expand Down
59 changes: 59 additions & 0 deletions test/parallel/test-aborted-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Flags: --expose-gc
'use strict';

const common = require('../common');
const { aborted } = require('util');
const assert = require('assert');
const { getEventListeners } = require('events');
const { spawn } = require('child_process');

{
// Test aborted works when provided a resource
const ac = new AbortController();
aborted(ac.signal, {}).then(common.mustCall());
ac.abort();
assert.strictEqual(ac.signal.aborted, true);
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 0);
}

{
// Test aborted with gc cleanup
const ac = new AbortController();
aborted(ac.signal, {}).then(common.mustNotCall());
setImmediate(() => {
global.gc();
ac.abort();
assert.strictEqual(ac.signal.aborted, true);
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 0);
});
}

{
// Fails with error if not provided abort signal
Promise.all([{}, null, undefined, Symbol(), [], 1, 0, 1n, true, false, 'a', () => {}].map((sig) =>
assert.rejects(aborted(sig, {}), {
code: 'ERR_INVALID_ARG_TYPE',
})
)).then(common.mustCall());
}

{
// Fails if not provided a resource
const ac = new AbortController();
Promise.all([null, undefined, 0, 1, 0n, 1n, Symbol(), '', 'a'].map((resource) =>
assert.rejects(aborted(ac.signal, resource), {
code: 'ERR_INVALID_ARG_TYPE',
})
)).then(common.mustCall());
}

{
const childProcess = spawn(process.execPath, ['--input-type=module']);
childProcess.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 13);
}));
childProcess.stdin.end(`
import { aborted } from 'node:util';
await aborted(new AbortController().signal, {});
`);
}

0 comments on commit 6d584ae

Please sign in to comment.