KristofferStrube.Blazor.WebWorkers 0.1.0-alpha.4

Prefix Reserved
This is a prerelease version of KristofferStrube.Blazor.WebWorkers.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package KristofferStrube.Blazor.WebWorkers --version 0.1.0-alpha.4                
NuGet\Install-Package KristofferStrube.Blazor.WebWorkers -Version 0.1.0-alpha.4                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="KristofferStrube.Blazor.WebWorkers" Version="0.1.0-alpha.4" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add KristofferStrube.Blazor.WebWorkers --version 0.1.0-alpha.4                
#r "nuget: KristofferStrube.Blazor.WebWorkers, 0.1.0-alpha.4"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install KristofferStrube.Blazor.WebWorkers as a Cake Addin
#addin nuget:?package=KristofferStrube.Blazor.WebWorkers&version=0.1.0-alpha.4&prerelease

// Install KristofferStrube.Blazor.WebWorkers as a Cake Tool
#tool nuget:?package=KristofferStrube.Blazor.WebWorkers&version=0.1.0-alpha.4&prerelease                

License: MIT GitHub issues GitHub forks GitHub stars

Blazor.WebWorkers

A Blazor wrapper for the Web Workers part of the HTML API. The API defines ways to run scripts in the background independently of any the primary thread. This allows for long-running scripts that are not interrupted by scripts that respond to clicks or other user interactions, and allows long tasks to be executed without yielding to keep the page responsive. This project implements a wrapper around the API for Blazor so that we can easily and safely create workers.

This wrapper is still just an experiment.

Demo

The sample project can be demoed at https://kristofferstrube.github.io/Blazor.WebWorkers/

On each page, you can find the corresponding code for the example in the top right corner.

Getting Started

Many others like Tewr/BlazorWorker and LostBeard/SpawnDev.BlazorJS have made libraries like this before. This project differs a bit from the other projects by utilizing the wasm-experimental workload. This simplifies the code needed for this to work a lot. The catch to this is that you will need to have the code for your workers in another project. For me this is not only a negative as it also makes it very clear that they do not share memory and that they run in separate contexts, similar to how the Blazor WASM project is separate in a Blazor WebApp.

So to get started you really only need to create a new console project and then make a few adjustments to the .csproj. In the end it should look something like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="<path-to-blazor-webworkers-until-i-release-a-nuget-package>\KristofferStrube.Blazor.WebWorkers.csproj" />
  </ItemGroup>

</Project>

And then you can do whatever you want in the Program.cs file, but I've added some helpers that make it easier to communicate with the main window and create objects.

SlimWorker

Here I have an example of the code needed for a simple pong worker that broadcasts when it is ready to listen for a ping, responds with a pong when it receives that, and then shuts down.

using KristofferStrube.Blazor.WebWorkers;

if (!OperatingSystem.IsBrowser())
    throw new PlatformNotSupportedException("Can only be run in the browser!");

Console.WriteLine("Hey this is running on another thread!");

bool keepRunning = true;

if (args.Length >= 1)
{
    Console.WriteLine($"The worker was initialized with arguments: [{string.Join(", ", args)}]");
}

// This is a helper for listening on messages.
Imports.RegisterOnMessage(e =>
{
    // If we receive a "ping", respond with "pong".
    if (e.GetTypeOfProperty("data") == "string" && e.GetPropertyAsString("data") == "ping")
    {
        // Helper for posting a message.
        Console.WriteLine("Received ping; Sending pong!");
        Imports.PostMessage("pong");
        keepRunning = false;
    }
});

Console.WriteLine("We are now listening for messages.");
Imports.PostMessage("ready");

// We run forever to keep it alive.
while (keepRunning)
    await Task.Delay(100);

Console.WriteLine("Worker done, so stopping!");

And with very little extra setup we can start this and post a message to it once it is ready. For this we use a very simple abstraction over the worker that enable us to specify an assembly name and some arguments for the worker.

SlimWorker slimWorker = await SlimWorker.CreateAsync(
    jSRuntime: JSRuntime,
    assembly: typeof(AssemblyPongWorker).Assembly.GetName().Name!,
    ["Argument1", "Argument2"]
);

EventListener<MessageEvent> eventListener = default!;
eventListener = await EventListener<MessageEvent>.CreateAsync(JSRuntime, async e =>
{
    object? data = await e.Data.GetValueAsync();
    switch (data)
    {
        case "ready":
            Log("We are sending a ping!");
            await slimWorker.PostMessageAsync("ping");
            break;
        case "pong":
            Log("We received a pong!");
            await slimWorker.RemoveOnMessageEventListenerAsync(eventListener);
            await eventListener.DisposeAsync();
            await slimWorker.DisposeAsync();
            break;
    }
});
await slimWorker.AddOnMessageEventListenerAsync(eventListener);

This looks like so:

ping pong demo

JobWorker

Another more basic abstraction is the JobWorker. This simple abstraction runs some job with an input and an output on a worker. The .csproj look identical to the one used for the SlimWorker.

But what differs is that we need to create a class that implements the interface IJob<TInput, TOutput> in the worker project. A simple way to do this is by extending the abstract class JsonJob which uses JSON as the format for transfering its input and output. This limits us to only use inputs and outputs that can be JSON serialized and deserialized.

Here were implement a job that can find the sum of the codes of each individual char in a string.

public class StringSumJob : JsonJob<string, int>
{
    public override int Work(string input)
    {
        int result = 0;
        for (int i = 0; i < input.Length; i++)
        {
            result += input[i];
        }
        return result;
    }
}

Then we need to replace the content of the Program.cs in the worker project with the following to instantiate the job.

if (!OperatingSystem.IsBrowser())
    throw new PlatformNotSupportedException("Can only be run in the browser!");

new StringSumJob().Execute(args);

Finally to call the worker from our main Blazor program we only need the following.

var jobWorker = await JobWorker<string, int, StringSumJob>.CreateAsync(JSRuntime);

int result = await jobWorker.ExecuteAsync(input);

We can create the JobWorker a single time and then run it multiple times with different inputs. Doing this spares us from importing the needed WASM assemblies multiple times which can make consecutive runs much faster.

The library uses the following other packages to support its features:

This repository was built with inspiration and help from the following series of articles:

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.1.0-alpha.7 86 10/2/2024
0.1.0-alpha.6 275 7/8/2024
0.1.0-alpha.5 40 7/8/2024
0.1.0-alpha.4 41 7/2/2024
0.1.0-alpha.3 53 6/27/2024
0.1.0-alpha.2 57 6/27/2024
0.1.0-alpha.1 59 6/20/2024