browserscale-ts 1.4.0 → 1.7.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 +34 -3
- package/dist/auth-session.d.ts +37 -0
- package/dist/auth-session.js +1 -0
- package/dist/browser.d.ts +5 -1
- package/dist/browser.js +5 -0
- package/dist/browserscale.browser.js +1662 -42
- package/dist/client.d.ts +350 -7
- package/dist/client.js +637 -8
- package/dist/dom-mirror.d.ts +271 -0
- package/dist/dom-mirror.js +613 -0
- package/dist/gen/wrc_pb.d.ts +1490 -168
- package/dist/gen/wrc_pb.js +234 -39
- package/dist/index.d.ts +32 -1
- package/dist/index.js +61 -0
- package/dist/internal/convert.d.ts +11 -2
- package/dist/internal/convert.js +84 -1
- package/dist/network-capture.d.ts +84 -0
- package/dist/network-capture.js +107 -0
- package/dist/scripts.d.ts +205 -0
- package/dist/scripts.js +234 -0
- package/dist/types.d.ts +157 -0
- package/dist/ws-transport.d.ts +15 -1
- package/dist/ws-transport.js +155 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,6 +9,11 @@ export { Locator, css, js, node, at, AllFrames } from "./locator.js";
|
|
|
9
9
|
export { DefaultWaitTimeoutMs, DefaultVisible, DefaultSteadyMs, } from "./defaults.js";
|
|
10
10
|
// Errors — base class plus the typed semantic-failure subclasses
|
|
11
11
|
export { BrowserScaleError, ClickError, FillError, DragError, ScrollError, MoveError, SelectOptionError, WaitError, } from "./errors.js";
|
|
12
|
+
// Scripts (automation running inside the browser process)
|
|
13
|
+
export { ScriptRun, ScriptFollow, } from "./scripts.js";
|
|
14
|
+
// Network capture (traffic log)
|
|
15
|
+
export { NetworkCapture } from "./network-capture.js";
|
|
16
|
+
export { DomMirror, } from "./dom-mirror.js";
|
|
12
17
|
// Rent / stop ──────────────────────────────────────────────────────────
|
|
13
18
|
let apiEndpoint = "https://api.browserscale.cloud";
|
|
14
19
|
/**
|
|
@@ -106,6 +111,62 @@ export function connectSession(grpcUrl, apiKey, sessionId) {
|
|
|
106
111
|
export async function stopBrowser(apiKey, sessionId) {
|
|
107
112
|
await callStopApi(apiKey, sessionId);
|
|
108
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Reports the sessions an API key currently holds.
|
|
116
|
+
*
|
|
117
|
+
* Use it to recover session ids the process lost — after a restart, or from a
|
|
118
|
+
* different machine entirely. Without it a rental is only reachable through the
|
|
119
|
+
* handle that created it, so a crash between rent and stop leaves a paid session
|
|
120
|
+
* running with nothing able to name it.
|
|
121
|
+
*
|
|
122
|
+
* Each entry carries the `grpcUrl` it is driven from, so a listed session can be
|
|
123
|
+
* handed straight to {@link connectSession}. Only live sessions are listed; a
|
|
124
|
+
* stopped one is gone, not reported as ended.
|
|
125
|
+
*
|
|
126
|
+
* @param apiKey - API key whose sessions to list
|
|
127
|
+
*
|
|
128
|
+
* @returns the running sessions, oldest first; empty when the key holds none
|
|
129
|
+
*
|
|
130
|
+
* @throws UNKNOWN_ERROR - the list API rejected the request
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* const browsers = await listBrowsers(apiKey);
|
|
134
|
+
* for (const b of browsers) console.log(b.sessionId, b.countryCode);
|
|
135
|
+
*
|
|
136
|
+
* // and to drive one of them
|
|
137
|
+
* const browser = connectSession(browsers[0].grpcUrl, apiKey, browsers[0].sessionId);
|
|
138
|
+
*/
|
|
139
|
+
export async function listBrowsers(apiKey) {
|
|
140
|
+
const resp = await fetch(`${apiEndpoint}/sessions`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/json" },
|
|
143
|
+
body: JSON.stringify({ apiKey }),
|
|
144
|
+
});
|
|
145
|
+
// Named explicitly, because this is the one endpoint a caller can reach on a
|
|
146
|
+
// deployment that does not have it: listing came after rent and stop. Letting
|
|
147
|
+
// it fall through would report a JSON parse failure against an error page,
|
|
148
|
+
// which says nothing about the actual problem.
|
|
149
|
+
if (resp.status === 404) {
|
|
150
|
+
throw new Error(`the API at ${apiEndpoint} does not support listing sessions`);
|
|
151
|
+
}
|
|
152
|
+
const data = await resp.json();
|
|
153
|
+
if (!resp.ok || !data.success) {
|
|
154
|
+
throw new Error(`list failed: ${data.error ?? resp.statusText}`);
|
|
155
|
+
}
|
|
156
|
+
const sessions = (data.sessions ?? []);
|
|
157
|
+
return sessions.map((s) => ({
|
|
158
|
+
sessionId: s.sessionId,
|
|
159
|
+
grpcUrl: s.grpcUrl ?? "",
|
|
160
|
+
startTime: s.startTime ?? 0,
|
|
161
|
+
rentDuration: s.rentDuration ?? 0,
|
|
162
|
+
remainingSeconds: s.remainingSeconds,
|
|
163
|
+
countryCode: s.countryCode ?? "",
|
|
164
|
+
timezone: s.timezone ?? "",
|
|
165
|
+
proxyHost: s.proxyHost ?? "",
|
|
166
|
+
publicIp: s.publicIp ?? "",
|
|
167
|
+
gpuIndex: s.gpuIndex,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
109
170
|
// Maps the rent response's gRPC URL to a transport base URL. The scheme
|
|
110
171
|
// signals transport security: grpcs:// dials TLS, grpc:// (or no scheme,
|
|
111
172
|
// for older servers) dials plaintext.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Rect as ProtoRect, type ClickResult as ProtoClickResult, type FillResult as ProtoFillResult, type MoveResult as ProtoMoveResult, type ScrollResult as ProtoScrollResult, type DragResult as ProtoDragResult, type SelectOptionResult as ProtoSelectOptionResult, type WaitResult as ProtoWaitResult, type FrameInfo as ProtoFrameInfo, type PageInfo as ProtoPageInfo, type Header as ProtoHeader, type InterceptedRequest as ProtoInterceptedRequest, type InterceptedResponse as ProtoInterceptedResponse, type HeaderModification as ProtoHeaderModification, type CookieParam as ProtoCookieParam, type StorageOriginEntry as ProtoStorageOriginEntry } from "../gen/wrc_pb.ts";
|
|
2
|
-
import type { DragResult, ElementResult, FrameInfo, Header, InterceptedRequest, InterceptedResponse, PageInfo, Rect, SelectOptionResult, WaitResult } from "../types.ts";
|
|
1
|
+
import { type Rect as ProtoRect, type ClickResult as ProtoClickResult, type FillResult as ProtoFillResult, type MoveResult as ProtoMoveResult, type ScrollResult as ProtoScrollResult, type DragResult as ProtoDragResult, type SelectOptionResult as ProtoSelectOptionResult, type WaitResult as ProtoWaitResult, type FrameInfo as ProtoFrameInfo, type PageInfo as ProtoPageInfo, type Header as ProtoHeader, type InterceptedRequest as ProtoInterceptedRequest, type InterceptedResponse as ProtoInterceptedResponse, type NetworkExchange as ProtoNetworkExchange, type HeaderModification as ProtoHeaderModification, type CookieParam as ProtoCookieParam, type StorageOriginEntry as ProtoStorageOriginEntry, type AuthSession as ProtoAuthSession } from "../gen/wrc_pb.ts";
|
|
2
|
+
import type { DragResult, ElementResult, FrameInfo, Header, InterceptedRequest, InterceptedResponse, NetworkExchange, PageInfo, Rect, SelectOptionResult, WaitResult } from "../types.ts";
|
|
3
3
|
import type { CookieParam } from "../cookies.ts";
|
|
4
4
|
import type { StorageOriginEntry } from "../storage.ts";
|
|
5
|
+
import type { AuthSession } from "../auth-session.ts";
|
|
5
6
|
import type { HeaderModification, RequestPattern } from "../network.ts";
|
|
6
7
|
import { type Locator } from "../locator.ts";
|
|
7
8
|
/**
|
|
@@ -38,11 +39,19 @@ export declare function pageInfoFromProto(p: ProtoPageInfo): PageInfo;
|
|
|
38
39
|
export declare function headerFromProto(h: ProtoHeader): Header;
|
|
39
40
|
export declare function headersToProto(headers: Header[] | undefined): ProtoHeader[];
|
|
40
41
|
export declare function interceptedRequestFromProto(r: ProtoInterceptedRequest | undefined): InterceptedRequest | null;
|
|
42
|
+
/**
|
|
43
|
+
* The int64/uint64 fields arrive as bigint. Byte counts stay far below
|
|
44
|
+
* Number.MAX_SAFE_INTEGER, so they are narrowed to number to keep the SDK
|
|
45
|
+
* surface free of bigint.
|
|
46
|
+
*/
|
|
47
|
+
export declare function networkExchangeFromProto(e: ProtoNetworkExchange): NetworkExchange;
|
|
41
48
|
export declare function interceptedResponseFromProto(r: ProtoInterceptedResponse | undefined): InterceptedResponse | null;
|
|
42
49
|
export declare function cookieParamFromProto(c: ProtoCookieParam): CookieParam;
|
|
43
50
|
export declare function cookieParamsToProto(cookies: CookieParam[]): ProtoCookieParam[];
|
|
44
51
|
export declare function storageEntryFromProto(e: ProtoStorageOriginEntry): StorageOriginEntry;
|
|
45
52
|
export declare function storageEntriesToProto(storage: StorageOriginEntry[]): ProtoStorageOriginEntry[];
|
|
53
|
+
export declare function authSessionFromProto(s: ProtoAuthSession): AuthSession;
|
|
54
|
+
export declare function authSessionToProto(s: AuthSession): ProtoAuthSession;
|
|
46
55
|
/**
|
|
47
56
|
* splitRequestPatterns turns a RequestPattern[] into the proto's parallel
|
|
48
57
|
* URL + abort-flag slices. When no pattern has abort set, the aborts
|
package/dist/internal/convert.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Internal proto ↔ TS helpers used by client.ts. Anything in here is
|
|
2
2
|
// implementation detail and is NOT exported from the package entrypoints.
|
|
3
3
|
import { create } from "@bufbuild/protobuf";
|
|
4
|
-
import { HeaderModificationSchema, CookiePartitionKeySchema, CookieParamSchema, HeaderSchema, StorageItemSchema, StorageOriginEntrySchema, } from "../gen/wrc_pb.js";
|
|
4
|
+
import { HeaderModificationSchema, CookiePartitionKeySchema, CookieParamSchema, HeaderSchema, StorageItemSchema, StorageOriginEntrySchema, AuthSessionSchema, DbscSessionSchema, } from "../gen/wrc_pb.js";
|
|
5
5
|
import { ClickError, DragError, FillError, MoveError, ScrollError, SelectOptionError, WaitError, } from "../errors.js";
|
|
6
6
|
import { pickFrame } from "../locator.js";
|
|
7
7
|
export function elementFields(target, optsInFrame) {
|
|
@@ -257,6 +257,45 @@ export function interceptedRequestFromProto(r) {
|
|
|
257
257
|
resourceType: r.resourceType,
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
261
|
+
// Network capture
|
|
262
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* The int64/uint64 fields arrive as bigint. Byte counts stay far below
|
|
265
|
+
* Number.MAX_SAFE_INTEGER, so they are narrowed to number to keep the SDK
|
|
266
|
+
* surface free of bigint.
|
|
267
|
+
*/
|
|
268
|
+
export function networkExchangeFromProto(e) {
|
|
269
|
+
return {
|
|
270
|
+
requestId: e.requestId,
|
|
271
|
+
chainId: e.chainId,
|
|
272
|
+
redirectIndex: e.redirectIndex,
|
|
273
|
+
frameId: e.frameId,
|
|
274
|
+
isOopif: e.isOopif,
|
|
275
|
+
resourceType: e.resourceType,
|
|
276
|
+
method: e.method,
|
|
277
|
+
url: e.url,
|
|
278
|
+
initiatorUrl: e.initiatorUrl,
|
|
279
|
+
requestHeaders: e.requestHeaders.map(headerFromProto),
|
|
280
|
+
requestHeadersAreWire: e.requestHeadersAreWire,
|
|
281
|
+
requestBody: e.requestBody,
|
|
282
|
+
requestBodyTruncated: e.requestBodyTruncated,
|
|
283
|
+
hasResponse: e.hasResponse,
|
|
284
|
+
statusCode: e.statusCode,
|
|
285
|
+
statusText: e.statusText,
|
|
286
|
+
mimeType: e.mimeType,
|
|
287
|
+
protocol: e.protocol,
|
|
288
|
+
remoteAddress: e.remoteAddress,
|
|
289
|
+
servedFrom: e.servedFrom,
|
|
290
|
+
responseHeaders: e.responseHeaders.map(headerFromProto),
|
|
291
|
+
responseHeadersAreWire: e.responseHeadersAreWire,
|
|
292
|
+
responseBody: e.responseBody,
|
|
293
|
+
responseBodyTruncated: e.responseBodyTruncated,
|
|
294
|
+
responseBodyCaptured: e.responseBodyCaptured,
|
|
295
|
+
encodedDataLength: Number(e.encodedDataLength),
|
|
296
|
+
error: e.error,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
260
299
|
export function interceptedResponseFromProto(r) {
|
|
261
300
|
if (!r)
|
|
262
301
|
return null;
|
|
@@ -346,6 +385,50 @@ export function storageEntriesToProto(storage) {
|
|
|
346
385
|
});
|
|
347
386
|
}
|
|
348
387
|
// ──────────────────────────────────────────────────────────────────────
|
|
388
|
+
// Auth / DBSC
|
|
389
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
390
|
+
export function authSessionFromProto(s) {
|
|
391
|
+
const out = {};
|
|
392
|
+
if (s.gaiaId !== undefined)
|
|
393
|
+
out.gaiaId = s.gaiaId;
|
|
394
|
+
if (s.email !== undefined)
|
|
395
|
+
out.email = s.email;
|
|
396
|
+
if (s.refreshToken !== undefined)
|
|
397
|
+
out.refreshToken = s.refreshToken;
|
|
398
|
+
if (s.wrappedBindingKey !== undefined)
|
|
399
|
+
out.wrappedBindingKey = s.wrappedBindingKey;
|
|
400
|
+
if (s.signinScopedDeviceId !== undefined)
|
|
401
|
+
out.signinScopedDeviceId = s.signinScopedDeviceId;
|
|
402
|
+
if (s.syncConsent !== undefined)
|
|
403
|
+
out.syncConsent = s.syncConsent;
|
|
404
|
+
if (s.dbscSessions.length > 0) {
|
|
405
|
+
out.dbscSessions = s.dbscSessions.map(d => ({ site: d.site, session: d.session }));
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
export function authSessionToProto(s) {
|
|
410
|
+
const msg = create(AuthSessionSchema);
|
|
411
|
+
if (s.gaiaId !== undefined)
|
|
412
|
+
msg.gaiaId = s.gaiaId;
|
|
413
|
+
if (s.email !== undefined)
|
|
414
|
+
msg.email = s.email;
|
|
415
|
+
if (s.refreshToken !== undefined)
|
|
416
|
+
msg.refreshToken = s.refreshToken;
|
|
417
|
+
if (s.wrappedBindingKey !== undefined)
|
|
418
|
+
msg.wrappedBindingKey = s.wrappedBindingKey;
|
|
419
|
+
if (s.signinScopedDeviceId !== undefined)
|
|
420
|
+
msg.signinScopedDeviceId = s.signinScopedDeviceId;
|
|
421
|
+
if (s.syncConsent !== undefined)
|
|
422
|
+
msg.syncConsent = s.syncConsent;
|
|
423
|
+
msg.dbscSessions = (s.dbscSessions ?? []).map(d => {
|
|
424
|
+
const item = create(DbscSessionSchema);
|
|
425
|
+
item.site = d.site;
|
|
426
|
+
item.session = d.session;
|
|
427
|
+
return item;
|
|
428
|
+
});
|
|
429
|
+
return msg;
|
|
430
|
+
}
|
|
431
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
349
432
|
// Network — patterns + header modifications
|
|
350
433
|
// ──────────────────────────────────────────────────────────────────────
|
|
351
434
|
/**
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { NetworkExchangeEvent } from "./gen/wrc_pb.ts";
|
|
2
|
+
import type { NetworkExchange } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* NetworkExchangeHandler is called once per completed exchange.
|
|
5
|
+
*
|
|
6
|
+
* Calls are sequential and in the order the browser finished the requests, so
|
|
7
|
+
* the hops of a redirect chain arrive in order.
|
|
8
|
+
*
|
|
9
|
+
* Blocking here stalls the capture: awaiting something slow means the server
|
|
10
|
+
* keeps buffering, and once its per-reader bound is reached it drops the oldest
|
|
11
|
+
* entries, which {@link NetworkCapture.dropped} reports. Hand slow work
|
|
12
|
+
* (uploads, IndexedDB) to a queue of your own instead of awaiting it here.
|
|
13
|
+
*/
|
|
14
|
+
export type NetworkExchangeHandler = (exchange: NetworkExchange) => void;
|
|
15
|
+
/**
|
|
16
|
+
* NetworkCapture is a running capture, returned by
|
|
17
|
+
* {@link CloudBrowser.captureNetwork}. Exchanges are delivered to the handler
|
|
18
|
+
* passed there; this handle only exists to stop the capture and to report how
|
|
19
|
+
* it went.
|
|
20
|
+
*/
|
|
21
|
+
export declare class NetworkCapture {
|
|
22
|
+
private readonly abort;
|
|
23
|
+
private readonly disarm;
|
|
24
|
+
private readonly finished;
|
|
25
|
+
private armed;
|
|
26
|
+
private stopped;
|
|
27
|
+
private failure;
|
|
28
|
+
private droppedCount;
|
|
29
|
+
/** @internal Constructed by CloudBrowser; not part of the public API. */
|
|
30
|
+
constructor(init: {
|
|
31
|
+
stream: AsyncIterable<NetworkExchangeEvent>;
|
|
32
|
+
onExchange: NetworkExchangeHandler;
|
|
33
|
+
disarm: () => Promise<unknown>;
|
|
34
|
+
abort: AbortController;
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* @internal Marks the capture as armed by this handle, so stop() disarms it
|
|
38
|
+
* server-side. Views that only attached to someone else's capture stay
|
|
39
|
+
* unarmed and merely detach.
|
|
40
|
+
*/
|
|
41
|
+
arm(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Reads the stream and calls the handler for each exchange. It never rejects:
|
|
44
|
+
* a failure is recorded for {@link NetworkCapture.error} instead, so nothing
|
|
45
|
+
* surfaces as an unhandled rejection.
|
|
46
|
+
*/
|
|
47
|
+
private pump;
|
|
48
|
+
/**
|
|
49
|
+
* Why the capture ended: null while it is still running, and after a clean
|
|
50
|
+
* stop or the session ending normally.
|
|
51
|
+
*/
|
|
52
|
+
get error(): Error | null;
|
|
53
|
+
/**
|
|
54
|
+
* How many exchanges the server discarded because this reader fell behind.
|
|
55
|
+
* Anything above zero means the log has holes: make the handler cheaper,
|
|
56
|
+
* narrow `patterns`, or stop capturing bodies.
|
|
57
|
+
*/
|
|
58
|
+
get dropped(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Resolves once the capture ends — {@link NetworkCapture.stop}, a dead
|
|
61
|
+
* session or a transport failure.
|
|
62
|
+
*
|
|
63
|
+
* Use it to capture for as long as the session lives. It is not needed when
|
|
64
|
+
* you drive the browser yourself and call stop when done.
|
|
65
|
+
*
|
|
66
|
+
* @throws the transport failure that ended the capture, if any
|
|
67
|
+
*/
|
|
68
|
+
wait(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Ends the capture. Idempotent, and safe to call from a `finally`.
|
|
71
|
+
*
|
|
72
|
+
* Once it resolves the handler is no longer running, so data it collected is
|
|
73
|
+
* complete. Views from {@link CloudBrowser.streamNetworkExchanges} only
|
|
74
|
+
* detach — they never disarm a capture other readers may share.
|
|
75
|
+
*
|
|
76
|
+
* To stop from inside the handler, call
|
|
77
|
+
* {@link CloudBrowser.stopNetworkCapture} instead: stop awaits the reader,
|
|
78
|
+
* which cannot finish while the handler it called is still running.
|
|
79
|
+
*
|
|
80
|
+
* @throws UNKNOWN_ERROR - the capture could not be disarmed; the local reader
|
|
81
|
+
* is shut down regardless
|
|
82
|
+
*/
|
|
83
|
+
stop(): Promise<void>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { networkExchangeFromProto } from "./internal/convert.js";
|
|
2
|
+
/**
|
|
3
|
+
* NetworkCapture is a running capture, returned by
|
|
4
|
+
* {@link CloudBrowser.captureNetwork}. Exchanges are delivered to the handler
|
|
5
|
+
* passed there; this handle only exists to stop the capture and to report how
|
|
6
|
+
* it went.
|
|
7
|
+
*/
|
|
8
|
+
export class NetworkCapture {
|
|
9
|
+
/** @internal Constructed by CloudBrowser; not part of the public API. */
|
|
10
|
+
constructor(init) {
|
|
11
|
+
this.armed = false;
|
|
12
|
+
this.stopped = false;
|
|
13
|
+
this.failure = null;
|
|
14
|
+
this.droppedCount = 0;
|
|
15
|
+
this.abort = init.abort;
|
|
16
|
+
this.disarm = init.disarm;
|
|
17
|
+
this.finished = this.pump(init.stream, init.onExchange);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @internal Marks the capture as armed by this handle, so stop() disarms it
|
|
21
|
+
* server-side. Views that only attached to someone else's capture stay
|
|
22
|
+
* unarmed and merely detach.
|
|
23
|
+
*/
|
|
24
|
+
arm() {
|
|
25
|
+
this.armed = true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reads the stream and calls the handler for each exchange. It never rejects:
|
|
29
|
+
* a failure is recorded for {@link NetworkCapture.error} instead, so nothing
|
|
30
|
+
* surfaces as an unhandled rejection.
|
|
31
|
+
*/
|
|
32
|
+
async pump(stream, onExchange) {
|
|
33
|
+
try {
|
|
34
|
+
for await (const event of stream) {
|
|
35
|
+
if (event.dropped > 0n) {
|
|
36
|
+
this.droppedCount = Number(event.dropped);
|
|
37
|
+
}
|
|
38
|
+
if (!event.exchange)
|
|
39
|
+
continue;
|
|
40
|
+
onExchange(networkExchangeFromProto(event.exchange));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
// Cancelling is how stop() ends the stream, so the resulting error is
|
|
45
|
+
// expected rather than a failure worth reporting.
|
|
46
|
+
if (!this.stopped) {
|
|
47
|
+
this.failure = err instanceof Error ? err : new Error(String(err));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Why the capture ended: null while it is still running, and after a clean
|
|
53
|
+
* stop or the session ending normally.
|
|
54
|
+
*/
|
|
55
|
+
get error() {
|
|
56
|
+
return this.failure;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* How many exchanges the server discarded because this reader fell behind.
|
|
60
|
+
* Anything above zero means the log has holes: make the handler cheaper,
|
|
61
|
+
* narrow `patterns`, or stop capturing bodies.
|
|
62
|
+
*/
|
|
63
|
+
get dropped() {
|
|
64
|
+
return this.droppedCount;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolves once the capture ends — {@link NetworkCapture.stop}, a dead
|
|
68
|
+
* session or a transport failure.
|
|
69
|
+
*
|
|
70
|
+
* Use it to capture for as long as the session lives. It is not needed when
|
|
71
|
+
* you drive the browser yourself and call stop when done.
|
|
72
|
+
*
|
|
73
|
+
* @throws the transport failure that ended the capture, if any
|
|
74
|
+
*/
|
|
75
|
+
async wait() {
|
|
76
|
+
await this.finished;
|
|
77
|
+
if (this.failure)
|
|
78
|
+
throw this.failure;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Ends the capture. Idempotent, and safe to call from a `finally`.
|
|
82
|
+
*
|
|
83
|
+
* Once it resolves the handler is no longer running, so data it collected is
|
|
84
|
+
* complete. Views from {@link CloudBrowser.streamNetworkExchanges} only
|
|
85
|
+
* detach — they never disarm a capture other readers may share.
|
|
86
|
+
*
|
|
87
|
+
* To stop from inside the handler, call
|
|
88
|
+
* {@link CloudBrowser.stopNetworkCapture} instead: stop awaits the reader,
|
|
89
|
+
* which cannot finish while the handler it called is still running.
|
|
90
|
+
*
|
|
91
|
+
* @throws UNKNOWN_ERROR - the capture could not be disarmed; the local reader
|
|
92
|
+
* is shut down regardless
|
|
93
|
+
*/
|
|
94
|
+
async stop() {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return;
|
|
97
|
+
this.stopped = true;
|
|
98
|
+
try {
|
|
99
|
+
if (this.armed)
|
|
100
|
+
await this.disarm();
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.abort.abort();
|
|
104
|
+
await this.finished;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ScriptEvent as ScriptEventMessage, ScriptLogEntry as ScriptLogEntryMessage } from "./gen/wrc_pb.ts";
|
|
2
|
+
/** One `console.*` call from a script. */
|
|
3
|
+
export interface ScriptLogEntry {
|
|
4
|
+
/** `"info"`, `"warning"` or `"error"`, from console.log / .warn / .error. */
|
|
5
|
+
level: string;
|
|
6
|
+
/** The logged arguments, already stringified the way console does it. */
|
|
7
|
+
message: string;
|
|
8
|
+
/** When the script printed the line, stamped in the browser. */
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}
|
|
11
|
+
/** The outcome of a blocking {@link CloudBrowser.runScript}. */
|
|
12
|
+
export interface ScriptResult {
|
|
13
|
+
/**
|
|
14
|
+
* False when the script failed to compile or threw; `result` then holds the
|
|
15
|
+
* message.
|
|
16
|
+
*/
|
|
17
|
+
success: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The return value as JSON, or `"undefined"` when the script returned
|
|
20
|
+
* nothing. On failure it is the error message.
|
|
21
|
+
*/
|
|
22
|
+
result: string;
|
|
23
|
+
/**
|
|
24
|
+
* Names the run. It arrives with the reply, so it is only useful after the
|
|
25
|
+
* fact — to match up log lines a separate follower already saw.
|
|
26
|
+
*/
|
|
27
|
+
runId: string;
|
|
28
|
+
/** Everything the script printed, in order. */
|
|
29
|
+
log: ScriptLogEntry[];
|
|
30
|
+
/**
|
|
31
|
+
* True when the script printed more than the reply holds, in which case `log`
|
|
32
|
+
* is the tail of the output rather than all of it.
|
|
33
|
+
*/
|
|
34
|
+
truncated: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** How a run ended. */
|
|
37
|
+
export interface ScriptFinished {
|
|
38
|
+
/**
|
|
39
|
+
* False when the script failed to compile or threw; `result` then holds the
|
|
40
|
+
* message.
|
|
41
|
+
*/
|
|
42
|
+
success: boolean;
|
|
43
|
+
/** The return value as JSON, or the error message. */
|
|
44
|
+
result: string;
|
|
45
|
+
/**
|
|
46
|
+
* True when the run was cancelled, or the session went away under it, rather
|
|
47
|
+
* than the script returning on its own.
|
|
48
|
+
*/
|
|
49
|
+
stopped: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** One run still in flight, as {@link CloudBrowser.listScriptRuns} reports it. */
|
|
52
|
+
export interface ScriptRunInfo {
|
|
53
|
+
runId: string;
|
|
54
|
+
/** How long the run has been going, in milliseconds. */
|
|
55
|
+
runningMs: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One item on a session's script event stream. Exactly one of `log` and
|
|
59
|
+
* `finished` is set.
|
|
60
|
+
*/
|
|
61
|
+
export interface ScriptEvent {
|
|
62
|
+
/** The run that produced this event. */
|
|
63
|
+
runId: string;
|
|
64
|
+
/** A console line the script printed. */
|
|
65
|
+
log?: ScriptLogEntry;
|
|
66
|
+
/** The end of the run. No further event for that run follows. */
|
|
67
|
+
finished?: ScriptFinished;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* ScriptEventHandler is called once per script event.
|
|
71
|
+
*
|
|
72
|
+
* Calls are sequential and in the order the browser produced them, so a run's
|
|
73
|
+
* last log line always arrives before its `finished`.
|
|
74
|
+
*
|
|
75
|
+
* Blocking here stalls delivery: the server buffers a bounded number of events
|
|
76
|
+
* per reader and then drops its oldest, which {@link ScriptRun.dropped}
|
|
77
|
+
* reports. Hand slow work to a queue of your own instead of awaiting it here.
|
|
78
|
+
*/
|
|
79
|
+
export type ScriptEventHandler = (event: ScriptEvent) => void;
|
|
80
|
+
/** @internal Converts one log entry off the wire. */
|
|
81
|
+
export declare function scriptLogEntryFromProto(e: ScriptLogEntryMessage): ScriptLogEntry;
|
|
82
|
+
/**
|
|
83
|
+
* @internal Converts one stream item, or returns null for an event carrying
|
|
84
|
+
* neither variant — which a newer server could send and an older client should
|
|
85
|
+
* ignore rather than report as an empty log line.
|
|
86
|
+
*/
|
|
87
|
+
export declare function scriptEventFromProto(event: ScriptEventMessage): ScriptEvent | null;
|
|
88
|
+
/**
|
|
89
|
+
* ScriptRun is a script running in the background, returned by
|
|
90
|
+
* {@link CloudBrowser.startScript}. Its output is delivered to the handler
|
|
91
|
+
* passed there; this handle exists to await the outcome and to cancel the run.
|
|
92
|
+
*/
|
|
93
|
+
export declare class ScriptRun {
|
|
94
|
+
private readonly abort;
|
|
95
|
+
private readonly cancel;
|
|
96
|
+
private readonly finished;
|
|
97
|
+
private readonly id;
|
|
98
|
+
private ended;
|
|
99
|
+
private readonly endedPromise;
|
|
100
|
+
private detached;
|
|
101
|
+
private failure;
|
|
102
|
+
private droppedCount;
|
|
103
|
+
private result;
|
|
104
|
+
/** @internal Constructed by CloudBrowser; not part of the public API. */
|
|
105
|
+
constructor(init: {
|
|
106
|
+
runId: string;
|
|
107
|
+
stream: AsyncIterable<ScriptEventMessage>;
|
|
108
|
+
onEvent: ScriptEventHandler;
|
|
109
|
+
cancel: (runId: string) => Promise<unknown>;
|
|
110
|
+
abort: AbortController;
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* The id the browser gave this run. Pass it to
|
|
114
|
+
* {@link CloudBrowser.stopScripts} to cancel the run from elsewhere, or to
|
|
115
|
+
* {@link CloudBrowser.followScript} to watch it from another tab.
|
|
116
|
+
*/
|
|
117
|
+
get runId(): string;
|
|
118
|
+
/**
|
|
119
|
+
* Reads the stream and calls the handler for each event belonging to this
|
|
120
|
+
* run. Filtering happens here rather than server-side because the
|
|
121
|
+
* subscription had to exist before the run id did.
|
|
122
|
+
*
|
|
123
|
+
* It never rejects: a failure is recorded for {@link ScriptRun.error}
|
|
124
|
+
* instead, so nothing surfaces as an unhandled rejection.
|
|
125
|
+
*/
|
|
126
|
+
private pump;
|
|
127
|
+
/**
|
|
128
|
+
* Why the stream ended: null while it is still open, and after a clean stop
|
|
129
|
+
* or the session ending normally.
|
|
130
|
+
*/
|
|
131
|
+
get error(): Error | null;
|
|
132
|
+
/**
|
|
133
|
+
* How many events the server discarded because this reader fell behind.
|
|
134
|
+
* Anything above zero means the log has holes: make the handler cheaper, or
|
|
135
|
+
* have the script print less.
|
|
136
|
+
*/
|
|
137
|
+
get dropped(): number;
|
|
138
|
+
/**
|
|
139
|
+
* Resolves once the run ends, with how it ended.
|
|
140
|
+
*
|
|
141
|
+
* A script that threw is an outcome, not an error: it resolves with
|
|
142
|
+
* `success: false`. It rejects when the run's fate is unknown — the stream
|
|
143
|
+
* broke or the session died before the script finished.
|
|
144
|
+
*
|
|
145
|
+
* @returns how the script ended
|
|
146
|
+
*
|
|
147
|
+
* @throws UNKNOWN_ERROR - the outcome could not be observed
|
|
148
|
+
*/
|
|
149
|
+
wait(): Promise<ScriptFinished>;
|
|
150
|
+
/**
|
|
151
|
+
* Cancels the run and detaches this reader. Idempotent, and safe to call from
|
|
152
|
+
* a `finally`.
|
|
153
|
+
*
|
|
154
|
+
* A script that is executing is interrupted; one parked on an await unwinds
|
|
155
|
+
* at its next operation in the page. Either way the handler sees a `finished`
|
|
156
|
+
* with `stopped` set, unless the local reader is torn down first.
|
|
157
|
+
*
|
|
158
|
+
* @throws UNKNOWN_ERROR - the run could not be cancelled server-side; the
|
|
159
|
+
* local reader is detached regardless
|
|
160
|
+
*/
|
|
161
|
+
stop(): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Stops reading this run's output without cancelling the run. The script
|
|
164
|
+
* keeps going with nobody watching, which is what makes a detached run
|
|
165
|
+
* outlive the page that started it.
|
|
166
|
+
*/
|
|
167
|
+
detach(): Promise<void>;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* ScriptFollow is a read-only view of script output, returned by
|
|
171
|
+
* {@link CloudBrowser.followScript}.
|
|
172
|
+
*/
|
|
173
|
+
export declare class ScriptFollow {
|
|
174
|
+
private readonly abort;
|
|
175
|
+
private readonly finished;
|
|
176
|
+
private stopped;
|
|
177
|
+
private failure;
|
|
178
|
+
private droppedCount;
|
|
179
|
+
/** @internal Constructed by CloudBrowser; not part of the public API. */
|
|
180
|
+
constructor(init: {
|
|
181
|
+
stream: AsyncIterable<ScriptEventMessage>;
|
|
182
|
+
onEvent: ScriptEventHandler;
|
|
183
|
+
abort: AbortController;
|
|
184
|
+
});
|
|
185
|
+
private pump;
|
|
186
|
+
/**
|
|
187
|
+
* Why the subscription ended: null while it is still open and after a clean
|
|
188
|
+
* stop.
|
|
189
|
+
*/
|
|
190
|
+
get error(): Error | null;
|
|
191
|
+
/** How many events the server discarded because this reader fell behind. */
|
|
192
|
+
get dropped(): number;
|
|
193
|
+
/**
|
|
194
|
+
* Resolves once the subscription ends — {@link ScriptFollow.stop}, a dead
|
|
195
|
+
* session or a transport failure.
|
|
196
|
+
*
|
|
197
|
+
* @throws the transport failure that ended the subscription, if any
|
|
198
|
+
*/
|
|
199
|
+
wait(): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Ends the subscription. Idempotent, and safe to call from a `finally`. It
|
|
202
|
+
* never cancels a run: other readers, and the script itself, are unaffected.
|
|
203
|
+
*/
|
|
204
|
+
stop(): Promise<void>;
|
|
205
|
+
}
|