@solarisdk/browser 0.1.3 → 0.1.5
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/browser-session.d.ts +27 -4
- package/dist/browser-session.js +30 -4
- package/dist/index.d.ts +24 -7
- package/dist/index.js +151 -13
- package/package.json +3 -1
|
@@ -5,8 +5,14 @@ export declare class BrowserSession {
|
|
|
5
5
|
private readonly client;
|
|
6
6
|
readonly session: Session;
|
|
7
7
|
private readonly browser;
|
|
8
|
+
/** The session's primary context — seeded with the attached profile's
|
|
9
|
+
* storageState when there is one. See {@link context}. */
|
|
10
|
+
private readonly primaryContext?;
|
|
8
11
|
private closed;
|
|
9
|
-
constructor(client: Solari, session: Session, browser: Browser
|
|
12
|
+
constructor(client: Solari, session: Session, browser: Browser,
|
|
13
|
+
/** The session's primary context — seeded with the attached profile's
|
|
14
|
+
* storageState when there is one. See {@link context}. */
|
|
15
|
+
primaryContext?: BrowserContext | undefined);
|
|
10
16
|
/** Session id. */
|
|
11
17
|
get id(): string;
|
|
12
18
|
/** Plan-tier deadline (ISO 8601 UTC); session auto-releases at this point. */
|
|
@@ -23,11 +29,28 @@ export declare class BrowserSession {
|
|
|
23
29
|
isConnected(): boolean;
|
|
24
30
|
/** Browser version string (e.g. `"Chromium/130.0.6723.31"`). */
|
|
25
31
|
version(): string;
|
|
26
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The session's primary context — the one carrying the attached profile's
|
|
34
|
+
* cookies and localStorage. Use this (or {@link newPage}) rather than
|
|
35
|
+
* `contexts()[0]`, which is not guaranteed to be the seeded one.
|
|
36
|
+
*/
|
|
37
|
+
get context(): BrowserContext | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* All open contexts, straight from the browser.
|
|
40
|
+
*
|
|
41
|
+
* NOTE: this is NOT guaranteed to be non-empty, and `contexts()[0]` is NOT
|
|
42
|
+
* guaranteed to be the profile-seeded context — the pool only pre-creates an
|
|
43
|
+
* eager context when a proxy was requested. Prefer {@link context}.
|
|
44
|
+
*/
|
|
27
45
|
contexts(): BrowserContext[];
|
|
28
|
-
/** Open
|
|
46
|
+
/** Open an ADDITIONAL, empty browser context. This does NOT carry the
|
|
47
|
+
* attached profile — use {@link context} for that. */
|
|
29
48
|
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
|
|
30
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Open a page in the session's primary context, so an attached profile's
|
|
51
|
+
* cookies/localStorage are actually present. Falls back to a browser-level
|
|
52
|
+
* page only when there is no primary context.
|
|
53
|
+
*/
|
|
31
54
|
newPage(): Promise<Page>;
|
|
32
55
|
/** Close the browser and release the session. Idempotent. */
|
|
33
56
|
close(): Promise<void>;
|
package/dist/browser-session.js
CHANGED
|
@@ -2,11 +2,16 @@ export class BrowserSession {
|
|
|
2
2
|
client;
|
|
3
3
|
session;
|
|
4
4
|
browser;
|
|
5
|
+
primaryContext;
|
|
5
6
|
closed = false;
|
|
6
|
-
constructor(client, session, browser
|
|
7
|
+
constructor(client, session, browser,
|
|
8
|
+
/** The session's primary context — seeded with the attached profile's
|
|
9
|
+
* storageState when there is one. See {@link context}. */
|
|
10
|
+
primaryContext) {
|
|
7
11
|
this.client = client;
|
|
8
12
|
this.session = session;
|
|
9
13
|
this.browser = browser;
|
|
14
|
+
this.primaryContext = primaryContext;
|
|
10
15
|
}
|
|
11
16
|
/** Session id. */
|
|
12
17
|
get id() {
|
|
@@ -40,16 +45,37 @@ export class BrowserSession {
|
|
|
40
45
|
version() {
|
|
41
46
|
return this.browser.version();
|
|
42
47
|
}
|
|
43
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The session's primary context — the one carrying the attached profile's
|
|
50
|
+
* cookies and localStorage. Use this (or {@link newPage}) rather than
|
|
51
|
+
* `contexts()[0]`, which is not guaranteed to be the seeded one.
|
|
52
|
+
*/
|
|
53
|
+
get context() {
|
|
54
|
+
return this.primaryContext;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* All open contexts, straight from the browser.
|
|
58
|
+
*
|
|
59
|
+
* NOTE: this is NOT guaranteed to be non-empty, and `contexts()[0]` is NOT
|
|
60
|
+
* guaranteed to be the profile-seeded context — the pool only pre-creates an
|
|
61
|
+
* eager context when a proxy was requested. Prefer {@link context}.
|
|
62
|
+
*/
|
|
44
63
|
contexts() {
|
|
45
64
|
return this.browser.contexts();
|
|
46
65
|
}
|
|
47
|
-
/** Open
|
|
66
|
+
/** Open an ADDITIONAL, empty browser context. This does NOT carry the
|
|
67
|
+
* attached profile — use {@link context} for that. */
|
|
48
68
|
async newContext(options) {
|
|
49
69
|
return this.browser.newContext(options);
|
|
50
70
|
}
|
|
51
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Open a page in the session's primary context, so an attached profile's
|
|
73
|
+
* cookies/localStorage are actually present. Falls back to a browser-level
|
|
74
|
+
* page only when there is no primary context.
|
|
75
|
+
*/
|
|
52
76
|
async newPage() {
|
|
77
|
+
if (this.primaryContext)
|
|
78
|
+
return this.primaryContext.newPage();
|
|
53
79
|
return this.browser.newPage();
|
|
54
80
|
}
|
|
55
81
|
/** Close the browser and release the session. Idempotent. */
|
package/dist/index.d.ts
CHANGED
|
@@ -103,11 +103,6 @@ export interface CreateSessionOptions {
|
|
|
103
103
|
stealth?: boolean;
|
|
104
104
|
/** Enable managed captcha solving. Requires `stealth: true`. */
|
|
105
105
|
captcha?: boolean;
|
|
106
|
-
/** Opt in to Cloudflare Web Bot Auth — sign every outbound HTTP
|
|
107
|
-
* request with an Ed25519 key registered to our verified bot
|
|
108
|
-
* directory. Independent of `stealth`. Silently inert if the
|
|
109
|
-
* acquired slot has no signing key configured. */
|
|
110
|
-
webBotAuth?: boolean;
|
|
111
106
|
/** Request managed proxy egress. Requires `stealth: true`.
|
|
112
107
|
*
|
|
113
108
|
* proxy: "us"
|
|
@@ -136,6 +131,17 @@ export interface LaunchOptions extends CreateSessionOptions {
|
|
|
136
131
|
probeTimeoutMs?: number;
|
|
137
132
|
}
|
|
138
133
|
export type SolariErrorCode = "FeatureRequiresPlan" | "ConcurrencyLimitExceeded" | "PlanLimitExceeded" | "BrowserUnhealthy" | "InvalidSessionId";
|
|
134
|
+
/** Supported egress countries and residential-proxy status, from
|
|
135
|
+
* {@link Solari.proxy}.countries() (`GET /proxy/countries`). */
|
|
136
|
+
export interface ProxyCountries {
|
|
137
|
+
/** Whether RESIDENTIAL managed-proxy credentials are configured on the
|
|
138
|
+
* gateway. Reflects only the residential tier — the static/mobile tiers
|
|
139
|
+
* have separate credentials this flag does not report, so `true` is not a
|
|
140
|
+
* guarantee that `tier: "mobile"` will resolve. */
|
|
141
|
+
enabled: boolean;
|
|
142
|
+
/** Supported ISO-3166-1 alpha-2 egress countries (lowercase, sorted). */
|
|
143
|
+
countries: string[];
|
|
144
|
+
}
|
|
139
145
|
export declare class SolariError extends Error {
|
|
140
146
|
readonly status?: number | undefined;
|
|
141
147
|
readonly cause?: unknown | undefined;
|
|
@@ -155,6 +161,14 @@ declare class ProfilesResource {
|
|
|
155
161
|
sizeBytes: number;
|
|
156
162
|
}>;
|
|
157
163
|
}
|
|
164
|
+
declare class ProxyResource {
|
|
165
|
+
private readonly client;
|
|
166
|
+
constructor(client: Solari);
|
|
167
|
+
/** `GET /proxy/countries` — the egress countries this gateway supports and
|
|
168
|
+
* whether residential proxy credentials are configured. Matches the Go/Rust
|
|
169
|
+
* `proxy.countries()` and C++ `proxyCountries()`. */
|
|
170
|
+
countries(): Promise<ProxyCountries>;
|
|
171
|
+
}
|
|
158
172
|
declare class SessionsResource {
|
|
159
173
|
private readonly client;
|
|
160
174
|
constructor(client: Solari);
|
|
@@ -170,12 +184,13 @@ declare class SessionsResource {
|
|
|
170
184
|
export declare class Solari {
|
|
171
185
|
readonly sessions: SessionsResource;
|
|
172
186
|
readonly profiles: ProfilesResource;
|
|
187
|
+
readonly proxy: ProxyResource;
|
|
173
188
|
private readonly apiKey;
|
|
174
189
|
private readonly baseUrl;
|
|
175
190
|
private readonly maxAttempts;
|
|
176
191
|
private readonly backoffMs;
|
|
177
192
|
private readonly timeoutMs;
|
|
178
|
-
private readonly
|
|
193
|
+
private readonly localProxy;
|
|
179
194
|
constructor(opts: SolariOptions);
|
|
180
195
|
close(): Promise<void>;
|
|
181
196
|
/**
|
|
@@ -194,5 +209,7 @@ export declare class Solari {
|
|
|
194
209
|
private _probeBrowserHealth;
|
|
195
210
|
_wrapSessionEndpoint(sessionId: string, upstreamEndpoint: string, kind?: ProxyKind): Promise<string>;
|
|
196
211
|
_unregisterSession(sessionId: string): void;
|
|
197
|
-
request(method: string, path: string, body?: unknown
|
|
212
|
+
request(method: string, path: string, body?: unknown, opts?: {
|
|
213
|
+
idempotencyKey?: string;
|
|
214
|
+
}): Promise<Response>;
|
|
198
215
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,39 @@ import { chromium } from "patchright-core";
|
|
|
2
2
|
import { LocalProxy } from "./local-proxy.js";
|
|
3
3
|
import { BrowserSession } from "./browser-session.js";
|
|
4
4
|
export { BrowserSession } from "./browser-session.js";
|
|
5
|
+
/**
|
|
6
|
+
* Narrow a {@link StorageState} to the shape `newContext({storageState})` wants.
|
|
7
|
+
*
|
|
8
|
+
* Our StorageState is deliberately permissive — every field optional, plus an
|
|
9
|
+
* index signature — because it doubles as the INPUT to `profiles.save()`, where
|
|
10
|
+
* callers hand-build partial objects. Playwright's parameter type requires the
|
|
11
|
+
* cookie fields to be present, so passing ours straight through is a TS2322 and
|
|
12
|
+
* callers were resorting to `as any`.
|
|
13
|
+
*
|
|
14
|
+
* Rather than tighten the public type (which would break anyone constructing a
|
|
15
|
+
* partial state to save), fill the defaults at this one seam. Values that are
|
|
16
|
+
* actually present always win; the defaults only cover fields the gateway
|
|
17
|
+
* omitted, and they match Playwright's own defaults for a host-only cookie.
|
|
18
|
+
*/
|
|
19
|
+
function toPlaywrightStorageState(state) {
|
|
20
|
+
return {
|
|
21
|
+
cookies: (state.cookies ?? []).map((c) => ({
|
|
22
|
+
name: c.name,
|
|
23
|
+
value: c.value,
|
|
24
|
+
domain: c.domain ?? "",
|
|
25
|
+
path: c.path ?? "/",
|
|
26
|
+
// -1 is Playwright's "session cookie" sentinel.
|
|
27
|
+
expires: c.expires ?? -1,
|
|
28
|
+
httpOnly: c.httpOnly ?? false,
|
|
29
|
+
secure: c.secure ?? false,
|
|
30
|
+
sameSite: c.sameSite ?? "Lax",
|
|
31
|
+
})),
|
|
32
|
+
origins: (state.origins ?? []).map((o) => ({
|
|
33
|
+
origin: o.origin,
|
|
34
|
+
localStorage: o.localStorage ?? [],
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
5
38
|
export class SolariError extends Error {
|
|
6
39
|
status;
|
|
7
40
|
cause;
|
|
@@ -21,8 +54,60 @@ const DEFAULT_REGION = "us-west";
|
|
|
21
54
|
const DEFAULT_MAX_ATTEMPTS = 2;
|
|
22
55
|
const DEFAULT_BACKOFF_MS = 500;
|
|
23
56
|
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
57
|
+
// 507 is in the list although THIS gateway does not emit one (censused
|
|
58
|
+
// 2026-09-22: browser emits 501/502/503 only). It is here so the client is
|
|
59
|
+
// right on its own terms: the desktop gateway's InsufficientCapacity 507 is
|
|
60
|
+
// transient, and a client's correctness must not depend on which gateway
|
|
61
|
+
// build it reaches. Inert today, and deliberately so — do not delete it as
|
|
62
|
+
// dead code.
|
|
24
63
|
function isRetryableStatus(status) {
|
|
25
|
-
return status === 502 || status === 503 || status === 504;
|
|
64
|
+
return status === 502 || status === 503 || status === 504 || status === 507;
|
|
65
|
+
}
|
|
66
|
+
// Methods that are safe to send twice. The gateway issues no Idempotency-Key
|
|
67
|
+
// for the browser API, so the `retryable` hint is honoured ONLY here: a
|
|
68
|
+
// re-sent POST /sessions could leave a second live session behind.
|
|
69
|
+
function isIdempotentMethod(method) {
|
|
70
|
+
const m = method.toUpperCase();
|
|
71
|
+
return m === "GET" || m === "HEAD" || m === "DELETE" || m === "PUT";
|
|
72
|
+
}
|
|
73
|
+
// Is THIS REQUEST safe to send again? Deliberately a per-request question, not
|
|
74
|
+
// a per-method one. A POST is not idempotent by verb, but a POST carrying an
|
|
75
|
+
// Idempotency-Key is safe to replay because the server answers the second copy
|
|
76
|
+
// from the first one's result instead of creating twice.
|
|
77
|
+
//
|
|
78
|
+
// This is what the old comment here was waiting for. It read: "The browser API
|
|
79
|
+
// issues no Idempotency-Key, so the `retryable` hint is honoured ONLY for
|
|
80
|
+
// idempotent methods: a re-sent POST /sessions could leave a second live
|
|
81
|
+
// session behind." That reason is being removed, not worked around.
|
|
82
|
+
function isSafeToReplay(method, idempotencyKey) {
|
|
83
|
+
return isIdempotentMethod(method) || idempotencyKey !== undefined;
|
|
84
|
+
}
|
|
85
|
+
// A key must identify the CALL, not the attempt: minted once by the caller and
|
|
86
|
+
// reused by every retry of that call. Minting per attempt would make each retry
|
|
87
|
+
// a fresh create and defeat the whole mechanism.
|
|
88
|
+
function newIdempotencyKey() {
|
|
89
|
+
const c = typeof globalThis.crypto !== "undefined" ? globalThis.crypto : undefined;
|
|
90
|
+
if (c?.randomUUID)
|
|
91
|
+
return c.randomUUID();
|
|
92
|
+
// Pre-19 Node / older browsers: not a v4 UUID, just unique enough to key on.
|
|
93
|
+
return `slr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
|
|
94
|
+
}
|
|
95
|
+
// The gateway can mark a response retryable on a status that is NOT in the
|
|
96
|
+
// 5xx allowlist — today `404 ReplayPending`, where the recording upload is
|
|
97
|
+
// still in flight. Honour that hint rather than flattening it into a
|
|
98
|
+
// terminal error.
|
|
99
|
+
//
|
|
100
|
+
// The body is read from a CLONE: the caller still owns the original and
|
|
101
|
+
// parses `code` out of it, and a Response body can only be consumed once.
|
|
102
|
+
async function responseSaysRetryable(res) {
|
|
103
|
+
try {
|
|
104
|
+
const body = (await res.clone().json());
|
|
105
|
+
return body?.retryable === true;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Non-JSON or already-consumed body: no hint, not retryable.
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
26
111
|
}
|
|
27
112
|
function isRetryableError(_err) {
|
|
28
113
|
return true;
|
|
@@ -153,6 +238,23 @@ async function releaseRejection(res) {
|
|
|
153
238
|
return null;
|
|
154
239
|
return { body, code };
|
|
155
240
|
}
|
|
241
|
+
class ProxyResource {
|
|
242
|
+
client;
|
|
243
|
+
constructor(client) {
|
|
244
|
+
this.client = client;
|
|
245
|
+
}
|
|
246
|
+
/** `GET /proxy/countries` — the egress countries this gateway supports and
|
|
247
|
+
* whether residential proxy credentials are configured. Matches the Go/Rust
|
|
248
|
+
* `proxy.countries()` and C++ `proxyCountries()`. */
|
|
249
|
+
async countries() {
|
|
250
|
+
const res = await this.client.request("GET", "/proxy/countries");
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
const body = await res.text().catch(() => "");
|
|
253
|
+
throw new SolariError(`Solari GET /proxy/countries failed: ${res.status} ${body}`, res.status);
|
|
254
|
+
}
|
|
255
|
+
return (await res.json());
|
|
256
|
+
}
|
|
257
|
+
}
|
|
156
258
|
class SessionsResource {
|
|
157
259
|
client;
|
|
158
260
|
constructor(client) {
|
|
@@ -168,11 +270,12 @@ class SessionsResource {
|
|
|
168
270
|
body.stealth = true;
|
|
169
271
|
if (options?.captcha === true)
|
|
170
272
|
body.captcha = true;
|
|
171
|
-
if (options?.webBotAuth === true)
|
|
172
|
-
body.webBotAuth = true;
|
|
173
273
|
if (options?.proxy !== undefined)
|
|
174
274
|
body.proxy = options.proxy;
|
|
175
|
-
|
|
275
|
+
// A create allocates a slot and launches a browser before it can answer,
|
|
276
|
+
// so a response lost after that point makes a retry a SECOND session.
|
|
277
|
+
// The key lets the gateway answer the retry from the first result.
|
|
278
|
+
const res = await this.client.request("POST", "/sessions", Object.keys(body).length ? body : undefined, { idempotencyKey: newIdempotencyKey() });
|
|
176
279
|
if (!res.ok) {
|
|
177
280
|
const text = await res.text().catch(() => "");
|
|
178
281
|
let code;
|
|
@@ -265,12 +368,13 @@ class SessionsResource {
|
|
|
265
368
|
export class Solari {
|
|
266
369
|
sessions;
|
|
267
370
|
profiles;
|
|
371
|
+
proxy;
|
|
268
372
|
apiKey;
|
|
269
373
|
baseUrl;
|
|
270
374
|
maxAttempts;
|
|
271
375
|
backoffMs;
|
|
272
376
|
timeoutMs;
|
|
273
|
-
|
|
377
|
+
localProxy = new LocalProxy();
|
|
274
378
|
constructor(opts) {
|
|
275
379
|
if (!opts.apiKey) {
|
|
276
380
|
throw new SolariError("Solari: apiKey is required");
|
|
@@ -287,9 +391,10 @@ export class Solari {
|
|
|
287
391
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
288
392
|
this.sessions = new SessionsResource(this);
|
|
289
393
|
this.profiles = new ProfilesResource(this);
|
|
394
|
+
this.proxy = new ProxyResource(this);
|
|
290
395
|
}
|
|
291
396
|
async close() {
|
|
292
|
-
await this.
|
|
397
|
+
await this.localProxy.stop();
|
|
293
398
|
}
|
|
294
399
|
/**
|
|
295
400
|
* Create a session and return a connected browser.
|
|
@@ -322,7 +427,32 @@ export class Solari {
|
|
|
322
427
|
if (probeEnabled) {
|
|
323
428
|
await this._probeBrowserHealth(browser, probeTimeoutMs);
|
|
324
429
|
}
|
|
325
|
-
|
|
430
|
+
// Materialise ONE primary context for the session.
|
|
431
|
+
//
|
|
432
|
+
// The gateway already fetches an attached profile's cookies +
|
|
433
|
+
// origins into session.storageState (and its own comment says it
|
|
434
|
+
// does so "so the SDK skips the newContext({storageState}) seed")
|
|
435
|
+
// — but the SDK never performed that seed, so every run wrote a
|
|
436
|
+
// new profile version and read none back. Seed it here.
|
|
437
|
+
//
|
|
438
|
+
// Also makes a context reliably available: the pool only
|
|
439
|
+
// pre-creates one when a proxy was requested, so on a plain
|
|
440
|
+
// launch() browser.contexts() was empty and contexts()[0]
|
|
441
|
+
// undefined — which the SDK's own docs pointed callers at.
|
|
442
|
+
const ctxOptions = {};
|
|
443
|
+
const tz = session.proxy?.timezoneId;
|
|
444
|
+
if (tz)
|
|
445
|
+
ctxOptions.timezoneId = tz;
|
|
446
|
+
if (session.storageState) {
|
|
447
|
+
ctxOptions.storageState = toPlaywrightStorageState(session.storageState);
|
|
448
|
+
}
|
|
449
|
+
const primary =
|
|
450
|
+
// Reuse the pool's eager context only when we have nothing
|
|
451
|
+
// to seed — storageState can only be applied at creation.
|
|
452
|
+
!ctxOptions.storageState && browser.contexts().length > 0
|
|
453
|
+
? browser.contexts()[0]
|
|
454
|
+
: await browser.newContext(ctxOptions);
|
|
455
|
+
return new BrowserSession(this, session, browser, primary);
|
|
326
456
|
}
|
|
327
457
|
catch (err) {
|
|
328
458
|
if (browser) {
|
|
@@ -387,18 +517,23 @@ export class Solari {
|
|
|
387
517
|
catch (e) {
|
|
388
518
|
throw new SolariError(`Solari: invalid upstream ${kind}Endpoint: ${upstreamEndpoint}`, undefined, e);
|
|
389
519
|
}
|
|
390
|
-
await this.
|
|
391
|
-
this.
|
|
392
|
-
return this.
|
|
520
|
+
await this.localProxy.start();
|
|
521
|
+
this.localProxy.register(sessionId, upstream, kind);
|
|
522
|
+
return this.localProxy.getLocalEndpoint(sessionId, kind);
|
|
393
523
|
}
|
|
394
524
|
_unregisterSession(sessionId) {
|
|
395
|
-
this.
|
|
525
|
+
this.localProxy.unregister(sessionId);
|
|
396
526
|
}
|
|
397
|
-
async request(method, path, body) {
|
|
527
|
+
async request(method, path, body, opts) {
|
|
398
528
|
const url = `${this.baseUrl}${path}`;
|
|
529
|
+
// Built ONCE, outside the retry loop, so every attempt of this call
|
|
530
|
+
// carries the same key. See newIdempotencyKey.
|
|
399
531
|
const headers = {
|
|
400
532
|
Authorization: `Bearer ${this.apiKey}`,
|
|
401
533
|
"Content-Type": "application/json",
|
|
534
|
+
...(opts?.idempotencyKey
|
|
535
|
+
? { "Idempotency-Key": opts.idempotencyKey }
|
|
536
|
+
: {}),
|
|
402
537
|
};
|
|
403
538
|
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
404
539
|
let lastErr;
|
|
@@ -415,7 +550,10 @@ export class Solari {
|
|
|
415
550
|
clearTimeout(timer);
|
|
416
551
|
if (res.ok)
|
|
417
552
|
return res;
|
|
418
|
-
|
|
553
|
+
const retryThis = isRetryableStatus(res.status) ||
|
|
554
|
+
(isSafeToReplay(method, opts?.idempotencyKey) &&
|
|
555
|
+
(await responseSaysRetryable(res)));
|
|
556
|
+
if (!retryThis)
|
|
419
557
|
return res;
|
|
420
558
|
await res.text().catch(() => { });
|
|
421
559
|
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.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Official client SDK for Solari Browser.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc",
|
|
39
|
+
"typecheck": "node ../scripts/typecheck-guard.mjs 2 -- tsc --noEmit",
|
|
40
|
+
"test": "tsc && node --test test/*.test.mjs",
|
|
39
41
|
"prepublishOnly": "node ../scripts/check-sdk-version-pin.mjs && tsc"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|