Skip to content

Commit

Permalink
lib: refactor transferable AbortSignal
Browse files Browse the repository at this point in the history
Co-authored-by: James M Snell <jasnell@gmail.com>
PR-URL: nodejs#44048
Backport-PR-URL: nodejs#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>
  • Loading branch information
2 people authored and danielleadams committed Oct 11, 2022
1 parent ce3cb29 commit 3f20e5b
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 12 deletions.
31 changes: 31 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,37 @@ Returns the `string` after replacing any surrogate code points
(or equivalently, any unpaired surrogate code units) with the
Unicode "replacement character" U+FFFD.
## `util.transferableAbortController()`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
Creates and returns an {AbortController} instance whose {AbortSignal} is marked
as transferable and can be used with `structuredClone()` or `postMessage()`.
## `util.transferableAbortSignal(signal)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `signal` {AbortSignal}
* Returns: {AbortSignal}
Marks the given {AbortSignal} as transferable so that it can be used with
`structuredClone()` and `postMessage()`.
```js
const signal = transferableAbortSignal(AbortSignal.timeout(100));
const channel = new MessageChannel();
channel.port2.postMessage(signal, [signal]);
```
## `util.types`
<!-- YAML
Expand Down
60 changes: 51 additions & 9 deletions lib/internal/abort_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ const {
const {
customInspectSymbol,
kEnumerableProperty,
kEmptyObject,
} = require('internal/util');
const { inspect } = require('internal/util/inspect');
const {
codes: {
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
}
} = require('internal/errors');
Expand Down Expand Up @@ -79,6 +81,7 @@ const kAborted = Symbol('kAborted');
const kReason = Symbol('kReason');
const kCloneData = Symbol('kCloneData');
const kTimeout = Symbol('kTimeout');
const kMakeTransferable = Symbol('kMakeTransferable');

function customInspect(self, obj, depth, options) {
if (depth < 0)
Expand Down Expand Up @@ -159,7 +162,7 @@ class AbortSignal extends EventTarget {
*/
static abort(
reason = new DOMException('This operation was aborted', 'AbortError')) {
return createAbortSignal(true, reason);
return createAbortSignal({ aborted: true, reason });
}

/**
Expand All @@ -179,10 +182,10 @@ class AbortSignal extends EventTarget {
[kNewListener](size, type, listener, once, capture, passive, weak) {
super[kNewListener](size, type, listener, once, capture, passive, weak);
if (this[kTimeout] &&
type === 'abort' &&
!this.aborted &&
!weak &&
size === 1) {
type === 'abort' &&
!this.aborted &&
!weak &&
size === 1) {
// If this is a timeout signal, and we're adding a non-weak abort
// listener, then we don't want it to be gc'd while the listener
// is attached and the timer still hasn't fired. So, we retain a
Expand Down Expand Up @@ -256,9 +259,9 @@ class AbortSignal extends EventTarget {
}

function ClonedAbortSignal() {
return createAbortSignal();
return createAbortSignal({ transferable: true });
}
ClonedAbortSignal.prototype[kDeserialize] = () => {};
ClonedAbortSignal.prototype[kDeserialize] = () => { };

ObjectDefineProperties(AbortSignal.prototype, {
aborted: kEnumerableProperty,
Expand All @@ -274,12 +277,25 @@ ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, {

defineEventHandler(AbortSignal.prototype, 'abort');

function createAbortSignal(aborted = false, reason = undefined) {
/**
* @param {{
* aborted? : boolean,
* reason? : any,
* transferable? : boolean
* }} [init]
* @returns {AbortSignal}
*/
function createAbortSignal(init = kEmptyObject) {
const {
aborted = false,
reason = undefined,
transferable = false,
} = init;
const signal = new EventTarget();
ObjectSetPrototypeOf(signal, AbortSignal.prototype);
signal[kAborted] = aborted;
signal[kReason] = reason;
return lazyMakeTransferable(signal);
return transferable ? lazyMakeTransferable(signal) : signal;
}

function abortSignal(signal, reason) {
Expand Down Expand Up @@ -327,6 +343,30 @@ class AbortController {
signal: this.signal
}, depth, options);
}

static [kMakeTransferable]() {
const controller = new AbortController();
controller[kSignal] = transferableAbortSignal(controller[kSignal]);
return controller;
}
}

/**
* Enables the AbortSignal to be transferable using structuredClone/postMessage.
* @param {AbortSignal} signal
* @returns {AbortSignal}
*/
function transferableAbortSignal(signal) {
if (signal?.[kAborted] === undefined)
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
return lazyMakeTransferable(signal);
}

/**
* Creates an AbortController with a transferable AbortSignal
*/
function transferableAbortController() {
return AbortController[kMakeTransferable]();
}

ObjectDefineProperties(AbortController.prototype, {
Expand All @@ -347,4 +387,6 @@ module.exports = {
AbortController,
AbortSignal,
ClonedAbortSignal,
transferableAbortSignal,
transferableAbortController,
};
2 changes: 2 additions & 0 deletions lib/internal/worker/js_transferable.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ function setup() {
}

function makeTransferable(obj) {
// If the object is already transferable, skip all this.
if (obj instanceof JSTransferable) return obj;
const inst = ReflectConstruct(JSTransferable, [], obj.constructor);
const properties = ObjectGetOwnPropertyDescriptors(obj);
const propertiesValues = ObjectValues(properties);
Expand Down
13 changes: 13 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ const {
toUSVString,
} = require('internal/util');

let abortController;

function lazyAbortController() {
abortController ??= require('internal/abort_controller');
return abortController;
}

let internalDeepEqual;

/**
Expand Down Expand Up @@ -384,5 +391,11 @@ module.exports = {
toUSVString,
TextDecoder,
TextEncoder,
get transferableAbortSignal() {
return lazyAbortController().transferableAbortSignal;
},
get transferableAbortController() {
return lazyAbortController().transferableAbortController;
},
types
};
11 changes: 8 additions & 3 deletions test/parallel/test-abortsignal-cloneable.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
const common = require('../common');
const { ok, strictEqual } = require('assert');
const { setImmediate: pause } = require('timers/promises');
const {
transferableAbortSignal,
transferableAbortController,
} = require('util');


function deferred() {
let res;
Expand All @@ -11,7 +16,7 @@ function deferred() {
}

(async () => {
const ac = new AbortController();
const ac = transferableAbortController();
const mc = new MessageChannel();

const deferred1 = deferred();
Expand Down Expand Up @@ -54,7 +59,7 @@ function deferred() {
})().then(common.mustCall());

{
const signal = AbortSignal.abort('boom');
const signal = transferableAbortSignal(AbortSignal.abort('boom'));
ok(signal.aborted);
strictEqual(signal.reason, 'boom');
const mc = new MessageChannel();
Expand All @@ -70,7 +75,7 @@ function deferred() {
{
// The cloned AbortSignal does not keep the event loop open
// waiting for the abort to be triggered.
const ac = new AbortController();
const ac = transferableAbortController();
const mc = new MessageChannel();
mc.port1.onmessage = common.mustCall();
mc.port2.postMessage(ac.signal, [ac.signal]);
Expand Down

0 comments on commit 3f20e5b

Please sign in to comment.