You're reading the Rask v1.0.0 guides. Updated .View source
All guides

Broadcast — push a change to every open page

IBroadcast publishes a message on a topic, and every component subscribed to that topic — in every open session — runs its handler and re-renders where it is. A new order appears on every admin's open order list the moment it is placed, with no refresh, no polling and no infrastructure beyond the live connection each page already has.

Included in Rask.Core: registered by the server host and the browser-WASM host, and injected like any other service.

The whole of it

Declare a topic once, as a static field. It names the channel and fixes the message type:


using Rask.Core.Messaging;

public static class Topics
{
    public static readonly Topic<OrderPlaced> Orders = new("orders");
}

public sealed record OrderPlaced(Guid Id, string Customer, decimal Total);

Publish from anywhere that can inject IBroadcast — an event handler, a CQRS handler, a background job:


public sealed class PlaceOrderHandler(IBroadcast broadcast, IDbContextFactory<AppDbContext> contexts)
    : ICommandHandler<PlaceOrder, Guid>
{
    public async Task<Guid> HandleAsync(PlaceOrder command, CancellationToken ct)
    {
        await using var db = await contexts.CreateDbContextAsync(ct);
        var order = Order.Create(command.Customer, command.Total);
        db.Add(order);
        await db.SaveChangesAsync(ct);

        await broadcast.PublishAsync(Topics.Orders, new OrderPlaced(order.Id, order.Customer, order.Total), ct);
        return order.Id;
    }
}

Subscribe in OnMount:


public sealed partial class OrderList(IBroadcast broadcast) : Component
{
    private readonly List<OrderPlaced> _orders = [];

    protected override void OnMount() =>
        broadcast.Subscribe(this, Topics.Orders, order => _orders.Insert(0, order));

    protected override Component? Render() =>
        Ul[_orders.Select(o => Li.Key(o.Id)[$"{o.Customer} — {o.Total:C}"])];
}

That is all. There is no subscription to dispose and no StateHasChanged to call:

  • The subscription lives as long as the component. When OrderList unmounts — the visitor navigates away, the tab closes, the session ends — the subscription goes with it. It is tied to the component's lifetime, not to a handler's cancellation token, so subscribing from inside an event handler does not end at that handler's timeout.
  • The handler runs like an event handler. The component re-renders after it, and every state change it made paints in one frame. An async handler works too: broadcast.Subscribe(this, Topics.Orders, async order => …).

Try it: the button publishes, and the two boards — which know nothing about the button or each other — each receive every order.


using Rask.Core.Messaging;

namespace Rask.Site.Features;

// Publishes on a topic. It holds no reference to the boards below — they only share the topic — so a board added
// anywhere else on the page (or, on a server, in anyone else's open tab) would receive the same message.
public sealed partial class BroadcastDemo(IBroadcast broadcast) : Component
{
    internal static readonly Topic<string> Orders = new("docs.orders");

    private static readonly string[] Items = ["anvil", "rocket skates", "giant magnet", "earthquake pills"];

    private int _placed;

    protected override Component? Render() =>
        Div.Id("broadcast-demo").Class("flex flex-col gap-3")[
            UiButton.Tone(UiTone.Primary).Id("broadcast-publish").OnClick(PlaceOrderAsync)["Place an order"],
            Div.Class("grid gap-3 sm:grid-cols-2")[
                OrderBoard.Title("Warehouse"),
                OrderBoard.Title("Accounting")
            ]
        ];

    private async Task PlaceOrderAsync() =>
        await broadcast.PublishAsync(Orders, $"#{++_placed} {Items[(_placed - 1) % Items.Length]}");
}
Live result

Warehouse

No orders yet.

Accounting

No orders yet.

How a message is delivered

  • In order with the page's own events. Each session runs a message's handlers on its dispatch queue, behind the clicks and keystrokes already waiting there, under the same lock. A handler never races an event handler over the component's state.
  • Once per session. Every subscriber a page has — three components on one page, say — handles the message in the same dispatch, and the page renders once.
  • Without holding the publisher up. PublishAsync returns as soon as the message is queued for every subscribed session; it does not wait for their renders. A slow page cannot slow the code that published.
  • Skipped where the queue is full. A session already holding RaskServerOptions.MaxPendingHandlers dispatches does not get this message, and Rask logs a warning. Its connection is left alone: the backlog is not the visitor's doing.
  • Kept for a page that is reconnecting. A session whose socket has dropped still applies the message, and shows the result on the catch-up render when the browser reconnects.
  • Faults stay with the subscriber. A handler that throws is logged, and the remaining subscribers still run.

What it is not

  • Not a queue. Delivery is at most once, to the subscribers that exist when the message is published. A component that mounts afterwards does not see earlier messages, and nothing is replayed. When a page needs the current state rather than the latest change, load it in OnMountAsync and subscribe for what happens next.
  • Not across servers. A message reaches the sessions this process holds. Behind a load balancer with several instances, a publish on one reaches only that one's visitors; a backplane that carries messages between hosts is planned (#1115). Messages are live objects and are never serialized, so a topic can carry any type, including ones that cannot be.
  • One tab in a WebAssembly app. In a browser-WASM app the whole app is one session, so a broadcast connects the components of that tab — as in the demo above — and never leaves the browser.

Topics

A topic is its name and its message type. Two new Topic<OrderPlaced>("orders") instances are the same topic, so declaring it twice by accident still works; a Topic<string>("orders") is a different topic, so a message can never arrive as the wrong type. Keep topics in one static class per feature so a reader can find everything published in it.

→ Related: composition — callbacks & context for parent–child communication on one page · live pages for how a session renders · CQRS for publishing from a command handler