browserscale-ts 1.7.0 → 1.8.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/dist/client.js CHANGED
@@ -3,8 +3,7 @@ import { create } from "@bufbuild/protobuf";
3
3
  import { Browser,
4
4
  // request / response schemas
5
5
  SetProxyRequestSchema, GetPagesRequestSchema, NavigateRequestSchema, LoadHTMLRequestSchema, EvaluateRequestSchema, WaitForAnyParamsSchema, WaitConditionSchema, ClickRequestSchema, FillRequestSchema, MoveToRequestSchema, ScrollToRequestSchema, DragRequestSchema, SelectOptionRequestSchema, GetDOMRequestSchema, GetDOMHashRequestSchema, GetObservationRequestSchema, ScreenshotRequestSchema, ReadCanvasRequestSchema, SetBlockListRequestSchema, SetStaticPathsRequestSchema, WaitForAnyRequestRequestSchema, WaitForAnyResponseRequestSchema, ModifyRequestRequestSchema, GetCookiesRequestSchema, SetCookiesRequestSchema, ClearCookiesRequestSchema, GetStorageRequestSchema, SetStorageRequestSchema, ClearStorageRequestSchema, GetAuthSessionRequestSchema, SetAuthSessionRequestSchema, InspectAtPositionRequestSchema, HighlightNodeRequestSchema, InsertTextRequestSchema, TypeRequestSchema, PressKeyRequestSchema, ReleaseKeyRequestSchema, GetSelectionRequestSchema, SolveCaptchaRequestSchema, GetStreamConfigRequestSchema, StartStreamRequestSchema, StopStreamRequestSchema, AddReactionRequestSchema, RemoveReactionRequestSchema, ListReactionsRequestSchema, StartNetworkCaptureRequestSchema, StopNetworkCaptureRequestSchema, StartDomMirrorRequestSchema, StopDomMirrorRequestSchema, GetDomChildrenRequestSchema, ReleaseDomSubtreeRequestSchema, RevealDomNodeRequestSchema, GetDomRevisionRequestSchema, StreamDomEventsRequestSchema, StreamNetworkExchangesRequestSchema, RunScriptRequestSchema, StartScriptRequestSchema, StopScriptsRequestSchema, ListScriptRunsRequestSchema, StreamScriptEventsRequestSchema, } from "./gen/wrc_pb.js";
6
- import { DefaultWaitTimeoutMs } from "./defaults.js";
7
- import { BrowserScaleError } from "./errors.js";
6
+ import { BrowserScaleError, CommandError, throwCommandError } from "./errors.js";
8
7
  import { NetworkCapture } from "./network-capture.js";
9
8
  import { ScriptFollow, ScriptRun, scriptLogEntryFromProto, } from "./scripts.js";
10
9
  import { DomMirror, } from "./dom-mirror.js";
