effect-browserbase 0.1.0-beta.102 → 0.2.0-beta.0

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 (130) hide show
  1. package/README.md +61 -231
  2. package/dist/Account.d.mts +4 -6
  3. package/dist/Account.mjs +4 -9
  4. package/dist/Account.mjs.map +1 -1
  5. package/dist/{Acquisition-WBafHwXH.mjs → Acquisition-D8o4jtCg.mjs} +22 -68
  6. package/dist/Acquisition-D8o4jtCg.mjs.map +1 -0
  7. package/dist/Agents.d.mts +4 -6
  8. package/dist/Agents.mjs +3 -19
  9. package/dist/Agents.mjs.map +1 -1
  10. package/dist/Allocation.d.mts +9 -11
  11. package/dist/Allocation.mjs +2 -4
  12. package/dist/Allocation.mjs.map +1 -1
  13. package/dist/Browser.d.mts +25 -140
  14. package/dist/Browser.mjs +266 -1
  15. package/dist/Browser.mjs.map +1 -0
  16. package/dist/BrowserBinding.d.mts +7 -34
  17. package/dist/BrowserBinding.mjs +30 -1
  18. package/dist/BrowserBinding.mjs.map +1 -0
  19. package/dist/Certificates.d.mts +3 -5
  20. package/dist/Certificates.mjs +2 -7
  21. package/dist/Certificates.mjs.map +1 -1
  22. package/dist/{Cleanup-G2YpTCuU.d.mts → Cleanup-DMTDBir8.d.mts} +5 -6
  23. package/dist/Cleanup.d.mts +3 -2
  24. package/dist/Cleanup.mjs +1 -6
  25. package/dist/Cleanup.mjs.map +1 -1
  26. package/dist/{Client-D1_xYM9o.mjs → Client-BQteUePT.mjs} +2 -4
  27. package/dist/{Client-D1_xYM9o.mjs.map → Client-BQteUePT.mjs.map} +1 -1
  28. package/dist/Client.d.mts +51 -2
  29. package/dist/Client.mjs +1 -1
  30. package/dist/Connection-DcvJZEdc.mjs +65 -0
  31. package/dist/Connection-DcvJZEdc.mjs.map +1 -0
  32. package/dist/ContextCoordination.d.mts +5 -6
  33. package/dist/ContextCoordination.mjs +1 -8
  34. package/dist/ContextCoordination.mjs.map +1 -1
  35. package/dist/Contexts.d.mts +4 -6
  36. package/dist/Contexts.mjs +2 -8
  37. package/dist/Contexts.mjs.map +1 -1
  38. package/dist/Downloads.d.mts +6 -8
  39. package/dist/Downloads.mjs +3 -8
  40. package/dist/Downloads.mjs.map +1 -1
  41. package/dist/Errors.d.mts +3 -26
  42. package/dist/Errors.mjs +1 -137
  43. package/dist/Errors.mjs.map +1 -1
  44. package/dist/{Extensions-ClZbYb1H.mjs → Extensions-nMNMPGCc.mjs} +3 -9
  45. package/dist/{Extensions-ClZbYb1H.mjs.map → Extensions-nMNMPGCc.mjs.map} +1 -1
  46. package/dist/Extensions.d.mts +4 -6
  47. package/dist/Extensions.mjs +1 -1
  48. package/dist/Functions.d.mts +4 -6
  49. package/dist/Functions.mjs +3 -17
  50. package/dist/Functions.mjs.map +1 -1
  51. package/dist/{Launch-BHcAEq05.mjs → Launch-m0fxzJ49.mjs} +5 -5
  52. package/dist/Launch-m0fxzJ49.mjs.map +1 -0
  53. package/dist/Launch.d.mts +130 -2
  54. package/dist/Launch.mjs +4 -11
  55. package/dist/Launch.mjs.map +1 -1
  56. package/dist/PageFetch.d.mts +3 -5
  57. package/dist/PageFetch.mjs +2 -8
  58. package/dist/PageFetch.mjs.map +1 -1
  59. package/dist/Projects.d.mts +3 -5
  60. package/dist/Projects.mjs +2 -8
  61. package/dist/Projects.mjs.map +1 -1
  62. package/dist/Recordings.d.mts +6 -8
  63. package/dist/Recordings.mjs +3 -5
  64. package/dist/Recordings.mjs.map +1 -1
  65. package/dist/{References-URDAtapt.d.mts → References-DOH3Wu44.d.mts} +5 -6
  66. package/dist/References.d.mts +3 -2
  67. package/dist/References.mjs +10 -18
  68. package/dist/References.mjs.map +1 -1
  69. package/dist/Replays.d.mts +6 -8
  70. package/dist/Replays.mjs +3 -5
  71. package/dist/Replays.mjs.map +1 -1
  72. package/dist/Search.d.mts +3 -5
  73. package/dist/Search.mjs +2 -9
  74. package/dist/Search.mjs.map +1 -1
  75. package/dist/{SessionData-DRKcKCqw.d.mts → SessionData-5xYZDshJ.d.mts} +5 -6
  76. package/dist/SessionData.d.mts +3 -2
  77. package/dist/SessionData.mjs +1 -8
  78. package/dist/SessionData.mjs.map +1 -1
  79. package/dist/{Sessions-CtbseWEh.mjs → Sessions-DoQGg9nB.mjs} +34 -18
  80. package/dist/Sessions-DoQGg9nB.mjs.map +1 -0
  81. package/dist/{Sessions-B8E4kMxo.d.mts → Sessions-tFJYaXFq.d.mts} +8 -8
  82. package/dist/Sessions.d.mts +4 -3
  83. package/dist/Sessions.mjs +1 -1
  84. package/dist/{Transfers-Czv5IZzJ.d.mts → Transfers-GVqzrDWl.d.mts} +22 -9
  85. package/dist/Transfers.d.mts +3 -2
  86. package/dist/Transfers.mjs +12 -28
  87. package/dist/Transfers.mjs.map +1 -1
  88. package/dist/Uploads.d.mts +6 -8
  89. package/dist/Uploads.mjs +3 -5
  90. package/dist/Uploads.mjs.map +1 -1
  91. package/dist/Webhooks.d.mts +3 -5
  92. package/dist/Webhooks.mjs +2 -11
  93. package/dist/Webhooks.mjs.map +1 -1
  94. package/dist/{WriterFacts-osvYAsIx.d.mts → WriterFacts-5CPBGG1P.d.mts} +4 -3
  95. package/dist/index.d.mts +27 -30
  96. package/dist/index.mjs +26 -30
  97. package/package.json +3 -24
  98. package/dist/Acquisition-WBafHwXH.mjs.map +0 -1
  99. package/dist/Association-CEps4hYP.mjs +0 -10
  100. package/dist/Association-CEps4hYP.mjs.map +0 -1
  101. package/dist/Bootstrap-BXuzZojd.mjs +0 -194
  102. package/dist/Bootstrap-BXuzZojd.mjs.map +0 -1
  103. package/dist/Bootstrap-m7lHgj0D.d.mts +0 -177
  104. package/dist/Bootstrap.d.mts +0 -2
  105. package/dist/Bootstrap.mjs +0 -2
  106. package/dist/Browser-joE4Zgys.mjs +0 -1557
  107. package/dist/Browser-joE4Zgys.mjs.map +0 -1
  108. package/dist/BrowserBinding-qbQA6JhH.mjs +0 -2452
  109. package/dist/BrowserBinding-qbQA6JhH.mjs.map +0 -1
  110. package/dist/BrowserData-LpcAuAub.d.mts +0 -369
  111. package/dist/BrowserData.d.mts +0 -2
  112. package/dist/BrowserData.mjs +0 -442
  113. package/dist/BrowserData.mjs.map +0 -1
  114. package/dist/Capture-CZJPuD7b.mjs +0 -532
  115. package/dist/Capture-CZJPuD7b.mjs.map +0 -1
  116. package/dist/Capture-Dr5MkIJb.d.mts +0 -125
  117. package/dist/Capture.d.mts +0 -2
  118. package/dist/Capture.mjs +0 -2
  119. package/dist/Client-Ca0zSg1n.d.mts +0 -53
  120. package/dist/Images-DgscEfSf.mjs +0 -127
  121. package/dist/Images-DgscEfSf.mjs.map +0 -1
  122. package/dist/Launch-BHcAEq05.mjs.map +0 -1
  123. package/dist/Launch-C8CfaYxb.d.mts +0 -132
  124. package/dist/PageControl.d.mts +0 -16
  125. package/dist/PageControl.mjs +0 -36
  126. package/dist/PageControl.mjs.map +0 -1
  127. package/dist/PageControlAssociation-DabEg5AR.mjs +0 -10
  128. package/dist/PageControlAssociation-DabEg5AR.mjs.map +0 -1
  129. package/dist/Sessions-CtbseWEh.mjs.map +0 -1
  130. package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
