@electric-sql/client 1.1.1 → 1.1.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 +74 -12
- package/dist/cjs/index.cjs +83 -65
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +45 -18
- package/dist/index.browser.mjs +3 -3
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +45 -18
- package/dist/index.legacy-esm.js +83 -65
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +83 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +142 -21
- package/src/constants.ts +1 -0
- package/src/fetch.ts +37 -88
package/README.md
CHANGED
|
@@ -99,26 +99,84 @@ shape.subscribe(({ rows }) => {
|
|
|
99
99
|
|
|
100
100
|
### Error Handling
|
|
101
101
|
|
|
102
|
-
The ShapeStream provides
|
|
102
|
+
The ShapeStream provides robust error handling with automatic retry support:
|
|
103
103
|
|
|
104
|
-
1.
|
|
104
|
+
#### 1. Stream-level error handler with retry control
|
|
105
|
+
|
|
106
|
+
The `onError` handler gives you full control over error recovery:
|
|
105
107
|
|
|
106
108
|
```typescript
|
|
107
109
|
const stream = new ShapeStream({
|
|
108
110
|
url: `${BASE_URL}/v1/shape`,
|
|
109
|
-
params: {
|
|
110
|
-
table: `foo`,
|
|
111
|
-
},
|
|
111
|
+
params: { table: `foo` },
|
|
112
112
|
onError: (error) => {
|
|
113
|
-
// Handle all stream errors here
|
|
114
113
|
console.error('Stream error:', error)
|
|
114
|
+
|
|
115
|
+
// IMPORTANT: Return an object to keep syncing!
|
|
116
|
+
// Return void/undefined to stop syncing permanently.
|
|
117
|
+
|
|
118
|
+
// Note: 5xx errors and network errors are automatically retried,
|
|
119
|
+
// so onError is mainly for handling client errors (4xx)
|
|
120
|
+
|
|
121
|
+
if (error instanceof FetchError) {
|
|
122
|
+
if (error.status === 401) {
|
|
123
|
+
// Unauthorized - refresh token and retry
|
|
124
|
+
const newToken = getRefreshedToken()
|
|
125
|
+
return {
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${newToken}`,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (error.status === 403) {
|
|
133
|
+
// Forbidden - maybe change user context
|
|
134
|
+
return {
|
|
135
|
+
params: {
|
|
136
|
+
table: `foo`,
|
|
137
|
+
where: `user_id = $1`,
|
|
138
|
+
params: [fallbackUserId],
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Stop syncing for other errors (return void)
|
|
115
145
|
},
|
|
116
146
|
})
|
|
117
147
|
```
|
|
118
148
|
|
|
119
|
-
|
|
149
|
+
**Critical**: The `onError` callback's return value controls whether syncing continues:
|
|
120
150
|
|
|
121
|
-
|
|
151
|
+
- **Return an object** (even empty `{}`) to retry syncing:
|
|
152
|
+
- `{}` - Retry with same params and headers
|
|
153
|
+
- `{ params }` - Retry with modified params
|
|
154
|
+
- `{ headers }` - Retry with modified headers
|
|
155
|
+
- `{ params, headers }` - Retry with both modified
|
|
156
|
+
- **Return void/undefined** to stop the stream permanently
|
|
157
|
+
|
|
158
|
+
The handler supports async operations:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
onError: async (error) => {
|
|
162
|
+
if (error instanceof FetchError && error.status === 401) {
|
|
163
|
+
// Perform async token refresh
|
|
164
|
+
const newToken = await refreshAuthToken()
|
|
165
|
+
return {
|
|
166
|
+
headers: { Authorization: `Bearer ${newToken}` },
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {} // Retry other errors
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Automatic retries**: The client automatically retries 5xx server errors, network errors, and 429 rate limits with exponential backoff. The `onError` callback is only invoked after these retries are exhausted, or for non-retryable errors like 4xx client errors.
|
|
174
|
+
|
|
175
|
+
**Without `onError`**: If no `onError` handler is provided, non-retryable errors (like 4xx client errors) will be thrown and the stream will stop.
|
|
176
|
+
|
|
177
|
+
#### 2. Subscription-level error callbacks
|
|
178
|
+
|
|
179
|
+
Individual subscribers can handle errors specific to their subscription:
|
|
122
180
|
|
|
123
181
|
```typescript
|
|
124
182
|
stream.subscribe(
|
|
@@ -132,7 +190,11 @@ stream.subscribe(
|
|
|
132
190
|
)
|
|
133
191
|
```
|
|
134
192
|
|
|
135
|
-
|
|
193
|
+
Note: Subscription error callbacks cannot control retry behavior - use the stream-level `onError` for that.
|
|
194
|
+
|
|
195
|
+
#### Common Error Types
|
|
196
|
+
|
|
197
|
+
Setup errors:
|
|
136
198
|
|
|
137
199
|
- `MissingShapeUrlError`: Missing required URL parameter
|
|
138
200
|
- `InvalidSignalError`: Invalid AbortSignal instance
|
|
@@ -140,12 +202,12 @@ Common error types include:
|
|
|
140
202
|
|
|
141
203
|
Runtime errors:
|
|
142
204
|
|
|
143
|
-
- `FetchError`: HTTP errors during shape fetching
|
|
205
|
+
- `FetchError`: HTTP errors during shape fetching (includes `status`, `url`, `headers`)
|
|
144
206
|
- `FetchBackoffAbortError`: Fetch aborted using AbortSignal
|
|
145
207
|
- `MissingShapeHandleError`: Missing required shape handle
|
|
146
|
-
- `ParserNullValueError`:
|
|
208
|
+
- `ParserNullValueError`: NULL value in a non-nullable column
|
|
147
209
|
|
|
148
|
-
See the [
|
|
210
|
+
See the [TypeScript client docs](https://electric-sql.com/docs/api/clients/typescript#error-handling) for more details.
|
|
149
211
|
|
|
150
212
|
And in general, see the [docs website](https://electric-sql.com) and [examples](https://electric-sql.com/demos) for more information.
|
|
151
213
|
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -353,6 +353,7 @@ var SUBSET_PARAM_ORDER_BY = `subset__order_by`;
|
|
|
353
353
|
var SUBSET_PARAM_WHERE_PARAMS = `subset__params`;
|
|
354
354
|
var ELECTRIC_PROTOCOL_QUERY_PARAMS = [
|
|
355
355
|
LIVE_QUERY_PARAM,
|
|
356
|
+
LIVE_SSE_QUERY_PARAM,
|
|
356
357
|
SHAPE_HANDLE_QUERY_PARAM,
|
|
357
358
|
OFFSET_QUERY_PARAM,
|
|
358
359
|
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
@@ -372,11 +373,22 @@ var BackoffDefaults = {
|
|
|
372
373
|
maxDelay: 6e4,
|
|
373
374
|
// Cap at 60s - reasonable for long-lived connections
|
|
374
375
|
multiplier: 1.3,
|
|
375
|
-
maxRetries: Infinity
|
|
376
|
+
maxRetries: Infinity
|
|
376
377
|
// Retry forever - clients may go offline and come back
|
|
377
|
-
retryBudgetPercent: 0.1
|
|
378
|
-
// 10% retry budget prevents amplification
|
|
379
378
|
};
|
|
379
|
+
function parseRetryAfterHeader(retryAfter) {
|
|
380
|
+
if (!retryAfter) return 0;
|
|
381
|
+
const retryAfterSec = Number(retryAfter);
|
|
382
|
+
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
383
|
+
return retryAfterSec * 1e3;
|
|
384
|
+
}
|
|
385
|
+
const retryDate = Date.parse(retryAfter);
|
|
386
|
+
if (!isNaN(retryDate)) {
|
|
387
|
+
const deltaMs = retryDate - Date.now();
|
|
388
|
+
return Math.max(0, Math.min(deltaMs, 36e5));
|
|
389
|
+
}
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
380
392
|
function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
381
393
|
const {
|
|
382
394
|
initialDelay,
|
|
@@ -384,28 +396,8 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
384
396
|
multiplier,
|
|
385
397
|
debug = false,
|
|
386
398
|
onFailedAttempt,
|
|
387
|
-
maxRetries = Infinity
|
|
388
|
-
retryBudgetPercent = 0.1
|
|
399
|
+
maxRetries = Infinity
|
|
389
400
|
} = backoffOptions;
|
|
390
|
-
let totalRequests = 0;
|
|
391
|
-
let totalRetries = 0;
|
|
392
|
-
let budgetResetTime = Date.now() + 6e4;
|
|
393
|
-
function checkRetryBudget(percent) {
|
|
394
|
-
const now = Date.now();
|
|
395
|
-
if (now > budgetResetTime) {
|
|
396
|
-
totalRequests = 0;
|
|
397
|
-
totalRetries = 0;
|
|
398
|
-
budgetResetTime = now + 6e4;
|
|
399
|
-
}
|
|
400
|
-
totalRequests++;
|
|
401
|
-
if (totalRequests < 10) return true;
|
|
402
|
-
const currentRetryRate = totalRetries / totalRequests;
|
|
403
|
-
const hasCapacity = currentRetryRate < percent;
|
|
404
|
-
if (hasCapacity) {
|
|
405
|
-
totalRetries++;
|
|
406
|
-
}
|
|
407
|
-
return hasCapacity;
|
|
408
|
-
}
|
|
409
401
|
return (...args) => __async(this, null, function* () {
|
|
410
402
|
var _a;
|
|
411
403
|
const url = args[0];
|
|
@@ -416,7 +408,6 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
416
408
|
try {
|
|
417
409
|
const result = yield fetchClient(...args);
|
|
418
410
|
if (result.ok) {
|
|
419
|
-
delay = initialDelay;
|
|
420
411
|
return result;
|
|
421
412
|
}
|
|
422
413
|
const err = yield FetchError.fromResponse(result, url.toString());
|
|
@@ -429,7 +420,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
429
420
|
throw e;
|
|
430
421
|
} else {
|
|
431
422
|
attempt++;
|
|
432
|
-
if (attempt
|
|
423
|
+
if (attempt > maxRetries) {
|
|
433
424
|
if (debug) {
|
|
434
425
|
console.log(
|
|
435
426
|
`Max retries reached (${attempt}/${maxRetries}), giving up`
|
|
@@ -437,31 +428,7 @@ function createFetchWithBackoff(fetchClient, backoffOptions = BackoffDefaults) {
|
|
|
437
428
|
}
|
|
438
429
|
throw e;
|
|
439
430
|
}
|
|
440
|
-
|
|
441
|
-
if (debug) {
|
|
442
|
-
console.log(
|
|
443
|
-
`Retry budget exhausted (attempt ${attempt}), backing off`
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
yield new Promise((resolve) => setTimeout(resolve, maxDelay));
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
let serverMinimumMs = 0;
|
|
450
|
-
if (e instanceof FetchError && e.headers) {
|
|
451
|
-
const retryAfter = e.headers[`retry-after`];
|
|
452
|
-
if (retryAfter) {
|
|
453
|
-
const retryAfterSec = Number(retryAfter);
|
|
454
|
-
if (Number.isFinite(retryAfterSec) && retryAfterSec > 0) {
|
|
455
|
-
serverMinimumMs = retryAfterSec * 1e3;
|
|
456
|
-
} else {
|
|
457
|
-
const retryDate = Date.parse(retryAfter);
|
|
458
|
-
if (!isNaN(retryDate)) {
|
|
459
|
-
const deltaMs = retryDate - Date.now();
|
|
460
|
-
serverMinimumMs = Math.max(0, Math.min(deltaMs, 36e5));
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
431
|
+
const serverMinimumMs = e instanceof FetchError && e.headers ? parseRetryAfterHeader(e.headers[`retry-after`]) : 0;
|
|
465
432
|
const jitter = Math.random() * delay;
|
|
466
433
|
const clientBackoffMs = Math.min(jitter, maxDelay);
|
|
467
434
|
const waitMs = Math.max(serverMinimumMs, clientBackoffMs);
|
|
@@ -843,8 +810,9 @@ function canonicalShapeKey(url) {
|
|
|
843
810
|
cleanUrl.searchParams.sort();
|
|
844
811
|
return cleanUrl.toString();
|
|
845
812
|
}
|
|
846
|
-
var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _isMidStream, _connected, _shapeHandle, _mode, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _activeSnapshotRequests, _midStreamPromise, _midStreamPromiseResolver, _ShapeStream_instances, start_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, pause_fn, resume_fn, nextTick_fn, waitForStreamEnd_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn, fetchSnapshot_fn;
|
|
813
|
+
var _error, _fetchClient2, _sseFetchClient, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _isMidStream, _connected, _shapeHandle, _mode, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _messageChain, _snapshotTracker, _activeSnapshotRequests, _midStreamPromise, _midStreamPromiseResolver, _lastSseConnectionStartTime, _minSseConnectionDuration, _consecutiveShortSseConnections, _maxShortSseConnections, _sseFallbackToLongPolling, _sseBackoffBaseDelay, _sseBackoffMaxDelay, _ShapeStream_instances, start_fn, requestShape_fn, constructUrl_fn, createAbortListener_fn, onInitialResponse_fn, onMessages_fn, fetchShape_fn, requestShapeLongPoll_fn, requestShapeSSE_fn, pause_fn, resume_fn, nextTick_fn, waitForStreamEnd_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn, fetchSnapshot_fn;
|
|
847
814
|
var ShapeStream = class {
|
|
815
|
+
// Maximum delay cap (ms)
|
|
848
816
|
constructor(options) {
|
|
849
817
|
__privateAdd(this, _ShapeStream_instances);
|
|
850
818
|
__privateAdd(this, _error, null);
|
|
@@ -878,6 +846,16 @@ var ShapeStream = class {
|
|
|
878
846
|
// counter for concurrent snapshot requests
|
|
879
847
|
__privateAdd(this, _midStreamPromise);
|
|
880
848
|
__privateAdd(this, _midStreamPromiseResolver);
|
|
849
|
+
__privateAdd(this, _lastSseConnectionStartTime);
|
|
850
|
+
__privateAdd(this, _minSseConnectionDuration, 1e3);
|
|
851
|
+
// Minimum expected SSE connection duration (1 second)
|
|
852
|
+
__privateAdd(this, _consecutiveShortSseConnections, 0);
|
|
853
|
+
__privateAdd(this, _maxShortSseConnections, 3);
|
|
854
|
+
// Fall back to long polling after this many short connections
|
|
855
|
+
__privateAdd(this, _sseFallbackToLongPolling, false);
|
|
856
|
+
__privateAdd(this, _sseBackoffBaseDelay, 100);
|
|
857
|
+
// Base delay for exponential backoff (ms)
|
|
858
|
+
__privateAdd(this, _sseBackoffMaxDelay, 5e3);
|
|
881
859
|
var _a, _b, _c, _d;
|
|
882
860
|
this.options = __spreadValues({ subscribe: true }, options);
|
|
883
861
|
validateOptions(this.options);
|
|
@@ -1053,10 +1031,17 @@ _snapshotTracker = new WeakMap();
|
|
|
1053
1031
|
_activeSnapshotRequests = new WeakMap();
|
|
1054
1032
|
_midStreamPromise = new WeakMap();
|
|
1055
1033
|
_midStreamPromiseResolver = new WeakMap();
|
|
1034
|
+
_lastSseConnectionStartTime = new WeakMap();
|
|
1035
|
+
_minSseConnectionDuration = new WeakMap();
|
|
1036
|
+
_consecutiveShortSseConnections = new WeakMap();
|
|
1037
|
+
_maxShortSseConnections = new WeakMap();
|
|
1038
|
+
_sseFallbackToLongPolling = new WeakMap();
|
|
1039
|
+
_sseBackoffBaseDelay = new WeakMap();
|
|
1040
|
+
_sseBackoffMaxDelay = new WeakMap();
|
|
1056
1041
|
_ShapeStream_instances = new WeakSet();
|
|
1057
1042
|
start_fn = function() {
|
|
1058
1043
|
return __async(this, null, function* () {
|
|
1059
|
-
var _a;
|
|
1044
|
+
var _a, _b, _c, _d, _e;
|
|
1060
1045
|
__privateSet(this, _started, true);
|
|
1061
1046
|
try {
|
|
1062
1047
|
yield __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
@@ -1064,24 +1049,34 @@ start_fn = function() {
|
|
|
1064
1049
|
__privateSet(this, _error, err);
|
|
1065
1050
|
if (__privateGet(this, _onError)) {
|
|
1066
1051
|
const retryOpts = yield __privateGet(this, _onError).call(this, err);
|
|
1067
|
-
if (typeof retryOpts === `object`) {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
this.options.params = retryOpts.params;
|
|
1052
|
+
if (retryOpts && typeof retryOpts === `object`) {
|
|
1053
|
+
if (retryOpts.params) {
|
|
1054
|
+
this.options.params = __spreadValues(__spreadValues({}, (_a = this.options.params) != null ? _a : {}), retryOpts.params);
|
|
1071
1055
|
}
|
|
1072
|
-
if (
|
|
1073
|
-
this.options.headers = retryOpts.headers;
|
|
1056
|
+
if (retryOpts.headers) {
|
|
1057
|
+
this.options.headers = __spreadValues(__spreadValues({}, (_b = this.options.headers) != null ? _b : {}), retryOpts.headers);
|
|
1074
1058
|
}
|
|
1059
|
+
__privateSet(this, _error, null);
|
|
1075
1060
|
__privateSet(this, _started, false);
|
|
1076
|
-
__privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
1061
|
+
yield __privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (err instanceof Error) {
|
|
1065
|
+
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
|
|
1077
1066
|
}
|
|
1067
|
+
__privateSet(this, _connected, false);
|
|
1068
|
+
(_c = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _c.call(this);
|
|
1078
1069
|
return;
|
|
1079
1070
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1071
|
+
if (err instanceof Error) {
|
|
1072
|
+
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, err);
|
|
1073
|
+
}
|
|
1082
1074
|
__privateSet(this, _connected, false);
|
|
1083
|
-
(
|
|
1075
|
+
(_d = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _d.call(this);
|
|
1076
|
+
throw err;
|
|
1084
1077
|
}
|
|
1078
|
+
__privateSet(this, _connected, false);
|
|
1079
|
+
(_e = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _e.call(this);
|
|
1085
1080
|
});
|
|
1086
1081
|
};
|
|
1087
1082
|
requestShape_fn = function() {
|
|
@@ -1128,7 +1123,6 @@ requestShape_fn = function() {
|
|
|
1128
1123
|
yield __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, Array.isArray(e.json) ? e.json : [e.json]);
|
|
1129
1124
|
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
1130
1125
|
} else {
|
|
1131
|
-
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
|
|
1132
1126
|
throw e;
|
|
1133
1127
|
}
|
|
1134
1128
|
} finally {
|
|
@@ -1280,7 +1274,7 @@ fetchShape_fn = function(opts) {
|
|
|
1280
1274
|
return __async(this, null, function* () {
|
|
1281
1275
|
var _a;
|
|
1282
1276
|
const useSse = (_a = this.options.liveSse) != null ? _a : this.options.experimentalLiveSse;
|
|
1283
|
-
if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause) {
|
|
1277
|
+
if (__privateGet(this, _isUpToDate) && useSse && !__privateGet(this, _isRefreshing) && !opts.resumingFromPause && !__privateGet(this, _sseFallbackToLongPolling)) {
|
|
1284
1278
|
opts.fetchUrl.searchParams.set(EXPERIMENTAL_LIVE_SSE_QUERY_PARAM, `true`);
|
|
1285
1279
|
opts.fetchUrl.searchParams.set(LIVE_SSE_QUERY_PARAM, `true`);
|
|
1286
1280
|
return __privateMethod(this, _ShapeStream_instances, requestShapeSSE_fn).call(this, opts);
|
|
@@ -1308,6 +1302,7 @@ requestShapeSSE_fn = function(opts) {
|
|
|
1308
1302
|
return __async(this, null, function* () {
|
|
1309
1303
|
const { fetchUrl, requestAbortController, headers } = opts;
|
|
1310
1304
|
const fetch2 = __privateGet(this, _sseFetchClient);
|
|
1305
|
+
__privateSet(this, _lastSseConnectionStartTime, Date.now());
|
|
1311
1306
|
try {
|
|
1312
1307
|
let buffer = [];
|
|
1313
1308
|
yield (0, import_fetch_event_source.fetchEventSource)(fetchUrl.toString(), {
|
|
@@ -1341,6 +1336,27 @@ requestShapeSSE_fn = function(opts) {
|
|
|
1341
1336
|
throw new FetchBackoffAbortError();
|
|
1342
1337
|
}
|
|
1343
1338
|
throw error;
|
|
1339
|
+
} finally {
|
|
1340
|
+
const connectionDuration = Date.now() - __privateGet(this, _lastSseConnectionStartTime);
|
|
1341
|
+
const wasAborted = requestAbortController.signal.aborted;
|
|
1342
|
+
if (connectionDuration < __privateGet(this, _minSseConnectionDuration) && !wasAborted) {
|
|
1343
|
+
__privateWrapper(this, _consecutiveShortSseConnections)._++;
|
|
1344
|
+
if (__privateGet(this, _consecutiveShortSseConnections) >= __privateGet(this, _maxShortSseConnections)) {
|
|
1345
|
+
__privateSet(this, _sseFallbackToLongPolling, true);
|
|
1346
|
+
console.warn(
|
|
1347
|
+
`[Electric] SSE connections are closing immediately (possibly due to proxy buffering or misconfiguration). Falling back to long polling. Your proxy must support streaming SSE responses (not buffer the complete response). Configuration: Nginx add 'X-Accel-Buffering: no', Caddy add 'flush_interval -1' to reverse_proxy. Note: Do NOT disable caching entirely - Electric uses cache headers to enable request collapsing for efficiency.`
|
|
1348
|
+
);
|
|
1349
|
+
} else {
|
|
1350
|
+
const maxDelay = Math.min(
|
|
1351
|
+
__privateGet(this, _sseBackoffMaxDelay),
|
|
1352
|
+
__privateGet(this, _sseBackoffBaseDelay) * Math.pow(2, __privateGet(this, _consecutiveShortSseConnections))
|
|
1353
|
+
);
|
|
1354
|
+
const delayMs = Math.floor(Math.random() * maxDelay);
|
|
1355
|
+
yield new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1356
|
+
}
|
|
1357
|
+
} else if (connectionDuration >= __privateGet(this, _minSseConnectionDuration)) {
|
|
1358
|
+
__privateSet(this, _consecutiveShortSseConnections, 0);
|
|
1359
|
+
}
|
|
1344
1360
|
}
|
|
1345
1361
|
});
|
|
1346
1362
|
};
|
|
@@ -1439,6 +1455,8 @@ reset_fn = function(handle) {
|
|
|
1439
1455
|
__privateSet(this, _connected, false);
|
|
1440
1456
|
__privateSet(this, _schema, void 0);
|
|
1441
1457
|
__privateSet(this, _activeSnapshotRequests, 0);
|
|
1458
|
+
__privateSet(this, _consecutiveShortSseConnections, 0);
|
|
1459
|
+
__privateSet(this, _sseFallbackToLongPolling, false);
|
|
1442
1460
|
};
|
|
1443
1461
|
fetchSnapshot_fn = function(url, headers) {
|
|
1444
1462
|
return __async(this, null, function* () {
|