effect-browser 0.3.0-beta.20 → 0.3.0-beta.21

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 (70) hide show
  1. package/README.md +92 -0
  2. package/dist/Browser-CiXHT1yO.d.mts +129 -0
  3. package/dist/{Browser-BwCldIff.mjs → Browser-CxXUjxOp.mjs} +10 -20
  4. package/dist/Browser-CxXUjxOp.mjs.map +1 -0
  5. package/dist/Browser.d.mts +2 -129
  6. package/dist/Browser.mjs +1 -1
  7. package/dist/Cdp-CzCRnMlM.d.mts +10 -0
  8. package/dist/Cdp.d.mts +5 -26
  9. package/dist/Cdp.mjs +4 -45
  10. package/dist/Cdp.mjs.map +1 -1
  11. package/dist/Chromium.d.mts +1 -1
  12. package/dist/Chromium.mjs +3 -3
  13. package/dist/Chromium.mjs.map +1 -1
  14. package/dist/Expect-u3P_ENLX.d.mts +62 -0
  15. package/dist/Expect.d.mts +2 -62
  16. package/dist/Expect.mjs +2 -2
  17. package/dist/LICENSE-jpeg-js +28 -0
  18. package/dist/Moment.mjs +1 -1
  19. package/dist/Page-BP02bmNv.mjs +264 -0
  20. package/dist/{Page-CfB6Zn1p.mjs.map → Page-BP02bmNv.mjs.map} +1 -1
  21. package/dist/Page.mjs +1 -1
  22. package/dist/{Plan-C-d5QYS8.mjs → Plan-BG1WifW2.mjs} +48 -4
  23. package/dist/Plan-BG1WifW2.mjs.map +1 -0
  24. package/dist/Plan.mjs +1 -1
  25. package/dist/Policy-BGWjbqCl.d.mts +76 -0
  26. package/dist/Policy.d.mts +2 -76
  27. package/dist/Policy.mjs +1 -1
  28. package/dist/Presentation-DxVikn9_.d.mts +48 -0
  29. package/dist/Presentation.d.mts +2 -48
  30. package/dist/Presentation.mjs +2 -2
  31. package/dist/Stage-NHp8N81k.d.mts +65 -0
  32. package/dist/Stage.d.mts +2 -65
  33. package/dist/Stage.mjs +2 -2
  34. package/dist/{Supervisor-mVPeEUp8.d.mts → Supervisor-CfRxbaso.d.mts} +2 -2
  35. package/dist/Supervisor.d.mts +1 -1
  36. package/dist/cdp-BMpYMWYe.mjs +69 -0
  37. package/dist/cdp-BMpYMWYe.mjs.map +1 -0
  38. package/dist/cdp-DL7XfEj5.d.mts +41 -0
  39. package/dist/{context-w3NJPRLo.mjs → context-EGhOqjNN.mjs} +5 -5
  40. package/dist/context-EGhOqjNN.mjs.map +1 -0
  41. package/dist/index.d.mts +7 -6
  42. package/dist/index.mjs +4 -3
  43. package/dist/page-PTM8m5wC.mjs +4057 -0
  44. package/dist/page-PTM8m5wC.mjs.map +1 -0
  45. package/dist/{url-Cd04Okid.mjs → url-Bsp2y55M.mjs} +2 -2
  46. package/dist/{url-Cd04Okid.mjs.map → url-Bsp2y55M.mjs.map} +1 -1
  47. package/dist/workerd/Cdp.d.mts +3 -0
  48. package/dist/workerd/Cdp.mjs +18 -0
  49. package/dist/workerd/Cdp.mjs.map +1 -0
  50. package/dist/workerd/index.d.mts +16 -0
  51. package/dist/workerd/index.mjs +17 -0
  52. package/package.json +12 -1
  53. package/src/Browser.ts +20 -30
  54. package/src/Cdp.ts +7 -92
  55. package/src/index.ts +1 -15
  56. package/src/internal/cdp.ts +134 -0
  57. package/src/internal/page/bridge.ts +2 -2
  58. package/src/internal/page/context.ts +21 -24
  59. package/src/internal/page/identity.ts +6 -23
  60. package/src/internal/pictures/motion.ts +3 -3
  61. package/src/workerd/Cdp.ts +11 -0
  62. package/src/workerd/index.ts +17 -0
  63. package/dist/Browser-BwCldIff.mjs.map +0 -1
  64. package/dist/Page-CfB6Zn1p.mjs +0 -610
  65. package/dist/Plan-C-d5QYS8.mjs.map +0 -1
  66. package/dist/context-w3NJPRLo.mjs.map +0 -1
  67. package/dist/match.inpage-xOe9LNO0.mjs +0 -48
  68. package/dist/match.inpage-xOe9LNO0.mjs.map +0 -1
  69. package/dist/page-BKwfF6z-.mjs +0 -5099
  70. package/dist/page-BKwfF6z-.mjs.map +0 -1
