@electric-sql/client 0.3.1 → 0.3.3
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 +2 -2
- package/dist/cjs/index.cjs +56 -26
- 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 +49 -4
- package/dist/index.legacy-esm.js +53 -25
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +53 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +20 -15
- package/src/helpers.ts +47 -0
- package/src/index.ts +1 -0
- package/src/parser.ts +39 -21
- package/src/types.ts +2 -2
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ArgumentsType } from 'vitest'
|
|
2
1
|
import { Message, Value, Offset, Schema } from './types'
|
|
3
2
|
import { MessageParser, Parser } from './parser'
|
|
3
|
+
import { isChangeMessage, isControlMessage } from './helpers'
|
|
4
4
|
|
|
5
5
|
export type ShapeData = Map<string, { [key: string]: Value }>
|
|
6
6
|
export type ShapeChangedCallback = (value: ShapeData) => void
|
|
@@ -196,7 +196,7 @@ export class ShapeStream {
|
|
|
196
196
|
this.backoffOptions = options.backoffOptions ?? BackoffDefaults
|
|
197
197
|
this.fetchClient =
|
|
198
198
|
options.fetchClient ??
|
|
199
|
-
((...args:
|
|
199
|
+
((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
200
200
|
|
|
201
201
|
this.start()
|
|
202
202
|
}
|
|
@@ -270,7 +270,8 @@ export class ShapeStream {
|
|
|
270
270
|
if (batch.length > 0) {
|
|
271
271
|
const lastMessage = batch[batch.length - 1]
|
|
272
272
|
if (
|
|
273
|
-
lastMessage
|
|
273
|
+
isControlMessage(lastMessage) &&
|
|
274
|
+
lastMessage.headers.control === `up-to-date` &&
|
|
274
275
|
!this.isUpToDate
|
|
275
276
|
) {
|
|
276
277
|
this.isUpToDate = true
|
|
@@ -514,7 +515,7 @@ export class Shape {
|
|
|
514
515
|
let newlyUpToDate = false
|
|
515
516
|
|
|
516
517
|
messages.forEach((message) => {
|
|
517
|
-
if (
|
|
518
|
+
if (isChangeMessage(message)) {
|
|
518
519
|
dataMayHaveChanged = [`insert`, `update`, `delete`].includes(
|
|
519
520
|
message.headers.operation
|
|
520
521
|
)
|
|
@@ -535,19 +536,22 @@ export class Shape {
|
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
538
|
|
|
538
|
-
if (message
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
539
|
+
if (isControlMessage(message)) {
|
|
540
|
+
switch (message.headers.control) {
|
|
541
|
+
case `up-to-date`:
|
|
542
|
+
isUpToDate = true
|
|
543
|
+
if (!this.hasNotifiedSubscribersUpToDate) {
|
|
544
|
+
newlyUpToDate = true
|
|
545
|
+
}
|
|
546
|
+
break
|
|
547
|
+
case `must-refetch`:
|
|
548
|
+
this.data.clear()
|
|
549
|
+
this.error = false
|
|
550
|
+
isUpToDate = false
|
|
551
|
+
newlyUpToDate = false
|
|
552
|
+
break
|
|
542
553
|
}
|
|
543
554
|
}
|
|
544
|
-
|
|
545
|
-
if (message.headers?.[`control`] === `must-refetch`) {
|
|
546
|
-
this.data.clear()
|
|
547
|
-
this.error = false
|
|
548
|
-
isUpToDate = false
|
|
549
|
-
newlyUpToDate = false
|
|
550
|
-
}
|
|
551
555
|
})
|
|
552
556
|
|
|
553
557
|
// Always notify subscribers when the Shape first is up to date.
|
|
@@ -561,6 +565,7 @@ export class Shape {
|
|
|
561
565
|
private handleError(e: Error): void {
|
|
562
566
|
if (e instanceof FetchError) {
|
|
563
567
|
this.error = e
|
|
568
|
+
this.notify()
|
|
564
569
|
}
|
|
565
570
|
}
|
|
566
571
|
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ChangeMessage, ControlMessage, Message, Value } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard for checking {@link Message} is {@link ChangeMessage}.
|
|
5
|
+
*
|
|
6
|
+
* See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
|
|
7
|
+
* for information on how to use type guards.
|
|
8
|
+
*
|
|
9
|
+
* @param message - the message to check
|
|
10
|
+
* @returns true if the message is a {@link ChangeMessage}
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* if (isChangeMessage(message)) {
|
|
15
|
+
* const msgChng: ChangeMessage = message // Ok
|
|
16
|
+
* const msgCtrl: ControlMessage = message // Err, type mismatch
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function isChangeMessage<T extends Value = { [key: string]: Value }>(
|
|
21
|
+
message: Message<T>
|
|
22
|
+
): message is ChangeMessage<T> {
|
|
23
|
+
return `key` in message
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type guard for checking {@link Message} is {@link ControlMessage}.
|
|
28
|
+
*
|
|
29
|
+
* See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
|
|
30
|
+
* for information on how to use type guards.
|
|
31
|
+
*
|
|
32
|
+
* @param message - the message to check
|
|
33
|
+
* @returns true if the message is a {@link ControlMessage}
|
|
34
|
+
*
|
|
35
|
+
* * @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* if (isControlMessage(message)) {
|
|
38
|
+
* const msgChng: ChangeMessage = message // Err, type mismatch
|
|
39
|
+
* const msgCtrl: ControlMessage = message // Ok
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function isControlMessage<T extends Value = { [key: string]: Value }>(
|
|
44
|
+
message: Message<T>
|
|
45
|
+
): message is ControlMessage {
|
|
46
|
+
return !isChangeMessage(message)
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
package/src/parser.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { ColumnInfo, Message, Schema, Value } from './types'
|
|
2
2
|
|
|
3
|
+
type NullToken = null | `NULL`
|
|
4
|
+
type Token = Exclude<string, NullToken>
|
|
5
|
+
type NullableToken = Token | NullToken
|
|
3
6
|
export type ParseFunction = (
|
|
4
|
-
value:
|
|
7
|
+
value: Token,
|
|
8
|
+
additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
|
|
9
|
+
) => Value
|
|
10
|
+
type NullableParseFunction = (
|
|
11
|
+
value: NullableToken,
|
|
5
12
|
additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
|
|
6
13
|
) => Value
|
|
7
14
|
export type Parser = { [key: string]: ParseFunction }
|
|
@@ -10,6 +17,7 @@ const parseNumber = (value: string) => Number(value)
|
|
|
10
17
|
const parseBool = (value: string) => value === `true` || value === `t`
|
|
11
18
|
const parseBigInt = (value: string) => BigInt(value)
|
|
12
19
|
const parseJson = (value: string) => JSON.parse(value)
|
|
20
|
+
const identityParser: ParseFunction = (v: string) => v
|
|
13
21
|
|
|
14
22
|
export const defaultParser: Parser = {
|
|
15
23
|
int2: parseNumber,
|
|
@@ -23,10 +31,7 @@ export const defaultParser: Parser = {
|
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
// Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279
|
|
26
|
-
export function pgArrayParser(
|
|
27
|
-
value: string,
|
|
28
|
-
parser?: (s: string) => Value
|
|
29
|
-
): Value {
|
|
34
|
+
export function pgArrayParser(value: Token, parser?: ParseFunction): Value {
|
|
30
35
|
let i = 0
|
|
31
36
|
let char = null
|
|
32
37
|
let str = ``
|
|
@@ -92,7 +97,7 @@ export class MessageParser {
|
|
|
92
97
|
// Parse the row values
|
|
93
98
|
const row = value as Record<string, Value>
|
|
94
99
|
Object.keys(row).forEach((key) => {
|
|
95
|
-
row[key] = this.parseRow(key, row[key] as
|
|
100
|
+
row[key] = this.parseRow(key, row[key] as NullableToken, schema)
|
|
96
101
|
})
|
|
97
102
|
}
|
|
98
103
|
return value
|
|
@@ -100,7 +105,7 @@ export class MessageParser {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
// Parses the message values using the provided parser based on the schema information
|
|
103
|
-
private parseRow(key: string, value:
|
|
108
|
+
private parseRow(key: string, value: NullableToken, schema: Schema): Value {
|
|
104
109
|
const columnInfo = schema[key]
|
|
105
110
|
if (!columnInfo) {
|
|
106
111
|
// We don't have information about the value
|
|
@@ -114,13 +119,17 @@ export class MessageParser {
|
|
|
114
119
|
// Pick the right parser for the type
|
|
115
120
|
// and support parsing null values if needed
|
|
116
121
|
// if no parser is provided for the given type, just return the value as is
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const parser = makeNullableParser(typParser, columnInfo.not_null)
|
|
122
|
+
const typeParser = this.parser[typ] ?? identityParser
|
|
123
|
+
const parser = makeNullableParser(typeParser, columnInfo, key)
|
|
120
124
|
|
|
121
125
|
if (dimensions && dimensions > 0) {
|
|
122
126
|
// It's an array
|
|
123
|
-
|
|
127
|
+
const nullablePgArrayParser = makeNullableParser(
|
|
128
|
+
(value, _) => pgArrayParser(value, parser),
|
|
129
|
+
columnInfo,
|
|
130
|
+
key
|
|
131
|
+
)
|
|
132
|
+
return nullablePgArrayParser(value)
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
return parser(value, additionalInfo)
|
|
@@ -129,15 +138,24 @@ export class MessageParser {
|
|
|
129
138
|
|
|
130
139
|
function makeNullableParser(
|
|
131
140
|
parser: ParseFunction,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
columnInfo: ColumnInfo,
|
|
142
|
+
columnName?: string
|
|
143
|
+
): NullableParseFunction {
|
|
144
|
+
const isNullable = !(columnInfo.not_null ?? false)
|
|
145
|
+
// The sync service contains `null` value for a column whose value is NULL
|
|
146
|
+
// but if the column value is an array that contains a NULL value
|
|
147
|
+
// then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
|
|
148
|
+
return (value: NullableToken) => {
|
|
149
|
+
if (isPgNull(value)) {
|
|
150
|
+
if (!isNullable) {
|
|
151
|
+
throw new Error(`Column ${columnName ?? `unknown`} is not nullable`)
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
return parser(value, columnInfo)
|
|
141
156
|
}
|
|
142
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isPgNull(value: NullableToken): value is NullToken {
|
|
160
|
+
return value === null || value === `NULL`
|
|
143
161
|
}
|
package/src/types.ts
CHANGED
|
@@ -10,11 +10,11 @@ export type Value =
|
|
|
10
10
|
export type Offset = `-1` | `${number}_${number}`
|
|
11
11
|
|
|
12
12
|
interface Header {
|
|
13
|
-
[key: string]: Value
|
|
13
|
+
[key: Exclude<string, `operation` | `control`>]: Value
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type ControlMessage = {
|
|
17
|
-
headers: Header
|
|
17
|
+
headers: Header & { control: `up-to-date` | `must-refetch` }
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export type ChangeMessage<T> = {
|