@rebasepro/client 0.9.1-canary.e2fc7b6 → 0.9.1-canary.e3f810f

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/dist/index.es.js CHANGED
@@ -1397,9 +1397,11 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1397
1397
  function extractMessageError(message) {
1398
1398
  const payload = message.payload;
1399
1399
  const errPayload = payload?.error;
1400
+ const errorMessage = typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error";
1401
+ const errorCode = typeof errPayload === "object" ? errPayload.code : payload?.code;
1400
1402
  return {
1401
- errorMessage: typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error",
1402
- errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
1403
+ errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
1404
+ errorCode
1403
1405
  };
1404
1406
  }
1405
1407
  /**
@@ -1435,6 +1437,7 @@ var RebaseWebSocketClient = class {
1435
1437
  isConnected = false;
1436
1438
  messageQueue = [];
1437
1439
  requestTimeoutMs = 3e4;
1440
+ subscriptionTimeoutMs = 3e4;
1438
1441
  reconnectTimeout = null;
1439
1442
  isAuthenticated = false;
1440
1443
  authPromise = null;
@@ -1540,6 +1543,7 @@ var RebaseWebSocketClient = class {
1540
1543
  this.emit(wasReconnect ? "reconnect" : "connect");
1541
1544
  this.processMessageQueue();
1542
1545
  if (wasReconnect) this.resubscribeAll();
1546
+ this.armPendingSubscribeWatchdogs();
1543
1547
  };
1544
1548
  this.ws.onmessage = (event) => {
1545
1549
  try {
@@ -1554,6 +1558,7 @@ var RebaseWebSocketClient = class {
1554
1558
  this.isConnected = false;
1555
1559
  this.isAuthenticated = false;
1556
1560
  this.authPromise = null;
1561
+ this.suspendSubscribeWatchdogs();
1557
1562
  this.emit("disconnect");
1558
1563
  for (const [reqId, request] of this.pendingRequests.entries()) {
1559
1564
  if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
@@ -1585,6 +1590,7 @@ var RebaseWebSocketClient = class {
1585
1590
  attemptReconnect() {
1586
1591
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1587
1592
  console.error("Max reconnection attempts reached");
1593
+ this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
1588
1594
  return;
1589
1595
  }
1590
1596
  this.reconnectAttempts++;
@@ -1639,29 +1645,18 @@ var RebaseWebSocketClient = class {
1639
1645
  subscription.backendSubscriptionId = newBackendId;
1640
1646
  backendKeyMap.delete(oldBackendId);
1641
1647
  backendKeyMap.set(newBackendId, subscriptionKey);
1642
- this.sendMessage({
1643
- type: messageType,
1644
- payload: {
1645
- ...subscription.props,
1646
- subscriptionId: newBackendId
1647
- }
1648
- }).catch((error) => {
1649
- console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
1650
- subscription.callbacks.forEach((callback) => {
1651
- if (callback.onError) callback.onError(error);
1652
- });
1653
- });
1654
- } else {
1655
- const { errorMessage, errorCode } = extractMessageError(message);
1656
- const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1657
- subscription.callbacks.forEach((callback) => {
1658
- if (callback.onError) callback.onError(error);
1659
- });
1648
+ if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
1649
+ else this.sendEntitySubscribe(subscriptionKey);
1650
+ return;
1660
1651
  }
1652
+ const { errorMessage, errorCode } = extractMessageError(message);
1653
+ const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1654
+ if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
1655
+ else this.failEntitySubscription(subscriptionKey, error);
1661
1656
  }).catch((err) => {
1662
- subscription.callbacks.forEach((callback) => {
1663
- if (callback.onError) callback.onError(err);
1664
- });
1657
+ const error = err instanceof Error ? err : new Error(String(err));
1658
+ if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
1659
+ else this.failEntitySubscription(subscriptionKey, error);
1665
1660
  });
1666
1661
  }
1667
1662
  handleWebSocketMessage(message) {
@@ -1702,6 +1697,9 @@ var RebaseWebSocketClient = class {
1702
1697
  collectionSub.latestData = rows;
1703
1698
  collectionSub.lastUpdated = Date.now();
1704
1699
  collectionSub.isInitialDataReceived = true;
1700
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
1701
+ collectionSub.subscribeTimeout = void 0;
1702
+ collectionSub.subscribeInFlight = false;
1705
1703
  collectionSub.callbacks.forEach((callback) => {
1706
1704
  try {
1707
1705
  callback.onUpdate(rows);
@@ -1757,6 +1755,9 @@ var RebaseWebSocketClient = class {
1757
1755
  entitySub.latestData = row;
1758
1756
  entitySub.lastUpdated = Date.now();
1759
1757
  entitySub.isInitialDataReceived = true;
1758
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
1759
+ entitySub.subscribeTimeout = void 0;
1760
+ entitySub.subscribeInFlight = false;
1760
1761
  entitySub.callbacks.forEach((callback) => {
1761
1762
  try {
1762
1763
  callback.onUpdate(row);
@@ -1778,6 +1779,9 @@ var RebaseWebSocketClient = class {
1778
1779
  this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
1779
1780
  return;
1780
1781
  }
1782
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
1783
+ collectionSub.subscribeTimeout = void 0;
1784
+ collectionSub.subscribeInFlight = false;
1781
1785
  const { errorMessage, errorCode } = extractMessageError(message);
1782
1786
  const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1783
1787
  collectionSub.callbacks.forEach((callback) => {
@@ -1794,6 +1798,9 @@ var RebaseWebSocketClient = class {
1794
1798
  this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
1795
1799
  return;
1796
1800
  }
1801
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
1802
+ entitySub.subscribeTimeout = void 0;
1803
+ entitySub.subscribeInFlight = false;
1797
1804
  const { errorMessage, errorCode } = extractMessageError(message);
1798
1805
  const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1799
1806
  entitySub.callbacks.forEach((callback) => {
@@ -2142,9 +2149,12 @@ var RebaseWebSocketClient = class {
2142
2149
  console.error("Error in collection subscription callback:", error);
2143
2150
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
2144
2151
  }
2152
+ else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
2145
2153
  return () => {
2146
2154
  callbackMap.delete(callbackId);
2147
2155
  if (callbackMap.size === 0) {
2156
+ if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
2157
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
2148
2158
  this.collectionSubscriptions.delete(subscriptionKey);
2149
2159
  this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
2150
2160
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2166,21 +2176,14 @@ var RebaseWebSocketClient = class {
2166
2176
  props
2167
2177
  });
2168
2178
  this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
2169
- this.sendMessage({
2170
- type: "subscribe_collection",
2171
- payload: {
2172
- ...props,
2173
- subscriptionId: backendSubscriptionId
2174
- }
2175
- }).catch((error) => {
2176
- if (onError) onError(error);
2177
- });
2179
+ this.sendCollectionSubscribe(subscriptionKey);
2178
2180
  return () => {
2179
2181
  const subscription = this.collectionSubscriptions.get(subscriptionKey);
2180
2182
  if (subscription) {
2181
2183
  const callbacks = subscription.callbacks;
2182
2184
  callbacks.delete(callbackId);
2183
2185
  if (callbacks.size === 0) {
2186
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2184
2187
  this.collectionSubscriptions.delete(subscriptionKey);
2185
2188
  this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
2186
2189
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2207,9 +2210,12 @@ var RebaseWebSocketClient = class {
2207
2210
  console.error("Error in row subscription callback:", error);
2208
2211
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
2209
2212
  }
2213
+ else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
2210
2214
  return () => {
2211
2215
  callbackMap.delete(callbackId);
2212
2216
  if (callbackMap.size === 0) {
2217
+ if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
2218
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
2213
2219
  this.singleSubscriptions.delete(subscriptionKey);
2214
2220
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
2215
2221
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2231,15 +2237,7 @@ var RebaseWebSocketClient = class {
2231
2237
  props
2232
2238
  });
2233
2239
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
2234
- this.sendMessage({
2235
- type: "subscribe_one",
2236
- payload: {
2237
- ...props,
2238
- subscriptionId: backendSubscriptionId
2239
- }
2240
- }).catch((error) => {
2241
- if (onError) onError(error);
2242
- });
2240
+ this.sendEntitySubscribe(subscriptionKey);
2243
2241
  return () => {
2244
2242
  const subscription = this.singleSubscriptions.get(subscriptionKey);
2245
2243
  if (subscription) {
@@ -2257,6 +2255,157 @@ var RebaseWebSocketClient = class {
2257
2255
  };
2258
2256
  }
2259
2257
  /**
2258
+ * Send a `subscribe_collection` for an already-registered subscription and
2259
+ * arm its watchdog.
2260
+ *
2261
+ * Every path that registers a collection subscription goes through here, so
2262
+ * that a subscribe which never lands — a rejected send, or a server that
2263
+ * never answers — always ends up in `failCollectionSubscription` rather than
2264
+ * leaving the entry parked with `isInitialDataReceived === false` forever.
2265
+ */
2266
+ sendCollectionSubscribe(subscriptionKey) {
2267
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
2268
+ if (!subscription) return;
2269
+ const backendSubscriptionId = subscription.backendSubscriptionId;
2270
+ subscription.subscribeInFlight = true;
2271
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2272
+ subscription.subscribeTimeout = void 0;
2273
+ if (this.isConnected) this.sendCollectionSubscribeWatchdog(subscriptionKey);
2274
+ this.sendMessage({
2275
+ type: "subscribe_collection",
2276
+ payload: {
2277
+ ...subscription.props,
2278
+ subscriptionId: backendSubscriptionId
2279
+ }
2280
+ }).catch((error) => {
2281
+ const current = this.collectionSubscriptions.get(subscriptionKey);
2282
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
2283
+ this.failCollectionSubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
2284
+ });
2285
+ }
2286
+ /** The `listenOne` counterpart of {@link sendCollectionSubscribe}. */
2287
+ sendEntitySubscribe(subscriptionKey) {
2288
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
2289
+ if (!subscription) return;
2290
+ const backendSubscriptionId = subscription.backendSubscriptionId;
2291
+ subscription.subscribeInFlight = true;
2292
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2293
+ subscription.subscribeTimeout = void 0;
2294
+ if (this.isConnected) this.sendEntitySubscribeWatchdog(subscriptionKey);
2295
+ this.sendMessage({
2296
+ type: "subscribe_one",
2297
+ payload: {
2298
+ ...subscription.props,
2299
+ subscriptionId: backendSubscriptionId
2300
+ }
2301
+ }).catch((error) => {
2302
+ const current = this.singleSubscriptions.get(subscriptionKey);
2303
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
2304
+ this.failEntitySubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
2305
+ });
2306
+ }
2307
+ /**
2308
+ * Report a subscribe failure to every listener and drop the registration.
2309
+ *
2310
+ * Dropping it is the point: the callbacks stay live (their components are
2311
+ * still mounted and have been told), but the next `listenCollection` for
2312
+ * these params finds no entry and issues a fresh subscribe instead of
2313
+ * silently attaching to a dead one.
2314
+ */
2315
+ failCollectionSubscription(subscriptionKey, error) {
2316
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
2317
+ if (!subscription) return;
2318
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2319
+ subscription.subscribeInFlight = false;
2320
+ this.collectionSubscriptions.delete(subscriptionKey);
2321
+ this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
2322
+ subscription.callbacks.forEach((callback) => {
2323
+ if (callback.onError) try {
2324
+ callback.onError(error);
2325
+ } catch (callbackError) {
2326
+ console.error("Error in collection subscription error callback:", callbackError);
2327
+ }
2328
+ });
2329
+ }
2330
+ /** The `listenOne` counterpart of {@link failCollectionSubscription}. */
2331
+ failEntitySubscription(subscriptionKey, error) {
2332
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
2333
+ if (!subscription) return;
2334
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2335
+ subscription.subscribeInFlight = false;
2336
+ this.singleSubscriptions.delete(subscriptionKey);
2337
+ this.backendToEntityKey.delete(subscription.backendSubscriptionId);
2338
+ subscription.callbacks.forEach((callback) => {
2339
+ if (callback.onError) try {
2340
+ callback.onError(error);
2341
+ } catch (callbackError) {
2342
+ console.error("Error in row subscription error callback:", callbackError);
2343
+ }
2344
+ });
2345
+ }
2346
+ /**
2347
+ * Stop the watchdogs without failing anything — used when the socket drops,
2348
+ * since the reconnect path re-subscribes everything anyway and a watchdog
2349
+ * firing mid-reconnect would tear down healthy subscriptions.
2350
+ */
2351
+ suspendSubscribeWatchdogs() {
2352
+ for (const sub of this.collectionSubscriptions.values()) {
2353
+ if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
2354
+ sub.subscribeTimeout = void 0;
2355
+ sub.subscribeInFlight = false;
2356
+ }
2357
+ for (const sub of this.singleSubscriptions.values()) {
2358
+ if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
2359
+ sub.subscribeTimeout = void 0;
2360
+ sub.subscribeInFlight = false;
2361
+ }
2362
+ }
2363
+ /**
2364
+ * Arm watchdogs for subscribes that were requested while offline and have
2365
+ * just been flushed to the socket. Their timers were deliberately not set at
2366
+ * request time, so without this they would have no timeout at all.
2367
+ */
2368
+ armPendingSubscribeWatchdogs() {
2369
+ for (const [key, sub] of this.collectionSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendCollectionSubscribeWatchdog(key);
2370
+ for (const [key, sub] of this.singleSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendEntitySubscribeWatchdog(key);
2371
+ }
2372
+ sendCollectionSubscribeWatchdog(subscriptionKey) {
2373
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
2374
+ if (!subscription) return;
2375
+ const backendSubscriptionId = subscription.backendSubscriptionId;
2376
+ subscription.subscribeTimeout = setTimeout(() => {
2377
+ const current = this.collectionSubscriptions.get(subscriptionKey);
2378
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
2379
+ if (!current.subscribeInFlight) return;
2380
+ this.failCollectionSubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
2381
+ }, this.subscriptionTimeoutMs);
2382
+ }
2383
+ sendEntitySubscribeWatchdog(subscriptionKey) {
2384
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
2385
+ if (!subscription) return;
2386
+ const backendSubscriptionId = subscription.backendSubscriptionId;
2387
+ subscription.subscribeTimeout = setTimeout(() => {
2388
+ const current = this.singleSubscriptions.get(subscriptionKey);
2389
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
2390
+ if (!current.subscribeInFlight) return;
2391
+ this.failEntitySubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
2392
+ }, this.subscriptionTimeoutMs);
2393
+ }
2394
+ /**
2395
+ * Fail every subscription that never received data. Called when reconnection
2396
+ * is given up on, so views surface an error instead of spinning forever.
2397
+ */
2398
+ failAllPendingSubscriptions(error) {
2399
+ for (const key of [...this.collectionSubscriptions.keys()]) {
2400
+ const sub = this.collectionSubscriptions.get(key);
2401
+ if (sub && !sub.isInitialDataReceived) this.failCollectionSubscription(key, error);
2402
+ }
2403
+ for (const key of [...this.singleSubscriptions.keys()]) {
2404
+ const sub = this.singleSubscriptions.get(key);
2405
+ if (sub && !sub.isInitialDataReceived) this.failEntitySubscription(key, error);
2406
+ }
2407
+ }
2408
+ /**
2260
2409
  * Re-send all active subscriptions to the backend after a reconnect.
2261
2410
  * The server wipes subscription state when a client disconnects, so
2262
2411
  * we need to re-register everything to resume receiving updates.
@@ -2269,15 +2418,7 @@ var RebaseWebSocketClient = class {
2269
2418
  sub.backendSubscriptionId = newBackendId;
2270
2419
  this.backendToCollectionKey.delete(oldBackendId);
2271
2420
  this.backendToCollectionKey.set(newBackendId, key);
2272
- this.sendMessage({
2273
- type: "subscribe_collection",
2274
- payload: {
2275
- ...sub.props,
2276
- subscriptionId: newBackendId
2277
- }
2278
- }).catch((error) => {
2279
- console.error("[WS] Failed to re-subscribe collection:", key, error);
2280
- });
2421
+ this.sendCollectionSubscribe(key);
2281
2422
  }
2282
2423
  for (const [key, sub] of this.singleSubscriptions.entries()) {
2283
2424
  const oldBackendId = sub.backendSubscriptionId;
@@ -2285,15 +2426,7 @@ var RebaseWebSocketClient = class {
2285
2426
  sub.backendSubscriptionId = newBackendId;
2286
2427
  this.backendToEntityKey.delete(oldBackendId);
2287
2428
  this.backendToEntityKey.set(newBackendId, key);
2288
- this.sendMessage({
2289
- type: "subscribe_one",
2290
- payload: {
2291
- ...sub.props,
2292
- subscriptionId: newBackendId
2293
- }
2294
- }).catch((error) => {
2295
- console.error("[WS] Failed to re-subscribe row:", key, error);
2296
- });
2429
+ this.sendEntitySubscribe(key);
2297
2430
  }
2298
2431
  }
2299
2432
  createCollectionSubscriptionKey(props) {