Zum Inhalt springen

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

JavaScript
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:

JavaScript
ctrl.abort(new Error('user cancelled'));

fetch wird mit dem übergebenen Wert zurückgewiesen.

Timeout

JavaScript
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). 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

JavaScript
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:

JavaScript
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

JavaScript
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 – vollständige Request-, Response- und Signal-Schnittstelle für benutzerdefinierte Transporte.
  • Web-APIs (WPT) – Bestehensquote der AbortController/AbortSignal-WPT-Tests.