
# AbortController 사용 패턴

`AbortController`와 `AbortSignal`는 Phasis의 완전한 WHATWG 구현입니다. 표준 합성 도우미(`AbortSignal.any`, `AbortSignal.timeout`)도 제공됩니다. 이 페이지에서는 실제로 자주 사용하는 패턴과 PHP 경계를 넘어 취소가 전파되는 방식을 다룹니다.

## fetch 취소

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

기본적으로 이유는 `AbortError`이지만, 직접 지정할 수도 있습니다.

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

`fetch`는 전달한 값과 함께 거부됩니다.

## 시간 초과

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

`AbortSignal.timeout(ms)`는 `ms` 후 자체적으로 중단되는 신호를 반환합니다. 타이머는 `setTimeout` / `queueMicrotask`를 구동하는 것과 동일한 엔진 이벤트 루프를 통해 실행됩니다([API: 이벤트 루프](https://phasis.inline0.dev/ko/docs/api#event-loop) 참조). fetch 전송 계층은 전송 중 신호를 확인합니다. 기본 `CurlTransport`의 경우 `CURLOPT_PROGRESSFUNCTION`를 통해 확인하므로 기한에 도달하면 요청이 신속하게 중단됩니다.

이 루프는 기본적으로 **가상 시간**으로 동작합니다. 잠들지 않고 다음 대기 중인 기한으로 시계를 진행하므로 PHP, `fetch(url, { signal: AbortSignal.timeout(5000) })`은 기한 전에 완료되거나 다음 루프 틱에서 중단됩니다. PHP이 기다리는 일은 없습니다. 실시간 속도로 동작하려면 호스트가 벽시계에 맞춰 `tickEventLoop()`을 구동해야 합니다.

## 경쟁 상태: 먼저 취소된 작업이 우선

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

## 계층 간 전파

신호를 인식하는 루프로 자체 비동기 작업을 감싸세요:

```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는 신호를 직접 상속합니다. 수동 `if (signal.aborted)`은 요청 사이의 공백을 처리해 페이지 루프가 계속 실행되는 일을 막습니다.

## 중단 이벤트 수신

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

리스너가 실행되기 전에 `signal.reason`이 설정됩니다.

## 사용자 지정 fetch 전송 계층의 신호

`setFetchTransport()`을 통해 전송 계층을 바꾸면 신호는 호출 가능한 함수에 `JsObject`로 전달됩니다. 다음과 같이 폴링해 신호를 처리하세요:

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

`kind: 'aborted'`와 함께 `TransportException`을 던지면 JS 측의 `fetch`이 `AbortError DOMException`로 거부됩니다. 사양의 중단 루프가 실행된 경우와 같습니다.

네이티브 취소를 지원하는 백엔드(Guzzle의 `on_progress`, Symfony의 `ResponseInterface::cancel`)에는 동일한 폴링 후 예외 발생 방식을 연결하세요.

## 아직 작동하지 않는 패턴

- **`signal.addEventListener('abort', () => fetch(cleanupUrl))`** — 리스너는 동기적으로 실행됩니다. 정리용 fetch는 시작되지만 중단 이벤트가 전달되는 동안 완료되지 않습니다. 주변의 `await`을 통해 예약하세요.

`setTimeout(() => ctrl.abort(), N)`은 작동합니다(타이머 계열이 제공되며 WPT 테스트를 통과합니다). 하지만 기한이 지나면 중단 신호를 보내려는 경우에는 `AbortSignal.timeout(N)`이 더 짧고 의도가 잘 드러납니다.

## 관련 항목

- [Fetch 전송 계층](https://phasis.inline0.dev/ko/docs/interop/fetch-transport) — 사용자 지정 전송 계층을 위한 전체 요청/응답/신호 인터페이스.
- [웹 API(WPT)](https://phasis.inline0.dev/ko/docs/compatibility/web-apis) — AbortController/AbortSignal의 WPT 통과율.
