@electric-sql/client 1.5.10 → 1.5.11

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.10",
4
+ "version": "1.5.11",
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
@@ -96,6 +96,10 @@ const RESERVED_PARAMS: Set<ReservedParamKeys> = new Set([
96
96
 
97
97
  const TROUBLESHOOTING_URL = `https://electric-sql.com/docs/guides/troubleshooting`
98
98
 
99
+ function createCacheBuster(): string {
100
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
101
+ }
102
+
99
103
  type Replica = `full` | `default`
100
104
  export type LogMode = `changes_only` | `full`
101
105
 
@@ -617,6 +621,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
617
621
  #fastLoopBackoffMaxMs = 5_000
618
622
  #fastLoopConsecutiveCount = 0
619
623
  #fastLoopMaxCount = 5
624
+ #refetchCacheBuster?: string
620
625
 
621
626
  constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
622
627
  this.options = { subscribe: true, ...options }
@@ -875,10 +880,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
875
880
  if (!(e instanceof FetchError)) throw e // should never happen
876
881
 
877
882
  if (e.status == 409) {
878
- // Upon receiving a 409, we should start from scratch
879
- // with the newly provided shape handle, or a fallback
880
- // pseudo-handle based on the current one to act as a
881
- // consistent cache buster
883
+ // Upon receiving a 409, start from scratch with the newly
884
+ // provided shape handle. If the header is missing (e.g. proxy
885
+ // stripped it), reset without a handle and use a random
886
+ // cache-buster query param to ensure the retry URL is unique.
882
887
 
883
888
  // Store the current shape URL as expired to avoid future 409s
884
889
  if (this.#syncState.handle) {
@@ -886,8 +891,14 @@ export class ShapeStream<T extends Row<unknown> = Row>
886
891
  expiredShapesCache.markExpired(shapeKey, this.#syncState.handle)
887
892
  }
888
893
 
889
- const newShapeHandle =
890
- e.headers[SHAPE_HANDLE_HEADER] || `${this.#syncState.handle!}-next`
894
+ const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER]
895
+ if (!newShapeHandle) {
896
+ console.warn(
897
+ `[Electric] Received 409 response without a shape handle header. ` +
898
+ `This likely indicates a proxy or CDN stripping required headers.`
899
+ )
900
+ this.#refetchCacheBuster = createCacheBuster()
901
+ }
891
902
  this.#reset(newShapeHandle)
892
903
 
893
904
  // must refetch control message might be in a list or not depending
@@ -1144,6 +1155,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
1144
1155
  fetchUrl.searchParams.set(EXPIRED_HANDLE_QUERY_PARAM, expiredHandle)
1145
1156
  }
1146
1157
 
1158
+ // Add one-shot cache buster when a 409 response lacked a handle header
1159
+ // (e.g. proxy stripped it). Ensures each retry has a unique URL.
1160
+ if (this.#refetchCacheBuster) {
1161
+ fetchUrl.searchParams.set(
1162
+ CACHE_BUSTER_QUERY_PARAM,
1163
+ this.#refetchCacheBuster
1164
+ )
1165
+ this.#refetchCacheBuster = undefined
1166
+ }
1167
+
1147
1168
  // sort query params in-place for stable URLs and improved cache hits
1148
1169
  fetchUrl.searchParams.sort()
1149
1170
 
@@ -1199,8 +1220,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1199
1220
  expiredHandle,
1200
1221
  now: Date.now(),
1201
1222
  maxStaleCacheRetries: this.#maxStaleCacheRetries,
1202
- createCacheBuster: () =>
1203
- `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
1223
+ createCacheBuster,
1204
1224
  })
1205
1225
 
1206
1226
  this.#syncState = transition.state
@@ -1786,8 +1806,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
1786
1806
  expiredHandle: null,
1787
1807
  now: Date.now(),
1788
1808
  maxStaleCacheRetries: this.#maxStaleCacheRetries,
1789
- createCacheBuster: () =>
1790
- `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
1809
+ createCacheBuster,
1791
1810
  })
1792
1811
  if (transition.action === `accepted`) {
1793
1812
  this.#syncState = transition.state
@@ -1869,9 +1888,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
1869
1888
 
1870
1889
  // For snapshot 409s, only update the handle — don't reset offset/schema/etc.
1871
1890
  // The main stream is paused and should not be disturbed.
1872
- const nextHandle =
1873
- e.headers[SHAPE_HANDLE_HEADER] || `${usedHandle ?? `handle`}-next`
1874
- this.#syncState = this.#syncState.withHandle(nextHandle)
1891
+ const nextHandle = e.headers[SHAPE_HANDLE_HEADER]
1892
+ if (nextHandle) {
1893
+ this.#syncState = this.#syncState.withHandle(nextHandle)
1894
+ } else {
1895
+ console.warn(
1896
+ `[Electric] Received 409 response without a shape handle header. ` +
1897
+ `This likely indicates a proxy or CDN stripping required headers.`
1898
+ )
1899
+ this.#refetchCacheBuster = createCacheBuster()
1900
+ }
1875
1901
 
1876
1902
  return this.fetchSnapshot(opts)
1877
1903
  }