@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/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: ArgumentsType<typeof fetch>) => fetch(...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.headers?.[`control`] === `up-to-date` &&
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 (`key` in message) {
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.headers?.[`control`] === `up-to-date`) {
539
- isUpToDate = true
540
- if (!this.hasNotifiedSubscribersUpToDate) {
541
- newlyUpToDate = true
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
@@ -1,2 +1,3 @@
1
1
  export * from './client'
2
2
  export * from './types'
3
+ export * from './helpers'
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: string,
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 string, schema)
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: string, schema: Schema): 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 identityParser = (v: string) => v
118
- const typParser = this.parser[typ] ?? identityParser
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
- return pgArrayParser(value, parser)
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
- notNullable?: boolean
133
- ): ParseFunction {
134
- const isNullable = !(notNullable ?? false)
135
- if (isNullable) {
136
- // The sync service contains `null` value for a column whose value is NULL
137
- // but if the column value is an array that contains a NULL value
138
- // then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
139
- return (value: string) =>
140
- value === null || value === `NULL` ? null : parser(value)
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
- return parser
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> = {