@rebasepro/client 0.9.1-canary.e3f810f → 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 +237 -1
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +89 -0
- package/dist/websocket.d.ts +11 -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 +44 -1
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
|
@@ -1419,6 +1419,23 @@ var RebaseWebSocketClient = class {
|
|
|
1419
1419
|
getAuthToken;
|
|
1420
1420
|
subscriptions = /* @__PURE__ */ new Map();
|
|
1421
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
|
+
}
|
|
1422
1439
|
on(event, cb) {
|
|
1423
1440
|
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1424
1441
|
this.listeners.get(event).add(cb);
|
|
@@ -1685,6 +1702,15 @@ var RebaseWebSocketClient = class {
|
|
|
1685
1702
|
}
|
|
1686
1703
|
return;
|
|
1687
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
|
+
}
|
|
1688
1714
|
if (subscriptionId && type === "collection_update") {
|
|
1689
1715
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1690
1716
|
if (subscriptionKey) {
|
|
@@ -1873,6 +1899,10 @@ var RebaseWebSocketClient = class {
|
|
|
1873
1899
|
throw error;
|
|
1874
1900
|
}
|
|
1875
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Public because `RebaseRealtimeChannel` sends channel frames through it.
|
|
1904
|
+
* Not part of the stable surface — prefer `client.realtime.channel(name)`.
|
|
1905
|
+
*/
|
|
1876
1906
|
sendMessage(message) {
|
|
1877
1907
|
const queuedMsg = message;
|
|
1878
1908
|
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
@@ -2453,6 +2483,190 @@ var RebaseWebSocketClient = class {
|
|
|
2453
2483
|
}
|
|
2454
2484
|
};
|
|
2455
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
|
|
2456
2670
|
//#region src/index.ts
|
|
2457
2671
|
/**
|
|
2458
2672
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
@@ -2503,6 +2717,8 @@ function createRebaseClient(options) {
|
|
|
2503
2717
|
};
|
|
2504
2718
|
const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
2505
2719
|
let ws;
|
|
2720
|
+
/** One channel object per name — see `realtime.channel`. */
|
|
2721
|
+
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
2506
2722
|
if (resolvedWsUrl) {
|
|
2507
2723
|
ws = new RebaseWebSocketClient({
|
|
2508
2724
|
websocketUrl: resolvedWsUrl,
|
|
@@ -2613,6 +2829,24 @@ function createRebaseClient(options) {
|
|
|
2613
2829
|
createStorageSource,
|
|
2614
2830
|
fetchStorageSources,
|
|
2615
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
|
+
} },
|
|
2616
2850
|
/**
|
|
2617
2851
|
* Release the realtime socket and its reconnect timer.
|
|
2618
2852
|
*
|
|
@@ -2621,6 +2855,8 @@ function createRebaseClient(options) {
|
|
|
2621
2855
|
* was never started, and safe to call twice.
|
|
2622
2856
|
*/
|
|
2623
2857
|
close: () => {
|
|
2858
|
+
for (const channel of realtimeChannels.values()) channel.leave();
|
|
2859
|
+
realtimeChannels.clear();
|
|
2624
2860
|
ws?.disconnect();
|
|
2625
2861
|
},
|
|
2626
2862
|
setToken: transport.setToken,
|
|
@@ -2641,6 +2877,6 @@ function createRebaseClient(options) {
|
|
|
2641
2877
|
};
|
|
2642
2878
|
}
|
|
2643
2879
|
//#endregion
|
|
2644
|
-
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 };
|
|
2645
2881
|
|
|
2646
2882
|
//# sourceMappingURL=index.es.js.map
|