Przejdź do treści

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

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

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

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

Limit czasu

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

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

Propagacja przez kolejne warstwy

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

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

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

JavaScript
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