@electric-sql/client 1.5.8 → 1.5.10

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.8",
4
+ "version": "1.5.10",
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
@@ -698,7 +698,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
698
698
  this.#fetchClient = createFetchWithConsumedMessages(this.#sseFetchClient)
699
699
 
700
700
  this.#subscribeToVisibilityChanges()
701
- this.#subscribeToWakeDetection()
702
701
  }
703
702
 
704
703
  get shapeHandle() {
@@ -723,6 +722,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
723
722
 
724
723
  async #start(): Promise<void> {
725
724
  this.#started = true
725
+ this.#subscribeToWakeDetection()
726
726
 
727
727
  try {
728
728
  await this.#requestShape()
@@ -1643,6 +1643,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1643
1643
  // Store cleanup function to remove the event listener
1644
1644
  this.#unsubscribeFromVisibilityChanges = () => {
1645
1645
  document.removeEventListener(`visibilitychange`, visibilityHandler)
1646
+ this.#unsubscribeFromVisibilityChanges = undefined
1646
1647
  }
1647
1648
  }
1648
1649
  }
@@ -1661,6 +1662,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1661
1662
  */
1662
1663
  #subscribeToWakeDetection() {
1663
1664
  if (this.#hasBrowserVisibilityAPI()) return
1665
+ if (this.#unsubscribeFromWakeDetection) return
1664
1666
 
1665
1667
  const INTERVAL_MS = 2_000
1666
1668
  const WAKE_THRESHOLD_MS = 4_000
@@ -1695,6 +1697,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1695
1697
 
1696
1698
  this.#unsubscribeFromWakeDetection = () => {
1697
1699
  clearInterval(timer)
1700
+ this.#unsubscribeFromWakeDetection = undefined
1698
1701
  }
1699
1702
  }
1700
1703
 
@@ -75,7 +75,7 @@ export type ResponseMetadataTransition =
75
75
  | { action: `ignored`; state: ShapeStreamState }
76
76
  | {
77
77
  action: `stale-retry`
78
- state: StaleRetryState
78
+ state: ShapeStreamState
79
79
  exceededMaxRetries: boolean
80
80
  }
81
81
 
