@bisibility/sdk 0.1.0 → 0.1.1

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
@@ -1,6 +1,12 @@
1
1
  import { BisibilityApiError, BisibilityConfigurationError, BisibilityNetworkError, BisibilityResponseError, } from "./errors.js";
2
+ import { iterateCursorPagination } from "./pagination.js";
3
+ import { SDK_VERSION } from "./version.js";
2
4
  const DEFAULT_BASE_URL = "https://bisibility.com/api/v1";
5
+ const DEFAULT_MAX_RETRIES = 2;
6
+ const DEFAULT_TIMEOUT = 30_000;
7
+ const CLIENT_ID = `bisibility-sdk-ts/${SDK_VERSION}`;
3
8
  const RELATIVE_BASE_ORIGIN = "https://bisibility.local";
9
+ const IDEMPOTENT_METHODS = new Set(["DELETE", "GET", "HEAD", "PUT"]);
4
10
  function isAbsoluteUrl(value) {
5
11
  return /^[a-z][a-z\d+\-.]*:/i.test(value);
6
12
  }
@@ -69,7 +75,7 @@ function problemFromJson(value) {
69
75
  return problem;
70
76
  }
71
77
  function composedSignal(signal, timeout) {
72
- if (timeout === undefined) {
78
+ if (timeout === null || timeout === undefined) {
73
79
  return signal;
74
80
  }
75
81
  if (!Number.isFinite(timeout) || timeout <= 0) {
@@ -92,6 +98,53 @@ function composedSignal(signal, timeout) {
92
98
  }
93
99
  return controller.signal;
94
100
  }
101
+ function validatedMaxRetries(value) {
102
+ const maxRetries = value ?? DEFAULT_MAX_RETRIES;
103
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
104
+ throw new BisibilityConfigurationError("maxRetries must be a non-negative integer.");
105
+ }
106
+ return maxRetries;
107
+ }
108
+ function validatedTimeout(value) {
109
+ if (value === null || value === undefined) {
110
+ return value;
111
+ }
112
+ if (!Number.isFinite(value) || value <= 0) {
113
+ throw new BisibilityConfigurationError("timeout must be a positive finite number or null.");
114
+ }
115
+ return value;
116
+ }
117
+ function retryBackoffMs(attempt) {
118
+ return Math.min(500 * 2 ** attempt, 8_000);
119
+ }
120
+ function sleep(milliseconds, signal) {
121
+ if (signal?.aborted) {
122
+ return Promise.reject(signal.reason);
123
+ }
124
+ return new Promise((resolve, reject) => {
125
+ const timer = setTimeout(() => {
126
+ signal?.removeEventListener("abort", onAbort);
127
+ resolve();
128
+ }, milliseconds);
129
+ const onAbort = () => {
130
+ clearTimeout(timer);
131
+ reject(signal?.reason);
132
+ };
133
+ signal?.addEventListener("abort", onAbort, { once: true });
134
+ });
135
+ }
136
+ async function sleepForRetry(milliseconds, signal, method, url) {
137
+ try {
138
+ await sleep(milliseconds, signal);
139
+ }
140
+ catch (cause) {
141
+ throw new BisibilityNetworkError("Network error while calling the Bisibility API.", {
142
+ cause,
143
+ method,
144
+ url,
145
+ });
146
+ }
147
+ }
95
148
  function mergeHeaders(first, second) {
96
149
  const headers = new Headers(first);
97
150
  if (second) {
@@ -105,14 +158,18 @@ export class BisibilityClient {
105
158
  #apiKey;
106
159
  #defaultHeaders;
107
160
  #fetchImpl;
161
+ #maxRetries;
108
162
  #projectId;
163
+ #timeout;
109
164
  baseUrl;
110
165
  constructor(config = {}) {
111
166
  this.#apiKey = config.apiKey;
112
167
  this.baseUrl = normalizeBaseUrl(config.baseUrl);
113
168
  this.#defaultHeaders = config.headers;
114
169
  this.#fetchImpl = config.fetch;
170
+ this.#maxRetries = validatedMaxRetries(config.maxRetries);
115
171
  this.#projectId = config.projectId;
172
+ this.#timeout = validatedTimeout(config.timeout);
116
173
  }
117
174
  getHealth(options) {
118
175
  return this.request("GET", "/health", { ...options, auth: false });
@@ -154,6 +211,16 @@ export class BisibilityClient {
154
211
  },
155
212
  });
156
213
  }
214
+ searchLocations(input, options) {
215
+ return this.request("GET", "/locations/search", {
216
+ ...options,
217
+ query: {
218
+ country: input.country,
219
+ limit: input.limit,
220
+ q: input.q,
221
+ },
222
+ });
223
+ }
157
224
  getMe(options) {
158
225
  return this.request("GET", "/me", options);
159
226
  }
@@ -166,11 +233,7 @@ export class BisibilityClient {
166
233
  createMyToken(input, options) {
167
234
  return this.request("POST", "/me/tokens", {
168
235
  ...options,
169
- body: {
170
- expires_in_days: input.expiresInDays,
171
- name: input.name,
172
- scope: input.scope,
173
- },
236
+ body: input,
174
237
  });
175
238
  }
176
239
  revokeMyToken(tokenId, options) {
@@ -182,12 +245,7 @@ export class BisibilityClient {
182
245
  createProject(input, options) {
183
246
  return this.request("POST", "/projects", {
184
247
  ...options,
185
- body: {
186
- defaults: input.defaults,
187
- domain: input.domain,
188
- name: input.name,
189
- tracking_scope: input.trackingScope,
190
- },
248
+ body: input,
191
249
  });
192
250
  }
193
251
  getProject(projectId, options) {
@@ -215,6 +273,9 @@ export class BisibilityClient {
215
273
  },
216
274
  });
217
275
  }
276
+ iterateApiKeys(options = {}, requestOptions) {
277
+ return iterateCursorPagination((pageOptions) => this.listApiKeys(pageOptions, requestOptions), options);
278
+ }
218
279
  createApiKey(input, options) {
219
280
  return this.request("POST", "/api-keys", { ...options, body: input });
220
281
  }
@@ -231,6 +292,9 @@ export class BisibilityClient {
231
292
  },
232
293
  });
233
294
  }
