Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ import {AbortController} from './AbortController';
const ABORTED_KEY: symbol = Symbol('aborted');
const REASON_KEY: symbol = Symbol('reason');

const dependentSignals: WeakMap<AbortSignal, Set<AbortSignal>> = new WeakMap();
const sourceSignalRefs: WeakMap<
AbortSignal,
ReadonlyArray<WeakRef<AbortSignal>>,
> = new WeakMap();

/**
* The signal class.
* @see https://dom.spec.whatwg.org/#abortsignal
Expand Down Expand Up @@ -113,22 +119,37 @@ export class AbortSignal extends EventTarget {
}
}

// Third pass: none of the signals is aborted, so subscribe to all of them
// and abort the resulting signal when any of them aborts.
const controller = new AbortController();
const listeners = [];
const cleanup = () => listeners.forEach(unsubscribe => unsubscribe());
const resultSignal = createAbortSignal();
const resultSources: Set<AbortSignal> = new Set();

for (let i = 0; i < signals.length; i++) {
const signal = signals[i];
const onAbort = () => {
controller.abort(signal.reason);
cleanup();
};
signal.addEventListener('abort', onAbort);
listeners.push(() => signal.removeEventListener('abort', onAbort));
const sourceRefs = sourceSignalRefs.get(signal);
if (sourceRefs == null) {
resultSources.add(signal);
} else {
for (const sourceRef of sourceRefs) {
const sourceSignal = sourceRef.deref();
if (sourceSignal != null) {
resultSources.add(sourceSignal);
}
}
}
}
return controller.signal;

const resultSourceRefs: Array<WeakRef<AbortSignal>> = [];
for (const sourceSignal of resultSources) {
let dependents = dependentSignals.get(sourceSignal);
if (dependents == null) {
dependents = new Set();
dependentSignals.set(sourceSignal, dependents);
}
dependents.add(resultSignal);
resultSourceRefs.push(new WeakRef(sourceSignal));
}
sourceSignalRefs.set(resultSignal, resultSourceRefs);

return resultSignal;
}

// $FlowExpectedError[unsupported-syntax]
Expand Down Expand Up @@ -201,7 +222,43 @@ export function abortSignal(
signal[ABORTED_KEY] = true;
// $FlowExpectedError[prop-missing]
signal[REASON_KEY] = reason;

const dependentSignalsToAbort: Array<AbortSignal> = [];
const dependents = dependentSignals.get(signal);
if (dependents != null) {
dependentSignals.delete(signal);
for (const dependentSignal of dependents) {
// $FlowExpectedError[invalid-compare]
// $FlowExpectedError[prop-missing]
if (dependentSignal[ABORTED_KEY] === false) {
// $FlowExpectedError[prop-missing]
dependentSignal[ABORTED_KEY] = true;
// $FlowExpectedError[prop-missing]
dependentSignal[REASON_KEY] = reason;
unregisterDependentSignal(dependentSignal);
dependentSignalsToAbort.push(dependentSignal);
}
}
}

dispatchTrustedEvent(signal, new Event('abort'));
for (const dependentSignal of dependentSignalsToAbort) {
dispatchTrustedEvent(dependentSignal, new Event('abort'));
}
}

function unregisterDependentSignal(dependentSignal: AbortSignal): void {
const sourceRefs = sourceSignalRefs.get(dependentSignal);
if (sourceRefs == null) {
return;
}
sourceSignalRefs.delete(dependentSignal);
for (const sourceRef of sourceRefs) {
const sourceSignal = sourceRef.deref();
if (sourceSignal != null) {
dependentSignals.get(sourceSignal)?.delete(dependentSignal);
}
}
}

// Properties should be enumerable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,17 @@ describe('AbortController', () => {
const second = new AbortController();
const reason = new Error('stop');

const signal = AbortSignal.any([first.signal, second.signal]);
Fantom.runTask(() => {
const signal = AbortSignal.any([first.signal, second.signal]);

expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);
expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);

second.abort(reason);
second.abort(reason);

expect(signal.aborted).toBe(true);
expect(signal.reason).toBe(reason);
expect(signal.aborted).toBe(true);
expect(signal.reason).toBe(reason);
});
});

it('should abort immediately if one of the provided signals is already aborted', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import {AbortController} from '../AbortController';
import {AbortSignal} from '../AbortSignal';

describe('AbortSignal.any', () => {
it('aborts the dependent signal even if a source listener stops propagation', () => {
const source = new AbortController();
source.signal.addEventListener('abort', event => {
event.stopImmediatePropagation();
});
const signal = AbortSignal.any([source.signal]);

source.abort('cancelled');

expect(signal.aborted).toBe(true);
expect(signal.reason).toBe('cancelled');
});

it('marks dependent signals as aborted before dispatching to the source', () => {
const source = new AbortController();
let observed;
source.signal.addEventListener('abort', () => {
observed = [signal.aborted, signal.reason];
});
const signal = AbortSignal.any([source.signal]);

source.abort('reason');

expect(observed).toEqual([true, 'reason']);
});

it('dispatches to the source and then to its dependents in creation order', () => {
const first = new AbortController();
const second = new AbortController();
const inner = AbortSignal.any([first.signal, second.signal]);
const outer = AbortSignal.any([inner, inner, first.signal]);
const unrelated = AbortSignal.any([second.signal]);
const events: Array<string> = [];
first.signal.addEventListener('abort', () => {
events.push('first');
});
inner.addEventListener('abort', () => {
events.push('inner');
});
outer.addEventListener('abort', () => {
events.push('outer');
});
unrelated.addEventListener('abort', () => {
events.push('unrelated');
});

first.abort('first reason');
second.abort('second reason');

expect(events).toEqual(['first', 'inner', 'outer', 'unrelated']);
expect(outer.reason).toBe('first reason');
expect(unrelated.reason).toBe('second reason');
});

it('keeps the first reason when a source listener aborts another source', () => {
const first = new AbortController();
const second = new AbortController();
first.signal.addEventListener('abort', () => second.abort('second'));
const signal = AbortSignal.any([first.signal, second.signal]);
const listener = jest.fn();
signal.addEventListener('abort', listener);

first.abort('first');

expect(signal.reason).toBe('first');
expect(listener).toHaveBeenCalledTimes(1);
});
});
Loading