@rebasepro/client 0.9.1-canary.1d2d8b5 → 0.9.1-canary.5809e39
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.d.ts +9 -0
- package/dist/index.es.js +250 -70
- package/dist/index.es.js.map +1 -1
- package/dist/transport.d.ts +34 -0
- package/dist/websocket.d.ts +56 -0
- package/package.json +4 -4
- package/src/collection.ts +16 -0
- package/src/index.ts +26 -1
- 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 +358 -70
- 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.d.ts
CHANGED
|
@@ -74,11 +74,20 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
74
74
|
apiKeys: ReturnType<typeof createApiKeys>;
|
|
75
75
|
functions: ReturnType<typeof createFunctionsClient>;
|
|
76
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;
|
|
77
85
|
storage: StorageSource;
|
|
78
86
|
storageRegistry: StorageSourceRegistry;
|
|
79
87
|
createStorageSource: (storageId: string) => StorageSource;
|
|
80
88
|
fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
|
|
81
89
|
call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
|
|
90
|
+
collection: <M extends Record<string, unknown> = Record<string, unknown>>(slug: string) => CollectionClient<M>;
|
|
82
91
|
data: TypedDataLayer<DB>;
|
|
83
92
|
};
|
|
84
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) {
|
|
@@ -1035,7 +1035,7 @@ var SDKQueryBuilder = class {
|
|
|
1035
1035
|
* Listen to realtime updates matching this query.
|
|
1036
1036
|
*/
|
|
1037
1037
|
listen(onUpdate, onError) {
|
|
1038
|
-
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.");
|
|
1039
1039
|
return this.collection.listen(this.params, onUpdate, onError);
|
|
1040
1040
|
}
|
|
1041
1041
|
};
|
|
@@ -1070,6 +1070,17 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1070
1070
|
body: JSON.stringify(body)
|
|
1071
1071
|
});
|
|
1072
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
|
+
},
|
|
1073
1084
|
async update(id, data) {
|
|
1074
1085
|
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
1075
1086
|
method: "PUT",
|
|
@@ -1386,9 +1397,11 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
|
|
|
1386
1397
|
function extractMessageError(message) {
|
|
1387
1398
|
const payload = message.payload;
|
|
1388
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;
|
|
1389
1402
|
return {
|
|
1390
|
-
errorMessage: typeof
|
|
1391
|
-
errorCode
|
|
1403
|
+
errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
|
|
1404
|
+
errorCode
|
|
1392
1405
|
};
|
|
1393
1406
|
}
|
|
1394
1407
|
/**
|
|
@@ -1424,6 +1437,7 @@ var RebaseWebSocketClient = class {
|
|
|
1424
1437
|
isConnected = false;
|
|
1425
1438
|
messageQueue = [];
|
|
1426
1439
|
requestTimeoutMs = 3e4;
|
|
1440
|
+
subscriptionTimeoutMs = 3e4;
|
|
1427
1441
|
reconnectTimeout = null;
|
|
1428
1442
|
isAuthenticated = false;
|
|
1429
1443
|
authPromise = null;
|
|
@@ -1529,6 +1543,7 @@ var RebaseWebSocketClient = class {
|
|
|
1529
1543
|
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
1530
1544
|
this.processMessageQueue();
|
|
1531
1545
|
if (wasReconnect) this.resubscribeAll();
|
|
1546
|
+
this.armPendingSubscribeWatchdogs();
|
|
1532
1547
|
};
|
|
1533
1548
|
this.ws.onmessage = (event) => {
|
|
1534
1549
|
try {
|
|
@@ -1543,6 +1558,7 @@ var RebaseWebSocketClient = class {
|
|
|
1543
1558
|
this.isConnected = false;
|
|
1544
1559
|
this.isAuthenticated = false;
|
|
1545
1560
|
this.authPromise = null;
|
|
1561
|
+
this.suspendSubscribeWatchdogs();
|
|
1546
1562
|
this.emit("disconnect");
|
|
1547
1563
|
for (const [reqId, request] of this.pendingRequests.entries()) {
|
|
1548
1564
|
if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
|
|
@@ -1574,6 +1590,7 @@ var RebaseWebSocketClient = class {
|
|
|
1574
1590
|
attemptReconnect() {
|
|
1575
1591
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1576
1592
|
console.error("Max reconnection attempts reached");
|
|
1593
|
+
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1577
1594
|
return;
|
|
1578
1595
|
}
|
|
1579
1596
|
this.reconnectAttempts++;
|
|
@@ -1628,29 +1645,18 @@ var RebaseWebSocketClient = class {
|
|
|
1628
1645
|
subscription.backendSubscriptionId = newBackendId;
|
|
1629
1646
|
backendKeyMap.delete(oldBackendId);
|
|
1630
1647
|
backendKeyMap.set(newBackendId, subscriptionKey);
|
|
1631
|
-
this.
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
...subscription.props,
|
|
1635
|
-
subscriptionId: newBackendId
|
|
1636
|
-
}
|
|
1637
|
-
}).catch((error) => {
|
|
1638
|
-
console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
|
|
1639
|
-
subscription.callbacks.forEach((callback) => {
|
|
1640
|
-
if (callback.onError) callback.onError(error);
|
|
1641
|
-
});
|
|
1642
|
-
});
|
|
1643
|
-
} else {
|
|
1644
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1645
|
-
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1646
|
-
subscription.callbacks.forEach((callback) => {
|
|
1647
|
-
if (callback.onError) callback.onError(error);
|
|
1648
|
-
});
|
|
1648
|
+
if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
|
|
1649
|
+
else this.sendEntitySubscribe(subscriptionKey);
|
|
1650
|
+
return;
|
|
1649
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);
|
|
1650
1656
|
}).catch((err) => {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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);
|
|
1654
1660
|
});
|
|
1655
1661
|
}
|
|
1656
1662
|
handleWebSocketMessage(message) {
|
|
@@ -1685,10 +1691,15 @@ var RebaseWebSocketClient = class {
|
|
|
1685
1691
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1686
1692
|
if (collectionSub) {
|
|
1687
1693
|
const incomingRows = message.rows || [];
|
|
1688
|
-
const
|
|
1694
|
+
const updatePks = message.pks;
|
|
1695
|
+
if (updatePks) collectionSub.pks = updatePks;
|
|
1696
|
+
const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
|
|
1689
1697
|
collectionSub.latestData = rows;
|
|
1690
1698
|
collectionSub.lastUpdated = Date.now();
|
|
1691
1699
|
collectionSub.isInitialDataReceived = true;
|
|
1700
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1701
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1702
|
+
collectionSub.subscribeInFlight = false;
|
|
1692
1703
|
collectionSub.callbacks.forEach((callback) => {
|
|
1693
1704
|
try {
|
|
1694
1705
|
callback.onUpdate(rows);
|
|
@@ -1707,12 +1718,14 @@ var RebaseWebSocketClient = class {
|
|
|
1707
1718
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1708
1719
|
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
1709
1720
|
const patchWireEntity = message.row ?? null;
|
|
1710
|
-
const
|
|
1721
|
+
const patchMessage = message;
|
|
1722
|
+
const patchEntityId = patchMessage.id;
|
|
1723
|
+
if (patchMessage.pks) collectionSub.pks = patchMessage.pks;
|
|
1711
1724
|
const patchRow = patchWireEntity ? patchWireEntity : null;
|
|
1712
1725
|
let updated;
|
|
1713
|
-
if (patchRow === null) updated = collectionSub.latestData.filter((e) =>
|
|
1726
|
+
if (patchRow === null) updated = collectionSub.latestData.filter((e) => this.rowAddress(e, collectionSub.pks) !== String(patchEntityId));
|
|
1714
1727
|
else {
|
|
1715
|
-
const idx = collectionSub.latestData.findIndex((e) =>
|
|
1728
|
+
const idx = collectionSub.latestData.findIndex((e) => this.rowAddress(e, collectionSub.pks) === String(patchEntityId));
|
|
1716
1729
|
if (idx >= 0) {
|
|
1717
1730
|
updated = [...collectionSub.latestData];
|
|
1718
1731
|
updated[idx] = patchRow;
|
|
@@ -1742,6 +1755,9 @@ var RebaseWebSocketClient = class {
|
|
|
1742
1755
|
entitySub.latestData = row;
|
|
1743
1756
|
entitySub.lastUpdated = Date.now();
|
|
1744
1757
|
entitySub.isInitialDataReceived = true;
|
|
1758
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1759
|
+
entitySub.subscribeTimeout = void 0;
|
|
1760
|
+
entitySub.subscribeInFlight = false;
|
|
1745
1761
|
entitySub.callbacks.forEach((callback) => {
|
|
1746
1762
|
try {
|
|
1747
1763
|
callback.onUpdate(row);
|
|
@@ -1763,6 +1779,9 @@ var RebaseWebSocketClient = class {
|
|
|
1763
1779
|
this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
|
|
1764
1780
|
return;
|
|
1765
1781
|
}
|
|
1782
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1783
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1784
|
+
collectionSub.subscribeInFlight = false;
|
|
1766
1785
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1767
1786
|
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1768
1787
|
collectionSub.callbacks.forEach((callback) => {
|
|
@@ -1779,6 +1798,9 @@ var RebaseWebSocketClient = class {
|
|
|
1779
1798
|
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
|
|
1780
1799
|
return;
|
|
1781
1800
|
}
|
|
1801
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1802
|
+
entitySub.subscribeTimeout = void 0;
|
|
1803
|
+
entitySub.subscribeInFlight = false;
|
|
1782
1804
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1783
1805
|
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1784
1806
|
entitySub.callbacks.forEach((callback) => {
|
|
@@ -2062,17 +2084,39 @@ var RebaseWebSocketClient = class {
|
|
|
2062
2084
|
return val;
|
|
2063
2085
|
}
|
|
2064
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
|
+
/**
|
|
2065
2105
|
* Merge incoming rows with cached data, preserving cached references
|
|
2066
2106
|
* for rows whose values haven't changed. This avoids unnecessary
|
|
2067
2107
|
* React re-renders when the server refetches all rows but most
|
|
2068
2108
|
* haven't actually changed.
|
|
2069
2109
|
*/
|
|
2070
|
-
mergeRows(cached, incoming) {
|
|
2110
|
+
mergeRows(cached, incoming, pks) {
|
|
2071
2111
|
if (!cached || cached.length === 0) return incoming;
|
|
2072
2112
|
const cachedById = /* @__PURE__ */ new Map();
|
|
2073
|
-
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
|
+
}
|
|
2074
2117
|
return incoming.map((incomingRow) => {
|
|
2075
|
-
const
|
|
2118
|
+
const address = this.rowAddress(incomingRow, pks);
|
|
2119
|
+
const cachedRow = address === void 0 ? void 0 : cachedById.get(address);
|
|
2076
2120
|
if (!cachedRow) return incomingRow;
|
|
2077
2121
|
const normCached = this.normalizeForComparison(cachedRow);
|
|
2078
2122
|
const normIncoming = this.normalizeForComparison(incomingRow);
|
|
@@ -2084,7 +2128,7 @@ var RebaseWebSocketClient = class {
|
|
|
2084
2128
|
cached: normCached[key],
|
|
2085
2129
|
incoming: normIncoming[key]
|
|
2086
2130
|
};
|
|
2087
|
-
console.debug(`[RebaseWS] Row ${
|
|
2131
|
+
console.debug(`[RebaseWS] Row ${address} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
2088
2132
|
}
|
|
2089
2133
|
return incomingRow;
|
|
2090
2134
|
});
|
|
@@ -2105,9 +2149,12 @@ var RebaseWebSocketClient = class {
|
|
|
2105
2149
|
console.error("Error in collection subscription callback:", error);
|
|
2106
2150
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
2107
2151
|
}
|
|
2152
|
+
else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
|
|
2108
2153
|
return () => {
|
|
2109
2154
|
callbackMap.delete(callbackId);
|
|
2110
2155
|
if (callbackMap.size === 0) {
|
|
2156
|
+
if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2157
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
2111
2158
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2112
2159
|
this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
|
|
2113
2160
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2129,21 +2176,14 @@ var RebaseWebSocketClient = class {
|
|
|
2129
2176
|
props
|
|
2130
2177
|
});
|
|
2131
2178
|
this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
|
|
2132
|
-
this.
|
|
2133
|
-
type: "subscribe_collection",
|
|
2134
|
-
payload: {
|
|
2135
|
-
...props,
|
|
2136
|
-
subscriptionId: backendSubscriptionId
|
|
2137
|
-
}
|
|
2138
|
-
}).catch((error) => {
|
|
2139
|
-
if (onError) onError(error);
|
|
2140
|
-
});
|
|
2179
|
+
this.sendCollectionSubscribe(subscriptionKey);
|
|
2141
2180
|
return () => {
|
|
2142
2181
|
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2143
2182
|
if (subscription) {
|
|
2144
2183
|
const callbacks = subscription.callbacks;
|
|
2145
2184
|
callbacks.delete(callbackId);
|
|
2146
2185
|
if (callbacks.size === 0) {
|
|
2186
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2147
2187
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2148
2188
|
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
2149
2189
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2170,9 +2210,12 @@ var RebaseWebSocketClient = class {
|
|
|
2170
2210
|
console.error("Error in row subscription callback:", error);
|
|
2171
2211
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
2172
2212
|
}
|
|
2213
|
+
else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
|
|
2173
2214
|
return () => {
|
|
2174
2215
|
callbackMap.delete(callbackId);
|
|
2175
2216
|
if (callbackMap.size === 0) {
|
|
2217
|
+
if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2218
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
2176
2219
|
this.singleSubscriptions.delete(subscriptionKey);
|
|
2177
2220
|
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
2178
2221
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2194,15 +2237,7 @@ var RebaseWebSocketClient = class {
|
|
|
2194
2237
|
props
|
|
2195
2238
|
});
|
|
2196
2239
|
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
2197
|
-
this.
|
|
2198
|
-
type: "subscribe_one",
|
|
2199
|
-
payload: {
|
|
2200
|
-
...props,
|
|
2201
|
-
subscriptionId: backendSubscriptionId
|
|
2202
|
-
}
|
|
2203
|
-
}).catch((error) => {
|
|
2204
|
-
if (onError) onError(error);
|
|
2205
|
-
});
|
|
2240
|
+
this.sendEntitySubscribe(subscriptionKey);
|
|
2206
2241
|
return () => {
|
|
2207
2242
|
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2208
2243
|
if (subscription) {
|
|
@@ -2220,6 +2255,157 @@ var RebaseWebSocketClient = class {
|
|
|
2220
2255
|
};
|
|
2221
2256
|
}
|
|
2222
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
|
+
/**
|
|
2223
2409
|
* Re-send all active subscriptions to the backend after a reconnect.
|
|
2224
2410
|
* The server wipes subscription state when a client disconnects, so
|
|
2225
2411
|
* we need to re-register everything to resume receiving updates.
|
|
@@ -2232,15 +2418,7 @@ var RebaseWebSocketClient = class {
|
|
|
2232
2418
|
sub.backendSubscriptionId = newBackendId;
|
|
2233
2419
|
this.backendToCollectionKey.delete(oldBackendId);
|
|
2234
2420
|
this.backendToCollectionKey.set(newBackendId, key);
|
|
2235
|
-
this.
|
|
2236
|
-
type: "subscribe_collection",
|
|
2237
|
-
payload: {
|
|
2238
|
-
...sub.props,
|
|
2239
|
-
subscriptionId: newBackendId
|
|
2240
|
-
}
|
|
2241
|
-
}).catch((error) => {
|
|
2242
|
-
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
2243
|
-
});
|
|
2421
|
+
this.sendCollectionSubscribe(key);
|
|
2244
2422
|
}
|
|
2245
2423
|
for (const [key, sub] of this.singleSubscriptions.entries()) {
|
|
2246
2424
|
const oldBackendId = sub.backendSubscriptionId;
|
|
@@ -2248,15 +2426,7 @@ var RebaseWebSocketClient = class {
|
|
|
2248
2426
|
sub.backendSubscriptionId = newBackendId;
|
|
2249
2427
|
this.backendToEntityKey.delete(oldBackendId);
|
|
2250
2428
|
this.backendToEntityKey.set(newBackendId, key);
|
|
2251
|
-
this.
|
|
2252
|
-
type: "subscribe_one",
|
|
2253
|
-
payload: {
|
|
2254
|
-
...sub.props,
|
|
2255
|
-
subscriptionId: newBackendId
|
|
2256
|
-
}
|
|
2257
|
-
}).catch((error) => {
|
|
2258
|
-
console.error("[WS] Failed to re-subscribe row:", key, error);
|
|
2259
|
-
});
|
|
2429
|
+
this.sendEntitySubscribe(key);
|
|
2260
2430
|
}
|
|
2261
2431
|
}
|
|
2262
2432
|
createCollectionSubscriptionKey(props) {
|
|
@@ -2331,7 +2501,7 @@ function createRebaseClient(options) {
|
|
|
2331
2501
|
});
|
|
2332
2502
|
return storageSourcesPromise;
|
|
2333
2503
|
};
|
|
2334
|
-
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
2504
|
+
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
2335
2505
|
let ws;
|
|
2336
2506
|
if (resolvedWsUrl) {
|
|
2337
2507
|
ws = new RebaseWebSocketClient({
|
|
@@ -2443,6 +2613,16 @@ function createRebaseClient(options) {
|
|
|
2443
2613
|
createStorageSource,
|
|
2444
2614
|
fetchStorageSources,
|
|
2445
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
|
+
},
|
|
2446
2626
|
setToken: transport.setToken,
|
|
2447
2627
|
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
2448
2628
|
setOnUnauthorized: transport.setOnUnauthorized,
|