effect-browser 0.3.0-beta.13 → 0.3.0-beta.14

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 (52) hide show
  1. package/README.md +50 -19
  2. package/dist/{Browser-QiT6Cdn_.mjs → Browser-BBSySyjT.mjs} +2 -2
  3. package/dist/{Browser-QiT6Cdn_.mjs.map → Browser-BBSySyjT.mjs.map} +1 -1
  4. package/dist/Browser.d.mts +1 -1
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/Cdp.mjs +1 -1
  7. package/dist/Chromium.mjs +1 -1
  8. package/dist/Expect.d.mts +1 -1
  9. package/dist/Expect.mjs +1 -1
  10. package/dist/{Frame-Di2PxyF7.d.mts → Frame-DkNdjjjr.d.mts} +3 -5
  11. package/dist/Frame.d.mts +1 -1
  12. package/dist/Frame.mjs +2 -4
  13. package/dist/Frame.mjs.map +1 -1
  14. package/dist/Moment.d.mts +1 -1
  15. package/dist/Moment.mjs +7 -3
  16. package/dist/Moment.mjs.map +1 -1
  17. package/dist/{Page-B7X6DQUn.d.mts → Page-DL9eylp3.d.mts} +2 -2
  18. package/dist/Page.d.mts +1 -1
  19. package/dist/{Plan-BS0jpENY.d.mts → Plan-6As6vcwT.d.mts} +3 -3
  20. package/dist/{Plan-Bl2NM1D5.mjs → Plan-CXA2BW-G.mjs} +4 -6
  21. package/dist/Plan-CXA2BW-G.mjs.map +1 -0
  22. package/dist/Plan.d.mts +1 -1
  23. package/dist/Plan.mjs +1 -1
  24. package/dist/Policy.d.mts +1 -1
  25. package/dist/Presentation.d.mts +1 -1
  26. package/dist/Presentation.mjs +1 -1
  27. package/dist/Stage.d.mts +5 -4
  28. package/dist/Stage.mjs +53 -20
  29. package/dist/Stage.mjs.map +1 -1
  30. package/dist/{Supervisor-TNKNOVzu.mjs → Supervisor-BE2sGHBK.mjs} +67 -41
  31. package/dist/Supervisor-BE2sGHBK.mjs.map +1 -0
  32. package/dist/{Supervisor-CYWFK3vM.d.mts → Supervisor-W60pZWHh.d.mts} +24 -8
  33. package/dist/Supervisor.d.mts +2 -2
  34. package/dist/Supervisor.mjs +1 -1
  35. package/dist/index.d.mts +4 -4
  36. package/dist/index.mjs +3 -3
  37. package/dist/{page-ZxMhw16l.mjs → page-B92QnRfT.mjs} +196 -166
  38. package/dist/page-B92QnRfT.mjs.map +1 -0
  39. package/package.json +1 -1
  40. package/src/Frame.ts +2 -4
  41. package/src/Moment.ts +14 -3
  42. package/src/Plan.ts +7 -6
  43. package/src/Stage.ts +61 -21
  44. package/src/Supervisor.ts +69 -37
  45. package/src/internal/pictures/capture.ts +172 -188
  46. package/src/internal/pictures/pictures.ts +50 -73
  47. package/src/internal/pictures/quiescence.ts +94 -0
  48. package/src/internal/reading/ready.ts +9 -3
  49. package/src/internal/supervisor/lifecycle.ts +27 -12
  50. package/dist/Plan-Bl2NM1D5.mjs.map +0 -1
  51. package/dist/Supervisor-TNKNOVzu.mjs.map +0 -1
  52. package/dist/page-ZxMhw16l.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Plan } from "./Motion.mjs";
2
2
  import { Action, Box, CursorChanged, KeyChanged, Navigated, PageLoaded, PointerPressed, PointerReleased, Subject, SubjectContext, TextInserted, TrackEvent, TrackPerformed, TrackPlanned, WheelScrolled } from "./BrowserEvent.mjs";
3
3
  import { BrowserError, Failed, InvalidRequest, NavigationFailed, NotActionable, NotFound, PolicyTimeout, StaleRef, Timeout } from "./BrowserError.mjs";
4
- import { a as make$19, c as Current, i as failWith, l as make$18, n as contextGone, o as reasonOf, r as decodeWith, s as undispatched } from "./context-BAjRj7Xo.mjs";
4
+ import { a as make$20, c as Current, i as failWith, l as make$19, n as contextGone, o as reasonOf, r as decodeWith, s as undispatched } from "./context-BAjRj7Xo.mjs";
5
5
  import { i as redact, n as parse, s as Correlation, t as addresses } from "./url-Cd04Okid.mjs";
6
6
  import { BrowserPaint, CaptureStats, Frame, Image, Screenshot } from "./Frame.mjs";
7
7
  import { _ as evidence, c as State, g as guard, h as ValidatedInputResultSchema, i as InputRequest, l as Text, m as PreparedInputResultSchema, o as Region, p as redacted, r as Found, s as ResolvedTarget, u as Zoom } from "./Page-B5cjaUp5.mjs";
@@ -9,7 +9,7 @@ import { Snapshot } from "./Snapshot.mjs";
9
9
  import { t as match } from "./match.inpage-xOe9LNO0.mjs";
10
10
  import { r as Changes, t as Change } from "./Change-DNvztNLu.mjs";
11
11
  import { Window } from "./Moment.mjs";
12
- import { Clock, Deferred, Duration, Effect, Exit, MutableRef, Option, Queue, Random, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, SynchronizedRef } from "effect";
12
+ import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Option, Queue, Random, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, SynchronizedRef } from "effect";
13
13
  //#region src/internal/input/edit.inpage.ts