295
+ iterateProjectApiKeys(projectId, options = {}, requestOptions) {
296
+ return iterateCursorPagination((pageOptions) => this.listProjectApiKeys(projectId, pageOptions, requestOptions), options);
297
+ }
234
298
  createProjectApiKey(projectId, input, options) {
235
299
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/api-keys`, { ...options, body: input });
236
300
  }
@@ -244,6 +308,9 @@ export class BisibilityClient {
244
308
  },
245
309
  });
246
310
  }
311
+ iterateWebhooks(projectId, options = {}, requestOptions) {
312
+ return iterateCursorPagination((pageOptions) => this.listWebhooks(projectId, pageOptions, requestOptions), options);
313
+ }
247
314
  createWebhook(projectId, input, options) {
248
315
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/webhooks`, {
249
316
  ...options,
@@ -275,12 +342,12 @@ export class BisibilityClient {
275
342
  },
276
343
  });
277
344
  }
345
+ iterateKeywords(projectId, options = {}, requestOptions) {
346
+ return iterateCursorPagination((pageOptions) => this.listKeywords(projectId, pageOptions, requestOptions), options);
347
+ }
278
348
  addKeywords(projectId, input, options) {
279
349
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/keywords`, { ...options, body: input });
280
350
  }
281
- createKeywords(projectId, input, options) {
282
- return this.addKeywords(projectId, input, options);
283
- }
284
351
  getKeyword(keywordId, options) {
285
352
  return this.request("GET", `/keywords/${encodedPathSegment(keywordId)}`, options);
286
353
  }
@@ -294,11 +361,72 @@ export class BisibilityClient {
294
361
  return this.updateKeyword(keywordId, { target_url: targetUrl }, options);
295
362
  }
296
363
  deleteKeyword(keywordId, options) {
297
- return this.request("DELETE", `/keywords/${encodedPathSegment(keywordId)}`, options);
364
+ return this.requestOrUndefined("DELETE", `/keywords/${encodedPathSegment(keywordId)}`, options);
298
365
  }
299
366
  bulkUpdateKeywords(input, options) {
300
367
  return this.request("POST", "/keywords/bulk", { ...options, body: input });
301
368
  }
369
+ listRankedKeywordSuggestions(projectId, options, requestOptions) {
370
+ const filters = options ?? {};
371
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/ranked-keyword-suggestions`, {
372
+ ...requestOptions,
373
+ query: {
374
+ connection_id: filters.connectionId,
375
+ fresh: filters.fresh,
376
+ limit: filters.limit,
377
+ offset: filters.offset,
378
+ },
379
+ });
380
+ }
381
+ /**
382
+ * Research keywords from one seed. This operation requires API write scope because a cache
383
+ * miss can spend the project's provider budget. Use `estimateOnly` for a free dry run.
384
+ */
385
+ researchKeywords(projectId, options, requestOptions) {
386
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/keyword-research`, {
387
+ ...requestOptions,
388
+ query: {
389
+ connection_id: options.connectionId,
390
+ estimate_only: options.estimateOnly,
391
+ fresh: options.fresh,
392
+ include_clickstream: options.includeClickstream,
393
+ max_cost_cents: options.maxCostCents,
394
+ mode: options.mode,
395
+ result_limit: options.resultLimit,
396
+ seed: options.seed,
397
+ },
398
+ });
399
+ }
400
+ /**
401
+ * Hydrate keyword metrics. This operation requires API write scope because cache misses can
402
+ * spend the project's provider budget. Use `estimate_only` for a free dry run.
403
+ */
404
+ getKeywordMetrics(projectId, input, options) {
405
+ return this.request("POST", `/projects/${encodedPathSegment(projectId)}/keyword-metrics`, { ...options, body: input });
406
+ }
407
+ exportRankHistory(projectId, options = {}, requestOptions) {
408
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/exports/rank-history`, {
409
+ ...requestOptions,
410
+ parseAs: options.format === "csv" ? "text" : "json",
411
+ query: {
412
+ cursor: "cursor" in options ? options.cursor : undefined,
413
+ format: options.format,
414
+ granularity: options.granularity,
415
+ keyword_id: options.keywordIds,
416
+ limit: "limit" in options ? options.limit : undefined,
417
+ range: options.range,
418
+ },
419
+ });
420
+ }
421
+ iterateRankHistoryExport(projectId, options = {}, requestOptions) {
422
+ return iterateCursorPagination((pageOptions) => this.exportRankHistory(projectId, { ...options, ...pageOptions, format: "json" }, requestOptions), options);
423
+ }
424
+ listSitemapMonitors(projectId, options) {
425
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/sitemap-monitors`, options);
426
+ }
427
+ updateSitemapMonitor(projectId, monitorId, input, options) {
428
+ return this.request("PATCH", `/projects/${encodedPathSegment(projectId)}/sitemap-monitors/${encodedPathSegment(monitorId)}`, { ...options, body: input });
429
+ }
302
430
  listRankChecks(keywordId, options, requestOptions) {
303
431
  const filters = options ?? {};
304
432
  return this.request("GET", `/keywords/${encodedPathSegment(keywordId)}/rank-checks`, {
@@ -312,6 +440,9 @@ export class BisibilityClient {
312
440
  },
313
441
  });
314
442
  }
443
+ iterateRankChecks(keywordId, options = {}, requestOptions) {
444
+ return iterateCursorPagination((pageOptions) => this.listRankChecks(keywordId, pageOptions, requestOptions), options);
445
+ }
315
446
  runRankCheck(keywordId, input, options) {
316
447
  const { async: runAsync, ...requestOptions } = options ?? {};
317
448
  const body = input && Object.keys(input).length ? input : undefined;
@@ -341,6 +472,36 @@ export class BisibilityClient {
341
472
  },
342
473
  });
343
474
  }
475
+ iterateSignals(projectId, options = {}, requestOptions) {
476
+ return iterateCursorPagination((pageOptions) => this.listSignals(projectId, pageOptions, requestOptions), options);
477
+ }
478
+ listTrafficSnapshots(projectId, options, requestOptions) {
479
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/analytics/traffic-snapshots`, {
480
+ ...requestOptions,
481
+ query: {
482
+ end_date: options.endDate,
483
+ limit: options.limit,
484
+ offset: options.offset,
485
+ path: options.paths,
486
+ start_date: options.startDate,
487
+ },
488
+ });
489
+ }
490
+ listSearchPerformanceQueryStats(projectId, options, requestOptions) {
491
+ return this.request("GET", `/projects/${encodedPathSegment(projectId)}/analytics/query-stats`, {
492
+ ...requestOptions,
493
+ query: {
494
+ connection_id: options.connectionId,
495
+ end_date: options.endDate,
496
+ limit: options.limit,
497
+ query: options.query,
498
+ start_date: options.startDate,
499
+ },
500
+ });
501
+ }
502
+ syncProjectTraffic(projectId, options) {
503
+ return this.request("POST", `/projects/${encodedPathSegment(projectId)}/analytics/sync`, options);
504
+ }
344
505
  listAlertRules(projectId, options, requestOptions) {
345
506
  const pagination = options ?? {};
346
507
  return this.request("GET", `/projects/${encodedPathSegment(projectId)}/alert-rules`, {
@@ -351,6 +512,9 @@ export class BisibilityClient {
351
512
  },
352
513
  });
353
514
  }
