@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 +1 -1
- package/dist/admin.d.ts +1 -0
- package/dist/backups.d.ts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.es.js +280 -72
- package/dist/index.es.js.map +1 -1
- package/dist/transport.d.ts +34 -0
- package/dist/websocket.d.ts +57 -1
- package/package.json +8 -9
- package/src/admin.ts +1 -1
- package/src/api-keys.ts +1 -1
- package/src/backups.ts +40 -0
- package/src/collection.ts +16 -0
- package/src/index.ts +33 -2
- package/src/realtime-optout.test.ts +119 -0
- package/src/realtime-row-identity.test.ts +254 -0
- package/src/sdk_query_builder.ts +4 -1
- package/src/transport.ts +34 -0
- package/src/websocket.ts +359 -71
- package/dist/collection.test.d.ts +0 -1
- package/dist/cron.test.d.ts +0 -1
- package/dist/data-proxy.test.d.ts +0 -1
- package/dist/index.umd.js +0 -2484
- package/dist/index.umd.js.map +0 -1
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/
|
|
164
|
+
- [`@rebasepro/app`](../auth) — React hook adapter that wraps `client.auth` for CMS integration
|
package/dist/admin.d.ts
CHANGED
|
@@ -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
|
|
1365
|
-
errorCode
|
|
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-
|
|
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.
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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)
|
|
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
|
|
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 ${
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|