opensteer 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/dist/chunk-2TIVULZY.js +4103 -0
- package/dist/chunk-2TIVULZY.js.map +1 -0
- package/dist/chunk-BMPUL66S.js +1170 -0
- package/dist/chunk-BMPUL66S.js.map +1 -0
- package/dist/chunk-FIMNKEG5.js +1800 -0
- package/dist/chunk-FIMNKEG5.js.map +1 -0
- package/dist/{chunk-656MQUSM.js → chunk-HD6KVZ42.js} +6080 -12739
- package/dist/chunk-HD6KVZ42.js.map +1 -0
- package/dist/{chunk-OIKLSFXA.js → chunk-KPYLS2KQ.js} +5 -35
- package/dist/chunk-KPYLS2KQ.js.map +1 -0
- package/dist/cli/bin.cjs +7436 -6861
- package/dist/cli/bin.cjs.map +1 -1
- package/dist/cli/bin.js +124 -7
- package/dist/cli/bin.js.map +1 -1
- package/dist/index.cjs +1048 -2584
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -757
- package/dist/index.d.ts +60 -757
- package/dist/index.js +4 -2
- package/dist/local-view/public/assets/app.css +770 -0
- package/dist/local-view/public/assets/app.js +2053 -0
- package/dist/local-view/public/index.html +235 -0
- package/dist/local-view/serve-entry.cjs +7203 -0
- package/dist/local-view/serve-entry.cjs.map +1 -0
- package/dist/local-view/serve-entry.d.cts +1 -0
- package/dist/local-view/serve-entry.d.ts +1 -0
- package/dist/local-view/serve-entry.js +23 -0
- package/dist/local-view/serve-entry.js.map +1 -0
- package/dist/opensteer-MIQ43CY4.js +6 -0
- package/dist/{opensteer-LKX3233A.js.map → opensteer-MIQ43CY4.js.map} +1 -1
- package/dist/session-control-IFE3IPS3.js +39 -0
- package/dist/session-control-IFE3IPS3.js.map +1 -0
- package/package.json +8 -8
- package/skills/README.md +3 -0
- package/skills/opensteer/SKILL.md +230 -49
- package/dist/chunk-656MQUSM.js.map +0 -1
- package/dist/chunk-OIKLSFXA.js.map +0 -1
- package/dist/opensteer-LKX3233A.js +0 -4
|
@@ -0,0 +1,1800 @@
|
|
|
1
|
+
import { writeLocalViewServiceState, CURRENT_PROCESS_OWNER, OPENSTEER_LOCAL_VIEW_SERVICE_VERSION, OPENSTEER_LOCAL_VIEW_SERVICE_LAYOUT, clearLocalViewServiceState, readLocalViewSessionManifest, listLocalViewSessionManifests, deleteLocalViewSessionManifest, isProcessRunning, pathExists, readPersistedLocalBrowserSessionRecord, inspectCdpEndpoint } from './chunk-BMPUL66S.js';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { once } from 'events';
|
|
7
|
+
import path2 from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import * as WebSocket2 from 'ws';
|
|
10
|
+
import WebSocket2__default from 'ws';
|
|
11
|
+
|
|
12
|
+
// src/local-view/resolve-browser-websocket.ts
|
|
13
|
+
async function resolveBrowserWebSocketUrl(record) {
|
|
14
|
+
if (record.engine === "playwright") {
|
|
15
|
+
if (!record.endpoint) {
|
|
16
|
+
throw new Error("Local Playwright session is missing a browser WebSocket endpoint.");
|
|
17
|
+
}
|
|
18
|
+
return record.endpoint;
|
|
19
|
+
}
|
|
20
|
+
if (!record.remoteDebuggingUrl) {
|
|
21
|
+
throw new Error("Local ABP session is missing a remote debugging URL.");
|
|
22
|
+
}
|
|
23
|
+
const inspected = await inspectCdpEndpoint({
|
|
24
|
+
endpoint: record.remoteDebuggingUrl,
|
|
25
|
+
timeoutMs: 5e3
|
|
26
|
+
});
|
|
27
|
+
return inspected.endpoint;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/local-view/discovery.ts
|
|
31
|
+
async function listResolvedLocalViewSessions() {
|
|
32
|
+
const manifests = await listLocalViewSessionManifests();
|
|
33
|
+
const resolved = await Promise.all(manifests.map((manifest) => resolveSessionSummary(manifest)));
|
|
34
|
+
return resolved.filter((session) => session !== void 0).sort(
|
|
35
|
+
(left, right) => right.startedAt - left.startedAt || left.label.localeCompare(right.label)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
async function resolveLocalViewSession(sessionId) {
|
|
39
|
+
const manifest = await readLocalViewSessionManifest(sessionId);
|
|
40
|
+
if (!manifest) {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
return readResolvedLocalViewSession(manifest);
|
|
44
|
+
}
|
|
45
|
+
async function resolveSessionSummary(manifest) {
|
|
46
|
+
const record = await readLiveRecord(manifest);
|
|
47
|
+
if (!record) {
|
|
48
|
+
await deleteLocalViewSessionManifest(manifest.sessionId);
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
const browserName = record.executablePath ? path2.basename(record.executablePath).replace(/\.[A-Za-z0-9]+$/u, "") : void 0;
|
|
52
|
+
return {
|
|
53
|
+
sessionId: manifest.sessionId,
|
|
54
|
+
label: manifest.workspace ?? (path2.basename(manifest.rootPath) || manifest.sessionId),
|
|
55
|
+
status: isProcessRunning(record.pid) ? "live" : "stale",
|
|
56
|
+
...manifest.workspace === void 0 ? {} : { workspace: manifest.workspace },
|
|
57
|
+
rootPath: manifest.rootPath,
|
|
58
|
+
engine: record.engine,
|
|
59
|
+
ownership: manifest.ownership,
|
|
60
|
+
pid: record.pid,
|
|
61
|
+
startedAt: record.startedAt,
|
|
62
|
+
...browserName === void 0 ? {} : { browserName }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function readResolvedLocalViewSession(manifest) {
|
|
66
|
+
const record = await readLiveRecord(manifest);
|
|
67
|
+
if (!record) {
|
|
68
|
+
await deleteLocalViewSessionManifest(manifest.sessionId);
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const browserWebSocketUrl = await resolveBrowserWebSocketUrl(record).catch(() => void 0);
|
|
72
|
+
if (!browserWebSocketUrl) {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
manifest,
|
|
77
|
+
record,
|
|
78
|
+
browserWebSocketUrl
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async function readLiveRecord(manifest) {
|
|
82
|
+
if (!await pathExists(manifest.rootPath)) {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
const record = await readPersistedLocalBrowserSessionRecord(manifest.rootPath);
|
|
86
|
+
if (!record) {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
if (record.pid !== manifest.pid || record.startedAt !== manifest.startedAt || !isProcessRunning(record.pid)) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/local-view/runtime-state.ts
|
|
96
|
+
var LocalViewRuntimeState = class {
|
|
97
|
+
activationIntentBySessionId = /* @__PURE__ */ new Map();
|
|
98
|
+
setPageActivationIntent(sessionId, targetId) {
|
|
99
|
+
this.activationIntentBySessionId.set(sessionId, {
|
|
100
|
+
targetId,
|
|
101
|
+
ts: Date.now()
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
getPageActivationIntent(sessionId) {
|
|
105
|
+
return this.activationIntentBySessionId.get(sessionId);
|
|
106
|
+
}
|
|
107
|
+
clearPageActivationIntent(sessionId, targetId) {
|
|
108
|
+
const current = this.activationIntentBySessionId.get(sessionId);
|
|
109
|
+
if (!current) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (targetId !== void 0 && current.targetId !== targetId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.activationIntentBySessionId.delete(sessionId);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var LocalViewWebSocketServer = WebSocket2.WebSocketServer;
|
|
119
|
+
|
|
120
|
+
// src/local-view/cdp-proxy.ts
|
|
121
|
+
var DEFAULT_MAX_PENDING_CLIENT_BUFFER_BYTES = 1e6;
|
|
122
|
+
var DEFAULT_UPSTREAM_OPEN_TIMEOUT_MS = 1e4;
|
|
123
|
+
var LocalViewCdpProxy = class {
|
|
124
|
+
constructor(deps) {
|
|
125
|
+
this.deps = deps;
|
|
126
|
+
this.wss = new LocalViewWebSocketServer({ noServer: true });
|
|
127
|
+
this.createUpstreamSocket = deps.createUpstreamSocket ?? ((url) => new WebSocket2__default(url));
|
|
128
|
+
this.maxPendingClientBufferBytes = deps.maxPendingClientBufferBytes ?? DEFAULT_MAX_PENDING_CLIENT_BUFFER_BYTES;
|
|
129
|
+
this.upstreamOpenTimeoutMs = deps.upstreamOpenTimeoutMs ?? DEFAULT_UPSTREAM_OPEN_TIMEOUT_MS;
|
|
130
|
+
}
|
|
131
|
+
wss;
|
|
132
|
+
createUpstreamSocket;
|
|
133
|
+
maxPendingClientBufferBytes;
|
|
134
|
+
upstreamOpenTimeoutMs;
|
|
135
|
+
handleUpgrade(req, socket, head) {
|
|
136
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
137
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
138
|
+
const isCdpPath = parts.length === 3 && parts[0] === "ws" && parts[1] === "cdp";
|
|
139
|
+
if (!isCdpPath) {
|
|
140
|
+
socket.destroy();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const sessionId = parts[2];
|
|
144
|
+
this.wss.handleUpgrade(req, socket, head, (clientSocket) => {
|
|
145
|
+
void this.bindProxy(clientSocket, sessionId).catch(() => {
|
|
146
|
+
safeCloseSocket(clientSocket);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
for (const client of this.wss.clients) {
|
|
152
|
+
safeCloseSocket(client);
|
|
153
|
+
}
|
|
154
|
+
this.wss.close();
|
|
155
|
+
}
|
|
156
|
+
async bindProxy(clientSocket, sessionId) {
|
|
157
|
+
const resolved = await resolveLocalViewSession(sessionId);
|
|
158
|
+
if (!resolved) {
|
|
159
|
+
safeCloseSocket(clientSocket);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const upstream = this.createUpstreamSocket(resolved.browserWebSocketUrl);
|
|
163
|
+
const pendingCreateTargetCommandIds = /* @__PURE__ */ new Set();
|
|
164
|
+
const pendingAttachTargetCommandTargetIds = /* @__PURE__ */ new Map();
|
|
165
|
+
const targetIdByAttachedSessionId = /* @__PURE__ */ new Map();
|
|
166
|
+
const pendingClientMessages = [];
|
|
167
|
+
let pendingClientBufferBytes = 0;
|
|
168
|
+
let closed = false;
|
|
169
|
+
let upstreamOpenTimeout = null;
|
|
170
|
+
const closeConnection = () => {
|
|
171
|
+
if (closed) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
closed = true;
|
|
175
|
+
if (upstreamOpenTimeout) {
|
|
176
|
+
clearTimeout(upstreamOpenTimeout);
|
|
177
|
+
upstreamOpenTimeout = null;
|
|
178
|
+
}
|
|
179
|
+
pendingClientMessages.length = 0;
|
|
180
|
+
pendingClientBufferBytes = 0;
|
|
181
|
+
safeCloseSocket(upstream);
|
|
182
|
+
safeCloseSocket(clientSocket);
|
|
183
|
+
};
|
|
184
|
+
upstreamOpenTimeout = setTimeout(() => {
|
|
185
|
+
if (upstream.readyState === WebSocket2__default.OPEN) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
closeConnection();
|
|
189
|
+
}, this.upstreamOpenTimeoutMs);
|
|
190
|
+
clientSocket.on("message", (data, isBinary) => {
|
|
191
|
+
const outboundData = data;
|
|
192
|
+
if (!isBinary) {
|
|
193
|
+
const message = parseCdpProtocolMessage(data);
|
|
194
|
+
if (message) {
|
|
195
|
+
const activatedTargetId = readActivateTargetCommandTargetId(message);
|
|
196
|
+
if (activatedTargetId) {
|
|
197
|
+
this.deps.runtimeState.setPageActivationIntent(sessionId, activatedTargetId);
|
|
198
|
+
}
|
|
199
|
+
const createTargetCommandId = readCreateTargetCommandId(message);
|
|
200
|
+
if (createTargetCommandId !== null) {
|
|
201
|
+
pendingCreateTargetCommandIds.add(createTargetCommandId);
|
|
202
|
+
}
|
|
203
|
+
const attachTargetCommand = readAttachTargetCommand(message);
|
|
204
|
+
if (attachTargetCommand) {
|
|
205
|
+
pendingAttachTargetCommandTargetIds.set(
|
|
206
|
+
attachTargetCommand.id,
|
|
207
|
+
attachTargetCommand.targetId
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const interactionTargetId = readInteractionTargetId(message, targetIdByAttachedSessionId);
|
|
211
|
+
if (interactionTargetId) {
|
|
212
|
+
this.deps.runtimeState.setPageActivationIntent(sessionId, interactionTargetId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (upstream.readyState === WebSocket2__default.OPEN) {
|
|
217
|
+
upstream.send(outboundData, { binary: isBinary });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (upstream.readyState !== WebSocket2__default.CONNECTING) {
|
|
221
|
+
closeConnection();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const sizeBytes = rawDataSizeBytes(outboundData);
|
|
225
|
+
if (pendingClientBufferBytes + sizeBytes > this.maxPendingClientBufferBytes) {
|
|
226
|
+
closeConnection();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
pendingClientMessages.push({ data: outboundData, isBinary });
|
|
230
|
+
pendingClientBufferBytes += sizeBytes;
|
|
231
|
+
});
|
|
232
|
+
upstream.on("open", () => {
|
|
233
|
+
if (upstreamOpenTimeout) {
|
|
234
|
+
clearTimeout(upstreamOpenTimeout);
|
|
235
|
+
upstreamOpenTimeout = null;
|
|
236
|
+
}
|
|
237
|
+
for (const pendingMessage of pendingClientMessages.splice(0)) {
|
|
238
|
+
upstream.send(pendingMessage.data, { binary: pendingMessage.isBinary });
|
|
239
|
+
}
|
|
240
|
+
pendingClientBufferBytes = 0;
|
|
241
|
+
});
|
|
242
|
+
upstream.on("message", (data, isBinary) => {
|
|
243
|
+
if (!isBinary) {
|
|
244
|
+
const message = parseCdpProtocolMessage(data);
|
|
245
|
+
if (message) {
|
|
246
|
+
const createdTargetId = readCreateTargetResultTargetId(
|
|
247
|
+
message,
|
|
248
|
+
pendingCreateTargetCommandIds
|
|
249
|
+
);
|
|
250
|
+
if (createdTargetId) {
|
|
251
|
+
this.deps.runtimeState.setPageActivationIntent(sessionId, createdTargetId);
|
|
252
|
+
}
|
|
253
|
+
const attachedTarget = readAttachTargetResult(
|
|
254
|
+
message,
|
|
255
|
+
pendingAttachTargetCommandTargetIds
|
|
256
|
+
);
|
|
257
|
+
if (attachedTarget) {
|
|
258
|
+
targetIdByAttachedSessionId.set(attachedTarget.sessionId, attachedTarget.targetId);
|
|
259
|
+
}
|
|
260
|
+
const detachedSessionId = readDetachedTargetSessionId(message);
|
|
261
|
+
if (detachedSessionId) {
|
|
262
|
+
targetIdByAttachedSessionId.delete(detachedSessionId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (clientSocket.readyState === WebSocket2__default.OPEN) {
|
|
267
|
+
clientSocket.send(data, { binary: isBinary });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
clientSocket.on("close", closeConnection);
|
|
271
|
+
clientSocket.on("error", closeConnection);
|
|
272
|
+
upstream.on("close", closeConnection);
|
|
273
|
+
upstream.on("error", closeConnection);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
function parseCdpProtocolMessage(data) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(rawDataToString(data));
|
|
279
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function readCreateTargetCommandId(message) {
|
|
285
|
+
return message.method === "Target.createTarget" && typeof message.id === "number" ? message.id : null;
|
|
286
|
+
}
|
|
287
|
+
function readCreateTargetResultTargetId(message, pendingCommandIds) {
|
|
288
|
+
if (typeof message.id !== "number" || !pendingCommandIds.has(message.id)) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
pendingCommandIds.delete(message.id);
|
|
292
|
+
const targetId = message.result?.targetId;
|
|
293
|
+
return typeof targetId === "string" && targetId.length > 0 ? targetId : null;
|
|
294
|
+
}
|
|
295
|
+
function readActivateTargetCommandTargetId(message) {
|
|
296
|
+
const targetId = message.method === "Target.activateTarget" ? message.params?.targetId : void 0;
|
|
297
|
+
return typeof targetId === "string" && targetId.length > 0 ? targetId : null;
|
|
298
|
+
}
|
|
299
|
+
function readAttachTargetCommand(message) {
|
|
300
|
+
if (message.method !== "Target.attachToTarget" || typeof message.id !== "number") {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const targetId = message.params?.targetId;
|
|
304
|
+
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
id: message.id,
|
|
309
|
+
targetId
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function readAttachTargetResult(message, pendingTargetIds) {
|
|
313
|
+
if (typeof message.id !== "number") {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const targetId = pendingTargetIds.get(message.id);
|
|
317
|
+
if (!targetId) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
pendingTargetIds.delete(message.id);
|
|
321
|
+
const sessionId = message.result?.sessionId;
|
|
322
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
sessionId,
|
|
327
|
+
targetId
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function readInteractionTargetId(message, targetIdByAttachedSessionId) {
|
|
331
|
+
const sessionId = message.sessionId;
|
|
332
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (!message.method || !message.method.startsWith("Input.") && !message.method.startsWith("Page.")) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return targetIdByAttachedSessionId.get(sessionId) ?? null;
|
|
339
|
+
}
|
|
340
|
+
function readDetachedTargetSessionId(message) {
|
|
341
|
+
if (message.method !== "Target.detachedFromTarget") {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const sessionId = message.params?.sessionId;
|
|
345
|
+
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
346
|
+
}
|
|
347
|
+
function rawDataToString(data) {
|
|
348
|
+
if (typeof data === "string") {
|
|
349
|
+
return data;
|
|
350
|
+
}
|
|
351
|
+
if (data instanceof ArrayBuffer) {
|
|
352
|
+
return Buffer.from(data).toString("utf8");
|
|
353
|
+
}
|
|
354
|
+
if (Array.isArray(data)) {
|
|
355
|
+
return Buffer.concat(data).toString("utf8");
|
|
356
|
+
}
|
|
357
|
+
return data.toString("utf8");
|
|
358
|
+
}
|
|
359
|
+
function rawDataSizeBytes(data) {
|
|
360
|
+
if (typeof data === "string") {
|
|
361
|
+
return Buffer.byteLength(data);
|
|
362
|
+
}
|
|
363
|
+
if (data instanceof ArrayBuffer) {
|
|
364
|
+
return data.byteLength;
|
|
365
|
+
}
|
|
366
|
+
if (Array.isArray(data)) {
|
|
367
|
+
return data.reduce((total, entry) => total + entry.byteLength, 0);
|
|
368
|
+
}
|
|
369
|
+
return data.byteLength;
|
|
370
|
+
}
|
|
371
|
+
function safeCloseSocket(socket) {
|
|
372
|
+
try {
|
|
373
|
+
socket.close();
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/local-view/tab-state-tracker.ts
|
|
379
|
+
var ACTIVATION_INTENT_DISCOVERY_GRACE_MS = 2e3;
|
|
380
|
+
var TabStateTracker = class {
|
|
381
|
+
deps;
|
|
382
|
+
timer = null;
|
|
383
|
+
running = false;
|
|
384
|
+
lastActivePage = null;
|
|
385
|
+
lastTabsSignature = "";
|
|
386
|
+
tickInFlight = false;
|
|
387
|
+
metadataByPage = /* @__PURE__ */ new Map();
|
|
388
|
+
targetIdByPage = /* @__PURE__ */ new WeakMap();
|
|
389
|
+
pageCleanupByPage = /* @__PURE__ */ new Map();
|
|
390
|
+
boundContextCleanup = null;
|
|
391
|
+
constructor(deps) {
|
|
392
|
+
this.deps = deps;
|
|
393
|
+
}
|
|
394
|
+
start() {
|
|
395
|
+
if (this.running) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.running = true;
|
|
399
|
+
this.bindContextEvents();
|
|
400
|
+
void this.reconcile({
|
|
401
|
+
includeFocus: true,
|
|
402
|
+
refreshMetadata: true
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
stop() {
|
|
406
|
+
this.running = false;
|
|
407
|
+
if (this.timer) {
|
|
408
|
+
clearInterval(this.timer);
|
|
409
|
+
this.timer = null;
|
|
410
|
+
}
|
|
411
|
+
this.boundContextCleanup?.();
|
|
412
|
+
this.boundContextCleanup = null;
|
|
413
|
+
for (const cleanup of this.pageCleanupByPage.values()) {
|
|
414
|
+
cleanup();
|
|
415
|
+
}
|
|
416
|
+
this.pageCleanupByPage.clear();
|
|
417
|
+
this.metadataByPage.clear();
|
|
418
|
+
}
|
|
419
|
+
bindContextEvents() {
|
|
420
|
+
if (this.boundContextCleanup) {
|
|
421
|
+
this.syncTrackedPages(this.deps.browserContext.pages());
|
|
422
|
+
this.updatePolling(this.deps.browserContext.pages().length);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const onPage = (page) => {
|
|
426
|
+
this.syncTrackedPages(this.deps.browserContext.pages());
|
|
427
|
+
this.updatePolling(this.deps.browserContext.pages().length);
|
|
428
|
+
this.attachPageListeners(page);
|
|
429
|
+
void this.reconcile({
|
|
430
|
+
includeFocus: true,
|
|
431
|
+
refreshMetadata: true
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
this.deps.browserContext.on("page", onPage);
|
|
435
|
+
this.boundContextCleanup = () => {
|
|
436
|
+
this.deps.browserContext.off("page", onPage);
|
|
437
|
+
};
|
|
438
|
+
this.syncTrackedPages(this.deps.browserContext.pages());
|
|
439
|
+
this.updatePolling(this.deps.browserContext.pages().length);
|
|
440
|
+
}
|
|
441
|
+
syncTrackedPages(pages) {
|
|
442
|
+
const nextPages = new Set(pages);
|
|
443
|
+
for (const [page, cleanup] of this.pageCleanupByPage.entries()) {
|
|
444
|
+
if (nextPages.has(page)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
cleanup();
|
|
448
|
+
this.pageCleanupByPage.delete(page);
|
|
449
|
+
this.metadataByPage.delete(page);
|
|
450
|
+
}
|
|
451
|
+
for (const page of pages) {
|
|
452
|
+
this.attachPageListeners(page);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
attachPageListeners(page) {
|
|
456
|
+
if (this.pageCleanupByPage.has(page)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const refreshMetadata = () => {
|
|
460
|
+
void this.reconcile({
|
|
461
|
+
includeFocus: false,
|
|
462
|
+
refreshMetadata: true
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
const handleClose = () => {
|
|
466
|
+
this.pageCleanupByPage.get(page)?.();
|
|
467
|
+
this.pageCleanupByPage.delete(page);
|
|
468
|
+
this.metadataByPage.delete(page);
|
|
469
|
+
void this.reconcile({
|
|
470
|
+
includeFocus: true,
|
|
471
|
+
refreshMetadata: true
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
page.on("close", handleClose);
|
|
475
|
+
page.on("domcontentloaded", refreshMetadata);
|
|
476
|
+
page.on("load", refreshMetadata);
|
|
477
|
+
page.on("framenavigated", refreshMetadata);
|
|
478
|
+
this.pageCleanupByPage.set(page, () => {
|
|
479
|
+
page.off("close", handleClose);
|
|
480
|
+
page.off("domcontentloaded", refreshMetadata);
|
|
481
|
+
page.off("load", refreshMetadata);
|
|
482
|
+
page.off("framenavigated", refreshMetadata);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
updatePolling(pageCount) {
|
|
486
|
+
const shouldPoll = this.running && pageCount > 0;
|
|
487
|
+
if (!shouldPoll) {
|
|
488
|
+
if (this.timer) {
|
|
489
|
+
clearInterval(this.timer);
|
|
490
|
+
this.timer = null;
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (this.timer) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.timer = setInterval(() => {
|
|
498
|
+
const trackedPageCount = this.deps.browserContext.pages().length;
|
|
499
|
+
void this.reconcile({
|
|
500
|
+
includeFocus: trackedPageCount > 1,
|
|
501
|
+
refreshMetadata: true
|
|
502
|
+
});
|
|
503
|
+
}, this.deps.pollMs);
|
|
504
|
+
}
|
|
505
|
+
async reconcile(args) {
|
|
506
|
+
if (!this.running || this.tickInFlight) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.tickInFlight = true;
|
|
510
|
+
try {
|
|
511
|
+
this.bindContextEvents();
|
|
512
|
+
const pages = this.deps.browserContext.pages();
|
|
513
|
+
this.syncTrackedPages(pages);
|
|
514
|
+
this.updatePolling(pages.length);
|
|
515
|
+
const preferredActivePage = this.lastActivePage ?? pages[0] ?? null;
|
|
516
|
+
const pageStates = await Promise.all(
|
|
517
|
+
pages.map(async (page, index) => {
|
|
518
|
+
const metadata = await this.readPageMetadata(page, {
|
|
519
|
+
refresh: args.refreshMetadata
|
|
520
|
+
});
|
|
521
|
+
const focusState = args.includeFocus ? await this.readFocusState(page) : {
|
|
522
|
+
isVisible: page === preferredActivePage,
|
|
523
|
+
hasFocus: page === preferredActivePage
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
page,
|
|
527
|
+
index,
|
|
528
|
+
targetId: metadata.targetId,
|
|
529
|
+
url: page.url(),
|
|
530
|
+
title: metadata.title,
|
|
531
|
+
isVisible: focusState.isVisible,
|
|
532
|
+
hasFocus: focusState.hasFocus
|
|
533
|
+
};
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
const activePage = this.pickActivePage(
|
|
537
|
+
pageStates,
|
|
538
|
+
this.lastActivePage,
|
|
539
|
+
preferredActivePage,
|
|
540
|
+
this.resolveIntentPage(pageStates)
|
|
541
|
+
);
|
|
542
|
+
if (activePage && activePage !== this.lastActivePage) {
|
|
543
|
+
this.lastActivePage = activePage;
|
|
544
|
+
this.deps.onActivePageChanged(activePage);
|
|
545
|
+
}
|
|
546
|
+
const tabs = pageStates.map((state) => ({
|
|
547
|
+
index: state.index,
|
|
548
|
+
...state.targetId === void 0 ? {} : { targetId: state.targetId },
|
|
549
|
+
url: state.url,
|
|
550
|
+
title: state.title,
|
|
551
|
+
active: activePage ? state.page === activePage : false
|
|
552
|
+
}));
|
|
553
|
+
const activeTabIndex = tabs.findIndex((tab) => tab.active);
|
|
554
|
+
const signature = JSON.stringify({
|
|
555
|
+
activeTabIndex,
|
|
556
|
+
tabs: tabs.map((tab) => ({
|
|
557
|
+
index: tab.index,
|
|
558
|
+
targetId: tab.targetId,
|
|
559
|
+
url: tab.url,
|
|
560
|
+
title: tab.title,
|
|
561
|
+
active: tab.active
|
|
562
|
+
}))
|
|
563
|
+
});
|
|
564
|
+
if (signature !== this.lastTabsSignature) {
|
|
565
|
+
this.lastTabsSignature = signature;
|
|
566
|
+
this.deps.onTabsChanged({
|
|
567
|
+
tabs,
|
|
568
|
+
activeTabIndex
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} finally {
|
|
572
|
+
this.tickInFlight = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async readPageMetadata(page, options) {
|
|
576
|
+
const cached = this.metadataByPage.get(page);
|
|
577
|
+
if (cached && !options.refresh) {
|
|
578
|
+
return cached;
|
|
579
|
+
}
|
|
580
|
+
const [title, targetId] = await Promise.all([
|
|
581
|
+
page.title().catch(() => cached?.title ?? ""),
|
|
582
|
+
this.resolveTargetId(page).catch(() => cached?.targetId)
|
|
583
|
+
]);
|
|
584
|
+
const nextMetadata = {
|
|
585
|
+
title,
|
|
586
|
+
targetId: targetId ?? void 0
|
|
587
|
+
};
|
|
588
|
+
this.metadataByPage.set(page, nextMetadata);
|
|
589
|
+
return nextMetadata;
|
|
590
|
+
}
|
|
591
|
+
async resolveTargetId(page) {
|
|
592
|
+
const cached = this.targetIdByPage.get(page);
|
|
593
|
+
if (cached) {
|
|
594
|
+
return cached;
|
|
595
|
+
}
|
|
596
|
+
const cdp = await page.context().newCDPSession(page);
|
|
597
|
+
try {
|
|
598
|
+
const result = await cdp.send("Target.getTargetInfo");
|
|
599
|
+
const targetId = result?.targetInfo?.targetId;
|
|
600
|
+
if (typeof targetId === "string" && targetId.length > 0) {
|
|
601
|
+
this.targetIdByPage.set(page, targetId);
|
|
602
|
+
return targetId;
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
} finally {
|
|
606
|
+
await cdp.detach().catch(() => void 0);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async readFocusState(page) {
|
|
610
|
+
try {
|
|
611
|
+
const result = await page.evaluate(() => ({
|
|
612
|
+
visibilityState: globalThis.document?.visibilityState,
|
|
613
|
+
hasFocus: globalThis.document?.hasFocus?.() ?? false
|
|
614
|
+
}));
|
|
615
|
+
return {
|
|
616
|
+
isVisible: result.visibilityState === "visible",
|
|
617
|
+
hasFocus: result.hasFocus === true
|
|
618
|
+
};
|
|
619
|
+
} catch {
|
|
620
|
+
return {
|
|
621
|
+
isVisible: false,
|
|
622
|
+
hasFocus: false
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
pickActivePage(pageStates, lastActivePage, fallbackPage, intent) {
|
|
627
|
+
if (intent) {
|
|
628
|
+
return intent.page;
|
|
629
|
+
}
|
|
630
|
+
const focusedVisiblePages = pageStates.filter((state) => state.isVisible && state.hasFocus);
|
|
631
|
+
if (focusedVisiblePages.length === 1) {
|
|
632
|
+
return focusedVisiblePages[0]?.page ?? null;
|
|
633
|
+
}
|
|
634
|
+
const visiblePages = pageStates.filter((state) => state.isVisible);
|
|
635
|
+
if (visiblePages.length === 1) {
|
|
636
|
+
return visiblePages[0]?.page ?? null;
|
|
637
|
+
}
|
|
638
|
+
const lastActivePageState = lastActivePage ? pageStates.find((state) => state.page === lastActivePage) ?? null : null;
|
|
639
|
+
if (lastActivePageState && (focusedVisiblePages.length > 1 && lastActivePageState.isVisible && lastActivePageState.hasFocus || visiblePages.length > 1 && lastActivePageState.isVisible || visiblePages.length === 0)) {
|
|
640
|
+
return lastActivePage;
|
|
641
|
+
}
|
|
642
|
+
const fallbackPageState = fallbackPage ? pageStates.find((state) => state.page === fallbackPage) ?? null : null;
|
|
643
|
+
if (fallbackPageState && (focusedVisiblePages.length > 1 && fallbackPageState.isVisible && fallbackPageState.hasFocus || visiblePages.length > 1 && fallbackPageState.isVisible)) {
|
|
644
|
+
return fallbackPage;
|
|
645
|
+
}
|
|
646
|
+
if (focusedVisiblePages.length > 0) {
|
|
647
|
+
return focusedVisiblePages[0]?.page ?? null;
|
|
648
|
+
}
|
|
649
|
+
if (visiblePages.length > 0) {
|
|
650
|
+
return visiblePages[0]?.page ?? null;
|
|
651
|
+
}
|
|
652
|
+
if (lastActivePageState) {
|
|
653
|
+
return lastActivePageState.page;
|
|
654
|
+
}
|
|
655
|
+
if (fallbackPageState) {
|
|
656
|
+
return fallbackPageState.page;
|
|
657
|
+
}
|
|
658
|
+
return pageStates[0]?.page ?? null;
|
|
659
|
+
}
|
|
660
|
+
resolveIntentPage(pageStates) {
|
|
661
|
+
const intent = this.deps.runtimeState.getPageActivationIntent(this.deps.sessionId);
|
|
662
|
+
if (!intent) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
const matched = pageStates.find((state) => state.targetId === intent.targetId);
|
|
666
|
+
if (!matched) {
|
|
667
|
+
if (Date.now() - intent.ts > ACTIVATION_INTENT_DISCOVERY_GRACE_MS) {
|
|
668
|
+
this.deps.runtimeState.clearPageActivationIntent(this.deps.sessionId, intent.targetId);
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
this.deps.runtimeState.clearPageActivationIntent(this.deps.sessionId, intent.targetId);
|
|
673
|
+
return { page: matched.page };
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
// src/local-view/view-stream-capture-policy.ts
|
|
678
|
+
var MIN_CAPTURE_DIMENSION_PX = 100;
|
|
679
|
+
var MAX_CAPTURE_DIMENSION_PX = 8192;
|
|
680
|
+
var CAPTURE_BUCKET_PX = 64;
|
|
681
|
+
function selectScreencastSize(args) {
|
|
682
|
+
const viewport = normalizeViewport(args.viewport);
|
|
683
|
+
if (!viewport) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
let maxRequestedWidth = 0;
|
|
687
|
+
let maxRequestedHeight = 0;
|
|
688
|
+
for (const requestedSize of args.requestedSizes) {
|
|
689
|
+
const normalized = normalizeRequestedSize(requestedSize);
|
|
690
|
+
if (!normalized) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
maxRequestedWidth = Math.max(maxRequestedWidth, normalized.width);
|
|
694
|
+
maxRequestedHeight = Math.max(maxRequestedHeight, normalized.height);
|
|
695
|
+
}
|
|
696
|
+
if (maxRequestedWidth < MIN_CAPTURE_DIMENSION_PX || maxRequestedHeight < MIN_CAPTURE_DIMENSION_PX) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
const desiredScale = Math.max(
|
|
700
|
+
maxRequestedWidth / viewport.width,
|
|
701
|
+
maxRequestedHeight / viewport.height
|
|
702
|
+
);
|
|
703
|
+
if (desiredScale >= 1) {
|
|
704
|
+
return viewport;
|
|
705
|
+
}
|
|
706
|
+
const landscape = viewport.width >= viewport.height;
|
|
707
|
+
const sourcePrimary = landscape ? viewport.width : viewport.height;
|
|
708
|
+
const sourceSecondary = landscape ? viewport.height : viewport.width;
|
|
709
|
+
const nextPrimary = bucketDimension(sourcePrimary * desiredScale);
|
|
710
|
+
const nextSecondary = clampDimension(Math.round(nextPrimary / sourcePrimary * sourceSecondary));
|
|
711
|
+
if (!nextSecondary) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
return landscape ? { width: nextPrimary, height: nextSecondary } : { width: nextSecondary, height: nextPrimary };
|
|
715
|
+
}
|
|
716
|
+
function normalizeViewport(viewport) {
|
|
717
|
+
const width = clampDimension(viewport.width);
|
|
718
|
+
const height = clampDimension(viewport.height);
|
|
719
|
+
return width && height ? { width, height } : null;
|
|
720
|
+
}
|
|
721
|
+
function normalizeRequestedSize(requestedSize) {
|
|
722
|
+
const width = clampDimension(requestedSize.width);
|
|
723
|
+
const height = clampDimension(requestedSize.height);
|
|
724
|
+
return width && height ? { width, height } : null;
|
|
725
|
+
}
|
|
726
|
+
function clampDimension(value) {
|
|
727
|
+
if (!Number.isFinite(value)) {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
const normalized = Math.floor(value);
|
|
731
|
+
if (normalized < MIN_CAPTURE_DIMENSION_PX) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
return Math.min(MAX_CAPTURE_DIMENSION_PX, normalized);
|
|
735
|
+
}
|
|
736
|
+
function bucketDimension(value) {
|
|
737
|
+
const bucketed = Math.ceil(Math.max(MIN_CAPTURE_DIMENSION_PX, value) / CAPTURE_BUCKET_PX) * CAPTURE_BUCKET_PX;
|
|
738
|
+
return Math.min(MAX_CAPTURE_DIMENSION_PX, bucketed);
|
|
739
|
+
}
|
|
740
|
+
function buildHelloMessage(args) {
|
|
741
|
+
return {
|
|
742
|
+
type: "hello",
|
|
743
|
+
sessionId: args.sessionId,
|
|
744
|
+
ts: Date.now(),
|
|
745
|
+
mimeType: "image/jpeg",
|
|
746
|
+
fps: args.fps,
|
|
747
|
+
quality: args.quality,
|
|
748
|
+
viewport: args.viewport
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function buildTabsMessage(args) {
|
|
752
|
+
return {
|
|
753
|
+
type: "tabs",
|
|
754
|
+
sessionId: args.sessionId,
|
|
755
|
+
ts: Date.now(),
|
|
756
|
+
tabs: args.tabs,
|
|
757
|
+
activeTabIndex: args.activeTabIndex
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function buildStatusMessage(args) {
|
|
761
|
+
return {
|
|
762
|
+
type: "status",
|
|
763
|
+
sessionId: args.sessionId,
|
|
764
|
+
ts: Date.now(),
|
|
765
|
+
status: args.status
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function buildErrorMessage(args) {
|
|
769
|
+
return {
|
|
770
|
+
type: "error",
|
|
771
|
+
sessionId: args.sessionId,
|
|
772
|
+
ts: Date.now(),
|
|
773
|
+
error: args.error
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function sendControlMessage(ws, message) {
|
|
777
|
+
if (ws.readyState !== WebSocket2__default.OPEN) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
ws.send(JSON.stringify(message), { binary: false });
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function parseViewClientMessage(raw) {
|
|
786
|
+
try {
|
|
787
|
+
const parsed = JSON.parse(raw);
|
|
788
|
+
if (parsed?.type !== "stream-config") {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
const renderWidth = normalizeRenderDimension(parsed.renderWidth);
|
|
792
|
+
const renderHeight = normalizeRenderDimension(parsed.renderHeight);
|
|
793
|
+
if (renderWidth === null || renderHeight === null) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
type: "stream-config",
|
|
798
|
+
renderWidth,
|
|
799
|
+
renderHeight
|
|
800
|
+
};
|
|
801
|
+
} catch {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function normalizeRenderDimension(value) {
|
|
806
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
const normalized = Math.floor(value);
|
|
810
|
+
if (normalized < 100) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
return Math.min(8192, normalized);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/local-view/view-stream.ts
|
|
817
|
+
var INITIAL_FRAME_CAPTURE_ATTEMPTS = 3;
|
|
818
|
+
var INITIAL_FRAME_CAPTURE_RETRY_DELAY_MS = 150;
|
|
819
|
+
var TAB_STATE_POLL_MS = 1e3;
|
|
820
|
+
var CLIENT_FRAME_FLUSH_RETRY_MS = 16;
|
|
821
|
+
var LocalViewStreamHub = class {
|
|
822
|
+
deps;
|
|
823
|
+
producers = /* @__PURE__ */ new Map();
|
|
824
|
+
constructor(deps) {
|
|
825
|
+
this.deps = deps;
|
|
826
|
+
}
|
|
827
|
+
attachClient(sessionId, ws) {
|
|
828
|
+
let producer = this.producers.get(sessionId);
|
|
829
|
+
if (!producer) {
|
|
830
|
+
producer = new SessionViewStreamProducer({
|
|
831
|
+
sessionId,
|
|
832
|
+
runtimeState: this.deps.runtimeState,
|
|
833
|
+
maxFps: this.deps.maxFps,
|
|
834
|
+
quality: this.deps.quality,
|
|
835
|
+
maxClientBufferBytes: this.deps.maxClientBufferBytes,
|
|
836
|
+
onDrained: () => {
|
|
837
|
+
this.producers.delete(sessionId);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
this.producers.set(sessionId, producer);
|
|
841
|
+
}
|
|
842
|
+
producer.addClient(ws);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
var SessionViewStreamProducer = class {
|
|
846
|
+
deps;
|
|
847
|
+
clients = /* @__PURE__ */ new Set();
|
|
848
|
+
clientStateBySocket = /* @__PURE__ */ new Map();
|
|
849
|
+
frameIntervalMs;
|
|
850
|
+
tracker = null;
|
|
851
|
+
browser = null;
|
|
852
|
+
browserDisconnectedHandler = null;
|
|
853
|
+
context = null;
|
|
854
|
+
cdpSession = null;
|
|
855
|
+
screencastHandler = null;
|
|
856
|
+
pageLifecycleCleanup = null;
|
|
857
|
+
activePage = null;
|
|
858
|
+
activeViewport = null;
|
|
859
|
+
activeScreencastSizeKey = null;
|
|
860
|
+
pendingFrameAckTimer = null;
|
|
861
|
+
starting = null;
|
|
862
|
+
started = false;
|
|
863
|
+
rebinding = Promise.resolve();
|
|
864
|
+
stopped = false;
|
|
865
|
+
lastFrameSentAt = 0;
|
|
866
|
+
lastFrameBuffer = null;
|
|
867
|
+
lastTabsPayload = null;
|
|
868
|
+
constructor(deps) {
|
|
869
|
+
this.deps = deps;
|
|
870
|
+
this.frameIntervalMs = Math.max(1, Math.floor(1e3 / Math.max(1, deps.maxFps)));
|
|
871
|
+
}
|
|
872
|
+
addClient(ws) {
|
|
873
|
+
if (this.stopped) {
|
|
874
|
+
ws.close(1011, "View stream is unavailable.");
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
this.clients.add(ws);
|
|
878
|
+
this.clientStateBySocket.set(ws, {
|
|
879
|
+
requestedRenderSize: null,
|
|
880
|
+
frameSendInFlight: false,
|
|
881
|
+
pendingFrameBuffer: null,
|
|
882
|
+
pendingFlushTimer: null
|
|
883
|
+
});
|
|
884
|
+
if (this.activeViewport) {
|
|
885
|
+
sendControlMessage(
|
|
886
|
+
ws,
|
|
887
|
+
buildHelloMessage({
|
|
888
|
+
sessionId: this.deps.sessionId,
|
|
889
|
+
fps: this.deps.maxFps,
|
|
890
|
+
quality: this.deps.quality,
|
|
891
|
+
viewport: this.activeViewport
|
|
892
|
+
})
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (this.lastTabsPayload) {
|
|
896
|
+
sendControlMessage(
|
|
897
|
+
ws,
|
|
898
|
+
buildTabsMessage({
|
|
899
|
+
sessionId: this.deps.sessionId,
|
|
900
|
+
tabs: this.lastTabsPayload.tabs,
|
|
901
|
+
activeTabIndex: this.lastTabsPayload.activeTabIndex
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
if (this.lastFrameBuffer) {
|
|
906
|
+
const queued = this.enqueueFrameForClient(ws, this.lastFrameBuffer);
|
|
907
|
+
if (!queued) {
|
|
908
|
+
this.removeClient(ws);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
ws.on("close", () => {
|
|
913
|
+
this.removeClient(ws);
|
|
914
|
+
});
|
|
915
|
+
ws.on("error", () => {
|
|
916
|
+
this.removeClient(ws);
|
|
917
|
+
});
|
|
918
|
+
ws.on("message", (raw, isBinary) => {
|
|
919
|
+
if (isBinary) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const message = parseViewClientMessage(readTextFrame(raw));
|
|
923
|
+
if (message?.type !== "stream-config") {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const nextSize = {
|
|
927
|
+
width: message.renderWidth,
|
|
928
|
+
height: message.renderHeight
|
|
929
|
+
};
|
|
930
|
+
const clientState = this.clientStateBySocket.get(ws);
|
|
931
|
+
if (!clientState) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const priorSize = clientState.requestedRenderSize;
|
|
935
|
+
if (priorSize?.width === nextSize.width && priorSize?.height === nextSize.height) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
clientState.requestedRenderSize = nextSize;
|
|
939
|
+
this.maybeRebindForStreamConfigChange();
|
|
940
|
+
});
|
|
941
|
+
void this.ensureStarted();
|
|
942
|
+
}
|
|
943
|
+
maybeRebindForStreamConfigChange() {
|
|
944
|
+
if (!this.activePage || !this.started || this.stopped) {
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const nextSizeKey = this.getRequestedScreencastSizeKey();
|
|
948
|
+
if (nextSizeKey === this.activeScreencastSizeKey) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
void this.queueBindToPage(this.activePage, { force: true }).catch(() => void 0);
|
|
952
|
+
}
|
|
953
|
+
removeClient(ws) {
|
|
954
|
+
this.clients.delete(ws);
|
|
955
|
+
const clientState = this.clientStateBySocket.get(ws);
|
|
956
|
+
if (clientState?.pendingFlushTimer) {
|
|
957
|
+
clearTimeout(clientState.pendingFlushTimer);
|
|
958
|
+
}
|
|
959
|
+
this.clientStateBySocket.delete(ws);
|
|
960
|
+
if (this.clients.size === 0) {
|
|
961
|
+
void this.stop();
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
this.maybeRebindForStreamConfigChange();
|
|
965
|
+
}
|
|
966
|
+
async ensureStarted() {
|
|
967
|
+
if (this.stopped || this.started) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (this.starting) {
|
|
971
|
+
return this.starting;
|
|
972
|
+
}
|
|
973
|
+
this.starting = this.start().then(() => {
|
|
974
|
+
if (!this.stopped) {
|
|
975
|
+
this.started = true;
|
|
976
|
+
}
|
|
977
|
+
}).finally(() => {
|
|
978
|
+
this.starting = null;
|
|
979
|
+
});
|
|
980
|
+
try {
|
|
981
|
+
await this.starting;
|
|
982
|
+
} catch {
|
|
983
|
+
this.broadcastControl(
|
|
984
|
+
buildErrorMessage({
|
|
985
|
+
sessionId: this.deps.sessionId,
|
|
986
|
+
error: "Failed to start live browser stream."
|
|
987
|
+
})
|
|
988
|
+
);
|
|
989
|
+
this.closeAllClients(1011, "View stream failed");
|
|
990
|
+
await this.stop();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async start() {
|
|
994
|
+
const session = await this.connectSession();
|
|
995
|
+
this.broadcastControl(
|
|
996
|
+
buildStatusMessage({
|
|
997
|
+
sessionId: this.deps.sessionId,
|
|
998
|
+
status: "starting"
|
|
999
|
+
})
|
|
1000
|
+
);
|
|
1001
|
+
this.browser = session.browser;
|
|
1002
|
+
this.browserDisconnectedHandler = () => {
|
|
1003
|
+
if (this.stopped) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
this.browserDisconnectedHandler = null;
|
|
1007
|
+
this.broadcastControl(
|
|
1008
|
+
buildErrorMessage({
|
|
1009
|
+
sessionId: this.deps.sessionId,
|
|
1010
|
+
error: "Live browser stream disconnected."
|
|
1011
|
+
})
|
|
1012
|
+
);
|
|
1013
|
+
this.closeAllClients(1011, "View stream failed");
|
|
1014
|
+
void this.stop();
|
|
1015
|
+
};
|
|
1016
|
+
this.browser.once("disconnected", this.browserDisconnectedHandler);
|
|
1017
|
+
this.context = session.context;
|
|
1018
|
+
this.activePage = session.page;
|
|
1019
|
+
this.activeViewport = await readViewportForPage(session.page);
|
|
1020
|
+
if (this.stopped) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (this.activeViewport) {
|
|
1024
|
+
this.broadcastControl(
|
|
1025
|
+
buildHelloMessage({
|
|
1026
|
+
sessionId: this.deps.sessionId,
|
|
1027
|
+
fps: this.deps.maxFps,
|
|
1028
|
+
quality: this.deps.quality,
|
|
1029
|
+
viewport: this.activeViewport
|
|
1030
|
+
})
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
this.tracker = new TabStateTracker({
|
|
1034
|
+
browserContext: session.context,
|
|
1035
|
+
sessionId: this.deps.sessionId,
|
|
1036
|
+
pollMs: TAB_STATE_POLL_MS,
|
|
1037
|
+
runtimeState: this.deps.runtimeState,
|
|
1038
|
+
onActivePageChanged: (page) => {
|
|
1039
|
+
this.activePage = page;
|
|
1040
|
+
void this.queueBindToPage(page).catch(() => void 0);
|
|
1041
|
+
},
|
|
1042
|
+
onTabsChanged: ({ tabs, activeTabIndex }) => {
|
|
1043
|
+
this.lastTabsPayload = { tabs, activeTabIndex };
|
|
1044
|
+
this.broadcastControl(
|
|
1045
|
+
buildTabsMessage({
|
|
1046
|
+
sessionId: this.deps.sessionId,
|
|
1047
|
+
tabs,
|
|
1048
|
+
activeTabIndex
|
|
1049
|
+
})
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
this.tracker.start();
|
|
1054
|
+
await this.queueBindToPage(session.page);
|
|
1055
|
+
if (this.stopped) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
this.broadcastControl(
|
|
1059
|
+
buildStatusMessage({
|
|
1060
|
+
sessionId: this.deps.sessionId,
|
|
1061
|
+
status: "live"
|
|
1062
|
+
})
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
queueBindToPage(page, options = {}) {
|
|
1066
|
+
this.rebinding = this.rebinding.catch(() => void 0).then(() => this.bindToPage(page, options));
|
|
1067
|
+
return this.rebinding;
|
|
1068
|
+
}
|
|
1069
|
+
async bindToPage(page, options = {}) {
|
|
1070
|
+
if (this.stopped) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const requestedSizeKey = this.getRequestedScreencastSizeKey();
|
|
1074
|
+
if (!options.force && this.activePage === page && this.cdpSession && this.activeScreencastSizeKey === requestedSizeKey) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
await this.stopScreencast();
|
|
1078
|
+
if (this.stopped) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const context = this.context;
|
|
1082
|
+
if (!context) {
|
|
1083
|
+
throw new Error("Browser context is unavailable.");
|
|
1084
|
+
}
|
|
1085
|
+
const requestedSize = this.getRequestedScreencastSize();
|
|
1086
|
+
this.activePage = page;
|
|
1087
|
+
this.activeScreencastSizeKey = requestedSizeKey;
|
|
1088
|
+
this.activeViewport = await readViewportForPage(page);
|
|
1089
|
+
if (this.activeViewport) {
|
|
1090
|
+
this.broadcastControl(
|
|
1091
|
+
buildHelloMessage({
|
|
1092
|
+
sessionId: this.deps.sessionId,
|
|
1093
|
+
fps: this.deps.maxFps,
|
|
1094
|
+
quality: this.deps.quality,
|
|
1095
|
+
viewport: this.activeViewport
|
|
1096
|
+
})
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const cdpSession = await context.newCDPSession(page);
|
|
1100
|
+
if (this.stopped) {
|
|
1101
|
+
await cdpSession.detach().catch(() => void 0);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
this.cdpSession = cdpSession;
|
|
1105
|
+
const onFrame = (event) => {
|
|
1106
|
+
void this.handleScreencastFrame(event);
|
|
1107
|
+
};
|
|
1108
|
+
this.screencastHandler = onFrame;
|
|
1109
|
+
cdpSession.on("Page.screencastFrame", onFrame);
|
|
1110
|
+
await cdpSession.send("Page.enable");
|
|
1111
|
+
await cdpSession.send("Page.startScreencast", {
|
|
1112
|
+
format: "jpeg",
|
|
1113
|
+
quality: this.deps.quality,
|
|
1114
|
+
everyNthFrame: 1,
|
|
1115
|
+
...requestedSize ? {
|
|
1116
|
+
maxWidth: requestedSize.width,
|
|
1117
|
+
maxHeight: requestedSize.height
|
|
1118
|
+
} : {}
|
|
1119
|
+
});
|
|
1120
|
+
this.bindPageLifecycleFrameRefresh(page, cdpSession);
|
|
1121
|
+
void this.seedInitialFrame(cdpSession).catch(() => void 0);
|
|
1122
|
+
}
|
|
1123
|
+
async connectSession() {
|
|
1124
|
+
const resolved = await resolveLocalViewSession(this.deps.sessionId);
|
|
1125
|
+
if (!resolved) {
|
|
1126
|
+
throw new Error(`Local view session ${this.deps.sessionId} is unavailable.`);
|
|
1127
|
+
}
|
|
1128
|
+
const browser = await connectPlaywrightChromiumBrowser({
|
|
1129
|
+
url: resolved.browserWebSocketUrl
|
|
1130
|
+
});
|
|
1131
|
+
try {
|
|
1132
|
+
const context = browser.contexts()[0];
|
|
1133
|
+
if (!context) {
|
|
1134
|
+
throw new Error("Connected browser did not expose a Chromium browser context.");
|
|
1135
|
+
}
|
|
1136
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
1137
|
+
return {
|
|
1138
|
+
browser,
|
|
1139
|
+
context,
|
|
1140
|
+
page
|
|
1141
|
+
};
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async handleScreencastFrame(event) {
|
|
1148
|
+
const cdpSession = this.cdpSession;
|
|
1149
|
+
if (!cdpSession || this.stopped) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const frameBuffer = Buffer.from(event.data, "base64");
|
|
1153
|
+
this.lastFrameBuffer = frameBuffer;
|
|
1154
|
+
const now = Date.now();
|
|
1155
|
+
const delayMs = Math.max(0, this.frameIntervalMs - (now - this.lastFrameSentAt));
|
|
1156
|
+
if (delayMs === 0) {
|
|
1157
|
+
this.flushScreencastFrame({
|
|
1158
|
+
cdpSession,
|
|
1159
|
+
sessionId: event.sessionId,
|
|
1160
|
+
frameBuffer
|
|
1161
|
+
});
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (this.pendingFrameAckTimer !== null) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
this.pendingFrameAckTimer = setTimeout(() => {
|
|
1168
|
+
this.pendingFrameAckTimer = null;
|
|
1169
|
+
if (this.stopped || this.cdpSession !== cdpSession) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
this.flushScreencastFrame({
|
|
1173
|
+
cdpSession,
|
|
1174
|
+
sessionId: event.sessionId,
|
|
1175
|
+
frameBuffer
|
|
1176
|
+
});
|
|
1177
|
+
}, delayMs);
|
|
1178
|
+
}
|
|
1179
|
+
flushScreencastFrame(args) {
|
|
1180
|
+
this.lastFrameSentAt = Date.now();
|
|
1181
|
+
this.broadcastFrame(args.frameBuffer);
|
|
1182
|
+
void args.cdpSession.send("Page.screencastFrameAck", { sessionId: args.sessionId }).catch(() => void 0);
|
|
1183
|
+
}
|
|
1184
|
+
broadcastFrame(frameBuffer) {
|
|
1185
|
+
for (const client of this.clients) {
|
|
1186
|
+
if (!this.enqueueFrameForClient(client, frameBuffer)) {
|
|
1187
|
+
this.removeClient(client);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (this.clients.size === 0) {
|
|
1191
|
+
void this.stop();
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
enqueueFrameForClient(client, frameBuffer) {
|
|
1195
|
+
if (client.readyState !== WebSocket2__default.OPEN) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
const clientState = this.clientStateBySocket.get(client);
|
|
1199
|
+
if (!clientState) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
clientState.pendingFrameBuffer = frameBuffer;
|
|
1203
|
+
this.flushQueuedFrameToClient(client);
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
flushQueuedFrameToClient(client) {
|
|
1207
|
+
if (client.readyState !== WebSocket2__default.OPEN) {
|
|
1208
|
+
this.removeClient(client);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const clientState = this.clientStateBySocket.get(client);
|
|
1212
|
+
if (!clientState || clientState.frameSendInFlight || !clientState.pendingFrameBuffer) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (clientState.pendingFlushTimer) {
|
|
1216
|
+
clearTimeout(clientState.pendingFlushTimer);
|
|
1217
|
+
clientState.pendingFlushTimer = null;
|
|
1218
|
+
}
|
|
1219
|
+
if (client.bufferedAmount > this.deps.maxClientBufferBytes) {
|
|
1220
|
+
clientState.pendingFlushTimer = setTimeout(() => {
|
|
1221
|
+
clientState.pendingFlushTimer = null;
|
|
1222
|
+
this.flushQueuedFrameToClient(client);
|
|
1223
|
+
}, CLIENT_FRAME_FLUSH_RETRY_MS);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const frameBuffer = clientState.pendingFrameBuffer;
|
|
1227
|
+
clientState.pendingFrameBuffer = null;
|
|
1228
|
+
clientState.frameSendInFlight = true;
|
|
1229
|
+
try {
|
|
1230
|
+
client.send(frameBuffer, { binary: true }, (error) => {
|
|
1231
|
+
const latestClientState = this.clientStateBySocket.get(client);
|
|
1232
|
+
if (latestClientState) {
|
|
1233
|
+
latestClientState.frameSendInFlight = false;
|
|
1234
|
+
}
|
|
1235
|
+
if (error) {
|
|
1236
|
+
this.removeClient(client);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
this.flushQueuedFrameToClient(client);
|
|
1240
|
+
});
|
|
1241
|
+
} catch {
|
|
1242
|
+
clientState.frameSendInFlight = false;
|
|
1243
|
+
this.removeClient(client);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
broadcastControl(message) {
|
|
1247
|
+
for (const client of this.clients) {
|
|
1248
|
+
sendControlMessage(client, message);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
closeAllClients(code, reason) {
|
|
1252
|
+
for (const client of this.clients) {
|
|
1253
|
+
try {
|
|
1254
|
+
client.close(code, reason);
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
this.clients.clear();
|
|
1259
|
+
for (const clientState of this.clientStateBySocket.values()) {
|
|
1260
|
+
if (clientState.pendingFlushTimer) {
|
|
1261
|
+
clearTimeout(clientState.pendingFlushTimer);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
this.clientStateBySocket.clear();
|
|
1265
|
+
}
|
|
1266
|
+
async stop() {
|
|
1267
|
+
if (this.stopped) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
this.stopped = true;
|
|
1271
|
+
this.started = false;
|
|
1272
|
+
if (this.tracker) {
|
|
1273
|
+
this.tracker.stop();
|
|
1274
|
+
this.tracker = null;
|
|
1275
|
+
}
|
|
1276
|
+
await this.rebinding.catch(() => void 0);
|
|
1277
|
+
await this.stopScreencast();
|
|
1278
|
+
const browser = this.browser;
|
|
1279
|
+
const browserDisconnectedHandler = this.browserDisconnectedHandler;
|
|
1280
|
+
this.browser = null;
|
|
1281
|
+
this.browserDisconnectedHandler = null;
|
|
1282
|
+
this.context = null;
|
|
1283
|
+
this.activePage = null;
|
|
1284
|
+
if (browser) {
|
|
1285
|
+
if (browserDisconnectedHandler) {
|
|
1286
|
+
browser.off("disconnected", browserDisconnectedHandler);
|
|
1287
|
+
}
|
|
1288
|
+
await disconnectPlaywrightChromiumBrowser(browser).catch(() => void 0);
|
|
1289
|
+
}
|
|
1290
|
+
this.deps.onDrained();
|
|
1291
|
+
}
|
|
1292
|
+
async stopScreencast() {
|
|
1293
|
+
const cdpSession = this.cdpSession;
|
|
1294
|
+
const handler = this.screencastHandler;
|
|
1295
|
+
const pageLifecycleCleanup = this.pageLifecycleCleanup;
|
|
1296
|
+
this.cdpSession = null;
|
|
1297
|
+
this.screencastHandler = null;
|
|
1298
|
+
this.pageLifecycleCleanup = null;
|
|
1299
|
+
this.activeScreencastSizeKey = null;
|
|
1300
|
+
if (this.pendingFrameAckTimer !== null) {
|
|
1301
|
+
clearTimeout(this.pendingFrameAckTimer);
|
|
1302
|
+
this.pendingFrameAckTimer = null;
|
|
1303
|
+
}
|
|
1304
|
+
pageLifecycleCleanup?.();
|
|
1305
|
+
if (!cdpSession) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (handler) {
|
|
1309
|
+
cdpSession.off("Page.screencastFrame", handler);
|
|
1310
|
+
}
|
|
1311
|
+
await cdpSession.send("Page.stopScreencast").catch(() => void 0);
|
|
1312
|
+
await cdpSession.detach().catch(() => void 0);
|
|
1313
|
+
}
|
|
1314
|
+
bindPageLifecycleFrameRefresh(page, cdpSession) {
|
|
1315
|
+
this.pageLifecycleCleanup?.();
|
|
1316
|
+
const refresh = () => {
|
|
1317
|
+
void this.refreshPageFrame(page, cdpSession).catch(() => void 0);
|
|
1318
|
+
};
|
|
1319
|
+
page.on("domcontentloaded", refresh);
|
|
1320
|
+
page.on("load", refresh);
|
|
1321
|
+
page.on("framenavigated", refresh);
|
|
1322
|
+
this.pageLifecycleCleanup = () => {
|
|
1323
|
+
page.off("domcontentloaded", refresh);
|
|
1324
|
+
page.off("load", refresh);
|
|
1325
|
+
page.off("framenavigated", refresh);
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
async refreshPageFrame(page, cdpSession) {
|
|
1329
|
+
if (this.stopped || this.cdpSession !== cdpSession || this.activePage !== page) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const viewport = await readViewportForPage(page);
|
|
1333
|
+
if (viewport && this.cdpSession === cdpSession && this.activePage === page) {
|
|
1334
|
+
this.activeViewport = viewport;
|
|
1335
|
+
this.broadcastControl(
|
|
1336
|
+
buildHelloMessage({
|
|
1337
|
+
sessionId: this.deps.sessionId,
|
|
1338
|
+
fps: this.deps.maxFps,
|
|
1339
|
+
quality: this.deps.quality,
|
|
1340
|
+
viewport
|
|
1341
|
+
})
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
if (this.stopped || this.cdpSession !== cdpSession || this.activePage !== page) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
await this.seedInitialFrame(cdpSession);
|
|
1348
|
+
}
|
|
1349
|
+
async seedInitialFrame(cdpSession) {
|
|
1350
|
+
let lastError = null;
|
|
1351
|
+
for (let attempt = 1; attempt <= INITIAL_FRAME_CAPTURE_ATTEMPTS; attempt += 1) {
|
|
1352
|
+
if (this.stopped || this.cdpSession !== cdpSession) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
try {
|
|
1356
|
+
const screenshotData = await this.captureCurrentFrame(cdpSession);
|
|
1357
|
+
if (this.stopped || this.cdpSession !== cdpSession) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
const frameBuffer = Buffer.from(screenshotData, "base64");
|
|
1361
|
+
this.lastFrameBuffer = frameBuffer;
|
|
1362
|
+
this.lastFrameSentAt = Date.now();
|
|
1363
|
+
this.broadcastFrame(frameBuffer);
|
|
1364
|
+
return;
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
lastError = error;
|
|
1367
|
+
}
|
|
1368
|
+
if (attempt < INITIAL_FRAME_CAPTURE_ATTEMPTS) {
|
|
1369
|
+
await new Promise((resolve) => setTimeout(resolve, INITIAL_FRAME_CAPTURE_RETRY_DELAY_MS));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (lastError instanceof Error) {
|
|
1373
|
+
throw lastError;
|
|
1374
|
+
}
|
|
1375
|
+
throw new Error("Failed to capture initial stream screenshot.");
|
|
1376
|
+
}
|
|
1377
|
+
async captureCurrentFrame(cdpSession) {
|
|
1378
|
+
const primaryParams = {
|
|
1379
|
+
format: "jpeg",
|
|
1380
|
+
quality: this.deps.quality,
|
|
1381
|
+
optimizeForSpeed: true
|
|
1382
|
+
};
|
|
1383
|
+
try {
|
|
1384
|
+
const result = await cdpSession.send("Page.captureScreenshot", primaryParams);
|
|
1385
|
+
if (result && typeof result.data === "string" && result.data.length > 0) {
|
|
1386
|
+
return result.data;
|
|
1387
|
+
}
|
|
1388
|
+
} catch {
|
|
1389
|
+
}
|
|
1390
|
+
const fallbackResult = await cdpSession.send("Page.captureScreenshot", {
|
|
1391
|
+
format: "jpeg",
|
|
1392
|
+
quality: this.deps.quality
|
|
1393
|
+
});
|
|
1394
|
+
if (!fallbackResult || typeof fallbackResult.data !== "string" || fallbackResult.data.length === 0) {
|
|
1395
|
+
throw new Error("Failed to capture initial stream screenshot.");
|
|
1396
|
+
}
|
|
1397
|
+
return fallbackResult.data;
|
|
1398
|
+
}
|
|
1399
|
+
getRequestedScreencastSize() {
|
|
1400
|
+
if (this.clients.size === 0 || !this.activeViewport) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
const requestedSizes = [];
|
|
1404
|
+
for (const client of this.clients) {
|
|
1405
|
+
const requestedSize = this.clientStateBySocket.get(client)?.requestedRenderSize ?? null;
|
|
1406
|
+
if (!requestedSize) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
requestedSizes.push(requestedSize);
|
|
1410
|
+
}
|
|
1411
|
+
return selectScreencastSize({
|
|
1412
|
+
viewport: this.activeViewport,
|
|
1413
|
+
requestedSizes
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
getRequestedScreencastSizeKey() {
|
|
1417
|
+
const size = this.getRequestedScreencastSize();
|
|
1418
|
+
return size ? `${size.width}x${size.height}` : null;
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
async function readViewportForPage(page) {
|
|
1422
|
+
const cdp = await page.context().newCDPSession(page);
|
|
1423
|
+
try {
|
|
1424
|
+
const result = await cdp.send("Page.getLayoutMetrics");
|
|
1425
|
+
const candidates = [
|
|
1426
|
+
result?.cssVisualViewport,
|
|
1427
|
+
result?.cssLayoutViewport,
|
|
1428
|
+
result?.visualViewport,
|
|
1429
|
+
result?.layoutViewport
|
|
1430
|
+
];
|
|
1431
|
+
for (const candidate of candidates) {
|
|
1432
|
+
const width = normalizeViewportDimension(candidate?.clientWidth);
|
|
1433
|
+
const height = normalizeViewportDimension(candidate?.clientHeight);
|
|
1434
|
+
if (width !== null && height !== null) {
|
|
1435
|
+
return { width, height };
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return null;
|
|
1439
|
+
} catch {
|
|
1440
|
+
const viewportSize = page.viewportSize();
|
|
1441
|
+
if (!viewportSize) {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
const width = normalizeViewportDimension(viewportSize.width);
|
|
1445
|
+
const height = normalizeViewportDimension(viewportSize.height);
|
|
1446
|
+
return width !== null && height !== null ? { width, height } : null;
|
|
1447
|
+
} finally {
|
|
1448
|
+
await cdp.detach().catch(() => void 0);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function normalizeViewportDimension(value) {
|
|
1452
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
const normalized = Math.floor(value);
|
|
1456
|
+
if (normalized < 100) {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
return Math.min(8192, normalized);
|
|
1460
|
+
}
|
|
1461
|
+
function readTextFrame(raw) {
|
|
1462
|
+
if (typeof raw === "string") {
|
|
1463
|
+
return raw;
|
|
1464
|
+
}
|
|
1465
|
+
if (raw instanceof ArrayBuffer) {
|
|
1466
|
+
return Buffer.from(raw).toString("utf8");
|
|
1467
|
+
}
|
|
1468
|
+
if (Array.isArray(raw)) {
|
|
1469
|
+
return Buffer.concat(raw).toString("utf8");
|
|
1470
|
+
}
|
|
1471
|
+
return raw.toString("utf8");
|
|
1472
|
+
}
|
|
1473
|
+
async function connectPlaywrightChromiumBrowser(input) {
|
|
1474
|
+
const { connectPlaywrightChromiumBrowser: connect } = await import('@opensteer/engine-playwright');
|
|
1475
|
+
return connect(input);
|
|
1476
|
+
}
|
|
1477
|
+
async function disconnectPlaywrightChromiumBrowser(browser) {
|
|
1478
|
+
const { disconnectPlaywrightChromiumBrowser: disconnect } = await import('@opensteer/engine-playwright');
|
|
1479
|
+
await disconnect(browser);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/local-view/server.ts
|
|
1483
|
+
var DEFAULT_MAX_FPS = 12;
|
|
1484
|
+
var DEFAULT_QUALITY = 75;
|
|
1485
|
+
var DEFAULT_MAX_CLIENT_BUFFER_BYTES = 512 * 1024;
|
|
1486
|
+
var LOCAL_VIEW_ACCESS_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
|
|
1487
|
+
async function startLocalViewServer(input = {}) {
|
|
1488
|
+
const token = input.token ?? randomBytes(24).toString("hex");
|
|
1489
|
+
const runtimeState = new LocalViewRuntimeState();
|
|
1490
|
+
const viewStreamHub = new LocalViewStreamHub({
|
|
1491
|
+
runtimeState,
|
|
1492
|
+
maxFps: DEFAULT_MAX_FPS,
|
|
1493
|
+
quality: DEFAULT_QUALITY,
|
|
1494
|
+
maxClientBufferBytes: DEFAULT_MAX_CLIENT_BUFFER_BYTES
|
|
1495
|
+
});
|
|
1496
|
+
const cdpProxy = new LocalViewCdpProxy({
|
|
1497
|
+
runtimeState
|
|
1498
|
+
});
|
|
1499
|
+
const httpServer = createServer((request, response) => {
|
|
1500
|
+
void handleHttpRequest({ request, response, token, shutdown: closeServer }).catch(() => {
|
|
1501
|
+
if (!response.headersSent && !response.writableEnded) {
|
|
1502
|
+
writeJson(response, 500, { error: "Internal server error." });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
response.destroy();
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
const viewWss = new LocalViewWebSocketServer({ noServer: true });
|
|
1509
|
+
viewWss.on("connection", (ws, request) => {
|
|
1510
|
+
const url2 = new URL(request.url ?? "/", "http://localhost");
|
|
1511
|
+
const parts = url2.pathname.split("/").filter(Boolean);
|
|
1512
|
+
const sessionId = parts[2];
|
|
1513
|
+
if (!sessionId) {
|
|
1514
|
+
ws.close(1008, "Session id is required.");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
viewStreamHub.attachClient(sessionId, ws);
|
|
1518
|
+
});
|
|
1519
|
+
httpServer.on("upgrade", (request, socket, head) => {
|
|
1520
|
+
const url2 = new URL(request.url ?? "/", "http://localhost");
|
|
1521
|
+
const tokenParam = url2.searchParams.get("token");
|
|
1522
|
+
if (tokenParam !== token || !isAllowedOrigin(request.headers.origin)) {
|
|
1523
|
+
socket.destroy();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const parts = url2.pathname.split("/").filter(Boolean);
|
|
1527
|
+
if (parts[0] !== "ws" || parts.length !== 3) {
|
|
1528
|
+
socket.destroy();
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (parts[1] === "view") {
|
|
1532
|
+
viewWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1533
|
+
viewWss.emit("connection", ws, request);
|
|
1534
|
+
});
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (parts[1] === "cdp") {
|
|
1538
|
+
cdpProxy.handleUpgrade(request, socket, head);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
socket.destroy();
|
|
1542
|
+
});
|
|
1543
|
+
let closePromise;
|
|
1544
|
+
async function closeServer() {
|
|
1545
|
+
closePromise ??= (async () => {
|
|
1546
|
+
viewWss.clients.forEach((client) => {
|
|
1547
|
+
try {
|
|
1548
|
+
client.close();
|
|
1549
|
+
} catch {
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
viewWss.close();
|
|
1553
|
+
cdpProxy.close();
|
|
1554
|
+
httpServer.close();
|
|
1555
|
+
await once(httpServer, "close");
|
|
1556
|
+
await clearLocalViewServiceState({ pid: process.pid, token });
|
|
1557
|
+
await input.onClosed?.();
|
|
1558
|
+
})();
|
|
1559
|
+
await closePromise;
|
|
1560
|
+
}
|
|
1561
|
+
httpServer.listen(input.port ?? 0, "127.0.0.1");
|
|
1562
|
+
await once(httpServer, "listening");
|
|
1563
|
+
const address = httpServer.address();
|
|
1564
|
+
if (!address || typeof address === "string") {
|
|
1565
|
+
throw new Error("Failed to resolve the local view server address.");
|
|
1566
|
+
}
|
|
1567
|
+
const url = `http://127.0.0.1:${String(address.port)}`;
|
|
1568
|
+
await writeLocalViewServiceState({
|
|
1569
|
+
layout: OPENSTEER_LOCAL_VIEW_SERVICE_LAYOUT,
|
|
1570
|
+
version: OPENSTEER_LOCAL_VIEW_SERVICE_VERSION,
|
|
1571
|
+
pid: process.pid,
|
|
1572
|
+
processStartedAtMs: CURRENT_PROCESS_OWNER.processStartedAtMs,
|
|
1573
|
+
startedAt: Date.now(),
|
|
1574
|
+
port: address.port,
|
|
1575
|
+
token,
|
|
1576
|
+
url
|
|
1577
|
+
});
|
|
1578
|
+
return {
|
|
1579
|
+
url,
|
|
1580
|
+
token,
|
|
1581
|
+
close: closeServer
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
async function handleHttpRequest(args) {
|
|
1585
|
+
const url = new URL(args.request.url ?? "/", "http://localhost");
|
|
1586
|
+
if (url.pathname === "/api/health") {
|
|
1587
|
+
if (!isAuthorizedApiRequest(args.request, args.token)) {
|
|
1588
|
+
writeJson(args.response, 401, { error: "Unauthorized." });
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
writeJson(args.response, 200, { ok: true });
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (url.pathname === "/api/sessions") {
|
|
1595
|
+
if (!isAuthorizedApiRequest(args.request, args.token)) {
|
|
1596
|
+
writeJson(args.response, 401, { error: "Unauthorized." });
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const sessions = await listResolvedLocalViewSessions();
|
|
1600
|
+
const payload = { sessions };
|
|
1601
|
+
writeJson(args.response, 200, payload);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (url.pathname === "/api/service/stop") {
|
|
1605
|
+
if (!isAuthorizedApiRequest(args.request, args.token)) {
|
|
1606
|
+
writeJson(args.response, 401, { error: "Unauthorized." });
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (args.request.method !== "POST") {
|
|
1610
|
+
writeJson(args.response, 405, { error: "Method not allowed." });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
args.response.once("finish", () => {
|
|
1614
|
+
void args.shutdown();
|
|
1615
|
+
});
|
|
1616
|
+
writeJson(args.response, 200, { stopped: true });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const accessMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/access$/u);
|
|
1620
|
+
if (accessMatch) {
|
|
1621
|
+
if (!isAuthorizedApiRequest(args.request, args.token)) {
|
|
1622
|
+
writeJson(args.response, 401, { error: "Unauthorized." });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const sessionId = decodeURIComponent(accessMatch[1]);
|
|
1626
|
+
if (!await resolveLocalViewSession(sessionId)) {
|
|
1627
|
+
writeJson(args.response, 404, { error: "Session not found." });
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const payload = {
|
|
1631
|
+
sessionId,
|
|
1632
|
+
expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT,
|
|
1633
|
+
grants: {
|
|
1634
|
+
view: {
|
|
1635
|
+
kind: "view",
|
|
1636
|
+
transport: "ws",
|
|
1637
|
+
url: `${resolveWsBaseUrl(args.request)}/ws/view/${encodeURIComponent(sessionId)}`,
|
|
1638
|
+
token: args.token,
|
|
1639
|
+
expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT
|
|
1640
|
+
},
|
|
1641
|
+
cdp: {
|
|
1642
|
+
kind: "cdp",
|
|
1643
|
+
transport: "ws",
|
|
1644
|
+
url: `${resolveWsBaseUrl(args.request)}/ws/cdp/${encodeURIComponent(sessionId)}`,
|
|
1645
|
+
token: args.token,
|
|
1646
|
+
expiresAt: LOCAL_VIEW_ACCESS_EXPIRES_AT
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
writeJson(args.response, 200, payload);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const closeMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/close$/u);
|
|
1654
|
+
if (closeMatch) {
|
|
1655
|
+
if (!isAuthorizedApiRequest(args.request, args.token)) {
|
|
1656
|
+
writeJson(args.response, 401, { error: "Unauthorized." });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (args.request.method !== "POST") {
|
|
1660
|
+
writeJson(args.response, 405, { error: "Method not allowed." });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const sessionId = decodeURIComponent(closeMatch[1]);
|
|
1664
|
+
const { closeLocalViewSessionBrowser, LocalViewSessionCloseError } = await import('./session-control-IFE3IPS3.js');
|
|
1665
|
+
try {
|
|
1666
|
+
await closeLocalViewSessionBrowser(sessionId);
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (error instanceof LocalViewSessionCloseError) {
|
|
1669
|
+
writeJson(args.response, error.statusCode, { error: error.message });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
throw error;
|
|
1673
|
+
}
|
|
1674
|
+
const payload = {
|
|
1675
|
+
sessionId,
|
|
1676
|
+
closed: true
|
|
1677
|
+
};
|
|
1678
|
+
writeJson(args.response, 200, payload);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (url.pathname === "/favicon.ico") {
|
|
1682
|
+
args.response.statusCode = 204;
|
|
1683
|
+
args.response.end();
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (url.pathname === "/" || url.pathname.startsWith("/assets/") || url.pathname.startsWith("/images/")) {
|
|
1687
|
+
await serveStaticAsset(args.response, url.pathname, args.token);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
args.response.statusCode = 404;
|
|
1691
|
+
args.response.end("not found");
|
|
1692
|
+
}
|
|
1693
|
+
async function serveStaticAsset(response, pathname, token) {
|
|
1694
|
+
const publicDir = resolveLocalViewPublicDir();
|
|
1695
|
+
const relativePath = pathname === "/" ? "index.html" : pathname.slice(1);
|
|
1696
|
+
const assetPath = path2.resolve(publicDir, relativePath);
|
|
1697
|
+
const relativeAssetPath = path2.relative(publicDir, assetPath);
|
|
1698
|
+
if (relativeAssetPath.startsWith("..") || path2.isAbsolute(relativeAssetPath) || !existsSync(assetPath)) {
|
|
1699
|
+
response.statusCode = 404;
|
|
1700
|
+
response.end("not found");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (relativePath === "index.html") {
|
|
1704
|
+
const html = await readFile(assetPath, "utf8");
|
|
1705
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
1706
|
+
response.setHeader("cache-control", "no-store");
|
|
1707
|
+
response.end(
|
|
1708
|
+
html.replace(
|
|
1709
|
+
"__OPENSTEER_LOCAL_BOOTSTRAP_JSON__",
|
|
1710
|
+
JSON.stringify({
|
|
1711
|
+
apiBasePath: "/api",
|
|
1712
|
+
token
|
|
1713
|
+
})
|
|
1714
|
+
)
|
|
1715
|
+
);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
response.setHeader("content-type", guessContentType(assetPath));
|
|
1719
|
+
response.setHeader("cache-control", "no-store");
|
|
1720
|
+
response.end(await readFile(assetPath));
|
|
1721
|
+
}
|
|
1722
|
+
function resolveLocalViewPublicDir() {
|
|
1723
|
+
const moduleDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
1724
|
+
const candidates = [
|
|
1725
|
+
path2.resolve(moduleDir, "local-view", "public"),
|
|
1726
|
+
path2.resolve(moduleDir, "public"),
|
|
1727
|
+
path2.resolve(moduleDir, "..", "local-view", "public")
|
|
1728
|
+
];
|
|
1729
|
+
for (const candidate of candidates) {
|
|
1730
|
+
if (existsSync(candidate)) {
|
|
1731
|
+
return candidate;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
throw new Error(`Could not resolve local view public assets from ${moduleDir}.`);
|
|
1735
|
+
}
|
|
1736
|
+
function isAuthorizedApiRequest(request, token) {
|
|
1737
|
+
return request.headers["x-opensteer-local-token"] === token && isAllowedOrigin(request.headers.origin);
|
|
1738
|
+
}
|
|
1739
|
+
function isAllowedOrigin(origin) {
|
|
1740
|
+
if (origin === void 0) {
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
try {
|
|
1744
|
+
const url = new URL(origin);
|
|
1745
|
+
const host = url.hostname;
|
|
1746
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1747
|
+
} catch {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
function resolveWsBaseUrl(request) {
|
|
1752
|
+
const host = request.headers.host ?? "127.0.0.1";
|
|
1753
|
+
return `ws://${host}`;
|
|
1754
|
+
}
|
|
1755
|
+
function writeJson(response, statusCode, value) {
|
|
1756
|
+
response.statusCode = statusCode;
|
|
1757
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
1758
|
+
response.end(JSON.stringify(value));
|
|
1759
|
+
}
|
|
1760
|
+
function guessContentType(assetPath) {
|
|
1761
|
+
if (assetPath.endsWith(".css")) {
|
|
1762
|
+
return "text/css; charset=utf-8";
|
|
1763
|
+
}
|
|
1764
|
+
if (assetPath.endsWith(".js")) {
|
|
1765
|
+
return "application/javascript; charset=utf-8";
|
|
1766
|
+
}
|
|
1767
|
+
if (assetPath.endsWith(".svg")) {
|
|
1768
|
+
return "image/svg+xml";
|
|
1769
|
+
}
|
|
1770
|
+
if (assetPath.endsWith(".json")) {
|
|
1771
|
+
return "application/json; charset=utf-8";
|
|
1772
|
+
}
|
|
1773
|
+
if (assetPath.endsWith(".png")) {
|
|
1774
|
+
return "image/png";
|
|
1775
|
+
}
|
|
1776
|
+
if (assetPath.endsWith(".ico")) {
|
|
1777
|
+
return "image/x-icon";
|
|
1778
|
+
}
|
|
1779
|
+
return "application/octet-stream";
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// src/local-view/serve.ts
|
|
1783
|
+
async function runLocalViewService() {
|
|
1784
|
+
const server = await startLocalViewServer({
|
|
1785
|
+
token: process.env.OPENSTEER_LOCAL_VIEW_BOOT_TOKEN ?? randomBytes(24).toString("hex"),
|
|
1786
|
+
onClosed: () => {
|
|
1787
|
+
process.exit(0);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
const handleShutdownSignal = () => {
|
|
1791
|
+
void server.close();
|
|
1792
|
+
};
|
|
1793
|
+
process.once("SIGINT", handleShutdownSignal);
|
|
1794
|
+
process.once("SIGTERM", handleShutdownSignal);
|
|
1795
|
+
await new Promise(() => void 0);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
export { runLocalViewService };
|
|
1799
|
+
//# sourceMappingURL=chunk-FIMNKEG5.js.map
|
|
1800
|
+
//# sourceMappingURL=chunk-FIMNKEG5.js.map
|