@electric-sql/client 0.6.0 → 0.6.2
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/dist/cjs/index.cjs +22 -3
- 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 +35 -23
- package/dist/index.legacy-esm.js +22 -3
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +22 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +40 -9
- package/src/constants.ts +2 -0
- package/src/fetch.ts +5 -0
- package/src/helpers.ts +3 -3
- package/src/parser.ts +29 -16
- package/src/shape.ts +3 -3
- package/src/types.ts +17 -7
package/src/client.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Message,
|
|
3
|
+
Offset,
|
|
4
|
+
Schema,
|
|
5
|
+
Row,
|
|
6
|
+
MaybePromise,
|
|
7
|
+
GetExtensions,
|
|
8
|
+
} from './types'
|
|
2
9
|
import { MessageParser, Parser } from './parser'
|
|
3
10
|
import { isUpToDateMessage } from './helpers'
|
|
4
11
|
import { FetchError, FetchBackoffAbortError } from './error'
|
|
@@ -10,6 +17,8 @@ import {
|
|
|
10
17
|
} from './fetch'
|
|
11
18
|
import {
|
|
12
19
|
CHUNK_LAST_OFFSET_HEADER,
|
|
20
|
+
LIVE_CACHE_BUSTER_HEADER,
|
|
21
|
+
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
13
22
|
LIVE_QUERY_PARAM,
|
|
14
23
|
OFFSET_QUERY_PARAM,
|
|
15
24
|
SHAPE_ID_HEADER,
|
|
@@ -21,7 +30,7 @@ import {
|
|
|
21
30
|
/**
|
|
22
31
|
* Options for constructing a ShapeStream.
|
|
23
32
|
*/
|
|
24
|
-
export interface ShapeStreamOptions {
|
|
33
|
+
export interface ShapeStreamOptions<T = never> {
|
|
25
34
|
/**
|
|
26
35
|
* The full URL to where the Shape is hosted. This can either be the Electric server
|
|
27
36
|
* directly or a proxy. E.g. for a local Electric instance, you might set `http://localhost:3000/v1/shape/foo`
|
|
@@ -46,6 +55,13 @@ export interface ShapeStreamOptions {
|
|
|
46
55
|
*/
|
|
47
56
|
shapeId?: string
|
|
48
57
|
backoffOptions?: BackoffOptions
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* HTTP headers to attach to requests made by the client.
|
|
61
|
+
* Can be used for adding authentication headers.
|
|
62
|
+
*/
|
|
63
|
+
headers?: Record<string, string>
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Automatically fetch updates to the Shape. If you just want to sync the current
|
|
51
67
|
* shape and stop, pass false.
|
|
@@ -53,10 +69,10 @@ export interface ShapeStreamOptions {
|
|
|
53
69
|
subscribe?: boolean
|
|
54
70
|
signal?: AbortSignal
|
|
55
71
|
fetchClient?: typeof fetch
|
|
56
|
-
parser?: Parser
|
|
72
|
+
parser?: Parser<T>
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
export interface ShapeStreamInterface<T extends Row = Row> {
|
|
75
|
+
export interface ShapeStreamInterface<T extends Row<unknown> = Row> {
|
|
60
76
|
subscribe(
|
|
61
77
|
callback: (messages: Message<T>[]) => MaybePromise<void>,
|
|
62
78
|
onError?: (error: FetchError | Error) => void
|
|
@@ -108,10 +124,10 @@ export interface ShapeStreamInterface<T extends Row = Row> {
|
|
|
108
124
|
* ```
|
|
109
125
|
*/
|
|
110
126
|
|
|
111
|
-
export class ShapeStream<T extends Row = Row>
|
|
127
|
+
export class ShapeStream<T extends Row<unknown> = Row>
|
|
112
128
|
implements ShapeStreamInterface<T>
|
|
113
129
|
{
|
|
114
|
-
readonly options: ShapeStreamOptions
|
|
130
|
+
readonly options: ShapeStreamOptions<GetExtensions<T>>
|
|
115
131
|
|
|
116
132
|
readonly #fetchClient: typeof fetch
|
|
117
133
|
readonly #messageParser: MessageParser<T>
|
|
@@ -129,16 +145,18 @@ export class ShapeStream<T extends Row = Row>
|
|
|
129
145
|
>()
|
|
130
146
|
|
|
131
147
|
#lastOffset: Offset
|
|
148
|
+
#liveCacheBuster: string // Seconds since our Electric Epoch 😎
|
|
132
149
|
#lastSyncedAt?: number // unix time
|
|
133
150
|
#isUpToDate: boolean = false
|
|
134
151
|
#connected: boolean = false
|
|
135
152
|
#shapeId?: string
|
|
136
153
|
#schema?: Schema
|
|
137
154
|
|
|
138
|
-
constructor(options: ShapeStreamOptions) {
|
|
155
|
+
constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
|
|
139
156
|
validateOptions(options)
|
|
140
157
|
this.options = { subscribe: true, ...options }
|
|
141
158
|
this.#lastOffset = this.options.offset ?? `-1`
|
|
159
|
+
this.#liveCacheBuster = ``
|
|
142
160
|
this.#shapeId = this.options.shapeId
|
|
143
161
|
this.#messageParser = new MessageParser<T>(options.parser)
|
|
144
162
|
|
|
@@ -183,6 +201,10 @@ export class ShapeStream<T extends Row = Row>
|
|
|
183
201
|
|
|
184
202
|
if (this.#isUpToDate) {
|
|
185
203
|
fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`)
|
|
204
|
+
fetchUrl.searchParams.set(
|
|
205
|
+
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
206
|
+
this.#liveCacheBuster
|
|
207
|
+
)
|
|
186
208
|
}
|
|
187
209
|
|
|
188
210
|
if (this.#shapeId) {
|
|
@@ -192,7 +214,10 @@ export class ShapeStream<T extends Row = Row>
|
|
|
192
214
|
|
|
193
215
|
let response!: Response
|
|
194
216
|
try {
|
|
195
|
-
response = await this.#fetchClient(fetchUrl.toString(), {
|
|
217
|
+
response = await this.#fetchClient(fetchUrl.toString(), {
|
|
218
|
+
signal,
|
|
219
|
+
headers: this.options.headers,
|
|
220
|
+
})
|
|
196
221
|
this.#connected = true
|
|
197
222
|
} catch (e) {
|
|
198
223
|
if (e instanceof FetchBackoffAbortError) break // interrupted
|
|
@@ -231,6 +256,11 @@ export class ShapeStream<T extends Row = Row>
|
|
|
231
256
|
this.#lastOffset = lastOffset as Offset
|
|
232
257
|
}
|
|
233
258
|
|
|
259
|
+
const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER)
|
|
260
|
+
if (liveCacheBuster) {
|
|
261
|
+
this.#liveCacheBuster = liveCacheBuster
|
|
262
|
+
}
|
|
263
|
+
|
|
234
264
|
const getSchema = (): Schema => {
|
|
235
265
|
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER)
|
|
236
266
|
return schemaHeader ? JSON.parse(schemaHeader) : {}
|
|
@@ -359,6 +389,7 @@ export class ShapeStream<T extends Row = Row>
|
|
|
359
389
|
*/
|
|
360
390
|
#reset(shapeId?: string) {
|
|
361
391
|
this.#lastOffset = `-1`
|
|
392
|
+
this.#liveCacheBuster = ``
|
|
362
393
|
this.#shapeId = shapeId
|
|
363
394
|
this.#isUpToDate = false
|
|
364
395
|
this.#connected = false
|
|
@@ -366,7 +397,7 @@ export class ShapeStream<T extends Row = Row>
|
|
|
366
397
|
}
|
|
367
398
|
}
|
|
368
399
|
|
|
369
|
-
function validateOptions(options: Partial<ShapeStreamOptions
|
|
400
|
+
function validateOptions<T>(options: Partial<ShapeStreamOptions<T>>): void {
|
|
370
401
|
if (!options.url) {
|
|
371
402
|
throw new Error(`Invalid shape option. It must provide the url`)
|
|
372
403
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const SHAPE_ID_HEADER = `electric-shape-id`
|
|
2
|
+
export const LIVE_CACHE_BUSTER_HEADER = `electric-next-cursor`
|
|
3
|
+
export const LIVE_CACHE_BUSTER_QUERY_PARAM = `cursor`
|
|
2
4
|
export const CHUNK_LAST_OFFSET_HEADER = `electric-chunk-last-offset`
|
|
3
5
|
export const CHUNK_UP_TO_DATE_HEADER = `electric-chunk-up-to-date`
|
|
4
6
|
export const SHAPE_SCHEMA_HEADER = `electric-schema`
|
package/src/fetch.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
} from './constants'
|
|
9
9
|
import { FetchError, FetchBackoffAbortError } from './error'
|
|
10
10
|
|
|
11
|
+
// Some specific 4xx and 5xx HTTP status codes that we definitely
|
|
12
|
+
// want to retry
|
|
13
|
+
const HTTP_RETRY_STATUS_CODES = [429]
|
|
14
|
+
|
|
11
15
|
export interface BackoffOptions {
|
|
12
16
|
/**
|
|
13
17
|
* Initial delay before retrying in milliseconds
|
|
@@ -63,6 +67,7 @@ export function createFetchWithBackoff(
|
|
|
63
67
|
throw new FetchBackoffAbortError()
|
|
64
68
|
} else if (
|
|
65
69
|
e instanceof FetchError &&
|
|
70
|
+
!HTTP_RETRY_STATUS_CODES.includes(e.status) &&
|
|
66
71
|
e.status >= 400 &&
|
|
67
72
|
e.status < 500
|
|
68
73
|
) {
|
package/src/helpers.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { ChangeMessage, ControlMessage, Message, Row } from './types'
|
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export function isChangeMessage<T extends Row = Row>(
|
|
20
|
+
export function isChangeMessage<T extends Row<unknown> = Row>(
|
|
21
21
|
message: Message<T>
|
|
22
22
|
): message is ChangeMessage<T> {
|
|
23
23
|
return `key` in message
|
|
@@ -40,13 +40,13 @@ export function isChangeMessage<T extends Row = Row>(
|
|
|
40
40
|
* }
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
|
-
export function isControlMessage<T extends Row = Row>(
|
|
43
|
+
export function isControlMessage<T extends Row<unknown> = Row>(
|
|
44
44
|
message: Message<T>
|
|
45
45
|
): message is ControlMessage {
|
|
46
46
|
return !isChangeMessage(message)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export function isUpToDateMessage<T extends Row = Row>(
|
|
49
|
+
export function isUpToDateMessage<T extends Row<unknown> = Row>(
|
|
50
50
|
message: Message<T>
|
|
51
51
|
): message is ControlMessage & { up_to_date: true } {
|
|
52
52
|
return isControlMessage(message) && message.headers.control === `up-to-date`
|
package/src/parser.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { ColumnInfo, Message, Row, Schema, Value } from './types'
|
|
1
|
+
import { ColumnInfo, GetExtensions, Message, Row, Schema, Value } from './types'
|
|
2
2
|
|
|
3
3
|
type NullToken = null | `NULL`
|
|
4
4
|
type Token = Exclude<string, NullToken>
|
|
5
5
|
type NullableToken = Token | NullToken
|
|
6
|
-
export type ParseFunction = (
|
|
6
|
+
export type ParseFunction<Extensions = never> = (
|
|
7
7
|
value: Token,
|
|
8
8
|
additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
|
|
9
|
-
) => Value
|
|
10
|
-
type NullableParseFunction = (
|
|
9
|
+
) => Value<Extensions>
|
|
10
|
+
type NullableParseFunction<Extensions = never> = (
|
|
11
11
|
value: NullableToken,
|
|
12
12
|
additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
|
|
13
|
-
) => Value
|
|
14
|
-
|
|
13
|
+
) => Value<Extensions>
|
|
14
|
+
/**
|
|
15
|
+
* @typeParam Extensions - Additional types that can be parsed by this parser beyond the standard SQL types.
|
|
16
|
+
* Defaults to no additional types.
|
|
17
|
+
*/
|
|
18
|
+
export type Parser<Extensions = never> = {
|
|
19
|
+
[key: string]: ParseFunction<Extensions>
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
const parseNumber = (value: string) => Number(value)
|
|
17
23
|
const parseBool = (value: string) => value === `true` || value === `t`
|
|
@@ -31,7 +37,10 @@ export const defaultParser: Parser = {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
// Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279
|
|
34
|
-
export function pgArrayParser(
|
|
40
|
+
export function pgArrayParser<Extensions>(
|
|
41
|
+
value: Token,
|
|
42
|
+
parser?: ParseFunction<Extensions>
|
|
43
|
+
): Value<Extensions> {
|
|
35
44
|
let i = 0
|
|
36
45
|
let char = null
|
|
37
46
|
let str = ``
|
|
@@ -39,7 +48,7 @@ export function pgArrayParser(value: Token, parser?: ParseFunction): Value {
|
|
|
39
48
|
let last = 0
|
|
40
49
|
let p: string | undefined = undefined
|
|
41
50
|
|
|
42
|
-
function loop(x: string): Value
|
|
51
|
+
function loop(x: string): Array<Value<Extensions>> {
|
|
43
52
|
const xs = []
|
|
44
53
|
for (; i < x.length; i++) {
|
|
45
54
|
char = x[i]
|
|
@@ -79,9 +88,9 @@ export function pgArrayParser(value: Token, parser?: ParseFunction): Value {
|
|
|
79
88
|
return loop(value)[0]
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
export class MessageParser<T extends Row
|
|
83
|
-
private parser: Parser
|
|
84
|
-
constructor(parser?: Parser) {
|
|
91
|
+
export class MessageParser<T extends Row<unknown>> {
|
|
92
|
+
private parser: Parser<GetExtensions<T>>
|
|
93
|
+
constructor(parser?: Parser<GetExtensions<T>>) {
|
|
85
94
|
// Merge the provided parser with the default parser
|
|
86
95
|
// to use the provided parser whenever defined
|
|
87
96
|
// and otherwise fall back to the default parser
|
|
@@ -96,7 +105,7 @@ export class MessageParser<T extends Row> {
|
|
|
96
105
|
// But `typeof null === 'object'` so we need to make an explicit check.
|
|
97
106
|
if (key === `value` && typeof value === `object` && value !== null) {
|
|
98
107
|
// Parse the row values
|
|
99
|
-
const row = value as Record<string, Value
|
|
108
|
+
const row = value as Record<string, Value<GetExtensions<T>>>
|
|
100
109
|
Object.keys(row).forEach((key) => {
|
|
101
110
|
row[key] = this.parseRow(key, row[key] as NullableToken, schema)
|
|
102
111
|
})
|
|
@@ -106,7 +115,11 @@ export class MessageParser<T extends Row> {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
// Parses the message values using the provided parser based on the schema information
|
|
109
|
-
private parseRow(
|
|
118
|
+
private parseRow(
|
|
119
|
+
key: string,
|
|
120
|
+
value: NullableToken,
|
|
121
|
+
schema: Schema
|
|
122
|
+
): Value<GetExtensions<T>> {
|
|
110
123
|
const columnInfo = schema[key]
|
|
111
124
|
if (!columnInfo) {
|
|
112
125
|
// We don't have information about the value
|
|
@@ -137,11 +150,11 @@ export class MessageParser<T extends Row> {
|
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
function makeNullableParser(
|
|
141
|
-
parser: ParseFunction
|
|
153
|
+
function makeNullableParser<Extensions>(
|
|
154
|
+
parser: ParseFunction<Extensions>,
|
|
142
155
|
columnInfo: ColumnInfo,
|
|
143
156
|
columnName?: string
|
|
144
|
-
): NullableParseFunction {
|
|
157
|
+
): NullableParseFunction<Extensions> {
|
|
145
158
|
const isNullable = !(columnInfo.not_null ?? false)
|
|
146
159
|
// The sync service contains `null` value for a column whose value is NULL
|
|
147
160
|
// but if the column value is an array that contains a NULL value
|
package/src/shape.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { isChangeMessage, isControlMessage } from './helpers'
|
|
|
3
3
|
import { FetchError } from './error'
|
|
4
4
|
import { ShapeStreamInterface } from './client'
|
|
5
5
|
|
|
6
|
-
export type ShapeData<T extends Row = Row> = Map<string, T>
|
|
7
|
-
export type ShapeChangedCallback<T extends Row = Row> = (
|
|
6
|
+
export type ShapeData<T extends Row<unknown> = Row> = Map<string, T>
|
|
7
|
+
export type ShapeChangedCallback<T extends Row<unknown> = Row> = (
|
|
8
8
|
value: ShapeData<T>
|
|
9
9
|
) => void
|
|
10
10
|
|
|
@@ -39,7 +39,7 @@ export type ShapeChangedCallback<T extends Row = Row> = (
|
|
|
39
39
|
* console.log(shapeData)
|
|
40
40
|
* })
|
|
41
41
|
*/
|
|
42
|
-
export class Shape<T extends Row = Row> {
|
|
42
|
+
export class Shape<T extends Row<unknown> = Row> {
|
|
43
43
|
readonly #stream: ShapeStreamInterface<T>
|
|
44
44
|
|
|
45
45
|
readonly #data: ShapeData<T> = new Map()
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Default types for SQL but can be extended with additional types when using a custom parser.
|
|
3
|
+
* @typeParam Extensions - Additional value types.
|
|
4
|
+
*/
|
|
5
|
+
export type Value<Extensions = never> =
|
|
2
6
|
| string
|
|
3
7
|
| number
|
|
4
8
|
| boolean
|
|
5
9
|
| bigint
|
|
6
10
|
| null
|
|
7
|
-
|
|
|
8
|
-
|
|
|
11
|
+
| Extensions
|
|
12
|
+
| Value<Extensions>[]
|
|
13
|
+
| { [key: string]: Value<Extensions> }
|
|
14
|
+
|
|
15
|
+
export type Row<Extensions = never> = Record<string, Value<Extensions>>
|
|
9
16
|
|
|
10
|
-
export type Row =
|
|
17
|
+
export type GetExtensions<T extends Row<unknown>> =
|
|
18
|
+
T extends Row<infer Extensions> ? Extensions : never
|
|
11
19
|
|
|
12
20
|
export type Offset = `-1` | `${number}_${number}`
|
|
13
21
|
|
|
@@ -19,7 +27,7 @@ export type ControlMessage = {
|
|
|
19
27
|
headers: Header & { control: `up-to-date` | `must-refetch` }
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
export type ChangeMessage<T extends Row = Row> = {
|
|
30
|
+
export type ChangeMessage<T extends Row<unknown> = Row> = {
|
|
23
31
|
key: string
|
|
24
32
|
value: T
|
|
25
33
|
headers: Header & { operation: `insert` | `update` | `delete` }
|
|
@@ -27,7 +35,9 @@ export type ChangeMessage<T extends Row = Row> = {
|
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
// Define the type for a record
|
|
30
|
-
export type Message<T extends Row = Row> =
|
|
38
|
+
export type Message<T extends Row<unknown> = Row> =
|
|
39
|
+
| ControlMessage
|
|
40
|
+
| ChangeMessage<T>
|
|
31
41
|
|
|
32
42
|
/**
|
|
33
43
|
* Common properties for all columns.
|
|
@@ -104,7 +114,7 @@ export type ColumnInfo =
|
|
|
104
114
|
|
|
105
115
|
export type Schema = { [key: string]: ColumnInfo }
|
|
106
116
|
|
|
107
|
-
export type TypedMessages<T extends Row = Row> = {
|
|
117
|
+
export type TypedMessages<T extends Row<unknown> = Row> = {
|
|
108
118
|
messages: Array<Message<T>>
|
|
109
119
|
schema: ColumnInfo
|
|
110
120
|
}
|