14
14
  /**
15
15
  * In the page: editing a field. It refuses text that a focused control could act on, focuses a
@@ -2166,7 +2166,7 @@ const mainFrame = (page, event) => "frame" in event ? event.frame.parentId === v
2166
2166
  } : void 0;
2167
2167
  /** A call to the page script, its arguments quoted as JavaScript literals. */
2168
2168
  const scriptCall = (name, ...args) => `${name}(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
2169
- const make$17 = Effect.fnUntraced(function* (page) {
2169
+ const make$18 = Effect.fnUntraced(function* (page) {
2170
2170
  const { id, cdp, native, span, publish, now } = page;
2171
2171
  const { send } = page.protocol;
2172
2172
  const registration = yield* SynchronizedRef.make("none");
@@ -2352,7 +2352,7 @@ const toBrowserSeconds = (calibration, hostMillis) => (hostMillis + calibration.
2352
2352
  * protocol session, stamped with the browser's clock when its run has a mapping, and is published
2353
2353
  * as it is submitted.
2354
2354
  */
2355
- const make$16 = (page) => {
2355
+ const make$17 = (page) => {
2356
2356
  const { id, pointer, publish, now, noteInput, closedBy } = page;
2357
2357
  const { send } = page.protocol;
2358
2358
  const inputClocks = /* @__PURE__ */ new WeakMap();
@@ -2458,7 +2458,7 @@ const inputFailure = (operation, targets, failure) => {
2458
2458
  return failWith(operation, failure.error === "outside" ? new InvalidRequest({ detail: failure.detail }) : failure.error === "stale" && typeof target === "string" ? new StaleRef({ ref: target }) : new NotActionable({ detail: failure.detail }));
2459
2459
  };
2460
2460
  const editFailure = (operation, ref, failure) => failWith(operation, failure.stale === true ? new StaleRef({ ref }) : new NotActionable({ detail: failure.error }));
2461
- const make$15 = (page, bridge) => {
2461
+ const make$16 = (page, bridge) => {
2462
2462
  const { id } = page;
2463
2463
  const { current, evaluateIn, evaluateWithContext } = bridge;
2464
2464
  const preparePolicy = (action, info, targets, flags = {}) => Effect.gen(function* () {
@@ -2686,7 +2686,7 @@ const description = (character) => {
2686
2686
  * Keys and typing: a key chord held and released, each typed event, and the document a run of
2687
2687
  * keys must stay in.
2688
2688
  */
2689
- const make$14 = (page, bridge, dispatch) => {
2689
+ const make$15 = (page, bridge, dispatch) => {
2690
2690
  const { playwright } = page;
2691
2691
  const { send } = page.protocol;
2692
2692
  const { currentDocument } = bridge;
@@ -2893,7 +2893,7 @@ const timedOut = (operation, timeout) => failWith(operation, new Timeout({ milli
2893
2893
  * Fail `effect` with `Timeout` once `timeout` has passed, and as much later again as `presented`
2894
2894
  * says presentation has planned meanwhile.
2895
2895
  */
2896
- const deadline$1 = (operation, timeout, now, presented) => (effect) => Effect.suspend(() => {
2896
+ const deadline$2 = (operation, timeout, now, presented) => (effect) => Effect.suspend(() => {
2897
2897
  const due = now() + Duration.toMillis(timeout);
2898
2898
  const expiry = Effect.suspend(() => {
2899
2899
  const left = due + presented() - now();
@@ -2931,10 +2931,10 @@ const useRun = (page, sender, name, run, action) => action.pipe(Effect.matchEffe
2931
2931
  onFailure: (error) => run.close.pipe(Effect.andThen(sender.flush(name, run).pipe(Effect.ignore)), Effect.andThen(Effect.fail(error))),
2932
2932
  onSuccess: (value) => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.as(value))
2933
2933
  }), Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))), Effect.provideService(Current, run));
2934
- const make$13 = (page, sender) => {
2934
+ const make$14 = (page, sender) => {
2935
2935
  const { id, settings, mapping, lane, publish, now, noteInput, span, owned } = page;
2936
2936
  const { inputClocks } = sender;
2937
- const input = make$18();
2937
+ const input = make$19();
2938
2938
  const admitted = /* @__PURE__ */ new Set();
2939
2939
  const perform = (name, info, timeout, prepare, body, style = plain) => Effect.gen(function* () {
2940
2940
  const startedAt = now();
@@ -2974,14 +2974,14 @@ const make$13 = (page, sender) => {
2974
2974
  orElse: () => timedOut(name, timeout)
2975
2975
  })))), Effect.tap(() => Effect.sync(() => {
2976
2976
  queuedMillis = Math.round(now() - asked);
2977
- })), Effect.andThen(deadline$1(name, timeout, now, () => presented)(action))));
2977
+ })), Effect.andThen(deadline$2(name, timeout, now, () => presented)(action))));
2978
2978
  });
2979
2979
  const guard = settings.guard;
2980
2980
  const action = guard === void 0 ? dispatch(useInput((run) => body({
2981
2981
  ...marks,
2982
2982
  input: run
2983
2983
  }, void 0))) : Effect.gen(function* () {
2984
- const plan = yield* lane.read(name)(deadline$1(name, settings.actionTimeout, now, () => 0)(prepare)).pipe(span("Page.prepare", {}, "Debug"));
2984
+ const plan = yield* lane.read(name)(deadline$2(name, settings.actionTimeout, now, () => 0)(prepare)).pipe(span("Page.prepare", {}, "Debug"));
2985
2985
  yield* guard(plan.request).pipe(Effect.mapError((reason) => undispatched(name, reason)), Effect.timeoutOrElse({
2986
2986
  duration: settings.policyTimeout,
2987
2987
  orElse: () => failWith(name, new PolicyTimeout({ millis: Duration.toMillis(settings.policyTimeout) }))
@@ -3049,7 +3049,7 @@ const make$13 = (page, sender) => {
3049
3049
  * plan, published before its first move, from where viewers last saw the pointer when it is
3050
3050
  * shown, and a wheel scroll goes in the style's steps.
3051
3051
  */
3052
- const make$12 = (page, dispatch, viewport) => {
3052
+ const make$13 = (page, dispatch, viewport) => {
3053
3053
  const { id, pointer, publish, now, span } = page;
3054
3054
  const { inputClocks, inputCall, dispatchMouse, sendMouse } = dispatch;
3055
3055
  const moveTo = (operation, marks, to, cursor, travel = "approach") => Effect.gen(function* () {
@@ -3146,8 +3146,8 @@ const make$12 = (page, dispatch, viewport) => {
3146
3146
  * `pushState` asks for no document, and a navigation a handler starts after a fetch answers is the
3147
3147
  * next look's to see. A tab the input opened waits for the browser's registration too.
3148
3148
  */
3149
- const settling$1 = Duration.seconds(5);
3150
- const make$11 = (page, bridge) => {
3149
+ const settling = Duration.seconds(5);
3150
+ const make$12 = (page, bridge) => {
3151
3151
  const document = () => bridge.frameTag().document;
3152
3152
  let parsed = -1;
3153
3153
  let changed = Deferred.makeUnsafe();
@@ -3166,7 +3166,7 @@ const make$11 = (page, bridge) => {
3166
3166
  yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void), Effect.onError(() => page.stopLoading));
3167
3167
  while (document() > before.document && parsed < document()) yield* Deferred.await(changed);
3168
3168
  }).pipe(Effect.timeoutOrElse({
3169
- duration: settling$1,
3169
+ duration: settling,
3170
3170
  orElse: () => Effect.fail("unsettled")
3171
3171
  }), Effect.catch(() => Effect.annotateCurrentSpan("settled", false)), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
3172
3172
  return {
@@ -3196,7 +3196,7 @@ const resolvedTarget = (result) => new ResolvedTarget({
3196
3196
  cursor: result.cursor,
3197
3197
  ...result.href === void 0 ? {} : { href: result.href }
3198
3198
  });
3199
- const make$10 = (bridge, viewport, guard, pointer, dispatch, settling) => {
3199
+ const make$11 = (bridge, viewport, guard, pointer, dispatch, settling) => {
3200
3200
  const { evaluate } = bridge;
3201
3201
  const { mutate } = guard;
3202
3202
  const { moveTo, wheel } = pointer;
@@ -3254,20 +3254,20 @@ const buttonMask = {
3254
3254
  middle: 4
3255
3255
  };
3256
3256
  const partsOf = (page, bridge, viewport) => {
3257
- const dispatch = make$16(page);
3258
- const pointer = make$12(page, dispatch, viewport);
3259
- const guard = make$15(page, bridge);
3260
- const settling = make$11(page, bridge);
3257
+ const dispatch = make$17(page);
3258
+ const pointer = make$13(page, dispatch, viewport);
3259
+ const guard = make$16(page, bridge);
3260
+ const settling = make$12(page, bridge);
3261
3261
  return {
3262
3262
  page,
3263
3263
  bridge,
3264
3264
  ...settling,
3265
3265
  viewportFor: viewport.viewportFor,
3266
- perform: make$13(page, dispatch),
3266
+ perform: make$14(page, dispatch),
3267
3267
  ...guard,
3268
- ...make$10(bridge, viewport, guard, pointer, dispatch, settling),
3268
+ ...make$11(bridge, viewport, guard, pointer, dispatch, settling),
3269
3269
  ...pointer,
3270
- ...make$14(page, bridge, dispatch),
3270
+ ...make$15(page, bridge, dispatch),
3271
3271
  sendMouse: dispatch.sendMouse,
3272
3272
  flush: dispatch.flush
3273
3273
  };
@@ -3584,7 +3584,7 @@ const select = (input) => {
3584
3584
  return result.detail;
3585
3585
  }));
3586
3586
  };
3587
- const make$9 = (page, bridge, viewport) => {
3587
+ const make$10 = (page, bridge, viewport) => {
3588
3588
  const parts = partsOf(page, bridge, viewport);
3589
3589
  return {
3590
3590
  perform: parts.perform,
@@ -3689,6 +3689,47 @@ const tally = (now) => {
3689
3689
  };
3690
3690
  };
3691
3691
  //#endregion
3692
+ //#region src/internal/pictures/quiescence.ts
3693
+ /** Actual native replies outlive callers; bounded transactions retain them without holding teardown. */
3694
+ const deadline$1 = Duration.seconds(2);
3695
+ const make$9 = (options) => {
3696
+ const replies = make$19();
3697
+ const pending = /* @__PURE__ */ new Map();
3698
+ const late = (kind) => options.error(/* @__PURE__ */ new Error(`${kind} exceeded its deadline`));
3699
+ const track = (operation, actual, failed) => {
3700
+ const reply = actual.then(() => void 0, failed);
3701
+ pending.set(operation, reply);
3702
+ return reply.finally(() => {
3703
+ if (pending.get(operation) === reply) pending.delete(operation);
3704
+ });
3705
+ };
3706
+ const wait = (operation) => (operation === void 0 ? replies.idle.pipe(Effect.andThen(Effect.promise(() => Promise.all(pending.values())))) : Effect.promise(() => pending.get(operation) ?? Promise.resolve())).pipe(Effect.asVoid, Effect.interruptible, Effect.timeoutOrElse({
3707
+ duration: deadline$1,
3708
+ orElse: () => Effect.fail(late(operation ?? pending.keys().next().value ?? "picture restoration"))
3709
+ }), Effect.provideService(Clock.Clock, options.clock));
3710
+ const excluding = (picture) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
3711
+ yield* restore(wait());
3712
+ if (options.closed()) return yield* options.error(/* @__PURE__ */ new Error("Target page has been closed"));
3713
+ const current = options.current();
3714
+ const run = yield* replies.begin;
3715
+ const result = yield* restore(options.pause(current).pipe(Effect.andThen(picture), Effect.provideService(Current, run))).pipe(Effect.exit);
3716
+ const cleaned = yield* wait().pipe(Effect.andThen(options.resume(current)), Effect.timeoutOrElse({
3717
+ duration: deadline$1,
3718
+ orElse: () => Effect.fail(late("picture restoration"))
3719
+ }), Effect.provideService(Clock.Clock, options.clock), Effect.exit, Effect.forkChild, Effect.flatMap(Fiber.join));
3720
+ if (Exit.isFailure(cleaned)) options.fail(current, options.error(Cause.squash(cleaned.cause)));
3721
+ if (Exit.isFailure(result)) return yield* Effect.failCause(result.cause);
3722
+ if (Exit.isFailure(cleaned)) return yield* Effect.failCause(cleaned.cause);
3723
+ return result.value;
3724
+ }));
3725
+ return {
3726
+ excluding,
3727
+ idle: wait(),
3728
+ wait,
3729
+ track
3730
+ };
3731
+ };
3732
+ //#endregion
3692
3733
  //#region src/internal/pictures/capture.ts
3693
3734
  /**
3694
3735
  * One CDP screencast per page, with bounded transport replies and per-reader loss accounting.
@@ -3700,36 +3741,6 @@ const deadline = Duration.seconds(2);
3700
3741
  const sameSize = (left, right) => Math.abs(left.width - right.width) < 1 && Math.abs(left.height - right.height) < 1;
3701
3742
  const sameShape = (image, device) => Math.abs(image.width * device.height - image.height * device.width) < device.width + device.height;
3702
3743
  const outlasting = Duration.seconds(1);
3703
- const excludedFor = Duration.seconds(10);
3704
- const settling = Duration.millis(50);
3705
- /**
3706
- * Chromium draws the library's own clipped pictures into the running screencast. Where Playwright
3707
- * emulates the viewport they keep the page's device size, and its shape when the crop has the
3708
- * viewport's proportions, so nothing in a frame tells them apart: a frame painted, or arriving,
3709
- * while one is taken is left out, the page's own included. A crop on the page's own session is
3710
- * drawn at the crop's size, so there only a frame of another size than the page's is left out.
3711
- */
3712
- const pictureWindows = (now) => {
3713
- let windows = [];
3714
- return {
3715
- drawn: ({ hostTime, timing, receivedAt }, foreign) => windows.some((window) => {
3716
- const until = window.until + Duration.toMillis(settling);
3717
- return (foreign || !window.resized) && (hostTime + timing.uncertaintyMillis >= window.from && hostTime - timing.uncertaintyMillis <= until || receivedAt >= window.from && receivedAt <= until);
3718
- }),
3719
- excluding: (picture, resized = false) => Effect.acquireUseRelease(Effect.sync(() => {
3720
- const window = {
3721
- from: now(),
3722
- until: Number.POSITIVE_INFINITY,
3723
- resized
3724
- };
3725
- const since = window.from - Duration.toMillis(excludedFor);
3726
- windows = [...windows.filter((kept) => kept.until > since), window];
3727
- return window;
3728
- }), () => picture, (window) => Effect.sync(() => {
3729
- window.until = now();
3730
- }))
3731
- };
3732
- };
3733
3744
  /**
3734
3745
  * A native frame as readers get it: its picture's size, else the capture's or its device's, and
3735
3746
  * its paint mapped by the browser's newest estimate, which may be older than the frame and says
@@ -3831,7 +3842,6 @@ const sizeFilter = (options, deliver, drop) => Effect.gen(function* () {
3831
3842
  expected = viewport;
3832
3843
  },
3833
3844
  /** Whether a frame of this device size shows the page, as far as the capture knows. */
3834
- expects: (device) => expected !== null && sameSize(device, expected),
3835
3845
  offer: (frame, timestamp, image, device) => {
3836
3846
  if (image !== void 0 && !sameShape(image, device)) {
3837
3847
  drop(1);
@@ -3895,7 +3905,7 @@ const confirmation = (read, current, why, error) => (frame) => Effect.suspend(()
3895
3905
  return after.generation === generation && after.replies === 0 && current(frame);
3896
3906
  }));
3897
3907
  });
3898
- const isCurrent = (frame, generation, latest) => generation !== void 0 && generation.accepting && generation.failure === void 0 && Option.getOrUndefined(latest) === frame && generation.transport.frameTag().document === frame.document;
3908
+ const isCurrent = (frame, generation, latest) => generation !== void 0 && generation.phase === "Capturing" && generation.failure === void 0 && Option.getOrUndefined(latest) === frame && generation.transport.frameTag().document === frame.document;
3899
3909
  const currentFrame = (generation, latest) => Option.getOrUndefined(Option.filter(latest, (frame) => isCurrent(frame, generation, latest)));
3900
3910
  /** One reader counts its own missed envelopes without hiding gaps inside stream chunks. */
3901
3911
  const frames = (subscription, baseline, why, missed) => {
@@ -3909,19 +3919,44 @@ const frames = (subscription, baseline, why, missed) => {
3909
3919
  });
3910
3920
  })));
3911
3921
  };
3922
+ const renewals = Effect.fnUntraced(function* (options) {
3923
+ const renewing = yield* Queue.sliding(1);
3924
+ yield* Queue.take(renewing).pipe(Effect.andThen(options.renew), Effect.andThen(Effect.sleep(Duration.seconds(1))), Effect.provideService(Clock.Clock, options.clock), Effect.forever, Effect.forkScoped);
3925
+ return renewing;
3926
+ });
3927
+ const receive = (options, current, replies, counts, sizes, renewing, fail) => (native) => {
3928
+ if (current.phase === "Stopped") return;
3929
+ counts.count("received");
3930
+ if (replies.size >= replyCapacity) {
3931
+ fail(current, options.error(/* @__PURE__ */ new Error("screencast acknowledgement capacity exceeded")));
3932
+ return;
3933
+ }
3934
+ const reply = current.transport.acknowledge(native.sessionId).then(() => void 0, (cause) => {
3935
+ if (current.phase !== "Stopped") fail(current, options.error(cause));
3936
+ }).then(() => void replies.delete(reply));
3937
+ replies.add(reply);
3938
+ const browserSeconds = native.metadata.timestamp;
3939
+ const timestamp = browserSeconds === void 0 ? NaN : browserSeconds * 1e3;
3940
+ if (!Number.isFinite(timestamp)) return counts.count("missingTimestamp");
3941
+ if (current.predecessor !== void 0 && timestamp <= current.predecessor) return counts.count("late");
3942
+ Queue.offerUnsafe(renewing, void 0);
3943
+ const { frame, image, device } = timed(options, native, timestamp, current);
3944
+ if (current.phase === "Paused") return counts.count("duringPictures");
3945
+ sizes.offer(frame, timestamp, image, device);
3946
+ };
3912
3947
  const make$8 = (options) => Effect.gen(function* () {
3913
3948
  const lock = yield* Semaphore.make(1);
3914
3949
  const replies = /* @__PURE__ */ new Set();
3915
3950
  let generation;
3916
3951
  const closed = yield* Deferred.make();
3917
- let unsettledStop;
3952
+ const pageScope = yield* Effect.scope;
3953
+ const isClosed = () => pageScope.state._tag === "Closed" || Deferred.isDoneUnsafe(closed);
3918
3954
  let latest = Option.none();
3919
3955
  let history = [];
3920
3956
  const keepMillis = Duration.toMillis(options.frameHistory);
3921
3957
  const now = () => Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6;
3922
3958
  const counts = tally(now);
3923
3959
  let delivered = 0;
3924
- const pictures = pictureWindows(now);
3925
3960
  const current = (frame) => isCurrent(frame, generation, latest);
3926
3961
  const deliver = (frame, timestamp) => {
3927
3962
  const current = generation;
@@ -3940,8 +3975,7 @@ const make$8 = (options) => Effect.gen(function* () {
3940
3975
  });
3941
3976
  };
3942
3977
  const sizes = yield* sizeFilter(options, deliver, (frames) => counts.count("foreignSize", frames));
3943
- const renewing = yield* Queue.sliding(1);
3944
- yield* Queue.take(renewing).pipe(Effect.andThen(options.renew), Effect.andThen(Effect.sleep(Duration.seconds(1))), Effect.provideService(Clock.Clock, options.clock), Effect.forever, Effect.forkScoped);
3978
+ const renewing = yield* renewals(options);
3945
3979
  const notifyFailure = (current, failure) => {
3946
3980
  if (current.failure !== void 0) return;
3947
3981
  current.failure = failure;
@@ -3949,37 +3983,67 @@ const make$8 = (options) => Effect.gen(function* () {
3949
3983
  _tag: "Failure",
3950
3984
  error: failure
3951
3985
  });
3986
+ stop(current);
3952
3987
  };
3988
+ const failed = (current) => (cause) => notifyFailure(current, options.error(cause));
3989
+ const stopNative = (current) => pictures.track("screencast stop", current.transport.stop(), failed(current));
3990
+ const startNative = (current) => pictures.track("screencast start", current.transport.start({
3991
+ format: "jpeg",
3992
+ quality: current.quality,
3993
+ ...current.size === null ? {} : {
3994
+ maxWidth: current.size.width,
3995
+ maxHeight: current.size.height
3996
+ }
3997
+ }), failed(current));
3953
3998
  const stop = (current) => {
3954
- if (!current.accepting) return;
3955
- current.accepting = false;
3999
+ if (current.phase === "Stopped") return;
4000
+ const paused = current.phase === "Paused";
4001
+ current.phase = "Stopped";
3956
4002
  sizes.end();
3957
4003
  current.unlisten();
3958
- const reply = current.transport.stop().then(() => void 0, (cause) => {
3959
- notifyFailure(current, options.error(cause));
3960
- });
3961
- current.stopReply = reply;
3962
- unsettledStop = reply;
3963
- reply.then(() => {
3964
- if (unsettledStop === reply) unsettledStop = void 0;
3965
- });
3966
- };
3967
- const fail = (current, failure) => {
3968
- notifyFailure(current, failure);
3969
- stop(current);
4004
+ if (!paused) stopNative(current);
3970
4005
  };
3971
4006
  const gone = () => options.error(/* @__PURE__ */ new Error("Target page has been closed"));
3972
- const stopDeadline = () => options.error(/* @__PURE__ */ new Error("screencast stop exceeded its deadline"));
3973
4007
  const why = told(closed, gone, options.clock);
3974
- const settled = (reply, late) => Effect.suspend(() => {
3975
- const pending = reply();
3976
- return pending === void 0 ? Effect.void : Effect.promise(() => pending).pipe(Effect.interruptible, Effect.timeoutOrElse({
3977
- duration: deadline,
3978
- orElse: () => late
3979
- }));
3980
- }).pipe(Effect.provideService(Clock.Clock, options.clock));
3981
- const awaitStop = (current) => settled(() => current.stopReply, Effect.sync(() => notifyFailure(current, stopDeadline())));
3982
- const awaitPreviousStop = settled(() => unsettledStop, Effect.fail(stopDeadline()));
4008
+ const awaitStop = (current) => pictures.wait("screencast stop").pipe(Effect.catch((error) => Effect.sync(() => notifyFailure(current, error))));
4009
+ const healthy = (current) => Effect.suspend(() => current.failure === void 0 ? Effect.void : Effect.fail(current.failure));
4010
+ const pictures = make$9({
4011
+ clock: options.clock,
4012
+ error: options.error,
4013
+ closed: isClosed,
4014
+ current: () => generation,
4015
+ fail: (current, error) => {
4016
+ if (current !== void 0) notifyFailure(current, error);
4017
+ },
4018
+ pause: (current) => Effect.gen(function* () {
4019
+ if (current?.phase !== "Capturing" || current.failure !== void 0) return;
4020
+ current.phase = "Paused";
4021
+ latest = Option.none();
4022
+ sizes.end();
4023
+ stopNative(current);
4024
+ yield* pictures.wait("screencast stop").pipe(Effect.tapError((error) => Effect.sync(() => notifyFailure(current, error))));
4025
+ yield* healthy(current);
4026
+ }),
4027
+ resume: (current) => Effect.gen(function* () {
4028
+ const resumable = () => {
4029
+ if (current === void 0 || generation !== current) return false;
4030
+ if (current.subscribers.size === 0) {
4031
+ stop(current);
4032
+ generation = void 0;
4033
+ return false;
4034
+ }
4035
+ return current.phase !== "Stopped" && current.failure === void 0 && !isClosed();
4036
+ };
4037
+ if (current === void 0 || !resumable()) return;
4038
+ const viewport = yield* readViewport(options);
4039
+ if (!resumable()) return;
4040
+ sizes.restart(viewport);
4041
+ current.phase = "Capturing";
4042
+ startNative(current);
4043
+ yield* pictures.wait("screencast start");
4044
+ yield* healthy(current);
4045
+ })
4046
+ });
3983
4047
  const leave = (current, subscription) => Effect.gen(function* () {
3984
4048
  current.subscribers.delete(subscription);
3985
4049
  yield* Queue.shutdown(subscription);
@@ -3989,50 +4053,25 @@ const make$8 = (options) => Effect.gen(function* () {
3989
4053
  if (generation === current) generation = void 0;
3990
4054
  });
3991
4055
  const acquire = (screencast) => lock.withPermits(1)(Effect.gen(function* () {
3992
- if (Deferred.isDoneUnsafe(closed)) return yield* gone();
4056
+ if (isClosed()) return yield* gone();
3993
4057
  let current = generation;
3994
4058
  const starting = current === void 0;
3995
4059
  if (current === void 0) {
3996
- yield* awaitPreviousStop;
4060
+ yield* pictures.idle;
3997
4061
  const [transport, { calibration, viewport }] = yield* prepare(options, screencast);
3998
4062
  const size = screencast.size ?? viewport;
3999
- if (Deferred.isDoneUnsafe(closed)) return yield* gone();
4000
- const created = {
4063
+ if (isClosed()) return yield* gone();
4064
+ current = {
4001
4065
  subscribers: /* @__PURE__ */ new Set(),
4002
4066
  transport,
4003
4067
  unlisten: () => void 0,
4004
4068
  calibration,
4005
4069
  quality: screencast.quality ?? 80,
4006
4070
  size,
4007
- accepting: true,
4071
+ phase: "Capturing",
4008
4072
  predecessor: void 0,
4009
- failure: void 0,
4010
- stopReply: void 0,
4011
- onFrame: (native) => {
4012
- if (!created.accepting) return;
4013
- counts.count("received");
4014
- if (replies.size >= replyCapacity) {
4015
- fail(created, options.error(/* @__PURE__ */ new Error("screencast acknowledgement capacity exceeded")));
4016
- return;
4017
- }
4018
- const reply = created.transport.acknowledge(native.sessionId).then(() => {
4019
- replies.delete(reply);
4020
- }, (cause) => {
4021
- replies.delete(reply);
4022
- if (created.accepting) fail(created, options.error(cause));
4023
- });
4024
- replies.add(reply);
4025
- const browserSeconds = native.metadata.timestamp;
4026
- const timestamp = browserSeconds === void 0 ? NaN : browserSeconds * 1e3;
4027
- if (!Number.isFinite(timestamp)) return counts.count("missingTimestamp");
4028
- if (created.predecessor !== void 0 && timestamp <= created.predecessor) return counts.count("late");
4029
- Queue.offerUnsafe(renewing, void 0);
4030
- const { frame, image, device } = timed(options, native, timestamp, created);
4031
- if (pictures.drawn(frame, !sizes.expects(device))) return counts.count("duringPictures");
4032
- sizes.offer(frame, timestamp, image, device);
4033
- }
4073
+ failure: void 0
4034
4074
  };
4035
- current = created;
4036
4075
  generation = current;
4037
4076
  latest = Option.none();
4038
4077
  sizes.restart(viewport);
@@ -4046,24 +4085,13 @@ const make$8 = (options) => Effect.gen(function* () {
4046
4085
  const baseline = delivered;
4047
4086
  const frame = currentFrame(current, latest);
4048
4087
  if (starting) {
4049
- const { transport, quality, size } = current;
4050
4088
  const listening = current;
4051
- current.unlisten = transport.listen({
4052
- frame: current.onFrame,
4053
- lost: (error) => fail(listening, error)
4054
- });
4055
- const screencast = Effect.tryPromise({
4056
- try: () => transport.start({
4057
- format: "jpeg",
4058
- quality,
4059
- ...size === null ? {} : {
4060
- maxWidth: size.width,
4061
- maxHeight: size.height
4062
- }
4063
- }),
4064
- catch: options.error
4089
+ current.unlisten = listening.transport.listen({
4090
+ frame: receive(options, current, replies, counts, sizes, renewing, notifyFailure),
4091
+ lost: (error) => notifyFailure(listening, error)
4065
4092
  });
4066
- const started = yield* Effect.all([options.watch, screencast], { concurrency: 2 }).pipe(Effect.interruptible, Effect.timeoutOrElse({
4093
+ const screencast = Effect.promise(() => startNative(listening)).pipe(Effect.andThen(healthy(listening)));
4094
+ const started = yield* options.watch.pipe(Effect.zip(screencast, { concurrent: true }), Effect.interruptible, Effect.timeoutOrElse({
4067
4095
  duration: deadline,
4068
4096
  orElse: () => Effect.fail(options.error(/* @__PURE__ */ new Error("screencast start exceeded its deadline")))
4069
4097
  }), Effect.provideService(Clock.Clock, options.clock), Effect.exit);
@@ -4079,30 +4107,29 @@ const make$8 = (options) => Effect.gen(function* () {
4079
4107
  frame
4080
4108
  };
4081
4109
  }));
4082
- const open = (screencast = {}) => Effect.acquireRelease(acquire(screencast), ({ current, subscription }) => lock.withPermits(1)(leave(current, subscription))).pipe(Effect.map((lease) => ({
4110
+ const open = (screencast = {}) => Effect.acquireRelease(acquire(screencast), ({ current, subscription }) => Effect.sync(() => current.subscribers.delete(subscription)).pipe(Effect.andThen(lock.withPermits(1)(leave(current, subscription))))).pipe(Effect.map((lease) => ({
4083
4111
  current: lease.frame,
4084
4112
  stream: frames(lease.subscription, lease.baseline, why, (count) => counts.count("lost", count))
4085
4113
  })));
4086
- yield* Effect.addFinalizer(() => lock.withPermits(1)(Effect.suspend(() => {
4114
+ yield* Effect.addFinalizer(() => Effect.sync(() => Deferred.doneUnsafe(closed, Exit.void)).pipe(Effect.andThen(lock.withPermits(1)(Effect.suspend(() => {
4087
4115
  const current = generation;
4088
- Deferred.doneUnsafe(closed, Exit.void);
4089
4116
  if (current === void 0) return Effect.void;
4090
- fail(current, gone());
4117
+ notifyFailure(current, gone());
4091
4118
  return awaitStop(current);
4092
- })));
4119
+ })))));
4093
4120
  return {
4094
4121
  open,
4095
4122
  stream: (options) => Stream.unwrap(Effect.map(open(options), ({ stream }) => stream)),
4096
4123
  latest: Effect.sync(() => latest),
4097
4124
  recent: Effect.sync(() => history),
4098
- active: Effect.sync(() => generation?.accepting === true),
4125
+ active: Effect.sync(() => generation !== void 0 && generation.phase !== "Stopped"),
4099
4126
  sync: Effect.suspend(() => synced(generation, why, options.error)),
4100
4127
  current,
4101
4128
  confirm: confirmation(() => ({
4102
4129
  generation,
4103
4130
  replies: replies.size
4104
4131
  }), current, why, options.error),
4105
- excluding: pictures.excluding,
4132
+ excluding: (picture) => lock.withPermits(1)(pictures.excluding(picture)),
4106
4133
  stats: (windowMillis) => Effect.sync(() => new CaptureStats({
4107
4134
  ...counts.read(windowMillis),
4108
4135
  ackBacklog: replies.size
@@ -4366,22 +4393,23 @@ const ScreenSize = Schema.Struct({
4366
4393
  height: Schema.Int
4367
4394
  });
4368
4395
  const camera = (page, bridge, viewport, capture) => {
4369
- const { playwright, settings, native, within } = page;
4396
+ const { playwright, native, within } = page;
4370
4397
  const { send } = page.protocol;
4371
- const actionMillis = Duration.toMillis(settings.actionTimeout);
4372
4398
  let ratio = 1;
4373
- const alone = Semaphore.makeUnsafe(1).withPermits(1);
4374
4399
  let screenHeld = false;
4375
- const holdScreen = (operation) => Effect.suspend(() => screenHeld ? Effect.void : bridge.evaluate(operation, scriptCall("screenSize")).pipe(Effect.flatMap(decodeWith(operation, ScreenSize)), Effect.flatMap(({ width, height }) => native(operation, () => send("Emulation.setDeviceMetricsOverride", {
4376
- width: 0,
4377
- height: 0,
4378
- deviceScaleFactor: 0,
4379
- mobile: false,
4380
- screenWidth: width,
4381
- screenHeight: height
4382
- })).pipe(alone)), Effect.andThen(Effect.sync(() => {
4400
+ const holdScreen = Effect.fnUntraced(function* (operation) {
4401
+ if (screenHeld) return;
4402
+ const { width, height } = yield* bridge.evaluate(operation, scriptCall("screenSize")).pipe(Effect.flatMap(decodeWith(operation, ScreenSize)));
4403
+ yield* native(operation, () => send("Emulation.setDeviceMetricsOverride", {
4404
+ width: 0,
4405
+ height: 0,
4406
+ deviceScaleFactor: 0,
4407
+ mobile: false,
4408
+ screenWidth: width,
4409
+ screenHeight: height
4410
+ }));
4383
4411
  screenHeld = true;
4384
- }))));
4412
+ });
4385
4413
  const decoded = (operation, data, format) => {
4386
4414
  const size = format === "png" ? pngSize(data) : jpegSize(data);
4387
4415
  return size === void 0 ? failWith(operation, new Failed({ detail: `the browser's picture is not a ${format.toUpperCase()}` })) : Effect.succeed(new Image({
