@electric-sql/client 1.5.1 → 1.5.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@electric-sql/client",
3
3
  "description": "Postgres everywhere - your data, in sync, wherever you need it.",
4
- "version": "1.5.1",
4
+ "version": "1.5.2",
5
5
  "author": "ElectricSQL team and contributors.",
6
6
  "bugs": {
7
7
  "url": "https://github.com/electric-sql/electric/issues"
package/src/client.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  REPLICA_PARAM,
52
52
  FORCE_DISCONNECT_AND_REFRESH,
53
53
  PAUSE_STREAM,
54
+ SYSTEM_WAKE,
54
55
  EXPERIMENTAL_LIVE_SSE_QUERY_PARAM,
55
56
  LIVE_SSE_QUERY_PARAM,
56
57
  ELECTRIC_PROTOCOL_QUERY_PARAMS,
@@ -598,6 +599,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
598
599
  #sseBackoffBaseDelay = 100 // Base delay for exponential backoff (ms)
599
600
  #sseBackoffMaxDelay = 5000 // Maximum delay cap (ms)
600
601
  #unsubscribeFromVisibilityChanges?: () => void
602
+ #unsubscribeFromWakeDetection?: () => void
601
603
  #staleCacheBuster?: string // Cache buster set when stale CDN response detected, used on retry requests to bypass cache
602
604
  #staleCacheRetryCount = 0
603
605
  #maxStaleCacheRetries = 3
@@ -666,6 +668,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
666
668
  this.#fetchClient = createFetchWithConsumedMessages(this.#sseFetchClient)
667
669
 
668
670
  this.#subscribeToVisibilityChanges()
671
+ this.#subscribeToWakeDetection()
669
672
  }
670
673
 
671
674
  get shapeHandle() {
@@ -735,6 +738,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
735
738
  }
736
739
  this.#connected = false
737
740
  this.#tickPromiseRejecter?.()
741
+ this.#unsubscribeFromWakeDetection?.()
738
742
  return
739
743
  }
740
744
 
@@ -745,12 +749,14 @@ export class ShapeStream<T extends Row<unknown> = Row>
745
749
  }
746
750
  this.#connected = false
747
751
  this.#tickPromiseRejecter?.()
752
+ this.#unsubscribeFromWakeDetection?.()
748
753
  throw err
749
754
  }
750
755
 
751
756
  // Normal completion, clean up
752
757
  this.#connected = false
753
758
  this.#tickPromiseRejecter?.()
759
+ this.#unsubscribeFromWakeDetection?.()
754
760
  }
755
761
 
756
762
  async #requestShape(): Promise<void> {
@@ -785,13 +791,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
785
791
  resumingFromPause,
786
792
  })
787
793
  } catch (e) {
788
- // Handle abort error triggered by refresh
794
+ const abortReason = requestAbortController.signal.reason
795
+ const isRestartAbort =
796
+ requestAbortController.signal.aborted &&
797
+ (abortReason === FORCE_DISCONNECT_AND_REFRESH ||
798
+ abortReason === SYSTEM_WAKE)
799
+
789
800
  if (
790
801
  (e instanceof FetchError || e instanceof FetchBackoffAbortError) &&
791
- requestAbortController.signal.aborted &&
792
- requestAbortController.signal.reason === FORCE_DISCONNECT_AND_REFRESH
802
+ isRestartAbort
793
803
  ) {
794
- // Start a new request
795
804
  return this.#requestShape()
796
805
  }
797
806
 
@@ -1432,6 +1441,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1432
1441
  unsubscribeAll(): void {
1433
1442
  this.#subscribers.clear()
1434
1443
  this.#unsubscribeFromVisibilityChanges?.()
1444
+ this.#unsubscribeFromWakeDetection?.()
1435
1445
  }
1436
1446
 
1437
1447
  /** Unix time at which we last synced. Undefined when `isLoading` is true. */
@@ -1543,12 +1553,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
1543
1553
  })
1544
1554
  }
1545
1555
 
1546
- #subscribeToVisibilityChanges() {
1547
- if (
1556
+ #hasBrowserVisibilityAPI(): boolean {
1557
+ return (
1548
1558
  typeof document === `object` &&
1549
1559
  typeof document.hidden === `boolean` &&
1550
1560
  typeof document.addEventListener === `function`
1551
- ) {
1561
+ )
1562
+ }
1563
+
1564
+ #subscribeToVisibilityChanges() {
1565
+ if (this.#hasBrowserVisibilityAPI()) {
1552
1566
  const visibilityHandler = () => {
1553
1567
  if (document.hidden) {
1554
1568
  this.#pause()
@@ -1566,6 +1580,52 @@ export class ShapeStream<T extends Row<unknown> = Row>
1566
1580
  }
1567
1581
  }
1568
1582
 
1583
+ /**
1584
+ * Detects system wake from sleep using timer gap detection.
1585
+ * When the system sleeps, setInterval timers are paused. On wake,
1586
+ * the elapsed wall-clock time since the last tick will be much larger
1587
+ * than the interval period, indicating the system was asleep.
1588
+ *
1589
+ * Only active in non-browser environments (Bun, Node.js) where
1590
+ * `document.visibilitychange` is not available. In browsers,
1591
+ * `#subscribeToVisibilityChanges` handles this instead. Without wake
1592
+ * detection, in-flight HTTP requests (long-poll or SSE) may hang until
1593
+ * the OS TCP timeout.
1594
+ */
1595
+ #subscribeToWakeDetection() {
1596
+ if (this.#hasBrowserVisibilityAPI()) return
1597
+
1598
+ const INTERVAL_MS = 2_000
1599
+ const WAKE_THRESHOLD_MS = 4_000
1600
+
1601
+ let lastTickTime = Date.now()
1602
+
1603
+ const timer = setInterval(() => {
1604
+ const now = Date.now()
1605
+ const elapsed = now - lastTickTime
1606
+ lastTickTime = now
1607
+
1608
+ if (elapsed > INTERVAL_MS + WAKE_THRESHOLD_MS) {
1609
+ if (this.#state === `active` && this.#requestAbortController) {
1610
+ this.#isRefreshing = true
1611
+ this.#requestAbortController.abort(SYSTEM_WAKE)
1612
+ queueMicrotask(() => {
1613
+ this.#isRefreshing = false
1614
+ })
1615
+ }
1616
+ }
1617
+ }, INTERVAL_MS)
1618
+
1619
+ // Ensure the timer doesn't prevent the process from exiting
1620
+ if (typeof timer === `object` && `unref` in timer) {
1621
+ timer.unref()
1622
+ }
1623
+
1624
+ this.#unsubscribeFromWakeDetection = () => {
1625
+ clearInterval(timer)
1626
+ }
1627
+ }
1628
+
1569
1629
  /**
1570
1630
  * Resets the state of the stream, optionally with a provided
1571
1631
  * shape handle
package/src/constants.ts CHANGED
@@ -20,6 +20,7 @@ export const EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`
20
20
  export const LIVE_SSE_QUERY_PARAM = `live_sse`
21
21
  export const FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`
22
22
  export const PAUSE_STREAM = `pause-stream`
23
+ export const SYSTEM_WAKE = `system-wake`
23
24
  export const LOG_MODE_QUERY_PARAM = `log`
24
25
  export const SUBSET_PARAM_WHERE = `subset__where`
25
26
  export const SUBSET_PARAM_LIMIT = `subset__limit`