@electric-sql/client 1.5.4 → 1.5.6
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 +45 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.legacy-esm.js +45 -4
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +45 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +41 -6
- package/src/fetch.ts +26 -0
- package/src/helpers.ts +2 -2
- package/src/shape-stream-state.ts +13 -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.5.
|
|
4
|
+
"version": "1.5.6",
|
|
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
|
@@ -865,11 +865,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
865
865
|
this.#reset(newShapeHandle)
|
|
866
866
|
|
|
867
867
|
// must refetch control message might be in a list or not depending
|
|
868
|
-
// on whether it came from an SSE request or long poll
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
868
|
+
// on whether it came from an SSE request or long poll. The body may
|
|
869
|
+
// also be null/undefined if a proxy returned an unexpected response.
|
|
870
|
+
// Handle all cases defensively here.
|
|
871
|
+
const messages409 = Array.isArray(e.json)
|
|
872
|
+
? e.json
|
|
873
|
+
: e.json != null
|
|
874
|
+
? [e.json]
|
|
875
|
+
: []
|
|
876
|
+
await this.#publish(messages409 as Message<T>[])
|
|
873
877
|
return this.#requestShape()
|
|
874
878
|
} else {
|
|
875
879
|
// errors that have reached this point are not actionable without
|
|
@@ -1142,6 +1146,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1142
1146
|
}
|
|
1143
1147
|
|
|
1144
1148
|
async #onMessages(batch: Array<Message<T>>, isSseMessage = false) {
|
|
1149
|
+
if (!Array.isArray(batch)) {
|
|
1150
|
+
console.warn(
|
|
1151
|
+
`[Electric] #onMessages called with non-array argument (${typeof batch}). ` +
|
|
1152
|
+
`This is a client bug — please report it.`
|
|
1153
|
+
)
|
|
1154
|
+
return
|
|
1155
|
+
}
|
|
1145
1156
|
if (batch.length === 0) return
|
|
1146
1157
|
|
|
1147
1158
|
const lastMessage = batch[batch.length - 1]
|
|
@@ -1249,6 +1260,19 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1249
1260
|
const messages = res || `[]`
|
|
1250
1261
|
const batch = this.#messageParser.parse<Array<Message<T>>>(messages, schema)
|
|
1251
1262
|
|
|
1263
|
+
if (!Array.isArray(batch)) {
|
|
1264
|
+
const preview = JSON.stringify(batch)?.slice(0, 200)
|
|
1265
|
+
throw new FetchError(
|
|
1266
|
+
response.status,
|
|
1267
|
+
`Received non-array response body from shape endpoint. ` +
|
|
1268
|
+
`This may indicate a proxy or CDN is returning an unexpected response. ` +
|
|
1269
|
+
`Expected a JSON array, got ${typeof batch}: ${preview}`,
|
|
1270
|
+
undefined,
|
|
1271
|
+
Object.fromEntries(response.headers.entries()),
|
|
1272
|
+
fetchUrl.toString()
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1252
1276
|
await this.#onMessages(batch)
|
|
1253
1277
|
}
|
|
1254
1278
|
|
|
@@ -1318,7 +1342,18 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1318
1342
|
// #start handles it correctly.
|
|
1319
1343
|
throw new FetchBackoffAbortError()
|
|
1320
1344
|
}
|
|
1321
|
-
throw
|
|
1345
|
+
// Re-throw known Electric errors so the caller can handle them
|
|
1346
|
+
// (e.g., 409 shape rotation, stale cache retry, missing headers).
|
|
1347
|
+
// Other errors (body parsing, SSE protocol failures, null body)
|
|
1348
|
+
// are SSE connection failures handled by the fallback mechanism
|
|
1349
|
+
// in the finally block below.
|
|
1350
|
+
if (
|
|
1351
|
+
error instanceof FetchError ||
|
|
1352
|
+
error instanceof StaleCacheError ||
|
|
1353
|
+
error instanceof MissingHeadersError
|
|
1354
|
+
) {
|
|
1355
|
+
throw error
|
|
1356
|
+
}
|
|
1322
1357
|
} finally {
|
|
1323
1358
|
// Check if the SSE connection closed too quickly
|
|
1324
1359
|
// This can happen when responses are cached or when the proxy/server
|
package/src/fetch.ts
CHANGED
|
@@ -226,6 +226,17 @@ export function createFetchWithChunkBuffer(
|
|
|
226
226
|
|
|
227
227
|
const prefetchClient = async (...args: Parameters<typeof fetchClient>) => {
|
|
228
228
|
const url = args[0].toString()
|
|
229
|
+
const method = getRequestMethod(args[0], args[1])
|
|
230
|
+
|
|
231
|
+
// Prefetch is only valid for GET requests. The prefetch queue matches
|
|
232
|
+
// requests by URL alone and ignores HTTP method/body, so a POST request
|
|
233
|
+
// with the same URL would incorrectly consume the prefetched stream
|
|
234
|
+
// response instead of making its own request.
|
|
235
|
+
if (method !== `GET`) {
|
|
236
|
+
prefetchQueue?.abort()
|
|
237
|
+
prefetchQueue = undefined
|
|
238
|
+
return fetchClient(...args)
|
|
239
|
+
}
|
|
229
240
|
|
|
230
241
|
// try to consume from the prefetch queue first, and if request is
|
|
231
242
|
// not present abort the prefetch queue as it must no longer be valid
|
|
@@ -494,3 +505,18 @@ function chainAborter(
|
|
|
494
505
|
}
|
|
495
506
|
|
|
496
507
|
function noop() {}
|
|
508
|
+
|
|
509
|
+
function getRequestMethod(
|
|
510
|
+
input: Parameters<typeof fetch>[0],
|
|
511
|
+
init?: Parameters<typeof fetch>[1]
|
|
512
|
+
): string {
|
|
513
|
+
if (init?.method) {
|
|
514
|
+
return init.method.toUpperCase()
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (typeof Request !== `undefined` && input instanceof Request) {
|
|
518
|
+
return input.method.toUpperCase()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return `GET`
|
|
522
|
+
}
|
package/src/helpers.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
export function isChangeMessage<T extends Row<unknown> = Row>(
|
|
29
29
|
message: Message<T>
|
|
30
30
|
): message is ChangeMessage<T> {
|
|
31
|
-
return `key` in message
|
|
31
|
+
return message != null && `key` in message
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -51,7 +51,7 @@ export function isChangeMessage<T extends Row<unknown> = Row>(
|
|
|
51
51
|
export function isControlMessage<T extends Row<unknown> = Row>(
|
|
52
52
|
message: Message<T>
|
|
53
53
|
): message is ControlMessage {
|
|
54
|
-
return !isChangeMessage(message)
|
|
54
|
+
return message != null && !isChangeMessage(message)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function isUpToDateMessage<T extends Row<unknown> = Row>(
|
|
@@ -380,6 +380,19 @@ abstract class FetchingState extends ActiveState {
|
|
|
380
380
|
if (staleResult) return staleResult
|
|
381
381
|
|
|
382
382
|
const shared = this.parseResponseFields(input)
|
|
383
|
+
|
|
384
|
+
// NOTE: 204s are deprecated, the Electric server should not send these
|
|
385
|
+
// in latest versions but this is here for backwards compatibility.
|
|
386
|
+
// A 204 means "no content, you're caught up" — transition to live.
|
|
387
|
+
// Skip SSE detection: a 204 gives no indication SSE will work, and
|
|
388
|
+
// the 3-attempt fallback cycle adds unnecessary latency.
|
|
389
|
+
if (input.status === 204) {
|
|
390
|
+
return {
|
|
391
|
+
action: `accepted`,
|
|
392
|
+
state: new LiveState(shared, { sseFallbackToLongPolling: true }),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
383
396
|
return { action: `accepted`, state: new SyncingState(shared) }
|
|
384
397
|
}
|
|
385
398
|
|