effect-browser 0.2.0-beta.9 → 0.3.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +705 -1184
  2. package/dist/Browser-CM65Uo7f.mjs +437 -0
  3. package/dist/Browser-CM65Uo7f.mjs.map +1 -0
  4. package/dist/Browser.d.mts +115 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-DXXIA6Rk.d.mts +135 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +202 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-DzdNdvoh.d.mts +277 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +295 -0
  13. package/dist/BrowserEvent.mjs.map +1 -0
  14. package/dist/Cdp.d.mts +32 -0
  15. package/dist/Cdp.mjs +61 -0
  16. package/dist/Cdp.mjs.map +1 -0
  17. package/dist/Change-DNvztNLu.mjs +138 -0
  18. package/dist/Change-DNvztNLu.mjs.map +1 -0
  19. package/dist/Change-DVP2IJWm.d.mts +71 -0
  20. package/dist/Change.d.mts +2 -0
  21. package/dist/Change.mjs +2 -0
  22. package/dist/Chromium.d.mts +29 -81
  23. package/dist/Chromium.mjs +57 -392
  24. package/dist/Chromium.mjs.map +1 -1
  25. package/dist/Expect.d.mts +62 -0
  26. package/dist/Expect.mjs +114 -0
  27. package/dist/Expect.mjs.map +1 -0
  28. package/dist/Frame-BERmf_gM.d.mts +117 -0
  29. package/dist/Frame.d.mts +2 -0
  30. package/dist/Frame.mjs +123 -0
  31. package/dist/Frame.mjs.map +1 -0
  32. package/dist/Moment.d.mts +2 -0
  33. package/dist/Moment.mjs +288 -0
  34. package/dist/Moment.mjs.map +1 -0
  35. package/dist/Motion.d.mts +40 -0
  36. package/dist/Motion.mjs +122 -0
  37. package/dist/Motion.mjs.map +1 -0
  38. package/dist/Page-D4281dIi.mjs +618 -0
  39. package/dist/Page-D4281dIi.mjs.map +1 -0
  40. package/dist/Page-DYTdVhYa.d.mts +593 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-CNNOU1Tx.d.mts +112 -0
  44. package/dist/Plan-Dtrb-8im.mjs +311 -0
  45. package/dist/Plan-Dtrb-8im.mjs.map +1 -0
  46. package/dist/Plan.d.mts +2 -3
  47. package/dist/Plan.mjs +2 -2
  48. package/dist/Policy.d.mts +76 -0
  49. package/dist/Policy.mjs +237 -0
  50. package/dist/Policy.mjs.map +1 -0
  51. package/dist/Presentation.d.mts +48 -0
  52. package/dist/Presentation.mjs +117 -0
  53. package/dist/Presentation.mjs.map +1 -0
  54. package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
  55. package/dist/Snapshot.d.mts +2 -0
  56. package/dist/Snapshot.mjs +79 -0
  57. package/dist/Snapshot.mjs.map +1 -0
  58. package/dist/Stage.d.mts +54 -0
  59. package/dist/Stage.mjs +133 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-BtEG93gR.mjs +420 -0
  62. package/dist/Supervisor-BtEG93gR.mjs.map +1 -0
  63. package/dist/Supervisor-Im6-wbg5.d.mts +230 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-DAYjATgC.mjs +137 -0
  67. package/dist/context-DAYjATgC.mjs.map +1 -0
  68. package/dist/index.d.mts +17 -14
  69. package/dist/index.mjs +18 -13
  70. package/dist/match.inpage-xOe9LNO0.mjs +48 -0
  71. package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
  72. package/dist/page-Banfm_zc.mjs +5097 -0
  73. package/dist/page-Banfm_zc.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-CcL0K7S0.mjs +262 -0
  76. package/dist/url-CcL0K7S0.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +625 -0
  79. package/src/BrowserError.ts +225 -0
  80. package/src/BrowserEvent.ts +294 -0
  81. package/src/Cdp.ts +101 -0
  82. package/src/Change.ts +88 -0
  83. package/src/Chromium.ts +102 -0
  84. package/src/Expect.ts +180 -0
  85. package/src/Frame.ts +130 -0
  86. package/src/Moment.ts +479 -0
  87. package/src/Motion.ts +166 -0
  88. package/src/Page.ts +520 -0
  89. package/src/Plan.ts +350 -0
  90. package/src/Policy.ts +325 -0
  91. package/src/Presentation.ts +195 -0
  92. package/src/Snapshot.ts +89 -0
  93. package/src/Stage.ts +223 -0
  94. package/src/Supervisor.ts +353 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +607 -0
  97. package/src/internal/input/dispatch.ts +161 -0
  98. package/src/internal/input/edit.inpage.ts +162 -0
  99. package/src/internal/input/evidence.inpage.ts +164 -0
  100. package/src/internal/input/guard.inpage.ts +605 -0
  101. package/src/internal/input/guard.ts +183 -0
  102. package/src/internal/input/keyboard.ts +112 -0
  103. package/src/internal/input/keys.ts +127 -0
  104. package/src/internal/input/perform.ts +322 -0
  105. package/src/internal/input/pointer.ts +153 -0
  106. package/src/internal/input/replies.ts +159 -0
  107. package/src/internal/input/settle.ts +50 -0
  108. package/src/internal/input/style.ts +186 -0
  109. package/src/internal/input/targets.inpage.ts +293 -0
  110. package/src/internal/input/targets.ts +148 -0
  111. package/src/internal/page/bridge.ts +386 -0
  112. package/src/internal/page/context.ts +302 -0
  113. package/src/internal/page/lane.ts +301 -0
  114. package/src/internal/page/navigation.ts +183 -0
  115. package/src/internal/page/openings.ts +87 -0
  116. package/src/internal/page/page.ts +137 -0
  117. package/src/internal/page/url.ts +100 -0
  118. package/src/internal/page/viewport.ts +43 -0
  119. package/src/internal/pictures/capture.ts +778 -0
  120. package/src/internal/pictures/clock.ts +141 -0
  121. package/src/internal/pictures/pictures.ts +520 -0
  122. package/src/internal/pictures/transport.ts +242 -0
  123. package/src/internal/reading/choose.ts +100 -0
  124. package/src/internal/reading/context.inpage.ts +201 -0
  125. package/src/internal/reading/controls.inpage.ts +158 -0
  126. package/src/internal/reading/expect.inpage.ts +70 -0
  127. package/src/internal/reading/match.inpage.ts +92 -0
  128. package/src/internal/reading/names.inpage.ts +306 -0
  129. package/src/internal/reading/outline.inpage.ts +260 -0
  130. package/src/internal/reading/reading.ts +223 -0
  131. package/src/internal/reading/ready.inpage.ts +154 -0
  132. package/src/internal/reading/ready.ts +105 -0
  133. package/src/internal/reading/subjects.inpage.ts +188 -0
  134. package/src/internal/reading/text.inpage.ts +135 -0
  135. package/src/internal/reading/walk.inpage.ts +220 -0
  136. package/src/internal/supervisor/lifecycle.ts +265 -0
  137. package/src/internal/timeline/changes.inpage.ts +216 -0
  138. package/src/internal/timeline/changes.ts +100 -0
  139. package/src/internal/timeline/events.ts +93 -0
  140. package/src/internal/timeline/fold.inpage.ts +123 -0
  141. package/src/internal/timeline/history.inpage.ts +229 -0
  142. package/src/internal/timeline/marks.inpage.ts +207 -0
  143. package/src/internal/timeline/record.inpage.ts +133 -0
  144. package/src/internal/timeline/sight.inpage.ts +122 -0
  145. package/src/internal/timeline/window.ts +96 -0
  146. package/src/internal/timeline/words.ts +65 -0
  147. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  148. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  149. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  150. package/dist/Bootstrap.d.mts +0 -3
  151. package/dist/Bootstrap.mjs +0 -2
  152. package/dist/Browser-BF9_sORP.d.mts +0 -388
  153. package/dist/Browser.mjs.map +0 -1
  154. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  155. package/dist/BrowserData.d.mts +0 -3
  156. package/dist/BrowserData.mjs +0 -695
  157. package/dist/BrowserData.mjs.map +0 -1
  158. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  159. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  160. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  161. package/dist/BrowserRuntime.d.mts +0 -3
  162. package/dist/BrowserRuntime.mjs +0 -3
  163. package/dist/Capture.d.mts +0 -56
  164. package/dist/Capture.mjs +0 -678
  165. package/dist/Capture.mjs.map +0 -1
  166. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  167. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  168. package/dist/CaptureData.d.mts +0 -219
  169. package/dist/CaptureData.mjs +0 -2
  170. package/dist/Errors.d.mts +0 -158
  171. package/dist/Errors.mjs +0 -207
  172. package/dist/Errors.mjs.map +0 -1
  173. package/dist/Images-yCG9BmRU.mjs +0 -76
  174. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  175. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  176. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  177. package/dist/PageControl.d.mts +0 -15
  178. package/dist/PageControl.mjs +0 -34
  179. package/dist/PageControl.mjs.map +0 -1
  180. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  181. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  182. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  183. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  184. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  185. package/dist/PlanData.d.mts +0 -3
  186. package/dist/PlanData.mjs +0 -268
  187. package/dist/PlanData.mjs.map +0 -1
  188. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  189. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  190. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  191. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  192. package/dist/Testing.d.mts +0 -479
  193. package/dist/Testing.mjs +0 -2147
  194. package/dist/Testing.mjs.map +0 -1
  195. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  196. package/dist/Timeline.d.mts +0 -4
  197. package/dist/Timeline.mjs +0 -20
  198. package/dist/Timeline.mjs.map +0 -1
  199. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  200. package/dist/TimelineData.d.mts +0 -3
  201. package/dist/TimelineData.mjs +0 -482
  202. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The one error every browser operation fails with.
