Перейти к содержимому

Паттерны AbortController

AbortController и AbortSignal — полноценные реализации WHATWG в Phasis. Стандартные вспомогательные методы композиции (AbortSignal.any, AbortSignal.timeout) тоже доступны. На этой странице описаны часто используемые паттерны и передача отмены через границу PHP.

Отмена fetch-запроса

JavaScript
const ctrl = new AbortController();
const p = fetch(url, { signal: ctrl.signal });
ctrl.abort();                     // p rejects with AbortError DOMException

Причина по умолчанию — AbortError, но можно указать собственную:

JavaScript
ctrl.abort(new Error('user cancelled'));

fetch выполняет отклонение с переданным вами значением.

Тайм-аут

JavaScript
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() по настенным часам.

Гонка: побеждает первая отмена

JavaScript
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

Передача отмены через слои

Оборачивайте собственные асинхронные операции циклом, учитывающим сигнал:

JavaScript
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) обрабатывает промежутки между запросами, в которых цикл страницы иначе продолжал бы выполняться.

Обработка события отмены

JavaScript
ctrl.signal.addEventListener('abort', () => {
  releaseDatabaseLock();
  cancelDownstreamWork();
});

signal.reason устанавливается до вызова обработчиков.

Сигнал в пользовательском транспорте fetch

Если вы меняете транспорт с помощью setFetchTransport(), сигнал передается вашей вызываемой функции как JsObject. Учитывайте его, выполняя опрос:

PHP
$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.