@electric-sql/client 0.8.0 → 0.9.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/README.md +10 -4
- package/dist/cjs/index.cjs +45 -46
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +394 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +49 -38
- package/dist/index.legacy-esm.js +44 -46
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +44 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +33 -28
- package/src/client.ts +87 -68
- package/src/constants.ts +0 -1
- package/src/fetch.ts +3 -2
- package/src/shape.ts +17 -12
- package/src/types.ts +3 -1
package/src/client.ts
CHANGED
|
@@ -34,27 +34,49 @@ import {
|
|
|
34
34
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
35
35
|
SHAPE_SCHEMA_HEADER,
|
|
36
36
|
WHERE_QUERY_PARAM,
|
|
37
|
-
DATABASE_ID_QUERY_PARAM,
|
|
38
37
|
TABLE_QUERY_PARAM,
|
|
39
38
|
REPLICA_PARAM,
|
|
40
39
|
} from './constants'
|
|
41
40
|
|
|
42
41
|
const RESERVED_PARAMS = new Set([
|
|
43
|
-
DATABASE_ID_QUERY_PARAM,
|
|
44
|
-
COLUMNS_QUERY_PARAM,
|
|
45
42
|
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
46
43
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
47
44
|
LIVE_QUERY_PARAM,
|
|
48
45
|
OFFSET_QUERY_PARAM,
|
|
49
|
-
TABLE_QUERY_PARAM,
|
|
50
|
-
WHERE_QUERY_PARAM,
|
|
51
|
-
REPLICA_PARAM,
|
|
52
46
|
])
|
|
53
47
|
|
|
54
48
|
type Replica = `full` | `default`
|
|
55
49
|
|
|
50
|
+
/**
|
|
51
|
+
* PostgreSQL-specific shape parameters that can be provided externally
|
|
52
|
+
*/
|
|
53
|
+
type PostgresParams = {
|
|
54
|
+
/** The root table for the shape. Not required if you set the table in your proxy. */
|
|
55
|
+
table?: string
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The columns to include in the shape.
|
|
59
|
+
* Must include primary keys, and can only include valid columns.
|
|
60
|
+
*/
|
|
61
|
+
columns?: string[]
|
|
62
|
+
|
|
63
|
+
/** The where clauses for the shape */
|
|
64
|
+
where?: string
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* If `replica` is `default` (the default) then Electric will only send the
|
|
68
|
+
* changed columns in an update.
|
|
69
|
+
*
|
|
70
|
+
* If it's `full` Electric will send the entire row with both changed and
|
|
71
|
+
* unchanged values.
|
|
72
|
+
*
|
|
73
|
+
* Setting `replica` to `full` will result in higher bandwidth
|
|
74
|
+
* usage and so is not generally recommended.
|
|
75
|
+
*/
|
|
76
|
+
replica?: Replica
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
type ReservedParamKeys =
|
|
57
|
-
| typeof DATABASE_ID_QUERY_PARAM
|
|
58
80
|
| typeof COLUMNS_QUERY_PARAM
|
|
59
81
|
| typeof LIVE_CACHE_BUSTER_QUERY_PARAM
|
|
60
82
|
| typeof SHAPE_HANDLE_QUERY_PARAM
|
|
@@ -64,12 +86,38 @@ type ReservedParamKeys =
|
|
|
64
86
|
| typeof WHERE_QUERY_PARAM
|
|
65
87
|
| typeof REPLICA_PARAM
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
/**
|
|
90
|
+
* External params type - what users provide.
|
|
91
|
+
* Includes documented PostgreSQL params and allows string or string[] values for any additional params.
|
|
92
|
+
*/
|
|
93
|
+
type ExternalParamsRecord = Partial<PostgresParams> & {
|
|
94
|
+
[K in string as K extends ReservedParamKeys ? never : K]: string | string[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Internal params type - used within the library.
|
|
99
|
+
* All values are converted to strings.
|
|
100
|
+
*/
|
|
101
|
+
type InternalParamsRecord = {
|
|
102
|
+
[K in string as K extends ReservedParamKeys ? never : K]: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Helper function to convert external params to internal format
|
|
107
|
+
*/
|
|
108
|
+
function toInternalParams(params: ExternalParamsRecord): InternalParamsRecord {
|
|
109
|
+
const result: InternalParamsRecord = {}
|
|
110
|
+
for (const [key, value] of Object.entries(params)) {
|
|
111
|
+
result[key] = Array.isArray(value) ? value.join(`,`) : value
|
|
112
|
+
}
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
68
115
|
|
|
69
116
|
type RetryOpts = {
|
|
70
|
-
params?:
|
|
117
|
+
params?: ExternalParamsRecord
|
|
71
118
|
headers?: Record<string, string>
|
|
72
119
|
}
|
|
120
|
+
|
|
73
121
|
type ShapeStreamErrorHandler = (
|
|
74
122
|
error: Error
|
|
75
123
|
) => void | RetryOpts | Promise<void | RetryOpts>
|
|
@@ -84,40 +132,6 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
84
132
|
*/
|
|
85
133
|
url: string
|
|
86
134
|
|
|
87
|
-
/**
|
|
88
|
-
* Which database to use.
|
|
89
|
-
* This is optional unless Electric is used with multiple databases.
|
|
90
|
-
*/
|
|
91
|
-
databaseId?: string
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The root table for the shape. Passed as a query parameter. Not required if you set the table in your proxy.
|
|
95
|
-
*/
|
|
96
|
-
table?: string
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The where clauses for the shape.
|
|
100
|
-
*/
|
|
101
|
-
where?: string
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The columns to include in the shape.
|
|
105
|
-
* Must include primary keys, and can only inlude valid columns.
|
|
106
|
-
*/
|
|
107
|
-
columns?: string[]
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* If `replica` is `default` (the default) then Electric will only send the
|
|
111
|
-
* changed columns in an update.
|
|
112
|
-
*
|
|
113
|
-
* If it's `full` Electric will send the entire row with both changed and
|
|
114
|
-
* unchanged values.
|
|
115
|
-
*
|
|
116
|
-
* Setting `replica` to `full` will obviously result in higher bandwidth
|
|
117
|
-
* usage and so is not recommended.
|
|
118
|
-
*/
|
|
119
|
-
replica?: Replica
|
|
120
|
-
|
|
121
135
|
/**
|
|
122
136
|
* The "offset" on the shape log. This is typically not set as the ShapeStream
|
|
123
137
|
* will handle this automatically. A common scenario where you might pass an offset
|
|
@@ -144,9 +158,12 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
144
158
|
* Additional request parameters to attach to the URL.
|
|
145
159
|
* These will be merged with Electric's standard parameters.
|
|
146
160
|
* Note: You cannot use Electric's reserved parameter names
|
|
147
|
-
* (
|
|
161
|
+
* (offset, handle, live, cursor).
|
|
162
|
+
*
|
|
163
|
+
* PostgreSQL-specific options like table, where, columns, and replica
|
|
164
|
+
* should be specified here.
|
|
148
165
|
*/
|
|
149
|
-
params?:
|
|
166
|
+
params?: ExternalParamsRecord
|
|
150
167
|
|
|
151
168
|
/**
|
|
152
169
|
* Automatically fetch updates to the Shape. If you just want to sync the current
|
|
@@ -162,7 +179,7 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
162
179
|
/**
|
|
163
180
|
* A function for handling shapestream errors.
|
|
164
181
|
* This is optional, when it is not provided any shapestream errors will be thrown.
|
|
165
|
-
* If the function
|
|
182
|
+
* If the function returns an object containing parameters and/or headers
|
|
166
183
|
* the shapestream will apply those changes and try syncing again.
|
|
167
184
|
* If the function returns void the shapestream is stopped.
|
|
168
185
|
*/
|
|
@@ -173,7 +190,7 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
173
190
|
subscribe(
|
|
174
191
|
callback: (messages: Message<T>[]) => MaybePromise<void>,
|
|
175
192
|
onError?: (error: FetchError | Error) => void
|
|
176
|
-
): void
|
|
193
|
+
): () => void
|
|
177
194
|
unsubscribeAll(): void
|
|
178
195
|
|
|
179
196
|
isLoading(): boolean
|
|
@@ -246,10 +263,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
246
263
|
#isUpToDate: boolean = false
|
|
247
264
|
#connected: boolean = false
|
|
248
265
|
#shapeHandle?: string
|
|
249
|
-
#databaseId?: string
|
|
250
266
|
#schema?: Schema
|
|
251
267
|
#onError?: ShapeStreamErrorHandler
|
|
252
|
-
#replica?: Replica
|
|
253
268
|
|
|
254
269
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
255
270
|
this.options = { subscribe: true, ...options }
|
|
@@ -257,9 +272,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
257
272
|
this.#lastOffset = this.options.offset ?? `-1`
|
|
258
273
|
this.#liveCacheBuster = ``
|
|
259
274
|
this.#shapeHandle = this.options.handle
|
|
260
|
-
this.#databaseId = this.options.databaseId
|
|
261
275
|
this.#messageParser = new MessageParser<T>(options.parser)
|
|
262
|
-
this.#replica = this.options.replica
|
|
263
276
|
this.#onError = this.options.onError
|
|
264
277
|
|
|
265
278
|
const baseFetchClient =
|
|
@@ -303,7 +316,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
303
316
|
(!this.options.signal?.aborted && !this.#isUpToDate) ||
|
|
304
317
|
this.options.subscribe
|
|
305
318
|
) {
|
|
306
|
-
const { url,
|
|
319
|
+
const { url, signal } = this.options
|
|
307
320
|
|
|
308
321
|
const fetchUrl = new URL(url)
|
|
309
322
|
|
|
@@ -319,16 +332,30 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
319
332
|
)
|
|
320
333
|
}
|
|
321
334
|
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
// Add PostgreSQL-specific parameters from params
|
|
336
|
+
const params = toInternalParams(this.options.params)
|
|
337
|
+
if (params.table)
|
|
338
|
+
fetchUrl.searchParams.set(TABLE_QUERY_PARAM, params.table)
|
|
339
|
+
if (params.where)
|
|
340
|
+
fetchUrl.searchParams.set(WHERE_QUERY_PARAM, params.where)
|
|
341
|
+
if (params.columns)
|
|
342
|
+
fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, params.columns)
|
|
343
|
+
if (params.replica)
|
|
344
|
+
fetchUrl.searchParams.set(REPLICA_PARAM, params.replica)
|
|
345
|
+
|
|
346
|
+
// Add any remaining custom parameters
|
|
347
|
+
const customParams = { ...params }
|
|
348
|
+
delete customParams.table
|
|
349
|
+
delete customParams.where
|
|
350
|
+
delete customParams.columns
|
|
351
|
+
delete customParams.replica
|
|
352
|
+
|
|
353
|
+
for (const [key, value] of Object.entries(customParams)) {
|
|
354
|
+
fetchUrl.searchParams.set(key, value as string)
|
|
324
355
|
}
|
|
325
356
|
}
|
|
326
357
|
|
|
327
358
|
// Add Electric's internal parameters
|
|
328
|
-
if (table) fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table)
|
|
329
|
-
if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)
|
|
330
|
-
if (columns && columns.length > 0)
|
|
331
|
-
fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`))
|
|
332
359
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)
|
|
333
360
|
|
|
334
361
|
if (this.#isUpToDate) {
|
|
@@ -347,16 +374,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
347
374
|
)
|
|
348
375
|
}
|
|
349
376
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
(this.#replica ?? ShapeStream.Replica.DEFAULT) !=
|
|
356
|
-
ShapeStream.Replica.DEFAULT
|
|
357
|
-
) {
|
|
358
|
-
fetchUrl.searchParams.set(REPLICA_PARAM, this.#replica as string)
|
|
359
|
-
}
|
|
377
|
+
// sort query params in-place for stable URLs and improved cache hits
|
|
378
|
+
fetchUrl.searchParams.sort()
|
|
360
379
|
|
|
361
380
|
let response!: Response
|
|
362
381
|
try {
|
package/src/constants.ts
CHANGED
|
@@ -3,7 +3,6 @@ export const SHAPE_HANDLE_HEADER = `electric-handle`
|
|
|
3
3
|
export const CHUNK_LAST_OFFSET_HEADER = `electric-offset`
|
|
4
4
|
export const SHAPE_SCHEMA_HEADER = `electric-schema`
|
|
5
5
|
export const CHUNK_UP_TO_DATE_HEADER = `electric-up-to-date`
|
|
6
|
-
export const DATABASE_ID_QUERY_PARAM = `database_id`
|
|
7
6
|
export const COLUMNS_QUERY_PARAM = `columns`
|
|
8
7
|
export const LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`
|
|
9
8
|
export const SHAPE_HANDLE_QUERY_PARAM = `handle`
|
package/src/fetch.ts
CHANGED
|
@@ -177,13 +177,13 @@ export function createFetchWithResponseHeadersCheck(
|
|
|
177
177
|
const input = args[0]
|
|
178
178
|
const urlString = input.toString()
|
|
179
179
|
const url = new URL(urlString)
|
|
180
|
-
if (url.searchParams.
|
|
180
|
+
if (url.searchParams.get(LIVE_QUERY_PARAM) === `true`) {
|
|
181
181
|
addMissingHeaders(requiredLiveResponseHeaders)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
if (
|
|
185
185
|
!url.searchParams.has(LIVE_QUERY_PARAM) ||
|
|
186
|
-
url.searchParams.
|
|
186
|
+
url.searchParams.get(LIVE_QUERY_PARAM) === `false`
|
|
187
187
|
) {
|
|
188
188
|
addMissingHeaders(requiredNonLiveResponseHeaders)
|
|
189
189
|
}
|
|
@@ -312,6 +312,7 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
312
312
|
|
|
313
313
|
nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle)
|
|
314
314
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)
|
|
315
|
+
nextUrl.searchParams.sort()
|
|
315
316
|
return nextUrl.toString()
|
|
316
317
|
}
|
|
317
318
|
|
package/src/shape.ts
CHANGED
|
@@ -21,7 +21,12 @@ export type ShapeChangedCallback<T extends Row<unknown> = Row> = (data: {
|
|
|
21
21
|
* @param {ShapeStream<T extends Row>} - the underlying shape stream
|
|
22
22
|
* @example
|
|
23
23
|
* ```
|
|
24
|
-
* const shapeStream = new ShapeStream<{ foo: number }>(
|
|
24
|
+
* const shapeStream = new ShapeStream<{ foo: number }>({
|
|
25
|
+
* url: `http://localhost:3000/v1/shape`,
|
|
26
|
+
* params: {
|
|
27
|
+
* table: `foo`
|
|
28
|
+
* }
|
|
29
|
+
* })
|
|
25
30
|
* const shape = new Shape(shapeStream)
|
|
26
31
|
* ```
|
|
27
32
|
*
|
|
@@ -41,7 +46,7 @@ export type ShapeChangedCallback<T extends Row<unknown> = Row> = (data: {
|
|
|
41
46
|
* })
|
|
42
47
|
*/
|
|
43
48
|
export class Shape<T extends Row<unknown> = Row> {
|
|
44
|
-
readonly
|
|
49
|
+
readonly stream: ShapeStreamInterface<T>
|
|
45
50
|
|
|
46
51
|
readonly #data: ShapeData<T> = new Map()
|
|
47
52
|
readonly #subscribers = new Map<number, ShapeChangedCallback<T>>()
|
|
@@ -50,23 +55,23 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
50
55
|
#error: FetchError | false = false
|
|
51
56
|
|
|
52
57
|
constructor(stream: ShapeStreamInterface<T>) {
|
|
53
|
-
this
|
|
54
|
-
this
|
|
58
|
+
this.stream = stream
|
|
59
|
+
this.stream.subscribe(
|
|
55
60
|
this.#process.bind(this),
|
|
56
61
|
this.#handleError.bind(this)
|
|
57
62
|
)
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
get isUpToDate(): boolean {
|
|
61
|
-
return this
|
|
66
|
+
return this.stream.isUpToDate
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
get lastOffset(): Offset {
|
|
65
|
-
return this
|
|
70
|
+
return this.stream.lastOffset
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
get handle(): string | undefined {
|
|
69
|
-
return this
|
|
74
|
+
return this.stream.shapeHandle
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
get rows(): Promise<T[]> {
|
|
@@ -79,7 +84,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
79
84
|
|
|
80
85
|
get value(): Promise<ShapeData<T>> {
|
|
81
86
|
return new Promise((resolve, reject) => {
|
|
82
|
-
if (this
|
|
87
|
+
if (this.stream.isUpToDate) {
|
|
83
88
|
resolve(this.currentValue)
|
|
84
89
|
} else {
|
|
85
90
|
const unsubscribe = this.subscribe(({ value }) => {
|
|
@@ -101,22 +106,22 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
101
106
|
|
|
102
107
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
103
108
|
lastSyncedAt(): number | undefined {
|
|
104
|
-
return this
|
|
109
|
+
return this.stream.lastSyncedAt()
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
/** Time elapsed since last sync (in ms). Infinity if we did not yet sync. */
|
|
108
113
|
lastSynced() {
|
|
109
|
-
return this
|
|
114
|
+
return this.stream.lastSynced()
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
/** True during initial fetch. False afterwise. */
|
|
113
118
|
isLoading() {
|
|
114
|
-
return this
|
|
119
|
+
return this.stream.isLoading()
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/** Indicates if we are connected to the Electric sync service. */
|
|
118
123
|
isConnected(): boolean {
|
|
119
|
-
return this
|
|
124
|
+
return this.stream.isConnected()
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
subscribe(callback: ShapeChangedCallback<T>): () => void {
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,8 @@ interface Header {
|
|
|
23
23
|
[key: Exclude<string, `operation` | `control`>]: Value
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type Operation = `insert` | `update` | `delete`
|
|
27
|
+
|
|
26
28
|
export type ControlMessage = {
|
|
27
29
|
headers: Header & { control: `up-to-date` | `must-refetch` }
|
|
28
30
|
}
|
|
@@ -30,7 +32,7 @@ export type ControlMessage = {
|
|
|
30
32
|
export type ChangeMessage<T extends Row<unknown> = Row> = {
|
|
31
33
|
key: string
|
|
32
34
|
value: T
|
|
33
|
-
headers: Header & { operation:
|
|
35
|
+
headers: Header & { operation: Operation }
|
|
34
36
|
offset: Offset
|
|
35
37
|
}
|
|
36
38
|
|