KristofferStrube.Blazor.WebWorkers
0.1.0-alpha.6
Prefix Reserved
dotnet add package KristofferStrube.Blazor.WebWorkers --version 0.1.0-alpha.6
NuGet\Install-Package KristofferStrube.Blazor.WebWorkers -Version 0.1.0-alpha.6
<PackageReference Include="KristofferStrube.Blazor.WebWorkers" Version="0.1.0-alpha.6" />
paket add KristofferStrube.Blazor.WebWorkers --version 0.1.0-alpha.6
#r "nuget: KristofferStrube.Blazor.WebWorkers, 0.1.0-alpha.6"
// Install KristofferStrube.Blazor.WebWorkers as a Cake Addin #addin nuget:?package=KristofferStrube.Blazor.WebWorkers&version=0.1.0-alpha.6&prerelease // Install KristofferStrube.Blazor.WebWorkers as a Cake Tool #tool nuget:?package=KristofferStrube.Blazor.WebWorkers&version=0.1.0-alpha.6&prerelease
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:
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.
Related repositories
The library uses the following other packages to support its features:
- https://github.com/KristofferStrube/Blazor.WebIDL (To make error handling JSInterop)
- https://github.com/KristofferStrube/Blazor.DOM (To implement EventTarget's in the package like
Worker
) - https://github.com/KristofferStrube/Blazor.Window (To use the
MessageEvent
type)
Related articles
This repository was built with inspiration and help from the following series of articles:
Product | Versions 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. |
-
net8.0
- KristofferStrube.Blazor.DOM (>= 0.2.2)
- KristofferStrube.Blazor.Window (>= 0.1.0-alpha.4)
- Microsoft.AspNetCore.Components.Web (>= 8.0.3)
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.6 | 63 | 7/8/2024 |
0.1.0-alpha.5 | 32 | 7/8/2024 |
0.1.0-alpha.4 | 33 | 7/2/2024 |
0.1.0-alpha.3 | 45 | 6/27/2024 |
0.1.0-alpha.2 | 49 | 6/27/2024 |
0.1.0-alpha.1 | 51 | 6/20/2024 |