
# AbortController-patronen

`AbortController` en `AbortSignal` zijn volledige WHATWG-implementaties in Phasis. Ook de standaardcompositiehelpers (`AbortSignal.any`, `AbortSignal.timeout`) worden meegeleverd. Op deze pagina staan de patronen die je in de praktijk gebruikt en hoe annulering zich over de grens van PHP verspreidt.

## Een fetch annuleren

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

De reden is standaard `AbortError`, maar je kunt er zelf een opgeven:

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

`fetch` wordt afgewezen met de waarde die je hebt doorgegeven.

## Time-out

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

`AbortSignal.timeout(ms)` geeft een signaal terug dat zichzelf na `ms` afbreekt. De timer wordt geactiveerd via de eventloop van de engine, dezelfde die `setTimeout` / `queueMicrotask` aanstuurt (zie [API: Eventloop](https://phasis.inline0.dev/nl/docs/api#event-loop)). Het fetchtransport controleert het signaal tijdens de overdracht — voor de standaard-`CurlTransport` via `CURLOPT_PROGRESSFUNCTION` — zodat het verzoek direct wordt afgebroken zodra de deadline verstrijkt.

De loop gebruikt standaard **virtuele tijd**: de klok wordt vooruitgezet naar de volgende openstaande deadline in plaats van dat de engine PHP slaapt. Daardoor is een `fetch(url, { signal: AbortSignal.timeout(5000) })` óf klaar voordat de deadline verstrijkt, óf wordt deze bij de volgende looptik afgebroken — PHP hoeft nooit te wachten. Voor timing in echte tijd moet de host `tickEventLoop()` aansturen met een wandklok.

## Race: de eerste annulering wint

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

## Propagatie door lagen heen

Omwikkel je eigen asynchrone werk met een signaalgevoelige loop:

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

De fetch erft het signaal rechtstreeks; de handmatige `if (signal.aborted)` dekt de gaten tussen verzoeken waarin de paginaloop anders zou blijven draaien.

## Luisteren naar de abort-gebeurtenis

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

`signal.reason` wordt ingesteld voordat listeners worden aangeroepen.

## Signaal in een aangepast fetchtransport

Als je het transport vervangt via `setFetchTransport()`, wordt het signaal als `JsObject` doorgegeven aan je callable. Houd er rekening mee door het te controleren:

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

Door `TransportException` te gooien met `kind: 'aborted'` wordt JS-zijde `fetch` afgewezen met `AbortError DOMException` — net alsof de abort-loop uit de specificatie was uitgevoerd.

Gebruik voor backends met native annulering (Guzzle's `on_progress`, Symfony's `ResponseInterface::cancel`) dezelfde aanpak: controleer het signaal en gooi een fout.

## Patronen die nog niet werken

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — listeners worden synchroon aangeroepen. De opschoon-fetch start, maar wordt niet voltooid tijdens de abort-dispatch. Plan deze via de omringende `await`.

`setTimeout(() => ctrl.abort(), N)` werkt (de timerfamilie wordt meegeleverd en is gedekt door WPT), maar `AbortSignal.timeout(N)` is korter en geeft de bedoeling beter weer als je alleen een signaal nodig hebt dat na een deadline wordt afgebroken.

## Zie ook

- [Fetchtransport](https://phasis.inline0.dev/nl/docs/interop/fetch-transport) — volledige request-, response- en signaalinterface voor aangepaste transports.
- [Web-API's (WPT)](https://phasis.inline0.dev/nl/docs/compatibility/web-apis) — slaagpercentage voor AbortController/AbortSignal in WPT.
