@@ -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