Files

34 lines
2.7 KiB
JavaScript

/**
* 隔离浏览器 fixture 专用的有界 CDP 请求路由器,不作为产品运行时代码。
* 每个正整数 ID 对应一个 Promise 和截止定时器;连接/目标级故障是整个实例的终态。
* 本函数不负责建立或关闭 socket,也不提供独立 dispose;外层 runner 持有连接。
*/
// 接收已连接 socket 和可替换定时器;默认给每个调用设置 30 秒截止定时器,事件由 onEvent 转发。
export function createCdpCalls(socket,{timeoutMs=30000,onEvent=()=>{},setTimer=setTimeout,clearTimer=clearTimeout}={}){
// pending 是未回应调用的唯一查找表;failure 保留首个致命错误,使未来调用立刻拒绝。
let serial=0,failure;const pending=new Map();
// 全局失败清除每个截止定时器并拒绝所有 Promise;重复故障不覆盖最初原因。
const fail=error=>{failure??=error;for(const row of pending.values()){clearTimer(row.timer);row.reject(failure);}pending.clear();};
// 连接关闭和传输错误都不再允许继续发命令。
socket.addEventListener('close',()=>fail(Error('CDP connection closed')));
socket.addEventListener('error',()=>fail(Error('CDP connection failed')));
// 收到消息先解析 JSON;解析或事件回调异常同样会进入全局失败。
socket.addEventListener('message',event=>{
try{
const message=JSON.parse(event.data);
// 无请求 ID 的消息是事件;先通知观察者,再把崩溃、脱离或对话框视为目标不可继续使用。
if(!message.id){onEvent(message);if(['Inspector.targetCrashed','Inspector.detached','Target.targetCrashed','Page.javascriptDialogOpening'].includes(message.method))fail(Error('CDP target unavailable: '+message.method+' '+JSON.stringify(message.params??{})));return;}
// 已超时/完成的响应无对应 row 时忽略;普通协议 error 只拒绝该请求,不使整个连接失效。
const row=pending.get(message.id);if(!row)return;pending.delete(message.id);clearTimer(row.timer);message.error?row.reject(Error(JSON.stringify(message.error))):row.resolve(message.result);
}catch(error){fail(error);}
});
// 为新命令分配递增 ID,先登记 pending 与定时器再发送;send 同步抛错也会拒绝全部在途工作。
return(method,params={})=>new Promise((resolve,reject)=>{
if(failure){reject(failure);return;}
const id=++serial,row={resolve,reject,timer:undefined};pending.set(id,row);
// 任意一个调用超时都使整个调用器失败,避免测试随后无限挂在另一个依赖命令。
row.timer=setTimer(()=>fail(Error(`CDP ${method} timed out after ${timeoutMs}ms`)),timeoutMs);
try{socket.send(JSON.stringify({id,method,params}));}catch(error){fail(error);}
});
}