515
+ iterateAlertRules(projectId, options = {}, requestOptions) {
516
+ return iterateCursorPagination((pageOptions) => this.listAlertRules(projectId, pageOptions, requestOptions), options);
517
+ }
354
518
  createAlertRule(projectId, input, options) {
355
519
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/alert-rules`, { ...options, body: input });
356
520
  }
@@ -373,6 +537,15 @@ export class BisibilityClient {
373
537
  },
374
538
  });
375
539
  }
540
+ iterateTriggeredAlerts(projectId, options = {}, requestOptions) {
541
+ return iterateCursorPagination((pageOptions) => this.listTriggeredAlerts(projectId, pageOptions, requestOptions), options);
542
+ }
543
+ muteTriggeredAlert(projectId, alertId, options) {
544
+ return this.request("POST", `/projects/${encodedPathSegment(projectId)}/triggered-alerts/${encodedPathSegment(alertId)}/mute`, options);
545
+ }
546
+ markProjectAlertsRead(projectId, options) {
547
+ return this.request("POST", `/projects/${encodedPathSegment(projectId)}/triggered-alerts/mark-read`, options);
548
+ }
376
549
  listTeamMembers(projectId, options, requestOptions) {
377
550
  const pagination = options ?? {};
378
551
  return this.request("GET", `/projects/${encodedPathSegment(projectId)}/team/members`, {
@@ -383,6 +556,9 @@ export class BisibilityClient {
383
556
  },
384
557
  });
385
558
  }
559
+ iterateTeamMembers(projectId, options = {}, requestOptions) {
560
+ return iterateCursorPagination((pageOptions) => this.listTeamMembers(projectId, pageOptions, requestOptions), options);
561
+ }
386
562
  listTeamInvites(projectId, options, requestOptions) {
387
563
  const pagination = options ?? {};
388
564
  return this.request("GET", `/projects/${encodedPathSegment(projectId)}/team/invites`, {
@@ -393,6 +569,9 @@ export class BisibilityClient {
393
569
  },
394
570
  });
395
571
  }
572
+ iterateTeamInvites(projectId, options = {}, requestOptions) {
573
+ return iterateCursorPagination((pageOptions) => this.listTeamInvites(projectId, pageOptions, requestOptions), options);
574
+ }
396
575
  createTeamInvite(projectId, input, options) {
397
576
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/team/invites`, { ...options, body: input });
398
577
  }
