Skip to content

Commit 5d373d6

Browse files
committed
Added support for creating new window AppInstances using WebWorkerService.OpenWindow.
1 parent c510bf7 commit 5d373d6

4 files changed

Lines changed: 181 additions & 12 deletions

File tree

SpawnDev.BlazorJS.WebWorkers.Demo/Pages/Counter.razor

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
<p role="status">Current count: @currentCount</p>
1616

17-
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
17+
<button class="btn btn-primary" @onclick="IncrementCount">Increment Count</button>
18+
19+
<button class="btn btn-primary" @onclick="OpenNewWindow">Open New Window</button>
1820

1921
@code {
2022
[Inject]
@@ -24,6 +26,12 @@
2426
// holds the running instance of this component, if one
2527
static Counter? instance = null;
2628

29+
async Task OpenNewWindow()
30+
{
31+
var window = await WebWorkerService.OpenWindow();
32+
var nmt = true;
33+
}
34+
2735
// this static method can be called by other running instances
2836
static void SetInstanceCount(int count)
2937
{

SpawnDev.BlazorJS.WebWorkers.Demo/Program.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
var host = await builder.Build().StartBackgroundServices();
1717

1818

19-
var arg = new SharedCancellationTokenSource();
20-
var token = arg.Token;
19+
//var arg = new SharedCancellationTokenSource();
20+
//var token = arg.Token;
2121

22-
JS.Set("_token1", token);
23-
var token12 = JS.Get<JSObject>("_token1");
24-
var keys = token12.JSRef?.Keys();
25-
var token1 = JS.Get<SharedCancellationToken>("_token1");
22+
//JS.Set("_token1", token);
23+
//var token12 = JS.Get<JSObject>("_token1");
24+
//var keys = token12.JSRef?.Keys();
25+
//var token1 = JS.Get<SharedCancellationToken>("_token1");
2626

2727
await host.BlazorJSRunAsync();

SpawnDev.BlazorJS.WebWorkers/AppInstanceInfo.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,46 @@
22

33
namespace SpawnDev.BlazorJS.WebWorkers
44
{
5+
///// <summary>
6+
///// Configuration data with information a new instance will use on startup
7+
///// </summary>
8+
//public class InstanceConfiguration
9+
//{
10+
// /// <summary>
11+
// /// The configuration id of this instance
12+
// /// </summary>
13+
// public string ConfigId { get; set; }
14+
// /// <summary>
15+
// /// The InstanceId of the instance that created this config
16+
// /// </summary>
17+
// public string OwnerId { get; set; }
18+
// /// <summary>
19+
// /// When this config was created
20+
// /// </summary>
21+
// public DateTimeOffset Created { get; set; }
22+
// /// <summary>
23+
// /// The path to load
24+
// /// </summary>
25+
// public string? Url { get; set; }
26+
//}
27+
/// <summary>
28+
/// Information about an instance of a Blazor app
29+
/// </summary>
530
public class AppInstanceInfo
631
{
732
/// <summary>
833
/// The instance's instanceId, a unique and randomly generated Guid string created during BlazorJSRuntime startup
934
/// </summary>
1035
public string InstanceId { get; set; }
1136
/// <summary>
37+
/// The InstanceId of the instance that created this instance (if one)
38+
/// </summary>
39+
public string? OwnerId { get; set; }
40+
/// <summary>
41+
/// The id set by the owner (OwnerId) when they created this instance
42+
/// </summary>
43+
public string? ChildId { get; set; }
44+
/// <summary>
1245
/// The instance's location at startup
1346
/// </summary>
1447
public string Url { get; set; }

SpawnDev.BlazorJS.WebWorkers/WebWorkerService.cs

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace SpawnDev.BlazorJS.WebWorkers
1515
/// </summary>
1616
public class WebWorkerService : IDisposable, IAsyncBackgroundService
1717
{
18+
const string instanceOwnerIdKey = "instanceOwnerIdKey";
19+
const string childIdKey = "tempIdKey";
1820
/// <summary>
1921
/// Completes successfully when asynchronous initialization has completed
2022
/// </summary>
@@ -148,16 +150,19 @@ public class WebWorkerService : IDisposable, IAsyncBackgroundService
148150
/// IWebAssemblyServices singleton
149151
/// </summary>
150152
public IWebAssemblyServices WebAssemblyServices { get; init; }
153+
154+
NavigationManager NavigationManager;
151155
/// <summary>
152156
/// Creates a new instance of WebWorkerService
153157
/// </summary>
154158
/// <param name="webAssemblyServices"></param>
155159
/// <param name="js"></param>
156-
public WebWorkerService(IWebAssemblyServices webAssemblyServices, BlazorJSRuntime js)
160+
public WebWorkerService(IWebAssemblyServices webAssemblyServices, BlazorJSRuntime js, NavigationManager navigationManager)
157161
{
158162
JS = js;
159163
GlobalScope = JS.GlobalScope;
160164
InstanceId = JS.InstanceId;
165+
NavigationManager = navigationManager;
161166
WebAssemblyServices = webAssemblyServices;
162167
ServiceProvider = WebAssemblyServices.Services;
163168
ServiceDescriptors = WebAssemblyServices.Descriptors;
@@ -168,14 +173,29 @@ public WebWorkerService(IWebAssemblyServices webAssemblyServices, BlazorJSRuntim
168173
ServiceWorkerSupported = !JS.IsUndefined("ServiceWorkerRegistration");
169174
AppBaseUri = JS.Get<string>("document.baseURI");
170175
var locationHref = JS.Get<string>("location.href");
176+
var locationUri = new Uri(locationHref);
171177
var workerScriptUri = new Uri(new Uri(AppBaseUri), WebWorkerJSScript);
172178
WebWorkerJSScript = workerScriptUri.ToString();
173179
Locks = JS.Get<LockManager>("navigator.locks");
174180
LockManagerSupported = Locks != null;
175-
var queryParams = HttpUtility.ParseQueryString(new Uri(locationHref).Query);
181+
var queryParams = HttpUtility.ParseQueryString(locationUri.Query);
176182
var isTaskPoolWorker = queryParams["taskPool"] == "1" && JS.IsScope(GlobalScope.DedicatedAndSharedWorkers);
183+
var instanceOwnerId = queryParams[instanceOwnerIdKey];
184+
var instanceChildId = queryParams[childIdKey];
185+
if (!string.IsNullOrEmpty(instanceOwnerId) && !string.IsNullOrEmpty(instanceChildId))
186+
{
187+
queryParams.Remove(childIdKey);
188+
queryParams.Remove(instanceOwnerIdKey);
189+
locationHref = locationUri.GetLeftPart(UriPartial.Path) + (queryParams.Count == 0 ? "" : "?" + queryParams.ToString());
190+
locationUri = new Uri(locationHref);
191+
var newPath = NavigationManager.ToBaseRelativePath(locationHref);
192+
// remove the instanceOwnerIdKey attribute from the url
193+
NavigationManager.NavigateTo(newPath, false, true);
194+
}
177195
Info = new AppInstanceInfo
178196
{
197+
OwnerId = instanceOwnerId,
198+
ChildId = instanceChildId,
179199
InstanceId = InstanceId,
180200
Scope = GlobalScope,
181201
Name = GetName(),
@@ -208,7 +228,8 @@ public WebWorkerService(IWebAssemblyServices webAssemblyServices, BlazorJSRuntim
208228
if (IServiceCollectionExtensions.ServiceWorkerConfig != null)
209229
{
210230
ServiceWorkerConfig = IServiceCollectionExtensions.ServiceWorkerConfig;
211-
};
231+
}
232+
;
212233
if (ServiceWorkerConfig == null) ServiceWorkerConfig = new ServiceWorkerConfig { Register = ServiceWorkerStartupRegistration.None };
213234
if (string.IsNullOrEmpty(ServiceWorkerConfig.ScriptURL))
214235
{
@@ -457,12 +478,27 @@ private bool InstanceFound(AppInstanceInfo instanceInfo, bool fireChangedEvent)
457478
{
458479
// TODO
459480
// fallback termination detection
481+
// most relatively recent browsers support locks.
482+
// if needed, can test with old version of Safari on macOS as they do not support locks
483+
}
484+
}
485+
if (instance.Info.OwnerId == InstanceId && !string.IsNullOrEmpty(instance.Info.ChildId))
486+
{
487+
// this instance created 'instance'
488+
if (OpenWindowWaiters.TryGetValue(instance.Info.ChildId, out var openWindowWaiters))
489+
{
490+
OpenWindowWaiters.Remove(instance.Info.ChildId);
491+
openWindowWaiters(instance);
460492
}
461493
}
462494
OnInstanceFound?.Invoke(instance);
463495
if (fireChangedEvent) OnInstancesChanged?.Invoke();
464496
return true;
465497
}
498+
499+
Dictionary<string, Action<AppInstance>> OpenWindowWaiters = new Dictionary<string, Action<AppInstance>>();
500+
501+
466502
/// <summary>
467503
/// Returns information about running instances using locks to obtain it
468504
/// </summary>
@@ -541,6 +577,12 @@ private async Task InitAsync()
541577
await UnregisterServiceWorker();
542578
break;
543579
}
580+
if (!string.IsNullOrEmpty(Info.OwnerId) && !string.IsNullOrEmpty(Info.ChildId))
581+
{
582+
// this window is owned by another instance
583+
// todo I guess nothing really, it knows when this winow is started and is ready
584+
// could load Info.Url if it is set
585+
}
544586
}
545587
else if (JS.GlobalThis is DedicatedWorkerGlobalScope workerGlobalScope)
546588
{
@@ -726,7 +768,8 @@ public async Task<bool> UnregisterServiceWorker()
726768
return webWorker;
727769
}
728770
/// <summary>
729-
/// Creates a new a WebWorker instance and returns.
771+
/// Creates a new a WebWorker instance and returns.<br/>
772+
/// The property WhenReady will complete when Blazor is loaded and ready in the worker
730773
/// </summary>
731774
/// <returns></returns>
732775
public WebWorker? GetWebWorkerSync(Dictionary<string, string>? queryParams = null)
@@ -776,7 +819,8 @@ public async Task<bool> UnregisterServiceWorker()
776819
return sharedWebWorker;
777820
}
778821
/// <summary>
779-
/// Returns a new SharedWebWorker instance. If a SharedWorker already existed by this name SharedWebWorker will be connected to that instance.
822+
/// Returns a new SharedWebWorker instance. If a SharedWorker already existed by this name SharedWebWorker will be connected to that instance.<br/>
823+
/// The property WhenReady will complete when Blazor is loaded and ready in the worker
780824
/// </summary>
781825
/// <param name="sharedWorkerName"></param>
782826
/// <returns></returns>
@@ -818,5 +862,89 @@ private void AddIncomingPort(MessagePort incomingPort)
818862
SharedWorkerIncomingConnections.Add(incomingHandler);
819863
incomingHandler.SendReadyFlag();
820864
}
865+
/// <summary>
866+
/// Tries to open a new window instance with the given url (must be a path in this app)<br/>
867+
/// </summary>
868+
/// <param name="path">Optional relative path to your app's base path.</param>
869+
/// <param name="target">
870+
/// A string, without whitespace, specifying the name of the browsing context the resource is being loaded into. If the name doesn't identify an existing context, a new context is created and given the specified name. The special target keywords, _self, _blank (default), _parent, _top, and _unfencedTop can also be used. _unfencedTop is only relevant to fenced frames.<br/>
871+
/// This parameter is ignored if not running in a window scope
872+
/// </param>
873+
/// <param name="windowFeatures">
874+
/// A string containing a comma-separated list of window features in the form name=value. Boolean values can be set to true using one of: name, name=yes, name=true, or name=n where n is any non-zero integer. These features include options such as the window's default size and position, whether or not to open a minimal popup window, and so forth.<br/>
875+
/// This parameter is ignored if not running in a window scope
876+
/// </param>
877+
/// <param name="cancellationToken">
878+
/// Can be used to cancel waiting for the window opening<br/>
879+
/// The default timeout is 20 seconds
880+
/// </param>
881+
/// <returns></returns>
882+
public async Task<AppInstance?> OpenWindow(string? path = null, string? target = null, string? windowFeatures = null, CancellationToken cancellationToken = default)
883+
{
884+
AppInstance? ret = null;
885+
if (!JS.IsWindow && !JS.IsServiceWorkerGlobalScope)
886+
{
887+
// only works in window and service work scopes
888+
return ret;
889+
}
890+
var childId = Guid.NewGuid().ToString();
891+
var queryParams = new Dictionary<string, string>();
892+
queryParams[instanceOwnerIdKey] = InstanceId;
893+
queryParams[childIdKey] = childId;
894+
var pathUrl = new Uri(new Uri(AppBaseUri), path ?? "");
895+
var newWindowUrl = pathUrl.ToString(); ;
896+
newWindowUrl += (newWindowUrl.Contains("?") ? "&" : "?") + string.Join('&', queryParams.Select(o => $"{Uri.EscapeDataString(o.Key)}={Uri.EscapeDataString(o.Value)}"));
897+
if (JS.WindowThis != null)
898+
{
899+
// running in a window
900+
// use window.open
901+
using var window = JS.WindowThis!.Open(newWindowUrl);
902+
}
903+
else if (JS.ServiceWorkerThis != null)
904+
{
905+
// running in a service worker
906+
// use client.openWindow
907+
// this only works in specific circumstances
908+
// typically used inside a notifiction click event in a service worker
909+
using var window = JS.ServiceWorkerThis!.Clients.OpenWindow(newWindowUrl);
910+
}
911+
var tcs = new TaskCompletionSource<AppInstance?>();
912+
var task = tcs.Task;
913+
// set a deault timeout if one wasn't set
914+
CancellationTokenSource? cts = null;
915+
if (cancellationToken == default)
916+
{
917+
cts = new CancellationTokenSource(20000);
918+
cancellationToken = cts.Token;
919+
}
920+
cancellationToken.Register(() =>
921+
{
922+
if (task.IsCompleted) return;
923+
if (OpenWindowWaiters.ContainsKey(childId))
924+
{
925+
OpenWindowWaiters.Remove(childId);
926+
}
927+
tcs.TrySetException(new Exception("Timedout"));
928+
});
929+
var newInstanceHandler = new Action<AppInstance>((appInstance) =>
930+
{
931+
if (task.IsCompleted) return;
932+
if (OpenWindowWaiters.ContainsKey(childId))
933+
{
934+
OpenWindowWaiters.Remove(childId);
935+
}
936+
tcs.TrySetResult(appInstance);
937+
});
938+
OpenWindowWaiters.Add(childId, newInstanceHandler);
939+
try
940+
{
941+
ret = await task;
942+
}
943+
finally
944+
{
945+
cts?.Dispose();
946+
}
947+
return ret;
948+
}
821949
}
822950
}

0 commit comments

Comments
 (0)