@electric-sql/client 1.2.2 → 1.3.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/dist/cjs/index.cjs +533 -575
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +10 -8
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.legacy-esm.js +33 -9
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +533 -575
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +29 -10
- package/src/fetch.ts +30 -3
- package/src/helpers.ts +2 -4
- package/src/types.ts +9 -0
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.
|
|
4
|
+
"version": "1.3.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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
GetExtensions,
|
|
8
8
|
ChangeMessage,
|
|
9
9
|
SnapshotMetadata,
|
|
10
|
+
SubsetParams,
|
|
10
11
|
} from './types'
|
|
11
12
|
import { MessageParser, Parser, TransformFunction } from './parser'
|
|
12
13
|
import {
|
|
@@ -132,14 +133,6 @@ export type ExternalParamsRecord<T extends Row<unknown> = Row> = {
|
|
|
132
133
|
[K in string]: ParamValue | undefined
|
|
133
134
|
} & Partial<PostgresParams<T>> & { [K in ReservedParamKeys]?: never }
|
|
134
135
|
|
|
135
|
-
export type SubsetParams = {
|
|
136
|
-
where?: string
|
|
137
|
-
params?: Record<string, string>
|
|
138
|
-
limit?: number
|
|
139
|
-
offset?: number
|
|
140
|
-
orderBy?: string
|
|
141
|
-
}
|
|
142
|
-
|
|
143
136
|
type ReservedParamKeys =
|
|
144
137
|
| typeof LIVE_CACHE_BUSTER_QUERY_PARAM
|
|
145
138
|
| typeof SHAPE_HANDLE_QUERY_PARAM
|
|
@@ -731,7 +724,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
731
724
|
async #requestShape(): Promise<void> {
|
|
732
725
|
if (this.#state === `pause-requested`) {
|
|
733
726
|
this.#state = `paused`
|
|
734
|
-
|
|
735
727
|
return
|
|
736
728
|
}
|
|
737
729
|
|
|
@@ -990,7 +982,26 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
990
982
|
const { headers, status } = response
|
|
991
983
|
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER)
|
|
992
984
|
if (shapeHandle) {
|
|
993
|
-
this
|
|
985
|
+
// Don't accept a handle we know is expired - this can happen if a
|
|
986
|
+
// proxy serves a stale cached response despite the expired_handle
|
|
987
|
+
// cache buster parameter
|
|
988
|
+
const shapeKey = this.#currentFetchUrl
|
|
989
|
+
? canonicalShapeKey(this.#currentFetchUrl)
|
|
990
|
+
: null
|
|
991
|
+
const expiredHandle = shapeKey
|
|
992
|
+
? expiredShapesCache.getExpiredHandle(shapeKey)
|
|
993
|
+
: null
|
|
994
|
+
if (shapeHandle !== expiredHandle) {
|
|
995
|
+
this.#shapeHandle = shapeHandle
|
|
996
|
+
} else {
|
|
997
|
+
console.warn(
|
|
998
|
+
`[Electric] Received stale cached response with expired shape handle. ` +
|
|
999
|
+
`This should not happen and indicates a proxy/CDN caching misconfiguration. ` +
|
|
1000
|
+
`The response contained handle "${shapeHandle}" which was previously marked as expired. ` +
|
|
1001
|
+
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
|
|
1002
|
+
`Ignoring the stale handle and continuing with handle "${this.#shapeHandle}".`
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
994
1005
|
}
|
|
995
1006
|
|
|
996
1007
|
const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER)
|
|
@@ -1259,6 +1270,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1259
1270
|
this.#started &&
|
|
1260
1271
|
(this.#state === `paused` || this.#state === `pause-requested`)
|
|
1261
1272
|
) {
|
|
1273
|
+
// Don't resume if the user's signal is already aborted
|
|
1274
|
+
// This can happen if the signal was aborted while we were paused
|
|
1275
|
+
// (e.g., TanStack DB collection was GC'd)
|
|
1276
|
+
if (this.options.signal?.aborted) {
|
|
1277
|
+
return
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1262
1280
|
// If we're resuming from pause-requested state, we need to set state back to active
|
|
1263
1281
|
// to prevent the pause from completing
|
|
1264
1282
|
if (this.#state === `pause-requested`) {
|
|
@@ -1480,6 +1498,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1480
1498
|
|
|
1481
1499
|
const dataWithEndBoundary = (data as Array<Message<T>>).concat([
|
|
1482
1500
|
{ headers: { control: `snapshot-end`, ...metadata } },
|
|
1501
|
+
{ headers: { control: `subset-end`, ...opts } },
|
|
1483
1502
|
])
|
|
1484
1503
|
|
|
1485
1504
|
this.#snapshotTracker.addSnapshot(
|
package/src/fetch.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CHUNK_LAST_OFFSET_HEADER,
|
|
3
3
|
CHUNK_UP_TO_DATE_HEADER,
|
|
4
|
+
EXPIRED_HANDLE_QUERY_PARAM,
|
|
4
5
|
LIVE_QUERY_PARAM,
|
|
5
6
|
OFFSET_QUERY_PARAM,
|
|
6
7
|
SHAPE_HANDLE_HEADER,
|
|
@@ -221,7 +222,7 @@ export function createFetchWithChunkBuffer(
|
|
|
221
222
|
): typeof fetch {
|
|
222
223
|
const { maxChunksToPrefetch } = prefetchOptions
|
|
223
224
|
|
|
224
|
-
let prefetchQueue: PrefetchQueue
|
|
225
|
+
let prefetchQueue: PrefetchQueue | undefined
|
|
225
226
|
|
|
226
227
|
const prefetchClient = async (...args: Parameters<typeof fetchClient>) => {
|
|
227
228
|
const url = args[0].toString()
|
|
@@ -233,7 +234,10 @@ export function createFetchWithChunkBuffer(
|
|
|
233
234
|
return prefetchedRequest
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
// Clear the prefetch queue after aborting to prevent returning
|
|
238
|
+
// stale/aborted requests on future calls with the same URL
|
|
236
239
|
prefetchQueue?.abort()
|
|
240
|
+
prefetchQueue = undefined
|
|
237
241
|
|
|
238
242
|
// perform request and fire off prefetch queue if request is eligible
|
|
239
243
|
const response = await fetchClient(...args)
|
|
@@ -340,16 +344,24 @@ class PrefetchQueue {
|
|
|
340
344
|
|
|
341
345
|
abort(): void {
|
|
342
346
|
this.#prefetchQueue.forEach(([_, aborter]) => aborter.abort())
|
|
347
|
+
this.#prefetchQueue.clear()
|
|
343
348
|
}
|
|
344
349
|
|
|
345
350
|
consume(...args: Parameters<typeof fetch>): Promise<Response> | void {
|
|
346
351
|
const url = args[0].toString()
|
|
347
352
|
|
|
348
|
-
const
|
|
353
|
+
const entry = this.#prefetchQueue.get(url)
|
|
349
354
|
// only consume if request is in queue and is the queue "head"
|
|
350
355
|
// if request is in the queue but not the head, the queue is being
|
|
351
356
|
// consumed out of order and should be restarted
|
|
352
|
-
if (!
|
|
357
|
+
if (!entry || url !== this.#queueHeadUrl) return
|
|
358
|
+
|
|
359
|
+
const [request, aborter] = entry
|
|
360
|
+
// Don't return aborted requests - they will reject with AbortError
|
|
361
|
+
if (aborter.signal.aborted) {
|
|
362
|
+
this.#prefetchQueue.delete(url)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
353
365
|
this.#prefetchQueue.delete(url)
|
|
354
366
|
|
|
355
367
|
// fire off new prefetch since request has been consumed
|
|
@@ -425,6 +437,21 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
425
437
|
// potentially miss more recent data
|
|
426
438
|
if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return
|
|
427
439
|
|
|
440
|
+
// don't prefetch if the response handle is the expired handle from the request
|
|
441
|
+
// this can happen when a proxy serves a stale cached response despite the
|
|
442
|
+
// expired_handle cache buster parameter
|
|
443
|
+
const expiredHandle = nextUrl.searchParams.get(EXPIRED_HANDLE_QUERY_PARAM)
|
|
444
|
+
if (expiredHandle && shapeHandle === expiredHandle) {
|
|
445
|
+
console.warn(
|
|
446
|
+
`[Electric] Received stale cached response with expired shape handle. ` +
|
|
447
|
+
`This should not happen and indicates a proxy/CDN caching misconfiguration. ` +
|
|
448
|
+
`The response contained handle "${shapeHandle}" which was previously marked as expired. ` +
|
|
449
|
+
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
|
|
450
|
+
`Skipping prefetch to prevent infinite 409 loop.`
|
|
451
|
+
)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
428
455
|
nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle)
|
|
429
456
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)
|
|
430
457
|
nextUrl.searchParams.sort()
|
package/src/helpers.ts
CHANGED
|
@@ -66,11 +66,9 @@ export function isUpToDateMessage<T extends Row<unknown> = Row>(
|
|
|
66
66
|
* If we are not in SSE mode this function will return undefined.
|
|
67
67
|
*/
|
|
68
68
|
export function getOffset(message: ControlMessage): Offset | undefined {
|
|
69
|
+
if (message.headers.control != `up-to-date`) return
|
|
69
70
|
const lsn = message.headers.global_last_seen_lsn
|
|
70
|
-
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
return `${lsn}_0` as Offset
|
|
71
|
+
return lsn ? (`${lsn}_0` as Offset) : undefined
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
/**
|
package/src/types.ts
CHANGED
|
@@ -68,6 +68,14 @@ export type MoveTag = string
|
|
|
68
68
|
*/
|
|
69
69
|
export type MoveOutPattern = { pos: number; value: string }
|
|
70
70
|
|
|
71
|
+
export type SubsetParams = {
|
|
72
|
+
where?: string
|
|
73
|
+
params?: Record<string, string>
|
|
74
|
+
limit?: number
|
|
75
|
+
offset?: number
|
|
76
|
+
orderBy?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
export type ControlMessage = {
|
|
72
80
|
headers:
|
|
73
81
|
| (Header & {
|
|
@@ -75,6 +83,7 @@ export type ControlMessage = {
|
|
|
75
83
|
global_last_seen_lsn?: string
|
|
76
84
|
})
|
|
77
85
|
| (Header & { control: `snapshot-end` } & PostgresSnapshot)
|
|
86
|
+
| (Header & { control: `subset-end` } & SubsetParams)
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
export type EventMessage = {
|