effect-browser 0.3.0-beta.15 → 0.3.0-beta.19

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 (96) hide show
  1. package/README.md +18 -6
  2. package/dist/{Browser-BBSySyjT.mjs → Browser-H63wiekr.mjs} +5 -5
  3. package/dist/Browser-H63wiekr.mjs.map +1 -0
  4. package/dist/Browser.d.mts +2 -2
  5. package/dist/Browser.mjs +1 -1
  6. package/dist/{BrowserEvent-BRu2juEz.d.mts → BrowserEvent-dq6FRT8w.d.mts} +9 -9
  7. package/dist/BrowserEvent.d.mts +1 -1
  8. package/dist/BrowserEvent.mjs +7 -12
  9. package/dist/BrowserEvent.mjs.map +1 -1
  10. package/dist/Cdp.mjs +2 -2
  11. package/dist/{Change-iMlcuzVD.d.mts → Change-DqING3tr.d.mts} +2 -2
  12. package/dist/Change.d.mts +1 -1
  13. package/dist/Chromium.mjs +2 -2
  14. package/dist/Expect.d.mts +1 -1
  15. package/dist/Expect.mjs +2 -2
  16. package/dist/Moment.d.mts +1 -1
  17. package/dist/Moment.mjs +3 -3
  18. package/dist/Moment.mjs.map +1 -1
  19. package/dist/Motion-DND6I-64.d.mts +40 -0
  20. package/dist/Motion.d.mts +2 -40
  21. package/dist/Motion.mjs +1 -2
  22. package/dist/Motion.mjs.map +1 -1
  23. package/dist/{Page-B5cjaUp5.mjs → Page-CfB6Zn1p.mjs} +7 -15
  24. package/dist/Page-CfB6Zn1p.mjs.map +1 -0
  25. package/dist/{Page-DL9eylp3.d.mts → Page-ku6j5t-v.d.mts} +23 -25
  26. package/dist/Page.d.mts +1 -1
  27. package/dist/Page.mjs +1 -1
  28. package/dist/{Plan-CXA2BW-G.mjs → Plan-C-d5QYS8.mjs} +6 -9
  29. package/dist/Plan-C-d5QYS8.mjs.map +1 -0
  30. package/dist/{Plan-6As6vcwT.d.mts → Plan-DJ4hEEna.d.mts} +3 -3
  31. package/dist/Plan.d.mts +1 -1
  32. package/dist/Plan.mjs +1 -1
  33. package/dist/Policy.d.mts +1 -1
  34. package/dist/Policy.mjs +1 -1
  35. package/dist/Presentation.d.mts +2 -2
  36. package/dist/Presentation.mjs +3 -2
  37. package/dist/Presentation.mjs.map +1 -1
  38. package/dist/Stage.d.mts +1 -1
  39. package/dist/Stage.mjs +8 -7
  40. package/dist/Stage.mjs.map +1 -1
  41. package/dist/{Supervisor-DaGlQVjv.mjs → Supervisor-2mPjIYYs.mjs} +44 -19
  42. package/dist/Supervisor-2mPjIYYs.mjs.map +1 -0
  43. package/dist/{Supervisor-5tZ47hR5.d.mts → Supervisor-mVPeEUp8.d.mts} +12 -5
  44. package/dist/Supervisor.d.mts +1 -1
  45. package/dist/Supervisor.mjs +1 -1
  46. package/dist/{context-BAjRj7Xo.mjs → context-w3NJPRLo.mjs} +81 -9
  47. package/dist/context-w3NJPRLo.mjs.map +1 -0
  48. package/dist/index.d.mts +6 -6
  49. package/dist/index.mjs +4 -4
  50. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -1
  51. package/dist/{page-B92QnRfT.mjs → page-BKwfF6z-.mjs} +213 -218
  52. package/dist/page-BKwfF6z-.mjs.map +1 -0
  53. package/package.json +1 -1
  54. package/src/Browser.ts +1 -1
  55. package/src/BrowserEvent.ts +7 -12
  56. package/src/Moment.ts +3 -6
  57. package/src/Motion.ts +1 -2
  58. package/src/Page.ts +18 -20
  59. package/src/Plan.ts +1 -2
  60. package/src/Presentation.ts +2 -1
  61. package/src/Stage.ts +10 -17
  62. package/src/Supervisor.ts +9 -9
  63. package/src/internal/input/actions.ts +53 -97
  64. package/src/internal/input/dispatch.ts +25 -11
  65. package/src/internal/input/edit.inpage.ts +26 -11
  66. package/src/internal/input/evidence.inpage.ts +1 -21
  67. package/src/internal/input/guard.inpage.ts +9 -29
  68. package/src/internal/input/guard.ts +15 -10
  69. package/src/internal/input/perform.ts +14 -10
  70. package/src/internal/input/pointer.ts +3 -6
  71. package/src/internal/input/replies.ts +19 -8
  72. package/src/internal/input/settle.ts +3 -2
  73. package/src/internal/input/targets.inpage.ts +6 -13
  74. package/src/internal/input/targets.ts +13 -32
  75. package/src/internal/page/bridge.ts +2 -2
  76. package/src/internal/page/context.ts +15 -3
  77. package/src/internal/page/navigation.ts +3 -12
  78. package/src/internal/pictures/capture.ts +11 -4
  79. package/src/internal/pictures/clock.ts +8 -3
  80. package/src/internal/pictures/motion.ts +65 -0
  81. package/src/internal/pictures/pictures.ts +22 -17
  82. package/src/internal/pictures/utilsBundle.d.ts +9 -0
  83. package/src/internal/reading/controls.inpage.ts +8 -1
  84. package/src/internal/reading/match.inpage.ts +2 -1
  85. package/src/internal/reading/outline.inpage.ts +7 -4
  86. package/src/internal/reading/reading.ts +10 -25
  87. package/src/internal/reading/ready.ts +66 -14
  88. package/src/internal/supervisor/lifecycle.ts +56 -13
  89. package/src/internal/timeline/events.ts +5 -8
  90. package/src/internal/timeline/window.ts +3 -8
  91. package/dist/Browser-BBSySyjT.mjs.map +0 -1
  92. package/dist/Page-B5cjaUp5.mjs.map +0 -1
  93. package/dist/Plan-CXA2BW-G.mjs.map +0 -1
  94. package/dist/Supervisor-DaGlQVjv.mjs.map +0 -1
  95. package/dist/context-BAjRj7Xo.mjs.map +0 -1
  96. package/dist/page-B92QnRfT.mjs.map +0 -1
@@ -1,15 +1,16 @@
1
- import { Plan } from "./Motion.mjs";
2
- import { Action, Box, CursorChanged, KeyChanged, Navigated, PageLoaded, PointerPressed, PointerReleased, Subject, SubjectContext, TextInserted, TrackEvent, TrackPerformed, TrackPlanned, WheelScrolled } from "./BrowserEvent.mjs";
1
+ import { Plan, Point } from "./Motion.mjs";
2
+ import { Action, Box, CursorChanged, KeyChanged, Navigated, PageLoaded, PointerPressed, PointerReleased, Subject, 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$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";
4
+ import { a as make$20, c as undispatched, d as monotonicMillis, f as toBrowserSeconds, g as make$19, h as Current, i as failWith, l as estimate, m as uncertaintyAt, n as contextGone, o as reasonOf, p as toHostTime, r as decodeWith, s as refuse } from "./context-w3NJPRLo.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
- 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";
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-CfB6Zn1p.mjs";
8
8
  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
12
  import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Option, Queue, Random, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, SynchronizedRef } from "effect";
13
+ import { jpegjs } from "playwright-core/lib/utilsBundle";
13
14
  //#region src/internal/input/edit.inpage.ts
14
15
  /**
15
16
  * In the page: editing a field. It refuses text that a focused control could act on, focuses a
@@ -17,7 +18,7 @@ import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Opti
17
18
  * may use.
18
19
  */
