
# Паттерны AbortController

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

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

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

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

```js
ctrl.abort(new Error('user cancelled'));
```

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

## Тайм-аут

```js
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
```

`AbortSignal.timeout(ms)` возвращает сигнал, который отменяет операцию через `ms`. Таймер срабатывает через цикл событий движка — тот же цикл обслуживает `setTimeout` / `queueMicrotask` (см. [API: Цикл событий](https://phasis.inline0.dev/ru/docs/api#event-loop)). Во время передачи транспорт Fetch опрашивает сигнал — для транспорта `CurlTransport` по умолчанию это делает `CURLOPT_PROGRESSFUNCTION`, — поэтому запрос отменяется сразу после наступления срока.

По умолчанию цикл работает в **виртуальном времени**: он переводит часы на ближайший ожидающий срок, а не ждет PHP, поэтому `fetch(url, { signal: AbortSignal.timeout(5000) })` либо завершается до истечения срока, либо отменяется на следующем шаге цикла — ожидание PHP никогда не задерживается. Чтобы соблюдать реальное время, хост должен управлять `tickEventLoop()` по настенным часам.

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

```js
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
```

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

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

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

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

```js
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](https://phasis.inline0.dev/ru/docs/interop/fetch-transport) — полный интерфейс запроса, ответа и сигнала для пользовательских транспортов.
- [Веб-API (WPT)](https://phasis.inline0.dev/ru/docs/compatibility/web-apis) — доля успешно пройденных тестов WPT для AbortController/AbortSignal.
