@whatwg-node/server
Version:
Fetch API compliant HTTP Server adapter
87 lines (86 loc) • 3.82 kB
JavaScript
;
/**
* ⚠️ straight up copy from @graphql-hive/signal@2.0.0
*
* copied because of circular dependencies, where hive gateway monorepo uses whatwg server and also
* hosts the signal package. importing that package from whatwg-server will fail with tsx. it was
* thought that this was fixed with a package patch in hive gateway - but apparently not...
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.abortSignalAny = abortSignalAny;
// https://github.com/unjs/std-env/blob/ab15595debec9e9115a9c1d31bc7597a8e71dbfd/src/runtimes.ts
const isNode = !globalThis.Bun && globalThis.process?.release?.name === 'node';
const anySignalRegistry = isNode ? new FinalizationRegistry(cb => cb()) : null;
const controllerInSignalSy = Symbol('CONTROLLER_IN_SIGNAL');
/**
* Memory safe ponyfill of `AbortSignal.any`. In Node environments, the native
* `AbortSignal.any` seems to be leaky and can lead to subtle memory leaks over
* a larger period of time.
*
* This ponyfill is a custom implementation that makes sure AbortSignals get properly
* GC-ed as well as aborted.
*/
function abortSignalAny(signals) {
if (signals.length === 0) {
// if no signals are passed, return undefined because the abortcontroller
// wouldnt ever be aborted (should be when GCd, but it's only a waste of memory)
// furthermore, the native AbortSignal.any will also never abort if receiving no signals
return undefined;
}
if (signals.length === 1) {
// no need to waste resources by wrapping a single signal, simply return it
return signals[0];
}
if (!isNode) {
// AbortSignal.any seems to be leaky only in Node env
// TODO: should we ponyfill other envs, will they always have AbortSignal.any?
return AbortSignal.any(signals);
}
for (const signal of signals) {
if (signal.aborted) {
// if any of the signals has already been aborted, return it immediately no need to continue at all
return signal;
}
}
// we use weak refs for both the root controller and the passed signals
// because we want to make sure that signals are aborted and disposed of
// in both cases when GC-ed and actually aborted
const ctrl = new AbortController();
const ctrlRef = new WeakRef(ctrl);
const eventListenerPairs = [];
let retainedSignalsCount = signals.length;
for (const signal of signals) {
const signalRef = new WeakRef(signal);
function abort() {
ctrlRef.deref()?.abort(signalRef.deref()?.reason);
}
signal.addEventListener('abort', abort);
eventListenerPairs.push([signalRef, abort]);
anySignalRegistry.register(signal, () =>
// dispose when all of the signals have been GCed
!--retainedSignalsCount && dispose(), signal);
}
function dispose() {
for (const [signalRef, abort] of eventListenerPairs) {
const signal = signalRef.deref();
if (signal) {
signal.removeEventListener('abort', abort);
anySignalRegistry.unregister(signal);
}
const ctrl = ctrlRef.deref();
if (ctrl) {
anySignalRegistry.unregister(ctrl.signal);
// @ts-expect-error property will exist
delete ctrl.signal[controllerInSignalSy];
}
}
}
// cleanup when aborted
ctrl.signal.addEventListener('abort', dispose);
// cleanup when GCed
anySignalRegistry.register(ctrl.signal, dispose, ctrl.signal);
// keeping a strong reference of the cotroller binding it to the lifecycle of its signal
// @ts-expect-error property will exist
ctrl.signal[controllerInSignalSy] = ctrl;
return ctrl.signal;
}