@rebasepro/client 0.9.1-canary.a639738 → 0.9.1-canary.a8dbf1c

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/auth.d.ts CHANGED
@@ -147,6 +147,11 @@ export declare function createAuth(transport: Transport, options?: CreateAuthOpt
147
147
  success: boolean;
148
148
  message: string;
149
149
  }>;
150
+ linkProvider: (providerId: string, payload: Record<string, unknown>) => Promise<{
151
+ success: boolean;
152
+ provider: string;
153
+ alreadyLinked: boolean;
154
+ }>;
150
155
  sendVerificationEmail: () => Promise<{
151
156
  success: boolean;
152
157
  message: string;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
7
7
  import { CollectionClient } from "./collection";
8
8
  import { createFunctionsClient } from "./functions";
9
9
  import { RebaseWebSocketClient } from "./websocket";
10
+ import { RebaseRealtimeChannel, type ChannelOptions } from "./realtime-channel";
10
11
  import { InsertOf, RebaseClient, RebaseSdkData, RowOf, StorageSource, StorageSourceDefinition, StorageSourceRegistry, UpdateOf } from "@rebasepro/types";
11
12
  export { RebaseApiError } from "./transport";
12
13
  export { RebaseClientError } from "./errors";
@@ -28,7 +29,7 @@ export type { ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequ
28
29
  export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
29
30
  export { RebaseWebSocketClient } from "./websocket";
30
31
  export { RebaseRealtimeChannel } from "./realtime-channel";
31
- export type { PresenceState, PresenceDiff, BroadcastEvent, ChannelTransport } from "./realtime-channel";
32
+ export type { PresenceState, PresenceDiff, BroadcastEvent, ChannelTransport, ChannelOptions, ChannelHistoryEntry, ChannelHistoryResult } from "./realtime-channel";
32
33
  export interface CreateRebaseClientOptions extends RebaseClientConfig {
33
34
  auth?: CreateAuthOptions;
34
35
  admin?: CreateAdminOptions;
@@ -76,6 +77,24 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
76
77
  apiKeys: ReturnType<typeof createApiKeys>;
77
78
  functions: ReturnType<typeof createFunctionsClient>;
78
79
  ws?: RebaseWebSocketClient;
80
+ /**
81
+ * Broadcast and presence channels.
82
+ *
83
+ * Was missing from this type while present on the returned object, which
84
+ * made `client.realtime.channel(...)` a type error and forced every adopter
85
+ * to cast around the feature before they could reach it.
86
+ */
87
+ realtime: {
88
+ /**
89
+ * Join a broadcast/presence channel. Repeated calls with the same name
90
+ * return the same channel object. Throws only when the client was
91
+ * created with `realtime: false`.
92
+ *
93
+ * Pass `{ history: true }` to have the channel replay what it missed on
94
+ * join and on every reconnect, for channels the server retains.
95
+ */
96
+ channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
97
+ };
79
98
  /**
80
99
  * Release the realtime socket and its reconnect timer.
81
100
  *
package/dist/index.es.js CHANGED
@@ -54,6 +54,28 @@ function buildQueryString(params) {
54
54
  }
55
55
  return parts.length > 0 ? "?" + parts.join("&") : "";
56
56
  }
57
+ /**
58
+ * The base every request and every caller-built URL resolves against.
59
+ *
60
+ * `baseUrl` is optional because the common production shape is a Rebase
61
+ * backend serving its own SPA, where the API is simply the page's origin.
62
+ * Leaving it unset is therefore the *correct* configuration there — and the
63
+ * one that keeps working when a second hostname (a custom domain) points at
64
+ * the same app.
65
+ *
66
+ * When unset in a browser this resolves to the page origin rather than "".
67
+ * Requests behave identically either way, but the empty string is a trap for
68
+ * anything that builds a URL from `client.baseUrl`: `new URL("" + path)`
69
+ * throws, so apps "fixed" it by baking an absolute host into their bundle —
70
+ * which is exactly what breaks the day a custom domain is added, and which no
71
+ * amount of CORS configuration repairs, because a SameSite=Lax auth cookie is
72
+ * not sent cross-site either.
73
+ */
74
+ function resolveBaseUrl(configured) {
75
+ if (configured) return configured.replace(/\/$/, "");
76
+ if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
77
+ return "";
78
+ }
57
79
  function createTransport(config) {
58
80
  const fetchFn = config.fetch || globalThis.fetch;
59
81
  const apiPath = config.apiPath || "/api";
@@ -68,7 +90,7 @@ function createTransport(config) {
68
90
  };
69
91
  }
70
92
  async function request(path, init) {
71
- const url = (config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "") + apiPath + path;
93
+ const url = resolveBaseUrl(config.baseUrl) + apiPath + path;
72
94
  let activeToken = token;
73
95
  if (tokenGetter) try {
74
96
  const fetched = await tokenGetter();
@@ -143,7 +165,7 @@ function createTransport(config) {
143
165
  onUnauthorizedHandler = handler;
144
166
  },
145
167
  get baseUrl() {
146
- return config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "";
168
+ return resolveBaseUrl(config.baseUrl);
147
169
  },
148
170
  get apiPath() {
149
171
  return apiPath;
@@ -605,6 +627,30 @@ function createAuth(transport, options) {
605
627
  })
606
628
  });
607
629
  }
630
+ /**
631
+ * Link an OAuth provider to the **currently signed-in** account.
632
+ *
633
+ * Use this when `signIn*` failed with `EMAIL_NOT_VERIFIED` — an account
634
+ * with that email already exists under a different sign-in method — or to
635
+ * attach a provider whose email differs from the account's.
636
+ *
637
+ * The payload is the same one the provider's sign-in method takes, e.g.
638
+ * `linkProvider("google", { idToken })`.
639
+ *
640
+ * Unlike sign-in, this does not require the provider to have verified the
641
+ * email, and the emails need not match: the active session already proves
642
+ * account ownership.
643
+ *
644
+ * Throws `IDENTITY_ALREADY_LINKED` (409) if that provider identity is
645
+ * attached to a different user. Succeeds idempotently (`alreadyLinked:
646
+ * true`) if it is already attached to the current one.
647
+ */
648
+ async function linkProvider(providerId, payload) {
649
+ return transport.request(authPath + "/link/" + providerId, {
650
+ method: "POST",
651
+ body: JSON.stringify(payload)
652
+ });
653
+ }
608
654
  async function sendVerificationEmail() {
609
655
  return transport.request(authPath + "/send-verification", { method: "POST" });
610
656
  }
@@ -726,6 +772,7 @@ function createAuth(transport, options) {
726
772
  resetPasswordForEmail,
727
773
  resetPassword,
728
774
  changePassword,
775
+ linkProvider,
729
776
  sendVerificationEmail,
730
777
  verifyEmail,
731
778
  sendMagicLink,
@@ -1416,7 +1463,8 @@ var CHANNEL_MESSAGE_TYPES = new Set([
1416
1463
  "broadcast",
1417
1464
  "presence_track",
1418
1465
  "presence_untrack",
1419
- "presence_state"
1466
+ "presence_state",
1467
+ "channel_history"
1420
1468
  ]);
1421
1469
  /**
1422
1470
  * Low-level realtime WebSocket client.
@@ -1755,7 +1803,7 @@ var RebaseWebSocketClient = class {
1755
1803
  }
1756
1804
  return;
1757
1805
  }
1758
- if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff")) {
1806
+ if (typeof message.channel === "string" && (type === "broadcast" || type === "presence_state" || type === "presence_diff" || type === "channel_history")) {
1759
1807
  const handlers = this.channelHandlers.get(message.channel);
1760
1808
  if (handlers) for (const handler of [...handlers]) try {
1761
1809
  handler(message);
@@ -2051,6 +2099,9 @@ var RebaseWebSocketClient = class {
2051
2099
  async fetchAvailableRoles() {
2052
2100
  return (await this.sendMessage({ type: "FETCH_ROLES" })).roles || [];
2053
2101
  }
2102
+ async fetchApplicationRoles() {
2103
+ return (await this.sendMessage({ type: "FETCH_APPLICATION_ROLES" })).roles || [];
2104
+ }
2054
2105
  async fetchCurrentDatabase() {
2055
2106
  return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
2056
2107
  }
@@ -2539,6 +2590,14 @@ var RebaseWebSocketClient = class {
2539
2590
  * before the entry is reaped, so a single dropped frame is not a disappearance.
2540
2591
  */
2541
2592
  var PRESENCE_HEARTBEAT_MS = 2e4;
2593
+ /**
2594
+ * How long live messages are held back waiting for a catch-up response.
2595
+ *
2596
+ * Short, because the cost of waiting is visible — on a collaborative document
2597
+ * this is a stall in everyone else's edits appearing. Long enough that a slow
2598
+ * replay of a busy channel is not abandoned needlessly.
2599
+ */
2600
+ var CATCH_UP_TIMEOUT_MS = 1e4;
2542
2601
  var RebaseRealtimeChannel = class {
2543
2602
  name;
2544
2603
  transport;
@@ -2551,9 +2610,63 @@ var RebaseRealtimeChannel = class {
2551
2610
  trackedState = null;
2552
2611
  heartbeat = null;
2553
2612
  joined = false;
2554
- constructor(name, transport) {
2613
+ /** Whether this handle asks the server to replay missed messages. */
2614
+ wantsHistory;
2615
+ /**
2616
+ * Highest sequence number delivered to handlers so far.
2617
+ *
2618
+ * This is the resume point sent as `sinceSeq`, and the watermark that makes
2619
+ * replay idempotent: catch-up ranges overlap with what arrived live, and
2620
+ * anything at or below this has already been seen.
2621
+ */
2622
+ lastSeq = 0;
2623
+ /**
2624
+ * Live messages that arrived while a catch-up was in flight.
2625
+ *
2626
+ * Without this they would be delivered ahead of the older messages being
2627
+ * fetched, and — worse — would advance {@link lastSeq} past them, so the
2628
+ * catch-up response would then be discarded as already-seen and those
2629
+ * messages would be lost for good. Held here and flushed, in order, once
2630
+ * the replay lands.
2631
+ */
2632
+ pendingLive = [];
2633
+ catchUpInFlight = false;
2634
+ /**
2635
+ * Deadline for a catch-up response.
2636
+ *
2637
+ * Buffering live messages is only safe because the wait is bounded. A
2638
+ * catch-up frame that never arrives — a server that dropped it, a socket
2639
+ * that died between request and reply — would otherwise leave the channel
2640
+ * silently holding every subsequent edit forever, which is a worse failure
2641
+ * than the one replay was added to fix.
2642
+ */
2643
+ catchUpTimeout = null;
2644
+ /**
2645
+ * Callers of {@link history} awaiting the next `channel_history` frame.
2646
+ *
2647
+ * These frames are addressed by channel rather than by request id, so they
2648
+ * are matched in arrival order. Requests on one channel are serialized by
2649
+ * the socket, so FIFO is the right correlation here.
2650
+ */
2651
+ historyWaiters = [];
2652
+ constructor(name, transport, options = {}) {
2555
2653
  this.name = name;
2556
2654
  this.transport = transport;
2655
+ this.wantsHistory = options.history ?? false;
2656
+ }
2657
+ /**
2658
+ * Turn on catch-up for a handle that was created without it.
2659
+ *
2660
+ * The client hands back the same channel object for a given name, so a
2661
+ * later `channel(name, { history: true })` has no new object to configure —
2662
+ * it upgrades this one instead. Idempotent, and never downgrades: one
2663
+ * caller asking for history must not be switched off by another that did
2664
+ * not ask.
2665
+ */
2666
+ enableHistory() {
2667
+ if (this.wantsHistory) return;
2668
+ this.wantsHistory = true;
2669
+ if (this.joined) this.requestHistory();
2557
2670
  }
2558
2671
  /**
2559
2672
  * Join the channel and ask for the current roster.
@@ -2562,6 +2675,27 @@ var RebaseRealtimeChannel = class {
2562
2675
  * `onBroadcast`; calling it directly is only needed to start receiving
2563
2676
  * before there is anything to send.
2564
2677
  */
2678
+ /**
2679
+ * Send a channel message.
2680
+ *
2681
+ * Every channel message is read by the server out of a `payload` envelope
2682
+ * (`payload?.channel`, `payload?.state`, `payload?.event`). Sending those
2683
+ * fields flat does not error: `payload?.channel` simply reads as
2684
+ * `undefined`, so the client is registered into channel `undefined` with
2685
+ * empty state, and the echo comes back with no `channel` for
2686
+ * `onChannelMessage` to match — presence and broadcast both go quiet with
2687
+ * nothing logged. Funnelled through one place so a new message type cannot
2688
+ * reintroduce that.
2689
+ */
2690
+ send(type, fields = {}) {
2691
+ return this.transport.sendMessage({
2692
+ type,
2693
+ payload: {
2694
+ channel: this.name,
2695
+ ...fields
2696
+ }
2697
+ });
2698
+ }
2565
2699
  async join() {
2566
2700
  if (this.joined) return;
2567
2701
  this.joined = true;
@@ -2569,33 +2703,60 @@ var RebaseRealtimeChannel = class {
2569
2703
  this.unsubscribers.push(this.transport.onReconnect(() => {
2570
2704
  this.rejoin();
2571
2705
  }));
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
- });
2706
+ await this.send("join_channel");
2707
+ await this.send("presence_state");
2708
+ if (this.wantsHistory) await this.requestHistory();
2580
2709
  }
2581
2710
  async rejoin() {
2582
2711
  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
- });
2712
+ await this.send("join_channel");
2713
+ await this.send("presence_state");
2714
+ if (this.trackedState) await this.send("presence_track", { state: this.trackedState });
2715
+ if (this.wantsHistory) await this.requestHistory();
2596
2716
  } catch {}
2597
2717
  }
2598
2718
  /**
2719
+ * Ask the server for everything after {@link lastSeq}.
2720
+ *
2721
+ * Live messages are buffered from here until the answer arrives — see
2722
+ * {@link pendingLive}.
2723
+ */
2724
+ async requestHistory(limit) {
2725
+ this.catchUpInFlight = true;
2726
+ if (this.catchUpTimeout) clearTimeout(this.catchUpTimeout);
2727
+ this.catchUpTimeout = setTimeout(() => this.abandonCatchUp(), CATCH_UP_TIMEOUT_MS);
2728
+ this.catchUpTimeout.unref?.();
2729
+ try {
2730
+ await this.send("channel_history", {
2731
+ sinceSeq: this.lastSeq,
2732
+ ...limit !== void 0 ? { limit } : {}
2733
+ });
2734
+ } catch {
2735
+ this.abandonCatchUp();
2736
+ }
2737
+ }
2738
+ /**
2739
+ * Give up waiting for a catch-up and release what was held back.
2740
+ *
2741
+ * The buffered messages are still the freshest thing this client has, so
2742
+ * they are delivered rather than dropped. Callers of {@link history} are
2743
+ * answered with `retained: false` — accurate in the sense that matters:
2744
+ * this client has no history to work from and has to resync.
2745
+ */
2746
+ abandonCatchUp() {
2747
+ if (this.catchUpTimeout) {
2748
+ clearTimeout(this.catchUpTimeout);
2749
+ this.catchUpTimeout = null;
2750
+ }
2751
+ if (!this.catchUpInFlight) return;
2752
+ this.catchUpInFlight = false;
2753
+ for (const resolve of this.historyWaiters.splice(0)) resolve({
2754
+ messages: [],
2755
+ retained: false
2756
+ });
2757
+ this.flushPendingLive();
2758
+ }
2759
+ /**
2599
2760
  * Publish this client's presence state, and keep publishing it.
2600
2761
  *
2601
2762
  * Calling `track` again replaces the state (and restarts the heartbeat),
@@ -2604,19 +2765,11 @@ var RebaseRealtimeChannel = class {
2604
2765
  async track(state) {
2605
2766
  await this.join();
2606
2767
  this.trackedState = state;
2607
- await this.transport.sendMessage({
2608
- type: "presence_track",
2609
- channel: this.name,
2610
- state
2611
- });
2768
+ await this.send("presence_track", { state });
2612
2769
  if (!this.heartbeat) {
2613
2770
  this.heartbeat = setInterval(() => {
2614
2771
  if (!this.trackedState) return;
2615
- this.transport.sendMessage({
2616
- type: "presence_track",
2617
- channel: this.name,
2618
- state: this.trackedState
2619
- }).catch(() => {});
2772
+ this.send("presence_track", { state: this.trackedState }).catch(() => {});
2620
2773
  }, PRESENCE_HEARTBEAT_MS);
2621
2774
  this.heartbeat.unref?.();
2622
2775
  }
@@ -2625,10 +2778,7 @@ var RebaseRealtimeChannel = class {
2625
2778
  async untrack() {
2626
2779
  this.stopHeartbeat();
2627
2780
  this.trackedState = null;
2628
- if (this.joined) await this.transport.sendMessage({
2629
- type: "presence_untrack",
2630
- channel: this.name
2631
- });
2781
+ if (this.joined) await this.send("presence_untrack");
2632
2782
  }
2633
2783
  /**
2634
2784
  * Observe the roster. The handler fires immediately with what is already
@@ -2643,9 +2793,7 @@ var RebaseRealtimeChannel = class {
2643
2793
  /** Send a broadcast. The sender does not receive its own message. */
2644
2794
  async broadcast(event, payload) {
2645
2795
  await this.join();
2646
- await this.transport.sendMessage({
2647
- type: "broadcast",
2648
- channel: this.name,
2796
+ await this.send("broadcast", {
2649
2797
  event,
2650
2798
  payload
2651
2799
  });
@@ -2658,6 +2806,34 @@ var RebaseRealtimeChannel = class {
2658
2806
  this.join();
2659
2807
  return () => this.broadcastHandlers.delete(wrapped);
2660
2808
  }
2809
+ /**
2810
+ * The last sequence number this channel has delivered.
2811
+ *
2812
+ * Zero on a channel that retains nothing. Persist it if you want catch-up
2813
+ * to survive a page reload as well as a reconnect, and pass it back via
2814
+ * {@link history}.
2815
+ */
2816
+ get sequence() {
2817
+ return this.lastSeq;
2818
+ }
2819
+ /**
2820
+ * Fetch retained messages explicitly, instead of waiting for join or
2821
+ * reconnect to do it.
2822
+ *
2823
+ * Defaults to resuming from {@link sequence}. Messages are delivered to
2824
+ * `onBroadcast` handlers as usual — the returned value is for callers that
2825
+ * want to inspect the batch, or to learn from `retained` that the channel
2826
+ * keeps no history at all.
2827
+ */
2828
+ async history(options = {}) {
2829
+ await this.join();
2830
+ if (options.sinceSeq !== void 0) this.lastSeq = options.sinceSeq;
2831
+ const result = new Promise((resolve) => {
2832
+ this.historyWaiters.push(resolve);
2833
+ });
2834
+ await this.requestHistory(options.limit);
2835
+ return result;
2836
+ }
2661
2837
  /** Leave the channel and release every listener and timer. */
2662
2838
  async leave() {
2663
2839
  this.stopHeartbeat();
@@ -2665,14 +2841,22 @@ var RebaseRealtimeChannel = class {
2665
2841
  this.presences = {};
2666
2842
  this.presenceHandlers.clear();
2667
2843
  this.broadcastHandlers.clear();
2844
+ this.lastSeq = 0;
2845
+ this.pendingLive = [];
2846
+ this.catchUpInFlight = false;
2847
+ if (this.catchUpTimeout) {
2848
+ clearTimeout(this.catchUpTimeout);
2849
+ this.catchUpTimeout = null;
2850
+ }
2851
+ for (const resolve of this.historyWaiters.splice(0)) resolve({
2852
+ messages: [],
2853
+ retained: false
2854
+ });
2668
2855
  for (const off of this.unsubscribers) off();
2669
2856
  this.unsubscribers = [];
2670
2857
  if (this.joined) {
2671
2858
  this.joined = false;
2672
- await this.transport.sendMessage({
2673
- type: "leave_channel",
2674
- channel: this.name
2675
- });
2859
+ await this.send("leave_channel");
2676
2860
  }
2677
2861
  }
2678
2862
  stopHeartbeat() {
@@ -2700,15 +2884,71 @@ var RebaseRealtimeChannel = class {
2700
2884
  break;
2701
2885
  }
2702
2886
  case "broadcast": {
2887
+ const seq = typeof message.seq === "number" ? message.seq : void 0;
2703
2888
  const event = {
2704
2889
  event: message.event,
2705
- payload: message.payload
2890
+ payload: message.payload,
2891
+ ...seq !== void 0 ? { seq } : {}
2706
2892
  };
2707
- for (const handler of this.broadcastHandlers) handler(event);
2893
+ if (seq === void 0) {
2894
+ this.deliver(event);
2895
+ break;
2896
+ }
2897
+ if (this.catchUpInFlight) {
2898
+ this.pendingLive.push(event);
2899
+ break;
2900
+ }
2901
+ if (seq <= this.lastSeq) break;
2902
+ this.lastSeq = seq;
2903
+ this.deliver(event);
2904
+ break;
2905
+ }
2906
+ case "channel_history": {
2907
+ this.catchUpInFlight = false;
2908
+ if (this.catchUpTimeout) {
2909
+ clearTimeout(this.catchUpTimeout);
2910
+ this.catchUpTimeout = null;
2911
+ }
2912
+ const entries = message.messages ?? [];
2913
+ const retained = message.retained === true;
2914
+ const latestSeq = typeof message.latestSeq === "number" ? message.latestSeq : void 0;
2915
+ for (const resolve of this.historyWaiters.splice(0)) resolve({
2916
+ messages: entries,
2917
+ retained,
2918
+ latestSeq
2919
+ });
2920
+ for (const entry of entries) {
2921
+ if (entry.seq <= this.lastSeq) continue;
2922
+ this.lastSeq = entry.seq;
2923
+ this.deliver({
2924
+ event: entry.event,
2925
+ payload: entry.payload,
2926
+ seq: entry.seq,
2927
+ replayed: true
2928
+ });
2929
+ }
2930
+ this.flushPendingLive();
2708
2931
  break;
2709
2932
  }
2710
2933
  }
2711
2934
  }
2935
+ /** Deliver everything held back during a catch-up, in sequence order. */
2936
+ flushPendingLive() {
2937
+ if (this.pendingLive.length === 0) return;
2938
+ const buffered = this.pendingLive.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
2939
+ this.pendingLive = [];
2940
+ for (const event of buffered) {
2941
+ const seq = event.seq;
2942
+ if (seq !== void 0) {
2943
+ if (seq <= this.lastSeq) continue;
2944
+ this.lastSeq = seq;
2945
+ }
2946
+ this.deliver(event);
2947
+ }
2948
+ }
2949
+ deliver(event) {
2950
+ for (const handler of [...this.broadcastHandlers]) handler(event);
2951
+ }
2712
2952
  emitPresence(diff) {
2713
2953
  const snapshot = { ...this.presences };
2714
2954
  for (const handler of this.presenceHandlers) handler(snapshot, diff);
@@ -2886,13 +3126,13 @@ function createRebaseClient(options) {
2886
3126
  * own membership — and `leave()` from one would otherwise silently
2887
3127
  * cut off the others.
2888
3128
  */
2889
- channel: (name) => {
3129
+ channel: (name, options) => {
2890
3130
  if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
2891
3131
  let existing = realtimeChannels.get(name);
2892
3132
  if (!existing) {
2893
- existing = new RebaseRealtimeChannel(name, ws);
3133
+ existing = new RebaseRealtimeChannel(name, ws, options);
2894
3134
  realtimeChannels.set(name, existing);
2895
- }
3135
+ } else if (options?.history) existing.enableHistory();
2896
3136
  return existing;
2897
3137
  } },
2898
3138
  /**