import EventEmitter from "events/mod.ts";
import { nextTick } from "../../deno/next-tick.ts";
import * as api from "./api.js";
import * as defaults from "./defaults.js";
import * as devtools from "./devtools.js";
class ProtocolError extends Error {
constructor(request, response) {
let { message } = response;
if (response.data) {
message += ` (${response.data})`;
}
super(message);
this.request = request;
this.response = response;
}
}
async function tryUntilTimeout(cb, timeout = 3000) {
const interval = 50;
let soFar = 0;
let lastE;
do {
try {
const result = await cb();
return result;
} catch (e) {
lastE = e;
soFar += interval;
await new Promise((resolve) => setTimeout(resolve, interval));
}
} while (soFar < timeout);
throw lastE;
}
export default class Chrome extends EventEmitter {
constructor(options, notifier) {
super();
const defaultTarget = async (targets) => {
let target;
if (!targets.length) {
target = await devtools.New(options);
if (!target.id) {
throw new Error("No inspectable targets and unable to create one");
}
} else {
let backup;
target = targets.find((target) => {
if (target.webSocketDebuggerUrl) {
backup = backup || target;
return target.type === "page";
} else {
return false;
}
});
target = target || backup;
}
if (target) {
return target;
} else {
throw new Error("No inspectable targets");
}
};
options = options || {};
this.host = options.host || defaults.HOST;
this.port = options.port || defaults.PORT;
this.secure = !!options.secure;
this.useHostName = !!options.useHostName;
this.alterPath = options.alterPath || ((path) => path);
this.protocol = options.protocol;
this.local = !!options.local;
this.target = options.target || defaultTarget;
this._notifier = notifier;
this._callbacks = {};
this._nextCommandId = 1;
this.webSocketUrl = undefined;
this._start();
}
inspect(_depth, options) {
options.customInspect = false;
return Deno.inspect(this, options);
}
send(method, params, sessionId, callback) {
const optionals = Array.from(arguments).slice(1);
params = optionals.find((x) => typeof x === "object");
sessionId = optionals.find((x) => typeof x === "string");
callback = optionals.find((x) => typeof x === "function");
if (typeof callback === "function") {
this._enqueueCommand(method, params, sessionId, callback);
return undefined;
} else {
return new Promise((fulfill, reject) => {
this._enqueueCommand(method, params, sessionId, (error, response) => {
if (error) {
const request = { method, params, sessionId };
reject(
error instanceof Error
? error
: new ProtocolError(request, response)
);
} else {
fulfill(response);
}
});
});
}
}
close(callback) {
const closeWebSocket = (callback) => {
if (this._ws.readyState === 3) {
callback();
} else {
const onclose = this._ws.onclose;
this._ws.onclose = () => {
callback();
this._ws.onclose = onclose;
this._ws.onclose && this._ws.onclose();
};
this._ws.close();
}
};
if (typeof callback === "function") {
closeWebSocket(callback);
return undefined;
} else {
return new Promise((fulfill, _reject) => {
closeWebSocket(fulfill);
});
}
}
async _start() {
const options = {
host: this.host,
port: this.port,
secure: this.secure,
useHostName: this.useHostName,
alterPath: this.alterPath,
};
try {
const url = await this._fetchDebuggerURL(options);
const urlObject = new URL(url);
urlObject.pathname = options.alterPath(urlObject.pathname);
this.webSocketUrl = urlObject.href;
options.host = urlObject.hostname;
options.port = urlObject.port || options.port;
const protocol = await this._fetchProtocol(options);
api.prepare(this, protocol);
await this._connectToWebSocket();
nextTick(() => {
this._notifier.emit("connect", this);
});
} catch (err) {
this._notifier.emit("error", err);
}
}
async _fetchDebuggerURL(options) {
const userTarget = this.target;
switch (typeof userTarget) {
case "string": {
let idOrUrl = userTarget;
if (idOrUrl.startsWith("/")) {
idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
}
if (idOrUrl.match(/^wss?:/i)) {
return idOrUrl;
}
else {
const targets = await tryUntilTimeout(async () => {
return await devtools.List(options);
});
const object = targets.find((target) => target.id === idOrUrl);
return object.webSocketDebuggerUrl;
}
}
case "object": {
const object = userTarget;
return object.webSocketDebuggerUrl;
}
case "function": {
const func = userTarget;
const targets = await devtools.List(options);
const result = await func(targets);
const object = typeof result === "number" ? targets[result] : result;
return object.webSocketDebuggerUrl;
}
default:
throw new Error(`Invalid target argument "${this.target}"`);
}
}
async _fetchProtocol(options) {
if (this.protocol) {
return this.protocol;
}
else {
options.local = this.local;
return await devtools.Protocol(options);
}
}
_connectToWebSocket() {
return new Promise((fulfill, reject) => {
try {
if (this.secure) {
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, "wss:");
}
this._ws = new WebSocket(this.webSocketUrl);
} catch (err) {
reject(err);
return;
}
this._ws.onopen = () => {
fulfill();
};
this._ws.onmessage = (data) => {
const message = JSON.parse(data.data);
this._handleMessage(message);
};
this._ws.onclose = (_code) => {
this.emit("disconnect");
};
this._ws.onerror = (err) => {
reject(err);
};
});
}
_handleMessage(message) {
if (message.id) {
const callback = this._callbacks[message.id];
if (!callback) {
return;
}
if (message.error) {
callback(true, message.error);
} else {
callback(false, message.result || {});
}
delete this._callbacks[message.id];
if (Object.keys(this._callbacks).length === 0) {
this.emit("ready");
}
}
else if (message.method) {
const { method, params, sessionId } = message;
this.emit("event", message);
this.emit(method, params, sessionId);
this.emit(`${method}.${sessionId}`, params, sessionId);
}
}
_enqueueCommand(method, params, sessionId, callback) {
const id = this._nextCommandId++;
const message = {
id,
method,
sessionId,
params: params || {},
};
this._ws.send(JSON.stringify(message));
this._callbacks[id] = callback;
}
}