@solarisdk/browser 0.2.0 → 0.3.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 +15 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +72 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,21 @@ const session = await solari.sessions.create();
|
|
|
58
58
|
const browser = await chromium.connectOverCDP(session.cdpEndpoint);
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Get and list sessions
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const view = await solari.sessions.get(session.id); // status, kind, userMetadata, endpoints while active
|
|
65
|
+
let page = await solari.sessions.list({ limit: 50, metadata: { job: "nightly-42" } });
|
|
66
|
+
// page.sessions, page.nextCursor (absent on the last page)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`list` never returns the websocket endpoints (they are bearer capabilities): call `get`
|
|
70
|
+
for a single session's. Pass `page.nextCursor` as `cursor` for the next page.
|
|
71
|
+
|
|
72
|
+
Every REST error carries the gateway's `code` when it sends one, so you can branch on
|
|
73
|
+
it whichever call failed: `e.code === "ReplayPending"` (a replay still uploading; wait and
|
|
74
|
+
call again), `"SessionNotFound"`, `"InvalidSessionId"`, `"PlanLimitExceeded"` and so on.
|
|
75
|
+
|
|
61
76
|
### Timeout, keep-alive and metadata
|
|
62
77
|
|
|
63
78
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -133,6 +133,42 @@ export interface ReplayUrl {
|
|
|
133
133
|
expiresInSeconds: number;
|
|
134
134
|
contentEncoding: string;
|
|
135
135
|
}
|
|
136
|
+
/** `GET /sessions/:id`. Fields the gateway adds later are kept (index signature). */
|
|
137
|
+
export interface SessionView {
|
|
138
|
+
id: string;
|
|
139
|
+
status: string;
|
|
140
|
+
kind?: string | null;
|
|
141
|
+
org?: string;
|
|
142
|
+
createdAt?: string | null;
|
|
143
|
+
expiresAt?: string | null;
|
|
144
|
+
userMetadata?: Record<string, unknown>;
|
|
145
|
+
/** Present while the session is active. */
|
|
146
|
+
wsEndpoint?: string;
|
|
147
|
+
cdpEndpoint?: string;
|
|
148
|
+
[k: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
/** One row of `sessions.list()`. Never carries the websocket endpoints. */
|
|
151
|
+
export interface SessionSummary {
|
|
152
|
+
id: string;
|
|
153
|
+
status: string;
|
|
154
|
+
kind?: string | null;
|
|
155
|
+
createdAt?: string | null;
|
|
156
|
+
expiresAt?: string | null;
|
|
157
|
+
userMetadata?: Record<string, unknown>;
|
|
158
|
+
}
|
|
159
|
+
export interface SessionList {
|
|
160
|
+
sessions: SessionSummary[];
|
|
161
|
+
/** Absent on the last page; pass it as `ListSessionsOptions.cursor`. */
|
|
162
|
+
nextCursor?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface ListSessionsOptions {
|
|
165
|
+
/** Page size; unset uses the gateway default. */
|
|
166
|
+
limit?: number;
|
|
167
|
+
/** Continue from a previous page's `nextCursor`. */
|
|
168
|
+
cursor?: string;
|
|
169
|
+
/** Keep sessions whose `userMetadata` has all these string values. */
|
|
170
|
+
metadata?: Record<string, string>;
|
|
171
|
+
}
|
|
136
172
|
/** Options for `Solari.launch()`. Strict superset of `CreateSessionOptions`. */
|
|
137
173
|
export interface LaunchOptions extends CreateSessionOptions {
|
|
138
174
|
/** Additional re-launch attempts after the first failure. Default 0. */
|
|
@@ -191,6 +227,15 @@ declare class SessionsResource {
|
|
|
191
227
|
/** Fire-and-forget release. Use `releaseAndWait` for confirmation. */
|
|
192
228
|
release(id: string): void;
|
|
193
229
|
releaseAndWait(id: string): Promise<void>;
|
|
230
|
+
/** The gateway's current view of one session (`GET /sessions/:id`). */
|
|
231
|
+
get(id: string): Promise<SessionView>;
|
|
232
|
+
/**
|
|
233
|
+
* One page of the organisation's sessions (`GET /sessions`). Rows never carry the
|
|
234
|
+
* websocket endpoints (they are bearer capabilities): call `get(id)` for those.
|
|
235
|
+
* `metadata` keeps sessions whose `userMetadata` has all these string values
|
|
236
|
+
* (at most 10 pairs; keys and values cannot contain a single quote).
|
|
237
|
+
*/
|
|
238
|
+
list(options?: ListSessionsOptions): Promise<SessionList>;
|
|
194
239
|
/** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
|
|
195
240
|
getReplayUrl(id: string): Promise<ReplayUrl>;
|
|
196
241
|
/** Download the replay as NDJSON bytes. */
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,21 @@ function toPlaywrightStorageState(state) {
|
|
|
35
35
|
})),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/** The gateway's list filter grammar, in key order; throws for a pair it cannot express. */
|
|
39
|
+
function metadataFilter(m) {
|
|
40
|
+
const keys = Object.keys(m).sort();
|
|
41
|
+
if (keys.length > 10)
|
|
42
|
+
throw new SolariError("Solari: at most 10 metadata filter pairs");
|
|
43
|
+
return keys
|
|
44
|
+
.map((k) => {
|
|
45
|
+
const v = m[k];
|
|
46
|
+
if (k.length < 1 || k.length > 128 || k.includes("'") || v.includes("'")) {
|
|
47
|
+
throw new SolariError(`Solari: metadata filter key ${JSON.stringify(k)} or its value cannot be expressed (1-128 characters, no single quote)`);
|
|
48
|
+
}
|
|
49
|
+
return `user_metadata['${k}']:'${v}'`;
|
|
50
|
+
})
|
|
51
|
+
.join(" AND ");
|
|
52
|
+
}
|
|
38
53
|
export class SolariError extends Error {
|
|
39
54
|
status;
|
|
40
55
|
cause;
|
|
@@ -47,6 +62,24 @@ export class SolariError extends Error {
|
|
|
47
62
|
this.name = "SolariError";
|
|
48
63
|
}
|
|
49
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the error for a non-ok API response: the status, the body text in the
|
|
67
|
+
* message, and the gateway's `code` (for example `ReplayPending`,
|
|
68
|
+
* `SessionNotFound`, `InvalidSessionId`) when the body is JSON with a string
|
|
69
|
+
* `code`. Every REST call uses this so a caller can branch on `e.code` no matter
|
|
70
|
+
* which endpoint failed.
|
|
71
|
+
*/
|
|
72
|
+
async function apiError(what, res) {
|
|
73
|
+
const body = await res.text().catch(() => "");
|
|
74
|
+
let code;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(body);
|
|
77
|
+
if (typeof parsed?.code === "string")
|
|
78
|
+
code = parsed.code;
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
return new SolariError(`Solari ${what} failed: ${res.status} ${body}`, res.status, undefined, code);
|
|
82
|
+
}
|
|
50
83
|
const REGION_URLS = {
|
|
51
84
|
"us-west": "https://api.getsolari.com",
|
|
52
85
|
};
|
|
@@ -210,25 +243,19 @@ class ProfilesResource {
|
|
|
210
243
|
}
|
|
211
244
|
async list() {
|
|
212
245
|
const res = await this.client.request("GET", "/profiles");
|
|
213
|
-
if (!res.ok)
|
|
214
|
-
|
|
215
|
-
throw new SolariError(`Solari GET /profiles failed: ${res.status} ${body}`, res.status);
|
|
216
|
-
}
|
|
246
|
+
if (!res.ok)
|
|
247
|
+
throw await apiError("GET /profiles", res);
|
|
217
248
|
return (await res.json());
|
|
218
249
|
}
|
|
219
250
|
async delete(id) {
|
|
220
251
|
const res = await this.client.request("DELETE", `/profiles/${encodeURIComponent(id)}`);
|
|
221
|
-
if (!res.ok && res.status !== 404)
|
|
222
|
-
|
|
223
|
-
throw new SolariError(`Solari DELETE /profiles/${id} failed: ${res.status} ${body}`, res.status);
|
|
224
|
-
}
|
|
252
|
+
if (!res.ok && res.status !== 404)
|
|
253
|
+
throw await apiError(`DELETE /profiles/${id}`, res);
|
|
225
254
|
}
|
|
226
255
|
async save(id, storageState) {
|
|
227
256
|
const res = await this.client.request("POST", `/profiles/${encodeURIComponent(id)}/save`, { storageState });
|
|
228
|
-
if (!res.ok)
|
|
229
|
-
|
|
230
|
-
throw new SolariError(`Solari POST /profiles/${id}/save failed: ${res.status} ${body}`, res.status);
|
|
231
|
-
}
|
|
257
|
+
if (!res.ok)
|
|
258
|
+
throw await apiError(`POST /profiles/${id}/save`, res);
|
|
232
259
|
const data = (await res.json());
|
|
233
260
|
return {
|
|
234
261
|
version: data.version ?? 0,
|
|
@@ -279,10 +306,8 @@ class ProxyResource {
|
|
|
279
306
|
* `proxy.countries()` and C++ `proxyCountries()`. */
|
|
280
307
|
async countries() {
|
|
281
308
|
const res = await this.client.request("GET", "/proxy/countries");
|
|
282
|
-
if (!res.ok)
|
|
283
|
-
|
|
284
|
-
throw new SolariError(`Solari GET /proxy/countries failed: ${res.status} ${body}`, res.status);
|
|
285
|
-
}
|
|
309
|
+
if (!res.ok)
|
|
310
|
+
throw await apiError("GET /proxy/countries", res);
|
|
286
311
|
return (await res.json());
|
|
287
312
|
}
|
|
288
313
|
}
|
|
@@ -378,13 +403,40 @@ class SessionsResource {
|
|
|
378
403
|
throw new SolariError(`Solari DELETE /sessions/${id} failed: ${res.status} ${rejection.body}`, res.status, undefined, rejection.code);
|
|
379
404
|
}
|
|
380
405
|
}
|
|
406
|
+
/** The gateway's current view of one session (`GET /sessions/:id`). */
|
|
407
|
+
async get(id) {
|
|
408
|
+
const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}`);
|
|
409
|
+
if (!res.ok)
|
|
410
|
+
throw await apiError(`GET /sessions/${id}`, res);
|
|
411
|
+
return (await res.json());
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* One page of the organisation's sessions (`GET /sessions`). Rows never carry the
|
|
415
|
+
* websocket endpoints (they are bearer capabilities): call `get(id)` for those.
|
|
416
|
+
* `metadata` keeps sessions whose `userMetadata` has all these string values
|
|
417
|
+
* (at most 10 pairs; keys and values cannot contain a single quote).
|
|
418
|
+
*/
|
|
419
|
+
async list(options) {
|
|
420
|
+
const params = new URLSearchParams();
|
|
421
|
+
if (options?.limit !== undefined && options.limit > 0)
|
|
422
|
+
params.set("limit", String(options.limit));
|
|
423
|
+
if (options?.cursor)
|
|
424
|
+
params.set("cursor", options.cursor);
|
|
425
|
+
if (options?.metadata && Object.keys(options.metadata).length > 0) {
|
|
426
|
+
params.set("q", metadataFilter(options.metadata));
|
|
427
|
+
}
|
|
428
|
+
const qs = params.toString();
|
|
429
|
+
const res = await this.client.request("GET", `/sessions${qs ? `?${qs}` : ""}`);
|
|
430
|
+
if (!res.ok)
|
|
431
|
+
throw await apiError("GET /sessions", res);
|
|
432
|
+
const data = (await res.json());
|
|
433
|
+
return { sessions: data.sessions ?? [], ...(data.nextCursor ? { nextCursor: data.nextCursor } : {}) };
|
|
434
|
+
}
|
|
381
435
|
/** Presigned URL for the session's replay. Available ~1-3s after `releaseAndWait`. */
|
|
382
436
|
async getReplayUrl(id) {
|
|
383
437
|
const res = await this.client.request("GET", `/sessions/${encodeURIComponent(id)}/replay-url`);
|
|
384
|
-
if (!res.ok)
|
|
385
|
-
|
|
386
|
-
throw new SolariError(`Solari GET /sessions/${id}/replay-url failed: ${res.status} ${body}`, res.status);
|
|
387
|
-
}
|
|
438
|
+
if (!res.ok)
|
|
439
|
+
throw await apiError(`GET /sessions/${id}/replay-url`, res);
|
|
388
440
|
const data = (await res.json());
|
|
389
441
|
if (!data.url) {
|
|
390
442
|
throw new SolariError(`Solari: unexpected replay-url response: ${JSON.stringify(data)}`);
|