@@ -122,7 +122,7 @@ export interface UrlParamsContext {
122
122
  * Each concrete state carries only its relevant fields — there is no shared
123
123
  * flat context bag. Transitions create new immutable state objects.
124
124
  *
125
- * `isUpToDate` is derived from state kind (only LiveState returns true).
125
+ * `isUpToDate` returns true for LiveState and delegating states wrapping LiveState.
126
126
  */
127
127
  export abstract class ShapeStreamState {
128
128
  abstract readonly kind: ShapeStreamStateKind
@@ -368,9 +368,10 @@ abstract class ActiveState extends ShapeStreamState {
368
368
 
369
369
  /**
370
370
  * Captures shared behavior of InitialState, SyncingState, StaleRetryState:
371
- * - handleResponseMetadata: stale check → parse fields → new SyncingState
372
- * - canEnterReplayModetrue
373
- * - enterReplayMode new ReplayingState
371
+ * - handleResponseMetadata: stale check → parse fields → new SyncingState (or LiveState for 204)
372
+ * - enterReplayMode(cursor)new ReplayingState
373
+ * - canEnterReplayMode(): boolean returns true (StaleRetryState overrides to return false,
374
+ * because entering replay would lose the stale-retry count; see C1 in SPEC.md)
374
375
  */
375
376
  abstract class FetchingState extends ActiveState {
376
377
  handleResponseMetadata(
@@ -659,48 +660,60 @@ export class ReplayingState extends ActiveState {
659
660
  // Delegating states (Paused / Error)
660
661
  // ---------------------------------------------------------------------------
661
662
 
663
+ // Union of all non-delegating states — used to type previousState fields so
664
+ // that PausedState.previousState is never another PausedState, and
665
+ // ErrorState.previousState is never another ErrorState.
666
+ export type ShapeStreamActiveState =
667
+ | InitialState
668
+ | SyncingState
669
+ | LiveState
670
+ | ReplayingState
671
+ | StaleRetryState
672
+
662
673
  export class PausedState extends ShapeStreamState {
663
674
  readonly kind = `paused` as const
664
- readonly previousState: ShapeStreamState
675
+ readonly previousState: ShapeStreamActiveState | ErrorState
665
676
 
666
677
  constructor(previousState: ShapeStreamState) {
667
678
  super()
668
- this.previousState = previousState
679
+ this.previousState = (
680
+ previousState instanceof PausedState
681
+ ? previousState.previousState
682
+ : previousState
683
+ ) as ShapeStreamActiveState | ErrorState
669
684
  }
670
685
 
671
- get handle() {
686
+ get handle(): string | undefined {
672
687
  return this.previousState.handle
673
688
  }
674
- get offset() {
689
+ get offset(): Offset {
675
690
  return this.previousState.offset
676
691
  }
677
- get schema() {
692
+ get schema(): Schema | undefined {
678
693
  return this.previousState.schema
679
694
  }
680
- get liveCacheBuster() {
695
+ get liveCacheBuster(): string {
681
696
  return this.previousState.liveCacheBuster
682
697
  }
683
- get lastSyncedAt() {
698
+ get lastSyncedAt(): number | undefined {
684
699
  return this.previousState.lastSyncedAt
685
700
  }
686
-
687
701
  get isUpToDate(): boolean {
688
702
  return this.previousState.isUpToDate
689
703
  }
690
-
691
- get staleCacheBuster() {
704
+ get staleCacheBuster(): string | undefined {
692
705
  return this.previousState.staleCacheBuster
693
706
  }
694
- get staleCacheRetryCount() {
707
+ get staleCacheRetryCount(): number {
695
708
  return this.previousState.staleCacheRetryCount
696
709
  }
697
- get sseFallbackToLongPolling() {
710
+ get sseFallbackToLongPolling(): boolean {
698
711
  return this.previousState.sseFallbackToLongPolling
699
712
  }
700
- get consecutiveShortSseConnections() {
713
+ get consecutiveShortSseConnections(): number {
701
714
  return this.previousState.consecutiveShortSseConnections
702
715
  }
703
- get replayCursor() {
716
+ get replayCursor(): string | undefined {
704
717
  return this.previousState.replayCursor
705
718
  }
706
719
 
@@ -711,7 +724,20 @@ export class PausedState extends ShapeStreamState {
711
724
  if (transition.action === `accepted`) {
712
725
  return { action: `accepted`, state: new PausedState(transition.state) }
713
726
  }
714
- return transition
727
+ if (transition.action === `ignored`) {
728
+ return { action: `ignored`, state: this }
729
+ }
730
+ if (transition.action === `stale-retry`) {
731
+ return {
732
+ action: `stale-retry`,
733
+ state: new PausedState(transition.state),
734
+ exceededMaxRetries: transition.exceededMaxRetries,
735
+ }
736
+ }
737
+ const _exhaustive: never = transition
738
+ throw new Error(
739
+ `PausedState.handleResponseMetadata: unhandled transition action "${(_exhaustive as ResponseMetadataTransition).action}"`
740
+ )
715
741
  }
716
742
 
717
743
  withHandle(handle: string): PausedState {
@@ -733,34 +759,52 @@ export class PausedState extends ShapeStreamState {
733
759
 
734
760
  export class ErrorState extends ShapeStreamState {
735
761
  readonly kind = `error` as const
736
- readonly previousState: ShapeStreamState
762
+ readonly previousState: ShapeStreamActiveState | PausedState
737
763
  readonly error: Error
738
764
 
739
765
  constructor(previousState: ShapeStreamState, error: Error) {
740
766
  super()
741
- this.previousState = previousState
767
+ this.previousState = (
768
+ previousState instanceof ErrorState
769
+ ? previousState.previousState
770
+ : previousState
771
+ ) as ShapeStreamActiveState | PausedState
742
772
  this.error = error
743
773
  }
744
774
 
745
- get handle() {
775
+ get handle(): string | undefined {
746
776
  return this.previousState.handle
747
777
  }
748
- get offset() {
778
+ get offset(): Offset {
749
779
  return this.previousState.offset
750
780
  }
751
- get schema() {
781
+ get schema(): Schema | undefined {
752
782
  return this.previousState.schema
753
783
  }
754
- get liveCacheBuster() {
784
+ get liveCacheBuster(): string {
755
785
  return this.previousState.liveCacheBuster
756
786
  }
757
- get lastSyncedAt() {
787
+ get lastSyncedAt(): number | undefined {
758
788
  return this.previousState.lastSyncedAt
759
789
  }
760
-
761
790
  get isUpToDate(): boolean {
762
791
  return this.previousState.isUpToDate
763
792
  }
793
+ get staleCacheBuster(): string | undefined {
794
+ return this.previousState.staleCacheBuster
795
+ }
796
+ get staleCacheRetryCount(): number {
797
+ return this.previousState.staleCacheRetryCount
798
+ }
799
+ get sseFallbackToLongPolling(): boolean {
800
+ return this.previousState.sseFallbackToLongPolling
801
+ }
802
+ get consecutiveShortSseConnections(): number {
803
+ return this.previousState.consecutiveShortSseConnections
804
+ }
805
+ get replayCursor(): string | undefined {
806
+ return this.previousState.replayCursor
807
+ }
764
808
 
765
809
  withHandle(handle: string): ErrorState {
766
810
  return new ErrorState(this.previousState.withHandle(handle), this.error)
@@ -783,13 +827,6 @@ export class ErrorState extends ShapeStreamState {
783
827
  // Type alias & factory
784
828
  // ---------------------------------------------------------------------------
785
829
 
786
- export type ShapeStreamActiveState =
787
- | InitialState
788
- | SyncingState
789
- | LiveState
790
- | ReplayingState
791
- | StaleRetryState
792
-
793
830
  export function createInitialState(opts: {
794
831
  offset: Offset
795
832
  handle?: string