@electric-sql/client 1.0.5 → 1.0.7

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
@@ -398,22 +398,12 @@ export class ShapeStream<T extends Row<unknown> = Row>
398
398
  backOffOpts
399
399
  )
400
400
 
401
- this.#fetchClient = createFetchWithConsumedMessages(
402
- createFetchWithResponseHeadersCheck(
403
- createFetchWithChunkBuffer(fetchWithBackoffClient)
404
- )
405
- )
406
-
407
- const sseFetchWithBackoffClient = createFetchWithBackoff(
408
- baseFetchClient,
409
- backOffOpts,
410
- true
411
- )
412
-
413
401
  this.#sseFetchClient = createFetchWithResponseHeadersCheck(
414
- createFetchWithChunkBuffer(sseFetchWithBackoffClient)
402
+ createFetchWithChunkBuffer(fetchWithBackoffClient)
415
403
  )
416
404
 
405
+ this.#fetchClient = createFetchWithConsumedMessages(this.#sseFetchClient)
406
+
417
407
  this.#subscribeToVisibilityChanges()
418
408
  }
419
409
 
@@ -498,7 +488,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
498
488
  fetchUrl,
499
489
  requestAbortController,
500
490
  headers: requestHeaders,
501
- resumingFromPause: true,
491
+ resumingFromPause,
502
492
  })
503
493
  } catch (e) {
504
494
  // Handle abort error triggered by refresh
@@ -671,9 +661,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
671
661
  }
672
662
  }
673
663
 
674
- async #onMessages(messages: string, schema: Schema, isSseMessage = false) {
675
- const batch = this.#messageParser.parse(messages, schema)
676
-
664
+ async #onMessages(batch: Array<Message<T>>, isSseMessage = false) {
677
665
  // Update isUpToDate
678
666
  if (batch.length > 0) {
679
667
  const lastMessage = batch[batch.length - 1]
@@ -738,8 +726,9 @@ export class ShapeStream<T extends Row<unknown> = Row>
738
726
  const schema = this.#schema! // we know that it is not undefined because it is set by `this.#onInitialResponse`
739
727
  const res = await response.text()
740
728
  const messages = res || `[]`
729
+ const batch = this.#messageParser.parse<Array<Message<T>>>(messages, schema)
741
730
 
742
- await this.#onMessages(messages, schema)
731
+ await this.#onMessages(batch)
743
732
  }
744
733
 
745
734
  async #requestShapeSSE(opts: {
@@ -750,6 +739,7 @@ export class ShapeStream<T extends Row<unknown> = Row>
750
739
  const { fetchUrl, requestAbortController, headers } = opts
751
740
  const fetch = this.#sseFetchClient
