Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polling Thread in a Blazor Webassembly App

My question is similar to this one, but instead of pertaining to a Blazor server app, I'm asking in the context of a Blazor webassembly app. I realize that there is only one (UI) thread in this browser execution context, but I figure there must be some kind of framework for a worker or background service. All my googling has come up empty.

I simply need to kick off a background service that polls a web API continually every second for the lifetime of the app.

like image 990
BCA Avatar asked Sep 06 '25 03:09

BCA


1 Answers

I see two different approaches. The first is a simple timer-based call in your AppCompontent. The second is to create a javascript web worker and call it via interop.

Timer-based in App Component

@inject HttpClient client
@implements IDisposable

<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {

    private async void DoPeriodicCall(Object state)
    {
        //a better version can found here https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#timer-callbacks
        var response = await client.GetFromJsonAsync<Boolean>("something here");

        //Call a service, fire an event to inform components, etc
    }

    private System.Threading.Timer _timer;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        _timer = new System.Threading.Timer(DoPeriodicCall, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
    }

    public void Dispose()
    {
        //maybe to a "final" call
        _timer.Dispose();
    }
} 

The result can be observed in the developer tools. enter image description here

Javascript Web worker

A good starting point for background workers can be found here.

If you want to use the result of the call in your WASM app, you need to implement JS interop. The App component calls a javascript method that starts the worker. The javascript method has three inputs, the URL, the interval, and the reference to the App component. The URL and interval are wrapped inside a "cmd" object and passed to the worker when the worker starts. When the worker finished the API call, it sends a message to the javascript back. This javascript invokes a method on the app component.

// js/apicaller.js

let timerId;

self.addEventListener('message',  e => {
    if (e.data.cmd == 'start') {

        let url = e.data.url;
        let interval = e.data.interval;

        timerId = setInterval( () => {

            fetch(url).then(res => {
                if (res.ok) {
                    res.json().then((result) => {
                        self.postMessage(result);
                    });
                } else {
                    throw new Error('error with server');
                }
            }).catch(err => {
                self.postMessage(err.message);
            })
        }, interval);
    } else if(e.data.cmd == 'stop') {
        clearInterval(timerId);
    }
});

// js/apicaller.js

window.apiCaller = {};
window.apiCaller.worker =  new Worker('/js/apicallerworker.js');
window.apiCaller.workerStarted = false;

window.apiCaller.start = function (url, interval, dotNetObjectReference) {

    if (window.apiCaller.workerStarted  == true) {
        return;
    }

    window.apiCaller.worker.postMessage({ cmd: 'start', url: url, interval: interval });

    window.apiCaller.worker.onmessage = (e) => {
        dotNetObjectReference.invokeMethodAsync('HandleInterval', e.data);
    }

    window.apiCaller.workerStarted  = true;
}

window.apiCaller.end = function () {
    window.apiCaller.worker.postMessage({ cmd: 'stop' });
}

You need to modify the index.html to reference the apicaller.js script. I'd recommend including it before the blazor framework to make sure it is available before.

...
    <script src="js/apicaller.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
...
    

The app component needs to be slightly modified.

@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {

    private DotNetObjectReference<App> _selfReference;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (firstRender)
        {
            _selfReference = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeVoidAsync("apiCaller.start", "/sample-data/weather.json", 1000, _selfReference);
        }
    }

    [JSInvokable("HandleInterval")]
    public void ServiceCalled(WeatherForecast[] forecasts)
    {
        //Call a service, fire an event to inform components, etc
    }

    public async ValueTask DisposeAsync()
    {
        await JSRuntime.InvokeVoidAsync("apiCaller.stop");
        _selfReference.Dispose();
    }
}

In the developer tools, you can see a worker does the calls.

enter image description here

Concurrency, multithreading and other concerns

The worker is a true multithreading approach. The thread pooling is handle by the browser. Calls within the worker won't block any statements in the "main" thread. However, it is not as convenient as the first approach. What method to choose depends on your context. As long as your Blazor application wouldn't do much, the first approach might be a reasonable choice. In case your Blazor app already has tons of things to do, offloading to a worker can be very beneficial.

If you go for the worker solution but need a non-default client like with authentication or special headers, you would need to find a mechanism to synchronize the Blazor HttpClient and the calls to the fetch API.

like image 192
Just the benno Avatar answered Sep 07 '25 21:09

Just the benno