71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
// main/ai.js cancelAll —— 顺序竞争 epoch 闸(audit P1-5)
|
||
//
|
||
// 背景:cancelAll 内部三步
|
||
// 1) for (controller of pending.values()) controller.abort();
|
||
// 2) pending.clear();
|
||
// 3) inFlightRequestIds.clear();
|
||
// 三步非原子。竞态:cancelAll 走到 step 1 刚 abort 完,runEdit 同步阶段
|
||
// 已经把「新 requestId + 新 controller」加进 pending + inFlightRequestIds →
|
||
// step 2/3 把这条新 entry 也清掉,runEdit 自以为还在跑,await 不到结果,
|
||
// 任何同 requestId 重试都找不到 controller。
|
||
//
|
||
// 修复:makeCleanupEpoch() 工厂 → { current, snapshot, invalidate, isCurrent }。
|
||
// - cancelAll 入口 invalidate() → 序号 + 1(任何已 snapshot 的 in-flight 任务
|
||
// 之后 isCurrent() === false → 自行 bail)
|
||
// - runEdit 入口 snapshot() 拿当前序号;await 后 isCurrent() === false
|
||
// (说明本任务期间发生过 cancelAll)→ 不再 apply 任何副作用、直接 return
|
||
//
|
||
// 序号从 1 开始自增(0 留给「初始」/「无 epoch」语义)—— isCurrent 比较时
|
||
// 0 vs 0 也算成立(测试覆盖),但 cancelAll invalidate 之后序号一定 > 0,
|
||
// 真实 runEdit 拿的 snapshot 至少 1。
|
||
|
||
'use strict';
|
||
|
||
/**
|
||
* 创建一个 epoch 计数器对象。cancelAll.invalidate / runEdit.snapshot / runEdit.isCurrent
|
||
* 配合使用,挡住「cancelAll 中途新请求进入」的竞态。
|
||
* @returns {{
|
||
* current: () => number,
|
||
* snapshot: () => number,
|
||
* invalidate: () => void,
|
||
* isCurrent: (snap: number) => boolean,
|
||
* }}
|
||
*/
|
||
function makeCleanupEpoch() {
|
||
let epoch = 0;
|
||
return {
|
||
/**
|
||
* 当前 epoch 序号。0 = 初始(无 cancelAll 发生过)。
|
||
* @returns {number}
|
||
*/
|
||
current() {
|
||
return epoch;
|
||
},
|
||
/**
|
||
* 拍下当前 epoch 的快照,用于稍后 isCurrent() 校验。
|
||
* runEdit 入口调用,await 后比对。
|
||
* @returns {number}
|
||
*/
|
||
snapshot() {
|
||
return epoch;
|
||
},
|
||
/**
|
||
* 自增 epoch,使所有「更早的 snapshot」isCurrent 失效。
|
||
* cancelAll 入口调用。
|
||
*/
|
||
invalidate() {
|
||
epoch += 1;
|
||
},
|
||
/**
|
||
* 校验「snap 是否仍是当前 epoch」。
|
||
* @param {number} snap snapshot() 返回的序号
|
||
* @returns {boolean} snap === current
|
||
*/
|
||
isCurrent(snap) {
|
||
return epoch === snap;
|
||
},
|
||
};
|
||
}
|
||
|
||
module.exports = { makeCleanupEpoch };
|