@@ -0,0 +1,4057 @@
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
+ import { BrowserError, Failed, InvalidRequest, NavigationFailed, NotActionable, NotFound, PolicyTimeout, StaleRef, Timeout } from "./BrowserError.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-EGhOqjNN.mjs";
5
+ import { o as Correlation, r as redact, t as parse } from "./url-Bsp2y55M.mjs";
6
+ import { BrowserPaint, CaptureStats, Frame, Image, Screenshot } from "./Frame.mjs";
7
+ import { c as State, 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-BP02bmNv.mjs";
8
+ import { Snapshot } from "./Snapshot.mjs";
9
+ import { r as Changes, t as Change } from "./Change-DNvztNLu.mjs";
10
+ import { Window } from "./Moment.mjs";
11
+ import { Cause, Clock, Deferred, Duration, Effect, Exit, Fiber, MutableRef, Option, Queue, Random, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, SynchronizedRef } from "effect";
12
+ //#region src/internal/input/edit.inpage.ts
13
+ /**
14
+ * In the page: editing a field. It refuses text that a focused control could act on, focuses a
15
+ * field and chooses a select's options. See `reading/names.inpage.ts` for what a page-side part
16
+ * may use.
17
+ */
18
+ const TypeableResultSchema = Schema.Union([Schema.Struct({
19
+ ok: Schema.Literal(true),
20
+ secret: Schema.Boolean,
21
+ subject: Schema.optional(Subject)
22
+ }), Schema.Struct({
23
+ error: Schema.Literals(["stale", "untypeable"]),
24
+ detail: Schema.String
25
+ })]);
26
+ const EditFailure = Schema.Struct({
27
+ error: Schema.String,
28
+ stale: Schema.optional(Schema.Boolean)
29
+ });
30
+ const FocusResultSchema = Schema.Union([Schema.Struct({
31
+ ok: Schema.Literal(true),
32
+ detail: Schema.String,
33
+ secret: Schema.Boolean
34
+ }), EditFailure]);
35
+ const EditResultSchema = Schema.Union([Schema.Struct({
36
+ ok: Schema.Literal(true),
37
+ detail: Schema.String
38
+ }), EditFailure]);
39
+ //#endregion
40
+ //#region src/internal/input/targets.inpage.ts
41
+ /**
42
+ * In the page: where input lands. It resolves a ref or a point to the control under it, in
43
+ * top-document viewport pixels, with the subject's context, and plans a wheel scroll that brings a
44
+ * ref into view. See `reading/names.inpage.ts` for what a page-side part may use.
45
+ */
46
+ const ResolvedPointSchema = Schema.Struct({
47
+ ...Point.fields,
48
+ ...Subject.fields,
49
+ element: Schema.String,
50
+ box: Schema.optional(Box),
51
+ cursor: Schema.String,
52
+ href: Schema.optional(Schema.String)
53
+ });
54
+ const PointResultSchema = Schema.Union([ResolvedPointSchema, Schema.Struct({
55
+ error: Schema.Literals([
56
+ "stale",
57
+ "hidden",
58
+ "offscreen",
59
+ "disabled",
60
+ "covered",
61
+ "outside"
62
+ ]),
63
+ detail: Schema.String
64
+ })]);
65
+ const ScrollPlanSchema = Schema.NullOr(Schema.Struct({
66
+ ...Point.fields,
67
+ dx: Schema.Finite,
68
+ dy: Schema.Finite
69
+ }));
70
+ //#endregion
71
+ //#region src/internal/reading/controls.inpage.ts
72
+ /**
73
+ * 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
74
+ * read to; and a condition `Page.waitFor` waits for. See `names.inpage.ts` for what a page-side part
75
+ * may use.
76
+ */
77
+ const ConditionResultSchema = Schema.Union([Schema.Struct({ met: Schema.Boolean }), Schema.Struct({ invalid: Schema.String })]);
78
+ //#endregion
79
+ //#region src/internal/reading/outline.inpage.ts
80
+ /**
81
+ * In the page: the outline a model reads, with a ref for each control. See `names.inpage.ts` for
82
+ * what a page-side part may use.
83
+ */
84
+ const SnapshotResultSchema = Schema.Union([Schema.Struct({
85
+ snapshot: Snapshot,
86
+ nextRef: Schema.Finite
87
+ }), Schema.Struct({ invalid: Schema.String })]);
88
+ const ViewportResultSchema = Schema.Struct({
89
+ width: Schema.Finite,
90
+ height: Schema.Finite
91
+ });
92
+ //#endregion
93
+ //#region src/internal/reading/ready.inpage.ts
94
+ /**
95
+ * In the page: whether it is ready to be shown, checked until it is or until a deadline. Its
96
+ * document is parsed and has painted since, nothing that ends is animating in view, its fonts and
97
+ * the images in view have loaded, nothing in view is marked busy (`aria-busy`), and the viewport
98
+ * shows something: text, a canvas drawn on, a picture or drawing larger than an icon, a video or a
99
+ * frame. So a canvas mounted blank, or a spinner alone, is still loading. A screen that only says
100
+ * "Loading…" in words reads as ready, and a WebGL canvas drawn once, without keeping its drawing,
101
+ * reads as blank. Asked for a quiet spell, where the page's changes are recorded, nothing in view
102
+ * may have changed for that long either. Content mode starts and refreshes the recorder and
103
+ * observes a full quiet interval from its first check in each document. After input, it waits a
104
+ * task and a frame. See `names.inpage.ts` for what a page-side part may use.
105
+ */
106
+ /** What the page is still waiting for; none when it is ready. */
107
+ const ReadinessSchema = Schema.Array(Schema.Literals([
108
+ "load",
109
+ "paint",
110
+ "animations",
111
+ "images",
112
+ "fonts",
113
+ "busy",
114
+ "content",
115
+ "changing"
116
+ ]));
117
+ //#endregion
118
+ //#region src/internal/reading/text.inpage.ts
119
+ /**
120
+ * In the page: the text a page shows, and the one rule for what a field shows. A field's value
121
+ * reads `••••` unless a caller unmasks it, and a secret field's always does, including a field
122
+ * that was secret when the library saw it, such as a password its page now reveals. Text, and the
123
+ * outline, are cut at a line. See `names.inpage.ts` for what a page-side part may use.
124
+ */
125
+ /** What `read` sends back, or null when its ref no longer names an element. */
126
+ const TextResultSchema = Schema.NullOr(Schema.Struct({
127
+ text: Schema.String,
128
+ truncated: Schema.Boolean,
129
+ url: Schema.String,
130
+ title: Schema.String
131
+ }));
132
+ //#endregion
133
+ //#region src/internal/timeline/changes.inpage.ts
134
+ /**
135
+ * In the page: what changed after `since`, up to `until`, as a read returns it. A read starts the
136
+ * record if it has not, and keeps it going. What waited for the page to render is judged first, as
137
+ * it stands; then each element's history is folded over the window (`history.inpage.ts`), and only
138
+ * now named: its role, its name when something else names it, its tag, and its context, where it
139
+ * is or, for what was removed, where it was. A field's value reads `••••` unless the read unmasks
140
+ * it, and a secret field's always does. A change names the input it followed only where it was
141
+ * the input's own doing, by `Change.cause`'s rule. See `names.inpage.ts` for what a page-side part
142
+ * may use.
143
+ */
144
+ /**
145
+ * What a read sends back: its window and the record's state, and each change as `Change` encodes
146
+ * it, all on the page's clock.
147
+ */
148
+ const ChangesResultSchema = Schema.Struct({
149
+ until: Schema.Finite,
150
+ from: Schema.Finite,
151
+ dropped: Schema.Int,
152
+ records: Schema.Array(Change)
153
+ });
154
+ //#endregion
155
+ //#region src/internal/page/bridge.ts
156
+ /**
157
+ * The bridge to the page script, and the page's documents as its own session sees them commit and
158
+ * load, so a load follows its commit. At the library's first read of a page, its own protocol
159
+ * session registers the script, so every later document runs it in an isolated world from its
160
+ * start; calls go to the current document's world. The script is composed from the domains'
161
+ * page-side parts, the `*.inpage.ts` modules, in the order they depend on one another. At the first
162
+ * read of a page's changes, its session registers the recorder too, so every later document
163
+ * records from its start.
164
+ */
165
+ /** The expression that installs the script and evaluates to its API; packaging freezes it. */
166
+ const installSource = "(() => {var pageScript = (function() {\n //#region src/internal/input/edit.inpage.ts\n const edit = (names, guard, placing) => {\n const { activeElement, clean, describe, inCurrentDocument, isDisabled, isHtml, isInput, isSecret, isSelect, isTextArea, lookup, nameOf, roleOf } = names;\n const { textEntry, typingRefusal } = guard;\n /** Whether text may be typed into a ref or focus, and, for a `secret`, only into a secret field. */\n const typeable = (ref, secret) => {\n const element = ref === null ? activeElement() : lookup(ref);\n if (element === void 0) return {\n error: \"stale\",\n detail: `${ref ?? \"the focused element\"} is not on the page any more`\n };\n const refusal = element === null ? secret ? \"nothing has focus to type a secret into\" : void 0 : typingRefusal(element, ref !== null) ?? (secret && !isSecret(element) ? `${describe(element)} is not a secret field` : void 0);\n if (refusal !== void 0) return {\n error: \"untypeable\",\n detail: refusal\n };\n if (element === null || ref !== null || !textEntry(element)) return {\n ok: true,\n secret: element !== null && isSecret(element)\n };\n const role = roleOf(element);\n const tag = element.tagName.toLowerCase();\n return {\n ok: true,\n secret: isSecret(element),\n subject: {\n role,\n name: nameOf(element, role),\n tag,\n context: placing.contextOf(element)\n }\n };\n };\n /**\n * The focused field, when it is a text field named `name`: one a page put in place of a field\n * just pressed and gave focus to, as a typeahead does.\n */\n const replacement = (name) => {\n const active = activeElement();\n return active !== null && textEntry(active) && inCurrentDocument(active) && nameOf(active, roleOf(active)) === name ? active : void 0;\n };\n /**\n * Focus a field to type into. `follow` is the field's name when the action pressed it first: a\n * page may answer the press with another field of that name in its place, focused, and the keys\n * follow focus there, as a person's would.\n */\n const focus = (ref, replace, follow) => {\n const element = lookup(ref) ?? (follow === null ? void 0 : replacement(follow));\n if (element === void 0) return {\n error: `${ref} is not on the page any more`,\n stale: true\n };\n if (isDisabled(element)) return { error: `${ref} is disabled` };\n if (!textEntry(element)) return { error: `${ref} is ${describe(element)}, not a text field` };\n if (isHtml(element)) element.focus();\n if (replace) {\n if (isInput(element) || isTextArea(element)) element.select();\n else element.ownerDocument.getSelection()?.selectAllChildren(element);\n }\n return {\n ok: true,\n detail: describe(element),\n secret: isSecret(element)\n };\n };\n /** Focus any element that takes focus, such as a link or a button, for a key pressed on it. */\n const focusOn = (ref) => {\n const element = lookup(ref);\n if (element === void 0) return {\n error: `${ref} is not on the page any more`,\n stale: true\n };\n if (isDisabled(element)) return { error: `${ref} is disabled` };\n if (isHtml(element)) element.focus();\n return activeElement() === element ? {\n ok: true,\n detail: describe(element)\n } : { error: `${ref} is ${describe(element)}, which does not take focus` };\n };\n const select = (ref, values) => {\n const element = lookup(ref);\n if (element === void 0) return {\n error: `${ref} is not on the page any more`,\n stale: true\n };\n if (!isSelect(element)) return { error: `${ref} is ${describe(element)}, not a <select>; click it and pick an option` };\n const wanted = values.map((value) => value.trim().toLowerCase());\n const options = Array.from(element.options);\n const chosen = options.filter((option) => wanted.includes(option.value.toLowerCase()) || wanted.includes(clean(option.text).toLowerCase()));\n if (chosen.length === 0) return { error: `no option matches; the options are ${options.map((option) => JSON.stringify(clean(option.text, 40))).join(\", \")}` };\n for (const option of options) option.selected = chosen.includes(option);\n element.dispatchEvent(new Event(\"input\", { bubbles: true }));\n element.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return {\n ok: true,\n detail: chosen.map((option) => clean(option.text, 40)).join(\", \")\n };\n };\n return {\n focus,\n focusOn,\n select,\n typeable\n };\n };\n //#endregion\n //#region src/internal/input/evidence.inpage.ts\n const evidence = (names, placing) => {\n const { clean, isDocument, isInput, isSelect, isTextArea, nameOf, parentOf, roleOf, textOf } = names;\n const { groups, headingBefore, textBeside, treeOf } = placing;\n /** A field a submission would send. */\n const isField = (element) => (isInput(element) && ![\n \"button\",\n \"submit\",\n \"reset\",\n \"image\"\n ].includes(element.type) || isSelect(element) || isTextArea(element)) && !element.matches(\":disabled\");\n const isFilled = (field) => isInput(field) && (field.type === \"checkbox\" || field.type === \"radio\") ? field.checked : isInput(field) && field.type === \"file\" ? (field.files?.length ?? 0) > 0 : field.value !== \"\";\n const dialogOf = (element) => {\n for (let node = element; node !== null; node = parentOf(node)) if (node.tagName === \"DIALOG\" || /^(?:alert)?dialog$/.test(node.getAttribute(\"role\") ?? \"\")) return node;\n };\n /** What the page says about an input's first target, for a judge. Never a field's value. */\n const evidenceOf = (element, form, method, action) => {\n const name = nameOf(element, roleOf(element));\n const ids = element.getAttribute(\"aria-describedby\");\n const description = clean(ids === null ? element.getAttribute(\"aria-description\") ?? element.getAttribute(\"title\") : ids.split(/\\s+/).map((id) => element.ownerDocument.getElementById(id)?.textContent ?? \"\").join(\" \"), 120);\n const dialog = dialogOf(element);\n const title = dialog?.querySelector(\"h1,h2,h3,h4,h5,h6,[role=heading]\");\n const dialogName = dialog === void 0 ? \"\" : clean(nameOf(dialog, \"dialog\"), 120) || (title === null || title === void 0 ? \"\" : clean(textOf(title), 120));\n const root = element.getRootNode();\n const top = isDocument(root) ? root.body ?? root : root;\n const inTree = element.closest(\"dialog,[role=dialog],[role=alertdialog]\");\n let nearby = \"\";\n for (const scope of /* @__PURE__ */ new Set([\n element.closest(groups),\n inTree,\n top\n ])) if (scope !== null && nearby === \"\") nearby = textBeside(element, scope, true) || textBeside(element, scope, false);\n const heading = headingBefore(element, treeOf(element));\n const fields = form === null ? [] : Array.from(form.elements).filter(isField).slice(0, 16).map((field) => {\n const autocomplete = clean(field.getAttribute(\"autocomplete\"), 40);\n return {\n type: isInput(field) ? field.type : field.tagName.toLowerCase(),\n name: clean(isInput(field) && field.type === \"hidden\" ? field.getAttribute(\"name\") : nameOf(field, roleOf(field)), 40),\n ...autocomplete === \"\" ? {} : { autocomplete },\n filled: isFilled(field)\n };\n });\n return {\n title: clean(document.title, 120),\n ...description === \"\" || description === name ? {} : { description },\n ...dialogName === \"\" ? {} : { dialog: dialogName },\n ...heading === void 0 ? {} : { heading },\n ...nearby === \"\" ? {} : { nearby },\n ...form === null ? {} : { form: {\n method: method ?? form.method,\n action: action ?? form.action,\n fields\n } }\n };\n };\n return {\n evidenceOf,\n isField,\n isFilled\n };\n };\n //#endregion\n //#region src/internal/input/guard.inpage.ts\n const guard = (names, targets, evidence) => {\n const { activeElement, clean, describe, inCurrentDocument, interactiveRoles, isButton, isDisabled, isHtml, isInput, isLabel, isRoot, isSecret, isSelect, isTextArea, lookup, parentOf, refFor, roleOf } = names;\n const { controlOf, details, hitAt, hrefAttribute, linkOf, receives, within } = targets;\n const { evidenceOf, isField, isFilled } = evidence;\n /**\n * The element whose activation behaviour a click or key on `element` runs, as the browser\n * resolves it along the composed path: a link, button, input, summary or a label's control.\n */\n const activationTarget = (element) => {\n for (let node = element; node !== null; node = parentOf(node)) {\n if (hrefAttribute(node) !== null || node.tagName === \"BUTTON\" || node.tagName === \"SUMMARY\" || isInput(node) && node.type !== \"hidden\") return node;\n if (isLabel(node) && node.control !== null) return node.control;\n }\n };\n /**\n * A control between a press's hit and the approved target that is not the target's own\n * activation, such as a link or button a hover handler nested into it. The approval inspected\n * the target, so that control would act unapproved.\n */\n const nestedControl = (element, hit) => {\n if (!within(element, hit)) return void 0;\n const own = activationTarget(element);\n for (let node = hit; node !== null && node !== element; node = parentOf(node) ?? node.ownerDocument.defaultView?.frameElement ?? null) if (node !== own && (hrefAttribute(node) !== null || node.tagName === \"BUTTON\" || node.tagName === \"SUMMARY\" || isInput(node) && node.type !== \"hidden\" || isSelect(node) || isTextArea(node) || isLabel(node) && node.control !== null && node.control !== own || activatingRoles.has(roleOf(node) ?? \"\"))) return node;\n };\n const textEntry = (element) => isInput(element) && ![\n \"button\",\n \"reset\",\n \"submit\",\n \"image\",\n \"checkbox\",\n \"radio\",\n \"file\",\n \"range\",\n \"color\",\n \"hidden\"\n ].includes(element.type) || isTextArea(element) || isHtml(element) && element.isContentEditable;\n const activatingRoles = /* @__PURE__ */ new Set([\n \"button\",\n \"link\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"tab\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"treeitem\"\n ]);\n const typingRefusal = (element, explicit) => textEntry(element) ? void 0 : explicit ? `${describe(element)} is not a text field` : activationTarget(element) !== void 0 || isSelect(element) || activatingRoles.has(roleOf(element) ?? \"\") ? `focus is on ${describe(element)}, which typed text could activate or change; type into a text field by ref, or use press for keys` : void 0;\n const isSubmitter = (element) => isButton(element) && element.type === \"submit\" || isInput(element) && (element.type === \"submit\" || element.type === \"image\");\n const defaultSubmitter = (form) => {\n const root = form.getRootNode();\n return isRoot(root) ? Array.from(root.querySelectorAll(\"button,input\")).find((candidate) => isSubmitter(candidate) && candidate.form === form) : void 0;\n };\n const purpose = (control, form) => [\n control.getAttribute(\"id\"),\n control.getAttribute(\"name\"),\n control.getAttribute(\"inputmode\"),\n control.getAttribute(\"autocomplete\"),\n control.getAttribute(\"aria-label\"),\n form?.getAttribute(\"id\"),\n form?.getAttribute(\"name\"),\n form?.getAttribute(\"aria-label\")\n ];\n const boundUrl = (url) => {\n const hash = url.indexOf(\"#\");\n return hash === -1 || /^#[/!]/.test(url.slice(hash)) ? url : url.slice(0, hash);\n };\n const inspectInput = (element, plan) => {\n const metadata = details(element, element, 0, 0);\n const control = activationTarget(element) ?? element;\n const form = isInput(control) || isButton(control) || isTextArea(control) || isSelect(control) ? control.form : null;\n const key = plan.keys === null ? void 0 : /(?:^|\\+)(\\+|[^+]+)$/.exec(plan.keys)?.[1];\n const enter = key === \"Enter\";\n const activation = plan.action === \"click\" || plan.action === \"press\" && (enter || key === \"Space\");\n const fieldEnter = isInput(control) && ![\n \"button\",\n \"reset\",\n \"submit\",\n \"image\",\n \"file\",\n \"color\",\n \"hidden\"\n ].includes(control.type) && (plan.action === \"press\" && enter || plan.action === \"type\" && plan.submit);\n const submits = form !== null && (activation && isSubmitter(control) || fieldEnter);\n const submitter = isSubmitter(control) ? control : form === null ? void 0 : defaultSubmitter(form);\n const formDestination = form === null ? void 0 : submitter?.hasAttribute(\"formaction\") === true ? submitter.formAction : form.action;\n const formMethod = form === null ? void 0 : submitter?.hasAttribute(\"formmethod\") === true ? submitter.formMethod : form.method;\n const formTarget = form === null ? void 0 : submitter?.hasAttribute(\"formtarget\") === true ? submitter.formTarget : form.target;\n const link = linkOf(element);\n const href = link === void 0 ? null : hrefAttribute(link);\n const sends = submits && formMethod !== \"dialog\";\n const destination = sends ? formDestination : activation ? metadata.href : void 0;\n const acts = plan.action !== \"hover\" && plan.action !== \"scroll\";\n const builtIn = sends || href !== null && !/^\\s*(?:#\\s*|javascript:[^]*)$/i.test(href) || isInput(control) && ![\n \"button\",\n \"submit\",\n \"image\",\n \"reset\"\n ].includes(control.type) || (isInput(control) || isButton(control)) && control.type === \"reset\" && form !== null || control.tagName === \"SUMMARY\" || control.closest(\"select\") !== null || textEntry(control);\n const facts = [];\n if (sends) facts.push(\"form-submit\");\n if (activation && link?.hasAttribute(\"download\") === true) facts.push(\"download\");\n if (activation && isInput(control) && control.type === \"file\") facts.push(\"upload\");\n if (plan.action === \"type\" && isSecret(control) || sends && form !== null && Array.from(form.elements).some((field) => isField(field) && isSecret(field) && isFilled(field))) facts.push(\"secret\");\n if (activation && !builtIn) facts.push(\"scripted\");\n if (acts && (metadata.role === \"canvas\" || metadata.role === \"iframe\" || metadata.name === \"\")) facts.push(\"opaque\");\n const role = metadata.role;\n const boundName = role !== null && (interactiveRoles.has(role) || role === \"canvas\" || role === \"iframe\") && !(isHtml(element) && element.isContentEditable) ? metadata.name : null;\n const fingerprint = JSON.stringify([\n purpose(control, form),\n element.tagName,\n element.id,\n role,\n boundName,\n metadata.href,\n control === element ? null : refFor(control),\n isInput(control) || isButton(control) ? control.type : control.tagName,\n control.matches(\":disabled\") || isDisabled(control),\n control.getAttribute(\"readonly\"),\n control.getAttribute(\"aria-readonly\"),\n isHtml(control) && control.isContentEditable,\n control.getAttribute(\"name\"),\n control.getAttribute(\"accept\"),\n control.hasAttribute(\"multiple\"),\n link?.getAttribute(\"download\"),\n form === null ? null : refFor(form),\n form === null ? null : boundUrl(form.action),\n form?.method,\n form?.target,\n submitter === void 0 ? null : refFor(submitter),\n formDestination === void 0 ? void 0 : boundUrl(formDestination),\n formMethod,\n formTarget,\n submitter?.formNoValidate,\n form?.noValidate\n ]);\n return {\n inspected: {\n ref: refFor(element),\n element: metadata.element,\n role: metadata.role,\n name: metadata.name,\n cursor: metadata.cursor,\n ...metadata.href === void 0 ? {} : { href: metadata.href },\n secret: isSecret(control),\n fingerprint\n },\n facts,\n destination,\n explain: () => evidenceOf(element, form, formMethod, formDestination)\n };\n };\n const inspect = (plan) => {\n const targets = [];\n const facts = /* @__PURE__ */ new Set();\n let destination = plan.destination ?? void 0;\n let explain = () => ({ title: clean(document.title, 120) });\n for (const [index, target] of plan.targets.entries()) {\n let element;\n if (target === null) element = activeElement();\n else if (typeof target === \"string\") {\n element = lookup(target);\n if (element === void 0) return {\n error: \"stale\",\n detail: target + \" is not on the page any more\",\n index\n };\n } else {\n if (!Number.isFinite(target.x) || !Number.isFinite(target.y) || target.x < 0 || target.y < 0 || target.x >= window.innerWidth || target.y >= window.innerHeight) return {\n error: \"outside\",\n detail: \"the point is outside the viewport\"\n };\n const hit = hitAt(document, target.x, target.y);\n element = hit === null ? null : controlOf(hit);\n if (element === null) return {\n error: \"offscreen\",\n detail: \"nothing is painted at the point\"\n };\n }\n if (element === null) {\n targets.push(null);\n continue;\n }\n if (!inCurrentDocument(element)) return {\n error: \"stale\",\n detail: \"the input target belongs to a replaced document\",\n index\n };\n const refusal = plan.action === \"type\" ? typingRefusal(element, target !== null) : void 0;\n if (refusal !== void 0) return {\n error: \"untypeable\",\n detail: refusal\n };\n const inspected = inspectInput(element, plan);\n if (index === 0) explain = inspected.explain;\n targets.push(inspected.inspected);\n for (const fact of inspected.facts) facts.add(fact);\n destination ??= inspected.destination;\n if (inspected.destination !== void 0) try {\n if (new URL(inspected.destination, location.href).origin !== location.origin) facts.add(\"cross-origin\");\n } catch {\n return {\n error: \"outside\",\n detail: \"the input destination is not a valid URL\"\n };\n }\n }\n if (destination !== void 0) try {\n destination = new URL(destination, location.href).href;\n if (new URL(destination).origin !== location.origin) facts.add(\"cross-origin\");\n } catch {\n return {\n error: \"outside\",\n detail: \"the input destination is not a valid URL\"\n };\n }\n return {\n url: boundUrl(location.href),\n targets,\n facts: [...facts],\n ...destination === void 0 ? {} : { destination },\n explain\n };\n };\n const prepareInput = (plan) => {\n const inspected = inspect(plan);\n if (\"error\" in inspected) return inspected;\n const { explain, ...prepared } = inspected;\n return {\n ...prepared,\n evidence: explain()\n };\n };\n const validate = (plan, prepared, options) => {\n const current = inspect(plan);\n if (\"error\" in current) return current.error === \"stale\" ? current : {\n error: \"changed\",\n detail: current.detail\n };\n if (current.url !== prepared.url || (current.destination === void 0 || prepared.destination === void 0 ? current.destination !== prepared.destination : boundUrl(current.destination) !== boundUrl(prepared.destination)) || JSON.stringify(current.facts) !== JSON.stringify(prepared.facts) || current.targets.length !== prepared.targets.length || current.targets.some((target, index) => {\n const previous = prepared.targets[index];\n return target === null ? previous !== null : previous === null || previous === void 0 || target.ref !== previous.ref || target.fingerprint !== previous.fingerprint;\n })) return {\n error: \"changed\",\n detail: \"the page or input target changed while the policy was deciding\"\n };\n if (options.focused === true) {\n const active = activeElement();\n const expected = prepared.targets[0];\n if (active === null || expected === null || expected === void 0 || refFor(active) !== expected.ref) return {\n error: \"changed\",\n detail: \"focus moved away from the approved element\"\n };\n }\n for (const press of options.presses ?? []) {\n const expected = prepared.targets[press.index];\n const element = expected === null || expected === void 0 ? void 0 : lookup(expected.ref);\n const hit = hitAt(document, press.x, press.y);\n if (element === void 0 || hit === null || !receives(element, hit)) return {\n error: \"changed\",\n detail: `the approved target is no longer under the pointer at (${press.x}, ${press.y})`\n };\n const nested = nestedControl(element, hit);\n if (nested !== void 0) return {\n error: \"changed\",\n detail: `${describe(nested)} inside the approved target would receive the press at (${press.x}, ${press.y})`\n };\n }\n return { ok: true };\n };\n const validateInput = (plan, prepared, options = {}) => {\n if ((options.presses ?? []).length === 0) return validate(plan, prepared, options);\n const { promise, resolve } = Promise.withResolvers();\n if (document.visibilityState === \"hidden\") resolve();\n else requestAnimationFrame(() => requestAnimationFrame(() => resolve()));\n return promise.then(() => validate(plan, prepared, options));\n };\n return {\n prepareInput,\n textEntry,\n typingRefusal,\n validateInput\n };\n };\n //#endregion\n //#region src/internal/input/targets.inpage.ts\n const targets = (names, walked, placing) => {\n const { describe, isDisabled, lookup, nameOf, parentOf, roleOf } = names;\n const { boxOf, controlOf, hitAt } = walked;\n const { contextOf } = placing;\n /** Whether `node` is `ancestor` or inside it, across shadow roots and same-origin frames. */\n const within = (ancestor, node) => {\n for (let current = node; current !== null;) {\n if (current === ancestor) return true;\n current = parentOf(current) ?? current.ownerDocument.defaultView?.frameElement ?? null;\n }\n return false;\n };\n const receives = (element, hit) => within(element, hit) || within(hit, element);\n const hrefAttribute = (element) => element.localName === \"a\" || element.localName === \"area\" ? element.getAttribute(\"href\") ?? element.getAttributeNS(\"http://www.w3.org/1999/xlink\", \"href\") : null;\n const linkOf = (element) => {\n for (let node = element; node !== null; node = parentOf(node)) if (hrefAttribute(node) !== null) return node;\n };\n const details = (element, hit, x, y) => {\n const link = linkOf(element);\n let href;\n if (link !== void 0) try {\n href = new URL(hrefAttribute(link) ?? \"\", link.ownerDocument.baseURI).href;\n } catch {\n href = void 0;\n }\n const role = roleOf(element);\n const view = hit.ownerDocument.defaultView ?? window;\n return {\n x,\n y,\n element: describe(element),\n tag: element.tagName.toLowerCase(),\n role,\n name: nameOf(element, role),\n cursor: view.getComputedStyle(hit).cursor,\n ...href === void 0 ? {} : { href }\n };\n };\n const point = (target, scroll = true) => {\n if (typeof target !== \"string\") {\n const { x, y } = target;\n if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight) return {\n error: \"outside\",\n detail: `(${x}, ${y}) is outside the viewport`\n };\n const hit = hitAt(document, x, y);\n if (hit === null) return {\n error: \"offscreen\",\n detail: `nothing is painted at (${x}, ${y})`\n };\n const control = controlOf(hit);\n return {\n ...details(control, hit, x, y),\n context: contextOf(control),\n box: boxOf(control)\n };\n }\n const ref = target;\n const element = lookup(ref);\n if (element === void 0) return {\n error: \"stale\",\n detail: `${ref} is not on the page any more`\n };\n if (isDisabled(element)) return {\n error: \"disabled\",\n detail: `${ref} is disabled`\n };\n const rect = element.getBoundingClientRect();\n if (rect.width === 0 && rect.height === 0) return {\n error: \"hidden\",\n detail: `${ref} has no size on the page`\n };\n let box = {\n left: rect.left,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom\n };\n let visible = { ...box };\n let outside = false;\n for (let inner = element;;) {\n const view = inner.ownerDocument.defaultView ?? window;\n outside ||= box.left < 0 || box.top < 0 || box.right > view.innerWidth || box.bottom > view.innerHeight;\n visible = {\n left: Math.max(visible.left, 0),\n top: Math.max(visible.top, 0),\n right: Math.min(visible.right, view.innerWidth),\n bottom: Math.min(visible.bottom, view.innerHeight)\n };\n for (let ancestor = parentOf(inner); ancestor !== null; ancestor = parentOf(ancestor)) {\n const style = view.getComputedStyle(ancestor);\n const clip = ancestor.getBoundingClientRect();\n if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {\n visible.left = Math.max(visible.left, clip.left + ancestor.clientLeft);\n visible.right = Math.min(visible.right, clip.left + ancestor.clientLeft + ancestor.clientWidth);\n }\n if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {\n visible.top = Math.max(visible.top, clip.top + ancestor.clientTop);\n visible.bottom = Math.min(visible.bottom, clip.top + ancestor.clientTop + ancestor.clientHeight);\n }\n }\n const frame = view.frameElement;\n if (frame === null) break;\n const frameBox = frame.getBoundingClientRect();\n const dx = frameBox.left + frame.clientLeft;\n const dy = frameBox.top + frame.clientTop;\n box = {\n left: box.left + dx,\n top: box.top + dy,\n right: box.right + dx,\n bottom: box.bottom + dy\n };\n visible = {\n left: Math.max(visible.left + dx, dx),\n top: Math.max(visible.top + dy, dy),\n right: Math.min(visible.right + dx, dx + frame.clientWidth),\n bottom: Math.min(visible.bottom + dy, dy + frame.clientHeight)\n };\n inner = frame;\n }\n if (outside || visible.right <= visible.left || visible.bottom <= visible.top) {\n if (scroll) {\n element.scrollIntoView({\n block: \"center\",\n inline: \"center\",\n behavior: \"instant\"\n });\n return point(target, false);\n }\n return {\n error: \"offscreen\",\n detail: outside ? ref + \" is outside the viewport\" : `${ref} could not be scrolled into view`\n };\n }\n const x = (visible.left + visible.right) / 2;\n const y = (visible.top + visible.bottom) / 2;\n const hit = hitAt(document, x, y);\n if (hit !== null && !receives(element, hit)) return {\n error: \"covered\",\n detail: `${ref} is covered by ${describe(hit)}`\n };\n return {\n ...details(element, hit ?? element, Math.round(x), Math.round(y)),\n context: contextOf(element)\n };\n };\n const scrollPlan = (ref) => {\n const element = lookup(ref);\n if (element === void 0 || element.ownerDocument !== document) return null;\n const target = element.getBoundingClientRect();\n const centerX = (target.left + target.right) / 2;\n const centerY = (target.top + target.bottom) / 2;\n for (let ancestor = parentOf(element); ancestor !== null; ancestor = parentOf(ancestor)) {\n const style = window.getComputedStyle(ancestor);\n const horizontal = /^(auto|scroll)$/.test(style.overflowX) && ancestor.scrollWidth > ancestor.clientWidth;\n const vertical = /^(auto|scroll)$/.test(style.overflowY) && ancestor.scrollHeight > ancestor.clientHeight;\n if (!horizontal && !vertical) continue;\n const box = ancestor.getBoundingClientRect();\n const left = Math.max(0, box.left + ancestor.clientLeft);\n const top = Math.max(0, box.top + ancestor.clientTop);\n const right = Math.min(window.innerWidth, box.left + ancestor.clientLeft + ancestor.clientWidth);\n const bottom = Math.min(window.innerHeight, box.top + ancestor.clientTop + ancestor.clientHeight);\n if (right <= left || bottom <= top) continue;\n const x = (left + right) / 2;\n const y = (top + bottom) / 2;\n const dx = horizontal && (target.left < left || target.right > right) ? centerX - x : 0;\n const dy = vertical && (target.top < top || target.bottom > bottom) ? centerY - y : 0;\n if (dx === 0 && dy === 0) continue;\n const hit = hitAt(document, x, y);\n if (hit !== null && (hit === ancestor || ancestor.contains(hit))) return {\n x,\n y,\n dx,\n dy\n };\n }\n const dx = target.left < 0 || target.right > window.innerWidth ? centerX - window.innerWidth / 2 : 0;\n const dy = target.top < 0 || target.bottom > window.innerHeight ? centerY - window.innerHeight / 2 : 0;\n return dx === 0 && dy === 0 ? null : {\n x: window.innerWidth / 2,\n y: window.innerHeight / 2,\n dx,\n dy\n };\n };\n return {\n controlOf,\n details,\n hitAt,\n hrefAttribute,\n linkOf,\n point,\n receives,\n scrollPlan,\n within\n };\n };\n //#endregion\n //#region src/internal/reading/context.inpage.ts\n const context = (names, walked) => {\n const { clean, isButton, isDocument, isHtml, isInput, isRoot } = names;\n const { isSelect, isTextArea, roleOf, textOf } = names;\n const { visible } = walked;\n const known = () => ({\n headers: /* @__PURE__ */ new Map(),\n headings: /* @__PURE__ */ new Map()\n });\n const unspoken = \"button,select,textarea,option,script,style,noscript,template,[role=button],[role=menuitem],[role=option],[role=tab],[role=switch],[role=checkbox],[role=radio],[aria-hidden=true]\";\n const groups = \"tr,li,article,fieldset,form,section,dialog,[role=row],[role=listitem],[role=group],[role=region],[role=dialog],[role=alertdialog]\";\n /** The visible text just before (or after) a target within `scope`, nearest kept, ≤120 characters. */\n const textBeside = (target, scope, backwards) => {\n const walker = target.ownerDocument.createTreeWalker(scope, NodeFilter.SHOW_TEXT);\n const labels = isInput(target) || isTextArea(target) || isSelect(target) || isButton(target) ? Array.from(target.labels ?? []) : [];\n let text = \"\";\n walker.currentNode = target;\n for (let steps = 0; steps < 400 && text.replace(/\\s+/g, \"\").length < 120; steps++) {\n const node = backwards ? walker.previousNode() : walker.nextNode();\n if (node === null) break;\n const parent = node.parentElement;\n if (parent === null || target.contains(parent) || parent.closest(unspoken) !== null || isHtml(parent) && parent.isContentEditable || labels.some((label) => label.contains(parent)) || !visible(parent)) continue;\n text = backwards ? `${node.textContent ?? \"\"} ${text}` : `${text} ${node.textContent ?? \"\"}`;\n }\n const words = text.replace(/\\s+/g, \" \").trim();\n return words.length <= 120 ? words : backwards ? `…${words.slice(-119).trimStart()}` : `${words.slice(0, 119).trimEnd()}…`;\n };\n /** The nearest visible heading before `target` in tree order, within `scope`. */\n const headingBefore = (target, scope, read = known()) => {\n let headings = read.headings.get(scope);\n if (headings === void 0) {\n headings = Array.from(scope.querySelectorAll(\"h1,h2,h3,h4,h5,h6,[role=heading]\")).filter((element) => roleOf(element) === \"heading\");\n read.headings.set(scope, headings);\n }\n for (const heading of headings.toReversed()) {\n if ((heading.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING) === 0) continue;\n const text = visible(heading) ? clean(textOf(heading), 120) : \"\";\n if (text !== \"\") return text;\n }\n };\n /** Whether an element is pinned to the viewport, where the markup around it says nothing. */\n const pinned = (element) => {\n if (!isHtml(element)) return false;\n let top = element;\n while (top.offsetParent !== null && isHtml(top.offsetParent)) top = top.offsetParent;\n return top !== element.ownerDocument.body && top !== element.ownerDocument.documentElement && getComputedStyle(top).position === \"fixed\";\n };\n const span = (cell) => Math.max(1, Number(cell.getAttribute(\"colspan\")) || 1);\n const tables = \"table,[role=table],[role=grid],[role=treegrid]\";\n /** A cell's row and column names, or nothing when it is not in a table with either. */\n const tableContext = (cell, read) => {\n const row = cell.closest(\"tr,[role=row]\");\n const table = row?.closest(tables);\n if (row === null || row === void 0 || table === null || table === void 0) return {};\n let header = read.headers.get(table);\n if (header === void 0) {\n const first = table.querySelector(\"thead tr, tr:has(> th), [role=row]:has(> [role=columnheader])\");\n header = first?.closest(tables) === table ? first : null;\n read.headers.set(table, header);\n }\n if (row === header) return {};\n const cells = Array.from(row.children);\n const index = cells.indexOf(cell);\n const lettered = (other) => /\\p{L}/u.test(textOf(other));\n const named = cells.find((other) => other.matches(\"th,[role=rowheader]\") && lettered(other)) ?? cells.find((other) => other !== cell && lettered(other));\n let left = cells.slice(0, index).reduce((sum, other) => sum + span(other), 0);\n const over = index === -1 || header === null ? void 0 : Array.from(header.children).find((candidate) => (left -= span(candidate)) < 0);\n return {\n row: named === void 0 || named === cell ? void 0 : clean(textOf(named), 60) || void 0,\n column: over === void 0 ? void 0 : clean(textOf(over), 60) || void 0\n };\n };\n /** Where text around an element is read: its dialog, else its own tree, never across a shadow root. */\n const treeOf = (element) => {\n const root = element.getRootNode();\n return element.closest(\"dialog,[role=dialog],[role=alertdialog]\") ?? (isDocument(root) ? root.body ?? root : isRoot(root) ? root : element.ownerDocument);\n };\n /** The words around an element that say which one it is. */\n const contextOf = (element, read = known()) => {\n const cell = element.closest(\"td,th,[role=cell],[role=gridcell],[role=rowheader],[role=columnheader]\");\n const table = cell === null ? {} : tableContext(cell, read);\n const parent = element.parentElement;\n const scope = element.closest(groups) ?? (parent === null || parent === element.ownerDocument.body || parent.tagName === \"HTML\" ? void 0 : parent);\n const beside = table.row !== void 0 || table.column !== void 0 || scope === void 0 ? \"\" : textBeside(element, scope, true);\n const heading = pinned(element) ? void 0 : headingBefore(element, treeOf(element), read);\n return {\n ...table,\n label: beside.length > 40 ? `…${beside.slice(-39).trimStart()}` : beside || void 0,\n heading: heading === void 0 ? void 0 : clean(heading, 60)\n };\n };\n return {\n contextOf,\n groups,\n headingBefore,\n known,\n textBeside,\n treeOf\n };\n };\n //#endregion\n //#region src/internal/reading/controls.inpage.ts\n const controls = (names, walked, subjects, texts) => {\n const { clean, isDisabled, isHtml, isInput, isSelect, isTextArea, refFor, textOf } = names;\n const { visible } = walked;\n const { stateOf } = subjects;\n const { shown } = texts;\n const states = (element, role) => {\n const { level, ...flags } = stateOf(element, role);\n const held = Object.entries(flags).flatMap(([name, value]) => value === true ? [name] : []);\n return [...level === void 0 ? [] : [`level=${level}`], ...held].map((name) => ` [${name}]`).join(\"\");\n };\n const valueOf = (element) => {\n if (isSelect(element)) {\n const selected = Array.from(element.selectedOptions, (option) => clean(option.text, 40));\n const options = Array.from(element.options).slice(0, 12).map((option) => clean(option.text, 40));\n const more = element.options.length > 12 ? ` +${element.options.length - 12} more` : \"\";\n return ` value=${JSON.stringify(selected.join(\", \"))} options=${JSON.stringify(options.join(\" | \") + more)}`;\n }\n const value = isInput(element) || isTextArea(element) ? shown(element, true) : void 0;\n return value === void 0 || value === \"\" ? \"\" : ` value=${JSON.stringify(clean(value, 80))}`;\n };\n const untyped = [\n \"button\",\n \"reset\",\n \"submit\",\n \"image\",\n \"checkbox\",\n \"radio\",\n \"file\",\n \"range\"\n ];\n /** A control as a value, as its outline line shows it. */\n const controlOf = (element, role, kind, name) => {\n const { disabled, checked } = stateOf(element, role);\n const select = isSelect(element) ? element : void 0;\n const value = select !== void 0 ? Array.from(select.selectedOptions, (option) => clean(option.text, 80)).join(\", \") : isInput(element) || isTextArea(element) ? clean(shown(element, true), 200) : \"\";\n const editable = isTextArea(element) || isInput(element) && !untyped.includes(element.type) || isHtml(element) && element.isContentEditable;\n return {\n ref: refFor(element),\n kind,\n name,\n value,\n ...select === void 0 ? {} : {\n options: Array.from(select.options).slice(0, 64).map((option) => clean(option.text, 80)),\n optionCount: select.options.length\n },\n ...disabled ? { disabled } : {},\n ...checked === void 0 ? {} : { checked },\n ...editable ? { editable } : {}\n };\n };\n /**\n * The elements a selector matches, each read whole, so one inside another that matched is left\n * to it; or why the selector is not CSS.\n */\n const rootsOf = (selector) => {\n let matched;\n try {\n matched = Array.from(document.querySelectorAll(selector));\n } catch {\n return { invalid: `${JSON.stringify(selector)} is not a CSS selector` };\n }\n return matched.filter((root) => !matched.some((other) => other !== root && other.contains(root)));\n };\n const holds = (request) => {\n const roots = request.selector === null ? [document.body].filter(Boolean) : rootsOf(request.selector);\n if (\"invalid\" in roots) return roots;\n const text = request.text === null ? null : clean(request.text, Infinity);\n const showing = roots.filter((element) => visible(element) && (text === null || clean(textOf(element), Infinity).includes(text)));\n return { met: request.state === \"hidden\" ? showing.length === 0 : request.state === \"enabled\" ? showing.some((element) => !isDisabled(element)) : showing.length > 0 };\n };\n const waitUntil = async (request, millis) => {\n const until = performance.now() + millis;\n for (;;) {\n const result = holds(request);\n const left = until - performance.now();\n if (\"invalid\" in result || result.met || left <= 0) return result;\n const { promise, resolve } = Promise.withResolvers();\n setTimeout(resolve, Math.min(100, left));\n await promise;\n }\n };\n return {\n controlOf,\n rootsOf,\n states,\n valueOf,\n waitUntil\n };\n };\n //#endregion\n //#region src/internal/reading/expect.inpage.ts\n const expectations = (names, subjected) => {\n const { isInput, isSelect, isTextArea, roleOf, textOf } = names;\n const { elementsOf, stateOf } = subjected;\n const markOf = (elements) => {\n let hash = 2166136261;\n for (const element of elements) {\n const { focused: _focused, ...state } = stateOf(element, roleOf(element));\n const value = isInput(element) || isSelect(element) || isTextArea(element) ? element.value : \"\";\n const shown = `${textOf(element)}\\u0000${value}\\u0000${JSON.stringify(state)}\\u0001`;\n for (let index = 0; index < shown.length; index++) hash = Math.imul(hash ^ shown.charCodeAt(index), 16777619);\n }\n return `${elements.length}:${(hash >>> 0).toString(16)}`;\n };\n /**\n * What `find` matches, looked at every 100 ms until `want` holds of it or `millis` have passed,\n * with its mark then. It gives no refs.\n */\n const until = async (request, want, millis) => {\n const deadline = performance.now() + millis;\n for (;;) {\n const elements = elementsOf(request);\n const mark = markOf(elements);\n const met = want === \"some\" ? elements.length > 0 : want === \"none\" ? elements.length === 0 : mark !== want.other;\n const left = deadline - performance.now();\n if (met || left <= 0) return {\n met,\n mark\n };\n const { promise, resolve } = Promise.withResolvers();\n setTimeout(resolve, Math.min(100, left));\n await promise;\n }\n };\n return { until };\n };\n //#endregion\n //#region src/internal/reading/match.inpage.ts\n const match = () => {\n /**\n * How names, text and context compare: one Unicode form, without invisible format characters,\n * spaces collapsed, and case folded upper first, so that \"ß\" and \"SS\" read the same.\n */\n const normalize = (value) => value.normalize(\"NFKC\").replace(/\\p{Cf}/gu, \"\").replace(/\\s+/g, \" \").trim().toUpperCase().toLowerCase();\n /** A string that reads the same, or, unless `whole`, appears within; or a pattern found within. */\n const rule = (wanted, whole) => {\n if (typeof wanted !== \"string\") {\n const found = new RegExp(wanted.source, wanted.flags.replace(/[gy]/g, \"\"));\n return (value) => found.test(value.replace(/\\s+/g, \" \").trim());\n }\n const target = normalize(wanted);\n return whole ? (value) => normalize(value) === target : (value) => normalize(value).includes(target);\n };\n /** Whether a value's words appear, whole and in order, among a text's: \"BTC\" not in \"WBTC\". */\n const holds = (text, value) => ` ${normalize(text)} `.includes(` ${normalize(value)} `);\n /** Whether a candidate meets every rule a query gives; a rule left out always holds. */\n const compile = (request) => {\n const role = request.role === null ? null : normalize(request.role);\n const name = request.name === null ? null : rule(request.name, true);\n const text = request.text === null ? null : rule(request.text, false);\n const { near } = request;\n return (candidate) => {\n if (role !== null && normalize(candidate.role ?? \"\") !== role) return false;\n if (text !== null && !text(candidate.text())) return false;\n if (name !== null && !name(candidate.name())) return false;\n if (near === null) return true;\n const { row, column, label, heading } = candidate.context();\n return [\n row,\n column,\n label,\n heading\n ].some((value) => value !== void 0 && holds(value, near));\n };\n };\n return {\n compile,\n holds,\n normalize\n };\n };\n //#endregion\n //#region src/internal/reading/names.inpage.ts\n const names = () => {\n const byElement = /* @__PURE__ */ new WeakMap();\n const byRef = /* @__PURE__ */ new Map();\n const refs = { next: 1 };\n const interactiveRoles = new Set(\"button link checkbox radio tab menuitem menuitemcheckbox menuitemradio option switch slider combobox listbox textbox searchbox spinbutton treeitem\".split(\" \"));\n const containers = {\n HEADER: \"banner\",\n NAV: \"navigation\",\n MAIN: \"main\",\n ASIDE: \"complementary\",\n FOOTER: \"contentinfo\",\n FORM: \"form\",\n DIALOG: \"dialog\"\n };\n const isInput = (element) => element.tagName === \"INPUT\";\n const isTextArea = (element) => element.tagName === \"TEXTAREA\";\n const isSelect = (element) => element.tagName === \"SELECT\";\n const isFrame = (element) => element.tagName === \"IFRAME\";\n const isHtml = (element) => \"innerText\" in element;\n const isButton = (element) => element.tagName === \"BUTTON\";\n const isLabel = (element) => element.tagName === \"LABEL\";\n const textOf = (element) => isHtml(element) ? element.innerText : element.textContent ?? \"\";\n const isElement = (node) => node.nodeType === Node.ELEMENT_NODE;\n const isDocument = (node) => node.nodeType === Node.DOCUMENT_NODE;\n const valueRoles = new Set(\"combobox listbox option textbox searchbox spinbutton slider\".split(\" \"));\n /**\n * A label's own words, without the options or values of the controls inside it, native or\n * scripted, such as the custom select Browserbase puts in place of a native one.\n */\n const labelText = (label) => {\n let text = \"\";\n const collect = (node) => {\n for (const child of Array.from(node.childNodes)) if (child.nodeType === Node.TEXT_NODE) text += child.textContent ?? \"\";\n else if (isElement(child) && ![\n \"SELECT\",\n \"TEXTAREA\",\n \"INPUT\",\n \"SCRIPT\",\n \"STYLE\"\n ].includes(child.tagName) && !valueRoles.has(roleOf(child) ?? \"\")) collect(child);\n };\n collect(label);\n return text;\n };\n const clean = (value, max = 160) => {\n const text = (value ?? \"\").replace(/\\s+/g, \" \").trim();\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n };\n const roleOf = (element) => {\n const explicit = element.getAttribute(\"role\");\n if (explicit !== null && explicit.trim() !== \"\") return explicit.trim().split(/\\s+/)[0] ?? null;\n const tag = element.tagName;\n if (tag === \"A\") return element.hasAttribute(\"href\") ? \"link\" : null;\n if (tag === \"BUTTON\" || tag === \"SUMMARY\") return \"button\";\n if (isSelect(element)) return element.multiple ? \"listbox\" : \"combobox\";\n if (tag === \"TEXTAREA\") return \"textbox\";\n if (isInput(element)) {\n const type = element.type;\n if (type === \"hidden\") return null;\n if (type === \"checkbox\" || type === \"radio\") return type;\n if (type === \"range\") return \"slider\";\n if (type === \"number\") return \"spinbutton\";\n if (type === \"search\") return \"searchbox\";\n if (type === \"button\" || type === \"submit\" || type === \"reset\" || type === \"image\") return \"button\";\n return \"textbox\";\n }\n if (/^H[1-6]$/.test(tag)) return \"heading\";\n if (tag === \"IMG\") return \"img\";\n if (tag === \"CANVAS\") return \"canvas\";\n if (tag === \"IFRAME\") return \"iframe\";\n if (element.getAttribute(\"contenteditable\") === \"true\") return \"textbox\";\n return containers[tag] ?? null;\n };\n const nameOf = (element, role) => {\n const label = element.getAttribute(\"aria-label\");\n if (label !== null && clean(label) !== \"\") return clean(label);\n const labelledBy = element.getAttribute(\"aria-labelledby\");\n if (labelledBy !== null) {\n const text = labelledBy.split(/\\s+/).map((id) => {\n const labelling = element.ownerDocument.getElementById(id);\n return labelling === null ? \"\" : labelText(labelling);\n }).join(\" \");\n if (clean(text) !== \"\") return clean(text);\n }\n if (isInput(element) || isTextArea(element) || isSelect(element)) {\n const labels = element.labels === null ? \"\" : Array.from(element.labels, labelText).join(\" \");\n if (clean(labels) !== \"\") return clean(labels);\n if (isInput(element) && (element.type === \"button\" || element.type === \"submit\" || element.type === \"reset\")) return clean(element.value);\n return clean(element.getAttribute(\"placeholder\") ?? element.getAttribute(\"title\") ?? element.getAttribute(\"name\"));\n }\n if (role === \"img\") return clean(element.getAttribute(\"alt\") ?? element.getAttribute(\"title\"));\n if (role === \"iframe\") return clean(element.getAttribute(\"title\") ?? element.getAttribute(\"name\"));\n if (role === null || interactiveRoles.has(role) || role === \"heading\") {\n const text = isSecret(element) ? \"\" : clean(textOf(element), 120);\n if (text !== \"\") return text;\n const titled = element.querySelector(\"[title],img[alt],svg title\");\n const fallback = titled === null ? null : titled.tagName === \"IMG\" ? titled.getAttribute(\"alt\") : titled.tagName.toLowerCase() === \"title\" ? titled.textContent : titled.getAttribute(\"title\");\n return clean(element.getAttribute(\"title\") ?? fallback);\n }\n return \"\";\n };\n const isDisabled = (element) => \"disabled\" in element && element.disabled === true || element.getAttribute(\"aria-disabled\") === \"true\";\n const refFor = (element) => {\n const known = byElement.get(element);\n if (known !== void 0 && byRef.get(known)?.deref() === element) return known;\n const ref = `e${refs.next++}`;\n byElement.set(element, ref);\n byRef.set(ref, new WeakRef(element));\n return ref;\n };\n const lookup = (ref) => {\n const element = byRef.get(ref)?.deref();\n return element !== void 0 && element.isConnected && inCurrentDocument(element) ? element : void 0;\n };\n const describe = (element) => {\n const id = element.id === \"\" ? \"\" : `#${element.id}`;\n const name = clean(nameOf(element, roleOf(element)), 40);\n return `<${element.tagName.toLowerCase()}${id}>${name === \"\" ? \"\" : ` \"${name}\"`}`;\n };\n const isRoot = (node) => node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;\n const parentOf = (element) => {\n const root = element.getRootNode();\n return element.parentElement ?? (isRoot(root) && \"host\" in root ? root.host : null);\n };\n const activeElement = () => {\n let active = document.activeElement;\n while (active !== null) {\n const inner = active.shadowRoot?.activeElement ?? (isFrame(active) ? active.contentDocument?.activeElement : null);\n if (inner === null || inner === void 0 || inner === active) return active;\n active = inner;\n }\n return null;\n };\n const inCurrentDocument = (element) => {\n let owner = element.ownerDocument;\n while (owner !== document) {\n const frame = owner.defaultView?.frameElement;\n if (frame === null || frame === void 0 || !frame.isConnected || isFrame(frame) && frame.contentDocument !== owner) return false;\n owner = frame.ownerDocument;\n }\n return true;\n };\n const secretToken = /(?:^|\\s)(?:current-password|new-password|one-time-code|cc-number|cc-csc|cc-exp(?:-month|-year)?)(?=\\s|$)/i;\n const secrets = /* @__PURE__ */ new WeakSet();\n /**\n * A field the DOM marks as secret, now or before: by its type, its autocomplete tokens, or a\n * style that shows dots for what it holds, as a PIN field may.\n */\n const isSecret = (element) => {\n if (isInput(element) && element.type === \"password\" || secretToken.test(element.getAttribute(\"autocomplete\") ?? \"\") || (isInput(element) || isTextArea(element)) && !/^(none)?$/.test(getComputedStyle(element).getPropertyValue(\"-webkit-text-security\"))) secrets.add(element);\n return secrets.has(element);\n };\n return {\n activeElement,\n clean,\n containers,\n describe,\n inCurrentDocument,\n interactiveRoles,\n isButton,\n isDisabled,\n isDocument,\n isElement,\n isFrame,\n isHtml,\n isInput,\n isLabel,\n isRoot,\n isSecret,\n isSelect,\n isTextArea,\n lookup,\n nameOf,\n parentOf,\n refFor,\n refs,\n roleOf,\n textOf\n };\n };\n //#endregion\n //#region src/internal/reading/outline.inpage.ts\n const outline = (names, walked, subjects, texts, controlling, url) => {\n const { clean, containers, isFrame, nameOf, refFor, refs, roleOf, textOf } = names;\n const { visit } = walked;\n const { isControl } = subjects;\n const { cut } = texts;\n const textBlocks = /^(?:P|LI|TD|TH|DT|DD|LABEL|SPAN|BLOCKQUOTE|FIGCAPTION|CAPTION|PRE|STRONG|EM|B|I|SMALL|TIME|CODE|LEGEND)$/;\n const nested = \"a[href],button,input,select,textarea,summary,[role],[onclick],[tabindex],[contenteditable],canvas,iframe\";\n const noise = /^[\\s|•·\\-–—,.:;()[\\]{}\"'/\\\\]*$/;\n /** Text inside a control's label is already that control's name. */\n const inControlLabel = (element) => {\n const label = element.closest(\"label\");\n return label !== null && label.control !== null;\n };\n const hrefOf = (element) => {\n if (element.tagName !== \"A\") return \"\";\n const raw = element.getAttribute(\"href\") ?? \"\";\n if (raw === \"\" || raw.startsWith(\"javascript:\")) return \"\";\n try {\n const { origin, href } = new URL(raw, element.ownerDocument.baseURI);\n const reported = url.redact(href);\n const own = origin === location.origin && reported.startsWith(origin);\n return ` -> ${clean(own ? reported.slice(origin.length) : reported, 80)}`;\n } catch {\n return \"\";\n }\n };\n const snapshot = (request) => {\n const roots = request.within === null ? [null] : controlling.rootsOf(request.within);\n if (\"invalid\" in roots) return roots;\n if (refs.next < request.firstRef) refs.next = request.firstRef;\n const width = window.innerWidth;\n const height = window.innerHeight;\n const query = request.query === null ? null : request.query.toLowerCase();\n const lines = [];\n const controls = [];\n let above = 0;\n let below = 0;\n const placement = (rect) => {\n if (request.full) return \"in\";\n if (rect.bottom <= 0) return \"above\";\n if (rect.top >= height) return \"below\";\n if (rect.right <= 0 || rect.left >= width) return \"aside\";\n return \"in\";\n };\n const count = (rect) => {\n const where = placement(rect);\n if (where === \"above\") above++;\n if (where === \"below\") below++;\n return where;\n };\n const emit = (depth, line, rect) => {\n if (count(rect) !== \"in\") return false;\n if (query !== null && !line.toLowerCase().includes(query)) return false;\n lines.push(`${\" \".repeat(Math.min(depth, 8))}- ${line}`);\n return true;\n };\n const control = (element, rect, role, depth) => {\n const name = nameOf(element, role);\n const kind = role ?? \"clickable\";\n const box = role === \"canvas\" || role === \"iframe\" ? ` ${Math.round(rect.width)}x${Math.round(rect.height)} at (${Math.round(rect.x)},${Math.round(rect.y)})` : \"\";\n const line = placement(rect) === \"in\" ? `${kind}${name === \"\" ? \"\" : ` ${JSON.stringify(name)}`} [ref=${refFor(element)}]${controlling.states(element, role)}${controlling.valueOf(element)}${hrefOf(element)}${box}` : kind;\n if (emit(depth, line, rect)) controls.push(controlling.controlOf(element, role, kind, name));\n if (isFrame(element)) {\n const body = element.contentDocument?.body;\n if ((body === null || body === void 0) && placement(rect) === \"in\") lines.push(`${\" \".repeat(Math.min(depth + 1, 8))}- (another site's frame: use a screenshot and coordinates)`);\n return {\n depth: depth + 1,\n insidePointer: false,\n controls: false\n };\n }\n if ([\n \"INPUT\",\n \"SELECT\",\n \"TEXTAREA\",\n \"CANVAS\",\n \"IMG\"\n ].includes(element.tagName)) return void 0;\n return {\n depth: depth + 1,\n insidePointer: true,\n controls: true\n };\n };\n const enter = (element, style, rect, { depth, insidePointer }) => {\n const role = roleOf(element);\n if (isControl(element, role, style, rect, insidePointer)) return control(element, rect, role, depth);\n if (role === \"heading\") {\n emit(depth, `heading ${JSON.stringify(nameOf(element, role))}${controlling.states(element, role)}`, rect);\n return;\n }\n if (role === \"img\") {\n const alt = nameOf(element, role);\n if (alt !== \"\") emit(depth, `img ${JSON.stringify(alt)}`, rect);\n return;\n }\n let inner = depth;\n if (role !== null && (containers[element.tagName] !== void 0 || role === \"dialog\")) {\n const label = element.getAttribute(\"aria-label\");\n if (emit(depth, `${role}${label === null ? \"\" : ` ${JSON.stringify(clean(label))}`}:`, rect)) inner = depth + 1;\n }\n if (textBlocks.test(element.tagName) && element.querySelector(nested) === null) {\n if (inControlLabel(element)) return void 0;\n const text = clean(textOf(element), 300);\n if (text !== \"\" && !noise.test(text)) emit(inner, `text: ${text}`, rect);\n return;\n }\n let own = \"\";\n for (const node of Array.from(element.childNodes)) if (node.nodeType === Node.TEXT_NODE) own += node.textContent ?? \"\";\n own = clean(own, 300);\n if (own !== \"\" && !noise.test(own) && !inControlLabel(element)) emit(inner, `text: ${own}`, rect);\n return {\n depth: inner,\n insidePointer: insidePointer || style.cursor === \"pointer\",\n controls: false\n };\n };\n const visitor = {\n enter,\n admit: (child, place) => !place.controls || child.matches(nested) || child.querySelector(nested) !== null,\n outside: count\n };\n for (const root of roots) visit(root, !request.full, {\n depth: 0,\n insidePointer: false,\n controls: false\n }, visitor);\n const outlined = cut(lines.join(\"\\n\"), request.maxChars);\n return {\n snapshot: {\n ...outlined,\n url: location.href,\n title: document.title,\n above,\n below,\n viewport: {\n width,\n height\n },\n scroll: {\n y: Math.round(window.scrollY),\n height: Math.round(document.documentElement.scrollHeight)\n },\n controls: controls.filter((control) => outlined.text.includes(`[ref=${control.ref}]`))\n },\n nextRef: refs.next\n };\n };\n const viewport = () => ({\n width: window.innerWidth,\n height: window.innerHeight\n });\n return {\n snapshot,\n viewport\n };\n };\n //#endregion\n //#region src/internal/reading/ready.inpage.ts\n const ready = (walked, texts, kept, recorder) => {\n const { inView, visible } = walked;\n let copy;\n const boxed = (element) => {\n const rect = element.getBoundingClientRect();\n return rect.width * rect.height > 0 && inView(rect);\n };\n const showing = (elements) => Array.from(elements).filter((element) => boxed(element) && visible(element));\n const drawn = (canvas) => {\n copy ??= new OffscreenCanvas(32, 32).getContext(\"2d\", { willReadFrequently: true });\n if (copy === null) return true;\n try {\n copy.clearRect(0, 0, 32, 32);\n copy.imageSmoothingQuality = \"high\";\n copy.drawImage(canvas, 0, 0, 32, 32);\n return new Uint32Array(copy.getImageData(0, 0, 32, 32).data.buffer).some((pixel) => pixel !== 0);\n } catch {\n return true;\n }\n };\n const content = () => showing(document.querySelectorAll(\"video, iframe, embed, object\")).length > 0 || showing(document.querySelectorAll(\"img, svg\")).some((picture) => {\n const { width, height } = picture.getBoundingClientRect();\n return width > 64 && height > 64;\n }) || (texts.read({\n ref: null,\n maxChars: 1,\n unmask: false\n })?.text ?? \"\") !== \"\" || showing(document.querySelectorAll(\"canvas\")).some(drawn);\n const painted = () => {\n const { promise, resolve } = Promise.withResolvers();\n if (document.visibilityState === \"hidden\") resolve(false);\n else requestAnimationFrame(() => resolve(true));\n return promise;\n };\n const check = (quietMillis, observed) => painted().then((paints) => {\n const waiting = [];\n if (document.readyState === \"loading\") waiting.push(\"load\");\n if (!paints) waiting.push(\"paint\");\n if (document.getAnimations().some((animation) => {\n const effect = animation.effect;\n return animation.playState === \"running\" && effect !== null && Number.isFinite(effect.getComputedTiming().endTime) && \"target\" in effect && effect.target instanceof Element && boxed(effect.target);\n })) waiting.push(\"animations\");\n if (Array.from(document.images).some((image) => !image.complete && boxed(image))) waiting.push(\"images\");\n if (document.fonts.status !== \"loaded\") waiting.push(\"fonts\");\n if (showing(document.querySelectorAll(\"[aria-busy=true]\")).length > 0) waiting.push(\"busy\");\n if (!content()) waiting.push(\"content\");\n if (Math.max(observed, kept.changing()) > performance.timeOrigin + performance.now() - quietMillis) waiting.push(\"changing\");\n return waiting;\n });\n /**\n * Check until the page is ready, or for `millis`, and say what it is still waiting for; with\n * `quietMillis`, until nothing the record saw in view has changed for that long, too.\n */\n const wait = async (millis, quietMillis = 0, contentOnly = false) => {\n const until = performance.now() + millis;\n let observed = contentOnly ? void 0 : Number.NEGATIVE_INFINITY;\n for (;;) {\n if (contentOnly) {\n recorder.start();\n if (kept.begun()) observed ??= performance.timeOrigin + performance.now();\n }\n const waiting = await check(quietMillis, observed ?? Number.POSITIVE_INFINITY);\n if (waiting.length === 0 || performance.now() >= until) return waiting;\n const { promise, resolve } = Promise.withResolvers();\n setTimeout(resolve, Math.min(50, until - performance.now()));\n await promise;\n }\n };\n /**\n * One task, then a frame: what the page's handlers for input did at once has run, a navigation\n * one of them started in a timer has been asked for, and their effect has painted. Says whether\n * the page painted, which a hidden one does not.\n */\n const settle = () => {\n const { promise, resolve } = Promise.withResolvers();\n setTimeout(resolve, 0);\n return promise.then(painted);\n };\n return {\n wait,\n settle\n };\n };\n //#endregion\n //#region src/internal/reading/subjects.inpage.ts\n const subjects = (names, walked, matching, placing) => {\n const { isDisabled, isInput, nameOf, refFor, refs, roleOf, textOf } = names;\n const { acts, boxOf, controlOf, hitAt, inView } = walked;\n const { contextOf, known } = placing;\n /** What the outline lists as a control, with a ref: something a person would act on. */\n const isControl = (element, role, style, rect, insidePointer) => acts(element, role) || style.cursor === \"pointer\" && !insidePointer && rect.width > 0 && rect.height > 0;\n const stateOf = (element, role) => {\n const given = (attribute) => {\n const value = element.getAttribute(attribute);\n return value === \"true\" || (value === \"false\" ? false : void 0);\n };\n const checkable = element.hasAttribute(\"aria-checked\") || [\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"menuitemcheckbox\",\n \"menuitemradio\"\n ].includes(role ?? \"\");\n return {\n disabled: isDisabled(element),\n checked: checkable ? isInput(element) && element.checked || element.getAttribute(\"aria-checked\") === \"true\" : void 0,\n expanded: given(\"aria-expanded\"),\n selected: given(\"aria-selected\"),\n pressed: given(\"aria-pressed\"),\n focused: element.ownerDocument.activeElement === element,\n level: role === \"heading\" ? Number(element.getAttribute(\"aria-level\")) || Number(/^H([1-6])$/.exec(element.tagName)?.[1]) || 2 : void 0\n };\n };\n /**\n * The elements in scope that match a query, in tree order. Without text, an element matches\n * when it has a role or is a control. With text, any element can, but text inside a control or\n * heading is that control's or heading's, and an element is left out when one inside it\n * matches too: the smallest element showing the text is found, not each one around it.\n */\n const matched = (request) => {\n const { at } = request;\n const matches = matching.compile(request);\n const byText = request.text !== null;\n const viewport = request.scope === \"viewport\";\n const read = known();\n const entries = [];\n const open = [];\n const named = (entry) => entry.name ??= nameOf(entry.element, entry.role);\n const placed = (entry) => entry.context ??= contextOf(entry.element, read);\n const judge = (entry, text) => matches({\n role: entry.role,\n name: () => named(entry),\n text,\n context: () => placed(entry)\n });\n const hit = at === null ? null : hitAt(document, at.x, at.y);\n if (hit !== null) {\n const element = controlOf(hit);\n const { x, y, width, height } = boxOf(element);\n const entry = {\n element,\n rect: new DOMRect(x, y, width, height),\n role: roleOf(element),\n matched: false\n };\n entry.matched = judge(entry, () => textOf(element));\n entries.push(entry);\n }\n if (at === null) walked.visit(null, viewport, {\n pointer: false,\n owned: false\n }, {\n enter: (element, style, rect, state) => {\n const role = roleOf(element);\n const control = isControl(element, role, style, rect, state.pointer);\n const unit = control || role === \"heading\";\n const entry = (byText ? unit || !state.owned : role !== null || control) && (!viewport || inView(rect)) ? {\n element,\n rect,\n role,\n matched: false\n } : void 0;\n if (entry !== void 0) entries.push(entry);\n open.push({\n entry,\n inline: style.display === \"inline\",\n parts: [],\n inside: false\n });\n return {\n pointer: state.pointer || style.cursor === \"pointer\",\n owned: state.owned || unit\n };\n },\n text: byText ? (node) => open.at(-1)?.parts.push(node.textContent ?? \"\") : void 0,\n leave: () => {\n const closed = open.pop();\n const parent = open.at(-1);\n if (closed === void 0) return;\n const { entry } = closed;\n const text = closed.parts.join(\"\");\n if (parent !== void 0) parent.parts.push(closed.inline ? text : ` ${text} `);\n if (entry !== void 0 && !(byText && closed.inside)) entry.matched = judge(entry, () => text);\n if (parent !== void 0) parent.inside ||= closed.inside || entry?.matched === true;\n }\n });\n return {\n entries: entries.filter((entry) => entry.matched),\n named,\n placed\n };\n };\n const find = (request) => {\n if (refs.next < request.firstRef) refs.next = request.firstRef;\n const { entries, named, placed } = matched(request);\n return {\n found: entries.map((entry) => ({\n ref: refFor(entry.element),\n subject: {\n role: entry.role,\n name: named(entry),\n tag: entry.element.tagName.toLowerCase(),\n context: placed(entry)\n },\n box: {\n x: Math.round(entry.rect.x),\n y: Math.round(entry.rect.y),\n width: Math.round(entry.rect.width),\n height: Math.round(entry.rect.height)\n },\n inViewport: inView(entry.rect),\n state: stateOf(entry.element, entry.role)\n })),\n nextRef: refs.next\n };\n };\n /** The elements `find` would find, in tree order, with no refs given. */\n const elementsOf = (request) => matched(request).entries.map((entry) => entry.element);\n return {\n find,\n elementsOf,\n isControl,\n stateOf\n };\n };\n //#endregion\n //#region src/internal/reading/text.inpage.ts\n const text = (names, walked) => {\n const { isHtml, isInput, isSecret, isSelect, isTextArea, lookup } = names;\n const valueless = [\n \"checkbox\",\n \"radio\",\n \"button\",\n \"submit\",\n \"reset\",\n \"image\",\n \"file\"\n ];\n /** What a field holds, or undefined for an element that holds nothing entered. */\n const valueOf = (element) => {\n if (isInput(element)) return valueless.includes(element.type) ? void 0 : element.value;\n if (isTextArea(element)) return element.value;\n if (isSelect(element)) return Array.from(element.selectedOptions, (option) => option.text).join(\", \");\n const parent = element.parentElement;\n return isHtml(element) && element.isContentEditable && !(parent !== null && isHtml(parent) && parent.isContentEditable) ? element.innerText : void 0;\n };\n /** What a field shows: `••••` unless `unmask`, and always for a secret field; \"\" when empty. */\n const shown = (element, unmask) => {\n const value = valueOf(element);\n if (value === void 0 || value.trim() === \"\") return value === void 0 ? void 0 : \"\";\n return unmask && !isSecret(element) ? value : \"••••\";\n };\n /** Lines cut after the last whole line within `max` characters, or at `max` when none is. */\n const cut = (lines, max) => {\n if (lines.length <= max) return {\n text: lines,\n truncated: false\n };\n const end = lines.lastIndexOf(\"\\n\", max);\n return {\n text: lines.slice(0, end > 0 ? end : max),\n truncated: true\n };\n };\n /**\n * What the viewport, or one element whole, shows as text: a line per block, table cells apart by\n * a tab, and each field as `shown` gives it.\n */\n const lines = (root, unmask) => {\n const lines = [];\n let line = \"\";\n const end = () => {\n const words = line.replace(/[^\\S\\t]+/g, \" \").replace(/ ?\\t ?/g, \" \").trim();\n if (words !== \"\") lines.push(words);\n line = \"\";\n };\n walked.visit(root, root === null, false, {\n enter: (element, style) => {\n const { display } = style;\n const value = shown(element, unmask);\n const block = !display.startsWith(\"inline\") && display !== \"contents\" && display !== \"table-cell\";\n if (display === \"table-cell\") line += \" \";\n if (block || element.tagName === \"BR\") end();\n if (value === void 0 && !(isInput(element) && valueless.includes(element.type))) return block;\n const label = isInput(element) && /^(button|submit|reset)$/.test(element.type);\n line += ` ${value ?? (label ? element.value : \"\")} `;\n if (block) end();\n },\n text: (node) => {\n line += node.textContent ?? \"\";\n },\n leave: (_, block) => {\n if (block) end();\n }\n });\n end();\n return lines.join(\"\\n\");\n };\n const read = (request) => {\n const root = request.ref === null ? null : lookup(request.ref);\n return root === void 0 ? null : {\n ...cut(lines(root, request.unmask), request.maxChars),\n url: location.href,\n title: document.title\n };\n };\n return {\n cut,\n lines,\n read,\n shown\n };\n };\n //#endregion\n //#region src/internal/reading/walk.inpage.ts\n const walk = (names) => {\n const { interactiveRoles, isElement, isFrame, parentOf, roleOf } = names;\n const skipped = new Set(\"SCRIPT STYLE NOSCRIPT TEMPLATE HEAD META LINK SVG\".split(\" \"));\n /** Whether a box, in top-document pixels, meets the viewport. */\n const inView = (rect, width = innerWidth, height = innerHeight) => rect.right > 0 && rect.bottom > 0 && rect.left < width && rect.top < height;\n /** Whether an element reached on its own shows, by the walk's rule and its ancestors'. */\n const visible = (element) => element.checkVisibility({\n opacityProperty: true,\n visibilityProperty: true\n }) && element.closest(\"[aria-hidden=true]\") === null;\n const isVisible = (element, style, rect) => style.display !== \"none\" && style.visibility === \"visible\" && style.opacity !== \"0\" && element.getAttribute(\"aria-hidden\") !== \"true\" && (rect.width > 0 || rect.height > 0 || style.display === \"contents\");\n const untransformed = (frame, rect) => {\n if (rect.width !== frame.offsetWidth || rect.height !== frame.offsetHeight) return false;\n let ancestor = frame;\n while (ancestor !== null) {\n const style = (ancestor.ownerDocument.defaultView ?? window).getComputedStyle(ancestor);\n if (style.transform !== \"none\" || style.scale !== \"none\" || style.rotate !== \"none\" || style.perspective !== \"none\" || style.zoom !== \"1\") return false;\n ancestor = parentOf(ancestor);\n }\n return true;\n };\n /** The element painted at a point, inside shadow roots and same-origin frames. */\n const hitAt = (root, x, y) => {\n let hit = root.elementFromPoint(x, y);\n while (hit?.shadowRoot !== null && hit?.shadowRoot !== void 0) {\n const inner = hit.shadowRoot.elementFromPoint(x, y);\n if (inner === null || inner === hit) break;\n hit = inner;\n }\n if (hit !== null && isFrame(hit)) {\n const document = hit.contentDocument;\n if (document !== null) {\n const rect = hit.getBoundingClientRect();\n if (untransformed(hit, rect)) return hitAt(document, x - rect.left - hit.clientLeft, y - rect.top - hit.clientTop) ?? hit;\n }\n }\n return hit;\n };\n /** Whether its role or a click handler makes an element a control. */\n const acts = (element, role) => role !== null && (interactiveRoles.has(role) || role === \"canvas\" || role === \"iframe\") || element.hasAttribute(\"onclick\");\n const controlOf = (hit) => {\n let element = hit;\n while (element !== null) {\n if (acts(element, roleOf(element))) return element;\n element = parentOf(element);\n }\n return hit;\n };\n /** An element's box in top-document viewport pixels, through the frames around it. */\n const boxOf = (element) => {\n const { x, y, width, height } = element.getBoundingClientRect();\n const frame = element.ownerDocument.defaultView?.frameElement ?? null;\n if (frame === null) return {\n x,\n y,\n width,\n height\n };\n const outer = boxOf(frame);\n return {\n x: x + outer.x + frame.clientLeft,\n y: y + outer.y + frame.clientTop,\n width,\n height\n };\n };\n /** What is painted where pinned things sit, what is pinned in view, and every element holding it. */\n const painted = (width, height) => {\n const keep = /* @__PURE__ */ new Set();\n const hold = (element) => {\n for (let node = element; node !== null && !keep.has(node); node = parentOf(node) ?? node.ownerDocument.defaultView?.frameElement ?? null) keep.add(node);\n };\n for (const y of [\n 8,\n 48,\n height / 2,\n height - 48,\n height - 8\n ]) for (const x of [\n 8,\n 48,\n width / 4,\n width / 2,\n width * 3 / 4,\n width - 48,\n width - 8\n ]) if (x >= 0 && y >= 0 && x < width && y < height) {\n hold(hitAt(document, x, y));\n for (const beneath of document.elementsFromPoint(x, y)) hold(beneath);\n }\n for (const element of Array.from(document.querySelectorAll(\":modal, :popover-open\"))) hold(element);\n for (const element of Array.from(document.querySelectorAll(\"[style*=\\\"fixed\\\" i]\"))) if (inView(element.getBoundingClientRect(), width, height)) hold(element);\n return keep;\n };\n /**\n * Visit `root` and what it holds, or the whole document when `root` is null. With `viewport`,\n * a subtree outside the viewport is skipped, as described above.\n */\n const visit = (root, viewport, state, visitor) => {\n const width = window.innerWidth;\n const height = window.innerHeight;\n let keep;\n const children = (parent, inner, dx, dy) => {\n const nodes = visitor.text === void 0 ? parent.children : parent.childNodes;\n for (const node of Array.from(nodes)) if (node.nodeType === Node.TEXT_NODE) visitor.text?.(node, inner);\n else if (isElement(node) && (visitor.admit === void 0 || visitor.admit(node, inner))) one(node, inner, dx, dy);\n };\n const one = (element, outer, dx, dy) => {\n if (skipped.has(element.tagName.toUpperCase())) return;\n const local = element.getBoundingClientRect();\n const rect = new DOMRect(local.x + dx, local.y + dy, local.width, local.height);\n if (viewport && local.width > 0 && local.height > 0 && !inView(rect, width, height) && !(keep ??= painted(width, height)).has(element)) {\n visitor.outside?.(rect);\n return;\n }\n const style = getComputedStyle(element);\n if (!isVisible(element, style, local)) return;\n const inner = visitor.enter(element, style, rect, outer);\n if (inner === void 0) return;\n if (isFrame(element)) {\n const body = element.contentDocument?.body;\n if (body !== null && body !== void 0) children(body, inner, rect.x, rect.y);\n } else {\n if (element.shadowRoot !== null) children(element.shadowRoot, inner, dx, dy);\n children(element, inner, dx, dy);\n }\n visitor.leave?.(element, inner);\n };\n if (root === null) children(document, state, 0, 0);\n else one(root, state, 0, 0);\n };\n return {\n acts,\n boxOf,\n controlOf,\n hitAt,\n inView,\n visible,\n visit\n };\n };\n //#endregion\n //#region src/internal/timeline/changes.inpage.ts\n const changes = (names, placing, kept, seeing, marking, recorder) => {\n const { clean, isInput, roleOf, nameOf } = names;\n const groups = \"tr,[role=row],form,dialog,[role=dialog],[role=alertdialog]\";\n /** Whether `element` lies in what an input on `target` acted on, its group, or what it controls. */\n const local = (target, element) => {\n const controls = target.closest(\"[aria-controls]\")?.getAttribute(\"aria-controls\") ?? \"\";\n const group = target.closest(groups);\n return target.contains(element) || group?.contains(element) === true || controls.split(/\\s+/).some((id) => id !== \"\" && document.getElementById(id)?.contains(element) === true);\n };\n /** What an element sits in, or sat in as it went. */\n const parentOf = (key) => key instanceof Element ? key.isConnected ? key.parentElement : marking.placeOf(key) ?? null : null;\n const fluxMillis = 1e4;\n const stillMillis = 2e3;\n /**\n * Each input a change may follow, with what the page did before it: what changed in the 10 s\n * before, the parents that gained or lost a child then, and whether nothing changed, and nothing\n * was let go, in the 2 s before. What holds the input's target, as a menu holds its item, came\n * with it, so it counts only as changing itself.\n */\n const before = () => {\n const inputs = recorder.inputs();\n if (inputs.length === 0) return [];\n const { tracks, losses } = kept.recent();\n return inputs.map(({ at, target }) => {\n const changed = /* @__PURE__ */ new Set();\n const moved = /* @__PURE__ */ new Set();\n let still = losses.every((lost) => lost >= at || lost < at - stillMillis);\n for (const { key, kind, initial, samples } of tracks) {\n const prior = samples.flatMap(({ at: when, shown }, index) => kind === \"value\" || when >= at || when < at - fluxMillis ? [] : [shown === null || (index === 0 ? initial : samples[index - 1]?.shown) === null]);\n if (prior.length > 0) changed.add(key);\n if (prior.length === 0 || key instanceof Element && key.contains(target)) continue;\n if (prior.includes(true)) moved.add(parentOf(key));\n if (samples.some(({ at: when }) => when < at && when >= at - stillMillis)) still = false;\n }\n return {\n at,\n target,\n changed,\n moved,\n still\n };\n });\n };\n /** The input a change followed, on the page's clock, where `Change.cause`'s rule names one. */\n const causeOf = (one, element, inputs) => {\n const parent = parentOf(one.track.key);\n for (const input of inputs.toReversed()) {\n const after = one.startedAt - input.at;\n if (after < 0) continue;\n if (after > 3e3) break;\n if (!(one.track.known >= input.at - fluxMillis || input.changed.has(one.track.key) || parent !== null && input.moved.has(parent)) && (local(input.target, element) || after <= 500 && input.still)) return input.at;\n }\n };\n const describe = (one, known, unmask, inputs) => {\n const { track, kind, before, after, lowest, highest, ...times } = one;\n const element = track.key instanceof Element ? track.key : null;\n const where = element?.isConnected === true ? element : marking.placeOf(track.key) ?? null;\n const role = element === null ? null : roleOf(element);\n const field = kind === \"value\";\n const checkable = element !== null && isInput(element) && /^(checkbox|radio)$/.test(element.type);\n const masked = (value) => value === void 0 || !field || unmask || checkable || value === \"\" ? value : \"••••\";\n return {\n ...times,\n kind,\n subject: {\n role,\n name: element === null ? \"\" : field ? nameOf(element, role) : clean(element.getAttribute(\"aria-label\"), 120),\n tag: element === null ? \"title\" : element.tagName.toLowerCase(),\n context: kind === \"title\" || where === null ? {} : placing.contextOf(where, known)\n },\n before: masked(before),\n after: masked(after),\n lowest: field ? void 0 : lowest,\n highest: field ? void 0 : highest,\n cause: field || where === null ? void 0 : causeOf(one, where, inputs)\n };\n };\n /** What changed after `since`, up to `until`, and no later than now. */\n const read = (request) => {\n const now = performance.timeOrigin + performance.now();\n recorder.start();\n seeing.settle();\n const until = Math.min(request.until ?? now, now);\n const { from, dropped, folded } = kept.read(request.since ?? Number.NEGATIVE_INFINITY, until, now);\n const known = placing.known();\n const inputs = before();\n return {\n until,\n from,\n dropped,\n records: folded.map((one) => describe(one, known, request.unmask, inputs))\n };\n };\n return { read };\n };\n //#endregion\n //#region src/internal/timeline/fold.inpage.ts\n const fold = () => {\n /** The one number some words show, such as 61240 in \"$61,240\"; none for none or several. */\n const number = (words) => {\n const found = words.match(/[-+−]?\\d[\\d,]*(?:\\.\\d+)?/g);\n const value = Number(found?.length === 1 ? found[0]?.replaceAll(\",\", \"\").replace(\"−\", \"-\") : \"\");\n return found?.length === 1 && Number.isFinite(value) ? [value] : [];\n };\n /** A track's changes after `since`, up to `until`, if one was seen in view. */\n const over = (track, since, until) => {\n const inside = track.samples.filter((sample) => sample.at > since && sample.at <= until);\n const first = inside[0];\n const last = inside.at(-1);\n if (first === void 0 || last === void 0 || !inside.some((sample) => sample.seen === true)) return void 0;\n const prior = track.samples[track.samples.indexOf(first) - 1];\n const lost = track.known <= since ? track.known : void 0;\n const start = prior === void 0 ? lost === void 0 ? void 0 : track.initial : prior.shown;\n const words = [start, ...inside.map((sample) => sample.shown)].filter((each) => typeof each === \"string\");\n const numbers = words.flatMap(number);\n const ranged = inside.length > 1 && numbers.length === words.length;\n const kind = track.kind !== \"content\" ? track.kind : last.shown === null ? start === null ? \"brief\" : \"disappeared\" : start === null ? \"appeared\" : \"text\";\n return {\n track,\n kind,\n startedAt: first.at,\n at: last.at,\n before: kind === \"disappeared\" ? words.at(-1) : start ?? void 0,\n after: kind === \"brief\" ? words.at(-1) : last.shown ?? void 0,\n count: inside.length,\n lowest: ranged ? words[numbers.indexOf(Math.min(...numbers))] : void 0,\n highest: ranged ? words[numbers.indexOf(Math.max(...numbers))] : void 0,\n earlier: prior?.at ?? (lost === Number.NEGATIVE_INFINITY ? void 0 : lost)\n };\n };\n /**\n * How readily a track gives way when the record is full: one that holds no change first, then\n * one that keeps changing, one never seen in view and one that has gone; news never does.\n */\n const rank = ({ samples: [only, ...more] }) => only === void 0 ? 4 : more.length > 0 ? 3 : only.seen === false ? 2 : only.shown === null ? 1 : 0;\n return {\n number,\n over,\n rank\n };\n };\n //#endregion\n //#region src/internal/timeline/history.inpage.ts\n const history = (folding, bounds = {\n tracks: 256,\n samples: 32,\n retention: 6e4\n }) => {\n const { tracks: maxTracks, samples: maxSamples, retention } = bounds;\n const tracks = /* @__PURE__ */ new Map();\n let refused = /* @__PURE__ */ new WeakSet();\n let losses = [];\n let lastLoss = Number.NEGATIVE_INFINITY;\n const gone = [];\n let begun;\n const lose = (at, count) => {\n const last = losses.at(-1);\n if (count <= 0) return;\n lastLoss = Math.max(lastLoss, at);\n if (last?.[0] === at) last[1] += count;\n else losses.push([at, count]);\n if (losses.length > 256) losses.shift();\n };\n const latest = (track) => track.samples.at(-1)?.at ?? Number.NEGATIVE_INFINITY;\n const drop = (track) => {\n tracks.delete(track.key);\n gone.push(track.key);\n };\n let version = 0;\n let barren = -1;\n /** What gives way for a newcomer: one that holds no change, then one that keeps changing. */\n const victim = () => {\n let chosen;\n let rank = 0;\n for (const track of barren === version ? [] : tracks.values()) {\n const next = folding.rank(track);\n if (next > rank) [chosen, rank] = [track, next];\n if (rank === 4) break;\n }\n if (chosen === void 0) barren = version;\n return chosen;\n };\n const evict = (track) => {\n drop(track);\n if (track.samples.length > 0) refused.add(track.key);\n for (const sample of track.samples) lose(sample.at, 1);\n };\n /** A key the record cannot keep: its change is counted. */\n const refuse = (key, at) => {\n refused.add(key);\n lose(at, 1);\n };\n /**\n * A new state for `key` at `at`: its sample, or none when it is no change or finds no room.\n * `initial` says what a key not yet kept showed before.\n */\n const note = (key, kind, at, shown, initial) => {\n let track = tracks.get(key);\n if (track === void 0) {\n const full = !refused.has(key) && tracks.size >= maxTracks;\n const out = full ? victim() : void 0;\n if (refused.has(key) || full && out === void 0) return refuse(key, at);\n const before = initial();\n if (shown !== void 0 && shown === before) return void 0;\n if (out !== void 0) evict(out);\n track = {\n key,\n kind,\n initial: before,\n known: Number.NEGATIVE_INFINITY,\n samples: []\n };\n tracks.set(key, track);\n }\n const { samples } = track;\n version++;\n if (samples.at(-1)?.at === at) samples.pop();\n const previous = samples.length === 0 ? track.initial : samples.at(-1)?.shown;\n if (shown !== void 0 && shown === previous) return void 0;\n const oldest = samples.length >= maxSamples ? samples.shift() : void 0;\n if (oldest !== void 0) {\n track.initial = oldest.shown;\n track.known = oldest.at;\n lose(oldest.at, 1);\n }\n const sample = {\n at,\n shown,\n seen: void 0\n };\n samples.push(sample);\n return sample;\n };\n /** Judge a sample as the page rendered it; a state that repeats the one before is no change. */\n const settle = (key, sample, shown, seen) => {\n const track = tracks.get(key);\n version++;\n if (sample.shown === void 0) sample.shown = shown;\n if (sample.seen === void 0) sample.seen = seen;\n if (track === void 0) return;\n let previous = track.initial;\n const changes = track.samples.filter((each) => {\n const same = each.shown !== void 0 && each.shown === previous;\n if (!same) previous = each.shown;\n return !same;\n });\n track.samples.splice(0, track.samples.length, ...changes);\n };\n /**\n * The window after `since`, up to `until`: where the record is whole, how many changes in it the\n * record let go, and each element's changes, oldest first by when they began.\n */\n const read = (since, until, now) => {\n for (const track of tracks.values()) if (latest(track) < now - retention) drop(track);\n return {\n from: Math.min(until, Math.max(begun ?? now, now - retention, lastLoss)),\n dropped: losses.reduce((sum, [at, count]) => at > since && at <= until ? sum + count : sum, 0),\n folded: [...tracks.values()].flatMap((track) => folding.over(track, since, until) ?? []).toSorted((left, right) => left.startedAt - right.startedAt)\n };\n };\n /** When something in view last changed, or something not yet judged; for a wait for stillness. */\n const changing = () => Math.max(Number.NEGATIVE_INFINITY, ...Array.from(tracks.values(), (track) => track.samples.findLast((sample) => sample.seen !== false)?.at ?? Number.NEGATIVE_INFINITY));\n return {\n begin: (at) => {\n begun = at;\n },\n begun: () => begun !== void 0,\n clear: () => {\n for (const track of tracks.values()) drop(track);\n refused = /* @__PURE__ */ new WeakSet();\n losses = [];\n lastLoss = Number.NEGATIVE_INFINITY;\n begun = void 0;\n },\n changing,\n /** Keys whose tracks went since this was last asked, for the recorder to stop watching. */\n forgotten: () => gone.splice(0),\n has: (key) => tracks.has(key),\n /** Whether the record has let `key` go: its later changes are counted, never kept. */\n refuses: (key) => refused.has(key),\n lose,\n note,\n read,\n /** Each element's history, and when the record let changes go, for a read to judge causes. */\n recent: () => ({\n tracks: [...tracks.values()],\n losses: losses.map(([at]) => at)\n }),\n /** A track moves to the element that took its key's place, as a re-rendered price does. */\n rekey: (from, to) => {\n const track = tracks.get(from);\n if (track === void 0 || tracks.has(to)) return false;\n tracks.delete(from);\n track.key = to;\n tracks.set(to, track);\n return true;\n },\n settle\n };\n };\n //#endregion\n //#region src/internal/timeline/marks.inpage.ts\n const marks = (names, kept, seeing) => {\n const { clean, isElement, isHtml } = names;\n const unwritten = new Set(\"SCRIPT STYLE NOSCRIPT TEMPLATE TITLE\".split(\" \"));\n const hiding = /display\\s*:\\s*none|visibility\\s*:\\s*hidden|opacity\\s*:\\s*0(?![.\\d])/i;\n let places = /* @__PURE__ */ new WeakMap();\n let title = \"\";\n const words = (text) => clean(text, 200) || null;\n const content = (element, at, shown, initial) => {\n const sample = kept.note(element, \"content\", at, shown, initial);\n if (sample !== void 0) seeing.look(element, sample);\n };\n /** What a text-only element said before this batch: its text with the batch undone. */\n const before = (leaf, records) => {\n let nodes = Array.from(leaf.childNodes);\n const old = /* @__PURE__ */ new Map();\n for (const record of records.toReversed()) if (record.type === \"characterData\") old.set(record.target, record.oldValue ?? \"\");\n else {\n const added = new Set(Array.from(record.addedNodes));\n nodes = nodes.filter((node) => !added.has(node));\n const next = record.nextSibling === null ? -1 : nodes.indexOf(record.nextSibling);\n nodes.splice(next === -1 ? nodes.length : next, 0, ...Array.from(record.removedNodes));\n }\n return words(nodes.map((node) => old.get(node) ?? node.textContent).join(\"\"));\n };\n const attribute = (element, { attributeName: name, oldValue: old }, at) => {\n const was = name === \"hidden\" ? old !== null : name === \"aria-hidden\" ? old === \"true\" : name === \"open\" ? element.tagName === \"DIALOG\" && old === null : name === \"style\" && hiding.test(old ?? \"\");\n if (name === \"style\" && !was && !hiding.test(element.getAttribute(\"style\") ?? \"\")) return;\n const state = seeing.state(element);\n if (!kept.has(element) && !was && state === void 0 && name !== \"open\") return seeing.glance(element);\n content(element, at, void 0, () => was || state === false ? null : name === \"open\" && old === null ? words(element.querySelector(\":scope > summary\")?.textContent) : words(element.textContent));\n };\n /** What one mutation added to and removed from `target`. */\n const nodes = (change, target, batch) => {\n const added = Array.from(change.addedNodes).slice(0, 64);\n const removed = Array.from(change.removedNodes).slice(0, 64);\n const text = [...added, ...removed].some((node) => !isElement(node) && /\\S/.test(node.textContent ?? \"\"));\n kept.lose(batch.at, Math.max(change.addedNodes.length, change.removedNodes.length) - 64);\n if (text) batch.owners.add(target);\n removed.forEach((gone, index) => {\n const node = added[index];\n if (!isElement(gone)) return;\n batch.left.add(gone);\n if (node !== void 0 && isElement(node) && kept.rekey(gone, node)) seeing.unwatch(gone);\n else if (kept.has(gone)) {\n places.set(gone, target);\n const sample = kept.note(gone, \"content\", batch.at, null, () => void 0);\n if (sample !== void 0) sample.seen = seeing.viewed(gone);\n seeing.unwatch(gone);\n }\n });\n for (const node of added) if (isElement(node) && isHtml(node) && !unwritten.has(node.tagName) && !node.isContentEditable) {\n if (kept.has(node) || !text && !batch.left.has(node)) {\n batch.arrived.add(node);\n content(node, batch.at, void 0, () => null);\n }\n }\n };\n /** Whether `element` is inside something this batch added, which tells it with its own words. */\n const inside = (element, arrived) => {\n for (let up = element.parentElement; arrived.size > 0 && up !== null; up = up.parentElement) if (arrived.has(up)) return true;\n return false;\n };\n const ownerOf = (change) => change.type === \"characterData\" ? change.target.parentElement : change.target;\n /** Mark what one batch of mutations changed, at `at`. */\n const mark = (records, at) => {\n const batch = {\n at,\n owners: /* @__PURE__ */ new Set(),\n arrived: /* @__PURE__ */ new Set(),\n left: /* @__PURE__ */ new Set()\n };\n let byOwner;\n for (const change of records) {\n const target = ownerOf(change);\n if (target === null || !isElement(target) || inside(target, batch.arrived)) continue;\n const known = kept.has(target) || kept.refuses(target);\n if (change.type !== \"attributes\") if (change.type === \"characterData\" || target.childElementCount === 0 && known) batch.owners.add(target);\n else nodes(change, target, batch);\n else if (isHtml(target) && target !== document.body && target !== document.documentElement) attribute(target, change, at);\n }\n for (const owner of batch.owners) {\n const leaf = owner.childElementCount === 0;\n if (kept.refuses(owner)) kept.lose(at, 1);\n if (kept.refuses(owner) || !isHtml(owner) || unwritten.has(owner.tagName)) continue;\n if (owner === document.body || owner.isContentEditable) continue;\n if (batch.arrived.has(owner) || inside(owner, batch.arrived)) continue;\n content(owner, at, leaf ? words(owner.textContent) : void 0, () => {\n byOwner ??= Map.groupBy(records, ownerOf);\n return leaf ? before(owner, byOwner.get(owner) ?? []) : void 0;\n });\n }\n if (document.title === title) return;\n const previous = words(title);\n const sample = kept.note(document, \"title\", at, words(title = document.title), () => previous);\n if (sample !== void 0) sample.seen = true;\n };\n return {\n mark,\n /** Where a removed element was, which gives its context. */\n placeOf: (key) => places.get(key),\n /** Start afresh, from the document's title now. */\n reset: () => {\n places = /* @__PURE__ */ new WeakMap();\n title = document.title;\n }\n };\n };\n //#endregion\n //#region src/internal/timeline/record.inpage.ts\n const record = (names, texts, kept, seeing, marking) => {\n const { isElement, isInput } = names;\n const epoch = () => performance.timeOrigin + performance.now();\n const declared = \"[role=alert],[role=status],[role=log],[role=dialog],[role=alertdialog],[aria-live],dialog,[popover]\";\n let running = false;\n let lastRead = 0;\n let focused = /* @__PURE__ */ new WeakMap();\n const inputs = [];\n const targetOf = (event) => {\n const target = event.composedPath()[0];\n return target instanceof Node && isElement(target) ? target : void 0;\n };\n /** A field's value as the record keeps it: a secret one's as `••••`, a box's checked or not. */\n const valueOf = (field) => isInput(field) && (field.type === \"checkbox\" || field.type === \"radio\") ? field.checked ? \"checked\" : \"not checked\" : texts.shown(field, true);\n const onFocus = (event) => {\n const field = targetOf(event);\n const value = field === void 0 ? void 0 : valueOf(field);\n if (field !== void 0 && value !== void 0) focused.set(field, value);\n };\n const onValue = (event) => {\n const field = targetOf(event);\n const value = field === void 0 ? void 0 : valueOf(field);\n if (field === void 0 || value === void 0) return;\n const sample = kept.note(field, \"value\", epoch(), value, () => focused.get(field) ?? null);\n if (sample !== void 0) seeing.look(field, sample);\n };\n const onInput = (event) => {\n const target = targetOf(event);\n if (!event.isTrusted || target === void 0) return;\n inputs.push({\n at: performance.timeOrigin + event.timeStamp,\n target\n });\n if (inputs.length > 8) inputs.shift();\n };\n const listeners = [\n [\"focusin\", onFocus],\n [\"input\", onValue],\n [\"change\", onValue],\n [\"pointerdown\", onInput],\n [\"keydown\", onInput]\n ];\n const options = {\n capture: true,\n passive: true\n };\n const stop = () => {\n running = false;\n observer.disconnect();\n for (const [type, listener] of listeners) document.removeEventListener(type, listener, options);\n seeing.clear();\n kept.clear();\n kept.forgotten();\n inputs.length = 0;\n focused = /* @__PURE__ */ new WeakMap();\n };\n const observer = new MutationObserver((records) => {\n const at = epoch();\n if (at - lastRead > 12e4) stop();\n else marking.mark(records, at);\n });\n const begin = () => {\n if (!running || kept.begun()) return;\n observer.observe(document, {\n subtree: true,\n childList: true,\n characterData: true,\n characterDataOldValue: true,\n attributeFilter: [\n \"hidden\",\n \"open\",\n \"aria-hidden\",\n \"class\",\n \"style\"\n ],\n attributeOldValue: true\n });\n for (const [type, listener] of listeners) document.addEventListener(type, listener, options);\n marking.reset();\n for (const element of Array.from(document.querySelectorAll(declared)).slice(0, 64)) seeing.glance(element, true);\n kept.begin(epoch());\n };\n return {\n /** Record from now, once the document is parsed; each read keeps the record going. */\n start: () => {\n lastRead = epoch();\n if (running) return;\n running = true;\n if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", begin, { once: true });\n else begin();\n },\n inputs: () => inputs\n };\n };\n //#endregion\n //#region src/internal/timeline/sight.inpage.ts\n const sight = (names, walked, texts, kept) => {\n const { clean } = names;\n const { visible } = walked;\n let viewed = /* @__PURE__ */ new WeakMap();\n let watching = /* @__PURE__ */ new WeakSet();\n let states = /* @__PURE__ */ new WeakMap();\n const waiting = /* @__PURE__ */ new Map();\n /** What an element shows: its words, if its style shows it. */\n const shownBy = (element) => element.isConnected && visible(element) ? clean(texts.lines(element, false), 200) || null : null;\n /** Settle what waited on an element; its words are read once, if one of them needs them. */\n const judge = (element, pending, seen, was) => {\n const shown = pending.some((sample) => sample.shown === void 0) ? shownBy(element) : void 0;\n for (const sample of pending) {\n const now = sample.shown === void 0 ? shown : sample.shown;\n kept.settle(element, sample, now, now === null ? was : seen);\n }\n };\n const observer = new IntersectionObserver((entries) => {\n for (const { target, isIntersecting } of entries) {\n const was = viewed.get(target) ?? false;\n const seen = isIntersecting && visible(target);\n const pending = waiting.get(target);\n viewed.set(target, seen);\n waiting.delete(target);\n if (pending !== void 0) judge(target, pending, seen, was);\n if (!kept.has(target)) {\n states.set(target, visible(target));\n unwatch(target);\n }\n }\n });\n /** Watch an element, asking for a fresh report even if it is watched already. */\n const watch = (element) => {\n watching.add(element);\n observer.unobserve(element);\n observer.observe(element);\n };\n const unwatch = (element) => {\n watching.delete(element);\n observer.unobserve(element);\n };\n return {\n /** Judge a sample at once if what it shows and whether it is in view are known, else on render. */\n look: (element, sample) => {\n const view = viewed.get(element);\n if (sample.shown !== void 0 && view !== void 0 && watching.has(element)) {\n sample.seen = view;\n return;\n }\n waiting.set(element, [...waiting.get(element) ?? [], sample]);\n watch(element);\n },\n /**\n * Look at an element once, for its state when it next changes: as it renders, or `now`, by its\n * style, as for the few the record looks at as it starts.\n */\n glance: (element, now = false) => {\n if (now) states.set(element, visible(element));\n if (!watching.has(element)) watch(element);\n },\n /** Whether the style of an element looked at once showed it, if it was. */\n state: (element) => states.get(element),\n /** Whether a watched element was in view as the page last rendered it. */\n viewed: (element) => viewed.get(element) ?? false,\n unwatch,\n /** Judge now what the page has not rendered since it changed, as a hidden tab never does. */\n settle: () => {\n for (const [element, pending] of waiting) {\n const seen = element.isConnected && walked.inView(element.getBoundingClientRect()) && visible(element);\n judge(element, pending, seen, viewed.get(element) ?? false);\n viewed.set(element, seen);\n }\n waiting.clear();\n for (const key of kept.forgotten()) if (key instanceof Element) unwatch(key);\n },\n clear: () => {\n observer.disconnect();\n waiting.clear();\n viewed = /* @__PURE__ */ new WeakMap();\n watching = /* @__PURE__ */ new WeakSet();\n states = /* @__PURE__ */ new WeakMap();\n }\n };\n };\n //#endregion\n //#region src/internal/page/url.ts\n const addresses = () => {\n const withheld = \"••••••••\";\n const credential = /token|secret|passw|passcode|passphrase|signature|credential|assertion|verifier|bearer|jwt|hmac|apikey|accesskey|privatekey|sess(?:ion)?id|saml(?:response|art)|(?:sig|otp)$|(?:auth|api|access|private|secret|signing|client|encryption|licen[cs]e|session|master|app|device|verification|verify|confirmation|confirm|activation|activate|reset|recovery|login|security|magic|onetime|mfa|2fa|sms|email|otp|oob)(?:code|key)$|^(?:auth|authorization|pwd|pass)$/;\n const ambiguous = /^(?:code|key|session|sid|ticket)$/;\n const secret = (name, value) => {\n const bare = name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n return value !== \"\" && (credential.test(bare) || ambiguous.test(bare) && value.length >= 16 && /\\d/.test(value) && /[a-z]/i.test(value));\n };\n const masked = (parameters, separator) => parameters.split(separator).map((parameter) => {\n const [name = \"\", value = \"\"] = [...new URLSearchParams(parameter)][0] ?? [];\n return secret(name, value) ? `${parameter.slice(0, parameter.indexOf(\"=\") + 1)}${withheld}` : parameter;\n }).join(separator);\n /**\n * `url` as the library reports it: without userinfo, and with each credential's value withheld,\n * so the rest keeps its identity, such as a chart's `?ticker=ETH`. An address without a host,\n * such as `data:` or `about:blank`, is content or a fixed name, and stays as it is.\n */\n const redact = (url) => {\n const parsed = URL.parse(url);\n if (parsed === null || parsed.host === \"\") return url;\n parsed.username = \"\";\n parsed.password = \"\";\n parsed.pathname = masked(parsed.pathname, \";\");\n parsed.search = masked(parsed.search.slice(1), \"&\");\n if (parsed.hash.includes(\"=\")) {\n const fragment = parsed.hash.slice(1);\n const question = fragment.indexOf(\"?\");\n const start = question >= 0 && question < fragment.indexOf(\"=\") ? question + 1 : 0;\n parsed.hash = fragment.slice(0, start) + masked(fragment.slice(start), \"&\");\n }\n return parsed.href.replaceAll(encodeURIComponent(withheld), withheld);\n };\n return {\n withheld,\n redact\n };\n };\n //#endregion\n //#region src/internal/page/bridge.ts\n const install = (makeNames, makeWalk, makeMatch, makeContext, makeSubjects, makeExpectations, makeText, makeControls, makeFold, makeHistory, makeSight, makeMarks, makeRecord, makeChanges, makeReady, makeOutline, makeTargets, makeEvidence, makeGuard, makeEdit, makeAddresses) => {\n const installed = globalThis.__effectBrowser;\n if (installed !== void 0 && installed.version === 20) return installed;\n const named = makeNames();\n const walked = makeWalk(named);\n const placing = makeContext(named, walked);\n const subjected = makeSubjects(named, walked, makeMatch(), placing);\n const texts = makeText(named, walked);\n const controlled = makeControls(named, walked, subjected, texts);\n const kept = makeHistory(makeFold());\n const seeing = makeSight(named, walked, texts, kept);\n const marking = makeMarks(named, kept, seeing);\n const recorder = makeRecord(named, texts, kept, seeing, marking);\n const read = makeOutline(named, walked, subjected, texts, controlled, makeAddresses());\n const located = makeTargets(named, walked, placing);\n const guarded = makeGuard(named, located, makeEvidence(named, placing));\n const edited = makeEdit(named, guarded, placing);\n const readiness = makeReady(walked, texts, kept, recorder);\n const api = {\n version: 20,\n snapshot: read.snapshot,\n find: subjected.find,\n until: makeExpectations(named, subjected).until,\n waitUntil: controlled.waitUntil,\n text: texts.read,\n record: recorder.start,\n changes: makeChanges(named, placing, kept, seeing, marking, recorder).read,\n ready: readiness.wait,\n settle: readiness.settle,\n point: located.point,\n scrollPlan: located.scrollPlan,\n viewport: read.viewport,\n screenSize: () => ({\n width: screen.width,\n height: screen.height\n }),\n prepareInput: guarded.prepareInput,\n validateInput: guarded.validateInput,\n typeable: edited.typeable,\n focus: edited.focus,\n focusOn: edited.focusOn,\n select: edited.select\n };\n globalThis.__effectBrowser = api;\n return api;\n };\n //#endregion\n return install(...[\n names,\n walk,\n match,\n context,\n subjects,\n expectations,\n text,\n controls,\n fold,\n history,\n sight,\n marks,\n record,\n changes,\n ready,\n outline,\n targets,\n evidence,\n guard,\n edit,\n addresses\n ]);\n})();\n\nreturn pageScript;})()";
167
+ const worldName = "effect-browser";
168
+ const recorderSource = `if (globalThis === globalThis.top) globalThis.__effectBrowser?.record();`;
169
+ const missing = "effect-browser: no __effectBrowser in this world";
170
+ const isMissing = (error) => error.reason._tag === "Failed" && error.reason.detail === missing;
171
+ /** A frame as a page session reports its commit. */
172
+ const Commit = Schema.Struct({
173
+ parentId: Schema.optionalKey(Schema.String),
174
+ loaderId: Schema.String,
175
+ url: Schema.String,
176
+ urlFragment: Schema.optionalKey(Schema.String)
177
+ });
178
+ /** `Page.frameNavigated` or `Page.navigatedWithinDocument`, as far as `mainFrame` reads them. */
179
+ const MainFrameEvent = Schema.Union([Schema.Struct({ frame: Commit }), Schema.Struct({
180
+ frameId: Schema.String,
181
+ url: Schema.String
182
+ })]);
183
+ /** A commit's loader and its address, redacted. */
184
+ const commitOf = ({ loaderId, url, urlFragment = "" }) => ({
185
+ loader: loaderId,
186
+ url: redact(url + urlFragment)
187
+ });
188
+ /**
189
+ * What a page session's commit or move says of `page`'s main frame: a new document's `loader` and
190
+ * its address, or a move's address; nothing for another frame. Only a commit is a new document:
191
+ * `frameStartedLoading` also fires on `pushState`, a move within the document.
192
+ */
193
+ const mainFrame = (page, event) => "frame" in event ? event.frame.parentId === void 0 ? commitOf(event.frame) : void 0 : event.frameId === page ? {
194
+ loader: void 0,
195
+ url: redact(event.url)
196
+ } : void 0;
197
+ /** A call to the page script, its arguments quoted as JavaScript literals. */
198
+ const scriptCall = (name, ...args) => `${name}(${args.map((arg) => JSON.stringify(arg)).join(", ")})`;
199
+ const make$18 = Effect.fnUntraced(function* (page) {
200
+ const { id, cdp, native, span, publish, now } = page;
201
+ const { send } = page.protocol;
202
+ const registration = yield* SynchronizedRef.make("none");
203
+ let world;
204
+ let documents = 0;
205
+ let url = redact(page.url);
206
+ let loaded;
207
+ const loaders = /* @__PURE__ */ new Map();
208
+ const onMainFrame = (event) => {
209
+ const seen = mainFrame(id, event);
210
+ if (seen === void 0) return;
211
+ if (seen.loader !== void 0) {
212
+ documents++;
213
+ loaders.delete(seen.loader);
214
+ loaders.set(seen.loader, documents);
215
+ const [oldest] = loaders.keys();
216
+ if (loaders.size > 8 && oldest !== void 0) loaders.delete(oldest);
217
+ page.activity.documentAt = now();
218
+ world = void 0;
219
+ loaded = void 0;
220
+ }
221
+ url = seen.url;
222
+ const sameDocument = seen.loader === void 0;
223
+ publish(new Navigated({
224
+ at: now(),
225
+ page: id,
226
+ url,
227
+ document: documents,
228
+ sameDocument
229
+ }));
230
+ };
231
+ const onLoaded = (state) => () => {
232
+ loaded = new PageLoaded({
233
+ at: now(),
234
+ page: id,
235
+ state
236
+ });
237
+ publish(loaded);
238
+ };
239
+ cdp.on("Page.frameNavigated", onMainFrame);
240
+ cdp.on("Page.navigatedWithinDocument", onMainFrame);
241
+ cdp.on("Page.domContentEventFired", onLoaded("domcontentloaded"));
242
+ cdp.on("Page.loadEventFired", onLoaded("load"));
243
+ /**
244
+ * The main-frame commits seen so far, once the Page domain, which registration turned on, has
245
+ * answered, so that every later one counts. A commit within about one round trip of the count
246
+ * can still be missed.
247
+ */
248
+ const currentDocument = (operation) => page.paging(operation).pipe(Effect.map(() => documents));
249
+ /** What a frame that arrives now shows: its session, the page's current document and address. */
250
+ const frameTag = () => ({
251
+ session: page.session,
252
+ connection: page.connection,
253
+ document: documents,
254
+ url
255
+ });
256
+ /** The document the main frame's latest commit of `loader` began, if this session counted it. */
257
+ const documentOf = (loader) => loaders.get(loader);
258
+ const addScript = (operation, source) => native(operation, () => send("Page.addScriptToEvaluateOnNewDocument", {
259
+ source,
260
+ worldName,
261
+ runImmediately: true
262
+ }));
263
+ const register = (operation) => Effect.all([page.paging(operation), addScript(operation, `if (globalThis === globalThis.top) ${installSource};`)], { concurrency: "unbounded" }).pipe(span("Page.register", {}, "Debug"));
264
+ /** Take registration from `from` to `to` by `step`, unless it is past `from` already. */
265
+ const advance = (from, to, step) => SynchronizedRef.updateEffect(registration, (state) => state === from ? Effect.as(step, to) : Effect.succeed(state));
266
+ /** Register the script once, and the recorder after it once a read of changes asks for it. */
267
+ const ensure = (operation, recorder) => Effect.andThen(advance("none", "script", register(operation)), recorder ? advance("script", "recorder", addScript(operation, recorderSource)) : Effect.void);
268
+ const createWorld = (operation) => Effect.gen(function* () {
269
+ yield* ensure(operation, false);
270
+ const document = documents;
271
+ const created = yield* native(operation, () => send("Page.createIsolatedWorld", {
272
+ frameId: id,
273
+ worldName
274
+ }));
275
+ if (document === documents) world = created.executionContextId;
276
+ return created.executionContextId;
277
+ }).pipe(span("Page.createWorld", {}, "Debug"));
278
+ /** The current document's world, created if it has none yet. */
279
+ const current = (operation) => Effect.suspend(() => world === void 0 ? createWorld(operation) : Effect.succeed(world));
280
+ /** A world of the current document without the page script, as the clock probe needs. */
281
+ const bareWorld = (operation) => native(operation, () => send("Page.createIsolatedWorld", {
282
+ frameId: id,
283
+ worldName: `${worldName}-clock`
284
+ })).pipe(Effect.map((created) => created.executionContextId));
285
+ const run = (operation, expression, contextId) => native(operation, () => send("Runtime.evaluate", {
286
+ contextId,
287
+ expression,
288
+ returnByValue: true,
289
+ awaitPromise: true
290
+ })).pipe(Effect.flatMap(({ result, exceptionDetails: thrown }) => thrown === void 0 ? Effect.succeed(result.value) : failWith(operation, new Failed({ detail: thrown.exception?.description ?? thrown.text }))));
291
+ const evaluateIn = (operation, call, contextId) => run(operation, `globalThis.__effectBrowser === undefined ? ${JSON.stringify(missing)} : globalThis.__effectBrowser.${call}`, contextId).pipe(Effect.filterOrFail((value) => value !== missing, () => undispatched(operation, new Failed({ detail: missing }))), span("Page.evaluate", { function: call.split("(")[0] }, "Trace"));
292
+ const evaluateWithContext = (operation, call) => {
293
+ const attempt = (contextId) => evaluateIn(operation, call, contextId).pipe(Effect.map((value) => ({
294
+ contextId,
295
+ value
296
+ })));
297
+ const again = createWorld(operation).pipe(Effect.flatMap((contextId) => attempt(contextId).pipe(Effect.catchIf(isMissing, () => run(operation, `${installSource}.version`, contextId).pipe(Effect.andThen(attempt(contextId)))))));
298
+ return current(operation).pipe(Effect.flatMap(attempt), Effect.catchIf(contextGone, () => again));
299
+ };
300
+ const evaluate = (operation, call) => evaluateWithContext(operation, call).pipe(Effect.map(({ value }) => value));
301
+ return {
302
+ current,
303
+ bareWorld,
304
+ evaluate,
305
+ evaluateIn,
306
+ evaluateWithContext,
307
+ currentDocument,
308
+ frameTag,
309
+ documentOf,
310
+ /** How far the current document has loaded, as this session heard; nothing until parsed. */
311
+ loaded: () => loaded,
312
+ /** Register the recorder, so every later document records from its start. */
313
+ registerRecorder: (operation) => ensure(operation, true)
314
+ };
315
+ });
316
+ //#endregion
317
+ //#region src/internal/input/dispatch.ts
318
+ /**
319
+ * Sending input to the browser. Each mouse event, key and inserted text goes out on the page's
320
+ * protocol session, stamped with the browser's clock when its run has a mapping, and is published
321
+ * as it is submitted.
322
+ */
323
+ const buttonMask = {
324
+ none: 0,
325
+ left: 1,
326
+ right: 2,
327
+ middle: 4
328
+ };
329
+ /** A mouse button going down or up at a point, as the `clickCount`th click. */
330
+ const buttonAt = (type, point, button = "left", clickCount = 1) => ({
331
+ type,
332
+ ...point,
333
+ button,
334
+ buttons: type === "mousePressed" ? buttonMask[button] : 0,
335
+ clickCount
336
+ });
337
+ const make$17 = (page) => {
338
+ const { id, pointer, publish, now, noteInput, closedBy } = page;
339
+ const { send } = page.protocol;
340
+ const inputClocks = /* @__PURE__ */ new WeakMap();
341
+ const stamp = (estimate, at) => estimate === void 0 ? {} : { timestamp: toBrowserSeconds(estimate, at) };
342
+ const inputCall = (operation, effect) => effect.pipe(Effect.mapError((error) => undispatched(operation, reasonOf(error.cause, void 0, closedBy()))));
343
+ const dispatchMouse = (event, estimate, submitted) => {
344
+ const at = now();
345
+ const point = {
346
+ x: event.x,
347
+ y: event.y
348
+ };
349
+ const button = event.button ?? "left";
350
+ const ButtonEvent = event.type === "mousePressed" ? PointerPressed : PointerReleased;
351
+ const track = (event.type === "mousePressed" || event.type === "mouseReleased") && button !== "none" ? new ButtonEvent({
352
+ at,
353
+ page: id,
354
+ ...point,
355
+ button,
356
+ clickCount: event.clickCount ?? 1
357
+ }) : event.type === "mouseWheel" ? new WheelScrolled({
358
+ at,
359
+ page: id,
360
+ ...point,
361
+ dx: event.deltaX ?? 0,
362
+ dy: event.deltaY ?? 0
363
+ }) : void 0;
364
+ const response = send("Input.dispatchMouseEvent", {
365
+ ...event,
366
+ ...stamp(estimate, at)
367
+ });
368
+ noteInput();
369
+ if (event.type === "mouseMoved") MutableRef.set(pointer, Option.some(point));
370
+ if (track !== void 0) publish(track);
371
+ submitted?.();
372
+ return response;
373
+ };
374
+ const sendMouse = (operation, run, event, submitted) => inputCall(operation, Effect.gen(function* () {
375
+ const held = `mouse:${event.button ?? "left"}`;
376
+ const estimate = inputClocks.get(run);
377
+ if (event.type === "mouseReleased") return yield* run.up(held);
378
+ if (event.type === "mousePressed") {
379
+ yield* run.reserve(2);
380
+ yield* run.down(held, () => dispatchMouse(event, estimate, submitted), () => dispatchMouse(buttonAt("mouseReleased", Option.getOrElse(MutableRef.get(pointer), () => ({
381
+ x: event.x,
382
+ y: event.y
383
+ })), event.button, event.clickCount), estimate));
384
+ } else {
385
+ yield* run.reserve(1);
386
+ yield* run.send(() => dispatchMouse(event, estimate, submitted));
387
+ }
388
+ }));
389
+ const dispatchKey = (key, phase, command, run, secret = false) => {
390
+ const at = now();
391
+ const event = new KeyChanged({
392
+ at,
393
+ page: id,
394
+ key: secret ? "Unidentified" : key,
395
+ phase
396
+ });
397
+ const response = command(at, inputClocks.get(run));
398
+ noteInput();
399
+ publish(event);
400
+ return response;
401
+ };
402
+ const dispatchText = (text, secret) => {
403
+ const track = new TextInserted({
404
+ at: now(),
405
+ page: id,
406
+ text: secret ? "•" : text
407
+ });
408
+ const response = send("Input.insertText", { text });
409
+ noteInput();
410
+ publish(track);
411
+ return response;
412
+ };
413
+ const flush = (operation, run) => inputCall(operation, run.drain);
414
+ return {
415
+ inputClocks,
416
+ stamp,
417
+ inputCall,
418
+ dispatchMouse,
419
+ sendMouse,
420
+ dispatchKey,
421
+ dispatchText,
422
+ flush
423
+ };
424
+ };
425
+ //#endregion
426
+ //#region src/internal/input/guard.ts
427
+ /**
428
+ * The input guard's host side. It asks the page what an action would touch, builds the request a
429
+ * policy decides on, binds an approval to the document and targets it saw, and runs the page
430
+ * script's changes in the approved document only.
431
+ */
432
+ /** Recheck an approval, after any pending input it must see; neither runs without a guard. */
433
+ const check = (approval, options = {}, before = Effect.void) => approval === void 0 ? Effect.void : before.pipe(Effect.andThen(approval.check(options)));
434
+ const inputFailure = (operation, targets, failure) => {
435
+ const target = failure.index === void 0 ? void 0 : targets[failure.index];
436
+ 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 }));
437
+ };
438
+ const editFailure = (operation, ref, failure) => failWith(operation, failure.stale === true ? new StaleRef({ ref }) : new NotActionable({ detail: failure.error }));
439
+ const make$16 = (page, bridge) => {
440
+ const { id } = page;
441
+ const { current, evaluateIn, evaluateWithContext } = bridge;
442
+ const preparePolicy = (action, info, targets, flags = {}) => Effect.gen(function* () {
443
+ for (const target of targets) {
444
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return yield* refuse(action, `"${target}" is not a ref; refs look like e12`);
445
+ if (target !== null && typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return yield* refuse(action, "point coordinates must be finite");
446
+ }
447
+ const input = {
448
+ action,
449
+ targets,
450
+ submit: flags.submit ?? false,
451
+ keys: flags.keys ?? null,
452
+ destination: flags.destination ?? null
453
+ };
454
+ const { contextId, value } = yield* evaluateWithContext(action, scriptCall("prepareInput", input));
455
+ const prepared = yield* decodeWith(action, PreparedInputResultSchema)(value);
456
+ if ("error" in prepared) return yield* inputFailure(action, targets, prepared);
457
+ const first = prepared.targets[0];
458
+ const target = targets[0];
459
+ const { title, description, dialog, heading, nearby, form } = prepared.evidence;
460
+ const request = new InputRequest({
461
+ page: id,
462
+ url: redact(prepared.url),
463
+ title,
464
+ action,
465
+ target: info.target,
466
+ text: info.text !== void 0 && action === "type" && first?.secret === true ? redacted : info.text,
467
+ element: first?.element,
468
+ role: first?.role,
469
+ name: first?.name,
470
+ description,
471
+ href: first?.href === void 0 ? void 0 : redact(first.href),
472
+ point: target !== null && typeof target === "object" ? target : void 0,
473
+ destination: prepared.destination === void 0 ? void 0 : redact(prepared.destination),
474
+ facts: prepared.facts,
475
+ context: {
476
+ dialog,
477
+ heading,
478
+ nearby
479
+ },
480
+ form
481
+ });
482
+ const check = (options = {}) => evaluateIn(action, scriptCall("validateInput", input, prepared, options), contextId).pipe(Effect.catchIf(contextGone, () => failWith(action, new NotActionable({ detail: "the page changed while input policy was pending" }))), Effect.flatMap(decodeWith(action, ValidatedInputResultSchema)), Effect.flatMap((result) => "error" in result ? inputFailure(action, targets, result) : Effect.void));
483
+ return {
484
+ request,
485
+ validate: check().pipe(Effect.as({
486
+ contextId,
487
+ check
488
+ }))
489
+ };
490
+ });
491
+ 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" }))));
492
+ /** A read may follow the current document only when no approval binds it to one. */
493
+ const evaluate = (operation, call, approval) => approval === void 0 ? bridge.evaluate(operation, call) : mutate(operation, call, approval);
494
+ return {
495
+ preparePolicy,
496
+ mutate,
497
+ evaluate
498
+ };
499
+ };
500
+ //#endregion
501
+ //#region src/internal/input/keys.ts
502
+ /**
503
+ * Key aliases for Playwright and printable US key descriptions for pipelined Chromium typing.
504
+ */
505
+ const aliases = {
506
+ ctrl: "Control",
507
+ control: "Control",
508
+ cmd: "Meta",
509
+ command: "Meta",
510
+ meta: "Meta",
511
+ win: "Meta",
512
+ super: "Meta",
513
+ mod: "ControlOrMeta",
514
+ alt: "Alt",
515
+ option: "Alt",
516
+ shift: "Shift",
517
+ enter: "Enter",
518
+ return: "Enter",
519
+ esc: "Escape",
520
+ escape: "Escape",
521
+ tab: "Tab",
522
+ space: "Space",
523
+ spacebar: "Space",
524
+ " ": "Space",
525
+ backspace: "Backspace",
526
+ delete: "Delete",
527
+ del: "Delete",
528
+ insert: "Insert",
529
+ home: "Home",
530
+ end: "End",
531
+ pageup: "PageUp",
532
+ pagedown: "PageDown",
533
+ up: "ArrowUp",
534
+ down: "ArrowDown",
535
+ left: "ArrowLeft",
536
+ right: "ArrowRight",
537
+ arrowup: "ArrowUp",
538
+ arrowdown: "ArrowDown",
539
+ arrowleft: "ArrowLeft",
540
+ arrowright: "ArrowRight"
541
+ };
542
+ const named$1 = new Set(Object.values(aliases));
543
+ /** Parse and validate the whole chord before sending any down events. */
544
+ const parts = (keys) => {
545
+ const tokens = [];
546
+ let building = "";
547
+ for (const character of keys) if (character === "+" && building.trim() !== "") {
548
+ tokens.push(building);
549
+ building = "";
550
+ } else building += character;
551
+ tokens.push(building);
552
+ if (tokens.length > 4) return void 0;
553
+ const out = [];
554
+ for (const token of tokens) {
555
+ const part = token === " " ? "Space" : token.trim();
556
+ const lower = part.toLowerCase();
557
+ const alias = Object.hasOwn(aliases, lower) ? aliases[lower] : void 0;
558
+ if (alias !== void 0) out.push(alias);
559
+ else if (named$1.has(part) || /^F([1-9]|1[0-2])$/.test(part) || /^(Key[A-Z]|Digit[0-9]|Numpad[0-9])$/.test(part) || description(part) !== void 0) out.push(part);
560
+ else return void 0;
561
+ }
562
+ return out;
563
+ };
564
+ const punctuation = [
565
+ [
566
+ "`~",
567
+ "Backquote",
568
+ 192
569
+ ],
570
+ [
571
+ "-_",
572
+ "Minus",
573
+ 189
574
+ ],
575
+ [
576
+ "=+",
577
+ "Equal",
578
+ 187
579
+ ],
580
+ [
581
+ "\\|",
582
+ "Backslash",
583
+ 220
584
+ ],
585
+ [
586
+ "[{",
587
+ "BracketLeft",
588
+ 219
589
+ ],
590
+ [
591
+ "]}",
592
+ "BracketRight",
593
+ 221
594
+ ],
595
+ [
596
+ ";:",
597
+ "Semicolon",
598
+ 186
599
+ ],
600
+ [
601
+ "'\"",
602
+ "Quote",
603
+ 222
604
+ ],
605
+ [
606
+ ",<",
607
+ "Comma",
608
+ 188
609
+ ],
610
+ [
611
+ ".>",
612
+ "Period",
613
+ 190
614
+ ],
615
+ [
616
+ "/?",
617
+ "Slash",
618
+ 191
619
+ ],
620
+ [
621
+ " ",
622
+ "Space",
623
+ 32
624
+ ]
625
+ ];
626
+ /**
627
+ * Match Playwright's pinned US layout for literal typing. Uppercase and shifted punctuation
628
+ * carry their literal text without synthesizing Shift. Newlines also use insertText so a text
629
+ * payload cannot submit a form; only an explicit Enter or submit option may do that.
630
+ */
631
+ const description = (character) => {
632
+ if (character === "Backspace") return {
633
+ code: "Backspace",
634
+ keyCode: 8,
635
+ key: "Backspace",
636
+ text: ""
637
+ };
638
+ if (character.length !== 1) return void 0;
639
+ if (/^[A-Za-z]$/.test(character)) {
640
+ const upper = character.toUpperCase();
641
+ return {
642
+ code: `Key${upper}`,
643
+ keyCode: upper.charCodeAt(0),
644
+ key: character,
645
+ text: character
646
+ };
647
+ }
648
+ const digit = "0123456789".indexOf(character);
649
+ const number = digit === -1 ? ")!@#$%^&*(".indexOf(character) : digit;
650
+ if (number !== -1) return {
651
+ code: `Digit${number}`,
652
+ keyCode: 48 + number,
653
+ key: character,
654
+ text: character
655
+ };
656
+ const symbol = punctuation.find(([characters]) => characters.includes(character));
657
+ return symbol === void 0 ? void 0 : {
658
+ code: symbol[1],
659
+ keyCode: symbol[2],
660
+ key: character,
661
+ text: character
662
+ };
663
+ };
664
+ //#endregion
665
+ //#region src/internal/input/keyboard.ts
666
+ /**
667
+ * Keys and typing: a key chord held and released, each typed event, and the document a run of
668
+ * keys must stay in.
669
+ */
670
+ const make$15 = (page, bridge, dispatch) => {
671
+ const { playwright } = page;
672
+ const { send } = page.protocol;
673
+ const { currentDocument } = bridge;
674
+ const { stamp, inputCall, dispatchKey, dispatchText } = dispatch;
675
+ const sameDocument = (operation, since) => currentDocument(operation).pipe(Effect.flatMap((now) => now === since ? Effect.void : failWith(operation, new NotActionable({ detail: "the page moved to another document, so the remaining keys were not sent" }))));
676
+ const keyStroke = (operation, run, parts, holdMillis = 0) => Effect.gen(function* () {
677
+ yield* inputCall(operation, run.reserve(parts.length * 2));
678
+ for (const part of parts) yield* inputCall(operation, run.down(`playwright:${part}`, () => dispatchKey(part, "down", () => playwright.keyboard.down(part), run), () => dispatchKey(part, "up", () => playwright.keyboard.up(part), run)));
679
+ if (holdMillis > 0) yield* Effect.sleep(Duration.millis(holdMillis));
680
+ for (const part of parts.toReversed()) yield* inputCall(operation, run.up(`playwright:${part}`));
681
+ });
682
+ const typeEvent = (run, event, secret) => inputCall("type", Effect.gen(function* () {
683
+ if (event.phase === "insert") {
684
+ yield* run.reserve(1);
685
+ yield* run.send(() => dispatchText(event.key, secret));
686
+ return;
687
+ }
688
+ const description$2 = description(event.key);
689
+ if (description$2 === void 0) return yield* Effect.die("invalid planned key");
690
+ const { key, code, keyCode, text } = description$2;
691
+ const held = "raw:" + code;
692
+ if (event.phase === "up") return yield* run.up(held);
693
+ const keyEvent = (phase) => () => dispatchKey(key, phase, (at, estimate) => send("Input.dispatchKeyEvent", {
694
+ type: phase === "down" ? "keyDown" : "keyUp",
695
+ ...stamp(estimate, at),
696
+ modifiers: 0,
697
+ windowsVirtualKeyCode: keyCode,
698
+ code,
699
+ key,
700
+ location: 0,
701
+ ...phase === "down" ? {
702
+ commands: [],
703
+ text,
704
+ unmodifiedText: text,
705
+ autoRepeat: false,
706
+ isKeypad: false
707
+ } : {}
708
+ }), run, secret);
709
+ yield* run.reserve(2);
710
+ yield* run.down(held, keyEvent("down"), keyEvent("up"));
711
+ }));
712
+ return {
713
+ currentDocument,
714
+ sameDocument,
715
+ keyStroke,
716
+ typeEvent
717
+ };
718
+ };
719
+ //#endregion
720
+ //#region src/internal/input/style.ts
721
+ /**
722
+ * How input is sent: plainly, as fast as the page takes it, or performed for viewers, with a
723
+ * pointer that glides, keys typed at a person's pace, held buttons and wheels in bursts. A page's
724
+ * actions take one, and the input reaches the same controls either way; only its timing, and
725
+ * what viewers see of it, differ. A presenter builds the performed one.
726
+ */
727
+ /** Each key as it comes, with no pause; a drag moves in eight short steps so its target sees it. */
728
+ const plain = {
729
+ shown: false,
730
+ pointer: void 0,
731
+ glide: (from, to, dragging) => Effect.succeed(dragging ? Array.from({ length: 8 }, (_, index) => ({
732
+ x: index === 7 ? to.x : from.x + (to.x - from.x) * (index + 1) / 8,
733
+ y: index === 7 ? to.y : from.y + (to.y - from.y) * (index + 1) / 8,
734
+ afterMillis: (index + 1) * 8
735
+ })) : [{
736
+ ...to,
737
+ afterMillis: 0
738
+ }]),
739
+ hold: Effect.succeed(0),
740
+ typing: (text, guarded) => Effect.succeed(guarded && text !== "" ? [{
741
+ afterMillis: 0,
742
+ phase: "insert",
743
+ key: text
744
+ }] : [...text].flatMap((key) => description(key) === void 0 ? [{
745
+ afterMillis: 0,
746
+ phase: "insert",
747
+ key
748
+ }] : [{
749
+ afterMillis: 0,
750
+ phase: "down",
751
+ key
752
+ }, {
753
+ afterMillis: 0,
754
+ phase: "up",
755
+ key
756
+ }])),
757
+ keyGap: Effect.succeed(0),
758
+ wheel: (dx, dy) => Effect.succeed([{
759
+ dx,
760
+ dy,
761
+ afterMillis: 0
762
+ }])
763
+ };
764
+ const lognormal = Effect.fnUntraced(function* (median, sigma, minimum, maximum) {
765
+ const normal = Math.sqrt(-2 * Math.log(1 - (yield* Random.next))) * Math.cos(2 * Math.PI * (yield* Random.next));
766
+ return Math.max(minimum, Math.min(maximum, median * Math.exp(sigma * normal)));
767
+ });
768
+ /** A pause around `median`, as the research measured reactions: at most four times it. */
769
+ const pause = (median, sigma) => median <= 0 ? Effect.succeed(0) : lognormal(median, sigma, 0, median * 4);
770
+ const pressHold = lognormal(75.2, .35, 35, 200);
771
+ const keyHold = lognormal(107.8, .2, 60, 220);
772
+ const eventOrder = {
773
+ up: 0,
774
+ insert: 1,
775
+ down: 2
776
+ };
777
+ /**
778
+ * Plan the whole text before any key goes, so holds overlap without waiting on the network. A
779
+ * physical key is never held twice, as with A and a, or ! and 1. The first key waits for the hand
780
+ * to reach the keys, each later one follows the last by a gap drawn around `gap`, and a word's
781
+ * first key comes 60 ms later.
782
+ */
783
+ const typing = Effect.fnUntraced(function* (text, gap) {
784
+ const events = [];
785
+ const releases = /* @__PURE__ */ new Map();
786
+ let due = text === "" ? 0 : yield* Random.nextBetween(150, 400);
787
+ let previous = "";
788
+ for (const character of text) {
789
+ if (/\s/u.test(previous) && /[\p{L}\p{N}]/u.test(character)) due += 60;
790
+ const description$1 = description(character);
791
+ if (description$1 === void 0) events.push({
792
+ afterMillis: due,
793
+ phase: "insert",
794
+ key: character
795
+ });
796
+ else {
797
+ due = Math.max(due, (releases.get(description$1.code) ?? -1) + 1);
798
+ const released = due + (yield* keyHold);
799
+ events.push({
800
+ afterMillis: due,
801
+ phase: "down",
802
+ key: character
803
+ }, {
804
+ afterMillis: released,
805
+ phase: "up",
806
+ key: character
807
+ });
808
+ releases.set(description$1.code, released);
809
+ }
810
+ due += yield* Random.nextBetween(gap / 2, gap * 3 / 2);
811
+ previous = character;
812
+ }
813
+ return events.toSorted((left, right) => left.afterMillis - right.afterMillis || eventOrder[left.phase] - eventOrder[right.phase]);
814
+ });
815
+ /** A word is five characters; the gap leaves room for each word's later start. */
816
+ const keyGap = (wordsPerMinute) => 12e3 / wordsPerMinute - 10;
817
+ /**
818
+ * The wheel in 100 px notches, 30 to 140 ms apart, in bursts of at most nine, with 0.6 to 2.6 s
819
+ * between bursts, as people scroll.
820
+ */
821
+ const bursts = Effect.fnUntraced(function* (dx, dy) {
822
+ const notches = Math.max(1, Math.ceil(Math.hypot(dx, dy) / 100));
823
+ const steps = [];
824
+ let due = 0;
825
+ for (let index = 0; index < notches; index++) {
826
+ if (index > 0) due += yield* index % 9 === 0 ? Random.nextBetween(600, 2600) : Random.nextBetween(30, 140);
827
+ steps.push({
828
+ dx: dx / notches,
829
+ dy: dy / notches,
830
+ afterMillis: due
831
+ });
832
+ }
833
+ return steps;
834
+ });
835
+ /** Input performed for viewers, with this pointer and pace. */
836
+ const performed = (options) => {
837
+ const gap = keyGap(options.wordsPerMinute);
838
+ return {
839
+ shown: true,
840
+ pointer: options.pointer,
841
+ glide: (from, to) => options.motion.plan(from, to),
842
+ hold: pressHold,
843
+ typing: (text) => typing(text, gap),
844
+ keyGap: Random.nextBetween(gap / 2, gap * 3 / 2),
845
+ wheel: bursts
846
+ };
847
+ };
848
+ //#endregion
849
+ //#region src/internal/input/perform.ts
850
+ /**
851
+ * Performing one action: its turn on the page, the input guard's step, the input run that owns its
852
+ * replies, and the action's record. Its deadline leaves out the time its style takes to show it.
853
+ */
854
+ const subjectOf = (target) => new Subject({
855
+ role: target.role,
856
+ name: target.name,
857
+ tag: target.tag,
858
+ context: target.context
859
+ });
860
+ /** What an action records of what it acted on: subjects, and a point target's box. */
861
+ const actedOn = (subject, to) => ({
862
+ subject: subjectOf(subject),
863
+ box: subject.box,
864
+ ...to === void 0 ? {} : { to: subjectOf(to) }
865
+ });
866
+ /** A subject as span attributes under `prefix`, without a role it does not have. */
867
+ const subjectAttributes = (prefix, subject) => subject === void 0 ? {} : {
868
+ ...subject.role === null ? {} : { [`${prefix}.role`]: subject.role },
869
+ [`${prefix}.name`]: subject.name,
870
+ [`${prefix}.tag`]: subject.tag
871
+ };
872
+ const timedOut = (operation, timeout) => failWith(operation, new Timeout({ millis: Duration.toMillis(timeout) }));
873
+ /**
874
+ * Fail `effect` with `Timeout` once `timeout` has passed, and as much later again as `presented`
875
+ * says presentation has planned meanwhile.
876
+ */
877
+ const deadline$2 = (operation, timeout, now, presented) => (effect) => Effect.suspend(() => {
878
+ const due = now() + Duration.toMillis(timeout);
879
+ const expiry = Effect.suspend(() => {
880
+ const left = due + presented() - now();
881
+ return left <= 0 ? timedOut(operation, timeout) : Effect.sleep(Duration.millis(left)).pipe(Effect.andThen(expiry));
882
+ });
883
+ return Effect.raceFirst(effect, expiry);
884
+ });
885
+ /** An admitted operation stays owned through its Action and any unanswered native replies. */
886
+ const admission = (page, pending, name) => {
887
+ let run;
888
+ let end;
889
+ return {
890
+ enter: Effect.uninterruptible(Effect.gen(function* () {
891
+ const released = page.inputGate === void 0 ? void 0 : yield* page.inputGate.enter(name);
892
+ const done = Deferred.makeUnsafe();
893
+ pending.add(done);
894
+ end = () => {
895
+ const finish = () => {
896
+ released?.();
897
+ pending.delete(done);
898
+ Deferred.doneUnsafe(done, Exit.void);
899
+ };
900
+ if (run === void 0) finish();
901
+ else run.afterIdle(finish);
902
+ };
903
+ })),
904
+ using: (current) => {
905
+ run = current;
906
+ },
907
+ end: Effect.sync(() => end?.())
908
+ };
909
+ };
910
+ /** Close a run on every exit while preserving its native replies for the page's next input. */
911
+ 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));
912
+ const make$14 = (page, sender) => {
913
+ const { id, settings, mapping, lane, publish, now, noteInput, span, owned } = page;
914
+ const { inputClocks } = sender;
915
+ const input = make$19();
916
+ const admitted = /* @__PURE__ */ new Set();
917
+ const perform = (name, info, timeout, prepare, body, style = plain) => Effect.gen(function* () {
918
+ const startedAt = now();
919
+ const sent = yield* Ref.make(false);
920
+ const at = yield* Ref.make(Option.none());
921
+ const acted = yield* Ref.make({});
922
+ const touch = Effect.sync(noteInput);
923
+ let revealed = info.secret !== true;
924
+ let presented = 0;
925
+ const ticket = admission(page, admitted, name);
926
+ const marks = {
927
+ style,
928
+ present: (millis) => Effect.sync(() => {
929
+ presented += Math.max(0, millis);
930
+ }),
931
+ sent: Ref.set(sent, true).pipe(Effect.andThen(touch)),
932
+ touched: touch,
933
+ at: (point) => Ref.set(at, Option.some(point)),
934
+ on: (subject, to) => Ref.set(acted, actedOn(subject, to)),
935
+ reveal: Effect.sync(() => {
936
+ revealed = true;
937
+ })
938
+ };
939
+ let estimate;
940
+ const admit = input.idle.pipe(Effect.tap(() => Effect.sync(() => {
941
+ estimate = info.input ?? true ? mapping.latest() : void 0;
942
+ })));
943
+ const useInput = (action) => input.begin.pipe(Effect.tap((current) => Effect.sync(() => {
944
+ ticket.using(current);
945
+ if (estimate !== void 0) inputClocks.set(current, estimate);
946
+ })), Effect.flatMap((run) => useRun(page, sender, name, run, action(run), info.input === false ? Effect.succeed(false) : Ref.get(sent))));
947
+ let queuedMillis;
948
+ const dispatch = (action) => Effect.suspend(() => {
949
+ const asked = now();
950
+ return lane.write(name, timeout)(ticket.enter.pipe(Effect.andThen(Effect.suspend(() => admit.pipe(Effect.timeoutOrElse({
951
+ duration: Duration.millis(Math.max(0, Duration.toMillis(timeout) - (now() - asked))),
952
+ orElse: () => timedOut(name, timeout)
953
+ })))), Effect.tap(() => Effect.sync(() => {
954
+ queuedMillis = Math.round(now() - asked);
955
+ })), Effect.andThen(deadline$2(name, timeout, now, () => presented)(action))));
956
+ });
957
+ const guard = settings.guard;
958
+ const action = guard === void 0 ? dispatch(useInput((run) => body({
959
+ ...marks,
960
+ input: run
961
+ }, void 0))) : Effect.gen(function* () {
962
+ const plan = yield* lane.read(name)(deadline$2(name, settings.actionTimeout, now, () => 0)(prepare)).pipe(span("Page.prepare", {}, "Debug"));
963
+ yield* guard(plan.request).pipe(Effect.mapError((reason) => undispatched(name, reason)), Effect.timeoutOrElse({
964
+ duration: settings.policyTimeout,
965
+ orElse: () => failWith(name, new PolicyTimeout({ millis: Duration.toMillis(settings.policyTimeout) }))
966
+ }), span("Page.guard"));
967
+ return yield* dispatch(useInput((run) => plan.validate.pipe(Effect.flatMap((approval) => body({
968
+ ...marks,
969
+ input: run
970
+ }, approval)))));
971
+ });
972
+ const record = info.recorded === false ? () => 0 : publish;
973
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
974
+ const exit = yield* Effect.exit(restore((page.inputGate?.check(name) ?? Effect.void).pipe(Effect.andThen(action))));
975
+ const dispatched = yield* Ref.get(sent);
976
+ const point = yield* Ref.get(at);
977
+ const { subject, to, box } = yield* Ref.get(acted);
978
+ const failure = Exit.isFailure(exit) ? Exit.findErrorOption(exit) : Option.none();
979
+ yield* Effect.annotateCurrentSpan({
980
+ dispatched,
981
+ ...queuedMillis === void 0 ? {} : { queuedMillis },
982
+ ...info.text !== void 0 && revealed ? { chars: info.text.length } : {},
983
+ ...subjectAttributes("subject", subject),
984
+ ...subjectAttributes("to", to),
985
+ ...Option.match(failure, {
986
+ onNone: () => ({}),
987
+ onSome: (error) => ({ "error.type": error.reason._tag })
988
+ })
989
+ });
990
+ record(new Action({
991
+ at: now(),
992
+ startedAt,
993
+ page: id,
994
+ name,
995
+ url: redact(page.playwright.url()),
996
+ correlation: yield* Correlation,
997
+ target: info.target,
998
+ options: Exit.isSuccess(exit) ? info.options : void 0,
999
+ subject,
1000
+ to,
1001
+ box,
1002
+ text: info.text === void 0 ? void 0 : revealed ? info.text.slice(0, 200) : redacted,
1003
+ x: Option.getOrUndefined(Option.map(point, (p) => p.x)),
1004
+ y: Option.getOrUndefined(Option.map(point, (p) => p.y)),
1005
+ ok: Exit.isSuccess(exit),
1006
+ dispatched,
1007
+ error: Option.match(failure, {
1008
+ onNone: () => Exit.hasInterrupts(exit) ? "interrupted" : void 0,
1009
+ onSome: (error) => error.message
1010
+ })
1011
+ }));
1012
+ if (Exit.isSuccess(exit)) return exit.value;
1013
+ if (dispatched) return yield* Exit.mapError(exit, (error) => error.dispatched ? error : new BrowserError({
1014
+ operation: error.operation,
1015
+ reason: error.reason,
1016
+ dispatched: true
1017
+ }));
1018
+ return yield* exit;
1019
+ })).pipe(Effect.ensuring(ticket.end));
1020
+ }).pipe(span(`Page.${name}`, info.target === void 0 ? {} : { target: info.target }), owned);
1021
+ return Object.assign(perform, { idle: Effect.suspend(() => Effect.forEach(admitted, Deferred.await, { discard: true })) });
1022
+ };
1023
+ //#endregion
1024
+ //#region src/internal/input/pointer.ts
1025
+ /**
1026
+ * The pointer's travel and the wheel, as the action's style sends them. Travel follows a motion
1027
+ * plan, published before its first move, from where viewers last saw the pointer when it is
1028
+ * shown, and a wheel scroll goes in the style's steps.
1029
+ */
1030
+ const make$13 = (page, dispatch, viewport) => {
1031
+ const { id, pointer, publish, now, span } = page;
1032
+ const { inputClocks, inputCall, dispatchMouse, sendMouse } = dispatch;
1033
+ const moveTo = (operation, marks, to, cursor, travel = "approach") => Effect.gen(function* () {
1034
+ const dragging = travel === "drag";
1035
+ const { style } = marks;
1036
+ const drawn = style.pointer ?? pointer;
1037
+ const size = yield* viewport.viewportFor(operation);
1038
+ const previous = Option.getOrElse(MutableRef.get(drawn), () => ({
1039
+ x: Math.round(size.width / 2),
1040
+ y: Math.round(size.height / 2)
1041
+ }));
1042
+ const from = {
1043
+ x: Math.max(0, Math.min(size.width - 1, previous.x)),
1044
+ y: Math.max(0, Math.min(size.height - 1, previous.y))
1045
+ };
1046
+ const planned = yield* style.glide(from, to, dragging);
1047
+ const invalid = refuse(operation, "motion must be a bounded, ordered schedule ending at the target");
1048
+ const samples = yield* Schema.decodeEffect(Plan)(planned).pipe(Effect.catch(() => invalid));
1049
+ if (samples.at(-1)?.x !== to.x || samples.at(-1)?.y !== to.y) return yield* invalid;
1050
+ yield* Effect.annotateCurrentSpan("samples", samples.length);
1051
+ yield* marks.present(samples.at(-1)?.afterMillis ?? 0);
1052
+ const run = marks.input;
1053
+ yield* inputCall(operation, run.reserveMotion(samples.length));
1054
+ MutableRef.set(pointer, Option.some(from));
1055
+ const estimate = inputClocks.get(run);
1056
+ const at = now();
1057
+ let dispatched = 0;
1058
+ let last = from;
1059
+ yield* Effect.acquireUseRelease(Effect.sync(() => publish(new TrackPlanned({
1060
+ at,
1061
+ page: id,
1062
+ from,
1063
+ samples
1064
+ }))), () => Effect.gen(function* () {
1065
+ for (const sample of samples) {
1066
+ const remaining = at + sample.afterMillis - now();
1067
+ if (remaining > 0) yield* Effect.sleep(Duration.millis(remaining));
1068
+ yield* travel === "approach" ? marks.touched : marks.sent;
1069
+ yield* inputCall(operation, run.send(() => dispatchMouse({
1070
+ type: "mouseMoved",
1071
+ x: sample.x,
1072
+ y: sample.y,
1073
+ button: dragging ? "left" : "none",
1074
+ ...dragging ? { buttons: 1 } : {}
1075
+ }, estimate, () => {
1076
+ dispatched++;
1077
+ last = {
1078
+ x: sample.x,
1079
+ y: sample.y
1080
+ };
1081
+ MutableRef.set(drawn, Option.some(last));
1082
+ if (dispatched === samples.length && cursor !== void 0) publish(new CursorChanged({
1083
+ at: now(),
1084
+ page: id,
1085
+ cursor
1086
+ }));
1087
+ })));
1088
+ }
1089
+ }), (plan) => Effect.sync(() => publish(new TrackPerformed({
1090
+ at: now(),
1091
+ page: id,
1092
+ plan,
1093
+ dispatched,
1094
+ ...last,
1095
+ complete: dispatched === samples.length
1096
+ }))));
1097
+ }).pipe(span("Page.move", {}, "Debug"));
1098
+ return {
1099
+ moveTo,
1100
+ wheel: Effect.fnUntraced(function* (operation, marks, point, dx, dy) {
1101
+ const steps = yield* marks.style.wheel(dx, dy);
1102
+ const at = now();
1103
+ yield* marks.present(steps.at(-1)?.afterMillis ?? 0);
1104
+ for (const step of steps) {
1105
+ const remaining = at + step.afterMillis - now();
1106
+ if (remaining > 0) yield* Effect.sleep(Duration.millis(remaining));
1107
+ yield* sendMouse(operation, marks.input, {
1108
+ type: "mouseWheel",
1109
+ ...point,
1110
+ deltaX: step.dx,
1111
+ deltaY: step.dy
1112
+ });
1113
+ }
1114
+ })
1115
+ };
1116
+ };
1117
+ //#endregion
1118
+ //#region src/internal/input/settle.ts
1119
+ /**
1120
+ * The wait after input: one task and one frame in the page, in one call, then a document only if
1121
+ * one committed meanwhile, until it is parsed. Chromium answers a call to a page that a link, a form
1122
+ * or a script's timer has asked to navigate only once the new document commits, or the navigation
1123
+ * stops without one, as on a 204, so the call spans the navigation the input asked for. A
1124
+ * `pushState` asks for no document, and a navigation a handler starts after a fetch answers is the
1125
+ * next look's to see. A tab the input opened waits for the browser's registration too.
1126
+ */
1127
+ const settling = Duration.seconds(5);
1128
+ const make$12 = (page, bridge) => {
1129
+ const document = () => bridge.frameTag().document;
1130
+ let parsed = -1;
1131
+ let changed = Deferred.makeUnsafe();
1132
+ page.cdp.on("Page.domContentEventFired", () => {
1133
+ parsed = document();
1134
+ Deferred.doneUnsafe(changed, Exit.void);
1135
+ changed = Deferred.makeUnsafe();
1136
+ });
1137
+ /** The page's document before input, which `settle` measures from. */
1138
+ const mark = () => ({
1139
+ document: document(),
1140
+ openings: page.openings.mark()
1141
+ });
1142
+ /** Wait for the page to settle after input sent since `before`. */
1143
+ const settle = (operation, before) => Effect.gen(function* () {
1144
+ yield* bridge.evaluate(operation, scriptCall("settle")).pipe(Effect.provideService(Current, void 0), Effect.catchIf(contextGone, () => Effect.void));
1145
+ while (document() > before.document && parsed < document()) yield* Deferred.await(changed);
1146
+ }).pipe(Effect.timeoutOrElse({
1147
+ duration: settling,
1148
+ orElse: () => Effect.fail("unsettled")
1149
+ }), Effect.catch(() => page.stopLoading.pipe(Effect.andThen(Effect.annotateCurrentSpan("settled", false)))), Effect.andThen(page.openings.settle(before.openings)), page.span("Page.settle", {}, "Debug"));
1150
+ return {
1151
+ mark,
1152
+ settle
1153
+ };
1154
+ };
1155
+ //#endregion
1156
+ //#region src/internal/input/targets.ts
1157
+ /**
1158
+ * Where an action's input goes: a ref or a point resolved to a control and a viewport point,
1159
+ * scrolled into view first when it is outside the viewport: at once, or with the wheel where
1160
+ * viewers watch.
1161
+ */
1162
+ 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 }));
1163
+ const resolvedTarget = ({ x, y, box, ...target }) => new ResolvedTarget({
1164
+ point: {
1165
+ x,
1166
+ y
1167
+ },
1168
+ box,
1169
+ ...target
1170
+ });
1171
+ const make$11 = (viewport, guard, pointer, dispatch, settling) => {
1172
+ const { evaluate, mutate } = guard;
1173
+ const { moveTo, wheel } = pointer;
1174
+ const { flush } = dispatch;
1175
+ const readPoint = (operation, target, approval, scroll) => evaluate(operation, scriptCall("point", target, scroll), approval).pipe(Effect.flatMap(decodeWith(operation, PointResultSchema)));
1176
+ const resolve = (operation, target, approval, scroll = true) => {
1177
+ if (typeof target === "string" && !/^e\d+$/.test(target)) return refuse(operation, target + " is not a ref; refs look like e12");
1178
+ if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y))) return refuse(operation, "point coordinates must be finite");
1179
+ return readPoint(operation, target, approval, scroll).pipe(Effect.flatMap((result) => "error" in result ? pointFailure(operation, target, result) : Effect.succeed(resolvedTarget(result))));
1180
+ };
1181
+ return {
1182
+ resolve,
1183
+ targetFor: Effect.fnUntraced(function* (operation, target, approval, marks) {
1184
+ if (!marks.style.shown || typeof target !== "string") return yield* resolve(operation, target, approval);
1185
+ for (let attempt = 0; attempt < 3; attempt++) {
1186
+ const result = yield* readPoint(operation, target, approval, false);
1187
+ if (!("error" in result)) return resolvedTarget(result);
1188
+ if (result.error !== "offscreen") return yield* pointFailure(operation, target, result);
1189
+ const plan = yield* mutate(operation, scriptCall("scrollPlan", target), approval).pipe(Effect.flatMap(decodeWith(operation, ScrollPlanSchema)));
1190
+ if (plan === null) break;
1191
+ const size = yield* viewport.viewportFor(operation);
1192
+ const point = {
1193
+ x: plan.x,
1194
+ y: plan.y
1195
+ };
1196
+ yield* marks.at(point);
1197
+ yield* moveTo(operation, marks, point);
1198
+ const before = settling.mark();
1199
+ 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)));
1200
+ yield* flush(operation, marks.input);
1201
+ yield* settling.settle(operation, before);
1202
+ yield* check(approval);
1203
+ }
1204
+ yield* marks.touched;
1205
+ const before = settling.mark();
1206
+ yield* resolve(operation, target, approval);
1207
+ yield* settling.settle(operation, before);
1208
+ yield* check(approval);
1209
+ return yield* resolve(operation, target, approval, false);
1210
+ })
1211
+ };
1212
+ };
1213
+ //#endregion
1214
+ //#region src/internal/input/actions.ts
1215
+ /**
1216
+ * The page's actions: click, hover, drag, type, press, scroll and select, each sent in a style,
1217
+ * plainly or performed for viewers, and the aim a performed click starts early. Each runs through
1218
+ * `perform`, which admits it, puts it to the input guard and records it. A click, a key or a scroll
1219
+ * then waits for the page to settle: a task and a frame, and a document if the input asked for one.
1220
+ */
1221
+ const partsOf = (page, bridge, viewport) => {
1222
+ const dispatch = make$17(page);
1223
+ const pointer = make$13(page, dispatch, viewport);
1224
+ const guard = make$16(page, bridge);
1225
+ const settling = make$12(page, bridge);
1226
+ return {
1227
+ page,
1228
+ ...settling,
1229
+ viewportFor: viewport.viewportFor,
1230
+ perform: make$14(page, dispatch),
1231
+ ...guard,
1232
+ ...make$11(viewport, guard, pointer, dispatch, settling),
1233
+ ...pointer,
1234
+ ...make$15(page, bridge, dispatch),
1235
+ sendMouse: dispatch.sendMouse,
1236
+ flush: dispatch.flush
1237
+ };
1238
+ };
1239
+ /** Whether every number given is finite; a number left out is. */
1240
+ const finite = (...values) => values.every((value) => value === void 0 || Number.isFinite(value));
1241
+ const named = (target) => typeof target === "string" ? target : `${target.x},${target.y}`;
1242
+ /** The ref a target names, if it names one rather than a point. */
1243
+ const refOf = (target) => typeof target === "string" ? target : void 0;
1244
+ /** A click's options, once its counts are finite. */
1245
+ const clickable = (options) => finite(options.clickCount, options.holdMillis) ? Effect.void : refuse("click", "clickCount and holdMillis must be finite");
1246
+ /** The keys a press sends, once they and its options are valid. */
1247
+ const pressed = (keys, options) => {
1248
+ const parts$1 = parts(keys);
1249
+ if (!finite(options.times, options.holdMillis)) return refuse("press", "times and holdMillis must be finite");
1250
+ if (parts$1 === void 0) return refuse("press", `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`);
1251
+ return Effect.succeed(parts$1);
1252
+ };
1253
+ const click = (input) => {
1254
+ const { settings } = input.page;
1255
+ const { perform, preparePolicy, targetFor, moveTo, sendMouse, flush, mark, settle } = input;
1256
+ return (target, clickOptions = {}, style = plain) => perform("click", {
1257
+ target: named(target),
1258
+ options: {
1259
+ button: clickOptions.button,
1260
+ clickCount: clickOptions.clickCount,
1261
+ holdMillis: clickOptions.holdMillis
1262
+ }
1263
+ }, settings.actionTimeout, Effect.suspend(() => clickable(clickOptions)).pipe(Effect.andThen(preparePolicy("click", { target: refOf(target) }, [target]))), (marks, approval) => Effect.gen(function* () {
1264
+ yield* clickable(clickOptions);
1265
+ const resolved = yield* targetFor("click", target, approval, marks);
1266
+ const { point } = resolved;
1267
+ const button = clickOptions.button ?? "left";
1268
+ const count = Math.max(1, Math.min(3, clickOptions.clickCount ?? 1));
1269
+ yield* marks.at(point);
1270
+ yield* marks.on(resolved);
1271
+ yield* moveTo("click", marks, point, resolved.cursor);
1272
+ yield* check(approval, { presses: [{
1273
+ index: 0,
1274
+ ...point
1275
+ }] });
1276
+ const before = mark();
1277
+ yield* marks.sent;
1278
+ for (let index = 1; index <= count; index++) {
1279
+ if (index > 1) yield* check(approval, { presses: [{
1280
+ index: 0,
1281
+ ...point
1282
+ }] }, 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" }))));
1283
+ yield* sendMouse("click", marks.input, buttonAt("mousePressed", point, button, index));
1284
+ const hold = clickOptions.holdMillis ?? (yield* marks.style.hold);
1285
+ if (clickOptions.holdMillis === void 0) yield* marks.present(hold);
1286
+ if (hold > 0) yield* Effect.sleep(Duration.millis(hold));
1287
+ yield* sendMouse("click", marks.input, buttonAt("mouseReleased", point, button, index));
1288
+ }
1289
+ yield* flush("click", marks.input);
1290
+ yield* settle("click", before);
1291
+ return resolved;
1292
+ }), style);
1293
+ };
1294
+ const hover = (input) => {
1295
+ const { settings } = input.page;
1296
+ const { perform, preparePolicy, targetFor, moveTo, flush } = input;
1297
+ return (target, style = plain) => perform("hover", { target: named(target) }, settings.actionTimeout, preparePolicy("hover", { target: refOf(target) }, [target]), (marks, approval) => Effect.gen(function* () {
1298
+ const resolved = yield* targetFor("hover", target, approval, marks);
1299
+ const { point } = resolved;
1300
+ yield* marks.at(point);
1301
+ yield* marks.on(resolved);
1302
+ yield* moveTo("hover", marks, point, resolved.cursor, "hover");
1303
+ yield* flush("hover", marks.input);
1304
+ }), style);
1305
+ };
1306
+ /**
1307
+ * The pointer's travel to a target, before its action is asked: no guard holds it, no action is
1308
+ * recorded, and nothing is pressed. A guarded page aims nothing, as its input waits for approval.
1309
+ */
1310
+ const aim = (input) => {
1311
+ const { settings } = input.page;
1312
+ const { perform, targetFor, moveTo, flush } = input;
1313
+ return (target, style) => settings.guard === void 0 ? perform("aim", {
1314
+ target: named(target),
1315
+ recorded: false
1316
+ }, settings.actionTimeout, Effect.die("an aim is never put to a guard"), (marks) => targetFor("aim", target, void 0, marks).pipe(Effect.flatMap(({ point, cursor }) => moveTo("aim", marks, point, cursor)), Effect.andThen(flush("aim", marks.input))), style) : Effect.void;
1317
+ };
1318
+ const drag = (input) => {
1319
+ const { settings } = input.page;
1320
+ const { perform, preparePolicy, targetFor, resolve, moveTo, sendMouse, flush } = input;
1321
+ return (from, to, style = plain) => perform("drag", { target: `${JSON.stringify(from)} -> ${JSON.stringify(to)}` }, settings.actionTimeout, preparePolicy("drag", {}, [from, to]), (marks, approval) => Effect.gen(function* () {
1322
+ yield* targetFor("drag", from, approval, marks);
1323
+ yield* targetFor("drag", to, approval, marks);
1324
+ const start = yield* resolve("drag", from, approval, false);
1325
+ const end = yield* resolve("drag", to, approval, false);
1326
+ yield* marks.at(end.point);
1327
+ yield* marks.on(start, end);
1328
+ yield* moveTo("drag", marks, start.point, start.cursor);
1329
+ yield* check(approval, { presses: [{
1330
+ index: 0,
1331
+ ...start.point
1332
+ }, {
1333
+ index: 1,
1334
+ ...end.point
1335
+ }] });
1336
+ yield* marks.sent;
1337
+ yield* sendMouse("drag", marks.input, buttonAt("mousePressed", start.point));
1338
+ yield* moveTo("drag", marks, end.point, end.cursor, "drag");
1339
+ yield* sendMouse("drag", marks.input, buttonAt("mouseReleased", end.point));
1340
+ yield* flush("drag", marks.input);
1341
+ }), style);
1342
+ };
1343
+ const typeText = (input) => {
1344
+ const { settings, now } = input.page;
1345
+ const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, evaluate } = input;
1346
+ const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
1347
+ return (text, typeOptions = {}, style = plain) => perform("type", {
1348
+ target: typeOptions.into,
1349
+ options: {
1350
+ replace: typeOptions.into === void 0 ? false : typeOptions.replace,
1351
+ submit: typeOptions.submit
1352
+ },
1353
+ text,
1354
+ secret: true
1355
+ }, settings.actionTimeout, preparePolicy("type", {
1356
+ target: typeOptions.into,
1357
+ text
1358
+ }, [typeOptions.into ?? null], { submit: typeOptions.submit ?? false }), (marks, approval) => Effect.gen(function* () {
1359
+ const replace = typeOptions.replace ?? true;
1360
+ const into = typeOptions.into;
1361
+ if (into !== void 0 && !/^e\d+$/.test(into)) return yield* refuse("type", `"${into}" is not a ref; refs look like e12`);
1362
+ const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
1363
+ const typeable = yield* evaluate("type", asked, approval).pipe(Effect.flatMap(decodeWith("type", TypeableResultSchema)));
1364
+ if ("error" in typeable) return yield* failWith("type", typeable.error === "stale" && into !== void 0 ? new StaleRef({ ref: into }) : new NotActionable({ detail: typeable.detail }));
1365
+ if (typeable.subject !== void 0) yield* marks.on(typeable.subject);
1366
+ const since = yield* currentDocument("type");
1367
+ let secret = typeable.secret;
1368
+ if (into !== void 0) {
1369
+ const target = yield* targetFor("type", into, approval, marks);
1370
+ yield* marks.at(target.point);
1371
+ yield* marks.on(target);
1372
+ if (marks.style.shown) {
1373
+ yield* moveTo("type", marks, target.point, target.cursor);
1374
+ yield* check(approval, { presses: [{
1375
+ index: 0,
1376
+ ...target.point
1377
+ }] });
1378
+ yield* marks.sent;
1379
+ for (const type of ["mousePressed", "mouseReleased"]) yield* sendMouse("type", marks.input, buttonAt(type, target.point));
1380
+ yield* flush("type", marks.input);
1381
+ }
1382
+ yield* marks.sent;
1383
+ const follow = marks.style.shown && approval === void 0 && !secret && target.name !== "" ? target.name : null;
1384
+ const focused = yield* mutate("type", scriptCall("focus", into, replace, follow), approval).pipe(Effect.flatMap(decodeWith("type", FocusResultSchema)));
1385
+ if ("error" in focused) return yield* editFailure("type", into, focused);
1386
+ secret ||= focused.secret;
1387
+ }
1388
+ if (!secret) yield* marks.reveal;
1389
+ yield* check(approval, { focused: true });
1390
+ yield* marks.sent;
1391
+ const focusHeld = check(approval, { focused: true }, flush("type", marks.input));
1392
+ if (text === "" && replace && into !== void 0) {
1393
+ yield* sameDocument("type", since);
1394
+ yield* keyStroke("type", marks.input, ["Delete"]);
1395
+ } else {
1396
+ const events = yield* marks.style.typing(text, approval !== void 0);
1397
+ const keyed = events.some((event) => event.phase === "down");
1398
+ const checks = approval !== void 0 && keyed ? (text.match(/\s/gu)?.length ?? 0) + 1 : 0;
1399
+ const started = now();
1400
+ yield* marks.present((events.at(-1)?.afterMillis ?? 0) + checks * 250);
1401
+ for (const event of events) {
1402
+ const remaining = started + event.afterMillis - now();
1403
+ if (remaining > 0) yield* Effect.sleep(Duration.millis(remaining));
1404
+ if (event.phase === "down" && /\s/u.test(event.key)) yield* focusHeld;
1405
+ if (event.phase !== "up") yield* sameDocument("type", since);
1406
+ yield* typeEvent(marks.input, event, secret);
1407
+ }
1408
+ if (keyed) yield* focusHeld;
1409
+ }
1410
+ yield* flush("type", marks.input);
1411
+ if (typeOptions.submit === true) {
1412
+ yield* check(approval, { focused: true });
1413
+ yield* sameDocument("type", since);
1414
+ const before = mark();
1415
+ yield* keyStroke("type", marks.input, ["Enter"]);
1416
+ yield* flush("type", marks.input);
1417
+ yield* settle("type", before);
1418
+ }
1419
+ }), style);
1420
+ };
1421
+ const press = (input) => {
1422
+ const { settings, now } = input.page;
1423
+ const { perform, preparePolicy, currentDocument, sameDocument, keyStroke, flush } = input;
1424
+ const { mark, settle, targetFor, mutate } = input;
1425
+ return (keys, pressOptions = {}, style = plain) => perform("press", {
1426
+ target: keys,
1427
+ options: {
1428
+ times: pressOptions.times,
1429
+ holdMillis: pressOptions.holdMillis
1430
+ }
1431
+ }, settings.actionTimeout, Effect.suspend(() => pressed(keys, pressOptions)).pipe(Effect.flatMap((parts) => preparePolicy("press", {
1432
+ target: pressOptions.on,
1433
+ text: parts.join("+")
1434
+ }, [pressOptions.on ?? null], { keys: parts.join("+") }))), (marks, approval) => Effect.gen(function* () {
1435
+ const parts = yield* pressed(keys, pressOptions);
1436
+ const times = Math.max(1, Math.min(50, pressOptions.times ?? 1));
1437
+ const hold = pressOptions.holdMillis ?? 0;
1438
+ const activates = parts.at(-1) === "Enter" || parts.at(-1) === "Space";
1439
+ const since = yield* currentDocument("press");
1440
+ const on = pressOptions.on;
1441
+ if (on !== void 0) {
1442
+ yield* marks.on(yield* targetFor("press", on, approval, marks));
1443
+ const focused = yield* mutate("press", scriptCall("focusOn", on), approval).pipe(Effect.flatMap(decodeWith("press", EditResultSchema)));
1444
+ if ("error" in focused) return yield* editFailure("press", on, focused);
1445
+ yield* check(approval, { focused: true });
1446
+ }
1447
+ const before = mark();
1448
+ let due = now();
1449
+ yield* marks.sent;
1450
+ for (let index = 0; index < times; index++) {
1451
+ if (index > 0 && activates) yield* check(approval, { focused: true }, flush("press", marks.input));
1452
+ yield* sameDocument("press", since);
1453
+ yield* keyStroke("press", marks.input, parts, hold);
1454
+ if (index + 1 < times) {
1455
+ const gap = yield* marks.style.keyGap;
1456
+ yield* marks.present(gap);
1457
+ due += hold + gap;
1458
+ if (due > now()) yield* Effect.sleep(Duration.millis(due - now()));
1459
+ }
1460
+ }
1461
+ yield* flush("press", marks.input);
1462
+ yield* settle("press", before);
1463
+ }), style);
1464
+ };
1465
+ const scroll = (input) => {
1466
+ const { settings } = input.page;
1467
+ const { perform, preparePolicy, viewportFor, targetFor, resolve, moveTo, wheel, flush } = input;
1468
+ const { mark, settle } = input;
1469
+ return (scrollOptions = {}, style = plain) => {
1470
+ const target = scrollOptions.at;
1471
+ const valid = Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0) ? Effect.void : refuse("scroll", "scroll deltas must be finite");
1472
+ return perform("scroll", {
1473
+ target: target === void 0 ? void 0 : named(target),
1474
+ options: {
1475
+ dx: scrollOptions.dx,
1476
+ dy: scrollOptions.dy
1477
+ }
1478
+ }, settings.actionTimeout, valid.pipe(Effect.andThen(preparePolicy("scroll", { target: refOf(target) }, target === void 0 ? [] : [target]))), (marks, approval) => Effect.gen(function* () {
1479
+ yield* valid;
1480
+ const viewport = yield* viewportFor("scroll");
1481
+ const middle = {
1482
+ x: Math.round(viewport.width / 2),
1483
+ y: Math.round(viewport.height / 2)
1484
+ };
1485
+ const dx = scrollOptions.dx ?? 0;
1486
+ const dy = scrollOptions.dy ?? (scrollOptions.dx === void 0 ? Math.round(viewport.height * .8) : 0);
1487
+ const resolved = target !== void 0 ? yield* targetFor("scroll", target, approval, marks) : marks.style.shown ? yield* resolve("scroll", middle, approval).pipe(Effect.orElseSucceed(() => void 0)) : void 0;
1488
+ const point = resolved?.point ?? middle;
1489
+ yield* marks.at(point);
1490
+ if (target !== void 0 && resolved !== void 0) yield* marks.on(resolved);
1491
+ yield* moveTo("scroll", marks, point, resolved?.cursor);
1492
+ const before = mark();
1493
+ yield* marks.sent;
1494
+ yield* wheel("scroll", marks, point, dx, dy);
1495
+ yield* flush("scroll", marks.input);
1496
+ yield* settle("scroll", before);
1497
+ }), style);
1498
+ };
1499
+ };
1500
+ const select = (input) => {
1501
+ const { settings } = input.page;
1502
+ const { perform, preparePolicy, targetFor, mutate } = input;
1503
+ return (ref, values) => perform("select", {
1504
+ target: ref,
1505
+ options: { values },
1506
+ text: values.join(", ")
1507
+ }, settings.actionTimeout, preparePolicy("select", {
1508
+ target: ref,
1509
+ text: values.join(", ")
1510
+ }, [ref]), (marks, approval) => Effect.gen(function* () {
1511
+ yield* marks.on(yield* targetFor("select", ref, approval, marks));
1512
+ const result = yield* mutate("select", scriptCall("select", ref, values), approval, marks.sent).pipe(Effect.flatMap((value) => decodeWith("select", EditResultSchema)(value).pipe(Effect.tapError(() => marks.sent))));
1513
+ if ("error" in result) return yield* editFailure("select", ref, result);
1514
+ yield* marks.sent;
1515
+ return result.detail;
1516
+ }));
1517
+ };
1518
+ const make$10 = (page, bridge, viewport) => {
1519
+ const parts = partsOf(page, bridge, viewport);
1520
+ return {
1521
+ perform: parts.perform,
1522
+ preparePolicy: parts.preparePolicy,
1523
+ click: click(parts),
1524
+ hover: hover(parts),
1525
+ aim: aim(parts),
1526
+ drag: drag(parts),
1527
+ type: typeText(parts),
1528
+ press: press(parts),
1529
+ scroll: scroll(parts),
1530
+ select: select(parts)
1531
+ };
1532
+ };
1533
+ //#endregion
1534
+ //#region src/internal/pictures/counts.ts
1535
+ /** Bounded capture-loss and paint-gap statistics on the owning host clock. */
1536
+ const paintGaps = () => {
1537
+ let count = 0;
1538
+ let totalMillis = 0;
1539
+ let minMillis = null;
1540
+ let maxMillis = null;
1541
+ let lastMillis = null;
1542
+ return {
1543
+ add: (gap) => {
1544
+ count++;
1545
+ totalMillis += gap;
1546
+ minMillis = minMillis === null ? gap : Math.min(minMillis, gap);
1547
+ maxMillis = maxMillis === null ? gap : Math.max(maxMillis, gap);
1548
+ lastMillis = gap;
1549
+ },
1550
+ snapshot: () => ({
1551
+ count,
1552
+ totalMillis,
1553
+ minMillis,
1554
+ maxMillis,
1555
+ lastMillis
1556
+ })
1557
+ };
1558
+ };
1559
+ const none = () => ({
1560
+ received: 0,
1561
+ accepted: 0,
1562
+ late: 0,
1563
+ missingTimestamp: 0,
1564
+ foreignSize: 0,
1565
+ duringPictures: 0,
1566
+ lost: 0
1567
+ });
1568
+ /** How far back a window of capture counts can reach. */
1569
+ const countsKept = Duration.minutes(1);
1570
+ /**
1571
+ * A capture's counts and paint gaps over its page's life, and each with when it happened on the
1572
+ * owner's clock for the latest minute, so a window of them can be read.
1573
+ */
1574
+ const tally = (now) => {
1575
+ const totals = none();
1576
+ const gaps = paintGaps();
1577
+ let recent = [];
1578
+ let oldest = 0;
1579
+ const note = (counted, n) => {
1580
+ const at = now();
1581
+ recent.push({
1582
+ at,
1583
+ counted,
1584
+ n
1585
+ });
1586
+ while ((recent[oldest]?.at ?? at) < at - Duration.toMillis(countsKept)) oldest++;
1587
+ if (oldest > recent.length / 2) {
1588
+ recent = recent.slice(oldest);
1589
+ oldest = 0;
1590
+ }
1591
+ };
1592
+ return {
1593
+ count: (counted, n = 1) => {
1594
+ totals[counted] += n;
1595
+ note(counted, n);
1596
+ },
1597
+ gap: (millis) => {
1598
+ gaps.add(millis);
1599
+ note("gap", millis);
1600
+ },
1601
+ /** Lifetime counts, or those of the latest `windowMillis`, at most a minute. */
1602
+ read: (windowMillis) => {
1603
+ if (windowMillis === void 0) return {
1604
+ ...totals,
1605
+ gaps: gaps.snapshot()
1606
+ };
1607
+ const counts = none();
1608
+ const windowed = paintGaps();
1609
+ const since = now() - windowMillis;
1610
+ for (const { at, counted, n } of recent.slice(oldest)) {
1611
+ if (at < since) continue;
1612
+ if (counted === "gap") windowed.add(n);
1613
+ else counts[counted] += n;
1614
+ }
1615
+ return {
1616
+ ...counts,
1617
+ gaps: windowed.snapshot()
1618
+ };
1619
+ }
1620
+ };
1621
+ };
1622
+ //#endregion
1623
+ //#region src/internal/pictures/quiescence.ts
1624
+ /** Actual native replies outlive callers; bounded transactions retain them without holding teardown. */
1625
+ const deadline$1 = Duration.seconds(2);
1626
+ const make$9 = (options) => {
1627
+ const replies = make$19();
1628
+ const pending = /* @__PURE__ */ new Map();
1629
+ const late = (kind) => options.error(/* @__PURE__ */ new Error(`${kind} exceeded its deadline`));
1630
+ const track = (operation, actual, failed) => {
1631
+ const reply = actual.then(() => void 0, failed);
1632
+ pending.set(operation, reply);
1633
+ return reply.finally(() => {
1634
+ if (pending.get(operation) === reply) pending.delete(operation);
1635
+ });
1636
+ };
1637
+ 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({
1638
+ duration: deadline$1,
1639
+ orElse: () => Effect.fail(late(operation ?? pending.keys().next().value ?? "picture restoration"))
1640
+ }), Effect.provideService(Clock.Clock, options.clock));
1641
+ const excluding = (picture) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1642
+ yield* restore(wait());
1643
+ if (options.closed()) return yield* options.error(/* @__PURE__ */ new Error("Target page has been closed"));
1644
+ const current = options.current();
1645
+ const run = yield* replies.begin;
1646
+ const result = yield* restore(options.pause(current).pipe(Effect.andThen(picture), Effect.provideService(Current, run))).pipe(Effect.exit);
1647
+ const cleaned = yield* wait().pipe(Effect.andThen(options.resume(current)), Effect.timeoutOrElse({
1648
+ duration: deadline$1,
1649
+ orElse: () => Effect.fail(late("picture restoration"))
1650
+ }), Effect.provideService(Clock.Clock, options.clock), Effect.exit, Effect.forkChild, Effect.flatMap(Fiber.join));
1651
+ if (Exit.isFailure(cleaned)) options.fail(current, options.error(Cause.squash(cleaned.cause)));
1652
+ if (Exit.isFailure(result)) return yield* Effect.failCause(result.cause);
1653
+ if (Exit.isFailure(cleaned)) return yield* Effect.failCause(cleaned.cause);
1654
+ return result.value;
1655
+ }));
1656
+ return {
1657
+ excluding,
1658
+ idle: wait(),
1659
+ wait,
1660
+ track
1661
+ };
1662
+ };
1663
+ //#endregion
1664
+ //#region src/internal/pictures/capture.ts
1665
+ /**
1666
+ * One CDP screencast per page, with bounded transport replies and per-reader loss accounting.
1667
+ * Native callbacks own synchronous bookkeeping; the page scope owns startup and teardown.
1668
+ */
1669
+ const replyCapacity = 32;
1670
+ const subscriberCapacity = 16;
1671
+ const deadline = Duration.seconds(2);
1672
+ const sameSize = (left, right) => Math.abs(left.width - right.width) < 1 && Math.abs(left.height - right.height) < 1;
1673
+ const sameShape = (image, device) => Math.abs(image.width * device.height - image.height * device.width) < device.width + device.height;
1674
+ const outlasting = Duration.seconds(1);
1675
+ /**
1676
+ * A native frame as readers get it: its picture's size, else the capture's or its device's, and
1677
+ * its paint mapped by the browser's newest estimate, which may be older than the frame and says
1678
+ * less about later paint.
1679
+ */
1680
+ const timed = (options, native, timestamp, generation) => {
1681
+ const data = Buffer.from(native.data, "base64");
1682
+ const image = options.imageSize(data);
1683
+ const device = {
1684
+ width: native.metadata.deviceWidth,
1685
+ height: native.metadata.deviceHeight
1686
+ };
1687
+ const size = image ?? generation.size ?? device;
1688
+ const estimate = options.latest() ?? generation.calibration;
1689
+ const hostTime = toHostTime(estimate, timestamp);
1690
+ return {
1691
+ frame: new Frame({
1692
+ page: options.id,
1693
+ data,
1694
+ timing: new BrowserPaint({
1695
+ timestamp,
1696
+ hostTime,
1697
+ uncertaintyMillis: uncertaintyAt(estimate, hostTime)
1698
+ }),
1699
+ receivedAt: monotonicMillis(options.clock),
1700
+ width: size.width,
1701
+ height: size.height,
1702
+ ...generation.transport.frameTag()
1703
+ }),
1704
+ image,
1705
+ device
1706
+ };
1707
+ };
1708
+ const readViewport = (options) => options.viewport.pipe(Effect.interruptible, Effect.timeoutOrElse({
1709
+ duration: deadline,
1710
+ orElse: () => Effect.succeed(null)
1711
+ }), Effect.orElseSucceed(() => null), Effect.provideService(Clock.Clock, options.clock));
1712
+ /**
1713
+ * What a new capture starts with: where it runs, the clock's estimate, and the viewport unless
1714
+ * the reader gave a size. A capture connection attaches while the page's own session maps the
1715
+ * clock, and a page too busy to report its viewport is still captured, at device size.
1716
+ */
1717
+ const prepare = (options, screencast) => Effect.all([options.transport.pipe(Effect.interruptible), Effect.gen(function* () {
1718
+ return {
1719
+ calibration: yield* options.calibrate.pipe(Effect.interruptible),
1720
+ viewport: screencast.size === void 0 ? yield* readViewport(options) : null
1721
+ };
1722
+ })], { concurrency: 2 });
1723
+ /**
1724
+ * Keeps other captures' frames from readers. A clipped or scaled screenshot of the page, taken
1725
+ * from any session, draws frames into its running screencast without resizing the page, until
1726
+ * Chromium returns to the page's size. Either their picture has another shape than their device,
1727
+ * and they are dropped, or their device has another size than the page's frames.
1728
+ *
1729
+ * A frame of the expected device size goes to readers at once and drops any run of frames of
1730
+ * another size before it, as another capture's. A run waits, in order, until the page reports a
1731
+ * viewport of its size or until it outlasts any capture: then it was a real resize, so it goes to
1732
+ * readers and its size is expected. The page's report only shortens the wait, so a page too busy
1733
+ * to answer, or one measuring in other units as under browser zoom, never stalls the stream.
1734
+ */
1735
+ const sizeFilter = (options, deliver, drop) => Effect.gen(function* () {
1736
+ let expected = null;
1737
+ let run;
1738
+ const wake = yield* Queue.sliding(1);
1739
+ const end = () => {
1740
+ if (run === void 0) return;
1741
+ drop(run.frames.length);
1742
+ run = void 0;
1743
+ Queue.offerUnsafe(wake, void 0);
1744
+ };
1745
+ const now = () => monotonicMillis(options.clock);
1746
+ const adopt = (adopted) => {
1747
+ run = void 0;
1748
+ expected = adopted.device;
1749
+ for (const held of adopted.frames) deliver(held.frame, held.timestamp);
1750
+ };
1751
+ const decide = Effect.suspend(() => {
1752
+ const deciding = run;
1753
+ if (deciding === void 0) return Effect.void;
1754
+ return readViewport(options).pipe(Effect.flatMap((viewport) => {
1755
+ if (run !== deciding) return decide;
1756
+ if (viewport !== null && sameSize(viewport, deciding.device)) return Effect.sync(() => adopt(deciding));
1757
+ return outlast(deciding);
1758
+ }));
1759
+ });
1760
+ const outlast = (waiting) => Effect.suspend(() => {
1761
+ if (run !== waiting) return decide;
1762
+ const remaining = waiting.since + Duration.toMillis(outlasting) - now();
1763
+ if (remaining <= 0) return Effect.sync(() => adopt(waiting));
1764
+ return Effect.sleep(Duration.millis(remaining)).pipe(Effect.provideService(Clock.Clock, options.clock), Effect.race(Queue.take(wake)), Effect.andThen(outlast(waiting)));
1765
+ });
1766
+ yield* Queue.take(wake).pipe(Effect.andThen(decide), Effect.forever, Effect.forkScoped);
1767
+ return {
1768
+ /** Drops the frames still waiting; their capture has stopped. */
1769
+ end,
1770
+ /** A new capture expects the viewport it read, if any. */
1771
+ restart: (viewport) => {
1772
+ end();
1773
+ expected = viewport;
1774
+ },
1775
+ /** Whether a frame of this device size shows the page, as far as the capture knows. */
1776
+ offer: (frame, timestamp, image, device) => {
1777
+ if (image !== void 0 && !sameShape(image, device)) {
1778
+ drop(1);
1779
+ return;
1780
+ }
1781
+ expected ??= device;
1782
+ if (sameSize(device, expected)) {
1783
+ end();
1784
+ deliver(frame, timestamp);
1785
+ return;
1786
+ }
1787
+ if (run !== void 0 && !sameSize(run.device, device)) end();
1788
+ if (run === void 0) {
1789
+ run = {
1790
+ device,
1791
+ since: now(),
1792
+ frames: []
1793
+ };
1794
+ Queue.offerUnsafe(wake, void 0);
1795
+ }
1796
+ run.frames.push({
1797
+ frame,
1798
+ timestamp
1799
+ });
1800
+ if (run.frames.length > subscriberCapacity) {
1801
+ run.frames.shift();
1802
+ drop(1);
1803
+ }
1804
+ }
1805
+ };
1806
+ });
1807
+ /**
1808
+ * A running capture's failure, told once its page's fate is known. A capture can fail before the
1809
+ * page's own session hears why: a capture connection hears the browser's end before the connection
1810
+ * that drives the page does, and an acknowledgement can be refused before the browser's loss is
1811
+ * known. So the failure waits for the page's scope to close, `closed`, and is then the page's own
1812
+ * end or its browser's loss, as `gone` reads it; or, if the page still stands after `deadline`,
1813
+ * the capture's own.
1814
+ */
1815
+ const told = (closed, gone, clock) => (failure) => Deferred.await(closed).pipe(Effect.map(gone), Effect.timeoutOrElse({
1816
+ duration: deadline,
1817
+ orElse: () => Effect.succeed(failure)
1818
+ }), Effect.provideService(Clock.Clock, clock), Effect.flatMap(Effect.fail));
1819
+ /**
1820
+ * Once every frame the running capture's transport sent before now has arrived; a capture that
1821
+ * has failed, or whose round trip fails, fails as `why` tells it.
1822
+ */
1823
+ const synced = (current, why, error) => {
1824
+ if (current === void 0) return Effect.void;
1825
+ if (current.failure !== void 0) return why(current.failure);
1826
+ return Effect.tryPromise({
1827
+ try: () => current.transport.sync(),
1828
+ catch: error
1829
+ }).pipe(Effect.catch(why), Effect.asVoid);
1830
+ };
1831
+ const confirmation = (read, current, why, error) => (frame) => Effect.suspend(() => {
1832
+ const { generation } = read();
1833
+ if (generation === void 0) return Effect.succeed(false);
1834
+ return synced(generation, why, error).pipe(Effect.map(() => {
1835
+ const after = read();
1836
+ return after.generation === generation && after.replies === 0 && current(frame);
1837
+ }));
1838
+ });
1839
+ 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;
1840
+ const currentFrame = (generation, latest) => Option.getOrUndefined(Option.filter(latest, (frame) => isCurrent(frame, generation, latest)));
1841
+ /** One reader counts its own missed envelopes without hiding gaps inside stream chunks. */
1842
+ const frames = (subscription, baseline, why, missed) => {
1843
+ let previous = baseline;
1844
+ return Stream.fromEffectRepeat(Queue.take(subscription).pipe(Effect.flatMap((envelope) => {
1845
+ if (envelope._tag === "Failure") return why(envelope.error);
1846
+ return Effect.sync(() => {
1847
+ if (envelope.sequence > previous + 1) missed(envelope.sequence - previous - 1);
1848
+ previous = envelope.sequence;
1849
+ return envelope.frame;
1850
+ });
1851
+ })));
1852
+ };
1853
+ const renewals = Effect.fnUntraced(function* (options) {
1854
+ const renewing = yield* Queue.sliding(1);
1855
+ 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);
1856
+ return renewing;
1857
+ });
1858
+ const receive = (options, current, replies, counts, sizes, renewing, fail) => (native) => {
1859
+ if (current.phase === "Stopped") return;
1860
+ counts.count("received");
1861
+ if (replies.size >= replyCapacity) {
1862
+ fail(current, options.error(/* @__PURE__ */ new Error("screencast acknowledgement capacity exceeded")));
1863
+ return;
1864
+ }
1865
+ const reply = current.transport.acknowledge(native.sessionId).then(() => void 0, (cause) => {
1866
+ if (current.phase !== "Stopped") fail(current, options.error(cause));
1867
+ }).then(() => void replies.delete(reply));
1868
+ replies.add(reply);
1869
+ const browserSeconds = native.metadata.timestamp;
1870
+ const timestamp = browserSeconds === void 0 ? NaN : browserSeconds * 1e3;
1871
+ if (!Number.isFinite(timestamp)) return counts.count("missingTimestamp");
1872
+ if (current.predecessor !== void 0 && timestamp <= current.predecessor) return counts.count("late");
1873
+ Queue.offerUnsafe(renewing, void 0);
1874
+ const { frame, image, device } = timed(options, native, timestamp, current);
1875
+ if (current.phase === "Paused") return counts.count("duringPictures");
1876
+ sizes.offer(frame, timestamp, image, device);
1877
+ };
1878
+ const make$8 = (options) => Effect.gen(function* () {
1879
+ const lock = yield* Semaphore.make(1);
1880
+ const replies = /* @__PURE__ */ new Set();
1881
+ let generation;
1882
+ const closed = yield* Deferred.make();
1883
+ const pageScope = yield* Effect.scope;
1884
+ const isClosed = () => pageScope.state._tag === "Closed" || Deferred.isDoneUnsafe(closed);
1885
+ let latest = Option.none();
1886
+ let history = [];
1887
+ const keepMillis = Duration.toMillis(options.frameHistory);
1888
+ const now = () => monotonicMillis(options.clock);
1889
+ const counts = tally(now);
1890
+ let delivered = 0;
1891
+ const current = (frame) => isCurrent(frame, generation, latest);
1892
+ const deliver = (frame, timestamp) => {
1893
+ const current = generation;
1894
+ if (current === void 0) return;
1895
+ if (current.predecessor !== void 0 && timestamp <= current.predecessor) return counts.count("late");
1896
+ if (current.predecessor !== void 0) counts.gap(timestamp - current.predecessor);
1897
+ current.predecessor = timestamp;
1898
+ delivered++;
1899
+ counts.count("accepted");
1900
+ latest = Option.some(frame);
1901
+ history = [...history.filter((kept) => kept.hostTime >= frame.hostTime - keepMillis), frame];
1902
+ for (const subscriber of current.subscribers) Queue.offerUnsafe(subscriber, {
1903
+ _tag: "Frame",
1904
+ sequence: delivered,
1905
+ frame
1906
+ });
1907
+ };
1908
+ const sizes = yield* sizeFilter(options, deliver, (frames) => counts.count("foreignSize", frames));
1909
+ const renewing = yield* renewals(options);
1910
+ const notifyFailure = (current, failure) => {
1911
+ if (current.failure !== void 0) return;
1912
+ current.failure = failure;
1913
+ for (const subscriber of current.subscribers) Queue.offerUnsafe(subscriber, {
1914
+ _tag: "Failure",
1915
+ error: failure
1916
+ });
1917
+ stop(current);
1918
+ };
1919
+ const failed = (current) => (cause) => notifyFailure(current, options.error(cause));
1920
+ const stopNative = (current) => pictures.track("screencast stop", current.transport.stop(), failed(current));
1921
+ const startNative = (current) => pictures.track("screencast start", current.transport.start({
1922
+ format: "jpeg",
1923
+ quality: current.quality,
1924
+ ...current.size === null ? {} : {
1925
+ maxWidth: current.size.width,
1926
+ maxHeight: current.size.height
1927
+ }
1928
+ }), failed(current));
1929
+ const stop = (current) => {
1930
+ if (current.phase === "Stopped") return;
1931
+ const paused = current.phase === "Paused";
1932
+ current.phase = "Stopped";
1933
+ sizes.end();
1934
+ current.unlisten();
1935
+ if (!paused) stopNative(current);
1936
+ };
1937
+ const gone = () => options.error(/* @__PURE__ */ new Error("Target page has been closed"));
1938
+ const why = told(closed, gone, options.clock);
1939
+ const awaitStop = (current) => pictures.wait("screencast stop").pipe(Effect.catch((error) => Effect.sync(() => notifyFailure(current, error))));
1940
+ const healthy = (current) => Effect.suspend(() => current.failure === void 0 ? Effect.void : Effect.fail(current.failure));
1941
+ const pictures = make$9({
1942
+ clock: options.clock,
1943
+ error: options.error,
1944
+ closed: isClosed,
1945
+ current: () => generation,
1946
+ fail: (current, error) => {
1947
+ if (current !== void 0) notifyFailure(current, error);
1948
+ },
1949
+ pause: (current) => Effect.gen(function* () {
1950
+ if (current?.phase !== "Capturing" || current.failure !== void 0) return;
1951
+ current.phase = "Paused";
1952
+ latest = Option.none();
1953
+ sizes.end();
1954
+ stopNative(current);
1955
+ yield* pictures.wait("screencast stop").pipe(Effect.tapError((error) => Effect.sync(() => notifyFailure(current, error))));
1956
+ yield* healthy(current);
1957
+ }),
1958
+ resume: (current) => Effect.gen(function* () {
1959
+ const resumable = () => {
1960
+ if (current === void 0 || generation !== current) return false;
1961
+ if (current.subscribers.size === 0) {
1962
+ stop(current);
1963
+ generation = void 0;
1964
+ return false;
1965
+ }
1966
+ return current.phase !== "Stopped" && current.failure === void 0 && !isClosed();
1967
+ };
1968
+ if (current === void 0 || !resumable()) return;
1969
+ pictures.track("picture restoration", options.restore(), failed(current));
1970
+ yield* pictures.wait("picture restoration");
1971
+ yield* healthy(current);
1972
+ const viewport = yield* readViewport(options);
1973
+ if (!resumable()) return;
1974
+ sizes.restart(viewport);
1975
+ current.phase = "Capturing";
1976
+ startNative(current);
1977
+ yield* pictures.wait("screencast start");
1978
+ yield* healthy(current);
1979
+ })
1980
+ });
1981
+ const leave = (current, subscription) => Effect.gen(function* () {
1982
+ current.subscribers.delete(subscription);
1983
+ yield* Queue.shutdown(subscription);
1984
+ if (current.subscribers.size !== 0) return;
1985
+ yield* Effect.sync(() => stop(current));
1986
+ yield* awaitStop(current);
1987
+ if (generation === current) generation = void 0;
1988
+ });
1989
+ const acquire = (screencast) => lock.withPermits(1)(Effect.gen(function* () {
1990
+ if (isClosed()) return yield* gone();
1991
+ let current = generation;
1992
+ const starting = current === void 0;
1993
+ if (current === void 0) {
1994
+ yield* pictures.idle;
1995
+ const [transport, { calibration, viewport }] = yield* prepare(options, screencast);
1996
+ const size = screencast.size ?? viewport;
1997
+ if (isClosed()) return yield* gone();
1998
+ current = {
1999
+ subscribers: /* @__PURE__ */ new Set(),
2000
+ transport,
2001
+ unlisten: () => void 0,
2002
+ calibration,
2003
+ quality: screencast.quality ?? 80,
2004
+ size,
2005
+ phase: "Capturing",
2006
+ predecessor: void 0,
2007
+ failure: void 0
2008
+ };
2009
+ generation = current;
2010
+ latest = Option.none();
2011
+ sizes.restart(viewport);
2012
+ } else if (screencast.quality !== void 0 && screencast.quality !== current.quality || screencast.size !== void 0 && (screencast.size.width !== current.size?.width || screencast.size.height !== current.size.height)) return yield* undispatched("screencast", new InvalidRequest({ detail: `a screencast with quality ${current.quality}${current.size === null ? "" : ` and size ${current.size.width}x${current.size.height}`} is already running on this page; read it without options or with the same ones` }));
2013
+ const subscription = yield* Queue.sliding(subscriberCapacity);
2014
+ current.subscribers.add(subscription);
2015
+ if (current.failure !== void 0) Queue.offerUnsafe(subscription, {
2016
+ _tag: "Failure",
2017
+ error: current.failure
2018
+ });
2019
+ const baseline = delivered;
2020
+ const frame = currentFrame(current, latest);
2021
+ if (starting) {
2022
+ const listening = current;
2023
+ current.unlisten = listening.transport.listen({
2024
+ frame: receive(options, current, replies, counts, sizes, renewing, notifyFailure),
2025
+ lost: (error) => notifyFailure(listening, error)
2026
+ });
2027
+ const screencast = Effect.promise(() => startNative(listening)).pipe(Effect.andThen(healthy(listening)));
2028
+ const started = yield* options.watch.pipe(Effect.zip(screencast, { concurrent: true }), Effect.interruptible, Effect.timeoutOrElse({
2029
+ duration: deadline,
2030
+ orElse: () => Effect.fail(options.error(/* @__PURE__ */ new Error("screencast start exceeded its deadline")))
2031
+ }), Effect.provideService(Clock.Clock, options.clock), Effect.exit);
2032
+ if (Exit.isFailure(started)) {
2033
+ yield* leave(current, subscription);
2034
+ return yield* Effect.failCause(started.cause);
2035
+ }
2036
+ }
2037
+ return {
2038
+ current,
2039
+ subscription,
2040
+ baseline,
2041
+ frame
2042
+ };
2043
+ }));
2044
+ 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) => ({
2045
+ current: lease.frame,
2046
+ stream: frames(lease.subscription, lease.baseline, why, (count) => counts.count("lost", count))
2047
+ })));
2048
+ yield* Effect.addFinalizer(() => Effect.sync(() => Deferred.doneUnsafe(closed, Exit.void)).pipe(Effect.andThen(lock.withPermits(1)(Effect.suspend(() => {
2049
+ const current = generation;
2050
+ if (current === void 0) return Effect.void;
2051
+ notifyFailure(current, gone());
2052
+ return awaitStop(current);
2053
+ })))));
2054
+ return {
2055
+ open,
2056
+ stream: (options) => Stream.unwrap(Effect.map(open(options), ({ stream }) => stream)),
2057
+ latest: Effect.sync(() => latest),
2058
+ recent: Effect.sync(() => history),
2059
+ active: Effect.sync(() => generation !== void 0 && generation.phase !== "Stopped"),
2060
+ sync: Effect.suspend(() => synced(generation, why, options.error)),
2061
+ current,
2062
+ confirm: confirmation(() => ({
2063
+ generation,
2064
+ replies: replies.size
2065
+ }), current, why, options.error),
2066
+ excluding: (picture) => lock.withPermits(1)(pictures.excluding(picture)),
2067
+ stats: (windowMillis) => Effect.sync(() => new CaptureStats({
2068
+ ...counts.read(windowMillis),
2069
+ ackBacklog: replies.size
2070
+ }))
2071
+ };
2072
+ });
2073
+ //#endregion
2074
+ //#region src/internal/pictures/transport.ts
2075
+ /**
2076
+ * Where a page's screencast runs: on the page's own session, or on a capture connection that the
2077
+ * provider supplies, where frames and their acknowledgements never wait behind a large message on
2078
+ * the connection that drives the page. There the page's session attaches as its first capture
2079
+ * starts, with the Page domain on, and stays until the page closes or the session ends; the next
2080
+ * capture then attaches again.
2081
+ *
2082
+ * Frames take their document from the commits their own session reports, in the order the browser
2083
+ * sent them, so a frame never shows a document after the one it carries, as on the page's own
2084
+ * session. They are numbered as the page's own session counts documents: a commit it has counted
2085
+ * keeps its number, and one the capture connection sees first takes the next, until it is counted.
2086
+ */
2087
+ /** The page's own session. */
2088
+ const own = (page, bridge) => {
2089
+ const { send } = page.protocol;
2090
+ return {
2091
+ start: (settings) => send("Page.startScreencast", settings),
2092
+ acknowledge: (frame) => send("Page.screencastFrameAck", { sessionId: frame }),
2093
+ stop: () => send("Page.stopScreencast"),
2094
+ sync: () => Promise.resolve(),
2095
+ listen: ({ frame }) => {
2096
+ page.cdp.on("Page.screencastFrame", frame);
2097
+ return () => {
2098
+ page.cdp.off("Page.screencastFrame", frame);
2099
+ };
2100
+ },
2101
+ frameTag: bridge.frameTag
2102
+ };
2103
+ };
2104
+ const FrameTree = Schema.Struct({ frameTree: Schema.Struct({ frame: Commit }) });
2105
+ /** A native screencast frame, as Chromium sends it. */
2106
+ const ScreencastFrame = Schema.Struct({
2107
+ data: Schema.String,
2108
+ sessionId: Schema.Int,
2109
+ metadata: Schema.Struct({
2110
+ timestamp: Schema.optionalKey(Schema.Finite),
2111
+ deviceWidth: Schema.Finite,
2112
+ deviceHeight: Schema.Finite
2113
+ })
2114
+ });
2115
+ /**
2116
+ * The document and address a capture connection's frames show, from the commits its session
2117
+ * reports after `first`, the main frame's document as the session began.
2118
+ */
2119
+ const documents = (bridge, first) => {
2120
+ const counted = (loader) => bridge.documentOf(loader) ?? -1;
2121
+ const latest = bridge.frameTag().document;
2122
+ const shown = {
2123
+ ...first,
2124
+ document: counted(first.loader) >= 0 ? counted(first.loader) : latest === 0 ? 0 : latest + 1
2125
+ };
2126
+ return {
2127
+ /** A commit or a move the session reported, in the order the browser sent them. */
2128
+ see: (seen) => {
2129
+ if (seen?.loader !== void 0) {
2130
+ shown.document = Math.max(shown.document + 1, counted(seen.loader));
2131
+ shown.loader = seen.loader;
2132
+ }
2133
+ if (seen !== void 0) shown.url = seen.url;
2134
+ },
2135
+ tag: () => {
2136
+ shown.document = Math.max(shown.document, counted(shown.loader));
2137
+ return {
2138
+ document: shown.document,
2139
+ url: shown.url
2140
+ };
2141
+ }
2142
+ };
2143
+ };
2144
+ /**
2145
+ * The page's session on the capture connection, attached when a capture asks for it and kept for
2146
+ * the page's scope. Its state changes only here: attaching, attached, or ended by the session's
2147
+ * end, which also ends the capture listening to it.
2148
+ */
2149
+ const apart = (page, bridge, source, error, pageScope) => {
2150
+ let state = { _tag: "Detached" };
2151
+ let listening;
2152
+ const latest = () => state;
2153
+ const end = (scope, ended) => {
2154
+ if (state._tag !== "Attaching" && state._tag !== "Attached" || state.scope !== scope) return;
2155
+ const attached = state._tag === "Attached";
2156
+ state = {
2157
+ _tag: "Ended",
2158
+ scope,
2159
+ error: ended
2160
+ };
2161
+ if (attached) listening?.lost(ended);
2162
+ };
2163
+ const unreadable = undispatched("screencast", new Failed({ detail: "the capture connection sent an event it could not read" }));
2164
+ const attach = (scope) => Effect.gen(function* () {
2165
+ let shown;
2166
+ const event = (method, params) => {
2167
+ const decoded = (schema, act) => Option.match(Schema.decodeUnknownOption(schema)(params), {
2168
+ onNone: () => end(scope, unreadable),
2169
+ onSome: act
2170
+ });
2171
+ if (method === "Page.screencastFrame") decoded(ScreencastFrame, (frame) => listening?.frame(frame));
2172
+ else if (method === "Page.frameNavigated" || method === "Page.navigatedWithinDocument") decoded(MainFrameEvent, (event) => shown?.see(mainFrame(page.id, event)));
2173
+ else if (method === "Target.detachedFromTarget") end(scope, error(/* @__PURE__ */ new Error("Target page has been closed")));
2174
+ };
2175
+ const send = yield* source.attach(page.id, {
2176
+ event,
2177
+ lost: (lost) => end(scope, lost)
2178
+ });
2179
+ const tracked = yield* Effect.tryPromise({
2180
+ try: () => Promise.all([send("Page.enable"), send("Page.getFrameTree")]).then(([, tree]) => Option.match(Schema.decodeUnknownOption(FrameTree)(tree), {
2181
+ onNone: () => Promise.reject(unreadable),
2182
+ onSome: ({ frameTree }) => shown = documents(bridge, commitOf(frameTree.frame))
2183
+ })),
2184
+ catch: error
2185
+ });
2186
+ return {
2187
+ start: (settings) => send("Page.startScreencast", settings),
2188
+ acknowledge: (frame) => send("Page.screencastFrameAck", { sessionId: frame }),
2189
+ stop: () => send("Page.stopScreencast"),
2190
+ sync: () => send("Page.getFrameTree"),
2191
+ listen: (on) => {
2192
+ listening = on;
2193
+ return () => {
2194
+ if (listening === on) listening = void 0;
2195
+ };
2196
+ },
2197
+ frameTag: () => ({
2198
+ session: page.session,
2199
+ connection: page.connection,
2200
+ ...tracked.tag()
2201
+ })
2202
+ };
2203
+ }).pipe(Scope.provide(scope));
2204
+ const current = Effect.suspend(() => {
2205
+ switch (state._tag) {
2206
+ case "Attached": return Effect.succeed(state.transport);
2207
+ case "Ended": {
2208
+ const { scope } = state;
2209
+ state = { _tag: "Detached" };
2210
+ return Scope.close(scope, Exit.void).pipe(Effect.andThen(current));
2211
+ }
2212
+ case "Detached":
2213
+ case "Attaching": return Effect.gen(function* () {
2214
+ const scope = yield* Scope.fork(pageScope);
2215
+ state = {
2216
+ _tag: "Attaching",
2217
+ scope
2218
+ };
2219
+ const attached = yield* Effect.exit(attach(scope).pipe(page.within("screencast")));
2220
+ const settled = latest();
2221
+ if (Exit.isSuccess(attached) && settled._tag === "Attaching" && settled.scope === scope) {
2222
+ state = {
2223
+ _tag: "Attached",
2224
+ scope,
2225
+ transport: attached.value
2226
+ };
2227
+ return attached.value;
2228
+ }
2229
+ state = { _tag: "Detached" };
2230
+ yield* Scope.close(scope, Exit.void);
2231
+ if (Exit.isFailure(attached)) return yield* attached;
2232
+ return yield* settled._tag === "Ended" ? settled.error : unreadable;
2233
+ });
2234
+ }
2235
+ });
2236
+ return current;
2237
+ };
2238
+ //#endregion
2239
+ //#region src/internal/pictures/pictures.ts
2240
+ /**
2241
+ * Pictures of a page: screenshots, frames of a stated age, zooms, and its screencast with the clock
2242
+ * that times its frames. A new picture is taken on the page's own protocol session, one at a time:
2243
+ * one call where a device pixel is a CSS pixel and nothing is cropped, and two otherwise, unless
2244
+ * Playwright emulates the viewport. The page's first clipped picture also holds the screen it reads.
2245
+ */
2246
+ /** Read a JPEG's dimensions from its start-of-frame marker. */
2247
+ const jpegSize = (bytes) => {
2248
+ let offset = 2;
2249
+ while (offset + 9 < bytes.length) {
2250
+ if (bytes[offset] !== 255) return void 0;
2251
+ const marker = bytes[offset + 1] ?? 0;
2252
+ const length = (bytes[offset + 2] ?? 0) << 8 | (bytes[offset + 3] ?? 0);
2253
+ if (marker >= 192 && marker <= 195) return {
2254
+ height: (bytes[offset + 5] ?? 0) << 8 | (bytes[offset + 6] ?? 0),
2255
+ width: (bytes[offset + 7] ?? 0) << 8 | (bytes[offset + 8] ?? 0)
2256
+ };
2257
+ offset += 2 + length;
2258
+ }
2259
+ };
2260
+ /** Read a PNG's dimensions from its header chunk. */
2261
+ const pngSize = (bytes) => {
2262
+ if (bytes.length < 24 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71) return void 0;
2263
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2264
+ return {
2265
+ width: view.getUint32(16),
2266
+ height: view.getUint32(20)
2267
+ };
2268
+ };
2269
+ const LayoutMetrics = Schema.Struct({
2270
+ cssVisualViewport: Schema.Struct({
2271
+ pageX: Schema.Finite,
2272
+ pageY: Schema.Finite,
2273
+ scale: Schema.Finite.check(Schema.isGreaterThan(0))
2274
+ }),
2275
+ contentSize: Schema.Struct({ width: Schema.Finite }),
2276
+ cssContentSize: Schema.Struct({ width: Schema.Finite })
2277
+ });
2278
+ const defaultMaxAge = Duration.millis(250);
2279
+ const surfaceless = (error) => error.reason._tag === "Failed" && error.reason.detail.includes("Unable to capture screenshot");
2280
+ const untilPainted = (capture) => capture.pipe(Effect.retry({
2281
+ while: surfaceless,
2282
+ schedule: Schedule.spaced("16 millis")
2283
+ }));
2284
+ /**
2285
+ * Measure this page's clock against the browser's, in a world without the page script: no DOM
2286
+ * markers, input, navigation or extra tabs. Each of three probes reads the browser's time between
2287
+ * two stamps of the owner's clock, and the fastest bounds the offset best. A busy page answers each
2288
+ * probe late, behind its own work, so the probes that answered within two seconds serve, and the
2289
+ * measurement fails only if none did: a wide estimate keeps a capture going, and a renewal while
2290
+ * its frames flow narrows it.
2291
+ */
2292
+ const calibrator = (page, bridge) => {
2293
+ const { native, now, within } = page;
2294
+ const { send } = page.protocol;
2295
+ const probe = (contextId) => Effect.gen(function* () {
2296
+ const hostStart = now();
2297
+ const { result, exceptionDetails } = yield* native("calibrate", () => send("Runtime.evaluate", {
2298
+ contextId,
2299
+ expression: "performance.timeOrigin + performance.now()",
2300
+ returnByValue: true
2301
+ })).pipe(page.span("Page.evaluate", { function: "clock" }, "Trace"));
2302
+ const hostEnd = now();
2303
+ const browserTime = result.value;
2304
+ if (exceptionDetails !== void 0 || typeof browserTime !== "number") return yield* failWith("calibrate", new Failed({ detail: "the browser clock probe did not return a timestamp" }));
2305
+ return {
2306
+ hostStart,
2307
+ hostEnd,
2308
+ browserTime
2309
+ };
2310
+ });
2311
+ const answered = (contextId) => Effect.gen(function* () {
2312
+ const probes = [];
2313
+ yield* Effect.forEach([
2314
+ 1,
2315
+ 2,
2316
+ 3
2317
+ ], () => Effect.map(probe(contextId), (one) => probes.push(one)), { discard: true }).pipe(within("calibrate", Duration.seconds(2)), Effect.catch((error) => probes.length === 0 ? Effect.fail(error) : Effect.void));
2318
+ return probes;
2319
+ });
2320
+ return bridge.bareWorld("calibrate").pipe(Effect.flatMap(answered), Effect.flatMap((probes) => Option.match(estimate(probes), {
2321
+ onNone: () => failWith("calibrate", new Failed({ detail: "the browser clock probes did not define a finite interval" })),
2322
+ onSome: Effect.succeed
2323
+ })), within("calibrate", Duration.seconds(4)), Effect.tap((estimate) => Effect.annotateCurrentSpan("roundTripMillis", Math.round(estimate.roundTripMillis * 10) / 10)), page.span("Page.calibrateClock", {}, "Debug"), page.owned);
2324
+ };
2325
+ const ScreenSize = Schema.Struct({
2326
+ width: Schema.Int,
2327
+ height: Schema.Int
2328
+ });
2329
+ const camera = (page, bridge, viewport, capture) => {
2330
+ const { playwright, native, within } = page;
2331
+ const { send } = page.protocol;
2332
+ let ratio = 1;
2333
+ let screenHeld = false;
2334
+ const holdScreen = Effect.fnUntraced(function* (operation) {
2335
+ if (screenHeld) return;
2336
+ const { width, height } = yield* bridge.evaluate(operation, scriptCall("screenSize")).pipe(Effect.flatMap(decodeWith(operation, ScreenSize)));
2337
+ yield* native(operation, () => send("Emulation.setDeviceMetricsOverride", {
2338
+ width: 0,
2339
+ height: 0,
2340
+ deviceScaleFactor: 0,
2341
+ mobile: false,
2342
+ screenWidth: width,
2343
+ screenHeight: height
2344
+ }));
2345
+ screenHeld = true;
2346
+ });
2347
+ const decoded = (operation, data, format) => {
2348
+ const size = format === "png" ? pngSize(data) : jpegSize(data);
2349
+ return size === void 0 ? failWith(operation, new Failed({ detail: `the browser's picture is not a ${format.toUpperCase()}` })) : Effect.succeed(new Image({
2350
+ data,
2351
+ mediaType: `image/${format}`,
2352
+ ...size
2353
+ }));
2354
+ };
2355
+ const take = (operation, format, run) => native(operation, run).pipe(untilPainted, Effect.flatMap((data) => decoded(operation, data, format)));
2356
+ const shot = (operation, format, quality, clip) => take(operation, format, () => send("Page.captureScreenshot", {
2357
+ format,
2358
+ ...format === "jpeg" ? { quality } : {},
2359
+ ...clip === void 0 ? {} : { clip }
2360
+ }).then(({ data }) => Buffer.from(data, "base64")));
2361
+ const emulatedShot = (operation, format, quality, crop) => take(operation, format, () => playwright.screenshot({
2362
+ type: format,
2363
+ ...format === "jpeg" ? { quality } : {},
2364
+ scale: "css",
2365
+ timeout: 0,
2366
+ ...crop === void 0 ? {} : { clip: crop }
2367
+ }));
2368
+ return (operation, quality, crop, format = "jpeg") => Effect.gen(function* () {
2369
+ const known = viewport.knownViewport();
2370
+ let whole;
2371
+ if (crop === void 0 && ratio === 1) {
2372
+ whole = yield* shot(operation, format, quality);
2373
+ if (whole.width === known?.width && whole.height === known.height) return whole;
2374
+ }
2375
+ const emulated = playwright.viewportSize();
2376
+ if (emulated !== null) {
2377
+ if (whole !== void 0) ratio = whole.width / emulated.width;
2378
+ return yield* emulatedShot(operation, format, quality, crop);
2379
+ }
2380
+ 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 }));
2381
+ const measured = metrics.contentSize.width / metrics.cssContentSize.width;
2382
+ ratio = Number.isFinite(measured) && measured > 0 ? measured : 1;
2383
+ if (crop === void 0 && ratio === 1) {
2384
+ const image = whole ?? (yield* shot(operation, format, quality));
2385
+ viewport.remember(image);
2386
+ return image;
2387
+ }
2388
+ const rect = crop ?? {
2389
+ x: 0,
2390
+ y: 0,
2391
+ ...known ?? (yield* viewport.viewportFor(operation))
2392
+ };
2393
+ const { pageX, pageY, scale } = metrics.cssVisualViewport;
2394
+ yield* holdScreen(operation);
2395
+ return yield* shot(operation, format, quality, {
2396
+ x: pageX + rect.x,
2397
+ y: pageY + rect.y,
2398
+ width: Math.floor(rect.width / scale + .001),
2399
+ height: Math.floor(rect.height / scale + .001),
2400
+ scale: scale / ratio
2401
+ });
2402
+ }).pipe(capture.excluding, within(operation));
2403
+ };
2404
+ const millisOf = (input) => Option.match(Duration.fromInput(input), {
2405
+ onNone: () => NaN,
2406
+ onSome: Duration.toMillis
2407
+ });
2408
+ const reuse = (page, bridge, viewport, capture) => (operation, options) => Effect.gen(function* () {
2409
+ const maxAge = millisOf(options.maxAge ?? defaultMaxAge);
2410
+ if (!(maxAge >= 0)) return yield* refuse(operation, "maxAge must be a duration of zero or more");
2411
+ const latest = yield* capture.latest;
2412
+ const known = viewport.knownViewport();
2413
+ const { activity } = page;
2414
+ return Option.filter(latest, (frame) => {
2415
+ const earliest = frame.hostTime - frame.timing.uncertaintyMillis;
2416
+ return frame.document === bridge.frameTag().document && page.now() - earliest <= maxAge && earliest > activity.documentAt && (options.after !== "input" || earliest > activity.inputAt) && frame.width === known?.width && frame.height === known.height;
2417
+ });
2418
+ });
2419
+ /** Where a picture came from: a frame the screencast had delivered, or a new screenshot. */
2420
+ const sourceOf = (reused) => Option.isSome(reused) ? "frame" : "screenshot";
2421
+ const make$7 = Effect.fnUntraced(function* (page, bridge, viewport) {
2422
+ const { id, clock, settings, mapping, now, span, owned, lane, within } = page;
2423
+ const calibrateClock = calibrator(page, bridge);
2424
+ const error = (cause) => Schema.is(BrowserError)(cause) ? cause : undispatched("screencast", reasonOf(cause, void 0, page.closedBy()));
2425
+ const capture = yield* make$8({
2426
+ id,
2427
+ transport: page.capture === void 0 ? Effect.succeed(own(page, bridge)) : apart(page, bridge, page.capture, error, yield* Effect.scope),
2428
+ clock,
2429
+ calibrate: page.focused.pipe(within("focus"), Effect.andThen(mapping.current(calibrateClock))),
2430
+ latest: mapping.latest,
2431
+ renew: mapping.renew(calibrateClock),
2432
+ watch: bridge.currentDocument("screencast"),
2433
+ frameHistory: settings.frameHistory,
2434
+ viewport: Effect.suspend(() => viewport.viewportFor("screencast")),
2435
+ restore: () => page.protocol.send("Page.captureScreenshot", {
2436
+ format: "jpeg",
2437
+ quality: 0,
2438
+ optimizeForSpeed: true
2439
+ }),
2440
+ imageSize: jpegSize,
2441
+ error
2442
+ });
2443
+ const picture = camera(page, bridge, viewport, capture);
2444
+ const reusable = reuse(page, bridge, viewport, capture);
2445
+ const screenshots = lane.shared("screenshot", false);
2446
+ const frames = lane.shared("frame", false);
2447
+ const screenshot = (options = {}) => screenshots(JSON.stringify([
2448
+ millisOf(options.maxAge ?? defaultMaxAge),
2449
+ options.clip,
2450
+ options.quality,
2451
+ options.format
2452
+ ]), Effect.gen(function* () {
2453
+ const reused = options.clip === void 0 && options.format !== "png" ? yield* reusable("screenshot", {
2454
+ maxAge: options.maxAge,
2455
+ after: "input"
2456
+ }) : Option.none();
2457
+ const image = Option.isSome(reused) ? reused.value.image : yield* picture("screenshot", options.quality ?? 80, options.clip, options.format);
2458
+ yield* Effect.annotateCurrentSpan({
2459
+ source: sourceOf(reused),
2460
+ bytes: image.data.length
2461
+ });
2462
+ return image;
2463
+ })).pipe(span("Page.screenshot"), owned);
2464
+ const taken = (options) => Effect.gen(function* () {
2465
+ const reused = yield* reusable("frame", options);
2466
+ yield* Effect.annotateCurrentSpan("source", sourceOf(reused));
2467
+ if (Option.isSome(reused)) return reused.value;
2468
+ const startedAt = now();
2469
+ const image = yield* picture("frame", 80);
2470
+ const finishedAt = now();
2471
+ return new Frame({
2472
+ page: id,
2473
+ data: image.data,
2474
+ timing: new Screenshot({
2475
+ hostTime: startedAt + (finishedAt - startedAt) / 2,
2476
+ uncertaintyMillis: (finishedAt - startedAt) / 2
2477
+ }),
2478
+ receivedAt: finishedAt,
2479
+ width: image.width,
2480
+ height: image.height,
2481
+ ...bridge.frameTag()
2482
+ });
2483
+ });
2484
+ const frame = (options = {}) => (options.after === "input" ? Effect.succeed(Option.none()) : reusable("frame", options)).pipe(Effect.flatMap(Option.match({
2485
+ onSome: (reused) => Effect.as(Effect.annotateCurrentSpan("source", "frame"), reused),
2486
+ onNone: () => frames(JSON.stringify([millisOf(options.maxAge ?? defaultMaxAge), options.after]), taken(options))
2487
+ })), span("Page.frame"), owned);
2488
+ const zoom = (requested) => lane.read("zoom")(Effect.gen(function* () {
2489
+ const region = yield* Schema.decodeEffect(Region)(requested).pipe(Effect.mapError((error) => undispatched("zoom", new InvalidRequest({ detail: error.message }))));
2490
+ const size = viewport.knownViewport() ?? (yield* viewport.viewportFor("zoom"));
2491
+ 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");
2492
+ const image = yield* picture("zoom", 80, region);
2493
+ return new Zoom({
2494
+ page: id,
2495
+ region,
2496
+ image
2497
+ });
2498
+ }).pipe(within("zoom"))).pipe(span("Page.zoom"), owned);
2499
+ const captureStats = (options = {}) => {
2500
+ if (options.window === void 0) return capture.stats();
2501
+ const millis = millisOf(options.window);
2502
+ return millis > 0 && millis <= Duration.toMillis(countsKept) ? capture.stats(millis) : refuse("captureStats", "window must be over zero and at most a minute");
2503
+ };
2504
+ return {
2505
+ capture,
2506
+ screenshot,
2507
+ frame,
2508
+ zoom,
2509
+ captureStats,
2510
+ estimate: mapping.current(calibrateClock)
2511
+ };
2512
+ });
2513
+ //#endregion
2514
+ //#region src/internal/reading/reading.ts
2515
+ /**
2516
+ * Reading a page: its outline with refs, the elements a query finds and the text it shows. Each
2517
+ * read takes its turn on the page and shares its work with identical reads, as `lane.ts`
2518
+ * describes. The viewport's text as it was last read is kept, for `Page.state`.
2519
+ */
2520
+ const FindResults = Schema.Struct({
2521
+ found: Schema.Array(Found),
2522
+ nextRef: Schema.Finite
2523
+ });
2524
+ const UntilResult = Schema.Struct({
2525
+ met: Schema.Boolean,
2526
+ mark: Schema.String
2527
+ });
2528
+ const wanted = (value) => value === void 0 ? null : typeof value === "string" ? value : {
2529
+ source: value.source,
2530
+ flags: value.flags
2531
+ };
2532
+ /** A query as the page takes it, but for the first ref it may give. */
2533
+ const askedOf = (query, scope) => ({
2534
+ role: query.role ?? null,
2535
+ name: wanted(query.name),
2536
+ text: wanted(query.text),
2537
+ near: query.near ?? null,
2538
+ at: query.at ?? null,
2539
+ scope: query.scope ?? scope
2540
+ });
2541
+ const make$6 = Effect.fnUntraced(function* (page, bridge) {
2542
+ const { now, span, owned, within, lane } = page;
2543
+ const { evaluate } = bridge;
2544
+ const nextRef = yield* Ref.make(1);
2545
+ const counted = (next) => Ref.update(nextRef, (current) => Math.max(current, next));
2546
+ const snapshots = lane.shared("snapshot", true);
2547
+ const finds = lane.shared("find", true);
2548
+ const texts = lane.shared("text", true);
2549
+ let viewed;
2550
+ const snapshot = (snapshotOptions = {}) => {
2551
+ const asked = {
2552
+ full: snapshotOptions.full ?? false,
2553
+ query: snapshotOptions.query ?? null,
2554
+ within: snapshotOptions.within ?? null,
2555
+ maxChars: snapshotOptions.maxChars ?? 12e3
2556
+ };
2557
+ return snapshots(JSON.stringify(asked), Effect.gen(function* () {
2558
+ const request = {
2559
+ ...asked,
2560
+ firstRef: yield* Ref.get(nextRef)
2561
+ };
2562
+ const result = yield* evaluate("snapshot", scriptCall("snapshot", request)).pipe(Effect.flatMap(decodeWith("snapshot", SnapshotResultSchema)));
2563
+ if ("invalid" in result) return yield* refuse("snapshot", result.invalid);
2564
+ const { text, truncated, url } = result.snapshot;
2565
+ yield* counted(result.nextRef);
2566
+ yield* Effect.annotateCurrentSpan({
2567
+ chars: text.length,
2568
+ truncated
2569
+ });
2570
+ return new Snapshot({
2571
+ ...result.snapshot,
2572
+ url: redact(url)
2573
+ });
2574
+ }).pipe(within("snapshot"))).pipe(span("Page.snapshot", { full: asked.full }), owned);
2575
+ };
2576
+ const find = (query = {}) => {
2577
+ const asked = askedOf(query, "viewport");
2578
+ return finds(JSON.stringify(asked), Effect.gen(function* () {
2579
+ const request = {
2580
+ ...asked,
2581
+ firstRef: yield* Ref.get(nextRef)
2582
+ };
2583
+ const result = yield* evaluate("find", scriptCall("find", request)).pipe(Effect.flatMap(decodeWith("find", FindResults)));
2584
+ yield* counted(result.nextRef);
2585
+ yield* Effect.annotateCurrentSpan({ found: result.found.length });
2586
+ return result.found;
2587
+ }).pipe(within("find"))).pipe(span("Page.find", {
2588
+ scope: asked.scope,
2589
+ role: asked.role ?? ""
2590
+ }), owned);
2591
+ };
2592
+ const text = (options = {}) => {
2593
+ const scope = options.scope ?? "viewport";
2594
+ const request = {
2595
+ ref: scope === "viewport" ? null : scope,
2596
+ maxChars: options.maxChars ?? 12e3,
2597
+ unmask: options.unmask ?? false
2598
+ };
2599
+ 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* () {
2600
+ const result = yield* evaluate("text", scriptCall("text", request)).pipe(Effect.flatMap(decodeWith("text", TextResultSchema)));
2601
+ if (result === null) return yield* failWith("text", new StaleRef({ ref: scope }));
2602
+ yield* Effect.annotateCurrentSpan({
2603
+ chars: result.text.length,
2604
+ truncated: result.truncated
2605
+ });
2606
+ const shown = new Text({
2607
+ ...result,
2608
+ url: redact(result.url),
2609
+ at: now()
2610
+ });
2611
+ if (request.ref === null) viewed = shown;
2612
+ return shown;
2613
+ }).pipe(within("text")))).pipe(span("Page.text", { scope: scope === "viewport" ? "viewport" : "ref" }), owned);
2614
+ };
2615
+ const waitFor = (condition, timeout = Duration.seconds(10)) => {
2616
+ const request = {
2617
+ selector: condition.selector ?? null,
2618
+ text: condition.text ?? null,
2619
+ state: condition.state ?? "visible"
2620
+ };
2621
+ const millis = Math.min(Duration.toMillis(timeout), Number.MAX_SAFE_INTEGER);
2622
+ const unmet = () => failWith("waitFor", new Timeout({ millis }));
2623
+ 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()));
2624
+ return lane.read("waitFor")(Effect.void).pipe(Effect.andThen(Effect.retry(look, { while: contextGone })), Effect.timeoutOrElse({
2625
+ duration: timeout,
2626
+ orElse: unmet
2627
+ }), span("Page.waitFor", { state: request.state }), owned);
2628
+ };
2629
+ const until = (query, want, millis) => {
2630
+ const look = Effect.gen(function* () {
2631
+ const request = {
2632
+ ...askedOf(query, "document"),
2633
+ firstRef: yield* Ref.get(nextRef)
2634
+ };
2635
+ return yield* evaluate("expect", scriptCall("until", request, want, millis)).pipe(Effect.flatMap(decodeWith("expect", UntilResult)));
2636
+ });
2637
+ return lane.read("expect")(Effect.void).pipe(Effect.andThen(Effect.retry(look, { while: contextGone })), within("expect", Duration.sum(Duration.millis(millis), page.settings.actionTimeout)), span("Page.expect", { want: typeof want === "string" ? want : "other" }), owned);
2638
+ };
2639
+ /** The viewport's text as it was last read, if after `since`, as a document began. */
2640
+ const viewedSince = (since) => viewed !== void 0 && viewed.at > since ? viewed : void 0;
2641
+ return {
2642
+ snapshot,
2643
+ find,
2644
+ text,
2645
+ waitFor,
2646
+ until,
2647
+ viewedSince
2648
+ };
2649
+ });
2650
+ //#endregion
2651
+ //#region \0virtual:effect-browser/playwright-jpeg
2652
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2653
+ var __commonJS = (cb, mod) => function __require() {
2654
+ try {
2655
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
2656
+ } catch (e) {
2657
+ throw mod = 0, e;
2658
+ }
2659
+ };
2660
+ const jpegjs = { decode: __commonJS({ "node_modules/jpeg-js/lib/decoder.js"(exports2, module2) {
2661
+ var JpegImage = (function jpegImage() {
2662
+ "use strict";
2663
+ var dctZigZag = new Int32Array([
2664
+ 0,
2665
+ 1,
2666
+ 8,
2667
+ 16,
2668
+ 9,
2669
+ 2,
2670
+ 3,
2671
+ 10,
2672
+ 17,
2673
+ 24,
2674
+ 32,
2675
+ 25,
2676
+ 18,
2677
+ 11,
2678
+ 4,
2679
+ 5,
2680
+ 12,
2681
+ 19,
2682
+ 26,
2683
+ 33,
2684
+ 40,
2685
+ 48,
2686
+ 41,
2687
+ 34,
2688
+ 27,
2689
+ 20,
2690
+ 13,
2691
+ 6,
2692
+ 7,
2693
+ 14,
2694
+ 21,
2695
+ 28,
2696
+ 35,
2697
+ 42,
2698
+ 49,
2699
+ 56,
2700
+ 57,
2701
+ 50,
2702
+ 43,
2703
+ 36,
2704
+ 29,
2705
+ 22,
2706
+ 15,
2707
+ 23,
2708
+ 30,
2709
+ 37,
2710
+ 44,
2711
+ 51,
2712
+ 58,
2713
+ 59,
2714
+ 52,
2715
+ 45,
2716
+ 38,
2717
+ 31,
2718
+ 39,
2719
+ 46,
2720
+ 53,
2721
+ 60,
2722
+ 61,
2723
+ 54,
2724
+ 47,
2725
+ 55,
2726
+ 62,
2727
+ 63
2728
+ ]);
2729
+ var dctCos1 = 4017;
2730
+ var dctSin1 = 799;
2731
+ var dctCos3 = 3406;
2732
+ var dctSin3 = 2276;
2733
+ var dctCos6 = 1567;
2734
+ var dctSin6 = 3784;
2735
+ var dctSqrt2 = 5793;
2736
+ var dctSqrt1d2 = 2896;
2737
+ function constructor() {}
2738
+ function buildHuffmanTable(codeLengths, values) {
2739
+ var k = 0, code = [], i2, j, length = 16;
2740
+ while (length > 0 && !codeLengths[length - 1]) length--;
2741
+ code.push({
2742
+ children: [],
2743
+ index: 0
2744
+ });
2745
+ var p = code[0], q;
2746
+ for (i2 = 0; i2 < length; i2++) {
2747
+ for (j = 0; j < codeLengths[i2]; j++) {
2748
+ p = code.pop();
2749
+ p.children[p.index] = values[k];
2750
+ while (p.index > 0) {
2751
+ if (code.length === 0) throw new Error("Could not recreate Huffman Table");
2752
+ p = code.pop();
2753
+ }
2754
+ p.index++;
2755
+ code.push(p);
2756
+ while (code.length <= i2) {
2757
+ code.push(q = {
2758
+ children: [],
2759
+ index: 0
2760
+ });
2761
+ p.children[p.index] = q.children;
2762
+ p = q;
2763
+ }
2764
+ k++;
2765
+ }
2766
+ if (i2 + 1 < length) {
2767
+ code.push(q = {
2768
+ children: [],
2769
+ index: 0
2770
+ });
2771
+ p.children[p.index] = q.children;
2772
+ p = q;
2773
+ }
2774
+ }
2775
+ return code[0].children;
2776
+ }
2777
+ function decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successivePrev, successive, opts) {
2778
+ frame.precision;
2779
+ frame.samplesPerLine;
2780
+ frame.scanLines;
2781
+ var mcusPerLine = frame.mcusPerLine;
2782
+ var progressive = frame.progressive;
2783
+ frame.maxH;
2784
+ frame.maxV;
2785
+ var startOffset = offset, bitsData = 0, bitsCount = 0;
2786
+ function readBit() {
2787
+ if (bitsCount > 0) {
2788
+ bitsCount--;
2789
+ return bitsData >> bitsCount & 1;
2790
+ }
2791
+ bitsData = data[offset++];
2792
+ if (bitsData == 255) {
2793
+ var nextByte = data[offset++];
2794
+ if (nextByte) throw new Error("unexpected marker: " + (bitsData << 8 | nextByte).toString(16));
2795
+ }
2796
+ bitsCount = 7;
2797
+ return bitsData >>> 7;
2798
+ }
2799
+ function decodeHuffman(tree) {
2800
+ var node = tree, bit;
2801
+ while ((bit = readBit()) !== null) {
2802
+ node = node[bit];
2803
+ if (typeof node === "number") return node;
2804
+ if (typeof node !== "object") throw new Error("invalid huffman sequence");
2805
+ }
2806
+ return null;
2807
+ }
2808
+ function receive(length) {
2809
+ var n3 = 0;
2810
+ while (length > 0) {
2811
+ var bit = readBit();
2812
+ if (bit === null) return;
2813
+ n3 = n3 << 1 | bit;
2814
+ length--;
2815
+ }
2816
+ return n3;
2817
+ }
2818
+ function receiveAndExtend(length) {
2819
+ var n3 = receive(length);
2820
+ if (n3 >= 1 << length - 1) return n3;
2821
+ return n3 + (-1 << length) + 1;
2822
+ }
2823
+ function decodeBaseline(component2, zz) {
2824
+ var t = decodeHuffman(component2.huffmanTableDC);
2825
+ var diff2 = t === 0 ? 0 : receiveAndExtend(t);
2826
+ zz[0] = component2.pred += diff2;
2827
+ var k2 = 1;
2828
+ while (k2 < 64) {
2829
+ var rs = decodeHuffman(component2.huffmanTableAC);
2830
+ var s = rs & 15, r = rs >> 4;
2831
+ if (s === 0) {
2832
+ if (r < 15) break;
2833
+ k2 += 16;
2834
+ continue;
2835
+ }
2836
+ k2 += r;
2837
+ var z2 = dctZigZag[k2];
2838
+ zz[z2] = receiveAndExtend(s);
2839
+ k2++;
2840
+ }
2841
+ }
2842
+ function decodeDCFirst(component2, zz) {
2843
+ var t = decodeHuffman(component2.huffmanTableDC);
2844
+ var diff2 = t === 0 ? 0 : receiveAndExtend(t) << successive;
2845
+ zz[0] = component2.pred += diff2;
2846
+ }
2847
+ function decodeDCSuccessive(component2, zz) {
2848
+ zz[0] |= readBit() << successive;
2849
+ }
2850
+ var eobrun = 0;
2851
+ function decodeACFirst(component2, zz) {
2852
+ if (eobrun > 0) {
2853
+ eobrun--;
2854
+ return;
2855
+ }
2856
+ var k2 = spectralStart, e = spectralEnd;
2857
+ while (k2 <= e) {
2858
+ var rs = decodeHuffman(component2.huffmanTableAC);
2859
+ var s = rs & 15, r = rs >> 4;
2860
+ if (s === 0) {
2861
+ if (r < 15) {
2862
+ eobrun = receive(r) + (1 << r) - 1;
2863
+ break;
2864
+ }
2865
+ k2 += 16;
2866
+ continue;
2867
+ }
2868
+ k2 += r;
2869
+ var z2 = dctZigZag[k2];
2870
+ zz[z2] = receiveAndExtend(s) * (1 << successive);
2871
+ k2++;
2872
+ }
2873
+ }
2874
+ var successiveACState = 0, successiveACNextValue;
2875
+ function decodeACSuccessive(component2, zz) {
2876
+ var k2 = spectralStart, e = spectralEnd, r = 0;
2877
+ while (k2 <= e) {
2878
+ var z2 = dctZigZag[k2];
2879
+ var direction = zz[z2] < 0 ? -1 : 1;
2880
+ switch (successiveACState) {
2881
+ case 0:
2882
+ var rs = decodeHuffman(component2.huffmanTableAC);
2883
+ var s = rs & 15, r = rs >> 4;
2884
+ if (s === 0) {
2885
+ if (r < 15) {
2886
+ eobrun = receive(r) + (1 << r);
2887
+ successiveACState = 4;
2888
+ } else {
2889
+ r = 16;
2890
+ successiveACState = 1;
2891
+ }
2892
+ } else {
2893
+ if (s !== 1) throw new Error("invalid ACn encoding");
2894
+ successiveACNextValue = receiveAndExtend(s);
2895
+ successiveACState = r ? 2 : 3;
2896
+ }
2897
+ continue;
2898
+ case 1:
2899
+ case 2:
2900
+ if (zz[z2]) zz[z2] += (readBit() << successive) * direction;
2901
+ else {
2902
+ r--;
2903
+ if (r === 0) successiveACState = successiveACState == 2 ? 3 : 0;
2904
+ }
2905
+ break;
2906
+ case 3:
2907
+ if (zz[z2]) zz[z2] += (readBit() << successive) * direction;
2908
+ else {
2909
+ zz[z2] = successiveACNextValue << successive;
2910
+ successiveACState = 0;
2911
+ }
2912
+ break;
2913
+ case 4: if (zz[z2]) zz[z2] += (readBit() << successive) * direction;
2914
+ }
2915
+ k2++;
2916
+ }
2917
+ if (successiveACState === 4) {
2918
+ eobrun--;
2919
+ if (eobrun === 0) successiveACState = 0;
2920
+ }
2921
+ }
2922
+ function decodeMcu(component2, decode4, mcu2, row, col) {
2923
+ var mcuRow = mcu2 / mcusPerLine | 0;
2924
+ var mcuCol = mcu2 % mcusPerLine;
2925
+ var blockRow = mcuRow * component2.v + row;
2926
+ var blockCol = mcuCol * component2.h + col;
2927
+ if (component2.blocks[blockRow] === void 0 && opts.tolerantDecoding) return;
2928
+ decode4(component2, component2.blocks[blockRow][blockCol]);
2929
+ }
2930
+ function decodeBlock(component2, decode4, mcu2) {
2931
+ var blockRow = mcu2 / component2.blocksPerLine | 0;
2932
+ var blockCol = mcu2 % component2.blocksPerLine;
2933
+ if (component2.blocks[blockRow] === void 0 && opts.tolerantDecoding) return;
2934
+ decode4(component2, component2.blocks[blockRow][blockCol]);
2935
+ }
2936
+ var componentsLength = components.length;
2937
+ var component, i2, j, k, n2;
2938
+ var decodeFn;
2939
+ if (progressive) {
2940
+ if (spectralStart === 0) decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive;
2941
+ else decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive;
2942
+ } else decodeFn = decodeBaseline;
2943
+ var mcu = 0, marker;
2944
+ var mcuExpected;
2945
+ if (componentsLength == 1) mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn;
2946
+ else mcuExpected = mcusPerLine * frame.mcusPerColumn;
2947
+ if (!resetInterval) resetInterval = mcuExpected;
2948
+ var h2, v;
2949
+ while (mcu < mcuExpected) {
2950
+ for (i2 = 0; i2 < componentsLength; i2++) components[i2].pred = 0;
2951
+ eobrun = 0;
2952
+ if (componentsLength == 1) {
2953
+ component = components[0];
2954
+ for (n2 = 0; n2 < resetInterval; n2++) {
2955
+ decodeBlock(component, decodeFn, mcu);
2956
+ mcu++;
2957
+ }
2958
+ } else for (n2 = 0; n2 < resetInterval; n2++) {
2959
+ for (i2 = 0; i2 < componentsLength; i2++) {
2960
+ component = components[i2];
2961
+ h2 = component.h;
2962
+ v = component.v;
2963
+ for (j = 0; j < v; j++) for (k = 0; k < h2; k++) decodeMcu(component, decodeFn, mcu, j, k);
2964
+ }
2965
+ mcu++;
2966
+ if (mcu === mcuExpected) break;
2967
+ }
2968
+ if (mcu === mcuExpected) do {
2969
+ if (data[offset] === 255) {
2970
+ if (data[offset + 1] !== 0) break;
2971
+ }
2972
+ offset += 1;
2973
+ } while (offset < data.length - 2);
2974
+ bitsCount = 0;
2975
+ marker = data[offset] << 8 | data[offset + 1];
2976
+ if (marker < 65280) throw new Error("marker was not found");
2977
+ if (marker >= 65488 && marker <= 65495) offset += 2;
2978
+ else break;
2979
+ }
2980
+ return offset - startOffset;
2981
+ }
2982
+ function buildComponentData(frame, component) {
2983
+ var lines = [];
2984
+ var blocksPerLine = component.blocksPerLine;
2985
+ var blocksPerColumn = component.blocksPerColumn;
2986
+ var samplesPerLine = blocksPerLine << 3;
2987
+ var R = /* @__PURE__ */ new Int32Array(64), r = /* @__PURE__ */ new Uint8Array(64);
2988
+ function quantizeAndInverse(zz, dataOut, dataIn) {
2989
+ var qt = component.quantizationTable;
2990
+ var v0, v1, v2, v3, v4, v5, v6, v7, t;
2991
+ var p = dataIn;
2992
+ var i3 = 0;
2993
+ for (; i3 < 64; i3++) p[i3] = zz[i3] * qt[i3];
2994
+ for (i3 = 0; i3 < 8; ++i3) {
2995
+ var row = 8 * i3;
2996
+ if (p[1 + row] == 0 && p[2 + row] == 0 && p[3 + row] == 0 && p[4 + row] == 0 && p[5 + row] == 0 && p[6 + row] == 0 && p[7 + row] == 0) {
2997
+ t = dctSqrt2 * p[0 + row] + 512 >> 10;
2998
+ p[0 + row] = t;
2999
+ p[1 + row] = t;
3000
+ p[2 + row] = t;
3001
+ p[3 + row] = t;
3002
+ p[4 + row] = t;
3003
+ p[5 + row] = t;
3004
+ p[6 + row] = t;
3005
+ p[7 + row] = t;
3006
+ continue;
3007
+ }
3008
+ v0 = dctSqrt2 * p[0 + row] + 128 >> 8;
3009
+ v1 = dctSqrt2 * p[4 + row] + 128 >> 8;
3010
+ v2 = p[2 + row];
3011
+ v3 = p[6 + row];
3012
+ v4 = dctSqrt1d2 * (p[1 + row] - p[7 + row]) + 128 >> 8;
3013
+ v7 = dctSqrt1d2 * (p[1 + row] + p[7 + row]) + 128 >> 8;
3014
+ v5 = p[3 + row] << 4;
3015
+ v6 = p[5 + row] << 4;
3016
+ t = v0 - v1 + 1 >> 1;
3017
+ v0 = v0 + v1 + 1 >> 1;
3018
+ v1 = t;
3019
+ t = v2 * dctSin6 + v3 * dctCos6 + 128 >> 8;
3020
+ v2 = v2 * dctCos6 - v3 * dctSin6 + 128 >> 8;
3021
+ v3 = t;
3022
+ t = v4 - v6 + 1 >> 1;
3023
+ v4 = v4 + v6 + 1 >> 1;
3024
+ v6 = t;
3025
+ t = v7 + v5 + 1 >> 1;
3026
+ v5 = v7 - v5 + 1 >> 1;
3027
+ v7 = t;
3028
+ t = v0 - v3 + 1 >> 1;
3029
+ v0 = v0 + v3 + 1 >> 1;
3030
+ v3 = t;
3031
+ t = v1 - v2 + 1 >> 1;
3032
+ v1 = v1 + v2 + 1 >> 1;
3033
+ v2 = t;
3034
+ t = v4 * dctSin3 + v7 * dctCos3 + 2048 >> 12;
3035
+ v4 = v4 * dctCos3 - v7 * dctSin3 + 2048 >> 12;
3036
+ v7 = t;
3037
+ t = v5 * dctSin1 + v6 * dctCos1 + 2048 >> 12;
3038
+ v5 = v5 * dctCos1 - v6 * dctSin1 + 2048 >> 12;
3039
+ v6 = t;
3040
+ p[0 + row] = v0 + v7;
3041
+ p[7 + row] = v0 - v7;
3042
+ p[1 + row] = v1 + v6;
3043
+ p[6 + row] = v1 - v6;
3044
+ p[2 + row] = v2 + v5;
3045
+ p[5 + row] = v2 - v5;
3046
+ p[3 + row] = v3 + v4;
3047
+ p[4 + row] = v3 - v4;
3048
+ }
3049
+ for (i3 = 0; i3 < 8; ++i3) {
3050
+ var col = i3;
3051
+ if (p[8 + col] == 0 && p[16 + col] == 0 && p[24 + col] == 0 && p[32 + col] == 0 && p[40 + col] == 0 && p[48 + col] == 0 && p[56 + col] == 0) {
3052
+ t = dctSqrt2 * dataIn[i3 + 0] + 8192 >> 14;
3053
+ p[0 + col] = t;
3054
+ p[8 + col] = t;
3055
+ p[16 + col] = t;
3056
+ p[24 + col] = t;
3057
+ p[32 + col] = t;
3058
+ p[40 + col] = t;
3059
+ p[48 + col] = t;
3060
+ p[56 + col] = t;
3061
+ continue;
3062
+ }
3063
+ v0 = dctSqrt2 * p[0 + col] + 2048 >> 12;
3064
+ v1 = dctSqrt2 * p[32 + col] + 2048 >> 12;
3065
+ v2 = p[16 + col];
3066
+ v3 = p[48 + col];
3067
+ v4 = dctSqrt1d2 * (p[8 + col] - p[56 + col]) + 2048 >> 12;
3068
+ v7 = dctSqrt1d2 * (p[8 + col] + p[56 + col]) + 2048 >> 12;
3069
+ v5 = p[24 + col];
3070
+ v6 = p[40 + col];
3071
+ t = v0 - v1 + 1 >> 1;
3072
+ v0 = v0 + v1 + 1 >> 1;
3073
+ v1 = t;
3074
+ t = v2 * dctSin6 + v3 * dctCos6 + 2048 >> 12;
3075
+ v2 = v2 * dctCos6 - v3 * dctSin6 + 2048 >> 12;
3076
+ v3 = t;
3077
+ t = v4 - v6 + 1 >> 1;
3078
+ v4 = v4 + v6 + 1 >> 1;
3079
+ v6 = t;
3080
+ t = v7 + v5 + 1 >> 1;
3081
+ v5 = v7 - v5 + 1 >> 1;
3082
+ v7 = t;
3083
+ t = v0 - v3 + 1 >> 1;
3084
+ v0 = v0 + v3 + 1 >> 1;
3085
+ v3 = t;
3086
+ t = v1 - v2 + 1 >> 1;
3087
+ v1 = v1 + v2 + 1 >> 1;
3088
+ v2 = t;
3089
+ t = v4 * dctSin3 + v7 * dctCos3 + 2048 >> 12;
3090
+ v4 = v4 * dctCos3 - v7 * dctSin3 + 2048 >> 12;
3091
+ v7 = t;
3092
+ t = v5 * dctSin1 + v6 * dctCos1 + 2048 >> 12;
3093
+ v5 = v5 * dctCos1 - v6 * dctSin1 + 2048 >> 12;
3094
+ v6 = t;
3095
+ p[0 + col] = v0 + v7;
3096
+ p[56 + col] = v0 - v7;
3097
+ p[8 + col] = v1 + v6;
3098
+ p[48 + col] = v1 - v6;
3099
+ p[16 + col] = v2 + v5;
3100
+ p[40 + col] = v2 - v5;
3101
+ p[24 + col] = v3 + v4;
3102
+ p[32 + col] = v3 - v4;
3103
+ }
3104
+ for (i3 = 0; i3 < 64; ++i3) {
3105
+ var sample2 = 128 + (p[i3] + 8 >> 4);
3106
+ dataOut[i3] = sample2 < 0 ? 0 : sample2 > 255 ? 255 : sample2;
3107
+ }
3108
+ }
3109
+ requestMemoryAllocation(samplesPerLine * blocksPerColumn * 8);
3110
+ var i2, j;
3111
+ for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
3112
+ var scanLine = blockRow << 3;
3113
+ for (i2 = 0; i2 < 8; i2++) lines.push(new Uint8Array(samplesPerLine));
3114
+ for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
3115
+ quantizeAndInverse(component.blocks[blockRow][blockCol], r, R);
3116
+ var offset = 0, sample = blockCol << 3;
3117
+ for (j = 0; j < 8; j++) {
3118
+ var line = lines[scanLine + j];
3119
+ for (i2 = 0; i2 < 8; i2++) line[sample + i2] = r[offset++];
3120
+ }
3121
+ }
3122
+ }
3123
+ return lines;
3124
+ }
3125
+ function clampTo8bit(a2) {
3126
+ return a2 < 0 ? 0 : a2 > 255 ? 255 : a2;
3127
+ }
3128
+ constructor.prototype = {
3129
+ load: function load2(path4) {
3130
+ var xhr = new XMLHttpRequest();
3131
+ xhr.open("GET", path4, true);
3132
+ xhr.responseType = "arraybuffer";
3133
+ xhr.onload = (function() {
3134
+ var data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
3135
+ this.parse(data);
3136
+ if (this.onload) this.onload();
3137
+ }).bind(this);
3138
+ xhr.send(null);
3139
+ },
3140
+ parse: function parse3(data) {
3141
+ var maxResolutionInPixels = this.opts.maxResolutionInMP * 1e3 * 1e3, offset = 0;
3142
+ data.length;
3143
+ function readUint16() {
3144
+ var value = data[offset] << 8 | data[offset + 1];
3145
+ offset += 2;
3146
+ return value;
3147
+ }
3148
+ function readDataBlock() {
3149
+ var length2 = readUint16();
3150
+ var array2 = data.subarray(offset, offset + length2 - 2);
3151
+ offset += array2.length;
3152
+ return array2;
3153
+ }
3154
+ function prepareComponents(frame2) {
3155
+ var maxH2 = 1, maxV2 = 1;
3156
+ var component2, componentId2;
3157
+ for (componentId2 in frame2.components) if (frame2.components.hasOwnProperty(componentId2)) {
3158
+ component2 = frame2.components[componentId2];
3159
+ if (maxH2 < component2.h) maxH2 = component2.h;
3160
+ if (maxV2 < component2.v) maxV2 = component2.v;
3161
+ }
3162
+ var mcusPerLine = Math.ceil(frame2.samplesPerLine / 8 / maxH2);
3163
+ var mcusPerColumn = Math.ceil(frame2.scanLines / 8 / maxV2);
3164
+ for (componentId2 in frame2.components) if (frame2.components.hasOwnProperty(componentId2)) {
3165
+ component2 = frame2.components[componentId2];
3166
+ var blocksPerLine = Math.ceil(Math.ceil(frame2.samplesPerLine / 8) * component2.h / maxH2);
3167
+ var blocksPerColumn = Math.ceil(Math.ceil(frame2.scanLines / 8) * component2.v / maxV2);
3168
+ var blocksPerLineForMcu = mcusPerLine * component2.h;
3169
+ var blocksPerColumnForMcu = mcusPerColumn * component2.v;
3170
+ var blocksToAllocate = blocksPerColumnForMcu * blocksPerLineForMcu;
3171
+ var blocks = [];
3172
+ requestMemoryAllocation(blocksToAllocate * 256);
3173
+ for (var i3 = 0; i3 < blocksPerColumnForMcu; i3++) {
3174
+ var row = [];
3175
+ for (var j2 = 0; j2 < blocksPerLineForMcu; j2++) row.push(/* @__PURE__ */ new Int32Array(64));
3176
+ blocks.push(row);
3177
+ }
3178
+ component2.blocksPerLine = blocksPerLine;
3179
+ component2.blocksPerColumn = blocksPerColumn;
3180
+ component2.blocks = blocks;
3181
+ }
3182
+ frame2.maxH = maxH2;
3183
+ frame2.maxV = maxV2;
3184
+ frame2.mcusPerLine = mcusPerLine;
3185
+ frame2.mcusPerColumn = mcusPerColumn;
3186
+ }
3187
+ var jfif = null;
3188
+ var adobe = null;
3189
+ var frame, resetInterval;
3190
+ var quantizationTables = [], frames = [];
3191
+ var huffmanTablesAC = [], huffmanTablesDC = [];
3192
+ var fileMarker = readUint16();
3193
+ var malformedDataOffset = -1;
3194
+ this.comments = [];
3195
+ if (fileMarker != 65496) throw new Error("SOI not found");
3196
+ fileMarker = readUint16();
3197
+ while (fileMarker != 65497) {
3198
+ var i2, j;
3199
+ switch (fileMarker) {
3200
+ case 65280: break;
3201
+ case 65504:
3202
+ case 65505:
3203
+ case 65506:
3204
+ case 65507:
3205
+ case 65508:
3206
+ case 65509:
3207
+ case 65510:
3208
+ case 65511:
3209
+ case 65512:
3210
+ case 65513:
3211
+ case 65514:
3212
+ case 65515:
3213
+ case 65516:
3214
+ case 65517:
3215
+ case 65518:
3216
+ case 65519:
3217
+ case 65534:
3218
+ var appData = readDataBlock();
3219
+ if (fileMarker === 65534) {
3220
+ var comment = String.fromCharCode.apply(null, appData);
3221
+ this.comments.push(comment);
3222
+ }
3223
+ if (fileMarker === 65504) {
3224
+ if (appData[0] === 74 && appData[1] === 70 && appData[2] === 73 && appData[3] === 70 && appData[4] === 0) jfif = {
3225
+ version: {
3226
+ major: appData[5],
3227
+ minor: appData[6]
3228
+ },
3229
+ densityUnits: appData[7],
3230
+ xDensity: appData[8] << 8 | appData[9],
3231
+ yDensity: appData[10] << 8 | appData[11],
3232
+ thumbWidth: appData[12],
3233
+ thumbHeight: appData[13],
3234
+ thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13])
3235
+ };
3236
+ }
3237
+ if (fileMarker === 65505) {
3238
+ if (appData[0] === 69 && appData[1] === 120 && appData[2] === 105 && appData[3] === 102 && appData[4] === 0) this.exifBuffer = appData.subarray(5, appData.length);
3239
+ }
3240
+ if (fileMarker === 65518) {
3241
+ if (appData[0] === 65 && appData[1] === 100 && appData[2] === 111 && appData[3] === 98 && appData[4] === 101 && appData[5] === 0) adobe = {
3242
+ version: appData[6],
3243
+ flags0: appData[7] << 8 | appData[8],
3244
+ flags1: appData[9] << 8 | appData[10],
3245
+ transformCode: appData[11]
3246
+ };
3247
+ }
3248
+ break;
3249
+ case 65499:
3250
+ var quantizationTablesEnd = readUint16() + offset - 2;
3251
+ while (offset < quantizationTablesEnd) {
3252
+ var quantizationTableSpec = data[offset++];
3253
+ requestMemoryAllocation(256);
3254
+ var tableData = /* @__PURE__ */ new Int32Array(64);
3255
+ if (quantizationTableSpec >> 4 === 0) for (j = 0; j < 64; j++) {
3256
+ var z2 = dctZigZag[j];
3257
+ tableData[z2] = data[offset++];
3258
+ }
3259
+ else if (quantizationTableSpec >> 4 === 1) for (j = 0; j < 64; j++) {
3260
+ var z2 = dctZigZag[j];
3261
+ tableData[z2] = readUint16();
3262
+ }
3263
+ else throw new Error("DQT: invalid table spec");
3264
+ quantizationTables[quantizationTableSpec & 15] = tableData;
3265
+ }
3266
+ break;
3267
+ case 65472:
3268
+ case 65473:
3269
+ case 65474:
3270
+ readUint16();
3271
+ frame = {};
3272
+ frame.extended = fileMarker === 65473;
3273
+ frame.progressive = fileMarker === 65474;
3274
+ frame.precision = data[offset++];
3275
+ frame.scanLines = readUint16();
3276
+ frame.samplesPerLine = readUint16();
3277
+ frame.components = {};
3278
+ frame.componentsOrder = [];
3279
+ var pixelsInFrame = frame.scanLines * frame.samplesPerLine;
3280
+ if (pixelsInFrame > maxResolutionInPixels) {
3281
+ var exceededAmount = Math.ceil((pixelsInFrame - maxResolutionInPixels) / 1e6);
3282
+ throw new Error(`maxResolutionInMP limit exceeded by ${exceededAmount}MP`);
3283
+ }
3284
+ var componentsCount = data[offset++], componentId;
3285
+ for (i2 = 0; i2 < componentsCount; i2++) {
3286
+ componentId = data[offset];
3287
+ var h2 = data[offset + 1] >> 4;
3288
+ var v = data[offset + 1] & 15;
3289
+ var qId = data[offset + 2];
3290
+ if (h2 <= 0 || v <= 0) throw new Error("Invalid sampling factor, expected values above 0");
3291
+ frame.componentsOrder.push(componentId);
3292
+ frame.components[componentId] = {
3293
+ h: h2,
3294
+ v,
3295
+ quantizationIdx: qId
3296
+ };
3297
+ offset += 3;
3298
+ }
3299
+ prepareComponents(frame);
3300
+ frames.push(frame);
3301
+ break;
3302
+ case 65476:
3303
+ var huffmanLength = readUint16();
3304
+ for (i2 = 2; i2 < huffmanLength;) {
3305
+ var huffmanTableSpec = data[offset++];
3306
+ var codeLengths = /* @__PURE__ */ new Uint8Array(16);
3307
+ var codeLengthSum = 0;
3308
+ for (j = 0; j < 16; j++, offset++) codeLengthSum += codeLengths[j] = data[offset];
3309
+ requestMemoryAllocation(16 + codeLengthSum);
3310
+ var huffmanValues = new Uint8Array(codeLengthSum);
3311
+ for (j = 0; j < codeLengthSum; j++, offset++) huffmanValues[j] = data[offset];
3312
+ i2 += 17 + codeLengthSum;
3313
+ (huffmanTableSpec >> 4 === 0 ? huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] = buildHuffmanTable(codeLengths, huffmanValues);
3314
+ }
3315
+ break;
3316
+ case 65501:
3317
+ readUint16();
3318
+ resetInterval = readUint16();
3319
+ break;
3320
+ case 65500:
3321
+ readUint16();
3322
+ readUint16();
3323
+ break;
3324
+ case 65498:
3325
+ readUint16();
3326
+ var selectorsCount = data[offset++];
3327
+ var components = [], component;
3328
+ for (i2 = 0; i2 < selectorsCount; i2++) {
3329
+ component = frame.components[data[offset++]];
3330
+ var tableSpec = data[offset++];
3331
+ component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
3332
+ component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
3333
+ components.push(component);
3334
+ }
3335
+ var spectralStart = data[offset++];
3336
+ var spectralEnd = data[offset++];
3337
+ var successiveApproximation = data[offset++];
3338
+ var processed = decodeScan(data, offset, frame, components, resetInterval, spectralStart, spectralEnd, successiveApproximation >> 4, successiveApproximation & 15, this.opts);
3339
+ offset += processed;
3340
+ break;
3341
+ case 65535:
3342
+ if (data[offset] !== 255) offset--;
3343
+ break;
3344
+ default:
3345
+ if (data[offset - 3] == 255 && data[offset - 2] >= 192 && data[offset - 2] <= 254) {
3346
+ offset -= 3;
3347
+ break;
3348
+ } else if (fileMarker === 224 || fileMarker == 225) {
3349
+ if (malformedDataOffset !== -1) throw new Error(`first unknown JPEG marker at offset ${malformedDataOffset.toString(16)}, second unknown JPEG marker ${fileMarker.toString(16)} at offset ${(offset - 1).toString(16)}`);
3350
+ malformedDataOffset = offset - 1;
3351
+ const nextOffset = readUint16();
3352
+ if (data[offset + nextOffset - 2] === 255) {
3353
+ offset += nextOffset - 2;
3354
+ break;
3355
+ }
3356
+ }
3357
+ throw new Error("unknown JPEG marker " + fileMarker.toString(16));
3358
+ }
3359
+ fileMarker = readUint16();
3360
+ }
3361
+ if (frames.length != 1) throw new Error("only single frame JPEGs supported");
3362
+ for (var i2 = 0; i2 < frames.length; i2++) {
3363
+ var cp = frames[i2].components;
3364
+ for (var j in cp) {
3365
+ cp[j].quantizationTable = quantizationTables[cp[j].quantizationIdx];
3366
+ delete cp[j].quantizationIdx;
3367
+ }
3368
+ }
3369
+ this.width = frame.samplesPerLine;
3370
+ this.height = frame.scanLines;
3371
+ this.jfif = jfif;
3372
+ this.adobe = adobe;
3373
+ this.components = [];
3374
+ for (var i2 = 0; i2 < frame.componentsOrder.length; i2++) {
3375
+ var component = frame.components[frame.componentsOrder[i2]];
3376
+ this.components.push({
3377
+ lines: buildComponentData(frame, component),
3378
+ scaleX: component.h / frame.maxH,
3379
+ scaleY: component.v / frame.maxV
3380
+ });
3381
+ }
3382
+ },
3383
+ getData: function getData(width, height) {
3384
+ var scaleX = this.width / width, scaleY = this.height / height;
3385
+ var component1, component2, component3, component4;
3386
+ var component1Line, component2Line, component3Line, component4Line;
3387
+ var x, y;
3388
+ var offset = 0;
3389
+ var Y, Cb, Cr, K, C, M, Ye, R, G, B;
3390
+ var colorTransform;
3391
+ var dataLength = width * height * this.components.length;
3392
+ requestMemoryAllocation(dataLength);
3393
+ var data = new Uint8Array(dataLength);
3394
+ switch (this.components.length) {
3395
+ case 1:
3396
+ component1 = this.components[0];
3397
+ for (y = 0; y < height; y++) {
3398
+ component1Line = component1.lines[0 | y * component1.scaleY * scaleY];
3399
+ for (x = 0; x < width; x++) {
3400
+ Y = component1Line[0 | x * component1.scaleX * scaleX];
3401
+ data[offset++] = Y;
3402
+ }
3403
+ }
3404
+ break;
3405
+ case 2:
3406
+ component1 = this.components[0];
3407
+ component2 = this.components[1];
3408
+ for (y = 0; y < height; y++) {
3409
+ component1Line = component1.lines[0 | y * component1.scaleY * scaleY];
3410
+ component2Line = component2.lines[0 | y * component2.scaleY * scaleY];
3411
+ for (x = 0; x < width; x++) {
3412
+ Y = component1Line[0 | x * component1.scaleX * scaleX];
3413
+ data[offset++] = Y;
3414
+ Y = component2Line[0 | x * component2.scaleX * scaleX];
3415
+ data[offset++] = Y;
3416
+ }
3417
+ }
3418
+ break;
3419
+ case 3:
3420
+ colorTransform = true;
3421
+ if (this.adobe && this.adobe.transformCode) colorTransform = true;
3422
+ else if (typeof this.opts.colorTransform !== "undefined") colorTransform = !!this.opts.colorTransform;
3423
+ component1 = this.components[0];
3424
+ component2 = this.components[1];
3425
+ component3 = this.components[2];
3426
+ for (y = 0; y < height; y++) {
3427
+ component1Line = component1.lines[0 | y * component1.scaleY * scaleY];
3428
+ component2Line = component2.lines[0 | y * component2.scaleY * scaleY];
3429
+ component3Line = component3.lines[0 | y * component3.scaleY * scaleY];
3430
+ for (x = 0; x < width; x++) {
3431
+ if (!colorTransform) {
3432
+ R = component1Line[0 | x * component1.scaleX * scaleX];
3433
+ G = component2Line[0 | x * component2.scaleX * scaleX];
3434
+ B = component3Line[0 | x * component3.scaleX * scaleX];
3435
+ } else {
3436
+ Y = component1Line[0 | x * component1.scaleX * scaleX];
3437
+ Cb = component2Line[0 | x * component2.scaleX * scaleX];
3438
+ Cr = component3Line[0 | x * component3.scaleX * scaleX];
3439
+ R = clampTo8bit(Y + 1.402 * (Cr - 128));
3440
+ G = clampTo8bit(Y - .3441363 * (Cb - 128) - .71413636 * (Cr - 128));
3441
+ B = clampTo8bit(Y + 1.772 * (Cb - 128));
3442
+ }
3443
+ data[offset++] = R;
3444
+ data[offset++] = G;
3445
+ data[offset++] = B;
3446
+ }
3447
+ }
3448
+ break;
3449
+ case 4:
3450
+ if (!this.adobe) throw new Error("Unsupported color mode (4 components)");
3451
+ colorTransform = false;
3452
+ if (this.adobe && this.adobe.transformCode) colorTransform = true;
3453
+ else if (typeof this.opts.colorTransform !== "undefined") colorTransform = !!this.opts.colorTransform;
3454
+ component1 = this.components[0];
3455
+ component2 = this.components[1];
3456
+ component3 = this.components[2];
3457
+ component4 = this.components[3];
3458
+ for (y = 0; y < height; y++) {
3459
+ component1Line = component1.lines[0 | y * component1.scaleY * scaleY];
3460
+ component2Line = component2.lines[0 | y * component2.scaleY * scaleY];
3461
+ component3Line = component3.lines[0 | y * component3.scaleY * scaleY];
3462
+ component4Line = component4.lines[0 | y * component4.scaleY * scaleY];
3463
+ for (x = 0; x < width; x++) {
3464
+ if (!colorTransform) {
3465
+ C = component1Line[0 | x * component1.scaleX * scaleX];
3466
+ M = component2Line[0 | x * component2.scaleX * scaleX];
3467
+ Ye = component3Line[0 | x * component3.scaleX * scaleX];
3468
+ K = component4Line[0 | x * component4.scaleX * scaleX];
3469
+ } else {
3470
+ Y = component1Line[0 | x * component1.scaleX * scaleX];
3471
+ Cb = component2Line[0 | x * component2.scaleX * scaleX];
3472
+ Cr = component3Line[0 | x * component3.scaleX * scaleX];
3473
+ K = component4Line[0 | x * component4.scaleX * scaleX];
3474
+ C = 255 - clampTo8bit(Y + 1.402 * (Cr - 128));
3475
+ M = 255 - clampTo8bit(Y - .3441363 * (Cb - 128) - .71413636 * (Cr - 128));
3476
+ Ye = 255 - clampTo8bit(Y + 1.772 * (Cb - 128));
3477
+ }
3478
+ data[offset++] = 255 - C;
3479
+ data[offset++] = 255 - M;
3480
+ data[offset++] = 255 - Ye;
3481
+ data[offset++] = 255 - K;
3482
+ }
3483
+ }
3484
+ break;
3485
+ default: throw new Error("Unsupported color mode");
3486
+ }
3487
+ return data;
3488
+ },
3489
+ copyToImageData: function copyToImageData(imageData, formatAsRGBA) {
3490
+ var width = imageData.width, height = imageData.height;
3491
+ var imageDataArray = imageData.data;
3492
+ var data = this.getData(width, height);
3493
+ var i2 = 0, j = 0, x, y;
3494
+ var Y, K, C, M, R, G, B;
3495
+ switch (this.components.length) {
3496
+ case 1:
3497
+ for (y = 0; y < height; y++) for (x = 0; x < width; x++) {
3498
+ Y = data[i2++];
3499
+ imageDataArray[j++] = Y;
3500
+ imageDataArray[j++] = Y;
3501
+ imageDataArray[j++] = Y;
3502
+ if (formatAsRGBA) imageDataArray[j++] = 255;
3503
+ }
3504
+ break;
3505
+ case 3:
3506
+ for (y = 0; y < height; y++) for (x = 0; x < width; x++) {
3507
+ R = data[i2++];
3508
+ G = data[i2++];
3509
+ B = data[i2++];
3510
+ imageDataArray[j++] = R;
3511
+ imageDataArray[j++] = G;
3512
+ imageDataArray[j++] = B;
3513
+ if (formatAsRGBA) imageDataArray[j++] = 255;
3514
+ }
3515
+ break;
3516
+ case 4:
3517
+ for (y = 0; y < height; y++) for (x = 0; x < width; x++) {
3518
+ C = data[i2++];
3519
+ M = data[i2++];
3520
+ Y = data[i2++];
3521
+ K = data[i2++];
3522
+ R = 255 - clampTo8bit(C * (1 - K / 255) + K);
3523
+ G = 255 - clampTo8bit(M * (1 - K / 255) + K);
3524
+ B = 255 - clampTo8bit(Y * (1 - K / 255) + K);
3525
+ imageDataArray[j++] = R;
3526
+ imageDataArray[j++] = G;
3527
+ imageDataArray[j++] = B;
3528
+ if (formatAsRGBA) imageDataArray[j++] = 255;
3529
+ }
3530
+ break;
3531
+ default: throw new Error("Unsupported color mode");
3532
+ }
3533
+ }
3534
+ };
3535
+ var totalBytesAllocated = 0;
3536
+ var maxMemoryUsageBytes = 0;
3537
+ function requestMemoryAllocation(increaseAmount = 0) {
3538
+ var totalMemoryImpactBytes = totalBytesAllocated + increaseAmount;
3539
+ if (totalMemoryImpactBytes > maxMemoryUsageBytes) {
3540
+ var exceededAmount = Math.ceil((totalMemoryImpactBytes - maxMemoryUsageBytes) / 1024 / 1024);
3541
+ throw new Error(`maxMemoryUsageInMB limit exceeded by at least ${exceededAmount}MB`);
3542
+ }
3543
+ totalBytesAllocated = totalMemoryImpactBytes;
3544
+ }
3545
+ constructor.resetMaxMemoryUsage = function(maxMemoryUsageBytes_) {
3546
+ totalBytesAllocated = 0;
3547
+ maxMemoryUsageBytes = maxMemoryUsageBytes_;
3548
+ };
3549
+ constructor.getBytesAllocated = function() {
3550
+ return totalBytesAllocated;
3551
+ };
3552
+ constructor.requestMemoryAllocation = requestMemoryAllocation;
3553
+ return constructor;
3554
+ })();
3555
+ if (typeof module2 !== "undefined") module2.exports = decode3;
3556
+ else if (typeof window !== "undefined") {
3557
+ window["jpeg-js"] = window["jpeg-js"] || {};
3558
+ window["jpeg-js"].decode = decode3;
3559
+ }
3560
+ function decode3(jpegData, userOpts = {}) {
3561
+ var opts = {
3562
+ colorTransform: void 0,
3563
+ useTArray: false,
3564
+ formatAsRGBA: true,
3565
+ tolerantDecoding: true,
3566
+ maxResolutionInMP: 100,
3567
+ maxMemoryUsageInMB: 512,
3568
+ ...userOpts
3569
+ };
3570
+ var arr = new Uint8Array(jpegData);
3571
+ var decoder = new JpegImage();
3572
+ decoder.opts = opts;
3573
+ JpegImage.resetMaxMemoryUsage(opts.maxMemoryUsageInMB * 1024 * 1024);
3574
+ decoder.parse(arr);
3575
+ var channels = opts.formatAsRGBA ? 4 : 3;
3576
+ var bytesNeeded = decoder.width * decoder.height * channels;
3577
+ try {
3578
+ JpegImage.requestMemoryAllocation(bytesNeeded);
3579
+ var image = {
3580
+ width: decoder.width,
3581
+ height: decoder.height,
3582
+ exifBuffer: decoder.exifBuffer,
3583
+ data: opts.useTArray ? new Uint8Array(bytesNeeded) : Buffer.alloc(bytesNeeded)
3584
+ };
3585
+ if (decoder.comments.length > 0) image["comments"] = decoder.comments;
3586
+ } catch (err) {
3587
+ if (err instanceof RangeError) throw new Error("Could not allocate enough memory for the image. Required: " + bytesNeeded);
3588
+ if (err instanceof ReferenceError) {
3589
+ if (err.message === "Buffer is not defined") throw new Error("Buffer is not globally defined in this environment. Consider setting useTArray to true");
3590
+ }
3591
+ throw err;
3592
+ }
3593
+ decoder.copyToImageData(image, opts.formatAsRGBA);
3594
+ return image;
3595
+ }
3596
+ } })() };
3597
+ //#endregion
3598
+ //#region src/internal/pictures/motion.ts
3599
+ /**
3600
+ * Where a page's screen moves, from its screencast frames: each frame decoded and cut into
3601
+ * 16-pixel blocks, a block moving where a few of its pixels, sampled every other one, changed by
3602
+ * more than JPEG's noise in any color channel.
3603
+ * What moved among the frames before an input is the page's background, such as an idle
3604
+ * animation; foreground stillness ignores it.
3605
+ *
3606
+ * Frames use Playwright's bundled JPEG decoder. The package build ships that exact decoder
3607
+ * alone, keeping its Node utility bundle out of Workers. Source runs use its native export,
3608
+ * whose untyped entry point is declared by `utilsBundle.d.ts`.
3609
+ */
3610
+ const picture = (frame) => jpegjs.decode(frame.data, { useTArray: true });
3611
+ /** A sample changed where any color channel moved more than this, beyond JPEG's noise. */
3612
+ const noise = 32;
3613
+ /** The blocks where two pictures differ, by `row * columns + column`; -1 for different sizes. */
3614
+ const moved = (from, to) => {
3615
+ if (from.width !== to.width || from.height !== to.height) return [-1];
3616
+ const columns = Math.ceil(from.width / 16);
3617
+ const counts = /* @__PURE__ */ new Map();
3618
+ const differs = (at) => Math.abs((from.data[at] ?? 0) - (to.data[at] ?? 0)) > noise;
3619
+ for (let y = 0; y < from.height; y += 2) for (let x = 0; x < from.width; x += 2) {
3620
+ const at = (y * from.width + x) * 4;
3621
+ const block = Math.floor(y / 16) * columns + Math.floor(x / 16);
3622
+ if (differs(at) || differs(at + 1) || differs(at + 2)) counts.set(block, (counts.get(block) ?? 0) + 1);
3623
+ }
3624
+ return [...counts].flatMap(([block, count]) => count >= 3 ? [block] : []);
3625
+ };
3626
+ /** The blocks that moved from one picture to the next and their neighbours; none past a resize. */
3627
+ const background = (pictures) => {
3628
+ const columns = Math.ceil((pictures[0]?.width ?? 0) / 16);
3629
+ const around = (block) => [
3630
+ -1,
3631
+ 0,
3632
+ 1
3633
+ ].flatMap((row) => [
3634
+ -1,
3635
+ 0,
3636
+ 1
3637
+ ].filter((step) => block >= 0 && block % columns + step >= 0).filter((step) => block % columns + step < columns).map((step) => block + row * columns + step));
3638
+ return new Set(pictures.flatMap((to, index) => moved(pictures[index - 1] ?? to, to).flatMap(around)));
3639
+ };
3640
+ /** Whether anything moved from one picture to another outside the background. */
3641
+ const stirs = (from, to, mask) => moved(from, to).some((block) => !mask.has(block));
3642
+ //#endregion
3643
+ //#region src/internal/reading/ready.ts
3644
+ /**
3645
+ * Waiting until a page is ready to be shown. The page's own evidence leads: one page-script call
3646
+ * that checks in the page until nothing is left to wait for, or the time is up. A caller that also
3647
+ * needs the screen still, such as after spinning a canvas game's reels, gives a quiet spell, and
3648
+ * the capture must then show no frame that long. A capture's silence cannot prove the last paint
3649
+ * arrived: a frame can be held on its way by a stalled connection, and Chromium sends frames only
3650
+ * while few acknowledgements are unanswered. So the spell counts only while every acknowledgement
3651
+ * is answered, and it ends with the page's evidence once more and, where the capture runs on a
3652
+ * connection of its own, a round trip there: each answer comes behind every frame sent before it on
3653
+ * its connection, and a frame that arrives first starts the spell again. Where the page's changes
3654
+ * are recorded, its evidence includes them: nothing in view may have changed for the spell, which a
3655
+ * DOM page's changes show better than its paint, and no stall on the way can hide. Content
3656
+ * stillness starts that record itself and ignores capture silence. Foreground stillness lets
3657
+ * frames keep coming but compares them, so that only motion outside what was moving before the
3658
+ * page's latest input counts; it still waits out unanswered acknowledgements while no frame
3659
+ * comes, and its last look follows the round trip too. Each new document starts a
3660
+ * new observation, while retries of destroyed script contexts share the caller's deadline.
3661
+ */
3662
+ const make$5 = (page, bridge, capture) => (options = {}) => {
3663
+ const timeout = options.timeout ?? Duration.seconds(15);
3664
+ const settled = (until) => Effect.suspend(() => bridge.evaluate("ready", scriptCall("ready", Math.max(0, Math.round(until - page.now())), options.quietMillis ?? 0, options.still === "content"))).pipe(Effect.retry({
3665
+ while: contextGone,
3666
+ schedule: Schedule.spaced(Duration.millis(10))
3667
+ }), 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) }))));
3668
+ const quiet = (millis, until) => Effect.gen(function* () {
3669
+ const running = yield* capture.active;
3670
+ const foreground = options.still === "foreground";
3671
+ let counts = yield* capture.stats();
3672
+ let stirred = running ? page.now() : Number.POSITIVE_INFINITY;
3673
+ let reference;
3674
+ const decoded = (frame) => Effect.try({
3675
+ try: () => picture(frame),
3676
+ catch: (error) => undispatched("ready", new Failed({ detail: `no picture: ${String(error)}` }))
3677
+ });
3678
+ const { inputAt } = page.activity;
3679
+ const document = Option.getOrUndefined(yield* capture.latest)?.document;
3680
+ const before = (foreground ? yield* capture.recent : []).filter((frame) => frame.hostTime < inputAt && frame.hostTime >= inputAt - 1500 && frame.document === document);
3681
+ const sampled = before.filter((_, index) => index % Math.ceil(before.length / 8) === 0);
3682
+ const mask = background(yield* Effect.forEach(sampled, decoded));
3683
+ const look = Effect.gen(function* () {
3684
+ const stats = yield* capture.stats();
3685
+ const latest = Option.getOrUndefined(yield* capture.latest);
3686
+ const arrived = stats.received !== counts.received;
3687
+ const unseen = stats.accepted - counts.accepted;
3688
+ counts = stats;
3689
+ if (latest === void 0 || !capture.current(latest)) stirred = page.now();
3690
+ else if (!foreground ? arrived || stats.ackBacklog > 0 : !arrived && stats.ackBacklog > 0) stirred = page.now();
3691
+ else if (foreground && (unseen > 0 || reference === void 0)) {
3692
+ const frames = (yield* capture.recent).slice(-Math.max(1, Math.min(8, unseen)));
3693
+ if (unseen > frames.length) stirred = page.now();
3694
+ for (const frame of frames) {
3695
+ const seen = yield* decoded(frame);
3696
+ if (reference === void 0 || stirs(reference, seen, mask)) {
3697
+ reference = seen;
3698
+ stirred = page.now();
3699
+ }
3700
+ }
3701
+ }
3702
+ return page.now() - stirred;
3703
+ });
3704
+ const spell = Effect.repeat(look, {
3705
+ schedule: Schedule.fixed(Duration.millis(foreground ? 100 : 25)),
3706
+ until: (still) => still >= millis
3707
+ });
3708
+ const capturing = running ? Effect.never : capture.stream().pipe(Stream.runDrain, Effect.andThen(Effect.never));
3709
+ const still = Effect.gen(function* () {
3710
+ for (;;) {
3711
+ yield* spell;
3712
+ const before = counts.received;
3713
+ yield* Effect.all([settled(until), capture.sync], {
3714
+ concurrency: 2,
3715
+ discard: true
3716
+ });
3717
+ if ((yield* look) >= millis && (foreground || counts.received === before)) return;
3718
+ }
3719
+ });
3720
+ return yield* Effect.raceFirst(capturing, still);
3721
+ });
3722
+ return page.lane.read("ready")(Effect.void).pipe(Effect.andThen(Effect.gen(function* () {
3723
+ const until = page.now() + Duration.toMillis(timeout);
3724
+ if (options.still === "content") yield* bridge.registerRecorder("ready");
3725
+ yield* settled(until);
3726
+ if (options.quietMillis !== void 0 && options.still !== "content") yield* quiet(options.quietMillis, until);
3727
+ }).pipe(page.within("ready", timeout))), page.span("Page.ready"), page.owned);
3728
+ };
3729
+ //#endregion
3730
+ //#region src/internal/timeline/changes.ts
3731
+ /**
3732
+ * Reading a page's record of what changed, in one call to the page. The first read also registers
3733
+ * the recorder with the page's own session, in the same round trip, so that every later document
3734
+ * records from its start. The page's times map to the owner's clock through the browser's one
3735
+ * clock mapping, as frames' do, which a browser's first read measures, once, where no capture has;
3736
+ * and a window can begin on the page's own clock where the previous one ended, so consecutive
3737
+ * reads neither miss nor repeat a change. A read takes its turn on the page as other reads do, and
3738
+ * identical ones in flight share it; none is kept for a later caller, since its window ends when
3739
+ * it was read.
3740
+ */
3741
+ const isChanges = Schema.is(Changes);
3742
+ const markOf = (time) => time === void 0 || typeof time === "number" ? time : isChanges(time) ? `cursor ${time.cursor}` : `frame ${time.timestamp ?? time.hostTime}`;
3743
+ const make$4 = (page, bridge, estimate) => {
3744
+ const reads = page.lane.shared("changes", false);
3745
+ return (options = {}) => reads(JSON.stringify([
3746
+ markOf(options.since),
3747
+ markOf(options.until),
3748
+ options.unmask ?? false
3749
+ ]), Effect.gen(function* () {
3750
+ const mapping = yield* estimate;
3751
+ const host = (epoch) => toHostTime(mapping, epoch);
3752
+ const onPage = (time) => typeof time === "number" ? time + mapping.offsetMillis : time.timestamp ?? time.hostTime + mapping.offsetMillis;
3753
+ const request = {
3754
+ since: options.since === void 0 ? null : isChanges(options.since) ? options.since.cursor : onPage(options.since),
3755
+ until: options.until === void 0 ? null : onPage(options.until),
3756
+ unmask: options.unmask ?? false
3757
+ };
3758
+ const [, result] = yield* Effect.all([bridge.registerRecorder("changes"), bridge.evaluate("changes", scriptCall("changes", request)).pipe(Effect.flatMap(decodeWith("changes", ChangesResultSchema)))], { concurrency: "unbounded" });
3759
+ yield* Effect.annotateCurrentSpan({
3760
+ changes: result.records.length,
3761
+ dropped: result.dropped
3762
+ });
3763
+ const later = (epoch) => epoch === void 0 ? void 0 : host(epoch);
3764
+ return new Changes({
3765
+ document: bridge.frameTag().document,
3766
+ from: host(result.from),
3767
+ until: host(result.until),
3768
+ cursor: result.until,
3769
+ dropped: result.dropped,
3770
+ changes: result.records.map((one) => new Change({
3771
+ ...one,
3772
+ startedAt: host(one.startedAt),
3773
+ at: host(one.at),
3774
+ earlier: later(one.earlier),
3775
+ cause: later(one.cause)
3776
+ }))
3777
+ });
3778
+ }).pipe(page.within("changes"))).pipe(page.span("Page.changes"), page.owned);
3779
+ };
3780
+ //#endregion
3781
+ //#region src/internal/timeline/window.ts
3782
+ /**
3783
+ * A page's window over its three tracks on the browser's host clock: the events and frames it
3784
+ * keeps, at no call, and one read of its changes, the first starting its record. A read that fails
3785
+ * is missing, with why, and the window is still made.
3786
+ */
3787
+ const isWindow = Schema.is(Window);
3788
+ const isFrame = Schema.is(Frame);
3789
+ const isTrackEvent = Schema.is(TrackEvent);
3790
+ /**
3791
+ * Where a window asked at `now` starts and ends: never after now, nor starting after it ends.
3792
+ * Nothing for a bound that is not finite, or a negative reach back.
3793
+ */
3794
+ const boundsOf = ({ since, until }, now) => {
3795
+ const end = until === void 0 ? now : isFrame(until) ? until.hostTime : Math.min(until, now);
3796
+ const start = isWindow(since) ? since.until : isFrame(since) ? since.hostTime : !Duration.isDuration(since) ? since : Duration.isNegative(since) ? NaN : end - Duration.toMillis(since);
3797
+ return Number.isFinite(start) && Number.isFinite(end) ? Option.some({
3798
+ since: Math.min(start, end),
3799
+ until: end
3800
+ }) : Option.none();
3801
+ };
3802
+ /**
3803
+ * A window's share of the kept tracks: events after its start, frames painted from it, both up to
3804
+ * its end, so consecutive windows hold each event once and the next begins with the frame the last
3805
+ * ended on. Pointer, key and wheel input is the presentation track, not an event here.
3806
+ */
3807
+ const within = ({ since, until }, events, frames) => ({
3808
+ events: events.filter((event) => event.at > since && event.at <= until && !isTrackEvent(event)),
3809
+ frames: frames.filter((frame) => frame.hostTime >= since && frame.hostTime <= until)
3810
+ });
3811
+ const make$3 = (page, read, frames) => ({ since, until, unmask }) => Effect.gen(function* () {
3812
+ const asked = boundsOf({
3813
+ since,
3814
+ until
3815
+ }, page.now());
3816
+ if (Option.isNone(asked)) return yield* refuse("window", "a window's bounds must be finite, its reach not negative");
3817
+ const bounds = asked.value;
3818
+ const changes = yield* Effect.result(read({
3819
+ since: isWindow(since) ? since.changes ?? bounds.since : isFrame(since) ? since : bounds.since,
3820
+ until: isFrame(until) ? until : bounds.until,
3821
+ unmask
3822
+ }));
3823
+ return new Window({
3824
+ page: page.id,
3825
+ ...bounds,
3826
+ ...within(bounds, yield* page.recentEvents, yield* frames),
3827
+ changes: Result.getOrUndefined(changes),
3828
+ missing: Option.toArray(Result.getFailure(changes))
3829
+ });
3830
+ }).pipe(page.span("Page.window"), page.owned);
3831
+ //#endregion
3832
+ //#region src/internal/page/navigation.ts
3833
+ /**
3834
+ * Moving a page between documents: going to an address, back through its history, or reloading.
3835
+ * Each move is an action, so it takes the page's turn, goes through the input guard and is
3836
+ * recorded, but it sends no input.
3837
+ */
3838
+ const make$2 = (page, perform, preparePolicy) => {
3839
+ const { playwright, settings, native } = page;
3840
+ const { send } = page.protocol;
3841
+ const navigation = (name, run, address) => {
3842
+ const url = redact(address);
3843
+ return perform(name, {
3844
+ target: url,
3845
+ input: false
3846
+ }, settings.navigationTimeout, preparePolicy(name, { target: url }, [], { destination: url }), (marks) => marks.sent.pipe(Effect.andThen(native(name, run).pipe(Effect.onError(() => page.stopLoading))), Effect.mapError((error) => error.reason._tag === "Failed" && /net::|NS_ERROR|Cannot navigate/i.test(error.reason.detail) ? new BrowserError({
3847
+ operation: name,
3848
+ reason: new NavigationFailed({
3849
+ url,
3850
+ detail: error.reason.detail
3851
+ }),
3852
+ dispatched: true
3853
+ }) : error), Effect.asVoid));
3854
+ };
3855
+ const history = native("back", () => send("Page.getNavigationHistory"));
3856
+ const traversingHistory = history.pipe(Effect.retry({
3857
+ schedule: Schedule.spaced(Duration.millis(50)),
3858
+ while: (error) => error.reason._tag === "Failed" && /not attached to an active page/i.test(error.reason.detail)
3859
+ }));
3860
+ const title = native("title", () => send("Page.getNavigationHistory")).pipe(Effect.map(({ entries, currentIndex }) => entries[currentIndex]?.title ?? ""));
3861
+ const noPrevious = failWith("back", new NotFound({ target: "a previous page in this tab" }));
3862
+ const prepareBack = Effect.gen(function* () {
3863
+ const before = yield* history;
3864
+ const current = before.entries[before.currentIndex];
3865
+ const previous = before.entries[before.currentIndex - 1];
3866
+ if (previous === void 0) return yield* noPrevious;
3867
+ const plan = yield* preparePolicy("back", {}, [], { destination: redact(previous.url) });
3868
+ return {
3869
+ request: plan.request,
3870
+ validate: Effect.gen(function* () {
3871
+ const approval = yield* plan.validate;
3872
+ const latest = yield* history;
3873
+ if (latest.currentIndex !== before.currentIndex || latest.entries[latest.currentIndex]?.id !== current?.id || latest.entries[latest.currentIndex - 1]?.id !== previous.id || latest.entries[latest.currentIndex - 1]?.url !== previous.url) return yield* failWith("back", new NotActionable({ detail: "the navigation history changed while input policy was pending" }));
3874
+ return approval;
3875
+ })
3876
+ };
3877
+ });
3878
+ const goBack = (marks) => Effect.gen(function* () {
3879
+ const before = yield* history;
3880
+ const current = before.entries[before.currentIndex];
3881
+ const previous = before.entries[before.currentIndex - 1];
3882
+ if (current === void 0 || previous === void 0) return yield* noPrevious;
3883
+ const navigated = yield* Deferred.make();
3884
+ const onNavigation = () => {
3885
+ Deferred.doneUnsafe(navigated, Effect.void);
3886
+ };
3887
+ yield* Effect.acquireUseRelease(Effect.sync(() => playwright.on("framenavigated", onNavigation)), () => marks.sent.pipe(Effect.andThen(Effect.gen(function* () {
3888
+ yield* native("back", () => send("Page.navigateToHistoryEntry", { entryId: previous.id }));
3889
+ yield* traversingHistory.pipe(Effect.repeat({
3890
+ schedule: Schedule.spaced(Duration.millis(50)),
3891
+ until: (latest) => latest.entries[latest.currentIndex]?.id !== current.id
3892
+ }));
3893
+ yield* Deferred.await(navigated);
3894
+ yield* native("back", () => playwright.waitForLoadState("domcontentloaded", { timeout: 0 })).pipe(Effect.provideService(Current, void 0));
3895
+ }).pipe(Effect.onError(() => page.stopLoading)))), () => Effect.sync(() => playwright.off("framenavigated", onNavigation)));
3896
+ });
3897
+ const goto = (url) => {
3898
+ const parsed = parse(url);
3899
+ if (parsed === null || ![
3900
+ "http:",
3901
+ "https:",
3902
+ "about:",
3903
+ "data:",
3904
+ "file:"
3905
+ ].includes(parsed.protocol)) return refuse("navigate", `"${redact(url)}" is not a URL`);
3906
+ return navigation("navigate", () => playwright.goto(parsed.href, {
3907
+ waitUntil: "domcontentloaded",
3908
+ timeout: 0
3909
+ }), parsed.href);
3910
+ };
3911
+ return {
3912
+ goto,
3913
+ back: perform("back", {
3914
+ target: "back",
3915
+ input: false
3916
+ }, settings.navigationTimeout, prepareBack, goBack),
3917
+ reload: Effect.suspend(() => navigation("reload", () => playwright.reload({
3918
+ waitUntil: "domcontentloaded",
3919
+ timeout: 0
3920
+ }), playwright.url())),
3921
+ title
3922
+ };
3923
+ };
3924
+ //#endregion
3925
+ //#region src/internal/page/viewport.ts
3926
+ /**
3927
+ * The viewport's size in CSS pixels. Playwright knows it only for a context it created; over CDP it
3928
+ * reports none, so the page answers, and so does a picture taken at one device pixel per CSS
3929
+ * pixel. Pointer bounds, page scrolls, crops, capture size and frame reuse all read this one
3930
+ * source; the latest answer serves checks that cannot wait for the page.
3931
+ */
3932
+ const make$1 = (page, bridge) => {
3933
+ const { playwright } = page;
3934
+ let measuredViewport = null;
3935
+ const knownViewport = () => playwright.viewportSize() ?? measuredViewport;
3936
+ const viewportFor = (operation) => Effect.suspend(() => {
3937
+ const known = playwright.viewportSize();
3938
+ return known === null ? bridge.evaluate(operation, scriptCall("viewport")).pipe(Effect.flatMap(decodeWith(operation, ViewportResultSchema)), Effect.tap((measured) => Effect.sync(() => {
3939
+ measuredViewport = measured;
3940
+ }))) : Effect.succeed(known);
3941
+ });
3942
+ /** The size of an unscaled picture of the whole viewport, which is the viewport's own. */
3943
+ const remember = (size) => {
3944
+ measuredViewport = {
3945
+ width: size.width,
3946
+ height: size.height
3947
+ };
3948
+ };
3949
+ return {
3950
+ knownViewport,
3951
+ viewportFor,
3952
+ remember
3953
+ };
3954
+ };
3955
+ //#endregion
3956
+ //#region src/internal/page/page.ts
3957
+ /**
3958
+ * The page implementation behind `Page.Page`. `Browser` constructs pages and owns the state they
3959
+ * share (the clock mapping and event publication), so construction stays internal. A page is
3960
+ * assembled here from its domains: the script bridge, pictures, reading, its timeline, input and
3961
+ * navigation.
3962
+ * What a presenter and a stage need of a page, and no caller should, it keeps out of sight.
3963
+ */
3964
+ /** A page this library built: it keeps its internals where only this module reaches them. */
3965
+ var Built = class Built {
3966
+ #internals;
3967
+ constructor(internals) {
3968
+ this.#internals = internals;
3969
+ }
3970
+ static internalsOf(page) {
3971
+ return page instanceof Built ? page.#internals : void 0;
3972
+ }
3973
+ };
3974
+ /** What a page this library built keeps out of sight; nothing for any other `Page`. */
3975
+ const internalsOf = (page) => Built.internalsOf(page);
3976
+ /** `view`, made from `page` with some of its operations replaced, keeping what `page` keeps out of sight. */
3977
+ const viewOf = (page, view) => {
3978
+ const internals = internalsOf(page);
3979
+ return internals === void 0 ? view : Object.assign(new Built(internals), view);
3980
+ };
3981
+ const make = Effect.fnUntraced(function* (options) {
3982
+ const page = make$20(options, yield* Effect.scope);
3983
+ const { id, playwright, native, owned, within, lane, span } = page;
3984
+ const bridge = yield* make$18(page);
3985
+ const viewport = make$1(page, bridge);
3986
+ const input = make$10(page, bridge, viewport);
3987
+ const pictures = yield* make$7(page, bridge, viewport);
3988
+ const reading = yield* make$6(page, bridge);
3989
+ const navigation = make$2(page, input.perform, input.preparePolicy);
3990
+ const { capture } = pictures;
3991
+ const changes = make$4(page, bridge, pictures.estimate);
3992
+ const titles = lane.shared("title", true);
3993
+ const viewports = lane.shared("viewport", true);
3994
+ const state = Effect.map(capture.latest, (latest) => {
3995
+ const { document, url } = bridge.frameTag();
3996
+ const committedAt = page.activity.documentAt;
3997
+ return new State({
3998
+ page: id,
3999
+ at: page.now(),
4000
+ url,
4001
+ document,
4002
+ committedAt: Number.isFinite(committedAt) ? committedAt : void 0,
4003
+ load: bridge.loaded(),
4004
+ frame: Option.getOrUndefined(latest),
4005
+ text: reading.viewedSince(committedAt)
4006
+ });
4007
+ });
4008
+ const current = (frame) => capture.current(frame) && bridge.frameTag().document === frame.document && frame.hostTime - frame.timing.uncertaintyMillis > page.activity.documentAt;
4009
+ const assembled = {
4010
+ id,
4011
+ playwright,
4012
+ url: Effect.sync(() => redact(playwright.url())),
4013
+ title: titles("", navigation.title.pipe(within("title"))).pipe(span("Page.title"), owned),
4014
+ goto: navigation.goto,
4015
+ back: navigation.back,
4016
+ reload: navigation.reload,
4017
+ bringToFront: native("bringToFront", () => playwright.bringToFront()),
4018
+ close: native("close", () => playwright.close()),
4019
+ snapshot: reading.snapshot,
4020
+ screenshot: pictures.screenshot,
4021
+ frame: pictures.frame,
4022
+ zoom: pictures.zoom,
4023
+ viewport: viewports("", viewport.viewportFor("viewport").pipe(within("viewport"))).pipe(span("Page.viewport"), owned),
4024
+ find: reading.find,
4025
+ text: reading.text,
4026
+ changes,
4027
+ window: make$3(page, changes, capture.recent),
4028
+ state,
4029
+ click: (target, options) => input.click(target, options),
4030
+ hover: (target) => input.hover(target),
4031
+ drag: (from, to) => input.drag(from, to),
4032
+ type: (text, options) => input.type(text, options),
4033
+ press: (keys, options) => input.press(keys, options),
4034
+ scroll: (options) => input.scroll(options),
4035
+ select: input.select,
4036
+ waitFor: reading.waitFor,
4037
+ ready: make$5(page, bridge, capture),
4038
+ screencast: capture.stream,
4039
+ captureStats: pictures.captureStats,
4040
+ recentFrames: capture.recent,
4041
+ recentEvents: options.recentEvents
4042
+ };
4043
+ const internals = {
4044
+ input,
4045
+ afterInput: (effect) => lane.read("inputEnded", page.settings.navigationTimeout)(Effect.andThen(input.perform.idle, effect)).pipe(within("inputEnded", page.settings.navigationTimeout), owned),
4046
+ capture: capture.open,
4047
+ current,
4048
+ confirm: (frame) => native("shown", () => page.protocol.send("Page.getFrameTree")).pipe(Effect.flatMap(() => capture.confirm(frame)), Effect.map((confirmed) => confirmed && current(frame)), within("shown", "2 seconds"), owned),
4049
+ document: () => bridge.frameTag().document,
4050
+ until: reading.until
4051
+ };
4052
+ return Object.assign(new Built(internals), assembled);
4053
+ });
4054
+ //#endregion
4055
+ export { performed as a, pause as i, make as n, viewOf as r, internalsOf as t };
4056
+
4057
+ //# sourceMappingURL=page-PTM8m5wC.mjs.map