@rebasepro/client 0.9.1-canary.ff338b5 → 0.10.0
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 +5 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.es.js +261 -10
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +142 -1
- package/dist/websocket.d.ts +1 -0
- package/package.json +4 -4
- package/src/auth.ts +32 -0
- package/src/index.ts +23 -5
- package/src/realtime-channel.test.ts +206 -0
- package/src/realtime-channel.ts +294 -4
- package/src/transport-baseurl.test.ts +53 -0
- package/src/transport.ts +25 -3
- package/src/websocket.ts +13 -2
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,7 +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 } from "./realtime-channel";
|
|
10
|
+
import { RebaseRealtimeChannel, type ChannelOptions } from "./realtime-channel";
|
|
11
11
|
import { InsertOf, RebaseClient, RebaseSdkData, RowOf, StorageSource, StorageSourceDefinition, StorageSourceRegistry, UpdateOf } from "@rebasepro/types";
|
|
12
12
|
export { RebaseApiError } from "./transport";
|
|
13
13
|
export { RebaseClientError } from "./errors";
|
|
@@ -29,7 +29,7 @@ export type { ApiKeyMasked, ApiKeyPermission, ApiKeyWithSecret, CreateApiKeyRequ
|
|
|
29
29
|
export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
|
|
30
30
|
export { RebaseWebSocketClient } from "./websocket";
|
|
31
31
|
export { RebaseRealtimeChannel } from "./realtime-channel";
|
|
32
|
-
export type { PresenceState, PresenceDiff, BroadcastEvent, ChannelTransport } from "./realtime-channel";
|
|
32
|
+
export type { PresenceState, PresenceDiff, BroadcastEvent, ChannelTransport, ChannelOptions, ChannelHistoryEntry, ChannelHistoryResult } from "./realtime-channel";
|
|
33
33
|
export interface CreateRebaseClientOptions extends RebaseClientConfig {
|
|
34
34
|
auth?: CreateAuthOptions;
|
|
35
35
|
admin?: CreateAdminOptions;
|
|
@@ -89,8 +89,11 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
|
|
|
89
89
|
* Join a broadcast/presence channel. Repeated calls with the same name
|
|
90
90
|
* return the same channel object. Throws only when the client was
|
|
91
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.
|
|
92
95
|
*/
|
|
93
|
-
channel: (name: string) => RebaseRealtimeChannel;
|
|
96
|
+
channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
|
|
94
97
|
};
|
|
95
98
|
/**
|
|
96
99
|
* Release the realtime socket and its reconnect timer.
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -2592,15 +2705,58 @@ var RebaseRealtimeChannel = class {
|
|
|
2592
2705
|
}));
|
|
2593
2706
|
await this.send("join_channel");
|
|
2594
2707
|
await this.send("presence_state");
|
|
2708
|
+
if (this.wantsHistory) await this.requestHistory();
|
|
2595
2709
|
}
|
|
2596
2710
|
async rejoin() {
|
|
2597
2711
|
try {
|
|
2598
2712
|
await this.send("join_channel");
|
|
2599
2713
|
await this.send("presence_state");
|
|
2600
2714
|
if (this.trackedState) await this.send("presence_track", { state: this.trackedState });
|
|
2715
|
+
if (this.wantsHistory) await this.requestHistory();
|
|
2601
2716
|
} catch {}
|
|
2602
2717
|
}
|
|
2603
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
|
+
/**
|
|
2604
2760
|
* Publish this client's presence state, and keep publishing it.
|
|
2605
2761
|
*
|
|
2606
2762
|
* Calling `track` again replaces the state (and restarts the heartbeat),
|
|
@@ -2650,6 +2806,34 @@ var RebaseRealtimeChannel = class {
|
|
|
2650
2806
|
this.join();
|
|
2651
2807
|
return () => this.broadcastHandlers.delete(wrapped);
|
|
2652
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
|
+
}
|
|
2653
2837
|
/** Leave the channel and release every listener and timer. */
|
|
2654
2838
|
async leave() {
|
|
2655
2839
|
this.stopHeartbeat();
|
|
@@ -2657,6 +2841,17 @@ var RebaseRealtimeChannel = class {
|
|
|
2657
2841
|
this.presences = {};
|
|
2658
2842
|
this.presenceHandlers.clear();
|
|
2659
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
|
+
});
|
|
2660
2855
|
for (const off of this.unsubscribers) off();
|
|
2661
2856
|
this.unsubscribers = [];
|
|
2662
2857
|
if (this.joined) {
|
|
@@ -2689,15 +2884,71 @@ var RebaseRealtimeChannel = class {
|
|
|
2689
2884
|
break;
|
|
2690
2885
|
}
|
|
2691
2886
|
case "broadcast": {
|
|
2887
|
+
const seq = typeof message.seq === "number" ? message.seq : void 0;
|
|
2692
2888
|
const event = {
|
|
2693
2889
|
event: message.event,
|
|
2694
|
-
payload: message.payload
|
|
2890
|
+
payload: message.payload,
|
|
2891
|
+
...seq !== void 0 ? { seq } : {}
|
|
2695
2892
|
};
|
|
2696
|
-
|
|
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();
|
|
2697
2931
|
break;
|
|
2698
2932
|
}
|
|
2699
2933
|
}
|
|
2700
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
|
+
}
|
|
2701
2952
|
emitPresence(diff) {
|
|
2702
2953
|
const snapshot = { ...this.presences };
|
|
2703
2954
|
for (const handler of this.presenceHandlers) handler(snapshot, diff);
|
|
@@ -2875,13 +3126,13 @@ function createRebaseClient(options) {
|
|
|
2875
3126
|
* own membership — and `leave()` from one would otherwise silently
|
|
2876
3127
|
* cut off the others.
|
|
2877
3128
|
*/
|
|
2878
|
-
channel: (name) => {
|
|
3129
|
+
channel: (name, options) => {
|
|
2879
3130
|
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
2880
3131
|
let existing = realtimeChannels.get(name);
|
|
2881
3132
|
if (!existing) {
|
|
2882
|
-
existing = new RebaseRealtimeChannel(name, ws);
|
|
3133
|
+
existing = new RebaseRealtimeChannel(name, ws, options);
|
|
2883
3134
|
realtimeChannels.set(name, existing);
|
|
2884
|
-
}
|
|
3135
|
+
} else if (options?.history) existing.enableHistory();
|
|
2885
3136
|
return existing;
|
|
2886
3137
|
} },
|
|
2887
3138
|
/**
|