@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 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