
# Начини за работа с 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/bg/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))`** — извикването на слушателите е синхронно; заявката за почистване стартира, но няма да завърши по време на извикването за отмяна. Планирайте я чрез обграждащия `await`.

`setTimeout(() => ctrl.abort(), N)` работи (семейството таймери е включено и покрито от WPT), но `AbortSignal.timeout(N)` е по-кратко и изразява по-добре намерението, когато искате само сигнал за прекратяване след краен срок.

## Вижте също

- [Транспорт за fetch](https://phasis.inline0.dev/bg/docs/interop/fetch-transport) — пълен интерфейс за заявка/отговор/сигнал за потребителски транспорт.
- [Уеб API (WPT)](https://phasis.inline0.dev/bg/docs/compatibility/web-apis) — процент на успешно преминалите WPT тестове за AbortController/AbortSignal.
