@supabase/postgrest-js 2.101.1 → 2.102.0-beta.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/index.mjs CHANGED
@@ -1,3 +1,32 @@
1
+ //#region src/types/common/common.ts
2
+ /**
3
+ * Default number of retry attempts.
4
+ */
5
+ const DEFAULT_MAX_RETRIES = 3;
6
+ /**
7
+ * Default exponential backoff delay function.
8
+ * Delays: 1s, 2s, 4s, 8s, ... (max 30s)
9
+ *
10
+ * @param attemptIndex - Zero-based index of the retry attempt
11
+ * @returns Delay in milliseconds before the next retry
12
+ */
13
+ const getRetryDelay = (attemptIndex) => Math.min(1e3 * 2 ** attemptIndex, 3e4);
14
+ /**
15
+ * Status codes that are safe to retry.
16
+ * 520 = Cloudflare timeout/connection errors (transient)
17
+ * 503 = PostgREST schema cache not yet loaded (transient, signals retry via Retry-After header)
18
+ */
19
+ const RETRYABLE_STATUS_CODES = [520, 503];
20
+ /**
21
+ * HTTP methods that are safe to retry (idempotent operations).
22
+ */
23
+ const RETRYABLE_METHODS = [
24
+ "GET",
25
+ "HEAD",
26
+ "OPTIONS"
27
+ ];
28
+
29
+ //#endregion
1
30
  //#region src/PostgrestError.ts
