Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib: refactor transferable AbortSignal #44048

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions doc/api/util.md
Expand Up @@ -1593,6 +1593,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()`
flakey5 marked this conversation as resolved.
Show resolved Hide resolved

<!-- 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)`
flakey5 marked this conversation as resolved.
Show resolved Hide resolved

<!-- 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
50 changes: 46 additions & 4 deletions lib/internal/abort_controller.js
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 Down Expand Up @@ -256,7 +259,7 @@ class AbortSignal extends EventTarget {
}

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

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 @@ -314,6 +330,30 @@ class AbortController {
signal: this.signal
}, depth, options);
}

static [kMakeTransferable]() {
const controller = new AbortController();
controller.#signal = transferableAbortSignal(controller.#signal);
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() {
jasnell marked this conversation as resolved.
Show resolved Hide resolved
return AbortController[kMakeTransferable]();
}

ObjectDefineProperties(AbortController.prototype, {
Expand All @@ -334,4 +374,6 @@ module.exports = {
AbortController,
AbortSignal,
ClonedAbortSignal,
transferableAbortSignal,
transferableAbortController,
};
2 changes: 2 additions & 0 deletions lib/internal/worker/js_transferable.js
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
Expand Up @@ -78,6 +78,13 @@ const {
toUSVString,
} = require('internal/util');

let abortController;

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

let internalDeepEqual;

/**
Expand Down Expand Up @@ -377,5 +384,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
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