@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/README.md +76 -1
- package/dist/cjs/index.cjs +273 -196
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.browser.mjs +5 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +37 -10
- package/dist/index.legacy-esm.js +269 -192
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +273 -196
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +106 -71
- package/src/error.ts +66 -0
- package/src/fetch.ts +52 -1
- package/src/parser.ts +2 -1
- package/src/shape.ts +9 -10
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 {
|
|
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
|
|
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
|
}
|