
# Wzorce AbortController

`AbortController` i `AbortSignal` to pełne implementacje WHATWG w Phasis. Dostępne są również standardowe funkcje kompozycji (`AbortSignal.any`, `AbortSignal.timeout`). Ta strona omawia wzorce, po które faktycznie sięgasz, oraz sposób propagacji anulowania przez granicę PHP.

## Anulowanie fetch

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

Domyślnie powodem jest `AbortError`, ale możesz podać własny:

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

`fetch` odrzuca obietnicę z przekazaną wartością.

## Limit czasu

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

`AbortSignal.timeout(ms)` zwraca sygnał, który sam się anuluje po upływie `ms`. Timer uruchamia się przez pętlę zdarzeń silnika, tę samą, która obsługuje `setTimeout` / `queueMicrotask` (zobacz [API: Pętla zdarzeń](https://phasis.inline0.dev/pl/docs/api#event-loop)). Transport fetch sprawdza sygnał podczas transferu — w przypadku domyślnego `CurlTransport` za pośrednictwem `CURLOPT_PROGRESSFUNCTION` — więc żądanie jest szybko anulowane po upływie terminu.

Pętla domyślnie działa w **czasie wirtualnym**: przesuwa zegar do następnego oczekującego terminu zamiast czekać PHP, więc `fetch(url, { signal: AbortSignal.timeout(5000) })` albo kończy działanie przed upływem terminu, albo zostaje przerwane przy kolejnym tyknięciu pętli — nigdy nie każe PHP czekać. Aby sterować tempem w czasie rzeczywistym, host musi prowadzić `tickEventLoop()` według zegara ściennego.

## Wyścig: pierwsze anulowanie wygrywa

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

## Propagacja przez kolejne warstwy

Opakuj własną pracę asynchroniczną w pętlę reagującą na sygnał:

```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 dziedziczy sygnał bezpośrednio; ręczne `if (signal.aborted)` obejmuje przerwy między żądaniami, podczas których pętla strony działałaby dalej.

## Nasłuchiwanie zdarzenia przerwania

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

`signal.reason` jest ustawiane przed wywołaniem nasłuchujących.

## Sygnał we własnym transporcie fetch

Jeśli zmienisz transport przez `setFetchTransport()`, sygnał zostanie przekazany wywoływanej funkcji jako `JsObject`. Obsłuż go, sprawdzając okresowo:

```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',
        );
    }
    // ...
});
```

Rzucenie `TransportException` z `kind: 'aborted'` sprawia, że JS po stronie `fetch` odrzuca z `AbortError DOMException` — tak samo, jak gdyby zadziałała pętla przerwania ze specyfikacji.

W przypadku backendów z natywnym anulowaniem (Guzzle's `on_progress`, Symfony's `ResponseInterface::cancel`) zastosuj ten sam schemat sprawdzania i rzucania wyjątku.

## Wzorce, które jeszcze nie działają

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — nasłuchujące są wywoływane synchronicznie; żądanie fetch sprzątające po przerwaniu zostaje rozpoczęte, ale nie zakończy się podczas obsługi przerwania. Zaplanuj je przez otaczającą je `await`.

`setTimeout(() => ctrl.abort(), N)` działa (zestaw timerów jest dostępny i objęty testami WPT), ale `AbortSignal.timeout(N)` jest krótsze i lepiej wyraża zamiar, gdy potrzebujesz tylko sygnału przerwania po upływie terminu.

## Zobacz także

- [Transport fetch](https://phasis.inline0.dev/pl/docs/interop/fetch-transport) — pełny interfejs żądania, odpowiedzi i sygnału dla własnych transportów.
- [Interfejsy API sieci (WPT)](https://phasis.inline0.dev/pl/docs/compatibility/web-apis) — odsetek pomyślnych testów WPT dla AbortController/AbortSignal.
