@page "/live" @page "/live/{Room}" @inject NavigationManager Navigation @inject LiveRelay Relay Live, across tabs · Bcrdt

Live, across tabs

Every other page in this demo keeps its replicas side by side in one tab, talking over the library's in-memory transport. This one is the real thing: one replica per browser tab, meeting through the demo server. Open the link below in another tab, another browser or on your phone and type in both.

One replica per tab

This tab is one replica. Share the link, and every tab that opens it is another.

@if (Room is null || !RendererInfo.IsInteractive) {

Connecting…

} else {
Share this link @ShareLink

Opening the document…

You are @_me.Name
Here now:
@document.Text(Field).Count characters @(document.Presence.Others.Count == 1 ? "1 other tab" : $"{document.Presence.Others.Count} other tabs") on this document @if (!_online) { editing offline - changes are held locally until this tab reconnects } client @((document.ClientId % 100_000).ToString("D5"))
}
The thing worth trying. Take one tab offline, type a paragraph into it, type something else into another tab, then bring the first one back. Both paragraphs survive, in the same order in every tab - the handshake sends each side what the other is missing, in both directions.

The page an application would write

The document comes from the app's own AddBitBcrdtServices registration and is opened by BcrdtCollaborationScope; the editor is BcrdtTextArea and the list of people is BcrdtPresenceView. The server only relays: BcrdtHub from Bit.Bcrdt.Server forwards each message to the other tabs on the document without decoding it, and no copy of the document lives on the server - when the last tab closes, it is gone.

Two relays

The demo server maps both relays Bit.Bcrdt.Server ships: BcrdtHub for the SignalR transport of Bit.Bcrdt.SignalR, and the WebSocket endpoint for the WebSocket transport built into Bit.Bcrdt. Tabs only meet on the same relay, so the relay is part of the link you share.

// Demo/Server/Program.cs
builder.Services.AddBitBcrdtHub();   // AddSignalR, with a receive limit sized for sync messages
// ...
app.UseWebSockets();

app.MapHub<BcrdtHub>("/hub/bcrdt");
app.MapBitBcrdtWebSocket("/ws/bcrdt/{documentId}");
@code { private const string Field = "body"; // Who this tab is. A random name is enough for a demo; an application publishes its signed-in user. private readonly BcrdtCollaboratorInfo _me = CreateGuest(); private bool _online = true; /// The room the tabs share. A visit without one is sent to a fresh room. [Parameter] public string? Room { get; set; } /// websocket to meet through the WebSocket endpoint instead of the SignalR hub. [SupplyParameterFromQuery(Name = "relay")] public string? RelayName { get; set; } private LiveRelayKind Kind => string.Equals(RelayName, "websocket", StringComparison.OrdinalIgnoreCase) ? LiveRelayKind.WebSocket : LiveRelayKind.SignalR; // The relay is part of the document identifier: a tab on the hub and a tab on the WebSocket endpoint can never // reach each other, so they must not pretend to be editing the same document. private string DocumentId => $"live-{Room}-{(Kind == LiveRelayKind.WebSocket ? "ws" : "hub")}"; private string ShareLink => RelayLink(Kind); protected override void OnParametersSet() { if (!RendererInfo.IsInteractive) return; if (Room is null || !IsValidRoom(Room)) { Navigation.NavigateTo(RelayLink(Kind, NewRoom()), replace: true); return; } // Read when the scope opens the document, which happens after this. Relay.Kind = Kind; _online = true; } private void Opened(BcrdtCollaborativeDocument document) => _online = true; private async Task ToggleAsync(BcrdtCollaborativeDocument document) { _online = !_online; if (_online) { await document.ConnectAsync(); // Disconnecting withdraws this tab's presence so the others see it leave at once; coming back // publishes it again. document.Presence.SetUser(_me); } else { await document.DisconnectAsync(); } } private string RelayLink(LiveRelayKind kind) => RelayLink(kind, Room ?? NewRoom()); private string RelayLink(LiveRelayKind kind, string room) { string path = $"live/{room}" + (kind == LiveRelayKind.WebSocket ? "?relay=websocket" : string.Empty); return Navigation.ToAbsoluteUri(path).ToString(); } private string RelayClass(LiveRelayKind kind) => kind == Kind ? "live-relay live-relay-active active" : "live-relay"; private static bool IsValidRoom(string room) => room.Length is > 0 and <= 32 && room.All(c => c is (>= 'a' and <= 'z') or (>= '0' and <= '9') or '-'); private static string NewRoom() => Guid.NewGuid().ToString("N")[..10]; private static BcrdtCollaboratorInfo CreateGuest() { string id = Guid.NewGuid().ToString("N"); return new BcrdtCollaboratorInfo(id, $"Guest {Random.Shared.Next(100, 1000)}", BcrdtPresenceService.SuggestColor(id)); } }