@rebasepro/client 0.9.0 → 0.9.1-canary.0fce67c

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
@@ -161,4 +161,4 @@ const unsubscribe = client.data.products.listen(
161
161
  - [`@rebasepro/common`](../common) — `QueryBuilder`, `buildRebaseData`, shared utilities
162
162
  - [`@rebasepro/types`](../types) — `Snapshot`, `FindResponse`, `CollectionAccessor`, etc.
163
163
  - [`@rebasepro/utils`](../utils) — `toSnakeCase` and other helpers
164
- - [`@rebasepro/auth`](../auth) — React hook adapter that wraps `client.auth` for CMS integration
164
+ - [`@rebasepro/app`](../auth) — React hook adapter that wraps `client.auth` for CMS integration
package/dist/admin.d.ts CHANGED
@@ -48,6 +48,7 @@ export declare function createAdmin(transport: Transport, options?: CreateAdminO
48
48
  user: AdminUser;
49
49
  temporaryPassword?: string;
50
50
  invitationSent?: boolean;
51
+ emailDeliveryFailed?: boolean;
51
52
  }>;
52
53
  listRoles: () => Promise<{
53
54
  roles: Array<{
@@ -0,0 +1,13 @@
1
+ import { Transport } from "./transport";
2
+ import type { BackupInfo, BackupDestinationKind } from "@rebasepro/types";
3
+ export interface CreateBackupsOptions {
4
+ backupsPath?: string;
5
+ }
6
+ export declare function createBackups(transport: Transport, options?: CreateBackupsOptions): {
7
+ list: () => Promise<{
8
+ backups: BackupInfo[];
9
+ destinationKind: BackupDestinationKind;
10
+ configured: boolean;
11
+ }>;
12
+ download: (key: string) => Promise<Blob>;
13
+ };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { RebaseClientConfig } from "./transport";
2
2
  import { createAuth, CreateAuthOptions } from "./auth";
3
3
  import { createAdmin, CreateAdminOptions } from "./admin";
4
4
  import { createCron, CreateCronOptions } from "./cron";
5
+ import { createBackups } from "./backups";
5
6
  import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
6
7
  import { CollectionClient } from "./collection";
7
8
  import { createFunctionsClient } from "./functions";
@@ -21,6 +22,8 @@ export type { RebaseUser, RebaseTokens } from "./auth";
21
22
  export type { CreateAdminOptions } from "./admin";
22
23
  export type { AdminUser } from "./admin";
23
24
  export type { CreateCronOptions } from "./cron";
25
+ export { createBackups } from "./backups";
26
+ export type { CreateBackupsOptions } from "./backups";
24
27
  export type { ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, CreateApiKeysOptions, UpdateApiKeyRequest } from "./api-keys";
25
28
  export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
26
29
  export { RebaseWebSocketClient } from "./websocket";
@@ -67,14 +70,24 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
67
70
  auth: ReturnType<typeof createAuth>;
68
71
  admin: ReturnType<typeof createAdmin>;
69
72
  cron: ReturnType<typeof createCron>;
73
+ backups: ReturnType<typeof createBackups>;
70
74
  apiKeys: ReturnType<typeof createApiKeys>;
71
75
  functions: ReturnType<typeof createFunctionsClient>;
72
76
  ws?: RebaseWebSocketClient;
77
+ /**
78
+ * Release the realtime socket and its reconnect timer.
79
+ *
80
+ * An open socket keeps the Node event loop alive, so a script that does not
81
+ * call this will not exit on its own. Safe when realtime was never started
82
+ * (`realtime: false`), and safe to call twice.
83
+ */
84
+ close: () => void;
73
85
  storage: StorageSource;
74
86
  storageRegistry: StorageSourceRegistry;
75
87
  createStorageSource: (storageId: string) => StorageSource;
76
88
  fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
77
89
  call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
90
+ collection: <M extends Record<string, unknown> = Record<string, unknown>>(slug: string) => CollectionClient<M>;
78
91
  data: TypedDataLayer<DB>;
79
92
  };
80
93
  export declare function createRebaseClient<DB = Record<string, unknown>>(options: CreateRebaseClientOptions): CreateRebaseClientResult<DB>;
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, Vector, isPublicStoragePath } from "@rebasepro/types";
2
- import { QueryBuilder, and, cond, or, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
2
+ import { COMPOSITE_ID_SEPARATOR, QueryBuilder, and, buildCompositeId, cond, or, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
3
3
  import { toSnakeCase } from "@rebasepro/utils";
4
4
  //#region src/reviver.ts
5
5
  function rebaseReviver(_key, value) {
@@ -869,6 +869,32 @@ function createCron(transport, options) {
869
869
  };
870
870
  }
871
871
  //#endregion
872
+ //#region src/backups.ts
873
+ function createBackups(transport, options) {
874
+ const backupsPath = options?.backupsPath || "/admin/backups";
875
+ async function list() {
876
+ return transport.request(backupsPath, { method: "GET" });
877
+ }
878
+ /**
879
+ * Download a backup's bytes. Uses an authenticated fetch (not the JSON
880
+ * transport) so the octet-stream response comes back as a Blob.
881
+ */
882
+ async function download(key) {
883
+ const token = await transport.resolveToken();
884
+ const url = `${transport.baseUrl}${transport.apiPath}${backupsPath}/download?key=${encodeURIComponent(key)}`;
885
+ const res = await fetch(url, {
886
+ method: "GET",
887
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
888
+ });
889
+ if (!res.ok) throw new Error(`Failed to download backup (${res.status})`);
890
+ return res.blob();
891
+ }
892
+ return {
893
+ list,
894
+ download
895
+ };
896
+ }
897
+ //#endregion
872
898
  //#region src/api-keys.ts
873
899
  /**
874
900
  * Creates a client for managing API keys via the admin routes.
@@ -1009,7 +1035,7 @@ var SDKQueryBuilder = class {
1009
1035
  * Listen to realtime updates matching this query.
1010
1036
  */
1011
1037
  listen(onUpdate, onError) {
1012
- if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl.");
1038
+ if (!this.collection.listen) throw new Error("Listen is only available when RebaseClient is configured with a websocketUrl, and not when it was created with realtime: false.");
1013
1039
  return this.collection.listen(this.params, onUpdate, onError);
1014
1040
  }
1015
1041
  };
@@ -1044,6 +1070,17 @@ function createCollectionClient(transport, slug, ws) {
1044
1070
  body: JSON.stringify(body)
1045
1071
  });
1046
1072
  },
