Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/cri/deno-cri/chrome.js
3589 views
1
/*
2
* chrome.js
3
*
4
* Copyright (c) 2021 Andrea Cardaci <[email protected]>
5
*
6
* Deno port Copyright (C) 2022 Posit Software, PBC
7
*/
8
9
import EventEmitter from "events/mod.ts";
10
11
import { nextTick } from "../../deno/next-tick.ts";
12
13
import * as api from "./api.js";
14
import * as defaults from "./defaults.js";
15
import * as devtools from "./devtools.js";
16
17
/* const api = require('./api.js');
18
const defaults = require('./defaults.js');
19
const devtools = require('./devtools.js');
20
*/
21
22
class ProtocolError extends Error {
23
constructor(request, response) {
24
let { message } = response;
25
if (response.data) {
26
message += ` (${response.data})`;
27
}
28
super(message);
29
// attach the original response as well
30
this.request = request;
31
this.response = response;
32
}
33
}
34
35
async function tryUntilTimeout(cb, timeout = 3000) {
36
const interval = 50;
37
let soFar = 0;
38
let lastE;
39
40
do {
41
try {
42
const result = await cb();
43
return result;
44
} catch (e) {
45
lastE = e;
46
soFar += interval;
47
await new Promise((resolve) => setTimeout(resolve, interval));
48
}
49
} while (soFar < timeout);
50
throw lastE;
51
}
52
53
export default class Chrome extends EventEmitter {
54
constructor(options, notifier) {
55
super();
56
// options
57
const defaultTarget = async (targets) => {
58
let target;
59
// If no targets available in browser, create a new one
60
// Related to https://github.com/quarto-dev/quarto-cli/issues/4653
61
if (!targets.length) {
62
target = await devtools.New(options);
63
if (!target.id) {
64
throw new Error("No inspectable targets and unable to create one");
65
}
66
} else {
67
// prefer type = 'page' inspectable targets as they represents
68
// browser tabs (fall back to the first inspectable target
69
// otherwise)
70
let backup;
71
target = targets.find((target) => {
72
if (target.webSocketDebuggerUrl) {
73
backup = backup || target;
74
return target.type === "page";
75
} else {
76
return false;
77
}
78
});
79
target = target || backup;
80
}
81
if (target) {
82
return target;
83
} else {
84
throw new Error("No inspectable targets");
85
}
86
};
87
options = options || {};
88
this.host = options.host || defaults.HOST;
89
this.port = options.port || defaults.PORT;
90
this.secure = !!options.secure;
91
this.useHostName = !!options.useHostName;
92
this.alterPath = options.alterPath || ((path) => path);
93
this.protocol = options.protocol;
94
this.local = !!options.local;
95
this.target = options.target || defaultTarget;
96
// locals
97
this._notifier = notifier;
98
this._callbacks = {};
99
this._nextCommandId = 1;
100
// properties
101
this.webSocketUrl = undefined;
102
// operations
103
this._start();
104
}
105
106
// avoid misinterpreting protocol's members as custom util.inspect functions
107
inspect(_depth, options) {
108
options.customInspect = false;
109
return Deno.inspect(this, options);
110
}
111
112
send(method, params, sessionId, callback) {
113
// handle optional arguments
114
const optionals = Array.from(arguments).slice(1);
115
params = optionals.find((x) => typeof x === "object");
116
sessionId = optionals.find((x) => typeof x === "string");
117
callback = optionals.find((x) => typeof x === "function");
118
// return a promise when a callback is not provided
119
if (typeof callback === "function") {
120
this._enqueueCommand(method, params, sessionId, callback);
121
return undefined;
122
} else {
123
return new Promise((fulfill, reject) => {
124
this._enqueueCommand(method, params, sessionId, (error, response) => {
125
if (error) {
126
const request = { method, params, sessionId };
127
reject(
128
error instanceof Error
129
? error // low-level WebSocket error
130
: new ProtocolError(request, response)
131
);
132
} else {
133
fulfill(response);
134
}
135
});
136
});
137
}
138
}
139
140
close(callback) {
141
const closeWebSocket = (callback) => {
142
// don't close if it's already closed
143
if (this._ws.readyState === 3) {
144
callback();
145
} else {
146
// don't notify on user-initiated shutdown ('disconnect' event)
147
//this._ws.removeAllListeners('close');
148
const onclose = this._ws.onclose;
149
this._ws.onclose = () => {
150
// this._ws.removeAllListeners();
151
callback();
152
this._ws.onclose = onclose;
153
this._ws.onclose && this._ws.onclose();
154
};
155
this._ws.close();
156
}
157
};
158
if (typeof callback === "function") {
159
closeWebSocket(callback);
160
return undefined;
161
} else {
162
return new Promise((fulfill, _reject) => {
163
closeWebSocket(fulfill);
164
});
165
}
166
}
167
168
// initiate the connection process
169
async _start() {
170
const options = {
171
host: this.host,
172
port: this.port,
173
secure: this.secure,
174
useHostName: this.useHostName,
175
alterPath: this.alterPath,
176
};
177
try {
178
// fetch the WebSocket debugger URL
179
const url = await this._fetchDebuggerURL(options);
180
// allow the user to alter the URL
181
const urlObject = new URL(url);
182
urlObject.pathname = options.alterPath(urlObject.pathname);
183
this.webSocketUrl = urlObject.href;
184
// update the connection parameters using the debugging URL
185
options.host = urlObject.hostname;
186
options.port = urlObject.port || options.port;
187
// fetch the protocol and prepare the API
188
const protocol = await this._fetchProtocol(options);
189
api.prepare(this, protocol);
190
// finally connect to the WebSocket
191
await this._connectToWebSocket();
192
// since the handler is executed synchronously, the emit() must be
193
// performed in the next tick so that uncaught errors in the client code
194
// are not intercepted by the Promise mechanism and therefore reported
195
// via the 'error' event
196
nextTick(() => {
197
this._notifier.emit("connect", this);
198
});
199
} catch (err) {
200
this._notifier.emit("error", err);
201
}
202
}
203
204
// fetch the WebSocket URL according to 'target'
205
async _fetchDebuggerURL(options) {
206
const userTarget = this.target;
207
switch (typeof userTarget) {
208
case "string": {
209
let idOrUrl = userTarget;
210
// use default host and port if omitted (and a relative URL is specified)
211
if (idOrUrl.startsWith("/")) {
212
idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
213
}
214
// a WebSocket URL is specified by the user (e.g., node-inspector)
215
if (idOrUrl.match(/^wss?:/i)) {
216
return idOrUrl; // done!
217
}
218
// a target id is specified by the user
219
else {
220
const targets = await tryUntilTimeout(async () => {
221
return await devtools.List(options);
222
});
223
const object = targets.find((target) => target.id === idOrUrl);
224
return object.webSocketDebuggerUrl;
225
}
226
}
227
case "object": {
228
const object = userTarget;
229
return object.webSocketDebuggerUrl;
230
}
231
case "function": {
232
const func = userTarget;
233
const targets = await devtools.List(options);
234
const result = await func(targets);
235
const object = typeof result === "number" ? targets[result] : result;
236
return object.webSocketDebuggerUrl;
237
}
238
default:
239
throw new Error(`Invalid target argument "${this.target}"`);
240
}
241
}
242
243
// fetch the protocol according to 'protocol' and 'local'
244
async _fetchProtocol(options) {
245
// if a protocol has been provided then use it
246
if (this.protocol) {
247
return this.protocol;
248
}
249
// otherwise user either the local or the remote version
250
else {
251
options.local = this.local;
252
return await devtools.Protocol(options);
253
}
254
}
255
256
// establish the WebSocket connection and start processing user commands
257
_connectToWebSocket() {
258
return new Promise((fulfill, reject) => {
259
// create the WebSocket
260
try {
261
if (this.secure) {
262
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, "wss:");
263
}
264
this._ws = new WebSocket(this.webSocketUrl);
265
} catch (err) {
266
// handles bad URLs
267
reject(err);
268
return;
269
}
270
// set up event handlers
271
this._ws.onopen = () => {
272
fulfill();
273
};
274
this._ws.onmessage = (data) => {
275
const message = JSON.parse(data.data);
276
this._handleMessage(message);
277
};
278
this._ws.onclose = (_code) => {
279
this.emit("disconnect");
280
};
281
this._ws.onerror = (err) => {
282
reject(err);
283
};
284
});
285
}
286
287
// handle the messages read from the WebSocket
288
_handleMessage(message) {
289
// command response
290
if (message.id) {
291
const callback = this._callbacks[message.id];
292
if (!callback) {
293
return;
294
}
295
// interpret the lack of both 'error' and 'result' as success
296
// (this may happen with node-inspector)
297
if (message.error) {
298
callback(true, message.error);
299
} else {
300
callback(false, message.result || {});
301
}
302
// unregister command response callback
303
delete this._callbacks[message.id];
304
// notify when there are no more pending commands
305
if (Object.keys(this._callbacks).length === 0) {
306
this.emit("ready");
307
}
308
}
309
// event
310
else if (message.method) {
311
const { method, params, sessionId } = message;
312
this.emit("event", message);
313
this.emit(method, params, sessionId);
314
this.emit(`${method}.${sessionId}`, params, sessionId);
315
}
316
}
317
318
// send a command to the remote endpoint and register a callback for the reply
319
_enqueueCommand(method, params, sessionId, callback) {
320
const id = this._nextCommandId++;
321
const message = {
322
id,
323
method,
324
sessionId,
325
params: params || {},
326
};
327
this._ws.send(JSON.stringify(message));
328
this._callbacks[id] = callback;
329
}
330
}
331
332