19
20
  const edit = (names, guard, placing) => {
20
- const { activeElement, clean, describe, isDisabled, isHtml, isInput, isSecret, isSelect, isTextArea, lookup, nameOf, roleOf } = names;
21
+ const { activeElement, clean, describe, inCurrentDocument, isDisabled, isHtml, isInput, isSecret, isSelect, isTextArea, lookup, nameOf, roleOf } = names;
21
22
  const { textEntry, typingRefusal } = guard;
22
23
  /** Whether text may be typed into a ref or focus, and, for a `secret`, only into a secret field. */
23
24
  const typeable = (ref, secret) => {
@@ -48,8 +49,21 @@ const edit = (names, guard, placing) => {
48
49
  }
49
50
  };
50
51
  };
51
- const focus = (ref, replace) => {
52
- const element = lookup(ref);
52
+ /**
53
+ * The focused field, when it is a text field named `name`: one a page put in place of a field
54
+ * just pressed and gave focus to, as a typeahead does.
55
+ */
56
+ const replacement = (name) => {
57
+ const active = activeElement();
58
+ return active !== null && textEntry(active) && inCurrentDocument(active) && nameOf(active, roleOf(active)) === name ? active : void 0;
59
+ };
60
+ /**
61
+ * Focus a field to type into. `follow` is the field's name when the action pressed it first: a
62
+ * page may answer the press with another field of that name in its place, focused, and the keys
63
+ * follow focus there, as a person's would.
64
+ */
65
+ const focus = (ref, replace, follow) => {
66
+ const element = lookup(ref) ?? (follow === null ? void 0 : replacement(follow));
53
67
  if (element === void 0) return {
54
68
  error: `${ref} is not on the page any more`,
55
69
  stale: true
@@ -333,13 +347,9 @@ const targets = (names, walked, placing) => {
333
347
  };
334
348
  };
335
349
  const ResolvedPointSchema = Schema.Struct({
336
- x: Schema.Finite,
337
- y: Schema.Finite,
350
+ ...Point.fields,
351
+ ...Subject.fields,
338
352
  element: Schema.String,
339
- tag: Schema.String,
340
- role: Schema.NullOr(Schema.String),
341
- name: Schema.String,
342
- context: SubjectContext,
343
353
  box: Schema.optional(Box),
344
354
  cursor: Schema.String,
345
355
  href: Schema.optional(Schema.String)
@@ -356,8 +366,7 @@ const PointResultSchema = Schema.Union([ResolvedPointSchema, Schema.Struct({
356
366
  detail: Schema.String
357
367
  })]);
358
368
  const ScrollPlanSchema = Schema.NullOr(Schema.Struct({
359
- x: Schema.Finite,
360
- y: Schema.Finite,
369
+ ...Point.fields,
361
370
  dx: Schema.Finite,
362
371
  dy: Schema.Finite
363
372
  }));
@@ -464,6 +473,11 @@ const context = (names, walked) => {
464
473
  };
465
474
  //#endregion
466
475
  //#region src/internal/reading/controls.inpage.ts
476
+ /**
477
+ * In the page: a control's states and value as the outline shows them, and as a value; the elements a CSS selector scopes a
478
+ * read to; and a condition `Page.waitFor` waits for. See `names.inpage.ts` for what a page-side part
479
+ * may use.
480
+ */
467
481
  const controls = (names, walked, subjects, texts) => {
468
482
  const { clean, isDisabled, isHtml, isInput, isSelect, isTextArea, refFor, textOf } = names;
469
483
  const { visible } = walked;
@@ -553,6 +567,7 @@ const controls = (names, walked, subjects, texts) => {
553
567
  waitUntil
554
568
  };
555
569
  };
570
+ const ConditionResultSchema = Schema.Union([Schema.Struct({ met: Schema.Boolean }), Schema.Struct({ invalid: Schema.String })]);
556
571
  //#endregion
557
572
  //#region src/internal/reading/expect.inpage.ts
558
573
  const expectations = (names, subjected) => {
@@ -942,6 +957,10 @@ const outline = (names, walked, subjects, texts, controlling, url) => {
942
957
  viewport
943
958
  };
944
959
  };
960
+ const SnapshotResultSchema = Schema.Union([Schema.Struct({
961
+ snapshot: Snapshot,
962
+ nextRef: Schema.Finite
963
+ }), Schema.Struct({ invalid: Schema.String })]);
945
964
  const ViewportResultSchema = Schema.Struct({
946
965
  width: Schema.Finite,
947
966
  height: Schema.Finite
@@ -2066,7 +2085,7 @@ const sight = (names, walked, texts, kept) => {
2066
2085
  */
2067
2086
  const install = (makeNames, makeWalk, makeMatch, makeContext, makeSubjects, makeExpectations, makeText, makeControls, makeFold, makeHistory, makeSight, makeMarks, makeRecord, makeChanges, makeReady, makeOutline, makeTargets, makeEvidence, makeGuard, makeEdit, makeAddresses) => {
2068
2087
  const installed = globalThis.__effectBrowser;
2069
- if (installed !== void 0 && installed.version === 19) return installed;
2088
+ if (installed !== void 0 && installed.version === 20) return installed;
2070
2089
  const named = makeNames();
2071
2090
  const walked = makeWalk(named);
2072
2091
  const placing = makeContext(named, walked);
@@ -2083,7 +2102,7 @@ const install = (makeNames, makeWalk, makeMatch, makeContext, makeSubjects, make
2083
2102
  const edited = makeEdit(named, guarded, placing);
2084
2103
  const readiness = makeReady(walked, texts, kept, recorder);
2085
2104
  const api = {
2086
- version: 19,
2105
+ version: 20,
2087
2106
  snapshot: read.snapshot,
2088
2107
  find: subjected.find,
2089
2108
  until: makeExpectations(named, subjected).until,
@@ -2284,74 +2303,26 @@ const make$18 = Effect.fnUntraced(function* (page) {
2284
2303
  };
2285
2304
  });
2286
2305
  //#endregion
2287
- //#region src/internal/pictures/clock.ts
2288
- /**
2289
- * The browser's clock read on the owner's: estimates of their offset from timed probes, how an
2290
- * estimate ages, and the one mapping a browser's pages share. `pictures.ts` takes the probes.
2291
- */
2292
- /** The fastest complete probe limits uncertainty without mistaking transit delay for clock offset. */
2293
- const estimate = (probes) => {
2294
- let best;
2295
- for (const probe of probes) {
2296
- const roundTripMillis = probe.hostEnd - probe.hostStart;
2297
- if (!Number.isFinite(probe.hostStart) || !Number.isFinite(probe.hostEnd) || !Number.isFinite(probe.browserTime) || !Number.isFinite(roundTripMillis) || roundTripMillis < 0) continue;
2298
- const sampledAt = probe.hostStart + roundTripMillis / 2;
2299
- const offsetMillis = probe.browserTime - sampledAt;
2300
- if (!Number.isFinite(offsetMillis) || !Number.isFinite(probe.browserTime - probe.hostStart) || !Number.isFinite(probe.browserTime - probe.hostEnd)) continue;
2301
- if (best === void 0 || roundTripMillis < best.roundTripMillis) best = {
2302
- offsetMillis,
2303
- uncertaintyMillis: roundTripMillis / 2,
2304
- roundTripMillis,
2305
- sampledAt
2306
- };
2307
- }
2308
- return Option.fromUndefinedOr(best);
2309
- };
2310
- const driftPerMillis = 1e-4;
2311
- /** An estimate's uncertainty at `hostTime`, wider the further that is from when it was sampled. */
2312
- const uncertaintyAt = (estimate, hostTime) => estimate.uncertaintyMillis + Math.abs(hostTime - estimate.sampledAt) * driftPerMillis;
2313
- const renewAfter = Duration.seconds(10);
2314
- /**
2315
- * Whether a new measurement should replace the current estimate. Each interval contains the true
2316
- * offset, so overlapping intervals agree and the narrower one is kept, the current one widened by
2317
- * its age: a probe delayed behind a busy page cannot widen every tab's stamps. A disjoint
2318
- * measurement means the clocks moved (a wall clock step, or drift between a remote browser and
2319
- * this host), so the newer evidence wins.
2320
- */
2321
- const supersedes = (next, current) => {
2322
- const aged = uncertaintyAt(current, next.sampledAt);
2323
- return next.uncertaintyMillis <= aged || Math.abs(next.offsetMillis - current.offsetMillis) > next.uncertaintyMillis + aged;
2324
- };
2325
- /**
2326
- * Every renderer of a browser reads the same host wall clock, so the offset belongs to the
2327
- * browser rather than a page. A busy page cannot fail work that an earlier estimate can serve.
2328
- */
2329
- const mapping = (now) => {
2330
- let latest;
2331
- let measuredAt = Number.NEGATIVE_INFINITY;
2332
- const adopt = (measure) => {
2333
- measuredAt = now();
2334
- return measure.pipe(Effect.map((estimate) => {
2335
- if (latest === void 0 || supersedes(estimate, latest)) latest = estimate;
2336
- return latest;
2337
- }));
2338
- };
2339
- return {
2340
- latest: () => latest,
2341
- current: (measure) => Effect.suspend(() => latest === void 0 ? adopt(measure) : Effect.succeed(latest)),
2342
- renew: (measure) => Effect.suspend(() => latest === void 0 || now() - measuredAt < Duration.toMillis(renewAfter) ? Effect.void : Effect.ignore(adopt(measure)))
2343
- };
2344
- };
2345
- const toHostTime = (calibration, browserEpochMillis) => browserEpochMillis - calibration.offsetMillis;
2346
- /** CDP input timestamps use seconds, unlike browser frame timestamps and the owner's clock. */
2347
- const toBrowserSeconds = (calibration, hostMillis) => (hostMillis + calibration.offsetMillis) / 1e3;
2348
- //#endregion
2349
2306
  //#region src/internal/input/dispatch.ts
2350
2307
  /**
2351
2308
  * Sending input to the browser. Each mouse event, key and inserted text goes out on the page's
2352
2309
  * protocol session, stamped with the browser's clock when its run has a mapping, and is published
2353
2310
  * as it is submitted.
2354
2311
  */
2312
+ const buttonMask = {
2313
+ none: 0,
2314
+ left: 1,
2315
+ right: 2,
2316
+ middle: 4
2317
+ };
2318
+ /** A mouse button going down or up at a point, as the `clickCount`th click. */
2319
+ const buttonAt = (type, point, button = "left", clickCount = 1) => ({
2320
+ type,
2321
+ ...point,
2322
+ button,
2323
+ buttons: type === "mousePressed" ? buttonMask[button] : 0,
2324
+ clickCount
2325
+ });
2355
2326
  const make$17 = (page) => {
2356
2327
  const { id, pointer, publish, now, noteInput, closedBy } = page;
2357
2328
  const { send } = page.protocol;
@@ -2395,16 +2366,10 @@ const make$17 = (page) => {
2395
2366
  if (event.type === "mouseReleased") return yield* run.up(held);
2396
2367
  if (event.type === "mousePressed") {
2397
2368
  yield* run.reserve(2);
2398
- yield* run.down(held, () => dispatchMouse(event, estimate, submitted), () => dispatchMouse({
2399
- type: "mouseReleased",
2400
- ...Option.getOrElse(MutableRef.get(pointer), () => ({
2401
- x: event.x,
2402
- y: event.y
2403
- })),
2404
- button: event.button ?? "left",
2405
- buttons: 0,
2406
- clickCount: event.clickCount ?? 1
2407
- }, estimate));
2369
+ yield* run.down(held, () => dispatchMouse(event, estimate, submitted), () => dispatchMouse(buttonAt("mouseReleased", Option.getOrElse(MutableRef.get(pointer), () => ({
2370
+ x: event.x,
2371
+ y: event.y
2372
+ })), event.button, event.clickCount), estimate));
2408
2373
  } else {
2409
2374
  yield* run.reserve(1);
2410
2375
  yield* run.send(() => dispatchMouse(event, estimate, submitted));
@@ -2453,6 +2418,8 @@ const make$17 = (page) => {
2453
2418
  * policy decides on, binds an approval to the document and targets it saw, and runs the page
2454
2419
  * script's changes in the approved document only.
2455
2420
  */
2421
+ /** Recheck an approval, after any pending input it must see; neither runs without a guard. */
2422
+ const check = (approval, options = {}, before = Effect.void) => approval === void 0 ? Effect.void : before.pipe(Effect.andThen(approval.check(options)));
2456
2423
  const inputFailure = (operation, targets, failure) => {
2457
2424
  const target = failure.index === void 0 ? void 0 : targets[failure.index];
2458
2425
  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 }));
@@ -2463,8 +2430,8 @@ const make$16 = (page, bridge) => {
2463
2430
  const { current, evaluateIn, evaluateWithContext } = bridge;
2464
2431
  const preparePolicy = (action, info, targets, flags = {}) => Effect.gen(function* () {
2465
2432
  for (const target of targets) {
2466
- if (typeof target === "string" && !/^e\d+$/.test(target)) return yield* failWith(action, new InvalidRequest({ detail: `"${target}" is not a ref; refs look like e12` }));
2467
- if (target !== null && typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return yield* failWith(action, new InvalidRequest({ detail: "point coordinates must be finite" }));
2433
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return yield* refuse(action, `"${target}" is not a ref; refs look like e12`);
2434
+ if (target !== null && typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return yield* refuse(action, "point coordinates must be finite");
2468
2435
  }
2469
2436
  const input = {
2470
2437
  action,
@@ -2511,9 +2478,12 @@ const make$16 = (page, bridge) => {
2511
2478
  };
2512
2479
  });
2513
2480
  const mutate = (operation, call, approval, mayHaveRun = Effect.void) => (approval === void 0 ? current(operation) : Effect.succeed(approval.contextId)).pipe(Effect.flatMap((contextId) => evaluateIn(operation, call, contextId).pipe(Effect.onInterrupt(() => mayHaveRun), Effect.tapError((error) => error.reason._tag === "Failed" && /Cannot find context|__effectBrowser/i.test(error.reason.detail) ? Effect.void : mayHaveRun))), Effect.catchIf(contextGone, () => failWith(operation, new NotActionable({ detail: "the page changed before the approved input could run" }))));
2481
+ /** A read may follow the current document only when no approval binds it to one. */
2482
+ const evaluate = (operation, call, approval) => approval === void 0 ? bridge.evaluate(operation, call) : mutate(operation, call, approval);
2514
2483
  return {
2515
2484
  preparePolicy,
2516
- mutate
2485
+ mutate,
2486
+ evaluate
2517
2487
  };
2518
2488
  };
2519
2489
  //#endregion
@@ -2927,10 +2897,7 @@ const admission = (page, pending, name) => {
2927
2897
  };
2928
2898
  };
2929
2899
  /** Close a run on every exit while preserving its native replies for the page's next input. */
2930
- const useRun = (page, sender, name, run, action) => action.pipe(Effect.matchEffect({
2931
- onFailure: (error) => run.close.pipe(Effect.andThen(sender.flush(name, run).pipe(Effect.ignore)), Effect.andThen(Effect.fail(error))),
2932
- onSuccess: (value) => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.as(value))
2933
- }), Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))), Effect.provideService(Current, run));
2900
+ const useRun = (page, sender, name, run, action, canStop) => action.pipe(Effect.tapError(() => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.ignore)), Effect.tap(() => run.close.pipe(Effect.andThen(sender.flush(name, run)))), Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))), Effect.onError(() => run.recover(page.stopLoading, page.scope).pipe(Effect.when(canStop))), Effect.provideService(Current, run));
2934
2901
  const make$14 = (page, sender) => {
2935
2902
  const { id, settings, mapping, lane, publish, now, noteInput, span, owned } = page;
2936
2903
  const { inputClocks } = sender;
@@ -2965,7 +2932,7 @@ const make$14 = (page, sender) => {
2965
2932
  const useInput = (action) => input.begin.pipe(Effect.tap((current) => Effect.sync(() => {
2966
2933
  ticket.using(current);
2967
2934
  if (estimate !== void 0) inputClocks.set(current, estimate);
2968
- })), Effect.flatMap((run) => useRun(page, sender, name, run, action(run))));
2935
+ })), Effect.flatMap((run) => useRun(page, sender, name, run, action(run), info.input === false ? Effect.succeed(false) : Ref.get(sent))));
2969
2936
  let queuedMillis;
2970
2937
  const dispatch = (action) => Effect.suspend(() => {
2971
2938
  const asked = now();
@@ -3066,7 +3033,7 @@ const make$13 = (page, dispatch, viewport) => {
3066
3033
  y: Math.max(0, Math.min(size.height - 1, previous.y))
3067
3034
  };
3068
3035
  const planned = yield* style.glide(from, to, dragging);
3069
- const invalid = failWith(operation, new InvalidRequest({ detail: "motion must be a bounded, ordered schedule ending at the target" }));
3036
+ const invalid = refuse(operation, "motion must be a bounded, ordered schedule ending at the target");
3070
3037
  const samples = yield* Schema.decodeEffect(Plan)(planned).pipe(Effect.catch(() => invalid));
3071
3038
  if (samples.at(-1)?.x !== to.x || samples.at(-1)?.y !== to.y) return yield* invalid;
3072
3039
  yield* Effect.annotateCurrentSpan("samples", samples.length);
@@ -3163,12 +3130,12 @@ const make$12 = (page, bridge) => {
3163
3130
  });
3164
3131
  /** Wait for the page to settle after input sent since `before`. */
3165
3132
  const settle = (operation, before) => Effect.gen(function* () {
3166
- yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void), Effect.onError(() => page.stopLoading));
3133
+ yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void));
3167
3134
  while (document() > before.document && parsed < document()) yield* Deferred.await(changed);
3168
3135
  }).pipe(Effect.timeoutOrElse({
3169
3136
  duration: settling,
3170
3137
  orElse: () => Effect.fail("unsettled")
3171
- }), Effect.catch(() => Effect.annotateCurrentSpan("settled", false)), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
3138
+ }), Effect.catch(() => page.stopLoading.pipe(Effect.andThen(Effect.annotateCurrentSpan("settled", false)))), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
3172
3139
  return {
3173
3140
  mark,
3174
3141
  settle
@@ -3182,29 +3149,22 @@ const make$12 = (page, bridge) => {
3182
3149
  * viewers watch.
3183
3150
  */
3184
3151
  const pointFailure = (operation, target, result) => failWith(operation, result.error === "stale" && typeof target === "string" ? new StaleRef({ ref: target }) : result.error === "outside" ? new InvalidRequest({ detail: result.detail }) : new NotActionable({ detail: result.detail }));
3185
- const resolvedTarget = (result) => new ResolvedTarget({
3152
+ const resolvedTarget = ({ x, y, box, ...target }) => new ResolvedTarget({
3186
3153
  point: {
3187
- x: result.x,
3188
- y: result.y
3154
+ x,
3155
+ y
3189
3156
  },
3190
- element: result.element,
3191
- tag: result.tag,
3192
- role: result.role,
3193
- name: result.name,
3194
- context: result.context,
3195
- box: result.box,
3196
- cursor: result.cursor,
3197
- ...result.href === void 0 ? {} : { href: result.href }
3157
+ box,
3158
+ ...target
3198
3159
  });
3199
- const make$11 = (bridge, viewport, guard, pointer, dispatch, settling) => {
3200
- const { evaluate } = bridge;
3201
- const { mutate } = guard;
3160
+ const make$11 = (viewport, guard, pointer, dispatch, settling) => {
3161
+ const { evaluate, mutate } = guard;
3202
3162
  const { moveTo, wheel } = pointer;
3203
3163
  const { flush } = dispatch;
3204
- const readPoint = (operation, target, approval, scroll) => (approval === void 0 ? evaluate(operation, scriptCall("point", target, scroll)) : mutate(operation, scriptCall("point", target, scroll), approval)).pipe(Effect.flatMap(decodeWith(operation, PointResultSchema)));
3164
+ const readPoint = (operation, target, approval, scroll) => evaluate(operation, scriptCall("point", target, scroll), approval).pipe(Effect.flatMap(decodeWith(operation, PointResultSchema)));
3205
3165
  const resolve = (operation, target, approval, scroll = true) => {
3206
- if (typeof target === "string" && !/^e\d+$/.test(target)) return failWith(operation, new InvalidRequest({ detail: target + " is not a ref; refs look like e12" }));
3207
- if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return failWith(operation, new InvalidRequest({ detail: "point coordinates must be finite" }));
3166
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return refuse(operation, target + " is not a ref; refs look like e12");
3167
+ if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return refuse(operation, "point coordinates must be finite");
3208
3168
  return readPoint(operation, target, approval, scroll).pipe(Effect.flatMap((result) => "error" in result ? pointFailure(operation, target, result) : Effect.succeed(resolvedTarget(result))));
3209
3169
  };
3210
3170
  return {
@@ -3228,13 +3188,13 @@ const make$11 = (bridge, viewport, guard, pointer, dispatch, settling) => {
3228
3188
  yield* wheel(operation, marks, point, Math.max(-size.width * .9, Math.min(size.width * .9, plan.dx)), Math.max(-size.height * .9, Math.min(size.height * .9, plan.dy)));
3229
3189
  yield* flush(operation, marks.input);
3230
3190
  yield* settling.settle(operation, before);
3231
- if (approval !== void 0) yield* approval.check();
3191
+ yield* check(approval);
3232
3192
  }
3233
3193
  yield* marks.touched;
3234
3194
  const before = settling.mark();
3235
3195
  yield* resolve(operation, target, approval);
3236
3196
  yield* settling.settle(operation, before);
3237
- if (approval !== void 0) yield* approval.check();
3197
+ yield* check(approval);
3238
3198
  return yield* resolve(operation, target, approval, false);
3239
3199
  })
3240
3200
  };
@@ -3247,12 +3207,6 @@ const make$11 = (bridge, viewport, guard, pointer, dispatch, settling) => {
3247
3207
  * `perform`, which admits it, puts it to the input guard and records it. A click, a key or a scroll
3248
3208
  * then waits for the page to settle: a task and a frame, and a document if the input asked for one.
3249
3209
  */
3250
- const buttonMask = {
3251
- none: 0,
3252
- left: 1,
3253
- right: 2,
3254
- middle: 4
3255
- };
3256
3210
  const partsOf = (page, bridge, viewport) => {
3257
3211
  const dispatch = make$17(page);
3258
3212
  const pointer = make$13(page, dispatch, viewport);
@@ -3260,12 +3214,11 @@ const partsOf = (page, bridge, viewport) => {
3260
3214
  const settling = make$12(page, bridge);
3261
3215
  return {
3262
3216
  page,
3263
- bridge,
3264
3217
  ...settling,
3265
3218
  viewportFor: viewport.viewportFor,
3266
3219
  perform: make$14(page, dispatch),
3267
3220
  ...guard,
3268
- ...make$11(bridge, viewport, guard, pointer, dispatch, settling),
3221
+ ...make$11(viewport, guard, pointer, dispatch, settling),
3269
3222
  ...pointer,
3270
3223
  ...make$15(page, bridge, dispatch),
3271
3224
  sendMouse: dispatch.sendMouse,
@@ -3275,13 +3228,15 @@ const partsOf = (page, bridge, viewport) => {
3275
3228
  /** Whether every number given is finite; a number left out is. */
3276
3229
  const finite = (...values) => values.every((value) => value === void 0 || Number.isFinite(value));
3277
3230
  const named = (target) => typeof target === "string" ? target : `${target.x},${target.y}`;
3231
+ /** The ref a target names, if it names one rather than a point. */
3232
+ const refOf = (target) => typeof target === "string" ? target : void 0;
3278
3233
  /** A click's options, once its counts are finite. */
3279
- const clickable = (options) => finite(options.clickCount, options.holdMillis) ? Effect.void : failWith("click", new InvalidRequest({ detail: "clickCount and holdMillis must be finite" }));
3234
+ const clickable = (options) => finite(options.clickCount, options.holdMillis) ? Effect.void : refuse("click", "clickCount and holdMillis must be finite");
3280
3235
  /** The keys a press sends, once they and its options are valid. */
3281
3236
  const pressed = (keys, options) => {
3282
3237
  const parts$1 = parts(keys);
3283
- if (!finite(options.times, options.holdMillis)) return failWith("press", new InvalidRequest({ detail: "times and holdMillis must be finite" }));
3284
- if (parts$1 === void 0) return failWith("press", new InvalidRequest({ detail: `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A` }));
3238
+ if (!finite(options.times, options.holdMillis)) return refuse("press", "times and holdMillis must be finite");
3239
+ if (parts$1 === void 0) return refuse("press", `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`);
3285
3240
  return Effect.succeed(parts$1);
3286
3241
  };
3287
3242
  const click = (input) => {
@@ -3294,7 +3249,7 @@ const click = (input) => {
3294
3249
  clickCount: clickOptions.clickCount,
3295
3250
  holdMillis: clickOptions.holdMillis
3296
3251
  }
3297
- }, settings.actionTimeout, Effect.suspend(() => clickable(clickOptions)).pipe(Effect.andThen(preparePolicy("click", { target: typeof target === "string" ? target : void 0 }, [target]))), (marks, approval) => Effect.gen(function* () {
3252
+ }, settings.actionTimeout, Effect.suspend(() => clickable(clickOptions)).pipe(Effect.andThen(preparePolicy("click", { target: refOf(target) }, [target]))), (marks, approval) => Effect.gen(function* () {
3298
3253
  yield* clickable(clickOptions);
3299
3254
  const resolved = yield* targetFor("click", target, approval, marks);
3300
3255
  const { point } = resolved;
@@ -3303,37 +3258,22 @@ const click = (input) => {
3303
3258
  yield* marks.at(point);
3304
3259
  yield* marks.on(resolved);
3305
3260
  yield* moveTo("click", marks, point, resolved.cursor);
3306
- if (approval !== void 0) yield* approval.check({ presses: [{
3261
+ yield* check(approval, { presses: [{
3307
3262
  index: 0,
3308
3263
  ...point
3309
3264
  }] });
3310
3265
  const before = mark();
3311
3266
  yield* marks.sent;
3312
3267
  for (let index = 1; index <= count; index++) {
3313
- if (approval !== void 0 && index > 1) {
3314
- yield* flush("click", marks.input);
3315
- yield* approval.check({ presses: [{
3316
- index: 0,
3317
- ...point
3318
- }] }).pipe(Effect.catchIf((error) => error.reason._tag === "StaleRef", () => failWith("click", new NotActionable({ detail: "an earlier click removed the approved target, so no further press was sent" }))));
3319
- }
3320
- yield* sendMouse("click", marks.input, {
3321
- type: "mousePressed",
3322
- ...point,
3323
- button,
3324
- buttons: buttonMask[button],
3325
- clickCount: index
3326
- });
3268
+ if (index > 1) yield* check(approval, { presses: [{
3269
+ index: 0,
3270
+ ...point
3271
+ }] }, flush("click", marks.input)).pipe(Effect.catchIf((error) => error.reason._tag === "StaleRef", () => failWith("click", new NotActionable({ detail: "an earlier click removed the approved target, so no further press was sent" }))));
3272
+ yield* sendMouse("click", marks.input, buttonAt("mousePressed", point, button, index));
3327
3273
  const hold = clickOptions.holdMillis ?? (yield* marks.style.hold);
3328
3274
  if (clickOptions.holdMillis === void 0) yield* marks.present(hold);
3329
3275
  if (hold > 0) yield* Effect.sleep(Duration.millis(hold));
3330
- yield* sendMouse("click", marks.input, {
3331
- type: "mouseReleased",
3332
- ...point,
3333
- button,
3334
- buttons: 0,
3335
- clickCount: index
3336
- });
3276
+ yield* sendMouse("click", marks.input, buttonAt("mouseReleased", point, button, index));
3337
3277
  }
3338
3278
  yield* flush("click", marks.input);
3339
3279
  yield* settle("click", before);
@@ -3343,7 +3283,7 @@ const click = (input) => {
3343
3283
  const hover = (input) => {
3344
3284
  const { settings } = input.page;
3345
3285
  const { perform, preparePolicy, targetFor, moveTo, flush } = input;
3346
- return (target, style = plain) => perform("hover", { target: named(target) }, settings.actionTimeout, preparePolicy("hover", { target: typeof target === "string" ? target : void 0 }, [target]), (marks, approval) => Effect.gen(function* () {
3286
+ return (target, style = plain) => perform("hover", { target: named(target) }, settings.actionTimeout, preparePolicy("hover", { target: refOf(target) }, [target]), (marks, approval) => Effect.gen(function* () {
3347
3287
  const resolved = yield* targetFor("hover", target, approval, marks);
3348
3288
  const { point } = resolved;
3349
3289
  yield* marks.at(point);
@@ -3375,7 +3315,7 @@ const drag = (input) => {
3375
3315
  yield* marks.at(end.point);
3376
3316
  yield* marks.on(start, end);
3377
3317
  yield* moveTo("drag", marks, start.point, start.cursor);
3378
- if (approval !== void 0) yield* approval.check({ presses: [{
3318
+ yield* check(approval, { presses: [{
3379
3319
  index: 0,
3380
3320
  ...start.point
3381
3321
  }, {
@@ -3383,27 +3323,15 @@ const drag = (input) => {
3383
3323
  ...end.point
3384
3324
  }] });
3385
3325
  yield* marks.sent;
3386
- yield* sendMouse("drag", marks.input, {
3387
- type: "mousePressed",
3388
- ...start.point,
3389
- button: "left",
3390
- buttons: 1,
3391
- clickCount: 1
3392
- });
3326
+ yield* sendMouse("drag", marks.input, buttonAt("mousePressed", start.point));
3393
3327
  yield* moveTo("drag", marks, end.point, end.cursor, "drag");
3394
- yield* sendMouse("drag", marks.input, {
3395
- type: "mouseReleased",
3396
- ...end.point,
3397
- button: "left",
3398
- buttons: 0,
3399
- clickCount: 1
3400
- });
3328
+ yield* sendMouse("drag", marks.input, buttonAt("mouseReleased", end.point));
3401
3329
  yield* flush("drag", marks.input);
3402
3330
  }), style);
3403
3331
  };
3404
3332
  const typeText = (input) => {
3405
3333
  const { settings, now } = input.page;
3406
- const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, bridge } = input;
3334
+ const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, evaluate } = input;
3407
3335
  const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
3408
3336
  return (text, typeOptions = {}, style = plain) => perform("type", {
3409
3337
  target: typeOptions.into,
@@ -3419,9 +3347,9 @@ const typeText = (input) => {
3419
3347
  }, [typeOptions.into ?? null], { submit: typeOptions.submit ?? false }), (marks, approval) => Effect.gen(function* () {
3420
3348
  const replace = typeOptions.replace ?? true;
3421
3349
  const into = typeOptions.into;
3422
- if (into !== void 0 && !/^e\d+$/.test(into)) return yield* failWith("type", new InvalidRequest({ detail: `"${into}" is not a ref; refs look like e12` }));
3350
+ if (into !== void 0 && !/^e\d+$/.test(into)) return yield* refuse("type", `"${into}" is not a ref; refs look like e12`);
3423
3351
  const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
3424
- const typeable = yield* (approval === void 0 ? bridge.evaluate("type", asked) : mutate("type", asked, approval)).pipe(Effect.flatMap(decodeWith("type", TypeableResultSchema)));
3352
+ const typeable = yield* evaluate("type", asked, approval).pipe(Effect.flatMap(decodeWith("type", TypeableResultSchema)));
3425
3353
  if ("error" in typeable) return yield* failWith("type", typeable.error === "stale" && into !== void 0 ? new StaleRef({ ref: into }) : new NotActionable({ detail: typeable.detail }));
3426
3354
  if (typeable.subject !== void 0) yield* marks.on(typeable.subject);
3427
3355
  const since = yield* currentDocument("type");
@@ -3432,29 +3360,24 @@ const typeText = (input) => {
3432
3360
  yield* marks.on(target);
3433
3361
  if (marks.style.shown) {
3434
3362
  yield* moveTo("type", marks, target.point, target.cursor);
3435
- if (approval !== void 0) yield* approval.check({ presses: [{
3363
+ yield* check(approval, { presses: [{
3436
3364
  index: 0,
3437
3365
  ...target.point
3438
3366
  }] });
3439
3367
  yield* marks.sent;
3440
- for (const type of ["mousePressed", "mouseReleased"]) yield* sendMouse("type", marks.input, {
3441
- type,
3442
- ...target.point,
3443
- button: "left",
3444
- buttons: type === "mousePressed" ? 1 : 0,
3445
- clickCount: 1
3446
- });
3368
+ for (const type of ["mousePressed", "mouseReleased"]) yield* sendMouse("type", marks.input, buttonAt(type, target.point));
3447
3369
  yield* flush("type", marks.input);
3448
3370
  }
3449
3371
  yield* marks.sent;
3450
- const focused = yield* mutate("type", scriptCall("focus", into, replace), approval).pipe(Effect.flatMap(decodeWith("type", FocusResultSchema)));
3372
+ const follow = marks.style.shown && approval === void 0 && !secret && target.name !== "" ? target.name : null;
3373
+ const focused = yield* mutate("type", scriptCall("focus", into, replace, follow), approval).pipe(Effect.flatMap(decodeWith("type", FocusResultSchema)));
3451
3374
  if ("error" in focused) return yield* editFailure("type", into, focused);
3452
3375
  secret ||= focused.secret;
3453
3376
  }
3454
3377
  if (!secret) yield* marks.reveal;
3455
- if (approval !== void 0) yield* approval.check({ focused: true });
3378
+ yield* check(approval, { focused: true });
3456
3379
  yield* marks.sent;
3457
- const focusHeld = approval === void 0 ? Effect.void : flush("type", marks.input).pipe(Effect.andThen(approval.check({ focused: true })));
3380
+ const focusHeld = check(approval, { focused: true }, flush("type", marks.input));
3458
3381
  if (text === "" && replace && into !== void 0) {
3459
3382
  yield* sameDocument("type", since);
3460
3383
  yield* keyStroke("type", marks.input, ["Delete"]);
@@ -3475,7 +3398,7 @@ const typeText = (input) => {
3475
3398
  }
3476
3399
  yield* flush("type", marks.input);
3477
3400
  if (typeOptions.submit === true) {
3478
- if (approval !== void 0) yield* approval.check({ focused: true });
3401
+ yield* check(approval, { focused: true });
3479
3402
  yield* sameDocument("type", since);
3480
3403
  const before = mark();
3481
3404
  yield* keyStroke("type", marks.input, ["Enter"]);
@@ -3508,16 +3431,13 @@ const press = (input) => {
3508
3431
  yield* marks.on(yield* targetFor("press", on, approval, marks));
3509
3432
  const focused = yield* mutate("press", scriptCall("focusOn", on), approval).pipe(Effect.flatMap(decodeWith("press", EditResultSchema)));
3510
3433
  if ("error" in focused) return yield* editFailure("press", on, focused);
3511
- if (approval !== void 0) yield* approval.check({ focused: true });
3434
+ yield* check(approval, { focused: true });
3512
3435
  }
3513
3436
  const before = mark();
3514
3437
  let due = now();
3515
3438
  yield* marks.sent;
3516
3439
  for (let index = 0; index < times; index++) {
3517
- if (index > 0 && approval !== void 0 && activates) {
3518
- yield* flush("press", marks.input);
3519
- yield* approval.check({ focused: true });
3520
- }
3440
+ if (index > 0 && activates) yield* check(approval, { focused: true }, flush("press", marks.input));
3521
3441
  yield* sameDocument("press", since);
3522
3442
  yield* keyStroke("press", marks.input, parts, hold);
3523
3443
  if (index + 1 < times) {
@@ -3537,14 +3457,14 @@ const scroll = (input) => {
3537
3457
  const { mark, settle } = input;
3538
3458
  return (scrollOptions = {}, style = plain) => {
3539
3459
  const target = scrollOptions.at;
3540
- const valid = Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0) ? Effect.void : failWith("scroll", new InvalidRequest({ detail: "scroll deltas must be finite" }));
3460
+ const valid = Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0) ? Effect.void : refuse("scroll", "scroll deltas must be finite");
3541
3461
  return perform("scroll", {
3542
3462
  target: target === void 0 ? void 0 : named(target),
3543
3463
  options: {
3544
3464
  dx: scrollOptions.dx,
3545
3465
  dy: scrollOptions.dy
3546
3466
  }
3547
- }, settings.actionTimeout, valid.pipe(Effect.andThen(preparePolicy("scroll", { target: typeof target === "string" ? target : void 0 }, target === void 0 ? [] : [target]))), (marks, approval) => Effect.gen(function* () {
3467
+ }, settings.actionTimeout, valid.pipe(Effect.andThen(preparePolicy("scroll", { target: refOf(target) }, target === void 0 ? [] : [target]))), (marks, approval) => Effect.gen(function* () {
3548
3468
  yield* valid;
3549
3469
  const viewport = yield* viewportFor("scroll");
3550
3470
  const middle = {
@@ -3765,7 +3685,7 @@ const timed = (options, native, timestamp, generation) => {
3765
3685
  hostTime,
3766
3686
  uncertaintyMillis: uncertaintyAt(estimate, hostTime)
3767
3687
  }),
3768
- receivedAt: Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6,
3688
+ receivedAt: monotonicMillis(options.clock),
3769
3689
  width: size.width,
3770
3690
  height: size.height,
3771
3691
  ...generation.transport.frameTag()
@@ -3811,7 +3731,7 @@ const sizeFilter = (options, deliver, drop) => Effect.gen(function* () {
3811
3731
  run = void 0;
3812
3732
  Queue.offerUnsafe(wake, void 0);
3813
3733
  };
3814
- const now = () => Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6;
3734
+ const now = () => monotonicMillis(options.clock);
3815
3735
  const adopt = (adopted) => {
3816
3736
  run = void 0;
3817
3737
  expected = adopted.device;
@@ -3954,7 +3874,7 @@ const make$8 = (options) => Effect.gen(function* () {
3954
3874
  let latest = Option.none();
3955
3875
  let history = [];
3956
3876
  const keepMillis = Duration.toMillis(options.frameHistory);
3957
- const now = () => Number(options.clock.monotonicTimeNanosUnsafe()) / 1e6;
3877
+ const now = () => monotonicMillis(options.clock);
3958
3878
  const counts = tally(now);
3959
3879
  let delivered = 0;
3960
3880
  const current = (frame) => isCurrent(frame, generation, latest);
@@ -4035,6 +3955,9 @@ const make$8 = (options) => Effect.gen(function* () {
4035
3955
  return current.phase !== "Stopped" && current.failure === void 0 && !isClosed();
4036
3956
  };
4037
3957
  if (current === void 0 || !resumable()) return;
3958
+ pictures.track("picture restoration", options.restore(), failed(current));
3959
+ yield* pictures.wait("picture restoration");
3960
+ yield* healthy(current);
4038
3961
  const viewport = yield* readViewport(options);
4039
3962
  if (!resumable()) return;
4040
3963
  sizes.restart(viewport);
@@ -4473,7 +4396,7 @@ const millisOf = (input) => Option.match(Duration.fromInput(input), {
4473
4396
  });
4474
4397
  const reuse = (page, bridge, viewport, capture) => (operation, options) => Effect.gen(function* () {
4475
4398
  const maxAge = millisOf(options.maxAge ?? defaultMaxAge);
4476
- if (!(maxAge >= 0)) return yield* failWith(operation, new InvalidRequest({ detail: "maxAge must be a duration of zero or more" }));
4399
+ if (!(maxAge >= 0)) return yield* refuse(operation, "maxAge must be a duration of zero or more");
4477
4400
  const latest = yield* capture.latest;
4478
4401
  const known = viewport.knownViewport();
4479
4402
  const { activity } = page;
@@ -4498,6 +4421,11 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4498
4421
  watch: bridge.currentDocument("screencast"),
4499
4422
  frameHistory: settings.frameHistory,
4500
4423
  viewport: Effect.suspend(() => viewport.viewportFor("screencast")),
4424
+ restore: () => page.protocol.send("Page.captureScreenshot", {
4425
+ format: "jpeg",
4426
+ quality: 0,
4427
+ optimizeForSpeed: true
4428
+ }),
4501
4429
  imageSize: jpegSize,
4502
4430
  error
4503
4431
  });
@@ -4549,7 +4477,7 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4549
4477
  const zoom = (requested) => lane.read("zoom")(Effect.gen(function* () {
4550
4478
  const region = yield* Schema.decodeEffect(Region)(requested).pipe(Effect.mapError((error) => undispatched("zoom", new InvalidRequest({ detail: error.message }))));
4551
4479
  const size = viewport.knownViewport() ?? (yield* viewport.viewportFor("zoom"));
4552
- if (region.x + region.width > size.width || region.y + region.height > size.height) return yield* failWith("zoom", new InvalidRequest({ detail: "the crop must fit entirely within the viewport" }));
4480
+ if (region.x + region.width > size.width || region.y + region.height > size.height) return yield* refuse("zoom", "the crop must fit entirely within the viewport");
4553
4481
  const image = yield* picture("zoom", 80, region);
4554
4482
  return new Zoom({
4555
4483
  page: id,
@@ -4560,7 +4488,7 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4560
4488
  const captureStats = (options = {}) => {
4561
4489
  if (options.window === void 0) return capture.stats();
4562
4490
  const millis = millisOf(options.window);
4563
- return millis > 0 && millis <= Duration.toMillis(countsKept) ? capture.stats(millis) : failWith("captureStats", new InvalidRequest({ detail: "window must be over zero and at most a minute" }));
4491
+ return millis > 0 && millis <= Duration.toMillis(countsKept) ? capture.stats(millis) : refuse("captureStats", "window must be over zero and at most a minute");
4564
4492
  };
4565
4493
  return {
4566
4494
  capture,
@@ -4578,10 +4506,6 @@ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
4578
4506
  * read takes its turn on the page and shares its work with identical reads, as `lane.ts`
4579
4507
  * describes. The viewport's text as it was last read is kept, for `Page.state`.
4580
4508
  */
4581
- const SnapshotResult = Schema.Union([Schema.Struct({
4582
- snapshot: Snapshot,
4583
- nextRef: Schema.Finite
4584
- }), Schema.Struct({ invalid: Schema.String })]);
4585
4509
  const FindResults = Schema.Struct({
4586
4510
  found: Schema.Array(Found),
4587
4511
  nextRef: Schema.Finite
@@ -4590,7 +4514,6 @@ const UntilResult = Schema.Struct({
4590
4514
  met: Schema.Boolean,
4591
4515
  mark: Schema.String
4592
4516
  });
4593
- const ConditionResult = Schema.Union([Schema.Struct({ met: Schema.Boolean }), Schema.Struct({ invalid: Schema.String })]);
4594
4517
  const wanted = (value) => value === void 0 ? null : typeof value === "string" ? value : {
4595
4518
  source: value.source,
4596
4519
  flags: value.flags
@@ -4625,8 +4548,8 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4625
4548
  ...asked,
4626
4549
  firstRef: yield* Ref.get(nextRef)
4627
4550
  };
4628
- const result = yield* evaluate("snapshot", scriptCall("snapshot", request)).pipe(Effect.flatMap(decodeWith("snapshot", SnapshotResult)));
4629
- if ("invalid" in result) return yield* failWith("snapshot", new InvalidRequest({ detail: result.invalid }));
4551
+ const result = yield* evaluate("snapshot", scriptCall("snapshot", request)).pipe(Effect.flatMap(decodeWith("snapshot", SnapshotResultSchema)));
4552
+ if ("invalid" in result) return yield* refuse("snapshot", result.invalid);
4630
4553
  const { text, truncated, url } = result.snapshot;
4631
4554
  yield* counted(result.nextRef);
4632
4555
  yield* Effect.annotateCurrentSpan({
@@ -4662,7 +4585,7 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4662
4585
  maxChars: options.maxChars ?? 12e3,
4663
4586
  unmask: options.unmask ?? false
4664
4587
  };
4665
- return (scope !== "viewport" && !/^e\d+$/.test(scope) ? failWith("text", new InvalidRequest({ detail: `${scope} is neither "viewport" nor a ref such as e12` })) : texts(JSON.stringify(request), Effect.gen(function* () {
4588
+ return (scope !== "viewport" && !/^e\d+$/.test(scope) ? refuse("text", `${scope} is neither "viewport" nor a ref such as e12`) : texts(JSON.stringify(request), Effect.gen(function* () {
4666
4589
  const result = yield* evaluate("text", scriptCall("text", request)).pipe(Effect.flatMap(decodeWith("text", TextResultSchema)));
4667
4590
  if (result === null) return yield* failWith("text", new StaleRef({ ref: scope }));
4668
4591
  yield* Effect.annotateCurrentSpan({
@@ -4686,7 +4609,7 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4686
4609
  };
4687
4610
  const millis = Math.min(Duration.toMillis(timeout), Number.MAX_SAFE_INTEGER);
4688
4611
  const unmet = () => failWith("waitFor", new Timeout({ millis }));
4689
- const look = evaluate("waitFor", scriptCall("waitUntil", request, millis)).pipe(Effect.flatMap(decodeWith("waitFor", ConditionResult)), Effect.flatMap((result) => "invalid" in result ? failWith("waitFor", new InvalidRequest({ detail: result.invalid })) : result.met ? Effect.void : unmet()));
4612
+ const look = evaluate("waitFor", scriptCall("waitUntil", request, millis)).pipe(Effect.flatMap(decodeWith("waitFor", ConditionResultSchema)), Effect.flatMap((result) => "invalid" in result ? refuse("waitFor", result.invalid) : result.met ? Effect.void : unmet()));
4690
4613
  return lane.read("waitFor")(Effect.void).pipe(Effect.andThen(Effect.retry(look, { while: contextGone })), Effect.timeoutOrElse({
4691
4614
  duration: timeout,
4692
4615
  orElse: unmet
@@ -4714,6 +4637,51 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4714
4637
  };
4715
4638
  });
4716
4639
  //#endregion
4640
+ //#region src/internal/pictures/motion.ts
4641
+ /**
4642
+ * Where a page's screen moves, from its screencast frames: each frame decoded and cut into
4643
+ * 16-pixel blocks, a block moving where a few of its pixels, sampled every other one, changed by
4644
+ * more than JPEG's noise in any color channel.
4645
+ * What moved among the frames before an input is the page's background, such as an idle
4646
+ * animation; foreground stillness ignores it.
4647
+ *
4648
+ * Frames are decoded with the JPEG decoder playwright-core bundles and exports as
4649
+ * `playwright-core/lib/utilsBundle`: effect-browser depends on nothing else, and the runtime has
4650
+ * no image decoder. That entry point has no types, so `utilsBundle.d.ts` declares what is used.
4651
+ */
4652
+ const picture = (frame) => jpegjs.decode(frame.data, { useTArray: true });
4653
+ /** A sample changed where any color channel moved more than this, beyond JPEG's noise. */
4654
+ const noise = 32;
4655
+ /** The blocks where two pictures differ, by `row * columns + column`; -1 for different sizes. */
4656
+ const moved = (from, to) => {
4657
+ if (from.width !== to.width || from.height !== to.height) return [-1];
4658
+ const columns = Math.ceil(from.width / 16);
4659
+ const counts = /* @__PURE__ */ new Map();
4660
+ const differs = (at) => Math.abs((from.data[at] ?? 0) - (to.data[at] ?? 0)) > noise;
4661
+ for (let y = 0; y < from.height; y += 2) for (let x = 0; x < from.width; x += 2) {
4662
+ const at = (y * from.width + x) * 4;
4663
+ const block = Math.floor(y / 16) * columns + Math.floor(x / 16);
4664
+ if (differs(at) || differs(at + 1) || differs(at + 2)) counts.set(block, (counts.get(block) ?? 0) + 1);
4665
+ }
4666
+ return [...counts].flatMap(([block, count]) => count >= 3 ? [block] : []);
4667
+ };
4668
+ /** The blocks that moved from one picture to the next and their neighbours; none past a resize. */
4669
+ const background = (pictures) => {
4670
+ const columns = Math.ceil((pictures[0]?.width ?? 0) / 16);
4671
+ const around = (block) => [
4672
+ -1,
4673
+ 0,
4674
+ 1
4675
+ ].flatMap((row) => [
4676
+ -1,
4677
+ 0,
4678
+ 1
4679
+ ].filter((step) => block >= 0 && block % columns + step >= 0).filter((step) => block % columns + step < columns).map((step) => block + row * columns + step));
4680
+ return new Set(pictures.flatMap((to, index) => moved(pictures[index - 1] ?? to, to).flatMap(around)));
4681
+ };
4682
+ /** Whether anything moved from one picture to another outside the background. */
4683
+ const stirs = (from, to, mask) => moved(from, to).some((block) => !mask.has(block));
4684
+ //#endregion
4717
4685
  //#region src/internal/reading/ready.ts
4718
4686
  /**
4719
4687
  * Waiting until a page is ready to be shown. The page's own evidence leads: one page-script call
@@ -4727,7 +4695,10 @@ const make$6 = Effect.fnUntraced(function* (page, bridge) {
4727
4695
  * its connection, and a frame that arrives first starts the spell again. Where the page's changes
4728
4696
  * are recorded, its evidence includes them: nothing in view may have changed for the spell, which a
4729
4697
  * DOM page's changes show better than its paint, and no stall on the way can hide. Content
4730
- * stillness starts that record itself and ignores capture silence. Each new document starts a
4698
+ * stillness starts that record itself and ignores capture silence. Foreground stillness lets
4699
+ * frames keep coming but compares them, so that only motion outside what was moving before the
4700
+ * page's latest input counts; it still waits out unanswered acknowledgements while no frame
4701
+ * comes, and its last look follows the round trip too. Each new document starts a
4731
4702
  * new observation, while retries of destroyed script contexts share the caller's deadline.
4732
4703
  */
4733
4704
  const make$5 = (page, bridge, capture) => (options = {}) => {
@@ -4738,30 +4709,54 @@ const make$5 = (page, bridge, capture) => (options = {}) => {
4738
4709
  }), Effect.flatMap(decodeWith("ready", ReadinessSchema)), Effect.tap((waiting) => Effect.annotateCurrentSpan({ waiting: waiting.join(",") })), Effect.filterOrElse((waiting) => waiting.length === 0, () => failWith("ready", new Timeout({ millis: Duration.toMillis(timeout) }))));
4739
4710
  const quiet = (millis, until) => Effect.gen(function* () {
4740
4711
  const running = yield* capture.active;
4741
- let { received } = yield* capture.stats();
4712
+ const foreground = options.still === "foreground";
4713
+ let counts = yield* capture.stats();
4742
4714
  let stirred = running ? page.now() : Number.POSITIVE_INFINITY;
4715
+ let reference;
4716
+ const decoded = (frame) => Effect.try({
4717
+ try: () => picture(frame),
4718
+ catch: (error) => undispatched("ready", new Failed({ detail: `no picture: ${String(error)}` }))
4719
+ });
4720
+ const { inputAt } = page.activity;
4721
+ const document = Option.getOrUndefined(yield* capture.latest)?.document;
4722
+ const before = (foreground ? yield* capture.recent : []).filter((frame) => frame.hostTime < inputAt && frame.hostTime >= inputAt - 1500 && frame.document === document);
4723
+ const sampled = before.filter((_, index) => index % Math.ceil(before.length / 8) === 0);
4724
+ const mask = background(yield* Effect.forEach(sampled, decoded));
4743
4725
  const look = Effect.gen(function* () {
4744
4726
  const stats = yield* capture.stats();
4745
- if (!Option.exists(yield* capture.latest, capture.current) || stats.received !== received || stats.ackBacklog > 0) {
4746
- received = stats.received;
4747
- stirred = page.now();
4727
+ const latest = Option.getOrUndefined(yield* capture.latest);
4728
+ const arrived = stats.received !== counts.received;
4729
+ const unseen = stats.accepted - counts.accepted;
4730
+ counts = stats;
4731
+ if (latest === void 0 || !capture.current(latest)) stirred = page.now();
4732
+ else if (!foreground ? arrived || stats.ackBacklog > 0 : !arrived && stats.ackBacklog > 0) stirred = page.now();
4733
+ else if (foreground && (unseen > 0 || reference === void 0)) {
4734
+ const frames = (yield* capture.recent).slice(-Math.max(1, Math.min(8, unseen)));
4735
+ if (unseen > frames.length) stirred = page.now();
4736
+ for (const frame of frames) {
4737
+ const seen = yield* decoded(frame);
4738
+ if (reference === void 0 || stirs(reference, seen, mask)) {
4739
+ reference = seen;
4740
+ stirred = page.now();
4741
+ }
4742
+ }
4748
4743
  }
4749
4744
  return page.now() - stirred;
4750
4745
  });
4751
4746
  const spell = Effect.repeat(look, {
4752
- schedule: Schedule.spaced(Duration.millis(25)),
4747
+ schedule: Schedule.fixed(Duration.millis(foreground ? 100 : 25)),
4753
4748
  until: (still) => still >= millis
4754
4749
  });
4755
4750
  const capturing = running ? Effect.never : capture.stream().pipe(Stream.runDrain, Effect.andThen(Effect.never));
4756
4751
  const still = Effect.gen(function* () {
4757
4752
  for (;;) {
4758
4753
  yield* spell;
4759
- const before = received;
4754
+ const before = counts.received;
4760
4755
  yield* Effect.all([settled(until), capture.sync], {
4761
4756
  concurrency: 2,
4762
4757
  discard: true
4763
4758
  });
4764
- if ((yield* look) >= millis && received === before) return;
4759
+ if ((yield* look) >= millis && (foreground || counts.received === before)) return;
4765
4760
  }
4766
4761
  });
4767
4762
  return yield* Effect.raceFirst(capturing, still);
@@ -4860,7 +4855,7 @@ const make$3 = (page, read, frames) => ({ since, until, unmask }) => Effect.gen(
4860
4855
  since,
4861
4856
  until
4862
4857
  }, page.now());
4863
- if (Option.isNone(asked)) return yield* failWith("window", new InvalidRequest({ detail: "a window's bounds must be finite, its reach not negative" }));
4858
+ if (Option.isNone(asked)) return yield* refuse("window", "a window's bounds must be finite, its reach not negative");
4864
4859
  const bounds = asked.value;
4865
4860
  const changes = yield* Effect.result(read({
4866
4861
  since: isWindow(since) ? since.changes ?? bounds.since : isFrame(since) ? since : bounds.since,
@@ -4949,7 +4944,7 @@ const make$2 = (page, perform, preparePolicy) => {
4949
4944
  "about:",
4950
4945
  "data:",
4951
4946
  "file:"
4952
- ].includes(parsed.protocol)) return failWith("navigate", new InvalidRequest({ detail: `"${redact(url)}" is not a URL` }));
4947
+ ].includes(parsed.protocol)) return refuse("navigate", `"${redact(url)}" is not a URL`);
4953
4948
  return navigation("navigate", () => playwright.goto(parsed.href, {
4954
4949
  waitUntil: "domcontentloaded",
4955
4950
  timeout: 0
@@ -5099,6 +5094,6 @@ const make = Effect.fnUntraced(function* (options) {
5099
5094
  return Object.assign(new Built(internals), assembled);
5100
5095
  });
5101
5096
  //#endregion
5102
- export { performed as a, pause as i, make as n, mapping as o, viewOf as r, internalsOf as t };
5097
+ export { performed as a, pause as i, make as n, viewOf as r, internalsOf as t };
5103
5098
 
5104
- //# sourceMappingURL=page-B92QnRfT.mjs.map
5099
+ //# sourceMappingURL=page-BKwfF6z-.mjs.map