├── SpawnDev.BlazorJS.WebWorkers.Demo ├── Pages │ ├── Home.razor │ ├── Weather.razor │ ├── TestModuleWorker.razor │ └── Counter.razor ├── wwwroot │ ├── favicon.png │ ├── icon-192.png │ ├── app.worker.module.globals.js │ ├── app.service-worker.module.globals.js │ ├── app.worker.module.js │ ├── app.service-worker.module.js │ ├── sample-data │ │ └── weather.json │ ├── index.html │ └── css │ │ └── app.css ├── Layout │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css ├── _Imports.razor ├── App.razor ├── Properties │ └── launchSettings.json ├── SpawnDev.BlazorJS.WebWorkers.Demo.csproj ├── Program.cs └── Services │ └── AppServiceWorker.cs ├── .editorconfig ├── SpawnDev.BlazorJS.WebWorkers ├── Images │ └── icon-128.png ├── buildcontent │ ├── patch-post.min.js │ ├── patch-post.js │ ├── patch.min.js │ └── patch.js ├── TransferableListAttribute.cs ├── AssetManifest.cs ├── ManifestAsset.cs ├── ServiceWorkerStartupRegistration.cs ├── MissedEvents │ ├── MissedSyncEvent.cs │ ├── MissedPeriodicSyncEvent.cs │ ├── MissedPushEvent.cs │ ├── MissedContentIndexEvent.cs │ ├── MissedExtendableEvent.cs │ ├── MissedNotificationEvent.cs │ ├── MissedPaymentRequestEvent.cs │ ├── MissedBackgroundFetchEvent.cs │ ├── MissedExtendableMessageEvent.cs │ ├── IMissedEvent.cs │ ├── MissedPushSubscriptionChangeEvent.cs │ ├── MissedBackgroundFetchUpdateUIEvent.cs │ ├── MissedExtendableCookieChangeEvent.cs │ ├── MissedFetchEvent.cs │ └── MissedCanMakePaymentEvent.cs ├── ServiceCallDispatcherInfo.cs ├── OriginCallableAttribute.cs ├── WorkerTransferAttribute.cs ├── ClaimsIdentityExtensions.cs ├── WebWorkerOptions.cs ├── SharedWebWorkerOptions.cs ├── SharedWebWorker.cs ├── wwwroot │ ├── spawndev.blazorjs.webworkers.empty.js │ ├── spawndev.blazorjs.webworkers.module.js │ ├── spawndev.blazorjs.webworkers.interconnect.js │ ├── spawndev.blazorjs.webworkers.event-holder.js │ └── spawndev.blazorjs.webworkers.import-shim.js ├── FromServicesAttribute.cs ├── build │ ├── SpawnDev.BlazorJS.WebWorkers.props │ └── SpawnDev.BlazorJS.WebWorkers.targets ├── RemoteCallableAttribute.cs ├── WebWorker.cs ├── CHANGELOG.md ├── ServiceWorkerConfig.cs ├── ExceptionSerializer.cs ├── WorkerCancellationTokenSource.cs ├── AppInstanceInfo.cs ├── SharedCancellationToken.cs ├── WorkerCancellationToken.cs ├── IServiceCollectionExtensions.cs ├── CancellationTokenExtensions.cs ├── SpawnDev.BlazorJS.WebWorkers.csproj ├── InterfaceCallDispatcher.cs ├── MethodInfoExtension.cs ├── SerializableConstructorInfo.cs ├── SharedCancellationTokenSource.cs ├── SerializableMethodInfoSlim.cs ├── SerializableMethodBase.cs ├── AppInstance.cs └── AsyncCallDispatcherSlim.cs ├── SpawnDev.BlazorJS.WebWorkers.Build ├── Tasks │ ├── FileHasher.cs │ └── ImportShimBlazorWASM.cs └── SpawnDev.BlazorJS.WebWorkers.Build.csproj ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── LICENSE.txt ├── SpawnDev.BlazorJS.WebWorkers.sln ├── .gitattributes └── .gitignore /SpawnDev.BlazorJS.WebWorkers.Demo/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument 4 | dotnet_diagnostic.CA2018.severity = none 5 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/Images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.WebWorkers/main/SpawnDev.BlazorJS.WebWorkers/Images/icon-128.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.WebWorkers/main/SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/favicon.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.WebWorkers/main/SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/icon-192.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/buildcontent/patch-post.min.js: -------------------------------------------------------------------------------- 1 | globalThis.blazorConfig&&globalThis.blazorConfig.autoStart&&(globalThis.blazorConfig.webAssemblyConfig?Blazor.start(globalThis.blazorConfig.webAssemblyConfig):Blazor.start()); -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/TransferableListAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Use this attribute to mark a parameter as a transferable list indicating what objects to transfer when posting to a web worker. 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter)] 7 | public class TransferableListAttribute : Attribute 8 | { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 |
3 | 6 | 7 |
8 |
9 | About 10 |
11 | 12 |
13 | @Body 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/app.worker.module.globals.js: -------------------------------------------------------------------------------- 1 | // anything that needs to be set or run before other stuff must be in the first statically imported script because 2 | // static imports in the main file are loaded before any code, no matter the placement of the code or imports. 3 | 4 | // this tells spawndev.blazorjs.webworkers.module.js not to start Blazor. We will start it with Blazor.start() and optionally with any startup changes we need. 5 | globalThis.autoStart = false; 6 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/AssetManifest.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Asset manifest 5 | /// 6 | public class AssetManifest 7 | { 8 | /// 9 | /// Assets 10 | /// 11 | public List Assets { get; set; } 12 | /// 13 | /// Version 14 | /// 15 | public string Version { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using SpawnDev.BlazorJS.WebWorkers.Demo 10 | @using SpawnDev.BlazorJS.WebWorkers.Demo.Layout 11 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/app.service-worker.module.globals.js: -------------------------------------------------------------------------------- 1 | // anything that needs to be set or run before other stuff must be in the first statically imported script because 2 | // static imports in the main file are loaded before any code, no matter the placement of the code or imports. 3 | 4 | // this tells spawndev.blazorjs.webworkers.module.js not to start Blazor. We will start it with Blazor.start() and optionally with any startup changes we need. 5 | globalThis.autoStart = false; 6 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/app.worker.module.js: -------------------------------------------------------------------------------- 1 | // A custom service worker or web worker startup script can be used to static import Javascript libraries. 2 | // This was specifically tested to allow Blazor WASM to run in a Chrome Browser extension background ServiceWorker running in 'module' mode so it can run Transformers.js 3 | 4 | import * as _globals from "./app.worker.module.globals.js" 5 | import * as _importShim from "./spawndev.blazorjs.webworkers.module.js" 6 | 7 | // Start Blazor 8 | Blazor._startTask ??= Blazor.start(); 9 | 10 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/app.service-worker.module.js: -------------------------------------------------------------------------------- 1 | // A custom service worker or web worker startup script can be used to static import Javascript libraries. 2 | // This was specifically tested to allow Blazor WASM to run in a Chrome Browser extension background ServiceWorker running in 'module' mode so it can run Transformers.js 3 | 4 | import * as _globals from "./app.service-worker.module.globals.js" 5 | import * as _blazorWorker from "./spawndev.blazorjs.webworkers.module.js" 6 | 7 | // Start Blazor 8 | Blazor._startTask ??= Blazor.start(); 9 | 10 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ManifestAsset.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Manifest asset info 5 | /// 6 | public class ManifestAsset 7 | { 8 | /// 9 | /// File content hash. This should be the base-64-formatted SHA256 value. 10 | /// 11 | public string Hash { get; set; } 12 | /// 13 | /// Asset URL. Normally this will be relative to the application's base href. 14 | /// 15 | public string Url { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/buildcontent/patch-post.js: -------------------------------------------------------------------------------- 1 | //var locationPathname = new URL(globalThis.location.href).pathname; 2 | //var locationFilename = locationPathname.substring(locationPathname.lastIndexOf('/') + 1); 3 | //if (locationFilename.indexOf('blazor.webassembly.js') !== -1) { 4 | // globalThis.blazorConfig.autoStart = true; 5 | //} 6 | if (globalThis.blazorConfig && globalThis.blazorConfig.autoStart) { 7 | if (globalThis.blazorConfig.webAssemblyConfig) { 8 | Blazor.start(globalThis.blazorConfig.webAssemblyConfig); 9 | } else { 10 | Blazor.start(); 11 | } 12 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/sample-data/weather.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2022-01-06", 4 | "temperatureC": 1, 5 | "summary": "Freezing" 6 | }, 7 | { 8 | "date": "2022-01-07", 9 | "temperatureC": 14, 10 | "summary": "Bracing" 11 | }, 12 | { 13 | "date": "2022-01-08", 14 | "temperatureC": -13, 15 | "summary": "Freezing" 16 | }, 17 | { 18 | "date": "2022-01-09", 19 | "temperatureC": -16, 20 | "summary": "Balmy" 21 | }, 22 | { 23 | "date": "2022-01-10", 24 | "temperatureC": -2, 25 | "summary": "Chilly" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ServiceWorkerStartupRegistration.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// ServiceWorkerStartupRegistration enum 5 | /// 6 | public enum ServiceWorkerStartupRegistration 7 | { 8 | /// 9 | /// Do nothing 10 | /// 11 | None, 12 | /// 13 | /// Register the service worker 14 | /// 15 | Register, 16 | /// 17 | /// Unregister the service worker 18 | /// 19 | Unregister, 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedSyncEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | internal class MissedSyncEvent : SyncEvent, IMissedEvent 7 | { 8 | /// 9 | public MissedSyncEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 10 | /// 11 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 12 | /// 13 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 14 | /// 15 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedPeriodicSyncEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | internal class MissedPeriodicSyncEvent : PeriodicSyncEvent, IMissedEvent 7 | { 8 | /// 9 | public MissedPeriodicSyncEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 10 | /// 11 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 12 | /// 13 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 14 | /// 15 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ServiceCallDispatcherInfo.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Basic instance information 5 | /// 6 | public class ServiceCallDispatcherInfo 7 | { 8 | /// 9 | /// From the instance's BlazorJSRuntime.InstanceId property 10 | /// 11 | public string InstanceId { get; init; } = ""; 12 | /// 13 | /// The Javascript globalThis class name
14 | /// - Window
15 | /// - DedicatedWorkerGlobalScope
16 | /// - SharedWorkerGlobalScope
17 | /// - ServiceWorkerGlobalScope 18 | ///
19 | public string GlobalThisTypeName { get; init; } = ""; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Build/Tasks/FileHasher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace SpawnDev.BlazorJS.WebWorkers.Build.Tasks; 7 | 8 | public static class FileHasher 9 | { 10 | public static string GetStringHashBase64(string text) 11 | { 12 | using var hash = SHA256.Create(); 13 | var bytes = Encoding.UTF8.GetBytes(text); 14 | var hashBytes = hash.ComputeHash(bytes); 15 | return Convert.ToBase64String(hashBytes); 16 | } 17 | public static string GetFileHashBase64(string filePath) 18 | { 19 | var bytes = File.ReadAllBytes(filePath); 20 | using var hash = SHA256.Create(); 21 | var hashBytes = hash.ComputeHash(bytes); 22 | return Convert.ToBase64String(hashBytes); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/OriginCallableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Used to modify call options for SpawnDev.BlazorJS.WebWorkers.ServiceCallDispatcher 5 | /// 6 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 7 | public class OriginCallableAttribute : Attribute 8 | { 9 | /// 10 | /// Methods with NoReply = true will not send exceptions or results back to the caller
11 | /// That makes NoReply calls quicker and useful for broadcast/event messages
12 | /// This should only be used on methods that return void or Task (un-typed) and
13 | /// there may be issues with serialization of some types 14 | ///
15 | public bool NoReply { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedPushEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedPushEvent : PushEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedPushEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedContentIndexEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedContentIndexEvent : ContentIndexEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedContentIndexEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedExtendableEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An ExtendableEvent that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedExtendableEvent : ExtendableEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedExtendableEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedNotificationEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedNotificationEvent : NotificationEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedNotificationEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedPaymentRequestEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedPaymentRequestEvent : PaymentRequestEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedPaymentRequestEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedBackgroundFetchEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedBackgroundFetchEvent : BackgroundFetchEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedBackgroundFetchEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/WorkerTransferAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// The WorkerTransferAttribute is used to mark values that should be added to the transferred list when send to another context 5 | /// 6 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.ReturnValue | AttributeTargets.Parameter, Inherited = false, AllowMultiple = true)] 7 | public class WorkerTransferAttribute : Attribute 8 | { 9 | /// 10 | /// If true, the value will be added to the transferable list 11 | /// 12 | public bool Transfer { get; private set; } = true; 13 | /// 14 | /// New instance 15 | /// 16 | /// 17 | public WorkerTransferAttribute(bool transfer = true) => Transfer = transfer; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Build/SpawnDev.BlazorJS.WebWorkers.Build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 10.0 6 | ..\SpawnDev.BlazorJS.WebWorkers\tasks\netstandard2.0 7 | true 8 | 2.5.5 9 | 10 | 11 | 12 | False 13 | 14 | 15 | 16 | False 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedExtendableMessageEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedExtendableMessageEvent : ExtendableMessageEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedExtendableMessageEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [LostBeard] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/IMissedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// An ExtendableEvent that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
5 | /// Implementaors will be able to use the WaitResolve and WaitReject methods to resolve or reject the event.
6 | ///
7 | public interface IMissedEvent 8 | { 9 | /// 10 | /// Resolves the missed ExtendableEvent.
11 | ///
12 | void WaitResolve(); 13 | /// 14 | /// Rejects the missed ExtendableEvent.
15 | ///
16 | void WaitReject(); 17 | /// 18 | /// Returns true if the event is an ExtendableEvent.
19 | ///
20 | bool IsExtended { get; } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedPushSubscriptionChangeEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedPushSubscriptionChangeEvent: PushSubscriptionChangeEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedPushSubscriptionChangeEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedBackgroundFetchUpdateUIEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedBackgroundFetchUpdateUIEvent : BackgroundFetchUpdateUIEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedBackgroundFetchUpdateUIEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedExtendableCookieChangeEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedExtendableCookieChangeEvent : ExtendableCookieChangeEvent, IMissedEvent 10 | { 11 | /// 12 | public MissedExtendableCookieChangeEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | public void WaitResolve() => JSRef!.CallVoid("waitResolve"); 15 | /// 16 | public void WaitReject() => JSRef!.CallVoid("waitReject"); 17 | /// 18 | public bool IsExtended => !JSRef!.IsUndefined("waitResolve"); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ClaimsIdentityExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | internal static class ClaimsIdentityExtensions 6 | { 7 | public static string ToBase64(this ClaimsIdentity claimsIdentity) 8 | { 9 | using var buffer = new System.IO.MemoryStream(); 10 | using var writer = new System.IO.BinaryWriter(buffer); 11 | claimsIdentity.WriteTo(writer); 12 | var data = buffer.ToArray(); 13 | return Convert.ToBase64String(data); 14 | } 15 | public static ClaimsIdentity Base64ToClaimsIdentity(this string claimsIdentity) 16 | { 17 | var data = Convert.FromBase64String(claimsIdentity); 18 | using var buffer = new System.IO.MemoryStream(data); 19 | using var reader = new System.IO.BinaryReader(buffer); 20 | return new ClaimsIdentity(reader); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/WebWorkerOptions.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// Options used for CreateWebWorker 7 | /// 8 | public class WebWorkerOptions 9 | { 10 | /// 11 | /// WorkerOptions 12 | /// 13 | public WorkerOptions? WorkerOptions { get; set; } 14 | /// 15 | /// The URL to the worker script to load.
16 | /// Defaults to: 17 | /// module - "spawndev.blazorjs.webworkers.module.js"
18 | /// classic - "spawndev.blazorjs.webworkers.js"
19 | ///
20 | public string? ScriptUrl { get; set; } = null; 21 | /// 22 | /// Additional query parameters to add to the worker script URL.
23 | ///
24 | public Dictionary? QueryParams { get; set; } = null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SharedWebWorkerOptions.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// Options used for CreateWebWorker 7 | /// 8 | public class SharedWebWorkerOptions 9 | { 10 | /// 11 | /// WorkerOptions 12 | /// 13 | public SharedWorkerOptions? WorkerOptions { get; set; } 14 | /// 15 | /// The URL to the worker script to load.
16 | /// Defaults to: 17 | /// module - "spawndev.blazorjs.webworkers.module.js"
18 | /// classic - "spawndev.blazorjs.webworkers.js"
19 | ///
20 | public string? ScriptUrl { get; set; } = null; 21 | /// 22 | /// Additional query parameters to add to the worker script URL.
23 | ///
24 | public Dictionary? QueryParams { get; set; } = null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SharedWebWorker.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | public class SharedWebWorker : ServiceCallDispatcher, IDisposable 6 | { 7 | public static bool Supported; 8 | static SharedWebWorker() 9 | { 10 | Supported = !JS.IsUndefined("SharedWorker"); 11 | } 12 | SharedWorker _shareWorker { get; set; } 13 | public string Name { get; } 14 | public SharedWebWorker(string name, SharedWorker sharedWorker, IBackgroundServiceManager webAssemblyServices) : base(webAssemblyServices, sharedWorker.Port) 15 | { 16 | Name = name; 17 | _shareWorker = sharedWorker; 18 | if (_port is MessagePort port) port.Start(); 19 | } 20 | 21 | protected override void Dispose(bool disposing) 22 | { 23 | if (IsDisposed) return; 24 | _shareWorker?.Dispose(); 25 | _port?.Dispose(); 26 | base.Dispose(disposing); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedFetchEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedFetchEvent : FetchEvent 10 | { 11 | /// 12 | public MissedFetchEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | /// Resolves the FetchEvent.
15 | ///
16 | /// 17 | public void ResponseResolve(Response response) => JSRef!.CallVoid("responseResolve", response); 18 | /// 19 | /// Rejects the FetchEvent.
20 | ///
21 | public void ResponseReject() => JSRef!.CallVoid("responseReject"); 22 | /// 23 | public bool IsExtended => !JSRef!.IsUndefined("responseResolve"); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MissedEvents/MissedCanMakePaymentEvent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// An Event that was initially missed while Blazor was loading, but was held using waitUntil() so that Blazor can handle it.
8 | ///
9 | internal class MissedCanMakePaymentEvent : CanMakePaymentEvent 10 | { 11 | /// 12 | public MissedCanMakePaymentEvent(IJSInProcessObjectReference _ref) : base(_ref) { } 13 | /// 14 | /// Resolves the CanMakePaymentEvent.
15 | ///
16 | /// 17 | public void ResponseResolve(bool response) => JSRef!.CallVoid("responseResolve", response); 18 | /// 19 | /// Rejects the CanMakePaymentEvent.
20 | ///
21 | public void ResponseReject() => JSRef!.CallVoid("responseReject"); 22 | /// 23 | public bool IsExtended => !JSRef!.IsUndefined("responseResolve"); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/wwwroot/spawndev.blazorjs.webworkers.empty.js: -------------------------------------------------------------------------------- 1 | // to test dynamic import support this script will be loaded. 2 | // it is intentionally empty. 3 | // supports content-sceurity-policy that prohibits script-src from data: (previous test method) 4 | 5 | // dynamic import support tested using the below method which tries to load this (mostly) empty script 6 | //async function hasDynamicImport() { 7 | // try { 8 | // await import('empty.js'); 9 | // return true; 10 | // } catch (e) { 11 | // return false; 12 | // } 13 | //} 14 | 15 | // previous test method that was not compatible with stricter CSPs 16 | //async function hasDynamicImport() { 17 | // // ServiceWorkers have issues with dynamic imports even if detection says it is detected as supported; may jsut not have been loaded usign 'module' keyword 18 | // if (browserExtension) { 19 | // return true; 20 | // } 21 | // if (globalThisTypeName == 'ServiceWorkerGlobalScope') { 22 | // return false; 23 | // } 24 | // try { 25 | // await import('data:text/javascript;base64,Cg=='); 26 | // return true; 27 | // } catch (e) { 28 | // return false; 29 | // } 30 | //} 31 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/FromServicesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Method parameters marked with the FromServices attribute will be resolved from the fulfilling side's service provider 5 | /// 6 | [AttributeUsage(AttributeTargets.Parameter)] 7 | public class FromServicesAttribute : Attribute { } 8 | 9 | #if !NET8_0_OR_GREATER 10 | [AttributeUsage(AttributeTargets.Parameter)] 11 | public class FromKeyedServicesAttribute : Attribute 12 | { 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// The key of the keyed service to bind to. 17 | public FromKeyedServicesAttribute(object key) => Key = key; 18 | 19 | /// 20 | /// The key of the keyed service to bind to. 21 | /// 22 | public object Key { get; } 23 | } 24 | #endif 25 | 26 | /// 27 | /// Method parameters marked with the FromLocal attribute will be resolved from the fulfilling side 28 | /// 29 | [AttributeUsage(AttributeTargets.Parameter)] 30 | public class FromLocalAttribute : Attribute { } 31 | } 32 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/build/SpawnDev.BlazorJS.WebWorkers.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | false 8 | wwwroot 9 | $(Configuration)Extension 10 | 11 | <_DebugSpawnDevWebWorkersBuildTasks Condition="'$(_DebugSpawnDevWebWorkersBuildTasks)' == ''">false 12 | <_WebWorkerBuildTasksAssembly Condition="'$(_WebWorkerBuildTasksAssembly)' == ''">$(MSBuildThisFileDirectory)..\tasks\netstandard2.0\SpawnDev.BlazorJS.WebWorkers.Build.dll 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/RemoteCallableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.WebWorkers 2 | { 3 | /// 4 | /// Used to modify call options for SpawnDev.BlazorJS.WebWorkers.RemoteDispatcher 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 7 | public class RemoteCallableAttribute : Attribute 8 | { 9 | /// 10 | /// Methods with NoReply = true will not send exceptions or results back to the caller
11 | /// That makes NoReply calls quicker and useful for broadcast/event messages
12 | /// This should only be used on methods that return void or Task (un-typed) and
13 | /// there may be issues with serialization of some types 14 | ///
15 | public bool NoReply { get; set; } 16 | /// 17 | /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
18 | /// RemoteDispatcher.User will be checked for the specified roles.
19 | /// It is up to the implementing app to handle role management in RemoteDispatcher.User
20 | /// If no roles are set, no role based restrictions will be applied 21 | ///
22 | public string? Roles { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/WebWorker.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | public class WebWorker : ServiceCallDispatcher, IDisposable 6 | { 7 | public static bool Supported; 8 | static WebWorker() 9 | { 10 | Supported = !JS.IsUndefined("Worker"); 11 | } 12 | Worker _worker; 13 | public WebWorker(Worker worker, IBackgroundServiceManager webAssemblyServices) : base(webAssemblyServices, worker) 14 | { 15 | _worker = worker; 16 | } 17 | /// 18 | /// Called when being Disposed, before the disposal 19 | /// 20 | public event Action OnDisposing; 21 | /// 22 | /// Returns true if disposal has started 23 | /// 24 | public bool IsDisposing { get; protected set; } = false; 25 | protected override void Dispose(bool disposing) 26 | { 27 | if (IsDisposed) return; 28 | if (IsDisposing) return; 29 | IsDisposing = true; 30 | OnDisposing?.Invoke(this); 31 | try 32 | { 33 | _worker?.Terminate(); 34 | } 35 | catch { } 36 | _worker?.Dispose(); 37 | base.Dispose(disposing); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SpawnDev.BlazorJS.WebWorkers.Demo 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 |
31 | An unhandled error has occurred. 32 | Reload 33 | 🗙 34 |
35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [2.5.22] - 2024-11-25 5 | 6 | ### Changed 7 | - Updated SpawnDev.BlazorJS dependency to 2.5.22 8 | 9 | 10 | ## [2.5.17] - 2024-11-15 11 | 12 | ### Changed 13 | - Updated SpawnDev.BlazorJS dependency to 2.5.17 14 | 15 | ### Fixed 16 | - Fixed JsonConverterAttribute with HybridConverter and HybridConverterFactory not working (Ex: SharedCancellationTokenSource) 17 | 18 | 19 | ## [2.5.16] - 2024-11-15 20 | 21 | ### Changed 22 | - Updated SpawnDev.BlazorJS dependency to 2.5.16 23 | 24 | 25 | ## [2.5.15] - 2024-11-15 26 | 27 | ### Changed 28 | - Updated SpawnDev.BlazorJS dependency to 2.5.15 29 | 30 | 31 | ## [2.5.14] - 2024-11-14 32 | 33 | ### Changed 34 | - Updated SpawnDev.BlazorJS dependency to 2.5.14 35 | 36 | 37 | ## [2.5.13] - 2024-11-12 38 | 39 | ### Changed 40 | - Updated package icon 41 | - Updated net9.0 Microsoft.AspNetCore.Components.WebAssembly dependency to 9.0.0 42 | - Updated SpawnDev.BlazorJS dependency to 2.5.13 43 | 44 | 45 | ## [2.5.12] - 2024-11-10 46 | 47 | ### Changed 48 | - Updated SpawnDev.BlazorJS dependency to 2.5.12 49 | 50 | 51 | ## [2.5.11] - 2024-10-31 52 | 53 | ### Fixed 54 | - Fixed WebWorkerService startup issue when running on the server. Allows registering the WebWorkerService on the server so that server side rendering and pre-rendering of components that use the WebWorkerService do not throw an error. The service will only be functional when using WASM rendering. 55 | 56 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ServiceWorkerConfig.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// Options for managing ServiceWorker registration 7 | /// 8 | public class ServiceWorkerConfig 9 | { 10 | /// 11 | /// The registration action to take when the app starts in a Window scope 12 | /// 13 | public ServiceWorkerStartupRegistration Register { get; set; } = ServiceWorkerStartupRegistration.Register; 14 | /// 15 | /// Defaults to WebWorkerService.WebWorkerJSScript ('spawndev.blazorjs.webworkers.js')
16 | ///
17 | public string? ScriptURL { get; set; } 18 | /// 19 | /// By default, this is "service-worker-assets.js" 20 | /// This should be the value from <ServiceWorkerAssetsManifest> in your project's .csproj file if different than the default 21 | /// 22 | public string? ServiceWorkerAssetsManifest { get; set; } 23 | /// 24 | /// This should be true if using <ServiceWorkerAssetsManifest> in your project's .csproj file 25 | /// 26 | public bool ImportServiceWorkerAssets { get; set; } 27 | /// 28 | /// Options used when registering a ServiceWorker via ServiceWorkerContainer.Register() 29 | /// 30 | public ServiceWorkerRegistrationOptions? Options { get; set; } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:63109", 8 | "sslPort": 44300 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5030", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7288;http://localhost:5030", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @inject HttpClient Http 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates fetching data from the server.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); 45 | } 46 | 47 | public class WeatherForecast 48 | { 49 | public DateOnly Date { get; set; } 50 | 51 | public int TemperatureC { get; set; } 52 | 53 | public string? Summary { get; set; } 54 | 55 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 35 | @code { 36 | private bool collapseNavMenu = true; 37 | 38 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 39 | 40 | private void ToggleNavMenu() 41 | { 42 | collapseNavMenu = !collapseNavMenu; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/SpawnDev.BlazorJS.WebWorkers.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | false 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | # Run workflow on every push to the master branch 4 | on: workflow_dispatch 5 | 6 | jobs: 7 | deploy-to-github-pages: 8 | permissions: 9 | contents: write 10 | # use ubuntu-latest image to run steps on 11 | runs-on: ubuntu-latest 12 | steps: 13 | # uses GitHub's checkout action to checkout code form the master branch 14 | - uses: actions/checkout@v2 15 | 16 | # sets up .NET Core SDK 17 | - name: Setup .NET Core SDK 18 | uses: actions/setup-dotnet@v3.0.3 19 | with: 20 | dotnet-version: 8.0.400-preview.0.24324.5 21 | 22 | # Install dotnet wasm buildtools workload 23 | - name: Install .NET WASM Build Tools 24 | run: dotnet workload install wasm-tools 25 | 26 | # publishes Blazor project to the publish-folder 27 | - name: Publish .NET Core Project 28 | run: dotnet publish ./SpawnDev.BlazorJS.WebWorkers.Demo/ --nologo -c:Release --output publish 29 | 30 | # changes the base-tag in index.html from '/' to '/SpawnDev.BlazorJS.WebWorkers/' to match GitHub Pages repository subdirectory 31 | - name: Change base-tag in index.html from / to /SpawnDev.BlazorJS.WebWorkers/ 32 | run: sed -i 's/>> BlazorJS Running: {JS.GlobalScope.ToString()}"); 13 | 14 | builder.Services.AddWebWorkerService(o => 15 | { 16 | o.RestoreEnvironment = false; // true by default, set to false to leave the fake window environment in workers 17 | }); 18 | 19 | builder.Services.RegisterServiceWorker(new ServiceWorkerConfig 20 | { 21 | Options = new ServiceWorkerRegistrationOptions 22 | { 23 | //Type = "module", 24 | }, 25 | }); 26 | 27 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 28 | 29 | if (JS.IsWindow) 30 | { 31 | builder.RootComponents.Add("#app"); 32 | builder.RootComponents.Add("head::after"); 33 | } 34 | 35 | var host = await builder.Build().StartBackgroundServices(); 36 | 37 | 38 | JS.Set("_test", async (bool useModule) => 39 | { 40 | var webWorkerService = host.Services.GetRequiredService(); 41 | if (useModule) 42 | { 43 | var worker = await webWorkerService.GetWebWorker(new WebWorkerOptions { WorkerOptions = new WorkerOptions { Type = "module" } }); 44 | await worker!.Run(() => Console.WriteLine("module worker")); 45 | } 46 | else 47 | { 48 | var worker = await webWorkerService.GetWebWorker(); 49 | await worker!.Run(() => Console.WriteLine("classic worker")); 50 | } 51 | }); 52 | 53 | //var arg = new SharedCancellationTokenSource(); 54 | //var token = arg.Token; 55 | 56 | //JS.Set("_token1", token); 57 | //var token12 = JS.Get("_token1"); 58 | //var keys = token12.JSRef?.Keys(); 59 | //var token1 = JS.Get("_token1"); 60 | 61 | await host.BlazorJSRunAsync(); 62 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/wwwroot/spawndev.blazorjs.webworkers.module.js: -------------------------------------------------------------------------------- 1 | const params = new Proxy(new URLSearchParams(globalThis.location.search), { get: (searchParams, prop) => searchParams.get(prop), }); 2 | if (globalThis.constructor?.name !== 'Window') globalThis.autoStart ??= params.autoStart !== '0'; 3 | 4 | // importShim is handles the pre-patched Blazor framework scripts that have had their `import` and `export` calls changed to `importShim` and `exportShim` 5 | //import * as _importShim from "./spawndev.blazorjs.webworkers.import-shim.js" 6 | // load the event holder that holds events while Blazor loads 7 | import * as _eventHolder from "./spawndev.blazorjs.webworkers.event-holder.js" 8 | // Faux-Env creates a minimal window scope like environment (if needed) for Blazor to run in a Web Worker, Shared Worker, or Service Worker. 9 | import * as _fauxEnv from "./spawndev.blazorjs.webworkers.faux-env.js" 10 | // if the document is a fake document (faux-env) init it now 11 | if (document.initDocument) document.initDocument(); 12 | // Use static imports to load Blazor pre-patched scripts 13 | // import-shim is currently pre-pended to blazor.webassembly.js when it is pre-patched. 14 | //import * as _importShim from "./spawndev.blazorjs.webworkers.import-shim.js" 15 | import * as _blazor from "./_framework/blazor.webassembly.js" 16 | import * as _dotnet from "./_framework/dotnet.js" 17 | import * as _dotnetNative from "./_framework/dotnet.native.js" 18 | import * as _dotnetRuntime from "./_framework/dotnet.runtime.js" 19 | // SpawnDev.BlazorJS Javascript library is required 20 | import * as _bjs from "./SpawnDev.BlazorJS.lib.module.js" 21 | if (globalThis.exportShimValues) globalThis.exportShimValues['SpawnDev.BlazorJS.lib.module.js'] = _bjs; 22 | 23 | var verboseWebWorkers = !!params.verbose; 24 | 25 | globalThis.consoleLog ??= function () { 26 | if (!verboseWebWorkers) return; 27 | console.log(...arguments); 28 | }; 29 | // if library modules are needed, a custom loader should be created that loads those modules and then loads this file, optionally using a custom Blazor.start() 30 | 31 | // start Blazor if autoStart hasn't been disabled 32 | if (globalThis.autoStart) { 33 | Blazor._startTask = Blazor.start(); 34 | } 35 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/buildcontent/patch.min.js: -------------------------------------------------------------------------------- 1 | "undefined"==typeof document&&(document=globalThis.document),"undefined"==typeof history&&(history=globalThis.history),globalThis.blazorConfig=globalThis.blazorConfig??{},globalThis.blazorConfig=Object.assign({documentBaseURI:globalThis.document?globalThis.document.baseURI:new URL("./",globalThis.location.href),blazorBaseURI:"",frameworkFolderName:"_framework",contentFolderName:"_content"},globalThis.blazorConfig),globalThis.blazorConfig.blazorBaseURI||(globalThis.blazorConfig.blazorBaseURI=function(){var o=new URL("./",location.href);if(o.pathname.includes(`${globalThis.blazorConfig.contentFolderName}/`)){var l=o.pathname.substring(0,o.pathname.indexOf(`${globalThis.blazorConfig.contentFolderName}/`));return new URL(l,location.href).toString()}if(o.pathname.includes(`${globalThis.blazorConfig.frameworkFolderName}/`)){l=o.pathname.substring(0,o.pathname.indexOf(`${globalThis.blazorConfig.frameworkFolderName}/`));return new URL(l,location.href).toString()}return o.toString()}()),void 0===globalThis.constructor.name&&globalThis.window&&(globalThis.constructor.name="Window"),"Window"!==globalThis.constructor.name&&globalThis.document&&(globalThis.document.baseURI=globalThis.blazorConfig.blazorBaseURI),globalThis.exportShimValues={},globalThis.exportShim=(o,l)=>(l?globalThis.exportShimValues[o]=l:globalThis.exportShimValues[o]||(globalThis.exportShimValues[o]={}),globalThis.exportShimValues[o]),globalThis.dynamicImportSupported="Window"===globalThis.constructor.name,globalThis.importShim=function(o){var l=o.split("?")[0].split("#")[0];return l=(l=-1===l.indexOf("/")?l:l.substring(l.lastIndexOf("/")+1)).substring(0,l.lastIndexOf(".js")+3),new Promise((async function(a,i){if(!1!==globalThis.dynamicImportSupported)try{var e=await import(o);globalThis.dynamicImportSupported=!0;var r=globalThis.exportShimValues[l]??{};return globalThis.exportShimValues[l]=Object.assign(r,e),void a(r=globalThis.exportShimValues[l])}catch(o){globalThis.dynamicImportSupported=!1}if(void 0!==globalThis.importScripts)return importScripts(o),void a(r=globalThis.exportShimValues[l]??{});i()}))},globalThis.importShim.meta=function(o){return{url:new URL(`${globalThis.blazorConfig.frameworkFolderName}/${o}`,globalThis.blazorConfig.blazorBaseURI).toString()}}; -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35103.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.WebWorkers", "SpawnDev.BlazorJS.WebWorkers\SpawnDev.BlazorJS.WebWorkers.csproj", "{6199BC07-1C75-408A-B485-25D4A483DE1A}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {A855E2C2-27D1-4494-A9CA-58EBF11DC34D} = {A855E2C2-27D1-4494-A9CA-58EBF11DC34D} 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.WebWorkers.Build", "SpawnDev.BlazorJS.WebWorkers.Build\SpawnDev.BlazorJS.WebWorkers.Build.csproj", "{A855E2C2-27D1-4494-A9CA-58EBF11DC34D}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.BlazorJS.WebWorkers.Demo", "SpawnDev.BlazorJS.WebWorkers.Demo\SpawnDev.BlazorJS.WebWorkers.Demo.csproj", "{BBDA1D8E-A5FF-4B17-96A0-B56D1840CBAE}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {6199BC07-1C75-408A-B485-25D4A483DE1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {6199BC07-1C75-408A-B485-25D4A483DE1A}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {6199BC07-1C75-408A-B485-25D4A483DE1A}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {6199BC07-1C75-408A-B485-25D4A483DE1A}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {A855E2C2-27D1-4494-A9CA-58EBF11DC34D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {A855E2C2-27D1-4494-A9CA-58EBF11DC34D}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {A855E2C2-27D1-4494-A9CA-58EBF11DC34D}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {A855E2C2-27D1-4494-A9CA-58EBF11DC34D}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {BBDA1D8E-A5FF-4B17-96A0-B56D1840CBAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {BBDA1D8E-A5FF-4B17-96A0-B56D1840CBAE}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {BBDA1D8E-A5FF-4B17-96A0-B56D1840CBAE}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {BBDA1D8E-A5FF-4B17-96A0-B56D1840CBAE}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {B771CA8D-AD31-4F8D-ADFC-894E8DFE588B} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Build/Tasks/ImportShimBlazorWASM.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Framework; 2 | using System; 3 | using System.IO; 4 | 5 | namespace SpawnDev.BlazorJS.WebWorkers.Build.Tasks 6 | { 7 | public class ImportShimBlazorWASM : Microsoft.Build.Utilities.Task 8 | { 9 | 10 | public string ServiceWorkerAssetsManifest { get; set; } 11 | 12 | //[Required] 13 | //public ITaskItem[] StaticWebAsset { get; set; } 14 | 15 | [Required] 16 | public string ProjectDir { get; set; } 17 | 18 | [Required] 19 | public string OutputPath { get; set; } 20 | 21 | //[Required] 22 | //public string BasePath { get; set; } 23 | 24 | [Required] 25 | public string IntermediateOutputPath { get; set; } 26 | 27 | [Required] 28 | public bool PatchFramework { get; set; } 29 | 30 | [Required] 31 | public string PackageContentDir { get; set; } 32 | 33 | [Required] 34 | public bool DebugSpawnDevWebWorkersBuildTasks { get; set; } 35 | 36 | [Required] 37 | public bool PublishMode { get; set; } 38 | 39 | public string OutputWwwroot { get; set; } 40 | 41 | public override bool Execute() 42 | { 43 | if (DebugSpawnDevWebWorkersBuildTasks) 44 | { 45 | System.Diagnostics.Debugger.Launch(); 46 | } 47 | if (!PatchFramework) 48 | { 49 | return true; 50 | } 51 | OutputWwwroot = Path.GetFullPath(Path.Combine(OutputPath, "wwwroot")); 52 | PackageContentDir = Path.GetFullPath(PackageContentDir); 53 | var blazorPatchTool = new BlazorWASMFrameworkTool(OutputWwwroot, PackageContentDir, ServiceWorkerAssetsManifest); 54 | // patch Blazor _framework files to allow running in non-window scopes 55 | // this needs to run after build and after publish 56 | blazorPatchTool.ImportPatch(); 57 | // if the app has an `service-worker-assets.js` file, some hashes may need to be updated due to file patching 58 | // as this file is not normally used during debugging, it is only checked during publish. 59 | // most patched files will already have the correct hash, but usually the Blazor build process overwrites 1 of them during publish 60 | // so it is patched and the hash is updated here 61 | if (PublishMode) 62 | { 63 | blazorPatchTool.VerifyAssetsManifest(); 64 | } 65 | return true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/ExceptionSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// A simple serializer for exceptions that can be used to pass exceptions between window, service worker, dedicated worker, and shared worker contexts.
7 | ///
8 | public static class ExceptionSerializer 9 | { 10 | /// 11 | /// Exception type cache 12 | /// 13 | static Dictionary ExceptionTypes = new Dictionary(); 14 | /// 15 | /// Serializes an exception to a string. 16 | /// 17 | /// 18 | /// 19 | public static string? Serialize(Exception? exception) 20 | { 21 | if (exception == null) return null; 22 | // most of the time, Exceptions prefix the Exception type's FullName to the ToString() return value but it is not required 23 | // if it is not already there, add it 24 | var exceptionString = exception.ToString(); 25 | var typeNamePart = $"{exception.GetType().FullName}: "; 26 | return exceptionString.StartsWith(typeNamePart) ? exceptionString : $"{typeNamePart}{exceptionString}"; 27 | } 28 | /// 29 | /// Deserializes an exception from a serialized string. 30 | /// 31 | /// 32 | /// 33 | public static Exception? Deserialize(string? serializedException) 34 | { 35 | if (string.IsNullOrEmpty(serializedException)) return null; 36 | var parts = serializedException.Split(new[] { ": " }, 2, StringSplitOptions.None); 37 | if (parts.Length < 2) return null; 38 | var typeName = parts[0]; 39 | var message = parts[1]; 40 | if (!ExceptionTypes.TryGetValue(typeName, out var exTypeCached)) 41 | { 42 | exTypeCached = Type.GetType(typeName); 43 | ExceptionTypes[typeName] = exTypeCached; 44 | } 45 | if (exTypeCached == null) 46 | { 47 | return new Exception(serializedException); 48 | } 49 | try 50 | { 51 | return (Exception)Activator.CreateInstance(exTypeCached, new object?[] { message })!; 52 | } 53 | catch { } 54 | try 55 | { 56 | return (Exception)Activator.CreateInstance(exTypeCached)!; 57 | } 58 | catch (Exception ex) 59 | { 60 | ExceptionTypes[typeName] = null; 61 | return new Exception(serializedException); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Pages/TestModuleWorker.razor: -------------------------------------------------------------------------------- 1 | @page "/TestModuleWorker" 2 | @using SpawnDev.BlazorJS.JSObjects 3 | @implements IDisposable 4 | 5 | Module Worker Test 6 | 7 |

Module Worker Test

8 |

9 | This component tests starting a web worker in module mode.
10 | Some Javascript libraries require module mode, but service workers must be either 'classic' mode and use importScripts or 'module' mode and use static imports.
11 | Window, SharedWebWorker, and DedicatedWebWorker can use dynamic imports but that is not supported in a ServiceWorker.
12 | SpawnDev.BlazorJS.WebWorkers needs to support starting in a ServiceWorker that is running in 'module' mode to enable support for ESM Javascript modules.
13 | Running in a Worker that is started in module mode is very similar to running in a ServiceWorker that is started in module mode, but is easier to test. 14 |

15 | 16 | 17 | 18 | 19 | @code { 20 | [Inject] 21 | WebWorkerService WebWorkerService { get; set; } = default!; 22 | 23 | public void Dispose() 24 | { 25 | 26 | } 27 | private async Task IncrementCount2() 28 | { 29 | var worker = await WebWorkerService.GetWebWorker(new WebWorkerOptions 30 | { 31 | // ScriptUrl = new Uri(new Uri(WebWorkerService.AppBaseUri), "app.worker.module.js").ToString(), 32 | // QueryParams = new Dictionary 33 | // { 34 | // { "autoStart", "0" }, 35 | // { "verbose", "1" }, 36 | // }, 37 | WorkerOptions = new WorkerOptions 38 | { 39 | //Type = "module" 40 | } 41 | }); 42 | var ret = await worker!.Run(() => TestMethod(null!)); 43 | Console.WriteLine($"Worker returned: {ret}"); 44 | } 45 | private async Task IncrementCount() 46 | { 47 | var worker = await WebWorkerService.GetWebWorker(new WebWorkerOptions 48 | { 49 | // ScriptUrl = new Uri(new Uri(WebWorkerService.AppBaseUri), "app.worker.module.js").ToString(), 50 | // QueryParams = new Dictionary 51 | // { 52 | // { "autoStart", "0" }, 53 | // { "verbose", "1" }, 54 | // }, 55 | WorkerOptions = new WorkerOptions 56 | { 57 | Type = "module" 58 | } 59 | }); 60 | var ret = await worker!.Run(() => TestMethod(null!)); 61 | Console.WriteLine($"Worker returned: {ret}"); 62 | } 63 | static int TestMethod([FromServices] BlazorJSRuntime JS) 64 | { 65 | JS.Log($"TestMethod: {JS.GlobalScope}"); 66 | return 42; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .bi { 15 | display: inline-block; 16 | position: relative; 17 | width: 1.25rem; 18 | height: 1.25rem; 19 | margin-right: 0.75rem; 20 | top: -1px; 21 | background-size: cover; 22 | } 23 | 24 | .bi-house-door-fill-nav-menu { 25 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 26 | } 27 | 28 | .bi-plus-square-fill-nav-menu { 29 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 30 | } 31 | 32 | .bi-list-nested-nav-menu { 33 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 34 | } 35 | 36 | .nav-item { 37 | font-size: 0.9rem; 38 | padding-bottom: 0.5rem; 39 | } 40 | 41 | .nav-item:first-of-type { 42 | padding-top: 1rem; 43 | } 44 | 45 | .nav-item:last-of-type { 46 | padding-bottom: 1rem; 47 | } 48 | 49 | .nav-item ::deep a { 50 | color: #d7d7d7; 51 | border-radius: 4px; 52 | height: 3rem; 53 | display: flex; 54 | align-items: center; 55 | line-height: 3rem; 56 | } 57 | 58 | .nav-item ::deep a.active { 59 | background-color: rgba(255,255,255,0.37); 60 | color: white; 61 | } 62 | 63 | .nav-item ::deep a:hover { 64 | background-color: rgba(255,255,255,0.1); 65 | color: white; 66 | } 67 | 68 | @media (min-width: 641px) { 69 | .navbar-toggler { 70 | display: none; 71 | } 72 | 73 | .collapse { 74 | /* Never collapse the sidebar for wide screens */ 75 | display: block; 76 | } 77 | 78 | .nav-scrollable { 79 | /* Allow sidebar to scroll for tall menus */ 80 | height: calc(100vh - 3.5rem); 81 | overflow-y: auto; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/wwwroot/spawndev.blazorjs.webworkers.interconnect.js: -------------------------------------------------------------------------------- 1 | // Runs in a shared worker as a work around to a broadcast channel limitation that prevents passing of MessagePort 2 | 3 | var messages = {}; 4 | var methods = { 5 | dropOff: function (toInstanceId, fromInstanceInfo, ports) { 6 | var args = [...arguments]; 7 | console.log('dropOff()', args); 8 | var instanceMessages = messages[toInstanceId]; 9 | if (!instanceMessages) { 10 | instanceMessages = []; 11 | messages[toInstanceId] = instanceMessages; 12 | } 13 | // notify the instance there is a message waiting 14 | var toInstanceChannel = new BroadcastChannel(toInstanceId); 15 | toInstanceChannel.postMessage(['interconnect']); 16 | instanceMessages.push({ 17 | data: { 18 | fromInstanceInfo: fromInstanceInfo, 19 | time: new Date().getTime(), 20 | }, 21 | ports: ports, 22 | }); 23 | }, 24 | pickUp: function (forInstanceId) { 25 | var args = [...arguments]; 26 | console.log('pickUp()', args); 27 | var instanceMessages = messages[forInstanceId] ?? []; 28 | delete messages[forInstanceId]; 29 | return instanceMessages; 30 | } 31 | }; 32 | var instances = {}; 33 | self.addEventListener('connect', function (e) { 34 | console.log('*** connect', e); 35 | const port = e.ports[0]; 36 | port.addEventListener('message', function (e) { 37 | console.log('*** message', e); 38 | var msg = e.data; 39 | var ports = e.ports; 40 | if (!msg) return; 41 | var cmd = msg.shift(); 42 | switch (cmd) { 43 | case "init": 44 | var instanceInfo = msg.shift(); 45 | instances[instanceInfo.instanceId] = { 46 | instanceInfo: instanceInfo, 47 | port: port, 48 | }; 49 | case "dropOff": 50 | var toInstanceId = msg.shift(); 51 | var fromInstanceInfo = msg.shift(); 52 | methods.dropOff(toInstanceId, fromInstanceInfo, ports); 53 | console.log('recvd: dropOff', toInstanceId, fromInstanceInfo, ports); 54 | break; 55 | case "pickUp": 56 | var forInstanceId = msg.shift(); 57 | var messages = methods.pickUp(forInstanceId); 58 | console.log('recvd: pickUp', forInstanceId, messages); 59 | if (messages && messages.length) { 60 | for (var i = 0; i < messages.length; i++) { 61 | var message = messages[i]; 62 | console.log('Sending stored message to target:', message.data, message.ports); 63 | port.postMessage(message.data, message.ports); 64 | } 65 | } 66 | break; 67 | } 68 | }); 69 | port.addEventListener('close', function (e) { 70 | console.log('*** close', e); 71 | 72 | }); 73 | port.start(); 74 | }); 75 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/WorkerCancellationTokenSource.cs: -------------------------------------------------------------------------------- 1 | //using SpawnDev.BlazorJS.JSObjects; 2 | //using System.Diagnostics.CodeAnalysis; 3 | 4 | //namespace SpawnDev.BlazorJS.WebWorkers 5 | //{ 6 | // internal abstract class WorkerCancellationTokenSource : IDisposable 7 | // { 8 | // private bool _cancelled = false; 9 | // Lazy _token; 10 | // public WorkerCancellationToken Token => OnTokenGet(); 11 | // internal WorkerCancellationTokenSource() 12 | // { 13 | // _token = new Lazy(OnTokenGet); 14 | // } 15 | 16 | // protected abstract void OnCancel(); 17 | 18 | // protected abstract bool OnCancelCheck(); 19 | 20 | // protected abstract WorkerCancellationToken OnTokenGet(); 21 | 22 | // protected abstract void OnRelease(); 23 | // /// 24 | // /// Sets the cancelled flag to true 25 | // /// 26 | // public void Cancel() 27 | // { 28 | // ThrowIfDisposed(); 29 | // if (IsCancellationRequested) return; 30 | // OnCancel(); 31 | // } 32 | // /// 33 | // /// Cancels the token after a set amount of time 34 | // /// 35 | // /// 36 | // public void CancelAfter(int millisecondDelay) 37 | // { 38 | // ThrowIfDisposed(); 39 | // if (IsCancellationRequested) return; 40 | // var cts = new CancellationTokenSource(millisecondDelay); 41 | // cts.Token.Register(() => 42 | // { 43 | // cts.Dispose(); 44 | // if (IsCancellationRequested) return; 45 | // OnCancel(); 46 | // }); 47 | // } 48 | // /// Throws an exception if the source has been disposed. 49 | // private void ThrowIfDisposed() 50 | // { 51 | // if (IsDisposed) throw new ObjectDisposedException(nameof(SharedCancellationTokenSource)); 52 | // } 53 | // // Throws an OCE; separated out to enable better inlining of ThrowIfCancellationRequested 54 | // [DoesNotReturn] 55 | // private void ThrowOperationCanceledException() => throw new OperationCanceledException(); 56 | // /// 57 | // /// Returns true if the cancelled flag is set to true 58 | // /// 59 | // public bool IsCancellationRequested 60 | // { 61 | // get 62 | // { 63 | // if (_cancelled) return true; 64 | // _cancelled = OnCancelCheck(); 65 | // return _cancelled; 66 | // } 67 | // } 68 | // /// 69 | // /// Returns true if this instance has been disposed 70 | // /// 71 | // public bool IsDisposed { get; private set; } = false; 72 | // /// 73 | // /// Releases disposable resources 74 | // /// 75 | // public void Dispose() 76 | // { 77 | // if (IsDisposed) return; 78 | // IsDisposed = true; 79 | // OnRelease(); 80 | // } 81 | // } 82 | //} 83 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Services/AppServiceWorker.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers.Demo.Services 4 | { 5 | public class AppServiceWorker : ServiceWorkerEventHandler 6 | { 7 | public AppServiceWorker(BlazorJSRuntime js) : base(js) 8 | { 9 | 10 | } 11 | // called before any ServiceWorker events are handled 12 | protected override async Task OnInitializedAsync() 13 | { 14 | // This service may start in any scope. This will be called before the app runs. 15 | // If JS.IsWindow == true be careful not stall here. 16 | // you can do initialization based on the scope that is running 17 | Log("GlobalThisTypeName", JS.GlobalThisTypeName); 18 | } 19 | 20 | protected override async Task ServiceWorker_OnInstallAsync(ExtendableEvent e) 21 | { 22 | Log($"ServiceWorker_OnInstallAsync", JS.GlobalThisTypeName); 23 | _ = ServiceWorkerThis!.SkipWaiting(); // returned task can be ignored 24 | } 25 | 26 | protected override async Task ServiceWorker_OnActivateAsync(ExtendableEvent e) 27 | { 28 | Log($"ServiceWorker_OnActivateAsync"); 29 | await ServiceWorkerThis!.Clients.Claim(); 30 | } 31 | 32 | protected override async Task ServiceWorker_OnFetchAsync(FetchEvent e) 33 | { 34 | Log($"ServiceWorker_OnFetchAsync", e.Request.Method, e.Request.Url); 35 | Response ret; 36 | try 37 | { 38 | ret = await JS.Fetch(e.Request); 39 | } 40 | catch (Exception ex) 41 | { 42 | ret = new Response(ex.Message, new ResponseOptions { Status = 500, StatusText = ex.Message, Headers = new Dictionary { { "Content-Type", "text/plain" } } }); 43 | Log($"ServiceWorker_OnFetchAsync failed: {ex.Message}"); 44 | } 45 | return ret; 46 | } 47 | 48 | protected override async Task ServiceWorker_OnMessageAsync(ExtendableMessageEvent e) 49 | { 50 | Log($"ServiceWorker_OnMessageAsync"); 51 | } 52 | 53 | protected override async Task ServiceWorker_OnPushAsync(PushEvent e) 54 | { 55 | Log($"ServiceWorker_OnPushAsync"); 56 | } 57 | 58 | protected override async Task ServiceWorker_OnPushSubscriptionChangeAsync(PushSubscriptionChangeEvent e) 59 | { 60 | Log($"ServiceWorker_OnPushSubscriptionChangeAsync"); 61 | } 62 | 63 | protected override async Task ServiceWorker_OnSyncAsync(SyncEvent e) 64 | { 65 | Log($"ServiceWorker_OnSyncAsync"); 66 | } 67 | 68 | protected override async Task ServiceWorker_OnNotificationCloseAsync(NotificationEvent e) 69 | { 70 | Log($"ServiceWorker_OnNotificationCloseAsync"); 71 | } 72 | 73 | protected override async Task ServiceWorker_OnNotificationClickAsync(NotificationEvent e) 74 | { 75 | Log($"ServiceWorker_OnNotificationClickAsync"); 76 | } 77 | void Log(params object[] msg) => JS.Log(msg); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | @implements IDisposable 3 | 4 | Counter 5 | 6 |

Window Shared Counter

7 |

8 | Open this page in another window. You will see that the new window will have the same counter value as this window. 9 | Clicking the button on either window will increment the counter on all windows. 10 |

11 |

12 | This demonstrates calling a static method in another window inside a component.
13 |

14 | 15 |

Current count: @currentCount

16 | 17 | 18 | 19 | 20 | 21 | @code { 22 | [Inject] 23 | WebWorkerService WebWorkerService { get; set; } = default!; 24 | 25 | private int currentCount = 0; 26 | // holds the running instance of this component, if one 27 | static Counter? instance = null; 28 | 29 | async Task OpenNewWindow() 30 | { 31 | var window = await WebWorkerService.OpenWindow(); 32 | var nmt = true; 33 | } 34 | 35 | // this static method can be called by other running instances 36 | static void SetInstanceCount(int count) 37 | { 38 | if (instance == null) return; 39 | instance.currentCount = count; 40 | instance.StateHasChanged(); 41 | Console.WriteLine("Count set by another window"); 42 | } 43 | // this static method can be called by other running instances 44 | static int? GetInstanceCount() 45 | { 46 | return instance?.currentCount; 47 | } 48 | protected override async Task OnInitializedAsync() 49 | { 50 | // set the current count to the largest value in any other running window 51 | var windows = WebWorkerService.Windows.Where(o => o.Info.InstanceId != WebWorkerService.InstanceId).ToList(); 52 | Console.WriteLine($"init Windows found: {windows.Count}"); 53 | var values = new List(); 54 | var tasks = windows.Select(async o => 55 | { 56 | try 57 | { 58 | var instanceCurrentCount = await o.Run(() => GetInstanceCount()).WaitAsync(TimeSpan.FromSeconds(2)); 59 | if (instanceCurrentCount != null) values.Add(instanceCurrentCount.Value); 60 | } 61 | catch { } 62 | }).ToList(); 63 | await Task.WhenAll(tasks); 64 | Console.WriteLine($"Values found: {values.Count}"); 65 | currentCount = values.Any() ? values.Max() : 0; 66 | Console.WriteLine($"Max value found: {currentCount}"); 67 | instance = this; 68 | StateHasChanged(); 69 | } 70 | public void Dispose() 71 | { 72 | // unset this instance on the static var 73 | instance = null; 74 | } 75 | private async Task IncrementCount() 76 | { 77 | // increment for this window 78 | currentCount++; 79 | // set the value on all other windows 80 | var windows = WebWorkerService.Windows.Where(o => o.Info.InstanceId != WebWorkerService.InstanceId).ToList(); 81 | Console.WriteLine($"increment Windows found: {windows.Count}"); 82 | var tasks = windows.Select(o => o.Run(() => SetInstanceCount(currentCount))); 83 | try 84 | { 85 | await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(3)); 86 | } 87 | catch { } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/AppInstanceInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | ///// 6 | ///// Configuration data with information a new instance will use on startup 7 | ///// 8 | //public class InstanceConfiguration 9 | //{ 10 | // /// 11 | // /// The configuration id of this instance 12 | // /// 13 | // public string ConfigId { get; set; } 14 | // /// 15 | // /// The InstanceId of the instance that created this config 16 | // /// 17 | // public string OwnerId { get; set; } 18 | // /// 19 | // /// When this config was created 20 | // /// 21 | // public DateTimeOffset Created { get; set; } 22 | // /// 23 | // /// The path to load 24 | // /// 25 | // public string? Url { get; set; } 26 | //} 27 | /// 28 | /// Information about an instance of a Blazor app 29 | /// 30 | public class AppInstanceInfo 31 | { 32 | /// 33 | /// The instance's instanceId, a unique and randomly generated Guid string created during BlazorJSRuntime startup 34 | /// 35 | public string InstanceId { get; set; } 36 | /// 37 | /// The InstanceId of the instance that created this instance (if one) 38 | /// 39 | public string? OwnerId { get; set; } 40 | /// 41 | /// The id set by the owner (OwnerId) when they created this instance 42 | /// 43 | public string? ChildId { get; set; } 44 | /// 45 | /// The instance's location at startup 46 | /// 47 | public string Url { get; set; } 48 | /// 49 | /// The instance's app baseUri 50 | /// 51 | public string BaseUrl { get; set; } 52 | /// 53 | /// The scope the instance is running in 54 | /// 55 | public GlobalScope Scope { get; set; } 56 | /// 57 | /// The name property of the global scope. In shared workers, this is the shared worker name used when there were created. 58 | /// 59 | /// 60 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 61 | public string? Name { get; set; } 62 | /// 63 | /// If the scope is a dedicated worker, this is the parent's instance id
64 | /// If this is a window scope and running in an iframe, this is the parent window's instanceId 65 | ///
66 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 67 | public string? ParentInstanceId { get; set; } 68 | /// 69 | /// The instance's clientId. null if a undetermined. 70 | /// 71 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 72 | public string? ClientId { get; set; } 73 | /// 74 | /// The name of the instance's running indicator lock (if one) 75 | /// 76 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 77 | public string? LockName { get; set; } 78 | /// 79 | /// Returns true if this instance is a TaaskPool worker 80 | /// 81 | public bool TaskPoolWorker { get; set; } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/wwwroot/spawndev.blazorjs.webworkers.event-holder.js: -------------------------------------------------------------------------------- 1 | // Holds events that happen while Blazor WASM is loading 2 | 3 | (function () { 4 | var globalThisTypeName = globalThis.constructor?.name; 5 | if (globalThisTypeName == 'SharedWorkerGlobalScope') { 6 | // important for SharedWorker 7 | // catch any incoming connections that happen while .Net is loading 8 | let _missedConnections = []; 9 | globalThis.takeOverOnConnectEvent = function (newConnectFunction) { 10 | var tmp = _missedConnections; 11 | _missedConnections = []; 12 | globalThis.onconnect = newConnectFunction; 13 | return tmp; 14 | } 15 | globalThis.onconnect = function (e) { 16 | _missedConnections.push(e.ports[0]); 17 | }; 18 | } else if (globalThisTypeName == 'ServiceWorkerGlobalScope') { 19 | // Starting Blazor requires using importScripts inside async functions 20 | // e.waitUntil is used during the install event to allow importScripts inside async functions 21 | // it is resolved after loading is complete 22 | let holdEvents = true; 23 | let missedServiceWorkerEvents = []; 24 | function handleMissedEvent(e) { 25 | if (!holdEvents) return; 26 | consoleLog('ServiceWorker missed event:', e.type, e); 27 | if (e.respondWith) { 28 | // fetch and canmakepayment ExtendableEvents use respondWith 29 | var responsePromise = new Promise(function (resolve, reject) { 30 | e.responseResolve = resolve; 31 | e.responseReject = reject; 32 | }); 33 | e.respondWith(responsePromise); 34 | } else if (e.waitUntil) { 35 | // all other ExtendableEvents use waitUntil 36 | var waitUntilPromise = new Promise(function (resolve, reject) { 37 | e.waitResolve = resolve; 38 | e.waitReject = reject; 39 | }); 40 | e.waitUntil(waitUntilPromise); 41 | } 42 | missedServiceWorkerEvents.push(e); 43 | } 44 | globalThis.addEventListener('activate', handleMissedEvent); 45 | globalThis.addEventListener('backgroundfetchabort', handleMissedEvent); 46 | globalThis.addEventListener('backgroundfetchclick', handleMissedEvent); 47 | globalThis.addEventListener('backgroundfetchfail', handleMissedEvent); 48 | globalThis.addEventListener('backgroundfetchsuccess', handleMissedEvent); 49 | globalThis.addEventListener('canmakepayment', handleMissedEvent); 50 | globalThis.addEventListener('contentdelete', handleMissedEvent); 51 | globalThis.addEventListener('cookiechange', handleMissedEvent); 52 | globalThis.addEventListener('fetch', handleMissedEvent); 53 | globalThis.addEventListener('install', handleMissedEvent); 54 | globalThis.addEventListener('message', handleMissedEvent); 55 | globalThis.addEventListener('messageerror', handleMissedEvent); 56 | globalThis.addEventListener('notificationclick', handleMissedEvent); 57 | globalThis.addEventListener('notificationclose', handleMissedEvent); 58 | globalThis.addEventListener('paymentrequest', handleMissedEvent); 59 | globalThis.addEventListener('periodicsync', handleMissedEvent); 60 | globalThis.addEventListener('push', handleMissedEvent); 61 | globalThis.addEventListener('pushsubscriptionchange', handleMissedEvent); 62 | globalThis.addEventListener('sync', handleMissedEvent); 63 | // This method will be called by Blazor WASM when it starts up to collect missed events and handle them 64 | globalThis.GetMissedServiceWorkerEvents = function () { 65 | holdEvents = false; 66 | var ret = missedServiceWorkerEvents; 67 | missedServiceWorkerEvents = []; 68 | return ret; 69 | }; 70 | } 71 | })() -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SharedCancellationToken.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// SharedCancellationToken is a class that uses a SharedArrayBuffer to allow checking if a task has been cancelled by another browser thread
8 | /// synchronously and without relying on message event handling.
9 | ///
10 | [JsonConverter(typeof(JsonConverters.HybridObjectConverterFactory))] 11 | public class SharedCancellationToken : IDisposable 12 | { 13 | // JsonInclude on non-public properties is supported by HybridObjectConverter 14 | [JsonInclude] 15 | [JsonPropertyName("cancelled")] 16 | private bool _cancelled { get; set; } = false; 17 | 18 | // JsonInclude on non-public properties is supported by HybridObjectConverter 19 | [JsonInclude] 20 | [JsonPropertyName("source")] 21 | private SharedCancellationTokenSource? _source { get; set; } = null; 22 | internal SharedCancellationToken(SharedCancellationTokenSource source) 23 | { 24 | _source = source; 25 | } 26 | // json constructor (HybridObjectConverter will use this because it is marked JsonConstructorAttribute) 27 | [JsonConstructor] 28 | private SharedCancellationToken() { } 29 | /// 30 | /// Creates an instance of SharedCancellationToken and setting the cancelled state that cannot be cancelled in the future 31 | /// 32 | public SharedCancellationToken(bool cancelled = false) 33 | { 34 | _cancelled = cancelled; 35 | } 36 | /// 37 | /// Returns an instance of SharedCancellationToken that is not cancelled and will never be in the cancelled state 38 | /// 39 | public static SharedCancellationToken None => new SharedCancellationToken(false); 40 | /// 41 | /// Returns an instance of SharedCancellationToken that is cancelled. 42 | /// 43 | public static SharedCancellationToken Cancelled => new SharedCancellationToken(true); 44 | /// 45 | /// Throws an OperationCanceledException if the cancelled flag is set to true 46 | /// 47 | /// 48 | public void ThrowIfCancellationRequested() 49 | { 50 | if (IsCancellationRequested) ThrowOperationCanceledException(); 51 | } 52 | // Throws an OCE; separated out to enable better inlining of ThrowIfCancellationRequested 53 | [DoesNotReturn] 54 | private void ThrowOperationCanceledException() => throw new OperationCanceledException(); 55 | /// 56 | /// Returns true if the cancelled flag is set to true 57 | /// 58 | [JsonIgnore] 59 | public bool IsCancellationRequested 60 | { 61 | get 62 | { 63 | if (_cancelled) return true; 64 | if (_source != null) 65 | { 66 | // update local _cancelled flag from _source 67 | _cancelled = _source.IsCancellationRequested; 68 | } 69 | return _cancelled; 70 | } 71 | } 72 | /// 73 | /// Returns true of this SharedCancellationToken can be cancelled 74 | /// 75 | public bool CanBeCanceled => _source != null; 76 | /// 77 | /// Returns true if this instance has been disposed 78 | /// 79 | public bool IsDisposed { get; private set; } = false; 80 | /// 81 | /// Releases disposable resources 82 | /// 83 | public void Dispose() 84 | { 85 | if (IsDisposed) return; 86 | IsDisposed = true; 87 | if (_source != null) 88 | { 89 | _source.Dispose(); 90 | _source = null; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers.Demo/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h1:focus { 6 | outline: none; 7 | } 8 | 9 | a, .btn-link { 10 | color: #0071c1; 11 | } 12 | 13 | .btn-primary { 14 | color: #fff; 15 | background-color: #1b6ec2; 16 | border-color: #1861ac; 17 | } 18 | 19 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 20 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 21 | } 22 | 23 | .content { 24 | padding-top: 1.1rem; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid red; 33 | } 34 | 35 | .validation-message { 36 | color: red; 37 | } 38 | 39 | #blazor-error-ui { 40 | background: lightyellow; 41 | bottom: 0; 42 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 43 | display: none; 44 | left: 0; 45 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 46 | position: fixed; 47 | width: 100%; 48 | z-index: 1000; 49 | } 50 | 51 | #blazor-error-ui .dismiss { 52 | cursor: pointer; 53 | position: absolute; 54 | right: 0.75rem; 55 | top: 0.5rem; 56 | } 57 | 58 | .blazor-error-boundary { 59 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 60 | padding: 1rem 1rem 1rem 3.7rem; 61 | color: white; 62 | } 63 | 64 | .blazor-error-boundary::after { 65 | content: "An error has occurred." 66 | } 67 | 68 | .loading-progress { 69 | position: relative; 70 | display: block; 71 | width: 8rem; 72 | height: 8rem; 73 | margin: 20vh auto 1rem auto; 74 | } 75 | 76 | .loading-progress circle { 77 | fill: none; 78 | stroke: #e0e0e0; 79 | stroke-width: 0.6rem; 80 | transform-origin: 50% 50%; 81 | transform: rotate(-90deg); 82 | } 83 | 84 | .loading-progress circle:last-child { 85 | stroke: #1b6ec2; 86 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; 87 | transition: stroke-dasharray 0.05s ease-in-out; 88 | } 89 | 90 | .loading-progress-text { 91 | position: absolute; 92 | text-align: center; 93 | font-weight: bold; 94 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem; 95 | } 96 | 97 | .loading-progress-text:after { 98 | content: var(--blazor-load-percentage-text, "Loading"); 99 | } 100 | 101 | code { 102 | color: #c02d76; 103 | } 104 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/WorkerCancellationToken.cs: -------------------------------------------------------------------------------- 1 | //using System.Diagnostics.CodeAnalysis; 2 | //using System.Text.Json.Serialization; 3 | 4 | //namespace SpawnDev.BlazorJS.WebWorkers 5 | //{ 6 | // public class WorkerCancellationToken 7 | // { 8 | // [JsonInclude] 9 | // [JsonPropertyName("cancelled")] 10 | // private bool _cancelled = false; 11 | // [JsonInclude] 12 | // [JsonPropertyName("cancellationId")] 13 | // internal int CancellationId { get; private set; } 14 | // internal Func? _cancelledCheck { get; set; } 15 | // internal Action? _releaseCallback { get; set; } 16 | // internal string SourceInstanceId { get; set; } 17 | // internal WorkerCancellationToken(Func cancelledCheck, Action releaseCallback, int cancellationId) 18 | // { 19 | // _cancelledCheck = cancelledCheck; 20 | // _releaseCallback = releaseCallback; 21 | // CancellationId = cancellationId; 22 | // } 23 | // /// 24 | // /// Creates a new instance and sets the cancelled state 25 | // /// 26 | // /// 27 | // public WorkerCancellationToken(bool cancelled) 28 | // { 29 | // _cancelled = cancelled; 30 | // } 31 | // /// 32 | // /// Creates a new instance 33 | // /// 34 | // public WorkerCancellationToken() { } 35 | // /// 36 | // /// Returns an instance of SharedCancellationToken that is not cancelled and will never be in the cancelled state 37 | // /// 38 | // public static WorkerCancellationToken None => new WorkerCancellationToken(false); 39 | // /// 40 | // /// Returns an instance of SharedCancellationToken that is cancelled. 41 | // /// 42 | // public static WorkerCancellationToken Cancelled => new WorkerCancellationToken(true); 43 | // /// 44 | // /// Throws an OperationCanceledException if the cancelled flag is set to true 45 | // /// 46 | // /// 47 | // public void ThrowIfCancellationRequested() 48 | // { 49 | // if (IsCancellationRequested) ThrowOperationCanceledException(); 50 | // } 51 | // // Throws an OCE; separated out to enable better inlining of ThrowIfCancellationRequested 52 | // [DoesNotReturn] 53 | // private void ThrowOperationCanceledException() => throw new OperationCanceledException(); 54 | // /// 55 | // /// Returns true if the cancelled flag is set to true 56 | // /// 57 | // [JsonIgnore] 58 | // public bool IsCancellationRequested 59 | // { 60 | // get 61 | // { 62 | // if (_cancelled) return true; 63 | // if (_cancelledCheck != null) 64 | // { 65 | // // update local _cancelled flag from _source 66 | // // the below call may 67 | // _cancelled = _cancelledCheck(this); 68 | // } 69 | // return _cancelled; 70 | // } 71 | // } 72 | // /// 73 | // /// Returns true of this SharedCancellationToken can be cancelled 74 | // /// 75 | // [JsonIgnore] 76 | // public bool CanBeCanceled => _cancelledCheck != null; 77 | // /// 78 | // /// Returns true if this instance has been disposed 79 | // /// 80 | // [JsonIgnore] 81 | // public bool IsDisposed { get; private set; } = false; 82 | // /// 83 | // /// Releases disposable resources 84 | // /// 85 | // public void Dispose() 86 | // { 87 | // if (IsDisposed) return; 88 | // IsDisposed = true; 89 | // if (_releaseCallback != null) 90 | // { 91 | // _releaseCallback(this); 92 | // } 93 | // } 94 | // } 95 | //} 96 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/IServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// Adds SpawnDev.BlazorJS.WebWorkers specific methods to IServiceCollection 7 | /// 8 | public static class IServiceCollectionExtensions 9 | { 10 | static internal Type? ServiceWorkerEventHandlerTService { get; set; } = null; 11 | static internal Type? ServiceWorkerEventHandlerTImplementation { get; set; } = null; 12 | static internal ServiceWorkerConfig? ServiceWorkerConfig { get; set; } = null; 13 | /// 14 | /// Adds WebWorkerService as a singleton service 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static IServiceCollection AddWebWorkerService(this IServiceCollection _this, Action? configureCallback = null) 20 | { 21 | return _this.AddSingleton(sp => 22 | { 23 | // create the service and pass it to the configureCallback to allow configuration at startup 24 | var webWorkerService = ActivatorUtilities.CreateInstance(sp); 25 | configureCallback?.Invoke(webWorkerService); 26 | return webWorkerService; 27 | }); 28 | } 29 | 30 | /// 31 | /// RegisterServiceWorker a class that implements ServiceWorkerEventHandler to handle ServiceWorker events 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | public static IServiceCollection RegisterServiceWorker(this IServiceCollection _this, ServiceWorkerConfig? config = null) where TService : ServiceWorkerEventHandler 38 | { 39 | ServiceWorkerConfig = config ?? new ServiceWorkerConfig { Register = ServiceWorkerStartupRegistration.Register }; 40 | var typeTService = typeof(TService); 41 | ServiceWorkerEventHandlerTService = typeTService; 42 | ServiceWorkerEventHandlerTImplementation = typeTService; 43 | _this.AddSingleton(GlobalScope.ServiceWorker); 44 | _this.AddSingleton(sp => sp.GetRequiredService(), GlobalScope.ServiceWorker); 45 | return _this; 46 | } 47 | /// 48 | /// RegisterServiceWorker a class that implements ServiceWorkerEventHandler to handle ServiceWorker events 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | public static IServiceCollection RegisterServiceWorker(this IServiceCollection _this, GlobalScope startScope, ServiceWorkerConfig? config = null) where TService : ServiceWorkerEventHandler 56 | { 57 | ServiceWorkerConfig = config ?? new ServiceWorkerConfig { Register = ServiceWorkerStartupRegistration.Register }; 58 | var typeTService = typeof(TService); 59 | ServiceWorkerEventHandlerTService = typeTService; 60 | ServiceWorkerEventHandlerTImplementation = typeTService; 61 | _this.AddSingleton(GlobalScope.ServiceWorker | startScope); 62 | _this.AddSingleton(sp => sp.GetRequiredService(), GlobalScope.ServiceWorker); 63 | return _this; 64 | } 65 | /// 66 | /// If a ServiceWorker is no longer desired, it can be set to unregister. 67 | /// 68 | /// 69 | /// 70 | public static IServiceCollection UnregisterServiceWorker(this IServiceCollection _this) 71 | { 72 | ServiceWorkerConfig = new ServiceWorkerConfig { Register = ServiceWorkerStartupRegistration.Unregister }; 73 | return _this; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/CancellationTokenExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | public static class CancellationTokenExtensions 6 | { 7 | static Stopwatch? TokenTaskDelayStopSwatch { get; set; } 8 | /// 9 | /// Releases the thread to other async Tasks via a Task.Delay() call and then returns CancellationToken.IsCancellationRequested
10 | /// This can allow event handlers time to process events that may cancel the token 11 | ///
12 | /// 13 | /// 14 | /// 15 | public static async Task IsCancellationRequestedAsync(this CancellationToken _this, int millisecondsDelay = 1) 16 | { 17 | await Task.Delay(millisecondsDelay); 18 | return _this.IsCancellationRequested; 19 | } 20 | /// 21 | /// Releases the thread to other async Tasks via a Task.Delay() call and then calls CancellationToken.ThrowIfCancellationRequested()
22 | /// This can allow event handlers time to process events that may cancel the token 23 | ///
24 | /// 25 | /// 26 | /// 27 | public static async Task ThrowIfCancellationRequestedAsync(this CancellationToken _this, int millisecondsDelay = 1) 28 | { 29 | await Task.Delay(millisecondsDelay); 30 | _this.ThrowIfCancellationRequested(); 31 | } 32 | /// 33 | /// Releases the thread to other async Tasks via a Task.Delay() call and then returns CancellationToken.IsCancellationRequested
34 | /// This can allow event handlers time to process events that may cancel the token 35 | ///
36 | /// 37 | /// Minimum time that needs to have passed before calling Task.Delay again 38 | /// 39 | /// Value passed to Task.Delay(), which allows other async Tasks time to run.
40 | /// Too small a value will not allow time for waiting Tasks and events a chance to start. ~1 millisecond is recommended.
41 | /// 42 | /// 43 | public static async Task IsCancellationRequestedAsync(this CancellationToken _this, double minIntervalMilliseconds, int millisecondsDelay = 1) 44 | { 45 | if (TokenTaskDelayStopSwatch == null) 46 | { 47 | TokenTaskDelayStopSwatch = Stopwatch.StartNew(); 48 | await Task.Delay(millisecondsDelay); 49 | } 50 | else if (TokenTaskDelayStopSwatch.Elapsed.TotalMilliseconds > minIntervalMilliseconds) 51 | { 52 | TokenTaskDelayStopSwatch.Restart(); 53 | await Task.Delay(millisecondsDelay); 54 | } 55 | return _this.IsCancellationRequested; 56 | } 57 | /// 58 | /// Releases the thread to other async Tasks via a Task.Delay() call and then calls CancellationToken.ThrowIfCancellationRequested()
59 | /// This can allow event handlers time to process events that may cancel the token 60 | ///
61 | /// 62 | /// Minimum time that needs to have passed before calling Task.Delay again 63 | /// 64 | /// Value passed to Task.Delay(), which allows other async Tasks time to run.
65 | /// Too small a value will not allow time for waiting Tasks and events a chance to start. ~1 millisecond is recommended.
66 | /// 67 | /// 68 | public static async Task ThrowIfCancellationRequestedAsync(this CancellationToken _this, double minIntervalMilliseconds, int millisecondsDelay = 1) 69 | { 70 | if (TokenTaskDelayStopSwatch == null) 71 | { 72 | TokenTaskDelayStopSwatch = Stopwatch.StartNew(); 73 | await Task.Delay(millisecondsDelay); 74 | } 75 | else if (TokenTaskDelayStopSwatch.Elapsed.TotalMilliseconds > minIntervalMilliseconds) 76 | { 77 | TokenTaskDelayStopSwatch.Restart(); 78 | await Task.Delay(millisecondsDelay); 79 | } 80 | _this.ThrowIfCancellationRequested(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/build/SpawnDev.BlazorJS.WebWorkers.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <_WebWorkerBuildContentDirectory>$(MSBuildThisFileDirectory)..\buildcontent 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <_WebWorkerBuild_Project_Assets_Directory>$(ProjectDir)$(WebWorkerBuildAssetsPath) 21 | <_WebWorkerBuild_Project_Assets_ManifestJson_FilePath>$(_WebWorkerBuild_Project_Assets_Directory)\manifest.json 22 | <_WebWorkerBuild_Project_BuildOutput_Assets_Directory>$(TargetDir)$(WebWorkerBuildAssetsPath) 23 | <_WebWorkerBuild_Project_BuildOutput_WebWorkerBuild_Directory>$(TargetDir)$(WebWorkerBuildOutputPath) 24 | <_WebWorkerBuild_Project_BuildOutput_WebWorkerBuild_OriginalFramework_Directory>$(_WebWorkerBuild_Project_BuildOutput_WebWorkerBuild_Directory)\_framework 25 | 31 | <_WebWorkerBuild_Project_BuildOutput_StaticWebAssets_Manifest_FilePath>$(TargetDir)$(ProjectName).staticwebassets.runtime.json 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SpawnDev.BlazorJS.WebWorkers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0;net9.0;net10.0 5 | enable 6 | enable 7 | 2.26.0 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.BlazorJS.WebWorkers 13 | LostBeard 14 | Call Services and static methods in separate threads with WebWorkers and SharedWebWorkers. Run Blazor WASM in the ServiceWorker. 15 | https://github.com/LostBeard/SpawnDev.BlazorJS.WebWorkers 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/SpawnDev.BlazorJS.WebWorkers.git 20 | git 21 | Blazor;BlazorWebAssembly;MultiThreading;ServiceWorker;WebWorker 22 | true 23 | true 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | true 45 | build 46 | 47 | 48 | true 49 | buildcontent 50 | 51 | 52 | true 53 | tasks 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | / 67 | 68 | 69 | 70 | 74 | false 75 | false 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/InterfaceCallDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SpawnDev.BlazorJS.WebWorkers 4 | { 5 | /// 6 | /// This Proxy presents itself as the interface it is created with but calls are converted to MethodInfos with arguments and passed onto the ICallDispatcher given at creation 7 | /// 8 | /// 9 | public class InterfaceCallDispatcher : DispatchProxy where TServiceInterface : class 10 | { 11 | private Func? Resolver { get; set; } 12 | private Func>? AsyncResolver { get; set; } 13 | private Func? KeyedResolver { get; set; } 14 | private Func>? AsyncKeyedResolver { get; set; } 15 | /// 16 | /// Service Key 17 | /// 18 | public object? Key { get; private set; } 19 | /// 20 | /// True if the the service is a keyed service 21 | /// 22 | public bool Keyed { get; private set; } 23 | /// 24 | /// The service Type 25 | /// 26 | public Type ServiceType { get; private set; } = default!; 27 | private Task CallAsync(MethodInfo methodInfo, object?[]? args) 28 | { 29 | if (AsyncKeyedResolver != null) return AsyncKeyedResolver(ServiceType, Key!, methodInfo, args); 30 | if (AsyncResolver != null) return AsyncResolver(ServiceType, methodInfo, args); 31 | throw new NullReferenceException("InterfaceCallDispatcher: No asynchronous call resolver set"); 32 | } 33 | private object? Call(MethodInfo methodInfo, object?[]? args) 34 | { 35 | if (KeyedResolver != null) return KeyedResolver(ServiceType, Key, methodInfo, args); 36 | if (Resolver != null) return Resolver(ServiceType, methodInfo, args); 37 | throw new NullReferenceException("InterfaceCallDispatcher: No synchronous call resolver set"); 38 | } 39 | /// 40 | /// Handles all requests on interface TServiceInterface 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) 47 | { 48 | if (targetMethod == null) return null; 49 | var returnType = targetMethod.ReturnType; 50 | var isTask = returnType.IsTask(); 51 | var isValueTask = !isTask && returnType.IsValueTask(); 52 | Type finalReturnType = isTask || isValueTask ? returnType.GetGenericArguments().FirstOrDefault() ?? typeof(void) : returnType; 53 | if (isTask) return CallAsync(targetMethod, args).RecastTask(finalReturnType); 54 | if (isValueTask) return CallAsync(targetMethod, args).RecastValueTask(finalReturnType); 55 | return Call(targetMethod, args); 56 | } 57 | /// 58 | /// Creates a new AsyncInterfaceCallDispatcher returned as type TServiceInterface 59 | /// 60 | /// 61 | /// 62 | /// 63 | /// 64 | public static TServiceInterface CreateInterfaceDispatcher(object key, Func> keyedResolver, Func>? asyncKeyedResolver = null) 65 | { 66 | var ret = Create>(); 67 | var proxy = (ret as InterfaceCallDispatcher)!; 68 | proxy.KeyedResolver = keyedResolver; 69 | proxy.AsyncKeyedResolver = asyncKeyedResolver; 70 | proxy.Key = key; 71 | proxy.Keyed = true; 72 | proxy.ServiceType = typeof(TServiceInterface); 73 | return ret; 74 | } 75 | /// 76 | /// Creates a new AsyncInterfaceCallDispatcher returned as type TServiceInterface 77 | /// 78 | /// 79 | /// 80 | /// 81 | public static TServiceInterface CreateInterfaceDispatcher(object key, Func> asyncKeyedResolver) 82 | { 83 | var ret = Create>(); 84 | var proxy = (ret as InterfaceCallDispatcher)!; 85 | proxy.AsyncKeyedResolver = asyncKeyedResolver; 86 | proxy.Key = key; 87 | proxy.Keyed = true; 88 | proxy.ServiceType = typeof(TServiceInterface); 89 | return ret; 90 | } 91 | /// 92 | /// Creates a new AsyncInterfaceCallDispatcher returned as type TServiceInterface 93 | /// 94 | /// 95 | /// 96 | /// 97 | public static TServiceInterface CreateInterfaceDispatcher(Func> asyncResolver) 98 | { 99 | var ret = Create>(); 100 | var proxy = ret as InterfaceCallDispatcher; 101 | proxy!.AsyncResolver = asyncResolver; 102 | proxy.ServiceType = typeof(TServiceInterface); 103 | return ret; 104 | } 105 | /// 106 | /// Creates a new AsyncInterfaceCallDispatcher returned as type TServiceInterface 107 | /// 108 | /// 109 | /// 110 | /// 111 | public static TServiceInterface CreateInterfaceDispatcher(Func resolver, Func>? asyncResolver = null) 112 | { 113 | var ret = Create>(); 114 | var proxy = (ret as InterfaceCallDispatcher)!; 115 | proxy.AsyncResolver = asyncResolver; 116 | proxy.Resolver = resolver; 117 | proxy.ServiceType = typeof(TServiceInterface); 118 | return ret; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/MethodInfoExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace SpawnDev.BlazorJS.WebWorkers 6 | { 7 | /// 8 | /// Static class that contains MethodInfo extension methods 9 | /// 10 | public static class MethodInfoExtension 11 | { 12 | private static Type AsyncStateMachineAttributeType = typeof(AsyncStateMachineAttribute); 13 | /// 14 | /// Returns true if the method has an async operator.
15 | /// Checked by looking for AsyncStateMachineAttribute which indicates method has the async operator. 16 | ///
17 | /// 18 | /// 19 | public static bool IsAsyncMethod(this MethodInfo method) => method.GetCustomAttribute(AsyncStateMachineAttributeType) != null; 20 | 21 | /// 22 | /// Knuth hash
23 | /// https://stackoverflow.com/a/9545731/3786270 24 | ///
25 | /// 26 | /// UInt64 hash value 27 | static ulong CalculateHash(string? value) 28 | { 29 | if (string.IsNullOrEmpty(value)) return 0; 30 | ulong hashedValue = 3074457345618258791ul; 31 | for (int i = 0; i < value.Length; i++) 32 | { 33 | hashedValue += value[i]; 34 | hashedValue *= 3074457345618258799ul; 35 | } 36 | return hashedValue; 37 | } 38 | 39 | /// 40 | /// Hash will not change if the signature does not change. (Unlike MethodInfo.GetHashCode() which can change between builds)
41 | /// Return value is UInt64 Knuth hash of MethodInfo.ToString().
42 | /// Result will be unique per Type but not among all Types. 43 | ///
44 | /// 45 | /// 46 | public static ulong GetMethodHash(this MethodInfo _this) => CalculateHash(_this.ToString()); 47 | 48 | /// 49 | /// Returns a string that represents the method in a format that matches the method's signature code as much as possible.
50 | /// Needs work. Do not expect 100% accurate representation from this method at this time.
51 | /// Implementation will likely change. 52 | ///
53 | /// 54 | /// 55 | /// 56 | public static string GetMethodSignature(this MethodInfo _this, bool includeNames = true) 57 | { 58 | string ret; 59 | var mi = _this; 60 | if (_this.IsGenericMethod) 61 | { 62 | mi = mi.GetGenericMethodDefinition(); 63 | } 64 | var strBuilder = new StringBuilder(); 65 | if (mi.IsPublic) 66 | strBuilder.Append("public "); 67 | else if (mi.IsPrivate) 68 | strBuilder.Append("private "); 69 | else if (mi.IsAssembly) 70 | strBuilder.Append("internal "); 71 | if (mi.IsFamily) 72 | strBuilder.Append("protected "); 73 | if (mi.IsStatic) 74 | strBuilder.Append("static "); 75 | if (includeNames) 76 | { 77 | var parameterInfos = mi.GetParameters(); 78 | var paramStrings = new List(); 79 | foreach (var parameterInfo in parameterInfos) 80 | { 81 | paramStrings.Add($"{parameterInfo.ParameterType.GetFormattedName()} {parameterInfo.Name}"); 82 | } 83 | strBuilder.Append($"{mi.ReturnType.GetFormattedName()} {mi.Name}({string.Join(", ", paramStrings)})"); 84 | ret = strBuilder.ToString(); 85 | } 86 | else 87 | { 88 | var parameterTypes = mi.GetParameters().Select(o => o.ParameterType.GetFormattedName()); 89 | ret = $"{strBuilder.ToString()}{mi.ReturnType.GetFormattedName()} {mi.Name}({string.Join(", ", parameterTypes)})"; 90 | } 91 | #if DEBUG 92 | Console.WriteLine($"GetMethodSignature: {ret}"); 93 | #endif 94 | return ret; 95 | } 96 | 97 | /// 98 | /// Returns a MethodInfo's Name with formatted generic arguments if any. 99 | /// 100 | /// 101 | /// 102 | public static string GetFormattedName(this MethodInfo mi) 103 | { 104 | try 105 | { 106 | if (mi.IsGenericMethod) 107 | { 108 | var name = mi.Name; 109 | var generics = mi.GetGenericArguments(); 110 | string genericArguments = generics.Select(x => x.GetFormattedName()).Aggregate((x1, x2) => $"{x1}, {x2}"); 111 | return $"{name}<{genericArguments}>"; 112 | } 113 | } 114 | catch 115 | { 116 | // continue 117 | } 118 | return mi.Name; 119 | } 120 | 121 | /// 122 | /// If the return type is a Task<> or ValueTask<> the Result Type is returned
123 | /// If the return type is a Task or ValueTask typeof(void) is returned
124 | /// otherwise MethodInfo.ReturnType is returned 125 | ///
126 | /// 127 | /// 128 | public static Type GetFinalReturnType(this MethodInfo _this) 129 | { 130 | return _this.ReturnType.AsyncReturnType() ?? _this.ReturnType; 131 | } 132 | 133 | /// 134 | /// Invokes a method asynchronously and returns the result if any
135 | /// Task, Task<>, ValueTask<>, and ValueTask are awaited and their results are returned, if any. 136 | ///
137 | /// 138 | /// 139 | /// 140 | /// 141 | public static async Task InvokeAsync(this MethodInfo _this, object? instance, object?[]? args) 142 | { 143 | object? returnValue = null; 144 | object? tmpValue = _this.Invoke(instance, args); 145 | if (tmpValue != null) 146 | { 147 | var returnType = _this.ReturnType; 148 | if (returnType.IsTaskTyped()) 149 | { 150 | await (dynamic)tmpValue; 151 | returnValue = returnType.GetProperty("Result")!.GetValue(tmpValue, null); 152 | } 153 | else if (tmpValue is Task task) 154 | { 155 | await task; 156 | } 157 | else if (returnType.IsValueTaskTyped()) 158 | { 159 | await (dynamic)tmpValue; 160 | returnValue = returnType.GetProperty("Result")!.GetValue(tmpValue, null); 161 | } 162 | else if (tmpValue is ValueTask valueTask) 163 | { 164 | await valueTask; 165 | } 166 | else 167 | { 168 | returnValue = tmpValue; 169 | } 170 | } 171 | return returnValue; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SerializableConstructorInfo.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using System.Linq; 4 | //using System.Reflection; 5 | //using System.Text; 6 | //using System.Text.Json.Serialization; 7 | //using System.Text.Json; 8 | //using System.Threading.Tasks; 9 | 10 | //namespace SpawnDev.BlazorJS.WebWorkers 11 | //{ 12 | // /// 13 | // /// On creation, SerializableConstructorInfoSlim extracts the information needed to allow serialization, deserialization, and resolving of a ConstructorInfo.
14 | // ///
15 | // public class SerializableConstructorInfo 16 | // { 17 | // [JsonIgnore] 18 | // public ConstructorInfo? ConstructorInfo 19 | // { 20 | // get 21 | // { 22 | // if (!Resolved) Resolve(); 23 | // return _ConstructorInfo; 24 | // } 25 | // } 26 | // private ConstructorInfo? _ConstructorInfo = null; 27 | // private bool Resolved = false; 28 | // /// 29 | // /// ConstructorInfo.ReflectedType type name 30 | // /// 31 | // public string ReflectedTypeName { get; init; } = ""; 32 | // public List ReflectedTypeGenericTypes { get; init; } = new List(); 33 | // /// 34 | // /// ConstructorInfo.DeclaringType type name 35 | // /// 36 | // public string DeclaringTypeName { get; init; } = ""; 37 | // /// 38 | // /// constructorInfo.GetParameters() type names 39 | // /// 40 | // public List ParameterTypes { get; init; } = new List(); 41 | // /// 42 | // /// constructorInfo.GetGenericArguments() type names 43 | // /// 44 | // public List GenericArguments { get; init; } = new List(); 45 | // /// 46 | // /// Deserialization constructor 47 | // /// 48 | // public SerializableConstructorInfo() { } 49 | // /// 50 | // /// Creates a new instance of SerializableConstructorInfo that represents 51 | // /// 52 | // /// 53 | // /// 54 | // public SerializableConstructorInfo(ConstructorInfo constructorInfo) 55 | // { 56 | // var mi = constructorInfo; 57 | // if (constructorInfo.ReflectedType == null) throw new Exception("Cannot serialize ConstructorInfo without ReflectedType"); 58 | // if (constructorInfo.ReflectedType.IsGenericType) 59 | // { 60 | // var reflectedTypeGenericType = constructorInfo.ReflectedType.GetGenericTypeDefinition(); 61 | // var reflectedTypeGenericTypes = constructorInfo.ReflectedType.GetGenericArguments(); 62 | // ReflectedTypeGenericTypes = new List(); 63 | // //////// 64 | // var art = true; 65 | // } 66 | // if (constructorInfo.IsConstructedGenericMethod) 67 | // { 68 | // GenericArguments = constructorInfo.GetGenericArguments().Select(o => GetTypeName(o)).ToList(); 69 | // if (constructorInfo.ReflectedType!.IsGenericType) 70 | // { 71 | // var genericType = constructorInfo.ReflectedType.GetGenericTypeDefinition(); 72 | // // TODO 73 | // var nmt = true; 74 | // } 75 | // } 76 | // ReflectedTypeName = GetTypeName(constructorInfo.ReflectedType); 77 | // DeclaringTypeName = GetTypeName(constructorInfo.DeclaringType); 78 | // ParameterTypes = mi.GetParameters().Select(o => GetTypeName(o.ParameterType)).ToList(); 79 | // _ConstructorInfo = constructorInfo; 80 | // Resolved = true; 81 | // } 82 | // /// 83 | // /// Deserializes SerializableConstructorInfo instance from string using System.Text.Json
84 | // /// PropertyNameCaseInsensitive = true is used in deserialization 85 | // ///
86 | // /// 87 | // /// 88 | // public static SerializableConstructorInfo? FromString(string json) 89 | // { 90 | // var ret = string.IsNullOrEmpty(json) || !json.StartsWith("{") ? null : JsonSerializer.Deserialize(json, DefaultJsonSerializerOptions); 91 | // return ret; 92 | // } 93 | // /// 94 | // /// Serializes SerializableConstructorInfo to a string using System.Text.Json 95 | // /// 96 | // /// 97 | // public override string ToString() => JsonSerializer.Serialize(this); 98 | 99 | // internal static JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; 100 | 101 | // static string GetTypeName(Type? type) 102 | // { 103 | // if (type == null) return ""; 104 | // return !string.IsNullOrEmpty(type.AssemblyQualifiedName) ? type.AssemblyQualifiedName : (!string.IsNullOrEmpty(type.FullName) ? type.FullName : type.Name); 105 | // } 106 | 107 | // void Resolve() 108 | // { 109 | // ConstructorInfo? constructorInfo = null; 110 | // if (Resolved) return; 111 | // Resolved = true; 112 | // constructorInfo = null; 113 | // var reflectedType = TypeExtensions.GetType(ReflectedTypeName); 114 | // if (reflectedType == null) 115 | // { 116 | // // Reflected type not found 117 | // return; 118 | // } 119 | // var parameterTypesDeserialized = ParameterTypes.Select(o => TypeExtensions.GetType(o)).ToList(); 120 | // var constructors = reflectedType.GetConstructors(); 121 | // foreach (var ctor in constructors) 122 | // { 123 | // var parameterInfos = ctor.GetParameters(); 124 | // if (parameterTypesDeserialized.Count != parameterInfos.Length) continue; 125 | // for(var i = 0; i < parameterInfos.Length; i++) 126 | // { 127 | // var parameterType = parameterInfos[i].ParameterType; 128 | // if (parameterTypesDeserialized[i] != parameterType) 129 | // { 130 | // continue; 131 | // } 132 | // } 133 | // constructorInfo = ctor; 134 | // break; 135 | // } 136 | // _ConstructorInfo = constructorInfo; 137 | // } 138 | // /// 139 | // /// Converts a ConstructorInfo instance into a string 140 | // /// 141 | // /// 142 | // /// 143 | // public static string SerializeConstructorInfo(ConstructorInfo constructorInfo) => new SerializableConstructorInfo(constructorInfo).ToString(); 144 | // /// 145 | // /// Converts a ConstructorInfo that has been serialized using SerializeConstructorInfo into a ConstructorInfo if serialization is successful or a null otherwise. 146 | // /// 147 | // /// 148 | // /// 149 | // public static ConstructorInfo? DeserializeConstructorInfo(string serializableConstructorInfoJson) 150 | // { 151 | // var tmp = FromString(serializableConstructorInfoJson); 152 | // return tmp == null ? null : tmp.ConstructorInfo; 153 | // } 154 | // } 155 | //} 156 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SharedCancellationTokenSource.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace SpawnDev.BlazorJS.WebWorkers 6 | { 7 | /// 8 | /// SharedCancellationTokenSource works similarly to CancellationTokenSource allowing the creation and controlling of SharedCancellationTokens
9 | /// which can be passed to WekWorkers to allow workers a synchronous method of checking for a cancellation flag set in another thread
10 | /// Requires globalThis.crossOriginIsolated == true due to using SharedArrayBuffer for cancellation signaling 11 | ///
12 | [JsonConverter(typeof(JsonConverters.HybridObjectConverterFactory))] 13 | public class SharedCancellationTokenSource : IDisposable 14 | { 15 | /// 16 | /// Returns true if SharedCancellationTokenSource appears to be supported 17 | /// 18 | public static bool Supported => SharedArrayBuffer.Supported; 19 | private static BlazorJSRuntime JS => BlazorJSRuntime.JS; 20 | /// 21 | /// Returns true if the cancelled flag is set to true 22 | /// 23 | [JsonIgnore] 24 | public bool IsCancellationRequested 25 | { 26 | get 27 | { 28 | if (_cancelled) return true; 29 | if (_sharedArrayBuffer != null) 30 | { 31 | SharedArrayBufferView ??= new Uint8Array(_sharedArrayBuffer); 32 | var cancelledFlag = Atomics.Load(SharedArrayBufferView, 0); 33 | if (cancelledFlag != 0) 34 | { 35 | _cancelled = true; 36 | } 37 | } 38 | return _cancelled; 39 | } 40 | } 41 | const int BUFFER_SIZE = 1; 42 | 43 | private Uint8Array? SharedArrayBufferView = null; 44 | 45 | // JsonInclude on non-public properties is supported by HybridObjectConverter 46 | [JsonInclude] 47 | [JsonPropertyName("sharedArrayBuffer")] 48 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 49 | private SharedArrayBuffer? _sharedArrayBuffer { get; set; } 50 | 51 | // JsonInclude on non-public properties is supported by HybridObjectConverter 52 | [JsonInclude] 53 | [JsonPropertyName("cancelled")] 54 | private bool _cancelled { get; set; } = false; 55 | 56 | private SharedCancellationToken? _token; 57 | /// 58 | /// Returns the cancellation token 59 | /// 60 | [JsonIgnore] 61 | public SharedCancellationToken Token 62 | { 63 | get 64 | { 65 | ThrowIfDisposed(); 66 | if (_token == null) 67 | { 68 | if (!_cancelled && _sharedArrayBuffer != null) 69 | { 70 | _token = new SharedCancellationToken(new SharedCancellationTokenSource() { _sharedArrayBuffer = _sharedArrayBuffer }); 71 | } 72 | else 73 | { 74 | _token = new SharedCancellationToken(_cancelled); 75 | } 76 | } 77 | return _token; 78 | } 79 | } 80 | // json constructor (HybridObjectConverter will use this because it is marked JsonConstructorAttribute) 81 | [JsonConstructor] 82 | private SharedCancellationTokenSource() { } 83 | /// 84 | /// Creates a new instance with a pre-set cancelled state 85 | /// 86 | /// 87 | public SharedCancellationTokenSource(bool cancelled = false) 88 | { 89 | if (!cancelled) 90 | { 91 | //if (!CrossOriginIsolated) 92 | //{ 93 | // throw new NotSupportedException("Failed to create SharedCancellationTokenSource. CrossOriginIsolated is required due to SharedArrayBuffer restrictions"); 94 | //} 95 | _sharedArrayBuffer = new SharedArrayBuffer(BUFFER_SIZE); 96 | SharedArrayBufferView = new Uint8Array(_sharedArrayBuffer); 97 | } 98 | else 99 | { 100 | _cancelled = true; 101 | } 102 | } 103 | /// 104 | /// Creates a new instance that cancels after the given number of milliseconds 105 | /// 106 | /// 107 | public SharedCancellationTokenSource(int cancelAfterMillisecondDelay) 108 | { 109 | //if (!CrossOriginIsolated) 110 | //{ 111 | // throw new NotSupportedException("Failed to create SharedCancellationTokenSource. CrossOriginIsolated is required due to SharedArrayBuffer restrictions"); 112 | //} 113 | _sharedArrayBuffer = new SharedArrayBuffer(BUFFER_SIZE); 114 | SharedArrayBufferView = new Uint8Array(_sharedArrayBuffer); 115 | CancelAfter(cancelAfterMillisecondDelay); 116 | } 117 | /// 118 | /// Sets the cancelled flag to true 119 | /// 120 | public void Cancel() 121 | { 122 | ThrowIfDisposed(); 123 | SetCancelled(); 124 | } 125 | private void SetCancelled() 126 | { 127 | if (IsDisposed) return; 128 | if (IsCancellationRequested) return; 129 | if (_sharedArrayBuffer == null) return; 130 | _cancelled = true; 131 | SharedArrayBufferView ??= new Uint8Array(_sharedArrayBuffer); 132 | Atomics.Store(SharedArrayBufferView, 0, 1); 133 | } 134 | /// 135 | /// Cancels the token after a set amount of time 136 | /// 137 | /// 138 | public void CancelAfter(int millisecondDelay) 139 | { 140 | ThrowIfDisposed(); 141 | if (IsCancellationRequested) return; 142 | var cts = new CancellationTokenSource(millisecondDelay); 143 | cts.Token.Register(() => 144 | { 145 | cts.Dispose(); 146 | SetCancelled(); 147 | }); 148 | } 149 | /// Throws an exception if the source has been disposed. 150 | private void ThrowIfDisposed() 151 | { 152 | if (IsDisposed) throw new ObjectDisposedException(nameof(SharedCancellationTokenSource)); 153 | } 154 | // Throws an OCE; separated out to enable better inlining of ThrowIfCancellationRequested 155 | [DoesNotReturn] 156 | private void ThrowOperationCanceledException() => throw new OperationCanceledException(); 157 | /// 158 | /// Returns true if this instance has been disposed 159 | /// 160 | public bool IsDisposed { get; private set; } = false; 161 | /// 162 | /// Releases disposable resources 163 | /// 164 | public void Dispose() 165 | { 166 | if (IsDisposed) return; 167 | IsDisposed = true; 168 | if (SharedArrayBufferView != null) 169 | { 170 | SharedArrayBufferView.Dispose(); 171 | SharedArrayBufferView = null; 172 | } 173 | if (_sharedArrayBuffer != null) 174 | { 175 | _sharedArrayBuffer.Dispose(); 176 | _sharedArrayBuffer = null; 177 | } 178 | if (_token != null) 179 | { 180 | _token.Dispose(); 181 | _token = null; 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## custom 2 | SpawnDev.BlazorJS.WebWorkers/tasks/netstandard2.0/*.dll 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Oo]ut/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/buildcontent/patch.js: -------------------------------------------------------------------------------- 1 | // Todd Tanner 2 | // 2024 3 | // this script is loaded before the pre-patched Blazor startup script and handles the calls that have been converted from `import` to `importShim` 4 | // Blazor WASM .csproj must use true in a PropertyGroup to pre-patch the Blazor Javascript framework files. 5 | // - use importShim instead of import 6 | // - use exportShim instead of export 7 | // this allows the Blazor _framework scripts to load in browser any scope (window, worker, shared worker, service worker) 8 | // requires SpawnDev.BlazorJS.WebWorkers when running in non-window context to provide a faux document and window environment 9 | if (!globalThis.importShim) { 10 | //console.log('Defining importShim'); 11 | // if loaded into browser extension content mode, document and history will be undefined, but their globalThis versions are set. 12 | // should be harmless in every other situation 13 | if (typeof document === 'undefined' && globalThis.document) document = globalThis.document; 14 | if (typeof history === 'undefined' && globalThis.history) history = globalThis.history; 15 | // create globalThis.blazorConfig if it does not exist 16 | globalThis.blazorConfig = globalThis.blazorConfig ?? {}; 17 | // add defaults to globalThis.blazorConfig 18 | globalThis.blazorConfig = Object.assign( 19 | { 20 | documentBaseURI: globalThis.document ? globalThis.document.baseURI : new URL('./', globalThis.location.href).toString(), 21 | blazorBaseURI: '', 22 | frameworkFolderName: '_framework', 23 | contentFolderName: '_content', 24 | }, 25 | globalThis.blazorConfig 26 | ); 27 | if (!globalThis.blazorConfig.blazorBaseURI) { 28 | globalThis.blazorConfig.blazorBaseURI = (function () { 29 | var uri = new URL(`./`, location.href); 30 | if (uri.pathname.includes(`${globalThis.blazorConfig.contentFolderName}/`)) { 31 | var subpath = uri.pathname.substring(0, uri.pathname.indexOf(`${globalThis.blazorConfig.contentFolderName}/`)); 32 | return new URL(subpath, location.href).toString(); 33 | } else if (uri.pathname.includes(`${globalThis.blazorConfig.frameworkFolderName}/`)) { 34 | var subpath = uri.pathname.substring(0, uri.pathname.indexOf(`${globalThis.blazorConfig.frameworkFolderName}/`)); 35 | return new URL(subpath, location.href).toString(); 36 | } 37 | return uri.toString(); 38 | })(); 39 | } 40 | if (typeof globalThis.constructor.name === 'undefined' && globalThis.window) { 41 | // Running in Firefox extension content mode 42 | globalThis.constructor.name = 'Window'; 43 | } 44 | // sets the document.baseURI to Blazor app's base url 45 | if (globalThis.constructor.name !== 'Window' && globalThis.document) { 46 | globalThis.document.baseURI = globalThis.blazorConfig.blazorBaseURI; 47 | } 48 | // patched _framework scripts will call exportShim with their filename and optionally their exports 49 | // returns the script's exports object which may be modified to finish the export 50 | globalThis.exportShimValues = {}; 51 | globalThis.exportShim = (filename, moduleExports) => { 52 | if (moduleExports) { 53 | globalThis.exportShimValues[filename] = moduleExports; 54 | } else if (!globalThis.exportShimValues[filename]) { 55 | globalThis.exportShimValues[filename] = {}; 56 | } 57 | return globalThis.exportShimValues[filename]; 58 | }; 59 | function hasImportScripts() { 60 | if (typeof globalThis.importScripts !== void 0) { 61 | try { 62 | importScripts('./spawndev.blazorjs.webworkers.empty.js'); 63 | //console.log('importScripts supported'); 64 | return true; 65 | } catch { } 66 | } 67 | //console.log('importScripts not supported'); 68 | return false; 69 | } 70 | globalThis.importScriptsSupported = hasImportScripts(); 71 | // a custom resolver can be used if set 72 | globalThis.importShimResolver = null; 73 | // dynamic import shim for patched _framework scripts 74 | // uses dynamic import in Window scope 75 | // uses importScripts in non-window scopes running in 'classic' mode (DedicatedWorker, SharedWorker, ServiceWorker) 76 | // uses static imports in non-window scopes running in 'module' mode (DedicatedWorker, SharedWorker, ServiceWorker) 77 | globalThis.dynamicImportSupported = globalThis.constructor.name === 'Window'; 78 | globalThis.importShim = function (moduleName) { 79 | var filename = moduleName.split('?')[0].split('#')[0] 80 | filename = filename.indexOf('/') === -1 ? filename : filename.substring(filename.lastIndexOf('/') + 1); 81 | filename = filename.substring(0, filename.lastIndexOf('.js') + 3); 82 | //console.log(`importShim: ${moduleName}`, filename); 83 | return new Promise(async function (resolve, reject) { 84 | // _framework module scipts have been modified to call exportShim instead of using export { Var as ExportName, ... }, export default ..., etc. 85 | if (globalThis.exportShimValues[filename]) { 86 | var moduleExports = globalThis.exportShimValues[filename]; 87 | //console.log(`Pre-resolved: ${moduleName}`, filename, moduleExports); 88 | resolve(moduleExports); 89 | return; 90 | } 91 | if (globalThis.importShimResolver) { 92 | var tmp = await globalThis.importShimResolver(moduleName, filename); 93 | if (tmp !== void 0 || globalThis.exportShimValues[filename] !== void 0) { 94 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 95 | tmp ??= {}; 96 | globalThis.exportShimValues[filename] = Object.assign(moduleExports, tmp); 97 | moduleExports = globalThis.exportShimValues[filename]; 98 | resolve(moduleExports); 99 | return; 100 | } 101 | } 102 | if (globalThis.dynamicImportSupported !== false) { 103 | try { 104 | var tmp = await import(moduleName); 105 | globalThis.dynamicImportSupported = true; 106 | // if the script called exportShim, it may have already set an exportShimValues[filename], so map the return value to the existing on (if one) 107 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 108 | globalThis.exportShimValues[filename] = Object.assign(moduleExports, tmp); 109 | moduleExports = globalThis.exportShimValues[filename]; 110 | resolve(moduleExports); 111 | return; 112 | } catch { 113 | // dynamic import not supported 114 | globalThis.dynamicImportSupported = false; 115 | } 116 | } 117 | if (globalThis.importScriptsSupported) { 118 | try { 119 | importScripts(moduleName); 120 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 121 | resolve(moduleExports); 122 | } catch (e) { 123 | reject(e); 124 | } 125 | return; 126 | } 127 | console.warn(`${globalThis.constructor.name} - Unresolved import: ${filename}. To silence this warning set "globalThis.exportShimValues['${filename}']"`); 128 | // unsupported environment 129 | resolve({}); 130 | }); 131 | }; 132 | // import.meta has been patched in module scripts to call this method with their script name instead 133 | // that way we can determine the correct meta.url property 134 | globalThis.importShim.meta = function (filename) { 135 | var meta = { 136 | // only scripts 137 | url: new URL(`${globalThis.blazorConfig.frameworkFolderName}/${filename}`, globalThis.blazorConfig.blazorBaseURI).toString(), 138 | }; 139 | return meta; 140 | }; 141 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/wwwroot/spawndev.blazorjs.webworkers.import-shim.js: -------------------------------------------------------------------------------- 1 | // Todd Tanner 2 | // 2024 3 | // this script is loaded before the pre-patched Blazor startup script and handles the calls that have been converted from `import` to `importShim` 4 | // Blazor WASM .csproj must use true in a PropertyGroup to pre-patch the Blazor Javascript framework files. 5 | // - use importShim instead of import 6 | // - use exportShim instead of export 7 | // this allows the Blazor _framework scripts to load in browser any scope (window, worker, shared worker, service worker) 8 | // requires SpawnDev.BlazorJS.WebWorkers when running in non-window context to provide a faux document and window environment 9 | if (!globalThis.importShim) { 10 | //console.log('Defining importShim'); 11 | // if loaded into browser extension content mode, document and history will be undefined, but their globalThis versions are set. 12 | // should be harmless in every other situation 13 | if (typeof document === 'undefined' && globalThis.document) document = globalThis.document; 14 | if (typeof history === 'undefined' && globalThis.history) history = globalThis.history; 15 | // create globalThis.blazorConfig if it does not exist 16 | globalThis.blazorConfig = globalThis.blazorConfig ?? {}; 17 | // add defaults to globalThis.blazorConfig 18 | globalThis.blazorConfig = Object.assign( 19 | { 20 | documentBaseURI: globalThis.document ? globalThis.document.baseURI : new URL('./', globalThis.location.href).toString(), 21 | blazorBaseURI: '', 22 | frameworkFolderName: '_framework', 23 | contentFolderName: '_content', 24 | }, 25 | globalThis.blazorConfig 26 | ); 27 | if (!globalThis.blazorConfig.blazorBaseURI) { 28 | globalThis.blazorConfig.blazorBaseURI = (function () { 29 | var uri = new URL(`./`, location.href); 30 | if (uri.pathname.includes(`${globalThis.blazorConfig.contentFolderName}/`)) { 31 | var subpath = uri.pathname.substring(0, uri.pathname.indexOf(`${globalThis.blazorConfig.contentFolderName}/`)); 32 | return new URL(subpath, location.href).toString(); 33 | } else if (uri.pathname.includes(`${globalThis.blazorConfig.frameworkFolderName}/`)) { 34 | var subpath = uri.pathname.substring(0, uri.pathname.indexOf(`${globalThis.blazorConfig.frameworkFolderName}/`)); 35 | return new URL(subpath, location.href).toString(); 36 | } 37 | return uri.toString(); 38 | })(); 39 | } 40 | if (typeof globalThis.constructor.name === 'undefined' && globalThis.window) { 41 | // Running in Firefox extension content mode 42 | globalThis.constructor.name = 'Window'; 43 | } 44 | // sets the document.baseURI to Blazor app's base url 45 | if (globalThis.constructor.name !== 'Window' && globalThis.document) { 46 | globalThis.document.baseURI = globalThis.blazorConfig.blazorBaseURI; 47 | } 48 | // patched _framework scripts will call exportShim with their filename and optionally their exports 49 | // returns the script's exports object which may be modified to finish the export 50 | globalThis.exportShimValues = {}; 51 | globalThis.exportShim = (filename, moduleExports) => { 52 | if (moduleExports) { 53 | globalThis.exportShimValues[filename] = moduleExports; 54 | } else if (!globalThis.exportShimValues[filename]) { 55 | globalThis.exportShimValues[filename] = {}; 56 | } 57 | return globalThis.exportShimValues[filename]; 58 | }; 59 | function hasImportScripts() { 60 | if (typeof globalThis.importScripts !== void 0) { 61 | try { 62 | importScripts('./spawndev.blazorjs.webworkers.empty.js'); 63 | //console.log('importScripts supported'); 64 | return true; 65 | } catch { } 66 | } 67 | //console.log('importScripts not supported'); 68 | return false; 69 | } 70 | globalThis.importScriptsSupported = hasImportScripts(); 71 | // a custom resolver can be used if set 72 | globalThis.importShimResolver = null; 73 | // dynamic import shim for patched _framework scripts 74 | // uses dynamic import in Window scope 75 | // uses importScripts in non-window scopes running in 'classic' mode (DedicatedWorker, SharedWorker, ServiceWorker) 76 | // uses static imports in non-window scopes running in 'module' mode (DedicatedWorker, SharedWorker, ServiceWorker) 77 | globalThis.dynamicImportSupported = globalThis.constructor.name === 'Window'; 78 | globalThis.importShim = function (moduleName) { 79 | var filename = moduleName.split('?')[0].split('#')[0] 80 | filename = filename.indexOf('/') === -1 ? filename : filename.substring(filename.lastIndexOf('/') + 1); 81 | filename = filename.substring(0, filename.lastIndexOf('.js') + 3); 82 | //console.log(`importShim: ${moduleName}`, filename); 83 | return new Promise(async function (resolve, reject) { 84 | // _framework module scipts have been modified to call exportShim instead of using export { Var as ExportName, ... }, export default ..., etc. 85 | if (globalThis.exportShimValues[filename]) { 86 | var moduleExports = globalThis.exportShimValues[filename]; 87 | //console.log(`Pre-resolved: ${moduleName}`, filename, moduleExports); 88 | resolve(moduleExports); 89 | return; 90 | } 91 | if (globalThis.importShimResolver) { 92 | var tmp = await globalThis.importShimResolver(moduleName, filename); 93 | if (tmp !== void 0 || globalThis.exportShimValues[filename] !== void 0) { 94 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 95 | tmp ??= {}; 96 | globalThis.exportShimValues[filename] = Object.assign(moduleExports, tmp); 97 | moduleExports = globalThis.exportShimValues[filename]; 98 | resolve(moduleExports); 99 | return; 100 | } 101 | } 102 | if (globalThis.dynamicImportSupported !== false) { 103 | try { 104 | var tmp = await import(moduleName); 105 | globalThis.dynamicImportSupported = true; 106 | // if the script called exportShim, it may have already set an exportShimValues[filename], so map the return value to the existing on (if one) 107 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 108 | globalThis.exportShimValues[filename] = Object.assign(moduleExports, tmp); 109 | moduleExports = globalThis.exportShimValues[filename]; 110 | resolve(moduleExports); 111 | return; 112 | } catch { 113 | // dynamic import not supported 114 | globalThis.dynamicImportSupported = false; 115 | } 116 | } 117 | if (globalThis.importScriptsSupported) { 118 | try { 119 | importScripts(moduleName); 120 | var moduleExports = globalThis.exportShimValues[filename] ?? {}; 121 | resolve(moduleExports); 122 | } catch (e) { 123 | reject(e); 124 | } 125 | return; 126 | } 127 | console.warn(`${globalThis.constructor.name} - Unresolved import: ${filename}. To silence this warning set "globalThis.exportShimValues['${filename}']"`); 128 | // unsupported environment 129 | resolve({}); 130 | }); 131 | }; 132 | // import.meta has been patched in module scripts to call this method with their script name instead 133 | // that way we can determine the correct meta.url property 134 | globalThis.importShim.meta = function (filename) { 135 | var meta = { 136 | // only scripts 137 | url: new URL(`${globalThis.blazorConfig.frameworkFolderName}/${filename}`, globalThis.blazorConfig.blazorBaseURI).toString(), 138 | }; 139 | return meta; 140 | }; 141 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SerializableMethodInfoSlim.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace SpawnDev.BlazorJS.WebWorkers 7 | { 8 | public class SerializableMethodInfoSlim 9 | { 10 | [JsonIgnore] 11 | public MethodInfo? MethodInfo 12 | { 13 | get 14 | { 15 | if (!Resolved) Resolve(); 16 | return _MethodInfo; 17 | } 18 | } 19 | private MethodInfo? _MethodInfo = null; 20 | private bool Resolved = false; 21 | /// 22 | /// MethodInfo.ReflectedType type name 23 | /// 24 | public string ReflectedTypeName { get; init; } = ""; 25 | /// 26 | /// MethodInfo.DeclaringType type name 27 | /// 28 | public string DeclaringTypeName { get; init; } = ""; 29 | /// 30 | /// MethodInfo.Name 31 | /// 32 | public string MethodName { get; init; } = ""; 33 | /// 34 | /// methodInfo.GetParameters() type names 35 | /// 36 | public List ParameterTypes { get; init; } = new List(); 37 | /// 38 | /// MethodInfo.ReturnType type name 39 | /// 40 | public string ReturnType { get; init; } = ""; 41 | /// 42 | /// methodInfo.GetGenericArguments() type names 43 | /// 44 | public List GenericArguments { get; init; } = new List(); 45 | /// 46 | /// Deserialization constructor 47 | /// 48 | public SerializableMethodInfoSlim() { } 49 | /// 50 | /// Creates a new instance of SerializableMethodInfoSlim that represents 51 | /// 52 | /// 53 | /// 54 | public SerializableMethodInfoSlim(MethodInfo methodInfo) 55 | { 56 | var mi = methodInfo; 57 | if (methodInfo.ReflectedType == null) throw new Exception("Cannot serialize MethodInfo without ReflectedType"); 58 | if (methodInfo.IsConstructedGenericMethod) 59 | { 60 | GenericArguments = methodInfo.GetGenericArguments().Select(o => GetTypeName(o)).ToList(); 61 | mi = methodInfo.GetGenericMethodDefinition(); 62 | } 63 | MethodName = mi.Name; 64 | ReflectedTypeName = GetTypeName(methodInfo.ReflectedType); 65 | DeclaringTypeName = GetTypeName(methodInfo.DeclaringType); 66 | ReturnType = GetTypeName(mi.ReturnType); 67 | ParameterTypes = mi.GetParameters().Select(o => GetTypeName(o.ParameterType)).ToList(); 68 | _MethodInfo = methodInfo; 69 | Resolved = true; 70 | } 71 | public SerializableMethodInfoSlim(Delegate methodDelegate) 72 | { 73 | var methodInfo = methodDelegate.Method; 74 | var mi = methodInfo; 75 | if (methodInfo.ReflectedType == null) throw new Exception("Cannot serialize MethodInfo without ReflectedType"); 76 | if (methodInfo.IsConstructedGenericMethod) 77 | { 78 | GenericArguments = methodInfo.GetGenericArguments().Select(o => GetTypeName(o)).ToList(); 79 | mi = methodInfo.GetGenericMethodDefinition(); 80 | } 81 | MethodName = mi.Name; 82 | ReflectedTypeName = GetTypeName(methodInfo.ReflectedType); 83 | DeclaringTypeName = GetTypeName(methodInfo.DeclaringType); 84 | ReturnType = GetTypeName(mi.ReturnType); 85 | ParameterTypes = mi.GetParameters().Select(o => GetTypeName(o.ParameterType)).ToList(); 86 | _MethodInfo = methodInfo; 87 | Resolved = true; 88 | } 89 | 90 | /// 91 | /// Deserializes SerializableMethodInfoSlim instance from string using System.Text.Json
92 | /// PropertyNameCaseInsensitive = true is used in deserialization 93 | ///
94 | /// 95 | /// 96 | [RequiresUnreferencedCode("")] 97 | public static SerializableMethodInfoSlim? FromString(string json) 98 | { 99 | var ret = string.IsNullOrEmpty(json) || !json.StartsWith("{") ? null : JsonSerializer.Deserialize(json, DefaultJsonSerializerOptions); 100 | return ret; 101 | } 102 | /// 103 | /// Serializes SerializableMethodInfoSlim to a string using System.Text.Json 104 | /// 105 | /// 106 | public override string ToString() => JsonSerializer.Serialize(this); 107 | 108 | internal static JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; 109 | 110 | static string GetTypeName(Type? type) 111 | { 112 | if (type == null) return ""; 113 | return !string.IsNullOrEmpty(type.FullName) ? type.FullName : type.Name; 114 | } 115 | 116 | void Resolve() 117 | { 118 | MethodInfo? methodInfo = null; 119 | if (Resolved) return; 120 | Resolved = true; 121 | methodInfo = null; 122 | var reflectedType = TypeExtensions.GetType(ReflectedTypeName); 123 | if (reflectedType == null) 124 | { 125 | // Reflected type not found 126 | return; 127 | } 128 | var methodsWithName = reflectedType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static).Where(o => o.Name == MethodName); 129 | if (!methodsWithName.Any()) 130 | { 131 | // No method found with this MethodName found in ReflectedType 132 | return; 133 | } 134 | MethodInfo? mi = null; 135 | foreach (var method in methodsWithName) 136 | { 137 | var msi = new SerializableMethodInfoSlim(method); 138 | if (msi.ReturnType == ReturnType && msi.ParameterTypes.SequenceEqual(ParameterTypes)) 139 | { 140 | mi = method; 141 | break; 142 | } 143 | } 144 | if (mi == null) 145 | { 146 | // No method found that matches the base method signature 147 | return; 148 | } 149 | if (mi.IsGenericMethod) 150 | { 151 | if (GenericArguments == null || !GenericArguments.Any()) 152 | { 153 | // Generics information in GenericArguments is missing. Resolve not possible. 154 | return; 155 | } 156 | var genericTypes = new Type[GenericArguments.Count]; 157 | for (var i = 0; i < genericTypes.Length; i++) 158 | { 159 | var gTypeName = GenericArguments[i]; 160 | var gType = TypeExtensions.GetType(gTypeName); 161 | if (gType == null) 162 | { 163 | // One of the generic types needed to make the generic method was not found 164 | return; 165 | } 166 | genericTypes[i] = gType; 167 | } 168 | methodInfo = mi.MakeGenericMethod(genericTypes); 169 | } 170 | else 171 | { 172 | methodInfo = mi; 173 | } 174 | _MethodInfo = methodInfo; 175 | } 176 | /// 177 | /// Converts a MethodInfo instance into a string 178 | /// 179 | /// 180 | /// 181 | public static string SerializeMethodInfo(MethodInfo methodInfo) => new SerializableMethodInfoSlim(methodInfo).ToString(); 182 | public static string SerializeMethodInfo(Delegate methodDeleate) => new SerializableMethodInfoSlim(methodDeleate.Method).ToString(); 183 | 184 | /// 185 | /// Converts a MethodInfo that has been serialized using SerializeMethodInfo into a MethodInfo if serialization is successful or a null otherwise. 186 | /// 187 | /// 188 | /// 189 | [RequiresUnreferencedCode("")] 190 | public static MethodInfo? DeserializeMethodInfo(string serializableMethodInfoJson) 191 | { 192 | var tmp = FromString(serializableMethodInfoJson); 193 | return tmp == null ? null : tmp.MethodInfo; 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/SerializableMethodBase.cs: -------------------------------------------------------------------------------- 1 | //using System.Reflection; 2 | //using System.Text.Json; 3 | //using System.Text.Json.Serialization; 4 | 5 | //namespace SpawnDev.BlazorJS.WebWorkers 6 | //{ 7 | // public class SerializableMethodBase 8 | // { 9 | // [JsonIgnore] 10 | // public MethodBase? MethodInfo 11 | // { 12 | // get 13 | // { 14 | // if (!Resolved) Resolve(); 15 | // return _MethodInfo; 16 | // } 17 | // } 18 | // private MethodBase? _MethodInfo = null; 19 | // private bool Resolved = false; 20 | // /// 21 | // /// MethodInfo.ReflectedType type name 22 | // /// 23 | // public string ReflectedTypeName { get; init; } = ""; 24 | // /// 25 | // /// MethodInfo.DeclaringType type name 26 | // /// 27 | // public string DeclaringTypeName { get; init; } = ""; 28 | // /// 29 | // /// MethodInfo.Name 30 | // /// 31 | // public string MethodName { get; init; } = ""; 32 | // /// 33 | // /// methodInfo.GetParameters() type names 34 | // /// 35 | // public List ParameterTypes { get; init; } = new List(); 36 | // /// 37 | // /// MethodInfo.ReturnType type name 38 | // /// 39 | // public string ReturnType { get; init; } = ""; 40 | // /// 41 | // /// methodInfo.GetGenericArguments() type names 42 | // /// 43 | // public List GenericArguments { get; init; } = new List(); 44 | // /// 45 | // /// Deserialization constructor 46 | // /// 47 | // public SerializableMethodBase() { } 48 | // /// 49 | // /// Creates a new instance of SerializableMethodInfo that represents 50 | // /// 51 | // /// 52 | // /// 53 | // public SerializableMethodBase(MethodBase methodInfo) 54 | // { 55 | // var mi = methodInfo; 56 | // if (methodInfo.ReflectedType == null) throw new Exception("Cannot serialize MethodInfo without ReflectedType"); 57 | // if (methodInfo.IsConstructedGenericMethod) 58 | // { 59 | // GenericArguments = methodInfo.GetGenericArguments().Select(o => GetTypeName(o)).ToList(); 60 | // mi = ((MethodInfo)methodInfo).GetGenericMethodDefinition(); 61 | // } 62 | // MethodName = mi.Name; 63 | // ReflectedTypeName = GetTypeName(methodInfo.ReflectedType); 64 | // DeclaringTypeName = GetTypeName(methodInfo.DeclaringType); 65 | // ReturnType = methodInfo is MethodInfo mit ? GetTypeName(mit.ReturnType) : ""; 66 | // ParameterTypes = mi.GetParameters().Select(o => GetTypeName(o.ParameterType)).ToList(); 67 | // _MethodInfo = methodInfo; 68 | // Resolved = true; 69 | // } 70 | // public SerializableMethodBase(Delegate methodDelegate) 71 | // { 72 | // var methodInfo = methodDelegate.Method; 73 | // var mi = methodInfo; 74 | // if (methodInfo.ReflectedType == null) throw new Exception("Cannot serialize MethodInfo without ReflectedType"); 75 | // if (methodInfo.IsConstructedGenericMethod) 76 | // { 77 | // GenericArguments = methodInfo.GetGenericArguments().Select(o => GetTypeName(o)).ToList(); 78 | // mi = methodInfo.GetGenericMethodDefinition(); 79 | // } 80 | // MethodName = mi.Name; 81 | // ReflectedTypeName = GetTypeName(methodInfo.ReflectedType); 82 | // DeclaringTypeName = GetTypeName(methodInfo.DeclaringType); 83 | // ReturnType = GetTypeName(mi.ReturnType); 84 | // ParameterTypes = mi.GetParameters().Select(o => GetTypeName(o.ParameterType)).ToList(); 85 | // _MethodInfo = methodInfo; 86 | // Resolved = true; 87 | // } 88 | // /// 89 | // /// Deserializes SerializableMethodInfo instance from string using System.Text.Json
90 | // /// PropertyNameCaseInsensitive = true is used in deserialization 91 | // ///
92 | // /// 93 | // /// 94 | // public static SerializableMethodInfo? FromString(string json) 95 | // { 96 | // var ret = string.IsNullOrEmpty(json) || !json.StartsWith("{") ? null : JsonSerializer.Deserialize(json, DefaultJsonSerializerOptions); 97 | // return ret; 98 | // } 99 | // /// 100 | // /// Serializes SerializableMethodInfo to a string using System.Text.Json 101 | // /// 102 | // /// 103 | // public override string ToString() => JsonSerializer.Serialize(this); 104 | 105 | // internal static JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; 106 | 107 | // static string GetTypeName(Type? type) 108 | // { 109 | // if (type == null) return ""; 110 | // return !string.IsNullOrEmpty(type.AssemblyQualifiedName) ? type.AssemblyQualifiedName : (!string.IsNullOrEmpty(type.FullName) ? type.FullName : type.Name); 111 | // } 112 | 113 | // void Resolve() 114 | // { 115 | // MethodInfo? methodInfo = null; 116 | // if (Resolved) return; 117 | // Resolved = true; 118 | // methodInfo = null; 119 | // var reflectedType = TypeExtensions.GetType(ReflectedTypeName); 120 | // if (reflectedType == null) 121 | // { 122 | // // Reflected type not found 123 | // return; 124 | // } 125 | // var methodsWithName = reflectedType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static).Where(o => o.Name == MethodName); 126 | // if (!methodsWithName.Any()) 127 | // { 128 | // // No method found with this MethodName found in ReflectedType 129 | // return; 130 | // } 131 | // MethodInfo? mi = null; 132 | // foreach (var method in methodsWithName) 133 | // { 134 | // var msi = new SerializableMethodInfo(method); 135 | // if (msi.ReturnType == ReturnType && msi.ParameterTypes.SequenceEqual(ParameterTypes)) 136 | // { 137 | // mi = method; 138 | // break; 139 | // } 140 | // } 141 | // if (mi == null) 142 | // { 143 | // // No method found that matches the base method signature 144 | // return; 145 | // } 146 | // if (mi.IsGenericMethod) 147 | // { 148 | // if (GenericArguments == null || !GenericArguments.Any()) 149 | // { 150 | // // Generics information in GenericArguments is missing. Resolve not possible. 151 | // return; 152 | // } 153 | // var genericTypes = new Type[GenericArguments.Count]; 154 | // for (var i = 0; i < genericTypes.Length; i++) 155 | // { 156 | // var gTypeName = GenericArguments[i]; 157 | // var gType = TypeExtensions.GetType(gTypeName); 158 | // if (gType == null) 159 | // { 160 | // // One of the generic types needed to make the generic method was not found 161 | // return; 162 | // } 163 | // genericTypes[i] = gType; 164 | // } 165 | // methodInfo = mi.MakeGenericMethod(genericTypes); 166 | // } 167 | // else 168 | // { 169 | // methodInfo = mi; 170 | // } 171 | // _MethodInfo = methodInfo; 172 | // } 173 | // /// 174 | // /// Converts a MethodInfo instance into a string 175 | // /// 176 | // /// 177 | // /// 178 | // public static string SerializeMethodInfo(MethodInfo methodInfo) => new SerializableMethodInfo(methodInfo).ToString(); 179 | // public static string SerializeMethodInfo(Delegate methodDeleate) => new SerializableMethodInfo(methodDeleate.Method).ToString(); 180 | // /// 181 | // /// Converts a MethodInfo that has been serialized using SerializeMethodInfo into a MethodInfo if serialization is successful or a null otherwise. 182 | // /// 183 | // /// 184 | // /// 185 | // public static MethodInfo? DeserializeMethodInfo(string serializableMethodInfoJson) 186 | // { 187 | // var tmp = FromString(serializableMethodInfoJson); 188 | // return tmp == null ? null : tmp.MethodInfo; 189 | // } 190 | // } 191 | //} 192 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/AppInstance.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | using System.Reflection; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace SpawnDev.BlazorJS.WebWorkers 6 | { 7 | /// 8 | /// Represents a known WebWorkerService app instance connection 9 | /// 10 | public class AppInstance : AsyncCallDispatcher 11 | { 12 | List IncomingConnections = new List(); 13 | WebWorkerService? WebWorkerService = null; 14 | ServiceCallDispatcher? _Dispatcher = null; 15 | /// 16 | /// Returns true if the Dispatcher has been loaded 17 | /// 18 | public bool DispatcherLoaded => _Dispatcher != null; 19 | /// 20 | /// Returns true if the dispatcher failed to load 21 | /// 22 | public bool DispatcherLoadFailed { get; private set; } 23 | /// 24 | /// Returns true is this WebWorkerService instance is the local instance 25 | /// 26 | public bool IsLocal { get; private set; } 27 | /// 28 | /// Instance info 29 | /// 30 | public AppInstanceInfo Info { get; private set; } 31 | /// 32 | /// Returns the service call dispatcher for this instance 33 | /// 34 | ServiceCallDispatcher? Dispatcher 35 | { 36 | get 37 | { 38 | if (_Dispatcher != null) return _Dispatcher; 39 | if (!IsDisposed && !DispatcherLoadFailed) TryConnect(); 40 | return _Dispatcher; 41 | } 42 | } 43 | internal AppInstance(WebWorkerService webWorkerService, AppInstanceInfo info) 44 | { 45 | WebWorkerService = webWorkerService; 46 | Info = info; 47 | if (Info.InstanceId == WebWorkerService.InstanceId) 48 | { 49 | IsLocal = true; 50 | } 51 | else 52 | { 53 | _InstanceBroadcastChannel = new Lazy(() => new BroadcastChannel(Info.InstanceId)); 54 | } 55 | } 56 | void TryConnect() 57 | { 58 | if (WebWorkerService == null) return; 59 | try 60 | { 61 | if (IsLocal) 62 | { 63 | // best connection is via WebWorkerService.Local 64 | #if DEBUG && false 65 | Console.WriteLine("best connection is via WebWorkerService.Local"); 66 | #endif 67 | _Dispatcher = WebWorkerService.Local; 68 | } 69 | else if (Info.Scope == GlobalScope.SharedWorker) 70 | { 71 | // best connection is via a shared web worker connection 72 | #if DEBUG && false 73 | Console.WriteLine("best connection is via a shared web worker connection"); 74 | #endif 75 | _Dispatcher = WebWorkerService.GetSharedWebWorkerSync(Info.Name!); 76 | } 77 | else if (WebWorkerService.InterConnectSupported && WebWorkerService.InterConnectEnabled) 78 | { 79 | // best connection is a MessageChannel that we can pass along via interconnect (supports transferables) 80 | #if DEBUG && false 81 | Console.WriteLine("best connection is a MessageChannel that we can pass along via interconnect"); 82 | #endif 83 | using var messageChannel = new MessageChannel(); 84 | var port1 = messageChannel.Port1; 85 | using var port2 = messageChannel.Port2; 86 | _Dispatcher = new ServiceCallDispatcher(WebWorkerService.WebAssemblyServices, port1); 87 | port1.Start(); 88 | WebWorkerService.SendInterconnectPort(Info.InstanceId, port2); 89 | _Dispatcher.SendReadyFlag(); 90 | } 91 | else if (WebWorkerService.BroadcastChannelSupported) 92 | { 93 | // best connection is a separate BroadcastChannel (does not support transferables) 94 | #if DEBUG && false 95 | Console.WriteLine("best connection is a separate BroadcastChannel"); 96 | #endif 97 | var connectionId = Guid.NewGuid().ToString(); 98 | var messageChannel = new BroadcastChannel(connectionId); 99 | _Dispatcher = new ServiceCallDispatcher(WebWorkerService.WebAssemblyServices, messageChannel); 100 | SendConnectMessageToInstanceBroadcastChannel(connectionId); 101 | _Dispatcher.SendReadyFlag(); 102 | } 103 | else 104 | { 105 | throw new NotSupportedException("No communication channel available"); 106 | } 107 | } 108 | catch 109 | { 110 | DispatcherLoadFailed = true; 111 | } 112 | } 113 | Lazy? _InstanceBroadcastChannel = null; 114 | BroadcastChannel? InstanceBroadcastChannel => _InstanceBroadcastChannel?.Value; 115 | void SendConnectMessageToInstanceBroadcastChannel(string broadcastChannelId) 116 | { 117 | SendMessageToInstanceBroadcastChannel(new object[] { "broadcastChannelConnect", WebWorkerService!.Info, broadcastChannelId }); 118 | } 119 | void SendMessageToInstanceBroadcastChannel(object data) 120 | { 121 | if (!IsLocal) 122 | { 123 | InstanceBroadcastChannel?.PostMessage(data); 124 | } 125 | } 126 | internal void AddIncomingInterconnectPort(IMessagePortSimple incomingPort) 127 | { 128 | #if DEBUG && false 129 | Console.WriteLine($"AddIncomingConnection: {Info.InstanceId}"); 130 | #endif 131 | var incomingHandler = new ServiceCallDispatcher(WebWorkerService.WebAssemblyServices, incomingPort); 132 | IncomingConnections.Add(incomingHandler); 133 | if (incomingPort is MessagePort messagePort) 134 | { 135 | messagePort.Start(); 136 | } 137 | #if DEBUG && false 138 | Console.WriteLine($"AddIncomingInterconnectPort SupportsTransferable: {incomingHandler.MessagePortSupportsTransferable}"); 139 | #endif 140 | incomingHandler.SendReadyFlag(); 141 | } 142 | /// 143 | /// Returns true if this has been disposed 144 | /// 145 | [JsonIgnore] 146 | public bool IsDisposed { get; private set; } 147 | /// 148 | /// Releases all resources 149 | /// 150 | public void Dispose() 151 | { 152 | if (IsDisposed) return; 153 | IsDisposed = true; 154 | if (_InstanceBroadcastChannel != null && _InstanceBroadcastChannel.IsValueCreated) 155 | { 156 | _InstanceBroadcastChannel.Value.Close(); 157 | _InstanceBroadcastChannel.Value.Dispose(); 158 | _InstanceBroadcastChannel = null; 159 | } 160 | foreach (var incomingConnection in IncomingConnections) 161 | { 162 | incomingConnection.Dispose(); 163 | } 164 | IncomingConnections.Clear(); 165 | if (_Dispatcher != null) 166 | { 167 | if (IsLocal) 168 | { 169 | _Dispatcher = null; 170 | } 171 | else 172 | { 173 | _Dispatcher.Dispose(); 174 | _Dispatcher = null; 175 | } 176 | } 177 | WebWorkerService = null; 178 | } 179 | public override Task Call(Type serviceType, MethodInfo methodInfo, object?[]? args = null) 180 | { 181 | return Dispatcher!.Call(serviceType, methodInfo, args); 182 | } 183 | public override Task CallKeyed(Type serviceType, object serviceKey, MethodInfo methodInfo, object?[]? args = null) 184 | { 185 | return Dispatcher!.CallKeyed(serviceType, serviceKey, methodInfo, args); 186 | } 187 | public override Task CreateService(ConstructorInfo constructorInfo, Type? serviceType, object[]? args) 188 | { 189 | return Dispatcher!.CreateService(constructorInfo, serviceType, args); 190 | } 191 | public override Task CreateKeyedService(ConstructorInfo constructorInfo, Type? serviceType, object serviceKey, object[]? args) 192 | { 193 | return Dispatcher!.CreateKeyedService(constructorInfo, serviceType, serviceKey, args); 194 | } 195 | public override Task RemoveKeyedService(Type serviceType, object key) 196 | { 197 | return Dispatcher!.RemoveKeyedService(serviceType, key); 198 | } 199 | public override Task RemoveService(Type serviceType) 200 | { 201 | return Dispatcher!.RemoveService(serviceType); 202 | } 203 | public override Task KeyedServiceExists(Type serviceType, object key) 204 | { 205 | return Dispatcher!.KeyedServiceExists(serviceType, key); 206 | } 207 | public override Task ServiceExists(Type serviceType) 208 | { 209 | return Dispatcher!.ServiceExists(serviceType); 210 | } 211 | public override Task AddKeyedService(Type serviceType, Type implementationType, object key) 212 | { 213 | return Dispatcher!.AddKeyedService(serviceType, implementationType, key); 214 | } 215 | public override Task AddService(Type serviceType, Type implementationType) 216 | { 217 | return Dispatcher!.AddService(serviceType, implementationType); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.WebWorkers/AsyncCallDispatcherSlim.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | namespace SpawnDev.BlazorJS.WebWorkers 5 | { 6 | /// 7 | /// AsyncCallDispatcherSlim converts a call into a MethodInfo and an object[] containing the arguments used for the call and asynchronously returns the value
8 | /// The call is handled by an abstract method that must be implemented by the inheriting class.
9 | /// Usually this is done to allow serializing a call to be carried out elsewhere with the result being returned asynchronously.
10 | /// Task<object?> DispatchCallAsync(MethodInfo methodInfo, object?[]? args = null)
11 | /// Supported calling features*:
12 | /// Expressions - Supports asynchronous/synchronous public/private instance/static methods with generics, and property get and set. (Recommended)
13 | /// - Run, Set
14 | /// Interface proxy - Supports asynchronous public instance methods with generics. (uses DispatchProxy)
15 | /// - GetService
16 | /// *Calling feature support dependent on call serialization and deserialization mechanisms used by the inheriting class 17 | ///
18 | public abstract class AsyncCallDispatcherSlim 19 | { 20 | /// 21 | /// All calls are handled by this method 22 | /// 23 | /// 24 | /// 25 | /// 26 | protected abstract Task DispatchCallAsync(MethodInfo methodInfo, object?[]? args); 27 | #region Expressions 28 | /// 29 | /// Converts an Expression into a MethodInfo and a call arguments array
30 | /// Then calls DispatchCall with them 31 | ///
32 | /// 33 | /// 34 | /// 35 | /// 36 | protected Task CallExpression(Expression expr, object?[]? argsExt = null) 37 | { 38 | if (expr is MethodCallExpression methodCallExpression) 39 | { 40 | // method call 41 | var methodInfo = methodCallExpression.Method; 42 | var args = methodCallExpression.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object)), null).Compile()()).ToArray(); 43 | return DispatchCallAsync(methodInfo, args); 44 | } 45 | else if (expr is MemberExpression memberExpression) 46 | { 47 | if (argsExt == null || argsExt.Length == 0) 48 | { 49 | // get call 50 | var propertyInfo = (PropertyInfo)memberExpression.Member; 51 | var methodInfo = propertyInfo.GetMethod; 52 | if (methodInfo == null) 53 | { 54 | throw new Exception("Property getter does not exist."); 55 | } 56 | return DispatchCallAsync(methodInfo, null); 57 | } 58 | else 59 | { 60 | // set call 61 | var propertyInfo = (PropertyInfo)memberExpression.Member; 62 | var methodInfo = propertyInfo.SetMethod; 63 | if (methodInfo == null) 64 | { 65 | throw new Exception("Property setter does not exist."); 66 | } 67 | return DispatchCallAsync(methodInfo, argsExt); 68 | } 69 | } 70 | else 71 | { 72 | throw new Exception($"Unsupported dispatch call: {expr.GetType().Name}"); 73 | } 74 | } 75 | // Static 76 | // Method Calls and Property Getters 77 | // Action 78 | /// 79 | /// Asynchronously call a method 80 | /// 81 | /// 82 | /// 83 | public async Task Run(Expression expr) => await CallExpression(expr.Body); 84 | // Func 85 | /// 86 | /// Asynchronously call a method 87 | /// 88 | /// 89 | /// 90 | public async Task Run(Expression> expr) => await CallExpression(expr.Body); 91 | // Func 92 | /// 93 | /// Asynchronously call a method 94 | /// 95 | /// 96 | /// 97 | public async Task Run(Expression> expr) => await CallExpression(expr.Body); 98 | // Func<...,TResult> 99 | /// 100 | /// Asynchronously call a method 101 | /// 102 | /// 103 | /// 104 | /// 105 | public async Task Run(Expression> expr) => (TResult)await CallExpression(expr.Body); 106 | // Func<...,Task> 107 | /// 108 | /// Asynchronously call a method 109 | /// 110 | /// 111 | /// 112 | /// 113 | public async Task Run(Expression>> expr) => (TResult)await CallExpression(expr.Body); 114 | // Func<...,ValueTask> 115 | /// 116 | /// Asynchronously call a method 117 | /// 118 | /// 119 | /// 120 | /// 121 | public async Task Run(Expression>> expr) => (TResult)await CallExpression(expr.Body); 122 | // Property set 123 | /// 124 | /// Set property value
125 | /// Ex
126 | /// await caller.Set&let;AsyncCallDispatcherTest, string>(s => s.IITestProp1, "Yay!"); 127 | ///
128 | /// 129 | /// 130 | /// 131 | /// 132 | public async Task Set(Expression> expr, TProperty value) => await CallExpression(expr.Body, new object[] { value }); 133 | // Instance 134 | // Method Calls and Property Getters 135 | // Action 136 | /// 137 | /// Asynchronously call a method 138 | /// 139 | /// 140 | /// 141 | /// 142 | public async Task Run(Expression> expr) => await CallExpression(expr.Body); 143 | // Func 144 | /// 145 | /// Asynchronously call a method 146 | /// 147 | /// 148 | /// 149 | /// 150 | public async Task Run(Expression> expr) => await CallExpression(expr.Body); 151 | // Func 152 | /// 153 | /// Asynchronously call a method 154 | /// 155 | /// 156 | /// 157 | /// 158 | public async Task Run(Expression> expr) => await CallExpression(expr.Body); 159 | // Func<...,TResult> 160 | /// 161 | /// Asynchronously call a method 162 | /// 163 | /// 164 | /// 165 | /// 166 | /// 167 | public async Task Run(Expression> expr) => (TResult)await CallExpression(expr.Body); 168 | // Func<...,Task> 169 | /// 170 | /// Asynchronously call a method 171 | /// 172 | /// 173 | /// 174 | /// 175 | /// 176 | public async Task Run(Expression>> expr) => (TResult)await CallExpression(expr.Body); 177 | // Func<...,ValueTask> 178 | /// 179 | /// Asynchronously call a method 180 | /// 181 | /// 182 | /// 183 | /// 184 | /// 185 | public async Task Run(Expression>> expr) => (TResult)await CallExpression(expr.Body); 186 | // Property set 187 | /// 188 | /// Set property value
189 | /// Ex
190 | /// await caller.Set&let;AsyncCallDispatcherTest, string>(s => s.IITestProp1, "Yay!"); 191 | ///
192 | /// 193 | /// 194 | /// 195 | /// 196 | /// 197 | public async Task Set(Expression> expr, TProperty value) => await CallExpression(expr.Body, new object[] { value }); 198 | #endregion 199 | } 200 | } 201 | --------------------------------------------------------------------------------