
# Шаблони 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` відхиляє Promise зі значенням, яке ви передали.

## Тайм-аут

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

`AbortSignal.timeout(ms)` повертає сигнал, який скасовує сам себе через `ms`. Таймер спрацьовує в циклі подій рушія — тому самому, який обробляє `setTimeout` / `queueMicrotask` (див. [API: Цикл подій](https://phasis.inline0.dev/uk/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'`, 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/uk/docs/interop/fetch-transport) — повний інтерфейс запиту/відповіді/сигналу для власних транспортів.
- [Веб-API (WPT)](https://phasis.inline0.dev/uk/docs/compatibility/web-apis) — відсоток успішних тестів WPT для AbortController/AbortSignal.
