@electric-sql/client 0.7.2 → 0.8.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/README.md CHANGED
@@ -93,4 +93,79 @@ shape.subscribe(({ rows }) => {
93
93
  }
94
94
  ```
95
95
 
96
- See the [Docs](https://electric-sql.com) and [Examples](https://electric-sql.com/examples/basic) for more information.
96
+ ### Error Handling
97
+
98
+ The ShapeStream provides two ways to handle errors:
99
+
100
+ 1. Using the `onError` handler:
101
+ ```typescript
102
+ const stream = new ShapeStream({
103
+ url: `${BASE_URL}/v1/shape`,
104
+ table: `foo`,
105
+ onError: (error) => {
106
+ // Handle all stream errors here
107
+ console.error('Stream error:', error)
108
+ }
109
+ })
110
+ ```
111
+
112
+ If no `onError` handler is provided, the ShapeStream will throw errors that occur during streaming.
113
+
114
+ 2. Individual subscribers can optionally handle errors specific to their subscription:
115
+ ```typescript
116
+ stream.subscribe(
117
+ (messages) => {
118
+ // Handle messages
119
+ },
120
+ (error) => {
121
+ // Handle errors for this specific subscription
122
+ console.error('Subscription error:', error)
123
+ }
124
+ )
125
+ ```
126
+
127
+ Common error types include:
128
+ - `MissingShapeUrlError`: Missing required URL parameter
129
+ - `InvalidSignalError`: Invalid AbortSignal instance
130
+ - `ReservedParamError`: Using reserved parameter names
131
+
132
+ Runtime errors:
133
+ - `FetchError`: HTTP errors during shape fetching
134
+ - `FetchBackoffAbortError`: Fetch aborted using AbortSignal
135
+ - `MissingShapeHandleError`: Missing required shape handle
136
+ - `ParserNullValueError`: Parser encountered NULL value in a column that doesn't allow NULL values
137
+
138
+ See the [typescript client docs on the website](https://electric-sql.com/docs/api/clients/typescript#error-handling) for more details on error handling.
139
+
140
+ And in general, see the [docs website](https://electric-sql.com) and [examples folder](https://electric-sql.com/examples/basic) for more information.
141
+
142
+ ## Develop
143
+
144
+ Install the pnpm workspace at the repo root:
145
+
146
+ ```shell
147
+ pnpm install
148
+ ```
149
+
150
+ Build the package:
151
+
152
+ ```shell
153
+ cd packages/typescript-client
154
+ pnpm build
155
+ ```
156
+
157
+ ## Test
158
+
159
+ In one terminal, start the backend running:
160
+
161
+ ```shell
162
+ cd ../sync-service
163
+ mix deps.get
164
+ mix stop_dev && mix compile && mix start_dev && ies -S mix
165
+ ```
166
+
167
+ Then in this folder:
168
+
169
+ ```shell
170
+ pnpm test
171
+ ```
@@ -85,6 +85,91 @@ __export(src_exports, {
85
85
  });
86
86
  module.exports = __toCommonJS(src_exports);
87
87
 
88
+ // src/error.ts
89
+ var FetchError = class _FetchError extends Error {
90
+ constructor(status, text, json, headers, url, message) {
91
+ super(
92
+ message || `HTTP Error ${status} at ${url}: ${text != null ? text : JSON.stringify(json)}`
93
+ );
94
+ this.url = url;
95
+ this.name = `FetchError`;
96
+ this.status = status;
97
+ this.text = text;
98
+ this.json = json;
99
+ this.headers = headers;
100
+ }
101
+ static fromResponse(response, url) {
102
+ return __async(this, null, function* () {
103
+ const status = response.status;
104
+ const headers = Object.fromEntries([...response.headers.entries()]);
105
+ let text = void 0;
106
+ let json = void 0;
107
+ const contentType = response.headers.get(`content-type`);
108
+ if (contentType && contentType.includes(`application/json`)) {
109
+ json = yield response.json();
110
+ } else {
111
+ text = yield response.text();
112
+ }
113
+ return new _FetchError(status, text, json, headers, url);
114
+ });
115
+ }
116
+ };
117
+ var FetchBackoffAbortError = class extends Error {
118
+ constructor() {
119
+ super(`Fetch with backoff aborted`);
120
+ this.name = `FetchBackoffAbortError`;
121
+ }
122
+ };
123
+ var MissingShapeUrlError = class extends Error {
124
+ constructor() {
125
+ super(`Invalid shape options: missing required url parameter`);
126
+ this.name = `MissingShapeUrlError`;
127
+ }
128
+ };
129
+ var InvalidSignalError = class extends Error {
130
+ constructor() {
131
+ super(`Invalid signal option. It must be an instance of AbortSignal.`);
132
+ this.name = `InvalidSignalError`;
133
+ }
134
+ };
135
+ var MissingShapeHandleError = class extends Error {
136
+ constructor() {
137
+ super(
138
+ `shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
139
+ );
140
+ this.name = `MissingShapeHandleError`;
141
+ }
142
+ };
143
+ var ReservedParamError = class extends Error {
144
+ constructor(reservedParams) {
145
+ super(
146
+ `Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
147
+ );
148
+ this.name = `ReservedParamError`;
149
+ }
150
+ };
151
+ var ParserNullValueError = class extends Error {
152
+ constructor(columnName) {
153
+ super(`Column "${columnName != null ? columnName : `unknown`}" does not allow NULL values`);
154
+ this.name = `ParserNullValueError`;
155
+ }
156
+ };
157
+ var MissingHeadersError = class extends Error {
158
+ constructor(url, missingHeaders) {
159
+ let msg = `The response for the shape request to ${url} didn't include the following required headers:
160
+ `;
161
+ missingHeaders.forEach((h) => {
162
+ msg += `- ${h}
163
+ `;
164
+ });
165
+ msg += `
166
+ This is often due to a proxy not setting CORS correctly so that all Electric headers can be read by the client.`;
167
+ msg += `
168
+ For more information visit the troubleshooting guide: /docs/guides/troubleshooting/missing-headers`;
169
+ super(msg);
170
+ }
171
+ };
172
+
88
173
  // src/parser.ts
89
174
  var parseNumber = (value) => Number(value);
90
175
  var parseBool = (value) => value === `true` || value === `t`;
@@ -186,7 +271,7 @@ function makeNullableParser(parser, columnInfo, columnName) {
186
271
  return (value) => {
187
272
  if (isPgNull(value)) {
188
273
  if (!isNullable) {
189
- throw new Error(`Column ${columnName != null ? columnName : `unknown`} is not nullable`);
274
+ throw new ParserNullValueError(columnName != null ? columnName : `unknown`);
190
275
  }
191
276
  return null;
192
277
  }
@@ -208,41 +293,6 @@ function isUpToDateMessage(message) {
208
293
  return isControlMessage(message) && message.headers.control === `up-to-date`;
209
294
  }
210
295
 
211
- // src/error.ts
212
- var FetchError = class _FetchError extends Error {
213
- constructor(status, text, json, headers, url, message) {
214
- super(
215
- message || `HTTP Error ${status} at ${url}: ${text != null ? text : JSON.stringify(json)}`
216
- );
217
- this.url = url;
218
- this.name = `FetchError`;
219
- this.status = status;
220
- this.text = text;
221
- this.json = json;
222
- this.headers = headers;
223
- }
224
- static fromResponse(response, url) {
225
- return __async(this, null, function* () {
226
- const status = response.status;
227
- const headers = Object.fromEntries([...response.headers.entries()]);
228
- let text = void 0;
229
- let json = void 0;
230
- const contentType = response.headers.get(`content-type`);
231
- if (contentType && contentType.includes(`application/json`)) {
232
- json = yield response.json();
233
- } else {
234
- text = yield response.text();
235
- }
236
- return new _FetchError(status, text, json, headers, url);
237
- });
238
- }
239
- };
240
- var FetchBackoffAbortError = class extends Error {
241
- constructor() {
242
- super(`Fetch with backoff aborted`);
243
- }
244
- };
245
-
246
296
  // src/constants.ts
247
297
  var LIVE_CACHE_BUSTER_HEADER = `electric-cursor`;
248
298
  var SHAPE_HANDLE_HEADER = `electric-handle`;
@@ -330,6 +380,36 @@ function createFetchWithChunkBuffer(fetchClient, prefetchOptions = ChunkPrefetch
330
380
  });
331
381
  return prefetchClient;
332
382
  }
383
+ var requiredElectricResponseHeaders = [
384
+ `electric-offset`,
385
+ `electric-handle`
386
+ ];
387
+ var requiredLiveResponseHeaders = [`electric-cursor`];
388
+ var requiredNonLiveResponseHeaders = [`electric-schema`];
389
+ function createFetchWithResponseHeadersCheck(fetchClient) {
390
+ return (...args) => __async(this, null, function* () {
391
+ const response = yield fetchClient(...args);
392
+ if (response.ok) {
393
+ const headers = response.headers;
394
+ const missingHeaders = [];
395
+ const addMissingHeaders = (requiredHeaders) => missingHeaders.push(...requiredHeaders.filter((h) => !headers.has(h)));
396
+ addMissingHeaders(requiredElectricResponseHeaders);
397
+ const input = args[0];
398
+ const urlString = input.toString();
399
+ const url = new URL(urlString);
400
+ if (url.searchParams.has(LIVE_QUERY_PARAM, `true`)) {
401
+ addMissingHeaders(requiredLiveResponseHeaders);
402
+ }
403
+ if (!url.searchParams.has(LIVE_QUERY_PARAM) || url.searchParams.has(LIVE_QUERY_PARAM, `false`)) {
404
+ addMissingHeaders(requiredNonLiveResponseHeaders);
405
+ }
406
+ if (missingHeaders.length > 0) {
407
+ throw new MissingHeadersError(urlString, missingHeaders);
408
+ }
409
+ }
410
+ return response;
411
+ });
412
+ }
333
413
  var _fetchClient, _maxPrefetchedRequests, _prefetchQueue, _queueHeadUrl, _queueTailUrl, _PrefetchQueue_instances, prefetch_fn;
334
414
  var PrefetchQueue = class {
335
415
  constructor(options) {
@@ -429,14 +509,14 @@ var RESERVED_PARAMS = /* @__PURE__ */ new Set([
429
509
  WHERE_QUERY_PARAM,
430
510
  REPLICA_PARAM
431
511
  ]);
432
- var _fetchClient2, _messageParser, _subscribers, _upToDateSubscribers, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _connected, _shapeHandle, _databaseId, _schema, _error, _replica, _ShapeStream_instances, publish_fn, sendErrorToSubscribers_fn, notifyUpToDateSubscribers_fn, sendErrorToUpToDateSubscribers_fn, reset_fn;
512
+ var _error, _fetchClient2, _messageParser, _subscribers, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _connected, _shapeHandle, _databaseId, _schema, _onError, _replica, _ShapeStream_instances, start_fn, publish_fn, sendErrorToSubscribers_fn, reset_fn;
433
513
  var _ShapeStream = class _ShapeStream {
434
514
  constructor(options) {
435
515
  __privateAdd(this, _ShapeStream_instances);
516
+ __privateAdd(this, _error, null);
436
517
  __privateAdd(this, _fetchClient2);
437
518
  __privateAdd(this, _messageParser);
438
519
  __privateAdd(this, _subscribers, /* @__PURE__ */ new Map());
439
- __privateAdd(this, _upToDateSubscribers, /* @__PURE__ */ new Map());
440
520
  __privateAdd(this, _lastOffset);
441
521
  __privateAdd(this, _liveCacheBuster);
442
522
  // Seconds since our Electric Epoch 😎
@@ -447,17 +527,18 @@ var _ShapeStream = class _ShapeStream {
447
527
  __privateAdd(this, _shapeHandle);
448
528
  __privateAdd(this, _databaseId);
449
529
  __privateAdd(this, _schema);
450
- __privateAdd(this, _error);
530
+ __privateAdd(this, _onError);
451
531
  __privateAdd(this, _replica);
452
532
  var _a, _b, _c;
453
- validateOptions(options);
454
533
  this.options = __spreadValues({ subscribe: true }, options);
534
+ validateOptions(this.options);
455
535
  __privateSet(this, _lastOffset, (_a = this.options.offset) != null ? _a : `-1`);
456
536
  __privateSet(this, _liveCacheBuster, ``);
457
- __privateSet(this, _shapeHandle, this.options.shapeHandle);
537
+ __privateSet(this, _shapeHandle, this.options.handle);
458
538
  __privateSet(this, _databaseId, this.options.databaseId);
459
539
  __privateSet(this, _messageParser, new MessageParser(options.parser));
460
540
  __privateSet(this, _replica, this.options.replica);
541
+ __privateSet(this, _onError, this.options.onError);
461
542
  const baseFetchClient = (_b = options.fetchClient) != null ? _b : (...args) => fetch(...args);
462
543
  const fetchWithBackoffClient = createFetchWithBackoff(baseFetchClient, __spreadProps(__spreadValues({}, (_c = options.backoffOptions) != null ? _c : BackoffDefaults), {
463
544
  onFailedAttempt: () => {
@@ -466,128 +547,25 @@ var _ShapeStream = class _ShapeStream {
466
547
  (_b2 = (_a2 = options.backoffOptions) == null ? void 0 : _a2.onFailedAttempt) == null ? void 0 : _b2.call(_a2);
467
548
  }
468
549
  }));
469
- __privateSet(this, _fetchClient2, createFetchWithChunkBuffer(fetchWithBackoffClient));
470
- this.start();
550
+ __privateSet(this, _fetchClient2, createFetchWithResponseHeadersCheck(
551
+ createFetchWithChunkBuffer(fetchWithBackoffClient)
552
+ ));
553
+ __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
471
554
  }
472
555
  get shapeHandle() {
473
556
  return __privateGet(this, _shapeHandle);
474
557
  }
475
- get isUpToDate() {
476
- return __privateGet(this, _isUpToDate);
477
- }
478
558
  get error() {
479
559
  return __privateGet(this, _error);
480
560
  }
481
- start() {
482
- return __async(this, null, function* () {
483
- var _a, _b;
484
- __privateSet(this, _isUpToDate, false);
485
- const { url, table, where, columns, signal } = this.options;
486
- try {
487
- while (!(signal == null ? void 0 : signal.aborted) && !__privateGet(this, _isUpToDate) || this.options.subscribe) {
488
- const fetchUrl = new URL(url);
489
- if (this.options.params) {
490
- const reservedParams = Object.keys(this.options.params).filter(
491
- (key) => RESERVED_PARAMS.has(key)
492
- );
493
- if (reservedParams.length > 0) {
494
- throw new Error(
495
- `Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
496
- );
497
- }
498
- for (const [key, value] of Object.entries(this.options.params)) {
499
- fetchUrl.searchParams.set(key, value);
500
- }
501
- }
502
- if (table) fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table);
503
- if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where);
504
- if (columns && columns.length > 0)
505
- fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`));
506
- fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
507
- if (__privateGet(this, _isUpToDate)) {
508
- fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
509
- fetchUrl.searchParams.set(
510
- LIVE_CACHE_BUSTER_QUERY_PARAM,
511
- __privateGet(this, _liveCacheBuster)
512
- );
513
- }
514
- if (__privateGet(this, _shapeHandle)) {
515
- fetchUrl.searchParams.set(
516
- SHAPE_HANDLE_QUERY_PARAM,
517
- __privateGet(this, _shapeHandle)
518
- );
519
- }
520
- if (__privateGet(this, _databaseId)) {
521
- fetchUrl.searchParams.set(DATABASE_ID_QUERY_PARAM, __privateGet(this, _databaseId));
522
- }
523
- if (((_a = __privateGet(this, _replica)) != null ? _a : _ShapeStream.Replica.DEFAULT) != _ShapeStream.Replica.DEFAULT) {
524
- fetchUrl.searchParams.set(REPLICA_PARAM, __privateGet(this, _replica));
525
- }
526
- let response;
527
- try {
528
- response = yield __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
529
- signal,
530
- headers: this.options.headers
531
- });
532
- __privateSet(this, _connected, true);
533
- } catch (e) {
534
- if (e instanceof FetchBackoffAbortError) break;
535
- if (!(e instanceof FetchError)) throw e;
536
- if (e.status == 409) {
537
- const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER];
538
- __privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
539
- yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
540
- continue;
541
- } else if (e.status >= 400 && e.status < 500) {
542
- __privateMethod(this, _ShapeStream_instances, sendErrorToUpToDateSubscribers_fn).call(this, e);
543
- __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
544
- throw e;
545
- }
546
- }
547
- const { headers, status } = response;
548
- const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
549
- if (shapeHandle) {
550
- __privateSet(this, _shapeHandle, shapeHandle);
551
- }
552
- const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER);
553
- if (lastOffset) {
554
- __privateSet(this, _lastOffset, lastOffset);
555
- }
556
- const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER);
557
- if (liveCacheBuster) {
558
- __privateSet(this, _liveCacheBuster, liveCacheBuster);
559
- }
560
- const getSchema = () => {
561
- const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
562
- return schemaHeader ? JSON.parse(schemaHeader) : {};
563
- };
564
- __privateSet(this, _schema, (_b = __privateGet(this, _schema)) != null ? _b : getSchema());
565
- const messages = status === 204 ? `[]` : yield response.text();
566
- if (status === 204) {
567
- __privateSet(this, _lastSyncedAt, Date.now());
568
- }
569
- const batch = __privateGet(this, _messageParser).parse(messages, __privateGet(this, _schema));
570
- if (batch.length > 0) {
571
- const prevUpToDate = __privateGet(this, _isUpToDate);
572
- const lastMessage = batch[batch.length - 1];
573
- if (isUpToDateMessage(lastMessage)) {
574
- __privateSet(this, _lastSyncedAt, Date.now());
575
- __privateSet(this, _isUpToDate, true);
576
- }
577
- yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, batch);
578
- if (!prevUpToDate && __privateGet(this, _isUpToDate)) {
579
- __privateMethod(this, _ShapeStream_instances, notifyUpToDateSubscribers_fn).call(this);
580
- }
581
- }
582
- }
583
- } catch (err) {
584
- __privateSet(this, _error, err);
585
- } finally {
586
- __privateSet(this, _connected, false);
587
- }
588
- });
561
+ get isUpToDate() {
562
+ return __privateGet(this, _isUpToDate);
563
+ }
564
+ get lastOffset() {
565
+ return __privateGet(this, _lastOffset);
589
566
  }
590
- subscribe(callback, onError) {
567
+ subscribe(callback, onError = () => {
568
+ }) {
591
569
  const subscriptionId = Math.random();
592
570
  __privateGet(this, _subscribers).set(subscriptionId, [callback, onError]);
593
571
  return () => {
@@ -597,16 +575,6 @@ var _ShapeStream = class _ShapeStream {
597
575
  unsubscribeAll() {
598
576
  __privateGet(this, _subscribers).clear();
599
577
  }
600
- subscribeOnceToUpToDate(callback, error) {
601
- const subscriptionId = Math.random();
602
- __privateGet(this, _upToDateSubscribers).set(subscriptionId, [callback, error]);
603
- return () => {
604
- __privateGet(this, _upToDateSubscribers).delete(subscriptionId);
605
- };
606
- }
607
- unsubscribeAllUpToDateSubscribers() {
608
- __privateGet(this, _upToDateSubscribers).clear();
609
- }
610
578
  /** Unix time at which we last synced. Undefined when `isLoading` is true. */
611
579
  lastSyncedAt() {
612
580
  return __privateGet(this, _lastSyncedAt);
@@ -622,13 +590,13 @@ var _ShapeStream = class _ShapeStream {
622
590
  }
623
591
  /** True during initial fetch. False afterwise. */
624
592
  isLoading() {
625
- return !this.isUpToDate;
593
+ return !__privateGet(this, _isUpToDate);
626
594
  }
627
595
  };
596
+ _error = new WeakMap();
628
597
  _fetchClient2 = new WeakMap();
629
598
  _messageParser = new WeakMap();
630
599
  _subscribers = new WeakMap();
631
- _upToDateSubscribers = new WeakMap();
632
600
  _lastOffset = new WeakMap();
633
601
  _liveCacheBuster = new WeakMap();
634
602
  _lastSyncedAt = new WeakMap();
@@ -637,9 +605,127 @@ _connected = new WeakMap();
637
605
  _shapeHandle = new WeakMap();
638
606
  _databaseId = new WeakMap();
639
607
  _schema = new WeakMap();
640
- _error = new WeakMap();
608
+ _onError = new WeakMap();
641
609
  _replica = new WeakMap();
642
610
  _ShapeStream_instances = new WeakSet();
611
+ start_fn = function() {
612
+ return __async(this, null, function* () {
613
+ var _a, _b, _c;
614
+ try {
615
+ while (!((_a = this.options.signal) == null ? void 0 : _a.aborted) && !__privateGet(this, _isUpToDate) || this.options.subscribe) {
616
+ const { url, table, where, columns, signal } = this.options;
617
+ const fetchUrl = new URL(url);
618
+ if (this.options.params) {
619
+ const reservedParams = Object.keys(this.options.params).filter(
620
+ (key) => RESERVED_PARAMS.has(key)
621
+ );
622
+ if (reservedParams.length > 0) {
623
+ throw new Error(
624
+ `Cannot use reserved Electric parameter names in custom params: ${reservedParams.join(`, `)}`
625
+ );
626
+ }
627
+ for (const [key, value] of Object.entries(this.options.params)) {
628
+ fetchUrl.searchParams.set(key, value);
629
+ }
630
+ }
631
+ if (table) fetchUrl.searchParams.set(TABLE_QUERY_PARAM, table);
632
+ if (where) fetchUrl.searchParams.set(WHERE_QUERY_PARAM, where);
633
+ if (columns && columns.length > 0)
634
+ fetchUrl.searchParams.set(COLUMNS_QUERY_PARAM, columns.join(`,`));
635
+ fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
636
+ if (__privateGet(this, _isUpToDate)) {
637
+ fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
638
+ fetchUrl.searchParams.set(
639
+ LIVE_CACHE_BUSTER_QUERY_PARAM,
640
+ __privateGet(this, _liveCacheBuster)
641
+ );
642
+ }
643
+ if (__privateGet(this, _shapeHandle)) {
644
+ fetchUrl.searchParams.set(
645
+ SHAPE_HANDLE_QUERY_PARAM,
646
+ __privateGet(this, _shapeHandle)
647
+ );
648
+ }
649
+ if (__privateGet(this, _databaseId)) {
650
+ fetchUrl.searchParams.set(DATABASE_ID_QUERY_PARAM, __privateGet(this, _databaseId));
651
+ }
652
+ if (((_b = __privateGet(this, _replica)) != null ? _b : _ShapeStream.Replica.DEFAULT) != _ShapeStream.Replica.DEFAULT) {
653
+ fetchUrl.searchParams.set(REPLICA_PARAM, __privateGet(this, _replica));
654
+ }
655
+ let response;
656
+ try {
657
+ response = yield __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
658
+ signal,
659
+ headers: this.options.headers
660
+ });
661
+ __privateSet(this, _connected, true);
662
+ } catch (e) {
663
+ if (e instanceof FetchBackoffAbortError) break;
664
+ if (!(e instanceof FetchError)) throw e;
665
+ if (e.status == 409) {
666
+ const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER];
667
+ __privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
668
+ yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
669
+ continue;
670
+ } else if (e.status >= 400 && e.status < 500) {
671
+ __privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
672
+ throw e;
673
+ }
674
+ }
675
+ const { headers, status } = response;
676
+ const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
677
+ if (shapeHandle) {
678
+ __privateSet(this, _shapeHandle, shapeHandle);
679
+ }
680
+ const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER);
681
+ if (lastOffset) {
682
+ __privateSet(this, _lastOffset, lastOffset);
683
+ }
684
+ const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER);
685
+ if (liveCacheBuster) {
686
+ __privateSet(this, _liveCacheBuster, liveCacheBuster);
687
+ }
688
+ const getSchema = () => {
689
+ const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
690
+ return schemaHeader ? JSON.parse(schemaHeader) : {};
691
+ };
692
+ __privateSet(this, _schema, (_c = __privateGet(this, _schema)) != null ? _c : getSchema());
693
+ const messages = status === 204 ? `[]` : yield response.text();
694
+ if (status === 204) {
695
+ __privateSet(this, _lastSyncedAt, Date.now());
696
+ }
697
+ const batch = __privateGet(this, _messageParser).parse(messages, __privateGet(this, _schema));
698
+ if (batch.length > 0) {
699
+ const lastMessage = batch[batch.length - 1];
700
+ if (isUpToDateMessage(lastMessage)) {
701
+ __privateSet(this, _lastSyncedAt, Date.now());
702
+ __privateSet(this, _isUpToDate, true);
703
+ }
704
+ yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, batch);
705
+ }
706
+ }
707
+ } catch (err) {
708
+ __privateSet(this, _error, err);
709
+ if (__privateGet(this, _onError)) {
710
+ const retryOpts = yield __privateGet(this, _onError).call(this, err);
711
+ if (typeof retryOpts === `object`) {
712
+ __privateMethod(this, _ShapeStream_instances, reset_fn).call(this);
713
+ if (`params` in retryOpts) {
714
+ this.options.params = retryOpts.params;
715
+ }
716
+ if (`headers` in retryOpts) {
717
+ this.options.headers = retryOpts.headers;
718
+ }
719
+ __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
720
+ }
721
+ return;
722
+ }
723
+ throw err;
724
+ } finally {
725
+ __privateSet(this, _connected, false);
726
+ }
727
+ });
728
+ };
643
729
  publish_fn = function(messages) {
644
730
  return __async(this, null, function* () {
645
731
  yield Promise.all(
@@ -660,24 +746,14 @@ sendErrorToSubscribers_fn = function(error) {
660
746
  errorFn == null ? void 0 : errorFn(error);
661
747
  });
662
748
  };
663
- notifyUpToDateSubscribers_fn = function() {
664
- __privateGet(this, _upToDateSubscribers).forEach(([callback]) => {
665
- callback();
666
- });
667
- };
668
- sendErrorToUpToDateSubscribers_fn = function(error) {
669
- __privateGet(this, _upToDateSubscribers).forEach(
670
- ([_, errorCallback]) => errorCallback(error)
671
- );
672
- };
673
749
  /**
674
750
  * Resets the state of the stream, optionally with a provided
675
751
  * shape handle
676
752
  */
677
- reset_fn = function(shapeHandle) {
753
+ reset_fn = function(handle) {
678
754
  __privateSet(this, _lastOffset, `-1`);
679
755
  __privateSet(this, _liveCacheBuster, ``);
680
- __privateSet(this, _shapeHandle, shapeHandle);
756
+ __privateSet(this, _shapeHandle, handle);
681
757
  __privateSet(this, _isUpToDate, false);
682
758
  __privateSet(this, _connected, false);
683
759
  __privateSet(this, _schema, void 0);
@@ -689,17 +765,21 @@ _ShapeStream.Replica = {
689
765
  var ShapeStream = _ShapeStream;
690
766
  function validateOptions(options) {
691
767
  if (!options.url) {
692
- throw new Error(`Invalid shape options. It must provide the url`);
768
+ throw new MissingShapeUrlError();
693
769
  }
694
770
  if (options.signal && !(options.signal instanceof AbortSignal)) {
695
- throw new Error(
696
- `Invalid signal option. It must be an instance of AbortSignal.`
697
- );
771
+ throw new InvalidSignalError();
698
772
  }
699
- if (options.offset !== void 0 && options.offset !== `-1` && !options.shapeHandle) {
700
- throw new Error(
701
- `shapeHandle is required if this isn't an initial fetch (i.e. offset > -1)`
773
+ if (options.offset !== void 0 && options.offset !== `-1` && !options.handle) {
774
+ throw new MissingShapeHandleError();
775
+ }
776
+ if (options.params) {
777
+ const reservedParams = Object.keys(options.params).filter(
778
+ (key) => RESERVED_PARAMS.has(key)
702
779
  );
780
+ if (reservedParams.length > 0) {
781
+ throw new ReservedParamError(reservedParams);
782
+ }
703
783
  }
704
784
  return;
705
785
  }
@@ -719,19 +799,16 @@ var Shape = class {
719
799
  __privateMethod(this, _Shape_instances, process_fn).bind(this),
720
800
  __privateMethod(this, _Shape_instances, handleError_fn).bind(this)
721
801
  );
722
- const unsubscribe = __privateGet(this, _stream).subscribeOnceToUpToDate(
723
- () => {
724
- unsubscribe();
725
- },
726
- (e) => {
727
- __privateMethod(this, _Shape_instances, handleError_fn).call(this, e);
728
- throw e;
729
- }
730
- );
731
802
  }
732
803
  get isUpToDate() {
733
804
  return __privateGet(this, _stream).isUpToDate;
734
805
  }
806
+ get lastOffset() {
807
+ return __privateGet(this, _stream).lastOffset;
808
+ }
809
+ get handle() {
810
+ return __privateGet(this, _stream).shapeHandle;
811
+ }
735
812
  get rows() {
736
813
  return this.value.then((v) => Array.from(v.values()));
737
814
  }