/** * 隔离浏览器 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);} }); }