@electric-sql/client 1.2.1 → 1.3.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 +524 -564
- 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 +39 -13
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +524 -564
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +35 -12
- package/src/column-mapper.ts +25 -0
- package/src/fetch.ts +14 -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.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
|
@@ -7,9 +7,14 @@ 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
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ColumnMapper,
|
|
15
|
+
encodeWhereClause,
|
|
16
|
+
quoteIdentifier,
|
|
17
|
+
} from './column-mapper'
|
|
13
18
|
import { getOffset, isUpToDateMessage, isChangeMessage } from './helpers'
|
|
14
19
|
import {
|
|
15
20
|
FetchError,
|
|
@@ -128,14 +133,6 @@ export type ExternalParamsRecord<T extends Row<unknown> = Row> = {
|
|
|
128
133
|
[K in string]: ParamValue | undefined
|
|
129
134
|
} & Partial<PostgresParams<T>> & { [K in ReservedParamKeys]?: never }
|
|
130
135
|
|
|
131
|
-
export type SubsetParams = {
|
|
132
|
-
where?: string
|
|
133
|
-
params?: Record<string, string>
|
|
134
|
-
limit?: number
|
|
135
|
-
offset?: number
|
|
136
|
-
orderBy?: string
|
|
137
|
-
}
|
|
138
|
-
|
|
139
136
|
type ReservedParamKeys =
|
|
140
137
|
| typeof LIVE_CACHE_BUSTER_QUERY_PARAM
|
|
141
138
|
| typeof SHAPE_HANDLE_QUERY_PARAM
|
|
@@ -727,7 +724,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
727
724
|
async #requestShape(): Promise<void> {
|
|
728
725
|
if (this.#state === `pause-requested`) {
|
|
729
726
|
this.#state = `paused`
|
|
730
|
-
|
|
731
727
|
return
|
|
732
728
|
}
|
|
733
729
|
|
|
@@ -855,8 +851,27 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
855
851
|
)
|
|
856
852
|
setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere)
|
|
857
853
|
}
|
|
858
|
-
if (params.columns)
|
|
859
|
-
|
|
854
|
+
if (params.columns) {
|
|
855
|
+
// Get original columns array from options (before toInternalParams converted to string)
|
|
856
|
+
const originalColumns = await resolveValue(this.options.params?.columns)
|
|
857
|
+
if (Array.isArray(originalColumns)) {
|
|
858
|
+
// Apply columnMapper encoding if present
|
|
859
|
+
let encodedColumns = originalColumns.map(String)
|
|
860
|
+
if (this.options.columnMapper) {
|
|
861
|
+
encodedColumns = encodedColumns.map(
|
|
862
|
+
this.options.columnMapper.encode
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
// Quote each column name to handle special characters (commas, etc.)
|
|
866
|
+
const serializedColumns = encodedColumns
|
|
867
|
+
.map(quoteIdentifier)
|
|
868
|
+
.join(`,`)
|
|
869
|
+
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, serializedColumns)
|
|
870
|
+
} else {
|
|
871
|
+
// Fallback: columns was already a string
|
|
872
|
+
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
860
875
|
if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica)
|
|
861
876
|
if (params.params)
|
|
862
877
|
setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params)
|
|
@@ -1236,6 +1251,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1236
1251
|
this.#started &&
|
|
1237
1252
|
(this.#state === `paused` || this.#state === `pause-requested`)
|
|
1238
1253
|
) {
|
|
1254
|
+
// Don't resume if the user's signal is already aborted
|
|
1255
|
+
// This can happen if the signal was aborted while we were paused
|
|
1256
|
+
// (e.g., TanStack DB collection was GC'd)
|
|
1257
|
+
if (this.options.signal?.aborted) {
|
|
1258
|
+
return
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1239
1261
|
// If we're resuming from pause-requested state, we need to set state back to active
|
|
1240
1262
|
// to prevent the pause from completing
|
|
1241
1263
|
if (this.#state === `pause-requested`) {
|
|
@@ -1457,6 +1479,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
1457
1479
|
|
|
1458
1480
|
const dataWithEndBoundary = (data as Array<Message<T>>).concat([
|
|
1459
1481
|
{ headers: { control: `snapshot-end`, ...metadata } },
|
|
1482
|
+
{ headers: { control: `subset-end`, ...opts } },
|
|
1460
1483
|
])
|
|
1461
1484
|
|
|
1462
1485
|
this.#snapshotTracker.addSnapshot(
|
package/src/column-mapper.ts
CHANGED
|
@@ -3,6 +3,31 @@ import { Schema } from './types'
|
|
|
3
3
|
type DbColumnName = string
|
|
4
4
|
type AppColumnName = string
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Quote a PostgreSQL identifier for safe use in query parameters.
|
|
8
|
+
*
|
|
9
|
+
* Wraps the identifier in double quotes and escapes any internal
|
|
10
|
+
* double quotes by doubling them. This ensures identifiers with
|
|
11
|
+
* special characters (commas, spaces, etc.) are handled correctly.
|
|
12
|
+
*
|
|
13
|
+
* @param identifier - The identifier to quote
|
|
14
|
+
* @returns The quoted identifier
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* quoteIdentifier('user_id') // '"user_id"'
|
|
19
|
+
* quoteIdentifier('foo,bar') // '"foo,bar"'
|
|
20
|
+
* quoteIdentifier('has"quote') // '"has""quote"'
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function quoteIdentifier(identifier: string): string {
|
|
26
|
+
// Escape internal double quotes by doubling them
|
|
27
|
+
const escaped = identifier.replace(/"/g, `""`)
|
|
28
|
+
return `"${escaped}"`
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
/**
|
|
7
32
|
* A bidirectional column mapper that handles transforming column **names**
|
|
8
33
|
* between database format (e.g., snake_case) and application format (e.g., camelCase).
|
package/src/fetch.ts
CHANGED
|
@@ -221,7 +221,7 @@ export function createFetchWithChunkBuffer(
|
|
|
221
221
|
): typeof fetch {
|
|
222
222
|
const { maxChunksToPrefetch } = prefetchOptions
|
|
223
223
|
|
|
224
|
-
let prefetchQueue: PrefetchQueue
|
|
224
|
+
let prefetchQueue: PrefetchQueue | undefined
|
|
225
225
|
|
|
226
226
|
const prefetchClient = async (...args: Parameters<typeof fetchClient>) => {
|
|
227
227
|
const url = args[0].toString()
|
|
@@ -233,7 +233,10 @@ export function createFetchWithChunkBuffer(
|
|
|
233
233
|
return prefetchedRequest
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
// Clear the prefetch queue after aborting to prevent returning
|
|
237
|
+
// stale/aborted requests on future calls with the same URL
|
|
236
238
|
prefetchQueue?.abort()
|
|
239
|
+
prefetchQueue = undefined
|
|
237
240
|
|
|
238
241
|
// perform request and fire off prefetch queue if request is eligible
|
|
239
242
|
const response = await fetchClient(...args)
|
|
@@ -340,16 +343,24 @@ class PrefetchQueue {
|
|
|
340
343
|
|
|
341
344
|
abort(): void {
|
|
342
345
|
this.#prefetchQueue.forEach(([_, aborter]) => aborter.abort())
|
|
346
|
+
this.#prefetchQueue.clear()
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
consume(...args: Parameters<typeof fetch>): Promise<Response> | void {
|
|
346
350
|
const url = args[0].toString()
|
|
347
351
|
|
|
348
|
-
const
|
|
352
|
+
const entry = this.#prefetchQueue.get(url)
|
|
349
353
|
// only consume if request is in queue and is the queue "head"
|
|
350
354
|
// if request is in the queue but not the head, the queue is being
|
|
351
355
|
// consumed out of order and should be restarted
|
|
352
|
-
if (!
|
|
356
|
+
if (!entry || url !== this.#queueHeadUrl) return
|
|
357
|
+
|
|
358
|
+
const [request, aborter] = entry
|
|
359
|
+
// Don't return aborted requests - they will reject with AbortError
|
|
360
|
+
if (aborter.signal.aborted) {
|
|
361
|
+
this.#prefetchQueue.delete(url)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
353
364
|
this.#prefetchQueue.delete(url)
|
|
354
365
|
|
|
355
366
|
// fire off new prefetch since request has been consumed
|
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 = {
|