mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix(utils): polyfill AbortSignal.any for Chromium ≤115 (#1142)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { anyAbortSignal } from '@videojs/utils/events';
|
||||
import { generateId } from './utils/generate-id';
|
||||
|
||||
// =============================================================================
|
||||
@@ -76,7 +77,7 @@ export class Task<TValue = void, TError = unknown> implements TaskLike<TValue, T
|
||||
const rawId = config?.id;
|
||||
this.id = typeof rawId === 'function' ? rawId() : (rawId ?? generateId());
|
||||
this.#signal = config?.signal
|
||||
? AbortSignal.any([this.#abortController.signal, config.signal])
|
||||
? anyAbortSignal([this.#abortController.signal, config.signal])
|
||||
: this.#abortController.signal;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { anyAbortSignal } from '@videojs/utils/events';
|
||||
|
||||
export type SignalKey = PropertyKey;
|
||||
|
||||
export class AbortControllerRegistry {
|
||||
@@ -29,6 +31,6 @@ export class AbortControllerRegistry {
|
||||
this.#keys.get(key)?.abort();
|
||||
const controller = new AbortController();
|
||||
this.#keys.set(key, controller);
|
||||
return AbortSignal.any([this.#base.signal, controller.signal]);
|
||||
return anyAbortSignal([this.#base.signal, controller.signal]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
/**
|
||||
* Compose multiple abort signals into one that aborts when **any** input fires.
|
||||
* Uses native `AbortSignal.any` when available, otherwise falls back to a
|
||||
* manual `AbortController` composition for Chromium ≤115 and similar runtimes.
|
||||
*/
|
||||
export function anyAbortSignal(signals: AbortSignal[]): AbortSignal {
|
||||
if ('any' in AbortSignal) {
|
||||
return AbortSignal.any(signals);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
for (const signal of signals) {
|
||||
if (signal.aborted) {
|
||||
controller.abort(signal.reason);
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', () => controller.abort(signal.reason), {
|
||||
signal: controller.signal,
|
||||
});
|
||||
}
|
||||
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Race a promise against an abort signal. Rejects immediately if the signal
|
||||
* is already aborted or becomes aborted before the promise settles.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { abortable } from '../abort';
|
||||
import { abortable, anyAbortSignal } from '../abort';
|
||||
|
||||
describe('abortable', () => {
|
||||
it('resolves when promise resolves before abort', async () => {
|
||||
@@ -69,3 +69,132 @@ describe('abortable', () => {
|
||||
expect(removeEventListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('anyAbortSignal', () => {
|
||||
it('returns an AbortSignal', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
|
||||
expect(signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it('is not aborted initially when no input is aborted', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
|
||||
expect(signal.aborted).toBe(false);
|
||||
});
|
||||
|
||||
it('aborts immediately if the first input signal is already aborted', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const reason = new Error('already aborted');
|
||||
|
||||
a.abort(reason);
|
||||
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
it('aborts immediately if the second input signal is already aborted', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const reason = new Error('already aborted');
|
||||
|
||||
b.abort(reason);
|
||||
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
it('aborts when the first input signal fires', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
const reason = new Error('a aborted');
|
||||
|
||||
a.abort(reason);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
it('aborts when the second input signal fires', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
const reason = new Error('b aborted');
|
||||
|
||||
b.abort(reason);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
it('propagates the reason from the triggering signal', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
const reason = 'custom reason';
|
||||
|
||||
a.abort(reason);
|
||||
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
it('works with more than two signals', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const c = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal, c.signal]);
|
||||
const reason = new Error('c aborted');
|
||||
|
||||
c.abort(reason);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toBe(reason);
|
||||
});
|
||||
|
||||
describe('fallback path', () => {
|
||||
const nativeAny = AbortSignal.any;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error -- removing native to test fallback
|
||||
delete AbortSignal.any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
AbortSignal.any = nativeAny;
|
||||
});
|
||||
|
||||
it('works without native AbortSignal.any', () => {
|
||||
const a = new AbortController();
|
||||
const b = new AbortController();
|
||||
const signal = anyAbortSignal([a.signal, b.signal]);
|
||||
|
||||
expect(signal.aborted).toBe(false);
|
||||
|
||||
a.abort(new Error('fallback'));
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toEqual(new Error('fallback'));
|
||||
});
|
||||
|
||||
it('aborts immediately if input is already aborted (fallback)', () => {
|
||||
const a = new AbortController();
|
||||
|
||||
a.abort(new Error('pre-aborted'));
|
||||
|
||||
const signal = anyAbortSignal([a.signal]);
|
||||
|
||||
expect(signal.aborted).toBe(true);
|
||||
expect(signal.reason).toEqual(new Error('pre-aborted'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user