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.
- package/README.md +92 -0
- package/dist/Browser-CiXHT1yO.d.mts +129 -0
- package/dist/{Browser-BwCldIff.mjs → Browser-CxXUjxOp.mjs} +10 -20
- package/dist/Browser-CxXUjxOp.mjs.map +1 -0
- package/dist/Browser.d.mts +2 -129
- package/dist/Browser.mjs +1 -1
- package/dist/Cdp-CzCRnMlM.d.mts +10 -0
- package/dist/Cdp.d.mts +5 -26
- package/dist/Cdp.mjs +4 -45
- package/dist/Cdp.mjs.map +1 -1
- package/dist/Chromium.d.mts +1 -1
- package/dist/Chromium.mjs +3 -3
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect-u3P_ENLX.d.mts +62 -0
- package/dist/Expect.d.mts +2 -62
- package/dist/Expect.mjs +2 -2
- package/dist/LICENSE-jpeg-js +28 -0
- package/dist/Moment.mjs +1 -1
- package/dist/Page-BP02bmNv.mjs +264 -0
- package/dist/{Page-CfB6Zn1p.mjs.map → Page-BP02bmNv.mjs.map} +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-C-d5QYS8.mjs → Plan-BG1WifW2.mjs} +48 -4
- package/dist/Plan-BG1WifW2.mjs.map +1 -0
- package/dist/Plan.mjs +1 -1
- package/dist/Policy-BGWjbqCl.d.mts +76 -0
- package/dist/Policy.d.mts +2 -76
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation-DxVikn9_.d.mts +48 -0
- package/dist/Presentation.d.mts +2 -48
- package/dist/Presentation.mjs +2 -2
- package/dist/Stage-NHp8N81k.d.mts +65 -0
- package/dist/Stage.d.mts +2 -65
- package/dist/Stage.mjs +2 -2
- package/dist/{Supervisor-mVPeEUp8.d.mts → Supervisor-CfRxbaso.d.mts} +2 -2
- package/dist/Supervisor.d.mts +1 -1
- package/dist/cdp-BMpYMWYe.mjs +69 -0
- package/dist/cdp-BMpYMWYe.mjs.map +1 -0
- package/dist/cdp-DL7XfEj5.d.mts +41 -0
- package/dist/{context-w3NJPRLo.mjs → context-EGhOqjNN.mjs} +5 -5
- package/dist/context-EGhOqjNN.mjs.map +1 -0
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +4 -3
- package/dist/page-PTM8m5wC.mjs +4057 -0
- package/dist/page-PTM8m5wC.mjs.map +1 -0
- package/dist/{url-Cd04Okid.mjs → url-Bsp2y55M.mjs} +2 -2
- package/dist/{url-Cd04Okid.mjs.map → url-Bsp2y55M.mjs.map} +1 -1
- package/dist/workerd/Cdp.d.mts +3 -0
- package/dist/workerd/Cdp.mjs +18 -0
- package/dist/workerd/Cdp.mjs.map +1 -0
- package/dist/workerd/index.d.mts +16 -0
- package/dist/workerd/index.mjs +17 -0
- package/package.json +12 -1
- package/src/Browser.ts +20 -30
- package/src/Cdp.ts +7 -92
- package/src/index.ts +1 -15
- package/src/internal/cdp.ts +134 -0
- package/src/internal/page/bridge.ts +2 -2
- package/src/internal/page/context.ts +21 -24
- package/src/internal/page/identity.ts +6 -23
- package/src/internal/pictures/motion.ts +3 -3
- package/src/workerd/Cdp.ts +11 -0
- package/src/workerd/index.ts +17 -0
- package/dist/Browser-BwCldIff.mjs.map +0 -1
- package/dist/Page-CfB6Zn1p.mjs +0 -610
- package/dist/Plan-C-d5QYS8.mjs.map +0 -1
- package/dist/context-w3NJPRLo.mjs.map +0 -1
- package/dist/match.inpage-xOe9LNO0.mjs +0 -48
- package/dist/match.inpage-xOe9LNO0.mjs.map +0 -1
- package/dist/page-BKwfF6z-.mjs +0 -5099
- 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
|