@electric-sql/client 1.0.13 → 1.1.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/src/client.ts CHANGED
@@ -9,13 +9,7 @@ import {
9
9
  SnapshotMetadata,
10
10
  } from './types'
11
11
  import { MessageParser, Parser, TransformFunction } from './parser'
12
- import {
13
- getOffset,
14
- isUpToDateMessage,
15
- isChangeMessage,
16
- applySubdomainSharding,
17
- ShardSubdomainOption,
18
- } from './helpers'
12
+ import { getOffset, isUpToDateMessage, isChangeMessage } from './helpers'
19
13
  import {
20
14
  FetchError,
21
15
  FetchBackoffAbortError,
@@ -50,6 +44,7 @@ import {
50
44
  FORCE_DISCONNECT_AND_REFRESH,
51
45
  PAUSE_STREAM,
52
46
  EXPERIMENTAL_LIVE_SSE_QUERY_PARAM,
47
+ LIVE_SSE_QUERY_PARAM,
53
48
  ELECTRIC_PROTOCOL_QUERY_PARAMS,
54
49
  LOG_MODE_QUERY_PARAM,
55
50
  SUBSET_PARAM_WHERE,
@@ -279,51 +274,19 @@ export interface ShapeStreamOptions<T = never> {
279
274
  subscribe?: boolean
280
275
 
281
276
  /**
282
- * Experimental support for Server-Sent Events (SSE) for live updates.
277
+ * @deprecated No longer experimental, use {@link liveSse} instead.
283
278
  */
284
279
  experimentalLiveSse?: boolean
285
280
 
286
281
  /**
287
- * Initial data loading mode
282
+ * Use Server-Sent Events (SSE) for live updates.
288
283
  */
289
- log?: LogMode
284
+ liveSse?: boolean
290
285
 
291
286
  /**
292
- * Enable subdomain sharding to bypass browser HTTP/1.1 connection limits.
293
- * This is useful in local development and is enabled by default for localhost URLs.
294
- *
295
- * See https://electric-sql.com/docs/guides/troubleshooting#slow-shapes-mdash-why-are-my-shapes-slow-in-the-browser-in-local-development
296
- *
297
- * When sharded, each shape stream gets a unique subdomain (e.g., `a7f2c.localhost`),
298
- * which bypasses the browser HTTP/1.1 connection limits. This avoids the need to serve
299
- * the development server over HTTP/2 (and thus HTTPS) in development.
300
- *
301
- * Options:
302
- * - `'localhost'` - Automatically shard `localhost` and `*.localhost` URLs (the default)
303
- * - `'always'` - Shard URLs regardless of the hostname
304
- * - `'never'` - Disable sharding
305
- * - `true` - Alias for `'always'`
306
- * - `false` - Alias for `'never'`
307
- *
308
- * @default 'localhost'
309
- *
310
- * @example
311
- * { url: 'http://localhost:3000/v1/shape', shardSubdomain: 'localhost' }
312
- * // → http://a1c2f.localhost:3000/v1/shape
313
- *
314
- * @example
315
- * { url: 'https://api.example.com', shardSubdomain: 'localhost' }
316
- * // → https://api.example.com
317
- *
318
- * @example
319
- * { url: 'https://localhost:3000', shardSubdomain: 'never' }
320
- * // → https://localhost:3000
321
- *
322
- * @example
323
- * { url: 'https://api.example.com', shardSubdomain: 'always' }
324
- * // → https://b2d3g.api.example.com
287
+ * Initial data loading mode
325
288
  */
326
- shardSubdomain?: ShardSubdomainOption
289
+ log?: LogMode
327
290
 
328
291
  signal?: AbortSignal
329
292
  fetchClient?: typeof fetch
@@ -415,7 +378,7 @@ function canonicalShapeKey(url: URL): string {
415
378
  * ```
416
379
  * const stream = new ShapeStream({
417
380
  * url: `http://localhost:3000/v1/shape`,
418
- * experimentalLiveSse: true
381
+ * liveSse: true
419
382
  * })
420
383
  * ```
421
384
  *
@@ -482,10 +445,6 @@ export class ShapeStream<T extends Row<unknown> = Row>
482
445
  constructor(options: ShapeStreamOptions<GetExtensions<T>>) {
483
446
  this.options = { subscribe: true, ...options }
484
447
  validateOptions(this.options)
485
- this.options.url = applySubdomainSharding(
486
- this.options.url,
487
- this.options.shardSubdomain
488
- )
489
448
  this.#lastOffset = this.options.offset ?? `-1`
490
449
  this.#liveCacheBuster = ``
491
450
  this.#shapeHandle = this.options.handle
@@ -645,7 +604,13 @@ export class ShapeStream<T extends Row<unknown> = Row>
645
604
  const newShapeHandle =
646
605
  e.headers[SHAPE_HANDLE_HEADER] || `${this.#shapeHandle!}-next`
647
606
  this.#reset(newShapeHandle)
648
- await this.#publish(e.json as Message<T>[])
607
+
608
+ // must refetch control message might be in a list or not depending
609
+ // on whether it came from an SSE request or long poll - handle both
610
+ // cases for safety here but worth revisiting 409 handling
611
+ await this.#publish(
612
+ (Array.isArray(e.json) ? e.json : [e.json]) as Message<T>[]
613
+ )
649
614
  return this.#requestShape()
650
615
  } else {
651
616
  // Notify subscribers
@@ -862,13 +827,15 @@ export class ShapeStream<T extends Row<unknown> = Row>
862
827
  headers: Record<string, string>
863
828
  resumingFromPause?: boolean
864
829
  }): Promise<void> {
830
+ const useSse = this.options.liveSse ?? this.options.experimentalLiveSse
865
831
  if (
866
832
  this.#isUpToDate &&
867
- this.options.experimentalLiveSse &&
833
+ useSse &&
868
834
  !this.#isRefreshing &&
869
835
  !opts.resumingFromPause
870
836
  ) {
871
837
  opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`)
838
+ opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`)
872
839
  return this.#requestShapeSSE(opts)
873
840
  }
