@rebasepro/client 0.9.1-canary.eab7ae2 → 0.9.1-canary.ed943fa
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 +2 -0
- package/dist/index.es.js +429 -60
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +89 -0
- package/dist/websocket.d.ts +54 -1
- package/package.json +7 -5
- package/src/index.ts +33 -0
- package/src/realtime-channel.test.ts +241 -0
- package/src/realtime-channel.ts +238 -0
- package/src/websocket.ts +340 -60
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type { CreateBackupsOptions } from "./backups";
|
|
|
27
27
|
export type { ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequest, CreateApiKeysOptions, UpdateApiKeyRequest } from "./api-keys";
|
|
28
28
|
export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
|
|
29
29
|
export { RebaseWebSocketClient } from "./websocket";
|
|
30
|
+
export { RebaseRealtimeChannel } from "./realtime-channel";
|
|
31
|
+
export type { PresenceState, PresenceDiff, BroadcastEvent, ChannelTransport } from "./realtime-channel";
|
|
30
32
|
export interface CreateRebaseClientOptions extends RebaseClientConfig {
|
|
31
33
|
auth?: CreateAuthOptions;
|
|
32
34
|
admin?: CreateAdminOptions;
|
package/dist/index.es.js
CHANGED
|
@@ -1397,9 +1397,11 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
|
|
|
1397
1397
|
function extractMessageError(message) {
|
|
1398
1398
|
const payload = message.payload;
|
|
1399
1399
|
const errPayload = payload?.error;
|
|
1400
|
+
const errorMessage = typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error";
|
|
1401
|
+
const errorCode = typeof errPayload === "object" ? errPayload.code : payload?.code;
|
|
1400
1402
|
return {
|
|
1401
|
-
errorMessage: typeof
|
|
1402
|
-
errorCode
|
|
1403
|
+
errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
|
|
1404
|
+
errorCode
|
|
1403
1405
|
};
|
|
1404
1406
|
}
|
|
1405
1407
|
/**
|
|
@@ -1417,6 +1419,23 @@ var RebaseWebSocketClient = class {
|
|
|
1417
1419
|
getAuthToken;
|
|
1418
1420
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1419
1421
|
listeners = /* @__PURE__ */ new Map();
|
|
1422
|
+
/** Channel-name → handlers, for broadcast and presence frames. */
|
|
1423
|
+
channelHandlers = /* @__PURE__ */ new Map();
|
|
1424
|
+
/** Subscribe to broadcast/presence frames for one channel. */
|
|
1425
|
+
onChannelMessage(channel, handler) {
|
|
1426
|
+
if (!this.channelHandlers.has(channel)) this.channelHandlers.set(channel, /* @__PURE__ */ new Set());
|
|
1427
|
+
this.channelHandlers.get(channel).add(handler);
|
|
1428
|
+
return () => {
|
|
1429
|
+
const handlers = this.channelHandlers.get(channel);
|
|
1430
|
+
if (!handlers) return;
|
|
1431
|
+
handlers.delete(handler);
|
|
1432
|
+
if (handlers.size === 0) this.channelHandlers.delete(channel);
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
/** Notified after the socket comes back, so channels can re-join. */
|
|
1436
|
+
onReconnect(handler) {
|
|
1437
|
+
return this.on("reconnect", handler);
|
|
1438
|
+
}
|
|
1420
1439
|
on(event, cb) {
|
|
1421
1440
|
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1422
1441
|
this.listeners.get(event).add(cb);
|
|
@@ -1435,6 +1454,7 @@ var RebaseWebSocketClient = class {
|
|
|
1435
1454
|
isConnected = false;
|
|
1436
1455
|
messageQueue = [];
|
|
1437
1456
|
requestTimeoutMs = 3e4;
|
|
1457
|
+
subscriptionTimeoutMs = 3e4;
|
|
1438
1458
|
reconnectTimeout = null;
|
|
1439
1459
|
isAuthenticated = false;
|
|
1440
1460
|
authPromise = null;
|
|
@@ -1540,6 +1560,7 @@ var RebaseWebSocketClient = class {
|
|
|
1540
1560
|
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
1541
1561
|
this.processMessageQueue();
|
|
1542
1562
|
if (wasReconnect) this.resubscribeAll();
|
|
1563
|
+
this.armPendingSubscribeWatchdogs();
|
|
1543
1564
|
};
|
|
1544
1565
|
this.ws.onmessage = (event) => {
|
|
1545
1566
|
try {
|
|
@@ -1554,6 +1575,7 @@ var RebaseWebSocketClient = class {
|
|
|
1554
1575
|
this.isConnected = false;
|
|
1555
1576
|
this.isAuthenticated = false;
|
|
1556
1577
|
this.authPromise = null;
|
|
1578
|
+
this.suspendSubscribeWatchdogs();
|
|
1557
1579
|
this.emit("disconnect");
|
|
1558
1580
|
for (const [reqId, request] of this.pendingRequests.entries()) {
|
|
1559
1581
|
if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
|
|
@@ -1585,6 +1607,7 @@ var RebaseWebSocketClient = class {
|
|
|
1585
1607
|
attemptReconnect() {
|
|
1586
1608
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1587
1609
|
console.error("Max reconnection attempts reached");
|
|
1610
|
+
this.failAllPendingSubscriptions(new RebaseApiError$1("Connection lost", { code: "CONNECTION_LOST" }));
|
|
1588
1611
|
return;
|
|
1589
1612
|
}
|
|
1590
1613
|
this.reconnectAttempts++;
|
|
@@ -1639,29 +1662,18 @@ var RebaseWebSocketClient = class {
|
|
|
1639
1662
|
subscription.backendSubscriptionId = newBackendId;
|
|
1640
1663
|
backendKeyMap.delete(oldBackendId);
|
|
1641
1664
|
backendKeyMap.set(newBackendId, subscriptionKey);
|
|
1642
|
-
this.
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
...subscription.props,
|
|
1646
|
-
subscriptionId: newBackendId
|
|
1647
|
-
}
|
|
1648
|
-
}).catch((error) => {
|
|
1649
|
-
console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
|
|
1650
|
-
subscription.callbacks.forEach((callback) => {
|
|
1651
|
-
if (callback.onError) callback.onError(error);
|
|
1652
|
-
});
|
|
1653
|
-
});
|
|
1654
|
-
} else {
|
|
1655
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1656
|
-
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1657
|
-
subscription.callbacks.forEach((callback) => {
|
|
1658
|
-
if (callback.onError) callback.onError(error);
|
|
1659
|
-
});
|
|
1665
|
+
if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
|
|
1666
|
+
else this.sendEntitySubscribe(subscriptionKey);
|
|
1667
|
+
return;
|
|
1660
1668
|
}
|
|
1669
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1670
|
+
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1671
|
+
if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
|
|
1672
|
+
else this.failEntitySubscription(subscriptionKey, error);
|
|
1661
1673
|
}).catch((err) => {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1674
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1675
|
+
if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
|
|
1676
|
+
else this.failEntitySubscription(subscriptionKey, error);
|
|
1665
1677
|
});
|
|
1666
1678
|
}
|
|
1667
1679
|
handleWebSocketMessage(message) {
|
|
@@ -1690,6 +1702,15 @@ var RebaseWebSocketClient = class {
|
|
|
1690
1702
|
}
|
|
1691
1703
|
return;
|
|
1692
1704
|
}
|
|
1705
|
+
if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff")) {
|
|
1706
|
+
const handlers = this.channelHandlers.get(message.channel);
|
|
1707
|
+
if (handlers) for (const handler of [...handlers]) try {
|
|
1708
|
+
handler(message);
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.error("Error in channel handler:", error);
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1693
1714
|
if (subscriptionId && type === "collection_update") {
|
|
1694
1715
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1695
1716
|
if (subscriptionKey) {
|
|
@@ -1702,6 +1723,9 @@ var RebaseWebSocketClient = class {
|
|
|
1702
1723
|
collectionSub.latestData = rows;
|
|
1703
1724
|
collectionSub.lastUpdated = Date.now();
|
|
1704
1725
|
collectionSub.isInitialDataReceived = true;
|
|
1726
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1727
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1728
|
+
collectionSub.subscribeInFlight = false;
|
|
1705
1729
|
collectionSub.callbacks.forEach((callback) => {
|
|
1706
1730
|
try {
|
|
1707
1731
|
callback.onUpdate(rows);
|
|
@@ -1757,6 +1781,9 @@ var RebaseWebSocketClient = class {
|
|
|
1757
1781
|
entitySub.latestData = row;
|
|
1758
1782
|
entitySub.lastUpdated = Date.now();
|
|
1759
1783
|
entitySub.isInitialDataReceived = true;
|
|
1784
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1785
|
+
entitySub.subscribeTimeout = void 0;
|
|
1786
|
+
entitySub.subscribeInFlight = false;
|
|
1760
1787
|
entitySub.callbacks.forEach((callback) => {
|
|
1761
1788
|
try {
|
|
1762
1789
|
callback.onUpdate(row);
|
|
@@ -1778,6 +1805,9 @@ var RebaseWebSocketClient = class {
|
|
|
1778
1805
|
this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
|
|
1779
1806
|
return;
|
|
1780
1807
|
}
|
|
1808
|
+
if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
|
|
1809
|
+
collectionSub.subscribeTimeout = void 0;
|
|
1810
|
+
collectionSub.subscribeInFlight = false;
|
|
1781
1811
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1782
1812
|
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1783
1813
|
collectionSub.callbacks.forEach((callback) => {
|
|
@@ -1794,6 +1824,9 @@ var RebaseWebSocketClient = class {
|
|
|
1794
1824
|
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
|
|
1795
1825
|
return;
|
|
1796
1826
|
}
|
|
1827
|
+
if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
|
|
1828
|
+
entitySub.subscribeTimeout = void 0;
|
|
1829
|
+
entitySub.subscribeInFlight = false;
|
|
1797
1830
|
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1798
1831
|
const error = new RebaseApiError$1(errorMessage, { code: errorCode });
|
|
1799
1832
|
entitySub.callbacks.forEach((callback) => {
|
|
@@ -1866,6 +1899,10 @@ var RebaseWebSocketClient = class {
|
|
|
1866
1899
|
throw error;
|
|
1867
1900
|
}
|
|
1868
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Public because `RebaseRealtimeChannel` sends channel frames through it.
|
|
1904
|
+
* Not part of the stable surface — prefer `client.realtime.channel(name)`.
|
|
1905
|
+
*/
|
|
1869
1906
|
sendMessage(message) {
|
|
1870
1907
|
const queuedMsg = message;
|
|
1871
1908
|
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
@@ -2142,9 +2179,12 @@ var RebaseWebSocketClient = class {
|
|
|
2142
2179
|
console.error("Error in collection subscription callback:", error);
|
|
2143
2180
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
2144
2181
|
}
|
|
2182
|
+
else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
|
|
2145
2183
|
return () => {
|
|
2146
2184
|
callbackMap.delete(callbackId);
|
|
2147
2185
|
if (callbackMap.size === 0) {
|
|
2186
|
+
if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2187
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
2148
2188
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2149
2189
|
this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
|
|
2150
2190
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2166,21 +2206,14 @@ var RebaseWebSocketClient = class {
|
|
|
2166
2206
|
props
|
|
2167
2207
|
});
|
|
2168
2208
|
this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
|
|
2169
|
-
this.
|
|
2170
|
-
type: "subscribe_collection",
|
|
2171
|
-
payload: {
|
|
2172
|
-
...props,
|
|
2173
|
-
subscriptionId: backendSubscriptionId
|
|
2174
|
-
}
|
|
2175
|
-
}).catch((error) => {
|
|
2176
|
-
if (onError) onError(error);
|
|
2177
|
-
});
|
|
2209
|
+
this.sendCollectionSubscribe(subscriptionKey);
|
|
2178
2210
|
return () => {
|
|
2179
2211
|
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2180
2212
|
if (subscription) {
|
|
2181
2213
|
const callbacks = subscription.callbacks;
|
|
2182
2214
|
callbacks.delete(callbackId);
|
|
2183
2215
|
if (callbacks.size === 0) {
|
|
2216
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2184
2217
|
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2185
2218
|
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
2186
2219
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2207,9 +2240,12 @@ var RebaseWebSocketClient = class {
|
|
|
2207
2240
|
console.error("Error in row subscription callback:", error);
|
|
2208
2241
|
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
2209
2242
|
}
|
|
2243
|
+
else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
|
|
2210
2244
|
return () => {
|
|
2211
2245
|
callbackMap.delete(callbackId);
|
|
2212
2246
|
if (callbackMap.size === 0) {
|
|
2247
|
+
if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
|
|
2248
|
+
if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
|
|
2213
2249
|
this.singleSubscriptions.delete(subscriptionKey);
|
|
2214
2250
|
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
2215
2251
|
if (this.isConnected && this.ws) this.sendMessage({
|
|
@@ -2231,15 +2267,7 @@ var RebaseWebSocketClient = class {
|
|
|
2231
2267
|
props
|
|
2232
2268
|
});
|
|
2233
2269
|
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
2234
|
-
this.
|
|
2235
|
-
type: "subscribe_one",
|
|
2236
|
-
payload: {
|
|
2237
|
-
...props,
|
|
2238
|
-
subscriptionId: backendSubscriptionId
|
|
2239
|
-
}
|
|
2240
|
-
}).catch((error) => {
|
|
2241
|
-
if (onError) onError(error);
|
|
2242
|
-
});
|
|
2270
|
+
this.sendEntitySubscribe(subscriptionKey);
|
|
2243
2271
|
return () => {
|
|
2244
2272
|
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2245
2273
|
if (subscription) {
|
|
@@ -2257,6 +2285,157 @@ var RebaseWebSocketClient = class {
|
|
|
2257
2285
|
};
|
|
2258
2286
|
}
|
|
2259
2287
|
/**
|
|
2288
|
+
* Send a `subscribe_collection` for an already-registered subscription and
|
|
2289
|
+
* arm its watchdog.
|
|
2290
|
+
*
|
|
2291
|
+
* Every path that registers a collection subscription goes through here, so
|
|
2292
|
+
* that a subscribe which never lands — a rejected send, or a server that
|
|
2293
|
+
* never answers — always ends up in `failCollectionSubscription` rather than
|
|
2294
|
+
* leaving the entry parked with `isInitialDataReceived === false` forever.
|
|
2295
|
+
*/
|
|
2296
|
+
sendCollectionSubscribe(subscriptionKey) {
|
|
2297
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2298
|
+
if (!subscription) return;
|
|
2299
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2300
|
+
subscription.subscribeInFlight = true;
|
|
2301
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2302
|
+
subscription.subscribeTimeout = void 0;
|
|
2303
|
+
if (this.isConnected) this.sendCollectionSubscribeWatchdog(subscriptionKey);
|
|
2304
|
+
this.sendMessage({
|
|
2305
|
+
type: "subscribe_collection",
|
|
2306
|
+
payload: {
|
|
2307
|
+
...subscription.props,
|
|
2308
|
+
subscriptionId: backendSubscriptionId
|
|
2309
|
+
}
|
|
2310
|
+
}).catch((error) => {
|
|
2311
|
+
const current = this.collectionSubscriptions.get(subscriptionKey);
|
|
2312
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2313
|
+
this.failCollectionSubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
/** The `listenOne` counterpart of {@link sendCollectionSubscribe}. */
|
|
2317
|
+
sendEntitySubscribe(subscriptionKey) {
|
|
2318
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2319
|
+
if (!subscription) return;
|
|
2320
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2321
|
+
subscription.subscribeInFlight = true;
|
|
2322
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2323
|
+
subscription.subscribeTimeout = void 0;
|
|
2324
|
+
if (this.isConnected) this.sendEntitySubscribeWatchdog(subscriptionKey);
|
|
2325
|
+
this.sendMessage({
|
|
2326
|
+
type: "subscribe_one",
|
|
2327
|
+
payload: {
|
|
2328
|
+
...subscription.props,
|
|
2329
|
+
subscriptionId: backendSubscriptionId
|
|
2330
|
+
}
|
|
2331
|
+
}).catch((error) => {
|
|
2332
|
+
const current = this.singleSubscriptions.get(subscriptionKey);
|
|
2333
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2334
|
+
this.failEntitySubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Report a subscribe failure to every listener and drop the registration.
|
|
2339
|
+
*
|
|
2340
|
+
* Dropping it is the point: the callbacks stay live (their components are
|
|
2341
|
+
* still mounted and have been told), but the next `listenCollection` for
|
|
2342
|
+
* these params finds no entry and issues a fresh subscribe instead of
|
|
2343
|
+
* silently attaching to a dead one.
|
|
2344
|
+
*/
|
|
2345
|
+
failCollectionSubscription(subscriptionKey, error) {
|
|
2346
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2347
|
+
if (!subscription) return;
|
|
2348
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2349
|
+
subscription.subscribeInFlight = false;
|
|
2350
|
+
this.collectionSubscriptions.delete(subscriptionKey);
|
|
2351
|
+
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
2352
|
+
subscription.callbacks.forEach((callback) => {
|
|
2353
|
+
if (callback.onError) try {
|
|
2354
|
+
callback.onError(error);
|
|
2355
|
+
} catch (callbackError) {
|
|
2356
|
+
console.error("Error in collection subscription error callback:", callbackError);
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
/** The `listenOne` counterpart of {@link failCollectionSubscription}. */
|
|
2361
|
+
failEntitySubscription(subscriptionKey, error) {
|
|
2362
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2363
|
+
if (!subscription) return;
|
|
2364
|
+
if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
|
|
2365
|
+
subscription.subscribeInFlight = false;
|
|
2366
|
+
this.singleSubscriptions.delete(subscriptionKey);
|
|
2367
|
+
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
2368
|
+
subscription.callbacks.forEach((callback) => {
|
|
2369
|
+
if (callback.onError) try {
|
|
2370
|
+
callback.onError(error);
|
|
2371
|
+
} catch (callbackError) {
|
|
2372
|
+
console.error("Error in row subscription error callback:", callbackError);
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Stop the watchdogs without failing anything — used when the socket drops,
|
|
2378
|
+
* since the reconnect path re-subscribes everything anyway and a watchdog
|
|
2379
|
+
* firing mid-reconnect would tear down healthy subscriptions.
|
|
2380
|
+
*/
|
|
2381
|
+
suspendSubscribeWatchdogs() {
|
|
2382
|
+
for (const sub of this.collectionSubscriptions.values()) {
|
|
2383
|
+
if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
|
|
2384
|
+
sub.subscribeTimeout = void 0;
|
|
2385
|
+
sub.subscribeInFlight = false;
|
|
2386
|
+
}
|
|
2387
|
+
for (const sub of this.singleSubscriptions.values()) {
|
|
2388
|
+
if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
|
|
2389
|
+
sub.subscribeTimeout = void 0;
|
|
2390
|
+
sub.subscribeInFlight = false;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Arm watchdogs for subscribes that were requested while offline and have
|
|
2395
|
+
* just been flushed to the socket. Their timers were deliberately not set at
|
|
2396
|
+
* request time, so without this they would have no timeout at all.
|
|
2397
|
+
*/
|
|
2398
|
+
armPendingSubscribeWatchdogs() {
|
|
2399
|
+
for (const [key, sub] of this.collectionSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendCollectionSubscribeWatchdog(key);
|
|
2400
|
+
for (const [key, sub] of this.singleSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendEntitySubscribeWatchdog(key);
|
|
2401
|
+
}
|
|
2402
|
+
sendCollectionSubscribeWatchdog(subscriptionKey) {
|
|
2403
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
2404
|
+
if (!subscription) return;
|
|
2405
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2406
|
+
subscription.subscribeTimeout = setTimeout(() => {
|
|
2407
|
+
const current = this.collectionSubscriptions.get(subscriptionKey);
|
|
2408
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2409
|
+
if (!current.subscribeInFlight) return;
|
|
2410
|
+
this.failCollectionSubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
|
|
2411
|
+
}, this.subscriptionTimeoutMs);
|
|
2412
|
+
}
|
|
2413
|
+
sendEntitySubscribeWatchdog(subscriptionKey) {
|
|
2414
|
+
const subscription = this.singleSubscriptions.get(subscriptionKey);
|
|
2415
|
+
if (!subscription) return;
|
|
2416
|
+
const backendSubscriptionId = subscription.backendSubscriptionId;
|
|
2417
|
+
subscription.subscribeTimeout = setTimeout(() => {
|
|
2418
|
+
const current = this.singleSubscriptions.get(subscriptionKey);
|
|
2419
|
+
if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
|
|
2420
|
+
if (!current.subscribeInFlight) return;
|
|
2421
|
+
this.failEntitySubscription(subscriptionKey, new RebaseApiError$1("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
|
|
2422
|
+
}, this.subscriptionTimeoutMs);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Fail every subscription that never received data. Called when reconnection
|
|
2426
|
+
* is given up on, so views surface an error instead of spinning forever.
|
|
2427
|
+
*/
|
|
2428
|
+
failAllPendingSubscriptions(error) {
|
|
2429
|
+
for (const key of [...this.collectionSubscriptions.keys()]) {
|
|
2430
|
+
const sub = this.collectionSubscriptions.get(key);
|
|
2431
|
+
if (sub && !sub.isInitialDataReceived) this.failCollectionSubscription(key, error);
|
|
2432
|
+
}
|
|
2433
|
+
for (const key of [...this.singleSubscriptions.keys()]) {
|
|
2434
|
+
const sub = this.singleSubscriptions.get(key);
|
|
2435
|
+
if (sub && !sub.isInitialDataReceived) this.failEntitySubscription(key, error);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2260
2439
|
* Re-send all active subscriptions to the backend after a reconnect.
|
|
2261
2440
|
* The server wipes subscription state when a client disconnects, so
|
|
2262
2441
|
* we need to re-register everything to resume receiving updates.
|
|
@@ -2269,15 +2448,7 @@ var RebaseWebSocketClient = class {
|
|
|
2269
2448
|
sub.backendSubscriptionId = newBackendId;
|
|
2270
2449
|
this.backendToCollectionKey.delete(oldBackendId);
|
|
2271
2450
|
this.backendToCollectionKey.set(newBackendId, key);
|
|
2272
|
-
this.
|
|
2273
|
-
type: "subscribe_collection",
|
|
2274
|
-
payload: {
|
|
2275
|
-
...sub.props,
|
|
2276
|
-
subscriptionId: newBackendId
|
|
2277
|
-
}
|
|
2278
|
-
}).catch((error) => {
|
|
2279
|
-
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
2280
|
-
});
|
|
2451
|
+
this.sendCollectionSubscribe(key);
|
|
2281
2452
|
}
|
|
2282
2453
|
for (const [key, sub] of this.singleSubscriptions.entries()) {
|
|
2283
2454
|
const oldBackendId = sub.backendSubscriptionId;
|
|
@@ -2285,15 +2456,7 @@ var RebaseWebSocketClient = class {
|
|
|
2285
2456
|
sub.backendSubscriptionId = newBackendId;
|
|
2286
2457
|
this.backendToEntityKey.delete(oldBackendId);
|
|
2287
2458
|
this.backendToEntityKey.set(newBackendId, key);
|
|
2288
|
-
this.
|
|
2289
|
-
type: "subscribe_one",
|
|
2290
|
-
payload: {
|
|
2291
|
-
...sub.props,
|
|
2292
|
-
subscriptionId: newBackendId
|
|
2293
|
-
}
|
|
2294
|
-
}).catch((error) => {
|
|
2295
|
-
console.error("[WS] Failed to re-subscribe row:", key, error);
|
|
2296
|
-
});
|
|
2459
|
+
this.sendEntitySubscribe(key);
|
|
2297
2460
|
}
|
|
2298
2461
|
}
|
|
2299
2462
|
createCollectionSubscriptionKey(props) {
|
|
@@ -2320,6 +2483,190 @@ var RebaseWebSocketClient = class {
|
|
|
2320
2483
|
}
|
|
2321
2484
|
};
|
|
2322
2485
|
//#endregion
|
|
2486
|
+
//#region src/realtime-channel.ts
|
|
2487
|
+
/**
|
|
2488
|
+
* Re-send presence comfortably inside the server's 30s expiry.
|
|
2489
|
+
*
|
|
2490
|
+
* Two-thirds of the window: one lost heartbeat still leaves time for the next
|
|
2491
|
+
* before the entry is reaped, so a single dropped frame is not a disappearance.
|
|
2492
|
+
*/
|
|
2493
|
+
var PRESENCE_HEARTBEAT_MS = 2e4;
|
|
2494
|
+
var RebaseRealtimeChannel = class {
|
|
2495
|
+
name;
|
|
2496
|
+
transport;
|
|
2497
|
+
presenceHandlers = /* @__PURE__ */ new Set();
|
|
2498
|
+
broadcastHandlers = /* @__PURE__ */ new Set();
|
|
2499
|
+
unsubscribers = [];
|
|
2500
|
+
/** Last known roster, kept so handlers always get a full picture. */
|
|
2501
|
+
presences = {};
|
|
2502
|
+
/** What this client last tracked, replayed on reconnect and heartbeat. */
|
|
2503
|
+
trackedState = null;
|
|
2504
|
+
heartbeat = null;
|
|
2505
|
+
joined = false;
|
|
2506
|
+
constructor(name, transport) {
|
|
2507
|
+
this.name = name;
|
|
2508
|
+
this.transport = transport;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Join the channel and ask for the current roster.
|
|
2512
|
+
*
|
|
2513
|
+
* Called automatically by `track`, `broadcast`, `onPresence` and
|
|
2514
|
+
* `onBroadcast`; calling it directly is only needed to start receiving
|
|
2515
|
+
* before there is anything to send.
|
|
2516
|
+
*/
|
|
2517
|
+
async join() {
|
|
2518
|
+
if (this.joined) return;
|
|
2519
|
+
this.joined = true;
|
|
2520
|
+
this.unsubscribers.push(this.transport.onChannelMessage(this.name, (message) => this.handle(message)));
|
|
2521
|
+
this.unsubscribers.push(this.transport.onReconnect(() => {
|
|
2522
|
+
this.rejoin();
|
|
2523
|
+
}));
|
|
2524
|
+
await this.transport.sendMessage({
|
|
2525
|
+
type: "join_channel",
|
|
2526
|
+
channel: this.name
|
|
2527
|
+
});
|
|
2528
|
+
await this.transport.sendMessage({
|
|
2529
|
+
type: "presence_state",
|
|
2530
|
+
channel: this.name
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
async rejoin() {
|
|
2534
|
+
try {
|
|
2535
|
+
await this.transport.sendMessage({
|
|
2536
|
+
type: "join_channel",
|
|
2537
|
+
channel: this.name
|
|
2538
|
+
});
|
|
2539
|
+
await this.transport.sendMessage({
|
|
2540
|
+
type: "presence_state",
|
|
2541
|
+
channel: this.name
|
|
2542
|
+
});
|
|
2543
|
+
if (this.trackedState) await this.transport.sendMessage({
|
|
2544
|
+
type: "presence_track",
|
|
2545
|
+
channel: this.name,
|
|
2546
|
+
state: this.trackedState
|
|
2547
|
+
});
|
|
2548
|
+
} catch {}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Publish this client's presence state, and keep publishing it.
|
|
2552
|
+
*
|
|
2553
|
+
* Calling `track` again replaces the state (and restarts the heartbeat),
|
|
2554
|
+
* which is how you update e.g. a cursor position.
|
|
2555
|
+
*/
|
|
2556
|
+
async track(state) {
|
|
2557
|
+
await this.join();
|
|
2558
|
+
this.trackedState = state;
|
|
2559
|
+
await this.transport.sendMessage({
|
|
2560
|
+
type: "presence_track",
|
|
2561
|
+
channel: this.name,
|
|
2562
|
+
state
|
|
2563
|
+
});
|
|
2564
|
+
if (!this.heartbeat) {
|
|
2565
|
+
this.heartbeat = setInterval(() => {
|
|
2566
|
+
if (!this.trackedState) return;
|
|
2567
|
+
this.transport.sendMessage({
|
|
2568
|
+
type: "presence_track",
|
|
2569
|
+
channel: this.name,
|
|
2570
|
+
state: this.trackedState
|
|
2571
|
+
}).catch(() => {});
|
|
2572
|
+
}, PRESENCE_HEARTBEAT_MS);
|
|
2573
|
+
this.heartbeat.unref?.();
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
/** Stop publishing presence, without leaving the channel. */
|
|
2577
|
+
async untrack() {
|
|
2578
|
+
this.stopHeartbeat();
|
|
2579
|
+
this.trackedState = null;
|
|
2580
|
+
if (this.joined) await this.transport.sendMessage({
|
|
2581
|
+
type: "presence_untrack",
|
|
2582
|
+
channel: this.name
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Observe the roster. The handler fires immediately with what is already
|
|
2587
|
+
* known, then on every change.
|
|
2588
|
+
*/
|
|
2589
|
+
onPresence(handler) {
|
|
2590
|
+
this.presenceHandlers.add(handler);
|
|
2591
|
+
this.join();
|
|
2592
|
+
if (Object.keys(this.presences).length > 0) handler({ ...this.presences });
|
|
2593
|
+
return () => this.presenceHandlers.delete(handler);
|
|
2594
|
+
}
|
|
2595
|
+
/** Send a broadcast. The sender does not receive its own message. */
|
|
2596
|
+
async broadcast(event, payload) {
|
|
2597
|
+
await this.join();
|
|
2598
|
+
await this.transport.sendMessage({
|
|
2599
|
+
type: "broadcast",
|
|
2600
|
+
channel: this.name,
|
|
2601
|
+
event,
|
|
2602
|
+
payload
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
onBroadcast(eventOrHandler, maybeHandler) {
|
|
2606
|
+
const wrapped = typeof eventOrHandler === "string" ? (e) => {
|
|
2607
|
+
if (e.event === eventOrHandler) maybeHandler(e.payload);
|
|
2608
|
+
} : eventOrHandler;
|
|
2609
|
+
this.broadcastHandlers.add(wrapped);
|
|
2610
|
+
this.join();
|
|
2611
|
+
return () => this.broadcastHandlers.delete(wrapped);
|
|
2612
|
+
}
|
|
2613
|
+
/** Leave the channel and release every listener and timer. */
|
|
2614
|
+
async leave() {
|
|
2615
|
+
this.stopHeartbeat();
|
|
2616
|
+
this.trackedState = null;
|
|
2617
|
+
this.presences = {};
|
|
2618
|
+
this.presenceHandlers.clear();
|
|
2619
|
+
this.broadcastHandlers.clear();
|
|
2620
|
+
for (const off of this.unsubscribers) off();
|
|
2621
|
+
this.unsubscribers = [];
|
|
2622
|
+
if (this.joined) {
|
|
2623
|
+
this.joined = false;
|
|
2624
|
+
await this.transport.sendMessage({
|
|
2625
|
+
type: "leave_channel",
|
|
2626
|
+
channel: this.name
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
stopHeartbeat() {
|
|
2631
|
+
if (this.heartbeat) {
|
|
2632
|
+
clearInterval(this.heartbeat);
|
|
2633
|
+
this.heartbeat = null;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
/** Fold an incoming frame into the roster and fan it out. */
|
|
2637
|
+
handle(message) {
|
|
2638
|
+
switch (message.type) {
|
|
2639
|
+
case "presence_state":
|
|
2640
|
+
this.presences = message.presences ?? {};
|
|
2641
|
+
this.emitPresence();
|
|
2642
|
+
break;
|
|
2643
|
+
case "presence_diff": {
|
|
2644
|
+
const joins = message.joins ?? {};
|
|
2645
|
+
const leaves = message.leaves ?? {};
|
|
2646
|
+
for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
|
|
2647
|
+
for (const id of Object.keys(leaves)) delete this.presences[id];
|
|
2648
|
+
this.emitPresence({
|
|
2649
|
+
joins,
|
|
2650
|
+
leaves
|
|
2651
|
+
});
|
|
2652
|
+
break;
|
|
2653
|
+
}
|
|
2654
|
+
case "broadcast": {
|
|
2655
|
+
const event = {
|
|
2656
|
+
event: message.event,
|
|
2657
|
+
payload: message.payload
|
|
2658
|
+
};
|
|
2659
|
+
for (const handler of this.broadcastHandlers) handler(event);
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
emitPresence(diff) {
|
|
2665
|
+
const snapshot = { ...this.presences };
|
|
2666
|
+
for (const handler of this.presenceHandlers) handler(snapshot, diff);
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
//#endregion
|
|
2323
2670
|
//#region src/index.ts
|
|
2324
2671
|
/**
|
|
2325
2672
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
@@ -2370,6 +2717,8 @@ function createRebaseClient(options) {
|
|
|
2370
2717
|
};
|
|
2371
2718
|
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
2372
2719
|
let ws;
|
|
2720
|
+
/** One channel object per name — see `realtime.channel`. */
|
|
2721
|
+
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
2373
2722
|
if (resolvedWsUrl) {
|
|
2374
2723
|
ws = new RebaseWebSocketClient({
|
|
2375
2724
|
websocketUrl: resolvedWsUrl,
|
|
@@ -2480,6 +2829,24 @@ function createRebaseClient(options) {
|
|
|
2480
2829
|
createStorageSource,
|
|
2481
2830
|
fetchStorageSources,
|
|
2482
2831
|
ws,
|
|
2832
|
+
realtime: {
|
|
2833
|
+
/**
|
|
2834
|
+
* Join a broadcast/presence channel.
|
|
2835
|
+
*
|
|
2836
|
+
* Repeated calls with the same name return the same channel, so
|
|
2837
|
+
* separate components can attach handlers without each opening its
|
|
2838
|
+
* own membership — and `leave()` from one would otherwise silently
|
|
2839
|
+
* cut off the others.
|
|
2840
|
+
*/
|
|
2841
|
+
channel: (name) => {
|
|
2842
|
+
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
2843
|
+
let existing = realtimeChannels.get(name);
|
|
2844
|
+
if (!existing) {
|
|
2845
|
+
existing = new RebaseRealtimeChannel(name, ws);
|
|
2846
|
+
realtimeChannels.set(name, existing);
|
|
2847
|
+
}
|
|
2848
|
+
return existing;
|
|
2849
|
+
} },
|
|
2483
2850
|
/**
|
|
2484
2851
|
* Release the realtime socket and its reconnect timer.
|
|
2485
2852
|
*
|
|
@@ -2488,6 +2855,8 @@ function createRebaseClient(options) {
|
|
|
2488
2855
|
* was never started, and safe to call twice.
|
|
2489
2856
|
*/
|
|
2490
2857
|
close: () => {
|
|
2858
|
+
for (const channel of realtimeChannels.values()) channel.leave();
|
|
2859
|
+
realtimeChannels.clear();
|
|
2491
2860
|
ws?.disconnect();
|
|
2492
2861
|
},
|
|
2493
2862
|
setToken: transport.setToken,
|
|
@@ -2508,6 +2877,6 @@ function createRebaseClient(options) {
|
|
|
2508
2877
|
};
|
|
2509
2878
|
}
|
|
2510
2879
|
//#endregion
|
|
2511
|
-
export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
|
|
2880
|
+
export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
|
|
2512
2881
|
|
|
2513
2882
|
//# sourceMappingURL=index.es.js.map
|