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 #43388

Closed
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
27 changes: 27 additions & 0 deletions doc/api/util.md
Expand Up @@ -1470,6 +1470,33 @@ 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()`

<!--
added: REPLACEME
-->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> 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)`

<!--
added: REPLACEME
-->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> Stability: 1 - Experimental

* `signal` {AbortSignal}
* Returns: {AbortSignal}

Marks the given {AbortSignal} as transferable so that it can be used with
`structuredClone()` and `postMessage()`.

```cjs
const signal = transferableAbortSignal(AbortSignal.timeout(100));
const channel = new MessageChannel();
channel.port2.postMessage(signal, [signal]);
```

## `util.types`

<!-- YAML
Expand Down
45 changes: 41 additions & 4 deletions lib/internal/abort_controller.js
Expand Up @@ -9,6 +9,7 @@ const {
ObjectSetPrototypeOf,
ObjectDefineProperty,
SafeFinalizationRegistry,
ReflectConstruct,
SafeSet,
Symbol,
SymbolToStringTag,
Expand All @@ -31,6 +32,7 @@ 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 @@ -159,7 +161,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 +258,7 @@ class AbortSignal extends EventTarget {
}

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

Expand All @@ -274,12 +276,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function createAbortSignal(init) {
function createAbortSignal(init = kEmptyObject) {

and then requiring kEmptyObject from internal/util will probably fix the test failures. You might also want to rebase to include the change from #43159 for this to work.

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 @@ -329,6 +344,26 @@ class AbortController {
}
}

/**
* 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 makeTransferable(signal);
}

/**
* Creates an AbortController with a transferable AbortSignal
*/
function transferableAbortController() {
return ReflectConstruct(function() {
this[kSignal] = createAbortSignal({ transferable: true });
}, [], AbortController);
}

ObjectDefineProperties(AbortController.prototype, {
signal: kEnumerableProperty,
abort: kEnumerableProperty,
Expand All @@ -347,4 +382,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