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)
{
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));
}
}