
# AbortController-Muster

`AbortController` und `AbortSignal` sind vollständige WHATWG-Implementierungen in Phasis. Auch die standardmäßigen Kompositionshelfer (`AbortSignal.any`, `AbortSignal.timeout`) sind enthalten. Diese Seite behandelt gängige Muster und erklärt, wie sich ein Abbruch über die PHP-Grenze hinweg fortpflanzt.

## Einen Fetch abbrechen

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

Der Grund ist standardmäßig `AbortError`, du kannst aber einen eigenen angeben:

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

`fetch` wird mit dem übergebenen Wert zurückgewiesen.

## Timeout

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

`AbortSignal.timeout(ms)` gibt ein Signal zurück, das sich nach `ms` selbst abbricht. Der Timer wird über die Ereignisschleife der Engine ausgelöst, dieselbe, die auch `setTimeout` / `queueMicrotask` ausführt (siehe [API: Ereignisschleife](https://phasis.inline0.dev/de/docs/api#event-loop)). Der Fetch-Transport fragt das Signal während der Übertragung ab – beim standardmäßigen `CurlTransport` über `CURLOPT_PROGRESSFUNCTION` –, sodass die Anfrage sofort nach Ablauf der Frist abgebrochen wird.

Die Schleife verwendet standardmäßig **virtuelle Zeit**: Sie stellt ihre Uhr auf die nächste anstehende Frist vor, statt PHP zu schlafen. Ein `fetch(url, { signal: AbortSignal.timeout(5000) })` wird daher entweder vor der Frist abgeschlossen oder beim nächsten Schleifendurchlauf abgebrochen – PHP muss nie warten. Für die Taktung nach Echtzeit muss der Host `tickEventLoop()` anhand einer realen Uhr ausführen.

## Wettlauf: Der erste Abbruch gewinnt

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

## Weitergabe über mehrere Ebenen

Umschließe deine eigene asynchrone Arbeit mit einer signalbewussten Schleife:

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

Der Fetch übernimmt das Signal direkt; das manuelle `if (signal.aborted)` deckt die Lücken zwischen Anfragen ab, in denen die Seitenschleife sonst weiterlaufen würde.

## Auf das Abbruchereignis reagieren

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

`signal.reason` wird gesetzt, bevor Listener ausgelöst werden.

## Signal in einem benutzerdefinierten Fetch-Transport

Wenn du den Transport über `setFetchTransport()` austauschst, wird das Signal als `JsObject` an deine aufrufbare Funktion übergeben. Frage es regelmäßig ab, um darauf zu reagieren:

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

Wenn du `TransportException` mit `kind: 'aborted'` auslöst, wird das JS-seitige `fetch` mit `AbortError DOMException` zurückgewiesen – so, als wäre die Abbruchschleife der Spezifikation ausgeführt worden.

Bei Backends mit nativer Abbruchfunktion (`on_progress` von Guzzle, `ResponseInterface::cancel` von Symfony) verwendest du dieselbe Abfrage-und-Auslöse-Logik.

## Muster, die noch nicht funktionieren

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** – Listener werden synchron ausgelöst. Der Aufräum-Fetch startet, wird aber nicht während der Abbruchverarbeitung abgeschlossen. Plane ihn über die umgebende `await` ein.

`setTimeout(() => ctrl.abort(), N)` funktioniert (die Timer-Familie ist enthalten und durch WPT abgedeckt), aber `AbortSignal.timeout(N)` ist kürzer und beschreibt die Absicht besser, wenn du nur ein Signal möchtest, das nach einer Frist abbricht.

## Siehe auch

- [Fetch-Transport](https://phasis.inline0.dev/de/docs/interop/fetch-transport) – vollständige Request-, Response- und Signal-Schnittstelle für benutzerdefinierte Transporte.
- [Web-APIs (WPT)](https://phasis.inline0.dev/de/docs/compatibility/web-apis) – Bestehensquote der AbortController/AbortSignal-WPT-Tests.
