Sign in

    #abort —— 取消原语(AbortController / AbortSignal)

    AbortController 与 AbortSignal:控制器用来发起取消,信号随请求走、只负责观察。

    import { "q2316367743/moonhttp/abort", }

    #语义

    • 发起方与观察方分开:AbortController 留给需要叫停的一方(UI 的停止按钮、看门狗),随请求走的是 controller.signal() 给出的 AbortSignal。
    • 一次性:取消之后永远是取消状态,不能复用;一次请求一个信号就每次 AbortController::new()。
    • 可共享:同一个信号可以挂到任意多个请求上,abort 一次全部生效。
    • 幂等:重复 abort 只生效第一次,第二次连理由都不会改。
    • 已取消的信号可以直接传:拿它发请求会立刻以取消失败收场,理由就是 abort 时给的那个。
    • 从三个入口看到的取消失败都是 HttpError:error.is_cancelled() 为真,code 是 Cancelled(ERR_CANCELED)。

    完整语义与边界见 docs/12-cancellation.md。

    #API

    方法说明
    AbortController::new()造一个尚未取消的控制器
    AbortController::signal()取出它的信号
    AbortController::abort(reason?)发起取消;同步、不抛错、幂等、一次性
    AbortSignal::abort(reason?)静态构造:出生就已取消的信号
    AbortSignal::aborted()是否已取消
    AbortSignal::reason()取消理由,String?(默认文案在错误层)
    AbortSignal::throw_if_aborted()已取消就抛 AbortError

    attach / detach 是库内部的中断登记接口,写业务代码用不到。

    #用法

    let controller = @abort.AbortController::new()

    // 信号按请求传:它是入口的 signal? 参数,不在 Config 上
    client.request(Config::new("/reports/big.csv"), signal=controller.signal())

    // 另一条协程里(UI 的停止按钮、看门狗、超时兜底):
    controller.abort(reason="用户点了停止")

    // 已经不该再发的请求:把「已取消」变成一个可以直接传的值

    ///|
    let signal = @abort.AbortSignal::abort(reason="上游已整体超时")

    AbortError

    pub(all) suberror AbortError {
    Aborted(String?)
    }

    取消失败:AbortSignal::throw_if_aborted 抛出它,载荷就是 signal.reason() (取消时没给理由就是 None)。

    对应 JS 里 throwIfAborted() 抛出的那个 AbortError(一个 DOMException)。 差别只有形态:那边是 reason: any 原样抛出,这里收纳成字符串——与本项目 「理由落成错误文案、文案只在错误层一处」的既有链路一致(src/http_error.mbt)。

    这个类型只在 abort 包与根包之间流转,不是对外的错误:使用者从库接口拿到的 仍然是 HttpError(is_cancelled() 为真、错误码 ERR_CANCELED)。

    AbortController

    pub struct AbortController {
    // private fields
    }

    取消的发起方:谁造出它,谁就有权叫停拿着它信号的那些请求。

    与 JS 一样分成两个对象,是为了把「能取消」变成一项持有能力:信号可以到处传 (进配置、进传输层、交给别的库),控制器留在需要叫停的一方手里。

    AbortController::abort

    fn AbortController::abort(self : AbortController, reason? : String) -> Unit

    发起取消,对应 JS 的 controller.abort(reason)。

    reason 会原样成为这次请求失败时的错误描述(HttpError::message()); 不传就用库的默认文案(见 docs/04-errors.md)。

    同步、不抛错、幂等:第一次之后的所有调用都是空操作。触发的登记按登记顺序 同步执行,所以调用 abort 的那条协程会等所有清理动作做完 (例如关闭响应体连接)才继续。

    let controller = AbortController::new()
    // 信号按请求传(它是入口的 signal? 参数,不属于配置):
    client.request(Config::new("/reports/big.csv"), signal=controller.signal())
    // 另一处(另一个协程、或某个 UI 回调里):
    controller.abort(reason="用户点了停止")

    AbortController::new

    新建一个尚未取消的控制器(它的信号也尚未取消)。

    AbortController::signal

    取出这个控制器的信号,对应 JS 的 controller.signal。

    给出去的信号与控制器共享状态:abort() 之后,任何拿着这个信号的一方 都会看到取消(MoonBit 里带可变字段的类型是引用语义,副本指同一份状态)。

    AbortSignal

    pub struct AbortSignal {
    // private fields
    }

    取消信号:一个可以传给任意多次请求的「取消观察口」。

    它自己不能被取消——叫停只能由持有 AbortController 的一方发起 (对应 JS 的 controller.abort())。这条分工正是这套机制比 CancelToken 好的地方:拿到信号的代码只能观察,「误调用取消」在类型上就写不出来。

    几条刻意的语义(与 JS 一致):
    • 一次性:取消之后永远是取消状态,不能复用;需要「一次请求一个信号」 就每次 AbortController::new()。
    • 可共享,但只能按请求显式共享:同一个信号可以挂在任意多次调用上, abort 一次全部生效。它不会被实例默认值继承——信号是一次性的, 放进实例默认值会让那个实例之后的所有请求一起失效,所以本项目的 三个入口只从 signal? 参数收信号(理由见 docs/12-cancellation.md)。
    • 取消晚一步也算数:取消发生之后才开始的请求会立刻失败,不会 「取消晚了一步就照常发出去」——靠的是每个可取消的 I/O 入口先查 aborted()(见 attach 的说明与 docs/12-cancellation.md 的「两条兜底」)。
    • 幂等:重复 abort 只生效第一次,第二次连理由都不会改。

    AbortSignal::abort

    fn AbortSignal::abort(reason? : String) -> AbortSignal

    造一个出生就已取消的信号,对应 JS 的 AbortSignal.abort(reason)。

    用途是把「已经不该再发请求了」变成一个可以到处传的值:上游判定整体超时之后 给后续所有请求都挂上它;测试里也省掉「先造控制器再取消」那一步。

    AbortSignal::aborted

    fn AbortSignal::aborted(self : AbortSignal) -> Bool

    是否已取消,对应 JS 的 signal.aborted。

    它问的是「取消这件事发生了没有」,不是「这次请求失败了没有」——所以要区分 「没取消」与「取消了但没给理由」时,用 aborted() 配 reason()。

    AbortSignal::attach

    fn AbortSignal::attach(self : AbortSignal, handle : () -> Unit) -> Int

    登记一个「取消时要做的事」,返回注销用的票号(detach 按票号注销)—— 对应 JS 的 signal.addEventListener("abort", handle) 与 removeEventListener。

    这是给传输层用的内部机制,不是使用者 API:一次请求在开始一段可被取消的 I/O 之前把自己的中断手段挂上来,结束时注销。使用者只该观察 (aborted() / reason()),叫停由控制器发起。

    两条与 JS 一致、且与旧 CancelToken 不同的语义:
    • 已取消的信号上登记不会触发(JS 的 addEventListener 在 aborted 之后 也不会补触发),也不会被保留——没有会触发它的未来,留着只会拖住闭包。 所以「取消晚一步」的兜底是登记点自己先查 aborted(),不是登记的副作用: 重定向两跳之间、两次响应体读取之间都靠这条(见 docs/12-cancellation.md);
    • 触发时按登记顺序同步执行(顺序确定才好推理)。

    AbortSignal::detach

    fn AbortSignal::detach(self : AbortSignal, ticket : Int) -> Unit

    注销一次登记,对应 JS 的 removeEventListener。

    票号越界一律忽略——这既覆盖「重复注销」,也覆盖取消链路里的那次自注销 (abort_with 已经把数组整组取走,旧票号必然越界), 以及「已取消信号上登记未保留」时拿到的 NO_TICKET。

    AbortSignal::reason

    fn AbortSignal::reason(self : AbortSignal) -> String?

    取消时传入的 reason;没取消、或取消时没给 reason 时是 None。

    对应 JS 的 signal.reason(那边默认是一个 AbortError 异常对象;这里简化成 字符串,默认文案由错误层统一给)。

    AbortSignal::throw_if_aborted

    fn AbortSignal::throw_if_aborted(self : AbortSignal) -> Unit raise AbortError

    已取消就抛 AbortError(载荷是取消理由),没取消就什么都不做, 对应 JS 的 signal.throwIfAborted()。

    这是「每干一段之前先查一眼」的写法,串行流程里不必给每一步都手写 if signal.aborted() { ... }。

    抛出的 AbortError 不是对外的错误类型:它只在 abort 包与根包之间流转, 根包在管线入口把它归一成 HttpError(ERR_CANCELED), 见 docs/12-cancellation.md 的「取消长什么样」。

    // 串行流程里,每段开头查一次;已经取消就抛 AbortError
    signal.throw_if_aborted()

    Source Files