2
31
  /**
3
32
  * Error format
@@ -29,6 +58,36 @@ var PostgrestError = class extends Error {
29
58
 
30
59
  //#endregion
31
60
  //#region src/PostgrestBuilder.ts
61
+ /**
62
+ * Sleep for a given number of milliseconds.
63
+ * If an AbortSignal is provided, the sleep resolves early when the signal is aborted.
64
+ */
65
+ function sleep(ms, signal) {
66
+ return new Promise((resolve) => {
67
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
68
+ resolve();
69
+ return;
70
+ }
71
+ const id = setTimeout(() => {
72
+ signal === null || signal === void 0 || signal.removeEventListener("abort", onAbort);
73
+ resolve();
74
+ }, ms);
75
+ function onAbort() {
76
+ clearTimeout(id);
77
+ resolve();
78
+ }
79
+ signal === null || signal === void 0 || signal.addEventListener("abort", onAbort);
80
+ });
81
+ }
82
+ /**
83
+ * Check if a request should be retried based on method and status code.
84
+ */
85
+ function shouldRetry(method, status, attemptCount, retryEnabled) {
86
+ if (!retryEnabled || attemptCount >= DEFAULT_MAX_RETRIES) return false;
87
+ if (!RETRYABLE_METHODS.includes(method)) return false;
88
+ if (!RETRYABLE_STATUS_CODES.includes(status)) return false;
89
+ return true;
90
+ }
32
91
  var PostgrestBuilder = class {
33
92
  /**
34
93
  * Creates a builder configured for a specific PostgREST request.
@@ -56,8 +115,9 @@ var PostgrestBuilder = class {
56
115
  * ```
57
116
  */
58
117
  constructor(builder) {
59
- var _builder$shouldThrowO, _builder$isMaybeSingl, _builder$urlLengthLim;
118
+ var _builder$shouldThrowO, _builder$isMaybeSingl, _builder$urlLengthLim, _builder$retry;
60
119
  this.shouldThrowOnError = false;
120
+ this.retryEnabled = true;
61
121
  this.method = builder.method;
62
122
  this.url = builder.url;
63
123
  this.headers = new Headers(builder.headers);
@@ -67,6 +127,7 @@ var PostgrestBuilder = class {
67
127
  this.signal = builder.signal;
68
128
  this.isMaybeSingle = (_builder$isMaybeSingl = builder.isMaybeSingle) !== null && _builder$isMaybeSingl !== void 0 ? _builder$isMaybeSingl : false;
69
129
  this.urlLengthLimit = (_builder$urlLengthLim = builder.urlLengthLimit) !== null && _builder$urlLengthLim !== void 0 ? _builder$urlLengthLim : 8e3;
130
+ this.retryEnabled = (_builder$retry = builder.retry) !== null && _builder$retry !== void 0 ? _builder$retry : true;
70
131
  if (builder.fetch) this.fetch = builder.fetch;
71
132
  else this.fetch = fetch;
72
133
  }
@@ -92,77 +153,73 @@ var PostgrestBuilder = class {
92
153
  this.headers.set(name, value);
93
154
  return this;
94
155
  }
95
- /** *
156
+ /**
96
157
  * @category Database
158
+ *
159
+ * Configure retry behavior for this request.
160
+ *
161
+ * By default, retries are enabled for idempotent requests (GET, HEAD, OPTIONS)
162
+ * that fail with network errors or specific HTTP status codes (503, 520).
163
+ * Retries use exponential backoff (1s, 2s, 4s) with a maximum of 3 attempts.
164
+ *
165
+ * @param enabled - Whether to enable retries for this request
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * // Disable retries for a specific query
170
+ * const { data, error } = await supabase
171
+ * .from('users')
172
+ * .select()
173
+ * .retry(false)
174
+ * ```
97
175
  */
176
+ retry(enabled) {
177
+ this.retryEnabled = enabled;
178
+ return this;
179
+ }
98
180
  then(onfulfilled, onrejected) {
99
181
  var _this = this;
100
182
  if (this.schema === void 0) {} else if (["GET", "HEAD"].includes(this.method)) this.headers.set("Accept-Profile", this.schema);
101
183
  else this.headers.set("Content-Profile", this.schema);
102
184
  if (this.method !== "GET" && this.method !== "HEAD") this.headers.set("Content-Type", "application/json");
103
185
  const _fetch = this.fetch;
104
- let res = _fetch(this.url.toString(), {
105
- method: this.method,
106
- headers: this.headers,
107
- body: JSON.stringify(this.body),
108
- signal: this.signal
109
- }).then(async (res$1) => {
110
- let error = null;
111
- let data = null;
112
- let count = null;
113
- let status = res$1.status;
114
- let statusText = res$1.statusText;
115
- if (res$1.ok) {
116
- var _this$headers$get2, _res$headers$get;
117
- if (_this.method !== "HEAD") {
118
- var _this$headers$get;
119
- const body = await res$1.text();
120
- if (body === "") {} else if (_this.headers.get("Accept") === "text/csv") data = body;
121
- else if (_this.headers.get("Accept") && ((_this$headers$get = _this.headers.get("Accept")) === null || _this$headers$get === void 0 ? void 0 : _this$headers$get.includes("application/vnd.pgrst.plan+text"))) data = body;
122
- else data = JSON.parse(body);
123
- }
124
- const countHeader = (_this$headers$get2 = _this.headers.get("Prefer")) === null || _this$headers$get2 === void 0 ? void 0 : _this$headers$get2.match(/count=(exact|planned|estimated)/);
125
- const contentRange = (_res$headers$get = res$1.headers.get("content-range")) === null || _res$headers$get === void 0 ? void 0 : _res$headers$get.split("/");
126
- if (countHeader && contentRange && contentRange.length > 1) count = parseInt(contentRange[1]);
127
- if (_this.isMaybeSingle && Array.isArray(data)) if (data.length > 1) {
128
- error = {
129
- code: "PGRST116",
130
- details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
131
- hint: null,
132
- message: "JSON object requested, multiple (or no) rows returned"
133
- };
134
- data = null;
135
- count = null;
136
- status = 406;
137
- statusText = "Not Acceptable";
138
- } else if (data.length === 1) data = data[0];
139
- else data = null;
140
- } else {
141
- const body = await res$1.text();
186
+ const executeWithRetry = async () => {
187
+ let attemptCount = 0;
188
+ while (true) {
189
+ const requestHeaders = new Headers(_this.headers);
190
+ if (attemptCount > 0) requestHeaders.set("X-Retry-Count", String(attemptCount));
191
+ let res$1;
142
192
  try {
143
- error = JSON.parse(body);
144
- if (Array.isArray(error) && res$1.status === 404) {
145
- data = [];
146
- error = null;
147
- status = 200;
148
- statusText = "OK";
193
+ res$1 = await _fetch(_this.url.toString(), {
194
+ method: _this.method,
195
+ headers: requestHeaders,
196
+ body: JSON.stringify(_this.body),
197
+ signal: _this.signal
198
+ });
199
+ } catch (fetchError) {
200
+ if ((fetchError === null || fetchError === void 0 ? void 0 : fetchError.name) === "AbortError" || (fetchError === null || fetchError === void 0 ? void 0 : fetchError.code) === "ABORT_ERR") throw fetchError;
201
+ if (!RETRYABLE_METHODS.includes(_this.method)) throw fetchError;
202
+ if (_this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
203
+ const delay = getRetryDelay(attemptCount);
204
+ attemptCount++;
205
+ await sleep(delay, _this.signal);
206
+ continue;
149
207
  }
150
- } catch (_unused) {
151
- if (res$1.status === 404 && body === "") {
152
- status = 204;
153
- statusText = "No Content";
154
- } else error = { message: body };
208
+ throw fetchError;
209
+ }
210
+ if (shouldRetry(_this.method, res$1.status, attemptCount, _this.retryEnabled)) {
211
+ var _res$headers$get, _res$headers;
212
+ const retryAfterHeader = (_res$headers$get = (_res$headers = res$1.headers) === null || _res$headers === void 0 ? void 0 : _res$headers.get("Retry-After")) !== null && _res$headers$get !== void 0 ? _res$headers$get : null;
213
+ const delay = retryAfterHeader !== null ? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1e3 : getRetryDelay(attemptCount);
214
+ await res$1.text();
215
+ attemptCount++;
216
+ await sleep(delay, _this.signal);
217
+ continue;
155
218
  }
156
- if (error && _this.shouldThrowOnError) throw new PostgrestError(error);
219
+ return await _this.processResponse(res$1);
157
220
  }
158
- return {
159
- error,
160
- data,
161
- count,
162
- status,
163
- statusText
164
- };
165
- });
221
+ };
222
+ let res = executeWithRetry();
166
223
  if (!this.shouldThrowOnError) res = res.catch((fetchError) => {
167
224
  var _fetchError$name2;
168
225
  let errorDetails = "";
@@ -207,6 +264,67 @@ var PostgrestBuilder = class {
207
264
  return res.then(onfulfilled, onrejected);
208
265
  }
209
266
  /**
267
+ * Process a fetch response and return the standardized postgrest response.
268
+ */
269
+ async processResponse(res) {
270
+ var _this2 = this;
271
+ let error = null;
272
+ let data = null;
273
+ let count = null;
274
+ let status = res.status;
275
+ let statusText = res.statusText;
276
+ if (res.ok) {
277
+ var _this$headers$get2, _res$headers$get2;
278
+ if (_this2.method !== "HEAD") {
279
+ var _this$headers$get;
280
+ const body = await res.text();
281
+ if (body === "") {} else if (_this2.headers.get("Accept") === "text/csv") data = body;
282
+ else if (_this2.headers.get("Accept") && ((_this$headers$get = _this2.headers.get("Accept")) === null || _this$headers$get === void 0 ? void 0 : _this$headers$get.includes("application/vnd.pgrst.plan+text"))) data = body;
283
+ else data = JSON.parse(body);
284
+ }
285
+ const countHeader = (_this$headers$get2 = _this2.headers.get("Prefer")) === null || _this$headers$get2 === void 0 ? void 0 : _this$headers$get2.match(/count=(exact|planned|estimated)/);
286
+ const contentRange = (_res$headers$get2 = res.headers.get("content-range")) === null || _res$headers$get2 === void 0 ? void 0 : _res$headers$get2.split("/");
287
+ if (countHeader && contentRange && contentRange.length > 1) count = parseInt(contentRange[1]);
288
+ if (_this2.isMaybeSingle && Array.isArray(data)) if (data.length > 1) {
289
+ error = {
290
+ code: "PGRST116",
291
+ details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
292
+ hint: null,
293
+ message: "JSON object requested, multiple (or no) rows returned"
294
+ };
295
+ data = null;
296
+ count = null;
297
+ status = 406;
298
+ statusText = "Not Acceptable";
299
+ } else if (data.length === 1) data = data[0];
300
+ else data = null;
301
+ } else {
302
+ const body = await res.text();
303
+ try {
304
+ error = JSON.parse(body);
305
+ if (Array.isArray(error) && res.status === 404) {
306
+ data = [];
307
+ error = null;
308
+ status = 200;
309
+ statusText = "OK";
310
+ }
311
+ } catch (_unused) {
312
+ if (res.status === 404 && body === "") {
313
+ status = 204;
314
+ statusText = "No Content";
315
+ } else error = { message: body };
316
+ }
317
+ if (error && _this2.shouldThrowOnError) throw new PostgrestError(error);
318
+ }
319
+ return {
320
+ error,
321
+ data,
322
+ count,
323
+ status,
324
+ statusText
325
+ };
326
+ }
327
+ /**
210
328
  * Override the type of the returned `data`.
211
329
  *
212
330
  * @typeParam NewResult - The new result type to override with
@@ -2889,22 +3007,31 @@ var PostgrestQueryBuilder = class {
2889
3007
  *
2890
3008
  * @category Database
2891
3009
  *
3010
+ * @param url - The URL for the query
3011
+ * @param options - Named parameters
3012
+ * @param options.headers - Custom headers
3013
+ * @param options.schema - Postgres schema to use
3014
+ * @param options.fetch - Custom fetch implementation
3015
+ * @param options.urlLengthLimit - Maximum URL length before warning
3016
+ * @param options.retry - Enable automatic retries for transient errors (default: true)
3017
+ *
2892
3018
  * @example Creating a Postgrest query builder
2893
3019
  * ```ts
2894
3020
  * import { PostgrestQueryBuilder } from '@supabase/postgrest-js'
2895
3021
  *
2896
3022
  * const query = new PostgrestQueryBuilder(
2897
3023
  * new URL('https://xyzcompany.supabase.co/rest/v1/users'),
2898
- * { headers: { apikey: 'public-anon-key' } }
3024
+ * { headers: { apikey: 'public-anon-key' }, retry: true }
2899
3025
  * )
2900
3026
  * ```
2901
3027
  */
2902
- constructor(url, { headers = {}, schema, fetch: fetch$1, urlLengthLimit = 8e3 }) {
3028
+ constructor(url, { headers = {}, schema, fetch: fetch$1, urlLengthLimit = 8e3, retry }) {
2903
3029
  this.url = url;
2904
3030
  this.headers = new Headers(headers);
2905
3031
  this.schema = schema;
2906
3032
  this.fetch = fetch$1;
2907
3033
  this.urlLengthLimit = urlLengthLimit;
3034
+ this.retry = retry;
2908
3035
  }
2909
3036
  /**
2910
3037
  * Clone URL and headers to prevent shared state between operations.
@@ -3712,7 +3839,8 @@ var PostgrestQueryBuilder = class {
3712
3839
  headers,
3713
3840
  schema: this.schema,
3714
3841
  fetch: this.fetch,
3715
- urlLengthLimit: this.urlLengthLimit
3842
+ urlLengthLimit: this.urlLengthLimit,
3843
+ retry: this.retry
3716
3844
  });
3717
3845
  }
3718
3846
  /**
@@ -3846,7 +3974,8 @@ var PostgrestQueryBuilder = class {
3846
3974
  schema: this.schema,
3847
3975
  body: values,
3848
3976
  fetch: (_this$fetch = this.fetch) !== null && _this$fetch !== void 0 ? _this$fetch : fetch,
3849
- urlLengthLimit: this.urlLengthLimit
3977
+ urlLengthLimit: this.urlLengthLimit,
3978
+ retry: this.retry
3850
3979
  });
3851
3980
  }
3852
3981
  /**
@@ -4079,7 +4208,8 @@ var PostgrestQueryBuilder = class {
4079
4208
  schema: this.schema,
4080
4209
  body: values,
4081
4210
  fetch: (_this$fetch2 = this.fetch) !== null && _this$fetch2 !== void 0 ? _this$fetch2 : fetch,
4082
- urlLengthLimit: this.urlLengthLimit
4211
+ urlLengthLimit: this.urlLengthLimit,
4212
+ retry: this.retry
4083
4213
  });
4084
4214
  }
4085
4215
  /**
@@ -4233,7 +4363,8 @@ var PostgrestQueryBuilder = class {
4233
4363
  schema: this.schema,
4234
4364
  body: values,
4235
4365
  fetch: (_this$fetch3 = this.fetch) !== null && _this$fetch3 !== void 0 ? _this$fetch3 : fetch,
4236
- urlLengthLimit: this.urlLengthLimit
4366
+ urlLengthLimit: this.urlLengthLimit,
4367
+ retry: this.retry
4237
4368
  });
4238
4369
  }
4239
4370
  /**
@@ -4365,7 +4496,8 @@ var PostgrestQueryBuilder = class {
4365
4496
  headers,
4366
4497
  schema: this.schema,
4367
4498
  fetch: (_this$fetch4 = this.fetch) !== null && _this$fetch4 !== void 0 ? _this$fetch4 : fetch,
4368
- urlLengthLimit: this.urlLengthLimit
4499
+ urlLengthLimit: this.urlLengthLimit,
4500
+ retry: this.retry
4369
4501
  });
4370
4502
  }
4371
4503
  };
@@ -4459,6 +4591,10 @@ var PostgrestClient = class PostgrestClient {
4459
4591
  * @param options.fetch - Custom fetch
4460
4592
  * @param options.timeout - Optional timeout in milliseconds for all requests. When set, requests will automatically abort after this duration to prevent indefinite hangs.
4461
4593
  * @param options.urlLengthLimit - Maximum URL length in characters before warnings/errors are triggered. Defaults to 8000.
4594
+ * @param options.retry - Enable or disable automatic retries for transient errors.
4595
+ * When enabled, idempotent requests (GET, HEAD, OPTIONS) that fail with network
4596
+ * errors or HTTP 503/520 responses will be automatically retried up to 3 times
4597
+ * with exponential backoff (1s, 2s, 4s). Defaults to `true`.
4462
4598
  * @example
4463
4599
  * ```ts
4464
4600
  * import { PostgrestClient } from '@supabase/postgrest-js'
@@ -4494,10 +4630,11 @@ var PostgrestClient = class PostgrestClient {
4494
4630
  * headers: { apikey: 'public-anon-key' },
4495
4631
  * schema: 'public',
4496
4632
  * timeout: 30000, // 30 second timeout
4633
+ * retry: false, // Disable automatic retries
4497
4634
  * })
4498
4635
  * ```
4499
4636
  */
4500
- constructor(url, { headers = {}, schema, fetch: fetch$1, timeout, urlLengthLimit = 8e3 } = {}) {
4637
+ constructor(url, { headers = {}, schema, fetch: fetch$1, timeout, urlLengthLimit = 8e3, retry } = {}) {
4501
4638
  this.url = url;
4502
4639
  this.headers = new Headers(headers);
4503
4640
  this.schemaName = schema;
@@ -4525,6 +4662,7 @@ var PostgrestClient = class PostgrestClient {
4525
4662
  return originalFetch(input, _objectSpread2(_objectSpread2({}, init), {}, { signal: controller.signal })).finally(() => clearTimeout(timeoutId));
4526
4663
  };
4527
4664
  else this.fetch = originalFetch;
4665
+ this.retry = retry;
4528
4666
  }
4529
4667
  /**
4530
4668
  * Perform a query on a table or a view.
@@ -4539,7 +4677,8 @@ var PostgrestClient = class PostgrestClient {
4539
4677
  headers: new Headers(this.headers),
4540
4678
  schema: this.schemaName,
4541
4679
  fetch: this.fetch,
4542
- urlLengthLimit: this.urlLengthLimit
4680
+ urlLengthLimit: this.urlLengthLimit,
4681
+ retry: this.retry
4543
4682
  });
4544
4683
  }
4545
4684
  /**
@@ -4556,7 +4695,8 @@ var PostgrestClient = class PostgrestClient {
4556
4695
  headers: this.headers,
4557
4696
  schema,
4558
4697
  fetch: this.fetch,
4559
- urlLengthLimit: this.urlLengthLimit
4698
+ urlLengthLimit: this.urlLengthLimit,
4699
+ retry: this.retry
4560
4700
  });
4561
4701
  }
4562
4702
  /**
@@ -4753,7 +4893,8 @@ var PostgrestClient = class PostgrestClient {
4753
4893
  schema: this.schemaName,
4754
4894
  body,
4755
4895
  fetch: (_this$fetch = this.fetch) !== null && _this$fetch !== void 0 ? _this$fetch : fetch,
4756
- urlLengthLimit: this.urlLengthLimit
4896
+ urlLengthLimit: this.urlLengthLimit,
4897
+ retry: this.retry
4757
4898
  });
4758
4899
  }
4759
4900
  };