@rynx-ai/server 0.1.0 → 0.1.9

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.
Files changed (63) hide show
  1. package/dist/browser-surface-control-queue.d.ts +42 -0
  2. package/dist/browser-surface-control-queue.js +187 -0
  3. package/dist/browser-surface-ws.d.ts +14 -0
  4. package/dist/browser-surface-ws.js +355 -0
  5. package/dist/control-api.d.ts +59 -4
  6. package/dist/control-api.js +2213 -132
  7. package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
  8. package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
  9. package/dist/control-web/assets/index-ClpzuBJx.js +606 -0
  10. package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  18. package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  19. package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  20. package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  21. package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  22. package/dist/control-web/assets/mermaid-GHXKKRXX-DZn7Hbr2.js +131 -0
  23. package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
  24. package/dist/control-web/favicon.png +0 -0
  25. package/dist/control-web/favicon.svg +38 -0
  26. package/dist/control-web/index.html +16 -0
  27. package/dist/control-web-dist.d.ts +1 -1
  28. package/dist/control-web-dist.js +19 -14
  29. package/dist/desktop-browser-host.d.ts +153 -0
  30. package/dist/desktop-browser-host.js +936 -0
  31. package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
  32. package/dist/direct-runtime-browser-surface-server.js +821 -0
  33. package/dist/direct-runtime-server.d.ts +137 -0
  34. package/dist/direct-runtime-server.js +1931 -0
  35. package/dist/emulator-surface-ws.d.ts +6 -0
  36. package/dist/emulator-surface-ws.js +197 -0
  37. package/dist/machine-session-service.d.ts +182 -0
  38. package/dist/machine-session-service.js +559 -0
  39. package/dist/provider-cli-host.d.ts +10 -0
  40. package/dist/provider-cli-host.js +1 -0
  41. package/dist/remote-runtime-dispatcher.d.ts +94 -0
  42. package/dist/remote-runtime-dispatcher.js +380 -0
  43. package/dist/remote-runtime-session-projection.d.ts +19 -0
  44. package/dist/remote-runtime-session-projection.js +556 -0
  45. package/dist/remote-runtime.d.ts +22 -0
  46. package/dist/remote-runtime.js +32 -0
  47. package/dist/runtime-web-auth.d.ts +21 -0
  48. package/dist/runtime-web-auth.js +92 -0
  49. package/dist/server.d.ts +292 -10
  50. package/dist/server.js +314 -98
  51. package/dist/session-browser-service.d.ts +218 -0
  52. package/dist/session-browser-service.js +686 -0
  53. package/dist/session-browser-surface-coordinator.d.ts +23 -0
  54. package/dist/session-browser-surface-coordinator.js +563 -0
  55. package/dist/session-emulator-service.d.ts +167 -0
  56. package/dist/session-emulator-service.js +464 -0
  57. package/dist/session-runtime-index.d.ts +17 -0
  58. package/dist/session-runtime-index.js +86 -0
  59. package/dist/session-terminal-host.d.ts +46 -0
  60. package/dist/session-terminal-host.js +252 -0
  61. package/dist/terminal-ws.d.ts +9 -20
  62. package/dist/terminal-ws.js +420 -97
  63. package/package.json +9 -7
