Skip to content

Commit

Permalink
fixup! lib: add AbortSignal.timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Nov 25, 2021
1 parent 820e505 commit cf9cb71
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 9 deletions.
31 changes: 23 additions & 8 deletions lib/internal/abort_controller.js
Expand Up @@ -10,6 +10,7 @@ const {
ObjectDefineProperty,
Symbol,
SymbolToStringTag,
WeakRef,
} = primordials;

const {
Expand Down Expand Up @@ -41,7 +42,6 @@ const { setTimeout } = require('timers');

const kAborted = Symbol('kAborted');
const kReason = Symbol('kReason');
const kTimeout = Symbol('kTimeout');

function customInspect(self, obj, depth, options) {
if (depth < 0)
Expand All @@ -59,6 +59,27 @@ function validateAbortSignal(obj) {
throw new ERR_INVALID_THIS('AbortSignal');
}

// Because the AbortSignal timeout cannot be canceled, we don't want the
// presence of the timer alone to keep the AbortSignal from being garbage
// collected if it otherwise no longer accessible. We also don't want the
// timer to keep the Node.js process open on it's own. Therefore, we wrap
// the AbortSignal in a WeakRef and have the setTimeout callback close
// over the WeakRef rather than directly over the AbortSignal, and we unref
// the created timer object.
function setWeakAbortSignalTimeout(weakRef, delay) {
const timeout = setTimeout(() => {
const signal = weakRef.deref();
if (signal !== undefined) {
abortSignal(
signal,
new DOMException(
'The operation was aborted due to timeout',
'TimeoutError'));
}
}, delay);
timeout.unref();
}

class AbortSignal extends EventTarget {
constructor() {
throw new ERR_ILLEGAL_CONSTRUCTOR();
Expand Down Expand Up @@ -101,13 +122,7 @@ class AbortSignal extends EventTarget {
static timeout(delay) {
validateUint32(delay, 'delay', true);
const signal = createAbortSignal();
signal[kTimeout] = setTimeout(() => {
abortSignal(
signal,
new DOMException(
'The operation was aborted due to timeout',
'TimeoutError'));
}, delay);
setWeakAbortSignalTimeout(new WeakRef(signal), delay);
return signal;
}
}
Expand Down
22 changes: 21 additions & 1 deletion test/parallel/test-abortcontroller.js
@@ -1,10 +1,11 @@
// Flags: --no-warnings
// Flags: --no-warnings --expose-gc
'use strict';

const common = require('../common');
const { inspect } = require('util');

const { ok, strictEqual, throws } = require('assert');
const { setTimeout: sleep } = require('timers/promises');

{
// Tests that abort is fired with the correct event type on AbortControllers
Expand Down Expand Up @@ -164,3 +165,22 @@ const { ok, strictEqual, throws } = require('assert');
strictEqual(signal.reason.code, 23);
}), 20);
}

{
(async () => {
// Test AbortSignal timeout doesn't prevent the signal
// from being garbage collected.
let ref;
{
ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000));
}

await sleep(10);
globalThis.gc();
strictEqual(ref.deref(), undefined);
})().then(common.mustCall());

// Setting a long timeout (20 minutes here) should not
// keep the Node.js process open (the timer is unref'd)
AbortSignal.timeout(1_200_000);
}

0 comments on commit cf9cb71

Please sign in to comment.