3
+ *
4
+ * `reason` says what went wrong and `dispatched` says whether the action's own input (a press,
5
+ * key, wheel, selection or hover, not the pointer travelling toward a press) or a navigation
6
+ * reached the browser first. `consequence` says what that leaves: what was lost, and whether the
7
+ * call can be repeated. `message` is a sentence for operators.
8
+ *
9
+ * @since 0.3.0
10
+ */
11
+ import { Schema } from "effect";
12
+
13
+ import { DisconnectCause } from "./BrowserEvent.ts";
14
+
15
+ /** The operation did not finish in time. */
16
+ export class Timeout extends Schema.TaggedError<Timeout>()("Timeout", {
17
+ millis: Schema.Finite,
18
+ }) {
19
+ override get message() {
20
+ return `timed out after ${this.millis} ms`;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * The operation waited its turn behind others on its page and was not given one in time, or was
26
+ * asked to fail fast: the page may be well, only busy.
27
+ */
28
+ export class Busy extends Schema.TaggedError<Busy>()("Busy", {
29
+ waitedMillis: Schema.Finite,
30
+ /** The operations ahead of it: those holding the page and those asked before it. */
31
+ ahead: Schema.Int,
32
+ }) {
33
+ override get message() {
34
+ const others = this.ahead === 1 ? "operation" : "operations";
35
+
36
+ return `the page was busy: it waited ${this.waitedMillis} ms behind ${this.ahead} other ${others}`;
37
+ }
38
+ }
39
+
40
+ /** The browser has as many pages open as `maxPages` allows, and none closed in time. */
41
+ export class Limit extends Schema.TaggedError<Limit>()("Limit", {
42
+ maxPages: Schema.Int,
43
+ }) {
44
+ override get message() {
45
+ return `the browser already has ${this.maxPages} pages open, as many as it allows`;
46
+ }
47
+ }
48
+
49
+ /** A ref from an earlier snapshot no longer names an element on the page. */
50
+ export class StaleRef extends Schema.TaggedError<StaleRef>()("StaleRef", {
51
+ ref: Schema.String,
52
+ }) {
53
+ override get message() {
54
+ return `${this.ref} is not on the page any more`;
55
+ }
56
+ }
57
+
58
+ /** What the operation looked for is not on the page. */
59
+ export class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
60
+ target: Schema.String,
61
+ }) {
62
+ override get message() {
63
+ return `${this.target} was not found`;
64
+ }
65
+ }
66
+
67
+ /** The element exists but cannot take this input: covered, disabled, hidden or the wrong kind. */
68
+ export class NotActionable extends Schema.TaggedError<NotActionable>()("NotActionable", {
69
+ detail: Schema.String,
70
+ }) {
71
+ override get message() {
72
+ return this.detail;
73
+ }
74
+ }
75
+
76
+ /** A navigation failed, for example on a DNS error or a refused connection. */
77
+ export class NavigationFailed extends Schema.TaggedError<NavigationFailed>()("NavigationFailed", {
78
+ url: Schema.String,
79
+ detail: Schema.String,
80
+ }) {
81
+ override get message() {
82
+ return `could not load ${this.url}: ${this.detail}`;
83
+ }
84
+ }
85
+
86
+ const closedBy = {
87
+ page: "the page was closed",
88
+ crashed: "the page crashed",
89
+ connection: "the connection to the browser was lost",
90
+ session: "the browser's session ended",
91
+ released: "the browser was released",
92
+ };
93
+
94
+ /**
95
+ * The page is gone: it was closed (`page`) or crashed, or its browser was lost for one of the
96
+ * browser's `DisconnectCause`s.
97
+ */
98
+ export class Closed extends Schema.TaggedError<Closed>()("Closed", {
99
+ cause: Schema.Literals(["page", "crashed", ...DisconnectCause.literals]),
100
+ }) {
101
+ override get message() {
102
+ return closedBy[this.cause];
103
+ }
104
+ }
105
+
106
+ /** The request itself is invalid, such as a ref that is not a ref or a point off the screen. */
107
+ export class InvalidRequest extends Schema.TaggedError<InvalidRequest>()("InvalidRequest", {
108
+ detail: Schema.String,
109
+ }) {
110
+ override get message() {
111
+ return this.detail;
112
+ }
113
+ }
114
+
115
+ /** The input policy refused the action before any input was sent. */
116
+ export class PolicyDenied extends Schema.TaggedError<PolicyDenied>()("PolicyDenied", {
117
+ detail: Schema.String,
118
+ /** What made the policy deny, such as the failure of the judge it asked. */
119
+ cause: Schema.optional(Schema.Defect()),
120
+ }) {
121
+ override get message() {
122
+ return `the input policy denied this action: ${this.detail}`;
123
+ }
124
+ }
125
+
126
+ /** The input policy did not release its hold within its own bound. */
127
+ export class PolicyTimeout extends Schema.TaggedError<PolicyTimeout>()("PolicyTimeout", {
128
+ millis: Schema.Finite,
129
+ }) {
130
+ override get message() {
131
+ return `the input policy did not allow this action within ${this.millis} ms`;
132
+ }
133
+ }
134
+
135
+ /** The requested event cursor fell behind the browser's bounded replay history. */
136
+ export class EventHistoryExpired extends Schema.TaggedError<EventHistoryExpired>()(
137
+ "EventHistoryExpired",
138
+ {
139
+ after: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
140
+ oldest: Schema.Int.check(Schema.isGreaterThan(0)),
141
+ latest: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
142
+ },
143
+ ) {
144
+ override get message() {
145
+ return `event history after ${this.after} expired; retained sequences are ${this.oldest}–${this.latest}`;
146
+ }
147
+ }
148
+
149
+ /** The browser, the connection or the provider failed in a way the other reasons do not cover. */
150
+ export class Failed extends Schema.TaggedError<Failed>()("Failed", {
151
+ detail: Schema.String,
152
+ }) {
153
+ override get message() {
154
+ return this.detail;
155
+ }
156
+ }
157
+
158
+ export const Reason = Schema.Union([
159
+ Timeout,
160
+ Busy,
161
+ Limit,
162
+ StaleRef,
163
+ NotFound,
164
+ NotActionable,
165
+ NavigationFailed,
166
+ Closed,
167
+ InvalidRequest,
168
+ PolicyDenied,
169
+ PolicyTimeout,
170
+ EventHistoryExpired,
171
+ Failed,
172
+ ]);
173
+
174
+ export type Reason = typeof Reason.Type;
175
+
176
+ export class BrowserError extends Schema.TaggedError<BrowserError>()("BrowserError", {
177
+ operation: Schema.String,
178
+ reason: Reason,
179
+ dispatched: Schema.Boolean,
180
+ }) {
181
+ override get message() {
182
+ return `${this.operation} failed${this.dispatched ? " after it was sent" : ""}: ${this.reason.message}`;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * What a failure leaves. `lost` is `"page"` when the page is gone and its browser stands, and
188
+ * `"session"` when the browser is gone with its pages. `repeat` says whether the call can be made
189
+ * again, on what still stands: `"safe"`, as nothing reached the browser; `"check"`, as it may have
190
+ * taken effect, so look first, as an `Expect` attempt does; `"pointless"`, as it fails again unless
191
+ * something changes, such as
192
+ * a new ref; or `"resume"`, reading events again from a newer cursor.
193
+ */
194
+ export interface Consequence {
195
+ readonly lost: "nothing" | "page" | "session";
196
+ readonly repeat: "safe" | "check" | "pointless" | "resume";
197
+ }
198
+
199
+ /** What `error` leaves, from its reason and whether it was dispatched alone. */
200
+ export const consequence = ({ reason, dispatched }: BrowserError): Consequence => {
201
+ const repeat = dispatched ? "check" : "safe";
202
+
203
+ switch (reason._tag) {
204
+ case "Closed":
205
+ return {
206
+ lost: reason.cause === "page" || reason.cause === "crashed" ? "page" : "session",
207
+ repeat,
208
+ };
209
+ case "Busy":
210
+ case "Timeout":
211
+ case "Failed":
212
+ case "NavigationFailed":
213
+ return { lost: "nothing", repeat };
214
+ case "EventHistoryExpired":
215
+ return { lost: "nothing", repeat: "resume" };
216
+ case "StaleRef":
217
+ case "NotFound":
218
+ case "NotActionable":
219
+ case "InvalidRequest":
220
+ case "Limit":
221
+ case "PolicyDenied":
222
+ case "PolicyTimeout":
223
+ return { lost: "nothing", repeat: "pointless" };
224
+ }
225
+ };
@@ -0,0 +1,294 @@
1
+ /**
2
+ * What happened in a browser, in order: pages and their documents, actions, pointer motion, and
3
+ * the browser's own end.
4
+ *
5
+ * Every event carries `at`, host monotonic milliseconds from the owning browser’s Effect `Clock`.
6
+ * This clock also stamps screencast frames’ `receivedAt` and moments, so wall-clock corrections
7
+ * cannot disturb their order. Compare stamps only within the same browser clock, not across hosts.
8
+ * A page is named by its CDP target id, which stays the same across reconnects, and every URL is
9
+ * reported without its userinfo or known secret parameters.
10
+ *
11
+ * @since 0.3.0
12
+ */
13
+ import { Schema } from "effect";
14
+
15
+ import * as Motion from "./Motion.ts";
16
+
17
+ /** A page opened, and the page that opened it, as a link or `window.open` does, if one did. */
18
+ export class PageOpened extends Schema.TaggedClass<PageOpened>()("PageOpened", {
19
+ at: Schema.Finite,
20
+ page: Schema.String,
21
+ url: Schema.String,
22
+ opener: Schema.optional(Schema.String),
23
+ }) {}
24
+
25
+ /**
26
+ * The page closed (`page`), or crashed and was closed (`crashed`). A page lost with its browser
27
+ * has no event of its own: `Disconnected` stands for all of them.
28
+ */
29
+ export class PageClosed extends Schema.TaggedClass<PageClosed>()("PageClosed", {
30
+ at: Schema.Finite,
31
+ page: Schema.String,
32
+ cause: Schema.Literals(["page", "crashed"]),
33
+ }) {}
34
+
35
+ /**
36
+ * A tab the site opened that the library could not track, as its registration failed with
37
+ * `detail`. A tab that closed as it opened has no event.
38
+ */
39
+ export class PageUntracked extends Schema.TaggedClass<PageUntracked>()("PageUntracked", {
40
+ at: Schema.Finite,
41
+ url: Schema.String,
42
+ detail: Schema.String,
43
+ }) {}
44
+
45
+ /**
46
+ * The page's main frame moved to `url`: into a new document, or within the current one
47
+ * (`sameDocument`), as `pushState` and fragment links do. Frames carry the document they followed.
48
+ */
49
+ export class Navigated extends Schema.TaggedClass<Navigated>()("Navigated", {
50
+ at: Schema.Finite,
51
+ page: Schema.String,
52
+ url: Schema.String,
53
+ /**
54
+ * The page's documents counted from 0, the one it had when this browser began tracking it, one
55
+ * more for each new document. The count belongs to the page within one `Browser` and starts at
56
+ * 0 again on a new connection, so compare it only with the same page's in the same browser.
57
+ */
58
+ document: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
59
+ sameDocument: Schema.Boolean,
60
+ }) {}
61
+
62
+ /** The page's document finished parsing (`domcontentloaded`) or loading (`load`). */
63
+ export class PageLoaded extends Schema.TaggedClass<PageLoaded>()("PageLoaded", {
64
+ at: Schema.Finite,
65
+ page: Schema.String,
66
+ state: Schema.Literals(["domcontentloaded", "load"]),
67
+ }) {}
68
+
69
+ /**
70
+ * Why a browser was lost to its owner: its connection dropped (`connection`), its provider ended
71
+ * its session, at or after the session's `expiresAt` (`session`), or its owner released it.
72
+ */
73
+ export const DisconnectCause = Schema.Literals(["connection", "session", "released"]);
74
+
75
+ export type DisconnectCause = typeof DisconnectCause.Type;
76
+
77
+ /** The browser was lost to its owner, with every page in it. It happens once. */
78
+ export class Disconnected extends Schema.TaggedClass<Disconnected>()("Disconnected", {
79
+ at: Schema.Finite,
80
+ cause: DisconnectCause,
81
+ }) {}
82
+
83
+ /** The provider ends this browser's session at `expiresAt`, a wall-clock instant. */
84
+ export class SessionEnding extends Schema.TaggedClass<SessionEnding>()("SessionEnding", {
85
+ at: Schema.Finite,
86
+ expiresAt: Schema.DateTimeUtc,
87
+ }) {}
88
+
89
+ /**
90
+ * The words around a subject that say which one it is, each bound to it in the page rather than
91
+ * read off nearby text later. In a table, `row` is the row's header or first cell with text and
92
+ * `column` the header over it; elsewhere, `label` is the words just before it in its row, item,
93
+ * group or block, at most 40 characters. `heading` is the nearest heading above it, left out for
94
+ * what is pinned to the viewport. Row, column and heading are at most 60 characters.
95
+ */
96
+ export const SubjectContext = Schema.Struct({
97
+ row: Schema.optional(Schema.String),
98
+ column: Schema.optional(Schema.String),
99
+ label: Schema.optional(Schema.String),
100
+ heading: Schema.optional(Schema.String),
101
+ });
102
+
103
+ export type SubjectContext = typeof SubjectContext.Type;
104
+
105
+ /**
106
+ * What an input acted on, or what `Page.find` found, as the page named it then: its role (null
107
+ * when it has none), its accessible name (empty when it has none), its lowercase tag and its
108
+ * context. For a point, it is the control under the point, or else the element painted there.
109
+ * Unlike a ref, it keeps its meaning after the page changes.
110
+ */
111
+ export class Subject extends Schema.Class<Subject>("effect-browser/Subject")({
112
+ role: Schema.NullOr(Schema.String),
113
+ name: Schema.String,
114
+ tag: Schema.String,
115
+ context: SubjectContext,
116
+ }) {}
117
+
118
+ const Button = Schema.Literals(["left", "right", "middle"]);
119
+
120
+ /** An element's box in viewport CSS pixels; it may lie partly or wholly outside the viewport. */
121
+ export const Box = Schema.Struct({
122
+ x: Schema.Finite,
123
+ y: Schema.Finite,
124
+ width: Schema.Finite,
125
+ height: Schema.Finite,
126
+ });
127
+
128
+ export type Box = typeof Box.Type;
129
+
130
+ /**
131
+ * What else an action was asked, where it changes what the action does: a click's button, count
132
+ * and hold, how `type` enters its text, how often and how long `press` holds its keys, how far
133
+ * `scroll` moves, and the options `select` chose. Only a completed action records them.
134
+ */
135
+ export const ActionOptions = Schema.Struct({
136
+ button: Schema.optional(Button),
137
+ clickCount: Schema.optional(Schema.Finite),
138
+ holdMillis: Schema.optional(Schema.Finite),
139
+ replace: Schema.optional(Schema.Boolean),
140
+ submit: Schema.optional(Schema.Boolean),
141
+ times: Schema.optional(Schema.Finite),
142
+ dx: Schema.optional(Schema.Finite),
143
+ dy: Schema.optional(Schema.Finite),
144
+ values: Schema.optional(Schema.Array(Schema.String)),
145
+ });
146
+
147
+ export type ActionOptions = typeof ActionOptions.Type;
148
+
149
+ /**
150
+ * One page operation ended, including one its caller interrupted (`error: "interrupted"`).
151
+ * `target` is what the caller asked for: a ref, a point, a URL or keys, and `options` the rest.
152
+ * `subject` is what an element or point action found there, and `to` where a drag ended; an
153
+ * action that failed before finding its target has neither. Point actions carry the viewport
154
+ * point they used. `correlation` is the id its caller gave it with `Page.correlate`, such as a
155
+ * model's tool call id.
156
+ */
157
+ export class Action extends Schema.TaggedClass<Action>()("Action", {
158
+ at: Schema.Finite,
159
+ startedAt: Schema.Finite,
160
+ page: Schema.String,
161
+ name: Schema.String,
162
+ correlation: Schema.optional(Schema.String),
163
+ target: Schema.optional(Schema.String),
164
+ options: Schema.optional(ActionOptions),
165
+ subject: Schema.optional(Subject),
166
+ to: Schema.optional(Subject),
167
+ /** For an action at a point, the box of what it found there. */
168
+ box: Schema.optional(Box),
169
+ text: Schema.optional(Schema.String),
170
+ x: Schema.optional(Schema.Finite),
171
+ y: Schema.optional(Schema.Finite),
172
+ ok: Schema.Boolean,
173
+ dispatched: Schema.Boolean,
174
+ error: Schema.optional(Schema.String),
175
+ }) {}
176
+
177
+ const Sequence = Schema.Int.check(Schema.isGreaterThan(0));
178
+
179
+ /** One whole glide, published before its first input. Offsets use the host monotonic clock. */
180
+ export class TrackPlanned extends Schema.TaggedClass<TrackPlanned>()("TrackPlanned", {
181
+ at: Schema.Finite,
182
+ page: Schema.String,
183
+ from: Motion.Point,
184
+ samples: Motion.Plan,
185
+ }) {}
186
+
187
+ /**
188
+ * The submitted prefix of a glide. `plan` is its TrackPlanned envelope sequence; `dispatched`
189
+ * counts samples. `complete` means every sample was submitted, not acknowledged by the browser.
190
+ * The terminal point clips a canceled plan so a compositor never draws its unrealized future.
191
+ */
192
+ export class TrackPerformed extends Schema.TaggedClass<TrackPerformed>()("TrackPerformed", {
193
+ at: Schema.Finite,
194
+ page: Schema.String,
195
+ plan: Sequence,
196
+ dispatched: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
197
+ x: Schema.Finite,
198
+ y: Schema.Finite,
199
+ complete: Schema.Boolean,
200
+ }) {}
201
+
202
+ export class PointerPressed extends Schema.TaggedClass<PointerPressed>()("PointerPressed", {
203
+ at: Schema.Finite,
204
+ page: Schema.String,
205
+ x: Schema.Finite,
206
+ y: Schema.Finite,
207
+ button: Button,
208
+ clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
209
+ }) {}
210
+
211
+ export class PointerReleased extends Schema.TaggedClass<PointerReleased>()("PointerReleased", {
212
+ at: Schema.Finite,
213
+ page: Schema.String,
214
+ x: Schema.Finite,
215
+ y: Schema.Finite,
216
+ button: Button,
217
+ clickCount: Schema.Int.check(Schema.isGreaterThan(0)),
218
+ }) {}
219
+
220
+ export class WheelScrolled extends Schema.TaggedClass<WheelScrolled>()("WheelScrolled", {
221
+ at: Schema.Finite,
222
+ page: Schema.String,
223
+ x: Schema.Finite,
224
+ y: Schema.Finite,
225
+ dx: Schema.Finite,
226
+ dy: Schema.Finite,
227
+ }) {}
228
+
229
+ export class KeyChanged extends Schema.TaggedClass<KeyChanged>()("KeyChanged", {
230
+ at: Schema.Finite,
231
+ page: Schema.String,
232
+ key: Schema.String,
233
+ phase: Schema.Literals(["down", "up"]),
234
+ }) {}
235
+
236
+ /** Literal text insertion has no corresponding key events, including Unicode and newlines. */
237
+ export class TextInserted extends Schema.TaggedClass<TextInserted>()("TextInserted", {
238
+ at: Schema.Finite,
239
+ page: Schema.String,
240
+ text: Schema.String,
241
+ }) {}
242
+
243
+ /** The target's CSS cursor, obtained during target resolution without another browser call. */
244
+ export class CursorChanged extends Schema.TaggedClass<CursorChanged>()("CursorChanged", {
245
+ at: Schema.Finite,
246
+ page: Schema.String,
247
+ cursor: Schema.String,
248
+ }) {}
249
+
250
+ export const TrackEvent = Schema.Union([
251
+ TrackPlanned,
252
+ TrackPerformed,
253
+ PointerPressed,
254
+ PointerReleased,
255
+ WheelScrolled,
256
+ KeyChanged,
257
+ TextInserted,
258
+ CursorChanged,
259
+ ]);
260
+
261
+ export type TrackEvent = typeof TrackEvent.Type;
262
+
263
+ /**
264
+ * A JavaScript dialog appeared and was answered: an alert or a leave-page prompt accepted, a
265
+ * confirm or a prompt dismissed, as if its Cancel were pressed.
266
+ */
267
+ export class DialogShown extends Schema.TaggedClass<DialogShown>()("DialogShown", {
268
+ at: Schema.Finite,
269
+ page: Schema.String,
270
+ kind: Schema.String,
271
+ message: Schema.String,
272
+ answer: Schema.Literals(["accepted", "dismissed"]),
273
+ }) {}
274
+
275
+ export const BrowserEvent = Schema.Union([
276
+ PageOpened,
277
+ PageClosed,
278
+ PageUntracked,
279
+ Navigated,
280
+ PageLoaded,
281
+ Action,
282
+ ...TrackEvent.members,
283
+ DialogShown,
284
+ Disconnected,
285
+ SessionEnding,
286
+ ]);
287
+
288
+ export type BrowserEvent = typeof BrowserEvent.Type;
289
+
290
+ /** A position in the browser's one replayable event history. Sequences begin at one. */
291
+ export class RecordedEvent extends Schema.Class<RecordedEvent>("effect-browser/RecordedEvent")({
292
+ sequence: Sequence,
293
+ event: BrowserEvent,
294
+ }) {}
package/src/Cdp.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * A browser reached over the Chrome DevTools Protocol: a Chrome started with
3
+ * `--remote-debugging-port`, or any provider that hands out a CDP WebSocket URL.
4
+ *
5
+ * Closing the scope disconnects. It does not close the remote browser; its owner does that.
6
+ *
7
+ * @since 0.3.0
8
+ */
9
+ import { type DateTime, Effect, Layer, Redacted } from "effect";
10
+ import { chromium } from "playwright-core";
11
+
12
+ import * as Browser from "./Browser.ts";
13
+ import { type BrowserError, Failed, type Reason } from "./BrowserError.ts";
14
+ import { reasonOf, undispatched } from "./internal/page/context.ts";
15
+ import * as Url from "./internal/page/url.ts";
16
+
17
+ export interface Options extends Browser.Options {
18
+ /**
19
+ * `ws://`, `wss://` or `http://` DevTools endpoint. Often a credential, so it may be redacted;
20
+ * connect failures never include its userinfo, path or query.
21
+ */
22
+ readonly endpoint: string | Redacted.Redacted<string>;
23
+ readonly headers?: Readonly<Record<string, string>> | undefined;
24
+ /** Identifies the session in `Browser.id`. Defaults to `"cdp"`. */
25
+ readonly id?: string | undefined;
26
+ readonly provider?: string | undefined;
27
+ /** Bound on connecting. Defaults to 30 seconds. */
28
+ readonly connectTimeoutMillis?: number | undefined;
29
+ /** When the provider ends the session on its own, as a hosted session's timeout does. */
30
+ readonly expiresAt?: DateTime.Utc | undefined;
31
+ /**
32
+ * A connection apart from this one for the pages' screencasts, as a hosted provider supplies.
33
+ * Without one, each page's capture runs on its own session.
34
+ */
35
+ readonly capture?: Browser.CaptureSource | undefined;
36
+ }
37
+
38
+ /**
39
+ * Playwright can echo the endpoint, or a URL derived from it with its query, userinfo or path, in
40
+ * a connect failure. Those parts are often the credential, so a URL keeps its scheme, host and
41
+ * port, and any other echo of the endpoint is replaced.
42
+ */
43
+ const withoutEndpoint = (endpoint: string, reason: Reason): Reason => {
44
+ if (reason._tag !== "Failed") return reason;
45
+
46
+ const origins = reason.detail.replace(Url.quoted, (url) => {
47
+ const parsed = URL.parse(url);
48
+
49
+ return parsed === null || parsed.host === "" ? "<url>" : `${parsed.protocol}//${parsed.host}`;
50
+ });
51
+
52
+ return new Failed({
53
+ detail: endpoint === "" ? origins : origins.replaceAll(endpoint, "<endpoint>"),
54
+ });
55
+ };
56
+
57
+ /** Connect and open a `Browser` over the endpoint's first context. */
58
+ export const open = Effect.fn("Cdp.open")(function* (options: Options) {
59
+ const endpoint = Redacted.isRedacted(options.endpoint)
60
+ ? Redacted.value(options.endpoint)
61
+ : options.endpoint;
62
+
63
+ const timeout = options.connectTimeoutMillis ?? 30_000;
64
+
65
+ const failed = (cause: unknown, timeoutMillis?: number) =>
66
+ undispatched("connect", withoutEndpoint(endpoint, reasonOf(cause, timeoutMillis)));
67
+
68
+ // The endpoint is often a credential, so the connect span names no part of it.
69
+ const connected = yield* Effect.acquireRelease(
70
+ Effect.tryPromise({
71
+ try: () =>
72
+ chromium.connectOverCDP(endpoint, {
73
+ timeout,
74
+ ...(options.headers === undefined ? {} : { headers: { ...options.headers } }),
75
+ }),
76
+ catch: (cause) => failed(cause, timeout),
77
+ }).pipe(Effect.withSpan("Cdp.connect", {}, { captureStackTrace: false })),
78
+ (browser) => Effect.tryPromise(() => browser.close()).pipe(Effect.ignore),
79
+ );
80
+
81
+ const context =
82
+ connected.contexts()[0] ??
83
+ (yield* Effect.tryPromise({
84
+ try: () => connected.newContext(),
85
+ catch: (cause) => failed(cause),
86
+ }));
87
+
88
+ return yield* Browser.make(
89
+ context,
90
+ {
91
+ id: options.id ?? "cdp",
92
+ provider: options.provider ?? "cdp",
93
+ expiresAt: options.expiresAt,
94
+ capture: options.capture,
95
+ },
96
+ options,
97
+ );
98
+ });
99
+
100
+ export const layer = (options: Options): Layer.Layer<Browser.Browser, BrowserError> =>
101
+ Layer.effect(Browser.Browser, open(options));