
# Patrones de AbortController

`AbortController` y `AbortSignal` son implementaciones completas de WHATWG en Phasis. También incluyen los ayudantes de composición estándar (`AbortSignal.any`, `AbortSignal.timeout`). Esta página explica los patrones más habituales y cómo se propaga la cancelación a través del límite de PHP.

## Cancelar una solicitud Fetch

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

De forma predeterminada, el motivo es `AbortError`, pero puedes indicar otro:

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

`fetch` se rechaza con el valor que le hayas pasado.

## Tiempo de espera

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

`AbortSignal.timeout(ms)` devuelve una señal que se aborta por sí sola después de `ms`. El temporizador se activa mediante el bucle de eventos del motor, el mismo que impulsa `setTimeout` / `queueMicrotask` (consulta [API: Bucle de eventos](https://phasis.inline0.dev/es/docs/api#event-loop)). Durante la transferencia, el transporte Fetch consulta la señal; con el `CurlTransport` predeterminado, lo hace mediante `CURLOPT_PROGRESSFUNCTION`, así que la solicitud se aborta rápidamente cuando vence el plazo.

El bucle usa **tiempo virtual** de forma predeterminada: adelanta el reloj hasta el siguiente plazo pendiente en lugar de esperar PHP, así que `fetch(url, { signal: AbortSignal.timeout(5000) })` se completa antes del plazo o se cancela en el siguiente ciclo del bucle; nunca hace que PHP espere. Para avanzar en tiempo real, el host debe ejecutar `tickEventLoop()` según el reloj real.

## Carrera: gana la primera cancelación

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

## Propagación entre capas

Envuelve tu propio trabajo asíncrono en un bucle que tenga en cuenta la señal:

```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 solicitud fetch hereda la señal directamente; el `if (signal.aborted)` manual cubre los intervalos entre solicitudes en los que, de otro modo, el bucle de página seguiría ejecutándose.

## Escuchar el evento de cancelación

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

`signal.reason` se establece antes de que se ejecuten los listeners.

## Señal en un transporte fetch personalizado

Si cambias el transporte mediante `setFetchTransport()`, la señal se pasa a tu función como `JsObject`. Respétala comprobándola periódicamente:

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

Lanzar `TransportException` con `kind: 'aborted'` hace que el JS del lado de `fetch` se rechace con `AbortError DOMException`, igual que si se hubiera ejecutado el bucle de cancelación de la especificación.

Para los backends con cancelación nativa (Guzzle de `on_progress`, `ResponseInterface::cancel` de Symfony), implementa el mismo patrón de comprobación y lanzamiento.

## Patrones que aún no funcionan

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — la ejecución de los listeners es síncrona; la solicitud fetch de limpieza comienza, pero no puede completarse durante el envío de la cancelación. Prográmala mediante el `await` que la rodea.

`setTimeout(() => ctrl.abort(), N)` funciona (la familia de temporizadores está incluida y cuenta con cobertura de WPT), pero `AbortSignal.timeout(N)` es más breve y expresa mejor la intención cuando solo necesitas una señal de cancelación al vencer el plazo.

## Véase también

- [Transporte Fetch](https://phasis.inline0.dev/es/docs/interop/fetch-transport) — interfaz completa de solicitudes, respuestas y señales para transportes personalizados.
- [API web (WPT)](https://phasis.inline0.dev/es/docs/compatibility/web-apis) — tasa de aprobación de WPT para AbortController/AbortSignal.