@@ -13,9 +12,7 @@ import { authSessionFromProto, authSessionToProto, cookieParamFromProto, cookieP
13
12
  * CloudBrowser is the SDK-side handle for an active browserscale browser session.
14
13
  *
15
14
  * One CloudBrowser corresponds to exactly one browser context, which
16
- * always has at least one page. The session is implicitly bound to its
17
- * primary page server-side — the proto's page_id field is currently
18
- * ignored server-side, so the SDK never sets it.
15
+ * always has at least one page; its commands act on the primary page.
19
16
  *
20
17
  * Construct via rentBrowser() / createWebSocketBrowser() — never
21
18
  * directly.
@@ -52,7 +49,8 @@ export class CloudBrowser {
52
49
  * {@link createWebSocketBrowser} the rental stays untouched; only the
53
50
  * transport is closed.
54
51
  *
55
- * @throws UNKNOWN_ERROR - the stop API or the transport close failed
52
+ * Rejects only when the stop API or the transport close fails. The session is
53
+ * released either way; retrying a stop is safe.
56
54
  *
57
55
  * @example
58
56
  * await browser.stopBrowser();
@@ -77,7 +75,9 @@ export class CloudBrowser {
77
75
  * @param proxyUsername - proxy auth user; empty for unauthenticated proxies
78
76
  * @param proxyPassword - proxy auth password; empty for unauthenticated proxies
79
77
  *
80
- * @throws UNKNOWN_ERROR - the proxy could not be applied
78
+ * Rejects only on a transport failure - a dead session, a page that is gone,
79
+ * a broken connection. This call has no semantic failure of its own, so there
80
+ * are no error codes to branch on.
81
81
  *
82
82
  * @example
83
83
  * await browser.setProxy("proxy.example.com", 8080, "user", "pass");
@@ -95,7 +95,8 @@ export class CloudBrowser {
95
95
  if (proxyPassword)
96
96
  req.proxyPassword = proxyPassword;
97
97
  }
98
- await this.client.setProxy(req);
98
+ const res = await this.client.setProxy(req);
99
+ throwCommandError("setProxy", res.error);
99
100
  }
100
101
  /**
101
102
  * Returns all open pages (tabs and popups) for this session's browser
@@ -107,7 +108,9 @@ export class CloudBrowser {
107
108
  *
108
109
  * @returns PageInfo[] for every page currently open in the context
109
110
  *
110
- * @throws UNKNOWN_ERROR - the pages could not be enumerated
111
+ * Rejects only on a transport failure - a dead session, a page that is gone,
112
+ * a broken connection. This call has no semantic failure of its own, so there
113
+ * are no error codes to branch on.
111
114
  *
112
115
  * @example
113
116
  * const pages = await browser.getPages();
@@ -118,6 +121,7 @@ export class CloudBrowser {
118
121
  sessionId: this.sessionId,
119
122
  apiKey: this.apiKey,
120
123
  }));
124
+ throwCommandError("getPages", resp.error);
121
125
  return resp.pages.map(pageInfoFromProto);
122
126
  }
123
127
  // ──────────────────────────────────────────────────────────────────
@@ -136,7 +140,16 @@ export class CloudBrowser {
136
140
  * @returns NavigateResult with the final resolved URL and the frameId
137
141
  * of the main frame after navigation
138
142
  *
139
- * @throws UNKNOWN_ERROR - the navigation failed or timed out
143
+ * @throws timeout - nothing committed before the deadline; the page may still
144
+ * be loading, so a longer timeout can be the whole fix
145
+ * @throws net_error - the URL never loaded: DNS, TLS, a refused connection, or
146
+ * a proxy that could not reach it. The message carries the underlying net
147
+ * error name, which is what separates a bad proxy from a bad host - worth
148
+ * logging, since the two need different fixes
149
+ * @throws crashed - the renderer died mid-navigation; the page is unusable and
150
+ * has to be navigated again
151
+ *
152
+ * @see {@link CommandError} for reading the code off the rejection
140
153
  *
141
154
  * @example
142
155
  * await browser.navigate("https://example.com");
@@ -150,6 +163,7 @@ export class CloudBrowser {
150
163
  if (opts?.timeoutMs)
151
164
  req.timeout = opts.timeoutMs;
152
165
  const resp = await this.client.navigate(req);
166
+ throwCommandError("navigate", resp.error);
153
167
  return { frameId: resp.frameId, url: resp.url };
154
168
  }
155
169
  /**
@@ -164,7 +178,11 @@ export class CloudBrowser {
164
178
  * @param html - response body to serve
165
179
  * @param opts - optional headers and statusCode (default 200)
166
180
  *
167
- * @throws UNKNOWN_ERROR - the interceptor could not be installed
181
+ * @throws timeout - the page never requested the URL, so the prepared
182
+ * response had nobody to hand it to; usually the navigation was cancelled
183
+ * or redirected away before reaching it
184
+ *
185
+ * @see {@link CommandError} for reading the code off the rejection
168
186
  *
169
187
  * @example
170
188
  * await browser.loadHTML("https://example.com", "<h1>hi</h1>");
@@ -180,7 +198,8 @@ export class CloudBrowser {
180
198
  });
181
199
  if (opts?.statusCode)
182
200
  req.statusCode = opts.statusCode;
183
- await this.client.loadHTML(req);
201
+ const res = await this.client.loadHTML(req);
202
+ throwCommandError("loadHTML", res.error);
184
203
  }
185
204
  // ──────────────────────────────────────────────────────────────────
186
205
  // Evaluation
@@ -197,16 +216,34 @@ export class CloudBrowser {
197
216
  * The generic T is a TypeScript hint only — there is no runtime
198
217
  * validation that the JS expression actually returned that type.
199
218
  *
219
+ * A falsy answer and a broken expression are different outcomes. Returning
220
+ * null, false or undefined is a successful evaluation and resolves normally;
221
+ * an expression that throws or will not compile rejects with a
222
+ * {@link CommandError}, so a typo can never read as "the page says null".
223
+ *
200
224
  * @param expression - JavaScript expression evaluated in the main frame
201
225
  *
202
226
  * @returns EvaluateResult with either value (non-Element) or element
203
227
  * metadata (Element)
204
228
  *
205
- * @throws UNKNOWN_ERROR - the expression threw or could not be compiled
229
+ * @throws {@link CommandError} - code `"threw"` (the expression raised; the
230
+ * message carries the exception text), `"not_run"` (it could not be
231
+ * compiled, or execution never started), `"aborted"` (the browser stopped
232
+ * execution) or `"no_context"` (the frame had no live script context)
206
233
  *
207
234
  * @example
208
235
  * const res = await browser.evaluate<string>("document.title");
209
236
  * console.log(res.value);
237
+ *
238
+ * @example
239
+ * // Telling a false answer from a broken expression.
240
+ * try {
241
+ * const res = await browser.evaluate<boolean>("window.__ready === true");
242
+ * if (!res.value) { /* legitimately not ready yet *\/ }
243
+ * } catch (e) {
244
+ * if (e instanceof CommandError) throw new Error(`expression broken: ${e.message}`);
245
+ * throw e;
246
+ * }
210
247
  */
211
248
  async evaluate(expression) {
212
249
  return this._evaluate("", expression);
@@ -239,6 +276,15 @@ export class CloudBrowser {
239
276
  if (frameId)
240
277
  req.frameId = frameId;
241
278
  const resp = await this.client.evaluate(req);
279
+ // An expression that threw arrives as success=false in the payload, not as a
280
+ // transport error, so it is raised here rather than by the call above.
281
+ if (resp.error) {
282
+ throw new CommandError({
283
+ command: "evaluate",
284
+ code: resp.error.code,
285
+ message: resp.error.message,
286
+ });
287
+ }
242
288
  let value = null;
243
289
  if (resp.result !== "") {
244
290
  try {
@@ -249,6 +295,7 @@ export class CloudBrowser {
249
295
  }
250
296
  }
251
297
  return {
298
+ success: resp.success,
252
299
  value: value,
253
300
  backendNodeId: resp.backendNodeId,
254
301
  isVisible: resp.isVisible,
@@ -340,7 +387,8 @@ export class CloudBrowser {
340
387
  sessionId: this.sessionId,
341
388
  apiKey: this.apiKey,
342
389
  conditions: pbConds,
343
- timeout: opts?.timeoutMs ?? DefaultWaitTimeoutMs,
390
+ // Left unset unless the caller passed one, so the API applies its default.
391
+ timeout: opts?.timeoutMs,
344
392
  });
345
393
  if (frameId)
346
394
  req.frameId = frameId;
@@ -660,7 +708,9 @@ export class CloudBrowser {
660
708
  * @returns DOMResult with the JSON string in `.dom` (the `.hash` field
661
709
  * is populated by {@link getDOMHash}, not by this call)
662
710
  *
663
- * @throws UNKNOWN_ERROR - the DOM could not be retrieved
711
+ * Rejects only on a transport failure - a dead session, a page that is gone,
712
+ * a broken connection. This call has no semantic failure of its own, so there
713
+ * are no error codes to branch on.
664
714
  *
665
715
  * @example
666
716
  * const { dom } = await browser.getDOM();
@@ -675,6 +725,7 @@ export class CloudBrowser {
675
725
  if (opts?.depth !== undefined)
676
726
  req.depth = opts.depth;
677
727
  const resp = await this.client.getDOM(req);
728
+ throwCommandError("getDOM", resp.error);
678
729
  return { hash: "", dom: resp.dom };
679
730
  }
680
731
  /**
@@ -689,7 +740,9 @@ export class CloudBrowser {
689
740
  *
690
741
  * @returns 16-char hex string (the first 8 bytes of sha256 of the DOM JSON)
691
742
  *
692
- * @throws UNKNOWN_ERROR - the hash could not be computed
743
+ * Rejects only on a transport failure. The hash is computed from a serialized
744
+ * tree, so there is no semantic failure of its own and no error codes to branch
745
+ * on.
693
746
  *
694
747
  * @example
695
748
  * const hash = await browser.getDOMHash();
@@ -738,7 +791,11 @@ export class CloudBrowser {
738
791
  *
739
792
  * @returns the observation in the requested format, ready to hand to a model
740
793
  *
741
- * @throws UNKNOWN_ERROR - the observation could not be produced
794
+ * @throws not_found - the requested scope root is not on the page, so there
795
+ * was nothing to observe. Distinct from an observation that comes back
796
+ * empty, which means the scope exists and holds nothing worth reporting
797
+ *
798
+ * @see {@link CommandError} for reading the code off the rejection
742
799
  *
743
800
  * @example
744
801
  * const obs = await browser.getObservation();
@@ -771,6 +828,7 @@ export class CloudBrowser {
771
828
  if (opts?.frameId !== undefined)
772
829
  req.frameId = opts.frameId;
773
830
  const resp = await this.client.getObservation(req);
831
+ throwCommandError("getObservation", resp.error);
774
832
  return resp.observation;
775
833
  }
776
834
  /**
@@ -787,7 +845,11 @@ export class CloudBrowser {
787
845
  * @returns ScreenshotResult with the base64 image in `dataBase64` and the
788
846
  * physical pixel `width`/`height`
789
847
  *
790
- * @throws UNKNOWN_ERROR - the screenshot could not be captured
848
+ * @throws capture_failed - the page had no frame to copy. A page that has not
849
+ * produced one yet, or is not being composited at the moment, has nothing to
850
+ * hand over; retrying after it renders usually works
851
+ *
852
+ * @see {@link CommandError} for reading the code off the rejection
791
853
  *
792
854
  * @example
793
855
  * const shot = await browser.screenshot({ format: "png" });
@@ -803,6 +865,7 @@ export class CloudBrowser {
803
865
  if (opts?.quality !== undefined)
804
866
  req.quality = opts.quality;
805
867
  const resp = await this.client.screenshot(req);
868
+ throwCommandError("screenshot", resp.error);
806
869
  return {
807
870
  dataBase64: resp.dataBase64,
808
871
  width: resp.width,
@@ -826,11 +889,15 @@ export class CloudBrowser {
826
889
  * canvas `width`/`height`, resolved `frameId`/`backendNodeId` and the
827
890
  * `originClean` flag
828
891
  *
829
- * @throws INVALID_LOCATOR - target is empty, uses at(x,y), or has multiple targets
830
- * @throws ELEMENT_NOT_FOUND - no element matched the locator
831
- * @throws FRAME_NOT_FOUND - the requested frame does not exist
832
- * @throws TIMEOUT - the operation exceeded the server-side timeout
833
- * @throws PAGE_NOT_ALIVE - the page has been closed
892
+ * @throws not_found - no element matched the locator
893
+ * @throws not_element - the expression was truthy but did not yield an element
894
+ * @throws not_readable - the target was found but is not a readable canvas
895
+ *
896
+ * A target that is empty, uses `at(x, y)` or names several things at once is
897
+ * rejected before anything is sent. A closed page or a frame that is gone is a
898
+ * transport failure rather than a code.
899
+ *
900
+ * @see {@link CommandError} for reading the code off the rejection
834
901
  *
835
902
  * @example
836
903
  * const res = await browser.readCanvas(css("#game canvas"));
@@ -862,6 +929,7 @@ export class CloudBrowser {
862
929
  req.sh = opts.sh;
863
930
  }
864
931
  const resp = await this.client.readCanvas(req);
932
+ throwCommandError("readCanvas", resp.error);
865
933
  return {
866
934
  success: resp.success,
867
935
  frameId: resp.frameId,
@@ -885,7 +953,9 @@ export class CloudBrowser {
885
953
  *
886
954
  * @param patterns - URL wildcards to block; empty array clears the list
887
955
  *
888
- * @throws UNKNOWN_ERROR - the blocklist could not be applied
956
+ * Rejects only on a transport failure - a dead session, a page that is gone,
957
+ * a broken connection. This call has no semantic failure of its own, so there
958
+ * are no error codes to branch on.
889
959
  *
890
960
  * @example
891
961
  * await browser.setBlockList([
@@ -894,11 +964,12 @@ export class CloudBrowser {
894
964
  * ]);
895
965
  */
896
966
  async setBlockList(patterns) {
897
- await this.client.setBlockList(create(SetBlockListRequestSchema, {
967
+ const res = await this.client.setBlockList(create(SetBlockListRequestSchema, {
898
968
  sessionId: this.sessionId,
899
969
  apiKey: this.apiKey,
900
970
  patterns,
901
971
  }));
972
+ throwCommandError("setBlockList", res.error);
902
973
  }
903
974
  /**
904
975
  * Configures the session to serve cached static responses for requests
@@ -912,18 +983,21 @@ export class CloudBrowser {
912
983
  * @param blobName - server-side identifier of the snapshot to serve from
913
984
  * @param patterns - URL wildcards to redirect to the cache; empty disables
914
985
  *
915
- * @throws UNKNOWN_ERROR - the static paths could not be configured
986
+ * Rejects only on a transport failure - a dead session, a page that is gone,
987
+ * a broken connection. This call has no semantic failure of its own, so there
988
+ * are no error codes to branch on.
916
989
  *
917
990
  * @example
918
991
  * await browser.setStaticPaths("snap-2026-05", ["*.example.com/*"]);
919
992
  */
920
993
  async setStaticPaths(blobName, patterns) {
921
- await this.client.setStaticPaths(create(SetStaticPathsRequestSchema, {
994
+ const res = await this.client.setStaticPaths(create(SetStaticPathsRequestSchema, {
922
995
  sessionId: this.sessionId,
923
996
  apiKey: this.apiKey,
924
997
  blobName,
925
998
  patterns,
926
999
  }));
1000
+ throwCommandError("setStaticPaths", res.error);
927
1001
  }
928
1002
  /**
929
1003
  * Blocks until the next request whose URL matches one of the supplied
@@ -940,7 +1014,12 @@ export class CloudBrowser {
940
1014
  * (the captured method/URL/headers/body; null if intercepted with
941
1015
  * no body)
942
1016
  *
943
- * @throws UNKNOWN_ERROR - the wait timed out or no patterns were supplied
1017
+ * @throws timeout - no request matched any pattern before the deadline.
1018
+ * Nothing occurring is an answer, and it stays distinguishable from a
1019
+ * connection that died on the way. Supplying no patterns is a caller mistake
1020
+ * rather than an outcome, and rejects separately
1021
+ *
1022
+ * @see {@link CommandError} for reading the code off the rejection
944
1023
  *
945
1024
  * @example
946
1025
  * const { index, request } = await browser.waitForAnyRequest(
@@ -963,6 +1042,7 @@ export class CloudBrowser {
963
1042
  if (opts?.timeoutMs)
964
1043
  req.timeout = opts.timeoutMs;
965
1044
  const resp = await this.client.waitForAnyRequest(req);
1045
+ throwCommandError("waitForAnyRequest", resp.error);
966
1046
  return {
967
1047
  index: resp.index,
968
1048
  request: interceptedRequestFromProto(resp.request),
@@ -981,6 +1061,13 @@ export class CloudBrowser {
981
1061
  * @returns object with `index` (matched pattern index) and `response`
982
1062
  * (the captured status/headers/body; null if no body was returned)
983
1063
  *
1064
+ * @throws timeout - no response matched any pattern before the deadline.
1065
+ * Nothing occurring is an answer, and it stays distinguishable from a
1066
+ * connection that died on the way. Supplying no patterns is a caller mistake
1067
+ * rather than an outcome, and rejects separately
1068
+ *
1069
+ * @see {@link CommandError} for reading the code off the rejection
1070
+ *
984
1071
  * @example
985
1072
  * const { index, response } = await browser.waitForAnyResponse(
986
1073
  * [{ url: "*\/api/login" }],
@@ -1002,6 +1089,7 @@ export class CloudBrowser {
1002
1089
  if (opts?.timeoutMs)
1003
1090
  req.timeout = opts.timeoutMs;
1004
1091
  const resp = await this.client.waitForAnyResponse(req);
1092
+ throwCommandError("waitForAnyResponse", resp.error);
1005
1093
  return {
1006
1094
  index: resp.index,
1007
1095
  response: interceptedResponseFromProto(resp.response),
@@ -1023,7 +1111,10 @@ export class CloudBrowser {
1023
1111
  * were actually sent on the wire after modifications were applied;
1024
1112
  * null when no request payload was reported
1025
1113
  *
1026
- * @throws UNKNOWN_ERROR - no matching request appeared within the timeout
1114
+ * @throws timeout - no matching request appeared before the deadline, so
1115
+ * nothing was modified
1116
+ *
1117
+ * @see {@link CommandError} for reading the code off the rejection
1027
1118
  *
1028
1119
  * @example
1029
1120
  * const req = await browser.modifyRequest("*\/api/me", {
@@ -1047,6 +1138,7 @@ export class CloudBrowser {
1047
1138
  if (opts?.timeoutMs)
1048
1139
  req.timeout = opts.timeoutMs;
1049
1140
  const resp = await this.client.modifyRequest(req);
1141
+ throwCommandError("modifyRequest", resp.error);
1050
1142
  return interceptedRequestFromProto(resp.request);
1051
1143
  }
1052
1144
  // ──────────────────────────────────────────────────────────────────
@@ -1076,7 +1168,9 @@ export class CloudBrowser {
1076
1168
  * @returns NetworkCapture handle for stopping the capture and inspecting how
1077
1169
  * it ended
1078
1170
  *
1079
- * @throws UNKNOWN_ERROR - the capture could not be started
1171
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1172
+ * a broken connection. This call has no semantic failure of its own, so there
1173
+ * are no error codes to branch on.
1080
1174
  *
1081
1175
  * @example
1082
1176
  * const capture = await browser.captureNetwork(
@@ -1112,29 +1206,34 @@ export class CloudBrowser {
1112
1206
  *
1113
1207
  * @param opts - which requests to capture and whether to keep bodies
1114
1208
  *
1115
- * @throws UNKNOWN_ERROR - the capture could not be started
1209
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1210
+ * a broken connection. This call has no semantic failure of its own, so there
1211
+ * are no error codes to branch on.
1116
1212
  */
1117
1213
  async startNetworkCapture(opts) {
1118
- await this.client.startNetworkCapture(create(StartNetworkCaptureRequestSchema, {
1214
+ const res = await this.client.startNetworkCapture(create(StartNetworkCaptureRequestSchema, {
1119
1215
  sessionId: this.sessionId,
1120
1216
  apiKey: this.apiKey,
1121
1217
  patterns: opts.patterns ?? [],
1122
1218
  bodies: opts.bodies ?? "none",
1123
1219
  bodyPatterns: opts.bodyPatterns ?? [],
1124
1220
  }));
1221
+ throwCommandError("startNetworkCapture", res.error);
1125
1222
  }
1126
1223
  /**
1127
1224
  * Disarms the session's capture.
1128
1225
  *
1129
1226
  * @returns whether a capture was running
1130
1227
  *
1131
- * @throws UNKNOWN_ERROR - the capture could not be stopped
1228
+ * Rejects only on a transport failure. Stopping a capture that is not running
1229
+ * is a no-op rather than a failure, so there are no error codes to branch on.
1132
1230
  */
1133
1231
  async stopNetworkCapture() {
1134
1232
  const resp = await this.client.stopNetworkCapture(create(StopNetworkCaptureRequestSchema, {
1135
1233
  sessionId: this.sessionId,
1136
1234
  apiKey: this.apiKey,
1137
1235
  }));
1236
+ throwCommandError("stopNetworkCapture", resp.error);
1138
1237
  return resp.stopped;
1139
1238
  }
1140
1239
  /**
@@ -1151,7 +1250,8 @@ export class CloudBrowser {
1151
1250
  * @returns NetworkCapture attached to whatever capture is running; onExchange
1152
1251
  * simply never fires when none is
1153
1252
  *
1154
- * @throws UNKNOWN_ERROR - the subscription could not be opened
1253
+ * Rejects only on a transport failure: opening the subscription has no semantic
1254
+ * failure of its own.
1155
1255
  */
1156
1256
  async streamNetworkExchanges(onExchange) {
1157
1257
  return this.subscribeNetworkExchanges(onExchange);
@@ -1233,7 +1333,10 @@ export class CloudBrowser {
1233
1333
  * @param onResync called when the copy had to be rebuilt, after the new tree
1234
1334
  * is in place. Rebuilding is automatic; this is for telling the user why
1235
1335
  * their expanded nodes collapsed.
1236
- * @throws UNKNOWN_ERROR - the mirror could not be started
1336
+ * @throws mirror_failed - the page could not be serialized, usually a document
1337
+ * that went away while the tree was being built. No mirror is left running
1338
+ *
1339
+ * @see {@link CommandError} for reading the code off the rejection
1237
1340
  */
1238
1341
  async mirrorDom(opts, onChange, onResync) {
1239
1342
  const abort = new AbortController();
@@ -1282,6 +1385,11 @@ export class CloudBrowser {
1282
1385
  * Starts (or restarts) the page's mirror and returns the main document,
1283
1386
  * without subscribing to changes. {@link CloudBrowser.mirrorDom} is what you
1284
1387
  * normally want; this is the raw command.
1388
+ *
1389
+ * @throws mirror_failed - the page could not be serialized, usually a document
1390
+ * that went away while the tree was being built. No mirror is left running
1391
+ *
1392
+ * @see {@link CommandError} for reading the code off the rejection
1285
1393
  */
1286
1394
  async startDomMirror(opts = {}) {
1287
1395
  const req = create(StartDomMirrorRequestSchema, {
@@ -1293,6 +1401,7 @@ export class CloudBrowser {
1293
1401
  if (opts.pierce !== undefined)
1294
1402
  req.pierce = opts.pierce;
1295
1403
  const resp = await this.client.startDomMirror(req);
1404
+ throwCommandError("startDomMirror", resp.error);
1296
1405
  return {
1297
1406
  root: resp.root,
1298
1407
  frameId: resp.frameId,
@@ -1301,10 +1410,11 @@ export class CloudBrowser {
1301
1410
  }
1302
1411
  /** Stops the page's mirror, every frame of it. Idempotent. */
1303
1412
  async stopDomMirror() {
1304
- await this.client.stopDomMirror(create(StopDomMirrorRequestSchema, {
1413
+ const res = await this.client.stopDomMirror(create(StopDomMirrorRequestSchema, {
1305
1414
  sessionId: this.sessionId,
1306
1415
  apiKey: this.apiKey,
1307
1416
  }));
1417
+ throwCommandError("stopDomMirror", res.error);
1308
1418
  }
1309
1419
  /**
1310
1420
  * Fetches a node's children and starts reporting changes inside them.
@@ -1312,6 +1422,17 @@ export class CloudBrowser {
1312
1422
  *
1313
1423
  * On an `<iframe>` the one child is the document it hosts, and this call is
1314
1424
  * what starts mirroring that frame.
1425
+ *
1426
+ * An id that is simply unknown is not a failure: the call resolves with an
1427
+ * empty result.
1428
+ *
1429
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of the
1430
+ * one it has; start a mirror first, and after a resync fetch the current tree
1431
+ * before addressing nodes again
1432
+ * @throws mirror_failed - the subtree could not be serialized, usually a
1433
+ * document that went away mid-read
1434
+ *
1435
+ * @see {@link CommandError} for reading the code off the rejection
1315
1436
  */
1316
1437
  async getDomChildren(backendNodeId, frameId = "", depth) {
1317
1438
  const req = create(GetDomChildrenRequestSchema, {
@@ -1324,11 +1445,19 @@ export class CloudBrowser {
1324
1445
  if (depth !== undefined)
1325
1446
  req.depth = depth;
1326
1447
  const resp = await this.client.getDomChildren(req);
1448
+ throwCommandError("getDomChildren", resp.error);
1327
1449
  return { children: resp.children, seq: Number(resp.seq) };
1328
1450
  }
1329
1451
  /**
1330
1452
  * Stops reporting changes inside a node, and inside any frame below it.
1331
1453
  * {@link DomMirror.collapse} calls this.
1454
+ *
1455
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of
1456
+ * the one it has; this is what replaying ids from a tree that has since
1457
+ * been resynced looks like, so fetch the current tree and address the node
1458
+ * again
1459
+ *
1460
+ * @see {@link CommandError} for reading the code off the rejection
1332
1461
  */
1333
1462
  async releaseDomSubtree(backendNodeId, frameId = "") {
1334
1463
  const req = create(ReleaseDomSubtreeRequestSchema, {
@@ -1338,13 +1467,24 @@ export class CloudBrowser {
1338
1467
  });
1339
1468
  if (frameId)
1340
1469
  req.frameId = frameId;
1341
- await this.client.releaseDomSubtree(req);
1470
+ const res = await this.client.releaseDomSubtree(req);
1471
+ throwCommandError("releaseDomSubtree", res.error);
1342
1472
  }
1343
1473
  /**
1344
1474
  * Returns the chain from the main document down to a node, each ancestor
1345
1475
  * with its own children, crossing into frames where it has to and starting
1346
1476
  * the ones it passes through. {@link DomMirror.reveal} calls this and
1347
1477
  * splices it in.
1478
+ *
1479
+ * An id that is simply unknown is not a failure: the call resolves with an
1480
+ * empty result.
1481
+ *
1482
+ * @throws not_mirrored - the page has no mirror, or frameId is not part of the
1483
+ * one it has
1484
+ * @throws mirror_failed - the path could not be serialized, usually a document
1485
+ * that went away mid-read
1486
+ *
1487
+ * @see {@link CommandError} for reading the code off the rejection
1348
1488
  */
1349
1489
  async revealDomNode(backendNodeId, frameId = "") {
1350
1490
  const req = create(RevealDomNodeRequestSchema, {
@@ -1355,6 +1495,7 @@ export class CloudBrowser {
1355
1495
  if (frameId)
1356
1496
  req.frameId = frameId;
1357
1497
  const resp = await this.client.revealDomNode(req);
1498
+ throwCommandError("revealDomNode", resp.error);
1358
1499
  return { path: resp.path, seq: Number(resp.seq) };
1359
1500
  }
1360
1501
  /**
@@ -1366,6 +1507,10 @@ export class CloudBrowser {
1366
1507
  * whole tree just to hash it. The two answer different questions: a hash
1367
1508
  * compares content, a revision only says whether this document moved since
1368
1509
  * you last asked.
1510
+ *
1511
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1512
+ * a broken connection. This call has no semantic failure of its own, so there
1513
+ * are no error codes to branch on.
1369
1514
  */
1370
1515
  async getDomRevision(frameId = "") {
1371
1516
  const req = create(GetDomRevisionRequestSchema, {
@@ -1375,6 +1520,7 @@ export class CloudBrowser {
1375
1520
  if (frameId)
1376
1521
  req.frameId = frameId;
1377
1522
  const resp = await this.client.getDomRevision(req);
1523
+ throwCommandError("getDomRevision", resp.error);
1378
1524
  return Number(resp.revision);
1379
1525
  }
1380
1526
  // ──────────────────────────────────────────────────────────────────
@@ -1385,7 +1531,9 @@ export class CloudBrowser {
1385
1531
  *
1386
1532
  * @returns CookieParam[], one per cookie in the context
1387
1533
  *
1388
- * @throws UNKNOWN_ERROR - the cookies could not be read
1534
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1535
+ * a broken connection. This call has no semantic failure of its own, so there
1536
+ * are no error codes to branch on.
1389
1537
  *
1390
1538
  * @example
1391
1539
  * const cookies = await browser.getCookies();
@@ -1396,6 +1544,7 @@ export class CloudBrowser {
1396
1544
  sessionId: this.sessionId,
1397
1545
  apiKey: this.apiKey,
1398
1546
  }));
1547
+ throwCommandError("getCookies", resp.error);
1399
1548
  return resp.cookies.map(cookieParamFromProto);
1400
1549
  }
1401
1550
  /**
@@ -1406,7 +1555,9 @@ export class CloudBrowser {
1406
1555
  *
1407
1556
  * @param cookies - cookies to write; empty array is a no-op
1408
1557
  *
1409
- * @throws UNKNOWN_ERROR - the cookies could not be written
1558
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1559
+ * a broken connection. This call has no semantic failure of its own, so there
1560
+ * are no error codes to branch on.
1410
1561
  *
1411
1562
  * @example
1412
1563
  * await browser.setCookies([
@@ -1414,25 +1565,29 @@ export class CloudBrowser {
1414
1565
  * ]);
1415
1566
  */
1416
1567
  async setCookies(cookies) {
1417
- await this.client.setCookies(create(SetCookiesRequestSchema, {
1568
+ const res = await this.client.setCookies(create(SetCookiesRequestSchema, {
1418
1569
  sessionId: this.sessionId,
1419
1570
  apiKey: this.apiKey,
1420
1571
  cookies: cookieParamsToProto(cookies),
1421
1572
  }));
1573
+ throwCommandError("setCookies", res.error);
1422
1574
  }
1423
1575
  /**
1424
1576
  * Deletes every cookie in the browser context.
1425
1577
  *
1426
- * @throws UNKNOWN_ERROR - the cookies could not be cleared
1578
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1579
+ * a broken connection. This call has no semantic failure of its own, so there
1580
+ * are no error codes to branch on.
1427
1581
  *
1428
1582
  * @example
1429
1583
  * await browser.clearCookies();
1430
1584
  */
1431
1585
  async clearCookies() {
1432
- await this.client.clearCookies(create(ClearCookiesRequestSchema, {
1586
+ const res = await this.client.clearCookies(create(ClearCookiesRequestSchema, {
1433
1587
  sessionId: this.sessionId,
1434
1588
  apiKey: this.apiKey,
1435
1589
  }));
1590
+ throwCommandError("clearCookies", res.error);
1436
1591
  }
1437
1592
  // ──────────────────────────────────────────────────────────────────
1438
1593
  // Storage (localStorage)
@@ -1450,7 +1605,9 @@ export class CloudBrowser {
1450
1605
  *
1451
1606
  * @returns StorageOriginEntry[], one per origin with localStorage data
1452
1607
  *
1453
- * @throws UNKNOWN_ERROR - the storage could not be read
1608
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1609
+ * a broken connection. This call has no semantic failure of its own, so there
1610
+ * are no error codes to branch on.
1454
1611
  *
1455
1612
  * @example
1456
1613
  * const storage = await browser.getStorage();
@@ -1466,6 +1623,7 @@ export class CloudBrowser {
1466
1623
  if (origin)
1467
1624
  req.origin = origin;
1468
1625
  const resp = await this.client.getStorage(req);
1626
+ throwCommandError("getStorage", resp.error);
1469
1627
  return resp.storage.map(storageEntryFromProto);
1470
1628
  }
1471
1629
  /**
@@ -1479,7 +1637,9 @@ export class CloudBrowser {
1479
1637
  *
1480
1638
  * @param storage - entries to write, grouped by origin
1481
1639
  *
1482
- * @throws UNKNOWN_ERROR - the storage could not be written
1640
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1641
+ * a broken connection. This call has no semantic failure of its own, so there
1642
+ * are no error codes to branch on.
1483
1643
  *
1484
1644
  * @example
1485
1645
  * await browser.setStorage([
@@ -1493,11 +1653,12 @@ export class CloudBrowser {
1493
1653
  * ]);
1494
1654
  */
1495
1655
  async setStorage(storage) {
1496
- await this.client.setStorage(create(SetStorageRequestSchema, {
1656
+ const res = await this.client.setStorage(create(SetStorageRequestSchema, {
1497
1657
  sessionId: this.sessionId,
1498
1658
  apiKey: this.apiKey,
1499
1659
  storage: storageEntriesToProto(storage),
1500
1660
  }));
1661
+ throwCommandError("setStorage", res.error);
1501
1662
  }
1502
1663
  /**
1503
1664
  * Deletes localStorage in the browser context.
@@ -1505,7 +1666,9 @@ export class CloudBrowser {
1505
1666
  * @param origin - if set, only this origin's storage is deleted
1506
1667
  * (e.g. "https://example.com"); omit to delete all origins
1507
1668
  *
1508
- * @throws UNKNOWN_ERROR - the storage could not be cleared
1669
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1670
+ * a broken connection. This call has no semantic failure of its own, so there
1671
+ * are no error codes to branch on.
1509
1672
  *
1510
1673
  * @example
1511
1674
  * // Wipe one origin.
@@ -1521,7 +1684,8 @@ export class CloudBrowser {
1521
1684
  });
1522
1685
  if (origin)
1523
1686
  req.origin = origin;
1524
- await this.client.clearStorage(req);
1687
+ const res = await this.client.clearStorage(req);
1688
+ throwCommandError("clearStorage", res.error);
1525
1689
  }
1526
1690
  // ──────────────────────────────────────────────────────────────────
1527
1691
  // Auth / DBSC (portable signed-in persona)
@@ -1536,7 +1700,9 @@ export class CloudBrowser {
1536
1700
  *
1537
1701
  * @returns AuthSession, or undefined when there is nothing to export
1538
1702
  *
1539
- * @throws UNKNOWN_ERROR - the auth session could not be read
1703
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1704
+ * a broken connection. This call has no semantic failure of its own, so there
1705
+ * are no error codes to branch on.
1540
1706
  *
1541
1707
  * @example
1542
1708
  * const auth = await browser.getAuthSession();
@@ -1547,6 +1713,7 @@ export class CloudBrowser {
1547
1713
  sessionId: this.sessionId,
1548
1714
  apiKey: this.apiKey,
1549
1715
  }));
1716
+ throwCommandError("getAuthSession", resp.error);
1550
1717
  return resp.session ? authSessionFromProto(resp.session) : undefined;
1551
1718
  }
1552
1719
  /**
@@ -1558,18 +1725,21 @@ export class CloudBrowser {
1558
1725
  *
1559
1726
  * @param session - session as returned by getAuthSession()
1560
1727
  *
1561
- * @throws UNKNOWN_ERROR - the auth session could not be written
1728
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1729
+ * a broken connection. This call has no semantic failure of its own, so there
1730
+ * are no error codes to branch on.
1562
1731
  *
1563
1732
  * @example
1564
1733
  * await browser.setAuthSession(saved);
1565
1734
  * await browser.navigate("https://mail.google.com");
1566
1735
  */
1567
1736
  async setAuthSession(session) {
1568
- await this.client.setAuthSession(create(SetAuthSessionRequestSchema, {
1737
+ const res = await this.client.setAuthSession(create(SetAuthSessionRequestSchema, {
1569
1738
  sessionId: this.sessionId,
1570
1739
  apiKey: this.apiKey,
1571
1740
  session: authSessionToProto(session),
1572
1741
  }));
1742
+ throwCommandError("setAuthSession", res.error);
1573
1743
  }
1574
1744
  // ──────────────────────────────────────────────────────────────────
1575
1745
  // Devtools / live-UI helpers
@@ -1589,7 +1759,9 @@ export class CloudBrowser {
1589
1759
  * @returns InspectResult with the resolved backendNodeId, frameId, tag
1590
1760
  * name, trimmed textContent, visibility and bounds
1591
1761
  *
1592
- * @throws UNKNOWN_ERROR - the hit-test failed
1762
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1763
+ * a broken connection. This call has no semantic failure of its own, so there
1764
+ * are no error codes to branch on.
1593
1765
  *
1594
1766
  * @example
1595
1767
  * const r = await browser.inspectAtPosition(200, 300);
@@ -1602,6 +1774,7 @@ export class CloudBrowser {
1602
1774
  x,
1603
1775
  y,
1604
1776
  }));
1777
+ throwCommandError("inspectAtPosition", resp.error);
1605
1778
  return {
1606
1779
  backendNodeId: resp.backendNodeId,
1607
1780
  frameId: resp.frameId,
@@ -1621,7 +1794,9 @@ export class CloudBrowser {
1621
1794
  * @param frameId - id of the frame the node lives in; empty string
1622
1795
  * targets the main frame
1623
1796
  *
1624
- * @throws UNKNOWN_ERROR - the highlight could not be applied
1797
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1798
+ * a broken connection. This call has no semantic failure of its own, so there
1799
+ * are no error codes to branch on.
1625
1800
  *
1626
1801
  * @example
1627
1802
  * await browser.highlightNode(res.backendNodeId, res.frameId);
@@ -1634,7 +1809,8 @@ export class CloudBrowser {
1634
1809
  });
1635
1810
  if (frameId)
1636
1811
  req.frameId = frameId;
1637
- await this.client.highlightNode(req);
1812
+ const res = await this.client.highlightNode(req);
1813
+ throwCommandError("highlight", res.error);
1638
1814
  }
1639
1815
  /**
1640
1816
  * Pastes text at the current caret using IME-style input.
@@ -1646,17 +1822,22 @@ export class CloudBrowser {
1646
1822
  *
1647
1823
  * @param text - the text to insert at the caret
1648
1824
  *
1649
- * @throws UNKNOWN_ERROR - the text could not be inserted
1825
+ * @throws no_focus - nothing in the page holds focus, so there is no caret to
1826
+ * insert at; click the field first
1827
+ * @throws busy - another action is already running on this page
1828
+ *
1829
+ * @see {@link CommandError} for reading the code off the rejection
1650
1830
  *
1651
1831
  * @example
1652
1832
  * await browser.insertText("hello world");
1653
1833
  */
1654
1834
  async insertText(text) {
1655
- await this.client.insertText(create(InsertTextRequestSchema, {
1835
+ const res = await this.client.insertText(create(InsertTextRequestSchema, {
1656
1836
  sessionId: this.sessionId,
1657
1837
  apiKey: this.apiKey,
1658
1838
  text,
1659
1839
  }));
1840
+ throwCommandError("insertText", res.error);
1660
1841
  }
1661
1842
  /**
1662
1843
  * Types `text` into the currently focused element as a per-key stream of real
@@ -1678,7 +1859,10 @@ export class CloudBrowser {
1678
1859
  * @param opts - optional: `clearFirst` clears the focused field (Ctrl+A,
1679
1860
  * Delete) before typing
1680
1861
  *
1681
- * @throws UNKNOWN_ERROR - the page/context was torn down mid-stream
1862
+ * `type` has no semantic failure of its own: the keys land wherever focus
1863
+ * happens to be, so there is no target it can miss. Only the page or context
1864
+ * being torn down mid-stream surfaces, and that is a transport failure rather
1865
+ * than a code.
1682
1866
  *
1683
1867
  * @example
1684
1868
  * // OTP field that auto-advances across boxes.
@@ -1693,7 +1877,7 @@ export class CloudBrowser {
1693
1877
  });
1694
1878
  if (opts?.clearFirst)
1695
1879
  req.clearFirst = true;
1696
- await this.client.type(req);
1880
+ throwCommandError("type", (await this.client.type(req)).error);
1697
1881
  }
1698
1882
  /**
1699
1883
  * Fires a single key-down event.
@@ -1708,7 +1892,11 @@ export class CloudBrowser {
1708
1892
  * `modifiers` (bit-flag: Alt=1, Ctrl=2, Meta=4, Shift=8),
1709
1893
  * `location` (0=standard, 1=left, 2=right, 3=numpad)
1710
1894
  *
1711
- * @throws UNKNOWN_ERROR - the event could not be dispatched
1895
+ * @throws no_focus - nothing in the page holds focus, so the key has nowhere
1896
+ * to go; click the field first
1897
+ * @throws busy - another action is already running on this page
1898
+ *
1899
+ * @see {@link CommandError} for reading the code off the rejection
1712
1900
  *
1713
1901
  * @example
1714
1902
  * // Ctrl+A
@@ -1727,7 +1915,8 @@ export class CloudBrowser {
1727
1915
  req.modifiers = opts.modifiers;
1728
1916
  if (opts?.location !== undefined)
1729
1917
  req.location = opts.location;
1730
- await this.client.pressKey(req);
1918
+ const res = await this.client.pressKey(req);
1919
+ throwCommandError("pressKey", res.error);
1731
1920
  }
1732
1921
  /**
1733
1922
  * Fires a single key-up event.
@@ -1753,7 +1942,8 @@ export class CloudBrowser {
1753
1942
  req.modifiers = opts.modifiers;
1754
1943
  if (opts?.location !== undefined)
1755
1944
  req.location = opts.location;
1756
- await this.client.releaseKey(req);
1945
+ const res = await this.client.releaseKey(req);
1946
+ throwCommandError("releaseKey", res.error);
1757
1947
  }
1758
1948
  /**
1759
1949
  * Returns the current text selection.
@@ -1764,7 +1954,9 @@ export class CloudBrowser {
1764
1954
  *
1765
1955
  * @returns the selected text, or `""` when nothing is selected
1766
1956
  *
1767
- * @throws UNKNOWN_ERROR - the selection could not be read
1957
+ * Rejects only on a transport failure - a dead session, a page that is gone,
1958
+ * a broken connection. This call has no semantic failure of its own, so there
1959
+ * are no error codes to branch on.
1768
1960
  *
1769
1961
  * @example
1770
1962
  * const sel = await browser.getSelection();
@@ -1775,6 +1967,7 @@ export class CloudBrowser {
1775
1967
  sessionId: this.sessionId,
1776
1968
  apiKey: this.apiKey,
1777
1969
  }));
1970
+ throwCommandError("getSelection", resp.error);
1778
1971
  return resp.text;
1779
1972
  }
1780
1973
  // ──────────────────────────────────────────────────────────────────
@@ -1795,8 +1988,10 @@ export class CloudBrowser {
1795
1988
  *
1796
1989
  * @returns empty string on success — the solution is applied server-side
1797
1990
  *
1798
- * @throws UNKNOWN_ERROR - no captcha appeared within timeoutMs, or the
1799
- * detected captcha could not be solved within retryAmount attempts
1991
+ * Rejects when no captcha appeared within imeoutMs, or when the one that did
1992
+ * could not be solved within
1993
+ etryAmount attempts. Neither carries a code:
1994
+ * solving runs outside the page, so there is no per-command code set here.
1800
1995
  *
1801
1996
  * @example
1802
1997
  * await browser.solveCaptcha({ retryAmount: 2 });
@@ -1824,7 +2019,8 @@ export class CloudBrowser {
1824
2019
  *
1825
2020
  * @returns the ICE servers for the client `RTCPeerConnection`
1826
2021
  *
1827
- * @throws UNKNOWN_ERROR - TURN is not configured on the server
2022
+ * Rejects when TURN is not configured on the server. That is a deployment
2023
+ * condition rather than a per-call outcome, so it carries no code.
1828
2024
  *
1829
2025
  * @example
1830
2026
  * const ice = await browser.getStreamConfig();
@@ -1851,8 +2047,16 @@ export class CloudBrowser {
1851
2047
  * @returns the SDP answer to apply as the remote description, plus the
1852
2048
  * viewport to map input coordinates into
1853
2049
  *
1854
- * @throws UNKNOWN_ERROR - the offer was empty, TURN is unconfigured, or the
1855
- * browser could not negotiate the stream
2050
+ * @throws already_active - a stream is already running on this session; stop it
2051
+ * before starting another
2052
+ * @throws negotiation_failed - the browser could not agree on a connection. The
2053
+ * message carries the negotiator's own diagnostic, which is usually where the
2054
+ * actual cause is
2055
+ *
2056
+ * An empty offer or an unconfigured TURN setup is a caller mistake rather than
2057
+ * an outcome, and rejects separately.
2058
+ *
2059
+ * @see {@link CommandError} for reading the code off the rejection
1856
2060
  *
1857
2061
  * @example
1858
2062
  * const { answerSdp, viewport } = await browser.startStream(offer.sdp);
@@ -1864,6 +2068,7 @@ export class CloudBrowser {
1864
2068
  apiKey: this.apiKey,
1865
2069
  offerSdp,
1866
2070
  }));
2071
+ throwCommandError("startStream", resp.error);
1867
2072
  return {
1868
2073
  answerSdp: resp.answerSdp,
1869
2074
  viewport: resp.viewport
@@ -1875,25 +2080,27 @@ export class CloudBrowser {
1875
2080
  * Tears down the live video stream for the session's page. Safe to call even
1876
2081
  * if no stream is running.
1877
2082
  *
1878
- * @throws UNKNOWN_ERROR - the stream could not be stopped
2083
+ * Rejects only on a transport failure. Stopping a stream that is not running is
2084
+ * a no-op rather than a failure, so there are no error codes to branch on.
1879
2085
  *
1880
2086
  * @example
1881
2087
  * await browser.stopStream();
1882
2088
  */
1883
2089
  async stopStream() {
1884
- await this.client.stopStream(create(StopStreamRequestSchema, {
2090
+ const res = await this.client.stopStream(create(StopStreamRequestSchema, {
1885
2091
  sessionId: this.sessionId,
1886
2092
  apiKey: this.apiKey,
1887
2093
  }));
2094
+ throwCommandError("stopStream", res.error);
1888
2095
  }
1889
2096
  // ──────────────────────────────────────────────────────────────────
1890
2097
  // Reactions
1891
2098
  // ──────────────────────────────────────────────────────────────────
1892
2099
  /**
1893
- * Registers a one-shot "reaction": a background poller (one shared loop per
1894
- * page) watches for the `match` locator and, as soon as it matches, clicks it
1895
- * with the full smart-click machinery (scroll, human path, occlusion gate,
1896
- * evade) — then removes itself. The poller yields to any in-flight input
2100
+ * Registers a one-shot "reaction": the browser watches for the `match`
2101
+ * locator in the background and, as soon as it matches, clicks it the same
2102
+ * way {@link CloudBrowser.click} does (scroll, human path, occlusion check) —
2103
+ * then removes itself. The reaction yields to any in-flight input
1897
2104
  * action and only fires while the pointer is idle, so a reaction naturally
1898
2105
  * slots into the gaps of a retrying foreground action (e.g. it dismisses a
1899
2106
  * newsletter modal blocking a {@link CloudBrowser.click}, after which the
@@ -1951,6 +2158,7 @@ export class CloudBrowser {
1951
2158
  if (opts?.intervalMs)
1952
2159
  req.interval = opts.intervalMs;
1953
2160
  const resp = await this.client.addReaction(req);
2161
+ throwCommandError("addReaction", resp.error);
1954
2162
  return resp.reactionId;
1955
2163
  }
1956
2164
  /**
@@ -1961,6 +2169,10 @@ export class CloudBrowser {
1961
2169
  *
1962
2170
  * @returns true if a pending reaction with this id existed and was removed
1963
2171
  *
2172
+ * Removing an id that is not registered is a no-op rather than an error, so the
2173
+ * returned boolean - not a rejection - is what tells you whether anything was
2174
+ * there. Rejects only on a transport failure.
2175
+ *
1964
2176
  * @example
1965
2177
  * const removed = await browser.removeReaction(id);
1966
2178
  */
@@ -1970,6 +2182,7 @@ export class CloudBrowser {
1970
2182
  apiKey: this.apiKey,
1971
2183
  reactionId,
1972
2184
  }));
2185
+ throwCommandError("removeReaction", resp.error);
1973
2186
  return resp.removed;
1974
2187
  }
1975
2188
  /**
@@ -1978,6 +2191,10 @@ export class CloudBrowser {
1978
2191
  *
1979
2192
  * @returns the pending reactions for the page
1980
2193
  *
2194
+ * Rejects only on a transport failure - a dead session, a page that is gone,
2195
+ * a broken connection. This call has no semantic failure of its own, so there
2196
+ * are no error codes to branch on.
2197
+ *
1981
2198
  * @example
1982
2199
  * const pending = await browser.listReactions();
1983
2200
  * for (const r of pending) console.log(r.reactionId, r.matchSelector);
@@ -1987,6 +2204,7 @@ export class CloudBrowser {
1987
2204
  sessionId: this.sessionId,
1988
2205
  apiKey: this.apiKey,
1989
2206
  }));
2207
+ throwCommandError("listReactions", resp.error);
1990
2208
  return resp.reactions.map((r) => ({
1991
2209
  reactionId: r.reactionId,
1992
2210
  matchSelector: r.matchSelector ?? "",
@@ -2003,12 +2221,14 @@ export class CloudBrowser {
2003
2221
  /**
2004
2222
  * Runs `source` in the session's browser and waits for it to finish.
2005
2223
  *
2006
- * The script executes in a V8 isolate inside the browser process, not in the
2007
- * page, and reaches the same operations this SDK exposes through a `browser`
2008
- * object it is handed. The difference is cost: each call is a function call in
2009
- * the browser rather than a network round trip, so work that is chatty by
2010
- * nature — polling for a selector, walking a list, following pagination —
2011
- * runs in microseconds per step instead of tens of milliseconds.
2224
+ * The script runs beside the browser, in a V8 isolate of its own rather than
2225
+ * in the page, and reaches the document through the engine: a cross-origin
2226
+ * `<iframe>` is read as plain `contentDocument` with no frame ids anywhere,
2227
+ * values come back as live objects it can assign to rather than snapshots, an
2228
+ * element can be handed straight to `browser.click`, and the page sees nothing
2229
+ * injected. Steps cost microseconds rather than network round trips, so work
2230
+ * that is chatty by nature — polling for a selector, walking a list,
2231
+ * following pagination — is affordable there. A guide for it is still to come.
2012
2232
  *
2013
2233
  * This waits for as long as the script runs, and cannot be bounded: the run
2014
2234
  * id needed to cancel only arrives with the reply. Use
@@ -2021,7 +2241,10 @@ export class CloudBrowser {
2021
2241
  * @returns the return value and the script's whole console output. A script
2022
2242
  * that threw is reported as `success: false`, not as a rejection
2023
2243
  *
2024
- * @throws UNKNOWN_ERROR - the script could not be delivered to the browser
2244
+ * Rejects only on a transport failure. A script that fails to compile or throws
2245
+ * is not a rejection: the returned result has success: false and
2246
+ esult holds
2247
+ * the message, so a broken script stays distinguishable from a broken connection.
2025
2248
  *
2026
2249
  * @example
2027
2250
  * const result = await browser.runScript(`
@@ -2069,7 +2292,8 @@ export class CloudBrowser {
2069
2292
  *
2070
2293
  * @returns ScriptRun handle for awaiting or cancelling the run
2071
2294
  *
2072
- * @throws UNKNOWN_ERROR - the run could not be started
2295
+ * Rejects only on a transport failure: a script that fails to compile or throws
2296
+ * surfaces on the run itself rather than here.
2073
2297
  *
2074
2298
  * @example
2075
2299
  * const run = await browser.startScript(source, (ev) => {
@@ -2154,7 +2378,8 @@ export class CloudBrowser {
2154
2378
  *
2155
2379
  * @returns ScriptFollow handle for stopping the subscription
2156
2380
  *
2157
- * @throws UNKNOWN_ERROR - the subscription could not be opened
2381
+ * Rejects only on a transport failure: opening the subscription has no semantic
2382
+ * failure of its own.
2158
2383
  *
2159
2384
  * @example
2160
2385
  * const follow = await browser.followScript(runId, (ev) => {
@@ -2187,7 +2412,9 @@ export class CloudBrowser {
2187
2412
  * @returns how many runs were cancelled; 0 when the id named nothing in
2188
2413
  * flight
2189
2414
  *
2190
- * @throws UNKNOWN_ERROR - the cancel could not be delivered
2415
+ * Rejects only on a transport failure. Cancelling runs that have already
2416
+ * finished, or none at all, is a no-op - read the returned count to learn how
2417
+ * many were actually stopped.
2191
2418
  *
2192
2419
  * @example
2193
2420
  * await browser.stopScripts(""); // abandon everything running
@@ -2210,7 +2437,9 @@ export class CloudBrowser {
2210
2437
  *
2211
2438
  * @returns one entry per run still executing
2212
2439
  *
2213
- * @throws UNKNOWN_ERROR - the session could not be queried
2440
+ * Rejects only on a transport failure - a dead session, a broken connection.
2441
+ * This call has no semantic failure of its own, so there are no error codes to
2442
+ * branch on.
2214
2443
  *
2215
2444
  * @example
2216
2445
  * for (const run of await browser.listScriptRuns()) {