effect-browser 0.2.0-beta.6 → 0.2.0-beta.8

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 (68) hide show
  1. package/README.md +507 -130
  2. package/dist/Browser-n-bKgBNN.d.mts +372 -0
  3. package/dist/Browser.d.mts +3 -189
  4. package/dist/Browser.mjs +9 -1
  5. package/dist/Browser.mjs.map +1 -1
  6. package/dist/{BrowserData-DPh3Oaof.d.mts → BrowserData-CPIIIHol.d.mts} +84 -10
  7. package/dist/BrowserData.d.mts +2 -2
  8. package/dist/BrowserData.mjs +106 -7
  9. package/dist/BrowserData.mjs.map +1 -1
  10. package/dist/{BrowserRuntime-BBAjpVFa.d.mts → BrowserRuntime-CFe9uUTv.d.mts} +16 -11
  11. package/dist/{BrowserRuntime-DImn_Oe_.mjs → BrowserRuntime-HJ1yYlFS.mjs} +4977 -4134
  12. package/dist/BrowserRuntime-HJ1yYlFS.mjs.map +1 -0
  13. package/dist/BrowserRuntime.d.mts +2 -2
  14. package/dist/BrowserRuntime.mjs +2 -1
  15. package/dist/Capture.d.mts +9 -7
  16. package/dist/Capture.mjs +157 -32
  17. package/dist/Capture.mjs.map +1 -1
  18. package/dist/{CaptureData-Dys0Ko9d.mjs → CaptureData-CYriZHX1.mjs} +25 -6
  19. package/dist/CaptureData-CYriZHX1.mjs.map +1 -0
  20. package/dist/CaptureData.d.mts +62 -3
  21. package/dist/CaptureData.mjs +2 -2
  22. package/dist/Chromium.d.mts +2 -2
  23. package/dist/Chromium.mjs +19 -10
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Errors.d.mts +69 -17
  26. package/dist/Errors.mjs +57 -6
  27. package/dist/Errors.mjs.map +1 -1
  28. package/dist/Images-CaaPNeed.mjs +76 -0
  29. package/dist/Images-CaaPNeed.mjs.map +1 -0
  30. package/dist/OperationOptions-DAeRS6O9.mjs +55 -0
  31. package/dist/OperationOptions-DAeRS6O9.mjs.map +1 -0
  32. package/dist/PageControl.d.mts +6 -6
  33. package/dist/PageControl.mjs +16 -10
  34. package/dist/PageControl.mjs.map +1 -1
  35. package/dist/PageRegistry-B3q9DS3g.mjs +164 -0
  36. package/dist/PageRegistry-B3q9DS3g.mjs.map +1 -0
  37. package/dist/Plan-D8qQ48Ic.mjs +512 -0
  38. package/dist/Plan-D8qQ48Ic.mjs.map +1 -0
  39. package/dist/Plan.d.mts +3 -0
  40. package/dist/Plan.mjs +2 -0
  41. package/dist/PlanData-B668RxbQ.d.mts +4831 -0
  42. package/dist/PlanData.d.mts +3 -0
  43. package/dist/PlanData.mjs +268 -0
  44. package/dist/PlanData.mjs.map +1 -0
  45. package/dist/PlanInput-BJcPPncs.mjs +75 -0
  46. package/dist/PlanInput-BJcPPncs.mjs.map +1 -0
  47. package/dist/PublicSession-Bs61Eal8.mjs +5601 -0
  48. package/dist/PublicSession-Bs61Eal8.mjs.map +1 -0
  49. package/dist/Testing.d.mts +48 -6
  50. package/dist/Testing.mjs +675 -123
  51. package/dist/Testing.mjs.map +1 -1
  52. package/dist/Timeline-fV22Xk-S.d.mts +29 -0
  53. package/dist/Timeline.d.mts +4 -0
  54. package/dist/Timeline.mjs +20 -0
  55. package/dist/Timeline.mjs.map +1 -0
  56. package/dist/TimelineData-BsOn-YGZ.d.mts +3818 -0
  57. package/dist/TimelineData.d.mts +3 -0
  58. package/dist/TimelineData.mjs +482 -0
  59. package/dist/TimelineData.mjs.map +1 -0
  60. package/dist/index.d.mts +5 -1
  61. package/dist/index.mjs +5 -1
  62. package/package.json +17 -1
  63. package/dist/Association-BsSfBYVx.mjs +0 -138
  64. package/dist/Association-BsSfBYVx.mjs.map +0 -1
  65. package/dist/BrowserRuntime-DImn_Oe_.mjs.map +0 -1
  66. package/dist/CaptureData-Dys0Ko9d.mjs.map +0 -1
  67. package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
  68. package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
package/dist/Testing.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
2
- import { BrowserPolicy, ControlFacts, FrameInfo, Identifier, ObservedControl, PageExecutionState, PageInfo, PageSuspension, SafeFilename, TargetUrl, ViewportEvidence, ViewportRect } from "./BrowserData.mjs";
3
- import { a as cleanupStep, c as checked, l as fromNativeAttempt, n as make, s as reported, u as issueBinding } from "./BrowserRuntime-DImn_Oe_.mjs";
2
+ import { BrowserPolicy, ControlFacts, DocumentScroll, FrameInfo, Identifier, ObservedControl, ObservedElement, PageExecutionState, PageInfo, PageSuspension, SafeFilename, TargetUrl, ViewportEvidence, ViewportRect } from "./BrowserData.mjs";
3
+ import { a as stableIdentityOf, c as issueBinding, i as identityOf, n as make, o as MaximumGroupNodes, s as fromNativeAttempt } from "./BrowserRuntime-HJ1yYlFS.mjs";
4
+ import { l as reported, s as cleanupStep, t as checked } from "./PublicSession-Bs61Eal8.mjs";
5
+ import { n as pngGeometry } from "./Images-CaaPNeed.mjs";
4
6
  import { Crypto, Effect, Layer, Option, PlatformError, Redacted, Schema } from "effect";
5
7
  //#region src/internal/testing/Crypto.ts
6
8
  /**
7
9
  * A `Crypto` whose bytes count up from one, so every id and handoff token a scripted browser draws
8
- * is predictable. It is not random and computes no digest.
10
+ * is predictable. A namespace occupies UUID bytes untouched by its version/variant bits.
11
+ * It is not random and computes no digest.
9
12
  */
