effect-browser 0.2.0-beta.5 → 0.2.0-beta.7

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.
@@ -1,7 +1,7 @@
1
1
  import { BrowserError, InitializationError, Reasons } from "./Errors.mjs";
2
2
  import { ActionResult, AutomationOptions, BrowserDiagnostic, BrowserDiagnostics, BrowserPolicy, Checkpoint, CheckpointOptions, ClickRequest, ControlFacts, FillFormOptions, FillFormRequest, FillFormResult, FillRequest, FrameInfo, HoverRequest, Identifier, InlineFiles, InputReceipt, KeyStroke, NavigateRequest, NavigationResult, Observation, ObservationOptions, ObservedControl, ObservedElement, PageExecutionState, PageInfo, PageSuspension, PointerMoveRequest, PressRequest, ReadTextRequest, SafeFilename, ScreenshotRequest, ScreenshotResult, ScrollRequest, SelectOptions, Selector, SessionStatus, StartNavigationRequest, Target, TextResult as TextResult$1, TypeRequest, Viewport, WaitForElementRequest, WheelRequest } from "./BrowserData.mjs";
3
3
  import { m as snapshot, o as Plan, p as invoke, u as empty } from "./Bootstrap-CeQOWBy6.mjs";
4
- import { a as NativeFailure, c as publicError, d as timeout, i as pngGeometry, l as safeDecode, o as closeWithin, s as failure, t as associate, u as sanitize } from "./Association-BsSfBYVx.mjs";
4
+ import { a as NativeFailure, c as providerReason, d as sanitize, f as timeout, i as pngGeometry, l as publicError, o as closeWithin, s as failure, t as associate, u as safeDecode } from "./Association-CH3kPHXE.mjs";
5
5
  import { t as associatePageControl } from "./PageControlAssociation-DabEg5AR.mjs";
6
6
  import { Cause, Clock, Crypto, Deferred, Duration, Effect, Exit, FiberSet, Option, Redacted, Schema, Scope, Semaphore } from "effect";
7
7
  //#region src/internal/browser/Binding.ts
@@ -24,7 +24,7 @@ const fromNativeAttempt = (attempt) => ({ connect: (request) => Effect.tryPromis
24
24
  return pending;
25
25
  },
26
26
  catch: (error) => Schema.is(InitializationError)(error) ? error : publicError(error, "connect", {
27
- reason: Reasons.Provider.make({}),
27
+ reason: providerReason(error),
28
28
  outcome: "unknown"
29
29
  })
30
30
  }) });
