@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 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
- const body = await res.text().catch(() => "");
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
- const body = await res.text().catch(() => "");
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
- const body = await res.text().catch(() => "");
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
- const body = await res.text().catch(() => "");
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
- const body = await res.text().catch(() => "");
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)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarisdk/browser",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official client SDK for Solari Browser.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",