@electric-sql/client 1.4.2 → 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/dist/cjs/index.cjs +83 -19
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +34 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.legacy-esm.js +83 -19
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +83 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +121 -20
- package/src/types.ts +10 -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.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
|
@@ -429,6 +429,27 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
429
429
|
* ```
|
|
430
430
|
*/
|
|
431
431
|
onError?: ShapeStreamErrorHandler
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* HTTP method to use for subset snapshot requests (`requestSnapshot`/`fetchSnapshot`).
|
|
435
|
+
*
|
|
436
|
+
* - `'GET'` (default): Sends subset params as URL query parameters. May fail with
|
|
437
|
+
* HTTP 414 errors for large queries with many parameters.
|
|
438
|
+
* - `'POST'`: Sends subset params in request body as JSON. Recommended for queries
|
|
439
|
+
* with large parameter lists (e.g., `WHERE id = ANY($1)` with hundreds of IDs).
|
|
440
|
+
*
|
|
441
|
+
* This can be overridden per-request by passing `method` in the subset params.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* const stream = new ShapeStream({
|
|
446
|
+
* url: 'http://localhost:3000/v1/shape',
|
|
447
|
+
* params: { table: 'items' },
|
|
448
|
+
* subsetMethod: 'POST', // Use POST for all subset requests
|
|
449
|
+
* })
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
subsetMethod?: `GET` | `POST`
|
|
432
453
|
}
|
|
433
454
|
|
|
434
455
|
export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
@@ -1097,13 +1118,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1097
1118
|
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key.`
|
|
1098
1119
|
)
|
|
1099
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.
|
|
1100
1123
|
console.warn(
|
|
1101
1124
|
`[Electric] Received stale cached response with expired shape handle. ` +
|
|
1102
1125
|
`This should not happen and indicates a proxy/CDN caching misconfiguration. ` +
|
|
1103
1126
|
`The response contained handle "${shapeHandle}" which was previously marked as expired. ` +
|
|
1104
1127
|
`Check that your proxy includes all query parameters (especially 'handle' and 'offset') in its cache key. ` +
|
|
1105
|
-
`Ignoring the stale
|
|
1128
|
+
`Ignoring the stale response and continuing with handle "${this.#shapeHandle}".`
|
|
1106
1129
|
)
|
|
1130
|
+
return
|
|
1107
1131
|
}
|
|
1108
1132
|
}
|
|
1109
1133
|
|
|
@@ -1632,6 +1656,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1632
1656
|
* Fetch a snapshot for subset of data.
|
|
1633
1657
|
* Returns the metadata and the data, but does not inject it into the subscribed data stream.
|
|
1634
1658
|
*
|
|
1659
|
+
* By default, uses GET to send subset parameters as query parameters. This may hit URL length
|
|
1660
|
+
* limits (HTTP 414) with large WHERE clauses or many parameters. Set `method: 'POST'` or use
|
|
1661
|
+
* `subsetMethod: 'POST'` on the stream to send parameters in the request body instead.
|
|
1662
|
+
*
|
|
1635
1663
|
* @param opts - The options for the snapshot request.
|
|
1636
1664
|
* @returns The metadata and the data for the snapshot.
|
|
1637
1665
|
*/
|
|
@@ -1639,27 +1667,59 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1639
1667
|
metadata: SnapshotMetadata
|
|
1640
1668
|
data: Array<ChangeMessage<T>>
|
|
1641
1669
|
}> {
|
|
1642
|
-
const
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1670
|
+
const method = opts.method ?? this.options.subsetMethod ?? `GET`
|
|
1671
|
+
const usePost = method === `POST`
|
|
1672
|
+
|
|
1673
|
+
let fetchUrl: URL
|
|
1674
|
+
let fetchOptions: RequestInit
|
|
1675
|
+
|
|
1676
|
+
if (usePost) {
|
|
1677
|
+
const result = await this.#constructUrl(this.options.url, true)
|
|
1678
|
+
fetchUrl = result.fetchUrl
|
|
1679
|
+
fetchOptions = {
|
|
1680
|
+
method: `POST`,
|
|
1681
|
+
headers: {
|
|
1682
|
+
...result.requestHeaders,
|
|
1683
|
+
'Content-Type': `application/json`,
|
|
1684
|
+
},
|
|
1685
|
+
body: JSON.stringify(this.#buildSubsetBody(opts)),
|
|
1686
|
+
}
|
|
1687
|
+
} else {
|
|
1688
|
+
const result = await this.#constructUrl(this.options.url, true, opts)
|
|
1689
|
+
fetchUrl = result.fetchUrl
|
|
1690
|
+
fetchOptions = { headers: result.requestHeaders }
|
|
1691
|
+
}
|
|
1647
1692
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
})
|
|
1693
|
+
// Capture handle before fetch to avoid race conditions if it changes during the request
|
|
1694
|
+
const usedHandle = this.#shapeHandle
|
|
1651
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
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Handle non-OK responses from custom fetch clients that bypass the wrapper chain
|
|
1652
1719
|
if (!response.ok) {
|
|
1653
|
-
throw
|
|
1654
|
-
response.status,
|
|
1655
|
-
undefined,
|
|
1656
|
-
undefined,
|
|
1657
|
-
Object.fromEntries([...response.headers.entries()]),
|
|
1658
|
-
fetchUrl.toString()
|
|
1659
|
-
)
|
|
1720
|
+
throw await FetchError.fromResponse(response, fetchUrl.toString())
|
|
1660
1721
|
}
|
|
1661
1722
|
|
|
1662
|
-
// Use schema from stream if available, otherwise extract from response header
|
|
1663
1723
|
const schema: Schema =
|
|
1664
1724
|
this.#schema ??
|
|
1665
1725
|
getSchemaFromHeaders(response.headers, {
|
|
@@ -1673,10 +1733,51 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1673
1733
|
schema
|
|
1674
1734
|
)
|
|
1675
1735
|
|
|
1676
|
-
return {
|
|
1677
|
-
|
|
1678
|
-
|
|
1736
|
+
return { metadata, data }
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
#buildSubsetBody(opts: SubsetParams): Record<string, unknown> {
|
|
1740
|
+
const body: Record<string, unknown> = {}
|
|
1741
|
+
|
|
1742
|
+
if (opts.whereExpr) {
|
|
1743
|
+
body.where = compileExpression(
|
|
1744
|
+
opts.whereExpr,
|
|
1745
|
+
this.options.columnMapper?.encode
|
|
1746
|
+
)
|
|
1747
|
+
body.where_expr = opts.whereExpr
|
|
1748
|
+
} else if (opts.where && typeof opts.where === `string`) {
|
|
1749
|
+
body.where = encodeWhereClause(
|
|
1750
|
+
opts.where,
|
|
1751
|
+
this.options.columnMapper?.encode
|
|
1752
|
+
)
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (opts.params) {
|
|
1756
|
+
body.params = opts.params
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (opts.limit !== undefined) {
|
|
1760
|
+
body.limit = opts.limit
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (opts.offset !== undefined) {
|
|
1764
|
+
body.offset = opts.offset
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (opts.orderByExpr) {
|
|
1768
|
+
body.order_by = compileOrderBy(
|
|
1769
|
+
opts.orderByExpr,
|
|
1770
|
+
this.options.columnMapper?.encode
|
|
1771
|
+
)
|
|
1772
|
+
body.order_by_expr = opts.orderByExpr
|
|
1773
|
+
} else if (opts.orderBy && typeof opts.orderBy === `string`) {
|
|
1774
|
+
body.order_by = encodeWhereClause(
|
|
1775
|
+
opts.orderBy,
|
|
1776
|
+
this.options.columnMapper?.encode
|
|
1777
|
+
)
|
|
1679
1778
|
}
|
|
1779
|
+
|
|
1780
|
+
return body
|
|
1680
1781
|
}
|
|
1681
1782
|
}
|
|
1682
1783
|
|
package/src/types.ts
CHANGED
|
@@ -102,6 +102,16 @@ export type SubsetParams = {
|
|
|
102
102
|
whereExpr?: SerializedExpression
|
|
103
103
|
/** Structured ORDER BY clauses (preferred when available) */
|
|
104
104
|
orderByExpr?: SerializedOrderByClause[]
|
|
105
|
+
/**
|
|
106
|
+
* HTTP method to use for the request. Overrides `subsetMethod` from ShapeStreamOptions.
|
|
107
|
+
* - `GET` (default): Sends subset params as query parameters. May fail with 414 errors
|
|
108
|
+
* for large queries.
|
|
109
|
+
* - `POST`: Sends subset params in request body as JSON. Recommended to avoid URL
|
|
110
|
+
* length limits with large WHERE clauses or many parameters.
|
|
111
|
+
*
|
|
112
|
+
* In Electric 2.0, GET will be deprecated and only POST will be supported.
|
|
113
|
+
*/
|
|
114
|
+
method?: `GET` | `POST`
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
export type ControlMessage = {
|