Паттерны 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 выполняет отклонение с переданным вами значением.
Тайм-аут
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', Promise на стороне 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.