@solarisdk/browser 0.1.6 → 0.2.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 CHANGED
@@ -3,6 +3,9 @@
3
3
  Official client SDK for [Solari Browser](https://getsolari.com) — managed,
4
4
  stealth-hardened browsers as an API.
5
5
 
6
+ Feature coverage across all five languages is on the
7
+ [SDK feature matrix](https://docs.getsolari.com/sdk-capabilities).
8
+
6
9
  ## Install
7
10
 
8
11
  ```bash
@@ -55,6 +58,21 @@ const session = await solari.sessions.create();
55
58
  const browser = await chromium.connectOverCDP(session.cdpEndpoint);
56
59
  ```
57
60
 
61
+ ### Timeout, keep-alive and metadata
62
+
63
+ ```ts
64
+ const session = await solari.sessions.create({
65
+ sessionTimeoutMs: 600_000, // end earlier than the plan maximum, in ms (at least 60 000)
66
+ keepAlive: true, // hold the session if the client drops, until its deadline or release
67
+ userMetadata: { job: "nightly-42" }, // stored with the session
68
+ });
69
+ ```
70
+
71
+ Every duration option is in milliseconds. `sessionTimeoutMs` is rounded up to whole
72
+ seconds for the gateway (1001 ms is 2 s); the gateway refuses anything under 60 s;
73
+ zero or unset leaves the plan maximum, and a negative, `NaN`, `Infinity` or
74
+ fractional value throws before any request. `launch()` accepts the same options.
75
+
58
76
  ## Regions
59
77
 
60
78
  Solari runs in `us-west` (N. California) today, with additional regions
package/dist/index.d.ts CHANGED
@@ -88,6 +88,8 @@ export interface Session {
88
88
  /** Present only when a managed proxy was requested. Confirmation only
89
89
  * (country/tier/timezone) — egress is applied server-side. */
90
90
  proxy?: ResolvedProxyConfig;
91
+ /** Echo of the `userMetadata` sent at create; absent when none was sent. */
92
+ userMetadata?: Record<string, unknown>;
91
93
  }
92
94
  export interface Profile {
93
95
  id: string;
@@ -114,6 +116,17 @@ export interface CreateSessionOptions {
114
116
  * proxy: "off"
115
117
  */
116
118
  proxy?: string | ProxyRequest | "off" | "smart";
119
+ /** End the session earlier than the plan maximum, in milliseconds. The gateway
120
+ * counts whole seconds, so the value is rounded up (1001 ms is 2 s), and it
121
+ * refuses anything under 60 s (400). Zero/undefined leaves the plan maximum;
122
+ * a negative, NaN, Infinity or non-integer value throws. */
123
+ sessionTimeoutMs?: number;
124
+ /** Hold the session after the client disconnects, until its deadline
125
+ * (`sessionTimeoutMs`, else an hour, never past the plan maximum) or until
126
+ * it is released, so a dropped client can reconnect. */
127
+ keepAlive?: boolean;
128
+ /** Free-form JSON object stored with the session (labels, job ids, ...). */
129
+ userMetadata?: Record<string, unknown>;
117
130
  }
118
131
  export interface ReplayUrl {
119
132
  url: string;
package/dist/index.js CHANGED
@@ -60,6 +60,19 @@ const DEFAULT_TIMEOUT_MS = 90_000;
60
60
  // transient, and a client's correctness must not depend on which gateway
61
61
  // build it reaches. Inert today, and deliberately so — do not delete it as
62
62
  // dead code.
63
+ /**
64
+ * The gateway's `timeoutSeconds` for a `sessionTimeoutMs`: rounded UP to whole
65
+ * seconds (1001 -> 2). Zero/undefined means "omit". Throws for a negative, NaN,
66
+ * Infinity or non-integer value. Not clamped: the gateway still answers 400 under 60 s.
67
+ */
68
+ function sessionTimeoutSeconds(ms) {
69
+ if (ms === undefined || ms === 0)
70
+ return undefined;
71
+ if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 0) {
72
+ throw new SolariError(`Solari: sessionTimeoutMs must be a non-negative integer number of milliseconds, got ${String(ms)}`);
73
+ }
74
+ return Math.ceil(ms / 1000);
75
+ }
63
76
  function isRetryableStatus(status) {
64
77
  return status === 502 || status === 503 || status === 504 || status === 507;
65
78
  }
@@ -109,15 +122,19 @@ async function responseSaysRetryable(res) {
109
122
  return false;
110
123
  }
111
124
  }
112
- // A few 503s are answers, not outages: retrying cannot change them, and swallowing the body would hide the
113
- // code from the caller. `ProxyTierUnavailable` (explicitly requested proxy tier not servable) is one.
125
+ // Some 5xx are answers, not outages: retrying cannot change them, and swallowing the body would hide the
126
+ // code from the caller. The gateway says so itself with `retryable: false` (502 `PoolResponseUnparseable`:
127
+ // the session may already exist, so a retry creates a second one; 503 `EgressBlockPeekUnsupportedOnFlavor`).
128
+ // `ProxyTierUnavailable` (explicitly requested proxy tier not servable) is also terminal, whatever its flag.
114
129
  const TERMINAL_503_CODES = new Set(["ProxyTierUnavailable"]);
115
- async function responseIsTerminal503(res) {
116
- if (res.status !== 503)
117
- return false;
130
+ async function responseIsTerminal(res) {
118
131
  try {
119
132
  const body = (await res.clone().json());
120
- return typeof body?.code === "string" && TERMINAL_503_CODES.has(body.code);
133
+ if (body?.retryable === false)
134
+ return true;
135
+ return (res.status === 503 &&
136
+ typeof body?.code === "string" &&
137
+ TERMINAL_503_CODES.has(body.code));
121
138
  }
122
139
  catch {
123
140
  return false;
@@ -286,6 +303,13 @@ class SessionsResource {
286
303
  body.captcha = true;
287
304
  if (options?.proxy !== undefined)
288
305
  body.proxy = options.proxy;
306
+ const timeoutSeconds = sessionTimeoutSeconds(options?.sessionTimeoutMs);
307
+ if (timeoutSeconds !== undefined)
308
+ body.timeoutSeconds = timeoutSeconds;
309
+ if (options?.keepAlive === true)
310
+ body.keepAlive = true;
311
+ if (options?.userMetadata !== undefined)
312
+ body.userMetadata = options.userMetadata;
289
313
  // A create allocates a slot and launches a browser before it can answer,
290
314
  // so a response lost after that point makes a retry a SECOND session.
291
315
  // The key lets the gateway answer the retry from the first result.
@@ -327,6 +351,9 @@ class SessionsResource {
327
351
  if (data.proxy) {
328
352
  session.proxy = data.proxy;
329
353
  }
354
+ if (data.userMetadata) {
355
+ session.userMetadata = data.userMetadata;
356
+ }
330
357
  return session;
331
358
  }
332
359
  /** Fire-and-forget release. Use `releaseAndWait` for confirmation. */
@@ -575,10 +602,13 @@ export class Solari {
575
602
  clearTimeout(timer);
576
603
  if (res.ok)
577
604
  return res;
578
- const retryThis = (isRetryableStatus(res.status) && !(await responseIsTerminal503(res))) ||
605
+ const retryThis = (isRetryableStatus(res.status) && !(await responseIsTerminal(res))) ||
579
606
  (isSafeToReplay(method, opts?.idempotencyKey) &&
580
607
  (await responseSaysRetryable(res)));
581
- if (!retryThis)
608
+ // Out of attempts: hand the last response back so the caller maps its
609
+ // status and `code` like any other error, instead of a status-less
610
+ // "exhausted" error that hides why the gateway refused.
611
+ if (!retryThis || attempt === this.maxAttempts)
582
612
  return res;
583
613
  await res.text().catch(() => { });
584
614
  lastErr = new SolariError(`Solari ${method} ${path}: ${res.status}`, res.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Official client SDK for Solari Browser.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",