@electric-sql/client 1.1.0 → 1.1.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/dist/cjs/index.cjs +65 -14
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.legacy-esm.js +65 -14
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +65 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +97 -17
package/src/client.ts
CHANGED
|
@@ -441,6 +441,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
441
441
|
#activeSnapshotRequests = 0 // counter for concurrent snapshot requests
|
|
442
442
|
#midStreamPromise?: Promise<void>
|
|
443
443
|
#midStreamPromiseResolver?: () => void
|
|
444
|
+
#lastSseConnectionStartTime?: number
|
|
445
|
+
#minSseConnectionDuration = 1000 // Minimum expected SSE connection duration (1 second)
|
|
446
|
+
#consecutiveShortSseConnections = 0
|
|
447
|
+
#maxShortSseConnections = 3 // Fall back to long polling after this many short connections
|
|
448
|
+
#sseFallbackToLongPolling = false
|
|
449
|
+
#sseBackoffBaseDelay = 100 // Base delay for exponential backoff (ms)
|
|
450
|
+
#sseBackoffMaxDelay = 5000 // Maximum delay cap (ms)
|
|
444
451
|
|
|
445
452
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
446
453
|
this.options = { subscribe: true, ...options }
|
|
@@ -507,32 +514,61 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
507
514
|
await this.#requestShape()
|
|
508
515
|
} catch (err) {
|
|
509
516
|
this.#error = err
|
|
517
|
+
|
|
518
|
+
// Check if onError handler wants to retry
|
|
510
519
|
if (this.#onError) {
|
|
511
520
|
const retryOpts = await this.#onError(err as Error)
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
521
|
+
// Guard against null (typeof null === "object" in JavaScript)
|
|
522
|
+
if (retryOpts && typeof retryOpts === `object`) {
|
|
523
|
+
// Update params/headers but don't reset offset
|
|
524
|
+
// We want to continue from where we left off, not refetch everything
|
|
525
|
+
if (retryOpts.params) {
|
|
526
|
+
// Merge new params with existing params to preserve other parameters
|
|
527
|
+
this.options.params = {
|
|
528
|
+
...(this.options.params ?? {}),
|
|
529
|
+
...retryOpts.params,
|
|
530
|
+
}
|
|
517
531
|
}
|
|
518
532
|
|
|
519
|
-
if (
|
|
520
|
-
|
|
533
|
+
if (retryOpts.headers) {
|
|
534
|
+
// Merge new headers with existing headers to preserve other headers
|
|
535
|
+
this.options.headers = {
|
|
536
|
+
...(this.options.headers ?? {}),
|
|
537
|
+
...retryOpts.headers,
|
|
538
|
+
}
|
|
521
539
|
}
|
|
522
540
|
|
|
523
|
-
//
|
|
541
|
+
// Clear the error since we're retrying
|
|
542
|
+
this.#error = null
|
|
543
|
+
|
|
544
|
+
// Restart from current offset
|
|
524
545
|
this.#started = false
|
|
525
|
-
this.#start()
|
|
546
|
+
await this.#start()
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
// onError returned void, meaning it doesn't want to retry
|
|
550
|
+
// This is an unrecoverable error, notify subscribers
|
|
551
|
+
if (err instanceof Error) {
|
|
552
|
+
this.#sendErrorToSubscribers(err)
|
|
526
553
|
}
|
|
554
|
+
this.#connected = false
|
|
555
|
+
this.#tickPromiseRejecter?.()
|
|
527
556
|
return
|
|
528
557
|
}
|
|
529
558
|
|
|
530
|
-
//
|
|
531
|
-
throw
|
|
532
|
-
|
|
559
|
+
// No onError handler provided, this is an unrecoverable error
|
|
560
|
+
// Notify subscribers and throw
|
|
561
|
+
if (err instanceof Error) {
|
|
562
|
+
this.#sendErrorToSubscribers(err)
|
|
563
|
+
}
|
|
533
564
|
this.#connected = false
|
|
534
565
|
this.#tickPromiseRejecter?.()
|
|
566
|
+
throw err
|
|
535
567
|
}
|
|
568
|
+
|
|
569
|
+
// Normal completion, clean up
|
|
570
|
+
this.#connected = false
|
|
571
|
+
this.#tickPromiseRejecter?.()
|
|
536
572
|
}
|
|
537
573
|
|
|
538
574
|
async #requestShape(): Promise<void> {
|
|
@@ -613,12 +649,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
613
649
|
)
|
|
614
650
|
return this.#requestShape()
|
|
615
651
|
} else {
|
|
616
|
-
// Notify subscribers
|
|
617
|
-
this.#sendErrorToSubscribers(e)
|
|
618
|
-
|
|
619
652
|
// errors that have reached this point are not actionable without
|
|
620
653
|
// additional user input, such as 400s or failures to read the
|
|
621
|
-
// body of a response, so we exit the loop
|
|
654
|
+
// body of a response, so we exit the loop and let #start handle it
|
|
655
|
+
// Note: We don't notify subscribers here because onError might recover
|
|
622
656
|
throw e
|
|
623
657
|
}
|
|
624
658
|
} finally {
|
|
@@ -832,7 +866,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
832
866
|
this.#isUpToDate &&
|
|
833
867
|
useSse &&
|
|
834
868
|
!this.#isRefreshing &&
|
|
835
|
-
!opts.resumingFromPause
|
|
869
|
+
!opts.resumingFromPause &&
|
|
870
|
+
!this.#sseFallbackToLongPolling
|
|
836
871
|
) {
|
|
837
872
|
opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`)
|
|
838
873
|
opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`)
|
|
@@ -871,6 +906,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
871
906
|
}): Promise<void> {
|
|
872
907
|
const { fetchUrl, requestAbortController, headers } = opts
|
|
873
908
|
const fetch = this.#sseFetchClient
|
|
909
|
+
|
|
910
|
+
// Track when the SSE connection starts
|
|
911
|
+
this.#lastSseConnectionStartTime = Date.now()
|
|
912
|
+
|
|
874
913
|
try {
|
|
875
914
|
let buffer: Array<Message<T>> = []
|
|
876
915
|
await fetchEventSource(fetchUrl.toString(), {
|
|
@@ -916,6 +955,44 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
916
955
|
throw new FetchBackoffAbortError()
|
|
917
956
|
}
|
|
918
957
|
throw error
|
|
958
|
+
} finally {
|
|
959
|
+
// Check if the SSE connection closed too quickly
|
|
960
|
+
// This can happen when responses are cached or when the proxy/server
|
|
961
|
+
// is misconfigured for SSE and closes the connection immediately
|
|
962
|
+
const connectionDuration = Date.now() - this.#lastSseConnectionStartTime!
|
|
963
|
+
const wasAborted = requestAbortController.signal.aborted
|
|
964
|
+
|
|
965
|
+
if (connectionDuration < this.#minSseConnectionDuration && !wasAborted) {
|
|
966
|
+
// Connection was too short - likely a cached response or misconfiguration
|
|
967
|
+
this.#consecutiveShortSseConnections++
|
|
968
|
+
|
|
969
|
+
if (
|
|
970
|
+
this.#consecutiveShortSseConnections >= this.#maxShortSseConnections
|
|
971
|
+
) {
|
|
972
|
+
// Too many short connections - fall back to long polling
|
|
973
|
+
this.#sseFallbackToLongPolling = true
|
|
974
|
+
console.warn(
|
|
975
|
+
`[Electric] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). ` +
|
|
976
|
+
`Falling back to long polling. ` +
|
|
977
|
+
`Your proxy must support streaming SSE responses (not buffer the complete response). ` +
|
|
978
|
+
`Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy. ` +
|
|
979
|
+
`Note: Do NOT disable caching entirely - Electric uses cache headers to enable request collapsing for efficiency.`
|
|
980
|
+
)
|
|
981
|
+
} else {
|
|
982
|
+
// Add exponential backoff with full jitter to prevent tight infinite loop
|
|
983
|
+
// Formula: random(0, min(cap, base * 2^attempt))
|
|
984
|
+
const maxDelay = Math.min(
|
|
985
|
+
this.#sseBackoffMaxDelay,
|
|
986
|
+
this.#sseBackoffBaseDelay *
|
|
987
|
+
Math.pow(2, this.#consecutiveShortSseConnections)
|
|
988
|
+
)
|
|
989
|
+
const delayMs = Math.floor(Math.random() * maxDelay)
|
|
990
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
991
|
+
}
|
|
992
|
+
} else if (connectionDuration >= this.#minSseConnectionDuration) {
|
|
993
|
+
// Connection was healthy - reset counter
|
|
994
|
+
this.#consecutiveShortSseConnections = 0
|
|
995
|
+
}
|
|
919
996
|
}
|
|
920
997
|
}
|
|
921
998
|
|
|
@@ -1090,6 +1167,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1090
1167
|
this.#connected = false
|
|
1091
1168
|
this.#schema = undefined
|
|
1092
1169
|
this.#activeSnapshotRequests = 0
|
|
1170
|
+
// Reset SSE fallback state to try SSE again after reset
|
|
1171
|
+
this.#consecutiveShortSseConnections = 0
|
|
1172
|
+
this.#sseFallbackToLongPolling = false
|
|
1093
1173
|
}
|
|
1094
1174
|
|
|
1095
1175
|
/**
|