@@ -3525,7 +3525,7 @@ const clientNavigationEmitter = (frame) => {
3525
3525
  * The connection's pages, frames and document epochs, and the one selected target every other
3526
3526
  * seam acts on. A page beyond the configured bound is handed to policy recovery, never tracked.
3527
3527
  */
3528
- const makeTargets = (browser, context, options, connectionNamespace, closing, hooks) => {
3528
+ const makeTargets = (browser, context, options, connectionNamespace, closing, hooks, windows) => {
3529
3529
  const entries = /* @__PURE__ */ new Map();
3530
3530
  const byPage = /* @__PURE__ */ new WeakMap();
3531
3531
  const frameIds = /* @__PURE__ */ new WeakMap();
@@ -3534,6 +3534,7 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3534
3534
  const navigating = /* @__PURE__ */ new Set();
3535
3535
  let serial = 0, frameSerial = 0;
3536
3536
  let creatingPage = false;
3537
+ const arrivals = /* @__PURE__ */ new Set();
3537
3538
  const epochOf = (frame) => documentEpochs.get(frame) ?? 0;
3538
3539
  const frameId = (frame) => {
3539
3540
  let id = frameIds.get(frame);
@@ -3552,6 +3553,7 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3552
3553
  off: []
3553
3554
  };
3554
3555
  byPage.set(page, entry);
3556
+ for (const arrived of arrivals) arrived(entry);
3555
3557
  if (entries.size >= options.maxPages) {
3556
3558
  hooks.overflow(entry);
3557
3559
  return entry;
@@ -3657,6 +3659,63 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3657
3659
  }
3658
3660
  };
3659
3661
  const urlOf = (frame) => safeDecode(URLText, frame.url());
3662
+ /**
3663
+ * Opens one page: in a window of its own when the browser opens one, else as a tab. The window's
3664
+ * page arrives through the context's page event like any other, and its target id tells it
3665
+ * apart from a popup arriving at the same time. It is registered, and sized, when this returns.
3666
+ */
3667
+ const open = async (signal) => {
3668
+ const arrived = [];
3669
+ let wake = () => {};
3670
+ const arrival = (entry) => {
3671
+ arrived.push(entry);
3672
+ wake();
3673
+ };
3674
+ const next = () => new Promise((resolve, reject) => {
3675
+ const abort = () => reject(failure(Reasons.Interrupted.make({}), "unknown"));
3676
+ if (signal.aborted) {
3677
+ abort();
3678
+ return;
3679
+ }
3680
+ signal.addEventListener("abort", abort, { once: true });
3681
+ wake = () => {
3682
+ signal.removeEventListener("abort", abort);
3683
+ resolve();
3684
+ };
3685
+ });
3686
+ const adopt = async (created) => {
3687
+ for (let index = 0;; index++) {
3688
+ while (arrived.length <= index) await next();
3689
+ const candidate = arrived[index];
3690
+ const id = candidate === void 0 ? void 0 : await targetId(candidate).catch(() => {});
3691
+ if (candidate !== void 0 && id === created) return candidate;
3692
+ }
3693
+ };
3694
+ /** Every other page that arrived meanwhile is from outside, and its policy applies now. */
3695
+ const settle = (isOurs) => {
3696
+ for (const entry of arrived) if (!isOurs(entry) && entries.get(entry.id) === entry) hooks.external(entry);
3697
+ };
3698
+ arrivals.add(arrival);
3699
+ try {
3700
+ const opened = await windows.open();
3701
+ if (opened === void 0) {
3702
+ const entry = register(await context.newPage());
3703
+ settle((candidate) => candidate === entry);
3704
+ return entry;
3705
+ }
3706
+ try {
3707
+ const [entry] = await Promise.all([adopt(opened.targetId), opened.sized]);
3708
+ settle((candidate) => candidate === entry);
3709
+ return entry;
3710
+ } catch (error) {
3711
+ await closeWithin(opened.discard).catch(() => {});
3712
+ settle((candidate) => candidate.targetId === void 0 || candidate.targetId === opened.targetId);
3713
+ throw error;
3714
+ }
3715
+ } finally {
3716
+ arrivals.delete(arrival);
3717
+ }
3718
+ };
3660
3719
  const selectedUrl = () => urlOf(selectedCurrent().frame);
3661
3720
  const url = (target) => urlOf(current(target).frame);
3662
3721
  /**
@@ -3667,7 +3726,7 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3667
3726
  if (options.newPage || entries.size === 0) {
3668
3727
  creatingPage = true;
3669
3728
  try {
3670
- selection.entry = register(await context.newPage());
3729
+ selection.entry = await open(AbortSignal.timeout(15e3));
3671
3730
  } finally {
3672
3731
  creatingPage = false;
3673
3732
  }
@@ -3708,11 +3767,10 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3708
3767
  selected: selection.entry === entry
3709
3768
  });
3710
3769
  };
3770
+ /** Each page's title is its own round trip, so they are read together rather than in turn. */
3711
3771
  const listPages = (ticket) => sanitize(async () => {
3712
3772
  ticket.check();
3713
- const output = [];
3714
- for (const entry of entries.values()) output.push(await pageInfo(entry, ticket));
3715
- return output;
3773
+ return Promise.all([...entries.values()].map((entry) => pageInfo(entry, ticket)));
3716
3774
  });
3717
3775
  const explicitPage = async (page, ticket) => {
3718
3776
  ticket.check();
@@ -3723,6 +3781,7 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3723
3781
  if (entries.get(entry.id) !== entry || entry.page.isClosed()) throw failure(Reasons.Stale.make({}), "undispatched");
3724
3782
  return entry;
3725
3783
  };
3784
+ const describePage = (page, ticket) => sanitize(async () => pageInfo(await explicitPage(page, ticket), ticket));
3726
3785
  const resolvePage = (page, ticket) => sanitize(async () => {
3727
3786
  const entry = await explicitPage(page, ticket);
3728
3787
  return {
@@ -3748,12 +3807,11 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3748
3807
  creatingPage = true;
3749
3808
  let entry;
3750
3809
  try {
3751
- const page = await context.newPage();
3810
+ entry = await open(ticket.signal);
3752
3811
  if (ticket.signal.aborted) {
3753
- await closeWithin(() => page.close()).catch(() => {});
3812
+ await closeWithin(() => entry.page.close()).catch(() => {});
3754
3813
  ticket.check();
3755
3814
  }
3756
- entry = register(page);
3757
3815
  } finally {
3758
3816
  creatingPage = false;
3759
3817
  }
@@ -3765,6 +3823,12 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3765
3823
  await entry.page.close({ runBeforeUnload: false });
3766
3824
  ticket.check();
3767
3825
  });
3826
+ const containPage = async (pageId) => {
3827
+ const entry = entries.get(pageId);
3828
+ if (entry === void 0) return;
3829
+ await closeWithin(() => entry.page.close({ runBeforeUnload: false }));
3830
+ if (!entry.page.isClosed()) throw failure(Reasons.Failed.make({}));
3831
+ };
3768
3832
  const listFrames = (ticket, page) => sanitize(async () => {
3769
3833
  ticket.check();
3770
3834
  const frames = (page === void 0 ? selectedCurrent().entry : await explicitPage(page, ticket)).page.frames();
@@ -3824,10 +3888,12 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3824
3888
  selected,
3825
3889
  selectedTargetId,
3826
3890
  listPages,
3891
+ describePage,
3827
3892
  resolvePage,
3828
3893
  selectPage,
3829
3894
  newPage,
3830
3895
  closePage,
3896
+ containPage,
3831
3897
  listFrames,
3832
3898
  resolveFrame,
3833
3899
  selectFrame
@@ -3836,6 +3902,12 @@ const makeTargets = (browser, context, options, connectionNamespace, closing, ho
3836
3902
  //#endregion
3837
3903
  //#region src/internal/browser/Playwright.ts
3838
3904
  const NativeWindow = Schema.Struct({ windowId: Schema.Natural });
3905
+ const CreatedTarget = Schema.Struct({ targetId: Identifier });
3906
+ /**
3907
+ * Playwright 1.63.0 reports the browser's own error reply to a CDP command this way. A reply is
3908
+ * the browser's answer that it did not do what was asked; anything else leaves that unknown.
3909
+ */
3910
+ const refusedBy = (method, error) => error instanceof Error && error.message.includes(`Protocol error (${method})`) && !error.message.includes("session closed");
3839
3911
  /**
3840
3912
  * Connects to an already validated or host-resolved CDP endpoint and builds the one driver. The
3841
3913
  * import is lazy, so constructing any Layer cannot load Playwright or allocate a browser. A
@@ -3862,7 +3934,7 @@ const connectPlaywrightEndpoint = async (endpoint, signal, identity, options, ev
3862
3934
  return driver;
3863
3935
  } catch (error) {
3864
3936
  await closeWithin(() => browser.close()).catch(() => {});
3865
- throw Schema.is(BrowserError)(error) || Schema.is(InitializationError)(error) || Schema.is(NativeFailure)(error) ? error : failure(Reasons.Provider.make({}));
3937
+ throw Schema.is(BrowserError)(error) || Schema.is(InitializationError)(error) || Schema.is(NativeFailure)(error) ? error : failure(providerReason(error));
3866
3938
  }
3867
3939
  };
3868
3940
  /** Tests may supply a real already-connected browser. This is deliberately not an exported subpath. */
@@ -3882,6 +3954,7 @@ const makePlaywrightDriver = async (browser, identity, options, events) => {
3882
3954
  let closing = false;
3883
3955
  let initialized = false;
3884
3956
  let browserCdp;
3957
+ let windows = true;
3885
3958
  let sameDocumentCapture = () => {};
3886
3959
  const targets = makeTargets(browser, context, options, identity.namespace, () => closing, {
3887
3960
  opened: (entry, created) => {
@@ -3895,6 +3968,11 @@ const makePlaywrightDriver = async (browser, identity, options, events) => {
3895
3968
  if (initialized) initialization.attachPage(entry.page);
3896
3969
  if (initialized && !created && options.popupPolicy === "pause") events.pause("popup");
3897
3970
  },
3971
+ external: (entry) => {
3972
+ if (!initialized) return;
3973
+ if (options.popupPolicy === "close") policyCleanup.run(entry.page, () => entry.page.close({ runBeforeUnload: false }));
3974
+ else if (options.popupPolicy === "pause") events.pause("popup");
3975
+ },
3898
3976
  overflow: (entry) => {
3899
3977
  if (options.popupPolicy === "close") policyCleanup.run(entry.page, () => entry.page.close({ runBeforeUnload: false }), { overflow: "popup-overflow" });
3900
3978
  else events.fault({
@@ -3941,7 +4019,29 @@ const makePlaywrightDriver = async (browser, identity, options, events) => {
3941
4019
  }
3942
4020
  },
3943
4021
  changed: (reason, scope) => observation.changed(reason, scope)
3944
- });
4022
+ }, { open: async () => {
4023
+ if (!windows) return void 0;
4024
+ const cdp = browserSession();
4025
+ let created;
4026
+ try {
4027
+ created = await cdp.send("Target.createTarget", {
4028
+ url: "about:blank",
4029
+ newWindow: true
4030
+ });
4031
+ } catch (error) {
4032
+ if (!refusedBy("Target.createTarget", error)) throw error;
4033
+ windows = false;
4034
+ return;
4035
+ }
4036
+ const { targetId } = safeDecode(CreatedTarget, created);
4037
+ return {
4038
+ targetId,
4039
+ sized: options.preserveViewport ? Promise.resolve() : sizeNativeWindow(targetId),
4040
+ discard: async () => {
4041
+ await cdp.send("Target.closeTarget", { targetId });
4042
+ }
4043
+ };
4044
+ } });
3945
4045
  const { current, entries, register } = targets;
3946
4046
  const initialization = makeInitialization(context, options, identity.bindings, targets, callbacks, events, () => closing);
3947
4047
  const observation = makeObservation(targets, identity.namespace, events);
@@ -3970,26 +4070,32 @@ const makePlaywrightDriver = async (browser, identity, options, events) => {
3970
4070
  };
3971
4071
  context.on("page", onPage);
3972
4072
  browser.on("disconnected", onDisconnected);
3973
- const sizeNativeContents = async (entry) => {
4073
+ const browserSession = () => {
3974
4074
  if (browserCdp === void 0) throw failure(Reasons.Closed.make({}), "undispatched");
3975
- const targetId = await targets.targetId(entry);
3976
- const current = await browserCdp.send("Browser.getWindowForTarget", { targetId });
4075
+ return browserCdp;
4076
+ };
4077
+ const sizeNativeWindow = async (targetId) => {
4078
+ const cdp = browserSession();
4079
+ const current = await cdp.send("Browser.getWindowForTarget", { targetId });
3977
4080
  const native = safeDecode(NativeWindow, current);
3978
- await browserCdp.send("Browser.setContentsSize", {
4081
+ await cdp.send("Browser.setContentsSize", {
3979
4082
  windowId: native.windowId,
3980
4083
  width: options.viewport.width,
3981
4084
  height: options.viewport.height
3982
4085
  });
3983
4086
  };
4087
+ const sizeNativeContents = async (entry) => sizeNativeWindow(await targets.targetId(entry));
3984
4088
  const driver = {
3985
4089
  ...options.pageControl ? { pageControl: pageControl.operations } : {},
3986
4090
  selected: targets.selected,
3987
4091
  selectedTargetId: targets.selectedTargetId,
3988
4092
  listPages: targets.listPages,
4093
+ describePage: targets.describePage,
3989
4094
  resolvePage: targets.resolvePage,
3990
4095
  selectPage: targets.selectPage,
3991
4096
  newPage: targets.newPage,
3992
4097
  closePage: targets.closePage,
4098
+ containPage: targets.containPage,
3993
4099
  listFrames: targets.listFrames,
3994
4100
  resolveFrame: targets.resolveFrame,
3995
4101
  selectFrame: targets.selectFrame,
@@ -4236,6 +4342,7 @@ const makeSession = (controls, bindings) => {
4236
4342
  kind: "type"
4237
4343
  }))),
4238
4344
  pages: controls.pages,
4345
+ describePage: (page) => checked(PageInfo, page, "describe-page").pipe(Effect.flatMap(controls.describePage)),
4239
4346
  frames: controls.frames,
4240
4347
  framesOf: (page) => checked(PageInfo, page, "list-frames").pipe(Effect.flatMap(controls.framesOf)),
4241
4348
  pinPage: (page) => checked(PageInfo, page, "target").pipe(Effect.flatMap(controls.pinPage), Effect.map(makePinnedTarget)),
@@ -4526,6 +4633,7 @@ const makeOwner = Effect.fnUntraced(function* (limits) {
4526
4633
  };
4527
4634
  const guard = (operation, body, options = {}) => Effect.suspend(() => {
4528
4635
  let admitted;
4636
+ const waitUntil = options.waitUntil ?? (options.queue === true ? Number(clock.monotonicTimeNanosUnsafe()) / 1e6 + Math.min(options.timeoutMillis ?? limits.actionTimeoutMillis, limits.actionTimeoutMillis) : void 0);
4529
4637
  const work = Effect.gen(function* () {
4530
4638
  holdingPermit = true;
4531
4639
  if (!(options.phases ?? ["open"]).includes(state.phase) || policies.size > 0) return yield* BrowserError.make({
@@ -4543,7 +4651,7 @@ const makeOwner = Effect.fnUntraced(function* (limits) {
4543
4651
  outcome: "undispatched"
4544
4652
  });
4545
4653
  }
4546
- const deadline = Math.min(options.waitUntil ?? now + limits.actionTimeoutMillis, options.timeoutMillis === void 0 ? Number.POSITIVE_INFINITY : now + options.timeoutMillis, lifetimeDeadline);
4654
+ const deadline = Math.min(waitUntil ?? now + limits.actionTimeoutMillis, options.timeoutMillis === void 0 ? Number.POSITIVE_INFINITY : now + options.timeoutMillis, lifetimeDeadline);
4547
4655
  if (now >= deadline) return yield* BrowserError.make({
4548
4656
  operation,
4549
4657
  reason: Reasons.Timeout.make({}),
@@ -4600,14 +4708,42 @@ const makeOwner = Effect.fnUntraced(function* (limits) {
4600
4708
  }
4601
4709
  };
4602
4710
  admitted = ticket;
4603
- const uncertain = () => {
4711
+ let abandoned = false;
4712
+ const abandon = () => {
4604
4713
  if (Number(clock.monotonicTimeNanosUnsafe()) / 1e6 >= lifetimeDeadline) expire();
4605
4714
  const fenced = controller.signal.aborted;
4606
4715
  controller.abort();
4607
- if (dispatched && !fenced && state.phase !== "closing" && state.phase !== "closed") fence("uncertain", "uncertain");
4716
+ if (dispatched && !fenced && state.phase !== "closing" && state.phase !== "closed") abandoned = true;
4608
4717
  };
4718
+ const current = () => state.generation === generation && state.phase === "open";
4719
+ /**
4720
+ * Work given up after dispatch fences the owner, since nothing knows what it did or
4721
+ * whether more of it will land. An operation confined to one page it may close instead
4722
+ * closes that page: once the browser confirms it closed, nothing still bound for it can
4723
+ * land anywhere, and every other page keeps working. It is still never replayed.
4724
+ */
4725
+ const settle = Effect.uninterruptible(Effect.suspend(() => {
4726
+ if (!abandoned) return Effect.void;
4727
+ abandoned = false;
4728
+ const page = current() ? options.contain?.() : void 0;
4729
+ if (page === void 0) return Effect.sync(() => fence("uncertain", "uncertain"));
4730
+ return page.close.pipe(Effect.map((closed) => {
4731
+ if (!closed || !current()) {
4732
+ fence("uncertain", "uncertain");
4733
+ return;
4734
+ }
4735
+ unresolved.delete(controller);
4736
+ const reservation = reservations.get(page.pageId);
4737
+ if (reservation !== void 0) {
4738
+ reservations.delete(page.pageId);
4739
+ unresolved.delete(reservation);
4740
+ reservation.abort();
4741
+ }
4742
+ record("page-contained", "confirmed");
4743
+ }));
4744
+ }));
4609
4745
  return yield* within(body(ticket), ticket.deadline, () => {
4610
- uncertain();
4746
+ abandon();
4611
4747
  return BrowserError.make({
4612
4748
  operation,
4613
4749
  reason: Reasons.Timeout.make({}),
@@ -4621,21 +4757,21 @@ const makeOwner = Effect.fnUntraced(function* (limits) {
4621
4757
  outcome: dispatched ? "unknown" : "undispatched"
4622
4758
  })
4623
4759
  })), Effect.catch((error) => {
4624
- if (dispatched) uncertain();
4625
- if (!Schema.is(BrowserError)(error)) return Effect.fail(error);
4626
- return Effect.fail(BrowserError.make({
4760
+ if (dispatched) abandon();
4761
+ const failure = Schema.is(BrowserError)(error) ? Effect.fail(BrowserError.make({
4627
4762
  operation,
4628
4763
  reason: error.reason,
4629
4764
  outcome: error.outcome
4630
- }));
4631
- }), Effect.onInterrupt(() => Effect.sync(uncertain)), Effect.tap(() => Effect.sync(() => unresolved.delete(controller))), Effect.ensuring(Effect.sync(() => {
4765
+ })) : Effect.fail(error);
4766
+ return settle.pipe(Effect.andThen(failure));
4767
+ }), Effect.onInterrupt(() => Effect.sync(abandon).pipe(Effect.andThen(settle))), Effect.tap(() => Effect.sync(() => unresolved.delete(controller))), Effect.ensuring(Effect.sync(() => {
4632
4768
  controller.abort();
4633
4769
  if (active === controller) active = void 0;
4634
4770
  })));
4635
4771
  }).pipe(Effect.ensuring(Effect.sync(() => {
4636
4772
  holdingPermit = false;
4637
4773
  })));
4638
- if (options.waitUntil !== void 0) return within(semaphore.withPermits(1)(work), Math.min(options.waitUntil, lifetimeDeadline), () => BrowserError.make({
4774
+ if (waitUntil !== void 0) return within(semaphore.withPermits(1)(work), Math.min(waitUntil, lifetimeDeadline), () => BrowserError.make({
4639
4775
  operation,
4640
4776
  reason: Reasons.Timeout.make({}),
4641
4777
  outcome: admitted?.dispatched === true ? "unknown" : "undispatched"
@@ -4714,8 +4850,8 @@ const makeOwner = Effect.fnUntraced(function* (limits) {
4714
4850
  };
4715
4851
  });
4716
4852
  /** Without a native answer, whether the step was sent is all the owner knows about it. */
4717
- const unsettled = (ticket) => ({
4718
- reason: Reasons.Provider.make({}),
4853
+ const unsettled = (ticket, error) => ({
4854
+ reason: providerReason(error),
4719
4855
  outcome: ticket.dispatched ? "unknown" : "undispatched"
4720
4856
  });
4721
4857
  /**
@@ -4748,12 +4884,12 @@ const native = (operation, ticket, body) => Effect.callback((resume) => {
4748
4884
  }, (error) => {
4749
4885
  if (!done) {
4750
4886
  cleanup();
4751
- resume(Effect.fail(publicError(error, operation, unsettled(ticket))));
4887
+ resume(Effect.fail(publicError(error, operation, unsettled(ticket, error))));
4752
4888
  }
4753
4889
  });
4754
4890
  } catch (error) {
4755
4891
  cleanup();
4756
- resume(Effect.fail(publicError(error, operation, unsettled(ticket))));
4892
+ resume(Effect.fail(publicError(error, operation, unsettled(ticket, error))));
4757
4893
  }
4758
4894
  return Effect.sync(cleanup);
4759
4895
  });
@@ -5165,6 +5301,27 @@ const acquireSession = Effect.fnUntraced(function* (limits, options) {
5165
5301
  outcome: "undispatched"
5166
5302
  })) : Effect.void;
5167
5303
  });
5304
+ /**
5305
+ * A page other than the selected one can take an unknown outcome of its own mutation with it:
5306
+ * closing it ends everything still bound for it, where the selected page and every other one
5307
+ * go on working. The selected page's own outcome still fences, since direct operations follow
5308
+ * it and nothing could be selected in its place.
5309
+ */
5310
+ const containment = (pageId) => {
5311
+ const current = driver;
5312
+ if (current === void 0) return void 0;
5313
+ let selected;
5314
+ try {
5315
+ selected = current.selected().pageId;
5316
+ } catch {
5317
+ return;
5318
+ }
5319
+ if (selected === pageId) return void 0;
5320
+ return {
5321
+ pageId,
5322
+ close: Effect.promise(() => current.containPage(pageId).then(() => true, () => false))
5323
+ };
5324
+ };
5168
5325
  const waitFree = (operation, pageId) => Effect.suspend(() => owner.waitPending(pageId) ? Effect.fail(BrowserError.make({
5169
5326
  operation,
5170
5327
  reason: Reasons.Busy.make({}),
@@ -5257,7 +5414,8 @@ const acquireSession = Effect.fnUntraced(function* (limits, options) {
5257
5414
  }), {
5258
5415
  mutation,
5259
5416
  mutationScope: () => ({ pageId: browserTarget?.pageId ?? getDriver().selected().pageId }),
5260
- preflight: mutation ? Effect.suspend(check).pipe(Effect.andThen(unreserved(operation, browserTarget))) : Effect.suspend(check)
5417
+ preflight: mutation ? Effect.suspend(check).pipe(Effect.andThen(unreserved(operation, browserTarget))) : Effect.suspend(check),
5418
+ ...browserTarget === void 0 ? {} : { contain: () => containment(browserTarget.pageId) }
5261
5419
  });
5262
5420
  const operationTarget = () => retained?.target === void 0 ? capture.target() : Target.make({
5263
5421
  generation: retained.generation,
@@ -5322,6 +5480,7 @@ const acquireSession = Effect.fnUntraced(function* (limits, options) {
5322
5480
  });
5323
5481
  const loadingTimeout = Math.min(timeoutMillis, remainingLifetime);
5324
5482
  const navigation = await driver.beginNavigation(url, loadingTimeout, ticket, browserTarget, control);
5483
+ ticket.check();
5325
5484
  return {
5326
5485
  target,
5327
5486
  navigation,
@@ -5623,6 +5782,7 @@ const acquireSession = Effect.fnUntraced(function* (limits, options) {
5623
5782
  revalidate: (reference) => nativeOperation("revalidate", (driver, ticket) => driver.revalidate(reference, ticket), { charge: false }),
5624
5783
  readiness: owner.guard("ready", (ticket) => native("ready", ticket, () => getDriver().documentReadiness(ticket)), { charge: false }),
5625
5784
  pages: nativeOperation("list-pages", (driver, ticket) => driver.listPages(ticket), { charge: false }),
5785
+ describePage: (page) => nativeOperation("describe-page", (driver, ticket) => driver.describePage(page, ticket), { charge: false }),
5626
5786
  frames: nativeOperation("list-frames", (driver, ticket) => driver.listFrames(ticket), { charge: false }),
5627
5787
  framesOf: (page) => nativeOperation("list-frames", (driver, ticket) => driver.listFrames(ticket, page), { charge: false }),
5628
5788
  pinPage: (page) => pinned((driver, ticket) => driver.resolvePage(page, ticket)),
@@ -5635,11 +5795,13 @@ const acquireSession = Effect.fnUntraced(function* (limits, options) {
5635
5795
  await driver.selectFrame(id, ticket);
5636
5796
  owner.state.selection++;
5637
5797
  }, { charge: false }),
5798
+ /** No other page's work conflicts with opening one, so it waits its turn rather than refusing. */
5638
5799
  createPage: () => nativeOperation("new-page", (driver, ticket) => driver.newPage(ticket), {
5639
5800
  mutation: true,
5640
5801
  mutationScope: () => "none",
5641
5802
  charge: false,
5642
- anyPage: true
5803
+ anyPage: true,
5804
+ queue: true
5643
5805
  }),
5644
5806
  closePage: (page) => nativeOperation("close-page", (driver, ticket) => driver.closePage(page, ticket), {
5645
5807
  mutation: true,
@@ -5895,4 +6057,4 @@ const make = Effect.fnUntraced(function* (options) {
5895
6057
  //#endregion
5896
6058
  export { cleanupStep as a, checked as c, randomUuid as i, fromNativeAttempt as l, make as n, noConnection as o, playwright as r, reported as s, DownloadEvent as t, issueBinding as u };
5897
6059
 
5898
- //# sourceMappingURL=BrowserRuntime-DImn_Oe_.mjs.map
6060
+ //# sourceMappingURL=BrowserRuntime-Cr76CN4I.mjs.map