@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/src/client.ts CHANGED
@@ -11,7 +11,10 @@ import { isUpToDateMessage } from './helpers'
11
11
  import {
12
12
  FetchError,
13
13
  FetchBackoffAbortError,
14
- MissingHeadersError,
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
- type ParamsRecord = Omit<Record<string, string>, ReservedParamKeys>
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
- shapeHandle?: string
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
- * (table, where, columns, offset, handle, live, cursor, database_id, replica).
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?: ParamsRecord
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
- #error?: unknown
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.shapeHandle
244
- this.#databaseId = this.options.databaseId
274
+ this.#shapeHandle = this.options.handle
245
275
  this.#messageParser = new MessageParser<T>(options.parser)
246
- this.#replica = this.options.replica
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.start()
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
- get error() {
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
- for (const [key, value] of Object.entries(this.options.params)) {
308
- fetchUrl.searchParams.set(key, value)
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
- if (this.#databaseId) {
336
- fetchUrl.searchParams.set(DATABASE_ID_QUERY_PARAM, this.#databaseId!)
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?: (error: FetchError | Error) => void
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.isUpToDate
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(shapeHandle?: string) {
539
+ #reset(handle?: string) {
520
540
  this.#lastOffset = `-1`
521
541
  this.#liveCacheBuster = ``
522
- this.#shapeHandle = 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 Error(`Invalid shape options. It must provide the url`)
551
+ throw new MissingShapeUrlError()
532
552
  }
533
553
  if (options.signal && !(options.signal instanceof AbortSignal)) {
534
- throw new Error(
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.shapeHandle
560
+ !options.handle
543
561
  ) {
544
- throw new Error(
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 Error(
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 Error(`Column ${columnName ?? `unknown`} is not nullable`)
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 }>(url: `http://localhost:3000/v1/shape`, table: `foo`})
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
  }