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

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 (204) hide show
  1. package/README.md +849 -1161
  2. package/dist/Browser-S4kvpmqa.mjs +605 -0
  3. package/dist/Browser-S4kvpmqa.mjs.map +1 -0
  4. package/dist/Browser.d.mts +127 -3
  5. package/dist/Browser.mjs +2 -22
  6. package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
  7. package/dist/BrowserError.d.mts +2 -0
  8. package/dist/BrowserError.mjs +211 -0
  9. package/dist/BrowserError.mjs.map +1 -0
  10. package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
  11. package/dist/BrowserEvent.d.mts +2 -0
  12. package/dist/BrowserEvent.mjs +297 -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-iMlcuzVD.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-B5cjaUp5.mjs +618 -0
  39. package/dist/Page-B5cjaUp5.mjs.map +1 -0
  40. package/dist/Page-BlPjfibv.d.mts +595 -0
  41. package/dist/Page.d.mts +2 -0
  42. package/dist/Page.mjs +2 -0
  43. package/dist/Plan-BI8XGvDu.d.mts +209 -0
  44. package/dist/Plan-D1wrJVs8.mjs +508 -0
  45. package/dist/Plan-D1wrJVs8.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 +64 -0
  59. package/dist/Stage.mjs +237 -0
  60. package/dist/Stage.mjs.map +1 -0
  61. package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
  62. package/dist/Supervisor-TNKNOVzu.mjs +599 -0
  63. package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
  64. package/dist/Supervisor.d.mts +2 -0
  65. package/dist/Supervisor.mjs +2 -0
  66. package/dist/context-B6xr8nVi.mjs +261 -0
  67. package/dist/context-B6xr8nVi.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-B8tUQELg.mjs +5073 -0
  73. package/dist/page-B8tUQELg.mjs.map +1 -0
  74. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  75. package/dist/url-Cd04Okid.mjs +264 -0
  76. package/dist/url-Cd04Okid.mjs.map +1 -0
  77. package/package.json +96 -61
  78. package/src/Browser.ts +666 -0
  79. package/src/BrowserError.ts +234 -0
  80. package/src/BrowserEvent.ts +296 -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 +522 -0
  89. package/src/Plan.ts +663 -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 +467 -0
  94. package/src/Supervisor.ts +599 -0
  95. package/src/index.ts +20 -0
  96. package/src/internal/input/actions.ts +610 -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/gate.ts +224 -0
  101. package/src/internal/input/guard.inpage.ts +605 -0
  102. package/src/internal/input/guard.ts +184 -0
  103. package/src/internal/input/keyboard.ts +112 -0
  104. package/src/internal/input/keys.ts +127 -0
  105. package/src/internal/input/perform.ts +376 -0
  106. package/src/internal/input/pointer.ts +153 -0
  107. package/src/internal/input/replies.ts +195 -0
  108. package/src/internal/input/settle.ts +50 -0
  109. package/src/internal/input/style.ts +186 -0
  110. package/src/internal/input/targets.inpage.ts +293 -0
  111. package/src/internal/input/targets.ts +148 -0
  112. package/src/internal/page/bridge.ts +386 -0
  113. package/src/internal/page/context.ts +312 -0
  114. package/src/internal/page/lane.ts +311 -0
  115. package/src/internal/page/navigation.ts +183 -0
  116. package/src/internal/page/openings.ts +87 -0
  117. package/src/internal/page/page.ts +165 -0
  118. package/src/internal/page/url.ts +100 -0
  119. package/src/internal/page/viewport.ts +43 -0
  120. package/src/internal/pictures/capture.ts +776 -0
  121. package/src/internal/pictures/clock.ts +141 -0
  122. package/src/internal/pictures/counts.ts +92 -0
  123. package/src/internal/pictures/pictures.ts +520 -0
  124. package/src/internal/pictures/transport.ts +242 -0
  125. package/src/internal/reading/choose.ts +100 -0
  126. package/src/internal/reading/context.inpage.ts +201 -0
  127. package/src/internal/reading/controls.inpage.ts +158 -0
  128. package/src/internal/reading/expect.inpage.ts +70 -0
  129. package/src/internal/reading/match.inpage.ts +92 -0
  130. package/src/internal/reading/names.inpage.ts +306 -0
  131. package/src/internal/reading/outline.inpage.ts +260 -0
  132. package/src/internal/reading/reading.ts +223 -0
  133. package/src/internal/reading/ready.inpage.ts +164 -0
  134. package/src/internal/reading/ready.ts +111 -0
  135. package/src/internal/reading/subjects.inpage.ts +188 -0
  136. package/src/internal/reading/text.inpage.ts +135 -0
  137. package/src/internal/reading/walk.inpage.ts +220 -0
  138. package/src/internal/supervisor/lifecycle.ts +416 -0
  139. package/src/internal/timeline/changes.inpage.ts +216 -0
  140. package/src/internal/timeline/changes.ts +100 -0
  141. package/src/internal/timeline/events.ts +94 -0
  142. package/src/internal/timeline/fold.inpage.ts +123 -0
  143. package/src/internal/timeline/history.inpage.ts +229 -0
  144. package/src/internal/timeline/marks.inpage.ts +207 -0
  145. package/src/internal/timeline/record.inpage.ts +133 -0
  146. package/src/internal/timeline/sight.inpage.ts +122 -0
  147. package/src/internal/timeline/window.ts +96 -0
  148. package/src/internal/timeline/words.ts +65 -0
  149. package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
  150. package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
  151. package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
  152. package/dist/Bootstrap.d.mts +0 -3
  153. package/dist/Bootstrap.mjs +0 -2
  154. package/dist/Browser-BF9_sORP.d.mts +0 -388
  155. package/dist/Browser.mjs.map +0 -1
  156. package/dist/BrowserData-CPIIIHol.d.mts +0 -585
  157. package/dist/BrowserData.d.mts +0 -3
  158. package/dist/BrowserData.mjs +0 -695
  159. package/dist/BrowserData.mjs.map +0 -1
  160. package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
  161. package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
  162. package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
  163. package/dist/BrowserRuntime.d.mts +0 -3
  164. package/dist/BrowserRuntime.mjs +0 -3
  165. package/dist/Capture.d.mts +0 -56
  166. package/dist/Capture.mjs +0 -678
  167. package/dist/Capture.mjs.map +0 -1
  168. package/dist/CaptureData-CYriZHX1.mjs +0 -201
  169. package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
  170. package/dist/CaptureData.d.mts +0 -219
  171. package/dist/CaptureData.mjs +0 -2
  172. package/dist/Errors.d.mts +0 -158
  173. package/dist/Errors.mjs +0 -207
  174. package/dist/Errors.mjs.map +0 -1
  175. package/dist/Images-yCG9BmRU.mjs +0 -76
  176. package/dist/Images-yCG9BmRU.mjs.map +0 -1
  177. package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
  178. package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
  179. package/dist/PageControl.d.mts +0 -15
  180. package/dist/PageControl.mjs +0 -34
  181. package/dist/PageControl.mjs.map +0 -1
  182. package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
  183. package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
  184. package/dist/Plan-Dyxu2q8C.mjs +0 -543
  185. package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
  186. package/dist/PlanData-B668RxbQ.d.mts +0 -4831
  187. package/dist/PlanData.d.mts +0 -3
  188. package/dist/PlanData.mjs +0 -268
  189. package/dist/PlanData.mjs.map +0 -1
  190. package/dist/PlanInput-BJcPPncs.mjs +0 -75
  191. package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
  192. package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
  193. package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
  194. package/dist/Testing.d.mts +0 -479
  195. package/dist/Testing.mjs +0 -2147
  196. package/dist/Testing.mjs.map +0 -1
  197. package/dist/Timeline-fV22Xk-S.d.mts +0 -29
  198. package/dist/Timeline.d.mts +0 -4
  199. package/dist/Timeline.mjs +0 -20
  200. package/dist/Timeline.mjs.map +0 -1
  201. package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
  202. package/dist/TimelineData.d.mts +0 -3
  203. package/dist/TimelineData.mjs +0 -482
  204. package/dist/TimelineData.mjs.map +0 -1
