@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 +18 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +38 -8
- package/package.json +1 -1
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
|
-
//
|
|
113
|
-
// code from the caller.
|
|
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
|
|
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
|
-
|
|
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
|
|
605
|
+
const retryThis = (isRetryableStatus(res.status) && !(await responseIsTerminal(res))) ||
|
|
579
606
|
(isSafeToReplay(method, opts?.idempotencyKey) &&
|
|
580
607
|
(await responseSaysRetryable(res)));
|
|
581
|
-
|
|
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);
|