@@ -402,6 +581,15 @@ export class BisibilityClient {
402
581
  revokeTeamInviteById(inviteId, options) {
403
582
  return this.request("DELETE", `/team/invites/${encodedPathSegment(inviteId)}`, options);
404
583
  }
584
+ resendTeamInvite(projectId, inviteId, options) {
585
+ return this.request("POST", `/projects/${encodedPathSegment(projectId)}/team/invites/${encodedPathSegment(inviteId)}/resend`, options);
586
+ }
587
+ updateTeamMemberRole(projectId, memberId, input, options) {
588
+ return this.request("PATCH", `/projects/${encodedPathSegment(projectId)}/team/members/${encodedPathSegment(memberId)}`, { ...options, body: input });
589
+ }
590
+ removeTeamMember(projectId, memberId, options) {
591
+ return this.request("DELETE", `/projects/${encodedPathSegment(projectId)}/team/members/${encodedPathSegment(memberId)}`, options);
592
+ }
405
593
  listProviders(projectId, options, requestOptions) {
406
594
  const pagination = options ?? {};
407
595
  return this.request("GET", `/projects/${encodedPathSegment(projectId)}/providers`, {
@@ -412,6 +600,9 @@ export class BisibilityClient {
412
600
  },
413
601
  });
414
602
  }
603
+ iterateProviders(projectId, options = {}, requestOptions) {
604
+ return iterateCursorPagination((pageOptions) => this.listProviders(projectId, pageOptions, requestOptions), options);
605
+ }
415
606
  connectProvider(projectId, providerId, input = {}, options) {
416
607
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/providers/${encodedPathSegment(providerId)}/connect`, { ...options, body: bodyOrUndefined(input) });
417
608
  }
@@ -449,6 +640,9 @@ export class BisibilityClient {
449
640
  },
450
641
  });
451
642
  }
643
+ iterateSavedViews(projectId, options = {}, requestOptions) {
644
+ return iterateCursorPagination((pageOptions) => this.listSavedViews(projectId, pageOptions, requestOptions), options);
645
+ }
452
646
  createSavedView(projectId, input, options) {
453
647
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/saved-views`, { ...options, body: input });
454
648
  }
