@electric-sql/client 1.0.14 → 1.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/index.mjs CHANGED
@@ -311,6 +311,7 @@ var WHERE_QUERY_PARAM = `where`;
311
311
  var REPLICA_PARAM = `replica`;
312
312
  var WHERE_PARAMS_PARAM = `params`;
313
313
  var EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`;
314
+ var LIVE_SSE_QUERY_PARAM = `live_sse`;
314
315
  var FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`;
315
316
  var PAUSE_STREAM = `pause-stream`;
316
317
  var LOG_MODE_QUERY_PARAM = `log`;
@@ -337,8 +338,13 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
337
338
  var HTTP_RETRY_STATUS_CODES = [429];
338
339
  var BackoffDefaults = {
339
340
  initialDelay: 100,
340
- maxDelay: 1e4,
341
- multiplier: 1.3
341
+ maxDelay: 6e4,
342
+ // Cap at 60s - reasonable for long-lived connections
343
+ multiplier: 1.3,
344
+ maxRetries: Infinity,
345
+ // Retry forever - clients may go offline and come back
346
+ retryBudgetPercent: 0.1
347
+ // 10% retry budget prevents amplification
342
348
  };
343
349
  function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
344
350
  const {
@@ -346,8 +352,29 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
346
352
  maxDelay,
347
353
  multiplier,
348
354
  debug = false,
349
- onFailedAttempt
355
+ onFailedAttempt,
356
+ maxRetries = Infinity,
357
+ retryBudgetPercent = 0.1
350
358
  } = backoffOptions;
359
+ let totalRequests = 0;
360
+ let totalRetries = 0;
361
+ let budgetResetTime = Date.now() + 6e4;
362
+ function checkRetryBudget(percent) {
363
+ const now = Date.now();
364
+ if (now > budgetResetTime) {
365
+ totalRequests = 0;
366
+ totalRetries = 0;
367
+ budgetResetTime = now + 6e4;
368
+ }
369
+ totalRequests++;
370
+ if (totalRequests < 10) return true;
371
+ const currentRetryRate = totalRetries / totalRequests;
372
+ const hasCapacity = currentRetryRate < percent;
373
+ if (hasCapacity) {
374
+ totalRetries++;
375
+ }
376
+ return hasCapacity;
377
+ }
351
378
  return (...args) => __async(this, null, function* () {
352
379
  var _a;
353
380
  const url = args[0];
@@ -357,7 +384,10 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
357
384
  while (true) {
358
385
  try {
359
386
  const result = yield fetchClient(...args);
360
- if (result.ok) return result;
387
+ if (result.ok) {
388
+ delay = initialDelay;
389
+ return result;
390
+ }
361
391
  const err = yield FetchError.fromResponse(result, url.toString());
362
392
  throw err;
363
393
  } catch (e) {
@@ -367,12 +397,51 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
367
397
  } else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
368
398
  throw e;
369
399
  } else {
370
- yield new Promise((resolve) => setTimeout(resolve, delay));
371
- delay = Math.min(delay * multiplier, maxDelay);
400
+ attempt++;
401
+ if (attempt >= maxRetries) {
402
+ if (debug) {
403
+ console.log(
404
+ `Max retries reached (${attempt}/${maxRetries}), giving up`
405
+ );
406
+ }
407
+ throw e;
408
+ }
409
+ if (!checkRetryBudget(retryBudgetPercent)) {
410
+ if (debug) {
411
+ console.log(
412
+ `Retry budget exhausted (attempt ${attempt}), backing off`
413
+ );
414
+ }
415
+ yield new Promise((resolve) => setTimeout(resolve, maxDelay));
416
+ continue;
417
+ }
418
+ let serverMinimumMs = 0;
419
+ if (e instanceof FetchError && e.headers) {
420
+ const retryAfter = e.headers[`retry-after`];
421
+ if (retryAfter) {
422
+ const retryAfterSec = Number(retryAfter);
423
+ if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
424
+ serverMinimumMs = retryAfterSec * 1e3;
425
+ } else {
426
+ const retryDate = Date.parse(retryAfter);
427
+ if (!isNaN(retryDate)) {
428
+ const deltaMs = retryDate - Date.now();
429
+ serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
430
+ }
431
+ }
432
+ }
433
+ }
434
+ const jitter = Math.random() * delay;
435
+ const clientBackoffMs = Math.min(jitter, maxDelay);
436
+ const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
372
437
  if (debug) {
373
- attempt++;
374
- console.log(`Retry attempt #${attempt} after ${delay}ms`);
438
+ const source = serverMinimumMs > 0 ? `server+client` : `client`;
439
+ console.log(
440
+ `Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
441
+ );
375
442
  }
443
+ yield new Promise((resolve) => setTimeout(resolve, waitMs));
444
+ delay = Math.min(delay * multiplier, maxDelay);
376
445
  }
377
446
  }
378
447
  }
@@ -381,6 +450,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
381
450
  var NO_BODY_STATUS_CODES = [201, 204, 205];
382
451
  function createFetchWithConsumedMessages(fetchClient) {
383
452
  return (...args) => __async(this, null, function* () {
453
+ var _a, _b;
384
454
  const url = args[0];
385
455
  const res = yield fetchClient(...args);
386
456
  try {
@@ -390,6 +460,9 @@ function createFetchWithConsumedMessages(fetchClient) {
390
460
  const text = yield res.text();
391
461
  return new Response(text, res);
392
462
  } catch (err) {
463
+ if ((_b = (_a = args[1]) == null ? void 0 : _a.signal) == null ? void 0 : _b.aborted) {
464
+ throw new FetchBackoffAbortError();
465
+ }
393
466
  throw new FetchError(
394
467
  res.status,
395
468
  void 0,
@@ -1023,7 +1096,7 @@ requestShape_fn = function() {
1023
1096
  }
1024
1097
  const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER] || `${__privateGet(this, _shapeHandle)}-next`;
1025
1098
  __privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
1026
- yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
1099
+ yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
1027
1100
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1028
1101
  } else {
1029
1102
  __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
@@ -1176,8 +1249,11 @@ onMessages_fn = function(batch, isSseMessage = false) {
1176
1249
  };
1177
1250
  fetchShape_fn = function(opts) {
1178
1251
  return __async(this, null, function* () {
1179
- if (__privateGet(this, _isUpToDate) && this.options.experimentalLiveSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1252
+ var _a;
1253
+ const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1254
+ if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1180
1255
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
1256
+ opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
1181
1257
  return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
1182
1258
  }
1183
1259
  return __privateMethod(this, _ShapeStream_instances, requestShapeLongPoll_fn).call(this, opts);