Vai al contenuto

Schemi d'uso di AbortController

AbortController e AbortSignal sono implementazioni WHATWG complete in Phasis. Sono inclusi anche gli helper standard di composizione (AbortSignal.any, AbortSignal.timeout). Questa pagina illustra gli schemi più utili e il modo in cui l'annullamento si propaga attraverso il confine PHP.

Annullare una richiesta fetch

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

Il motivo predefinito è AbortError, ma potete specificarne uno vostro:

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

fetch viene rifiutato con il valore passato.

Timeout

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

AbortSignal.timeout(ms) restituisce un segnale che si annulla automaticamente dopo ms. Il timer scatta tramite l'event loop del motore, lo stesso che gestisce setTimeout / queueMicrotask (consultate API: Event loop). Il trasporto fetch controlla il segnale durante il trasferimento — per il valore predefinito CurlTransport, tramite CURLOPT_PROGRESSFUNCTION — quindi la richiesta viene annullata prontamente alla scadenza.

Per impostazione predefinita, il loop usa il tempo virtuale: porta avanti l'orologio fino alla prossima scadenza, invece di attendere PHP. Così una fetch(url, { signal: AbortSignal.timeout(5000) }) termina prima della scadenza oppure viene annullata al tick successivo del loop: non costringe mai PHP ad aspettare. Per scandire il tempo reale, l'host deve eseguire tickEventLoop() in base a un orologio reale.

Race: prevale il primo annullamento

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

Propagazione attraverso i livelli

Avvolgete il vostro lavoro asincrono in un loop che controlla il segnale:

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

La richiesta fetch eredita direttamente il segnale; l'uso manuale di if (signal.aborted) copre gli intervalli tra le richieste, durante i quali il loop della pagina continuerebbe altrimenti a funzionare.

Ascoltare l'evento di annullamento

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

signal.reason viene impostato prima che vengano eseguiti i listener.

Segnale in un trasporto fetch personalizzato

Se sostituite il trasporto tramite setFetchTransport(), il segnale viene passato al vostro callable come JsObject. Per rispettarlo, controllatelo periodicamente:

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

Lanciare TransportException con kind: 'aborted' fa sì che il lato JS di fetch venga rifiutato con AbortError DOMException, come se fosse stato eseguito il ciclo di annullamento previsto dalle specifiche.

Per i backend con annullamento nativo (Guzzle di on_progress, ResponseInterface::cancel di Symfony), usate lo stesso schema di controllo e lancio dell'errore.

Schemi che ancora non funzionano

  • signal.addEventListener('abort', () => fetch(cleanupUrl)) — i listener vengono eseguiti in modo sincrono; la richiesta fetch di pulizia si avvia, ma non può completarsi durante la gestione dell'annullamento. Pianificatela tramite await.

setTimeout(() => ctrl.abort(), N) funziona (la famiglia dei timer è inclusa e verificata dai WPT), ma AbortSignal.timeout(N) è più breve e comunica meglio l'intento quando volete soltanto un segnale di annullamento alla scadenza.

Vedi anche

  • Trasporto fetch — interfaccia completa per richieste, risposte e segnali per i trasporti personalizzati.
  • API web (WPT) — percentuale di superamento dei WPT per AbortController/AbortSignal.