@@ -0,0 +1,686 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { RUNTIME_BROWSER_MAX_PAGES, parseRuntimeBrowserCloseParams, parseRuntimeBrowserOpenParams, parseRuntimeBrowserPageCreateParams, parseRuntimeBrowserPageMutationParams, parseRuntimeBrowserPageNavigateParams, parseRuntimeBrowserState, parseRuntimeBrowserStateGetParams, } from "@rynx-ai/protocol/runtime-browser";
3
+ import { parseRuntimeBrowserEndpointDescriptor, } from "@rynx-ai/protocol/runtime-browser-bootstrap";
4
+ const MAX_HOST_PAGE_ID_CHARS = 2_048;
5
+ const MAX_PAGE_ID_FACTORY_ATTEMPTS = 64;
6
+ const MAX_RETIRED_HOST_PAGE_IDS = 64;
7
+ export class SessionBrowserSurfaceUnsupportedError extends Error {
8
+ constructor() {
9
+ super("Browser Host does not support presentation");
10
+ this.name = "SessionBrowserSurfaceUnsupportedError";
11
+ }
12
+ }
13
+ /** Expected failure reported by a Browser Host implementation. */
14
+ export class SessionBrowserHostError extends Error {
15
+ code;
16
+ constructor(code, message, options) {
17
+ super(message, options);
18
+ this.code = code;
19
+ this.name = "SessionBrowserHostError";
20
+ }
21
+ }
22
+ /** Stable application error; transports map the code without exposing Host diagnostics. */
23
+ export class SessionBrowserServiceError extends Error {
24
+ code;
25
+ constructor(code, message, options) {
26
+ super(message, options);
27
+ this.code = code;
28
+ this.name = "SessionBrowserServiceError";
29
+ }
30
+ }
31
+ /**
32
+ * Transport-neutral owner of the Runtime -> Session -> Browser -> Page model.
33
+ * Every operation for one Session shares one queue; different Sessions remain independent.
34
+ */
35
+ export class SessionBrowserService {
36
+ ports;
37
+ records = new Map();
38
+ generations = new Map();
39
+ queues = new Map();
40
+ deletingSessions = new Set();
41
+ observers = new Set();
42
+ pageIdFactory;
43
+ bootGenerationSeed;
44
+ shuttingDown = false;
45
+ closeAllPromise;
46
+ constructor(ports, options = {}) {
47
+ this.ports = ports;
48
+ this.pageIdFactory = options.pageIdFactory ?? (() => `page_${randomUUID()}`);
49
+ this.bootGenerationSeed = options.bootGenerationSeed ?? randomBootGenerationSeed();
50
+ if (!Number.isSafeInteger(this.bootGenerationSeed) ||
51
+ this.bootGenerationSeed < 0 ||
52
+ this.bootGenerationSeed >= Number.MAX_SAFE_INTEGER) {
53
+ throw new Error("Browser boot generation seed must be a nonnegative safe integer");
54
+ }
55
+ }
56
+ async getState(input) {
57
+ const params = parseInput(() => parseRuntimeBrowserStateGetParams(input), "Browser state request is invalid");
58
+ return this.exclusive(params.sessionId, async () => {
59
+ this.ensureRunning();
60
+ await this.ensureSessionExists(params.sessionId);
61
+ const record = this.records.get(params.sessionId);
62
+ if (!record)
63
+ return null;
64
+ if (record.status === "ready")
65
+ await this.reconcileForRead(record);
66
+ return this.project(record);
67
+ });
68
+ }
69
+ /**
70
+ * Runtime-local automation handoff. This method is deliberately absent from
71
+ * RemoteRuntimeBrowserHost and Direct RPC so a CDP endpoint cannot cross a
72
+ * Runtime boundary.
73
+ */
74
+ async getRuntimeLocalEndpoint(sessionId) {
75
+ const parsedSessionId = parseInput(() => parseRuntimeBrowserStateGetParams({ sessionId }).sessionId, "Browser endpoint request is invalid");
76
+ return this.exclusive(parsedSessionId, async () => {
77
+ this.ensureRunning();
78
+ await this.ensureSessionExists(parsedSessionId);
79
+ const record = this.requireRecord(parsedSessionId);
80
+ if (record.status !== "ready") {
81
+ throw new SessionBrowserServiceError("unavailable", "Browser is not available");
82
+ }
83
+ try {
84
+ const endpoint = await record.handle.getCdpEndpoint();
85
+ return parseRuntimeBrowserEndpointDescriptor({
86
+ sessionId: record.sessionId,
87
+ browserGeneration: record.browserGeneration,
88
+ executionBackend: record.executionBackend,
89
+ endpoint: endpoint.endpoint,
90
+ engineVersion: endpoint.engineVersion,
91
+ });
92
+ }
93
+ catch (error) {
94
+ record.status = "unavailable";
95
+ throw mapHostError(error, "Browser automation endpoint is unavailable");
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Resolve one opaque Page to its generation-local Host source. This is the
101
+ * only presentation path allowed to cross the semantic/physical Page seam.
102
+ */
103
+ async validateSurfaceTarget(input) {
104
+ const params = parseInput(() => parseRuntimeBrowserPageMutationParams(input), "Browser Surface request is invalid");
105
+ return this.exclusive(params.sessionId, async () => {
106
+ this.ensureRunning();
107
+ await this.ensureSessionExists(params.sessionId);
108
+ const record = this.requireReadyRecord(params.sessionId, params.browserGeneration);
109
+ await this.reconcileForMutation(record);
110
+ if (!record.pagesById.has(params.pageId)) {
111
+ throw new SessionBrowserServiceError("not_found", "Browser Page was not found");
112
+ }
113
+ if (!record.handle.openSurface)
114
+ throw new SessionBrowserSurfaceUnsupportedError();
115
+ });
116
+ }
117
+ async openSurface(input) {
118
+ const params = parseInput(() => parseRuntimeBrowserPageMutationParams({
119
+ sessionId: input.sessionId,
120
+ browserGeneration: input.browserGeneration,
121
+ pageId: input.pageId,
122
+ }), "Browser Surface request is invalid");
123
+ return this.exclusive(params.sessionId, async () => {
124
+ this.ensureRunning();
125
+ await this.ensureSessionExists(params.sessionId);
126
+ const record = this.requireReadyRecord(params.sessionId, params.browserGeneration);
127
+ await this.reconcileForMutation(record);
128
+ const page = record.pagesById.get(params.pageId);
129
+ if (!page) {
130
+ throw new SessionBrowserServiceError("not_found", "Browser Page was not found");
131
+ }
132
+ if (!record.handle.openSurface)
133
+ throw new SessionBrowserSurfaceUnsupportedError();
134
+ try {
135
+ return await record.handle.openSurface({
136
+ browserGeneration: params.browserGeneration,
137
+ hostPageId: page.hostPageId,
138
+ format: input.format,
139
+ quality: input.quality,
140
+ maximumFrameRate: input.maximumFrameRate,
141
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
142
+ });
143
+ }
144
+ catch (error) {
145
+ throw mapHostError(error, "Browser Surface could not open");
146
+ }
147
+ });
148
+ }
149
+ async open(input) {
150
+ const params = parseInput(() => parseRuntimeBrowserOpenParams(input), "Browser open request is invalid");
151
+ return this.exclusive(params.sessionId, async () => {
152
+ this.ensureRunning();
153
+ await this.ensureSessionExists(params.sessionId);
154
+ const existing = this.records.get(params.sessionId);
155
+ if (existing?.status === "ready" || existing?.status === "opening") {
156
+ if (existing.status === "ready")
157
+ await this.reconcileForRead(existing);
158
+ return this.project(existing);
159
+ }
160
+ if (existing)
161
+ await this.closeUnavailableForRecreate(existing);
162
+ return this.createBrowser(params);
163
+ });
164
+ }
165
+ async close(input) {
166
+ const params = parseInput(() => parseRuntimeBrowserCloseParams(input), "Browser close request is invalid");
167
+ return this.exclusive(params.sessionId, async () => {
168
+ this.ensureRunning();
169
+ await this.ensureSessionExists(params.sessionId);
170
+ const record = this.requireRecord(params.sessionId);
171
+ this.requireGeneration(record, params.browserGeneration);
172
+ record.status = "closing";
173
+ try {
174
+ await record.handle.close();
175
+ }
176
+ catch (error) {
177
+ record.status = "unavailable";
178
+ throw new SessionBrowserServiceError("outcome_unknown", "Browser close outcome is unknown", { cause: error });
179
+ }
180
+ if (this.records.get(params.sessionId) === record)
181
+ this.records.delete(params.sessionId);
182
+ return { closed: true, browserGeneration: params.browserGeneration };
183
+ });
184
+ }
185
+ async createPage(input) {
186
+ const params = parseInput(() => parseRuntimeBrowserPageCreateParams(input), "Browser page creation request is invalid");
187
+ return this.exclusive(params.sessionId, async () => {
188
+ this.ensureRunning();
189
+ await this.ensureSessionExists(params.sessionId);
190
+ const record = this.requireReadyRecord(params.sessionId, params.browserGeneration);
191
+ await this.reconcileForMutation(record);
192
+ if (record.pagesById.size >= RUNTIME_BROWSER_MAX_PAGES) {
193
+ throw new SessionBrowserServiceError("capacity", `Browser already contains the maximum of ${RUNTIME_BROWSER_MAX_PAGES} Pages`);
194
+ }
195
+ return this.runMutation(record, () => record.handle.createPage(params.url));
196
+ });
197
+ }
198
+ closePage(input) {
199
+ return this.pageMutation(input, "Browser page close request is invalid", (record, hostPageId) => record.handle.closePage(hostPageId));
200
+ }
201
+ activatePage(input) {
202
+ return this.pageMutation(input, "Browser page activation request is invalid", (record, hostPageId) => record.handle.activatePage(hostPageId));
203
+ }
204
+ async navigatePage(input) {
205
+ const params = parseInput(() => parseRuntimeBrowserPageNavigateParams(input), "Browser page navigation request is invalid");
206
+ return this.pageMutationParsed(params, (record, hostPageId) => record.handle.navigatePage(hostPageId, params.url));
207
+ }
208
+ goBack(input) {
209
+ return this.pageMutation(input, "Browser page back request is invalid", (record, hostPageId) => record.handle.goBack(hostPageId));
210
+ }
211
+ goForward(input) {
212
+ return this.pageMutation(input, "Browser page forward request is invalid", (record, hostPageId) => record.handle.goForward(hostPageId));
213
+ }
214
+ reload(input) {
215
+ return this.pageMutation(input, "Browser page reload request is invalid", (record, hostPageId) => record.handle.reload(hostPageId));
216
+ }
217
+ /** Session deletion fence: no later Browser operation may enter for this Session ID. */
218
+ async cleanupSession(sessionId) {
219
+ const parsed = parseInput(() => parseRuntimeBrowserStateGetParams({ sessionId }).sessionId, "Browser Session cleanup request is invalid");
220
+ this.deletingSessions.add(parsed);
221
+ await this.exclusive(parsed, async () => {
222
+ const failures = [];
223
+ const record = this.records.get(parsed);
224
+ if (record) {
225
+ record.status = "closing";
226
+ this.records.delete(parsed);
227
+ try {
228
+ await record.handle.close();
229
+ }
230
+ catch (error) {
231
+ failures.push(error);
232
+ }
233
+ }
234
+ try {
235
+ await this.ports.host.deleteSessionData?.(parsed);
236
+ }
237
+ catch (error) {
238
+ failures.push(error);
239
+ }
240
+ this.generations.delete(parsed);
241
+ if (failures.length > 0) {
242
+ throw new SessionBrowserServiceError("host_failure", "Browser Session cleanup did not complete", { cause: failures[0] });
243
+ }
244
+ }, true);
245
+ }
246
+ /** Daemon shutdown closes engines but intentionally preserves Session profile data. */
247
+ closeAll() {
248
+ if (this.closeAllPromise)
249
+ return this.closeAllPromise;
250
+ this.shuttingDown = true;
251
+ this.closeAllPromise = this.closeEveryRecord();
252
+ return this.closeAllPromise;
253
+ }
254
+ async closeEveryRecord() {
255
+ const failures = [];
256
+ // Operations admitted before the shutdown fence may still be creating a
257
+ // physical Browser. Drain them before snapshotting records to close.
258
+ await Promise.allSettled([...this.queues.values()]);
259
+ while (this.records.size > 0) {
260
+ const sessionIds = [...this.records.keys()];
261
+ await Promise.all(sessionIds.map((sessionId) => this.exclusive(sessionId, async () => {
262
+ const record = this.records.get(sessionId);
263
+ if (!record)
264
+ return;
265
+ record.status = "closing";
266
+ this.records.delete(sessionId);
267
+ try {
268
+ await record.handle.close();
269
+ }
270
+ catch (error) {
271
+ failures.push(error);
272
+ }
273
+ }, true)));
274
+ }
275
+ await Promise.allSettled([...this.observers]);
276
+ if (failures.length > 0) {
277
+ throw new SessionBrowserServiceError("host_failure", "One or more Browsers did not close during shutdown", { cause: failures[0] });
278
+ }
279
+ }
280
+ async createBrowser(params) {
281
+ const previousGeneration = this.generations.get(params.sessionId)
282
+ ?? this.bootGenerationSeed;
283
+ if (previousGeneration >= Number.MAX_SAFE_INTEGER) {
284
+ throw new SessionBrowserServiceError("capacity", "Browser generation space is exhausted");
285
+ }
286
+ const browserGeneration = previousGeneration + 1;
287
+ this.generations.set(params.sessionId, browserGeneration);
288
+ let handle;
289
+ try {
290
+ handle = await this.ports.host.createBrowser({
291
+ sessionId: params.sessionId,
292
+ browserGeneration,
293
+ ...(params.url === undefined ? {} : { url: params.url }),
294
+ });
295
+ }
296
+ catch (error) {
297
+ throw mapHostError(error, "Browser Host could not create the Browser");
298
+ }
299
+ if (handle.browserGeneration !== browserGeneration) {
300
+ await handle.close().catch(() => undefined);
301
+ throw new SessionBrowserServiceError("host_failure", "Browser Host returned the wrong generation");
302
+ }
303
+ const record = {
304
+ sessionId: params.sessionId,
305
+ browserGeneration,
306
+ status: "opening",
307
+ executionBackend: handle.executionBackend,
308
+ capabilities: [...handle.capabilities],
309
+ handle,
310
+ pagesById: new Map(),
311
+ pageIdByHostId: new Map(),
312
+ retiredPageIdByHostId: new Map(),
313
+ boundPageIdByHostId: new Map(),
314
+ pageOrder: [],
315
+ activePageId: null,
316
+ };
317
+ if (this.shuttingDown) {
318
+ record.status = "closing";
319
+ this.records.set(params.sessionId, record);
320
+ try {
321
+ await handle.close();
322
+ if (this.records.get(params.sessionId) === record)
323
+ this.records.delete(params.sessionId);
324
+ throw new SessionBrowserServiceError("shutting_down", "Browser service is shutting down");
325
+ }
326
+ catch (error) {
327
+ if (error instanceof SessionBrowserServiceError)
328
+ throw error;
329
+ record.status = "unavailable";
330
+ throw new SessionBrowserServiceError("host_failure", "Browser created during shutdown could not be closed", { cause: error });
331
+ }
332
+ }
333
+ this.records.set(params.sessionId, record);
334
+ try {
335
+ await this.reconcile(record);
336
+ record.status = "ready";
337
+ const state = this.project(record);
338
+ this.observe(record);
339
+ return state;
340
+ }
341
+ catch (error) {
342
+ record.status = "unavailable";
343
+ try {
344
+ await handle.close();
345
+ if (this.records.get(params.sessionId) === record)
346
+ this.records.delete(params.sessionId);
347
+ }
348
+ catch (closeError) {
349
+ this.observe(record);
350
+ throw new SessionBrowserServiceError("host_failure", "Browser Host state failed to initialize and could not be closed", { cause: new AggregateError([error, closeError]) });
351
+ }
352
+ throw new SessionBrowserServiceError("host_failure", "Browser Host state could not be initialized", { cause: error });
353
+ }
354
+ }
355
+ async closeUnavailableForRecreate(record) {
356
+ record.status = "closing";
357
+ try {
358
+ await record.handle.close();
359
+ }
360
+ catch (error) {
361
+ record.status = "unavailable";
362
+ throw new SessionBrowserServiceError("host_failure", "Unavailable Browser could not be closed before recreation", { cause: error });
363
+ }
364
+ if (this.records.get(record.sessionId) === record)
365
+ this.records.delete(record.sessionId);
366
+ }
367
+ pageMutation(input, invalidMessage, operation) {
368
+ const params = parseInput(() => parseRuntimeBrowserPageMutationParams(input), invalidMessage);
369
+ return this.pageMutationParsed(params, operation);
370
+ }
371
+ pageMutationParsed(params, operation) {
372
+ return this.exclusive(params.sessionId, async () => {
373
+ this.ensureRunning();
374
+ await this.ensureSessionExists(params.sessionId);
375
+ const record = this.requireReadyRecord(params.sessionId, params.browserGeneration);
376
+ await this.reconcileForMutation(record);
377
+ const page = record.pagesById.get(params.pageId);
378
+ if (!page) {
379
+ throw new SessionBrowserServiceError("not_found", "Browser Page was not found");
380
+ }
381
+ return this.runMutation(record, () => operation(record, page.hostPageId));
382
+ });
383
+ }
384
+ async runMutation(record, operation) {
385
+ try {
386
+ await operation();
387
+ }
388
+ catch (error) {
389
+ if (error instanceof SessionBrowserHostError &&
390
+ (error.code === "unavailable" || error.code === "outcome_unknown")) {
391
+ record.status = "unavailable";
392
+ }
393
+ if (error instanceof SessionBrowserHostError && error.code === "page_not_found") {
394
+ await this.tryReconcileAfterRejectedMutation(record);
395
+ }
396
+ throw mapHostError(error, "Browser Host rejected the operation");
397
+ }
398
+ try {
399
+ await this.reconcile(record);
400
+ return this.project(record);
401
+ }
402
+ catch (error) {
403
+ record.status = "unavailable";
404
+ throw new SessionBrowserServiceError("outcome_unknown", "Browser operation completed but its outcome could not be reconciled", { cause: error });
405
+ }
406
+ }
407
+ async reconcileForRead(record) {
408
+ try {
409
+ await this.reconcile(record);
410
+ }
411
+ catch (error) {
412
+ record.status = "unavailable";
413
+ throw new SessionBrowserServiceError("host_failure", "Browser Host state could not be reconciled", { cause: error });
414
+ }
415
+ }
416
+ async reconcileForMutation(record) {
417
+ try {
418
+ await this.reconcile(record);
419
+ }
420
+ catch (error) {
421
+ record.status = "unavailable";
422
+ throw new SessionBrowserServiceError("unavailable", "Browser became unavailable while reconciling Host state", { cause: error });
423
+ }
424
+ }
425
+ async tryReconcileAfterRejectedMutation(record) {
426
+ try {
427
+ await this.reconcile(record);
428
+ }
429
+ catch {
430
+ record.status = "unavailable";
431
+ }
432
+ }
433
+ async reconcile(record) {
434
+ const snapshot = await record.handle.snapshot();
435
+ if (!Array.isArray(snapshot.pages) || snapshot.pages.length > RUNTIME_BROWSER_MAX_PAGES) {
436
+ throw new Error("Browser Host returned too many Pages");
437
+ }
438
+ const nextPagesById = new Map();
439
+ const nextPageIdByHostId = new Map();
440
+ const nextPageOrder = [];
441
+ for (const hostPage of snapshot.pages) {
442
+ validateHostPageId(hostPage.hostPageId);
443
+ if (nextPageIdByHostId.has(hostPage.hostPageId)) {
444
+ throw new Error("Browser Host returned duplicate Page identity");
445
+ }
446
+ const pageId = record.pageIdByHostId.get(hostPage.hostPageId)
447
+ ?? record.retiredPageIdByHostId.get(hostPage.hostPageId)
448
+ ?? this.createPageId(record, nextPagesById);
449
+ nextPageIdByHostId.set(hostPage.hostPageId, pageId);
450
+ nextPageOrder.push(pageId);
451
+ nextPagesById.set(pageId, {
452
+ pageId,
453
+ hostPageId: hostPage.hostPageId,
454
+ url: hostPage.url,
455
+ title: hostPage.title,
456
+ loading: hostPage.loading,
457
+ canGoBack: hostPage.canGoBack,
458
+ canGoForward: hostPage.canGoForward,
459
+ });
460
+ }
461
+ if (snapshot.activeHostPageId !== null &&
462
+ !nextPageIdByHostId.has(snapshot.activeHostPageId)) {
463
+ throw new Error("Browser Host active Page is absent from its snapshot");
464
+ }
465
+ const activePageId = snapshot.activeHostPageId === null
466
+ ? null
467
+ : nextPageIdByHostId.get(snapshot.activeHostPageId);
468
+ const parsed = parseRuntimeBrowserState({
469
+ sessionId: record.sessionId,
470
+ browserGeneration: record.browserGeneration,
471
+ status: record.status,
472
+ executionBackend: record.executionBackend,
473
+ activePageId,
474
+ pages: nextPageOrder.map((pageId) => nextPagesById.get(pageId)),
475
+ capabilities: record.capabilities,
476
+ });
477
+ if (!parsed)
478
+ throw new Error("Browser Host returned no state");
479
+ const nextBoundPageIdByHostId = new Map();
480
+ if (record.handle.bindPage) {
481
+ for (const [hostPageId, pageId] of nextPageIdByHostId) {
482
+ if (record.boundPageIdByHostId.get(hostPageId) !== pageId) {
483
+ await record.handle.bindPage(hostPageId, pageId);
484
+ }
485
+ nextBoundPageIdByHostId.set(hostPageId, pageId);
486
+ }
487
+ }
488
+ for (const [hostPageId, pageId] of record.pageIdByHostId) {
489
+ if (!nextPageIdByHostId.has(hostPageId)) {
490
+ record.retiredPageIdByHostId.set(hostPageId, pageId);
491
+ }
492
+ }
493
+ for (const hostPageId of nextPageIdByHostId.keys()) {
494
+ record.retiredPageIdByHostId.delete(hostPageId);
495
+ }
496
+ while (record.retiredPageIdByHostId.size > MAX_RETIRED_HOST_PAGE_IDS) {
497
+ const oldest = record.retiredPageIdByHostId.keys().next().value;
498
+ if (oldest === undefined)
499
+ break;
500
+ record.retiredPageIdByHostId.delete(oldest);
501
+ }
502
+ record.pagesById = new Map(parsed.pages.map((page) => [
503
+ page.pageId,
504
+ { ...page, hostPageId: nextPagesById.get(page.pageId).hostPageId },
505
+ ]));
506
+ record.pageIdByHostId = nextPageIdByHostId;
507
+ record.boundPageIdByHostId = nextBoundPageIdByHostId;
508
+ record.pageOrder = parsed.pages.map((page) => page.pageId);
509
+ record.activePageId = parsed.activePageId;
510
+ record.capabilities = parsed.capabilities;
511
+ }
512
+ createPageId(record, pending) {
513
+ for (let attempt = 0; attempt < MAX_PAGE_ID_FACTORY_ATTEMPTS; attempt += 1) {
514
+ const pageId = this.pageIdFactory();
515
+ if (!record.pagesById.has(pageId) &&
516
+ !pending.has(pageId) &&
517
+ ![...record.retiredPageIdByHostId.values()].includes(pageId))
518
+ return pageId;
519
+ }
520
+ throw new Error("Browser Page identity factory did not produce a unique ID");
521
+ }
522
+ project(record) {
523
+ const parsed = parseRuntimeBrowserState({
524
+ sessionId: record.sessionId,
525
+ browserGeneration: record.browserGeneration,
526
+ status: record.status,
527
+ executionBackend: record.executionBackend,
528
+ activePageId: record.activePageId,
529
+ pages: record.pageOrder.map((pageId) => record.pagesById.get(pageId)),
530
+ capabilities: record.capabilities,
531
+ });
532
+ if (!parsed)
533
+ throw new Error("Browser record unexpectedly projected to absence");
534
+ return parsed;
535
+ }
536
+ observe(record) {
537
+ const observer = this.consumeEvents(record).finally(() => this.observers.delete(observer));
538
+ this.observers.add(observer);
539
+ }
540
+ async consumeEvents(record) {
541
+ let failed;
542
+ try {
543
+ for await (const event of record.handle.events) {
544
+ await this.exclusive(record.sessionId, async () => {
545
+ if (!this.isCurrent(record, event.browserGeneration))
546
+ return;
547
+ if (event.type === "unavailable") {
548
+ record.status = "unavailable";
549
+ return;
550
+ }
551
+ if (record.status !== "ready")
552
+ return;
553
+ if (event.type === "page.replaced")
554
+ this.applyReplacement(record, event);
555
+ try {
556
+ await this.reconcile(record);
557
+ }
558
+ catch {
559
+ record.status = "unavailable";
560
+ }
561
+ }, true);
562
+ }
563
+ }
564
+ catch (error) {
565
+ failed = error;
566
+ }
567
+ await this.exclusive(record.sessionId, async () => {
568
+ if (!this.isCurrent(record, record.browserGeneration))
569
+ return;
570
+ if (record.status === "ready" || failed !== undefined)
571
+ record.status = "unavailable";
572
+ }, true);
573
+ }
574
+ applyReplacement(record, event) {
575
+ validateHostPageId(event.previousHostPageId);
576
+ validateHostPageId(event.page.hostPageId);
577
+ const preservedPageId = record.pageIdByHostId.get(event.previousHostPageId)
578
+ ?? record.retiredPageIdByHostId.get(event.previousHostPageId);
579
+ if (!preservedPageId)
580
+ return;
581
+ const conflictingPageId = record.pageIdByHostId.get(event.page.hostPageId);
582
+ if (conflictingPageId && conflictingPageId !== preservedPageId) {
583
+ record.pagesById.delete(conflictingPageId);
584
+ record.pageOrder = record.pageOrder.filter((pageId) => pageId !== conflictingPageId);
585
+ }
586
+ record.pageIdByHostId.delete(event.previousHostPageId);
587
+ record.retiredPageIdByHostId.delete(event.previousHostPageId);
588
+ record.retiredPageIdByHostId.delete(event.page.hostPageId);
589
+ record.pageIdByHostId.set(event.page.hostPageId, preservedPageId);
590
+ const page = record.pagesById.get(preservedPageId);
591
+ if (page)
592
+ page.hostPageId = event.page.hostPageId;
593
+ }
594
+ isCurrent(record, generation) {
595
+ return this.records.get(record.sessionId) === record &&
596
+ record.browserGeneration === generation;
597
+ }
598
+ requireRecord(sessionId) {
599
+ const record = this.records.get(sessionId);
600
+ if (!record)
601
+ throw new SessionBrowserServiceError("not_found", "Browser was not found");
602
+ return record;
603
+ }
604
+ requireReadyRecord(sessionId, generation) {
605
+ const record = this.requireRecord(sessionId);
606
+ this.requireGeneration(record, generation);
607
+ if (record.status !== "ready") {
608
+ throw new SessionBrowserServiceError("unavailable", "Browser is not available");
609
+ }
610
+ return record;
611
+ }
612
+ requireGeneration(record, generation) {
613
+ if (record.browserGeneration !== generation) {
614
+ throw new SessionBrowserServiceError("generation_mismatch", "Browser generation is stale");
615
+ }
616
+ }
617
+ async ensureSessionExists(sessionId) {
618
+ if (this.deletingSessions.has(sessionId) ||
619
+ (this.ports.sessions && !(await this.ports.sessions.exists(sessionId)))) {
620
+ throw new SessionBrowserServiceError("not_found", "Session was not found");
621
+ }
622
+ }
623
+ ensureRunning() {
624
+ if (this.shuttingDown) {
625
+ throw new SessionBrowserServiceError("shutting_down", "Browser service is shutting down");
626
+ }
627
+ }
628
+ exclusive(sessionId, operation, allowShutdown = false) {
629
+ if (this.shuttingDown && !allowShutdown) {
630
+ return Promise.reject(new SessionBrowserServiceError("shutting_down", "Browser service is shutting down"));
631
+ }
632
+ const previous = this.queues.get(sessionId) ?? Promise.resolve();
633
+ const result = previous.then(operation, operation);
634
+ const completion = result.then(() => undefined, () => undefined);
635
+ this.queues.set(sessionId, completion);
636
+ void completion.finally(() => {
637
+ if (this.queues.get(sessionId) === completion)
638
+ this.queues.delete(sessionId);
639
+ });
640
+ return result;
641
+ }
642
+ }
643
+ function parseInput(parse, message) {
644
+ try {
645
+ return parse();
646
+ }
647
+ catch (error) {
648
+ throw new SessionBrowserServiceError("invalid_request", message, { cause: error });
649
+ }
650
+ }
651
+ function mapHostError(error, fallbackMessage) {
652
+ if (error instanceof SessionBrowserServiceError)
653
+ return error;
654
+ if (error instanceof SessionBrowserHostError) {
655
+ if (error.code === "page_not_found") {
656
+ return new SessionBrowserServiceError("not_found", "Browser Page was not found", {
657
+ cause: error,
658
+ });
659
+ }
660
+ if (error.code === "capacity") {
661
+ return new SessionBrowserServiceError("capacity", "Browser Host capacity was reached", {
662
+ cause: error,
663
+ });
664
+ }
665
+ if (error.code === "outcome_unknown") {
666
+ return new SessionBrowserServiceError("outcome_unknown", "Browser Host operation outcome is unknown", { cause: error });
667
+ }
668
+ return new SessionBrowserServiceError("unavailable", "Browser Host is unavailable", {
669
+ cause: error,
670
+ });
671
+ }
672
+ return new SessionBrowserServiceError("host_failure", fallbackMessage, { cause: error });
673
+ }
674
+ function validateHostPageId(value) {
675
+ if (typeof value !== "string" ||
676
+ value.length === 0 ||
677
+ value.length > MAX_HOST_PAGE_ID_CHARS ||
678
+ /[\u0000-\u001f\u007f]/.test(value)) {
679
+ throw new Error("Browser Host Page identity is invalid");
680
+ }
681
+ }
682
+ function randomBootGenerationSeed() {
683
+ // 48 random bits remain exactly representable as a JavaScript integer and
684
+ // leave ample monotonic headroom for generations within one daemon boot.
685
+ return randomBytes(6).readUIntBE(0, 6);
686
+ }