Browser APIs — the sharing model
Where each wrapper lives, how declarative and imperative sharing differ, and how subscriptions push updates back into C#.
‹ Back to Browser APIs
Shared APIs — Rask.Core.Browser
Work identically on Server and WASM. Shape is one-shot (a request/response call) or
subscription (you hold an IAsyncDisposable and the browser pushes updates to a C# handler — see
Subscriptions). Storage, clipboard, geolocation, matchMedia, the screen, cookies, crypto,
permissions, BroadcastChannel, media session, Web Locks, the storage estimate, speech synthesis and recognition, the
screen wake lock, the install prompt, live media streams, animations, files,
gamepads, notifications, the app badge and the rest of what the browser ships are MDN's own surface in
Rask.Web, not wrappers. Device tilt and motion are Window events there:
await Window.OnDeviceOrientation(e => _angle = e.Alpha, every: 100.Milliseconds), where every: throttles them in the
browser before they cross.
| Service | Wraps | What it does | Shape |
|---|---|---|---|
IIndexedDb |
IndexedDB | OpenStore(name) → large async key/value store |
one-shot |
IWebAuthn |
Web Authentication API | Passkeys — register / sign in with biometric or security key | one-shot |
ISignaling |
WebSocket | Join a room on Rask's signaling relay and pass payloads to one peer | subscription |
IWebRtc |
WebRTC | Peer-to-peer data channels between two browsers (you supply the signaling) | subscription |
The screen wake lock is await Navigator.WakeLock.Request(WakeLockType.Screen) and dictation is
await SpeechRecognition.Create(), both from Rask.Web on every host.
Web Push subscription is MDN's own PushManager, from Rask.Web.
Sharing — declarative (all hosts) vs imperative (in-process)
Shareable (Rask.Core) is the all-host way to share, and it's headless — you render the trigger,
it hands you the data-rask-share attribute to spread onto it:
Shareable.Data(new ShareData { Title = "Rask", Url = "https://…" })
.Template(share => Button.Type(ButtonType.Button).Class("inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium no-underline transition disabled:cursor-default disabled:opacity-50 bg-violet-600 text-white hover:bg-violet-500").Data(share)["Share"])
The shared client fires navigator.share inside the click gesture — no round-trip, so the transient
user activation survives even on the Server transport. Because it's headless, the trigger can be any element
with a Data prop (a link, an icon button, a Ui.Button), not just a <button>. Web Share is available on
mobile Safari / Android Chrome / Edge (not desktop Firefox); an unsupported browser no-ops.
Navigator.Share(…) (Rask.Web) is the imperative path — share
from code (a lifecycle hook, after an await). That needs the in-process transport to keep the activation, so it
compiles only in a WASM app.
| API | Home | Hosts | Use |
|---|---|---|---|
Shareable |
Rask.Core |
all (Server too) | Headless declarative share — attaches data-rask-share to your element; fires navigator.share in the gesture |
Navigator.Share(…) |
Rask.Web |
WASM | Imperative share from code |
Gesture bridge — activation-gated APIs on the Server host
Shareable's trick — run the call inside the click gesture so the transient user activation survives —
generalises. Trigger.Gesture and its six typed wrappers are headless the same way: they hand your element a
data-rask-gesture bundle, and the shared client runs the capability in the gesture. That makes normally-WASM-only,
activation-gated APIs reachable declaratively on the Server host (they're still not injectable there).
Capabilities that return a value (the eyedropper's hex, the install outcome) post it back to an
OnResult / OnColor / OnOutcome callback; the two <video> triggers target an element via its ElementRef.
Trigger.Fullscreen.Template(g => Button.Type(ButtonType.Button).Data(g)["Full screen"])
Trigger.ScreenOrientation.Orientation("landscape")
.Template(g => Button.Type(ButtonType.Button).Data(g)["Lock landscape"])
Trigger.EyeDropper.Template(g => Button.Type(ButtonType.Button).Data(g)["Pick a colour"])
.OnColor(hex => picked = hex)
Trigger.Install.Template(g => Button.Type(ButtonType.Button).Data(g)["Install app"])
.OnOutcome(o => outcome = o)
Trigger.MediaCapture.For(preview).Template(g => Button.Type(ButtonType.Button).Data(g)["Start camera"])
.Video()
// Keeps the stream reachable from C# — the only way a Server-hosted app can stop it later.
.OnStream(stream => camera = MediaStream.From(stream))
Trigger.PictureInPicture.For(preview).Template(g => Button.Type(ButtonType.Button).Data(g)["Pop out video"])
The required steps come first — Template on every trigger, plus Orientation or the target For where
the capability needs one; the optional ones follow.
All six ship: Trigger.Fullscreen, Trigger.ScreenOrientation, Trigger.EyeDropper, Trigger.Install,
Trigger.MediaCapture, and Trigger.PictureInPicture. See the capability matrix.
WASM-only APIs — Rask.Wasm.Browser
Registered only by the WASM host. Each needs something the Server transport cannot provide — the
installed-PWA instance / live document, or a browser-only device API. WebUSB, WebHID, Web Serial and Web Bluetooth are
Navigator.Usb, Navigator.Hid, Navigator.Serial and Navigator.Bluetooth in
Rask.Web. Fullscreen and Picture-in-Picture are
await _stage.RequestFullscreen() and await _video.RequestPictureInPicture() on an element ref there. The camera
and microphone are await Navigator.MediaDevices.GetUserMedia(new() { Video = new() }) (every host) and screen
capture is GetDisplayMedia() (WASM); show the stream with await _video.SetSrcObject(stream).
A custom "Install app" button is Window.OnBeforeInstallPrompt(e => _prompt = e), then await _prompt.Prompt() in the
click (WASM); Trigger.Install is the declarative one that also works on Server. See
Where a browser falls short.
| Service | Wraps | What it does | Why WASM-only |
|---|---|---|---|
IBackgroundSync |
Background Sync + Periodic Background Sync | Ask the browser to wake the app when connectivity returns, or on a schedule, to drain an offline queue | service-worker registration |
PWA infrastructure (the typed WebAppManifest, the default service worker, what the templates scaffold) is
covered separately in the Mobile & PWA guide.
Subscriptions — the push pattern
Most wrappers are one-shot request/response. Several are subscriptions, where the browser pushes each change back into C#:
ISignaling—Join(room, handlers, path?)→ connection (Send,IAsyncDisposable); pairs withAddRaskSignaling()/MapRaskSignaling()on the serverIWebRtc—Create(config, handlers)→ connection (IAsyncDisposable); its channels'Listen(onMessages)delivers batches, not single messages — on Server each push is a WebSocket frame, so the framework coalesces them
They share one mechanism: the JS event invokes a static [JSInvokable] via
window.DotNet.invokeMethodAsync (which Rask implements on both transports), routed back to your
handler by an id — so there's a single implementation, no DotNetObjectReference marshalling, and it's
rooted for the WASM trimmer. A Rask.Web object's events are On{Event} subscriptions instead — see
Web APIs from MDN.
Lifecycle. Open from a lifecycle hook (e.g. OnFirstRender()) and dispose the
returned handle on unmount (implement IAsyncDisposable on the component). A handler that updates state
calls StateHasChanged() — the same pattern as subscribing to a background feed. That's a subscription
handler, not a chain-set callback, so RASK026 (which forbids
StateHasChanged inside OnChange/OnClick/Bind/… callbacks) does not apply.
public sealed partial class LazyImages : Component
{
private readonly ElementRef _sentinel = ElementRef.New();
private Types.IntersectionObserver? _io;
protected override Component? Render() => Div.Ref(_sentinel)[ /* … */ ];
// The observers are MDN's own, from Rask.Web (docs/web-apis.md): the handler runs in this component and
// re-renders it, so it needs no StateHasChanged.
protected override async Task OnFirstRender()
{
_io ??= await IntersectionObserver.Create(entries =>
{
if (entries.Any(e => e.IsIntersecting)) LoadMore();
}, new() { RootMargin = "200px" });
await _io.Observe(_sentinel);
}
protected override async Task OnUnmount()
{
if (_io is not null) await _io.DisposeAsync();
}
}