SpawnDev.BlazorJS.WebWorkers
2.1.0
See the version list below for details.
dotnet add package SpawnDev.BlazorJS.WebWorkers --version 2.1.0
NuGet\Install-Package SpawnDev.BlazorJS.WebWorkers -Version 2.1.0
<PackageReference Include="SpawnDev.BlazorJS.WebWorkers" Version="2.1.0" />
paket add SpawnDev.BlazorJS.WebWorkers --version 2.1.0
#r "nuget: SpawnDev.BlazorJS.WebWorkers, 2.1.0"
// Install SpawnDev.BlazorJS.WebWorkers as a Cake Addin #addin nuget:?package=SpawnDev.BlazorJS.WebWorkers&version=2.1.0 // Install SpawnDev.BlazorJS.WebWorkers as a Cake Tool #tool nuget:?package=SpawnDev.BlazorJS.WebWorkers&version=2.1.0
NuGet
Package | Description | Link |
---|---|---|
SpawnDev.BlazorJS | Enhanced Blazor WebAssembly Javascript interop | |
SpawnDev.BlazorJS.WebWorkers | Blazor WebAssembly WebWorkers and SharedWebWorkers |
SpawnDev.BlazorJS
An easy Javascript interop library designed specifically for client side Blazor.
Supports Blazor WebAssembly .Net 6, 7, and 8.
- Use Javascript libraries in Blazor without writing any Javascript code
- An alternative JSRuntime that wraps the default one adding additional functionality.
- Create new Javascript objects directly from Blazor
- Get and set Javascript object properties as well as access methods.
- Easily pass .Net methods to Javascript using the Callback.Create or Callback.CreateOne methods
- Wrap Javascript objects for direct manipulation from Blazor
-
- Easily access Javascript objects by wrapping them in a simple interface that implements IJSObject
-
- Alternatively use the JSObject base class to wrap your objects for more control
- Over 100 strongly typed JSObject wrappers included in BlazorJS including Promises, WebGL, WebRTC, DOM, etc...
- Use SpawnDev.BlazorJS.WebWorkers to enable calling Blazor services in web worker threads
- Supports Promises, Union method parameters, passing undefined to Javascript, and more.
BlazorJSRuntime
Getting started. Add the BlazorJSRuntime service in your Program.cs
...
using SpawnDev.BlazorJS;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// SpawnDev.BlazorJS.BlazorJSRuntime service
builder.Services.AddSingleton<BlazorJSRuntime>(BlazorJSRuntime.JS);
builder.Services.AddScoped<WebTorrentService>();
await builder.Build().RunAsync();
And use.
[Inject]
BlazorJSRuntime JS { get; set; }
// Get Set
var innerHeight = JS.Get<int>("window.innerHeight");
JS.Set("document.title", "Hello World!");
// Call
var item = JS.Call<string?>("localStorage.getItem", "itemName");
JS.CallVoid("addEventListener", "resize", Callback.Create(() => Console.WriteLine("WindowResized"), _callBacks));
IJSInProcessObjectReference extended
// Get Set
var window = JS.Get<IJSInProcessObjectReference>("window");
window.Set("myVar", 5);
var myVar = window.Get<int>("myVar");
// Call
window.CallVoid("addEventListener", "resize", Callback.Create(() => Console.WriteLine("WindowResized")));
Create a new Javascript object
IJSInProcessObjectReference worker = JS.New("Worker", myWorkerScript);
Callback
Pass methods to Javascript using the Callback.Create and Callback.CreateOne methods. These methods use type arguments to set the types expected for incoming arguments (if any) and the expected return type (if any.) async methods are passed as Promises.
Pass lambda callbacks to Javascript
JS.Set("testCallback", Callback.Create<string>((strArg) => {
Console.WriteLine($"Javascript sent: {strArg}");
// this prints "Hello callback!"
}));
// in Javascript
testCallback('Hello callback!');
Pass method callbacks to Javascript
string SomeNetFn(string input){
return $"Recvd: {input}";
}
JS.Set("someNetFn", Callback.CreateOne<string, string>(SomeNetFn));
// in Javascript
someNetFn('Hello callback!');
// prints
Recvd: Hello callback!
Pass async method callbacks to Javascript Under the hood, BlazorJS is returning a Promise to Javascript when the method is called
async Task<string> SomeNetFnAsync(string input){
return $"Recvd: {input}";
}
JS.Set("someNetFnAsync", Callback.CreateOne<string, string>(SomeNetFnAsync));
// in Javascript
await someNetFnAsync('Hello callback!');
// prints
Recvd: Hello callback!
IJSObject Interface
SpawnDev.BlazorJS can now wrap Javascript objects using interfaces. Just like objects derived from the JSObject class, IJSObject interfaces internally use IJSInProcessObjectReference to wrap a Javascript object for direct manipulation and can be passed to and from Javascript. The main difference is IJSObjects use DispatchProxy to implement the desired interface at runtime instead of requiring a type that inherits JSObject. Currently SpawnDev.BlazorJS does not provide any interfaces for Javascript objects or apis but interfaces are simple to set up.
IJSObject Example
// create an interface for your Javascript object that implements IJSObject
public interface IWindow : IJSObject
{
string Name { get; set; }
void Alert(string msg = "");
// ...
}
// use your IJSObject interface to interact with the Javascript object
public void IJSObjectInterfaceTest() {
var w = JS.Get<IWindow>("window");
var randName = Guid.NewGuid().ToString();
// directly set the window.name property
w.Name = randName;
// verify the read back
if (w.Name != randName) throw new Exception("Interface property set/get failed");
}
JSObject Base Class
JSObjects are wrappers around IJSInProcessReference objects that can be passed to and from Javascript and allow strongly typed access to the underlying object. JSObjects take a bit more work to set up but offer more versatility.
JSObject type wrapper example (same as the IJSObject interface example above but with JSObject)
// create a class for your Javascript object that inherits from JSObject
public class Window : JSObject
{
// required constructor
public Window(IJSInProcessObjectReference _ref) : base(_ref) { }
public string Name { get => JSRef.Get<string>("name"); set => JSRef.Set("name", value); }
public void Alert(string msg = "") => JSRef.CallVoid(msg);
// ...
}
// use the JSObject class to interact with the Javascript object
public void JSObjectClassTest() {
var w = JS.Get<Window>("window");
var randName = Guid.NewGuid().ToString();
// directly set the window.name property
w.Name = randName;
// verify the read back
if (w.Name != randName) throw new Exception("Interface property set/get failed");
}
Use the extended functions of IJSInProcessObjectReference to work with Javascript objects or use the growing library of over 100 of the most common Javascript objects, including ones for Window, HTMLDocument, WebStorage (localStorage and sessionStorage), WebGL, WebRTC, and more in SpawnDev.BlazorJS.JSObjects. JSObjects are wrappers around IJSInProcessObjectReference that allow strongly typed use.
Below shows a section of the SpawnDev.BlazorJS.JSObjects.Window class. Window's base type, EventTarget, inherits from JSObject.
public class Window : EventTarget {
// all JSObject types must have this constructor
public Window(IJSInProcessObjectReference _ref) : base(_ref) { }
// here is a property with both getter and setter
public string? Name { get => JSRef.Get<string>("name"); set => JSRef.Set("name", value); }
// here is a read only property that returns another JSObject type
public WebStorage LocalStorage => JSRef.Get<WebStorage>("localStorage");
// here are methods
public long SetTimeout(Callback callback, double delay) => JSRef.Call<long>("setTimeout", callback, delay);
public void ClearTimeout(long requestId) => JSRef.CallVoid("clearTimeout", requestId);
// ...
}
Below the JSObject derived Window class is used
// below the JSObject derived Window class is used
using var window = JS.Get<Window>("window");
var randName = Guid.NewGuid().ToString();
// set and get properties
window.Name = randName;
var name = window.Name;
// call methods
window.Alert("Hello!");
Promise
SpawnDev.BlazorJS.JSObjects.Promise - is a JSObject wrapper for the Javascript Promise class. Promises can be created in .Net to wrap async methods or Tasks. They are essentially Javascript's version of Task.
Ways to create a Promise in .Net
var promise = new Promise();
// pass to Javascript api
...
// then later resolve
promise.Resolve();
Create Promise from lambda
var promise = new Promise(async () => {
await Task.Delay(5000);
});
// pass to Javascript api
Create Promise from lambda with return value
var promise = new Promise<string>(async () => {
await Task.Delay(5000);
return "Hello world!";
});
// pass to Javascript api
Create Promise from Task
var taskSource = new TaskCompletionSource<string>();
var promise = new Promise<string>(taskSource.Task);
// pass to Javascript api
...
// then later resolve
taskSource.TrySetResult("Hello world!");
Below is a an example that uses Promises to utilize the Web Locks API
using var navigator = JS.Get<Navigator>("navigator");
using var locks = navigator.Locks;
Console.WriteLine($"lock: 1");
using var waitLock = locks.Request("my_lock", Callback.CreateOne((Lock lockObj) => new Promise(async () => {
Console.WriteLine($"lock acquired 3");
await Task.Delay(5000);
Console.WriteLine($"lock released 4");
})));
using var waitLock2 = locks.Request("my_lock", Callback.CreateOne((Lock lockObj) => new Promise(async () => {
Console.WriteLine($"lock acquired 5");
await Task.Delay(5000);
Console.WriteLine($"lock released 6");
})));
Console.WriteLine($"lock: 2");
Custom JSObjects
Implement your own JSObject classes for Javascript objects not already available in the BlazorJS.JSObjects library.
Instead of this (simple but not as reusable)
var audio = JS.New("Audio", "https://some_audio_online");
audio.CallVoid("play");
You can do this...
Create a custom JSObject wrapper
public class Audio : JSObject
{
public Audio(IJSInProcessObjectReference _ref) : base(_ref) { }
public Audio(string url) : base(JS.New("Audio", url)) { }
public void Play() => JSRef.CallVoid("play");
}
Then use your new object
var audio = new Audio("https://some_audio_online");
audio.Play();
Union
Use the Union<T1, T2, ...> type with method parameters for strong typing while allowing unrelated types just like in TypeScript.
void UnionTypeTestMethod(string varName, Union<bool?, string?>? unionTypeValue)
{
JS.Set(varName, unionTypeValue);
}
var stringValue = "Hello world!";
UnionTypeTestMethod("_stringUnionValue", stringValue);
if (stringValue != JS.Get<string?>("_stringUnionValue")) throw new Exception("Unexpected result");
var boolValue = true;
UnionTypeTestMethod("_boolUnionValue", boolValue);
if (boolValue != JS.Get<bool?>("_boolUnionValue")) throw new Exception("Unexpected result");
Undefinable
Use Undefinable<T> type to pass undefined to Javascript
Some Javascript API calls may have optional parameters that behave differently depending on if you pass a null versus undefined. You can now retain strong typing on JSObject method calls and support passing undefined for JSObject parameters.
New Undefinable<T> type.
Example from Test app unit tests
// an example method with a parameter that can also be null or undefined
// T of Undefinable<T> must be nullable
void MethodWithUndefinableParams(string varName, Undefinable<bool?>? window)
{
JS.Set(varName, window);
}
bool? w = false;
// test to show the value is passed normally
MethodWithUndefinableParams("_willBeDefined2", w);
bool? r = JS.Get<bool?>("_willBeDefined2");
if (r != w) throw new Exception("Unexpected result");
w = null;
// null defaults to passing as undefined
MethodWithUndefinableParams("_willBeUndefined2", w);
if (!JS.IsUndefined("_willBeUndefined2")) throw new Exception("Unexpected result");
// if you need to pass null to an Undefinable parameter use Undefinable<T?>.Null
MethodWithUndefinableParams("_willBeNull2", Undefinable<bool?>.Null);
if (JS.IsUndefined("_willBeNull2")) throw new Exception("Unexpected result");
// another way to pass undefined
MethodWithUndefinableParams("_willAlsoBeUndefined2", Undefinable<bool?>.Undefined);
if (!JS.IsUndefined("_willAlsoBeUndefined2")) throw new Exception("Unexpected result");
If using JSObjects you can also use JSObject.Undefined<T> to create an instance that will be passed to Javascript as undefined.
// Create an instance of the Window JSObject class that is revived in Javascript as undefined
var undefinedWindow = JSObject.Undefined<Window>();
// undefinedWindow is an instance of Window that is revived in Javascript as undefined
JS.Set("_undefinedWindow", undefinedWindow);
var isUndefined = JS.IsUndefined("_undefinedWindow");
// isUndefined == true here
SpawnDev.BlazorJS.WebWorkers
Easily call Blazor Services in separate threads with WebWorkers and SharedWebWorkers
Does not require SharedArrayBuffer and therefore does not require the special HTTP headers associated with using it.
Supports and uses transferable objects whenever possible
Works in Blazor WASM .Net 6, 7, and 8.
Tested on (with .Net 8):
Chrome Windows - Working
MS Edge Windows - Working
Firefox Windows - Working
Chrome Android - Working
MS Edge Android - Working
Firefox Android - Working
Firefox WebWorkers note:
Firefox does not support dynamic modules in workers, which originally made BlazorJS.WebWorkers fail in that browser.
The web worker script now tries to detect this and changes the blazor wasm scripts before they are loaded to workaround this limitation. It is possible some other browsers may have this issue but may not be detected properly.
Issues can be reported here on GitHub.
Example WebWorkerService setup and usage
// Program.cs
...
using SpawnDev.BlazorJS;
using SpawnDev.BlazorJS.WebWorkers;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
if (JS.IsWindow)
{
// we can skip adding dom objects in non UI threads
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
}
// add services
builder.Services.AddSingleton((sp) => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// SpawnDev.BlazorJS.BlazorJSRuntime service
builder.Services.AddSingleton<BlazorJSRuntime>(BlazorJSRuntime.JS);
// SpawnDev.BlazorJS.WebWorkers.WebWorkerService service
builder.Services.AddSingleton<WebWorkerService>();
// app specific services...
// worker services should be registered with an interface to work with WebWorker.GetService<TServiceInterface>()
builder.Services.AddSingleton<IMathsService, MathsService>();
// build
WebAssemblyHost host = builder.Build();
// init WebWorkerService
var workerService = host.Services.GetRequiredService<WebWorkerService>();
await workerService.InitAsync();
await host.RunAsync();
WebWorker
// Create a WebWorker
[Inject]
WebWorkerService workerService { get; set; }
// ...
var webWorker = await workerService.GetWebWorker();
// Call GetService<ServiceInterface> on a web worker to get a proxy for the service on the web worker.
// GetService can only be called with Interface types
var workerMathService = webWorker.GetService<IMathsService>();
// Call async methods on your worker service
var result = await workerMathService.CalculatePi(piDecimalPlaces);
// Action types can be passed for progress reporting
var result = await workerMathService.CalculatePiWithActionProgress(piDecimalPlaces, new Action<int>((i) =>
{
// the worker thread can call this method to report progress if desired
piProgress = i;
StateHasChanged();
}));
SharedWebWorker
Calling GetSharedWebWorker in another window with the same sharedWorkerName will return the same SharedWebWorker
// Create or get SHaredWebWorker with the provided sharedWorkerName
var sharedWebWorker = await workerService.GetSharedWebWorker("workername");
// Just like WebWorker but shared
var workerMathService = sharedWebWorker.GetService<IMathsService>();
// Call async methods on your shared worker service
var result = await workerMathService.CalculatePi(piDecimalPlaces);
Send events
// Optionally listen for event messages
worker.OnMessage += (sender, msg) =>
{
if (msg.TargetName == "progress")
{
PiProgress msgData = msg.GetData<PiProgress>();
piProgress = msgData.Progress;
StateHasChanged();
}
};
// From SharedWebWorker or WebWorker threads send an event to connected parents
workerService.SendEventToParents("progress", new PiProgress { Progress = piProgress });
// Or on send an event to a connected worker
webWorker.SendEvent("progress", new PiProgress { Progress = piProgress });
Worker Transferable JSObjects
Faster is better. SpawnDev WebWorkers use transferable objects by default for better performance, but it can be disabled with WorkerTransferAttribute. Setting WorkerTransfer to false will cause the property, return value, or parameter to be copied to the receiving thread instead of transferred.
Example
public class ProcessFrameResult : IDisposable
{
[WorkerTransfer(false)]
public ArrayBuffer? ArrayBuffer { get; set; }
public byte[]? HomographyBytes { get; set; }
public void Dispose(){
ArrayBuffer?.Dispose();
}
}
[return: WorkerTransfer(false)]
public async Task<ProcessFrameResult?> ProcessFrame([WorkerTransfer(false)] ArrayBuffer? frameBuffer, int width, int height, int _canny0, int _canny1, double _needlePatternSize)
{
var ret = new ProcessFrameResult();
// ...
return ret;
}
In the above example; the WorkerTransferAttribute on the return type set to false will prevent all properties of the return type from being transferred.
Transferable JSObject types
ArrayBuffer
MessagePort
ReadableStream
WritableStream
TransformStream
AudioData
ImageBitmap
VideoFrame
OffscreenCanvas
RTCDataChannel
IDisposable
NOTE: The above code shows quick examples. Some objects implement IDisposable, such as JSObject, Callback, and IJSInProcessObjectReference types.
JSObject types will dispose of their IJSInProcessObjectReference object when their finalizer is called if not previously disposed.
Callback types must be disposed unless created with the Callback.CreateOne method, in which case they will dispose themselves after the first callback. Disposing a Callback prevents it from being called.
IJSInProcessObjectReference does not dispose of interop resources with a finalizer and MUST be disposed when no longer needed. Failing to dispose these will cause memory leaks.
IDisposable objects returned from a WebWorker or SharedWorker service are automatically disposed after the data has been sent to the calling thread.
Support
Issues can be reported here on GitHub.
Inspired by Tewr's BlazorWorker implementation. Thank you! I wrote my implementation from scratch as I needed workers in .Net 7.
https://github.com/Tewr/BlazorWorker
BlazorJS and WebWorkers Demo
https://blazorjs.spawndev.com/
Buy me a coffee
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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. |
-
net6.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 6.0.13)
- Microsoft.AspNetCore.Components.WebAssembly.DevServer (>= 6.0.13)
- SpawnDev.BlazorJS (>= 2.1.0)
-
net7.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 7.0.3)
- Microsoft.AspNetCore.Components.WebAssembly.DevServer (>= 7.0.3)
- SpawnDev.BlazorJS (>= 2.1.0)
-
net8.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 8.0.0-preview.2.23153.2)
- SpawnDev.BlazorJS (>= 2.1.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on SpawnDev.BlazorJS.WebWorkers:
Package | Downloads |
---|---|
SpawnDev.BlazorJS.PeerJS
PeerJS simplifies peer-to-peer data, video, and audio calls in Blazor WebAssembly |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.5.22 | 0 | 11/25/2024 |
2.5.21 | 33 | 11/21/2024 |
2.5.20 | 66 | 11/20/2024 |
2.5.19 | 72 | 11/18/2024 |
2.5.18 | 67 | 11/17/2024 |
2.5.17 | 75 | 11/16/2024 |
2.5.16 | 68 | 11/15/2024 |
2.5.15 | 66 | 11/15/2024 |
2.5.14 | 70 | 11/14/2024 |
2.5.13 | 77 | 11/13/2024 |
2.5.12 | 54 | 11/10/2024 |
2.5.11 | 161 | 10/31/2024 |
2.5.10 | 219 | 10/9/2024 |
2.5.9 | 93 | 9/27/2024 |
2.5.8 | 545 | 8/13/2024 |
2.5.6 | 73 | 8/8/2024 |
2.5.5 | 113 | 8/7/2024 |
2.5.4 | 88 | 8/6/2024 |
2.5.3 | 77 | 8/5/2024 |
2.5.2 | 81 | 8/5/2024 |
2.5.1 | 118 | 7/26/2024 |
2.5.0 | 79 | 7/26/2024 |
2.4.7 | 93 | 7/24/2024 |
2.4.6 | 95 | 7/22/2024 |
2.4.5 | 152 | 7/19/2024 |
2.4.4 | 93 | 7/18/2024 |
2.4.3 | 112 | 7/16/2024 |
2.4.2 | 76 | 7/15/2024 |
2.4.0 | 72 | 7/15/2024 |
2.3.8 | 79 | 7/14/2024 |
2.3.7 | 123 | 7/9/2024 |
2.3.6 | 98 | 7/8/2024 |
2.3.5 | 95 | 7/6/2024 |
2.3.4 | 92 | 7/4/2024 |
2.3.3 | 132 | 6/23/2024 |
2.3.2 | 127 | 6/16/2024 |
2.3.1 | 219 | 6/13/2024 |
2.3.0 | 100 | 6/12/2024 |
2.2.106 | 112 | 6/5/2024 |
2.2.105 | 133 | 5/31/2024 |
2.2.104 | 116 | 5/30/2024 |
2.2.103 | 99 | 5/29/2024 |
2.2.102 | 111 | 5/28/2024 |
2.2.101 | 111 | 5/22/2024 |
2.2.100 | 148 | 5/17/2024 |
2.2.99 | 101 | 5/17/2024 |
2.2.98 | 115 | 5/16/2024 |
2.2.97 | 132 | 5/15/2024 |
2.2.96 | 87 | 5/14/2024 |
2.2.95 | 95 | 5/13/2024 |
2.2.94 | 100 | 5/11/2024 |
2.2.93 | 120 | 5/7/2024 |
2.2.92 | 111 | 5/7/2024 |
2.2.91 | 131 | 5/3/2024 |
2.2.90 | 86 | 5/3/2024 |
2.2.89 | 66 | 5/2/2024 |
2.2.88 | 72 | 5/2/2024 |
2.2.87 | 128 | 4/26/2024 |
2.2.86 | 112 | 4/26/2024 |
2.2.85 | 134 | 4/18/2024 |
2.2.84 | 121 | 4/18/2024 |
2.2.83 | 125 | 4/16/2024 |
2.2.82 | 148 | 4/8/2024 |
2.2.81 | 118 | 4/8/2024 |
2.2.80 | 136 | 4/7/2024 |
2.2.79 | 121 | 4/6/2024 |
2.2.78 | 123 | 4/5/2024 |
2.2.77 | 121 | 4/5/2024 |
2.2.76 | 116 | 4/4/2024 |
2.2.75 | 104 | 4/4/2024 |
2.2.73 | 94 | 4/3/2024 |
2.2.72 | 111 | 4/3/2024 |
2.2.71 | 109 | 4/3/2024 |
2.2.70 | 109 | 4/2/2024 |
2.2.69 | 272 | 4/1/2024 |
2.2.68 | 117 | 3/29/2024 |
2.2.67 | 151 | 3/27/2024 |
2.2.66 | 125 | 3/24/2024 |
2.2.65 | 121 | 3/21/2024 |
2.2.64 | 174 | 3/11/2024 |
2.2.63 | 123 | 3/9/2024 |
2.2.62 | 132 | 3/7/2024 |
2.2.61 | 127 | 3/6/2024 |
2.2.60 | 123 | 3/6/2024 |
2.2.58 | 177 | 3/2/2024 |
2.2.57 | 198 | 2/24/2024 |
2.2.56 | 143 | 2/18/2024 |
2.2.55 | 118 | 2/17/2024 |
2.2.53 | 127 | 2/15/2024 |
2.2.52 | 120 | 2/15/2024 |
2.2.51 | 120 | 2/15/2024 |
2.2.49 | 871 | 2/2/2024 |
2.2.48 | 1,368 | 12/29/2023 |
2.2.47 | 181 | 12/20/2023 |
2.2.46 | 126 | 12/15/2023 |
2.2.45 | 141 | 12/10/2023 |
2.2.44 | 130 | 12/10/2023 |
2.2.42 | 140 | 12/9/2023 |
2.2.41 | 132 | 12/9/2023 |
2.2.40 | 125 | 12/8/2023 |
2.2.38 | 1,119 | 11/21/2023 |
2.2.37 | 449 | 11/16/2023 |
2.2.36 | 102 | 11/16/2023 |
2.2.35 | 155 | 11/14/2023 |
2.2.34 | 121 | 11/13/2023 |
2.2.33 | 82 | 11/10/2023 |
2.2.32 | 95 | 11/10/2023 |
2.2.31 | 89 | 11/9/2023 |
2.2.28 | 103 | 11/7/2023 |
2.2.27 | 153 | 10/31/2023 |
2.2.26 | 174 | 10/22/2023 |
2.2.25 | 96 | 10/20/2023 |
2.2.24 | 99 | 10/20/2023 |
2.2.23 | 107 | 10/20/2023 |
2.2.22 | 105 | 10/20/2023 |
2.2.21 | 100 | 10/20/2023 |
2.2.20 | 88 | 10/19/2023 |
2.2.19 | 90 | 10/19/2023 |
2.2.18 | 94 | 10/19/2023 |
2.2.17 | 187 | 10/13/2023 |
2.2.16 | 497 | 10/12/2023 |
2.2.15 | 86 | 10/12/2023 |
2.2.14 | 117 | 10/5/2023 |
2.2.13 | 97 | 10/5/2023 |
2.2.12 | 95 | 10/5/2023 |
2.2.11 | 255 | 10/3/2023 |
2.2.10 | 169 | 9/18/2023 |
2.2.9 | 95 | 9/18/2023 |
2.2.8 | 264 | 9/14/2023 |
2.2.7 | 106 | 9/13/2023 |
2.2.6 | 6,610 | 9/6/2023 |
2.2.5 | 147 | 8/30/2023 |
2.2.4 | 154 | 8/26/2023 |
2.2.3 | 121 | 8/20/2023 |
2.2.2 | 107 | 8/18/2023 |
2.2.1 | 118 | 8/11/2023 |
2.2.0 | 208 | 7/17/2023 |
2.1.15 | 131 | 5/26/2023 |
2.1.14 | 111 | 5/20/2023 |
2.1.13 | 126 | 4/26/2023 |
2.1.12 | 196 | 4/21/2023 |
2.1.11 | 116 | 4/19/2023 |
2.1.10 | 136 | 4/19/2023 |
2.1.8 | 145 | 4/10/2023 |
2.1.7 | 168 | 3/27/2023 |
2.1.6 | 141 | 3/24/2023 |
2.1.5 | 141 | 3/23/2023 |
2.1.4 | 146 | 3/23/2023 |
2.1.3 | 147 | 3/23/2023 |
2.1.2 | 142 | 3/21/2023 |
2.1.0 | 144 | 3/21/2023 |
2.0.3 | 151 | 3/21/2023 |
2.0.2 | 141 | 3/20/2023 |
2.0.1 | 142 | 3/20/2023 |
2.0.0 | 152 | 3/20/2023 |
1.9.2 | 153 | 3/14/2023 |
1.8.1 | 144 | 3/11/2023 |
1.8.0 | 142 | 3/10/2023 |
1.7.1 | 145 | 3/10/2023 |
1.7.0 | 135 | 3/8/2023 |
1.6.4 | 151 | 3/1/2023 |
1.6.3 | 308 | 1/31/2023 |
1.6.2 | 320 | 1/24/2023 |
1.6.1 | 329 | 1/11/2023 |
1.6.0 | 329 | 1/11/2023 |
1.5.0 | 373 | 12/23/2022 |
1.4.0 | 323 | 12/20/2022 |
1.3.0 | 344 | 12/16/2022 |
1.2.7 | 350 | 12/16/2022 |
1.2.5 | 321 | 12/14/2022 |
1.2.4.1 | 330 | 12/13/2022 |
1.2.4 | 319 | 12/13/2022 |
1.2.3 | 321 | 12/13/2022 |