Skip to content

Commit 3f20e5b

Browse files
flakey5jasnell
authored andcommitted
lib: refactor transferable AbortSignal
Co-authored-by: James M Snell <jasnell@gmail.com> PR-URL: #44048 Backport-PR-URL: #44941 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent ce3cb29 commit 3f20e5b

File tree

5 files changed

+105
-12
lines changed

5 files changed

+105
-12
lines changed

doc/api/util.md

+31
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,37 @@ Returns the `string` after replacing any surrogate code points
15971597
(or equivalently, any unpaired surrogate code units) with the
15981598
Unicode "replacement character" U+FFFD.
15991599
1600+
## `util.transferableAbortController()`
1601+
1602+
<!-- YAML
1603+
added: REPLACEME
1604+
-->
1605+
1606+
> Stability: 1 - Experimental
1607+
1608+
Creates and returns an {AbortController} instance whose {AbortSignal} is marked
1609+
as transferable and can be used with `structuredClone()` or `postMessage()`.
1610+
1611+
## `util.transferableAbortSignal(signal)`
1612+
1613+
<!-- YAML
1614+
added: REPLACEME
1615+
-->
1616+
1617+
> Stability: 1 - Experimental
1618+
1619+
* `signal` {AbortSignal}
1620+
* Returns: {AbortSignal}
1621+
1622+
Marks the given {AbortSignal} as transferable so that it can be used with
1623+
`structuredClone()` and `postMessage()`.
1624+
1625+
```js
1626+
const signal = transferableAbortSignal(AbortSignal.timeout(100));
1627+
const channel = new MessageChannel();
1628+
channel.port2.postMessage(signal, [signal]);
1629+
```
1630+
16001631
## `util.types`
16011632
16021633
<!-- YAML

lib/internal/abort_controller.js