@@ -4390,18 +4418,19 @@ const camera = (page, bridge, viewport, capture) => {
4390
4418
  ...size
4391
4419
  }));
4392
4420
  };
4393
- const shot = (operation, format, quality, clip) => native(operation, () => send("Page.captureScreenshot", {
4421
+ const take = (operation, format, run) => native(operation, run).pipe(untilPainted, Effect.flatMap((data) => decoded(operation, data, format)));
4422
+ const shot = (operation, format, quality, clip) => take(operation, format, () => send("Page.captureScreenshot", {
4394
4423
  format,
4395
4424
  ...format === "jpeg" ? { quality } : {},
4396
4425
  ...clip === void 0 ? {} : { clip }
4397
- })).pipe(alone, untilPainted, Effect.flatMap(({ data }) => decoded(operation, Buffer.from(data, "base64"), format)));
4398
- const emulatedShot = (operation, format, quality, crop) => native(operation, () => playwright.screenshot({
4426
+ }).then(({ data }) => Buffer.from(data, "base64")));
4427
+ const emulatedShot = (operation, format, quality, crop) => take(operation, format, () => playwright.screenshot({
4399
4428
  type: format,
4400
4429
  ...format === "jpeg" ? { quality } : {},
4401
4430
  scale: "css",
4402
- timeout: actionMillis,
4431
+ timeout: 0,
4403
4432
  ...crop === void 0 ? {} : { clip: crop }
4404
- }), actionMillis).pipe(alone, untilPainted, Effect.flatMap((data) => decoded(operation, data, format)));
4433
+ }));
4405
4434
  return (operation, quality, crop, format = "jpeg") => Effect.gen(function* () {
4406
4435
  const known = viewport.knownViewport();
4407
4436
  let whole;
@@ -4412,9 +4441,9 @@ const camera = (page, bridge, viewport, capture) => {
4412
4441
  const emulated = playwright.viewportSize();
4413
4442
  if (emulated !== null) {
4414
4443
  if (whole !== void 0) ratio = whole.width / emulated.width;
4415
- return yield* capture.excluding(emulatedShot(operation, format, quality, crop));
4444
+ return yield* emulatedShot(operation, format, quality, crop);
4416
4445
  }
4417
- const [metrics] = yield* Effect.all([native(operation, () => send("Page.getLayoutMetrics")).pipe(Effect.flatMap(decodeWith(operation, LayoutMetrics))), crop === void 0 ? Effect.void : holdScreen(operation)], { concurrency: 2 });
4446
+ const [metrics] = yield* native(operation, () => send("Page.getLayoutMetrics")).pipe(Effect.flatMap(decodeWith(operation, LayoutMetrics)), Effect.zip(crop === void 0 ? Effect.void : holdScreen(operation), { concurrent: true }));
4418
4447
  const measured = metrics.contentSize.width / metrics.cssContentSize.width;
4419
4448
  ratio = Number.isFinite(measured) && measured > 0 ? measured : 1;
4420
4449
  if (crop === void 0 && ratio === 1) {
@@ -4429,14 +4458,14 @@ const camera = (page, bridge, viewport, capture) => {
4429
4458
  };
4430
4459
  const { pageX, pageY, scale } = metrics.cssVisualViewport;
4431
4460
  yield* holdScreen(operation);
4432
- return yield* capture.excluding(shot(operation, format, quality, {
4461
+ return yield* shot(operation, format, quality, {
4433
4462
  x: pageX + rect.x,
4434
4463
  y: pageY + rect.y,
4435
4464
  width: Math.floor(rect.width / scale + .001),
4436
4465
  height: Math.floor(rect.height / scale + .001),
4437
4466
  scale: scale / ratio
4438
- }), crop !== void 0);
4439
- }).pipe(within(operation));
4467
+ });
4468
+ }).pipe(capture.excluding, within(operation));
4440
4469
  };