@@ -0,0 +1,605 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { DialogShown, Disconnected, PageClosed, PageOpened, PageUntracked, RecordedEvent, SessionEnding } from "./BrowserEvent.mjs";
3
+ import { BrowserError, Busy, Closed, EventHistoryExpired, Failed, InvalidRequest, Limit, Suspended } from "./BrowserError.mjs";
4
+ import { i as failWith, s as undispatched, t as call } from "./context-B6xr8nVi.mjs";
5
+ import { c as FailFast, i as redact } from "./url-Cd04Okid.mjs";
6
+ import { n as make$4, o as mapping } from "./page-B8tUQELg.mjs";
7
+ import { Cause, Clock, Context, DateTime, Deferred, Duration, Effect, Exit, Option, Queue, Schema, Scope, Semaphore, Stream } from "effect";
8
+ //#region src/internal/input/gate.ts
9
+ /**
10
+ * A browser's input authority: stop admitting writes, drain admitted operations through their
11
+ * records and native replies, then retain the stop for its scopes. Reads never enter this gate.
12
+ */
13
+ const without = (set, id) => new Set([...set].filter((each) => each !== id));
14
+ const step = (state, input, stopped) => {
15
+ switch (input._tag) {
16
+ case "Enter": return state._tag === "Open" ? {
17
+ ...state,
18
+ inputs: /* @__PURE__ */ new Set([...state.inputs, input.id])
19
+ } : state;
20
+ case "End": {
21
+ if (state._tag === "Held") return state;
22
+ const inputs = without(state.inputs, input.id);
23
+ return state._tag === "Draining" && inputs.size === 0 ? {
24
+ _tag: "Held",
25
+ claims: state.claims,
26
+ stopped
27
+ } : {
28
+ ...state,
29
+ inputs
30
+ };
31
+ }
32
+ case "Suspend":
33
+ if (state._tag !== "Open") return {
34
+ ...state,
35
+ claims: /* @__PURE__ */ new Set([...state.claims, input.id])
36
+ };
37
+ return state.inputs.size === 0 ? {
38
+ _tag: "Held",
39
+ claims: /* @__PURE__ */ new Set([input.id]),
40
+ stopped
41
+ } : {
42
+ _tag: "Draining",
43
+ inputs: state.inputs,
44
+ claims: /* @__PURE__ */ new Set([input.id]),
45
+ startedAt: stopped.at,
46
+ deadline: stopped.at + input.drain
47
+ };
48
+ case "Leave": {
49
+ if (state._tag === "Open" || !state.claims.has(input.id)) return state;
50
+ const claims = without(state.claims, input.id);
51
+ return claims.size > 0 ? {
52
+ ...state,
53
+ claims
54
+ } : {
55
+ _tag: "Open",
56
+ inputs: state._tag === "Draining" ? state.inputs : /* @__PURE__ */ new Set()
57
+ };
58
+ }
59
+ case "Expire": return state._tag === "Draining" && stopped.at >= state.deadline ? {
60
+ _tag: "Open",
61
+ inputs: state.inputs
62
+ } : state;
63
+ }
64
+ };
65
+ const make$3 = (options) => {
66
+ let state = {
67
+ _tag: "Open",
68
+ inputs: /* @__PURE__ */ new Set()
69
+ };
70
+ let next = 0;
71
+ let refused = Deferred.makeUnsafe();
72
+ let ready = Deferred.makeUnsafe();
73
+ const error = (operation) => new BrowserError({
74
+ operation,
75
+ reason: options.closedBy() === void 0 ? new Suspended() : new Closed({ cause: options.closedBy() ?? "connection" }),
76
+ dispatched: false
77
+ });
78
+ const stopped = () => ({
79
+ since: options.sequence(),
80
+ at: options.now()
81
+ });
82
+ const apply = (input) => {
83
+ if (input._tag !== "Expire" && state._tag === "Draining" && options.now() >= state.deadline) apply({ _tag: "Expire" });
84
+ const before = state;
85
+ state = step(state, input, stopped());
86
+ if (input._tag === "Expire" && before._tag === "Draining" && state._tag === "Open") Deferred.doneUnsafe(ready, Exit.fail(new BrowserError({
87
+ operation: "suspend",
88
+ reason: new Busy({
89
+ waitedMillis: Math.round(options.now() - before.startedAt),
90
+ ahead: before.inputs.size
91
+ }),
92
+ dispatched: false
93
+ })));
94
+ if (before._tag === "Open" && state._tag !== "Open") Deferred.doneUnsafe(refused, Exit.void);
95
+ if (before._tag === "Draining" && state._tag === "Held") Deferred.doneUnsafe(ready, Exit.succeed(state.stopped));
96
+ if (before._tag !== "Open" && state._tag === "Open") {
97
+ refused = Deferred.makeUnsafe();
98
+ ready = Deferred.makeUnsafe();
99
+ }
100
+ return state;
101
+ };
102
+ const check = (operation) => Effect.suspend(() => state._tag === "Open" && options.closedBy() === void 0 ? Effect.void : Effect.fail(error(operation)));
103
+ const suspend = (drain = options.drain) => Effect.suspend(() => {
104
+ const duration = Duration.fromInput(drain);
105
+ if (Option.isNone(duration) || !Duration.isFinite(duration.value) || Duration.toMillis(duration.value) < 0) return Effect.fail(new BrowserError({
106
+ operation: "suspend",
107
+ reason: new InvalidRequest({ detail: "drain must be a finite nonnegative duration" }),
108
+ dispatched: false
109
+ }));
110
+ const id = ++next;
111
+ const leave = Effect.sync(() => apply({
112
+ _tag: "Leave",
113
+ id
114
+ }));
115
+ return Effect.acquireRelease(Effect.gen(function* () {
116
+ if (options.closedBy() !== void 0) return yield* error("suspend");
117
+ const current = apply({
118
+ _tag: "Suspend",
119
+ id,
120
+ drain: Duration.toMillis(duration.value)
121
+ });
122
+ if (current._tag === "Held") return current.stopped;
123
+ if (current._tag !== "Draining") return yield* Effect.die("suspension did not retain authority");
124
+ const pending = ready;
125
+ const expire = Effect.suspend(() => {
126
+ const left = current.deadline - options.now();
127
+ return left > 0 ? Effect.sleep(Duration.millis(left)).pipe(Effect.andThen(expire)) : Effect.sync(() => apply({ _tag: "Expire" })).pipe(Effect.andThen(Deferred.await(pending)));
128
+ });
129
+ const wait = Effect.raceFirst(Deferred.await(pending), expire);
130
+ return yield* Effect.raceFirst(wait, options.lost.pipe(Effect.flatMap(() => Effect.fail(error("suspend"))))).pipe(Effect.interruptible, Effect.filterOrFail(() => options.closedBy() === void 0, () => error("suspend")), Effect.onError(() => leave));
131
+ }), () => leave);
132
+ }).pipe(Effect.provideService(Clock.Clock, options.clock));
133
+ return {
134
+ check,
135
+ suspend,
136
+ refused: (operation) => Effect.suspend(() => state._tag === "Open" ? Deferred.await(refused).pipe(Effect.andThen(Effect.fail(error(operation)))) : Effect.fail(error(operation))),
137
+ enter: (operation) => Effect.suspend(() => {
138
+ if (state._tag !== "Open" || options.closedBy() !== void 0) return Effect.fail(error(operation));
139
+ const id = ++next;
140
+ apply({
141
+ _tag: "Enter",
142
+ id
143
+ });
144
+ return Effect.succeed(() => {
145
+ apply({
146
+ _tag: "End",
147
+ id
148
+ });
149
+ });
150
+ })
151
+ };
152
+ };
153
+ //#endregion
154
+ //#region src/internal/page/openings.ts
155
+ /** A page's input waits for the actual tabs it opened to finish the browser's registration. */
156
+ const make$2 = Effect.fnUntraced(function* (playwright, cdp, registrations, closedBy) {
157
+ const failed = (reason) => Exit.fail(new BrowserError({
158
+ operation: "newPage",
159
+ reason,
160
+ dispatched: false
161
+ }));
162
+ let active;
163
+ const gone = Deferred.makeUnsafe();
164
+ const windowOpened = () => {
165
+ if (active !== void 0) active.windows += 1;
166
+ };
167
+ const popup = (opened) => {
168
+ if (active === void 0) return;
169
+ const registered = registrations.get(opened);
170
+ active.registrations.push(registered === void 0 ? failed(new Failed({ detail: "the popup has no registration" })) : Deferred.await(registered));
171
+ Deferred.doneUnsafe(active.changed, Exit.void);
172
+ active.changed = Deferred.makeUnsafe();
173
+ };
174
+ const clear = Effect.sync(() => {
175
+ active = void 0;
176
+ });
177
+ yield* Effect.acquireRelease(Effect.sync(() => {
178
+ cdp.on("Page.windowOpen", windowOpened);
179
+ playwright.on("popup", popup);
180
+ }), () => Effect.sync(() => {
181
+ active = void 0;
182
+ cdp.off("Page.windowOpen", windowOpened);
183
+ playwright.off("popup", popup);
184
+ Deferred.doneUnsafe(gone, failed(new Closed({ cause: closedBy() })));
185
+ }));
186
+ return {
187
+ mark: () => {
188
+ const mark = {
189
+ windows: 0,
190
+ registrations: [],
191
+ changed: Deferred.makeUnsafe()
192
+ };
193
+ active = mark;
194
+ return mark;
195
+ },
196
+ clear,
197
+ settle: (mark) => Effect.gen(function* () {
198
+ while (mark.registrations.length < mark.windows) yield* Deferred.await(mark.changed);
199
+ yield* Effect.forEach(mark.registrations, (registration) => registration);
200
+ }).pipe(Effect.raceFirst(Deferred.await(gone)), Effect.ensuring(clear))
201
+ };
202
+ });
203
+ //#endregion
204
+ //#region src/internal/timeline/events.ts
205
+ /**
206
+ * The browser's bounded event history and replay cursors. Readers share the same records and
207
+ * wakeup generation, so a delayed reader cannot block input or retain an unbounded private queue.
208
+ */
209
+ /** The owning Browser validates capacity and closes this timeline with its scope. */
210
+ const make$1 = (capacity) => {
211
+ let records = [];
212
+ let sequence = 0;
213
+ let closed = false;
214
+ let changed = Deferred.makeUnsafe();
215
+ const wake = () => {
216
+ const previous = changed;
217
+ changed = Deferred.makeUnsafe();
218
+ Deferred.doneUnsafe(previous, Effect.void);
219
+ };
220
+ const publish = (event) => {
221
+ if (closed) return sequence;
222
+ sequence += 1;
223
+ const recorded = new RecordedEvent({
224
+ sequence,
225
+ event
226
+ });
227
+ records = records.length < capacity ? [...records, recorded] : [...records.slice(1), recorded];
228
+ wake();
229
+ return sequence;
230
+ };
231
+ const stream = (after) => Stream.unwrap(Effect.sync(() => {
232
+ let cursor = after ?? sequence;
233
+ if (!Number.isSafeInteger(cursor) || cursor < 0 || cursor > sequence) return Stream.fail(undispatched("events", new InvalidRequest({ detail: "the event cursor must be a nonnegative integer no greater than the latest sequence" })));
234
+ return Stream.fromEffectRepeat(Effect.gen(function* () {
235
+ while (true) {
236
+ const oldest = records[0]?.sequence ?? sequence + 1;
237
+ if (cursor < oldest - 1) return yield* undispatched("events", new EventHistoryExpired({
238
+ after: cursor,
239
+ oldest,
240
+ latest: sequence
241
+ }));
242
+ const next = records[cursor - oldest + 1];
243
+ if (next !== void 0) {
244
+ cursor = next.sequence;
245
+ return next;
246
+ }
247
+ if (closed) return yield* Cause.done();
248
+ const waiting = changed;
249
+ yield* Deferred.await(waiting);
250
+ }
251
+ }));
252
+ }));
253
+ return {
254
+ publish,
255
+ sequence: () => sequence,
256
+ recent: Effect.sync(() => records),
257
+ stream,
258
+ close: Effect.sync(() => {
259
+ if (closed) return;
260
+ closed = true;
261
+ wake();
262
+ })
263
+ };
264
+ };
265
+ //#endregion
266
+ //#region src/Browser.ts
267
+ /**
268
+ * A live browser: its pages and what happens in it.
269
+ *
270
+ * `Browser` is a service with one implementation, built by a provider over a Playwright browser
271
+ * context: `Chromium` launches one locally, `Cdp` connects to any DevTools endpoint and
272
+ * `effect-browserbase` allocates a hosted one. Code written against `Browser` runs on all three.
273
+ * A page is named by its CDP target id, so a new connection to the same browser finds it again.
274
+ *
275
+ * @since 0.3.0
276
+ */
277
+ var Browser_exports = /* @__PURE__ */ __exportAll({
278
+ Browser: () => Browser,
279
+ Suspension: () => Suspension,
280
+ make: () => make
281
+ });
282
+ /** When admitted input finished; events after `since` describe the handoff interval. */
283
+ var Suspension = class extends Schema.Class("effect-browser/Suspension")({
284
+ since: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
285
+ at: Schema.Finite
286
+ }) {};
287
+ var Browser = class extends Context.Service()("effect-browser/Browser") {};
288
+ /** The settings every page shares, and how many events the browser keeps, from its options. */
289
+ const settingsOf = Effect.fnUntraced(function* (options) {
290
+ const invalid = (detail) => undispatched("make", new InvalidRequest({ detail }));
291
+ const bound = (name, input, fallback) => {
292
+ const decoded = Duration.fromInput(input ?? fallback);
293
+ return Option.isSome(decoded) && Number.isFinite(Duration.toMillis(decoded.value)) && Duration.isPositive(decoded.value) ? Effect.succeed(decoded.value) : Effect.fail(invalid(`${name} must be finite and greater than zero`));
294
+ };
295
+ const settings = {
296
+ actionTimeout: yield* bound("actionTimeout", options.actionTimeout, Duration.seconds(10)),
297
+ navigationTimeout: yield* bound("navigationTimeout", options.navigationTimeout, Duration.seconds(30)),
298
+ frameHistory: yield* bound("frameHistory", options.frameHistory, Duration.seconds(5)),
299
+ guard: options.guard,
300
+ policyTimeout: yield* bound("policyTimeout", options.policyTimeout, Duration.minutes(5))
301
+ };
302
+ const eventHistory = options.eventHistory ?? 4096;
303
+ const { maxPages } = options;
304
+ if (!Number.isSafeInteger(eventHistory) || eventHistory < 1) return yield* invalid("eventHistory must be a positive safe integer");
305
+ if (maxPages !== void 0 && (!Number.isSafeInteger(maxPages) || maxPages < 1)) return yield* invalid("maxPages must be a positive safe integer");
306
+ return {
307
+ settings,
308
+ eventHistory,
309
+ maxPages
310
+ };
311
+ });
312
+ /**
313
+ * A provider that ends a session at `expiresAt` by its own clock can cut the connection a little
314
+ * before this host's clock gets there.
315
+ */
316
+ const endMargin = Duration.seconds(5);
317
+ /**
318
+ * The browser's loss, once: its context closing from the other side, at or after the session's
319
+ * end or before it, or `lose`, as the owner's scope releases it. Playwright leaves the calls in
320
+ * flight on the library's own sessions unanswered when the connection drops, so `untilLost` fails
321
+ * each with the browser, as a closed page's call fails.
322
+ */
323
+ const watchLoss = Effect.fnUntraced(function* (context, expiresAt, clock, disconnected) {
324
+ let lostBy;
325
+ const lost = yield* Deferred.make();
326
+ const inFlight = /* @__PURE__ */ new Set();
327
+ const gone = () => /* @__PURE__ */ new Error("Target page, context or browser has been closed");
328
+ const lose = (cause) => {
329
+ if (lostBy !== void 0) return;
330
+ lostBy = cause;
331
+ for (const reject of inFlight) reject(gone());
332
+ disconnected(cause);
333
+ Deferred.doneUnsafe(lost, Exit.succeed(cause));
334
+ };
335
+ const untilLost = (reply) => {
336
+ if (lostBy !== void 0) {
337
+ reply.catch(() => void 0);
338
+ return Promise.reject(gone());
339
+ }
340
+ return new Promise((resolve, reject) => {
341
+ inFlight.add(reject);
342
+ reply.then(resolve, reject).finally(() => inFlight.delete(reject));
343
+ });
344
+ };
345
+ const ended = () => expiresAt !== void 0 && clock.currentTimeMillisUnsafe() >= DateTime.toEpochMillis(expiresAt) - Duration.toMillis(endMargin);
346
+ const onClose = () => lose(ended() ? "session" : "connection");
347
+ context.on("close", onClose);
348
+ yield* Effect.addFinalizer(() => Effect.sync(() => context.off("close", onClose)));
349
+ if (context.browser()?.isConnected() === false) onClose();
350
+ const native = (operation, run) => call(operation, run, () => lostBy ?? "page");
351
+ return {
352
+ lostBy: () => lostBy,
353
+ lost: Deferred.await(lost),
354
+ lose,
355
+ untilLost,
356
+ native
357
+ };
358
+ });
359
+ /**
360
+ * What a page's own Playwright events publish, its close and crash aside, while its scope is
361
+ * open. Dialogs are answered: alerts accepted, others dismissed. The page's own session tells its
362
+ * documents' commits and loads.
363
+ */
364
+ const listen = (playwright, page, publish, now, on) => {
365
+ const dialog = (shown) => {
366
+ const kind = shown.type();
367
+ const answer = kind === "alert" || kind === "beforeunload" ? "accepted" : "dismissed";
368
+ const message = shown.message().slice(0, 500);
369
+ publish(new DialogShown({
370
+ at: now(),
371
+ page,
372
+ kind,
373
+ message,
374
+ answer
375
+ }));
376
+ (answer === "accepted" ? shown.accept() : shown.dismiss()).catch(() => void 0);
377
+ };
378
+ return Effect.acquireRelease(Effect.sync(() => {
379
+ playwright.on("close", on.close);
380
+ playwright.on("crash", on.crash);
381
+ playwright.on("dialog", dialog);
382
+ }), () => Effect.sync(() => {
383
+ playwright.off("close", on.close);
384
+ playwright.off("crash", on.crash);
385
+ playwright.off("dialog", dialog);
386
+ }));
387
+ };
388
+ /**
389
+ * Register each page the site opens, such as a popup, as Playwright reports it, for as long as the
390
+ * scope is open. One that could not be tracked is published, unless it closed as it opened.
391
+ */
392
+ const followOpened = Effect.fnUntraced(function* (context, register, publish, now, registrations) {
393
+ const opened = yield* Queue.unbounded();
394
+ const onPage = (playwright) => {
395
+ registrations.set(playwright, Deferred.makeUnsafe());
396
+ Queue.offerUnsafe(opened, playwright);
397
+ };
398
+ const untracked = (playwright) => (error) => Effect.sync(() => {
399
+ const url = redact(playwright.url());
400
+ if (!playwright.isClosed()) publish(new PageUntracked({
401
+ at: now(),
402
+ url,
403
+ detail: error.message
404
+ }));
405
+ });
406
+ context.on("page", onPage);
407
+ yield* Effect.addFinalizer(() => Effect.sync(() => context.off("page", onPage)));
408
+ yield* Queue.take(opened).pipe(Effect.flatMap((playwright) => register(playwright).pipe(Effect.asVoid, Effect.onExit((exit) => Effect.sync(() => {
409
+ const done = registrations.get(playwright);
410
+ if (done !== void 0) Deferred.doneUnsafe(done, exit);
411
+ })), Effect.catch(untracked(playwright)))), Effect.forever, Effect.forkScoped);
412
+ });
413
+ /** Release something native, on the owner's clock, giving up after a second: cleanup only. */
414
+ const releaseNative = (clock) => (run) => Effect.tryPromise(run).pipe(Effect.interruptible, Effect.timeoutOrElse({
415
+ duration: Duration.seconds(1),
416
+ orElse: () => Effect.void
417
+ }), Effect.provideService(Clock.Clock, clock), Effect.ignore);
418
+ /** A page's own events among the browser's retained ones, and the openings of tabs it opened. */
419
+ const eventsOf = (recent, page) => Effect.map(recent, (records) => records.flatMap(({ event }) => "page" in event && event.page === page || event._tag === "PageOpened" && event.opener === page ? [event] : []));
420
+ /** A script as its documents run it: everywhere, or as a block where its `match` allows. */
421
+ const scoped = ({ match, source }) => match === void 0 ? source : `if (${String(match)}.test(location.href)) {\n${source}\n}`;
422
+ /**
423
+ * At most `maxPages` open pages, those a site opened included, which are never refused: an open
424
+ * past it waits for one to close, within `wait` on the owner's clock, then fails `Limit`, or fails
425
+ * at once under `FailFast`. An open counts until its page is tracked.
426
+ */
427
+ const pageBudget = (maxPages, tracked, wait, clock) => {
428
+ let opening = 0;
429
+ let roomMade = Deferred.makeUnsafe();
430
+ const makeRoom = () => {
431
+ Deferred.doneUnsafe(roomMade, Exit.void);
432
+ roomMade = Deferred.makeUnsafe();
433
+ };
434
+ const full = (limit) => tracked() + opening >= limit;
435
+ const room = (limit) => Effect.gen(function* () {
436
+ const refused = undispatched("newPage", new Limit({ maxPages: limit }));
437
+ if ((yield* FailFast) && full(limit)) return yield* refused;
438
+ yield* Effect.gen(function* () {
439
+ while (full(limit)) yield* Deferred.await(roomMade);
440
+ opening += 1;
441
+ }).pipe(Effect.timeoutOrElse({
442
+ duration: wait,
443
+ orElse: () => Effect.fail(refused)
444
+ }), Effect.provideService(Clock.Clock, clock));
445
+ });
446
+ const open = (opened) => maxPages === void 0 ? opened : Effect.uninterruptibleMask((restore) => restore(room(maxPages)).pipe(Effect.andThen(restore(opened).pipe(Effect.ensuring(Effect.sync(() => {
447
+ opening -= 1;
448
+ makeRoom();
449
+ }))))));
450
+ return {
451
+ makeRoom,
452
+ open
453
+ };
454
+ };
455
+ /** A call sent now and awaited later, by whatever needs its answer. */
456
+ const sentNow = (sent) => {
457
+ sent.catch(() => void 0);
458
+ return sent;
459
+ };
460
+ /** Close tab resources after publishing their close; a browser loss speaks for all its tabs. */
461
+ const closures = (lostBy, publish, now) => Effect.gen(function* () {
462
+ const closing = yield* Queue.unbounded();
463
+ yield* Queue.take(closing).pipe(Effect.tap(({ page, cause }) => Effect.sync(() => {
464
+ if (lostBy() === void 0) publish(new PageClosed({
465
+ at: now(),
466
+ page,
467
+ cause
468
+ }));
469
+ })), Effect.flatMap(({ scope }) => Scope.close(scope, Exit.void)), Effect.forever, Effect.forkScoped);
470
+ return closing;
471
+ });
472
+ /**
473
+ * Build the service over a context the provider owns. The provider closes the context; this only
474
+ * tracks its pages and events, for as long as the surrounding scope is open. Each page's protocol
475
+ * session, capture and listeners end when that page closes or when the scope does, so a closed
476
+ * tab retains nothing and the context keeps none of them afterwards.
477
+ */
478
+ const make = Effect.fn("Browser.make")(function* (context, info, options = {}) {
479
+ const clock = yield* Clock.Clock;
480
+ const now = () => Number(clock.monotonicTimeNanosUnsafe()) / 1e6;
481
+ const { settings, eventHistory, maxPages } = yield* settingsOf(options);
482
+ const mapping$1 = mapping(now);
483
+ const timeline = make$1(eventHistory);
484
+ yield* Effect.addFinalizer(() => timeline.close);
485
+ const publish = timeline.publish;
486
+ if (info.expiresAt !== void 0) publish(new SessionEnding({
487
+ at: now(),
488
+ expiresAt: info.expiresAt
489
+ }));
490
+ const { lostBy, lost, lose, untilLost, native } = yield* watchLoss(context, info.expiresAt, clock, (cause) => publish(new Disconnected({
491
+ at: now(),
492
+ cause
493
+ })));
494
+ const inputGate = make$3({
495
+ now,
496
+ sequence: timeline.sequence,
497
+ closedBy: lostBy,
498
+ lost,
499
+ clock,
500
+ drain: settings.navigationTimeout
501
+ });
502
+ const registry = /* @__PURE__ */ new Map();
503
+ const registrations = /* @__PURE__ */ new WeakMap();
504
+ const registering = yield* Semaphore.make(1);
505
+ const pagesScope = yield* Scope.fork(yield* Scope.Scope);
506
+ const release = releaseNative(clock);
507
+ const budget = pageBudget(maxPages, () => registry.size, settings.actionTimeout, clock);
508
+ const closing = yield* closures(lostBy, publish, now);
509
+ const register = (playwright) => registering.withPermits(1)(Effect.gen(function* () {
510
+ const known = registry.get(playwright);
511
+ if (known !== void 0) return known;
512
+ if (playwright.isClosed()) return yield* failWith("newPage", new Failed({ detail: "the page closed as it opened" }));
513
+ const scope = yield* Scope.fork(pagesScope);
514
+ return yield* Effect.gen(function* () {
515
+ let crashed = false;
516
+ const closedBy = () => lostBy() ?? (crashed ? "crashed" : "page");
517
+ const cdp = yield* native("newPage", () => context.newCDPSession(playwright));
518
+ yield* Effect.addFinalizer(() => release(() => cdp.detach()));
519
+ const send = (method, params) => untilLost(cdp.send(method, params));
520
+ const focusing = sentNow(send("Emulation.setFocusEmulationEnabled", { enabled: true }));
521
+ const paging = sentNow(send("Page.enable"));
522
+ const { targetInfo } = yield* native("newPage", () => send("Target.getTargetInfo"));
523
+ const id = targetInfo.targetId;
524
+ const page = yield* make$4({
525
+ id,
526
+ session: info.id,
527
+ url: targetInfo.url,
528
+ playwright,
529
+ cdp,
530
+ untilLost,
531
+ settings,
532
+ inputGate,
533
+ clock,
534
+ mapping: mapping$1,
535
+ publish,
536
+ recentEvents: eventsOf(timeline.recent, id),
537
+ openings: yield* make$2(playwright, cdp, registrations, closedBy),
538
+ focused: call("focus", () => focusing, closedBy).pipe(Effect.asVoid),
539
+ paging: (operation) => call(operation, () => paging, closedBy).pipe(Effect.asVoid),
540
+ closedBy,
541
+ capture: info.capture
542
+ });
543
+ const close = () => {
544
+ if (registry.get(playwright) !== page) return;
545
+ registry.delete(playwright);
546
+ budget.makeRoom();
547
+ Queue.offerUnsafe(closing, {
548
+ scope,
549
+ page: id,
550
+ cause: crashed ? "crashed" : "page"
551
+ });
552
+ };
553
+ const crash = () => {
554
+ crashed = true;
555
+ playwright.close().catch(() => void 0);
556
+ };
557
+ registry.set(playwright, page);
558
+ publish(new PageOpened({
559
+ at: now(),
560
+ page: id,
561
+ url: redact(targetInfo.url),
562
+ opener: targetInfo.openerId
563
+ }));
564
+ yield* listen(playwright, id, publish, now, {
565
+ close,
566
+ crash
567
+ });
568
+ if (playwright.isClosed()) close();
569
+ return page;
570
+ }).pipe(Scope.provide(scope), Effect.onError(() => Scope.close(scope, Exit.void)));
571
+ }));
572
+ for (const script of options.initScripts ?? []) yield* native("initScripts", () => context.addInitScript({ content: scoped(script) }));
573
+ yield* followOpened(context, register, publish, now, registrations);
574
+ for (const existing of context.pages()) yield* register(existing);
575
+ const pages = Effect.sync(() => [...registry.values()]);
576
+ const newPage = Effect.fn("Browser.newPage")(function* (url) {
577
+ const page = yield* budget.open(native("newPage", () => context.newPage()).pipe(Effect.flatMap((playwright) => register(playwright).pipe(Effect.onError(() => release(() => playwright.close()))))));
578
+ if (url !== void 0) yield* page.goto(url).pipe(Effect.onError(() => page.close.pipe(Effect.ignore)));
579
+ return page;
580
+ });
581
+ yield* Effect.addFinalizer(() => Effect.sync(() => lose("released")));
582
+ const service = {
583
+ id: info.id,
584
+ provider: info.provider,
585
+ expiresAt: info.expiresAt,
586
+ now: Effect.sync(now),
587
+ suspend: (options = {}) => Effect.map(inputGate.suspend(options.drain), (held) => new Suspension(held)),
588
+ context,
589
+ pages,
590
+ page: (id) => Effect.map(pages, (open) => Option.fromNullishOr(open.find((page) => page.id === id))),
591
+ firstPage: pages.pipe(Effect.map((open) => Option.fromNullishOr(open[0])), Effect.flatMap(Option.match({
592
+ onNone: () => newPage(),
593
+ onSome: Effect.succeed
594
+ }))),
595
+ newPage,
596
+ disconnected: lost,
597
+ events: (eventOptions = {}) => timeline.stream(eventOptions.after),
598
+ recentEvents: timeline.recent.pipe(Effect.map((records) => records.map((record) => record.event)))
599
+ };
600
+ return Browser.of(service);
601
+ });
602
+ //#endregion
603
+ export { make as i, Browser_exports as n, Suspension as r, Browser as t };
604
+
605
+ //# sourceMappingURL=Browser-S4kvpmqa.mjs.map