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