effect-browserbase 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.
- package/README.md +277 -319
- package/dist/Browserbase-CaPKwEY9.mjs +544 -0
- package/dist/Browserbase-CaPKwEY9.mjs.map +1 -0
- package/dist/Browserbase.d.mts +110 -0
- package/dist/Browserbase.mjs +3 -0
- package/dist/BrowserbaseClient-CtAQ2rg4.d.mts +195 -0
- package/dist/BrowserbaseClient.d.mts +2 -0
- package/dist/BrowserbaseClient.mjs +190 -0
- package/dist/BrowserbaseClient.mjs.map +1 -0
- package/dist/BrowserbaseError-D1EOrLNc.d.mts +79 -0
- package/dist/BrowserbaseError.d.mts +2 -0
- package/dist/BrowserbaseError.mjs +101 -0
- package/dist/BrowserbaseError.mjs.map +1 -0
- package/dist/ContextLease.d.mts +42 -0
- package/dist/ContextLease.mjs +80 -0
- package/dist/ContextLease.mjs.map +1 -0
- package/dist/index.d.mts +5 -27
- package/dist/index.mjs +5 -26
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/testing/index.d.mts +136 -0
- package/dist/testing/index.mjs +440 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +51 -121
- package/src/Browserbase.ts +729 -0
- package/src/BrowserbaseClient.ts +559 -0
- package/src/BrowserbaseError.ts +108 -0
- package/src/ContextLease.ts +118 -0
- package/src/index.ts +7 -0
- package/src/internal/captureConnection.ts +214 -0
- package/src/testing/BrowserbaseContract.ts +170 -0
- package/src/testing/TestBrowserbase.ts +509 -0
- package/src/testing/index.ts +7 -0
- package/dist/Account.d.mts +0 -32
- package/dist/Account.mjs +0 -36
- package/dist/Account.mjs.map +0 -1
- package/dist/Acquisition-BOABu_qJ.mjs +0 -244
- package/dist/Acquisition-BOABu_qJ.mjs.map +0 -1
- package/dist/Agents-CPaTOH_g.d.mts +0 -156
- package/dist/Agents.d.mts +0 -3
- package/dist/Agents.mjs +0 -292
- package/dist/Agents.mjs.map +0 -1
- package/dist/Allocation.d.mts +0 -97
- package/dist/Allocation.mjs +0 -27
- package/dist/Allocation.mjs.map +0 -1
- package/dist/Authorization-Ll5f_XNM.mjs +0 -12
- package/dist/Authorization-Ll5f_XNM.mjs.map +0 -1
- package/dist/Browser-ByrWJ0ss.mjs +0 -274
- package/dist/Browser-ByrWJ0ss.mjs.map +0 -1
- package/dist/Browser.d.mts +0 -83
- package/dist/Browser.mjs +0 -3
- package/dist/BrowserBinding.d.mts +0 -13
- package/dist/BrowserBinding.mjs +0 -31
- package/dist/BrowserBinding.mjs.map +0 -1
- package/dist/Certificates-D4NQmyeA.d.mts +0 -34
- package/dist/Certificates.d.mts +0 -3
- package/dist/Certificates.mjs +0 -85
- package/dist/Certificates.mjs.map +0 -1
- package/dist/Cleanup-DMTDBir8.d.mts +0 -23
- package/dist/Cleanup.d.mts +0 -3
- package/dist/Cleanup.mjs +0 -43
- package/dist/Cleanup.mjs.map +0 -1
- package/dist/Client-BEC4Q8ZS.mjs +0 -564
- package/dist/Client-BEC4Q8ZS.mjs.map +0 -1
- package/dist/Client-B_f6bBLT.d.mts +0 -52
- package/dist/Client.d.mts +0 -3
- package/dist/Client.mjs +0 -2
- package/dist/Connection-sxyxtwlk.mjs +0 -56
- package/dist/Connection-sxyxtwlk.mjs.map +0 -1
- package/dist/ContextCoordination.d.mts +0 -29
- package/dist/ContextCoordination.mjs +0 -94
- package/dist/ContextCoordination.mjs.map +0 -1
- package/dist/ContextWriter-CiEma24z.mjs +0 -204
- package/dist/ContextWriter-CiEma24z.mjs.map +0 -1
- package/dist/Contexts-DqdL7cyX.d.mts +0 -31
- package/dist/Contexts.d.mts +0 -3
- package/dist/Contexts.mjs +0 -97
- package/dist/Contexts.mjs.map +0 -1
- package/dist/Deadline-CrViaogr.mjs +0 -13
- package/dist/Deadline-CrViaogr.mjs.map +0 -1
- package/dist/Downloads-CX52GOFJ.d.mts +0 -43
- package/dist/Downloads.d.mts +0 -3
- package/dist/Downloads.mjs +0 -137
- package/dist/Downloads.mjs.map +0 -1
- package/dist/Errors.d.mts +0 -100
- package/dist/Errors.mjs +0 -194
- package/dist/Errors.mjs.map +0 -1
- package/dist/Extensions-BTUn9zhK.mjs +0 -231
- package/dist/Extensions-BTUn9zhK.mjs.map +0 -1
- package/dist/Extensions-DBXuVM1f.d.mts +0 -43
- package/dist/Extensions.d.mts +0 -3
- package/dist/Extensions.mjs +0 -2
- package/dist/Functions-CsKMMUuZ.d.mts +0 -226
- package/dist/Functions.d.mts +0 -3
- package/dist/Functions.mjs +0 -318
- package/dist/Functions.mjs.map +0 -1
- package/dist/Issued-DejqoMYL.mjs +0 -15
- package/dist/Issued-DejqoMYL.mjs.map +0 -1
- package/dist/Launch-cAdE2rrZ.mjs +0 -115
- package/dist/Launch-cAdE2rrZ.mjs.map +0 -1
- package/dist/Launch.d.mts +0 -127
- package/dist/Launch.mjs +0 -97
- package/dist/Launch.mjs.map +0 -1
- package/dist/PageFetch-CjAM_0DS.d.mts +0 -40
- package/dist/PageFetch.d.mts +0 -3
- package/dist/PageFetch.mjs +0 -59
- package/dist/PageFetch.mjs.map +0 -1
- package/dist/Projects-tm5bTDH0.d.mts +0 -38
- package/dist/Projects.d.mts +0 -3
- package/dist/Projects.mjs +0 -65
- package/dist/Projects.mjs.map +0 -1
- package/dist/Recordings-B9k5aODB.d.mts +0 -27
- package/dist/Recordings.d.mts +0 -3
- package/dist/Recordings.mjs +0 -144
- package/dist/Recordings.mjs.map +0 -1
- package/dist/References-DOH3Wu44.d.mts +0 -34
- package/dist/References.d.mts +0 -3
- package/dist/References.mjs +0 -33
- package/dist/References.mjs.map +0 -1
- package/dist/Replays-mV93WGtx.d.mts +0 -27
- package/dist/Replays.d.mts +0 -3
- package/dist/Replays.mjs +0 -107
- package/dist/Replays.mjs.map +0 -1
- package/dist/Resource-J7Kkk5mu.mjs +0 -54
- package/dist/Resource-J7Kkk5mu.mjs.map +0 -1
- package/dist/Search-B64e7g-h.d.mts +0 -37
- package/dist/Search.d.mts +0 -3
- package/dist/Search.mjs +0 -46
- package/dist/Search.mjs.map +0 -1
- package/dist/SessionData-5xYZDshJ.d.mts +0 -38
- package/dist/SessionData.d.mts +0 -3
- package/dist/SessionData.mjs +0 -48
- package/dist/SessionData.mjs.map +0 -1
- package/dist/Sessions-D2H5m6rF.mjs +0 -260
- package/dist/Sessions-D2H5m6rF.mjs.map +0 -1
- package/dist/Sessions-oJEUmZth.d.mts +0 -69
- package/dist/Sessions.d.mts +0 -4
- package/dist/Sessions.mjs +0 -3
- package/dist/Testing.d.mts +0 -119
- package/dist/Testing.mjs +0 -206
- package/dist/Testing.mjs.map +0 -1
- package/dist/TransferPolicy-DQ06i7SZ.mjs +0 -25
- package/dist/TransferPolicy-DQ06i7SZ.mjs.map +0 -1
- package/dist/Transfers-GVqzrDWl.d.mts +0 -89
- package/dist/Transfers.d.mts +0 -3
- package/dist/Transfers.mjs +0 -85
- package/dist/Transfers.mjs.map +0 -1
- package/dist/Uploads-C2_bACgv.d.mts +0 -28
- package/dist/Uploads.d.mts +0 -3
- package/dist/Uploads.mjs +0 -105
- package/dist/Uploads.mjs.map +0 -1
- package/dist/Webhooks-BoeRAxzG.d.mts +0 -59
- package/dist/Webhooks.d.mts +0 -3
- package/dist/Webhooks.mjs +0 -134
- package/dist/Webhooks.mjs.map +0 -1
- package/dist/WriterFacts-BBJRDZYY.d.mts +0 -54
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { BrowserbaseError, Decode, InvalidRequest, isTransient } from "./BrowserbaseError.mjs";
|
|
3
|
+
import { BrowserbaseClient } from "./BrowserbaseClient.mjs";
|
|
4
|
+
import { ContextHeld, ContextLease } from "./ContextLease.mjs";
|
|
5
|
+
import { DateTime, Duration, Effect, Exit, Layer, Option, Random, Redacted, Schedule, Schema, Scope, Semaphore } from "effect";
|
|
6
|
+
import * as Browser from "effect-browser/Browser";
|
|
7
|
+
import { BrowserError, Closed, Failed } from "effect-browser/BrowserError";
|
|
8
|
+
import * as Cdp from "effect-browser/Cdp";
|
|
9
|
+
import * as Supervisor from "effect-browser/Supervisor";
|
|
10
|
+
//#region src/internal/captureConnection.ts
|
|
11
|
+
/**
|
|
12
|
+
* The capture connection: a second DevTools connection to a session, for its pages' screencasts
|
|
13
|
+
* alone. A frame and its acknowledgement then never wait behind a large message on the
|
|
14
|
+
* connection Playwright drives the pages over, such as an upload or a read's answer: Browserbase
|
|
15
|
+
* refuses compression, and on air those waits stopped the picture for as long as the message took
|
|
16
|
+
* to cross.
|
|
17
|
+
*
|
|
18
|
+
* Raw CDP over the global WebSocket, with no Playwright. A page's session on it sends only the
|
|
19
|
+
* capture's commands, and receives that page's events. The connection opens as the first capture
|
|
20
|
+
* starts and stays until the scope closes, which closes it and waits, bounded, for it to close;
|
|
21
|
+
* one that fails ends every page's session on it, and the next capture opens another.
|
|
22
|
+
*/
|
|
23
|
+
const Message = Schema.fromJsonString(Schema.Struct({
|
|
24
|
+
id: Schema.optionalKey(Schema.Int),
|
|
25
|
+
sessionId: Schema.optionalKey(Schema.String),
|
|
26
|
+
method: Schema.optionalKey(Schema.String),
|
|
27
|
+
params: Schema.optionalKey(Schema.Unknown),
|
|
28
|
+
result: Schema.optionalKey(Schema.Unknown),
|
|
29
|
+
error: Schema.optionalKey(Schema.Struct({ message: Schema.String }))
|
|
30
|
+
}));
|
|
31
|
+
const Session = Schema.Struct({ sessionId: Schema.String });
|
|
32
|
+
const failure = (detail) => new BrowserError({
|
|
33
|
+
operation: "screencast",
|
|
34
|
+
reason: new Failed({ detail }),
|
|
35
|
+
dispatched: false
|
|
36
|
+
});
|
|
37
|
+
const closing = Duration.seconds(2);
|
|
38
|
+
/** The capture connection to the DevTools address `url`, for the scope. */
|
|
39
|
+
const make = Effect.fnUntraced(function* (url) {
|
|
40
|
+
let state = { _tag: "Idle" };
|
|
41
|
+
let ids = 0;
|
|
42
|
+
const opening = yield* Semaphore.make(1);
|
|
43
|
+
const lose = (open, error) => {
|
|
44
|
+
if (state._tag !== "Open" || state.open !== open) return;
|
|
45
|
+
state = { _tag: "Idle" };
|
|
46
|
+
for (const reply of open.replies.values()) reply.reject(error);
|
|
47
|
+
for (const listener of open.sessions.values()) listener.lost(error);
|
|
48
|
+
open.replies.clear();
|
|
49
|
+
open.sessions.clear();
|
|
50
|
+
};
|
|
51
|
+
const receive = (open, data) => {
|
|
52
|
+
const decoded = typeof data === "string" ? Schema.decodeOption(Message)(data) : Option.none();
|
|
53
|
+
if (Option.isNone(decoded)) return lose(open, failure("the capture connection sent a message it could not read"));
|
|
54
|
+
const { id, sessionId, method, params, result, error } = decoded.value;
|
|
55
|
+
if (id !== void 0) {
|
|
56
|
+
const reply = open.replies.get(id);
|
|
57
|
+
open.replies.delete(id);
|
|
58
|
+
if (error === void 0) reply?.resolve(result);
|
|
59
|
+
else reply?.reject(failure(error.message));
|
|
60
|
+
} else if (method === void 0) return;
|
|
61
|
+
else if (sessionId !== void 0) open.sessions.get(sessionId)?.event(method, params);
|
|
62
|
+
else if (method === "Target.detachedFromTarget") {
|
|
63
|
+
const detached = Schema.decodeUnknownOption(Session)(params);
|
|
64
|
+
const ended = Option.isSome(detached) ? detached.value.sessionId : "";
|
|
65
|
+
const listener = open.sessions.get(ended);
|
|
66
|
+
open.sessions.delete(ended);
|
|
67
|
+
listener?.event(method, params);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const send = (open, method, params, sessionId) => {
|
|
71
|
+
if (state._tag !== "Open" || state.open !== open) return Promise.reject(failure("the capture connection was lost"));
|
|
72
|
+
const id = ++ids;
|
|
73
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
74
|
+
open.replies.set(id, {
|
|
75
|
+
resolve,
|
|
76
|
+
reject
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
open.socket.send(JSON.stringify({
|
|
80
|
+
id,
|
|
81
|
+
method,
|
|
82
|
+
params,
|
|
83
|
+
sessionId
|
|
84
|
+
}));
|
|
85
|
+
} catch {
|
|
86
|
+
open.replies.delete(id);
|
|
87
|
+
reject(failure("the capture connection could not send"));
|
|
88
|
+
}
|
|
89
|
+
return promise;
|
|
90
|
+
};
|
|
91
|
+
const connect = Effect.callback((resume) => {
|
|
92
|
+
const socket = new WebSocket(Redacted.value(url));
|
|
93
|
+
const open = {
|
|
94
|
+
socket,
|
|
95
|
+
replies: /* @__PURE__ */ new Map(),
|
|
96
|
+
sessions: /* @__PURE__ */ new Map()
|
|
97
|
+
};
|
|
98
|
+
const unopened = () => resume(Effect.fail(failure("the capture connection did not open")));
|
|
99
|
+
socket.addEventListener("close", unopened, { once: true });
|
|
100
|
+
socket.addEventListener("open", () => {
|
|
101
|
+
socket.removeEventListener("close", unopened);
|
|
102
|
+
if (state._tag === "Closed") {
|
|
103
|
+
socket.close(1e3);
|
|
104
|
+
return resume(Effect.fail(failure("the capture connection is closed")));
|
|
105
|
+
}
|
|
106
|
+
state = {
|
|
107
|
+
_tag: "Open",
|
|
108
|
+
open
|
|
109
|
+
};
|
|
110
|
+
socket.addEventListener("close", () => lose(open, failure("the capture connection was lost")));
|
|
111
|
+
resume(Effect.succeed(open));
|
|
112
|
+
}, { once: true });
|
|
113
|
+
socket.addEventListener("message", ({ data }) => receive(open, data));
|
|
114
|
+
return Effect.sync(() => socket.close(1e3));
|
|
115
|
+
});
|
|
116
|
+
const current = opening.withPermits(1)(Effect.suspend(() => state._tag === "Open" ? Effect.succeed(state.open) : state._tag === "Closed" ? Effect.fail(failure("the capture connection is closed")) : connect));
|
|
117
|
+
const replied = (reply) => Effect.tryPromise({
|
|
118
|
+
try: reply,
|
|
119
|
+
catch: (cause) => Schema.is(BrowserError)(cause) ? cause : failure("the capture connection failed")
|
|
120
|
+
});
|
|
121
|
+
const attach = (target, listener) => Effect.gen(function* () {
|
|
122
|
+
const open = yield* current;
|
|
123
|
+
const { sessionId } = yield* replied(() => send(open, "Target.attachToTarget", {
|
|
124
|
+
targetId: target,
|
|
125
|
+
flatten: true
|
|
126
|
+
})).pipe(Effect.flatMap((result) => Schema.decodeUnknownEffect(Session)(result).pipe(Effect.mapError(() => failure("the capture connection's attach had no session")))));
|
|
127
|
+
open.sessions.set(sessionId, listener);
|
|
128
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
129
|
+
if (open.sessions.delete(sessionId)) send(open, "Target.detachFromTarget", { sessionId }, void 0).catch(() => void 0);
|
|
130
|
+
}));
|
|
131
|
+
return (method, params) => send(open, method, params, sessionId);
|
|
132
|
+
});
|
|
133
|
+
yield* Effect.addFinalizer(() => Effect.suspend(() => {
|
|
134
|
+
const previous = state;
|
|
135
|
+
state = { _tag: "Closed" };
|
|
136
|
+
if (previous._tag !== "Open") return Effect.void;
|
|
137
|
+
const { socket, replies, sessions } = previous.open;
|
|
138
|
+
for (const reply of replies.values()) reply.reject(failure("the capture connection closed"));
|
|
139
|
+
replies.clear();
|
|
140
|
+
sessions.clear();
|
|
141
|
+
return Effect.callback((resume) => {
|
|
142
|
+
if (socket.readyState === WebSocket.CLOSED) return resume(Effect.void);
|
|
143
|
+
socket.addEventListener("close", () => resume(Effect.void), { once: true });
|
|
144
|
+
socket.close(1e3);
|
|
145
|
+
}).pipe(Effect.timeoutOrElse({
|
|
146
|
+
duration: closing,
|
|
147
|
+
orElse: () => Effect.void
|
|
148
|
+
}));
|
|
149
|
+
}));
|
|
150
|
+
return { attach };
|
|
151
|
+
});
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/Browserbase.ts
|
|
154
|
+
/**
|
|
155
|
+
* A `Browser` running on Browserbase.
|
|
156
|
+
*
|
|
157
|
+
* `open` creates a session and connects to it over CDP. When its scope closes it disconnects,
|
|
158
|
+
* releases the session and confirms that Browserbase reports it ended, so billing stops then
|
|
159
|
+
* rather than at the session's timeout. `release` does the same sooner and says how it went:
|
|
160
|
+
* `Settled`, or `Unconfirmed` when Browserbase still reported the session running, or could not
|
|
161
|
+
* be asked, at the deadline. Each create carries a nonce of its own in its user metadata, so a
|
|
162
|
+
* session made by a create whose answer was lost is found and ended too, rather than billing
|
|
163
|
+
* unseen until its timeout.
|
|
164
|
+
*
|
|
165
|
+
* A session that persists to a stored context saves to it when the session ends, so two at once
|
|
166
|
+
* can lose one's changes. `open` holds the context through the `ContextLease` the application
|
|
167
|
+
* provides, from before it creates such a session until Browserbase reports the session ended and
|
|
168
|
+
* `contextSettle` has passed, since the save lands later and nothing acknowledges it. A writer that
|
|
169
|
+
* leaves a session that saves to the context possibly running, its release `Unconfirmed`, its
|
|
170
|
+
* create's answer lost or its session kept, leaves the context unsettled: the next writer first ends
|
|
171
|
+
* the context's sessions, which `open` labels in their user metadata, and fails with `ContextHeld`
|
|
172
|
+
* while they can't be confirmed ended. `reconcile` ends them without opening, and `verifyContext`
|
|
173
|
+
* reads a context back under the same hold, from a session that saves nothing.
|
|
174
|
+
*
|
|
175
|
+
* `attach` connects to a session that already runs, as from another process: its pages keep their
|
|
176
|
+
* ids, so a page stored before is found again. `supervise` keeps a session open across losses and
|
|
177
|
+
* session ends, as `Supervisor` generations, and with `keep`, past its own scope, for the next
|
|
178
|
+
* `supervise` to adopt.
|
|
179
|
+
*
|
|
180
|
+
* @since 0.3.0
|
|
181
|
+
*/
|
|
182
|
+
var Browserbase_exports = /* @__PURE__ */ __exportAll({
|
|
183
|
+
attach: () => attach,
|
|
184
|
+
layer: () => layer,
|
|
185
|
+
open: () => open,
|
|
186
|
+
reconcile: () => reconcile,
|
|
187
|
+
supervise: () => supervise,
|
|
188
|
+
verifyContext: () => verifyContext
|
|
189
|
+
});
|
|
190
|
+
/** The user metadata key naming the stored context a persisting session writes to. */
|
|
191
|
+
const contextLabel = "persistsContext";
|
|
192
|
+
/** The user metadata key holding each create's own nonce, which finds a session made unseen. */
|
|
193
|
+
const createLabel = "createNonce";
|
|
194
|
+
/** The user metadata key naming what a supervisor keeps its sessions under, past its scope. */
|
|
195
|
+
const keepLabel = "keptAs";
|
|
196
|
+
/**
|
|
197
|
+
* How long a create whose answer was lost has its session looked for: Browserbase can list a new
|
|
198
|
+
* session late, or still be making it when the client stops waiting.
|
|
199
|
+
*/
|
|
200
|
+
const orphanSearch = Duration.seconds(30);
|
|
201
|
+
/** How long a release waits for Browserbase to report the session ended. */
|
|
202
|
+
const releaseDeadline = Duration.minutes(1);
|
|
203
|
+
const ended = (session) => session.status !== "PENDING" && session.status !== "RUNNING";
|
|
204
|
+
/**
|
|
205
|
+
* End the session and confirm Browserbase reports it ended. The release request and the status
|
|
206
|
+
* reads are each tried again a second apart until the deadline; a session still running then, or
|
|
207
|
+
* one Browserbase could not be asked about, is `Unconfirmed`. Once it has ended, wait `settle`
|
|
208
|
+
* longer, for its context's save to land. It runs to the end once started.
|
|
209
|
+
*/
|
|
210
|
+
const confirm = (client, id, settle) => Effect.gen(function* () {
|
|
211
|
+
let last = "Browserbase did not answer";
|
|
212
|
+
const heard = (error) => Effect.sync(() => last = error.message);
|
|
213
|
+
const read = client.getSession(id).pipe(Effect.tapError(heard), Effect.tap((session) => Effect.sync(() => last = `the session was ${session.status}`)), Effect.retry(Schedule.spaced("1 second")), Effect.repeat({
|
|
214
|
+
until: ended,
|
|
215
|
+
schedule: Schedule.spaced("1 second")
|
|
216
|
+
}));
|
|
217
|
+
if (!(yield* client.releaseSession(id).pipe(Effect.tapError(heard), Effect.retry({
|
|
218
|
+
while: isTransient,
|
|
219
|
+
schedule: Schedule.spaced("1 second")
|
|
220
|
+
}), Effect.exit, Effect.andThen(read), Effect.as(true), Effect.timeoutOrElse({
|
|
221
|
+
duration: releaseDeadline,
|
|
222
|
+
orElse: () => Effect.succeed(false)
|
|
223
|
+
}), Effect.orElseSucceed(() => false)))) return new Supervisor.Unconfirmed({ detail: `${last} ${Duration.format(releaseDeadline)} after its release` });
|
|
224
|
+
if (settle !== void 0) yield* Effect.sleep(settle);
|
|
225
|
+
return new Supervisor.Settled();
|
|
226
|
+
}).pipe(Effect.tap((released) => Effect.annotateCurrentSpan({ released: released._tag })), Effect.withSpan("Browserbase.release", { attributes: {
|
|
227
|
+
session: id,
|
|
228
|
+
settle: settle !== void 0
|
|
229
|
+
} }, { captureStackTrace: false }), Effect.uninterruptible);
|
|
230
|
+
/** A search for the sessions with `value` under the user metadata key `label`. */
|
|
231
|
+
const labelled = (label, value) => `user_metadata['${label}']:'${value}'`;
|
|
232
|
+
/**
|
|
233
|
+
* The sessions a search finds that have not ended. Pending ones are listed first, since one listed
|
|
234
|
+
* as pending may be running by the second query.
|
|
235
|
+
*/
|
|
236
|
+
const unended = (client, query) => Effect.forEach(["PENDING", "RUNNING"], (status) => client.listSessions({
|
|
237
|
+
status,
|
|
238
|
+
query
|
|
239
|
+
})).pipe(Effect.map((found) => [...new Map(found.flat().map((session) => [session.id, session])).values()]));
|
|
240
|
+
/**
|
|
241
|
+
* End the sessions and confirm they ended; then wait `settle`, if given, for the last save to land.
|
|
242
|
+
* One still running at its release's deadline makes the outcome `Unconfirmed`.
|
|
243
|
+
*/
|
|
244
|
+
const endAll = (client, sessions, settle) => Effect.forEach(sessions, (session) => confirm(client, session, void 0), { concurrency: 4 }).pipe(Effect.flatMap((outcomes) => {
|
|
245
|
+
const unconfirmed = outcomes.find((outcome) => outcome._tag === "Unconfirmed");
|
|
246
|
+
return unconfirmed === void 0 ? Effect.sleep(settle ?? Duration.zero).pipe(Effect.as(new Supervisor.Settled())) : Effect.succeed(unconfirmed);
|
|
247
|
+
}));
|
|
248
|
+
/**
|
|
249
|
+
* End the session a create may have made though its answer was lost, found in any state by the
|
|
250
|
+
* create's own label, and then wait `settle`, if given, for its save. Browserbase can list a new
|
|
251
|
+
* session late, so it is looked for again, more and more seldom, until it is found or
|
|
252
|
+
* `orphanSearch` has passed. A search Browserbase never answered is `Unconfirmed`, as is a session
|
|
253
|
+
* still running at its release's deadline.
|
|
254
|
+
*/
|
|
255
|
+
const endOrphan = (client, nonce, settle) => Effect.gen(function* () {
|
|
256
|
+
let answered = false;
|
|
257
|
+
const found = yield* client.listSessions({ query: labelled(createLabel, nonce) }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
258
|
+
answered = true;
|
|
259
|
+
})), Effect.retry(Schedule.exponential("1 second")), Effect.repeat({
|
|
260
|
+
schedule: Schedule.exponential("1 second"),
|
|
261
|
+
until: (sessions) => sessions.length > 0
|
|
262
|
+
}), Effect.timeoutOption(orphanSearch));
|
|
263
|
+
if (Option.isNone(found) && !answered) return new Supervisor.Unconfirmed({ detail: `Browserbase did not answer a search for it within ${Duration.format(orphanSearch)}` });
|
|
264
|
+
const running = Option.getOrElse(found, () => []).filter((session) => !ended(session));
|
|
265
|
+
return yield* endAll(client, running.map((session) => session.id), settle);
|
|
266
|
+
});
|
|
267
|
+
/**
|
|
268
|
+
* Hold the stored context through the lease, as its only writer, until the scope closes or the
|
|
269
|
+
* session's end is known; the hold goes once, saying how this writer leaves the context.
|
|
270
|
+
*/
|
|
271
|
+
const holdContext = Effect.fnUntraced(function* (client, lease, id, settle) {
|
|
272
|
+
const scope = yield* Scope.fork(yield* Effect.scope);
|
|
273
|
+
const hold = yield* lease.hold(id).pipe(Scope.provide(scope), Effect.withSpan("Browserbase.holdContext", {}, { captureStackTrace: false }));
|
|
274
|
+
let unsettled = hold.unsettled;
|
|
275
|
+
const set = (now) => Effect.sync(() => {
|
|
276
|
+
unsettled = now;
|
|
277
|
+
});
|
|
278
|
+
yield* Scope.addFinalizer(scope, Effect.suspend(() => hold.leave(unsettled)));
|
|
279
|
+
const letGo = (now) => set(now).pipe(Effect.andThen(Scope.close(scope, Exit.void)));
|
|
280
|
+
/** End the context's sessions but `except`, found by the label `open` gives them. */
|
|
281
|
+
const end = (except) => unended(client, labelled(contextLabel, id)).pipe(Effect.flatMap((found) => endAll(client, found.map((session) => session.id).filter((session) => !except.includes(session)), settle)), Effect.onExit((exit) => set(!(Exit.isSuccess(exit) && exit.value._tag === "Settled"))));
|
|
282
|
+
const refuse = (outcome) => outcome._tag === "Settled" ? Effect.void : Effect.fail(new ContextHeld({
|
|
283
|
+
context: id,
|
|
284
|
+
detail: outcome.detail
|
|
285
|
+
}));
|
|
286
|
+
return {
|
|
287
|
+
end,
|
|
288
|
+
/**
|
|
289
|
+
* End the sessions, but `except`, that a writer before may have left saving to the context,
|
|
290
|
+
* failing with `ContextHeld` while they can't be confirmed ended.
|
|
291
|
+
*/
|
|
292
|
+
clear: (except) => Effect.suspend(() => unsettled ? Effect.flatMap(end(except), refuse) : Effect.void),
|
|
293
|
+
/** A session that saves to the context runs from now. */
|
|
294
|
+
writing: set(true),
|
|
295
|
+
/** How the writer's session ended: unconfirmed, it may still save to the context. */
|
|
296
|
+
released: (outcome) => letGo(outcome._tag === "Unconfirmed"),
|
|
297
|
+
/** How ending the session a lost create may have made went: unconfirmed, the open fails. */
|
|
298
|
+
lost: (outcome) => letGo(outcome._tag === "Unconfirmed").pipe(Effect.andThen(refuse(outcome))),
|
|
299
|
+
/** The writer's session runs on past its scope, saving to the context when it ends. */
|
|
300
|
+
kept: letGo(true)
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
/** A create that failed so may still have made its session: its answer is lost or unreadable. */
|
|
304
|
+
const mayHaveCreated = (error) => error.reason._tag === "Transport" || error.reason._tag === "Decode" || error.reason._tag === "Status" && (error.reason.status >= 500 || error.reason.status === 408);
|
|
305
|
+
/** A create's own nonce: two random draws, which no other create draws in practice. */
|
|
306
|
+
const nonce = Effect.map(Effect.all([Random.nextInt, Random.nextInt]), (draws) => draws.map((draw) => Math.abs(draw).toString(36)).join("-"));
|
|
307
|
+
/**
|
|
308
|
+
* The create request, labelled with its own nonce, a persisting session with its context too, so
|
|
309
|
+
* either can be found, and a kept one with what it is kept under, kept alive past disconnecting.
|
|
310
|
+
*/
|
|
311
|
+
const withLabels = (session, own, context, keep) => ({
|
|
312
|
+
...session,
|
|
313
|
+
...keep === void 0 ? {} : { keepAlive: true },
|
|
314
|
+
userMetadata: {
|
|
315
|
+
...session?.userMetadata,
|
|
316
|
+
[createLabel]: own,
|
|
317
|
+
...context === void 0 ? {} : { [contextLabel]: context },
|
|
318
|
+
...keep === void 0 ? {} : { [keepLabel]: keep }
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
/**
|
|
322
|
+
* A release by the browser's owner: it closes the browser first, so that whatever fails then, a
|
|
323
|
+
* capture's reader or a call in flight, says the session was released, and not that the
|
|
324
|
+
* connection Browserbase cuts as the session ends was lost.
|
|
325
|
+
*/
|
|
326
|
+
const releasing = (browser, end) => Scope.close(browser, Exit.void).pipe(Effect.andThen(end));
|
|
327
|
+
/**
|
|
328
|
+
* A browser over the session's DevTools address, its pages' screencasts on a capture connection of
|
|
329
|
+
* their own. A DevTools server's HTTP address, as a local Chromium gives in tests, keeps them on
|
|
330
|
+
* each page's own session: there the connection is loopback, and fast. A session that has ended
|
|
331
|
+
* fails `Closed` by the session, with no connect.
|
|
332
|
+
*/
|
|
333
|
+
const connect = Effect.fnUntraced(function* (operation, session, options) {
|
|
334
|
+
const endpoint = session.connectUrl;
|
|
335
|
+
if (ended(session)) return yield* new BrowserError({
|
|
336
|
+
operation,
|
|
337
|
+
reason: new Closed({ cause: "session" }),
|
|
338
|
+
dispatched: false
|
|
339
|
+
});
|
|
340
|
+
if (endpoint === void 0) return yield* new BrowserbaseError({
|
|
341
|
+
operation,
|
|
342
|
+
reason: new Decode({ detail: `session ${session.id} has no connectUrl` })
|
|
343
|
+
});
|
|
344
|
+
const apart = options.captureConnection !== false && /^wss?:/i.test(Redacted.value(endpoint));
|
|
345
|
+
return yield* Cdp.open({
|
|
346
|
+
...options,
|
|
347
|
+
endpoint,
|
|
348
|
+
id: session.id,
|
|
349
|
+
provider: "browserbase",
|
|
350
|
+
expiresAt: Option.getOrUndefined(DateTime.make(session.expiresAt)),
|
|
351
|
+
capture: apart ? yield* make(endpoint) : void 0
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
const keepName = /^[\w.:-]{1,64}$/;
|
|
355
|
+
const newestFirst = (sessions) => {
|
|
356
|
+
const created = (session) => Option.getOrElse(Option.map(DateTime.make(session.createdAt), DateTime.toEpochMillis), () => 0);
|
|
357
|
+
return sessions.toSorted((a, b) => created(b) - created(a));
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* The newest session kept under `name` that saves where this open would, to adopt. Every other
|
|
361
|
+
* session kept under it is to end, since nothing else adopts it: those that save to this open's
|
|
362
|
+
* context are its writers, for the context's hold to end, as `writers` says, and the rest end here.
|
|
363
|
+
*/
|
|
364
|
+
const adoptable = Effect.fnUntraced(function* (client, name, persisting) {
|
|
365
|
+
const [newest, ...older] = newestFirst(yield* unended(client, labelled(keepLabel, name)));
|
|
366
|
+
const fits = newest !== void 0 && newest.userMetadata?.[contextLabel] === persisting;
|
|
367
|
+
const others = [...fits || newest === void 0 ? [] : [newest], ...older];
|
|
368
|
+
const writes = (session) => persisting !== void 0 && session.userMetadata?.[contextLabel] === persisting;
|
|
369
|
+
const rest = others.filter((session) => !writes(session));
|
|
370
|
+
if (rest.length > 0) {
|
|
371
|
+
const outcome = yield* endAll(client, rest.map((session) => session.id), void 0);
|
|
372
|
+
yield* Effect.annotateCurrentSpan({
|
|
373
|
+
endedKept: rest.length,
|
|
374
|
+
endedKeptAs: outcome._tag
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
adoptee: fits ? newest : void 0,
|
|
379
|
+
writers: others.some(writes)
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
/**
|
|
383
|
+
* Open a browser on a new session, or, when keeping, adopt one a supervisor kept before. A kept
|
|
384
|
+
* session's scope only disconnects, unless it closes on a failure: `release` ends it.
|
|
385
|
+
*/
|
|
386
|
+
const start = Effect.fnUntraced(function* (options, keeping) {
|
|
387
|
+
const client = yield* BrowserbaseClient;
|
|
388
|
+
const lease = yield* ContextLease;
|
|
389
|
+
const context = options.session?.browserSettings?.context;
|
|
390
|
+
const persisting = context?.persist === true ? context.id : void 0;
|
|
391
|
+
const local = yield* Scope.fork(yield* Effect.scope);
|
|
392
|
+
const settle = Duration.fromInputUnsafe(options.contextSettle ?? Duration.seconds(10));
|
|
393
|
+
return yield* Effect.gen(function* () {
|
|
394
|
+
if (keeping !== void 0 && !keepName.test(keeping.name)) return yield* new BrowserbaseError({
|
|
395
|
+
operation: "supervise",
|
|
396
|
+
reason: new InvalidRequest({ detail: `keep must be 1 to 64 letters, digits, "_", ".", ":" or "-"` })
|
|
397
|
+
});
|
|
398
|
+
const writing = persisting === void 0 ? void 0 : yield* holdContext(client, lease, persisting, settle);
|
|
399
|
+
const adoption = keeping?.adopt ? yield* adoptable(client, keeping.name, persisting) : void 0;
|
|
400
|
+
const adopted = adoption?.adoptee;
|
|
401
|
+
if (adoption?.writers === true) yield* writing?.writing ?? Effect.void;
|
|
402
|
+
yield* writing?.clear(adopted === void 0 ? [] : [adopted.id]) ?? Effect.void;
|
|
403
|
+
const connected = yield* Scope.fork(local);
|
|
404
|
+
const own = yield* nonce;
|
|
405
|
+
const ending = (session) => Effect.gen(function* () {
|
|
406
|
+
let asked = false;
|
|
407
|
+
return {
|
|
408
|
+
session,
|
|
409
|
+
release: yield* Effect.cached(Effect.suspend(() => {
|
|
410
|
+
asked = true;
|
|
411
|
+
return releasing(connected, confirm(client, session.id, writing && settle)).pipe(Effect.tap((outcome) => writing?.released(outcome) ?? Effect.void));
|
|
412
|
+
})),
|
|
413
|
+
leave: Effect.suspend(() => asked ? Effect.void : Scope.close(connected, Exit.void).pipe(Effect.andThen(writing?.kept ?? Effect.void)))
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
const { session, release } = yield* Effect.acquireRelease(Effect.gen(function* () {
|
|
417
|
+
const session = adopted === void 0 ? yield* client.createSession(withLabels(options.session, own, persisting, keeping?.name)) : yield* client.getSession(adopted.id);
|
|
418
|
+
yield* writing?.writing ?? Effect.void;
|
|
419
|
+
return yield* ending(session);
|
|
420
|
+
}), ({ release, leave }, exit) => keeping !== void 0 && Exit.isSuccess(exit) ? leave : Effect.asVoid(release)).pipe(Effect.tapError((error) => adopted === void 0 && mayHaveCreated(error) ? endOrphan(client, own, writing && settle).pipe(Effect.flatMap((outcome) => writing?.lost(outcome) ?? Effect.void)) : Effect.void));
|
|
421
|
+
yield* Effect.annotateCurrentSpan({
|
|
422
|
+
session: session.id,
|
|
423
|
+
region: session.region,
|
|
424
|
+
adopted: adopted !== void 0
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
browser: yield* connect(adopted === void 0 ? "open" : "attach", session, options).pipe(Scope.provide(connected)),
|
|
428
|
+
session,
|
|
429
|
+
release
|
|
430
|
+
};
|
|
431
|
+
}).pipe(Scope.provide(local), Effect.onError((cause) => Scope.close(local, Exit.failCause(cause))));
|
|
432
|
+
});
|
|
433
|
+
/**
|
|
434
|
+
* Create a session and open a `Browser` on it, for as long as the scope is open. If opening
|
|
435
|
+
* fails, the session and context are released then, so a retry in the same scope can take them.
|
|
436
|
+
* A persisting open holds its context through the `ContextLease`; a session that may still save
|
|
437
|
+
* to it, as one whose release was left `Unconfirmed` or whose create's answer was lost, is ended
|
|
438
|
+
* first, and the open fails with `ContextHeld` while that can't be confirmed. A lost create's own
|
|
439
|
+
* open looks for its session by the create's nonce, within `orphanSearch`, ends it, then fails with
|
|
440
|
+
* the create's error. `release` disconnects the browser before it asks Browserbase to end the
|
|
441
|
+
* session, so what fails meanwhile says it was released.
|
|
442
|
+
*/
|
|
443
|
+
const open = Effect.fn("Browserbase.open")(function* (options = {}) {
|
|
444
|
+
return yield* start(options, void 0);
|
|
445
|
+
});
|
|
446
|
+
/**
|
|
447
|
+
* Open a `Browser` on a session that already runs, such as a `keepAlive` session another process
|
|
448
|
+
* opened. This is resume: its pages keep their ids, their CDP target ids, so `browser.page(id)`
|
|
449
|
+
* finds a page stored before. Closing the scope disconnects and leaves the session running;
|
|
450
|
+
* `release` ends it. A session that has ended fails `Closed` by the session. It holds no context.
|
|
451
|
+
*/
|
|
452
|
+
const attach = Effect.fn("Browserbase.attach")(function* (sessionId, options = {}) {
|
|
453
|
+
const client = yield* BrowserbaseClient;
|
|
454
|
+
const session = yield* client.getSession(sessionId);
|
|
455
|
+
yield* Effect.annotateCurrentSpan({
|
|
456
|
+
session: session.id,
|
|
457
|
+
region: session.region
|
|
458
|
+
});
|
|
459
|
+
const connected = yield* Scope.fork(yield* Effect.scope);
|
|
460
|
+
return {
|
|
461
|
+
browser: yield* connect("attach", session, options).pipe(Scope.provide(connected)),
|
|
462
|
+
session,
|
|
463
|
+
release: yield* Effect.cached(releasing(connected, confirm(client, session.id, void 0)))
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
/**
|
|
467
|
+
* End a stored context's sessions, the ones `open` labelled as writing to it, and let the context
|
|
468
|
+
* go once they are confirmed ended and its save has settled. `open` does this itself when a writer
|
|
469
|
+
* before it may still run; `reconcile` does it without opening, as for sessions another process
|
|
470
|
+
* left running. It holds the context through the `ContextLease`, so it waits for a writer. A
|
|
471
|
+
* session still running at the deadline makes it `Unconfirmed`, and leaves the context unsettled,
|
|
472
|
+
* as a failure does.
|
|
473
|
+
*/
|
|
474
|
+
const reconcile = Effect.fn("Browserbase.reconcile")(function* (contextId, options = {}) {
|
|
475
|
+
const client = yield* BrowserbaseClient;
|
|
476
|
+
const lease = yield* ContextLease;
|
|
477
|
+
const settle = Duration.fromInputUnsafe(options.contextSettle ?? Duration.seconds(10));
|
|
478
|
+
yield* client.getContext(contextId);
|
|
479
|
+
return yield* (yield* holdContext(client, lease, contextId, settle)).end([]);
|
|
480
|
+
}, Effect.scoped);
|
|
481
|
+
/**
|
|
482
|
+
* Read a stored context back, as a login: a session that loads it and saves nothing runs `check`
|
|
483
|
+
* on its browser, and is released. The context is held through the `ContextLease` meanwhile, so
|
|
484
|
+
* the read never runs beside a writer, which a site could sign out, and it reads what the last
|
|
485
|
+
* writer saved, once that save settled. A session a writer before may have left saving to it is
|
|
486
|
+
* ended first, as for `open`.
|
|
487
|
+
*/
|
|
488
|
+
const verifyContext = Effect.fn("Browserbase.verifyContext")(function* (contextId, check, options = {}) {
|
|
489
|
+
const client = yield* BrowserbaseClient;
|
|
490
|
+
const lease = yield* ContextLease;
|
|
491
|
+
const settle = Duration.fromInputUnsafe(options.contextSettle ?? Duration.seconds(10));
|
|
492
|
+
yield* (yield* holdContext(client, lease, contextId, settle)).clear([]);
|
|
493
|
+
const { browser } = yield* start({
|
|
494
|
+
...options,
|
|
495
|
+
session: {
|
|
496
|
+
...options.session,
|
|
497
|
+
browserSettings: {
|
|
498
|
+
...options.session?.browserSettings,
|
|
499
|
+
context: {
|
|
500
|
+
id: contextId,
|
|
501
|
+
persist: false
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}, void 0);
|
|
506
|
+
return yield* check(browser);
|
|
507
|
+
}, Effect.scoped);
|
|
508
|
+
/**
|
|
509
|
+
* Browserbase refused the request: no new try mends a refused key, request or id. A context held
|
|
510
|
+
* is not among them, since each try ends its sessions again.
|
|
511
|
+
*/
|
|
512
|
+
const refused = (error) => Schema.is(BrowserbaseError)(error) && !isTransient(error) && error.reason._tag !== "Decode";
|
|
513
|
+
function supervise(options = {}) {
|
|
514
|
+
return Effect.suspend(() => {
|
|
515
|
+
const { keep } = options;
|
|
516
|
+
let opened = false;
|
|
517
|
+
return Supervisor.make({
|
|
518
|
+
open: Effect.gen(function* () {
|
|
519
|
+
const hosted = yield* start(options, keep === void 0 ? void 0 : {
|
|
520
|
+
name: keep,
|
|
521
|
+
adopt: !opened
|
|
522
|
+
});
|
|
523
|
+
opened = true;
|
|
524
|
+
const value = options.build === void 0 ? hosted.session : yield* options.build(hosted);
|
|
525
|
+
return {
|
|
526
|
+
...hosted,
|
|
527
|
+
value
|
|
528
|
+
};
|
|
529
|
+
}).pipe(Effect.withSpan("Browserbase.open", {}, { captureStackTrace: false })),
|
|
530
|
+
exclusive: options.session?.browserSettings?.context?.persist === true,
|
|
531
|
+
keep: keep !== void 0,
|
|
532
|
+
reopen: options.reopen,
|
|
533
|
+
definite: refused,
|
|
534
|
+
rotateBefore: options.rotateBefore,
|
|
535
|
+
waitTimeout: options.waitTimeout,
|
|
536
|
+
handover: options.handover
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const layer = (options = {}) => Layer.effect(Browser.Browser, Effect.map(open(options), ({ browser }) => browser));
|
|
541
|
+
//#endregion
|
|
542
|
+
export { reconcile as a, open as i, attach as n, supervise as o, layer as r, verifyContext as s, Browserbase_exports as t };
|
|
543
|
+
|
|
544
|
+
//# sourceMappingURL=Browserbase-CaPKwEY9.mjs.map
|