@rebasepro/client 0.9.1-canary.97e305f → 0.9.1-canary.a639738

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
@@ -1405,6 +1405,20 @@ function extractMessageError(message) {
1405
1405
  };
1406
1406
  }
1407
1407
  /**
1408
+ * Broadcast and presence frames.
1409
+ *
1410
+ * Fire-and-forget (the server sends no response envelope), and exempt from the
1411
+ * client-side auth gate — a public channel is usable without an account.
1412
+ */
1413
+ var CHANNEL_MESSAGE_TYPES = new Set([
1414
+ "join_channel",
1415
+ "leave_channel",
1416
+ "broadcast",
1417
+ "presence_track",
1418
+ "presence_untrack",
1419
+ "presence_state"
1420
+ ]);
1421
+ /**
1408
1422
  * Low-level realtime WebSocket client.
1409
1423
  *
1410
1424
  * @internal Not a stable app-facing API. `createRebaseClient()` constructs and
@@ -1419,6 +1433,36 @@ var RebaseWebSocketClient = class {
1419
1433
  getAuthToken;
1420
1434
  subscriptions = /* @__PURE__ */ new Map();
1421
1435
  listeners = /* @__PURE__ */ new Map();
1436
+ /** Channel-name → handlers, for broadcast and presence frames. */
1437
+ channelHandlers = /* @__PURE__ */ new Map();
1438
+ /** Set by `close()`. Blocks any later operation from silently redialling. */
1439
+ closedByCaller = false;
1440
+ /**
1441
+ * Whether a socket exists at all (open or still opening).
1442
+ *
1443
+ * Lets callers distinguish "authenticate the live socket" from "there is
1444
+ * nothing to authenticate yet", without that question forcing a dial.
1445
+ */
1446
+ get hasSocket() {
1447
+ return this.ws !== null;
1448
+ }
1449
+ /** So the "no WebSocket in this environment" warning is said once, not per call. */
1450
+ warnedNoWebSocket = false;
1451
+ /** Subscribe to broadcast/presence frames for one channel. */
1452
+ onChannelMessage(channel, handler) {
1453
+ if (!this.channelHandlers.has(channel)) this.channelHandlers.set(channel, /* @__PURE__ */ new Set());
1454
+ this.channelHandlers.get(channel).add(handler);
1455
+ return () => {
1456
+ const handlers = this.channelHandlers.get(channel);
1457
+ if (!handlers) return;
1458
+ handlers.delete(handler);
1459
+ if (handlers.size === 0) this.channelHandlers.delete(channel);
1460
+ };
1461
+ }
1462
+ /** Notified after the socket comes back, so channels can re-join. */
1463
+ onReconnect(handler) {
1464
+ return this.on("reconnect", handler);
1465
+ }
1422
1466
  on(event, cb) {
1423
1467
  if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
1424
1468
  this.listeners.get(event).add(cb);
@@ -1449,8 +1493,25 @@ var RebaseWebSocketClient = class {
1449
1493
  this.getAuthToken = config.getAuthToken;
1450
1494
  this.onUnauthorized = config.onUnauthorized;
1451
1495
  this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
1452
- if (!this.WebSocketConstructor) console.warn("WebSocket is not defined in this environment. Realtime subscriptions will not work unless you provide a WebSocket implementation in the config.");
1453
- else this.initWebSocket();
1496
+ }
1497
+ /**
1498
+ * Open the socket if it is not open (or opening) already.
1499
+ *
1500
+ * Idempotent, synchronous, and safe to call on every operation that needs a
1501
+ * live socket — `initWebSocket` already no-ops on an open socket and is
1502
+ * re-entrant, since the reconnect path has always called it.
1503
+ */
1504
+ ensureConnected() {
1505
+ if (this.closedByCaller) return;
1506
+ if (!this.WebSocketConstructor) {
1507
+ if (!this.warnedNoWebSocket) {
1508
+ this.warnedNoWebSocket = true;
1509
+ console.warn("WebSocket is not defined in this environment. Realtime subscriptions will not work unless you provide a WebSocket implementation in the config.");
1510
+ }
1511
+ return;
1512
+ }
1513
+ if (this.ws || this.reconnectTimeout) return;
1514
+ this.initWebSocket();
1454
1515
  }
1455
1516
  /**
1456
1517
  * Authenticate the WebSocket connection
@@ -1500,7 +1561,16 @@ var RebaseWebSocketClient = class {
1500
1561
  });
1501
1562
  }
1502
1563
  }
1503
- disconnect() {
1564
+ /**
1565
+ * Drop the socket.
1566
+ *
1567
+ * `permanent` distinguishes the two callers. Signing out drops the socket
1568
+ * but the client stays usable — a later subscribe should reconnect
1569
+ * anonymously. `client.close()` is the caller saying they are done, and
1570
+ * must not be undone by a stray queued frame.
1571
+ */
1572
+ disconnect(permanent = false) {
1573
+ if (permanent) this.closedByCaller = true;
1504
1574
  this.isAuthenticated = false;
1505
1575
  this.authPromise = null;
1506
1576
  if (this.reconnectTimeout) {
@@ -1685,6 +1755,15 @@ var RebaseWebSocketClient = class {
1685
1755
  }
1686
1756
  return;
1687
1757
  }
1758
+ if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff")) {
1759
+ const handlers = this.channelHandlers.get(message.channel);
1760
+ if (handlers) for (const handler of [...handlers]) try {
1761
+ handler(message);
1762
+ } catch (error) {
1763
+ console.error("Error in channel handler:", error);
1764
+ }
1765
+ return;
1766
+ }
1688
1767
  if (subscriptionId && type === "collection_update") {
1689
1768
  const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
1690
1769
  if (subscriptionKey) {
@@ -1873,21 +1952,28 @@ var RebaseWebSocketClient = class {
1873
1952
  throw error;
1874
1953
  }
1875
1954
  }
1955
+ /**
1956
+ * Public because `RebaseRealtimeChannel` sends channel frames through it.
1957
+ * Not part of the stable surface — prefer `client.realtime.channel(name)`.
1958
+ */
1876
1959
  sendMessage(message) {
1877
1960
  const queuedMsg = message;
1878
1961
  if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
1879
- if (!this.isConnected || !this.ws) return new Promise((resolve, reject) => {
1880
- const queueable = message;
1881
- queueable._queuedResolve = resolve;
1882
- queueable._queuedReject = reject;
1883
- this.messageQueue.push(message);
1884
- });
1962
+ if (!this.isConnected || !this.ws) {
1963
+ this.ensureConnected();
1964
+ return new Promise((resolve, reject) => {
1965
+ const queueable = message;
1966
+ queueable._queuedResolve = resolve;
1967
+ queueable._queuedReject = reject;
1968
+ this.messageQueue.push(message);
1969
+ });
1970
+ }
1885
1971
  return new Promise((resolve, reject) => {
1886
1972
  this.doSendMessage(message, resolve, reject);
1887
1973
  });
1888
1974
  }
1889
1975
  async doSendMessage(message, resolve, reject) {
1890
- if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
1976
+ if (message.type !== "AUTHENTICATE" && !CHANNEL_MESSAGE_TYPES.has(message.type) && this.getAuthToken && !this.isAuthenticated) try {
1891
1977
  await this.ensureAuthenticated();
1892
1978
  } catch (error) {
1893
1979
  reject(new RebaseApiError$1(error instanceof Error ? error.message : "Authentication required"));
@@ -1895,17 +1981,7 @@ var RebaseWebSocketClient = class {
1895
1981
  }
1896
1982
  const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1897
1983
  message.requestId = requestId;
1898
- const expectsResponse = ![
1899
- "subscribe_collection",
1900
- "subscribe_one",
1901
- "unsubscribe",
1902
- "join_channel",
1903
- "leave_channel",
1904
- "broadcast",
1905
- "presence_track",
1906
- "presence_untrack",
1907
- "presence_state"
1908
- ].includes(message.type);
1984
+ const expectsResponse = !(message.type === "subscribe_collection" || message.type === "subscribe_one" || message.type === "unsubscribe" || CHANNEL_MESSAGE_TYPES.has(message.type));
1909
1985
  if (expectsResponse && !this.pendingRequests.has(requestId)) {
1910
1986
  const timeoutHandle = setTimeout(() => {
1911
1987
  if (this.pendingRequests.has(requestId)) {
@@ -2134,6 +2210,7 @@ var RebaseWebSocketClient = class {
2134
2210
  });
2135
2211
  }
2136
2212
  listenCollection(props, onUpdate, onError) {
2213
+ this.ensureConnected();
2137
2214
  const subscriptionKey = this.createCollectionSubscriptionKey(props);
2138
2215
  const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2139
2216
  const existingSubscription = this.collectionSubscriptions.get(subscriptionKey);
@@ -2195,6 +2272,7 @@ var RebaseWebSocketClient = class {
2195
2272
  };
2196
2273
  }
2197
2274
  listenOne(props, onUpdate, onError) {
2275
+ this.ensureConnected();
2198
2276
  const subscriptionKey = this.createSingleSubscriptionKey(props);
2199
2277
  const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
2200
2278
  const existingSubscription = this.singleSubscriptions.get(subscriptionKey);
@@ -2453,6 +2531,190 @@ var RebaseWebSocketClient = class {
2453
2531
  }
2454
2532
  };
2455
2533
  //#endregion
2534
+ //#region src/realtime-channel.ts
2535
+ /**
2536
+ * Re-send presence comfortably inside the server's 30s expiry.
2537
+ *
2538
+ * Two-thirds of the window: one lost heartbeat still leaves time for the next
2539
+ * before the entry is reaped, so a single dropped frame is not a disappearance.
2540
+ */
2541
+ var PRESENCE_HEARTBEAT_MS = 2e4;
2542
+ var RebaseRealtimeChannel = class {
2543
+ name;
2544
+ transport;
2545
+ presenceHandlers = /* @__PURE__ */ new Set();
2546
+ broadcastHandlers = /* @__PURE__ */ new Set();
2547
+ unsubscribers = [];
2548
+ /** Last known roster, kept so handlers always get a full picture. */
2549
+ presences = {};
2550
+ /** What this client last tracked, replayed on reconnect and heartbeat. */
2551
+ trackedState = null;
2552
+ heartbeat = null;
2553
+ joined = false;
2554
+ constructor(name, transport) {
2555
+ this.name = name;
2556
+ this.transport = transport;
2557
+ }
2558
+ /**
2559
+ * Join the channel and ask for the current roster.
2560
+ *
2561
+ * Called automatically by `track`, `broadcast`, `onPresence` and
2562
+ * `onBroadcast`; calling it directly is only needed to start receiving
2563
+ * before there is anything to send.
2564
+ */
2565
+ async join() {
2566
+ if (this.joined) return;
2567
+ this.joined = true;
2568
+ this.unsubscribers.push(this.transport.onChannelMessage(this.name, (message) => this.handle(message)));
2569
+ this.unsubscribers.push(this.transport.onReconnect(() => {
2570
+ this.rejoin();
2571
+ }));
2572
+ await this.transport.sendMessage({
2573
+ type: "join_channel",
2574
+ channel: this.name
2575
+ });
2576
+ await this.transport.sendMessage({
2577
+ type: "presence_state",
2578
+ channel: this.name
2579
+ });
2580
+ }
2581
+ async rejoin() {
2582
+ try {
2583
+ await this.transport.sendMessage({
2584
+ type: "join_channel",
2585
+ channel: this.name
2586
+ });
2587
+ await this.transport.sendMessage({
2588
+ type: "presence_state",
2589
+ channel: this.name
2590
+ });
2591
+ if (this.trackedState) await this.transport.sendMessage({
2592
+ type: "presence_track",
2593
+ channel: this.name,
2594
+ state: this.trackedState
2595
+ });
2596
+ } catch {}
2597
+ }
2598
+ /**
2599
+ * Publish this client's presence state, and keep publishing it.
2600
+ *
2601
+ * Calling `track` again replaces the state (and restarts the heartbeat),
2602
+ * which is how you update e.g. a cursor position.
2603
+ */
2604
+ async track(state) {
2605
+ await this.join();
2606
+ this.trackedState = state;
2607
+ await this.transport.sendMessage({
2608
+ type: "presence_track",
2609
+ channel: this.name,
2610
+ state
2611
+ });
2612
+ if (!this.heartbeat) {
2613
+ this.heartbeat = setInterval(() => {
2614
+ if (!this.trackedState) return;
2615
+ this.transport.sendMessage({
2616
+ type: "presence_track",
2617
+ channel: this.name,
2618
+ state: this.trackedState
2619
+ }).catch(() => {});
2620
+ }, PRESENCE_HEARTBEAT_MS);
2621
+ this.heartbeat.unref?.();
2622
+ }
2623
+ }
2624
+ /** Stop publishing presence, without leaving the channel. */
2625
+ async untrack() {
2626
+ this.stopHeartbeat();
2627
+ this.trackedState = null;
2628
+ if (this.joined) await this.transport.sendMessage({
2629
+ type: "presence_untrack",
2630
+ channel: this.name
2631
+ });
2632
+ }
2633
+ /**
2634
+ * Observe the roster. The handler fires immediately with what is already
2635
+ * known, then on every change.
2636
+ */
2637
+ onPresence(handler) {
2638
+ this.presenceHandlers.add(handler);
2639
+ this.join();
2640
+ if (Object.keys(this.presences).length > 0) handler({ ...this.presences });
2641
+ return () => this.presenceHandlers.delete(handler);
2642
+ }
2643
+ /** Send a broadcast. The sender does not receive its own message. */
2644
+ async broadcast(event, payload) {
2645
+ await this.join();
2646
+ await this.transport.sendMessage({
2647
+ type: "broadcast",
2648
+ channel: this.name,
2649
+ event,
2650
+ payload
2651
+ });
2652
+ }
2653
+ onBroadcast(eventOrHandler, maybeHandler) {
2654
+ const wrapped = typeof eventOrHandler === "string" ? (e) => {
2655
+ if (e.event === eventOrHandler) maybeHandler(e.payload);
2656
+ } : eventOrHandler;
2657
+ this.broadcastHandlers.add(wrapped);
2658
+ this.join();
2659
+ return () => this.broadcastHandlers.delete(wrapped);
2660
+ }
2661
+ /** Leave the channel and release every listener and timer. */
2662
+ async leave() {
2663
+ this.stopHeartbeat();
2664
+ this.trackedState = null;
2665
+ this.presences = {};
2666
+ this.presenceHandlers.clear();
2667
+ this.broadcastHandlers.clear();
2668
+ for (const off of this.unsubscribers) off();
2669
+ this.unsubscribers = [];
2670
+ if (this.joined) {
2671
+ this.joined = false;
2672
+ await this.transport.sendMessage({
2673
+ type: "leave_channel",
2674
+ channel: this.name
2675
+ });
2676
+ }
2677
+ }
2678
+ stopHeartbeat() {
2679
+ if (this.heartbeat) {
2680
+ clearInterval(this.heartbeat);
2681
+ this.heartbeat = null;
2682
+ }
2683
+ }
2684
+ /** Fold an incoming frame into the roster and fan it out. */
2685
+ handle(message) {
2686
+ switch (message.type) {
2687
+ case "presence_state":
2688
+ this.presences = message.presences ?? {};
2689
+ this.emitPresence();
2690
+ break;
2691
+ case "presence_diff": {
2692
+ const joins = message.joins ?? {};
2693
+ const leaves = message.leaves ?? {};
2694
+ for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
2695
+ for (const id of Object.keys(leaves)) delete this.presences[id];
2696
+ this.emitPresence({
2697
+ joins,
2698
+ leaves
2699
+ });
2700
+ break;
2701
+ }
2702
+ case "broadcast": {
2703
+ const event = {
2704
+ event: message.event,
2705
+ payload: message.payload
2706
+ };
2707
+ for (const handler of this.broadcastHandlers) handler(event);
2708
+ break;
2709
+ }
2710
+ }
2711
+ }
2712
+ emitPresence(diff) {
2713
+ const snapshot = { ...this.presences };
2714
+ for (const handler of this.presenceHandlers) handler(snapshot, diff);
2715
+ }
2716
+ };
2717
+ //#endregion
2456
2718
  //#region src/index.ts
2457
2719
  /**
2458
2720
  * Derive a WebSocket URL from an HTTP base URL.
@@ -2503,6 +2765,8 @@ function createRebaseClient(options) {
2503
2765
  };
2504
2766
  const resolvedWsUrl = options.realtime !== false ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
2505
2767
  let ws;
2768
+ /** One channel object per name — see `realtime.channel`. */
2769
+ const realtimeChannels = /* @__PURE__ */ new Map();
2506
2770
  if (resolvedWsUrl) {
2507
2771
  ws = new RebaseWebSocketClient({
2508
2772
  websocketUrl: resolvedWsUrl,
@@ -2526,7 +2790,7 @@ function createRebaseClient(options) {
2526
2790
  if (!ws) return;
2527
2791
  if (event === "SIGNED_OUT") ws.disconnect();
2528
2792
  else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
2529
- if (session?.accessToken) ws.authenticate(session.accessToken).catch(console.warn);
2793
+ if (session?.accessToken && ws.hasSocket) ws.authenticate(session.accessToken).catch(console.warn);
2530
2794
  }
2531
2795
  });
2532
2796
  }
@@ -2613,6 +2877,24 @@ function createRebaseClient(options) {
2613
2877
  createStorageSource,
2614
2878
  fetchStorageSources,
2615
2879
  ws,
2880
+ realtime: {
2881
+ /**
2882
+ * Join a broadcast/presence channel.
2883
+ *
2884
+ * Repeated calls with the same name return the same channel, so
2885
+ * separate components can attach handlers without each opening its
2886
+ * own membership — and `leave()` from one would otherwise silently
2887
+ * cut off the others.
2888
+ */
2889
+ channel: (name) => {
2890
+ if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
2891
+ let existing = realtimeChannels.get(name);
2892
+ if (!existing) {
2893
+ existing = new RebaseRealtimeChannel(name, ws);
2894
+ realtimeChannels.set(name, existing);
2895
+ }
2896
+ return existing;
2897
+ } },
2616
2898
  /**
2617
2899
  * Release the realtime socket and its reconnect timer.
2618
2900
  *
@@ -2621,7 +2903,9 @@ function createRebaseClient(options) {
2621
2903
  * was never started, and safe to call twice.
2622
2904
  */
2623
2905
  close: () => {
2624
- ws?.disconnect();
2906
+ for (const channel of realtimeChannels.values()) channel.leave();
2907
+ realtimeChannels.clear();
2908
+ ws?.disconnect(true);
2625
2909
  },
2626
2910
  setToken: transport.setToken,
2627
2911
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -2641,6 +2925,6 @@ function createRebaseClient(options) {
2641
2925
  };
2642
2926
  }
2643
2927
  //#endregion
2644
- export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
2928
+ export { QueryBuilder, RebaseApiError, RebaseClientError, RebaseRealtimeChannel, RebaseWebSocketClient, and, cond, createBackups, createCookieStorage, createMemoryStorage, createRebaseClient, or };
2645
2929
 
2646
2930
  //# sourceMappingURL=index.es.js.map