simplify code. remove methods

This commit is contained in:
Tommy D. Rossi
2025-11-14 16:20:15 +01:00
parent 362a8a55f0
commit e362d44a7c
3 changed files with 231 additions and 253 deletions
+21 -26
View File
@@ -25,12 +25,12 @@ class SimplifiedExtension {
constructor() { constructor() {
debugLog(`Using relay URL: ${RELAY_URL}`); debugLog(`Using relay URL: ${RELAY_URL}`);
chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this)); chrome.tabs.onRemoved.addListener(this._onTabRemoved);
chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this)); chrome.tabs.onActivated.addListener(this._onTabActivated);
chrome.action.onClicked.addListener(this._onActionClicked.bind(this)); chrome.action.onClicked.addListener(this._onActionClicked);
} }
private async _onActionClicked(tab: chrome.tabs.Tab): Promise<void> { private _onActionClicked = async (tab: chrome.tabs.Tab): Promise<void> => {
if (!tab.id) { if (!tab.id) {
debugLog('No tab ID available'); debugLog('No tab ID available');
return; return;
@@ -41,23 +41,7 @@ class SimplifiedExtension {
} else { } else {
await this._connectTab(tab.id); await this._connectTab(tab.id);
} }
} };
private async _waitForServer(): Promise<void> {
const httpUrl = 'http://localhost:9988';
while (true) {
try {
debugLog('Checking if relay server is available...');
await fetch(httpUrl, { method: 'HEAD' });
debugLog('Server is available');
return;
} catch (error: any) {
debugLog(`Server not available, retrying in 1 second...`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
private async _connectTab(tabId: number): Promise<void> { private async _connectTab(tabId: number): Promise<void> {
try { try {
@@ -67,7 +51,18 @@ class SimplifiedExtension {
if (!this._connection) { if (!this._connection) {
debugLog('No existing connection, creating new relay connection'); debugLog('No existing connection, creating new relay connection');
debugLog('Waiting for server at http://localhost:9988...'); debugLog('Waiting for server at http://localhost:9988...');
await this._waitForServer();
// Wait for server to be available
while (true) {
try {
await fetch('http://localhost:9988', { method: 'HEAD' });
debugLog('Server is available');
break;
} catch (error: any) {
debugLog('Server not available, retrying in 1 second...');
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL); debugLog('Server is ready, creating WebSocket connection to:', RELAY_URL);
const socket = new WebSocket(RELAY_URL); const socket = new WebSocket(RELAY_URL);
@@ -239,19 +234,19 @@ class SimplifiedExtension {
} }
} }
private async _onTabRemoved(tabId: number): Promise<void> { private _onTabRemoved = async (tabId: number): Promise<void> => {
debugLog('Tab removed event for tab:', tabId, 'is connected:', this._connectedTabs.has(tabId)); debugLog('Tab removed event for tab:', tabId, 'is connected:', this._connectedTabs.has(tabId));
if (!this._connectedTabs.has(tabId)) return; if (!this._connectedTabs.has(tabId)) return;
debugLog(`Connected tab ${tabId} was closed, disconnecting`); debugLog(`Connected tab ${tabId} was closed, disconnecting`);
await this._disconnectTab(tabId); await this._disconnectTab(tabId);
} };
private async _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo): Promise<void> { private _onTabActivated = async (activeInfo: chrome.tabs.TabActiveInfo): Promise<void> => {
const isConnected = this._connectedTabs.has(activeInfo.tabId); const isConnected = this._connectedTabs.has(activeInfo.tabId);
debugLog('Tab activated:', activeInfo.tabId, 'is connected:', isConnected); debugLog('Tab activated:', activeInfo.tabId, 'is connected:', isConnected);
await this._updateIcon(activeInfo.tabId, isConnected ? 'connected' : 'disconnected'); await this._updateIcon(activeInfo.tabId, isConnected ? 'connected' : 'disconnected');
} };
} }
new SimplifiedExtension(); new SimplifiedExtension();
+39 -53
View File
@@ -41,16 +41,42 @@ export class RelayConnection {
private _attachedTabs: Map<number, AttachedTab> = new Map(); private _attachedTabs: Map<number, AttachedTab> = new Map();
private _nextSessionId: number = 1; private _nextSessionId: number = 1;
private _ws: WebSocket; private _ws: WebSocket;
private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void;
private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void;
private _closed = false; private _closed = false;
onclose?: () => void; onclose?: () => void;
constructor(ws: WebSocket) { constructor(ws: WebSocket) {
this._ws = ws; this._ws = ws;
this._ws.onmessage = this._onMessage.bind(this); this._ws.onmessage = async (event: MessageEvent) => {
this._ws.onclose = (event) => { let message: ExtensionCommandMessage;
try {
message = JSON.parse(event.data);
} catch (error: any) {
debugLog('Error parsing message:', error);
this._sendMessage({
error: {
code: -32700,
message: `Error parsing message: ${error.message}`,
},
});
return;
}
debugLog('Received message:', message);
const response: ExtensionResponseMessage = {
id: message.id,
};
try {
response.result = await this._handleCommand(message);
} catch (error: any) {
debugLog('Error handling command:', error);
response.error = error.message;
}
debugLog('Sending response:', response);
this._sendMessage(response);
};
this._ws.onclose = (event: CloseEvent) => {
debugLog('WebSocket onclose event:', { debugLog('WebSocket onclose event:', {
code: event.code, code: event.code,
reason: event.reason, reason: event.reason,
@@ -58,13 +84,11 @@ export class RelayConnection {
}); });
this._onClose(); this._onClose();
}; };
this._ws.onerror = (event) => { this._ws.onerror = (event: Event) => {
debugLog('WebSocket onerror event:', event); debugLog('WebSocket onerror event:', event);
}; };
this._eventListener = this._onDebuggerEvent.bind(this); chrome.debugger.onEvent.addListener(this._onDebuggerEvent);
this._detachListener = this._onDebuggerDetach.bind(this); chrome.debugger.onDetach.addListener(this._onDebuggerDetach);
chrome.debugger.onEvent.addListener(this._eventListener);
chrome.debugger.onDetach.addListener(this._detachListener);
debugLog('RelayConnection created, WebSocket readyState:', this._ws.readyState); debugLog('RelayConnection created, WebSocket readyState:', this._ws.readyState);
} }
@@ -167,8 +191,8 @@ export class RelayConnection {
debugLog('Connection closing, attached tabs count:', this._attachedTabs.size); debugLog('Connection closing, attached tabs count:', this._attachedTabs.size);
this._closed = true; this._closed = true;
chrome.debugger.onEvent.removeListener(this._eventListener); chrome.debugger.onEvent.removeListener(this._onDebuggerEvent);
chrome.debugger.onDetach.removeListener(this._detachListener); chrome.debugger.onDetach.removeListener(this._onDebuggerDetach);
const tabIds = Array.from(this._attachedTabs.keys()); const tabIds = Array.from(this._attachedTabs.keys());
debugLog('Detaching all tabs:', tabIds); debugLog('Detaching all tabs:', tabIds);
@@ -191,7 +215,7 @@ export class RelayConnection {
this.onclose?.(); this.onclose?.();
} }
private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void { private _onDebuggerEvent = (source: chrome.debugger.DebuggerSession, method: string, params: any): void => {
const tab = this._attachedTabs.get(source.tabId!); const tab = this._attachedTabs.get(source.tabId!);
if (!tab) return; if (!tab) return;
@@ -221,9 +245,9 @@ export class RelayConnection {
params, params,
}, },
}); });
} };
private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void { private _onDebuggerDetach = (source: chrome.debugger.Debuggee, reason: string): void => {
const tabId = source.tabId; const tabId = source.tabId;
debugLog('_onDebuggerDetach called for tab:', tabId, 'reason:', reason, 'isAttached:', tabId ? this._attachedTabs.has(tabId) : false); debugLog('_onDebuggerDetach called for tab:', tabId, 'reason:', reason, 'isAttached:', tabId ? this._attachedTabs.has(tabId) : false);
@@ -234,36 +258,7 @@ export class RelayConnection {
debugLog(`Debugger detached from tab ${tabId}: ${reason}`); debugLog(`Debugger detached from tab ${tabId}: ${reason}`);
this.detachTab(tabId); this.detachTab(tabId);
} };
private _onMessage(event: MessageEvent): void {
this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e));
}
private async _onMessageAsync(event: MessageEvent): Promise<void> {
let message: ExtensionCommandMessage;
try {
message = JSON.parse(event.data);
} catch (error: any) {
debugLog('Error parsing message:', error);
this._sendError(-32700, `Error parsing message: ${error.message}`);
return;
}
debugLog('Received message:', message);
const response: ExtensionResponseMessage = {
id: message.id,
};
try {
response.result = await this._handleCommand(message);
} catch (error: any) {
debugLog('Error handling command:', error);
response.error = error.message;
}
debugLog('Sending response:', response);
this._sendMessage(response);
}
private async _handleCommand(message: ExtensionCommandMessage): Promise<any> { private async _handleCommand(message: ExtensionCommandMessage): Promise<any> {
if (message.method === 'attachToTab') { if (message.method === 'attachToTab') {
@@ -339,15 +334,6 @@ export class RelayConnection {
} }
} }
private _sendError(code: number, message: string): void {
this._sendMessage({
error: {
code,
message,
},
});
}
private _sendMessage(message: any): void { private _sendMessage(message: any): void {
if (this._ws.readyState === WebSocket.OPEN) { if (this._ws.readyState === WebSocket.OPEN) {
try { try {
+171 -174
View File
@@ -66,7 +66,31 @@ export class CDPRelayServer {
this._resetExtensionConnection(); this._resetExtensionConnection();
this._wss = new wsServer({ server }); this._wss = new wsServer({ server });
this._wss.on('connection', this._onConnection.bind(this)); this._wss.on('connection', (ws: WebSocket, request: http.IncomingMessage) => {
const url = new URL(`http://localhost${request.url}`);
debugLogger(`New connection to ${url.pathname}`);
if (url.pathname === this._cdpPath) {
this._handlePlaywrightConnection(ws);
} else if (url.pathname === this._extensionPath) {
if (this._extensionConnection) {
ws.close(1000, 'Another extension connection already established');
return;
}
this._extensionConnection = new ExtensionConnection(ws);
this._extensionConnection.onclose = (c, reason) => {
debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection);
if (this._extensionConnection !== c)
return;
this._resetExtensionConnection();
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
};
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
this._extensionConnectionPromise.resolve();
} else {
debugLogger(`Invalid path: ${url.pathname}`);
ws.close(4004, 'Invalid path');
}
});
} }
cdpEndpoint() { cdpEndpoint() {
@@ -88,18 +112,7 @@ export class CDPRelayServer {
this._closeExtensionConnection(reason); this._closeExtensionConnection(reason);
} }
private _onConnection(ws: WebSocket, request: http.IncomingMessage): void {
const url = new URL(`http://localhost${request.url}`);
debugLogger(`New connection to ${url.pathname}`);
if (url.pathname === this._cdpPath) {
this._handlePlaywrightConnection(ws);
} else if (url.pathname === this._extensionPath) {
this._handleExtensionConnection(ws);
} else {
debugLogger(`Invalid path: ${url.pathname}`);
ws.close(4004, 'Invalid path');
}
}
private _handlePlaywrightConnection(ws: WebSocket): void { private _handlePlaywrightConnection(ws: WebSocket): void {
if (this._playwrightConnection) { if (this._playwrightConnection) {
@@ -148,27 +161,12 @@ export class CDPRelayServer {
this._playwrightConnection = null; this._playwrightConnection = null;
} }
private _handleExtensionConnection(ws: WebSocket): void {
if (this._extensionConnection) {
ws.close(1000, 'Another extension connection already established');
return;
}
this._extensionConnection = new ExtensionConnection(ws);
this._extensionConnection.onclose = (c, reason) => {
debugLogger('Extension WebSocket closed:', reason, c === this._extensionConnection);
if (this._extensionConnection !== c)
return;
this._resetExtensionConnection();
this._closePlaywrightConnection(`Extension disconnected: ${reason}`);
};
this._extensionConnection.onmessage = this._handleExtensionMessage.bind(this);
this._extensionConnectionPromise.resolve();
}
private _handleExtensionMessage(message: ExtensionEventMessage) { private _handleExtensionMessage(message: ExtensionEventMessage) {
if (message.method === 'forwardCDPEvent') { if (message.method === 'forwardCDPEvent') {
const { method, params, sessionId } = message.params; const { method, params, sessionId } = message.params;
if (method === 'Target.attachedToTarget') { if (method === 'Target.attachedToTarget') {
const targetParams = params as Protocol.Target.AttachedToTargetEvent; const targetParams = params as Protocol.Target.AttachedToTargetEvent;
this._connectedTargets.set(targetParams.sessionId, { this._connectedTargets.set(targetParams.sessionId, {
@@ -176,25 +174,25 @@ export class CDPRelayServer {
targetId: targetParams.targetInfo.targetId, targetId: targetParams.targetInfo.targetId,
targetInfo: targetParams.targetInfo targetInfo: targetParams.targetInfo
}); });
debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.attachedToTarget sessionId=${targetParams.sessionId}, targetId=${targetParams.targetInfo.targetId}`); debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.attachedToTarget sessionId=${targetParams.sessionId}, targetId=${targetParams.targetInfo.targetId}`);
this._sendToPlaywright({ this._sendToPlaywright({
method: 'Target.attachedToTarget', method: 'Target.attachedToTarget',
params: targetParams params: targetParams
} as CDPEvent); } as CDPEvent);
} else if (method === 'Target.detachedFromTarget') { } else if (method === 'Target.detachedFromTarget') {
const detachParams = params as Protocol.Target.DetachedFromTargetEvent; const detachParams = params as Protocol.Target.DetachedFromTargetEvent;
this._connectedTargets.delete(detachParams.sessionId); this._connectedTargets.delete(detachParams.sessionId);
debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.detachedFromTarget sessionId=${detachParams.sessionId}`); debugLogger('\x1b[33m← Extension:\x1b[0m', `Target.detachedFromTarget sessionId=${detachParams.sessionId}`);
this._sendToPlaywright({ this._sendToPlaywright({
method: 'Target.detachedFromTarget', method: 'Target.detachedFromTarget',
params: detachParams params: detachParams
} as CDPEvent); } as CDPEvent);
} else { } else {
this._sendToPlaywright({ this._sendToPlaywright({
sessionId, sessionId,
@@ -208,8 +206,94 @@ export class CDPRelayServer {
private async _handlePlaywrightMessage(message: CDPCommand): Promise<void> { private async _handlePlaywrightMessage(message: CDPCommand): Promise<void> {
debugLogger('\x1b[36m← Playwright:\x1b[0m', `${message.method} (id=${message.id})`); debugLogger('\x1b[36m← Playwright:\x1b[0m', `${message.method} (id=${message.id})`);
const { id, sessionId, method, params } = message; const { id, sessionId, method, params } = message;
const forwardToExtension = async (method: string, params: any, sessionId: string | undefined): Promise<any> => {
if (!this._extensionConnection)
throw new Error('Extension not connected');
return await this._extensionConnection.send({
method: 'forwardCDPCommand',
params: { sessionId, method, params }
});
};
const handleCDPCommand = async (method: string, params: any, sessionId: string | undefined): Promise<any> => {
switch (method) {
case 'Browser.getVersion': {
return {
protocolVersion: '1.3',
product: 'Chrome/Extension-Bridge',
revision: '1.0.0',
userAgent: 'CDP-Bridge-Server/1.0.0',
jsVersion: 'V8',
} satisfies Protocol.Browser.GetVersionResponse;
}
case 'Browser.setDownloadBehavior': {
return { };
}
case 'Target.setAutoAttach': {
if (sessionId) {
break;
}
debugLogger('Target.setAutoAttach received (manual attach mode)');
debugLogger('Sending Target.attachedToTarget events for existing targets:', this._connectedTargets.size);
for (const target of this._connectedTargets.values()) {
debugLogger('Sending Target.attachedToTarget for sessionId:', target.sessionId, 'targetId:', target.targetId);
this._sendToPlaywright({
method: 'Target.attachedToTarget',
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
} satisfies CDPEvent);
}
return {};
}
case 'Target.getTargetInfo': {
const targetId = params?.targetId;
if (targetId) {
for (const target of this._connectedTargets.values()) {
if (target.targetId === targetId) {
return { targetInfo: target.targetInfo };
}
}
}
if (sessionId) {
const target = this._connectedTargets.get(sessionId);
if (target) {
return { targetInfo: target.targetInfo };
}
}
const firstTarget = this._connectedTargets.values().next().value;
return { targetInfo: firstTarget?.targetInfo };
}
case 'Target.getTargets': {
return {
targetInfos: Array.from(this._connectedTargets.values()).map(t => ({
...t.targetInfo,
attached: true,
}))
};
}
case 'Target.closeTarget': {
break;
}
}
return await forwardToExtension(method, params, sessionId);
};
try { try {
const result = await this._handleCDPCommand(method, params, sessionId); const result = await handleCDPCommand(method, params, sessionId);
this._sendToPlaywright({ id, sessionId, result }); this._sendToPlaywright({ id, sessionId, result });
} catch (e) { } catch (e) {
debugLogger('\x1b[31mError in the extension:\x1b[0m', e); debugLogger('\x1b[31mError in the extension:\x1b[0m', e);
@@ -221,94 +305,9 @@ export class CDPRelayServer {
} }
} }
private async _handleCDPCommand(method: string, params: any, sessionId: string | undefined): Promise<any> {
switch (method) {
case 'Browser.getVersion': {
return {
protocolVersion: '1.3',
product: 'Chrome/Extension-Bridge',
revision: '1.0.0',
userAgent: 'CDP-Bridge-Server/1.0.0',
jsVersion: 'V8',
} satisfies Protocol.Browser.GetVersionResponse;
}
case 'Browser.setDownloadBehavior': {
return { };
}
case 'Target.setAutoAttach': {
if (sessionId) {
break;
}
debugLogger('Target.setAutoAttach received (manual attach mode)');
debugLogger('Sending Target.attachedToTarget events for existing targets:', this._connectedTargets.size);
for (const target of this._connectedTargets.values()) {
debugLogger('Sending Target.attachedToTarget for sessionId:', target.sessionId, 'targetId:', target.targetId);
this._sendToPlaywright({
method: 'Target.attachedToTarget',
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
} as CDPEvent);
}
return {};
}
case 'Target.getTargetInfo': {
const targetId = params?.targetId;
if (targetId) {
for (const target of this._connectedTargets.values()) {
if (target.targetId === targetId) {
return { targetInfo: target.targetInfo };
}
}
}
if (sessionId) {
const target = this._connectedTargets.get(sessionId);
if (target) {
return { targetInfo: target.targetInfo };
}
}
const firstTarget = this._connectedTargets.values().next().value;
return { targetInfo: firstTarget?.targetInfo };
}
case 'Target.getTargets': {
return {
targetInfos: Array.from(this._connectedTargets.values()).map(t => ({
...t.targetInfo,
attached: true,
}))
};
}
case 'Target.closeTarget': {
break;
}
}
return await this._forwardToExtension(method, params, sessionId);
}
private async _forwardToExtension(method: string, params: any, sessionId: string | undefined): Promise<any> {
if (!this._extensionConnection)
throw new Error('Extension not connected');
return await this._extensionConnection.send({
method: 'forwardCDPCommand',
params: { sessionId, method, params }
});
}
private _sendToPlaywright(message: CDPResponse | CDPEvent): void { private _sendToPlaywright(message: CDPResponse | CDPEvent): void {
const logMessage = 'method' in message && message.method const logMessage = 'method' in message && message.method
? message.method ? message.method
: `response(id=${'id' in message ? message.id : 'unknown'})`; : `response(id=${'id' in message ? message.id : 'unknown'})`;
debugLogger('\x1b[32m→ Playwright:\x1b[0m', logMessage); debugLogger('\x1b[32m→ Playwright:\x1b[0m', logMessage);
this._playwrightConnection?.send(JSON.stringify(message)); this._playwrightConnection?.send(JSON.stringify(message));
@@ -325,9 +324,54 @@ class ExtensionConnection {
constructor(ws: WebSocket) { constructor(ws: WebSocket) {
this._ws = ws; this._ws = ws;
this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this)); this._ws.on('message', (event: websocket.RawData) => {
this._ws.on('error', this._onError.bind(this)); const eventData = event.toString();
let parsedJson;
try {
parsedJson = JSON.parse(eventData);
} catch (e: any) {
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
this._ws.close();
return;
}
const handleParsedMessage = (object: ExtensionMessage) => {
if ('id' in object && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);
if (object.error) {
const error = callback.error;
error.message = object.error;
callback.reject(error);
} else {
callback.resolve(object.result);
}
} else if ('id' in object) {
debugLogger('← Extension: unexpected response', object);
} else {
this.onmessage?.(object as ExtensionEventMessage);
}
};
try {
handleParsedMessage(parsedJson);
} catch (e: any) {
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
this._ws.close();
}
});
this._ws.on('close', (event: websocket.CloseEvent) => {
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
this._dispose();
this.onclose?.(this, event.reason);
});
this._ws.on('error', (event: websocket.ErrorEvent) => {
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
this._dispose();
});
} }
async send(command: Omit<ExtensionCommandMessage, 'id'>): Promise<any> { async send(command: Omit<ExtensionCommandMessage, 'id'>): Promise<any> {
@@ -347,53 +391,6 @@ class ExtensionConnection {
this._ws.close(1000, message); this._ws.close(1000, message);
} }
private _onMessage(event: websocket.RawData) {
const eventData = event.toString();
let parsedJson;
try {
parsedJson = JSON.parse(eventData);
} catch (e: any) {
debugLogger(`<closing ws> Closing websocket due to malformed JSON. eventData=${eventData} e=${e?.message}`);
this._ws.close();
return;
}
try {
this._handleParsedMessage(parsedJson);
} catch (e: any) {
debugLogger(`<closing ws> Closing websocket due to failed onmessage callback. eventData=${eventData} e=${e?.message}`);
this._ws.close();
}
}
private _handleParsedMessage(object: ExtensionMessage) {
if ('id' in object && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);
if (object.error) {
const error = callback.error;
error.message = object.error;
callback.reject(error);
} else {
callback.resolve(object.result);
}
} else if ('id' in object) {
debugLogger('← Extension: unexpected response', object);
} else {
this.onmessage?.(object as ExtensionEventMessage);
}
}
private _onClose(event: websocket.CloseEvent) {
debugLogger(`<ws closed> code=${event.code} reason=${event.reason}`);
this._dispose();
this.onclose?.(this, event.reason);
}
private _onError(event: websocket.ErrorEvent) {
debugLogger(`<ws error> message=${event.message} type=${event.type} target=${event.target}`);
this._dispose();
}
private _dispose() { private _dispose() {
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(new Error('WebSocket closed')); callback.reject(new Error('WebSocket closed'));