
# Modèles d'utilisation d'AbortController

`AbortController` et `AbortSignal` sont des implémentations WHATWG complètes dans Phasis. Les méthodes standard de composition (`AbortSignal.any`, `AbortSignal.timeout`) sont également fournies. Cette page présente les modèles d'utilisation courants et la propagation de l'annulation à travers la frontière PHP.

## Annuler une requête fetch

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

Par défaut, le motif est `AbortError`, mais vous pouvez fournir le vôtre :

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

`fetch` rejette avec la valeur que vous avez transmise.

## Délai d'attente

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

`AbortSignal.timeout(ms)` renvoie un signal qui s'annule de lui-même après `ms`. Le minuteur se déclenche via la boucle d'événements du moteur, la même que celle qui pilote `setTimeout` / `queueMicrotask` (voir [API : boucle d'événements](https://phasis.inline0.dev/fr/docs/api#event-loop)). Pendant le transfert, le transport fetch interroge le signal — pour le `CurlTransport` par défaut, via `CURLOPT_PROGRESSFUNCTION` — afin que la requête soit rapidement annulée dès que l'échéance est atteinte.

La boucle utilise par défaut le **temps virtuel** : elle fait avancer son horloge jusqu’à la prochaine échéance en attente au lieu d’attendre PHP. Ainsi, un `fetch(url, { signal: AbortSignal.timeout(5000) })` se termine avant l’échéance ou est interrompu au prochain cycle de boucle — il ne fait jamais attendre PHP. Pour un rythme en temps réel, l’hôte doit piloter `tickEventLoop()` en fonction de l’horloge réelle.

## Course : la première annulation l’emporte

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

## Propagation à travers les couches

Encapsulez votre propre travail asynchrone dans une boucle qui tient compte du signal :

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

La requête fetch hérite directement du signal ; le `if (signal.aborted)` manuel couvre les intervalles entre les requêtes, pendant lesquels la boucle de page continuerait sinon à s’exécuter.

## Écouter l’événement d’interruption

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

`signal.reason` est défini avant le déclenchement des écouteurs.

## Signal dans un transport fetch personnalisé

Si vous remplacez le transport via `setFetchTransport()`, le signal est transmis à votre fonction sous la forme d’un `JsObject`. Respectez-le en vérifiant régulièrement son état :

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

Lancer `TransportException` avec `kind: 'aborted'` fait rejeter le JS côté `fetch` avec `AbortError DOMException` — comme si la boucle d’interruption de la spécification avait été exécutée.

Pour les backends prenant en charge l’annulation nativement (le `on_progress` de Guzzle, le `ResponseInterface::cancel` de Symfony), utilisez le même schéma de vérification et de lancement d’exception.

## Modèles qui ne fonctionnent pas encore

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — les écouteurs se déclenchent de façon synchrone ; la requête fetch de nettoyage démarre, mais ne peut pas se terminer pendant la propagation de l’interruption. Planifiez-la via le `await` environnant.

`setTimeout(() => ctrl.abort(), N)` fonctionne (la famille des minuteurs est disponible et couverte par WPT), mais `AbortSignal.timeout(N)` est plus court et exprime mieux l’intention lorsque vous voulez simplement un signal d’interruption après une échéance.

## Voir aussi

- [Transport Fetch](https://phasis.inline0.dev/fr/docs/interop/fetch-transport) — interface complète de requête, réponse et signal pour les transports personnalisés.
- [API Web (WPT)](https://phasis.inline0.dev/fr/docs/compatibility/web-apis) — taux de réussite WPT d’AbortController/AbortSignal.
