
# AbortController-mallit

`AbortController` ja `AbortSignal` ovat kohteessa Phasis täysin WHATWG-määrittelyn mukaisia toteutuksia. Myös määrittelyn mukaiset yhdistämisapufunktiot (`AbortSignal.any`, `AbortSignal.timeout`) sisältyvät. Tällä sivulla käsitellään yleisimmin tarvittavia toimintamalleja ja peruutuksen välittymistä kohteiden PHP välillä.

## Fetch-pyynnön peruuttaminen

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

Oletusarvo on `AbortError`, mutta voit antaa oman syysi:

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

`fetch` hylkää minkä tahansa välittämäsi arvon kanssa.

## Aikakatkaisu

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

`AbortSignal.timeout(ms)` palauttaa signaalin, joka keskeyttää itsensä ajan `ms` kuluttua. Ajastin toimii moottorin tapahtumasilmukan kautta. Sama silmukka ohjaa toimintoja `setTimeout` / `queueMicrotask` (katso [API: Tapahtumasilmukka](https://phasis.inline0.dev/fi/docs/api#event-loop)). Fetch-siirtotapa tarkistaa signaalin siirron aikana — oletusarvoisella `CurlTransport`-siirtotavalla tarkistus tehdään funktiolla `CURLOPT_PROGRESSFUNCTION` — joten pyyntö keskeytetään heti määräajan koittaessa.

Oletusarvoisesti silmukka toimii **virtuaaliajassa**: se siirtää kellonsa seuraavaan odottavaan määräaikaan sen sijaan, että odottaisi PHP, joten `fetch(url, { signal: AbortSignal.timeout(5000) })` joko valmistuu ennen määräaikaa tai keskeytyy silmukan seuraavalla kierroksella — kohteen PHP odotusta ei koskaan viivästetä. Reaaliaikainen tahdistus edellyttää, että isäntä käyttää kohdetta `tickEventLoop()` seinäkellon mukaisesti.

## Kilpailu: ensimmäinen peruutus voittaa

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

## Välittyminen kerrosten läpi

Kääri oma asynkroninen työsi signaalin huomioivaan silmukkaan:

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

Fetch perii signaalin suoraan; manuaalinen `if (signal.aborted)` kattaa pyyntöjen välit, joiden aikana sivun silmukka muuten jatkaisi suoritusta.

## Keskeytystapahtuman kuuntelu

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

`signal.reason` asetetaan ennen kuuntelijoiden käynnistymistä.

## Signaali mukautetussa fetch-siirtotavassa

Jos vaihdat siirtotavan funktiolla `setFetchTransport()`, signaali välitetään kutsuttavalle `JsObject`-arvona. Huomioi se tarkistamalla:

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

Kun `TransportException` heitetään kohteessa `kind: 'aborted'`, kohteen JS puolen `fetch` hylätään arvolla `AbortError DOMException` — aivan kuin määrittelyn mukainen keskeytyssilmukka olisi suoritettu.

Jos taustajärjestelmä tukee natiivia peruutusta (kohteen Guzzle `on_progress`, kohteen Symfony `ResponseInterface::cancel`), käytä samaa tarkista ja heitä -mallia.

## Mallit, jotka eivät vielä toimi

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — kuuntelijat suoritetaan synkronisesti; siivous-Fetch-pyyntö käynnistyy, mutta ei valmistu keskeytyksen käsittelyn aikana. Ajoita se ympäröivän kohteen `await` avulla.

`setTimeout(() => ctrl.abort(), N)` toimii (ajastinperhe sisältyy ja sen toiminta katetaan WPT-testeillä), mutta `AbortSignal.timeout(N)` on lyhyempi ja kertoo tarkoituksen selkeämmin, kun haluat vain keskeytyssignaalin määräajan jälkeen.

## Katso myös

- [Fetch-siirtotapa](https://phasis.inline0.dev/fi/docs/interop/fetch-transport) — mukautettujen siirtotapojen koko pyyntö-, vastaus- ja signaalirajapinta.
- [Verkko-API:t (WPT)](https://phasis.inline0.dev/fi/docs/compatibility/web-apis) — AbortController/AbortSignal-testien läpäisyosuus WPT-testisarjassa.
