@electric-sql/client 0.7.3 → 0.9.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 +85 -4
- package/dist/cjs/index.cjs +246 -231
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +5 -4
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +56 -37
- package/dist/index.legacy-esm.js +242 -227
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +246 -231
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +153 -139
- package/src/constants.ts +0 -1
- package/src/error.ts +55 -0
- package/src/fetch.ts +2 -1
- package/src/parser.ts +2 -1
- package/src/shape.ts +10 -10
package/src/client.ts
CHANGED
|
@@ -11,7 +11,10 @@ import { isUpToDateMessage } from './helpers'
|
|
|
11
11
|
import {
|
|
12
12
|
FetchError,
|
|
13
13
|
FetchBackoffAbortError,
|
|
14
|
-
|
|
14
|
+
MissingShapeUrlError,
|
|
15
|
+
InvalidSignalError,
|
|
16
|
+
MissingShapeHandleError,
|
|
17
|
+
ReservedParamError,
|
|
15
18
|
} from './error'
|
|
16
19
|
import {
|
|
17
20
|
BackoffDefaults,
|
|
@@ -31,27 +34,49 @@ import {
|
|
|
31
34
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
32
35
|
SHAPE_SCHEMA_HEADER,
|
|
33
36
|
WHERE_QUERY_PARAM,
|
|
34
|
-
DATABASE_ID_QUERY_PARAM,
|
|
35
37
|
TABLE_QUERY_PARAM,
|
|
36
38
|
REPLICA_PARAM,
|
|
37
39
|
} from './constants'
|
|
38
40
|
|
|
39
41
|
const RESERVED_PARAMS = new Set([
|
|
40
|
-
DATABASE_ID_QUERY_PARAM,
|
|
41
|
-
COLUMNS_QUERY_PARAM,
|
|
42
42
|
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
43
43
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
44
44
|
LIVE_QUERY_PARAM,
|
|
45
45
|
OFFSET_QUERY_PARAM,
|
|
46
|
-
TABLE_QUERY_PARAM,
|
|
47
|
-
WHERE_QUERY_PARAM,
|
|
48
|
-
REPLICA_PARAM,
|
|
49
46
|
])
|
|
50
47
|
|
|
51
48
|
type Replica = `full` | `default`
|
|
52
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
|
+
|
|
53
79
|
type ReservedParamKeys =
|
|
54
|
-
| typeof DATABASE_ID_QUERY_PARAM
|
|
55
80
|
| typeof COLUMNS_QUERY_PARAM
|
|
56
81
|
| typeof LIVE_CACHE_BUSTER_QUERY_PARAM
|
|
57
82
|
| typeof SHAPE_HANDLE_QUERY_PARAM
|
|
@@ -61,7 +86,41 @@ type ReservedParamKeys =
|
|
|
61
86
|
| typeof WHERE_QUERY_PARAM
|
|
62
87
|
| typeof REPLICA_PARAM
|
|
63
88
|
|
|
64
|
-
|
|
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
|
+
}
|
|
115
|
+
|
|
116
|
+
type RetryOpts = {
|
|
117
|
+
params?: ExternalParamsRecord
|
|
118
|
+
headers?: Record<string, string>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type ShapeStreamErrorHandler = (
|
|
122
|
+
error: Error
|
|
123
|
+
) => void | RetryOpts | Promise<void | RetryOpts>
|
|
65
124
|
|
|
66
125
|
/**
|
|
67
126
|
* Options for constructing a ShapeStream.
|
|
@@ -73,39 +132,6 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
73
132
|
*/
|
|
74
133
|
url: string
|
|
75
134
|
|
|
76
|
-
/**
|
|
77
|
-
* Which database to use.
|
|
78
|
-
* This is optional unless Electric is used with multiple databases.
|
|
79
|
-
*/
|
|
80
|
-
databaseId?: string
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The root table for the shape. Passed as a query parameter. Not required if you set the table in your proxy.
|
|
84
|
-
*/
|
|
85
|
-
table?: string
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The where clauses for the shape.
|
|
89
|
-
*/
|
|
90
|
-
where?: string
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The columns to include in the shape.
|
|
94
|
-
* Must include primary keys, and can only inlude valid columns.
|
|
95
|
-
*/
|
|
96
|
-
columns?: string[]
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* If `replica` is `default` (the default) then Electric will only send the
|
|
100
|
-
* changed columns in an update.
|
|
101
|
-
*
|
|
102
|
-
* If it's `full` Electric will send the entire row with both changed and
|
|
103
|
-
* unchanged values.
|
|
104
|
-
*
|
|
105
|
-
* Setting `replica` to `full` will obviously result in higher bandwidth
|
|
106
|
-
* usage and so is not recommended.
|
|
107
|
-
*/
|
|
108
|
-
replica?: Replica
|
|
109
135
|
/**
|
|
110
136
|
* The "offset" on the shape log. This is typically not set as the ShapeStream
|
|
111
137
|
* will handle this automatically. A common scenario where you might pass an offset
|
|
@@ -115,12 +141,12 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
115
141
|
* so it knows at what point in the shape to catch you up from.
|
|
116
142
|
*/
|
|
117
143
|
offset?: Offset
|
|
144
|
+
|
|
118
145
|
/**
|
|
119
146
|
* Similar to `offset`, this isn't typically used unless you're maintaining
|
|
120
147
|
* a cache of the shape log.
|
|
121
148
|
*/
|
|
122
|
-
|
|
123
|
-
backoffOptions?: BackoffOptions
|
|
149
|
+
handle?: string
|
|
124
150
|
|
|
125
151
|
/**
|
|
126
152
|
* HTTP headers to attach to requests made by the client.
|
|
@@ -132,18 +158,32 @@ export interface ShapeStreamOptions<T = never> {
|
|
|
132
158
|
* Additional request parameters to attach to the URL.
|
|
133
159
|
* These will be merged with Electric's standard parameters.
|
|
134
160
|
* Note: You cannot use Electric's reserved parameter names
|
|
135
|
-
* (
|
|
161
|
+
* (offset, handle, live, cursor).
|
|
162
|
+
*
|
|
163
|
+
* PostgreSQL-specific options like table, where, columns, and replica
|
|
164
|
+
* should be specified here.
|
|
136
165
|
*/
|
|
137
|
-
params?:
|
|
166
|
+
params?: ExternalParamsRecord
|
|
138
167
|
|
|
139
168
|
/**
|
|
140
169
|
* Automatically fetch updates to the Shape. If you just want to sync the current
|
|
141
170
|
* shape and stop, pass false.
|
|
142
171
|
*/
|
|
143
172
|
subscribe?: boolean
|
|
173
|
+
|
|
144
174
|
signal?: AbortSignal
|
|
145
175
|
fetchClient?: typeof fetch
|
|
176
|
+
backoffOptions?: BackoffOptions
|
|
146
177
|
parser?: Parser<T>
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A function for handling shapestream errors.
|
|
181
|
+
* This is optional, when it is not provided any shapestream errors will be thrown.
|
|
182
|
+
* If the function returns an object containing parameters and/or headers
|
|
183
|
+
* the shapestream will apply those changes and try syncing again.
|
|
184
|
+
* If the function returns void the shapestream is stopped.
|
|
185
|
+
*/
|
|
186
|
+
onError?: ShapeStreamErrorHandler
|
|
147
187
|
}
|
|
148
188
|
|
|
149
189
|
export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
@@ -151,12 +191,7 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
151
191
|
callback: (messages: Message<T>[]) => MaybePromise<void>,
|
|
152
192
|
onError?: (error: FetchError | Error) => void
|
|
153
193
|
): void
|
|
154
|
-
unsubscribeAllUpToDateSubscribers(): void
|
|
155
194
|
unsubscribeAll(): void
|
|
156
|
-
subscribeOnceToUpToDate(
|
|
157
|
-
callback: () => MaybePromise<void>,
|
|
158
|
-
error: (err: FetchError | Error) => void
|
|
159
|
-
): () => void
|
|
160
195
|
|
|
161
196
|
isLoading(): boolean
|
|
162
197
|
lastSyncedAt(): number | undefined
|
|
@@ -166,6 +201,7 @@ export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
|
166
201
|
isUpToDate: boolean
|
|
167
202
|
lastOffset: Offset
|
|
168
203
|
shapeHandle?: string
|
|
204
|
+
error?: unknown
|
|
169
205
|
}
|
|
170
206
|
|
|
171
207
|
/**
|
|
@@ -208,6 +244,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
208
244
|
}
|
|
209
245
|
|
|
210
246
|
readonly options: ShapeStreamOptions<GetExtensions<T>>
|
|
247
|
+
#error: unknown = null
|
|
211
248
|
|
|
212
249
|
readonly #fetchClient: typeof fetch
|
|
213
250
|
readonly #messageParser: MessageParser<T>
|
|
@@ -219,10 +256,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
219
256
|
((error: Error) => void) | undefined,
|
|
220
257
|
]
|
|
221
258
|
>()
|
|
222
|
-
readonly #upToDateSubscribers = new Map<
|
|
223
|
-
number,
|
|
224
|
-
[() => void, (error: FetchError | Error) => void]
|
|
225
|
-
>()
|
|
226
259
|
|
|
227
260
|
#lastOffset: Offset
|
|
228
261
|
#liveCacheBuster: string // Seconds since our Electric Epoch 😎
|
|
@@ -230,20 +263,17 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
230
263
|
#isUpToDate: boolean = false
|
|
231
264
|
#connected: boolean = false
|
|
232
265
|
#shapeHandle?: string
|
|
233
|
-
#databaseId?: string
|
|
234
266
|
#schema?: Schema
|
|
235
|
-
#
|
|
236
|
-
#replica?: Replica
|
|
267
|
+
#onError?: ShapeStreamErrorHandler
|
|
237
268
|
|
|
238
269
|
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
239
|
-
validateOptions(options)
|
|
240
270
|
this.options = { subscribe: true, ...options }
|
|
271
|
+
validateOptions(this.options)
|
|
241
272
|
this.#lastOffset = this.options.offset ?? `-1`
|
|
242
273
|
this.#liveCacheBuster = ``
|
|
243
|
-
this.#shapeHandle = this.options.
|
|
244
|
-
this.#databaseId = this.options.databaseId
|
|
274
|
+
this.#shapeHandle = this.options.handle
|
|
245
275
|
this.#messageParser = new MessageParser<T>(options.parser)
|
|
246
|
-
this.#
|
|
276
|
+
this.#onError = this.options.onError
|
|
247
277
|
|
|
248
278
|
const baseFetchClient =
|
|
249
279
|
options.fetchClient ??
|
|
@@ -261,13 +291,17 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
261
291
|
createFetchWithChunkBuffer(fetchWithBackoffClient)
|
|
262
292
|
)
|
|
263
293
|
|
|
264
|
-
this
|
|
294
|
+
this.#start()
|
|
265
295
|
}
|
|
266
296
|
|
|
267
297
|
get shapeHandle() {
|
|
268
298
|
return this.#shapeHandle
|
|
269
299
|
}
|
|
270
300
|
|
|
301
|
+
get error() {
|
|
302
|
+
return this.#error
|
|
303
|
+
}
|
|
304
|
+
|
|
271
305
|
get isUpToDate() {
|
|
272
306
|
return this.#isUpToDate
|
|
273
307
|
}
|
|
@@ -276,20 +310,14 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
276
310
|
return this.#lastOffset
|
|
277
311
|
}
|
|
278
312
|
|
|
279
|
-
|
|
280
|
-
return this.#error
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async start() {
|
|
284
|
-
this.#isUpToDate = false
|
|
285
|
-
|
|
286
|
-
const { url, table, where, columns, signal } = this.options
|
|
287
|
-
|
|
313
|
+
async #start() {
|
|
288
314
|
try {
|
|
289
315
|
while (
|
|
290
|
-
(!signal?.aborted && !this.#isUpToDate) ||
|
|
316
|
+
(!this.options.signal?.aborted && !this.#isUpToDate) ||
|
|
291
317
|
this.options.subscribe
|
|
292
318
|
) {
|
|
319
|
+
const { url, signal } = this.options
|
|
320
|
+
|
|
293
321
|
const fetchUrl = new URL(url)
|
|
294
322
|
|
|
295
323
|
// Add any custom parameters first
|
|
@@ -304,16 +332,30 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
304
332
|
)
|
|
305
333
|
}
|
|
306
334
|
|
|
307
|
-
|
|
308
|
-
|
|
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)
|
|
309
355
|
}
|
|
310
356
|
}
|
|
311
357
|
|
|
312
358
|
// Add Electric's internal parameters
|
|
313
|
-
if (table) fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table)
|
|
314
|
-
if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where)
|
|
315
|
-
if (columns && columns.length > 0)
|
|
316
|
-
fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`))
|
|
317
359
|
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, this.#lastOffset)
|
|
318
360
|
|
|
319
361
|
if (this.#isUpToDate) {
|
|
@@ -332,16 +374,8 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
332
374
|
)
|
|
333
375
|
}
|
|
334
376
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
(this.#replica ?? ShapeStream.Replica.DEFAULT) !=
|
|
341
|
-
ShapeStream.Replica.DEFAULT
|
|
342
|
-
) {
|
|
343
|
-
fetchUrl.searchParams.set(REPLICA_PARAM, this.#replica as string)
|
|
344
|
-
}
|
|
377
|
+
// sort query params in-place for stable URLs and improved cache hits
|
|
378
|
+
fetchUrl.searchParams.sort()
|
|
345
379
|
|
|
346
380
|
let response!: Response
|
|
347
381
|
try {
|
|
@@ -352,7 +386,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
352
386
|
this.#connected = true
|
|
353
387
|
} catch (e) {
|
|
354
388
|
if (e instanceof FetchBackoffAbortError) break // interrupted
|
|
355
|
-
if (e instanceof MissingHeadersError) throw e
|
|
356
389
|
if (!(e instanceof FetchError)) throw e // should never happen
|
|
357
390
|
if (e.status == 409) {
|
|
358
391
|
// Upon receiving a 409, we should start from scratch
|
|
@@ -363,7 +396,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
363
396
|
continue
|
|
364
397
|
} else if (e.status >= 400 && e.status < 500) {
|
|
365
398
|
// Notify subscribers
|
|
366
|
-
this.#sendErrorToUpToDateSubscribers(e)
|
|
367
399
|
this.#sendErrorToSubscribers(e)
|
|
368
400
|
|
|
369
401
|
// 400 errors are not actionable without additional user input,
|
|
@@ -405,7 +437,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
405
437
|
|
|
406
438
|
// Update isUpToDate
|
|
407
439
|
if (batch.length > 0) {
|
|
408
|
-
const prevUpToDate = this.#isUpToDate
|
|
409
440
|
const lastMessage = batch[batch.length - 1]
|
|
410
441
|
if (isUpToDateMessage(lastMessage)) {
|
|
411
442
|
this.#lastSyncedAt = Date.now()
|
|
@@ -413,13 +444,31 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
413
444
|
}
|
|
414
445
|
|
|
415
446
|
await this.#publish(batch)
|
|
416
|
-
if (!prevUpToDate && this.#isUpToDate) {
|
|
417
|
-
this.#notifyUpToDateSubscribers()
|
|
418
|
-
}
|
|
419
447
|
}
|
|
420
448
|
}
|
|
421
449
|
} catch (err) {
|
|
422
450
|
this.#error = err
|
|
451
|
+
if (this.#onError) {
|
|
452
|
+
const retryOpts = await this.#onError(err as Error)
|
|
453
|
+
if (typeof retryOpts === `object`) {
|
|
454
|
+
this.#reset()
|
|
455
|
+
|
|
456
|
+
if (`params` in retryOpts) {
|
|
457
|
+
this.options.params = retryOpts.params
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (`headers` in retryOpts) {
|
|
461
|
+
this.options.headers = retryOpts.headers
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Restart
|
|
465
|
+
this.#start()
|
|
466
|
+
}
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// If no handler is provided for errors just throw so the error still bubbles up.
|
|
471
|
+
throw err
|
|
423
472
|
} finally {
|
|
424
473
|
this.#connected = false
|
|
425
474
|
}
|
|
@@ -427,7 +476,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
427
476
|
|
|
428
477
|
subscribe(
|
|
429
478
|
callback: (messages: Message<T>[]) => MaybePromise<void>,
|
|
430
|
-
onError
|
|
479
|
+
onError: (error: Error) => void = () => {}
|
|
431
480
|
) {
|
|
432
481
|
const subscriptionId = Math.random()
|
|
433
482
|
|
|
@@ -442,23 +491,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
442
491
|
this.#subscribers.clear()
|
|
443
492
|
}
|
|
444
493
|
|
|
445
|
-
subscribeOnceToUpToDate(
|
|
446
|
-
callback: () => MaybePromise<void>,
|
|
447
|
-
error: (err: FetchError | Error) => void
|
|
448
|
-
) {
|
|
449
|
-
const subscriptionId = Math.random()
|
|
450
|
-
|
|
451
|
-
this.#upToDateSubscribers.set(subscriptionId, [callback, error])
|
|
452
|
-
|
|
453
|
-
return () => {
|
|
454
|
-
this.#upToDateSubscribers.delete(subscriptionId)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
unsubscribeAllUpToDateSubscribers(): void {
|
|
459
|
-
this.#upToDateSubscribers.clear()
|
|
460
|
-
}
|
|
461
|
-
|
|
462
494
|
/** Unix time at which we last synced. Undefined when `isLoading` is true. */
|
|
463
495
|
lastSyncedAt(): number | undefined {
|
|
464
496
|
return this.#lastSyncedAt
|
|
@@ -477,7 +509,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
477
509
|
|
|
478
510
|
/** True during initial fetch. False afterwise. */
|
|
479
511
|
isLoading(): boolean {
|
|
480
|
-
return !this
|
|
512
|
+
return !this.#isUpToDate
|
|
481
513
|
}
|
|
482
514
|
|
|
483
515
|
async #publish(messages: Message<T>[]): Promise<void> {
|
|
@@ -500,26 +532,14 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
500
532
|
})
|
|
501
533
|
}
|
|
502
534
|
|
|
503
|
-
#notifyUpToDateSubscribers() {
|
|
504
|
-
this.#upToDateSubscribers.forEach(([callback]) => {
|
|
505
|
-
callback()
|
|
506
|
-
})
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
#sendErrorToUpToDateSubscribers(error: FetchError | Error) {
|
|
510
|
-
this.#upToDateSubscribers.forEach(([_, errorCallback]) =>
|
|
511
|
-
errorCallback(error)
|
|
512
|
-
)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
535
|
/**
|
|
516
536
|
* Resets the state of the stream, optionally with a provided
|
|
517
537
|
* shape handle
|
|
518
538
|
*/
|
|
519
|
-
#reset(
|
|
539
|
+
#reset(handle?: string) {
|
|
520
540
|
this.#lastOffset = `-1`
|
|
521
541
|
this.#liveCacheBuster = ``
|
|
522
|
-
this.#shapeHandle =
|
|
542
|
+
this.#shapeHandle = handle
|
|
523
543
|
this.#isUpToDate = false
|
|
524
544
|
this.#connected = false
|
|
525
545
|
this.#schema = undefined
|
|
@@ -528,22 +548,18 @@ export class ShapeStream<T extends Row<unknown> = Row>
|
|
|
528
548
|
|
|
529
549
|
function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
530
550
|
if (!options.url) {
|
|
531
|
-
throw new
|
|
551
|
+
throw new MissingShapeUrlError()
|
|
532
552
|
}
|
|
533
553
|
if (options.signal && !(options.signal instanceof AbortSignal)) {
|
|
534
|
-
throw new
|
|
535
|
-
`Invalid signal option. It must be an instance of AbortSignal.`
|
|
536
|
-
)
|
|
554
|
+
throw new InvalidSignalError()
|
|
537
555
|
}
|
|
538
556
|
|
|
539
557
|
if (
|
|
540
558
|
options.offset !== undefined &&
|
|
541
559
|
options.offset !== `-1` &&
|
|
542
|
-
!options.
|
|
560
|
+
!options.handle
|
|
543
561
|
) {
|
|
544
|
-
throw new
|
|
545
|
-
`shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
|
|
546
|
-
)
|
|
562
|
+
throw new MissingShapeHandleError()
|
|
547
563
|
}
|
|
548
564
|
|
|
549
565
|
// Check for reserved parameter names
|
|
@@ -552,9 +568,7 @@ function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
|
552
568
|
RESERVED_PARAMS.has(key)
|
|
553
569
|
)
|
|
554
570
|
if (reservedParams.length > 0) {
|
|
555
|
-
throw new
|
|
556
|
-
`Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
|
|
557
|
-
)
|
|
571
|
+
throw new ReservedParamError(reservedParams)
|
|
558
572
|
}
|
|
559
573
|
}
|
|
560
574
|
return
|
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/error.ts
CHANGED
|
@@ -46,6 +46,60 @@ export class FetchError extends Error {
|
|
|
46
46
|
export class FetchBackoffAbortError extends Error {
|
|
47
47
|
constructor() {
|
|
48
48
|
super(`Fetch with backoff aborted`)
|
|
49
|
+
this.name = `FetchBackoffAbortError`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class InvalidShapeOptionsError extends Error {
|
|
54
|
+
constructor(message: string) {
|
|
55
|
+
super(message)
|
|
56
|
+
this.name = `InvalidShapeOptionsError`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class MissingShapeUrlError extends Error {
|
|
61
|
+
constructor() {
|
|
62
|
+
super(`Invalid shape options: missing required url parameter`)
|
|
63
|
+
this.name = `MissingShapeUrlError`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class InvalidSignalError extends Error {
|
|
68
|
+
constructor() {
|
|
69
|
+
super(`Invalid signal option. It must be an instance of AbortSignal.`)
|
|
70
|
+
this.name = `InvalidSignalError`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class MissingShapeHandleError extends Error {
|
|
75
|
+
constructor() {
|
|
76
|
+
super(
|
|
77
|
+
`shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
|
|
78
|
+
)
|
|
79
|
+
this.name = `MissingShapeHandleError`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class ReservedParamError extends Error {
|
|
84
|
+
constructor(reservedParams: string[]) {
|
|
85
|
+
super(
|
|
86
|
+
`Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
|
|
87
|
+
)
|
|
88
|
+
this.name = `ReservedParamError`
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class ParserNullValueError extends Error {
|
|
93
|
+
constructor(columnName: string) {
|
|
94
|
+
super(`Column "${columnName ?? `unknown`}" does not allow NULL values`)
|
|
95
|
+
this.name = `ParserNullValueError`
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ShapeStreamAlreadyRunningError extends Error {
|
|
100
|
+
constructor() {
|
|
101
|
+
super(`ShapeStream is already running`)
|
|
102
|
+
this.name = `ShapeStreamAlreadyRunningError`
|
|
49
103
|
}
|
|
50
104
|
}
|
|
51
105
|
|
|
@@ -56,6 +110,7 @@ export class MissingHeadersError extends Error {
|
|
|
56
110
|
msg += `- ${h}\n`
|
|
57
111
|
})
|
|
58
112
|
msg += `\nThis is often due to a proxy not setting CORS correctly so that all Electric headers can be read by the client.`
|
|
113
|
+
msg += `\nFor more information visit the troubleshooting guide: /docs/guides/troubleshooting/missing-headers`
|
|
59
114
|
super(msg)
|
|
60
115
|
}
|
|
61
116
|
}
|
package/src/fetch.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function createFetchWithResponseHeadersCheck(
|
|
|
171
171
|
const missingHeaders: Array<string> = []
|
|
172
172
|
|
|
173
173
|
const addMissingHeaders = (requiredHeaders: Array<string>) =>
|
|
174
|
-
requiredHeaders.filter((h) => !headers.has(h))
|
|
174
|
+
missingHeaders.push(...requiredHeaders.filter((h) => !headers.has(h)))
|
|
175
175
|
addMissingHeaders(requiredElectricResponseHeaders)
|
|
176
176
|
|
|
177
177
|
const input = args[0]
|
|
@@ -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/parser.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ColumnInfo, GetExtensions, Message, Row, Schema, Value } from './types'
|
|
2
|
+
import { ParserNullValueError } from './error'
|
|
2
3
|
|
|
3
4
|
type NullToken = null | `NULL`
|
|
4
5
|
type Token = Exclude<string, NullToken>
|
|
@@ -162,7 +163,7 @@ function makeNullableParser<Extensions>(
|
|
|
162
163
|
return (value: NullableToken) => {
|
|
163
164
|
if (isPgNull(value)) {
|
|
164
165
|
if (!isNullable) {
|
|
165
|
-
throw new
|
|
166
|
+
throw new ParserNullValueError(columnName ?? `unknown`)
|
|
166
167
|
}
|
|
167
168
|
return null
|
|
168
169
|
}
|
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
|
*
|
|
@@ -55,15 +60,6 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
55
60
|
this.#process.bind(this),
|
|
56
61
|
this.#handleError.bind(this)
|
|
57
62
|
)
|
|
58
|
-
const unsubscribe = this.#stream.subscribeOnceToUpToDate(
|
|
59
|
-
() => {
|
|
60
|
-
unsubscribe()
|
|
61
|
-
},
|
|
62
|
-
(e) => {
|
|
63
|
-
this.#handleError(e)
|
|
64
|
-
throw e
|
|
65
|
-
}
|
|
66
|
-
)
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
get isUpToDate(): boolean {
|
|
@@ -74,6 +70,10 @@ export class Shape<T extends Row<unknown> = Row> {
|
|
|
74
70
|
return this.#stream.lastOffset
|
|
75
71
|
}
|
|
76
72
|
|
|
73
|
+
get handle(): string | undefined {
|
|
74
|
+
return this.#stream.shapeHandle
|
|
75
|
+
}
|
|
76
|
+
|
|
77
77
|
get rows(): Promise<T[]> {
|
|
78
78
|
return this.value.then((v) => Array.from(v.values()))
|
|
79
79
|
}
|