Шаблони AbortController
AbortController і AbortSignal — повні реалізації WHATWG у Phasis. Також доступні стандартні помічники композиції (AbortSignal.any, AbortSignal.timeout). На цій сторінці описано поширені на практиці шаблони та поширення скасування через межу PHP.
Скасування запиту fetch
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort(); // p rejects with AbortError DOMException
За замовчуванням причина — AbortError, але можна передати власну:
ctrl.abort(new Error('user cancelled'));
fetch відхиляє Promise зі значенням, яке ви передали.
Тайм-аут
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
AbortSignal.timeout(ms) повертає сигнал, який скасовує сам себе через ms. Таймер спрацьовує в циклі подій рушія — тому самому, який обробляє setTimeout / queueMicrotask (див. API: Цикл подій). Транспорт fetch перевіряє сигнал під час передавання — для транспорту CurlTransport за замовчуванням через CURLOPT_PROGRESSFUNCTION, — тож запит скасовується невдовзі після спливання терміну.
За замовчуванням цикл працює у віртуальному часі: він пересуває годинник до найближчого запланованого терміну замість того, щоб чекати PHP, тож fetch(url, { signal: AbortSignal.timeout(5000) }) або завершується до спливання терміну, або скасовується на наступному такті циклу — очікування PHP не затримується. Для роботи в реальному часі хост має запускати tickEventLoop() за настінним годинником.
Змагання: перемагає перше скасування
const user = new AbortController();
const signal = AbortSignal.any([user.signal, AbortSignal.timeout(10_000)]);
const res = await fetch(url, { signal });
// res rejects if EITHER the user clicks cancel OR 10s elapses
Поширення через рівні
Оберніть власну асинхронну роботу в цикл, що реагує на сигнал:
async function fetchPaged(url, signal) {
const out = [];
for (let page = 1; ; page++) {
if (signal.aborted) throw signal.reason;
const r = await fetch(`${url}?page=${page}`, { signal });
const data = await r.json();
if (data.length === 0) break;
out.push(...data);
}
return out;
}
Запит fetch безпосередньо успадковує сигнал; ручний if (signal.aborted) охоплює проміжки між запитами, коли цикл сторінки інакше продовжував би працювати.
Прослуховування події скасування
ctrl.signal.addEventListener('abort', () => {
releaseDatabaseLock();
cancelDownstreamWork();
});
signal.reason встановлюється до виклику обробників.
Сигнал у власному транспорті fetch
Якщо замінити транспорт через setFetchTransport(), сигнал буде передано вашій функції як JsObject. Перевіряйте його під час роботи:
$engine->setFetchTransport(function (array $req, $signal) {
$ch = curl_init($req['url']);
// ...
if ($signal !== null) {
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
fn () => $signal->get('aborted')->value ? 1 : 0);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
}
$body = curl_exec($ch);
if (curl_errno($ch) === CURLE_ABORTED_BY_CALLBACK) {
throw new \Phasis\BuiltIn\Fetch\TransportException(
'aborted', 'aborted',
);
}
// ...
});
Якщо викинути TransportException з kind: 'aborted', JS-й fetch буде відхилено з AbortError DOMException — так само, якби спрацював цикл скасування зі специфікації.
Для бекендів із вбудованим скасуванням (Guzzle — on_progress, Symfony — ResponseInterface::cancel) використовуйте такий самий шаблон перевірки й викидання помилки.
Шаблони, які поки що не працюватимуть
signal.addEventListener('abort', () => fetch(cleanupUrl))— обробники викликаються синхронно; запит fetch для очищення запускається, але не завершиться під час обробки події скасування. Заплануйте його через зовнішнійawait.
setTimeout(() => ctrl.abort(), N) працює (сімейство таймерів доступне й покрите тестами WPT), але AbortSignal.timeout(N) коротший і краще передає намір, коли потрібен лише сигнал скасування після завершення терміну.
Див. також
- Транспорт Fetch — повний інтерфейс запиту/відповіді/сигналу для власних транспортів.
- Веб-API (WPT) — відсоток успішних тестів WPT для AbortController/AbortSignal.