MessageChannel
A two-ended pipe you can hand one end of to somewhere else - a worker, an iframe, a service worker - and then talk over privately.
@inject Bit.Butil.MessageChannel messageChannelMDN reference
Supported by every current engine. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.MessageChannel messageChannel
var supported = await messageChannel.IsSupported();Neither port is privileged - there is no client end and no server end. Both are wired up here so the traffic is visible in both directions; in a real use one of them would have been sent somewhere else. Messages are JSON: what survives is what System.Text.Json can write and JSON.parse can read.
var channel = await messageChannel.Create();
await channel!.Port1.OnMessage(m => InvokeAsync(StateHasChanged));
await channel.Port2.OnMessage(m => InvokeAsync(StateHasChanged));
await channel.Port1.Start();
await channel.Port2.Start();
await channel.Port1.PostMessage(new { from = "port1", text = "hello" });A port queues everything it receives until it is started - which is what makes 'create it, hand it over, listen, then open it' work, and why listening does not start the port for you. The flip side: a port that is never started queues for as long as it lives, which is a slow leak rather than an error.
var channel = await messageChannel.Create();
await channel!.Port2.OnMessage(m => { /* not called yet */ });
await channel.Port1.PostMessage(new { queued = 1 });
await channel.Port1.PostMessage(new { queued = 2 });
// both arrive now, in order
await channel.Port2.Start();Transferring moves the underlying buffer to the receiver rather than copying it. The .NET array you pass in is a separate copy already and is unaffected; what gets detached is the JavaScript-side buffer.
await channel!.Port1.PostBytes([1, 2, 3, 4], transfer: true);
// arrives as: m.IsBinary == true, m.Data == [1, 2, 3, 4]A port can carry other ports, which is how a third party is introduced to a conversation it was not part of - hand it one end of a channel it did not create. The ports sent are transferred, so the handles passed here stop working and the receiver owns them.
var introduction = await messageChannel.Create();
// Port2 of the new channel travels over the existing channel
await channel!.Port1.PostWithPorts(new { op = "here is a line" }, [introduction!.Port2]);
// introduction.Port2 no longer works here; whoever received it owns it nowMessageChannel service does the same for anything left behind when its scope is torn
down. A port that was transferred away is no longer yours to close, and releasing it here is a
no-op rather than an error.
API reference
ValueTask<bool> IsSupported()ValueTask<MessageChannelHandle?> Create()ValueTask DisposeAsync()MessagePortHandle Port1, Port2ValueTask<ButilSubscription?> OnMessage(Action<ButilMessage> onMessage)ValueTask Start()ValueTask<bool> PostMessage<T>(T value, JsonSerializerOptions? options = null)ValueTask<bool> PostBytes(byte[] data, bool transfer = true)ValueTask<bool> PostWithPorts<T>(T value, MessagePortHandle[] ports, JsonSerializerOptions? options = null)ValueTask Close()record (bool IsBinary, string? Json, byte[]? Data) { T? Deserialize<T>() }