10
- const makeSequentialCrypto = () => {
13
+ const makeSequentialCrypto = (namespace = 0) => {
11
14
  let draws = 0;
12
15
  return Crypto.make({
13
16
  randomBytes: (size) => {
@@ -17,6 +20,12 @@ const makeSequentialCrypto = () => {
17
20
  bytes[index] = rest % 256;
18
21
  rest = Math.floor(rest / 256);
19
22
  }
23
+ let scope = BigInt(namespace);
24
+ for (let index = 0; index < size - 8 && scope > 0n; index++) {
25
+ if (index === 6) continue;
26
+ bytes[index] = Number(scope % 256n);
27
+ scope /= 256n;
28
+ }
20
29
  return bytes;
21
30
  },
22
31
  digest: () => Effect.fail(PlatformError.badArgument({
@@ -97,6 +106,11 @@ const DocumentScript = Schema.Struct({
97
106
  url: TargetUrl,
98
107
  title: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(512))),
99
108
  text: Schema.String.check(Schema.isMaxLength(131072)),
109
+ /**
110
+ * Authored root-document geometry, not inferred layout. Readings carry this exact snapshot;
111
+ * use document.update to supply the next state after a scripted scroll or resize.
112
+ */
113
+ documentScroll: Schema.optionalKey(DocumentScroll),
100
114
  controls: Schema.optionalKey(Schema.Array(ControlScript).check(Schema.isMaxLength(64), uniqueBy((control) => control.id, "unique control ids")))
101
115
  });
102
116
  /**
@@ -155,6 +169,9 @@ const ScriptableOperation = Schema.Literals([
155
169
  "read-text",
156
170
  "screenshot",
157
171
  "wait",
172
+ "resolve",
173
+ "run",
174
+ "settled",
158
175
  "click",
159
176
  "fill",
160
177
  "fill-form",
@@ -170,9 +187,9 @@ const ScriptableOperation = Schema.Literals([
170
187
  "press",
171
188
  "type",
172
189
  "list-pages",
190
+ "describe-page",
173
191
  "list-frames",
174
192
  "select-page",
175
- "select-frame",
176
193
  "new-page",
177
194
  "close-page",
178
195
  "resize",
@@ -300,6 +317,7 @@ const makeScriptedBrowser = (script, timers) => {
300
317
  url: document.url,
301
318
  title: document.title ?? "",
302
319
  text: document.text,
320
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
303
321
  controls: (document.controls ?? []).map(controlState)
304
322
  });
305
323
  const documentFor = (url) => {
@@ -320,6 +338,11 @@ const makeScriptedBrowser = (script, timers) => {
320
338
  registeredEpoch: registered ? 0 : -1,
321
339
  values: /* @__PURE__ */ new Map(),
322
340
  files: /* @__PURE__ */ new Map(),
341
+ scroll: {
342
+ x: 0,
343
+ y: 0
344
+ },
345
+ mutations: 0,
323
346
  focused: void 0,
324
347
  closed: false,
325
348
  held: void 0,
@@ -328,6 +351,7 @@ const makeScriptedBrowser = (script, timers) => {
328
351
  capture: void 0
329
352
  };
330
353
  pages.set(page.pageId, page);
354
+ for (const connection of live) connection.opened(page.pageId);
331
355
  return page;
332
356
  };
333
357
  const openPages = () => [...pages.values()].filter((page) => !page.closed);
@@ -338,12 +362,17 @@ const makeScriptedBrowser = (script, timers) => {
338
362
  /** The page shows a new document: nodes go stale, values reset, captures learn of it. */
339
363
  const commit = (page, document) => {
340
364
  page.document = document;
365
+ page.cachedTitle = void 0;
341
366
  page.epoch++;
342
367
  page.values.clear();
343
368
  page.files.clear();
344
369
  page.focused = void 0;
370
+ page.scroll.x = 0;
371
+ page.scroll.y = 0;
372
+ page.mutations++;
345
373
  page.navigation = void 0;
346
374
  for (const connection of live) connection.retire(page.pageId);
375
+ for (const connection of live) connection.navigated(page.pageId, false);
347
376
  if (page.capture !== void 0) {
348
377
  const { start } = page.capture;
349
378
  if (start.document === void 0) start.invalidate("target-changed");
@@ -354,6 +383,8 @@ const makeScriptedBrowser = (script, timers) => {
354
383
  };
355
384
  /** In-document change: identity survives by control id, so retained nodes and waits carry on. */
356
385
  const update = (page, document) => {
386
+ const previousUrl = page.document.url;
387
+ const before = JSON.stringify(page.document);
357
388
  const previous = new Map(page.document.controls.map((control) => [control.script.id, control]));
358
389
  const controls = (document.controls ?? []).map((script) => {
359
390
  const existing = previous.get(script.id);
@@ -367,9 +398,15 @@ const makeScriptedBrowser = (script, timers) => {
367
398
  url: document.url,
368
399
  title: document.title ?? "",
369
400
  text: document.text,
401
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
370
402
  controls
371
403
  };
404
+ if (JSON.stringify(page.document) !== before) page.mutations++;
372
405
  if (page.focused !== void 0 && !previous.has(page.focused)) page.focused = void 0;
406
+ if (previousUrl !== document.url) {
407
+ for (const connection of live) connection.navigated(page.pageId, true);
408
+ page.capture?.start.document?.(document.url, true);
409
+ }
373
410
  notify(page);
374
411
  };
375
412
  const take = (operation) => {
@@ -392,11 +429,13 @@ const makeScriptedBrowser = (script, timers) => {
392
429
  throw new Error("The scripted browser refused this connection");
393
430
  }
394
431
  const bindings = options.bindings ?? [];
395
- let viewport = { ...options.viewport };
432
+ const viewport = { ...options.viewport };
396
433
  const positions = /* @__PURE__ */ new Map();
397
434
  let pointer = null;
398
435
  let selectedId;
399
- let snapshot;
436
+ const snapshots = /* @__PURE__ */ new Map();
437
+ const privateSnapshots = /* @__PURE__ */ new Set();
438
+ const resolvedElements = /* @__PURE__ */ new WeakMap();
400
439
  let disconnected = false;
401
440
  let index = -1;
402
441
  const place = (pageId, point) => {
@@ -410,8 +449,16 @@ const makeScriptedBrowser = (script, timers) => {
410
449
  const dispatchObservers = /* @__PURE__ */ new WeakMap();
411
450
  const receipt = (pageId) => ({ position: positions.get(pageId) ?? null });
412
451
  const select = (pageId) => {
452
+ const previous = selectedId;
413
453
  selectedId = pageId;
414
454
  lastSelected = pageId;
455
+ for (const id of previous === pageId ? [pageId] : [previous, pageId]) {
456
+ const page = id === void 0 ? void 0 : pages.get(id);
457
+ if (page !== void 0 && !page.closed) events.pageLifecycle?.({
458
+ _tag: "Display",
459
+ page: cachedPage(page)
460
+ });
461
+ }
415
462
  };
416
463
  const selectedPage = (operation = "target") => {
417
464
  const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
@@ -419,7 +466,6 @@ const makeScriptedBrowser = (script, timers) => {
419
466
  return page;
420
467
  };
421
468
  const current = (target, operation) => {
422
- if (target === void 0) return selectedPage(operation);
423
469
  if (disconnected) throw fail(operation, Reasons.Closed.make({}));
424
470
  const page = pages.get(target.pageId);
425
471
  if (page === void 0 || page.closed || page.frameId !== target.frameId) throw fail(operation, Reasons.Stale.make({}));
@@ -431,26 +477,91 @@ const makeScriptedBrowser = (script, timers) => {
431
477
  if (page.closed || page.targetId !== info.targetId) throw fail(operation, Reasons.Stale.make({}));
432
478
  return page;
433
479
  };
434
- const info = (page) => PageInfo.make({
480
+ const cachedPage = (page) => Object.freeze({
435
481
  pageId: page.pageId,
482
+ frameId: page.frameId,
436
483
  targetId: page.targetId,
437
- url: page.document.url,
438
- title: page.document.title,
439
- selected: page.pageId === selectedId
484
+ documentEpoch: page.epoch,
485
+ url: page.document.url.length <= 8192 ? page.document.url : null,
486
+ urlQualification: page.document.url.length <= 8192 ? "NativeCached" : "Omitted",
487
+ title: page.cachedTitle ?? null,
488
+ titleQualification: page.cachedTitle === void 0 ? "Unread" : page.cachedTitle === null ? "Omitted" : "ObservedCached",
489
+ selected: page.pageId === selectedId,
490
+ displayState: options.pageControl === true ? page.held === void 0 ? "running" : "suspended" : "unknown"
440
491
  });
492
+ const info = (page) => {
493
+ page.cachedTitle = page.document.title.length <= 512 ? page.document.title : null;
494
+ const value = PageInfo.make({
495
+ pageId: page.pageId,
496
+ targetId: page.targetId,
497
+ url: page.document.url,
498
+ title: page.document.title,
499
+ selected: page.pageId === selectedId
500
+ });
501
+ events.pageLifecycle?.({
502
+ _tag: "Metadata",
503
+ page: cachedPage(page)
504
+ });
505
+ return value;
506
+ };
507
+ const picture = (page, ticket) => {
508
+ const target = {
509
+ pageId: page.pageId,
510
+ frameId: page.frameId
511
+ };
512
+ const geometry = pngGeometry(PNG);
513
+ ticket.picture?.({
514
+ phase: "Requested",
515
+ target,
516
+ documentEpoch: page.epoch,
517
+ geometry
518
+ });
519
+ const bytes = new Uint8Array(PNG);
520
+ ticket.picture?.({
521
+ phase: "Returned",
522
+ target,
523
+ documentEpoch: page.epoch,
524
+ geometry,
525
+ byteLength: bytes.length
526
+ });
527
+ return bytes;
528
+ };
441
529
  const frameInfo = (page) => FrameInfo.make({
442
530
  frameId: page.frameId,
443
531
  parentFrameId: null,
444
532
  url: page.document.url,
445
533
  name: ""
446
534
  });
447
- const invalidateSnapshot = (scope = "all") => {
448
- if (snapshot !== void 0 && scope !== "none" && (scope === "all" || scope.pageId === snapshot.pageId)) snapshot.validity = "invalid";
535
+ const invalidateSnapshot = (scope = "all", origin) => {
536
+ for (const snapshot of [...snapshots.values(), ...privateSnapshots]) if (scope !== "none" && (scope === "all" || scope.pageId === snapshot.pageId && (scope.frameId === void 0 || scope.frameId === snapshot.frameId))) {
537
+ if (privateSnapshots.has(snapshot) && origin !== void 0 && snapshot.origin === origin) continue;
538
+ snapshot.validity = "invalid";
539
+ }
540
+ };
541
+ /** The page closes as a browser closes it: every connection learns, and nothing on it lasts. */
542
+ const close = (target) => {
543
+ target.closed = true;
544
+ target.navigation?.drop();
545
+ target.capture?.start.invalidate("target-changed");
546
+ target.capture = void 0;
547
+ notify(target);
548
+ if (selectedId === target.pageId) select(void 0);
549
+ snapshots.delete(target.frameId);
550
+ events.pageClosed?.(target.pageId, cachedPage(target));
551
+ events.invalidate("target-changed", { pageId: target.pageId });
552
+ for (const other of live) {
553
+ if (other === connection) continue;
554
+ other.closed(target.pageId);
555
+ other.announce(target.pageId);
556
+ }
449
557
  };
450
558
  /** This connection ends: what it held on the browser's pages ends with it. */
451
559
  const dropConnection = (announce, ended = "dropped") => {
452
560
  if (disconnected) return;
453
561
  disconnected = true;
562
+ invalidateSnapshot();
563
+ snapshots.clear();
564
+ privateSnapshots.clear();
454
565
  connections[index] = ended;
455
566
  live.delete(connection);
456
567
  for (const page of pages.values()) {
@@ -460,6 +571,7 @@ const makeScriptedBrowser = (script, timers) => {
460
571
  notify(page);
461
572
  }
462
573
  if (announce) events.disconnected();
574
+ events.retired?.();
463
575
  };
464
576
  const dispatch = (ticket, record) => {
465
577
  if (record.dispatched) return;
@@ -532,13 +644,14 @@ const makeScriptedBrowser = (script, timers) => {
532
644
  if (ticket !== void 0) await applyArmed(operation, ticket, record);
533
645
  const value = await body(record);
534
646
  record.settled = "completed";
647
+ if (ticket !== void 0 && isTicket(ticket)) ticket.acknowledge?.();
535
648
  return value;
536
649
  } catch (error) {
537
650
  record.settled = "failed";
538
651
  throw error;
539
652
  }
540
653
  };
541
- const meta = (target) => target === void 0 ? {} : typeof target === "string" ? { selector: target } : { elementId: target.elementId };
654
+ const meta = (target) => target === void 0 ? {} : typeof target === "string" ? { selector: target } : { elementId: "_tag" in target ? resolvedElements.get(target)?.node.script.id : target.elementId };
542
655
  const facts = (control) => {
543
656
  const { script: value } = control;
544
657
  const extra = value.facts ?? {};
@@ -553,7 +666,7 @@ const makeScriptedBrowser = (script, timers) => {
553
666
  editable: extra.editable ?? isEditable(value),
554
667
  ...value.inputType === void 0 ? {} : { inputType: value.inputType },
555
668
  ...extra.autocomplete === void 0 ? {} : { autocomplete: extra.autocomplete },
556
- ...value.destination === void 0 ? {} : { destination: value.destination },
669
+ ...value.destination === void 0 || value.destination.length > 2048 ? {} : { destination: value.destination },
557
670
  ...extra.formMethod === void 0 ? {} : { formMethod: extra.formMethod },
558
671
  box: ViewportRect.make(extra.box ?? {
559
672
  x: 0,
@@ -588,33 +701,62 @@ const makeScriptedBrowser = (script, timers) => {
588
701
  return control;
589
702
  };
590
703
  /** A retained node is only as current as the observation that produced it. */
591
- const retained = (target, ticket, operation, allowSuspended = false, attached = true) => {
704
+ const retained = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => {
592
705
  ticket.check();
593
706
  const stale = () => fail(operation, Reasons.Stale.make({}));
594
- const current = snapshot;
595
- if (current === void 0 || current.id !== target.observationId) throw stale();
596
- const node = current.nodes.get(target.elementId);
707
+ const page = current(browserTarget, operation);
708
+ const snapshot = snapshots.get(page.frameId);
709
+ if (snapshot === void 0 || snapshot.pageId !== page.pageId || snapshot.id !== target.observationId) throw stale();
710
+ const node = snapshot.nodes.get(target.elementId);
597
711
  if (node === void 0) throw stale();
598
- if (current.validity === "invalid" || current.generation !== ticket.generation) throw stale();
599
- const page = pages.get(current.pageId);
600
- if (page === void 0 || page.closed || page.epoch !== current.epoch) throw stale();
601
- if (selectedId !== current.pageId) throw stale();
602
- if (current.validity === "suspended" && !allowSuspended && !current.revalidated.has(target.elementId)) throw stale();
712
+ if (snapshot.validity === "invalid" || snapshot.generation !== ticket.generation) throw stale();
713
+ if (page.epoch !== snapshot.epoch) throw stale();
714
+ if (snapshot.validity === "suspended" && !allowSuspended && !snapshot.revalidated.has(target.elementId)) throw stale();
603
715
  if (attached && !page.document.controls.includes(node)) throw stale();
604
716
  return {
605
717
  node,
606
718
  page,
607
- snapshot: current
719
+ snapshot
608
720
  };
609
721
  };
722
+ const privateRetained = (target, ticket, operation, browserTarget, attached = true) => {
723
+ ticket.check();
724
+ const record = resolvedElements.get(target);
725
+ const page = current(browserTarget, operation);
726
+ if (record === void 0 || record.snapshot.validity !== "valid" || !privateSnapshots.has(record.snapshot) || record.snapshot.generation !== ticket.generation || record.snapshot.pageId !== page.pageId || record.snapshot.frameId !== page.frameId || record.snapshot.epoch !== page.epoch || attached && !page.document.controls.includes(record.node) || record.parent !== void 0 && (!page.document.controls.includes(record.parent) || record.node.script.selectElementId !== record.parent.script.id)) throw fail(operation, Reasons.Stale.make({}));
727
+ return {
728
+ ...record,
729
+ page
730
+ };
731
+ };
732
+ const retainedElement = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => "_tag" in target ? privateRetained(target, ticket, operation, browserTarget, attached) : retained(target, ticket, operation, allowSuspended, attached, browserTarget);
733
+ const sample = (node, fresh, scope, descriptor) => ({
734
+ facts: fresh,
735
+ scope,
736
+ frameComplete: true,
737
+ completeness: {
738
+ label: "complete",
739
+ inputType: node.script.inputType === void 0 ? "absent" : "complete",
740
+ autocomplete: node.script.facts?.autocomplete === void 0 ? "absent" : "complete",
741
+ destination: node.script.destination === void 0 ? "absent" : node.script.destination.length > 2048 ? "omitted" : "complete",
742
+ formMethod: node.script.facts?.formMethod === void 0 ? "absent" : "complete"
743
+ },
744
+ ...descriptor?.ordinal === void 0 ? {} : { ordinal: descriptor.ordinal }
745
+ });
610
746
  const resolve = (target, ticket, operation, policy, browserTarget, allowSuspended = false, enablement = false) => {
611
747
  let node;
612
748
  let page;
613
749
  if (typeof target === "string") {
614
750
  page = current(browserTarget, operation);
615
751
  node = bySelector(page, target, operation);
616
- } else ({node, page} = retained(target, ticket, operation, allowSuspended));
752
+ } else ({node, page} = retainedElement(target, ticket, operation, allowSuspended, true, browserTarget));
617
753
  const fresh = facts(node);
754
+ const captured = ControlFacts.make({
755
+ ...fresh,
756
+ box: ViewportRect.make({ ...fresh.box })
757
+ });
758
+ const privateTarget = typeof target !== "string" && "_tag" in target ? resolvedElements.get(target) : void 0;
759
+ if (privateTarget !== void 0 && (enablement ? stableIdentityOf(fresh) !== privateTarget.stable : identityOf(fresh) !== privateTarget.identity)) throw fail(operation, Reasons.Stale.make({}));
618
760
  if (enablement && node.script.disabled === true) throw fail(operation, Reasons.Disabled.make({}));
619
761
  if (policy !== void 0) {
620
762
  let admitted = false;
@@ -626,17 +768,189 @@ const makeScriptedBrowser = (script, timers) => {
626
768
  if (!admitted) throw fail(operation, Reasons.Denied.make({}));
627
769
  }
628
770
  ticket.check();
771
+ if (isTicket(ticket)) {
772
+ const scope = privateTarget?.scope ?? (typeof target === "string" ? "document" : snapshots.get(page.frameId)?.scope ?? "document");
773
+ ticket.captureTarget?.(target, sample(node, captured, scope, privateTarget?.descriptor));
774
+ }
629
775
  return {
630
776
  node,
631
777
  page,
632
778
  facts: fresh
633
779
  };
634
780
  };
781
+ /** The complete scripted document, never a previously clipped observation, proves cardinality. */
782
+ const descriptorNode = (descriptor, page, operation, contextual, parent) => {
783
+ if (descriptor.frame !== void 0) throw fail(operation, Reasons.Unsupported.make({}));
784
+ const matches = page.document.controls.filter((node) => {
785
+ const fresh = facts(node);
786
+ const identity = descriptor.identity;
787
+ return fresh.kind === descriptor.kind && fresh.label === descriptor.label && (parent === void 0 || parent.script.kind === "select" && node.script.selectElementId === parent.script.id) && (descriptor.destination === void 0 || fresh.destination === descriptor.destination) && (identity?.inputType === void 0 || fresh.inputType === identity.inputType) && (identity?.autocomplete === void 0 || fresh.autocomplete === identity.autocomplete) && (identity?.formMethod === void 0 || fresh.formMethod === identity.formMethod);
788
+ });
789
+ const viewportMatches = matches.filter((node) => facts(node).hitTest === "self");
790
+ if (descriptor.matchScope === "viewport") {
791
+ if (matches.some((node) => {
792
+ const fresh = facts(node);
793
+ return fresh.hitTest === "uncertain" && fresh.placement !== "outside";
794
+ })) throw fail(operation, Reasons.Incomplete.make({}));
795
+ if (!contextual && matches.length > 1) throw fail(operation, Reasons.Ambiguous.make({ count: matches.length }));
796
+ }
797
+ const candidates = descriptor.matchScope === "viewport" ? viewportMatches : matches;
798
+ if (candidates.length === 0) throw fail(operation, Reasons.Missing.make({}));
799
+ if (descriptor.ordinal !== void 0 && descriptor.ordinal.of !== candidates.length) throw fail(operation, Reasons.Incomplete.make({}));
800
+ if (descriptor.ordinal === void 0 && candidates.length !== 1) throw fail(operation, Reasons.Ambiguous.make({ count: candidates.length }));
801
+ const picked = candidates[descriptor.ordinal?.index ?? 0];
802
+ if (picked === void 0) throw fail(operation, Reasons.Incomplete.make({}));
803
+ return picked;
804
+ };
805
+ const expectations = (conditions, ticket, browserTarget) => {
806
+ ticket.check();
807
+ const page = current(browserTarget, "run");
808
+ const size = page.viewport ?? viewport;
809
+ if (conditions.length > 16) throw fail("run", Reasons.Limit.make({
810
+ dimension: "controls",
811
+ maximum: 16,
812
+ observed: conditions.length
813
+ }));
814
+ for (const condition of conditions) {
815
+ ticket.check();
816
+ let matches;
817
+ switch (condition._tag) {
818
+ case "Origin":
819
+ matches = originOf(page.document.url) === condition.value;
820
+ break;
821
+ case "Path":
822
+ matches = new URL(page.document.url).pathname === condition.value;
823
+ break;
824
+ case "Viewport":
825
+ matches = size.width === condition.width && size.height === condition.height;
826
+ break;
827
+ case "Scroll":
828
+ matches = page.scroll.x === condition.x && page.scroll.y === condition.y;
829
+ break;
830
+ case "Text":
831
+ if (condition.scope === "viewport") throw fail("run", Reasons.Unsupported.make({}));
832
+ matches = condition.match === "equals" ? page.document.text === condition.value : page.document.text.includes(condition.value);
833
+ break;
834
+ case "Geometry": {
835
+ const node = descriptorNode(condition.target, page, "run", false);
836
+ const box = facts(node).box;
837
+ matches = box.x === condition.box.x && box.y === condition.box.y && box.width === condition.box.width && box.height === condition.box.height;
838
+ break;
839
+ }
840
+ }
841
+ if (!matches) throw fail("run", Reasons.Denied.make({}));
842
+ }
843
+ ticket.check();
844
+ };
845
+ const resolveGroup = (requests, ticket, browserTarget, guard) => {
846
+ ticket.check();
847
+ const page = current(browserTarget, "resolve");
848
+ if (requests.length === 0 || requests.length > 2081) throw fail("resolve", Reasons.Limit.make({
849
+ dimension: "controls",
850
+ maximum: MaximumGroupNodes,
851
+ observed: requests.length
852
+ }));
853
+ if (guard._tag === "ViewportContext") expectations(guard.before, ticket, browserTarget);
854
+ const retainedSnapshots = [...snapshots.values(), ...privateSnapshots];
855
+ const perPage = retainedSnapshots.filter((snapshot) => snapshot.pageId === page.pageId).length;
856
+ const pageMaximum = options.observationLimits?.maxSnapshotsPerPage ?? 16;
857
+ const sessionMaximum = options.observationLimits?.maxSnapshotsPerSession ?? 64;
858
+ if (perPage >= pageMaximum || retainedSnapshots.length >= sessionMaximum) throw fail("resolve", Reasons.Limit.make({
859
+ dimension: "observation-snapshots",
860
+ maximum: perPage >= pageMaximum ? pageMaximum : sessionMaximum,
861
+ observed: perPage >= pageMaximum ? perPage + 1 : retainedSnapshots.length + 1
862
+ }));
863
+ const snapshot = {
864
+ id: `observation-${++observationSerial}`,
865
+ pageId: page.pageId,
866
+ frameId: page.frameId,
867
+ epoch: page.epoch,
868
+ generation: ticket.generation,
869
+ scope: "document",
870
+ validity: "valid",
871
+ nodes: /* @__PURE__ */ new Map(),
872
+ identities: /* @__PURE__ */ new Map(),
873
+ revalidated: /* @__PURE__ */ new Set()
874
+ };
875
+ const elements = [];
876
+ const records = [];
877
+ const samples = [];
878
+ privateSnapshots.add(snapshot);
879
+ const release = async () => {
880
+ snapshot.validity = "invalid";
881
+ privateSnapshots.delete(snapshot);
882
+ };
883
+ try {
884
+ for (const [index, request] of requests.entries()) {
885
+ ticket.check();
886
+ if (request.parent !== void 0 && (!Number.isInteger(request.parent) || request.parent < 0 || request.parent >= index)) throw fail("resolve", Reasons.Malformed.make({}));
887
+ const parent = request.parent === void 0 ? void 0 : records[request.parent]?.node;
888
+ let node;
889
+ let scope;
890
+ let identity;
891
+ let stable;
892
+ if (request.target._tag === "Ref") {
893
+ const source = retained(request.target.reference, ticket, "resolve", false, true, browserTarget);
894
+ const captured = source.snapshot.identities.get(request.target.reference.elementId);
895
+ if (captured === void 0) throw fail("resolve", Reasons.Incomplete.make({}));
896
+ node = source.node;
897
+ scope = source.snapshot.scope;
898
+ ({identity, stable} = captured);
899
+ } else {
900
+ node = descriptorNode(request.target.descriptor, page, "resolve", guard._tag === "ViewportContext", parent);
901
+ scope = request.target.descriptor.matchScope;
902
+ const fresh = facts(node);
903
+ identity = identityOf(fresh);
904
+ stable = stableIdentityOf(fresh);
905
+ }
906
+ if (parent !== void 0 && (parent.script.kind !== "select" || node.script.selectElementId !== parent.script.id)) throw fail("resolve", Reasons.Stale.make({}));
907
+ const element = Object.freeze({
908
+ _tag: "ResolvedElement",
909
+ target: Object.freeze({
910
+ pageId: page.pageId,
911
+ frameId: page.frameId
912
+ })
913
+ });
914
+ const record = {
915
+ node,
916
+ snapshot,
917
+ scope,
918
+ identity,
919
+ stable,
920
+ ...parent === void 0 ? {} : { parent },
921
+ ...request.target._tag === "Descriptor" ? { descriptor: request.target.descriptor } : {}
922
+ };
923
+ snapshot.nodes.set(node.script.id, node);
924
+ snapshot.identities.set(node.script.id, {
925
+ identity,
926
+ stable
927
+ });
928
+ resolvedElements.set(element, record);
929
+ elements.push(element);
930
+ records.push(record);
931
+ samples.push(request.target._tag === "Descriptor" || request.captureInitial === true && ticket.captureTarget !== void 0 ? sample(node, facts(node), scope, record.descriptor) : void 0);
932
+ }
933
+ ticket.check();
934
+ return {
935
+ elements: Object.freeze(elements),
936
+ samples: Object.freeze(samples),
937
+ activate: (next) => {
938
+ for (const element of elements) privateRetained(element, next, "run", browserTarget);
939
+ snapshot.origin = next.signal;
940
+ },
941
+ release
942
+ };
943
+ } catch (error) {
944
+ snapshot.validity = "invalid";
945
+ privateSnapshots.delete(snapshot);
946
+ throw error;
947
+ }
948
+ };
635
949
  const requireRunning = (page, operation) => {
636
950
  if (page.held !== void 0) throw fail(operation, Reasons.Busy.make({}));
637
951
  };
638
952
  const heldObservation = (pageId) => {
639
- if (snapshot?.pageId === pageId && snapshot.validity === "valid") {
953
+ for (const snapshot of snapshots.values()) if (snapshot.pageId === pageId && snapshot.validity === "valid") {
640
954
  snapshot.validity = "suspended";
641
955
  snapshot.revalidated.clear();
642
956
  }
@@ -661,13 +975,29 @@ const makeScriptedBrowser = (script, timers) => {
661
975
  if (focused !== void 0 && isEditable(focused.script)) page.values.set(focused.script.id, (page.values.get(focused.script.id) ?? "") + text);
662
976
  };
663
977
  /** Issued options of one select, refused exactly where native selection refuses them. */
664
- const chosen = (node, ids, operation) => {
665
- const current = snapshot;
666
- if (current === void 0) throw fail(operation, Reasons.Stale.make({}));
978
+ const chosen = (page, node, ids, ticket, operation, enablement = false) => {
667
979
  if (!isSelectable(node.script.kind)) throw fail(operation, Reasons.Unsupported.make({}));
668
980
  const options = ids.map((id) => {
669
- const option = current.nodes.get(id);
670
- if (option === void 0 || option.script.selectElementId !== node.script.id) throw fail(operation, Reasons.Stale.make({}));
981
+ let option;
982
+ if (typeof id === "string") {
983
+ const snapshot = snapshots.get(page.frameId);
984
+ const kept = snapshot?.nodes.get(id);
985
+ if (snapshot === void 0 || kept === void 0 || snapshot.nodes.get(node.script.id) !== node) throw fail(operation, Reasons.Stale.make({}));
986
+ option = kept;
987
+ } else {
988
+ const kept = privateRetained(id, ticket, operation, {
989
+ pageId: page.pageId,
990
+ frameId: page.frameId
991
+ });
992
+ const fresh = facts(kept.node);
993
+ if (kept.parent !== node || (enablement ? stableIdentityOf(fresh) !== kept.stable : identityOf(fresh) !== kept.identity)) throw fail(operation, Reasons.Stale.make({}));
994
+ option = kept.node;
995
+ }
996
+ if (option.script.selectElementId !== node.script.id || !page.document.controls.includes(option)) throw fail(operation, Reasons.Stale.make({}));
997
+ ticket.captureTarget?.(typeof id === "string" ? ObservedElement.make({
998
+ observationId: snapshots.get(page.frameId)?.id ?? "",
999
+ elementId: id
1000
+ }) : id, sample(option, facts(option), typeof id === "string" ? snapshots.get(page.frameId)?.scope ?? "document" : resolvedElements.get(id)?.scope ?? "document", typeof id === "string" ? void 0 : resolvedElements.get(id)?.descriptor));
671
1001
  return option;
672
1002
  });
673
1003
  if (node.script.disabled === true || options.some((option) => option.script.disabled === true)) throw fail(operation, Reasons.Disabled.make({}));
@@ -695,7 +1025,7 @@ const makeScriptedBrowser = (script, timers) => {
695
1025
  let act;
696
1026
  let input;
697
1027
  if (field.options !== void 0) {
698
- const options = chosen(node, field.options, "fill-form");
1028
+ const options = chosen(page, node, field.options, ticket, "fill-form", true);
699
1029
  act = () => choose(page, node, options);
700
1030
  } else if (field.checked !== void 0) {
701
1031
  if (node.checked === void 0) throw fail("fill-form", Reasons.Unsupported.make({}));
@@ -801,6 +1131,98 @@ const makeScriptedBrowser = (script, timers) => {
801
1131
  page.watchers.add(check);
802
1132
  check();
803
1133
  });
1134
+ /** Quiet in the finite scripted source; the actual Chromium observer owns native root metrics. */
1135
+ const waitSettled = (options, ticket, page, record) => new Promise((resolve, reject) => {
1136
+ const epoch = page.epoch;
1137
+ const started = ticket.deadline - ticket.remainingMillis();
1138
+ const elapsed = () => Math.max(0, ticket.deadline - ticket.remainingMillis() - started);
1139
+ const geometry = () => JSON.stringify(page.document.controls.map((node) => facts(node).box));
1140
+ const size = () => page.viewport ?? viewport;
1141
+ let previousGeometry = geometry();
1142
+ let previousViewport = JSON.stringify(size());
1143
+ let previousScroll = JSON.stringify(page.scroll);
1144
+ let previousMutations = page.mutations;
1145
+ let lastChanged = 0;
1146
+ let samples = 0;
1147
+ let mutations = 0;
1148
+ let scrollChanges = 0;
1149
+ let geometryChanges = 0;
1150
+ let viewportChanges = 0;
1151
+ let finished = false;
1152
+ let timer;
1153
+ const cleanup = () => {
1154
+ timer?.cancel();
1155
+ timer = void 0;
1156
+ page.watchers.delete(check);
1157
+ ticket.signal.removeEventListener("abort", onAbort);
1158
+ };
1159
+ const failWait = (error) => {
1160
+ if (finished) return;
1161
+ finished = true;
1162
+ cleanup();
1163
+ record.settled = "failed";
1164
+ reject(error);
1165
+ };
1166
+ const onAbort = () => failWait(aborted(ticket, "settled"));
1167
+ const check = () => {
1168
+ if (finished) return;
1169
+ try {
1170
+ ticket.check();
1171
+ if (disconnected) throw fail("settled", Reasons.Disconnected.make({}));
1172
+ if (page.closed || page.epoch !== epoch) throw fail("settled", Reasons.Stale.make({}));
1173
+ const now = elapsed();
1174
+ const nextGeometry = geometry();
1175
+ const nextViewport = JSON.stringify(size());
1176
+ const nextScroll = JSON.stringify(page.scroll);
1177
+ const changed = page.mutations !== previousMutations || nextGeometry !== previousGeometry || nextViewport !== previousViewport || nextScroll !== previousScroll;
1178
+ samples = Math.min(Number.MAX_SAFE_INTEGER, samples + 1);
1179
+ mutations = Math.min(Number.MAX_SAFE_INTEGER, mutations + Math.max(0, page.mutations - previousMutations));
1180
+ if (nextGeometry !== previousGeometry) geometryChanges++;
1181
+ if (nextViewport !== previousViewport) viewportChanges++;
1182
+ if (nextScroll !== previousScroll) scrollChanges++;
1183
+ previousMutations = page.mutations;
1184
+ previousGeometry = nextGeometry;
1185
+ previousViewport = nextViewport;
1186
+ previousScroll = nextScroll;
1187
+ if (changed) lastChanged = now;
1188
+ if (now >= options.withinMillis) throw fail("settled", Reasons.Timeout.make({}));
1189
+ if (now - lastChanged >= options.quietMillis) {
1190
+ finished = true;
1191
+ cleanup();
1192
+ resolve({
1193
+ quietMillis: options.quietMillis,
1194
+ withinMillis: options.withinMillis,
1195
+ observedMillis: now,
1196
+ signals: [
1197
+ "dom-mutation",
1198
+ "scroll",
1199
+ "root-geometry",
1200
+ "viewport"
1201
+ ],
1202
+ samples,
1203
+ mutations,
1204
+ scrollChanges,
1205
+ geometryChanges,
1206
+ viewportChanges,
1207
+ visibility: "visible"
1208
+ });
1209
+ return;
1210
+ }
1211
+ timer?.cancel();
1212
+ timer = timers.sleep(Math.min(25, options.quietMillis - (now - lastChanged), options.withinMillis - now, ticket.remainingMillis()));
1213
+ timer.done.then(check, () => {});
1214
+ } catch (error) {
1215
+ failWait(error);
1216
+ }
1217
+ };
1218
+ if (ticket.signal.aborted) {
1219
+ onAbort();
1220
+ return;
1221
+ }
1222
+ ticket.signal.addEventListener("abort", onAbort, { once: true });
1223
+ page.watchers.add(check);
1224
+ check();
1225
+ });
804
1226
  const invokeBinding = async (name, input, origin) => {
805
1227
  const binding = bindings.find((candidate) => candidate.name === name);
806
1228
  if (binding === void 0) return { ok: false };
@@ -820,6 +1242,7 @@ const makeScriptedBrowser = (script, timers) => {
820
1242
  };
821
1243
  try {
822
1244
  const reply = await binding.invoke({
1245
+ origin: documentOrigin,
823
1246
  read: async (signal) => {
824
1247
  await check(signal);
825
1248
  return text;
@@ -857,6 +1280,7 @@ const makeScriptedBrowser = (script, timers) => {
857
1280
  select(previous !== void 0 && !previous.closed ? previous.pageId : openPages()[0]?.pageId);
858
1281
  }
859
1282
  const driver = {
1283
+ cachedPages: () => Object.freeze(openPages().map(cachedPage)),
860
1284
  ...options.pageControl === true ? { pageControl: {
861
1285
  state: async (page, ticket) => {
862
1286
  ticket.check();
@@ -877,6 +1301,11 @@ const makeScriptedBrowser = (script, timers) => {
877
1301
  by: connection
878
1302
  };
879
1303
  heldObservation(target.pageId);
1304
+ ticket.acknowledge?.();
1305
+ events.pageLifecycle?.({
1306
+ _tag: "Display",
1307
+ page: cachedPage(target)
1308
+ });
880
1309
  return PageSuspension.make({
881
1310
  pageId: target.pageId,
882
1311
  targetId: target.targetId,
@@ -889,6 +1318,11 @@ const makeScriptedBrowser = (script, timers) => {
889
1318
  dispatch(ticket, record);
890
1319
  target.held = void 0;
891
1320
  heldObservation(target.pageId);
1321
+ ticket.acknowledge?.();
1322
+ events.pageLifecycle?.({
1323
+ _tag: "Display",
1324
+ page: cachedPage(target)
1325
+ });
892
1326
  }),
893
1327
  checkTarget: async (target, ticket) => {
894
1328
  ticket.check();
@@ -904,6 +1338,7 @@ const makeScriptedBrowser = (script, timers) => {
904
1338
  },
905
1339
  selectedTargetId: async () => selectedPage().targetId,
906
1340
  listPages: (ticket) => attempt("list-pages", ticket, {}, async () => openPages().map(info)),
1341
+ describePage: (page, ticket) => attempt("describe-page", ticket, { pageId: page.pageId }, async () => info(pageOf(page, "describe-page"))),
907
1342
  resolvePage: async (page, ticket) => {
908
1343
  ticket.check();
909
1344
  const target = pageOf(page, "target");
@@ -929,24 +1364,20 @@ const makeScriptedBrowser = (script, timers) => {
929
1364
  text: ""
930
1365
  }), false));
931
1366
  }),
932
- closePage: (page, ticket) => attempt("close-page", ticket, { pageId: page.pageId }, async (record) => {
1367
+ closePage: (page, ticket, onDispatch) => attempt("close-page", ticket, { pageId: page.pageId }, async (record) => {
933
1368
  const target = pageOf(page, "close-page");
934
1369
  dispatch(ticket, record);
935
- target.closed = true;
936
- target.navigation?.drop();
937
- target.capture?.start.invalidate("target-changed");
938
- target.capture = void 0;
939
- notify(target);
940
- if (selectedId === target.pageId) select(void 0);
941
- invalidateSnapshot({ pageId: target.pageId });
942
- events.invalidate("target-changed", { pageId: target.pageId });
943
- for (const other of live) {
944
- if (other === connection) continue;
945
- other.retire(target.pageId);
946
- other.announce(target.pageId);
947
- }
1370
+ close(target);
1371
+ }, onDispatch),
1372
+ containPage: (pageId) => attempt("close-page", void 0, { pageId }, async (record) => {
1373
+ const target = pages.get(pageId);
1374
+ if (target === void 0 || target.closed) return;
1375
+ const outcome = take("close-page");
1376
+ record.dispatched = true;
1377
+ if (outcome?._tag === "Fail") throw fail("close-page", outcome.reason, outcome.outcome);
1378
+ close(target);
948
1379
  }),
949
- listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(page === void 0 ? selectedPage("list-frames") : pageOf(page, "list-frames"))]),
1380
+ listFrames: (ticket, page) => attempt("list-frames", ticket, {}, async () => [frameInfo(pageOf(page, "list-frames"))]),
950
1381
  resolveFrame: async (page, frame, ticket) => {
951
1382
  ticket.check();
952
1383
  const target = pageOf(page, "target");
@@ -956,11 +1387,7 @@ const makeScriptedBrowser = (script, timers) => {
956
1387
  frameId: target.frameId
957
1388
  };
958
1389
  },
959
- selectFrame: (id, ticket) => attempt("select-frame", ticket, {}, async () => {
960
- if (id !== selectedPage("select-frame").frameId) throw fail("select-frame", Reasons.NotFound.make({}));
961
- events.invalidate("target-changed", "none");
962
- }),
963
- beginNavigation: (url, timeoutMillis, ticket, target) => attempt("navigate", void 0, { pageId: target?.pageId }, async (record) => {
1390
+ beginNavigation: (url, timeoutMillis, ticket, target) => attempt("navigate", void 0, { pageId: target.pageId }, async (record) => {
964
1391
  ticket.check();
965
1392
  const page = current(target, "navigate");
966
1393
  requireRunning(page, "navigate");
@@ -1023,7 +1450,7 @@ const makeScriptedBrowser = (script, timers) => {
1023
1450
  };
1024
1451
  }),
1025
1452
  readText: (selector, maximumBytes, ticket, target) => attempt("read-text", ticket, {
1026
- pageId: target?.pageId,
1453
+ pageId: target.pageId,
1027
1454
  ...meta(selector)
1028
1455
  }, async () => {
1029
1456
  const page = current(target, "read-text");
@@ -1031,11 +1458,12 @@ const makeScriptedBrowser = (script, timers) => {
1031
1458
  const control = bySelector(page, selector, "read-text");
1032
1459
  return bounded(control.script.text ?? control.script.label, maximumBytes).text;
1033
1460
  }),
1034
- observe: (scope, maximumBytes, controls, ticket, match) => attempt("observe", ticket, {}, async () => {
1035
- const page = selectedPage("observe");
1461
+ observe: (scope, maximumBytes, controls, ticket, match, target) => attempt("observe", ticket, { pageId: target.pageId }, async () => {
1462
+ const page = current(target, "observe");
1463
+ const size = page.viewport ?? viewport;
1036
1464
  const needle = match?.toLowerCase();
1037
1465
  const matches = (value) => needle === void 0 || value.toLowerCase().includes(needle);
1038
- snapshot = void 0;
1466
+ snapshots.delete(page.frameId);
1039
1467
  const selects = new Set(page.document.controls.filter((control) => isSelectable(control.script.kind) && (matches(control.script.label) || page.document.controls.some((option) => option.script.selectElementId === control.script.id && matches(option.script.label)))).map((control) => control.script.id));
1040
1468
  const matching = page.document.controls.filter((control) => control.script.selectElementId !== void 0 ? selects.has(control.script.selectElementId) : isSelectable(control.script.kind) ? selects.has(control.script.id) : matches(control.script.label));
1041
1469
  const reachable = matching.filter((control) => scope === "document" || control.script.offscreen !== true);
@@ -1044,13 +1472,22 @@ const makeScriptedBrowser = (script, timers) => {
1044
1472
  const next = {
1045
1473
  id: `observation-${++observationSerial}`,
1046
1474
  pageId: page.pageId,
1475
+ frameId: page.frameId,
1047
1476
  epoch: page.epoch,
1048
1477
  generation: ticket.generation,
1478
+ scope,
1049
1479
  validity: "valid",
1050
1480
  nodes: new Map(kept.map((control) => [control.script.id, control])),
1481
+ identities: new Map(kept.map((control) => {
1482
+ const fresh = facts(control);
1483
+ return [control.script.id, {
1484
+ identity: identityOf(fresh),
1485
+ stable: stableIdentityOf(fresh)
1486
+ }];
1487
+ })),
1051
1488
  revalidated: /* @__PURE__ */ new Set()
1052
1489
  };
1053
- snapshot = next;
1490
+ snapshots.set(page.frameId, next);
1054
1491
  return {
1055
1492
  observationId: next.id,
1056
1493
  scope,
@@ -1061,8 +1498,9 @@ const makeScriptedBrowser = (script, timers) => {
1061
1498
  controls: kept.map(observed),
1062
1499
  controlsTruncated: kept.length < reachable.length,
1063
1500
  viewport: ViewportEvidence.make({
1064
- width: viewport.width,
1065
- height: viewport.height,
1501
+ width: size.width,
1502
+ height: size.height,
1503
+ ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1066
1504
  clippedText: 0,
1067
1505
  coveredText: 0,
1068
1506
  uncertainText: 0,
@@ -1071,8 +1509,9 @@ const makeScriptedBrowser = (script, timers) => {
1071
1509
  })
1072
1510
  };
1073
1511
  }),
1074
- checkpoint: (maximumBytes, controls, pictureBytes, ticket) => attempt("checkpoint", ticket, {}, async () => {
1075
- const page = selectedPage("checkpoint");
1512
+ checkpoint: (maximumBytes, controls, pictureBytes, ticket, target) => attempt("checkpoint", ticket, { pageId: target.pageId }, async () => {
1513
+ const page = current(target, "checkpoint");
1514
+ const size = page.viewport ?? viewport;
1076
1515
  const text = bounded(page.document.text, maximumBytes);
1077
1516
  const reachable = page.document.controls.filter((control) => control.script.offscreen !== true);
1078
1517
  return {
@@ -1082,8 +1521,9 @@ const makeScriptedBrowser = (script, timers) => {
1082
1521
  controls: reachable.slice(0, controls).map(facts),
1083
1522
  controlsTruncated: reachable.length > controls,
1084
1523
  viewport: ViewportEvidence.make({
1085
- width: viewport.width,
1086
- height: viewport.height,
1524
+ width: size.width,
1525
+ height: size.height,
1526
+ ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1087
1527
  clippedText: 0,
1088
1528
  coveredText: 0,
1089
1529
  uncertainText: 0,
@@ -1091,19 +1531,53 @@ const makeScriptedBrowser = (script, timers) => {
1091
1531
  exhausted: false
1092
1532
  }),
1093
1533
  documentChanged: false,
1094
- ...pictureBytes === void 0 ? {} : { picture: new Uint8Array(PNG) }
1534
+ ...pictureBytes === void 0 ? {} : { picture: picture(page, ticket) }
1095
1535
  };
1096
1536
  }),
1097
- controlFacts: (target, ticket) => attempt("control-facts", ticket, meta(target), async () => {
1098
- selectedPage("control-facts");
1099
- return resolve(target, ticket, "control-facts").facts;
1537
+ controlFacts: (target, ticket, browserTarget) => attempt("control-facts", ticket, {
1538
+ pageId: browserTarget.pageId,
1539
+ ...meta(target)
1540
+ }, async () => resolve(target, ticket, "control-facts", void 0, browserTarget).facts),
1541
+ resolveDescriptor: (descriptor, ticket, target, guard = { _tag: "Strict" }) => attempt("resolve", ticket, { pageId: target.pageId }, async () => {
1542
+ const group = resolveGroup([{ target: {
1543
+ _tag: "Descriptor",
1544
+ descriptor
1545
+ } }], ticket, target, guard);
1546
+ const element = group.elements[0];
1547
+ const resolved = element === void 0 ? void 0 : resolvedElements.get(element);
1548
+ if (resolved === void 0) {
1549
+ await group.release();
1550
+ throw fail("resolve", Reasons.Malformed.make({}));
1551
+ }
1552
+ try {
1553
+ ticket.check();
1554
+ const previous = snapshots.get(resolved.snapshot.frameId);
1555
+ if (previous !== void 0) previous.validity = "invalid";
1556
+ privateSnapshots.delete(resolved.snapshot);
1557
+ snapshots.set(resolved.snapshot.frameId, {
1558
+ ...resolved.snapshot,
1559
+ scope: resolved.scope
1560
+ });
1561
+ return ObservedElement.make({
1562
+ observationId: resolved.snapshot.id,
1563
+ elementId: resolved.node.script.id
1564
+ });
1565
+ } catch (error) {
1566
+ await group.release();
1567
+ throw error;
1568
+ }
1100
1569
  }),
1101
- revalidate: (target, ticket) => attempt("revalidate", ticket, meta(target), async () => {
1102
- const { snapshot: current } = retained(target, ticket, "revalidate", true);
1570
+ resolveGroup: (requests, ticket, target, guard) => attempt("resolve", ticket, { pageId: target.pageId }, async () => resolveGroup(requests, ticket, target, guard)),
1571
+ expectations: (conditions, ticket, target) => attempt("run", ticket, { pageId: target.pageId }, async () => expectations(conditions, ticket, target)),
1572
+ revalidate: (target, ticket, browserTarget) => attempt("revalidate", ticket, {
1573
+ pageId: browserTarget.pageId,
1574
+ ...meta(target)
1575
+ }, async () => {
1576
+ const { snapshot: current } = retained(target, ticket, "revalidate", true, true, browserTarget);
1103
1577
  current.revalidated.add(target.elementId);
1104
1578
  }),
1105
1579
  click: (target, ticket, capture, policy, browserTarget) => attempt("click", ticket, {
1106
- pageId: browserTarget?.pageId,
1580
+ pageId: browserTarget.pageId,
1107
1581
  ...meta(target)
1108
1582
  }, async (record) => {
1109
1583
  const { node, page } = resolve(target, ticket, "click", policy, browserTarget);
@@ -1117,9 +1591,9 @@ const makeScriptedBrowser = (script, timers) => {
1117
1591
  url,
1118
1592
  input
1119
1593
  };
1120
- }, () => invalidatePointer(browserTarget?.pageId ?? selectedId ?? "")),
1594
+ }, () => invalidatePointer(browserTarget.pageId)),
1121
1595
  fill: (target, value, ticket, policy, browserTarget) => attempt("fill", ticket, {
1122
- pageId: browserTarget?.pageId,
1596
+ pageId: browserTarget.pageId,
1123
1597
  ...meta(target)
1124
1598
  }, async (record) => {
1125
1599
  const { node, page } = resolve(target, ticket, "fill", policy, browserTarget);
@@ -1131,25 +1605,34 @@ const makeScriptedBrowser = (script, timers) => {
1131
1605
  page.values.set(node.script.id, value);
1132
1606
  return page.document.url;
1133
1607
  }),
1134
- selectOption: (target, ids, ticket, policy) => attempt("select-option", ticket, meta(target), async (record) => {
1135
- const { node, page } = resolve(target, ticket, "select-option", policy);
1608
+ selectOption: (target, ids, ticket, policy, browserTarget) => attempt("select-option", ticket, {
1609
+ pageId: browserTarget.pageId,
1610
+ ...meta(target)
1611
+ }, async (record) => {
1612
+ const { node, page } = resolve(target, ticket, "select-option", policy, browserTarget);
1136
1613
  requireRunning(page, "select-option");
1137
- const options = chosen(node, ids, "select-option");
1614
+ const options = chosen(page, node, ids, ticket, "select-option");
1138
1615
  dispatch(ticket, record);
1139
1616
  choose(page, node, options);
1140
1617
  return page.document.url;
1141
1618
  }),
1142
- formStep: (target, field, ticket, policy, _settleMillis, capture) => attempt("fill-form", ticket, meta(target), async (record) => {
1143
- const { node, page } = resolve(target, ticket, "fill-form", policy, void 0, false, true);
1619
+ formStep: (target, field, ticket, policy, _settleMillis, capture, browserTarget) => attempt("fill-form", ticket, {
1620
+ pageId: browserTarget.pageId,
1621
+ ...meta(target)
1622
+ }, async (record) => {
1623
+ const { node, page } = resolve(target, ticket, "fill-form", policy, browserTarget, false, true);
1144
1624
  requireRunning(page, "fill-form");
1145
1625
  return formStep(page, node, field, ticket, record, capture);
1146
- }, field.checked === void 0 ? void 0 : () => invalidatePointer(selectedId ?? "")),
1147
- formState: (targets, ticket) => attempt("fill-form", ticket, {}, async () => targets.map((target) => {
1148
- const { node, page } = retained(target, ticket, "fill-form", false, false);
1626
+ }, field.checked === void 0 ? void 0 : () => invalidatePointer(browserTarget.pageId)),
1627
+ formState: (targets, ticket, browserTarget) => attempt("fill-form", ticket, { pageId: browserTarget.pageId }, async () => targets.map((target) => {
1628
+ const { node, page } = retainedElement(target, ticket, "fill-form", false, false, browserTarget);
1149
1629
  return page.document.controls.includes(node) ? fieldState(page, node) : void 0;
1150
1630
  })),
1151
- formSubmit: (target, ticket, capture, policy) => attempt("fill-form", ticket, meta(target), async (record) => {
1152
- const { node, page } = resolve(target, ticket, "fill-form", policy, void 0, false, true);
1631
+ formSubmit: (target, ticket, capture, policy, browserTarget) => attempt("fill-form", ticket, {
1632
+ pageId: browserTarget.pageId,
1633
+ ...meta(target)
1634
+ }, async (record) => {
1635
+ const { node, page } = resolve(target, ticket, "fill-form", policy, browserTarget, false, true);
1153
1636
  requireRunning(page, "fill-form");
1154
1637
  dispatch(ticket, record);
1155
1638
  let url = "";
@@ -1160,14 +1643,29 @@ const makeScriptedBrowser = (script, timers) => {
1160
1643
  url,
1161
1644
  input
1162
1645
  };
1163
- }, () => invalidatePointer(selectedId ?? "")),
1164
- scroll: (_deltaX, _deltaY, ticket, target) => attempt("scroll", ticket, { pageId: target?.pageId }, async (record) => {
1646
+ }, () => invalidatePointer(browserTarget.pageId)),
1647
+ scroll: (deltaX, deltaY, ticket, target) => attempt("scroll", ticket, { pageId: target.pageId }, async (record) => {
1165
1648
  const page = current(target, "scroll");
1166
1649
  requireRunning(page, "scroll");
1167
1650
  dispatch(ticket, record);
1651
+ page.scroll.x += deltaX;
1652
+ page.scroll.y += deltaY;
1653
+ notify(page);
1654
+ return page.document.url;
1655
+ }),
1656
+ scrollTo: (target, ticket, browserTarget) => attempt("scroll", ticket, {
1657
+ pageId: browserTarget.pageId,
1658
+ ...meta(target)
1659
+ }, async (record) => {
1660
+ const { page, facts: fresh } = resolve(target, ticket, "scroll", void 0, browserTarget);
1661
+ requireRunning(page, "scroll");
1662
+ dispatch(ticket, record);
1663
+ page.scroll.x += fresh.box.x;
1664
+ page.scroll.y += fresh.box.y;
1665
+ notify(page);
1168
1666
  return page.document.url;
1169
1667
  }),
1170
- pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target?.pageId }, async (record) => {
1668
+ pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target.pageId }, async (record) => {
1171
1669
  const page = current(target, "pointer-move");
1172
1670
  requireRunning(page, "pointer-move");
1173
1671
  dispatch(ticket, record);
@@ -1178,7 +1676,7 @@ const makeScriptedBrowser = (script, timers) => {
1178
1676
  return receipt(page.pageId);
1179
1677
  }),
1180
1678
  hover: (target, ticket, policy, browserTarget) => attempt("hover", ticket, {
1181
- pageId: browserTarget?.pageId,
1679
+ pageId: browserTarget.pageId,
1182
1680
  ...meta(target)
1183
1681
  }, async (record) => {
1184
1682
  const { node, page, facts: fresh } = resolve(target, ticket, "hover", policy, browserTarget);
@@ -1191,7 +1689,7 @@ const makeScriptedBrowser = (script, timers) => {
1191
1689
  });
1192
1690
  return receipt(page.pageId);
1193
1691
  }),
1194
- wheel: (_deltaX, _deltaY, at, ticket, target) => attempt("wheel", ticket, { pageId: target?.pageId }, async (record) => {
1692
+ wheel: (deltaX, deltaY, at, ticket, target) => attempt("wheel", ticket, { pageId: target.pageId }, async (record) => {
1195
1693
  const page = current(target, "wheel");
1196
1694
  requireRunning(page, "wheel");
1197
1695
  dispatch(ticket, record);
@@ -1199,10 +1697,13 @@ const makeScriptedBrowser = (script, timers) => {
1199
1697
  x: at.x,
1200
1698
  y: at.y
1201
1699
  });
1700
+ page.scroll.x += deltaX;
1701
+ page.scroll.y += deltaY;
1702
+ notify(page);
1202
1703
  return receipt(page.pageId);
1203
1704
  }),
1204
1705
  press: (key, _modifiers, into, ticket, policy, browserTarget) => attempt("press", ticket, {
1205
- pageId: browserTarget?.pageId,
1706
+ pageId: browserTarget.pageId,
1206
1707
  ...meta(into)
1207
1708
  }, async (record) => {
1208
1709
  const page = focusedOrRefuse(into, ticket, "press", policy, browserTarget);
@@ -1212,7 +1713,7 @@ const makeScriptedBrowser = (script, timers) => {
1212
1713
  return receipt(page.pageId);
1213
1714
  }),
1214
1715
  type: (text, into, ticket, policy, browserTarget) => attempt("type", ticket, {
1215
- pageId: browserTarget?.pageId,
1716
+ pageId: browserTarget.pageId,
1216
1717
  ...meta(into)
1217
1718
  }, async (record) => {
1218
1719
  const page = focusedOrRefuse(into, ticket, "type", policy, browserTarget);
@@ -1221,19 +1722,19 @@ const makeScriptedBrowser = (script, timers) => {
1221
1722
  typed(page, text);
1222
1723
  return receipt(page.pageId);
1223
1724
  }),
1224
- screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target?.pageId }, async () => {
1225
- current(target, "screenshot");
1226
- return new Uint8Array(PNG);
1725
+ screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target.pageId }, async () => {
1726
+ const page = current(target, "screenshot");
1727
+ return picture(page, ticket);
1227
1728
  }),
1228
- resize: (next, ticket) => attempt("resize", ticket, {}, async (record) => {
1229
- const page = selectedPage("resize");
1729
+ resize: (next, ticket, target) => attempt("resize", ticket, { pageId: target.pageId }, async (record) => {
1730
+ const page = current(target, "resize");
1230
1731
  dispatch(ticket, record);
1231
- viewport = {
1732
+ page.viewport = {
1232
1733
  width: next.width,
1233
1734
  height: next.height
1234
1735
  };
1235
1736
  page.capture?.start.invalidate("resized");
1236
- events.invalidate("resized");
1737
+ events.invalidate("resized", { pageId: page.pageId });
1237
1738
  }),
1238
1739
  waitFor: (selector, state, ticket, target) => attempt("wait", ticket, {
1239
1740
  pageId: target.pageId,
@@ -1257,12 +1758,17 @@ const makeScriptedBrowser = (script, timers) => {
1257
1758
  }
1258
1759
  }, () => page.closed || page.epoch !== epoch);
1259
1760
  }).finally(() => ticket.retire()),
1761
+ settled: (options, ticket, target) => attempt("settled", ticket, { pageId: target.pageId }, async (record) => {
1762
+ const page = current(target, "settled");
1763
+ requireRunning(page, "settled");
1764
+ return waitSettled(options, ticket, page, record);
1765
+ }).finally(() => ticket.retire()),
1260
1766
  waitForElement: (reference, state, ticket, target) => attempt("wait", ticket, {
1261
1767
  pageId: target.pageId,
1262
- elementId: reference.elementId
1768
+ ...meta(reference)
1263
1769
  }, async (record) => {
1264
1770
  const page = current(target, "wait");
1265
- const { node } = retained(reference, ticket, "wait", true);
1771
+ const { node } = retainedElement(reference, ticket, "wait", true, true, target);
1266
1772
  const epoch = page.epoch;
1267
1773
  await waitUntil(ticket, page, "wait", record, () => {
1268
1774
  const present = page.document.controls.includes(node);
@@ -1275,8 +1781,11 @@ const makeScriptedBrowser = (script, timers) => {
1275
1781
  }
1276
1782
  }, () => page.closed || page.epoch !== epoch);
1277
1783
  }).finally(() => ticket.retire()),
1278
- clickAndWait: (target, ticket, capture) => attempt("click-and-wait", ticket, meta(target), async (record) => {
1279
- const { node, page } = resolve(target, ticket, "click-and-wait");
1784
+ clickAndWait: (target, ticket, capture, browserTarget) => attempt("click-and-wait", ticket, {
1785
+ pageId: browserTarget.pageId,
1786
+ ...meta(target)
1787
+ }, async (record) => {
1788
+ const { node, page } = resolve(target, ticket, "click-and-wait", void 0, browserTarget);
1280
1789
  requireRunning(page, "click-and-wait");
1281
1790
  dispatch(ticket, record);
1282
1791
  let url = "";
@@ -1287,9 +1796,12 @@ const makeScriptedBrowser = (script, timers) => {
1287
1796
  url,
1288
1797
  input
1289
1798
  };
1290
- }, () => invalidatePointer(selectedId ?? "")),
1291
- clickForDownload: (target, ticket) => attempt("download-action", ticket, meta(target), async (record) => {
1292
- const { node, page } = resolve(target, ticket, "download-action");
1799
+ }, () => invalidatePointer(browserTarget.pageId)),
1800
+ clickForDownload: (target, ticket, browserTarget) => attempt("download-action", ticket, {
1801
+ pageId: browserTarget.pageId,
1802
+ ...meta(target)
1803
+ }, async (record) => {
1804
+ const { node, page } = resolve(target, ticket, "download-action", void 0, browserTarget);
1293
1805
  requireRunning(page, "download-action");
1294
1806
  if (node.script.download === void 0) throw fail("download-action", Reasons.Unsupported.make({}));
1295
1807
  dispatch(ticket, record);
@@ -1300,8 +1812,14 @@ const makeScriptedBrowser = (script, timers) => {
1300
1812
  state: "completed"
1301
1813
  };
1302
1814
  }),
1303
- selectFiles: (target, files, ticket) => attempt("select-files", ticket, meta(target), async (record) => selectFilesOn(target, files, ticket, record, "select-files")),
1304
- clickForFileSelection: (target, files, ticket) => attempt("file-chooser", ticket, meta(target), async (record) => selectFilesOn(target, files, ticket, record, "file-chooser")),
1815
+ selectFiles: (target, files, ticket, browserTarget) => attempt("select-files", ticket, {
1816
+ pageId: browserTarget.pageId,
1817
+ ...meta(target)
1818
+ }, async (record) => selectFilesOn(target, files, ticket, record, "select-files", browserTarget)),
1819
+ clickForFileSelection: (target, files, ticket, browserTarget) => attempt("file-chooser", ticket, {
1820
+ pageId: browserTarget.pageId,
1821
+ ...meta(target)
1822
+ }, async (record) => selectFilesOn(target, files, ticket, record, "file-chooser", browserTarget)),
1305
1823
  documentReadiness: async (ticket, target) => {
1306
1824
  ticket.check();
1307
1825
  const page = current(target, "ready");
@@ -1313,7 +1831,7 @@ const makeScriptedBrowser = (script, timers) => {
1313
1831
  },
1314
1832
  dismissDialogs: async () => {},
1315
1833
  capture: async (target) => {
1316
- const page = target === void 0 ? selectedPage("capture") : pageOf(PageInfo.make({
1834
+ const page = pageOf(PageInfo.make({
1317
1835
  pageId: target.pageId,
1318
1836
  targetId: target.targetId,
1319
1837
  url: "",
@@ -1341,8 +1859,8 @@ const makeScriptedBrowser = (script, timers) => {
1341
1859
  }
1342
1860
  };
1343
1861
  },
1344
- invalidateObservation: (scope) => {
1345
- invalidateSnapshot(scope);
1862
+ invalidateObservation: (scope, origin) => {
1863
+ invalidateSnapshot(scope, origin);
1346
1864
  },
1347
1865
  disconnect: async () => {
1348
1866
  if (disconnected) return;
@@ -1359,8 +1877,8 @@ const makeScriptedBrowser = (script, timers) => {
1359
1877
  dropConnection(false, "closed");
1360
1878
  }
1361
1879
  };
1362
- const selectFilesOn = (target, files, ticket, record, operation) => {
1363
- const { node, page } = resolve(target, ticket, operation);
1880
+ const selectFilesOn = (target, files, ticket, record, operation, browserTarget) => {
1881
+ const { node, page } = resolve(target, ticket, operation, void 0, browserTarget);
1364
1882
  requireRunning(page, operation);
1365
1883
  if (node.script.kind !== "input" || node.script.inputType !== "file") throw fail(operation, Reasons.Unsupported.make({}));
1366
1884
  dispatch(ticket, record);
@@ -1368,12 +1886,43 @@ const makeScriptedBrowser = (script, timers) => {
1368
1886
  return page.document.url;
1369
1887
  };
1370
1888
  const connection = {
1889
+ opened: (pageId) => {
1890
+ const page = pages.get(pageId);
1891
+ if (page !== void 0) events.pageLifecycle?.({
1892
+ _tag: "Opened",
1893
+ page: cachedPage(page)
1894
+ });
1895
+ },
1896
+ navigated: (pageId, sameDocument) => {
1897
+ const page = pages.get(pageId);
1898
+ if (page !== void 0) events.pageLifecycle?.({
1899
+ _tag: "Navigated",
1900
+ page: cachedPage(page),
1901
+ frameId: page.frameId,
1902
+ documentEpoch: page.epoch,
1903
+ sameDocument,
1904
+ url: page.document.url.length <= 8192 ? page.document.url : null,
1905
+ urlQualification: page.document.url.length <= 8192 ? "NativeCached" : "Omitted"
1906
+ });
1907
+ },
1908
+ metadata: (pageId) => {
1909
+ const page = pages.get(pageId);
1910
+ if (page !== void 0) events.pageLifecycle?.({
1911
+ _tag: "Metadata",
1912
+ page: cachedPage(page)
1913
+ });
1914
+ },
1371
1915
  retire: (pageId) => invalidateSnapshot({ pageId }),
1916
+ closed: (pageId) => {
1917
+ const page = pages.get(pageId);
1918
+ if (page !== void 0) snapshots.delete(page.frameId);
1919
+ events.pageClosed?.(pageId, page === void 0 ? void 0 : cachedPage(page));
1920
+ },
1372
1921
  announce: (pageId) => events.invalidate("target-changed", { pageId }),
1373
1922
  drop: () => dropConnection(true),
1374
1923
  invoke: invokeBinding,
1375
1924
  pointer: () => pointer,
1376
- viewport: () => viewport
1925
+ viewport: (pageId) => pages.get(pageId)?.viewport ?? viewport
1377
1926
  };
1378
1927
  index = connections.push("open") - 1;
1379
1928
  live.add(connection);
@@ -1384,6 +1933,7 @@ const makeScriptedBrowser = (script, timers) => {
1384
1933
  url: document.url,
1385
1934
  title: document.title,
1386
1935
  text: document.text,
1936
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
1387
1937
  controls: document.controls.map((control) => ({
1388
1938
  ...control.script,
1389
1939
  ...control.checked === void 0 ? {} : { checked: control.checked },
@@ -1422,7 +1972,7 @@ const makeScriptedBrowser = (script, timers) => {
1422
1972
  const running = page.capture?.start;
1423
1973
  if (running === void 0) return Effect.fail(fail("capture-consume", Reasons.NotFound.make({})));
1424
1974
  const timestamp = frame.timestamp ?? lastTimestamp + 40;
1425
- const viewport = page.capture?.by.viewport() ?? {
1975
+ const viewport = page.capture?.by.viewport(page.pageId) ?? {
1426
1976
  width: 0,
1427
1977
  height: 0
1428
1978
  };
@@ -1535,23 +2085,24 @@ const makeEngine = Effect.fnUntraced(function* (script) {
1535
2085
  * Open one scripted browser in the caller's Scope. The session is the real owner over a
1536
2086
  * scripted native engine: admission, budgets, staleness, dispatch evidence, capture, page holds
1537
2087
  * and typed callbacks are the production code paths. Only the page and its outcomes are scripted,
1538
- * and the owner's ids and handoff tokens come from `sequentialCrypto`.
2088
+ * and the owner's ids and handoff tokens use deterministic Crypto namespaced by this
2089
+ * scripted reference, so independently opened sessions have distinct evidence domains.
1539
2090
  */
1540
2091
  const open = Effect.fnUntraced(function* (script, options = {}) {
1541
2092
  const fixed = yield* checked(Script, script, "configure");
1542
2093
  const policy = yield* checked(BrowserPolicy, options.policy ?? BrowserPolicy.unrestricted(), "configure");
1543
2094
  const engine = yield* makeEngine(fixed);
1544
- const runtime = yield* make({
2095
+ const namespace = ++references;
2096
+ const reference = Object.freeze(ScriptedReference.make({
2097
+ provider: "scripted",
2098
+ id: `scripted-${namespace}`
2099
+ }));
2100
+ const acquired = yield* (yield* make({
1545
2101
  implementation: "scripted",
1546
2102
  binding: engine.binding,
1547
2103
  ...options.automation === void 0 ? {} : { automation: options.automation },
1548
2104
  ...options.viewport === void 0 ? {} : { viewport: options.viewport }
1549
- }).pipe(Effect.provide(sequentialCrypto));
1550
- const reference = Object.freeze(ScriptedReference.make({
1551
- provider: "scripted",
1552
- id: `scripted-${++references}`
1553
- }));
1554
- const acquired = yield* runtime.acquire(policy, scriptedSource(reference, options.onCleanup), options.bootstrap === void 0 ? {} : { bootstrap: options.bootstrap });
2105
+ }).pipe(Effect.provide(Layer.sync(Crypto.Crypto, () => makeSequentialCrypto(namespace))))).acquire(policy, scriptedSource(reference, options.onCleanup), options.bootstrap === void 0 ? {} : { bootstrap: options.bootstrap });
1555
2106
  const connection = yield* acquired.connect;
1556
2107
  const control = engine.controls().at(-1);
1557
2108
  if (control === void 0) return yield* BrowserError.make({
@@ -1570,7 +2121,8 @@ const open = Effect.fnUntraced(function* (script, options = {}) {
1570
2121
  /**
1571
2122
  * The `Crypto` scripted browsers draw their ids and handoff tokens from. Its bytes count up from
1572
2123
  * one on each build, so every value is predictable; it is not random and computes no digest.
1573
- * `open` uses it already. A provider Layer built over `binding` still requires a `Crypto`, and
2124
+ * `open` uses the same counter with its original scripted-reference namespace. A provider
2125
+ * Layer built over `binding` still requires a `Crypto`, and
1574
2126
  * this one keeps such a test free of a platform package.
1575
2127
  */
1576
2128
  const sequentialCrypto = Layer.sync(Crypto.Crypto, makeSequentialCrypto);