@electric-sql/client 0.6.5 → 0.7.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/README.md +7 -5
- package/dist/cjs/index.cjs +72 -39
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +46 -16
- package/dist/index.legacy-esm.js +72 -39
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +72 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +81 -26
- package/src/constants.ts +11 -8
- package/src/fetch.ts +7 -7
- package/src/shape.ts +24 -15
package/src/client.ts
CHANGED
|
@@ -22,21 +22,38 @@ import {
|
|
|
22
22
|
COLUMNS_QUERY_PARAM,
|
|
23
23
|
LIVE_QUERY_PARAM,
|
|
24
24
|
OFFSET_QUERY_PARAM,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
SHAPE_HANDLE_HEADER,
|
|
26
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
27
27
|
SHAPE_SCHEMA_HEADER,
|
|
28
28
|
WHERE_QUERY_PARAM,
|
|
29
|
+
DATABASE_ID_QUERY_PARAM,
|
|
30
|
+
TABLE_QUERY_PARAM,
|
|
31
|
+
REPLICA_PARAM,
|
|
29
32
|
} from './constants'
|
|
30
33
|
|
|
34
|
+
type Replica = `full` | `default`
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Options for constructing a ShapeStream.
|
|
33
38
|
*/
|
|
34
39
|
export interface ShapeStreamOptions<T = never> {
|
|
35
40
|
/**
|
|
36
|
-
* The full URL to where the Shape is
|
|
37
|
-
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape
|
|
41
|
+
* The full URL to where the Shape is served. This can either be the Electric server
|
|
42
|
+
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape`
|
|
38
43
|
*/
|
|
39
44
|
url: string
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which database to use.
|
|
48
|
+
* This is optional unless Electric is used with multiple databases.
|
|
49
|
+
*/
|
|
50
|
+
databaseId?: string
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The root table for the shape.
|
|
54
|
+
*/
|
|
55
|
+
table: string
|
|
56
|
+
|
|
40
57
|
/**
|
|
41
58
|
* The where clauses for the shape.
|
|
42
59
|
*/
|
|
@@ -48,12 +65,23 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
48
65
|
*/
|
|
49
66
|
columns?: string[]
|
|
50
67
|
|
|
68
|
+
/**
|
|
69
|
+
* If `replica` is `default` (the default) then Electric will only send the
|
|
70
|
+
* changed columns in an update.
|
|
71
|
+
*
|
|
72
|
+
* If it's `full` Electric will send the entire row with both changed and
|
|
73
|
+
* unchanged values.
|
|
74
|
+
*
|
|
75
|
+
* Setting `replica` to `full` will obviously result in higher bandwidth
|
|
76
|
+
* usage and so is not recommended.
|
|
77
|
+
*/
|
|
78
|
+
replica?: Replica
|
|
51
79
|
/**
|
|
52
80
|
* The "offset" on the shape log. This is typically not set as the ShapeStream
|
|
53
81
|
* will handle this automatically. A common scenario where you might pass an offset
|
|
54
82
|
* is if you're maintaining a local cache of the log. If you've gone offline
|
|
55
83
|
* and are re-starting a ShapeStream to catch-up to the latest state of the Shape,
|
|
56
|
-
* you'd pass in the last offset and
|
|
84
|
+
* you'd pass in the last offset and shapeHandle you'd seen from the Electric server
|
|
57
85
|
* so it knows at what point in the shape to catch you up from.
|
|
58
86
|
*/
|
|
59
87
|
offset?: Offset
|
|
@@ -61,7 +89,7 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
61
89
|
* Similar to `offset`, this isn't typically used unless you're maintaining
|
|
62
90
|
* a cache of the shape log.
|
|
63
91
|
*/
|
|
64
|
-
|
|
92
|
+
shapeHandle?: string
|
|
65
93
|
backoffOptions?: BackoffOptions
|
|
66
94
|
|
|
67
95
|
/**
|
|
@@ -98,7 +126,7 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
98
126
|
isConnected(): boolean
|
|
99
127
|
|
|
100
128
|
isUpToDate: boolean
|
|
101
|
-
|
|
129
|
+
shapeHandle?: string
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
/**
|
|
@@ -135,6 +163,11 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
135
163
|
export class ShapeStream<T extends Row<unknown> = Row>
|
|
136
164
|
implements ShapeStreamInterface<T>
|
|
137
165
|
{
|
|
166
|
+
static readonly Replica = {
|
|
167
|
+
FULL: `full` as Replica,
|
|
168
|
+
DEFAULT: `default` as Replica,
|
|
169
|
+
}
|
|
170
|
+
|
|
138
171
|
readonly options: ShapeStreamOptions<GetExtensions<T>>
|
|
139
172
|
|
|
140
173
|
readonly #fetchClient: typeof fetch
|
|
@@ -157,17 +190,21 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
157
190
|
#lastSyncedAt?: number // unix time
|
|
158
191
|
#isUpToDate: boolean = false
|
|
159
192
|
#connected: boolean = false
|
|
160
|
-
#
|
|
193
|
+
#shapeHandle?: string
|
|
194
|
+
#databaseId?: string
|
|
161
195
|
#schema?: Schema
|
|
162
196
|
#error?: unknown
|
|
197
|
+
#replica?: Replica
|
|
163
198
|
|
|
164
199
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
165
200
|
validateOptions(options)
|
|
166
201
|
this.options = { subscribe: true, ...options }
|
|
167
202
|
this.#lastOffset = this.options.offset ?? `-1`
|
|
168
203
|
this.#liveCacheBuster = ``
|
|
169
|
-
this.#
|
|
204
|
+
this.#shapeHandle = this.options.shapeHandle
|
|
205
|
+
this.#databaseId = this.options.databaseId
|
|
170
206
|
this.#messageParser = new MessageParser<T>(options.parser)
|
|
207
|
+
this.#replica = this.options.replica
|
|
171
208
|
|
|
172
209
|
const baseFetchClient =
|
|
173
210
|
options.fetchClient ??
|
|
@@ -186,8 +223,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
186
223
|
this.start()
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
get
|
|
190
|
-
return this.#
|
|
226
|
+
get shapeHandle() {
|
|
227
|
+
return this.#shapeHandle
|
|
191
228
|
}
|
|
192
229
|
|
|
193
230
|
get isUpToDate() {
|
|
@@ -201,7 +238,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
201
238
|
async start() {
|
|
202
239
|
this.#isUpToDate = false
|
|
203
240
|
|
|
204
|
-
const { url, where, columns, signal } = this.options
|
|
241
|
+
const { url, table, where, columns, signal } = this.options
|
|
205
242
|
|
|
206
243
|
try {
|
|
207
244
|
while (
|
|
@@ -209,6 +246,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
209
246
|
this.options.subscribe
|
|
210
247
|
) {
|
|
211
248
|
const fetchUrl = new URL(url)
|
|
249
|
+
fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table)
|
|
212
250
|
if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)
|
|
213
251
|
if (columns && columns.length > 0)
|
|
214
252
|
fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`))
|
|
@@ -222,9 +260,23 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
222
260
|
)
|
|
223
261
|
}
|
|
224
262
|
|
|
225
|
-
if (this.#
|
|
263
|
+
if (this.#shapeHandle) {
|
|
226
264
|
// This should probably be a header for better cache breaking?
|
|
227
|
-
fetchUrl.searchParams.set(
|
|
265
|
+
fetchUrl.searchParams.set(
|
|
266
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
267
|
+
this.#shapeHandle!
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (this.#databaseId) {
|
|
272
|
+
fetchUrl.searchParams.set(DATABASE_ID_QUERY_PARAM, this.#databaseId!)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
(this.#replica ?? ShapeStream.Replica.DEFAULT) !=
|
|
277
|
+
ShapeStream.Replica.DEFAULT
|
|
278
|
+
) {
|
|
279
|
+
fetchUrl.searchParams.set(REPLICA_PARAM, this.#replica as string)
|
|
228
280
|
}
|
|
229
281
|
|
|
230
282
|
let response!: Response
|
|
@@ -239,9 +291,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
239
291
|
if (!(e instanceof FetchError)) throw e // should never happen
|
|
240
292
|
if (e.status == 409) {
|
|
241
293
|
// Upon receiving a 409, we should start from scratch
|
|
242
|
-
// with the newly provided shape
|
|
243
|
-
const
|
|
244
|
-
this.#reset(
|
|
294
|
+
// with the newly provided shape handle
|
|
295
|
+
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER]
|
|
296
|
+
this.#reset(newShapeHandle)
|
|
245
297
|
await this.#publish(e.json as Message<T>[])
|
|
246
298
|
continue
|
|
247
299
|
} else if (e.status >= 400 && e.status < 500) {
|
|
@@ -256,9 +308,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
256
308
|
}
|
|
257
309
|
|
|
258
310
|
const { headers, status } = response
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
this.#
|
|
311
|
+
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER)
|
|
312
|
+
if (shapeHandle) {
|
|
313
|
+
this.#shapeHandle = shapeHandle
|
|
262
314
|
}
|
|
263
315
|
|
|
264
316
|
const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER)
|
|
@@ -397,12 +449,12 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
397
449
|
|
|
398
450
|
/**
|
|
399
451
|
* Resets the state of the stream, optionally with a provided
|
|
400
|
-
* shape
|
|
452
|
+
* shape handle
|
|
401
453
|
*/
|
|
402
|
-
#reset(
|
|
454
|
+
#reset(shapeHandle?: string) {
|
|
403
455
|
this.#lastOffset = `-1`
|
|
404
456
|
this.#liveCacheBuster = ``
|
|
405
|
-
this.#
|
|
457
|
+
this.#shapeHandle = shapeHandle
|
|
406
458
|
this.#isUpToDate = false
|
|
407
459
|
this.#connected = false
|
|
408
460
|
this.#schema = undefined
|
|
@@ -411,7 +463,10 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
411
463
|
|
|
412
464
|
function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
413
465
|
if (!options.url) {
|
|
414
|
-
throw new Error(`Invalid shape
|
|
466
|
+
throw new Error(`Invalid shape options. It must provide the url`)
|
|
467
|
+
}
|
|
468
|
+
if (!options.table) {
|
|
469
|
+
throw new Error(`Invalid shape options. It must provide the table`)
|
|
415
470
|
}
|
|
416
471
|
if (options.signal && !(options.signal instanceof AbortSignal)) {
|
|
417
472
|
throw new Error(
|
|
@@ -422,10 +477,10 @@ function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
|
422
477
|
if (
|
|
423
478
|
options.offset !== undefined &&
|
|
424
479
|
options.offset !== `-1` &&
|
|
425
|
-
!options.
|
|
480
|
+
!options.shapeHandle
|
|
426
481
|
) {
|
|
427
482
|
throw new Error(
|
|
428
|
-
`
|
|
483
|
+
`shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
|
|
429
484
|
)
|
|
430
485
|
}
|
|
431
486
|
return
|
package/src/constants.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
export const
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
export const CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`
|
|
5
|
-
export const CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`
|
|
1
|
+
export const LIVE_CACHE_BUSTER_HEADER = `electric-cursor`
|
|
2
|
+
export const SHAPE_HANDLE_HEADER = `electric-handle`
|
|
3
|
+
export const CHUNK_LAST_OFFSET_HEADER = `electric-offset`
|
|
6
4
|
export const SHAPE_SCHEMA_HEADER = `electric-schema`
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const WHERE_QUERY_PARAM = `where`
|
|
5
|
+
export const CHUNK_UP_TO_DATE_HEADER = `electric-up-to-date`
|
|
6
|
+
export const DATABASE_ID_QUERY_PARAM = `database_id`
|
|
10
7
|
export const COLUMNS_QUERY_PARAM = `columns`
|
|
8
|
+
export const LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`
|
|
9
|
+
export const SHAPE_HANDLE_QUERY_PARAM = `handle`
|
|
11
10
|
export const LIVE_QUERY_PARAM = `live`
|
|
11
|
+
export const OFFSET_QUERY_PARAM = `offset`
|
|
12
|
+
export const TABLE_QUERY_PARAM = `table`
|
|
13
|
+
export const WHERE_QUERY_PARAM = `where`
|
|
14
|
+
export const REPLICA_PARAM = `replica`
|
package/src/fetch.ts
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
CHUNK_UP_TO_DATE_HEADER,
|
|
4
4
|
LIVE_QUERY_PARAM,
|
|
5
5
|
OFFSET_QUERY_PARAM,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
SHAPE_HANDLE_HEADER,
|
|
7
|
+
SHAPE_HANDLE_QUERY_PARAM,
|
|
8
8
|
} from './constants'
|
|
9
9
|
import { FetchError, FetchBackoffAbortError } from './error'
|
|
10
10
|
|
|
@@ -245,13 +245,13 @@ class PrefetchQueue {
|
|
|
245
245
|
* Generate the next chunk's URL if the url and response are valid
|
|
246
246
|
*/
|
|
247
247
|
function getNextChunkUrl(url: string, res: Response): string | void {
|
|
248
|
-
const
|
|
248
|
+
const shapeHandle = res.headers.get(SHAPE_HANDLE_HEADER)
|
|
249
249
|
const lastOffset = res.headers.get(CHUNK_LAST_OFFSET_HEADER)
|
|
250
250
|
const isUpToDate = res.headers.has(CHUNK_UP_TO_DATE_HEADER)
|
|
251
251
|
|
|
252
|
-
// only prefetch if shape
|
|
252
|
+
// only prefetch if shape handle and offset for next chunk are available, and
|
|
253
253
|
// response is not already up-to-date
|
|
254
|
-
if (!
|
|
254
|
+
if (!shapeHandle || !lastOffset || isUpToDate) return
|
|
255
255
|
|
|
256
256
|
const nextUrl = new URL(url)
|
|
257
257
|
|
|
@@ -259,7 +259,7 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
259
259
|
// potentially miss more recent data
|
|
260
260
|
if (nextUrl.searchParams.has(LIVE_QUERY_PARAM)) return
|
|
261
261
|
|
|
262
|
-
nextUrl.searchParams.set(
|
|
262
|
+
nextUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, shapeHandle)
|
|
263
263
|
nextUrl.searchParams.set(OFFSET_QUERY_PARAM, lastOffset)
|
|
264
264
|
return nextUrl.toString()
|
|
265
265
|
}
|
|
@@ -271,7 +271,7 @@ function getNextChunkUrl(url: string, res: Response): string | void {
|
|
|
271
271
|
*/
|
|
272
272
|
function chainAborter(
|
|
273
273
|
aborter: AbortController,
|
|
274
|
-
sourceSignal?: AbortSignal
|
|
274
|
+
sourceSignal?: AbortSignal | null
|
|
275
275
|
): AbortSignal {
|
|
276
276
|
if (!sourceSignal) return aborter.signal
|
|
277
277
|
if (sourceSignal.aborted) aborter.abort()
|
package/src/shape.ts
CHANGED
|
@@ -4,13 +4,14 @@ import { FetchError } from './error'
|
|
|
4
4
|
import { ShapeStreamInterface } from './client'
|
|
5
5
|
|
|
6
6
|
export type ShapeData<T extends Row<unknown> = Row> = Map<string, T>
|
|
7
|
-
export type ShapeChangedCallback<T extends Row<unknown> = Row> = (
|
|
7
|
+
export type ShapeChangedCallback<T extends Row<unknown> = Row> = (data: {
|
|
8
8
|
value: ShapeData<T>
|
|
9
|
-
|
|
9
|
+
rows: T[]
|
|
10
|
+
}) => void
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* A Shape is an object that subscribes to a shape log,
|
|
13
|
-
* keeps a materialised shape `.
|
|
14
|
+
* keeps a materialised shape `.rows` in memory and
|
|
14
15
|
* notifies subscribers when the value has changed.
|
|
15
16
|
*
|
|
16
17
|
* It can be used without a framework and as a primitive
|
|
@@ -20,23 +21,23 @@ export type ShapeChangedCallback<T extends Row<unknown> = Row> = (
|
|
|
20
21
|
* @param {ShapeStream<T extends Row>} - the underlying shape stream
|
|
21
22
|
* @example
|
|
22
23
|
* ```
|
|
23
|
-
* const shapeStream = new ShapeStream<{ foo: number }>(url:
|
|
24
|
+
* const shapeStream = new ShapeStream<{ foo: number }>(url: `http://localhost:3000/v1/shape`, table: `foo`})
|
|
24
25
|
* const shape = new Shape(shapeStream)
|
|
25
26
|
* ```
|
|
26
27
|
*
|
|
27
|
-
* `
|
|
28
|
+
* `rows` returns a promise that resolves the Shape data once the Shape has been
|
|
28
29
|
* fully loaded (and when resuming from being offline):
|
|
29
30
|
*
|
|
30
|
-
* const
|
|
31
|
+
* const rows = await shape.rows
|
|
31
32
|
*
|
|
32
|
-
* `
|
|
33
|
+
* `currentRows` returns the current data synchronously:
|
|
33
34
|
*
|
|
34
|
-
* const
|
|
35
|
+
* const rows = shape.currentRows
|
|
35
36
|
*
|
|
36
37
|
* Subscribe to updates. Called whenever the shape updates in Postgres.
|
|
37
38
|
*
|
|
38
|
-
* shape.subscribe(
|
|
39
|
-
* console.log(
|
|
39
|
+
* shape.subscribe(({ rows }) => {
|
|
40
|
+
* console.log(rows)
|
|
40
41
|
* })
|
|
41
42
|
*/
|
|
42
43
|
export class Shape<T extends Row<unknown> = Row> {
|
|
@@ -69,21 +70,29 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
69
70
|
return this.#stream.isUpToDate
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
get rows(): Promise<T[]> {
|
|
74
|
+
return this.value.then((v) => Array.from(v.values()))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get currentRows(): T[] {
|
|
78
|
+
return Array.from(this.currentValue.values())
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
get value(): Promise<ShapeData<T>> {
|
|
73
82
|
return new Promise((resolve, reject) => {
|
|
74
83
|
if (this.#stream.isUpToDate) {
|
|
75
|
-
resolve(this.
|
|
84
|
+
resolve(this.currentValue)
|
|
76
85
|
} else {
|
|
77
|
-
const unsubscribe = this.subscribe((
|
|
86
|
+
const unsubscribe = this.subscribe(({ value }) => {
|
|
78
87
|
unsubscribe()
|
|
79
88
|
if (this.#error) reject(this.#error)
|
|
80
|
-
resolve(
|
|
89
|
+
resolve(value)
|
|
81
90
|
})
|
|
82
91
|
}
|
|
83
92
|
})
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
get
|
|
95
|
+
get currentValue() {
|
|
87
96
|
return this.#data
|
|
88
97
|
}
|
|
89
98
|
|
|
@@ -192,7 +201,7 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
192
201
|
|
|
193
202
|
#notify(): void {
|
|
194
203
|
this.#subscribers.forEach((callback) => {
|
|
195
|
-
callback(this.
|
|
204
|
+
callback({ value: this.currentValue, rows: this.currentRows })
|
|
196
205
|
})
|
|
197
206
|
}
|
|
198
207
|
}
|