4441
4470
  const millisOf = (input) => Option.match(Duration.fromInput(input), {
4442
4471
  onNone: () => NaN,
@@ -4711,8 +4740,9 @@ const make$5 = (page, bridge, capture) => (options = {}) => {
4711
4740
  const running = yield* capture.active;
4712
4741
  let { received } = yield* capture.stats();
4713
4742
  let stirred = running ? page.now() : Number.POSITIVE_INFINITY;
4714
- const look = Effect.map(capture.stats(), (stats) => {
4715
- if (stats.received !== received || stats.ackBacklog > 0) {
4743
+ const look = Effect.gen(function* () {
4744
+ const stats = yield* capture.stats();
4745
+ if (!Option.exists(yield* capture.latest, capture.current) || stats.received !== received || stats.ackBacklog > 0) {
4716
4746
  received = stats.received;
4717
4747
  stirred = page.now();
4718
4748
  }
@@ -4996,11 +5026,11 @@ const viewOf = (page, view) => {
4996
5026
  return internals === void 0 ? view : Object.assign(new Built(internals), view);
4997
5027
  };
4998
5028
  const make = Effect.fnUntraced(function* (options) {
4999
- const page = make$19(options, yield* Effect.scope);
5029
+ const page = make$20(options, yield* Effect.scope);
5000
5030
  const { id, playwright, native, owned, within, lane, span } = page;
5001
- const bridge = yield* make$17(page);
5031
+ const bridge = yield* make$18(page);
5002
5032
  const viewport = make$1(page, bridge);
5003
- const input = make$9(page, bridge, viewport);
5033
+ const input = make$10(page, bridge, viewport);
5004
5034
  const pictures = yield* make$7(page, bridge, viewport);
5005
5035
  const reading = yield* make$6(page, bridge);
5006
5036
  const navigation = make$2(page, input.perform, input.preparePolicy);
@@ -5071,4 +5101,4 @@ const make = Effect.fnUntraced(function* (options) {
5071
5101
  //#endregion
5072
5102
  export { performed as a, pause as i, make as n, mapping as o, viewOf as r, internalsOf as t };
5073
5103
 
5074
- //# sourceMappingURL=page-ZxMhw16l.mjs.map
5104
+ //# sourceMappingURL=page-B92QnRfT.mjs.map