@@ -1,1557 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { Identifier } from "./References.mjs";
3
- import { BrowserError, InitializationError, SessionError } from "./Errors.mjs";
4
- import { DownloadObservation } from "./Transfers.mjs";
5
- import { t as BrowserbaseClient } from "./Client-D1_xYM9o.mjs";
6
- import { ActionResult, AutomationOptions, BrowserPolicy, Checkpoint, CheckpointOptions, ClickRequest, FillRequest, HoverRequest, InlineFiles, InputReceipt, KeyStroke, NavigateRequest, NavigationResult, Observation, ObservationOptions, ObservedElement, PointerMoveRequest, PressRequest, ReadTextRequest, ScreenshotRequest, ScreenshotResult, ScrollRequest, Selector, StartNavigationRequest, Target, TextResult, TypeRequest, Viewport, WheelRequest } from "./BrowserData.mjs";
7
- import { isTerminalSessionStatus } from "./SessionData.mjs";
8
- import { n as nowMillis, r as until, t as deadlineAfter } from "./Deadline-CrViaogr.mjs";
9
- import { o as issueLiveView, t as BrowserbaseSessions } from "./Sessions-CtbseWEh.mjs";
10
- import { t as issuedUpload } from "./Issued-DejqoMYL.mjs";
11
- import { a as reported, i as noLocalConnection, n as reportCleanup, o as connectionAddress, r as makeCleanup, t as acquireRemote } from "./Acquisition-WBafHwXH.mjs";
12
- import { d as empty, h as snapshot, m as invoke, s as Plan } from "./Bootstrap-BXuzZojd.mjs";
13
- import { o as publicError } from "./Images-DgscEfSf.mjs";
14
- import { a as bindingImplementation, t as BrowserbaseBrowserBinding } from "./BrowserBinding-qbQA6JhH.mjs";
15
- import { t as associate } from "./Association-CEps4hYP.mjs";
16
- import { t as associatePageControl } from "./PageControlAssociation-DabEg5AR.mjs";
17
- import { Cause, Clock, Context, Deferred, Effect, Exit, FiberSet, Layer, Option, Redacted, Schema, Scope, Semaphore } from "effect";
18
- //#region src/internal/browser/BindingRunner.ts
19
- const makeBindingRunner = (capacity, handle, onFault) => {
20
- if (!Number.isSafeInteger(capacity) || capacity < 1) throw new RangeError("Invalid binding callback capacity");
21
- return Effect.gen(function* () {
22
- const parent = yield* Scope.Scope;
23
- const runtimeScope = yield* Scope.fork(parent, "sequential");
24
- const run = yield* FiberSet.makeRuntimePromise().pipe(Scope.provide(runtimeScope));
25
- let accepting = true;
26
- let faulted = false;
27
- let inFlight = 0;
28
- const close = () => {
29
- accepting = false;
30
- };
31
- const fault = () => {
32
- if (!accepting || faulted) return;
33
- faulted = true;
34
- onFault();
35
- };
36
- yield* Scope.addFinalizer(runtimeScope, Effect.sync(close));
37
- const submit = (input, failureMode = "fail-session") => {
38
- if (!accepting || faulted) return {
39
- _tag: "Rejected",
40
- reason: "closed"
41
- };
42
- if (inFlight >= capacity) {
43
- if (failureMode === "fail-session") fault();
44
- return {
45
- _tag: "Rejected",
46
- reason: "capacity"
47
- };
48
- }
49
- inFlight++;
50
- const result = run(Effect.exit(Effect.suspend(() => handle(input)))).then((exit) => {
51
- if (failureMode === "fail-session" && Exit.isFailure(exit)) fault();
52
- return exit;
53
- }, (error) => {
54
- if (failureMode === "fail-session") fault();
55
- throw error;
56
- }).finally(() => {
57
- inFlight--;
58
- });
59
- result.catch(() => {});
60
- return {
61
- _tag: "Accepted",
62
- result
63
- };
64
- };
65
- return {
66
- close,
67
- submit
68
- };
69
- });
70
- };
71
- //#endregion
72
- //#region src/internal/browser/Bootstrap.ts
73
- const guarded = (content, origins) => origins === void 0 ? `(() => {\n${content}\n})();` : `(() => {\nconst origins = ${JSON.stringify([...origins])};\nif (!origins.includes(globalThis.location && globalThis.location.origin)) return;\n${content}\n})();`;
74
- /**
75
- * Dependent steps become one script in declared order: a failing step stops the ones after it
76
- * rather than letting a later step observe half-initialized state. Origin-excluded steps are
77
- * skipped inside the document that reports the origin, never by guessing from a navigation URL.
78
- */
79
- const compileBootstrap = (plan) => {
80
- if (plan.scripts.length === 0 && plan.permissions.length === 0) return void 0;
81
- const bundle = plan.scripts.length === 0 ? void 0 : plan.scripts.map((script) => guarded(script.content, script.origins)).join("\n");
82
- const readiness = plan.scripts.flatMap((script) => script.readiness === void 0 ? [] : [{
83
- step: script.id,
84
- expression: script.readiness.expression,
85
- timeoutMillis: script.readiness.timeoutMillis,
86
- ...script.origins === void 0 ? {} : { origins: script.origins }
87
- }]);
88
- const existingDocuments = plan.scripts.some((script) => script.readiness?.existingDocuments === "RequireFreshNavigation") ? "RequireFreshNavigation" : "AcceptAlreadyRunning";
89
- return {
90
- ...bundle === void 0 ? {} : { bundle },
91
- permissions: plan.permissions.map((grant) => ({
92
- origin: grant.origin,
93
- permissions: [...grant.permissions]
94
- })),
95
- readiness,
96
- existingDocuments
97
- };
98
- };
99
- /** Duplicate step identity would make registration order and readiness reports ambiguous. */
100
- const duplicateStep = (plan) => new Set(plan.scripts.map((script) => script.id)).size !== plan.scripts.length || new Set(plan.permissions.map((grant) => grant.origin)).size !== plan.permissions.length || new Set((plan.bindings ?? []).map((registration) => registration.name)).size !== (plan.bindings?.length ?? 0);
101
- //#endregion
102
- //#region src/internal/browser/Bindings.ts
103
- const invalidPlan = () => BrowserError.make({
104
- operation: "configure",
105
- reason: "configuration",
106
- outcome: "undispatched"
107
- });
108
- const AdmissionPlan = Schema.Struct({
109
- ...Plan.fields,
110
- bindings: Schema.optionalKey(Schema.Array(Schema.Unknown).check(Schema.isMaxLength(16)))
111
- });
112
- /** Validate before allocation and retain an immutable snapshot, including authenticated callbacks. */
113
- const preparePlan = Effect.fnUntraced(function* (plan) {
114
- const source = yield* Effect.try({
115
- try: () => ({ ...plan }),
116
- catch: invalidPlan
117
- });
118
- const data = yield* Schema.decodeEffect(AdmissionPlan, { onExcessProperty: "error" })(source).pipe(Effect.mapError(invalidPlan));
119
- if (duplicateStep({
120
- scripts: data.scripts,
121
- permissions: data.permissions
122
- })) return yield* invalidPlan();
123
- const candidates = source.bindings === void 0 ? [] : source.bindings;
124
- const bindings = yield* Effect.forEach([...candidates], (registration) => snapshot(registration));
125
- if (new Set(bindings.map((registration) => registration.name)).size !== bindings.length) return yield* invalidPlan();
126
- return Object.freeze({
127
- scripts: Object.freeze(data.scripts.map((script) => Object.freeze({
128
- ...script,
129
- ...script.origins === void 0 ? {} : { origins: Object.freeze([...script.origins]) },
130
- ...script.readiness === void 0 ? {} : { readiness: Object.freeze({ ...script.readiness }) }
131
- }))),
132
- permissions: Object.freeze(data.permissions.map((grant) => Object.freeze({
133
- ...grant,
134
- permissions: Object.freeze([...grant.permissions])
135
- }))),
136
- ...bindings.length === 0 ? {} : { bindings: Object.freeze(bindings) }
137
- });
138
- });
139
- const rejected = () => {
140
- const error = /* @__PURE__ */ new Error("Browser binding call rejected");
141
- error.name = "BrowserbaseBindingError";
142
- error.stack = "";
143
- const result = Promise.reject(error);
144
- result.catch(() => {});
145
- return result;
146
- };
147
- const increment = (value) => Math.min(Number.MAX_SAFE_INTEGER, value + 1);
148
- /** The caller must pass preparePlan's snapshot; dependencies are captured at acquisition. */
149
- const makeBindings = Effect.fnUntraced(function* (plan) {
150
- const consumer = yield* Effect.context();
151
- const ownerScope = yield* Scope.fork(yield* Scope.Scope, "sequential");
152
- const failure = yield* Deferred.make();
153
- const connections = /* @__PURE__ */ new Set();
154
- const failures = [];
155
- let droppedFailures = 0;
156
- let faulted = false;
157
- let ownerClosed = false;
158
- const entries = (plan.bindings ?? []).map((registration) => ({
159
- registration,
160
- state: {
161
- name: registration.name,
162
- inFlight: 0,
163
- pendingNative: 0,
164
- retired: 0,
165
- accepted: 0,
166
- succeeded: 0,
167
- rejected: 0
168
- }
169
- }));
170
- yield* Scope.addFinalizer(ownerScope, Effect.suspend(() => {
171
- ownerClosed = true;
172
- const owned = [...connections];
173
- for (const connection of owned) connection.close();
174
- return Effect.forEach(owned, (connection) => connection.dispose, { discard: true });
175
- }));
176
- const connect = Effect.fnUntraced(function* (onFault, isActive, isCurrent = isActive) {
177
- const connectionScope = yield* Scope.fork(yield* Scope.Scope, "sequential");
178
- let closed = ownerClosed || faulted;
179
- const fences = [];
180
- const current = () => !closed && !ownerClosed && !faulted && isCurrent();
181
- const active = () => current() && isActive();
182
- const close = () => {
183
- closed = true;
184
- for (const fence of fences) fence();
185
- };
186
- const record = (registration, cause) => {
187
- if (failures.length === 32) {
188
- failures.shift();
189
- droppedFailures = increment(droppedFailures);
190
- }
191
- failures.push(Object.freeze({
192
- name: registration.name,
193
- mode: registration.failureMode,
194
- cause
195
- }));
196
- if (registration.failureMode === "fail-session" && current()) {
197
- faulted = true;
198
- Deferred.doneUnsafe(failure, Effect.failCause(cause));
199
- for (const connection of connections) connection.close();
200
- onFault();
201
- }
202
- };
203
- const bindings = yield* Effect.forEach(entries, ({ registration, state }) => Effect.gen(function* () {
204
- const error = (reason, operation = "callback") => InitializationError.make({
205
- operation,
206
- step: registration.name,
207
- reason
208
- });
209
- const release = (work) => {
210
- if (!work.finished || work.pending !== 0 || work.released) return;
211
- work.released = true;
212
- state.inFlight--;
213
- state.retired--;
214
- };
215
- const track = (work, promise) => {
216
- work.pending++;
217
- state.pendingNative++;
218
- const settled = () => {
219
- work.pending--;
220
- state.pendingNative--;
221
- release(work);
222
- };
223
- promise.then(settled, settled);
224
- return promise;
225
- };
226
- const finish = (work) => {
227
- if (work.finished) return;
228
- work.finished = true;
229
- state.retired++;
230
- const disposing = Promise.resolve().then(() => work.call.dispose()).catch(() => {
231
- record(registration, Cause.fail(error("native", "dispose")));
232
- });
233
- track(work, disposing);
234
- };
235
- const native = (work, action) => Effect.tryPromise({
236
- try: (signal) => track(work, action(signal)),
237
- catch: (cause) => Schema.is(InitializationError)(cause) ? error(cause.reason) : error("native")
238
- });
239
- const execute = Effect.fnUntraced(function* (work) {
240
- const check = Effect.suspend(() => active() ? native(work, work.call.check) : Effect.fail(error("closed")));
241
- if (!active()) return yield* error("closed");
242
- const text = yield* native(work, work.call.read);
243
- const result = yield* invoke(registration, text, check);
244
- yield* check;
245
- return result;
246
- });
247
- const runner = yield* makeBindingRunner(registration.maxConcurrent, (work) => Effect.scoped(execute(work)).pipe(Effect.timeoutOrElse({
248
- duration: registration.timeoutMillis,
249
- orElse: () => Effect.fail(error("timeout"))
250
- }), Effect.provideContext(consumer)), () => {}).pipe(Scope.provide(connectionScope));
251
- fences.push(runner.close);
252
- const invoke$1 = (call) => {
253
- if (!active()) {
254
- state.rejected = increment(state.rejected);
255
- return rejected();
256
- }
257
- if (state.inFlight >= registration.maxConcurrent) {
258
- state.rejected = increment(state.rejected);
259
- record(registration, Cause.fail(error("busy")));
260
- return rejected();
261
- }
262
- state.inFlight++;
263
- state.accepted = increment(state.accepted);
264
- const work = {
265
- call,
266
- pending: 0,
267
- finished: false,
268
- released: false
269
- };
270
- const admission = runner.submit(work, "reject-call");
271
- if (admission._tag === "Rejected") {
272
- state.rejected = increment(state.rejected);
273
- finish(work);
274
- return rejected();
275
- }
276
- const result = admission.result.then((exit) => {
277
- if (Exit.isFailure(exit)) {
278
- state.rejected = increment(state.rejected);
279
- record(registration, exit.cause);
280
- return rejected();
281
- }
282
- if (!active()) {
283
- state.rejected = increment(state.rejected);
284
- return rejected();
285
- }
286
- state.succeeded = increment(state.succeeded);
287
- return exit.value;
288
- }, () => {
289
- state.rejected = increment(state.rejected);
290
- if (active()) record(registration, Cause.fail(error("native")));
291
- return rejected();
292
- }).finally(() => finish(work));
293
- result.catch(() => {});
294
- return result;
295
- };
296
- return Object.freeze({
297
- name: registration.name,
298
- origins: registration.origins,
299
- maxConcurrent: registration.maxConcurrent,
300
- maxInputBytes: registration.maxInputBytes,
301
- timeoutMillis: registration.timeoutMillis,
302
- invoke: invoke$1
303
- });
304
- }));
305
- const connection = {
306
- bindings: Object.freeze(bindings),
307
- close,
308
- reportFailure: (error) => {
309
- if (!current()) return;
310
- record({
311
- name: error.step,
312
- failureMode: "fail-session"
313
- }, Cause.fail(error));
314
- },
315
- dispose: Effect.sync(close).pipe(Effect.andThen(Scope.close(connectionScope, Exit.void)))
316
- };
317
- connections.add(connection);
318
- yield* Scope.addFinalizer(connectionScope, Effect.sync(() => {
319
- close();
320
- connections.delete(connection);
321
- }));
322
- return connection;
323
- }, Effect.uninterruptible);
324
- return {
325
- failure: Deferred.await(failure),
326
- diagnostics: Effect.sync(() => Object.freeze({
327
- faulted,
328
- bindings: Object.freeze(entries.map(({ state }) => Object.freeze({ ...state }))),
329
- failures: Object.freeze([...failures]),
330
- droppedFailures
331
- })),
332
- connect
333
- };
334
- });
335
- //#endregion
336
- //#region src/internal/session/Attachment.ts
337
- const failure = (reason) => SessionError.make({
338
- operation: "session-attach",
339
- reason,
340
- outcome: "undispatched"
341
- });
342
- /**
343
- * Borrowing an already running session. This scope owns its local connection and nothing
344
- * else: no allocation attempt is made, no release is requested, and no Context-writer
345
- * authority is claimed. Whoever allocated the session keeps all three.
346
- */
347
- const attachRemote = Effect.fnUntraced(function* (options, local = noLocalConnection, limits) {
348
- const client = yield* BrowserbaseClient;
349
- const sessions = yield* BrowserbaseSessions;
350
- const parent = yield* Scope.Scope;
351
- const pendingWaitMillis = options.pendingWaitMillis ?? 0;
352
- if (!Number.isSafeInteger(pendingWaitMillis) || pendingWaitMillis < 0 || pendingWaitMillis > 6e5) return yield* failure("configuration");
353
- const observed = yield* sessions.retrieve(options.reference);
354
- if (isTerminalSessionStatus(observed.status)) return yield* failure("expired");
355
- if (observed.status !== "RUNNING") {
356
- if (pendingWaitMillis === 0) return yield* failure("active");
357
- yield* sessions.waitUntilRunning(observed.reference, { timeoutMillis: pendingWaitMillis });
358
- }
359
- const reference = observed.reference;
360
- return yield* Effect.uninterruptible(Effect.gen(function* () {
361
- const resourceScope = yield* Scope.make();
362
- let closing = false;
363
- let cleanup;
364
- const terminate = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
365
- closing = true;
366
- yield* (yield* makeCleanup(reference, "borrowed", sessions, local, (result) => {
367
- cleanup = result;
368
- }, limits)).close;
369
- if (cleanup !== void 0) yield* reportCleanup(cleanup);
370
- if (cleanup !== void 0 && options.onCleanup !== void 0) yield* reported(options.onCleanup(cleanup));
371
- })));
372
- const closeScope = yield* Effect.cached(Scope.close(resourceScope, Exit.void));
373
- yield* Scope.addFinalizer(resourceScope, terminate);
374
- yield* Scope.addFinalizer(parent, closeScope);
375
- return {
376
- reference,
377
- status: observed,
378
- connection: (timeoutMillis) => Effect.suspend(() => closing ? Effect.fail(SessionError.make({
379
- operation: "session-connect",
380
- reason: "expired",
381
- outcome: "undispatched"
382
- })) : connectionAddress(client, sessions, reference, timeoutMillis)),
383
- release: closeScope.pipe(Effect.andThen(Effect.suspend(() => cleanup === void 0 ? Effect.die(/* @__PURE__ */ new Error("Borrowed session closed without cleanup evidence")) : Effect.succeed(cleanup)))),
384
- cleanupResult: Effect.sync(() => Option.fromNullishOr(cleanup))
385
- };
386
- }));
387
- });
388
- //#endregion
389
- //#region src/internal/browser/Owner.ts
390
- /** The browser domain's bounded step: one deadline, one declared BrowserError timeout. */
391
- const within = (effect, deadline, operation, onTimeout) => until(effect, deadline, () => {
392
- onTimeout?.();
393
- return BrowserError.make({
394
- operation,
395
- reason: "timeout"
396
- });
397
- });
398
- const makeOwner = Effect.fnUntraced(function* (limits) {
399
- const semaphore = yield* Semaphore.make(1);
400
- const clock = yield* Clock.Clock;
401
- const lifetimeDeadline = yield* deadlineAfter(limits.maxElapsedMillis);
402
- const hooks = /* @__PURE__ */ new Set();
403
- const state = {
404
- phase: "acquiring",
405
- generation: 0,
406
- revision: 0,
407
- selection: 0,
408
- actions: 0
409
- };
410
- let active;
411
- const reservations = /* @__PURE__ */ new Map();
412
- const invalidate = (reason) => {
413
- state.revision++;
414
- for (const hook of hooks) hook(reason);
415
- };
416
- const fence = (phase, reason) => {
417
- if (state.phase === "closed" || state.phase === "closing" && phase !== "closed" && phase !== "closing") return;
418
- state.phase = phase;
419
- state.generation++;
420
- active?.abort();
421
- for (const reservation of reservations.values()) reservation.abort();
422
- reservations.clear();
423
- invalidate(reason);
424
- };
425
- /** Taken under the permit that dispatched the work, so nothing can interleave before it. */
426
- const reserve = (key) => {
427
- const controller = new AbortController();
428
- reservations.set(key, controller);
429
- return {
430
- signal: controller.signal,
431
- settle: (outcome) => {
432
- if (reservations.get(key) !== controller) return;
433
- reservations.delete(key);
434
- if (outcome === "unknown") fence("uncertain", "uncertain");
435
- }
436
- };
437
- };
438
- const guard = (operation, body, options = {}) => semaphore.withPermitsIfAvailable(1)(Effect.gen(function* () {
439
- if (!(options.phases ?? ["open"]).includes(state.phase)) return yield* BrowserError.make({
440
- operation,
441
- reason: state.phase === "paused" ? "busy" : "closed",
442
- outcome: "undispatched"
443
- });
444
- yield* options.preflight ?? Effect.void;
445
- const now = yield* nowMillis;
446
- if (now >= lifetimeDeadline) {
447
- fence("uncertain", "uncertain");
448
- return yield* BrowserError.make({
449
- operation,
450
- reason: "timeout",
451
- outcome: "undispatched"
452
- });
453
- }
454
- if (options.charge !== false) {
455
- if (state.actions >= limits.maxActions) return yield* BrowserError.make({
456
- operation,
457
- reason: "limit",
458
- outcome: "undispatched"
459
- });
460
- state.actions++;
461
- }
462
- const controller = new AbortController();
463
- active = controller;
464
- let dispatched = false;
465
- const generation = state.generation;
466
- const allowed = options.phases ?? ["open"];
467
- const check = () => {
468
- if (controller.signal.aborted || state.generation !== generation || !allowed.includes(state.phase)) throw BrowserError.make({
469
- operation,
470
- reason: "stale",
471
- outcome: dispatched ? "unknown" : "undispatched"
472
- });
473
- };
474
- const ticket = {
475
- signal: controller.signal,
476
- deadline: Math.min(now + limits.actionTimeoutMillis, lifetimeDeadline),
477
- generation,
478
- remainingMillis: () => Math.max(1, Math.min(now + limits.actionTimeoutMillis, lifetimeDeadline) - Number(clock.monotonicTimeNanosUnsafe()) / 1e6),
479
- get dispatched() {
480
- return dispatched;
481
- },
482
- check,
483
- dispatch() {
484
- check();
485
- if (!dispatched && options.mutation) invalidate("observation");
486
- dispatched = true;
487
- }
488
- };
489
- const uncertain = () => {
490
- controller.abort();
491
- if (dispatched && state.phase !== "closing" && state.phase !== "closed") fence("uncertain", "uncertain");
492
- };
493
- return yield* within(body(ticket), ticket.deadline, operation, uncertain).pipe(Effect.tap((_) => options.verifyAfter === false ? Effect.void : Effect.try({
494
- try: check,
495
- catch: () => BrowserError.make({
496
- operation,
497
- reason: "stale",
498
- outcome: dispatched ? "unknown" : "undispatched"
499
- })
500
- })), Effect.catch((error) => {
501
- if (dispatched) uncertain();
502
- if (!Schema.is(BrowserError)(error)) return Effect.fail(error);
503
- return Effect.fail(BrowserError.make({
504
- operation,
505
- reason: error.reason,
506
- outcome: error.outcome ?? (dispatched ? "unknown" : "undispatched"),
507
- ...error.status === void 0 ? {} : { status: error.status },
508
- ...error.retryAfterMillis === void 0 ? {} : { retryAfterMillis: error.retryAfterMillis }
509
- }));
510
- }), Effect.onInterrupt(() => Effect.sync(uncertain)), Effect.ensuring(Effect.sync(() => {
511
- controller.abort();
512
- if (active === controller) active = void 0;
513
- })));
514
- })).pipe(Effect.flatMap(Effect.fromOption(() => BrowserError.make({
515
- operation,
516
- reason: "busy",
517
- outcome: "undispatched"
518
- }))));
519
- /** Lifecycle transitions are performed while holding guard's permit; close alone preempts it. */
520
- return {
521
- state,
522
- lifetimeDeadline,
523
- guard,
524
- reserve,
525
- reserved: (key) => reservations.has(key),
526
- invalidate,
527
- fence,
528
- onInvalidate(hook) {
529
- hooks.add(hook);
530
- return () => {
531
- hooks.delete(hook);
532
- };
533
- }
534
- };
535
- });
536
- /** Without a native answer, whether the step was sent is all the owner knows about it. */
537
- const unsettled = (ticket) => ({
538
- reason: "provider",
539
- outcome: ticket.dispatched ? "unknown" : "undispatched"
540
- });
541
- /**
542
- * The one place a native step becomes a public error: whatever the driver raised, the caller
543
- * sees the operation it asked for.
544
- */
545
- const native = (operation, ticket, body) => Effect.callback((resume) => {
546
- let done = false;
547
- const cleanup = () => {
548
- done = true;
549
- ticket.signal.removeEventListener("abort", abort);
550
- };
551
- const abort = () => {
552
- if (done) return;
553
- cleanup();
554
- resume(Effect.fail(BrowserError.make({
555
- operation,
556
- reason: "stale",
557
- outcome: ticket.dispatched ? "unknown" : "undispatched"
558
- })));
559
- };
560
- ticket.signal.addEventListener("abort", abort, { once: true });
561
- try {
562
- ticket.check();
563
- body().then((value) => {
564
- if (!done) {
565
- cleanup();
566
- resume(Effect.succeed(value));
567
- }
568
- }, (error) => {
569
- if (!done) {
570
- cleanup();
571
- resume(Effect.fail(publicError(error, operation, unsettled(ticket))));
572
- }
573
- });
574
- } catch (error) {
575
- cleanup();
576
- resume(Effect.fail(publicError(error, operation, unsettled(ticket))));
577
- }
578
- return Effect.sync(cleanup);
579
- });
580
- //#endregion
581
- //#region src/internal/browser/Session.ts
582
- /** Allocates the session, and therefore owns releasing it. */
583
- const ownedRemote = (options) => (local, lifetimeDeadline) => acquireRemote({
584
- launch: options.launch,
585
- ...options.contextWriter === void 0 ? {} : { contextWriter: options.contextWriter },
586
- allocationDeadline: lifetimeDeadline,
587
- ...options.onCleanup === void 0 ? {} : { onCleanup: options.onCleanup },
588
- ...options.onAllocationUncertain === void 0 ? {} : { onAllocationUncertain: options.onAllocationUncertain }
589
- }, local);
590
- /** Borrows a running session: local connection only, and never a release request. */
591
- const borrowedRemote = (options) => (local) => attachRemote(options, local);
592
- /**
593
- * The one owned browser. Remote allocation, release and terminal observation belong to the
594
- * canonical control plane; this owner supplies only the local connection's cleanup evidence.
595
- */
596
- const acquireSession = Effect.fnUntraced(function* (limits, options) {
597
- const engine = bindingImplementation(yield* BrowserbaseBrowserBinding);
598
- if (engine === void 0) return yield* BrowserError.make({
599
- operation: "connect",
600
- reason: "configuration",
601
- outcome: "undispatched"
602
- });
603
- const client = yield* BrowserbaseClient;
604
- const sessions = yield* BrowserbaseSessions;
605
- const parentScope = yield* Scope.Scope;
606
- const bindingLifetime = yield* Scope.fork(parentScope, "sequential");
607
- const ended = yield* Deferred.make();
608
- const owner = yield* makeOwner(limits);
609
- const clock = yield* Clock.Clock;
610
- let driver;
611
- let connectPending = false;
612
- let activeConnection;
613
- let handoffToken;
614
- let reconnectTarget;
615
- let activeBindings;
616
- const fenceBindings = () => {
617
- activeBindings?.close();
618
- driver?.fenceInitialization?.();
619
- };
620
- const disposeBindings = Effect.gen(function* () {
621
- const connection = activeBindings;
622
- activeBindings = void 0;
623
- const callbacks = yield* Effect.exit(connection?.dispose ?? Effect.void);
624
- const registrations = yield* Effect.tryPromise({
625
- try: () => driver?.disposeInitialization?.() ?? Promise.resolve(),
626
- catch: () => BrowserError.make({
627
- operation: "close",
628
- reason: "provider"
629
- })
630
- }).pipe(Effect.exit);
631
- if (Exit.isFailure(callbacks)) return yield* Effect.failCause(callbacks.cause);
632
- if (Exit.isFailure(registrations)) return yield* Effect.failCause(registrations.cause);
633
- });
634
- const capture = {
635
- owner,
636
- target: () => {
637
- if (driver === void 0) throw BrowserError.make({
638
- operation: "target",
639
- reason: "closed"
640
- });
641
- return Target.make({
642
- generation: owner.state.generation,
643
- ...driver.selected()
644
- });
645
- },
646
- resolve: (ticket, requested) => native("capture-start", ticket, async () => {
647
- if (driver === void 0) throw BrowserError.make({
648
- operation: "capture",
649
- reason: "closed"
650
- });
651
- const binding = await driver.capture(requested);
652
- return {
653
- key: binding.targetId,
654
- target: Target.make({
655
- generation: owner.state.generation,
656
- pageId: binding.pageId,
657
- frameId: binding.frameId
658
- }),
659
- source: binding.source
660
- };
661
- }),
662
- captureLeases: /* @__PURE__ */ new Map(),
663
- captureReservedBytes: 0
664
- };
665
- owner.onInvalidate((reason) => {
666
- driver?.invalidateObservation();
667
- if ([
668
- "disconnected",
669
- "uncertain",
670
- "closed"
671
- ].includes(reason)) fenceBindings();
672
- if ([
673
- "paused",
674
- "disconnected",
675
- "uncertain",
676
- "closed"
677
- ].includes(reason)) for (const lease of capture.captureLeases.values()) lease.invalidate(reason);
678
- });
679
- /**
680
- * The local half of cleanup. Each step is an independent fact for the canonical
681
- * coordinator, which owns the release request and the terminal status observation.
682
- */
683
- const local = {
684
- fence: Effect.sync(() => {
685
- owner.fence("closing", "closed");
686
- handoffToken = void 0;
687
- activeConnection = void 0;
688
- }),
689
- capture: Effect.suspend(() => Effect.forEach([...capture.captureLeases.values()], (lease) => lease.stop, { discard: true })),
690
- initialization: disposeBindings,
691
- disconnect: Effect.suspend(() => {
692
- const acquired = driver;
693
- driver = void 0;
694
- if (acquired === void 0) return Effect.succeed(connectPending ? "pending" : "not-connected");
695
- return Effect.tryPromise({
696
- try: () => acquired.disconnect(),
697
- catch: () => BrowserError.make({
698
- operation: "disconnect",
699
- reason: "provider"
700
- })
701
- }).pipe(Effect.as("closed"));
702
- }).pipe(Effect.ensuring(Effect.sync(() => {
703
- owner.state.phase = "closed";
704
- })))
705
- };
706
- const acquired = yield* options.remote(local, owner.lifetimeDeadline);
707
- const ref = acquired.reference;
708
- const closeScope = acquired.release.pipe(Effect.ensuring(Deferred.succeed(ended, void 0)), Effect.asVoid);
709
- const connectionEvents = (connectionLease) => ({
710
- invalidate: (reason) => {
711
- if (activeConnection === connectionLease) owner.invalidate(reason);
712
- },
713
- disconnected: () => {
714
- if (activeConnection !== connectionLease) return;
715
- if (owner.state.phase !== "closing" && owner.state.phase !== "closed" && owner.state.phase !== "detached") owner.fence("uncertain", "disconnected");
716
- },
717
- pause: () => {
718
- if (activeConnection !== connectionLease) return;
719
- if (owner.state.phase === "open") owner.fence("paused", "paused");
720
- else if (owner.state.phase === "acquiring") owner.fence("uncertain", "uncertain");
721
- },
722
- fault: () => {
723
- if (activeConnection !== connectionLease) return;
724
- if (owner.state.phase !== "closing" && owner.state.phase !== "closed") owner.fence("uncertain", "uncertain");
725
- }
726
- });
727
- const connectNative = (url, nativeOptions) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
728
- const connectionLease = {};
729
- activeConnection = connectionLease;
730
- connectPending = true;
731
- const events = connectionEvents(connectionLease);
732
- const bindings = options.connectBindings === void 0 ? void 0 : yield* options.connectBindings(events.fault, () => activeConnection === connectionLease && (owner.state.phase === "acquiring" || owner.state.phase === "open"), () => activeConnection === connectionLease && [
733
- "acquiring",
734
- "open",
735
- "paused"
736
- ].includes(owner.state.phase)).pipe(Scope.provide(bindingLifetime));
737
- activeBindings = bindings;
738
- const retired = () => activeConnection !== connectionLease || [
739
- "closing",
740
- "closed",
741
- "uncertain"
742
- ].includes(owner.state.phase);
743
- if (retired()) {
744
- bindings?.close();
745
- yield* bindings?.dispose ?? Effect.void;
746
- return yield* BrowserError.make({
747
- operation: "connect",
748
- reason: "closed"
749
- });
750
- }
751
- const acquired = yield* restore(engine.connect({
752
- connection: Redacted.value(url),
753
- options: {
754
- ...nativeOptions,
755
- ...bindings === void 0 ? {} : {
756
- bindings: bindings.bindings,
757
- onBindingFault: bindings.reportFailure
758
- }
759
- },
760
- events,
761
- onAbandoned: () => bindings?.close(),
762
- onSettled: () => {
763
- if (activeConnection === connectionLease) connectPending = false;
764
- }
765
- }));
766
- driver = acquired;
767
- if (activeConnection !== connectionLease || owner.state.phase === "closed" || owner.state.phase === "closing" || owner.state.phase === "uncertain") {
768
- yield* Effect.tryPromise({
769
- try: () => acquired.disconnect(),
770
- catch: () => BrowserError.make({
771
- operation: "connect",
772
- reason: "provider"
773
- })
774
- }).pipe(Effect.ignore);
775
- driver = void 0;
776
- return yield* BrowserError.make({
777
- operation: "connect",
778
- reason: "closed"
779
- });
780
- }
781
- return acquired;
782
- }));
783
- const getDriver = () => {
784
- if (driver === void 0) throw BrowserError.make({
785
- operation: "target",
786
- reason: "closed",
787
- outcome: "undispatched"
788
- });
789
- return driver;
790
- };
791
- /**
792
- * Work that depends on an initialized document waits for the current document's readiness.
793
- * Navigation, selection and page management deliberately do not: initialization must never
794
- * deadlock the navigation that produces the document it is waiting for.
795
- */
796
- const dependent = [
797
- "read-text",
798
- "checkpoint",
799
- "control-facts",
800
- "revalidate",
801
- "click",
802
- "fill",
803
- "scroll",
804
- "pointer-move",
805
- "hover",
806
- "wheel",
807
- "press",
808
- "type",
809
- "screenshot",
810
- "observe",
811
- "wait",
812
- "click-and-wait",
813
- "download-action",
814
- "select-files",
815
- "file-chooser"
816
- ];
817
- const requireReady = async (operation, ticket) => {
818
- if (!dependent.includes(operation)) return;
819
- const state = await getDriver().documentReadiness(ticket);
820
- if (state._tag === "Ready" || state._tag === "NotApplicable") return;
821
- throw BrowserError.make({
822
- operation,
823
- reason: state._tag === "RequiresNavigation" || state.reason === "stale" ? "stale" : state.reason === "timeout" ? "timeout" : "failed",
824
- outcome: "undispatched"
825
- });
826
- };
827
- /**
828
- * `dependent` is false for lifecycle observations, which report what is actually on the
829
- * reattached page. Readiness governs work that depends on initialization, not the reading
830
- * that tells a caller whether this document was initialized at all.
831
- */
832
- const observeInside = (ticket, maximumBytes = Math.min(options.maxReturnedBytes, 16384), controls = 32, dependent = true, scope = "document") => Effect.suspend(() => {
833
- const revision = owner.state.revision;
834
- return native("observe", ticket, async () => {
835
- await getDriver().pageControl?.checkSelected(ticket);
836
- if (dependent) await requireReady("observe", ticket);
837
- return getDriver().observe(scope, maximumBytes, controls, ticket);
838
- }).pipe(Effect.flatMap((raw) => Effect.gen(function* () {
839
- if (owner.state.revision !== revision) return yield* BrowserError.make({
840
- operation: "observe",
841
- reason: "stale"
842
- });
843
- const result = yield* Effect.try({
844
- try: () => Observation.make({
845
- ...raw,
846
- target: capture.target(),
847
- revision
848
- }),
849
- catch: (error) => publicError(error, "observe", { reason: "malformed" })
850
- });
851
- const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(Observation))(result).pipe(Effect.mapError(() => BrowserError.make({
852
- operation: "observe",
853
- reason: "malformed"
854
- })));
855
- if (new TextEncoder().encode(encoded).length > options.maxReturnedBytes) return yield* BrowserError.make({
856
- operation: "observe",
857
- reason: "limit"
858
- });
859
- return result;
860
- })));
861
- });
862
- /** A requested text bound may narrow the policy's bound, never widen it. */
863
- const textBudget = (operation, requested) => {
864
- const bytes = requested ?? Math.min(options.maxReturnedBytes, 16384);
865
- return bytes > options.maxReturnedBytes ? Effect.fail(BrowserError.make({
866
- operation,
867
- reason: "configuration",
868
- outcome: "undispatched"
869
- })) : Effect.succeed(bytes);
870
- };
871
- const readSelected = owner.guard("target", () => Effect.try({
872
- try: capture.target,
873
- catch: () => BrowserError.make({
874
- operation: "target",
875
- reason: "closed",
876
- outcome: "undispatched"
877
- })
878
- }), { charge: false });
879
- /**
880
- * While a navigation is in flight on the selected page, nothing else may change that page.
881
- * Reads, checkpoints, holds and every other page proceed.
882
- */
883
- const unreserved = (operation) => Effect.suspend(() => {
884
- let pageId;
885
- try {
886
- pageId = driver?.selected().pageId;
887
- } catch {}
888
- return pageId !== void 0 && owner.reserved(pageId) ? Effect.fail(BrowserError.make({
889
- operation,
890
- reason: "busy",
891
- outcome: "undispatched"
892
- })) : Effect.void;
893
- });
894
- const nativeOperation = (operation, action, options = {}) => owner.guard(operation, (ticket) => native(operation, ticket, async () => {
895
- if ([
896
- "resize",
897
- "wait",
898
- "click-and-wait",
899
- "download-action",
900
- "select-files",
901
- "file-chooser",
902
- "checkpoint",
903
- "control-facts",
904
- "revalidate"
905
- ].includes(operation)) await getDriver().pageControl?.checkSelected(ticket);
906
- await requireReady(operation, ticket);
907
- return action(getDriver(), ticket);
908
- }), {
909
- ...options,
910
- ...options.mutation === true && options.anyPage !== true ? { preflight: unreserved(operation) } : {}
911
- });
912
- /** A bound handle captures the page selection and connection generation, never the current DOM. */
913
- const bind = () => {
914
- const generation = owner.state.generation, selection = owner.state.selection;
915
- const check = () => {
916
- if (owner.state.generation !== generation || owner.state.selection !== selection) return Effect.fail(BrowserError.make({
917
- operation: "handle",
918
- reason: "stale",
919
- outcome: "undispatched"
920
- }));
921
- return Effect.void;
922
- };
923
- const run = (operation, action, mutation = false) => owner.guard(operation, (ticket) => native(operation, ticket, async () => {
924
- await getDriver().pageControl?.checkSelected(ticket);
925
- await requireReady(operation, ticket);
926
- return action(getDriver(), ticket);
927
- }), {
928
- mutation,
929
- preflight: mutation ? Effect.suspend(check).pipe(Effect.andThen(unreserved(operation))) : Effect.suspend(check)
930
- });
931
- /**
932
- * Native input, stamped on the host monotonic clock that stamps captured frames, around
933
- * the native command alone: admission and readiness are over before the clock is read.
934
- */
935
- const input = (operation, action) => run(operation, async (driver, ticket) => {
936
- const target = capture.target();
937
- const startedMonotonicNanos = clock.monotonicTimeNanosUnsafe();
938
- return {
939
- ...await action(driver, ticket),
940
- target,
941
- startedMonotonicNanos,
942
- completedMonotonicNanos: clock.monotonicTimeNanosUnsafe()
943
- };
944
- }, true);
945
- /**
946
- * Dispatches under a short permit and leaves the browser loading outside it. The reservation
947
- * is taken under that same permit, so no other mutation can reach the page in between, and
948
- * it is released only on a known outcome: an unsettled operation whose scope closes, or a
949
- * navigation that fails after dispatch, fences the owner exactly as an interrupted mutation
950
- * always has. Interrupting a waiter on `completed` stops nothing; `stop` is the one way to.
951
- */
952
- const startNavigation = Effect.fnUntraced(function* (url, timeoutMillis = limits.actionTimeoutMillis) {
953
- const begun = yield* run("navigate", async (driver, ticket) => {
954
- const target = capture.target();
955
- const navigation = await driver.beginNavigation(url, timeoutMillis, ticket);
956
- return {
957
- target,
958
- navigation,
959
- reservation: owner.reserve(navigation.pageId)
960
- };
961
- }, true);
962
- const { navigation, reservation } = begun;
963
- const outcome = yield* Deferred.make();
964
- const failed = (reason) => Effect.fail(BrowserError.make({
965
- operation: "navigate",
966
- reason,
967
- outcome: "unknown"
968
- }));
969
- /**
970
- * A navigation has exactly one outcome, and whoever decides it first settles the
971
- * reservation. It is released before anyone waiting is told, so the operation a waiter
972
- * runs next is admitted rather than finding its own page still reserved.
973
- */
974
- const decide = (result, known) => {
975
- if (Deferred.isDoneUnsafe(outcome)) return;
976
- reservation.settle(known ? "known" : "unknown");
977
- Deferred.doneUnsafe(outcome, result);
978
- };
979
- navigation.settled.then((url) => decide(Effect.succeed(url), true), (error) => decide(Effect.fail(publicError(error, "navigate", {
980
- reason: "provider",
981
- outcome: "unknown"
982
- })), false));
983
- reservation.signal.addEventListener("abort", () => decide(failed("stale"), true), { once: true });
984
- yield* Effect.addFinalizer(() => Effect.sync(() => decide(failed("stale"), false)));
985
- return {
986
- target: begun.target,
987
- completed: Deferred.await(outcome),
988
- /**
989
- * The browser's acknowledgement is the known outcome. Playwright's own promise is not
990
- * waited for: an aborted parse fires no DOMContentLoaded, so it only ever times out.
991
- */
992
- stop: Effect.tryPromise({
993
- try: () => navigation.stop(),
994
- catch: (error) => publicError(error, "navigate-stop", {
995
- reason: "provider",
996
- outcome: "unknown"
997
- })
998
- }).pipe(Effect.timeoutOrElse({
999
- duration: 3e3,
1000
- orElse: () => Effect.fail(BrowserError.make({
1001
- operation: "navigate-stop",
1002
- reason: "timeout",
1003
- outcome: "unknown"
1004
- }))
1005
- }), Effect.tap(() => Effect.sync(() => decide(failed("interrupted"), true))))
1006
- };
1007
- });
1008
- return {
1009
- startNavigation,
1010
- navigate: (url) => Effect.scoped(startNavigation(url).pipe(Effect.flatMap((operation) => operation.completed))),
1011
- readText: (selector) => run("read-text", (driver, ticket) => driver.readText(selector, options.maxReturnedBytes, ticket)),
1012
- click: (target, policy) => run("click", (driver, ticket) => driver.click(target, ticket, policy), true),
1013
- fill: (target, value, policy) => run("fill", (driver, ticket) => driver.fill(target, value, ticket, policy), true),
1014
- scroll: (x, y) => run("scroll", (driver, ticket) => driver.scroll(x, y, ticket), true),
1015
- pointerMove: (to) => input("pointer-move", (driver, ticket) => driver.pointerMove(to, ticket)),
1016
- hover: (target, policy) => input("hover", (driver, ticket) => driver.hover(target, ticket, policy)),
1017
- wheel: (deltaX, deltaY, at) => input("wheel", (driver, ticket) => driver.wheel(deltaX, deltaY, at, ticket)),
1018
- press: (key, modifiers, into, policy) => input("press", (driver, ticket) => driver.press(key, modifiers, into, ticket, policy)),
1019
- type: (text, into, policy) => input("type", (driver, ticket) => driver.type(text, into, ticket, policy)),
1020
- screenshot: (full) => run("screenshot", (driver, ticket) => driver.screenshot(full, options.maxReturnedBytes, ticket))
1021
- };
1022
- };
1023
- /**
1024
- * The remaining execution lifetime bounds waiting for a RUNNING session and its endpoint,
1025
- * within the resource service's own maximum wait. A long business budget does not become
1026
- * an invalid provider-wait configuration.
1027
- */
1028
- const remainingMillis = () => Math.min(6e5, Math.max(1, Math.ceil(owner.lifetimeDeadline - Number(clock.monotonicTimeNanosUnsafe()) / 1e6)));
1029
- const connectionUrl = (operation) => Effect.suspend(() => acquired.connection(remainingMillis())).pipe(Effect.mapError((error) => BrowserError.make({
1030
- operation,
1031
- reason: error.reason
1032
- })));
1033
- const connected = yield* Effect.cached(owner.guard("connect", (ticket) => connectionUrl("connect").pipe(Effect.flatMap((url) => connectNative(url, options.driver)), Effect.andThen(native("connect", ticket, async () => {
1034
- getDriver().selected();
1035
- })), Effect.tap(() => Effect.sync(() => {
1036
- owner.state.phase = "open";
1037
- }))), {
1038
- phases: ["acquiring"],
1039
- charge: false,
1040
- verifyAfter: false
1041
- }).pipe(Effect.onError(() => closeScope), Effect.onInterrupt(() => closeScope)));
1042
- const execution = () => {
1043
- const port = getDriver().pageControl;
1044
- if (port === void 0) throw BrowserError.make({
1045
- operation: "page-control",
1046
- reason: "unsupported",
1047
- outcome: "undispatched"
1048
- });
1049
- return port;
1050
- };
1051
- const controls = {
1052
- pageControl: {
1053
- state: (page) => nativeOperation("page-state", (_driver, ticket) => execution().state(page, ticket), { charge: false }),
1054
- suspend: (page) => nativeOperation("page-suspend", (_driver, ticket) => execution().suspend(page, ticket), { charge: false }),
1055
- resume: (receipt) => nativeOperation("page-resume", (_driver, ticket) => execution().resume(receipt, ticket), { charge: false })
1056
- },
1057
- reference: ref,
1058
- capture,
1059
- bind,
1060
- currentTarget: readSelected,
1061
- cleanupResult: acquired.cleanupResult,
1062
- close: acquired.release.pipe(Effect.ensuring(Deferred.succeed(ended, void 0))),
1063
- observe: (reading = { scope: "document" }) => textBudget("observe", reading.maxTextBytes).pipe(Effect.flatMap((bytes) => owner.guard("observe", (ticket) => observeInside(ticket, bytes, reading.maxControls ?? 32, true, reading.scope)))),
1064
- /**
1065
- * Passive: not a mutation, so the action observation and the selection are left exactly as
1066
- * they were. A held page is refused rather than woken to be read.
1067
- */
1068
- checkpoint: (reading) => textBudget("checkpoint", reading.maxTextBytes).pipe(Effect.flatMap((bytes) => nativeOperation("checkpoint", async (driver, ticket) => {
1069
- const target = capture.target();
1070
- const revision = owner.state.revision;
1071
- const startedMonotonicNanos = clock.monotonicTimeNanosUnsafe();
1072
- return {
1073
- ...await driver.checkpoint(bytes, reading.maxControls ?? 32, reading.picture ? options.maxReturnedBytes : void 0, ticket),
1074
- target,
1075
- revision,
1076
- startedMonotonicNanos,
1077
- completedMonotonicNanos: clock.monotonicTimeNanosUnsafe()
1078
- };
1079
- }))),
1080
- controlFacts: (reference) => nativeOperation("control-facts", (driver, ticket) => driver.controlFacts(reference, ticket)),
1081
- /** Not charged: it sends no input and reads one node the caller was already given. */
1082
- revalidate: (reference) => nativeOperation("revalidate", (driver, ticket) => driver.revalidate(reference, ticket), { charge: false }),
1083
- readiness: owner.guard("ready", (ticket) => native("ready", ticket, () => getDriver().documentReadiness(ticket)), { charge: false }),
1084
- pages: nativeOperation("list-pages", (driver, ticket) => driver.listPages(ticket), { charge: false }),
1085
- frames: nativeOperation("list-frames", (driver, ticket) => driver.listFrames(ticket), { charge: false }),
1086
- selectPage: (id) => nativeOperation("select-page", async (driver, ticket) => {
1087
- await driver.selectPage(id, ticket);
1088
- owner.state.selection++;
1089
- }, { charge: false }),
1090
- selectFrame: (id) => nativeOperation("select-frame", async (driver, ticket) => {
1091
- await driver.selectFrame(id, ticket);
1092
- owner.state.selection++;
1093
- }, { charge: false }),
1094
- createPage: () => nativeOperation("new-page", (driver, ticket) => driver.newPage(ticket), {
1095
- mutation: true,
1096
- charge: false,
1097
- anyPage: true
1098
- }),
1099
- closePage: (id) => nativeOperation("close-page", (driver, ticket) => driver.closePage(id, ticket), {
1100
- mutation: true,
1101
- charge: false,
1102
- anyPage: true
1103
- }),
1104
- resize: (viewport) => nativeOperation("resize", (driver, ticket) => driver.resize(viewport, ticket), {
1105
- mutation: true,
1106
- charge: false
1107
- }),
1108
- waitFor: (selector, state) => nativeOperation("wait", (driver, ticket) => driver.waitFor(selector, state, ticket)),
1109
- clickAndWait: (target) => nativeOperation("click-and-wait", (driver, ticket) => driver.clickAndWait(target, ticket), { mutation: true }),
1110
- clickForDownload: (target) => nativeOperation("download-action", (driver, ticket) => driver.clickForDownload(target, ticket), { mutation: true }),
1111
- selectFiles: (target, files) => nativeOperation("select-files", (driver, ticket) => driver.selectFiles(target, files, ticket), { mutation: true }),
1112
- clickForFileSelection: (target, files) => nativeOperation("file-chooser", (driver, ticket) => driver.clickForFileSelection(target, files, ticket), { mutation: true }),
1113
- liveView: (ttl) => owner.guard("live-view", () => issueLiveView(client, ref, ttl), {
1114
- charge: false,
1115
- phases: ["open", "paused"]
1116
- }),
1117
- beginHandoff: (ttl) => owner.guard("handoff", () => Effect.gen(function* () {
1118
- if (options.driver.pageControl) return yield* BrowserError.make({
1119
- operation: "handoff",
1120
- reason: "unsupported",
1121
- outcome: "undispatched"
1122
- });
1123
- if (owner.state.phase === "open") owner.fence("paused", "paused");
1124
- handoffToken ??= globalThis.crypto.randomUUID();
1125
- const view = yield* issueLiveView(client, ref, ttl);
1126
- return {
1127
- token: Redacted.make(handoffToken),
1128
- view
1129
- };
1130
- }), {
1131
- charge: false,
1132
- phases: ["open", "paused"],
1133
- verifyAfter: false
1134
- }),
1135
- resume: (token, operatorReleasedControl) => owner.guard("resume", (ticket) => Effect.gen(function* () {
1136
- if (!operatorReleasedControl || handoffToken === void 0 || Redacted.value(token) !== handoffToken) return yield* BrowserError.make({
1137
- operation: "resume",
1138
- reason: "authorization",
1139
- outcome: "undispatched"
1140
- });
1141
- yield* native("resume", ticket, () => getDriver().dismissDialogs(ticket));
1142
- const observation = yield* observeInside(ticket, void 0, void 0, false);
1143
- owner.state.phase = "open";
1144
- handoffToken = void 0;
1145
- return observation;
1146
- }), {
1147
- charge: false,
1148
- phases: ["paused"],
1149
- verifyAfter: false
1150
- }),
1151
- detach: owner.guard("detach", (ticket) => Effect.gen(function* () {
1152
- if (!options.keepAlive) return yield* BrowserError.make({
1153
- operation: "detach",
1154
- reason: "unsupported",
1155
- outcome: "undispatched"
1156
- });
1157
- reconnectTarget = yield* native("detach", ticket, () => getDriver().selectedTargetId());
1158
- const attached = getDriver();
1159
- activeConnection = void 0;
1160
- owner.fence("detached", "disconnected");
1161
- const initialization = yield* Effect.exit(disposeBindings);
1162
- const disconnected = yield* Effect.tryPromise({
1163
- try: () => attached.disconnect(),
1164
- catch: () => BrowserError.make({
1165
- operation: "detach",
1166
- reason: "disconnected"
1167
- })
1168
- }).pipe(Effect.exit);
1169
- driver = void 0;
1170
- if (Exit.isFailure(initialization)) return yield* Effect.failCause(initialization.cause);
1171
- if (Exit.isFailure(disconnected)) return yield* Effect.failCause(disconnected.cause);
1172
- return {
1173
- reference: ref,
1174
- targetId: reconnectTarget
1175
- };
1176
- }), {
1177
- charge: false,
1178
- verifyAfter: false
1179
- }).pipe(Effect.onError(() => Effect.sync(() => owner.fence("uncertain", "uncertain")))),
1180
- reconnect: (operatorReleasedControl) => owner.guard("reconnect", (ticket) => Effect.gen(function* () {
1181
- if (!options.keepAlive || reconnectTarget === void 0 || !operatorReleasedControl) return yield* BrowserError.make({
1182
- operation: "reconnect",
1183
- reason: "unsupported",
1184
- outcome: "undispatched"
1185
- });
1186
- if ((yield* sessions.retrieve(ref).pipe(Effect.mapError((error) => BrowserError.make({
1187
- operation: "reconnect",
1188
- reason: error.reason
1189
- })))).status !== "RUNNING") return yield* BrowserError.make({
1190
- operation: "reconnect",
1191
- reason: "expired"
1192
- });
1193
- owner.state.phase = "acquiring";
1194
- const endpoint = yield* connectionUrl("reconnect");
1195
- yield* connectNative(endpoint, {
1196
- ...options.driver,
1197
- initialTargetId: reconnectTarget,
1198
- newPage: false,
1199
- preserveViewport: true
1200
- });
1201
- const observation = yield* observeInside(ticket, void 0, void 0, false);
1202
- owner.state.phase = "open";
1203
- return observation;
1204
- }), {
1205
- charge: false,
1206
- phases: ["detached", "acquiring"],
1207
- verifyAfter: false
1208
- }).pipe(Effect.onError(() => closeScope), Effect.onInterrupt(() => closeScope))
1209
- };
1210
- const remaining = Math.max(0, owner.lifetimeDeadline - Number(clock.monotonicTimeNanosUnsafe()) / 1e6);
1211
- yield* Effect.raceFirst(Effect.sleep(remaining).pipe(Effect.as(true)), Deferred.await(ended).pipe(Effect.as(false))).pipe(Effect.flatMap((expired) => expired ? closeScope : Effect.void), Effect.forkIn(parentScope));
1212
- return {
1213
- reference: ref,
1214
- /** The caller keeps its own lease type, including whether an allocation attempt exists. */
1215
- lease: acquired,
1216
- close: controls.close,
1217
- connect: Effect.suspend(() => owner.state.phase === "closed" || owner.state.phase === "closing" ? Effect.fail(BrowserError.make({
1218
- operation: "connect",
1219
- reason: "closed",
1220
- outcome: "undispatched"
1221
- })) : connected.pipe(Effect.as(controls)))
1222
- };
1223
- });
1224
- //#endregion
1225
- //#region src/Browser.ts
1226
- var Browser_exports = /* @__PURE__ */ __exportAll({ BrowserbaseBrowser: () => BrowserbaseBrowser });
1227
- const checked = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value, { onExcessProperty: "error" }).pipe(Effect.mapError(() => BrowserError.make({
1228
- operation,
1229
- reason: "configuration",
1230
- outcome: "undispatched"
1231
- })));
1232
- const decoded = (schema, operation) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => BrowserError.make({
1233
- operation,
1234
- reason: "malformed"
1235
- })));
1236
- const action = decoded(ActionResult, "action-result");
1237
- const pointerMoved = decoded(InputReceipt, "pointer-move");
1238
- const hovered = decoded(InputReceipt, "hover");
1239
- const wheeled = decoded(InputReceipt, "wheel");
1240
- const pressed = decoded(InputReceipt, "press");
1241
- const typed = decoded(InputReceipt, "type");
1242
- const makeTarget = (bound) => ({
1243
- navigate: (request) => checked(NavigateRequest, request, "navigate").pipe(Effect.flatMap((value) => bound.navigate(value.url)), Effect.flatMap((url) => decoded(NavigationResult, "navigate")({ url }))),
1244
- startNavigation: (request) => checked(StartNavigationRequest, request, "navigate").pipe(Effect.flatMap((value) => bound.startNavigation(value.url, value.timeoutMillis)), Effect.map((operation) => ({
1245
- target: operation.target,
1246
- completed: operation.completed.pipe(Effect.flatMap((url) => decoded(NavigationResult, "navigate")({ url }))),
1247
- stop: operation.stop
1248
- }))),
1249
- readText: (request) => checked(ReadTextRequest, request, "read-text").pipe(Effect.flatMap((value) => bound.readText(value.selector)), Effect.flatMap((text) => decoded(TextResult, "read-text")({ text }))),
1250
- click: (request) => checked(ClickRequest, request, "click").pipe(Effect.flatMap((value) => bound.click(value.selector)), Effect.flatMap((url) => action({ url }))),
1251
- fill: (request) => checked(FillRequest, request, "fill").pipe(Effect.flatMap((value) => bound.fill(value.selector, value.value)), Effect.flatMap((url) => action({ url }))),
1252
- scroll: (request) => checked(ScrollRequest, request, "scroll").pipe(Effect.flatMap((value) => bound.scroll(value.deltaX, value.deltaY)), Effect.flatMap((url) => action({ url }))),
1253
- pointerMove: (request) => checked(PointerMoveRequest, request, "pointer-move").pipe(Effect.flatMap((value) => bound.pointerMove(value.to)), Effect.flatMap((input) => pointerMoved({
1254
- ...input,
1255
- kind: "pointer-move"
1256
- }))),
1257
- hover: (request) => checked(HoverRequest, request, "hover").pipe(Effect.flatMap((value) => bound.hover(value.selector)), Effect.flatMap((input) => hovered({
1258
- ...input,
1259
- kind: "hover"
1260
- }))),
1261
- wheel: (request) => checked(WheelRequest, request, "wheel").pipe(Effect.flatMap((value) => bound.wheel(value.deltaX, value.deltaY, value.at).pipe(Effect.flatMap((input) => wheeled({
1262
- ...input,
1263
- kind: "wheel",
1264
- delta: {
1265
- x: value.deltaX,
1266
- y: value.deltaY
1267
- }
1268
- }))))),
1269
- press: (request) => checked(PressRequest, request, "press").pipe(Effect.flatMap((value) => bound.press(value.key, value.modifiers ?? [], value.into)), Effect.flatMap((input) => pressed({
1270
- ...input,
1271
- kind: "press"
1272
- }))),
1273
- type: (request) => checked(TypeRequest, request, "type").pipe(Effect.flatMap((value) => bound.type(value.text, value.into)), Effect.flatMap((input) => typed({
1274
- ...input,
1275
- kind: "type"
1276
- }))),
1277
- screenshot: (request) => checked(ScreenshotRequest, request, "screenshot").pipe(Effect.flatMap((value) => bound.screenshot(value.fullPage)), Effect.flatMap((bytes) => decoded(ScreenshotResult, "screenshot")({
1278
- mediaType: "image/png",
1279
- bytes: new Uint8Array(bytes)
1280
- })))
1281
- });
1282
- /**
1283
- * Uploaded selection is admitted by the identity of a receipt this package issued for this
1284
- * exact session, never by a path read off the caller's value. A fabricated receipt has no
1285
- * issuance record, so an arbitrary server pathname can never become an attached file.
1286
- */
1287
- const selection = (request, reference, operation) => Effect.suspend(() => {
1288
- const invalid = BrowserError.make({
1289
- operation,
1290
- reason: "configuration",
1291
- outcome: "undispatched"
1292
- });
1293
- if (request.selection._tag === "Inline") return checked(InlineFiles, request.selection.files, operation).pipe(Effect.map((files) => files.map((file) => ({
1294
- _tag: "Inline",
1295
- name: file.name,
1296
- mediaType: file.mediaType,
1297
- bytes: new Uint8Array(file.bytes)
1298
- }))));
1299
- const uploads = request.selection.uploads;
1300
- if (!Array.isArray(uploads) || uploads.length < 1 || uploads.length > 8) return Effect.fail(invalid);
1301
- const files = [];
1302
- for (const receipt of uploads) {
1303
- const issued = issuedUpload(receipt);
1304
- if (issued === void 0) return Effect.fail(BrowserError.make({
1305
- operation,
1306
- reason: "authorization",
1307
- outcome: "undispatched"
1308
- }));
1309
- if (issued.reference.projectId !== reference.projectId || issued.reference.sessionId !== reference.sessionId) return Effect.fail(BrowserError.make({
1310
- operation,
1311
- reason: "authorization",
1312
- outcome: "undispatched"
1313
- }));
1314
- files.push({
1315
- _tag: "Remote",
1316
- path: issued.remotePath
1317
- });
1318
- }
1319
- return Effect.succeed(files);
1320
- });
1321
- /** A browser-operation failure keeps its meaning when it is reported as an initialization one. */
1322
- const initialization = (reason) => reason === "busy" ? "busy" : reason === "closed" || reason === "disconnected" ? "closed" : reason === "timeout" ? "timeout" : reason === "stale" ? "stale" : reason === "unsupported" ? "unsupported" : reason === "configuration" ? "configuration" : "native";
1323
- const makeSession = (controls, bindings) => {
1324
- const currentTarget = controls.currentTarget.pipe(Effect.map(() => makeTarget(controls.bind())));
1325
- const Wait = Schema.Struct({
1326
- selector: ClickRequest.fields.selector,
1327
- state: Schema.Literals([
1328
- "visible",
1329
- "hidden",
1330
- "attached",
1331
- "detached"
1332
- ])
1333
- });
1334
- const navigate = (url) => action({ url });
1335
- const session = {
1336
- reference: controls.reference,
1337
- failure: bindings.failure,
1338
- bindingDiagnostics: bindings.diagnostics,
1339
- bind: () => makeTarget(controls.bind()),
1340
- currentTarget,
1341
- target: controls.currentTarget,
1342
- observe: (options = {}) => checked(ObservationOptions, options, "observe").pipe(Effect.flatMap((value) => controls.observe({
1343
- ...value,
1344
- scope: value.scope ?? "document"
1345
- }))),
1346
- checkpoint: (options = {}) => checked(CheckpointOptions, options, "checkpoint").pipe(Effect.flatMap((value) => controls.checkpoint({
1347
- ...value,
1348
- picture: value.picture ?? false
1349
- })), Effect.flatMap(({ picture, ...sampled }) => decoded(Checkpoint, "checkpoint")({
1350
- ...sampled,
1351
- ...picture === void 0 ? {} : { picture: {
1352
- mediaType: "image/png",
1353
- bytes: new Uint8Array(picture)
1354
- } }
1355
- }))),
1356
- controlFacts: (reference) => checked(ObservedElement, reference, "control-facts").pipe(Effect.flatMap(controls.controlFacts)),
1357
- revalidateElement: (reference) => checked(ObservedElement, reference, "revalidate").pipe(Effect.flatMap((value) => controls.revalidate(value).pipe(Effect.as(value)))),
1358
- clickElement: (reference, admission) => checked(ObservedElement, reference, "click").pipe(Effect.flatMap((value) => controls.bind().click(value, admission?.admit)), Effect.flatMap(navigate)),
1359
- fillElement: (reference, value, admission) => checked(ObservedElement, reference, "fill").pipe(Effect.flatMap((element) => checked(FillRequest.fields.value, value, "fill").pipe(Effect.flatMap((text) => controls.bind().fill(element, text, admission?.admit)))), Effect.flatMap(navigate)),
1360
- hoverElement: (reference, admission) => checked(ObservedElement, reference, "hover").pipe(Effect.flatMap((value) => controls.bind().hover(value, admission?.admit)), Effect.flatMap((input) => hovered({
1361
- ...input,
1362
- kind: "hover"
1363
- }))),
1364
- pressElement: (reference, stroke, admission) => checked(ObservedElement, reference, "press").pipe(Effect.flatMap((element) => checked(KeyStroke, stroke, "press").pipe(Effect.flatMap((value) => controls.bind().press(value.key, value.modifiers ?? [], element, admission?.admit)))), Effect.flatMap((input) => pressed({
1365
- ...input,
1366
- kind: "press"
1367
- }))),
1368
- typeElement: (reference, text, admission) => checked(ObservedElement, reference, "type").pipe(Effect.flatMap((element) => checked(TypeRequest.fields.text, text, "type").pipe(Effect.flatMap((value) => controls.bind().type(value, element, admission?.admit)))), Effect.flatMap((input) => typed({
1369
- ...input,
1370
- kind: "type"
1371
- }))),
1372
- pages: controls.pages,
1373
- frames: controls.frames,
1374
- selectPage: (id) => checked(Identifier, id, "select-page").pipe(Effect.flatMap(controls.selectPage), Effect.andThen(currentTarget)),
1375
- selectFrame: (id) => checked(Identifier, id, "select-frame").pipe(Effect.flatMap(controls.selectFrame), Effect.andThen(currentTarget)),
1376
- createPage: controls.createPage(),
1377
- closePage: (id) => checked(Identifier, id, "close-page").pipe(Effect.flatMap(controls.closePage)),
1378
- resizeViewport: (viewport) => checked(Viewport, viewport, "resize").pipe(Effect.flatMap(controls.resize)),
1379
- waitFor: (request) => checked(Wait, request, "wait").pipe(Effect.flatMap((value) => controls.waitFor(value.selector, value.state))),
1380
- clickAndWait: (request) => checked(ClickRequest, request, "click-and-wait").pipe(Effect.flatMap((value) => controls.clickAndWait(value.selector)), Effect.flatMap((url) => action({ url }))),
1381
- clickForDownload: (request) => checked(ClickRequest, request, "download-action").pipe(Effect.flatMap((value) => controls.clickForDownload(value.selector)), Effect.flatMap((raw) => decoded(DownloadObservation, "download-action")({
1382
- ...raw,
1383
- reference: controls.reference
1384
- }))),
1385
- selectFiles: (request) => checked(Selector, request.selector, "select-files").pipe(Effect.flatMap((selector) => selection(request, controls.reference, "select-files").pipe(Effect.flatMap((files) => controls.selectFiles(selector, files)))), Effect.flatMap(navigate)),
1386
- clickForFileSelection: (request) => checked(Selector, request.selector, "file-chooser").pipe(Effect.flatMap((selector) => selection(request, controls.reference, "file-chooser").pipe(Effect.flatMap((files) => controls.clickForFileSelection(selector, files)))), Effect.flatMap(navigate)),
1387
- ready: controls.readiness.pipe(Effect.mapError((error) => InitializationError.make({
1388
- operation: "ready",
1389
- step: "session",
1390
- reason: initialization(error.reason)
1391
- })), Effect.flatMap((state) => state._tag === "NotReady" ? Effect.fail(InitializationError.make({
1392
- operation: "ready",
1393
- step: state.step,
1394
- reason: state.reason === "failed" ? "output" : state.reason
1395
- })) : Effect.succeed({ _tag: state._tag }))),
1396
- liveView: (ttl = 60) => controls.liveView(ttl),
1397
- beginHandoff: (ttl = 60) => controls.beginHandoff(ttl),
1398
- resume: (token, released) => controls.resume(token, released),
1399
- detach: controls.detach,
1400
- reconnect: (released) => controls.reconnect(released),
1401
- close: controls.close,
1402
- cleanupResult: controls.cleanupResult
1403
- };
1404
- associate(session, controls.capture);
1405
- associatePageControl(session, controls.pageControl);
1406
- return session;
1407
- };
1408
- /**
1409
- * Credentials, budgets and connection lifetime are fixed when this Layer is built. Building it
1410
- * allocates nothing and loads no native peer; every acquisition is scoped by its caller.
1411
- */
1412
- var BrowserbaseBrowser = class BrowserbaseBrowser extends Context.Service()("effect-browserbase/Browser") {
1413
- static layer(options) {
1414
- return Layer.effect(BrowserbaseBrowser, Effect.gen(function* () {
1415
- const client = yield* BrowserbaseClient;
1416
- const sessions = yield* BrowserbaseSessions;
1417
- const binding = yield* BrowserbaseBrowserBinding;
1418
- const automation = yield* checked(AutomationOptions, projected(options), "configure");
1419
- const viewport = yield* checked(Viewport, options.launch.viewport._tag === "Fixed" ? {
1420
- width: options.launch.viewport.width,
1421
- height: options.launch.viewport.height
1422
- } : {
1423
- width: 1280,
1424
- height: 720
1425
- }, "configure");
1426
- const actionTimeoutMillis = automation.actionTimeoutMillis ?? 1e4;
1427
- const maxPages = automation.maxPages ?? 10;
1428
- const pageControl = automation.pageControl ?? false;
1429
- const popupPolicy = automation.popupPolicy ?? "retain";
1430
- const dialogPolicy = automation.dialogPolicy ?? "dismiss";
1431
- if (pageControl && (options.launch.keepAlive === true || popupPolicy === "pause" || dialogPolicy === "pause")) return yield* BrowserError.make({
1432
- operation: "configure",
1433
- reason: "unsupported",
1434
- outcome: "undispatched"
1435
- });
1436
- if (options.launch.context?.persist === true && options.contextWriter === void 0) return yield* BrowserError.make({
1437
- operation: "configure",
1438
- reason: "context-lease"
1439
- });
1440
- if (Object.prototype.hasOwnProperty.call(options, "bootstrap")) return yield* BrowserError.make({
1441
- operation: "configure",
1442
- reason: "configuration",
1443
- outcome: "undispatched"
1444
- });
1445
- const driver = {
1446
- pageControl,
1447
- viewport,
1448
- maxPages,
1449
- popupPolicy,
1450
- dialogPolicy,
1451
- preserveViewport: options.launch.viewport._tag !== "Fixed",
1452
- ...automation.initialPage === void 0 ? {} : "targetId" in automation.initialPage ? { initialTargetId: automation.initialPage.targetId } : { newPage: true }
1453
- };
1454
- const acquire = Effect.fnUntraced(function* (policy, request = {}) {
1455
- const fixed = yield* checked(BrowserPolicy, policy, "configure");
1456
- const plan = yield* preparePlan(request.bootstrap ?? empty);
1457
- const scope = yield* Scope.fork(yield* Scope.Scope, "sequential");
1458
- const bindings = yield* makeBindings(plan).pipe(Scope.provide(scope));
1459
- const bootstrap = compileBootstrap(plan);
1460
- const acquired = yield* acquireSession({
1461
- maxActions: fixed.maxActions,
1462
- maxElapsedMillis: fixed.maxElapsedMillis,
1463
- actionTimeoutMillis
1464
- }, {
1465
- remote: ownedRemote({
1466
- launch: options.launch,
1467
- ...options.contextWriter === void 0 ? {} : { contextWriter: options.contextWriter },
1468
- ...options.onCleanup === void 0 ? {} : { onCleanup: options.onCleanup },
1469
- ...options.onAllocationUncertain === void 0 ? {} : { onAllocationUncertain: options.onAllocationUncertain }
1470
- }),
1471
- keepAlive: options.launch.keepAlive === true,
1472
- driver: {
1473
- ...driver,
1474
- ...bootstrap === void 0 ? {} : { bootstrap }
1475
- },
1476
- connectBindings: bindings.connect,
1477
- maxReturnedBytes: fixed.maxReturnedBytes
1478
- }).pipe(Effect.provideService(BrowserbaseClient, client), Effect.provideService(BrowserbaseSessions, sessions), Effect.provideService(BrowserbaseBrowserBinding, binding), Scope.provide(scope));
1479
- const connected = yield* Effect.cached(acquired.connect.pipe(Effect.map((controls) => makeSession(controls, bindings))));
1480
- return {
1481
- reference: acquired.reference,
1482
- attempt: acquired.lease.attempt,
1483
- failure: bindings.failure,
1484
- close: acquired.close,
1485
- connect: Effect.raceFirst(bindings.failure, acquired.connect.pipe(Effect.andThen(connected))).pipe(Effect.onError(() => acquired.close.pipe(Effect.asVoid)))
1486
- };
1487
- });
1488
- /**
1489
- * Borrowing a session another process allocated. The launch recipe is not replayed and
1490
- * no allocation is attempted: the requested target is resolved explicitly, credentials
1491
- * are fetched fresh because expiry races the caller's own status read, and this scope
1492
- * disconnects locally without ever requesting release. Detaching and reattaching inside
1493
- * a borrowed scope is deliberately absent; attaching again is the cross-process path,
1494
- * and it revalidates the session instead of assuming it is still there.
1495
- */
1496
- const attach = Effect.fnUntraced(function* (reference, request) {
1497
- const fixed = yield* checked(BrowserPolicy, request.policy, "configure");
1498
- const plan = yield* preparePlan(request.bootstrap ?? empty);
1499
- const scope = yield* Scope.fork(yield* Scope.Scope, "sequential");
1500
- const bindings = yield* makeBindings(plan).pipe(Scope.provide(scope));
1501
- const bootstrap = compileBootstrap(plan);
1502
- const targetId = request.target === void 0 ? void 0 : yield* checked(Identifier, request.target.targetId, "configure");
1503
- const acquired = yield* acquireSession({
1504
- maxActions: fixed.maxActions,
1505
- maxElapsedMillis: fixed.maxElapsedMillis,
1506
- actionTimeoutMillis
1507
- }, {
1508
- remote: borrowedRemote({
1509
- reference,
1510
- ...request.pendingWaitMillis === void 0 ? {} : { pendingWaitMillis: request.pendingWaitMillis },
1511
- ...options.onCleanup === void 0 ? {} : { onCleanup: options.onCleanup }
1512
- }),
1513
- keepAlive: false,
1514
- driver: {
1515
- ...driver,
1516
- ...bootstrap === void 0 ? {} : { bootstrap },
1517
- newPage: false,
1518
- preserveViewport: true,
1519
- ...targetId === void 0 ? {} : { initialTargetId: targetId }
1520
- },
1521
- maxReturnedBytes: fixed.maxReturnedBytes,
1522
- connectBindings: bindings.connect
1523
- }).pipe(Effect.provideService(BrowserbaseClient, client), Effect.provideService(BrowserbaseSessions, sessions), Effect.provideService(BrowserbaseBrowserBinding, binding), Scope.provide(scope));
1524
- return yield* Effect.raceFirst(bindings.failure, acquired.connect.pipe(Effect.map((controls) => makeSession(controls, bindings)))).pipe(Effect.onError(() => acquired.close.pipe(Effect.asVoid)));
1525
- });
1526
- return BrowserbaseBrowser.of({
1527
- acquire,
1528
- attach,
1529
- open: (policy, request) => acquire(policy, request).pipe(Effect.flatMap((acquired) => acquired.connect)),
1530
- withBrowser: (policy, request, use) => Effect.scoped(Effect.gen(function* () {
1531
- const session = yield* (yield* acquire(policy, request)).connect;
1532
- const result = yield* Effect.raceFirst(session.failure, Effect.suspend(() => use(session)));
1533
- const cleanup = yield* session.close;
1534
- if (cleanup.remote !== "confirmed" || cleanup.local !== "closed" || cleanup.issues.length > 0) return yield* BrowserError.make({
1535
- operation: "close",
1536
- reason: "provider",
1537
- outcome: "unknown"
1538
- });
1539
- return result;
1540
- }))
1541
- });
1542
- }));
1543
- }
1544
- };
1545
- /** Only the automation fields cross the schema boundary; launch and callbacks are separate. */
1546
- const projected = (options) => ({
1547
- ...options.actionTimeoutMillis === void 0 ? {} : { actionTimeoutMillis: options.actionTimeoutMillis },
1548
- ...options.maxPages === void 0 ? {} : { maxPages: options.maxPages },
1549
- ...options.initialPage === void 0 ? {} : { initialPage: options.initialPage },
1550
- ...options.popupPolicy === void 0 ? {} : { popupPolicy: options.popupPolicy },
1551
- ...options.dialogPolicy === void 0 ? {} : { dialogPolicy: options.dialogPolicy },
1552
- ...options.pageControl === void 0 ? {} : { pageControl: options.pageControl }
1553
- });
1554
- //#endregion
1555
- export { BrowserbaseBrowser as n, Browser_exports as t };
1556
-
1557
- //# sourceMappingURL=Browser-joE4Zgys.mjs.map