surfsky 0.0.6 → 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/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>;
@@ -1428,6 +1428,7 @@ declare class Browser extends Page implements AsyncDisposable {
1428
1428
  /** @internal */ _pages: Map<string, Page>;
1429
1429
  /** @internal aborts on close: background sleeps end early */
1430
1430
  _signal: AbortSignal;
1431
+ _changed: PromiseWithResolvers<void>;
1431
1432
  constructor(session: Session, options?: BrowserOptions);
1432
1433
  get internalUuid(): string;
1433
1434
  get cdp(): CDPClient;
@@ -1466,6 +1467,8 @@ declare class Browser extends Page implements AsyncDisposable {
1466
1467
  /** @internal */
1467
1468
  _onAttached(event: Record<string, any>): void;
1468
1469
  /** @internal */
1470
+ _notify(): void;
1471
+ /** @internal */
1469
1472
  _onDetached(event: Record<string, any>): void;
1470
1473
  /** @internal */
1471
1474
  _drop(page: Page): void;
@@ -1628,6 +1631,7 @@ interface RequestOptions {
1628
1631
  interface SessionStartOptions extends SessionOptions {
1629
1632
  /** A saved profile to start; a one-time session when absent. */
1630
1633
  profileUuid?: string;
1634
+ signal?: AbortSignal;
1631
1635
  }
1632
1636
  interface BrowserStartOptions extends SessionStartOptions {
1633
1637
  /** CDP resource types to drop: `["image", "font", "media"]` saves proxy traffic. */
@@ -1674,7 +1678,7 @@ declare class Surfsky implements AsyncDisposable {
1674
1678
  /** An open `BrowserPool`. `close()` stops every browser in it. */
1675
1679
  browsers(options?: PoolOptions): Promise<BrowserPool>;
1676
1680
  /** `browsers()` and `pool.map()` in 1 call. */
1677
- 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>[]>;
1678
1682
  /** Nothing to release with fetch; here for `await using` symmetry. */
1679
1683
  close(): Promise<void>;
1680
1684
  [Symbol.asyncDispose](): Promise<void>;
@@ -1704,6 +1708,9 @@ type PoolOutcome<I, R> = {
1704
1708
  index: number;
1705
1709
  error: unknown;
1706
1710
  };
1711
+ interface LeaseOptions {
1712
+ signal?: AbortSignal;
1713
+ }
1707
1714
  declare class BrowserPool implements AsyncDisposable {
1708
1715
  #private;
1709
1716
  readonly sessionOptions: SessionOptions;
@@ -1717,9 +1724,9 @@ declare class BrowserPool implements AsyncDisposable {
1717
1724
  close(): Promise<void>;
1718
1725
  [Symbol.asyncDispose](): Promise<void>;
1719
1726
  /** Run `fn` on a live browser, then hand the browser back. Waits while all are busy. */
1720
- lease<R>(fn: (browser: Browser) => Promise<R>): Promise<R>;
1727
+ lease<R>(fn: (browser: Browser) => Promise<R>, { signal }?: LeaseOptions): Promise<R>;
1721
1728
  /** Run every item through `handler` on a leased browser, `capacity` at a time. */
1722
- map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I>): Promise<PoolOutcome<I, R>[]>;
1729
+ map<I, R>(handler: PoolHandler<I, R>, items: Iterable<I>, { signal }?: LeaseOptions): Promise<PoolOutcome<I, R>[]>;
1723
1730
  }
1724
1731
  //#endregion
1725
1732
  //#region src/version.d.ts
package/dist/index.js CHANGED
@@ -920,9 +920,6 @@ var Deadline = class {
920
920
  error() {
921
921
  return new BrowserTimeoutError(`${this.message} within ${this.timeout}ms`);
922
922
  }
923
- check() {
924
- if (Date.now() >= this.until) throw this.error();
925
- }
926
923
  race(promise) {
927
924
  if (this.timeout === null) return promise;
928
925
  return withTimeout(promise, Math.max(0, this.until - Date.now()), () => this.error());
@@ -1064,6 +1061,9 @@ var Page = class extends Actions {
1064
1061
  }
1065
1062
  }
1066
1063
  /** @internal */
1064
+ _change() {
1065
+ return Promise.race([this._browser._changed.promise, sleep(100)]);
1066
+ }
1067
1067
  async _waitReady() {
1068
1068
  await this._ready.wait();
1069
1069
  if (this._setupError !== void 0) throw this._setupError;
@@ -1113,8 +1113,7 @@ var Page = class extends Actions {
1113
1113
  const deadline = new Deadline(options.timeout ?? 3e4, `the page did not reach '${state}'`);
1114
1114
  while (!this._milestones.has(milestone)) {
1115
1115
  this._requireOpen();
1116
- deadline.check();
1117
- await sleep(100);
1116
+ await deadline.race(this._change());
1118
1117
  }
1119
1118
  }
1120
1119
  async goto(url, options = {}) {
@@ -1166,8 +1165,7 @@ var Page = class extends Actions {
1166
1165
  const deadline = new Deadline(timeout, "history did not move");
1167
1166
  while (true) {
1168
1167
  if ((await deadline.race(this.send("Page.getNavigationHistory"))).currentIndex === index) break;
1169
- deadline.check();
1170
- await sleep(100);
1168
+ await deadline.race(sleep(100));
1171
1169
  }
1172
1170
  return this.url();
1173
1171
  }
@@ -1192,8 +1190,7 @@ var Page = class extends Actions {
1192
1190
  const found = this._responses.find((response) => response.url.includes(fragment));
1193
1191
  if (found !== void 0) return found;
1194
1192
  this._requireOpen();
1195
- deadline.check();
1196
- await sleep(100);
1193
+ await deadline.race(this._change());
1197
1194
  }
1198
1195
  }
1199
1196
  /** @internal */
@@ -1221,6 +1218,7 @@ var Page = class extends Actions {
1221
1218
  this._browser.logger.debug(`no body for ${String(response.url)}: ${String(err)}`);
1222
1219
  }
1223
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();
1224
1222
  }
1225
1223
  localStorage() {
1226
1224
  return this.#storage(true);
@@ -1338,8 +1336,7 @@ var Page = class extends Actions {
1338
1336
  while (true) {
1339
1337
  const value = await deadline.race(this.evaluate(expression, rest));
1340
1338
  if (value) return value;
1341
- deadline.check();
1342
- await sleep(100);
1339
+ await deadline.race(sleep(100));
1343
1340
  }
1344
1341
  }
1345
1342
  async innerText(selector) {
@@ -1413,10 +1410,7 @@ var Page = class extends Actions {
1413
1410
  const visible = options.visible ?? true;
1414
1411
  const state = visible ? "visible" : "in the document";
1415
1412
  const deadline = new Deadline(options.timeout ?? 3e4, `'${selector}' was not ${state}`);
1416
- while (!await deadline.race(this.#matches(selector, visible))) {
1417
- deadline.check();
1418
- await sleep(100);
1419
- }
1413
+ while (!await deadline.race(this.#matches(selector, visible))) await deadline.race(sleep(100));
1420
1414
  }
1421
1415
  async #matches(selector, visible) {
1422
1416
  let box;
@@ -1472,8 +1466,7 @@ var Page = class extends Actions {
1472
1466
  while (true) {
1473
1467
  const url = await deadline.race(this.url());
1474
1468
  if (url.includes(fragment)) return url;
1475
- deadline.check();
1476
- await sleep(100);
1469
+ await deadline.race(sleep(100));
1477
1470
  }
1478
1471
  }
1479
1472
  /** The first match has a bounding box. */
@@ -1551,6 +1544,7 @@ var Page = class extends Actions {
1551
1544
  this._loaderId = loaderId;
1552
1545
  this._milestones = /* @__PURE__ */ new Set([name]);
1553
1546
  }
1547
+ this._browser._notify();
1554
1548
  this._waiter?.observe(name, loaderId);
1555
1549
  }
1556
1550
  /** @internal */
@@ -1663,6 +1657,7 @@ var Browser = class extends Page {
1663
1657
  #client;
1664
1658
  #abort = new AbortController();
1665
1659
  #pending = /* @__PURE__ */ new Set();
1660
+ _changed = Promise.withResolvers();
1666
1661
  #closing;
1667
1662
  #retired = false;
1668
1663
  #useCount = 0;
@@ -1744,8 +1739,7 @@ var Browser = class extends Page {
1744
1739
  const last = opened[opened.length - 1];
1745
1740
  if (last !== void 0) return last;
1746
1741
  this._requireOpen();
1747
- deadline.check();
1748
- await sleep(100);
1742
+ await deadline.race(this._change());
1749
1743
  }
1750
1744
  }
1751
1745
  async connect() {
@@ -1817,8 +1811,7 @@ var Browser = class extends Page {
1817
1811
  let page = this.#pageFor(targetId);
1818
1812
  while (page === void 0) {
1819
1813
  this._requireOpen();
1820
- deadline.check();
1821
- await sleep(100);
1814
+ await deadline.race(this._change());
1822
1815
  page = this.#pageFor(targetId);
1823
1816
  }
1824
1817
  await deadline.race(page._waitReady());
@@ -1848,8 +1841,14 @@ var Browser = class extends Page {
1848
1841
  page._frameId = page._targetId;
1849
1842
  page._ready = new Flag();
1850
1843
  this._pages.set(sessionId, page);
1844
+ this._notify();
1851
1845
  this._spawn(page._setup(waiting));
1852
1846
  }
1847
+ /** @internal */
1848
+ _notify() {
1849
+ this._changed.resolve();
1850
+ this._changed = Promise.withResolvers();
1851
+ }
1853
1852
  async #letGo(sessionId, waiting) {
1854
1853
  const timeout = this.commandTimeout;
1855
1854
  const detach = async () => {
@@ -1867,6 +1866,7 @@ var Browser = class extends Page {
1867
1866
  _drop(page) {
1868
1867
  this._pages.delete(page._sessionId);
1869
1868
  page._closed = true;
1869
+ this._notify();
1870
1870
  page._waiter?.fail("page closed");
1871
1871
  if (page === this) {
1872
1872
  this.logger.warn(`page target gone for ${this.internalUuid}`);
@@ -1885,6 +1885,7 @@ var Browser = class extends Page {
1885
1885
  page._closed = true;
1886
1886
  page._waiter?.fail("CDP connection closed");
1887
1887
  }
1888
+ this._notify();
1888
1889
  }
1889
1890
  /** @internal track a background task; its failure is logged at debug level */
1890
1891
  _spawn(task) {
@@ -2230,7 +2231,6 @@ var BrowserPool = class {
2230
2231
  #idle = [];
2231
2232
  #owned = 0;
2232
2233
  #planFull = false;
2233
- #planFullError;
2234
2234
  #waiters = [];
2235
2235
  #capacity = 0;
2236
2236
  #slots;
@@ -2263,7 +2263,6 @@ var BrowserPool = class {
2263
2263
  this.#capacity = Math.max(1, total);
2264
2264
  this.#slots = new Semaphore(this.#capacity);
2265
2265
  this.#planFull = false;
2266
- this.#planFullError = void 0;
2267
2266
  this.#owned = 0;
2268
2267
  return this;
2269
2268
  }
@@ -2279,14 +2278,17 @@ var BrowserPool = class {
2279
2278
  return this.close();
2280
2279
  }
2281
2280
  /** Run `fn` on a live browser, then hand the browser back. Waits while all are busy. */
2282
- async lease(fn) {
2281
+ async lease(fn, { signal } = {}) {
2282
+ signal?.throwIfAborted();
2283
2283
  const slots = this.#requireOpen();
2284
2284
  await slots.acquire();
2285
2285
  try {
2286
2286
  if (this.#slots !== slots) throw new Error("the pool was closed while waiting for a slot");
2287
+ signal?.throwIfAborted();
2287
2288
  const browser = await this.#acquire();
2288
2289
  browser._lease();
2289
2290
  try {
2291
+ signal?.throwIfAborted();
2290
2292
  return await fn(browser);
2291
2293
  } finally {
2292
2294
  await this.#release(browser);
@@ -2317,7 +2319,6 @@ var BrowserPool = class {
2317
2319
  const idle = this.#idle.pop();
2318
2320
  if (idle !== void 0) return idle;
2319
2321
  if (this.#planFull) {
2320
- if (this.#owned === 0) throw this.#planFullError ?? new RateLimitError("parallel browser limit reached", { statusCode: 429 });
2321
2322
  await this.#wait();
2322
2323
  continue;
2323
2324
  }
@@ -2326,15 +2327,10 @@ var BrowserPool = class {
2326
2327
  try {
2327
2328
  browser = await this.#startBrowser();
2328
2329
  } catch (err) {
2329
- const full = planIsFull(err);
2330
2330
  this.#owned -= 1;
2331
- if (full) {
2332
- this.#planFull = true;
2333
- this.#planFullError = err;
2334
- }
2335
- const nothingLeft = this.#owned === 0;
2331
+ this.#planFull = planIsFull(err) && this.#owned > 0;
2336
2332
  this.#notifyAll();
2337
- if (!full || nothingLeft) throw err;
2333
+ if (!this.#planFull) throw err;
2338
2334
  this.#client.logger.info("plan is full, waiting for a browser of ours");
2339
2335
  continue;
2340
2336
  }
@@ -2358,20 +2354,20 @@ var BrowserPool = class {
2358
2354
  this.#notifyAll();
2359
2355
  }
2360
2356
  /** Run every item through `handler` on a leased browser, `capacity` at a time. */
2361
- async map(handler, items) {
2357
+ async map(handler, items, { signal } = {}) {
2362
2358
  const work = items[Symbol.iterator]();
2363
2359
  const outcomes = [];
2364
2360
  let index = 0;
2365
2361
  let stopped = false;
2366
2362
  const worker = async () => {
2367
- while (!stopped) {
2363
+ while (!stopped && !signal?.aborted) {
2368
2364
  const next = work.next();
2369
2365
  if (next.done) return;
2370
2366
  const item = next.value;
2371
2367
  const at = index;
2372
2368
  index += 1;
2373
2369
  try {
2374
- const value = await this.lease((browser) => handler(browser, item));
2370
+ const value = await this.lease((browser) => handler(browser, item), { signal });
2375
2371
  outcomes.push({
2376
2372
  ok: true,
2377
2373
  item,
@@ -2678,7 +2674,7 @@ var Proxies = class {
2678
2674
  };
2679
2675
  //#endregion
2680
2676
  //#region src/version.ts
2681
- const VERSION = "0.0.6";
2677
+ const VERSION = "0.0.8";
2682
2678
  //#endregion
2683
2679
  //#region src/client.ts
2684
2680
  function userAgent() {
@@ -2777,13 +2773,18 @@ var Surfsky = class Surfsky {
2777
2773
  }
2778
2774
  /** Start a session; `stop()` it (or `await using`) so it does not keep billing. */
2779
2775
  async session(options = {}) {
2780
- const { profileUuid, ...rest } = options;
2776
+ const { profileUuid, signal, ...rest } = options;
2777
+ signal?.throwIfAborted();
2781
2778
  const session = profileUuid === void 0 ? await this.profiles.startOneTime(rest) : await this.profiles.start(profileUuid, rest);
2782
2779
  let stopping;
2783
2780
  const stop = () => {
2784
2781
  stopping ??= stopSession(this, session.internal_uuid);
2785
2782
  return stopping;
2786
2783
  };
2784
+ if (signal?.aborted) {
2785
+ await stop();
2786
+ throw signal.reason;
2787
+ }
2787
2788
  return {
2788
2789
  ...session,
2789
2790
  stop,
@@ -2792,11 +2793,12 @@ var Surfsky = class Surfsky {
2792
2793
  }
2793
2794
  /** Start a session and connect a `Browser` to it. `close()` stops the session. */
2794
2795
  async browser(options = {}) {
2795
- const { profileUuid, blockResources, blockUrls, connectTimeout, commandTimeout, createWebSocket, ...sessionOptions } = options;
2796
+ const { profileUuid, signal, blockResources, blockUrls, connectTimeout, commandTimeout, createWebSocket, ...sessionOptions } = options;
2796
2797
  const blocked = normalizeBlocked(blockResources);
2797
2798
  const urls = normalizeUrls(blockUrls);
2798
2799
  const session = await this.session({
2799
2800
  profileUuid,
2801
+ signal,
2800
2802
  ...sessionOptions
2801
2803
  });
2802
2804
  const browser = new Browser(session, {
@@ -2808,7 +2810,19 @@ var Surfsky = class Surfsky {
2808
2810
  logger: this.logger,
2809
2811
  onClose: () => session.stop()
2810
2812
  });
2811
- await browser.connect();
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
+ }
2812
2826
  return browser;
2813
2827
  }
2814
2828
  /** An open `BrowserPool`. `close()` stops every browser in it. */
@@ -2817,9 +2831,10 @@ var Surfsky = class Surfsky {
2817
2831
  }
2818
2832
  /** `browsers()` and `pool.map()` in 1 call. */
2819
2833
  async map(handler, items, options = {}) {
2820
- const pool = await this.browsers(options);
2834
+ const { signal, ...poolOptions } = options;
2835
+ const pool = await this.browsers(poolOptions);
2821
2836
  try {
2822
- return await pool.map(handler, items);
2837
+ return await pool.map(handler, items, { signal });
2823
2838
  } finally {
2824
2839
  await pool.close();
2825
2840
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surfsky",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "TypeScript SDK for the Surfsky antidetect browser cloud.",
5
5
  "keywords": [
6
6
  "surfsky",