@electric-sql/client 0.7.2 → 0.8.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/error.ts CHANGED
@@ -46,5 +46,71 @@ 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`
103
+ }
104
+ }
105
+
106
+ export class MissingHeadersError extends Error {
107
+ constructor(url: string, missingHeaders: Array<string>) {
108
+ let msg = `The response for the shape request to ${url} didn't include the following required headers:\n`
109
+ missingHeaders.forEach((h) => {
110
+ msg += `- ${h}\n`
111
+ })
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`
114
+ super(msg)
49
115
  }
50
116
  }
package/src/fetch.ts CHANGED
@@ -6,7 +6,11 @@ import {
6
6
  SHAPE_HANDLE_HEADER,
7
7
  SHAPE_HANDLE_QUERY_PARAM,
8
8
  } from './constants'
9
- import { FetchError, FetchBackoffAbortError } from './error'
9
+ import {
10
+ FetchError,
11
+ FetchBackoffAbortError,
12
+ MissingHeadersError,
13
+ } from './error'
10
14
 
11
15
  // Some specific 4xx and 5xx HTTP status codes that we definitely
12
16
  // want to retry
@@ -146,6 +150,53 @@ export function createFetchWithChunkBuffer(
146
150
  return prefetchClient
147
151
  }
148
152
 
153
+ export const requiredElectricResponseHeaders = [
154
+ `electric-offset`,
155
+ `electric-handle`,
156
+ ]
157
+
158
+ export const requiredLiveResponseHeaders = [`electric-cursor`]
159
+
160
+ export const requiredNonLiveResponseHeaders = [`electric-schema`]
161
+
162
+ export function createFetchWithResponseHeadersCheck(
163
+ fetchClient: typeof fetch
164
+ ): typeof fetch {
165
+ return async (...args: Parameters<typeof fetchClient>) => {
166
+ const response = await fetchClient(...args)
167
+
168
+ if (response.ok) {
169
+ // Check that the necessary Electric headers are present on the response
170
+ const headers = response.headers
171
+ const missingHeaders: Array<string> = []
172
+
173
+ const addMissingHeaders = (requiredHeaders: Array<string>) =>
174
+ missingHeaders.push(...requiredHeaders.filter((h) => !headers.has(h)))
175
+ addMissingHeaders(requiredElectricResponseHeaders)
176
+
177
+ const input = args[0]
178
+ const urlString = input.toString()
179
+ const url = new URL(urlString)
180
+ if (url.searchParams.has(LIVE_QUERY_PARAM, `true`)) {
181
+ addMissingHeaders(requiredLiveResponseHeaders)
182
+ }
183
+
184
+ if (
185
+ !url.searchParams.has(LIVE_QUERY_PARAM) ||
186
+ url.searchParams.has(LIVE_QUERY_PARAM, `false`)
187
+ ) {
188
+ addMissingHeaders(requiredNonLiveResponseHeaders)
189
+ }
190
+
191
+ if (missingHeaders.length > 0) {
192
+ throw new MissingHeadersError(urlString, missingHeaders)
193
+ }
194
+ }
195
+
196
+ return response
197
+ }
198
+ }
199
+
149
200
  class PrefetchQueue {
150
201
  readonly #fetchClient: typeof fetch
151
202
  readonly #maxPrefetchedRequests: number
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
@@ -1,4 +1,4 @@
1
- import { Message, Row } from './types'
1
+ import { Message, Offset, Row } from './types'
2
2
  import { isChangeMessage, isControlMessage } from './helpers'
3
3
  import { FetchError } from './error'
4
4
  import { ShapeStreamInterface } from './client'
@@ -55,21 +55,20 @@ export class Shape<T extends Row<unknown> = Row> {
55
55
  this.#process.bind(this),
56
56
  this.#handleError.bind(this)
57
57
  )
58
- const unsubscribe = this.#stream.subscribeOnceToUpToDate(
59
- () => {
60
- unsubscribe()
61
- },
62
- (e) => {
63
- this.#handleError(e)
64
- throw e
65
- }
66
- )
67
58
  }
68
59
 
69
60
  get isUpToDate(): boolean {
70
61
  return this.#stream.isUpToDate
71
62
  }
72
63
 
64
+ get lastOffset(): Offset {
65
+ return this.#stream.lastOffset
66
+ }
67
+
68
+ get handle(): string | undefined {
69
+ return this.#stream.shapeHandle
70
+ }
71
+
73
72
  get rows(): Promise<T[]> {
74
73
  return this.value.then((v) => Array.from(v.values()))
75
74
  }