Skip to content

Commit

Permalink
util: allow safely adding listener to abortSignal
Browse files Browse the repository at this point in the history
  • Loading branch information
atlowChemi committed Jun 29, 2023
1 parent e8810b9 commit 0e45c8f
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 0 deletions.
54 changes: 54 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,60 @@ it:
const util = require('node:util');
```

## `util.addSafeAbortSignalAbortListener(signal, resource)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `signal` {AbortSignal}
* `listener` {Function|EventListener}
* `options` {Object}
* `passive` {boolean} When `true`, serves as a hint that the listener will
not call the `Event` object's `preventDefault()` method.
**Default:** `false`.
* `signal` {AbortSignal} The listener will be removed when the given
AbortSignal object's `abort()` method is called.
* Returns: {Disposable} that removes the `abort` listener.

Listens once to the `abort` event on the provided `signal`.
The listeners will be triggered even if the `abort` event's propagation has
been stopped.

```cjs
const { addSafeAbortSignalAbortListener } = require('node:util');

function example(signal) {
let disposable;
try {
signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
disposable = addSafeAbortSignalAbortListener(signal, (e) => {
// Do something when signal is aborted.
});
} finally {
disposable?.[Symbol.dispose]();
}
}
```
```mjs
import { addSafeAbortSignalAbortListener } from 'node:util';

function example(signal) {
let disposable;
try {
signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
disposable = addSafeAbortSignalAbortListener(signal, (e) => {
// Do something when signal is aborted.
});
} finally {
disposable?.[Symbol.dispose]();
}
}
```
## `util.callbackify(original)`
<!-- YAML
Expand Down
28 changes: 28 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const {
ObjectValues,
ReflectApply,
StringPrototypePadStart,
SymbolDispose,
} = primordials;

const {
Expand All @@ -63,6 +64,7 @@ const {
} = require('internal/util/inspect');
const { debuglog } = require('internal/util/debuglog');
const {
validateAbortSignal,
validateFunction,
validateNumber,
} = require('internal/validators');
Expand All @@ -73,6 +75,7 @@ const {
deprecate,
getSystemErrorMap,
getSystemErrorName: internalErrorName,
kEmptyObject,
promisify,
toUSVString,
defineLazyProperties,
Expand All @@ -86,6 +89,7 @@ function lazyAbortController() {
}

let internalDeepEqual;
let kResistStopPropagation;

/**
* @deprecated since v4.0.0
Expand Down Expand Up @@ -345,11 +349,35 @@ function getSystemErrorName(err) {
return internalErrorName(err);
}

function addSafeAbortSignalAbortListener(signal, listener, options = kEmptyObject) {
if (signal === undefined) {
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
}
validateAbortSignal(signal, 'signal');
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
// TODO(atlowChemi) add { subscription: true } and return directly
signal.addEventListener(
'abort',
listener,
{ __proto__: null, ...options, once: true, [kResistStopPropagation]: true },
);
const removeEventListener = () => {
signal.removeEventListener('abort', listener, options);
};
return {
__proto__: null,
[SymbolDispose]() {
removeEventListener();
},
};
}

// Keep the `exports =` so that various functions can still be monkeypatched
module.exports = {
_errnoException: errnoException,
_exceptionWithHostPort: exceptionWithHostPort,
_extend,
addSafeAbortSignalAbortListener,
callbackify,
debug: debuglog,
debuglog,
Expand Down
43 changes: 43 additions & 0 deletions test/parallel/test-util-add-safe-abort-signal-abort-listener.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as common from '../common/index.mjs';
import * as util from 'node:util';
import * as assert from 'node:assert';
import { describe, it } from 'node:test';

describe('util.addSafeAbortSignalAbortListener', () => {
it('should throw if signal not provided', () => {
assert.throws(() => util.addSafeAbortSignalAbortListener(), { code: 'ERR_INVALID_ARG_TYPE' });
});

it('should throw if provided signal is invalid', () => {
assert.throws(() => util.addSafeAbortSignalAbortListener(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => util.addSafeAbortSignalAbortListener(null), { code: 'ERR_INVALID_ARG_TYPE' });
assert.throws(() => util.addSafeAbortSignalAbortListener({}), { code: 'ERR_INVALID_ARG_TYPE' });
});

it('should return a Disposable', () => {
const { signal } = new AbortController();
const disposable = util.addSafeAbortSignalAbortListener(signal, common.mustNotCall());

assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
});

it('should execute the listener even when event propagation stopped', () => {
const controller = new AbortController();
const { signal } = controller;

signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
util.addSafeAbortSignalAbortListener(
signal,
common.mustCall((e) => assert.strictEqual(e.target, signal)),
);

controller.abort();
});

it('should remove event listeners when disposed', () => {
const controller = new AbortController();
const disposable = util.addSafeAbortSignalAbortListener(controller.signal, common.mustNotCall());
disposable[Symbol.dispose]();
controller.abort();
});
});

0 comments on commit 0e45c8f

Please sign in to comment.