@electric-sql/client 1.5.6 → 1.5.7
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 +41 -7
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +4 -2
- package/dist/index.browser.mjs +4 -4
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.legacy-esm.js +41 -7
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +41 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +44 -7
- package/src/helpers.ts +15 -0
- package/src/shape-stream-state.ts +10 -0
- package/src/shape.ts +6 -2
- package/src/types.ts +1 -1
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.7",
|
|
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
|
@@ -15,7 +15,12 @@ import {
|
|
|
15
15
|
encodeWhereClause,
|
|
16
16
|
quoteIdentifier,
|
|
17
17
|
} from './column-mapper'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getOffset,
|
|
20
|
+
isUpToDateMessage,
|
|
21
|
+
isChangeMessage,
|
|
22
|
+
bigintSafeStringify,
|
|
23
|
+
} from './helpers'
|
|
19
24
|
import {
|
|
20
25
|
FetchError,
|
|
21
26
|
FetchBackoffAbortError,
|
|
@@ -987,7 +992,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
987
992
|
// Serialize params as JSON to keep the parameter name constant for proxy configs
|
|
988
993
|
fetchUrl.searchParams.set(
|
|
989
994
|
SUBSET_PARAM_WHERE_PARAMS,
|
|
990
|
-
|
|
995
|
+
bigintSafeStringify(subsetParams.params)
|
|
991
996
|
)
|
|
992
997
|
if (subsetParams.limit)
|
|
993
998
|
setQueryParam(fetchUrl, SUBSET_PARAM_LIMIT, subsetParams.limit)
|
|
@@ -1261,7 +1266,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1261
1266
|
const batch = this.#messageParser.parse<Array<Message<T>>>(messages, schema)
|
|
1262
1267
|
|
|
1263
1268
|
if (!Array.isArray(batch)) {
|
|
1264
|
-
const preview =
|
|
1269
|
+
const preview = bigintSafeStringify(batch)?.slice(0, 200)
|
|
1265
1270
|
throw new FetchError(
|
|
1266
1271
|
response.status,
|
|
1267
1272
|
`Received non-array response body from shape endpoint. ` +
|
|
@@ -1650,7 +1655,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1650
1655
|
}, 30_000)
|
|
1651
1656
|
|
|
1652
1657
|
try {
|
|
1653
|
-
const { metadata, data } =
|
|
1658
|
+
const { metadata, data, responseOffset, responseHandle } =
|
|
1659
|
+
await this.fetchSnapshot(opts)
|
|
1654
1660
|
|
|
1655
1661
|
const dataWithEndBoundary = (data as Array<Message<T>>).concat([
|
|
1656
1662
|
{ headers: { control: `snapshot-end`, ...metadata } },
|
|
@@ -1663,6 +1669,31 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1663
1669
|
)
|
|
1664
1670
|
this.#onMessages(dataWithEndBoundary, false)
|
|
1665
1671
|
|
|
1672
|
+
// On cold start the stream's offset is still at "now". Advance it
|
|
1673
|
+
// to the snapshot's position so no updates are missed in between.
|
|
1674
|
+
if (responseOffset !== null || responseHandle !== null) {
|
|
1675
|
+
const transition = this.#syncState.handleResponseMetadata({
|
|
1676
|
+
status: 200,
|
|
1677
|
+
responseHandle,
|
|
1678
|
+
responseOffset,
|
|
1679
|
+
responseCursor: null,
|
|
1680
|
+
expiredHandle: null,
|
|
1681
|
+
now: Date.now(),
|
|
1682
|
+
maxStaleCacheRetries: this.#maxStaleCacheRetries,
|
|
1683
|
+
createCacheBuster: () =>
|
|
1684
|
+
`${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1685
|
+
})
|
|
1686
|
+
if (transition.action === `accepted`) {
|
|
1687
|
+
this.#syncState = transition.state
|
|
1688
|
+
} else {
|
|
1689
|
+
console.warn(
|
|
1690
|
+
`[Electric] Snapshot response metadata was not accepted ` +
|
|
1691
|
+
`by state "${this.#syncState.kind}" (action: ${transition.action}). ` +
|
|
1692
|
+
`Stream offset was not advanced from snapshot.`
|
|
1693
|
+
)
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1666
1697
|
return {
|
|
1667
1698
|
metadata,
|
|
1668
1699
|
data,
|
|
@@ -1682,11 +1713,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1682
1713
|
* `subsetMethod: 'POST'` on the stream to send parameters in the request body instead.
|
|
1683
1714
|
*
|
|
1684
1715
|
* @param opts - The options for the snapshot request.
|
|
1685
|
-
* @returns The metadata and the
|
|
1716
|
+
* @returns The metadata, data, and the response's offset/handle for state advancement.
|
|
1686
1717
|
*/
|
|
1687
1718
|
async fetchSnapshot(opts: SubsetParams): Promise<{
|
|
1688
1719
|
metadata: SnapshotMetadata
|
|
1689
1720
|
data: Array<ChangeMessage<T>>
|
|
1721
|
+
responseOffset: Offset | null
|
|
1722
|
+
responseHandle: string | null
|
|
1690
1723
|
}> {
|
|
1691
1724
|
const method = opts.method ?? this.options.subsetMethod ?? `GET`
|
|
1692
1725
|
const usePost = method === `POST`
|
|
@@ -1703,7 +1736,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1703
1736
|
...result.requestHeaders,
|
|
1704
1737
|
'Content-Type': `application/json`,
|
|
1705
1738
|
},
|
|
1706
|
-
body:
|
|
1739
|
+
body: bigintSafeStringify(this.#buildSubsetBody(opts)),
|
|
1707
1740
|
}
|
|
1708
1741
|
} else {
|
|
1709
1742
|
const result = await this.#constructUrl(this.options.url, true, opts)
|
|
@@ -1757,7 +1790,11 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1757
1790
|
schema
|
|
1758
1791
|
)
|
|
1759
1792
|
|
|
1760
|
-
|
|
1793
|
+
const responseOffset =
|
|
1794
|
+
(response.headers.get(CHUNK_LAST_OFFSET_HEADER) as Offset) || null
|
|
1795
|
+
const responseHandle = response.headers.get(SHAPE_HANDLE_HEADER)
|
|
1796
|
+
|
|
1797
|
+
return { metadata, data, responseOffset, responseHandle }
|
|
1761
1798
|
}
|
|
1762
1799
|
|
|
1763
1800
|
#buildSubsetBody(opts: SubsetParams): Record<string, unknown> {
|
package/src/helpers.ts
CHANGED
|
@@ -71,6 +71,21 @@ export function getOffset(message: ControlMessage): Offset | undefined {
|
|
|
71
71
|
return lsn ? (`${lsn}_0` as Offset) : undefined
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function bigintReplacer(_key: string, value: unknown): unknown {
|
|
75
|
+
return typeof value === `bigint` ? value.toString() : value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* BigInt-safe version of JSON.stringify.
|
|
80
|
+
* Converts BigInt values to their string representation (as JSON strings,
|
|
81
|
+
* e.g. `{ id: 42n }` becomes `{"id":"42"}`) instead of throwing.
|
|
82
|
+
* Assumes input is a JSON-serializable value — passing `undefined` at the
|
|
83
|
+
* top level will return `undefined` (matching `JSON.stringify` behavior).
|
|
84
|
+
*/
|
|
85
|
+
export function bigintSafeStringify(value: unknown): string {
|
|
86
|
+
return JSON.stringify(value, bigintReplacer)
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
/**
|
|
75
90
|
* Checks if a transaction is visible in a snapshot.
|
|
76
91
|
*
|
|
@@ -704,6 +704,16 @@ export class PausedState extends ShapeStreamState {
|
|
|
704
704
|
return this.previousState.replayCursor
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
+
handleResponseMetadata(
|
|
708
|
+
input: ResponseMetadataInput
|
|
709
|
+
): ResponseMetadataTransition {
|
|
710
|
+
const transition = this.previousState.handleResponseMetadata(input)
|
|
711
|
+
if (transition.action === `accepted`) {
|
|
712
|
+
return { action: `accepted`, state: new PausedState(transition.state) }
|
|
713
|
+
}
|
|
714
|
+
return transition
|
|
715
|
+
}
|
|
716
|
+
|
|
707
717
|
withHandle(handle: string): PausedState {
|
|
708
718
|
return new PausedState(this.previousState.withHandle(handle))
|
|
709
719
|
}
|
package/src/shape.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Message, Offset, Row } from './types'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isChangeMessage,
|
|
4
|
+
isControlMessage,
|
|
5
|
+
bigintSafeStringify,
|
|
6
|
+
} from './helpers'
|
|
3
7
|
import { FetchError } from './error'
|
|
4
8
|
import { LogMode, ShapeStreamInterface } from './client'
|
|
5
9
|
|
|
@@ -141,7 +145,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
141
145
|
params: Parameters<ShapeStreamInterface<T>[`requestSnapshot`]>[0]
|
|
142
146
|
): Promise<void> {
|
|
143
147
|
// Track this snapshot request for future re-execution on shape rotation
|
|
144
|
-
const key =
|
|
148
|
+
const key = bigintSafeStringify(params)
|
|
145
149
|
this.#requestedSubSnapshots.add(key)
|
|
146
150
|
// Ensure the stream is up-to-date so schema is available for parsing
|
|
147
151
|
await this.#awaitUpToDate()
|
package/src/types.ts
CHANGED
|
@@ -91,7 +91,7 @@ export type SubsetParams = {
|
|
|
91
91
|
/** Legacy string format WHERE clause */
|
|
92
92
|
where?: string
|
|
93
93
|
/** Positional parameter values for WHERE clause */
|
|
94
|
-
params?: Record<string, string>
|
|
94
|
+
params?: Record<string, string | bigint | number>
|
|
95
95
|
/** Maximum number of rows to return */
|
|
96
96
|
limit?: number
|
|
97
97
|
/** Number of rows to skip */
|