@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.
@@ -342,6 +342,7 @@ var WHERE_QUERY_PARAM = `where`;
342
342
  var REPLICA_PARAM = `replica`;
343
343
  var WHERE_PARAMS_PARAM = `params`;
344
344
  var EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`;
345
+ var LIVE_SSE_QUERY_PARAM = `live_sse`;
345
346
  var FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`;
346
347
  var PAUSE_STREAM = `pause-stream`;
347
348
  var LOG_MODE_QUERY_PARAM = `log`;
@@ -368,8 +369,13 @@ var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
368
369
  var HTTP_RETRY_STATUS_CODES = [429];
369
370
  var BackoffDefaults = {
370
371
  initialDelay: 100,
371
- maxDelay: 1e4,
372
- multiplier: 1.3
372
+ maxDelay: 6e4,
373
+ // Cap at 60s - reasonable for long-lived connections
374
+ multiplier: 1.3,
375
+ maxRetries: Infinity,
376
+ // Retry forever - clients may go offline and come back
377
+ retryBudgetPercent: 0.1
378
+ // 10% retry budget prevents amplification
373
379
  };
374
380
  function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
375
381
  const {
@@ -377,8 +383,29 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
377
383
  maxDelay,
378
384
  multiplier,
379
385
  debug = false,
380
- onFailedAttempt
386
+ onFailedAttempt,
387
+ maxRetries = Infinity,
388
+ retryBudgetPercent = 0.1
381
389
  } = backoffOptions;
390
+ let totalRequests = 0;
391
+ let totalRetries = 0;
392
+ let budgetResetTime = Date.now() + 6e4;
393
+ function checkRetryBudget(percent) {
394
+ const now = Date.now();
395
+ if (now > budgetResetTime) {
396
+ totalRequests = 0;
397
+ totalRetries = 0;
398
+ budgetResetTime = now + 6e4;
399
+ }
400
+ totalRequests++;
401
+ if (totalRequests < 10) return true;
402
+ const currentRetryRate = totalRetries / totalRequests;
403
+ const hasCapacity = currentRetryRate < percent;
404
+ if (hasCapacity) {
405
+ totalRetries++;
406
+ }
407
+ return hasCapacity;
408
+ }
382
409
  return (...args) => __async(this, null, function* () {
383
410
  var _a;
384
411
  const url = args[0];
@@ -388,7 +415,10 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
388
415
  while (true) {
389
416
  try {
390
417
  const result = yield fetchClient(...args);
391
- if (result.ok) return result;
418
+ if (result.ok) {
419
+ delay = initialDelay;
420
+ return result;
421
+ }
392
422
  const err = yield FetchError.fromResponse(result, url.toString());
393
423
  throw err;
394
424
  } catch (e) {
@@ -398,12 +428,51 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
398
428
  } else if (e instanceof FetchError && !HTTP_RETRY_STATUS_CODES.includes(e.status) && e.status >= 400 && e.status < 500) {
399
429
  throw e;
400
430
  } else {
401
- yield new Promise((resolve) => setTimeout(resolve, delay));
402
- delay = Math.min(delay * multiplier, maxDelay);
431
+ attempt++;
432
+ if (attempt >= maxRetries) {
433
+ if (debug) {
434
+ console.log(
435
+ `Max retries reached (${attempt}/${maxRetries}), giving up`
436
+ );
437
+ }
438
+ throw e;
439
+ }
440
+ if (!checkRetryBudget(retryBudgetPercent)) {
441
+ if (debug) {
442
+ console.log(
443
+ `Retry budget exhausted (attempt ${attempt}), backing off`
444
+ );
445
+ }
446
+ yield new Promise((resolve) => setTimeout(resolve, maxDelay));
447
+ continue;
448
+ }
449
+ let serverMinimumMs = 0;
450
+ if (e instanceof FetchError && e.headers) {
451
+ const retryAfter = e.headers[`retry-after`];
452
+ if (retryAfter) {
453
+ const retryAfterSec = Number(retryAfter);
454
+ if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
455
+ serverMinimumMs = retryAfterSec * 1e3;
456
+ } else {
457
+ const retryDate = Date.parse(retryAfter);
458
+ if (!isNaN(retryDate)) {
459
+ const deltaMs = retryDate - Date.now();
460
+ serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
461
+ }
462
+ }
463
+ }
464
+ }
465
+ const jitter = Math.random() * delay;
466
+ const clientBackoffMs = Math.min(jitter, maxDelay);
467
+ const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
403
468
  if (debug) {
404
- attempt++;
405
- console.log(`Retry attempt #${attempt} after ${delay}ms`);
469
+ const source = serverMinimumMs > 0 ? `server+client` : `client`;
470
+ console.log(
471
+ `Retry attempt #${attempt} after ${waitMs}ms (${source}, serverMin=${serverMinimumMs}ms, clientBackoff=${clientBackoffMs}ms)`
472
+ );
406
473
  }
474
+ yield new Promise((resolve) => setTimeout(resolve, waitMs));
475
+ delay = Math.min(delay * multiplier, maxDelay);
407
476
  }
408
477
  }
409
478
  }
@@ -412,6 +481,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
412
481
  var NO_BODY_STATUS_CODES = [201, 204, 205];
413
482
  function createFetchWithConsumedMessages(fetchClient) {
414
483
  return (...args) => __async(this, null, function* () {
484
+ var _a, _b;
415
485
  const url = args[0];
416
486
  const res = yield fetchClient(...args);
417
487
  try {
@@ -421,6 +491,9 @@ function createFetchWithConsumedMessages(fetchClient) {
421
491
  const text = yield res.text();
422
492
  return new Response(text, res);
423
493
  } catch (err) {
494
+ if ((_b = (_a = args[1]) == null ? void 0 : _a.signal) == null ? void 0 : _b.aborted) {
495
+ throw new FetchBackoffAbortError();
496
+ }
424
497
  throw new FetchError(
425
498
  res.status,
426
499
  void 0,
@@ -1052,7 +1125,7 @@ requestShape_fn = function() {
1052
1125
  }
1053
1126
  const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER] || `${__privateGet(this, _shapeHandle)}-next`;
1054
1127
  __privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
1055
- yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
1128
+ yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
1056
1129
  return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
1057
1130
  } else {
1058
1131
  __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
@@ -1205,8 +1278,11 @@ onMessages_fn = function(batch, isSseMessage = false) {
1205
1278
  };
1206
1279
  fetchShape_fn = function(opts) {
1207
1280
  return __async(this, null, function* () {
1208
- if (__privateGet(this, _isUpToDate) && this.options.experimentalLiveSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1281
+ var _a;
1282
+ const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
1283
+ if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
1209
1284
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
1285
+ opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
1210
1286
  return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
1211
1287
  }
1212
1288
  return __privateMethod(this, _ShapeStream_instances, requestShapeLongPoll_fn).call(this, opts);