effect-browser 0.2.0-beta.7 → 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 +486 -152
  2. package/dist/Browser-n-bKgBNN.d.mts +372 -0
  3. package/dist/Browser.d.mts +3 -200
  4. package/dist/Browser.mjs +9 -1
  5. package/dist/Browser.mjs.map +1 -1
  6. package/dist/{BrowserData-KHk4o__c.d.mts → BrowserData-CPIIIHol.d.mts} +81 -7
  7. package/dist/BrowserData.d.mts +2 -2
  8. package/dist/BrowserData.mjs +104 -6
  9. package/dist/BrowserData.mjs.map +1 -1
  10. package/dist/{BrowserRuntime-hxkTNlRa.d.mts → BrowserRuntime-CFe9uUTv.d.mts} +16 -11
  11. package/dist/{BrowserRuntime-Cr76CN4I.mjs → BrowserRuntime-HJ1yYlFS.mjs} +4990 -4309
  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 +66 -17
  26. package/dist/Errors.mjs +47 -5
  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 +651 -113
  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-CH3kPHXE.mjs +0 -167
  64. package/dist/Association-CH3kPHXE.mjs.map +0 -1
  65. package/dist/BrowserRuntime-Cr76CN4I.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-Cr76CN4I.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",
@@ -173,7 +190,6 @@ const ScriptableOperation = Schema.Literals([
173
190
  "describe-page",
174
191
  "list-frames",
175
192
  "select-page",
176
- "select-frame",
177
193
  "new-page",
178
194
  "close-page",
179
195
  "resize",
@@ -301,6 +317,7 @@ const makeScriptedBrowser = (script, timers) => {
301
317
  url: document.url,
302
318
  title: document.title ?? "",
303
319
  text: document.text,
320
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
304
321
  controls: (document.controls ?? []).map(controlState)
305
322
  });
306
323
  const documentFor = (url) => {
@@ -321,6 +338,11 @@ const makeScriptedBrowser = (script, timers) => {
321
338
  registeredEpoch: registered ? 0 : -1,
322
339
  values: /* @__PURE__ */ new Map(),
323
340
  files: /* @__PURE__ */ new Map(),
341
+ scroll: {
342
+ x: 0,
343
+ y: 0
344
+ },
345
+ mutations: 0,
324
346
  focused: void 0,
325
347
  closed: false,
326
348
  held: void 0,
@@ -329,6 +351,7 @@ const makeScriptedBrowser = (script, timers) => {
329
351
  capture: void 0
330
352
  };
331
353
  pages.set(page.pageId, page);
354
+ for (const connection of live) connection.opened(page.pageId);
332
355
  return page;
333
356
  };
334
357
  const openPages = () => [...pages.values()].filter((page) => !page.closed);
@@ -339,12 +362,17 @@ const makeScriptedBrowser = (script, timers) => {
339
362
  /** The page shows a new document: nodes go stale, values reset, captures learn of it. */
340
363
  const commit = (page, document) => {
341
364
  page.document = document;
365
+ page.cachedTitle = void 0;
342
366
  page.epoch++;
343
367
  page.values.clear();
344
368
  page.files.clear();
345
369
  page.focused = void 0;
370
+ page.scroll.x = 0;
371
+ page.scroll.y = 0;
372
+ page.mutations++;
346
373
  page.navigation = void 0;
347
374
  for (const connection of live) connection.retire(page.pageId);
375
+ for (const connection of live) connection.navigated(page.pageId, false);
348
376
  if (page.capture !== void 0) {
349
377
  const { start } = page.capture;
350
378
  if (start.document === void 0) start.invalidate("target-changed");
@@ -355,6 +383,8 @@ const makeScriptedBrowser = (script, timers) => {
355
383
  };
356
384
  /** In-document change: identity survives by control id, so retained nodes and waits carry on. */
357
385
  const update = (page, document) => {
386
+ const previousUrl = page.document.url;
387
+ const before = JSON.stringify(page.document);
358
388
  const previous = new Map(page.document.controls.map((control) => [control.script.id, control]));
359
389
  const controls = (document.controls ?? []).map((script) => {
360
390
  const existing = previous.get(script.id);
@@ -368,9 +398,15 @@ const makeScriptedBrowser = (script, timers) => {
368
398
  url: document.url,
369
399
  title: document.title ?? "",
370
400
  text: document.text,
401
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
371
402
  controls
372
403
  };
404
+ if (JSON.stringify(page.document) !== before) page.mutations++;
373
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
+ }
374
410
  notify(page);
375
411
  };
376
412
  const take = (operation) => {
@@ -393,11 +429,13 @@ const makeScriptedBrowser = (script, timers) => {
393
429
  throw new Error("The scripted browser refused this connection");
394
430
  }
395
431
  const bindings = options.bindings ?? [];
396
- let viewport = { ...options.viewport };
432
+ const viewport = { ...options.viewport };
397
433
  const positions = /* @__PURE__ */ new Map();
398
434
  let pointer = null;
399
435
  let selectedId;
400
- let snapshot;
436
+ const snapshots = /* @__PURE__ */ new Map();
437
+ const privateSnapshots = /* @__PURE__ */ new Set();
438
+ const resolvedElements = /* @__PURE__ */ new WeakMap();
401
439
  let disconnected = false;
402
440
  let index = -1;
403
441
  const place = (pageId, point) => {
@@ -411,8 +449,16 @@ const makeScriptedBrowser = (script, timers) => {
411
449
  const dispatchObservers = /* @__PURE__ */ new WeakMap();
412
450
  const receipt = (pageId) => ({ position: positions.get(pageId) ?? null });
413
451
  const select = (pageId) => {
452
+ const previous = selectedId;
414
453
  selectedId = pageId;
415
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
+ }
416
462
  };
417
463
  const selectedPage = (operation = "target") => {
418
464
  const page = selectedId === void 0 ? void 0 : pages.get(selectedId);
@@ -420,7 +466,6 @@ const makeScriptedBrowser = (script, timers) => {
420
466
  return page;
421
467
  };
422
468
  const current = (target, operation) => {
423
- if (target === void 0) return selectedPage(operation);
424
469
  if (disconnected) throw fail(operation, Reasons.Closed.make({}));
425
470
  const page = pages.get(target.pageId);
426
471
  if (page === void 0 || page.closed || page.frameId !== target.frameId) throw fail(operation, Reasons.Stale.make({}));
@@ -432,21 +477,66 @@ const makeScriptedBrowser = (script, timers) => {
432
477
  if (page.closed || page.targetId !== info.targetId) throw fail(operation, Reasons.Stale.make({}));
433
478
  return page;
434
479
  };
435
- const info = (page) => PageInfo.make({
480
+ const cachedPage = (page) => Object.freeze({
436
481
  pageId: page.pageId,
482
+ frameId: page.frameId,
437
483
  targetId: page.targetId,
438
- url: page.document.url,
439
- title: page.document.title,
440
- 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"
441
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
+ };
442
529
  const frameInfo = (page) => FrameInfo.make({
443
530
  frameId: page.frameId,
444
531
  parentFrameId: null,
445
532
  url: page.document.url,
446
533
  name: ""
447
534
  });
448
- const invalidateSnapshot = (scope = "all") => {
449
- 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
+ }
450
540
  };
451
541
  /** The page closes as a browser closes it: every connection learns, and nothing on it lasts. */
452
542
  const close = (target) => {
@@ -456,11 +546,12 @@ const makeScriptedBrowser = (script, timers) => {
456
546
  target.capture = void 0;
457
547
  notify(target);
458
548
  if (selectedId === target.pageId) select(void 0);
459
- invalidateSnapshot({ pageId: target.pageId });
549
+ snapshots.delete(target.frameId);
550
+ events.pageClosed?.(target.pageId, cachedPage(target));
460
551
  events.invalidate("target-changed", { pageId: target.pageId });
461
552
  for (const other of live) {
462
553
  if (other === connection) continue;
463
- other.retire(target.pageId);
554
+ other.closed(target.pageId);
464
555
  other.announce(target.pageId);
465
556
  }
466
557
  };
@@ -468,6 +559,9 @@ const makeScriptedBrowser = (script, timers) => {
468
559
  const dropConnection = (announce, ended = "dropped") => {
469
560
  if (disconnected) return;
470
561
  disconnected = true;
562
+ invalidateSnapshot();
563
+ snapshots.clear();
564
+ privateSnapshots.clear();
471
565
  connections[index] = ended;
472
566
  live.delete(connection);
473
567
  for (const page of pages.values()) {
@@ -477,6 +571,7 @@ const makeScriptedBrowser = (script, timers) => {
477
571
  notify(page);
478
572
  }
479
573
  if (announce) events.disconnected();
574
+ events.retired?.();
480
575
  };
481
576
  const dispatch = (ticket, record) => {
482
577
  if (record.dispatched) return;
@@ -549,13 +644,14 @@ const makeScriptedBrowser = (script, timers) => {
549
644
  if (ticket !== void 0) await applyArmed(operation, ticket, record);
550
645
  const value = await body(record);
551
646
  record.settled = "completed";
647
+ if (ticket !== void 0 && isTicket(ticket)) ticket.acknowledge?.();
552
648
  return value;
553
649
  } catch (error) {
554
650
  record.settled = "failed";
555
651
  throw error;
556
652
  }
557
653
  };
558
- 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 };
559
655
  const facts = (control) => {
560
656
  const { script: value } = control;
561
657
  const extra = value.facts ?? {};
@@ -570,7 +666,7 @@ const makeScriptedBrowser = (script, timers) => {
570
666
  editable: extra.editable ?? isEditable(value),
571
667
  ...value.inputType === void 0 ? {} : { inputType: value.inputType },
572
668
  ...extra.autocomplete === void 0 ? {} : { autocomplete: extra.autocomplete },
573
- ...value.destination === void 0 ? {} : { destination: value.destination },
669
+ ...value.destination === void 0 || value.destination.length > 2048 ? {} : { destination: value.destination },
574
670
  ...extra.formMethod === void 0 ? {} : { formMethod: extra.formMethod },
575
671
  box: ViewportRect.make(extra.box ?? {
576
672
  x: 0,
@@ -605,33 +701,62 @@ const makeScriptedBrowser = (script, timers) => {
605
701
  return control;
606
702
  };
607
703
  /** A retained node is only as current as the observation that produced it. */
608
- const retained = (target, ticket, operation, allowSuspended = false, attached = true) => {
704
+ const retained = (target, ticket, operation, allowSuspended = false, attached = true, browserTarget) => {
609
705
  ticket.check();
610
706
  const stale = () => fail(operation, Reasons.Stale.make({}));
611
- const current = snapshot;
612
- if (current === void 0 || current.id !== target.observationId) throw stale();
613
- 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);
614
711
  if (node === void 0) throw stale();
615
- if (current.validity === "invalid" || current.generation !== ticket.generation) throw stale();
616
- const page = pages.get(current.pageId);
617
- if (page === void 0 || page.closed || page.epoch !== current.epoch) throw stale();
618
- if (selectedId !== current.pageId) throw stale();
619
- 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();
620
715
  if (attached && !page.document.controls.includes(node)) throw stale();
621
716
  return {
622
717
  node,
623
718
  page,
624
- snapshot: current
719
+ snapshot
720
+ };
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
625
730
  };
626
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
+ });
627
746
  const resolve = (target, ticket, operation, policy, browserTarget, allowSuspended = false, enablement = false) => {
628
747
  let node;
629
748
  let page;
630
749
  if (typeof target === "string") {
631
750
  page = current(browserTarget, operation);
632
751
  node = bySelector(page, target, operation);
633
- } else ({node, page} = retained(target, ticket, operation, allowSuspended));
752
+ } else ({node, page} = retainedElement(target, ticket, operation, allowSuspended, true, browserTarget));
634
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({}));
635
760
  if (enablement && node.script.disabled === true) throw fail(operation, Reasons.Disabled.make({}));
636
761
  if (policy !== void 0) {
637
762
  let admitted = false;
@@ -643,17 +768,189 @@ const makeScriptedBrowser = (script, timers) => {
643
768
  if (!admitted) throw fail(operation, Reasons.Denied.make({}));
644
769
  }
645
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
+ }
646
775
  return {
647
776
  node,
648
777
  page,
649
778
  facts: fresh
650
779
  };
651
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
+ };
652
949
  const requireRunning = (page, operation) => {
653
950
  if (page.held !== void 0) throw fail(operation, Reasons.Busy.make({}));
654
951
  };
655
952
  const heldObservation = (pageId) => {
656
- if (snapshot?.pageId === pageId && snapshot.validity === "valid") {
953
+ for (const snapshot of snapshots.values()) if (snapshot.pageId === pageId && snapshot.validity === "valid") {
657
954
  snapshot.validity = "suspended";
658
955
  snapshot.revalidated.clear();
659
956
  }
@@ -678,13 +975,29 @@ const makeScriptedBrowser = (script, timers) => {
678
975
  if (focused !== void 0 && isEditable(focused.script)) page.values.set(focused.script.id, (page.values.get(focused.script.id) ?? "") + text);
679
976
  };
680
977
  /** Issued options of one select, refused exactly where native selection refuses them. */
681
- const chosen = (node, ids, operation) => {
682
- const current = snapshot;
683
- if (current === void 0) throw fail(operation, Reasons.Stale.make({}));
978
+ const chosen = (page, node, ids, ticket, operation, enablement = false) => {
684
979
  if (!isSelectable(node.script.kind)) throw fail(operation, Reasons.Unsupported.make({}));
685
980
  const options = ids.map((id) => {
686
- const option = current.nodes.get(id);
687
- 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));
688
1001
  return option;
689
1002
  });
690
1003
  if (node.script.disabled === true || options.some((option) => option.script.disabled === true)) throw fail(operation, Reasons.Disabled.make({}));
@@ -712,7 +1025,7 @@ const makeScriptedBrowser = (script, timers) => {
712
1025
  let act;
713
1026
  let input;
714
1027
  if (field.options !== void 0) {
715
- const options = chosen(node, field.options, "fill-form");
1028
+ const options = chosen(page, node, field.options, ticket, "fill-form", true);
716
1029
  act = () => choose(page, node, options);
717
1030
  } else if (field.checked !== void 0) {
718
1031
  if (node.checked === void 0) throw fail("fill-form", Reasons.Unsupported.make({}));
@@ -818,6 +1131,98 @@ const makeScriptedBrowser = (script, timers) => {
818
1131
  page.watchers.add(check);
819
1132
  check();
820
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
+ });
821
1226
  const invokeBinding = async (name, input, origin) => {
822
1227
  const binding = bindings.find((candidate) => candidate.name === name);
823
1228
  if (binding === void 0) return { ok: false };
@@ -837,6 +1242,7 @@ const makeScriptedBrowser = (script, timers) => {
837
1242
  };
838
1243
  try {
839
1244
  const reply = await binding.invoke({
1245
+ origin: documentOrigin,
840
1246
  read: async (signal) => {
841
1247
  await check(signal);
842
1248
  return text;
@@ -874,6 +1280,7 @@ const makeScriptedBrowser = (script, timers) => {
874
1280
  select(previous !== void 0 && !previous.closed ? previous.pageId : openPages()[0]?.pageId);
875
1281
  }
876
1282
  const driver = {
1283
+ cachedPages: () => Object.freeze(openPages().map(cachedPage)),
877
1284
  ...options.pageControl === true ? { pageControl: {
878
1285
  state: async (page, ticket) => {
879
1286
  ticket.check();
@@ -894,6 +1301,11 @@ const makeScriptedBrowser = (script, timers) => {
894
1301
  by: connection
895
1302
  };
896
1303
  heldObservation(target.pageId);
1304
+ ticket.acknowledge?.();
1305
+ events.pageLifecycle?.({
1306
+ _tag: "Display",
1307
+ page: cachedPage(target)
1308
+ });
897
1309
  return PageSuspension.make({
898
1310
  pageId: target.pageId,
899
1311
  targetId: target.targetId,
@@ -906,6 +1318,11 @@ const makeScriptedBrowser = (script, timers) => {
906
1318
  dispatch(ticket, record);
907
1319
  target.held = void 0;
908
1320
  heldObservation(target.pageId);
1321
+ ticket.acknowledge?.();
1322
+ events.pageLifecycle?.({
1323
+ _tag: "Display",
1324
+ page: cachedPage(target)
1325
+ });
909
1326
  }),
910
1327
  checkTarget: async (target, ticket) => {
911
1328
  ticket.check();
@@ -947,11 +1364,11 @@ const makeScriptedBrowser = (script, timers) => {
947
1364
  text: ""
948
1365
  }), false));
949
1366
  }),
950
- 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) => {
951
1368
  const target = pageOf(page, "close-page");
952
1369
  dispatch(ticket, record);
953
1370
  close(target);
954
- }),
1371
+ }, onDispatch),
955
1372
  containPage: (pageId) => attempt("close-page", void 0, { pageId }, async (record) => {
956
1373
  const target = pages.get(pageId);
957
1374
  if (target === void 0 || target.closed) return;
@@ -960,7 +1377,7 @@ const makeScriptedBrowser = (script, timers) => {
960
1377
  if (outcome?._tag === "Fail") throw fail("close-page", outcome.reason, outcome.outcome);
961
1378
  close(target);
962
1379
  }),
963
- 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"))]),
964
1381
  resolveFrame: async (page, frame, ticket) => {
965
1382
  ticket.check();
966
1383
  const target = pageOf(page, "target");
@@ -970,11 +1387,7 @@ const makeScriptedBrowser = (script, timers) => {
970
1387
  frameId: target.frameId
971
1388
  };
972
1389
  },
973
- selectFrame: (id, ticket) => attempt("select-frame", ticket, {}, async () => {
974
- if (id !== selectedPage("select-frame").frameId) throw fail("select-frame", Reasons.NotFound.make({}));
975
- events.invalidate("target-changed", "none");
976
- }),
977
- 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) => {
978
1391
  ticket.check();
979
1392
  const page = current(target, "navigate");
980
1393
  requireRunning(page, "navigate");
@@ -1037,7 +1450,7 @@ const makeScriptedBrowser = (script, timers) => {
1037
1450
  };
1038
1451
  }),
1039
1452
  readText: (selector, maximumBytes, ticket, target) => attempt("read-text", ticket, {
1040
- pageId: target?.pageId,
1453
+ pageId: target.pageId,
1041
1454
  ...meta(selector)
1042
1455
  }, async () => {
1043
1456
  const page = current(target, "read-text");
@@ -1045,11 +1458,12 @@ const makeScriptedBrowser = (script, timers) => {
1045
1458
  const control = bySelector(page, selector, "read-text");
1046
1459
  return bounded(control.script.text ?? control.script.label, maximumBytes).text;
1047
1460
  }),
1048
- observe: (scope, maximumBytes, controls, ticket, match) => attempt("observe", ticket, {}, async () => {
1049
- 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;
1050
1464
  const needle = match?.toLowerCase();
1051
1465
  const matches = (value) => needle === void 0 || value.toLowerCase().includes(needle);
1052
- snapshot = void 0;
1466
+ snapshots.delete(page.frameId);
1053
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));
1054
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));
1055
1469
  const reachable = matching.filter((control) => scope === "document" || control.script.offscreen !== true);
@@ -1058,13 +1472,22 @@ const makeScriptedBrowser = (script, timers) => {
1058
1472
  const next = {
1059
1473
  id: `observation-${++observationSerial}`,
1060
1474
  pageId: page.pageId,
1475
+ frameId: page.frameId,
1061
1476
  epoch: page.epoch,
1062
1477
  generation: ticket.generation,
1478
+ scope,
1063
1479
  validity: "valid",
1064
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
+ })),
1065
1488
  revalidated: /* @__PURE__ */ new Set()
1066
1489
  };
1067
- snapshot = next;
1490
+ snapshots.set(page.frameId, next);
1068
1491
  return {
1069
1492
  observationId: next.id,
1070
1493
  scope,
@@ -1075,8 +1498,9 @@ const makeScriptedBrowser = (script, timers) => {
1075
1498
  controls: kept.map(observed),
1076
1499
  controlsTruncated: kept.length < reachable.length,
1077
1500
  viewport: ViewportEvidence.make({
1078
- width: viewport.width,
1079
- height: viewport.height,
1501
+ width: size.width,
1502
+ height: size.height,
1503
+ ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1080
1504
  clippedText: 0,
1081
1505
  coveredText: 0,
1082
1506
  uncertainText: 0,
@@ -1085,8 +1509,9 @@ const makeScriptedBrowser = (script, timers) => {
1085
1509
  })
1086
1510
  };
1087
1511
  }),
1088
- checkpoint: (maximumBytes, controls, pictureBytes, ticket) => attempt("checkpoint", ticket, {}, async () => {
1089
- 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;
1090
1515
  const text = bounded(page.document.text, maximumBytes);
1091
1516
  const reachable = page.document.controls.filter((control) => control.script.offscreen !== true);
1092
1517
  return {
@@ -1096,8 +1521,9 @@ const makeScriptedBrowser = (script, timers) => {
1096
1521
  controls: reachable.slice(0, controls).map(facts),
1097
1522
  controlsTruncated: reachable.length > controls,
1098
1523
  viewport: ViewportEvidence.make({
1099
- width: viewport.width,
1100
- height: viewport.height,
1524
+ width: size.width,
1525
+ height: size.height,
1526
+ ...page.document.documentScroll === void 0 ? {} : { documentScroll: { ...page.document.documentScroll } },
1101
1527
  clippedText: 0,
1102
1528
  coveredText: 0,
1103
1529
  uncertainText: 0,
@@ -1105,19 +1531,53 @@ const makeScriptedBrowser = (script, timers) => {
1105
1531
  exhausted: false
1106
1532
  }),
1107
1533
  documentChanged: false,
1108
- ...pictureBytes === void 0 ? {} : { picture: new Uint8Array(PNG) }
1534
+ ...pictureBytes === void 0 ? {} : { picture: picture(page, ticket) }
1109
1535
  };
1110
1536
  }),
1111
- controlFacts: (target, ticket) => attempt("control-facts", ticket, meta(target), async () => {
1112
- selectedPage("control-facts");
1113
- 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
+ }
1114
1569
  }),
1115
- revalidate: (target, ticket) => attempt("revalidate", ticket, meta(target), async () => {
1116
- 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);
1117
1577
  current.revalidated.add(target.elementId);
1118
1578
  }),
1119
1579
  click: (target, ticket, capture, policy, browserTarget) => attempt("click", ticket, {
1120
- pageId: browserTarget?.pageId,
1580
+ pageId: browserTarget.pageId,
1121
1581
  ...meta(target)
1122
1582
  }, async (record) => {
1123
1583
  const { node, page } = resolve(target, ticket, "click", policy, browserTarget);
@@ -1131,9 +1591,9 @@ const makeScriptedBrowser = (script, timers) => {
1131
1591
  url,
1132
1592
  input
1133
1593
  };
1134
- }, () => invalidatePointer(browserTarget?.pageId ?? selectedId ?? "")),
1594
+ }, () => invalidatePointer(browserTarget.pageId)),
1135
1595
  fill: (target, value, ticket, policy, browserTarget) => attempt("fill", ticket, {
1136
- pageId: browserTarget?.pageId,
1596
+ pageId: browserTarget.pageId,
1137
1597
  ...meta(target)
1138
1598
  }, async (record) => {
1139
1599
  const { node, page } = resolve(target, ticket, "fill", policy, browserTarget);
@@ -1145,25 +1605,34 @@ const makeScriptedBrowser = (script, timers) => {
1145
1605
  page.values.set(node.script.id, value);
1146
1606
  return page.document.url;
1147
1607
  }),
1148
- selectOption: (target, ids, ticket, policy) => attempt("select-option", ticket, meta(target), async (record) => {
1149
- 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);
1150
1613
  requireRunning(page, "select-option");
1151
- const options = chosen(node, ids, "select-option");
1614
+ const options = chosen(page, node, ids, ticket, "select-option");
1152
1615
  dispatch(ticket, record);
1153
1616
  choose(page, node, options);
1154
1617
  return page.document.url;
1155
1618
  }),
1156
- formStep: (target, field, ticket, policy, _settleMillis, capture) => attempt("fill-form", ticket, meta(target), async (record) => {
1157
- 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);
1158
1624
  requireRunning(page, "fill-form");
1159
1625
  return formStep(page, node, field, ticket, record, capture);
1160
- }, field.checked === void 0 ? void 0 : () => invalidatePointer(selectedId ?? "")),
1161
- formState: (targets, ticket) => attempt("fill-form", ticket, {}, async () => targets.map((target) => {
1162
- 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);
1163
1629
  return page.document.controls.includes(node) ? fieldState(page, node) : void 0;
1164
1630
  })),
1165
- formSubmit: (target, ticket, capture, policy) => attempt("fill-form", ticket, meta(target), async (record) => {
1166
- 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);
1167
1636
  requireRunning(page, "fill-form");
1168
1637
  dispatch(ticket, record);
1169
1638
  let url = "";
@@ -1174,14 +1643,29 @@ const makeScriptedBrowser = (script, timers) => {
1174
1643
  url,
1175
1644
  input
1176
1645
  };
1177
- }, () => invalidatePointer(selectedId ?? "")),
1178
- 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) => {
1179
1648
  const page = current(target, "scroll");
1180
1649
  requireRunning(page, "scroll");
1181
1650
  dispatch(ticket, record);
1651
+ page.scroll.x += deltaX;
1652
+ page.scroll.y += deltaY;
1653
+ notify(page);
1182
1654
  return page.document.url;
1183
1655
  }),
1184
- pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target?.pageId }, async (record) => {
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);
1666
+ return page.document.url;
1667
+ }),
1668
+ pointerMove: (to, ticket, target) => attempt("pointer-move", ticket, { pageId: target.pageId }, async (record) => {
1185
1669
  const page = current(target, "pointer-move");
1186
1670
  requireRunning(page, "pointer-move");
1187
1671
  dispatch(ticket, record);
@@ -1192,7 +1676,7 @@ const makeScriptedBrowser = (script, timers) => {
1192
1676
  return receipt(page.pageId);
1193
1677
  }),
1194
1678
  hover: (target, ticket, policy, browserTarget) => attempt("hover", ticket, {
1195
- pageId: browserTarget?.pageId,
1679
+ pageId: browserTarget.pageId,
1196
1680
  ...meta(target)
1197
1681
  }, async (record) => {
1198
1682
  const { node, page, facts: fresh } = resolve(target, ticket, "hover", policy, browserTarget);
@@ -1205,7 +1689,7 @@ const makeScriptedBrowser = (script, timers) => {
1205
1689
  });
1206
1690
  return receipt(page.pageId);
1207
1691
  }),
1208
- 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) => {
1209
1693
  const page = current(target, "wheel");
1210
1694
  requireRunning(page, "wheel");
1211
1695
  dispatch(ticket, record);
@@ -1213,10 +1697,13 @@ const makeScriptedBrowser = (script, timers) => {
1213
1697
  x: at.x,
1214
1698
  y: at.y
1215
1699
  });
1700
+ page.scroll.x += deltaX;
1701
+ page.scroll.y += deltaY;
1702
+ notify(page);
1216
1703
  return receipt(page.pageId);
1217
1704
  }),
1218
1705
  press: (key, _modifiers, into, ticket, policy, browserTarget) => attempt("press", ticket, {
1219
- pageId: browserTarget?.pageId,
1706
+ pageId: browserTarget.pageId,
1220
1707
  ...meta(into)
1221
1708
  }, async (record) => {
1222
1709
  const page = focusedOrRefuse(into, ticket, "press", policy, browserTarget);
@@ -1226,7 +1713,7 @@ const makeScriptedBrowser = (script, timers) => {
1226
1713
  return receipt(page.pageId);
1227
1714
  }),
1228
1715
  type: (text, into, ticket, policy, browserTarget) => attempt("type", ticket, {
1229
- pageId: browserTarget?.pageId,
1716
+ pageId: browserTarget.pageId,
1230
1717
  ...meta(into)
1231
1718
  }, async (record) => {
1232
1719
  const page = focusedOrRefuse(into, ticket, "type", policy, browserTarget);
@@ -1235,19 +1722,19 @@ const makeScriptedBrowser = (script, timers) => {
1235
1722
  typed(page, text);
1236
1723
  return receipt(page.pageId);
1237
1724
  }),
1238
- screenshot: (_fullPage, _maximumBytes, ticket, target) => attempt("screenshot", ticket, { pageId: target?.pageId }, async () => {
1239
- current(target, "screenshot");
1240
- 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);
1241
1728
  }),
1242
- resize: (next, ticket) => attempt("resize", ticket, {}, async (record) => {
1243
- const page = selectedPage("resize");
1729
+ resize: (next, ticket, target) => attempt("resize", ticket, { pageId: target.pageId }, async (record) => {
1730
+ const page = current(target, "resize");
1244
1731
  dispatch(ticket, record);
1245
- viewport = {
1732
+ page.viewport = {
1246
1733
  width: next.width,
1247
1734
  height: next.height
1248
1735
  };
1249
1736
  page.capture?.start.invalidate("resized");
1250
- events.invalidate("resized");
1737
+ events.invalidate("resized", { pageId: page.pageId });
1251
1738
  }),
1252
1739
  waitFor: (selector, state, ticket, target) => attempt("wait", ticket, {
1253
1740
  pageId: target.pageId,
@@ -1271,12 +1758,17 @@ const makeScriptedBrowser = (script, timers) => {
1271
1758
  }
1272
1759
  }, () => page.closed || page.epoch !== epoch);
1273
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()),
1274
1766
  waitForElement: (reference, state, ticket, target) => attempt("wait", ticket, {
1275
1767
  pageId: target.pageId,
1276
- elementId: reference.elementId
1768
+ ...meta(reference)
1277
1769
  }, async (record) => {
1278
1770
  const page = current(target, "wait");
1279
- const { node } = retained(reference, ticket, "wait", true);
1771
+ const { node } = retainedElement(reference, ticket, "wait", true, true, target);
1280
1772
  const epoch = page.epoch;
1281
1773
  await waitUntil(ticket, page, "wait", record, () => {
1282
1774
  const present = page.document.controls.includes(node);
@@ -1289,8 +1781,11 @@ const makeScriptedBrowser = (script, timers) => {
1289
1781
  }
1290
1782
  }, () => page.closed || page.epoch !== epoch);
1291
1783
  }).finally(() => ticket.retire()),
1292
- clickAndWait: (target, ticket, capture) => attempt("click-and-wait", ticket, meta(target), async (record) => {
1293
- 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);
1294
1789
  requireRunning(page, "click-and-wait");
1295
1790
  dispatch(ticket, record);
1296
1791
  let url = "";
@@ -1301,9 +1796,12 @@ const makeScriptedBrowser = (script, timers) => {
1301
1796
  url,
1302
1797
  input
1303
1798
  };
1304
- }, () => invalidatePointer(selectedId ?? "")),
1305
- clickForDownload: (target, ticket) => attempt("download-action", ticket, meta(target), async (record) => {
1306
- 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);
1307
1805
  requireRunning(page, "download-action");
1308
1806
  if (node.script.download === void 0) throw fail("download-action", Reasons.Unsupported.make({}));
1309
1807
  dispatch(ticket, record);
@@ -1314,8 +1812,14 @@ const makeScriptedBrowser = (script, timers) => {
1314
1812
  state: "completed"
1315
1813
  };
1316
1814
  }),
1317
- selectFiles: (target, files, ticket) => attempt("select-files", ticket, meta(target), async (record) => selectFilesOn(target, files, ticket, record, "select-files")),
1318
- 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)),
1319
1823
  documentReadiness: async (ticket, target) => {
1320
1824
  ticket.check();
1321
1825
  const page = current(target, "ready");
@@ -1327,7 +1831,7 @@ const makeScriptedBrowser = (script, timers) => {
1327
1831
  },
1328
1832
  dismissDialogs: async () => {},
1329
1833
  capture: async (target) => {
1330
- const page = target === void 0 ? selectedPage("capture") : pageOf(PageInfo.make({
1834
+ const page = pageOf(PageInfo.make({
1331
1835
  pageId: target.pageId,
1332
1836
  targetId: target.targetId,
1333
1837
  url: "",
@@ -1355,8 +1859,8 @@ const makeScriptedBrowser = (script, timers) => {
1355
1859
  }
1356
1860
  };
1357
1861
  },
1358
- invalidateObservation: (scope) => {
1359
- invalidateSnapshot(scope);
1862
+ invalidateObservation: (scope, origin) => {
1863
+ invalidateSnapshot(scope, origin);
1360
1864
  },
1361
1865
  disconnect: async () => {
1362
1866
  if (disconnected) return;
@@ -1373,8 +1877,8 @@ const makeScriptedBrowser = (script, timers) => {
1373
1877
  dropConnection(false, "closed");
1374
1878
  }
1375
1879
  };
1376
- const selectFilesOn = (target, files, ticket, record, operation) => {
1377
- 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);
1378
1882
  requireRunning(page, operation);
1379
1883
  if (node.script.kind !== "input" || node.script.inputType !== "file") throw fail(operation, Reasons.Unsupported.make({}));
1380
1884
  dispatch(ticket, record);
@@ -1382,12 +1886,43 @@ const makeScriptedBrowser = (script, timers) => {
1382
1886
  return page.document.url;
1383
1887
  };
1384
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
+ },
1385
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
+ },
1386
1921
  announce: (pageId) => events.invalidate("target-changed", { pageId }),
1387
1922
  drop: () => dropConnection(true),
1388
1923
  invoke: invokeBinding,
1389
1924
  pointer: () => pointer,
1390
- viewport: () => viewport
1925
+ viewport: (pageId) => pages.get(pageId)?.viewport ?? viewport
1391
1926
  };
1392
1927
  index = connections.push("open") - 1;
1393
1928
  live.add(connection);
@@ -1398,6 +1933,7 @@ const makeScriptedBrowser = (script, timers) => {
1398
1933
  url: document.url,
1399
1934
  title: document.title,
1400
1935
  text: document.text,
1936
+ ...document.documentScroll === void 0 ? {} : { documentScroll: { ...document.documentScroll } },
1401
1937
  controls: document.controls.map((control) => ({
1402
1938
  ...control.script,
1403
1939
  ...control.checked === void 0 ? {} : { checked: control.checked },
@@ -1436,7 +1972,7 @@ const makeScriptedBrowser = (script, timers) => {
1436
1972
  const running = page.capture?.start;
1437
1973
  if (running === void 0) return Effect.fail(fail("capture-consume", Reasons.NotFound.make({})));
1438
1974
  const timestamp = frame.timestamp ?? lastTimestamp + 40;
1439
- const viewport = page.capture?.by.viewport() ?? {
1975
+ const viewport = page.capture?.by.viewport(page.pageId) ?? {
1440
1976
  width: 0,
1441
1977
  height: 0
1442
1978
  };
@@ -1549,23 +2085,24 @@ const makeEngine = Effect.fnUntraced(function* (script) {
1549
2085
  * Open one scripted browser in the caller's Scope. The session is the real owner over a
1550
2086
  * scripted native engine: admission, budgets, staleness, dispatch evidence, capture, page holds
1551
2087
  * and typed callbacks are the production code paths. Only the page and its outcomes are scripted,
1552
- * 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.
1553
2090
  */
1554
2091
  const open = Effect.fnUntraced(function* (script, options = {}) {
1555
2092
  const fixed = yield* checked(Script, script, "configure");
1556
2093
  const policy = yield* checked(BrowserPolicy, options.policy ?? BrowserPolicy.unrestricted(), "configure");
1557
2094
  const engine = yield* makeEngine(fixed);
1558
- 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({
1559
2101
  implementation: "scripted",
1560
2102
  binding: engine.binding,
1561
2103
  ...options.automation === void 0 ? {} : { automation: options.automation },
1562
2104
  ...options.viewport === void 0 ? {} : { viewport: options.viewport }
1563
- }).pipe(Effect.provide(sequentialCrypto));
1564
- const reference = Object.freeze(ScriptedReference.make({
1565
- provider: "scripted",
1566
- id: `scripted-${++references}`
1567
- }));
1568
- 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 });
1569
2106
  const connection = yield* acquired.connect;
1570
2107
  const control = engine.controls().at(-1);
1571
2108
  if (control === void 0) return yield* BrowserError.make({
@@ -1584,7 +2121,8 @@ const open = Effect.fnUntraced(function* (script, options = {}) {
1584
2121
  /**
1585
2122
  * The `Crypto` scripted browsers draw their ids and handoff tokens from. Its bytes count up from
1586
2123
  * one on each build, so every value is predictable; it is not random and computes no digest.
1587
- * `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
1588
2126
  * this one keeps such a test free of a platform package.
1589
2127
  */
1590
2128
  const sequentialCrypto = Layer.sync(Crypto.Crypto, makeSequentialCrypto);