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