@@ -468,6 +662,9 @@ export class BisibilityClient {
468
662
  },
469
663
  });
470
664
  }
665
+ iterateCompetitors(projectId, options = {}, requestOptions) {
666
+ return iterateCursorPagination((pageOptions) => this.listCompetitors(projectId, pageOptions, requestOptions), options);
667
+ }
471
668
  addCompetitor(projectId, input, options) {
472
669
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/competitors`, { ...options, body: input });
473
670
  }
@@ -493,6 +690,9 @@ export class BisibilityClient {
493
690
  },
494
691
  });
495
692
  }
693
+ iterateMigrationTokens(projectId, options = {}, requestOptions) {
694
+ return iterateCursorPagination((pageOptions) => this.listMigrationTokens(projectId, pageOptions, requestOptions), options);
695
+ }
496
696
  mintMigrationToken(projectId, input = {}, options) {
497
697
  return this.request("POST", `/projects/${encodedPathSegment(projectId)}/migration-tokens`, { ...options, body: bodyOrUndefined(input) });
498
698
  }
@@ -502,21 +702,69 @@ export class BisibilityClient {
502
702
  revokeMigrationTokenById(tokenId, options) {
503
703
  return this.request("DELETE", `/migration-tokens/${encodedPathSegment(tokenId)}`, options);
504
704
  }
705
+ getCloudImportCompatibility(options) {
706
+ return this.request("GET", "/cloud/import/compatibility", {
707
+ ...options,
708
+ auth: false,
709
+ });
710
+ }
711
+ importCloudExport(input, options) {
712
+ return this.request("POST", "/cloud/import", {
713
+ ...options,
714
+ body: input,
715
+ });
716
+ }
717
+ createCloudImportSession(input, options) {
718
+ return this.request("POST", "/cloud/import/sessions", {
719
+ ...options,
720
+ body: input,
721
+ });
722
+ }
723
+ uploadCloudImportChunk(sessionId, index, input, options) {
724
+ const { contentEncoding, headers, ...requestOptions } = options ?? {};
725
+ const mergedHeaders = new Headers(headers);
726
+ if (contentEncoding !== undefined) {
727
+ mergedHeaders.set("Content-Encoding", contentEncoding);
728
+ }
729
+ return this.request("PUT", `/cloud/import/sessions/${encodedPathSegment(sessionId)}/chunks/${encodedPathSegment(String(index))}`, { ...requestOptions, body: input, headers: mergedHeaders });
730
+ }
731
+ finalizeCloudImportSession(sessionId, options) {
732
+ return this.request("POST", `/cloud/import/sessions/${encodedPathSegment(sessionId)}/finalize`, options);
733
+ }
505
734
  buildUrl(path, query) {
506
735
  const absolute = isAbsoluteUrl(this.baseUrl);
507
736
  const base = `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
508
737
  const url = new URL(base, absolute ? undefined : RELATIVE_BASE_ORIGIN);
509
738
  if (query) {
510
739
  for (const [key, rawValue] of Object.entries(query)) {
511
- const value = queryValue(rawValue);
512
- if (value !== undefined) {
513
- url.searchParams.set(key, value);
740
+ const rawValues = Array.isArray(rawValue)
741
+ ? rawValue
742
+ : [rawValue];
743
+ for (const rawItem of rawValues) {
744
+ const value = queryValue(rawItem);
745
+ if (value !== undefined) {
746
+ url.searchParams.append(key, value);
747
+ }
514
748
  }
515
749
  }
516
750
  }
517
751
  return absolute ? url.toString() : `${url.pathname}${url.search}`;
518
752
  }
519
753
  async request(method, path, options = {}) {
754
+ const statusRef = { status: 0 };
755
+ const result = await this.requestOrUndefined(method, path, options, statusRef);
756
+ if (result === undefined) {
757
+ throw new BisibilityResponseError("Bisibility API returned an empty response body.", {
758
+ body: "",
759
+ cause: undefined,
760
+ method,
761
+ status: statusRef.status,
762
+ url: this.buildUrl(path, options.query),
763
+ });
764
+ }
765
+ return result;
766
+ }
767
+ async requestOrUndefined(method, path, options = {}, statusRef) {
520
768
  const fetchImpl = this.#fetchImpl ?? globalThis.fetch;
521
769
  const url = this.buildUrl(path, options.query);
522
770
  if (!fetchImpl) {
@@ -535,35 +783,71 @@ export class BisibilityClient {
535
783
  if (options.idempotencyKey) {
536
784
  headers.set("Idempotency-Key", options.idempotencyKey);
537
785
  }
538
- const init = { headers, method, redirect: "error" };
539
- const signal = composedSignal(options.signal, options.timeout);
540
- if (signal) {
541
- init.signal = signal;
786
+ headers.set("X-Bisibility-Client", CLIENT_ID);
787
+ if (!headers.has("User-Agent")) {
788
+ try {
789
+ headers.set("User-Agent", CLIENT_ID);
790
+ }
791
+ catch {
792
+ // Browsers may forbid setting User-Agent. X-Bisibility-Client remains authoritative.
793
+ }
542
794
  }
795
+ const baseInit = { headers, method, redirect: "error" };
543
796
  if (options.body !== undefined) {
544
797
  if (!headers.has("Content-Type")) {
545
798
  headers.set("Content-Type", "application/json");
546
799
  }
547
- init.body = JSON.stringify(options.body);
548
- }
549
- let response;
550
- try {
551
- response = await fetchImpl(url, init);
552
- }
553
- catch (error) {
554
- throw new BisibilityNetworkError("Network error while calling the Bisibility API.", {
555
- cause: error,
556
- method,
557
- url,
558
- });
800
+ baseInit.body = JSON.stringify(options.body);
559
801
  }
560
- if (!response.ok) {
561
- throw await this.errorFromResponse(response, method, url);
562
- }
563
- if (options.parseAs === "text") {
564
- return response.text();
802
+ const timeout = options.timeout !== undefined
803
+ ? validatedTimeout(options.timeout)
804
+ : this.#timeout !== undefined
805
+ ? this.#timeout
806
+ : options.signal
807
+ ? null
808
+ : DEFAULT_TIMEOUT;
809
+ const retryable = IDEMPOTENT_METHODS.has(method.toUpperCase()) || headers.has("Idempotency-Key");
810
+ for (let attempt = 0;; attempt += 1) {
811
+ const init = { ...baseInit };
812
+ const signal = composedSignal(options.signal, timeout);
813
+ if (signal) {
814
+ init.signal = signal;
815
+ }
816
+ let response;
817
+ try {
818
+ response = await fetchImpl(url, init);
819
+ }
820
+ catch (cause) {
821
+ if (!retryable || attempt >= this.#maxRetries || options.signal?.aborted) {
822
+ throw new BisibilityNetworkError("Network error while calling the Bisibility API.", {
823
+ cause,
824
+ method,
825
+ url,
826
+ });
827
+ }
828
+ await sleepForRetry(retryBackoffMs(attempt), options.signal, method, url);
829
+ continue;
830
+ }
831
+ if (!response.ok) {
832
+ const error = await this.errorFromResponse(response, method, url);
833
+ if (retryable &&
834
+ attempt < this.#maxRetries &&
835
+ (error.status === 429 || error.status === 503)) {
836
+ await sleepForRetry(error.retryAfterSeconds === null
837
+ ? retryBackoffMs(attempt)
838
+ : error.retryAfterSeconds * 1_000, options.signal, method, url);
839
+ continue;
840
+ }
841
+ throw error;
842
+ }
843
+ if (statusRef) {
844
+ statusRef.status = response.status;
845
+ }
846
+ if (options.parseAs === "text") {
847
+ return response.text();
848
+ }
849
+ return this.jsonFromResponse(response, method, url);
565
850
  }
566
- return this.jsonFromResponse(response, method, url);
567
851
  }
568
852
  async jsonFromResponse(response, method, url) {
569
853
  const body = await response.text();