@electric-sql/client 1.4.1 → 1.5.0
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 +70 -20
- 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 +70 -20
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +70 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +99 -23
- package/src/shape.ts +2 -2
- 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.0",
|
|
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> {
|
|
@@ -538,7 +559,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
538
559
|
readonly #messageParser: MessageParser<T>
|
|
539
560
|
|
|
540
561
|
readonly #subscribers = new Map<
|
|
541
|
-
|
|
562
|
+
object,
|
|
542
563
|
[
|
|
543
564
|
(messages: Message<T>[]) => MaybePromise<void>,
|
|
544
565
|
((error: Error) => void) | undefined,
|
|
@@ -1161,8 +1182,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1161
1182
|
const currentCursor = this.#liveCacheBuster
|
|
1162
1183
|
|
|
1163
1184
|
if (currentCursor === this.#lastSeenCursor) {
|
|
1164
|
-
// Same cursor
|
|
1165
|
-
//
|
|
1185
|
+
// Same cursor as previous session - suppress this up-to-date notification.
|
|
1186
|
+
// Exit replay mode after first suppression to ensure we don't get stuck
|
|
1187
|
+
// if CDN keeps returning the same cursor indefinitely.
|
|
1188
|
+
this.#lastSeenCursor = undefined
|
|
1166
1189
|
return
|
|
1167
1190
|
}
|
|
1168
1191
|
}
|
|
@@ -1393,7 +1416,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1393
1416
|
callback: (messages: Message<T>[]) => MaybePromise<void>,
|
|
1394
1417
|
onError: (error: Error) => void = () => {}
|
|
1395
1418
|
) {
|
|
1396
|
-
const subscriptionId =
|
|
1419
|
+
const subscriptionId = {}
|
|
1397
1420
|
|
|
1398
1421
|
this.#subscribers.set(subscriptionId, [callback, onError])
|
|
1399
1422
|
if (!this.#started) this.#start()
|
|
@@ -1630,6 +1653,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1630
1653
|
* Fetch a snapshot for subset of data.
|
|
1631
1654
|
* Returns the metadata and the data, but does not inject it into the subscribed data stream.
|
|
1632
1655
|
*
|
|
1656
|
+
* By default, uses GET to send subset parameters as query parameters. This may hit URL length
|
|
1657
|
+
* limits (HTTP 414) with large WHERE clauses or many parameters. Set `method: 'POST'` or use
|
|
1658
|
+
* `subsetMethod: 'POST'` on the stream to send parameters in the request body instead.
|
|
1659
|
+
*
|
|
1633
1660
|
* @param opts - The options for the snapshot request.
|
|
1634
1661
|
* @returns The metadata and the data for the snapshot.
|
|
1635
1662
|
*/
|
|
@@ -1637,27 +1664,35 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1637
1664
|
metadata: SnapshotMetadata
|
|
1638
1665
|
data: Array<ChangeMessage<T>>
|
|
1639
1666
|
}> {
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1667
|
+
const method = opts.method ?? this.options.subsetMethod ?? `GET`
|
|
1668
|
+
const usePost = method === `POST`
|
|
1669
|
+
|
|
1670
|
+
let fetchUrl: URL
|
|
1671
|
+
let fetchOptions: RequestInit
|
|
1672
|
+
|
|
1673
|
+
if (usePost) {
|
|
1674
|
+
const result = await this.#constructUrl(this.options.url, true)
|
|
1675
|
+
fetchUrl = result.fetchUrl
|
|
1676
|
+
fetchOptions = {
|
|
1677
|
+
method: `POST`,
|
|
1678
|
+
headers: {
|
|
1679
|
+
...result.requestHeaders,
|
|
1680
|
+
'Content-Type': `application/json`,
|
|
1681
|
+
},
|
|
1682
|
+
body: JSON.stringify(this.#buildSubsetBody(opts)),
|
|
1683
|
+
}
|
|
1684
|
+
} else {
|
|
1685
|
+
const result = await this.#constructUrl(this.options.url, true, opts)
|
|
1686
|
+
fetchUrl = result.fetchUrl
|
|
1687
|
+
fetchOptions = { headers: result.requestHeaders }
|
|
1688
|
+
}
|
|
1645
1689
|
|
|
1646
|
-
const response = await this.#fetchClient(fetchUrl.toString(),
|
|
1647
|
-
headers: requestHeaders,
|
|
1648
|
-
})
|
|
1690
|
+
const response = await this.#fetchClient(fetchUrl.toString(), fetchOptions)
|
|
1649
1691
|
|
|
1650
1692
|
if (!response.ok) {
|
|
1651
|
-
throw
|
|
1652
|
-
response.status,
|
|
1653
|
-
undefined,
|
|
1654
|
-
undefined,
|
|
1655
|
-
Object.fromEntries([...response.headers.entries()]),
|
|
1656
|
-
fetchUrl.toString()
|
|
1657
|
-
)
|
|
1693
|
+
throw await FetchError.fromResponse(response, fetchUrl.toString())
|
|
1658
1694
|
}
|
|
1659
1695
|
|
|
1660
|
-
// Use schema from stream if available, otherwise extract from response header
|
|
1661
1696
|
const schema: Schema =
|
|
1662
1697
|
this.#schema ??
|
|
1663
1698
|
getSchemaFromHeaders(response.headers, {
|
|
@@ -1671,10 +1706,51 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1671
1706
|
schema
|
|
1672
1707
|
)
|
|
1673
1708
|
|
|
1674
|
-
return {
|
|
1675
|
-
|
|
1676
|
-
|
|
1709
|
+
return { metadata, data }
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
#buildSubsetBody(opts: SubsetParams): Record<string, unknown> {
|
|
1713
|
+
const body: Record<string, unknown> = {}
|
|
1714
|
+
|
|
1715
|
+
if (opts.whereExpr) {
|
|
1716
|
+
body.where = compileExpression(
|
|
1717
|
+
opts.whereExpr,
|
|
1718
|
+
this.options.columnMapper?.encode
|
|
1719
|
+
)
|
|
1720
|
+
body.where_expr = opts.whereExpr
|
|
1721
|
+
} else if (opts.where && typeof opts.where === `string`) {
|
|
1722
|
+
body.where = encodeWhereClause(
|
|
1723
|
+
opts.where,
|
|
1724
|
+
this.options.columnMapper?.encode
|
|
1725
|
+
)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if (opts.params) {
|
|
1729
|
+
body.params = opts.params
|
|
1677
1730
|
}
|
|
1731
|
+
|
|
1732
|
+
if (opts.limit !== undefined) {
|
|
1733
|
+
body.limit = opts.limit
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if (opts.offset !== undefined) {
|
|
1737
|
+
body.offset = opts.offset
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (opts.orderByExpr) {
|
|
1741
|
+
body.order_by = compileOrderBy(
|
|
1742
|
+
opts.orderByExpr,
|
|
1743
|
+
this.options.columnMapper?.encode
|
|
1744
|
+
)
|
|
1745
|
+
body.order_by_expr = opts.orderByExpr
|
|
1746
|
+
} else if (opts.orderBy && typeof opts.orderBy === `string`) {
|
|
1747
|
+
body.order_by = encodeWhereClause(
|
|
1748
|
+
opts.orderBy,
|
|
1749
|
+
this.options.columnMapper?.encode
|
|
1750
|
+
)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return body
|
|
1678
1754
|
}
|
|
1679
1755
|
}
|
|
1680
1756
|
|
package/src/shape.ts
CHANGED
|
@@ -51,7 +51,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
51
51
|
readonly stream: ShapeStreamInterface<T>
|
|
52
52
|
|
|
53
53
|
readonly #data: ShapeData<T> = new Map()
|
|
54
|
-
readonly #subscribers = new Map<
|
|
54
|
+
readonly #subscribers = new Map<object, ShapeChangedCallback<T>>()
|
|
55
55
|
readonly #insertedKeys = new Set<string>()
|
|
56
56
|
readonly #requestedSubSnapshots = new Set<string>()
|
|
57
57
|
#reexecuteSnapshotsPending = false
|
|
@@ -149,7 +149,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
subscribe(callback: ShapeChangedCallback<T>): () => void {
|
|
152
|
-
const subscriptionId =
|
|
152
|
+
const subscriptionId = {}
|
|
153
153
|
|
|
154
154
|
this.#subscribers.set(subscriptionId, callback)
|
|
155
155
|
|
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 = {
|