752
741
  try {
742
+ let buffer: Array<Message<T>> = []
753
743
  await fetchEventSource(fetchUrl.toString(), {
754
744
  headers,
755
745
  fetch,
@@ -759,11 +749,20 @@ export class ShapeStream<T extends Row<unknown> = Row>
759
749
  },
760
750
  onmessage: (event: EventSourceMessage) => {
761
751
  if (event.data) {
762
- // Process the SSE message
763
- // The event.data is a single JSON object, so we wrap it in an array
764
- const messages = `[${event.data}]`
752
+ // event.data is a single JSON object
765
753
  const schema = this.#schema! // we know that it is not undefined because it is set in onopen when we call this.#onInitialResponse
766
- this.#onMessages(messages, schema, true)
754
+ const message = this.#messageParser.parse<Message<T>>(
755
+ event.data,
756
+ schema
757
+ )
758
+ buffer.push(message)
759
+
760
+ if (isUpToDateMessage(message)) {
761
+ // Flush the buffer on up-to-date message.
762
+ // Ensures that we only process complete batches of operations.
763
+ this.#onMessages(buffer, true)
764
+ buffer = []
765
+ }
767
766
  }
768
767
  },
769
768
  onerror: (error: Error) => {
package/src/fetch.ts CHANGED
@@ -38,8 +38,7 @@ export const BackoffDefaults = {
38
38
 
39
39
  export function createFetchWithBackoff(
40
40
  fetchClient: typeof fetch,
41
- backoffOptions: BackoffOptions = BackoffDefaults,
42
- sseMode: boolean = false
41
+ backoffOptions: BackoffOptions = BackoffDefaults
43
42
  ): typeof fetch {
44
43
  const {
45
44
  initialDelay,
@@ -67,12 +66,6 @@ export function createFetchWithBackoff(
67
66
  if (result.ok) return result
68
67
 
69
68
  const err = await FetchError.fromResponse(result, url.toString())
70
- if (err.status === 409 && sseMode) {
71
- // The json body is [ { headers: { control: 'must-refetch' } } ] in normal mode
72
- // and is { headers: { control: 'must-refetch' } } in SSE mode
73
- // So in SSE mode we need to wrap it in an array
74
- err.json = [err.json]
75
- }
76
69
 
77
70
  throw err
78
71
  } catch (e) {
package/src/helpers.ts CHANGED
@@ -58,8 +58,9 @@ export function isUpToDateMessage<T extends Row<unknown> = Row>(
58
58
  * If we are not in SSE mode this function will return undefined.
59
59
  */
60
60
  export function getOffset(message: ControlMessage): Offset | undefined {
61
- const lsn = Number(message.headers.global_last_seen_lsn)
62
- if (lsn && !isNaN(lsn)) {
63
- return `${lsn}_0`
61
+ const lsn = message.headers.global_last_seen_lsn
62
+ if (!lsn) {
63
+ return
64
64
  }
65
+ return `${lsn}_0` as Offset
65
66
  }
package/src/parser.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { ColumnInfo, GetExtensions, Message, Row, Schema, Value } from './types'
1
+ import { ColumnInfo, GetExtensions, Row, Schema, Value } from './types'
2
2
  import { ParserNullValueError } from './error'
3
3
 
4
- type NullToken = null | `NULL`
5
- type Token = Exclude<string, NullToken>
6
- type NullableToken = Token | NullToken
4
+ type Token = string
5
+ type NullableToken = Token | null
7
6
  export type ParseFunction<Extensions = never> = (
8
7
  value: Token,
9
8
  additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
@@ -40,7 +39,7 @@ export const defaultParser: Parser = {
40
39
  // Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279
41
40
  export function pgArrayParser<Extensions>(
42
41
  value: Token,
43
- parser?: ParseFunction<Extensions>
42
+ parser?: NullableParseFunction<Extensions>
44
43
  ): Value<Extensions> {
45
44
  let i = 0
46
45
  let char = null
@@ -49,6 +48,12 @@ export function pgArrayParser<Extensions>(
49
48
  let last = 0
50
49
  let p: string | undefined = undefined
51
50
 
51
+ function extractValue(x: Token, start: number, end: number) {
52
+ let val: Token | null = x.slice(start, end)
53
+ val = val === `NULL` ? null : val
54
+ return parser ? parser(val) : val
55
+ }
56
+
52
57
  function loop(x: string): Array<Value<Extensions>> {
53
58
  const xs = []
54
59
  for (; i < x.length; i++) {
@@ -71,18 +76,16 @@ export function pgArrayParser<Extensions>(
71
76
  xs.push(loop(x))
72
77
  } else if (char === `}`) {
73
78
  quoted = false
74
- last < i &&
75
- xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))
79
+ last < i && xs.push(extractValue(x, last, i))
76
80
  last = i + 1
77
81
  break
78
82
  } else if (char === `,` && p !== `}` && p !== `"`) {
79
- xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))
83
+ xs.push(extractValue(x, last, i))
80
84
  last = i + 1
81
85
  }
82
86
  p = char
83
87
  }
84
- last < i &&
85
- xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1))
88
+ last < i && xs.push(xs.push(extractValue(x, last, i + 1)))
86
89
  return xs
87
90
  }
88
91
 
@@ -98,7 +101,7 @@ export class MessageParser<T extends Row<unknown>> {
98
101
  this.parser = { ...defaultParser, ...parser }
99
102
  }
100
103
 
101
- parse(messages: string, schema: Schema): Message<T>[] {
104
+ parse<Result>(messages: string, schema: Schema): Result {
102
105
  return JSON.parse(messages, (key, value) => {
103
106
  // typeof value === `object` && value !== null
104
107
  // is needed because there could be a column named `value`
@@ -117,7 +120,7 @@ export class MessageParser<T extends Row<unknown>> {
117
120
  })
118
121
  }
119
122
  return value
120
- }) as Message<T>[]
123
+ }) as Result
121
124
  }
122
125
 
123
126
  // Parses the message values using the provided parser based on the schema information
@@ -166,7 +169,7 @@ function makeNullableParser<Extensions>(
166
169
  // but if the column value is an array that contains a NULL value
167
170
  // then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
168
171
  return (value: NullableToken) => {
169
- if (isPgNull(value)) {
172
+ if (value === null) {
170
173
  if (!isNullable) {
171
174
  throw new ParserNullValueError(columnName ?? `unknown`)
172
175
  }
@@ -175,7 +178,3 @@ function makeNullableParser<Extensions>(
175
178
  return parser(value, columnInfo)
176
179
  }
177
180
  }
178
-
179
- function isPgNull(value: NullableToken): value is NullToken {
180
- return value === null || value === `NULL`
181
- }
package/src/types.ts CHANGED
@@ -17,7 +17,7 @@ export type Row<Extensions = never> = Record<string, Value<Extensions>>
17
17
  export type GetExtensions<T extends Row<unknown>> =
18
18
  T extends Row<infer Extensions> ? Extensions : never
19
19
 
20
- export type Offset = `-1` | `${number}_${number}`
20
+ export type Offset = `-1` | `${number}_${number}` | `${bigint}_${number}`
21
21
 
22
22
  interface Header {
23
23
  [key: Exclude<string, `operation` | `control`>]: Value