surfsky 0.0.5 → 0.0.8
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 +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +157 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Cookies and browser state persist between leases. Handlers are not retried.
|
|
|
99
99
|
|
|
100
100
|
- [SDK API reference](https://github.com/surfskyio/surfsky-js/blob/main/docs/api.md)
|
|
101
101
|
- [REST API](https://docs.surfsky.io/api-reference)
|
|
102
|
-
- [Examples](https://github.com/surfskyio/surfsky-js/tree/main/examples): forms, tabs, retries, profiles and CDP connections.
|
|
102
|
+
- [Examples](https://github.com/surfskyio/surfsky-js/tree/main/examples): forms, tabs, retries, profiles, a closed shadow root and CDP connections.
|
|
103
103
|
|
|
104
104
|
`client.session()` exposes a WebSocket URL for external browser clients.
|
|
105
105
|
See the [Playwright](https://github.com/surfskyio/surfsky-js/blob/main/examples/playwright_connect.ts),
|
package/dist/index.d.ts
CHANGED
|
@@ -1266,7 +1266,6 @@ declare class Deadline {
|
|
|
1266
1266
|
readonly until: number;
|
|
1267
1267
|
constructor(timeout: number | null | undefined, message: string);
|
|
1268
1268
|
error(): BrowserTimeoutError;
|
|
1269
|
-
check(): void;
|
|
1270
1269
|
race<T>(promise: Promise<T>): Promise<T>;
|
|
1271
1270
|
}
|
|
1272
1271
|
declare class CapturedResponse {
|
|
@@ -1330,6 +1329,7 @@ declare class Page extends Actions {
|
|
|
1330
1329
|
/** @internal */
|
|
1331
1330
|
_send(method: string, params?: Record<string, unknown>): Promise<any>;
|
|
1332
1331
|
/** @internal */
|
|
1332
|
+
_change(): Promise<void>;
|
|
1333
1333
|
_waitReady(): Promise<void>;
|
|
1334
1334
|
/** @internal */
|
|
1335
1335
|
_setup(waiting: boolean): Promise<void>;
|
|
@@ -1370,6 +1370,12 @@ declare class Page extends Actions {
|
|
|
1370
1370
|
fill(selector: string, text: string): Promise<any>;
|
|
1371
1371
|
getAttribute(selector: string, name: string): Promise<string | null>;
|
|
1372
1372
|
count(selector: string): Promise<number>;
|
|
1373
|
+
boundingBox(selector: string): Promise<{
|
|
1374
|
+
x: number;
|
|
1375
|
+
y: number;
|
|
1376
|
+
width: number;
|
|
1377
|
+
height: number;
|
|
1378
|
+
} | null>;
|
|
1373
1379
|
selectOption(selector: string, option: string | {
|
|
1374
1380
|
value?: string;
|
|
1375
1381
|
label?: string;
|
|
@@ -1422,6 +1428,7 @@ declare class Browser extends Page implements AsyncDisposable {
|
|
|
1422
1428
|
/** @internal */ _pages: Map<string, Page>;
|
|
1423
1429
|
/** @internal aborts on close: background sleeps end early */
|
|
1424
1430
|
_signal: AbortSignal;
|
|
1431
|
+
_changed: PromiseWithResolvers<void>;
|
|
1425
1432
|
constructor(session: Session, options?: BrowserOptions);
|
|
1426
1433
|
get internalUuid(): string;
|
|
1427
1434
|
get cdp(): CDPClient;
|
|
@@ -1460,6 +1467,8 @@ declare class Browser extends Page implements AsyncDisposable {
|
|
|
1460
1467
|
/** @internal */
|
|
1461
1468
|
_onAttached(event: Record<string, any>): void;
|
|
1462
1469
|
/** @internal */
|
|
1470
|
+
_notify(): void;
|
|
1471
|
+
/** @internal */
|
|
1463
1472
|
_onDetached(event: Record<string, any>): void;
|
|
1464
1473
|
/** @internal */
|
|
1465
1474
|
_drop(page: Page): void;
|
|
@@ -1622,6 +1631,7 @@ interface RequestOptions {
|
|
|
1622
1631
|
interface SessionStartOptions extends SessionOptions {
|
|
1623
1632
|
/** A saved profile to start; a one-time session when absent. */
|
|
1624
1633
|
profileUuid?: string;
|
|
1634
|
+
signal?: AbortSignal;
|
|
1625
1635
|
}
|
|
1626
1636
|
interface BrowserStartOptions extends SessionStartOptions {
|
|
1627
1637
|
/** CDP resource types to drop: `["image", "font", "media"]` saves proxy traffic. */
|
|
@@ -1668,7 +1678,7 @@ declare class Surfsky implements AsyncDisposable {
|
|
|
1668
1678
|
/** An open `BrowserPool`. `close()` stops every browser in it. */
|
|
1669
1679
|
browsers(options?: PoolOptions): Promise<BrowserPool>;
|
|
1670
1680
|
/** `browsers()` and `pool.map()` in 1 call. */
|
|
1671
|
-
map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I>, options?: PoolOptions): Promise<PoolOutcome<I, R>[]>;
|
|
1681
|
+
map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I>, options?: PoolOptions & LeaseOptions): Promise<PoolOutcome<I, R>[]>;
|
|
1672
1682
|
/** Nothing to release with fetch; here for `await using` symmetry. */
|
|
1673
1683
|
close(): Promise<void>;
|
|
1674
1684
|
[Symbol.asyncDispose](): Promise<void>;
|
|
@@ -1698,6 +1708,9 @@ type PoolOutcome<I, R> = {
|
|
|
1698
1708
|
index: number;
|
|
1699
1709
|
error: unknown;
|
|
1700
1710
|
};
|
|
1711
|
+
interface LeaseOptions {
|
|
1712
|
+
signal?: AbortSignal;
|
|
1713
|
+
}
|
|
1701
1714
|
declare class BrowserPool implements AsyncDisposable {
|
|
1702
1715
|
#private;
|
|
1703
1716
|
readonly sessionOptions: SessionOptions;
|
|
@@ -1711,9 +1724,9 @@ declare class BrowserPool implements AsyncDisposable {
|
|
|
1711
1724
|
close(): Promise<void>;
|
|
1712
1725
|
[Symbol.asyncDispose](): Promise<void>;
|
|
1713
1726
|
/** Run `fn` on a live browser, then hand the browser back. Waits while all are busy. */
|
|
1714
|
-
lease<R>(fn: (browser: Browser) => Promise<R
|
|
1727
|
+
lease<R>(fn: (browser: Browser) => Promise<R>, { signal }?: LeaseOptions): Promise<R>;
|
|
1715
1728
|
/** Run every item through `handler` on a leased browser, `capacity` at a time. */
|
|
1716
|
-
map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I
|
|
1729
|
+
map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I>, { signal }?: LeaseOptions): Promise<PoolOutcome<I, R>[]>;
|
|
1717
1730
|
}
|
|
1718
1731
|
//#endregion
|
|
1719
1732
|
//#region src/version.d.ts
|
package/dist/index.js
CHANGED
|
@@ -860,18 +860,26 @@ const FUNCTION = /^\s*(async\s+)?(function\b|\([^()]*\)\s*=>|[\w$]+\s*=>)/;
|
|
|
860
860
|
function asXPath(s) {
|
|
861
861
|
return /^\s*(xpath=|\/\/|\.\.)/.test(s) ? s.trim().replace(/^xpath=/, "") : void 0;
|
|
862
862
|
}
|
|
863
|
-
const SELECT = `(
|
|
864
|
-
const
|
|
865
|
-
if (!el) return null;
|
|
866
|
-
const option = Array.from(el.options).find(
|
|
863
|
+
const SELECT = `function (value, label) {
|
|
864
|
+
const option = Array.from(this.options).find(
|
|
867
865
|
o => label === null ? o.value === value : o.label === label || o.text === label
|
|
868
866
|
);
|
|
869
867
|
if (!option) return false;
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
868
|
+
this.value = option.value;
|
|
869
|
+
this.dispatchEvent(new Event("input", {bubbles: true}));
|
|
870
|
+
this.dispatchEvent(new Event("change", {bubbles: true}));
|
|
873
871
|
return option.value;
|
|
874
872
|
}`;
|
|
873
|
+
const INNER_TEXT = "function () { return this.innerText }";
|
|
874
|
+
const IS_FOCUSED = "function () { return this.getRootNode().activeElement === this }";
|
|
875
|
+
const MATCHES = "function (s) { return this.nodeType === 1 && (s == null || this.matches(s)) }";
|
|
876
|
+
const WORLD_GONE = ["Cannot find context", "does not belong to the document"];
|
|
877
|
+
const worldIsGone = (err) => err instanceof CDPError && WORLD_GONE.some((text) => err.message.includes(text));
|
|
878
|
+
function remoteResult(result, what) {
|
|
879
|
+
const details = result.exceptionDetails;
|
|
880
|
+
if (details) throw new CDPError(`${what} failed: ${(details.exception ?? {}).description ?? details.text}`);
|
|
881
|
+
return result.result;
|
|
882
|
+
}
|
|
875
883
|
function cookieParam(cookie) {
|
|
876
884
|
const out = dropNulls(cookie);
|
|
877
885
|
const expires = out.expirationDate;
|
|
@@ -912,9 +920,6 @@ var Deadline = class {
|
|
|
912
920
|
error() {
|
|
913
921
|
return new BrowserTimeoutError(`${this.message} within ${this.timeout}ms`);
|
|
914
922
|
}
|
|
915
|
-
check() {
|
|
916
|
-
if (Date.now() >= this.until) throw this.error();
|
|
917
|
-
}
|
|
918
923
|
race(promise) {
|
|
919
924
|
if (this.timeout === null) return promise;
|
|
920
925
|
return withTimeout(promise, Math.max(0, this.until - Date.now()), () => this.error());
|
|
@@ -1056,17 +1061,22 @@ var Page = class extends Actions {
|
|
|
1056
1061
|
}
|
|
1057
1062
|
}
|
|
1058
1063
|
/** @internal */
|
|
1064
|
+
_change() {
|
|
1065
|
+
return Promise.race([this._browser._changed.promise, sleep(100)]);
|
|
1066
|
+
}
|
|
1059
1067
|
async _waitReady() {
|
|
1060
1068
|
await this._ready.wait();
|
|
1061
1069
|
if (this._setupError !== void 0) throw this._setupError;
|
|
1062
1070
|
}
|
|
1063
1071
|
/** @internal */
|
|
1064
1072
|
async _setup(waiting) {
|
|
1073
|
+
const patterns = this._browser._fetchPatterns;
|
|
1065
1074
|
const commands = [
|
|
1066
1075
|
["Page.enable", void 0],
|
|
1067
1076
|
["Page.setLifecycleEventsEnabled", { enabled: true }],
|
|
1068
|
-
["
|
|
1077
|
+
["Network.enable", void 0]
|
|
1069
1078
|
];
|
|
1079
|
+
if (patterns.length > 0) commands.push(["Fetch.enable", { patterns }]);
|
|
1070
1080
|
if (waiting) commands.push(["Runtime.runIfWaitingForDebugger", void 0]);
|
|
1071
1081
|
const timeout = this._browser.commandTimeout;
|
|
1072
1082
|
let posted = [];
|
|
@@ -1103,8 +1113,7 @@ var Page = class extends Actions {
|
|
|
1103
1113
|
const deadline = new Deadline(options.timeout ?? 3e4, `the page did not reach '${state}'`);
|
|
1104
1114
|
while (!this._milestones.has(milestone)) {
|
|
1105
1115
|
this._requireOpen();
|
|
1106
|
-
deadline.
|
|
1107
|
-
await sleep(100);
|
|
1116
|
+
await deadline.race(this._change());
|
|
1108
1117
|
}
|
|
1109
1118
|
}
|
|
1110
1119
|
async goto(url, options = {}) {
|
|
@@ -1156,8 +1165,7 @@ var Page = class extends Actions {
|
|
|
1156
1165
|
const deadline = new Deadline(timeout, "history did not move");
|
|
1157
1166
|
while (true) {
|
|
1158
1167
|
if ((await deadline.race(this.send("Page.getNavigationHistory"))).currentIndex === index) break;
|
|
1159
|
-
deadline.
|
|
1160
|
-
await sleep(100);
|
|
1168
|
+
await deadline.race(sleep(100));
|
|
1161
1169
|
}
|
|
1162
1170
|
return this.url();
|
|
1163
1171
|
}
|
|
@@ -1167,15 +1175,13 @@ var Page = class extends Actions {
|
|
|
1167
1175
|
/** Record responses whose URL contains a fragment. Call before navigating. */
|
|
1168
1176
|
async captureResponses(...fragments) {
|
|
1169
1177
|
if (fragments.length === 0) throw new TypeError("captureResponses needs at least one URL fragment");
|
|
1170
|
-
|
|
1178
|
+
this._requireOpen();
|
|
1171
1179
|
this._captures.push(...fragments);
|
|
1172
1180
|
}
|
|
1173
1181
|
async stopCapturing() {
|
|
1174
|
-
if (this._captures.length === 0) return;
|
|
1175
1182
|
this._captures = [];
|
|
1176
1183
|
this._responses = [];
|
|
1177
1184
|
this._inFlight.clear();
|
|
1178
|
-
await this.send("Network.disable");
|
|
1179
1185
|
}
|
|
1180
1186
|
async waitForResponse(fragment, options = {}) {
|
|
1181
1187
|
if (this._captures.length === 0) throw new Error("call captureResponses() before the navigation");
|
|
@@ -1184,13 +1190,13 @@ var Page = class extends Actions {
|
|
|
1184
1190
|
const found = this._responses.find((response) => response.url.includes(fragment));
|
|
1185
1191
|
if (found !== void 0) return found;
|
|
1186
1192
|
this._requireOpen();
|
|
1187
|
-
deadline.
|
|
1188
|
-
await sleep(100);
|
|
1193
|
+
await deadline.race(this._change());
|
|
1189
1194
|
}
|
|
1190
1195
|
}
|
|
1191
1196
|
/** @internal */
|
|
1192
1197
|
_onResponse(event) {
|
|
1193
1198
|
const response = isRecord(event.response) ? event.response : {};
|
|
1199
|
+
if (event.type === "Document" && event.frameId === this._frameId) this._status = typeof response.status === "number" ? response.status : void 0;
|
|
1194
1200
|
const url = typeof response.url === "string" ? response.url : "";
|
|
1195
1201
|
if (this._captures.some((fragment) => url.includes(fragment))) this._inFlight.set(String(event.requestId), response);
|
|
1196
1202
|
}
|
|
@@ -1212,6 +1218,7 @@ var Page = class extends Actions {
|
|
|
1212
1218
|
this._browser.logger.debug(`no body for ${String(response.url)}: ${String(err)}`);
|
|
1213
1219
|
}
|
|
1214
1220
|
into.push(new CapturedResponse(typeof response.url === "string" ? response.url : "", typeof response.status === "number" ? response.status : 0, isRecord(response.headers) ? response.headers : {}, body));
|
|
1221
|
+
this._browser._notify();
|
|
1215
1222
|
}
|
|
1216
1223
|
localStorage() {
|
|
1217
1224
|
return this.#storage(true);
|
|
@@ -1279,14 +1286,38 @@ var Page = class extends Actions {
|
|
|
1279
1286
|
try {
|
|
1280
1287
|
result = await this.send("Runtime.evaluate", params);
|
|
1281
1288
|
} catch (err) {
|
|
1282
|
-
if (!isolated || !(err
|
|
1289
|
+
if (!isolated || !worldIsGone(err)) throw err;
|
|
1283
1290
|
this._worldId = void 0;
|
|
1284
1291
|
params.contextId = await this.#world();
|
|
1285
1292
|
result = await this.send("Runtime.evaluate", params);
|
|
1286
1293
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1294
|
+
return remoteResult(result, "evaluate");
|
|
1295
|
+
}
|
|
1296
|
+
async #call(nodeId, fn, ...args) {
|
|
1297
|
+
const params = {
|
|
1298
|
+
functionDeclaration: fn,
|
|
1299
|
+
arguments: args.map((value) => ({ value })),
|
|
1300
|
+
returnByValue: true
|
|
1301
|
+
};
|
|
1302
|
+
let result;
|
|
1303
|
+
try {
|
|
1304
|
+
result = await this.#callInWorld(nodeId, params);
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
if (!worldIsGone(err)) throw err;
|
|
1307
|
+
this._worldId = void 0;
|
|
1308
|
+
result = await this.#callInWorld(nodeId, params);
|
|
1309
|
+
}
|
|
1310
|
+
return remoteResult(result, "call").value;
|
|
1311
|
+
}
|
|
1312
|
+
async #callInWorld(nodeId, params) {
|
|
1313
|
+
const resolved = await this.send("DOM.resolveNode", {
|
|
1314
|
+
nodeId,
|
|
1315
|
+
executionContextId: await this.#world()
|
|
1316
|
+
});
|
|
1317
|
+
return await this.send("Runtime.callFunctionOn", {
|
|
1318
|
+
...params,
|
|
1319
|
+
objectId: resolved.object.objectId
|
|
1320
|
+
});
|
|
1290
1321
|
}
|
|
1291
1322
|
async #world() {
|
|
1292
1323
|
if (this._worldId === void 0) {
|
|
@@ -1305,12 +1336,13 @@ var Page = class extends Actions {
|
|
|
1305
1336
|
while (true) {
|
|
1306
1337
|
const value = await deadline.race(this.evaluate(expression, rest));
|
|
1307
1338
|
if (value) return value;
|
|
1308
|
-
deadline.
|
|
1309
|
-
await sleep(100);
|
|
1339
|
+
await deadline.race(sleep(100));
|
|
1310
1340
|
}
|
|
1311
1341
|
}
|
|
1312
|
-
innerText(selector) {
|
|
1313
|
-
|
|
1342
|
+
async innerText(selector) {
|
|
1343
|
+
const nodeId = await this.#nodeId(selector);
|
|
1344
|
+
if (nodeId === void 0) return null;
|
|
1345
|
+
return await this.#call(nodeId, INNER_TEXT);
|
|
1314
1346
|
}
|
|
1315
1347
|
allInnerTexts(selector) {
|
|
1316
1348
|
return this.evaluate("s => Array.from(document.querySelectorAll(s), e => e.innerText)", { args: [selector] });
|
|
@@ -1327,7 +1359,8 @@ var Page = class extends Actions {
|
|
|
1327
1359
|
return this.keyboard.type(text);
|
|
1328
1360
|
}
|
|
1329
1361
|
async #validateFocus(selector) {
|
|
1330
|
-
|
|
1362
|
+
const nodeId = await this.#nodeId(selector);
|
|
1363
|
+
if (nodeId === void 0 || !await this.#call(nodeId, IS_FOCUSED)) throw new Error(`'${selector}' did not take focus`);
|
|
1331
1364
|
}
|
|
1332
1365
|
async getAttribute(selector, name) {
|
|
1333
1366
|
const nodeId = await this.#nodeId(selector);
|
|
@@ -1338,22 +1371,38 @@ var Page = class extends Actions {
|
|
|
1338
1371
|
}
|
|
1339
1372
|
async count(selector) {
|
|
1340
1373
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
return ((await this.send("DOM.querySelectorAll", {
|
|
1374
|
+
if (asXPath(selector) !== void 0) return (await this.#search(selector)).length;
|
|
1375
|
+
const nodeIds = (await this.send("DOM.querySelectorAll", {
|
|
1344
1376
|
nodeId: document.root.nodeId,
|
|
1345
1377
|
selector
|
|
1346
|
-
})).nodeIds ?? []
|
|
1378
|
+
})).nodeIds ?? [];
|
|
1379
|
+
if (nodeIds.length > 0) return nodeIds.length;
|
|
1380
|
+
return (await this.#search(selector)).length;
|
|
1381
|
+
}
|
|
1382
|
+
async boundingBox(selector) {
|
|
1383
|
+
const nodeId = await this.#nodeId(selector);
|
|
1384
|
+
if (nodeId === void 0) return null;
|
|
1385
|
+
let box;
|
|
1386
|
+
try {
|
|
1387
|
+
box = await this.send("DOM.getBoxModel", { nodeId });
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
if (err instanceof CDPError) return null;
|
|
1390
|
+
throw err;
|
|
1391
|
+
}
|
|
1392
|
+
const quad = box.model.content;
|
|
1393
|
+
return {
|
|
1394
|
+
x: quad[0],
|
|
1395
|
+
y: quad[1],
|
|
1396
|
+
width: quad[2] - quad[0],
|
|
1397
|
+
height: quad[5] - quad[1]
|
|
1398
|
+
};
|
|
1347
1399
|
}
|
|
1348
1400
|
async selectOption(selector, option) {
|
|
1349
1401
|
const { value, label } = typeof option === "string" ? { value: option } : option;
|
|
1350
1402
|
if (value === void 0 === (label === void 0)) throw new TypeError("selectOption takes either a value or a label");
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
label ?? null
|
|
1355
|
-
] });
|
|
1356
|
-
if (picked === null) throw new Error(`nothing matches '${selector}'`);
|
|
1403
|
+
const nodeId = await this.#nodeId(selector);
|
|
1404
|
+
if (nodeId === void 0) throw new Error(`nothing matches '${selector}'`);
|
|
1405
|
+
const picked = await this.#call(nodeId, SELECT, value ?? null, label ?? null);
|
|
1357
1406
|
if (picked === false) throw new Error(`'${selector}' has no option '${label ?? value}'`);
|
|
1358
1407
|
return picked;
|
|
1359
1408
|
}
|
|
@@ -1361,10 +1410,7 @@ var Page = class extends Actions {
|
|
|
1361
1410
|
const visible = options.visible ?? true;
|
|
1362
1411
|
const state = visible ? "visible" : "in the document";
|
|
1363
1412
|
const deadline = new Deadline(options.timeout ?? 3e4, `'${selector}' was not ${state}`);
|
|
1364
|
-
while (!await deadline.race(this.#matches(selector, visible)))
|
|
1365
|
-
deadline.check();
|
|
1366
|
-
await sleep(100);
|
|
1367
|
-
}
|
|
1413
|
+
while (!await deadline.race(this.#matches(selector, visible))) await deadline.race(sleep(100));
|
|
1368
1414
|
}
|
|
1369
1415
|
async #matches(selector, visible) {
|
|
1370
1416
|
let box;
|
|
@@ -1382,22 +1428,34 @@ var Page = class extends Actions {
|
|
|
1382
1428
|
}
|
|
1383
1429
|
async #nodeId(selector) {
|
|
1384
1430
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
return (await this.send("DOM.querySelector", {
|
|
1431
|
+
if (asXPath(selector) !== void 0) return (await this.#search(selector))[0];
|
|
1432
|
+
const found = await this.send("DOM.querySelector", {
|
|
1388
1433
|
nodeId: document.root.nodeId,
|
|
1389
1434
|
selector
|
|
1390
|
-
})
|
|
1435
|
+
});
|
|
1436
|
+
if (found.nodeId) return found.nodeId;
|
|
1437
|
+
return (await this.#search(selector))[0];
|
|
1391
1438
|
}
|
|
1392
|
-
async #search(
|
|
1393
|
-
const
|
|
1439
|
+
async #search(selector) {
|
|
1440
|
+
const xpath = asXPath(selector);
|
|
1441
|
+
const { searchId, resultCount = 0 } = await this.send("DOM.performSearch", {
|
|
1442
|
+
query: xpath ?? selector,
|
|
1443
|
+
includeUserAgentShadowDOM: false
|
|
1444
|
+
});
|
|
1394
1445
|
try {
|
|
1395
1446
|
if (!resultCount) return [];
|
|
1396
|
-
|
|
1447
|
+
const nodeIds = (await this.send("DOM.getSearchResults", {
|
|
1397
1448
|
searchId,
|
|
1398
1449
|
fromIndex: 0,
|
|
1399
1450
|
toIndex: resultCount
|
|
1400
1451
|
})).nodeIds ?? [];
|
|
1452
|
+
const css = xpath === void 0 ? selector : null;
|
|
1453
|
+
await this.#world();
|
|
1454
|
+
const kept = await Promise.all(nodeIds.map((nodeId) => this.#call(nodeId, MATCHES, css).catch((err) => {
|
|
1455
|
+
if (err instanceof CDPError) return false;
|
|
1456
|
+
throw err;
|
|
1457
|
+
})));
|
|
1458
|
+
return nodeIds.filter((_, i) => kept[i]);
|
|
1401
1459
|
} finally {
|
|
1402
1460
|
await this.send("DOM.discardSearchResults", { searchId }).catch(() => void 0);
|
|
1403
1461
|
}
|
|
@@ -1408,8 +1466,7 @@ var Page = class extends Actions {
|
|
|
1408
1466
|
while (true) {
|
|
1409
1467
|
const url = await deadline.race(this.url());
|
|
1410
1468
|
if (url.includes(fragment)) return url;
|
|
1411
|
-
deadline.
|
|
1412
|
-
await sleep(100);
|
|
1469
|
+
await deadline.race(sleep(100));
|
|
1413
1470
|
}
|
|
1414
1471
|
}
|
|
1415
1472
|
/** The first match has a bounding box. */
|
|
@@ -1487,6 +1544,7 @@ var Page = class extends Actions {
|
|
|
1487
1544
|
this._loaderId = loaderId;
|
|
1488
1545
|
this._milestones = /* @__PURE__ */ new Set([name]);
|
|
1489
1546
|
}
|
|
1547
|
+
this._browser._notify();
|
|
1490
1548
|
this._waiter?.observe(name, loaderId);
|
|
1491
1549
|
}
|
|
1492
1550
|
/** @internal */
|
|
@@ -1520,12 +1578,7 @@ var Page = class extends Actions {
|
|
|
1520
1578
|
this._browser.logger.warn("Fetch.requestPaused without a requestId");
|
|
1521
1579
|
return;
|
|
1522
1580
|
}
|
|
1523
|
-
|
|
1524
|
-
if (status !== void 0 && status !== null) {
|
|
1525
|
-
if (event.frameId === this._frameId) this._status = status;
|
|
1526
|
-
this._spawn(this._send("Fetch.continueResponse", { requestId }));
|
|
1527
|
-
} else if (event.responseErrorReason) this._spawn(this._send("Fetch.continueRequest", { requestId }));
|
|
1528
|
-
else this._spawn(this._send("Fetch.failRequest", {
|
|
1581
|
+
this._spawn(this._send("Fetch.failRequest", {
|
|
1529
1582
|
requestId,
|
|
1530
1583
|
errorReason: "BlockedByClient"
|
|
1531
1584
|
}));
|
|
@@ -1537,11 +1590,6 @@ var Page = class extends Actions {
|
|
|
1537
1590
|
};
|
|
1538
1591
|
//#endregion
|
|
1539
1592
|
//#region src/browser/browser.ts
|
|
1540
|
-
const STATUS_PATTERN = {
|
|
1541
|
-
urlPattern: "*",
|
|
1542
|
-
resourceType: "Document",
|
|
1543
|
-
requestStage: "Response"
|
|
1544
|
-
};
|
|
1545
1593
|
const AUTO_ATTACH = {
|
|
1546
1594
|
autoAttach: true,
|
|
1547
1595
|
waitForDebuggerOnStart: true,
|
|
@@ -1609,6 +1657,7 @@ var Browser = class extends Page {
|
|
|
1609
1657
|
#client;
|
|
1610
1658
|
#abort = new AbortController();
|
|
1611
1659
|
#pending = /* @__PURE__ */ new Set();
|
|
1660
|
+
_changed = Promise.withResolvers();
|
|
1612
1661
|
#closing;
|
|
1613
1662
|
#retired = false;
|
|
1614
1663
|
#useCount = 0;
|
|
@@ -1659,7 +1708,7 @@ var Browser = class extends Page {
|
|
|
1659
1708
|
resourceType: RESOURCE_TYPES[name]
|
|
1660
1709
|
}));
|
|
1661
1710
|
for (const pattern of this.blockedUrls) patterns.push({ urlPattern: pattern });
|
|
1662
|
-
return
|
|
1711
|
+
return patterns;
|
|
1663
1712
|
}
|
|
1664
1713
|
/** Replace this browser with a fresh one after the lease. */
|
|
1665
1714
|
retire() {
|
|
@@ -1690,8 +1739,7 @@ var Browser = class extends Page {
|
|
|
1690
1739
|
const last = opened[opened.length - 1];
|
|
1691
1740
|
if (last !== void 0) return last;
|
|
1692
1741
|
this._requireOpen();
|
|
1693
|
-
deadline.
|
|
1694
|
-
await sleep(100);
|
|
1742
|
+
await deadline.race(this._change());
|
|
1695
1743
|
}
|
|
1696
1744
|
}
|
|
1697
1745
|
async connect() {
|
|
@@ -1763,8 +1811,7 @@ var Browser = class extends Page {
|
|
|
1763
1811
|
let page = this.#pageFor(targetId);
|
|
1764
1812
|
while (page === void 0) {
|
|
1765
1813
|
this._requireOpen();
|
|
1766
|
-
deadline.
|
|
1767
|
-
await sleep(100);
|
|
1814
|
+
await deadline.race(this._change());
|
|
1768
1815
|
page = this.#pageFor(targetId);
|
|
1769
1816
|
}
|
|
1770
1817
|
await deadline.race(page._waitReady());
|
|
@@ -1794,8 +1841,14 @@ var Browser = class extends Page {
|
|
|
1794
1841
|
page._frameId = page._targetId;
|
|
1795
1842
|
page._ready = new Flag();
|
|
1796
1843
|
this._pages.set(sessionId, page);
|
|
1844
|
+
this._notify();
|
|
1797
1845
|
this._spawn(page._setup(waiting));
|
|
1798
1846
|
}
|
|
1847
|
+
/** @internal */
|
|
1848
|
+
_notify() {
|
|
1849
|
+
this._changed.resolve();
|
|
1850
|
+
this._changed = Promise.withResolvers();
|
|
1851
|
+
}
|
|
1799
1852
|
async #letGo(sessionId, waiting) {
|
|
1800
1853
|
const timeout = this.commandTimeout;
|
|
1801
1854
|
const detach = async () => {
|
|
@@ -1813,6 +1866,7 @@ var Browser = class extends Page {
|
|
|
1813
1866
|
_drop(page) {
|
|
1814
1867
|
this._pages.delete(page._sessionId);
|
|
1815
1868
|
page._closed = true;
|
|
1869
|
+
this._notify();
|
|
1816
1870
|
page._waiter?.fail("page closed");
|
|
1817
1871
|
if (page === this) {
|
|
1818
1872
|
this.logger.warn(`page target gone for ${this.internalUuid}`);
|
|
@@ -1831,6 +1885,7 @@ var Browser = class extends Page {
|
|
|
1831
1885
|
page._closed = true;
|
|
1832
1886
|
page._waiter?.fail("CDP connection closed");
|
|
1833
1887
|
}
|
|
1888
|
+
this._notify();
|
|
1834
1889
|
}
|
|
1835
1890
|
/** @internal track a background task; its failure is logged at debug level */
|
|
1836
1891
|
_spawn(task) {
|
|
@@ -2176,7 +2231,6 @@ var BrowserPool = class {
|
|
|
2176
2231
|
#idle = [];
|
|
2177
2232
|
#owned = 0;
|
|
2178
2233
|
#planFull = false;
|
|
2179
|
-
#planFullError;
|
|
2180
2234
|
#waiters = [];
|
|
2181
2235
|
#capacity = 0;
|
|
2182
2236
|
#slots;
|
|
@@ -2209,7 +2263,6 @@ var BrowserPool = class {
|
|
|
2209
2263
|
this.#capacity = Math.max(1, total);
|
|
2210
2264
|
this.#slots = new Semaphore(this.#capacity);
|
|
2211
2265
|
this.#planFull = false;
|
|
2212
|
-
this.#planFullError = void 0;
|
|
2213
2266
|
this.#owned = 0;
|
|
2214
2267
|
return this;
|
|
2215
2268
|
}
|
|
@@ -2225,14 +2278,17 @@ var BrowserPool = class {
|
|
|
2225
2278
|
return this.close();
|
|
2226
2279
|
}
|
|
2227
2280
|
/** Run `fn` on a live browser, then hand the browser back. Waits while all are busy. */
|
|
2228
|
-
async lease(fn) {
|
|
2281
|
+
async lease(fn, { signal } = {}) {
|
|
2282
|
+
signal?.throwIfAborted();
|
|
2229
2283
|
const slots = this.#requireOpen();
|
|
2230
2284
|
await slots.acquire();
|
|
2231
2285
|
try {
|
|
2232
2286
|
if (this.#slots !== slots) throw new Error("the pool was closed while waiting for a slot");
|
|
2287
|
+
signal?.throwIfAborted();
|
|
2233
2288
|
const browser = await this.#acquire();
|
|
2234
2289
|
browser._lease();
|
|
2235
2290
|
try {
|
|
2291
|
+
signal?.throwIfAborted();
|
|
2236
2292
|
return await fn(browser);
|
|
2237
2293
|
} finally {
|
|
2238
2294
|
await this.#release(browser);
|
|
@@ -2263,7 +2319,6 @@ var BrowserPool = class {
|
|
|
2263
2319
|
const idle = this.#idle.pop();
|
|
2264
2320
|
if (idle !== void 0) return idle;
|
|
2265
2321
|
if (this.#planFull) {
|
|
2266
|
-
if (this.#owned === 0) throw this.#planFullError ?? new RateLimitError("parallel browser limit reached", { statusCode: 429 });
|
|
2267
2322
|
await this.#wait();
|
|
2268
2323
|
continue;
|
|
2269
2324
|
}
|
|
@@ -2272,15 +2327,10 @@ var BrowserPool = class {
|
|
|
2272
2327
|
try {
|
|
2273
2328
|
browser = await this.#startBrowser();
|
|
2274
2329
|
} catch (err) {
|
|
2275
|
-
const full = planIsFull(err);
|
|
2276
2330
|
this.#owned -= 1;
|
|
2277
|
-
|
|
2278
|
-
this.#planFull = true;
|
|
2279
|
-
this.#planFullError = err;
|
|
2280
|
-
}
|
|
2281
|
-
const nothingLeft = this.#owned === 0;
|
|
2331
|
+
this.#planFull = planIsFull(err) && this.#owned > 0;
|
|
2282
2332
|
this.#notifyAll();
|
|
2283
|
-
if (!
|
|
2333
|
+
if (!this.#planFull) throw err;
|
|
2284
2334
|
this.#client.logger.info("plan is full, waiting for a browser of ours");
|
|
2285
2335
|
continue;
|
|
2286
2336
|
}
|
|
@@ -2304,20 +2354,20 @@ var BrowserPool = class {
|
|
|
2304
2354
|
this.#notifyAll();
|
|
2305
2355
|
}
|
|
2306
2356
|
/** Run every item through `handler` on a leased browser, `capacity` at a time. */
|
|
2307
|
-
async map(handler, items) {
|
|
2357
|
+
async map(handler, items, { signal } = {}) {
|
|
2308
2358
|
const work = items[Symbol.iterator]();
|
|
2309
2359
|
const outcomes = [];
|
|
2310
2360
|
let index = 0;
|
|
2311
2361
|
let stopped = false;
|
|
2312
2362
|
const worker = async () => {
|
|
2313
|
-
while (!stopped) {
|
|
2363
|
+
while (!stopped && !signal?.aborted) {
|
|
2314
2364
|
const next = work.next();
|
|
2315
2365
|
if (next.done) return;
|
|
2316
2366
|
const item = next.value;
|
|
2317
2367
|
const at = index;
|
|
2318
2368
|
index += 1;
|
|
2319
2369
|
try {
|
|
2320
|
-
const value = await this.lease((browser) => handler(browser, item));
|
|
2370
|
+
const value = await this.lease((browser) => handler(browser, item), { signal });
|
|
2321
2371
|
outcomes.push({
|
|
2322
2372
|
ok: true,
|
|
2323
2373
|
item,
|
|
@@ -2624,7 +2674,7 @@ var Proxies = class {
|
|
|
2624
2674
|
};
|
|
2625
2675
|
//#endregion
|
|
2626
2676
|
//#region src/version.ts
|
|
2627
|
-
const VERSION = "0.0.
|
|
2677
|
+
const VERSION = "0.0.8";
|
|
2628
2678
|
//#endregion
|
|
2629
2679
|
//#region src/client.ts
|
|
2630
2680
|
function userAgent() {
|
|
@@ -2723,13 +2773,18 @@ var Surfsky = class Surfsky {
|
|
|
2723
2773
|
}
|
|
2724
2774
|
/** Start a session; `stop()` it (or `await using`) so it does not keep billing. */
|
|
2725
2775
|
async session(options = {}) {
|
|
2726
|
-
const { profileUuid, ...rest } = options;
|
|
2776
|
+
const { profileUuid, signal, ...rest } = options;
|
|
2777
|
+
signal?.throwIfAborted();
|
|
2727
2778
|
const session = profileUuid === void 0 ? await this.profiles.startOneTime(rest) : await this.profiles.start(profileUuid, rest);
|
|
2728
2779
|
let stopping;
|
|
2729
2780
|
const stop = () => {
|
|
2730
2781
|
stopping ??= stopSession(this, session.internal_uuid);
|
|
2731
2782
|
return stopping;
|
|
2732
2783
|
};
|
|
2784
|
+
if (signal?.aborted) {
|
|
2785
|
+
await stop();
|
|
2786
|
+
throw signal.reason;
|
|
2787
|
+
}
|
|
2733
2788
|
return {
|
|
2734
2789
|
...session,
|
|
2735
2790
|
stop,
|
|
@@ -2738,11 +2793,12 @@ var Surfsky = class Surfsky {
|
|
|
2738
2793
|
}
|
|
2739
2794
|
/** Start a session and connect a `Browser` to it. `close()` stops the session. */
|
|
2740
2795
|
async browser(options = {}) {
|
|
2741
|
-
const { profileUuid, blockResources, blockUrls, connectTimeout, commandTimeout, createWebSocket, ...sessionOptions } = options;
|
|
2796
|
+
const { profileUuid, signal, blockResources, blockUrls, connectTimeout, commandTimeout, createWebSocket, ...sessionOptions } = options;
|
|
2742
2797
|
const blocked = normalizeBlocked(blockResources);
|
|
2743
2798
|
const urls = normalizeUrls(blockUrls);
|
|
2744
2799
|
const session = await this.session({
|
|
2745
2800
|
profileUuid,
|
|
2801
|
+
signal,
|
|
2746
2802
|
...sessionOptions
|
|
2747
2803
|
});
|
|
2748
2804
|
const browser = new Browser(session, {
|
|
@@ -2754,7 +2810,19 @@ var Surfsky = class Surfsky {
|
|
|
2754
2810
|
logger: this.logger,
|
|
2755
2811
|
onClose: () => session.stop()
|
|
2756
2812
|
});
|
|
2757
|
-
|
|
2813
|
+
const onAbort = () => void browser.close();
|
|
2814
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2815
|
+
try {
|
|
2816
|
+
await browser.connect();
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
throw signal?.aborted ? signal.reason : err;
|
|
2819
|
+
} finally {
|
|
2820
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2821
|
+
}
|
|
2822
|
+
if (signal?.aborted) {
|
|
2823
|
+
await browser.close();
|
|
2824
|
+
throw signal.reason;
|
|
2825
|
+
}
|
|
2758
2826
|
return browser;
|
|
2759
2827
|
}
|
|
2760
2828
|
/** An open `BrowserPool`. `close()` stops every browser in it. */
|
|
@@ -2763,9 +2831,10 @@ var Surfsky = class Surfsky {
|
|
|
2763
2831
|
}
|
|
2764
2832
|
/** `browsers()` and `pool.map()` in 1 call. */
|
|
2765
2833
|
async map(handler, items, options = {}) {
|
|
2766
|
-
const
|
|
2834
|
+
const { signal, ...poolOptions } = options;
|
|
2835
|
+
const pool = await this.browsers(poolOptions);
|
|
2767
2836
|
try {
|
|
2768
|
-
return await pool.map(handler, items);
|
|
2837
|
+
return await pool.map(handler, items, { signal });
|
|
2769
2838
|
} finally {
|
|
2770
2839
|
await pool.close();
|
|
2771
2840
|
}
|