874
841
 
package/src/constants.ts CHANGED
@@ -13,7 +13,11 @@ export const TABLE_QUERY_PARAM = `table`
13
13
  export const WHERE_QUERY_PARAM = `where`
14
14
  export const REPLICA_PARAM = `replica`
15
15
  export const WHERE_PARAMS_PARAM = `params`
16
+ /**
17
+ * @deprecated Use {@link LIVE_SSE_QUERY_PARAM} instead.
18
+ */
16
19
  export const EXPERIMENTAL_LIVE_SSE_QUERY_PARAM = `experimental_live_sse`
20
+ export const LIVE_SSE_QUERY_PARAM = `live_sse`
17
21
  export const FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`
18
22
  export const PAUSE_STREAM = `pause-stream`
19
23
  export const LOG_MODE_QUERY_PARAM = `log`
package/src/fetch.ts CHANGED
@@ -59,13 +59,7 @@ export function createFetchWithBackoff(
59
59
  let delay = initialDelay
60
60
  let attempt = 0
61
61
 
62
- /* eslint-disable no-constant-condition -- we re-fetch the shape log
63
- * continuously until we get a non-ok response. For recoverable errors,
64
- * we retry the fetch with exponential backoff. Users can pass in an
65
- * AbortController to abort the fetching an any point.
66
- * */
67
62
  while (true) {
68
- /* eslint-enable no-constant-condition */
69
63
  try {
70
64
  const result = await fetchClient(...args)
71
65
  if (result.ok) return result
@@ -118,6 +112,10 @@ export function createFetchWithConsumedMessages(fetchClient: typeof fetch) {
118
112
  const text = await res.text()
119
113
  return new Response(text, res)
120
114
  } catch (err) {
115
+ if (args[1]?.signal?.aborted) {
116
+ throw new FetchBackoffAbortError()
117
+ }
118
+
121
119
  throw new FetchError(
122
120
  res.status,
123
121
  undefined,
package/src/helpers.ts CHANGED
@@ -97,41 +97,3 @@ export function isVisibleInSnapshot(
97
97
 
98
98
  return xid < xmin || (xid < xmax && !xip.includes(xid))
99
99
  }
100
-
101
- export function generateShardId(): string {
102
- return Math.floor(Math.random() * 0xfffff)
103
- .toString(16)
104
- .padStart(5, `0`)
105
- }
106
-
107
- export function isLocalhostUrl(url: URL): boolean {
108
- const hostname = url.hostname.toLowerCase()
109
- return hostname === `localhost` || hostname.endsWith(`.localhost`)
110
- }
111
-
112
- export type ShardSubdomainOption = `always` | `localhost` | `never` | boolean
113
-
114
- export function applySubdomainSharding(
115
- originalUrl: string,
116
- option: ShardSubdomainOption = `localhost`
117
- ): string {
118
- if (option === `never` || option === false) {
119
- return originalUrl
120
- }
121
-
122
- const url = new URL(originalUrl)
123
-
124
- const shouldShard =
125
- option === `always` ||
126
- option === true ||
127
- (option === `localhost` && isLocalhostUrl(url))
128
-
129
- if (!shouldShard) {
130
- return originalUrl
131
- }
132
-
133
- const shardId = generateShardId()
134
- url.hostname = `${shardId}.${url.hostname}`
135
-
136
- return url.toString()
137
- }
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@ export {
5
5
  isChangeMessage,
6
6
  isControlMessage,
7
7
  isVisibleInSnapshot,
8
- type ShardSubdomainOption,
9
8
  } from './helpers'
10
9
  export { FetchError } from './error'
11
10
  export { type BackoffOptions, BackoffDefaults } from './fetch'