Skip to content

Commit 2f79f3f

Browse files
debadree25danielleadams
authored andcommitted
lib: add aborted() utility function
Fixes: #37220 Refs: #36607 PR-URL: #46494 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 4fb2fc7 commit 2f79f3f

File tree

4 files changed

+136
-5
lines changed

4 files changed

+136
-5
lines changed

doc/api/util.md

+45
Original file line numberDiff line numberDiff line change
@@ -1977,6 +1977,51 @@ const channel = new MessageChannel();
19771977
channel.port2.postMessage(signal, [signal]);
19781978
```
19791979
1980+
## `util.aborted(signal, resource)`
1981+
1982+
<!-- YAML
1983+
added: REPLACEME
1984+
-->
1985+
1986+
> Stability: 1 - Experimental
1987+
1988+
* `signal` {AbortSignal}
1989+
* `resource` {Object} Any non-null entity, reference to which is held weakly.
1990+
* Returns: {Promise}
1991+
1992+
Listens to abort event on the provided `signal` and
1993+
returns a promise that is fulfilled when the `signal` is
1994+
aborted. If the passed `resource` is garbage collected before the `signal` is
1995+
aborted, the returned promise shall remain pending indefinitely.
1996+
1997+
```cjs
1998+
const { aborted } = require('node:util');
1999+
2000+
const dependent = obtainSomethingAbortable();
2001+
2002+
aborted(dependent.signal, dependent).then(() => {
2003+
// Do something when dependent is aborted.
2004+
});
2005+
2006+
dependent.on('event', () => {
2007+
dependent.abort();
2008+
});
2009+
```
2010+
2011+
```mjs
2012+
import { aborted } from 'node:util';
2013+
2014+
const dependent = obtainSomethingAbortable();
2015+
2016+
aborted(dependent.signal, dependent).then(() => {
2017+
// Do something when dependent is aborted.
2018+
});
2019+
2020+
dependent.on('event', () => {
2021+
dependent.abort();
2022+
});
2023+
```
2024+
19802025
## `util.types`
19812026
19822027
<!-- YAML

lib/internal/abort_controller.js

+29-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const {
88
ObjectDefineProperties,
99
ObjectSetPrototypeOf,
1010
ObjectDefineProperty,
11+
PromiseResolve,
1112
SafeFinalizationRegistry,
1213
SafeSet,
1314
Symbol,
@@ -22,11 +23,13 @@ const {
2223
kTrustEvent,
2324
kNewListener,
2425
kRemoveListener,
26+
kWeakHandler,
2527
} = require('internal/event_target');
2628
const {
29+
createDeferredPromise,
2730
customInspectSymbol,
28-
kEnumerableProperty,
2931
kEmptyObject,
32+
kEnumerableProperty,
3033
} = require('internal/util');
3134
const { inspect } = require('internal/util/inspect');
3235
const {
@@ -38,6 +41,8 @@ const {
3841
} = require('internal/errors');
3942

4043
const {
44+
validateAbortSignal,
45+
validateObject,
4146
validateUint32,
4247
} = require('internal/validators');
4348

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

97-
function validateAbortSignal(obj) {
102+
function validateThisAbortSignal(obj) {
98103
if (obj?.[kAborted] === undefined)
99104
throw new ERR_INVALID_THIS('AbortSignal');
100105
}
@@ -132,15 +137,15 @@ class AbortSignal extends EventTarget {
132137
* @type {boolean}
133138
*/
134139
get aborted() {
135-
validateAbortSignal(this);
140+
validateThisAbortSignal(this);
136141
return !!this[kAborted];
137142
}
138143

139144
/**
140145
* @type {any}
141146
*/
142147
get reason() {
143-
validateAbortSignal(this);
148+
validateThisAbortSignal(this);
144149
return this[kReason];
145150
}
146151

@@ -202,7 +207,7 @@ class AbortSignal extends EventTarget {
202207
}
203208

204209
[kTransfer]() {
205-
validateAbortSignal(this);
210+
validateThisAbortSignal(this);
206211
const aborted = this.aborted;
207212
if (aborted) {
208213
const reason = this.reason;
@@ -369,6 +374,24 @@ function transferableAbortController() {
369374
return AbortController[kMakeTransferable]();
370375
}
371376

377+
/**
378+
* @param {AbortSignal} signal
379+
* @param {any} resource
380+
* @returns {Promise<void>}
381+
*/
382+
async function aborted(signal, resource) {
383+
if (signal === undefined) {
384+
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
385+
}
386+
validateAbortSignal(signal, 'signal');
387+
validateObject(resource, 'resource', { nullable: false, allowFunction: true, allowArray: true });
388+
if (signal.aborted)
389+
return PromiseResolve();
390+
const abortPromise = createDeferredPromise();
391+
signal.addEventListener('abort', abortPromise.resolve, { [kWeakHandler]: resource, once: true });
392+
return abortPromise.promise;
393+
}
394+
372395
ObjectDefineProperties(AbortController.prototype, {
373396
signal: kEnumerableProperty,
374397
abort: kEnumerableProperty,
@@ -387,6 +410,7 @@ module.exports = {
387410
AbortController,
388411
AbortSignal,
389412
ClonedAbortSignal,
413+
aborted,
390414
transferableAbortSignal,
391415
transferableAbortController,
392416
};

lib/util.js

+3
Original file line numberDiff line numberDiff line change
@@ -400,5 +400,8 @@ module.exports = {
400400
get transferableAbortController() {
401401
return lazyAbortController().transferableAbortController;
402402
},
403+
get aborted() {
404+
return lazyAbortController().aborted;
405+
},
403406
types
404407
};

test/parallel/test-aborted-util.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Flags: --expose-gc
2+
'use strict';
3+
4+
const common = require('../common');
5+
const { aborted } = require('util');
6+
const assert = require('assert');
7+
const { getEventListeners } = require('events');
8+
const { spawn } = require('child_process');
9+
10+
{
11+
// Test aborted works when provided a resource
12+
const ac = new AbortController();
13+
aborted(ac.signal, {}).then(common.mustCall());
14+
ac.abort();
15+
assert.strictEqual(ac.signal.aborted, true);
16+
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 0);
17+
}
18+
19+
{
20+
// Test aborted with gc cleanup
21+
const ac = new AbortController();
22+
aborted(ac.signal, {}).then(common.mustNotCall());
23+
setImmediate(() => {
24+
global.gc();
25+
ac.abort();
26+
assert.strictEqual(ac.signal.aborted, true);
27+
assert.strictEqual(getEventListeners(ac.signal, 'abort').length, 0);
28+
});
29+
}
30+
31+
{
32+
// Fails with error if not provided abort signal
33+
Promise.all([{}, null, undefined, Symbol(), [], 1, 0, 1n, true, false, 'a', () => {}].map((sig) =>
34+
assert.rejects(aborted(sig, {}), {
35+
code: 'ERR_INVALID_ARG_TYPE',
36+
})
37+
)).then(common.mustCall());
38+
}
39+
40+
{
41+
// Fails if not provided a resource
42+
const ac = new AbortController();
43+
Promise.all([null, undefined, 0, 1, 0n, 1n, Symbol(), '', 'a'].map((resource) =>
44+
assert.rejects(aborted(ac.signal, resource), {
45+
code: 'ERR_INVALID_ARG_TYPE',
46+
})
47+
)).then(common.mustCall());
48+
}
49+
50+
{
51+
const childProcess = spawn(process.execPath, ['--input-type=module']);
52+
childProcess.on('exit', common.mustCall((code) => {
53+
assert.strictEqual(code, 13);
54+
}));
55+
childProcess.stdin.end(`
56+
import { aborted } from 'node:util';
57+
await aborted(new AbortController().signal, {});
58+
`);
59+
}

0 commit comments

Comments
 (0)