1073
+ async createMany(data, options) {
1074
+ if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
1075
+ if (data.length === 0) return [];
1076
+ return (await transport.request(`${basePath}/bulk`, {
1077
+ method: "POST",
1078
+ body: JSON.stringify({
1079
+ rows: data,
1080
+ ...options?.upsert ? { upsert: true } : {}
1081
+ })
1082
+ })).data || [];
1083
+ },
1047
1084
  async update(id, data) {
1048
1085
  return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
1049
1086
  method: "PUT",
@@ -1360,9 +1397,11 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
1360
1397
  function extractMessageError(message) {
1361
1398
  const payload = message.payload;
1362
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;
1363
1402
  return {
1364
- errorMessage: typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error",
1365
- errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
1403
+ errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
1404
+ errorCode
1366
1405
  };
1367
1406
  }
1368
1407
  /**
@@ -1371,7 +1410,7 @@ function extractMessageError(message) {
1371
1410
  * @internal Not a stable app-facing API. `createRebaseClient()` constructs and
1372
1411
  * manages this internally (exposed as `client.ws`, typed by the minimal
1373
1412
  * `RebaseWebSocket` contract in `@rebasepro/types`). It is re-exported from the
1374
- * package root only because the `@rebasepro/client-postgresql` driver
1413
+ * package root only because the `@rebasepro/client-postgres` driver
1375
1414
  * instantiates it directly; its surface may change without a major bump.
1376
1415
  */
1377
1416
  var RebaseWebSocketClient = class {
@@ -1398,6 +1437,7 @@ var RebaseWebSocketClient = class {
1398
1437
  isConnected = false;
1399
1438
  messageQueue = [];
1400
1439
  requestTimeoutMs = 3e4;
1440
+ subscriptionTimeoutMs = 3e4;
1401
1441
  reconnectTimeout = null;
1402
1442
  isAuthenticated = false;
1403
1443
  authPromise = null;
@@ -1503,6 +1543,7 @@ var RebaseWebSocketClient = class {
1503
1543
  this.emit(wasReconnect ? "reconnect" : "connect");
1504
1544
  this.processMessageQueue();
1505
1545
  if (wasReconnect) this.resubscribeAll();
1546
+ this.armPendingSubscribeWatchdogs();
1506
1547
  };
1507
1548
  this.ws.onmessage = (event) => {
1508
1549
  try {
@@ -1517,6 +1558,7 @@ var RebaseWebSocketClient = class {
1517
1558
  this.isConnected = false;
1518
1559
  this.isAuthenticated = false;
1519
1560
  this.authPromise = null;
1561
+ this.suspendSubscribeWatchdogs();
1520
1562
  this.emit("disconnect");
1521
1563
  for (const [reqId, request] of this.pendingRequests.entries()) {
1522
1564
  if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
@@ -1548,6 +1590,7 @@ var RebaseWebSocketClient = class {
1548
1590
  attemptReconnect() {
1549
1591
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1550
1592
  console.error("Max reconnection attempts reached");
1593
+ this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
1551
1594
  return;
1552
1595
  }
1553
1596
  this.reconnectAttempts++;
@@ -1602,29 +1645,18 @@ var RebaseWebSocketClient = class {
1602
1645
  subscription.backendSubscriptionId = newBackendId;
1603
1646
  backendKeyMap.delete(oldBackendId);
1604
1647
  backendKeyMap.set(newBackendId, subscriptionKey);
1605
- this.sendMessage({
1606
- type: messageType,
1607
- payload: {
1608
- ...subscription.props,
1609
- subscriptionId: newBackendId
1610
- }
1611
- }).catch((error) => {
1612
- console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
1613
- subscription.callbacks.forEach((callback) => {
1614
- if (callback.onError) callback.onError(error);
1615
- });
1616
- });
1617
- } else {
1618
- const { errorMessage, errorCode } = extractMessageError(message);
1619
- const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1620
- subscription.callbacks.forEach((callback) => {
1621
- if (callback.onError) callback.onError(error);
1622
- });
1648
+ if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
1649
+ else this.sendEntitySubscribe(subscriptionKey);
1650
+ return;
1623
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);
1624
1656
  }).catch((err) => {
1625
- subscription.callbacks.forEach((callback) => {
1626
- if (callback.onError) callback.onError(err);
1627
- });
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);
1628
1660
  });
1629
1661
  }
1630
1662
  handleWebSocketMessage(message) {
@@ -1659,10 +1691,15 @@ var RebaseWebSocketClient = class {
1659
1691
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1660
1692
  if (collectionSub) {
1661
1693
  const incomingRows = message.rows || [];
1662
- const rows = this.mergeRows(collectionSub.latestData, incomingRows);
1694
+ const updatePks = message.pks;
1695
+ if (updatePks) collectionSub.pks = updatePks;
1696
+ const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
1663
1697
  collectionSub.latestData = rows;
1664
1698
  collectionSub.lastUpdated = Date.now();
1665
1699
  collectionSub.isInitialDataReceived = true;
1700
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
1701
+ collectionSub.subscribeTimeout = void 0;
1702
+ collectionSub.subscribeInFlight = false;
1666
1703
  collectionSub.callbacks.forEach((callback) => {
1667
1704
  try {
1668
1705
  callback.onUpdate(rows);
@@ -1681,12 +1718,14 @@ var RebaseWebSocketClient = class {
1681
1718
  const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
1682
1719
  if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
1683
1720
  const patchWireEntity = message.row ?? null;
1684
- const patchEntityId = message.id;
1721
+ const patchMessage = message;
1722
+ const patchEntityId = patchMessage.id;
1723
+ if (patchMessage.pks) collectionSub.pks = patchMessage.pks;
1685
1724
  const patchRow = patchWireEntity ? patchWireEntity : null;
1686
1725
  let updated;
1687
- if (patchRow === null) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
1726
+ if (patchRow === null) updated = collectionSub.latestData.filter((e) => this.rowAddress(e, collectionSub.pks) !== String(patchEntityId));
1688
1727
  else {
1689
- const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchRow.id));
1728
+ const idx = collectionSub.latestData.findIndex((e) => this.rowAddress(e, collectionSub.pks) === String(patchEntityId));
1690
1729
  if (idx >= 0) {
1691
1730
  updated = [...collectionSub.latestData];
1692
1731
  updated[idx] = patchRow;
@@ -1716,6 +1755,9 @@ var RebaseWebSocketClient = class {
1716
1755
  entitySub.latestData = row;
1717
1756
  entitySub.lastUpdated = Date.now();
1718
1757
  entitySub.isInitialDataReceived = true;
1758
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
1759
+ entitySub.subscribeTimeout = void 0;
1760
+ entitySub.subscribeInFlight = false;
1719
1761
  entitySub.callbacks.forEach((callback) => {
1720
1762
  try {
1721
1763
  callback.onUpdate(row);
@@ -1737,6 +1779,9 @@ var RebaseWebSocketClient = class {
1737
1779
  this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
1738
1780
  return;
1739
1781
  }
1782
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
1783
+ collectionSub.subscribeTimeout = void 0;
1784
+ collectionSub.subscribeInFlight = false;
1740
1785
  const { errorMessage, errorCode } = extractMessageError(message);
1741
1786
  const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1742
1787
  collectionSub.callbacks.forEach((callback) => {
@@ -1753,6 +1798,9 @@ var RebaseWebSocketClient = class {
1753
1798
  this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
1754
1799
  return;
1755
1800
  }
1801
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
1802
+ entitySub.subscribeTimeout = void 0;
1803
+ entitySub.subscribeInFlight = false;
1756
1804
  const { errorMessage, errorCode } = extractMessageError(message);
1757
1805
  const error = new RebaseApiError$1(errorMessage, { code: errorCode });
1758
1806
  entitySub.callbacks.forEach((callback) => {
@@ -2036,17 +2084,39 @@ var RebaseWebSocketClient = class {
2036
2084
  return val;
2037
2085
  }
2038
2086
  /**
2087
+ * The address of a row, for matching it against another copy of itself.
2088
+ *
2089
+ * A row is exactly its columns and carries no address, so it is derived
2090
+ * from the key columns the server named — including the ordinary case where
2091
+ * that key is `id`, which the server reports like any other.
2092
+ *
2093
+ * Undefined when there are no keys, which means the server could not
2094
+ * resolve any: such rows genuinely cannot be recognised, and guessing at a
2095
+ * column called `id` would be inventing an identity for a table that has
2096
+ * none.
2097
+ */
2098
+ rowAddress(row, pks) {
2099
+ if (!pks || pks.length === 0) return void 0;
2100
+ const address = buildCompositeId(row, pks);
2101
+ if (!address || address.split(COMPOSITE_ID_SEPARATOR).every((part) => part === "")) return void 0;
2102
+ return address;
2103
+ }
2104
+ /**
2039
2105
  * Merge incoming rows with cached data, preserving cached references
2040
2106
  * for rows whose values haven't changed. This avoids unnecessary
2041
2107
  * React re-renders when the server refetches all rows but most
2042
2108
  * haven't actually changed.
2043
2109
  */
2044
- mergeRows(cached, incoming) {
2110
+ mergeRows(cached, incoming, pks) {
2045
2111
  if (!cached || cached.length === 0) return incoming;
2046
2112
  const cachedById = /* @__PURE__ */ new Map();
2047
- for (const row of cached) cachedById.set(row.id, row);
2113
+ for (const row of cached) {
2114
+ const address = this.rowAddress(row, pks);
2115
+ if (address !== void 0) cachedById.set(address, row);
2116
+ }
2048
2117
  return incoming.map((incomingRow) => {
2049
- const cachedRow = cachedById.get(incomingRow.id);
2118
+ const address = this.rowAddress(incomingRow, pks);
2119
+ const cachedRow = address === void 0 ? void 0 : cachedById.get(address);
2050
2120
  if (!cachedRow) return incomingRow;
2051
2121
  const normCached = this.normalizeForComparison(cachedRow);
2052
2122
  const normIncoming = this.normalizeForComparison(incomingRow);
@@ -2058,7 +2128,7 @@ var RebaseWebSocketClient = class {
2058
2128
  cached: normCached[key],
2059
2129
  incoming: normIncoming[key]
2060
2130
  };
2061
- console.debug(`[RebaseWS] Row ${incomingRow.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
2131
+ console.debug(`[RebaseWS] Row ${address} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
2062
2132
  }
2063
2133
  return incomingRow;
2064
2134
  });
@@ -2079,9 +2149,12 @@ var RebaseWebSocketClient = class {
2079
2149
  console.error("Error in collection subscription callback:", error);
2080
2150
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
2081
2151
  }
2152
+ else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
2082
2153
  return () => {
2083
2154
  callbackMap.delete(callbackId);
2084
2155
  if (callbackMap.size === 0) {
2156
+ if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
2157
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
2085
2158
  this.collectionSubscriptions.delete(subscriptionKey);
2086
2159
  this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
2087
2160
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2103,21 +2176,14 @@ var RebaseWebSocketClient = class {
2103
2176
  props
2104
2177
  });
2105
2178
  this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
2106
- this.sendMessage({
2107
- type: "subscribe_collection",
2108
- payload: {
2109
- ...props,
2110
- subscriptionId: backendSubscriptionId
2111
- }
2112
- }).catch((error) => {
2113
- if (onError) onError(error);
2114
- });
2179
+ this.sendCollectionSubscribe(subscriptionKey);
2115
2180
  return () => {
2116
2181
  const subscription = this.collectionSubscriptions.get(subscriptionKey);
2117
2182
  if (subscription) {
2118
2183
  const callbacks = subscription.callbacks;
2119
2184
  callbacks.delete(callbackId);
2120
2185
  if (callbacks.size === 0) {
2186
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
2121
2187
  this.collectionSubscriptions.delete(subscriptionKey);
2122
2188
  this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
2123
2189
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2144,9 +2210,12 @@ var RebaseWebSocketClient = class {
2144
2210
  console.error("Error in row subscription callback:", error);
2145
2211
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
2146
2212
  }
2213
+ else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
2147
2214
  return () => {
2148
2215
  callbackMap.delete(callbackId);
2149
2216
  if (callbackMap.size === 0) {
2217
+ if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
2218
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
2150
2219
  this.singleSubscriptions.delete(subscriptionKey);
2151
2220
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
2152
2221
  if (this.isConnected && this.ws) this.sendMessage({
@@ -2168,15 +2237,7 @@ var RebaseWebSocketClient = class {
2168
2237
  props
2169
2238
  });
2170
2239
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
2171
- this.sendMessage({
2172
- type: "subscribe_one",
2173
- payload: {
2174
- ...props,
2175
- subscriptionId: backendSubscriptionId
2176
- }
2177
- }).catch((error) => {
2178
- if (onError) onError(error);
2179
- });
2240
+ this.sendEntitySubscribe(subscriptionKey);
2180
2241
  return () => {
2181
2242
  const subscription = this.singleSubscriptions.get(subscriptionKey);
2182
2243
  if (subscription) {
@@ -2194,6 +2255,157 @@ var RebaseWebSocketClient = class {
2194
2255
  };
2195
2256
  }
2196
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
+ /**
2197
2409
  * Re-send all active subscriptions to the backend after a reconnect.
2198
2410
  * The server wipes subscription state when a client disconnects, so
2199
2411
  * we need to re-register everything to resume receiving updates.
@@ -2206,15 +2418,7 @@ var RebaseWebSocketClient = class {
2206
2418
  sub.backendSubscriptionId = newBackendId;
2207
2419
  this.backendToCollectionKey.delete(oldBackendId);
2208
2420
  this.backendToCollectionKey.set(newBackendId, key);
2209
- this.sendMessage({
2210
- type: "subscribe_collection",
2211
- payload: {
2212
- ...sub.props,
2213
- subscriptionId: newBackendId
2214
- }
2215
- }).catch((error) => {
2216
- console.error("[WS] Failed to re-subscribe collection:", key, error);
2217
- });
2421
+ this.sendCollectionSubscribe(key);
2218
2422
  }
2219
2423
  for (const [key, sub] of this.singleSubscriptions.entries()) {
2220
2424
  const oldBackendId = sub.backendSubscriptionId;
@@ -2222,15 +2426,7 @@ var RebaseWebSocketClient = class {
2222
2426
  sub.backendSubscriptionId = newBackendId;
2223
2427
  this.backendToEntityKey.delete(oldBackendId);
2224
2428
  this.backendToEntityKey.set(newBackendId, key);
2225
- this.sendMessage({
2226
- type: "subscribe_one",
2227
- payload: {
2228
- ...sub.props,
2229
- subscriptionId: newBackendId
2230
- }
2231
- }).catch((error) => {
2232
- console.error("[WS] Failed to re-subscribe row:", key, error);
2233
- });
2429
+ this.sendEntitySubscribe(key);
2234
2430
  }
2235
2431
  }
2236
2432
  createCollectionSubscriptionKey(props) {
@@ -2284,6 +2480,7 @@ function createRebaseClient(options) {
2284
2480
  const auth = createAuth(transport, options.auth);
2285
2481
  const admin = createAdmin(transport, options.admin);
2286
2482
  const cron = createCron(transport, options.cron);
2483
+ const backups = createBackups(transport);
2287
2484
  const apiKeys = createApiKeys(transport, options.apiKeys);
2288
2485
  const storage = createStorage(transport);
2289
2486
  const functions = createFunctionsClient(transport);
@@ -2304,7 +2501,7 @@ function createRebaseClient(options) {
2304
2501
  });
2305
2502
  return storageSourcesPromise;
2306
2503
  };
2307
- const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
2504
+ const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
2308
2505
  let ws;
2309
2506
  if (resolvedWsUrl) {
2310
2507
  ws = new RebaseWebSocketClient({
@@ -2408,6 +2605,7 @@ function createRebaseClient(options) {
2408
2605
  auth,
2409
2606
  admin,
2410
2607
  cron,
2608
+ backups,
2411
2609
  apiKeys,
2412
2610
  functions,
2413
2611
  storage,
@@ -2415,6 +2613,16 @@ function createRebaseClient(options) {
2415
2613
  createStorageSource,
2416
2614
  fetchStorageSources,
2417
2615
  ws,
2616
+ /**
2617
+ * Release the realtime socket and its reconnect timer.
2618
+ *
2619
+ * Until this returns, the open socket keeps the Node event loop alive
2620
+ * and the process will not exit on its own. Safe to call when realtime
2621
+ * was never started, and safe to call twice.
2622
+ */
2623
+ close: () => {
2624
+ ws?.disconnect();
2625
+ },
2418
2626
  setToken: transport.setToken,
2419
2627
  setAuthTokenGetter: transport.setAuthTokenGetter,
2420
2628
  setOnUnauthorized: transport.setOnUnauthorized,
@@ -2433,6 +2641,6 @@ function createRebaseClient(options) {
2433
2641
  };
2434
2642
  }
2435
2643
  //#endregion
2436
- export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createCookieStorage, createMemoryStorage, createRebaseClient, or };
2644
+ export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
2437
2645
 
2438
2646
  //# sourceMappingURL=index.es.js.map