+51-9
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ const {
2626
const {
2727
customInspectSymbol,
2828
kEnumerableProperty,
29+
kEmptyObject,
2930
} = require('internal/util');
3031
const { inspect } = require('internal/util/inspect');
3132
const {
3233
codes: {
3334
ERR_ILLEGAL_CONSTRUCTOR,
35+
ERR_INVALID_ARG_TYPE,
3436
ERR_INVALID_THIS,
3537
}
3638
} = require('internal/errors');
@@ -79,6 +81,7 @@ const kAborted = Symbol('kAborted');
7981
const kReason = Symbol('kReason');
8082
const kCloneData = Symbol('kCloneData');
8183
const kTimeout = Symbol('kTimeout');
84+
const kMakeTransferable = Symbol('kMakeTransferable');
8285

8386
function customInspect(self, obj, depth, options) {
8487
if (depth < 0)
@@ -159,7 +162,7 @@ class AbortSignal extends EventTarget {
159162
*/
160163
static abort(
161164
reason = new DOMException('This operation was aborted', 'AbortError')) {
162-
return createAbortSignal(true, reason);
165+
return createAbortSignal({ aborted: true, reason });
163166
}
164167

165168
/**
@@ -179,10 +182,10 @@ class AbortSignal extends EventTarget {
179182
[kNewListener](size, type, listener, once, capture, passive, weak) {
180183
super[kNewListener](size, type, listener, once, capture, passive, weak);
181184
if (this[kTimeout] &&
182-
type === 'abort' &&
183-
!this.aborted &&
184-
!weak &&
185-
size === 1) {
185+
type === 'abort' &&
186+
!this.aborted &&
187+
!weak &&
188+
size === 1) {
186189
// If this is a timeout signal, and we're adding a non-weak abort
187190
// listener, then we don't want it to be gc'd while the listener
188191
// is attached and the timer still hasn't fired. So, we retain a
@@ -256,9 +259,9 @@ class AbortSignal extends EventTarget {
256259
}
257260

258261
function ClonedAbortSignal() {
259-
return createAbortSignal();
262+
return createAbortSignal({ transferable: true });
260263
}
261-
ClonedAbortSignal.prototype[kDeserialize] = () => {};
264+
ClonedAbortSignal.prototype[kDeserialize] = () => { };
262265

263266
ObjectDefineProperties(AbortSignal.prototype, {
264267
aborted: kEnumerableProperty,
@@ -274,12 +277,25 @@ ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, {
274277

275278
defineEventHandler(AbortSignal.prototype, 'abort');
276279

277-
function createAbortSignal(aborted = false, reason = undefined) {
280+
/**
281+
* @param {{
282+
* aborted? : boolean,
283+
* reason? : any,
284+
* transferable? : boolean
285+
* }} [init]
286+
* @returns {AbortSignal}
287+
*/
288+
function createAbortSignal(init = kEmptyObject) {
289+
const {
290+
aborted = false,
291+
reason = undefined,
292+
transferable = false,
293+
} = init;
278294
const signal = new EventTarget();
279295
ObjectSetPrototypeOf(signal, AbortSignal.prototype);
280296
signal[kAborted] = aborted;
281297
signal[kReason] = reason;
282-
return lazyMakeTransferable(signal);
298+
return transferable ? lazyMakeTransferable(signal) : signal;
283299
}
284300

285301
function abortSignal(signal, reason) {
@@ -327,6 +343,30 @@ class AbortController {
327343
signal: this.signal
328344
}, depth, options);
329345
}
346+
347+
static [kMakeTransferable]() {
348+
const controller = new AbortController();
349+
controller[kSignal] = transferableAbortSignal(controller[kSignal]);
350+
return controller;
351+
}
352+
}
353+
354+
/**
355+
* Enables the AbortSignal to be transferable using structuredClone/postMessage.
356+
* @param {AbortSignal} signal
357+
* @returns {AbortSignal}
358+
*/
359+
function transferableAbortSignal(signal) {
360+
if (signal?.[kAborted] === undefined)
361+
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
362+
return lazyMakeTransferable(signal);
363+
}
364+
365+
/**
366+
* Creates an AbortController with a transferable AbortSignal
367+
*/
368+
function transferableAbortController() {
369+
return AbortController[kMakeTransferable]();
330370
}
331371

332372
ObjectDefineProperties(AbortController.prototype, {
@@ -347,4 +387,6 @@ module.exports = {
347387
AbortController,
348388
AbortSignal,
349389
ClonedAbortSignal,
390+
transferableAbortSignal,
391+
transferableAbortController,
350392
};

lib/internal/worker/js_transferable.js

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ function setup() {
4040
}
4141

4242
function makeTransferable(obj) {
43+
// If the object is already transferable, skip all this.
44+
if (obj instanceof JSTransferable) return obj;
4345
const inst = ReflectConstruct(JSTransferable, [], obj.constructor);
4446
const properties = ObjectGetOwnPropertyDescriptors(obj);
4547
const propertiesValues = ObjectValues(properties);

lib/util.js

+13
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ const {
7979
toUSVString,
8080
} = require('internal/util');
8181

82+
let abortController;
83+
84+
function lazyAbortController() {
85+
abortController ??= require('internal/abort_controller');
86+
return abortController;
87+
}
88+
8289
let internalDeepEqual;
8390

8491
/**
@@ -384,5 +391,11 @@ module.exports = {
384391
toUSVString,
385392
TextDecoder,
386393
TextEncoder,
394+
get transferableAbortSignal() {
395+
return lazyAbortController().transferableAbortSignal;
396+
},
397+
get transferableAbortController() {
398+
return lazyAbortController().transferableAbortController;
399+
},
387400
types
388401
};

test/parallel/test-abortsignal-cloneable.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
const common = require('../common');
44
const { ok, strictEqual } = require('assert');
55
const { setImmediate: pause } = require('timers/promises');
6+
const {
7+
transferableAbortSignal,
8+
transferableAbortController,
9+
} = require('util');
10+
611

712
function deferred() {
813
let res;
@@ -11,7 +16,7 @@ function deferred() {
1116
}
1217

1318
(async () => {
14-
const ac = new AbortController();
19+
const ac = transferableAbortController();
1520
const mc = new MessageChannel();
1621

1722
const deferred1 = deferred();
@@ -54,7 +59,7 @@ function deferred() {
5459
})().then(common.mustCall());
5560

5661
{
57-
const signal = AbortSignal.abort('boom');
62+
const signal = transferableAbortSignal(AbortSignal.abort('boom'));
5863
ok(signal.aborted);
5964
strictEqual(signal.reason, 'boom');
6065
const mc = new MessageChannel();
@@ -70,7 +75,7 @@ function deferred() {
7075
{
7176
// The cloned AbortSignal does not keep the event loop open
7277
// waiting for the abort to be triggered.
73-
const ac = new AbortController();
78+
const ac = transferableAbortController();
7479
const mc = new MessageChannel();
7580
mc.port1.onmessage = common.mustCall();
7681
mc.port2.postMessage(ac.signal, [ac.signal]);

0 commit comments

Comments
 (0)