@electric-sql/client 1.5.0 → 1.5.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.
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.0",
4
+ "version": "1.5.1",
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
@@ -1118,13 +1118,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
1118
1118
  `Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key.`
1119
1119
  )
1120
1120
  } else {
1121
+ // We already have a valid handle, so ignore the stale response entirely
1122
+ // to prevent a mismatch between our current handle and the stale offset.
1121
1123
  console.warn(
1122
1124
  `[Electric] Received stale cached response with expired shape handle. ` +
1123
1125
  `This should not happen and indicates a proxy/CDN caching misconfiguration. ` +
1124
1126
  `The response contained handle "${shapeHandle}" which was previously marked as expired. ` +
1125
1127
  `Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
1126
- `Ignoring the stale handle and continuing with handle "${this.#shapeHandle}".`
1128
+ `Ignoring the stale response and continuing with handle "${this.#shapeHandle}".`
1127
1129
  )
1130
+ return
1128
1131
  }
1129
1132
  }
1130
1133
 
@@ -1687,8 +1690,32 @@ export class ShapeStream<T extends Row<unknown> = Row>
1687
1690
  fetchOptions = { headers: result.requestHeaders }
1688
1691
  }
1689
1692
 
1690
- const response = await this.#fetchClient(fetchUrl.toString(), fetchOptions)
1693
+ // Capture handle before fetch to avoid race conditions if it changes during the request
1694
+ const usedHandle = this.#shapeHandle
1695
+
1696
+ let response: Response
1697
+ try {
1698
+ response = await this.#fetchClient(fetchUrl.toString(), fetchOptions)
1699
+ } catch (e) {
1700
+ // Handle 409 "must-refetch" - shape handle changed/expired.
1701
+ // The fetch wrapper throws FetchError for non-OK responses, so we catch here.
1702
+ // Unlike #requestShape, we don't call #reset() here as that would
1703
+ // clear #activeSnapshotRequests and break requestSnapshot's pause/resume logic.
1704
+ if (e instanceof FetchError && e.status === 409) {
1705
+ if (usedHandle) {
1706
+ const shapeKey = canonicalShapeKey(fetchUrl)
1707
+ expiredShapesCache.markExpired(shapeKey, usedHandle)
1708
+ }
1709
+
1710
+ this.#shapeHandle =
1711
+ e.headers[SHAPE_HANDLE_HEADER] || `${usedHandle ?? `handle`}-next`
1712
+
1713
+ return this.fetchSnapshot(opts)
1714
+ }
1715
+ throw e
1716
+ }
1691
1717
 
1718
+ // Handle non-OK responses from custom fetch clients that bypass the wrapper chain
1692
1719
  if (!response.ok) {
1693
1720
  throw await FetchError.fromResponse(response, fetchUrl.toString())
1694
1721
  }