
# 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/zh/docs/api#event-loop)）。默认 `CurlTransport` 的 fetch 传输会通过 `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/zh/docs/interop/fetch-transport) — 自定义传输所需的完整请求、响应和信号接口。
- [Web API (WPT)](https://phasis.inline0.dev/zh/docs/compatibility/web-apis) — AbortController/AbortSignal 的 WPT 通过率。
