From 212bfd4d9956c9be720144239e2b2257ca2cab8f Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 19 Oct 2017 08:25:29 -0500 Subject: [PATCH] refactor(ReactInstanceManager): Support awaitable ReactContext Currently, the ReactInstanceManager kicks off the process to create the ReactContext in the background and its not awaitable. This ensures a single asynchronous thread is available from start to finish for ReactContext creation. Fixes #1448 --- ReactWindows/ReactNative.Net46/ReactPage.cs | 5 +- .../Bridge/JavaScriptBundleLoader.cs | 16 +- .../Bridge/ReactInstance.cs | 5 +- .../DevSupport/DevServerHelper.cs | 7 +- .../DevSupport/DevSupportManager.cs | 311 +++++++++++------- .../DevSupport/DisabledDevSupportManager.cs | 12 +- .../DevSupport/IDevSupportManager.cs | 15 +- .../IReactInstanceDevCommandsHandler.cs | 23 +- .../ReactContextInitializedEventArgs.cs | 26 -- .../ReactInstanceManager.cs | 234 +++++++------ .../ReactInstanceManagerExtensions.cs | 30 ++ .../ReactNative.Shared.projitems | 2 +- .../ReactNative.Shared/ReactRootView.cs | 14 +- .../Internal/DispatcherHelpers.cs | 21 ++ .../ReactInstanceManagerTests.cs | 179 +++++++--- .../UIManager/Events/EventDispatcherTests.cs | 2 +- ReactWindows/ReactNative/ReactPage.cs | 5 +- .../ReactNative/ReactRootViewExtensions.cs | 5 +- 18 files changed, 556 insertions(+), 356 deletions(-) delete mode 100644 ReactWindows/ReactNative.Shared/ReactContextInitializedEventArgs.cs create mode 100644 ReactWindows/ReactNative.Shared/ReactInstanceManagerExtensions.cs diff --git a/ReactWindows/ReactNative.Net46/ReactPage.cs b/ReactWindows/ReactNative.Net46/ReactPage.cs index 753327c6d70..aaf9171a68d 100644 --- a/ReactWindows/ReactNative.Net46/ReactPage.cs +++ b/ReactWindows/ReactNative.Net46/ReactPage.cs @@ -4,6 +4,7 @@ using ReactNative.Modules.Core; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -181,7 +182,7 @@ protected virtual ReactRootView CreateRootView() /// /// /// - private void OnAcceleratorKeyActivated(object sender, KeyEventArgs e) + private async void OnAcceleratorKeyActivated(object sender, KeyEventArgs e) { if (ReactInstanceManager.DevSupportManager.IsEnabled) { @@ -196,7 +197,7 @@ private void OnAcceleratorKeyActivated(object sender, KeyEventArgs e) // Ctrl+R if (isCtrlKeyDown && e.Key == Key.R) { - ReactInstanceManager.DevSupportManager.HandleReloadJavaScript(); + await ReactInstanceManager.DevSupportManager.CreateReactContextFromPackagerAsync(CancellationToken.None); } } diff --git a/ReactWindows/ReactNative.Shared/Bridge/JavaScriptBundleLoader.cs b/ReactWindows/ReactNative.Shared/Bridge/JavaScriptBundleLoader.cs index f4a22d6a7e0..342a5261f99 100644 --- a/ReactWindows/ReactNative.Shared/Bridge/JavaScriptBundleLoader.cs +++ b/ReactWindows/ReactNative.Shared/Bridge/JavaScriptBundleLoader.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; #if WINDOWS_UWP using Windows.Storage; @@ -27,8 +28,9 @@ public abstract class JavaScriptBundleLoader /// Initializes the JavaScript bundle loader, typically making an /// asynchronous call to cache the bundle in memory. /// + /// A token to cancel the initialization. /// A task to await initialization. - public abstract Task InitializeAsync(); + public abstract Task InitializeAsync(CancellationToken token); /// /// Loads the bundle into a JavaScript executor. @@ -89,13 +91,13 @@ public override string SourceUrl } #if WINDOWS_UWP - public override async Task InitializeAsync() + public override async Task InitializeAsync(CancellationToken token) { - var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(SourceUrl)).AsTask().ConfigureAwait(false); + var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(SourceUrl)).AsTask(token).ConfigureAwait(false); _script = storageFile.Path; } #else - public override Task InitializeAsync() + public override Task InitializeAsync(CancellationToken token) { var assembly = Assembly.GetAssembly(typeof(JavaScriptBundleLoader)); var assemblyName = assembly.GetName(); @@ -134,11 +136,11 @@ public CachedJavaScriptBundleLoader(string sourceUrl, string cachedFileLocation) public override string SourceUrl { get; } - public override async Task InitializeAsync() + public override async Task InitializeAsync(CancellationToken token) { #if WINDOWS_UWP var localFolder = ApplicationData.Current.LocalFolder; - var storageFile = await localFolder.GetFileAsync(_cachedFileLocation).AsTask().ConfigureAwait(false); + var storageFile = await localFolder.GetFileAsync(_cachedFileLocation).AsTask(token).ConfigureAwait(false); #else var localFolder = FileSystem.Current.LocalStorage; var storageFile = await localFolder.GetFileAsync(_cachedFileLocation).ConfigureAwait(false); @@ -170,7 +172,7 @@ public override string SourceUrl get; } - public override Task InitializeAsync() + public override Task InitializeAsync(CancellationToken token) { return Task.CompletedTask; } diff --git a/ReactWindows/ReactNative.Shared/Bridge/ReactInstance.cs b/ReactWindows/ReactNative.Shared/Bridge/ReactInstance.cs index 25eb8ab7ea5..8a8008b13ab 100644 --- a/ReactWindows/ReactNative.Shared/Bridge/ReactInstance.cs +++ b/ReactWindows/ReactNative.Shared/Bridge/ReactInstance.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading; using System.Threading.Tasks; using static System.FormattableString; @@ -81,9 +82,9 @@ public void Initialize() QueueConfiguration.NativeModulesQueue.Dispatch(_registry.NotifyReactInstanceInitialize); } - public async Task InitializeBridgeAsync() + public async Task InitializeBridgeAsync(CancellationToken token) { - await _bundleLoader.InitializeAsync().ConfigureAwait(false); + await _bundleLoader.InitializeAsync(token).ConfigureAwait(false); using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "initializeBridge").Start()) { diff --git a/ReactWindows/ReactNative.Shared/DevSupport/DevServerHelper.cs b/ReactWindows/ReactNative.Shared/DevSupport/DevServerHelper.cs index 6cb8e566b13..6c070ac65c4 100644 --- a/ReactWindows/ReactNative.Shared/DevSupport/DevServerHelper.cs +++ b/ReactWindows/ReactNative.Shared/DevSupport/DevServerHelper.cs @@ -1,4 +1,4 @@ -using ReactNative.Bridge; +using ReactNative.Bridge; using System; using System.Globalization; using System.IO; @@ -139,13 +139,14 @@ public async Task DownloadBundleFromUrlAsync(string jsModulePath, Stream outputS /// /// Checks if the packager is running. /// + /// A token to cancel the request. /// A task to await the packager status. - public async Task IsPackagerRunningAsync() + public async Task IsPackagerRunningAsync(CancellationToken token) { var statusUrl = CreatePackagerStatusUrl(DebugServerHost); try { - using (var response = await _client.GetAsync(statusUrl).ConfigureAwait(false)) + using (var response = await _client.GetAsync(statusUrl, token).ConfigureAwait(false)) { if (!response.IsSuccessStatusCode) { diff --git a/ReactWindows/ReactNative.Shared/DevSupport/DevSupportManager.cs b/ReactWindows/ReactNative.Shared/DevSupport/DevSupportManager.cs index c326cf88a9b..76310140f4c 100644 --- a/ReactWindows/ReactNative.Shared/DevSupport/DevSupportManager.cs +++ b/ReactWindows/ReactNative.Shared/DevSupport/DevSupportManager.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Common; using ReactNative.Modules.Core; @@ -40,7 +40,7 @@ class DevSupportManager : IDevSupportManager, IDisposable private bool _isDevSupportEnabled = true; - private ReactContext _currentContext; + private ReactContext _currentReactContext; private RedBoxDialog _redBoxDialog; private Action _dismissRedBoxDialog; private bool _redBoxDialogOpen; @@ -155,17 +155,17 @@ public void HandleException(Exception exception) } } - public async Task HasUpToDateBundleInCacheAsync() + public async Task HasUpToDateBundleInCacheAsync(CancellationToken token) { if (_isDevSupportEnabled) { #if WINDOWS_UWP var lastUpdateTime = Windows.ApplicationModel.Package.Current.InstalledDate; var localFolder = ApplicationData.Current.LocalFolder; - var bundleItem = await localFolder.TryGetItemAsync(JSBundleFileName); + var bundleItem = await localFolder.TryGetItemAsync(JSBundleFileName).AsTask(token); if (bundleItem != null) { - var bundleProperties = await bundleItem.GetBasicPropertiesAsync(); + var bundleProperties = await bundleItem.GetBasicPropertiesAsync().AsTask(token); return bundleProperties.DateModified > lastUpdateTime; } #else @@ -272,7 +272,9 @@ public void ShowDevOptionsDialog() () => { _devSettings.IsElementInspectorEnabled = !_devSettings.IsElementInspectorEnabled; - _reactInstanceCommandsHandler.ToggleElementInspector(); + _currentReactContext? + .GetJavaScriptModule() + .emit("toggleElementInspector", null); }), }; @@ -341,73 +343,35 @@ public void OnNewReactContextCreated(ReactContext context) public void OnReactContextDestroyed(ReactContext context) { - if (context == _currentContext) + if (context == _currentReactContext) { ResetCurrentContext(null); } } - public Task IsPackagerRunningAsync() + public Task IsPackagerRunningAsync(CancellationToken token) { - return _devServerHelper.IsPackagerRunningAsync(); + return _devServerHelper.IsPackagerRunningAsync(token); } - public async void HandleReloadJavaScript() + public Task CreateReactContextFromPackagerAsync(CancellationToken token) { DispatcherHelpers.AssertOnDispatcher(); HideRedboxDialog(); HideDevOptionsDialog(); - Action cancel; - CancellationToken token; - - if (IsProgressDialogEnabled) - { - var message = !IsRemoteDebuggingEnabled - ? "Fetching JavaScript bundle." - : "Connecting to remote debugger."; - - ProgressDialog progressDialog = new ProgressDialog("Please wait...", message); - -#if WINDOWS_UWP - var dialogOperation = progressDialog.ShowAsync(); - cancel = dialogOperation.Cancel; -#else - if (Application.Current != null && Application.Current.MainWindow != null && Application.Current.MainWindow.IsLoaded) - { - progressDialog.Owner = Application.Current.MainWindow; - } - else - { - progressDialog.Topmost = true; - progressDialog.WindowStartupLocation = WindowStartupLocation.CenterScreen; - } - - cancel = progressDialog.Close; - progressDialog.Show(); -#endif - token = progressDialog.Token; - } - else - { - // Progress not enabled - provide empty implementations - cancel = () => { }; - token = default(CancellationToken); - } - if (IsRemoteDebuggingEnabled) { - await ReloadJavaScriptInProxyMode(cancel, token).ConfigureAwait(false); + return ReloadJavaScriptInProxyModeAsync(token); } else if (_shouldLoadFromPackagerServer) { - await ReloadJavaScriptFromServerAsync(cancel, token).ConfigureAwait(false); + return ReloadJavaScriptFromServerAsync(token); } else { - await ReloadJavaScriptFromFileAsync(token); - cancel(); + return _reactInstanceCommandsHandler.CreateReactContextFromBundleAsync(token); } } @@ -450,14 +414,56 @@ public void Dispose() _devServerHelper.Dispose(); } + private async void HandleReloadJavaScript() + { + await CreateReactContextFromPackagerAsync(CancellationToken.None); + } + + private ProgressDialog CreateProgressDialog(string message) + { + if (IsProgressDialogEnabled) + { + var progressDialog = new ProgressDialog("Please wait...", message); + +#if !WINDOWS_UWP + if (Application.Current != null && Application.Current.MainWindow != null && Application.Current.MainWindow.IsLoaded) + { + progressDialog.Owner = Application.Current.MainWindow; + } + else + { + progressDialog.Topmost = true; + progressDialog.WindowStartupLocation = WindowStartupLocation.CenterScreen; + } +#endif + + return progressDialog; + } + else + { + return null; + } + } + + private Action ShowProgressDialog(ProgressDialog progressDialog) + { +#if WINDOWS_UWP + var operation = progressDialog.ShowAsync(); + return operation.Cancel; +#else + progressDialog.Show(); + return progressDialog.Close; +#endif + } + private void ResetCurrentContext(ReactContext context) { - if (_currentContext == context) + if (_currentReactContext == context) { return; } - _currentContext = context; + _currentReactContext = context; if (_devSettings.IsHotModuleReplacementEnabled && context != null) { @@ -514,72 +520,38 @@ private void ShowNewError(string message, IStackFrame[] stack, int errorCookie) }); } - private async Task ReloadJavaScriptInProxyMode(Action dismissProgress, CancellationToken token) - { - try - { - await _devServerHelper.LaunchDevToolsAsync(token).ConfigureAwait(true); - var factory = new Func(() => - { - var executor = new WebSocketJavaScriptExecutor(); - executor.ConnectAsync(_devServerHelper.WebsocketProxyUrl, token).Wait(); - return executor; - }); - - _reactInstanceCommandsHandler.OnReloadWithJavaScriptDebugger(factory); - dismissProgress(); - } - catch (DebugServerException ex) - { - dismissProgress(); - ShowNewNativeError(ex.Message, ex); - } - catch (Exception ex) - { - dismissProgress(); - ShowNewNativeError( - "Unable to download JS bundle. Did you forget to " + - "start the development server or connect your device?", - ex); - } - } - - private async Task ReloadJavaScriptFromServerAsync(Action dismissProgress, CancellationToken token) + private async Task DownloadBundleFromPackagerAsync(CancellationToken token) { var moved = false; #if WINDOWS_UWP - var temporaryFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync(JSBundleFileName, CreationCollisionOption.GenerateUniqueName); + var temporaryFile = await ApplicationData.Current.TemporaryFolder.CreateFileAsync( + JSBundleFileName, + CreationCollisionOption.GenerateUniqueName) + .AsTask(token).ConfigureAwait(false); + try { - using (var stream = await temporaryFile.OpenStreamForWriteAsync()) + using (var stream = await temporaryFile.OpenStreamForWriteAsync().ConfigureAwait(false)) { - await _devServerHelper.DownloadBundleFromUrlAsync(_jsAppBundleName, stream, token); + await _devServerHelper.DownloadBundleFromUrlAsync(_jsAppBundleName, stream, token).ConfigureAwait(false); } - await temporaryFile.MoveAsync(ApplicationData.Current.LocalFolder, JSBundleFileName, NameCollisionOption.ReplaceExisting); + // CancellationToken not used because we don't want to + // interrupt the move operation (or else the delete operation + // below may throw a FileNotFoundException + await temporaryFile.MoveAsync( + ApplicationData.Current.LocalFolder, + JSBundleFileName, + NameCollisionOption.ReplaceExisting).AsTask().ConfigureAwait(false); moved = true; - - dismissProgress(); - _reactInstanceCommandsHandler.OnJavaScriptBundleLoadedFromServer(); - } - catch (DebugServerException ex) - { - dismissProgress(); - ShowNewNativeError(ex.Message, ex); - } - catch (Exception ex) - { - dismissProgress(); - ShowNewNativeError( - "Unable to download JS bundle. Did you forget to " + - "start the development server or connect your device?", - ex); } finally { if (!moved) { - await temporaryFile.DeleteAsync(); + // CancellationToken not used because we should always + // clean up the temporary file regardless of cancellation. + await temporaryFile.DeleteAsync().AsTask().ConfigureAwait(false); } } #else @@ -588,51 +560,138 @@ private async Task ReloadJavaScriptFromServerAsync(Action dismissProgress, Cance { using (var stream = new FileStream(temporaryFilePath, FileMode.Create)) { - await _devServerHelper.DownloadBundleFromUrlAsync(_jsAppBundleName, stream, token); + await _devServerHelper.DownloadBundleFromUrlAsync(_jsAppBundleName, stream, token).ConfigureAwait(false); } - var temporaryFile = await FileSystem.Current.GetFileFromPathAsync(temporaryFilePath, token); + var temporaryFile = await FileSystem.Current.GetFileFromPathAsync(temporaryFilePath, token).ConfigureAwait(false); var localStorage = FileSystem.Current.LocalStorage; string newPath = PortablePath.Combine(localStorage.Path, JSBundleFileName); - await temporaryFile.MoveAsync(newPath, NameCollisionOption.ReplaceExisting, token); + // CancellationToken not used because we don't want to + // interrupt the move operation (or else the delete operation + // below may throw a FileNotFoundException + await temporaryFile.MoveAsync(newPath, NameCollisionOption.ReplaceExisting).ConfigureAwait(false); moved = true; + } + finally + { + if (!moved) + { + var temporaryFile = await FileSystem.Current.GetFileFromPathAsync(temporaryFilePath).ConfigureAwait(false); - dismissProgress(); - _reactInstanceCommandsHandler.OnJavaScriptBundleLoadedFromServer(); + if (temporaryFile != null) + { + // CancellationToken not used because we should always + // clean up the temporary file regardless of cancellation. + await temporaryFile.DeleteAsync().ConfigureAwait(false); + } + } + } +#endif + } + + private async Task ReloadJavaScriptInProxyModeAsync(CancellationToken token) + { + var webSocketExecutor = default(WebSocketJavaScriptExecutor); + try + { + var progressDialog = CreateProgressDialog("Connecting to remote debugger."); + var dismissed = await RunWithProgressAsync( + async progressToken => + { + await _devServerHelper.LaunchDevToolsAsync(progressToken).ConfigureAwait(false); + webSocketExecutor = new WebSocketJavaScriptExecutor(); + await webSocketExecutor.ConnectAsync(_devServerHelper.WebsocketProxyUrl, progressToken).ConfigureAwait(false); + }, + progressDialog, + token); + } + catch (OperationCanceledException) + when (token.IsCancellationRequested) + { + token.ThrowIfCancellationRequested(); } catch (DebugServerException ex) { - dismissProgress(); ShowNewNativeError(ex.Message, ex); + return null; } catch (Exception ex) { - dismissProgress(); ShowNewNativeError( - "Unable to download JS bundle. Did you forget to " + - "start the development server or connect your device?", + "Unable to connect to remote debugger. Did you forget " + + "to start the development server or connect your device?", ex); + return null; } - finally + + return await _reactInstanceCommandsHandler.CreateReactContextWithRemoteDebuggerAsync(() => webSocketExecutor, token); + } + + private async Task ReloadJavaScriptFromServerAsync(CancellationToken token) + { + try { - if (!moved) + var progressDialog = CreateProgressDialog("Fetching JavaScript bundle."); + var dismissed = await RunWithProgressAsync( + progressToken => DownloadBundleFromPackagerAsync(progressToken), + progressDialog, + token); + if (dismissed) { - var temporaryFile = await FileSystem.Current.GetFileFromPathAsync(temporaryFilePath, token).ConfigureAwait(false); - - if (temporaryFile != null) - { - await temporaryFile.DeleteAsync(token).ConfigureAwait(false); - } + return null; } } -#endif + catch (OperationCanceledException) + when (token.IsCancellationRequested) + { + token.ThrowIfCancellationRequested(); + } + catch (DebugServerException ex) + { + ShowNewNativeError(ex.Message, ex); + return null; + } + catch (Exception ex) + { + ShowNewNativeError( + "Unable to download JS bundle. Did you forget to start " + + "the development server or connect your device?", + ex); + return null; + } + + return await _reactInstanceCommandsHandler.CreateReactContextFromCachedPackagerBundleAsync(token); } - private Task ReloadJavaScriptFromFileAsync(CancellationToken token) + private async Task RunWithProgressAsync(Func asyncAction, ProgressDialog progressDialog, CancellationToken token) { - _reactInstanceCommandsHandler.OnBundleFileReloadRequest(); - return Task.CompletedTask; + var hideProgress = ShowProgressDialog(progressDialog); + using (var cancellationDisposable = new CancellationDisposable()) + using (token.Register(cancellationDisposable.Dispose)) + using (progressDialog.Token.Register(cancellationDisposable.Dispose)) + { + try + { + await asyncAction(cancellationDisposable.Token); + } + catch (OperationCanceledException) + when (progressDialog.Token.IsCancellationRequested) + { + return true; + } + catch (OperationCanceledException) + { + token.ThrowIfCancellationRequested(); + throw; + } + finally + { + hideProgress(); + } + } + + return false; } #if WINDOWS_UWP diff --git a/ReactWindows/ReactNative.Shared/DevSupport/DisabledDevSupportManager.cs b/ReactWindows/ReactNative.Shared/DevSupport/DisabledDevSupportManager.cs index 7500d3d1e86..9b863ce055e 100644 --- a/ReactWindows/ReactNative.Shared/DevSupport/DisabledDevSupportManager.cs +++ b/ReactWindows/ReactNative.Shared/DevSupport/DisabledDevSupportManager.cs @@ -1,8 +1,9 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Modules.DevSupport; using System; using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; namespace ReactNative.DevSupport @@ -76,7 +77,12 @@ public void HandleReloadJavaScript() { } - public Task HasUpToDateBundleInCacheAsync() + public Task CreateReactContextFromPackagerAsync(CancellationToken token) + { + return Task.FromResult(default(ReactContext)); + } + + public Task HasUpToDateBundleInCacheAsync(CancellationToken token) { return Task.FromResult(false); } @@ -85,7 +91,7 @@ public void HideRedboxDialog() { } - public Task IsPackagerRunningAsync() + public Task IsPackagerRunningAsync(CancellationToken token) { return Task.FromResult(false); } diff --git a/ReactWindows/ReactNative.Shared/DevSupport/IDevSupportManager.cs b/ReactWindows/ReactNative.Shared/DevSupport/IDevSupportManager.cs index d48fc5c3df7..f0b28cf603b 100644 --- a/ReactWindows/ReactNative.Shared/DevSupport/IDevSupportManager.cs +++ b/ReactWindows/ReactNative.Shared/DevSupport/IDevSupportManager.cs @@ -1,7 +1,8 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Modules.DevSupport; using System; +using System.Threading; using System.Threading.Tasks; namespace ReactNative.DevSupport @@ -62,13 +63,16 @@ public interface IDevSupportManager /// /// Handles reloading the JavaScript bundle. /// - void HandleReloadJavaScript(); + /// A token to cancel the operation. + /// A task to await the result. + Task CreateReactContextFromPackagerAsync(CancellationToken token); /// /// Checks if an up-to-date JavaScript bundle is ready. /// + /// A token to cancel the check. /// A task to await the result. - Task HasUpToDateBundleInCacheAsync(); + Task HasUpToDateBundleInCacheAsync(CancellationToken token); /// /// Dismisses the red box exception dialog. @@ -78,10 +82,11 @@ public interface IDevSupportManager /// /// Checks if the packager is running. /// + /// A token to cancel the request. /// /// true if the packager is running, otherwise false. /// - Task IsPackagerRunningAsync(); + Task IsPackagerRunningAsync(CancellationToken token); /// /// Notify when a new React context is created. @@ -128,4 +133,4 @@ public interface IDevSupportManager /// An identifier for the exception. void UpdateJavaScriptError(string title, JArray details, int exceptionId); } -} \ No newline at end of file +} diff --git a/ReactWindows/ReactNative.Shared/DevSupport/IReactInstanceDevCommandsHandler.cs b/ReactWindows/ReactNative.Shared/DevSupport/IReactInstanceDevCommandsHandler.cs index 5e6f25f89c9..b39ee5908c5 100644 --- a/ReactWindows/ReactNative.Shared/DevSupport/IReactInstanceDevCommandsHandler.cs +++ b/ReactWindows/ReactNative.Shared/DevSupport/IReactInstanceDevCommandsHandler.cs @@ -1,5 +1,7 @@ -using ReactNative.Bridge; +using ReactNative.Bridge; using System; +using System.Threading; +using System.Threading.Tasks; namespace ReactNative.DevSupport { @@ -14,13 +16,17 @@ public interface IReactInstanceDevCommandsHandler /// Action to notify the about the /// availability of a new JavaScript bundle downloaded from the server. /// - void OnJavaScriptBundleLoadedFromServer(); + /// A token to cancel the request. + /// A task to await the React context. + Task CreateReactContextFromCachedPackagerBundleAsync(CancellationToken token); /// /// Action triggered when the user requests that the application be /// reloaded from the initially specified bundle file. /// - void OnBundleFileReloadRequest(); + /// A token to cancel the request. + /// A task to await the React context. + Task CreateReactContextFromBundleAsync(CancellationToken token); /// /// Action triggered when the user requests that the application be @@ -29,11 +35,10 @@ public interface IReactInstanceDevCommandsHandler /// /// The JavaScript executor factory. /// - void OnReloadWithJavaScriptDebugger(Func javaScriptExecutorFactory); - - /// - /// Toggles the element inspector. - /// - void ToggleElementInspector(); + /// A token to cancel the request. + /// A task to await the React context. + Task CreateReactContextWithRemoteDebuggerAsync( + Func javaScriptExecutorFactory, + CancellationToken token); } } diff --git a/ReactWindows/ReactNative.Shared/ReactContextInitializedEventArgs.cs b/ReactWindows/ReactNative.Shared/ReactContextInitializedEventArgs.cs deleted file mode 100644 index fc253fd7f10..00000000000 --- a/ReactWindows/ReactNative.Shared/ReactContextInitializedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ReactNative.Bridge; -using System; - -namespace ReactNative -{ - /// - /// Event arguments for the - /// event. - /// - public sealed class ReactContextInitializedEventArgs : EventArgs - { - /// - /// Instantiates the . - /// - /// The React context. - internal ReactContextInitializedEventArgs(ReactContext context) - { - Context = context; - } - - /// - /// The React context. - /// - public ReactContext Context { get; } - } -} diff --git a/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs b/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs index a29e859070e..aef9bb3d22e 100644 --- a/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs +++ b/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs @@ -1,17 +1,15 @@ using ReactNative.Bridge; using ReactNative.Bridge.Queue; -using ReactNative.Chakra.Executor; using ReactNative.Common; using ReactNative.DevSupport; using ReactNative.Modules.Core; -using ReactNative.Touch; using ReactNative.Tracing; using ReactNative.UIManager; using System; using System.Collections.Generic; -using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using System.Threading; using System.Threading.Tasks; -using static System.FormattableString; namespace ReactNative { @@ -38,6 +36,7 @@ public class ReactInstanceManager private readonly List _attachedRootViews = new List(); private readonly object _lifecycleStateLock = new object(); + private readonly SerialDisposable _currentInitializationToken = new SerialDisposable(); private readonly string _jsBundleFile; private readonly string _jsMainModuleName; @@ -50,18 +49,12 @@ public class ReactInstanceManager private LifecycleState _lifecycleState; private bool _hasStartedCreatingInitialContext; - private Task _contextInitializationTask; - private Func _pendingJsExecutorFactory; - private JavaScriptBundleLoader _pendingJsBundleLoader; + private Task _contextInitializationTask; + private int _pendingInitializationTasks; private string _sourceUrl; private ReactContext _currentReactContext; private Action _defaultBackButtonHandler; - /// - /// Event triggered when a React context has been initialized. - /// - public event EventHandler ReactContextInitialized; - internal ReactInstanceManager( string jsBundleFile, string jsMainModuleName, @@ -109,7 +102,7 @@ public IDevSupportManager DevSupportManager } /// - /// Signals whether has + /// Signals whether has /// been called. Will return false after /// until a new initial /// context has been created. @@ -153,22 +146,9 @@ public ReactContext CurrentReactContext /// enforced to keep developers from accidentally creating their /// applications multiple times. /// - public async void CreateReactContextInBackground() - { - await CreateReactContextInBackgroundAsync().ConfigureAwait(false); - } - - /// - /// Trigger the React context initialization asynchronously in a - /// background task. This enables applications to pre-load the - /// application JavaScript, and execute global core code before the - /// is available and measure. This should - /// only be called the first time the application is set up, which is - /// enforced to keep developers from accidentally creating their - /// applications multiple times. - /// + /// A token to cancel the request. /// A task to await the result. - internal async Task CreateReactContextInBackgroundAsync() + public Task CreateReactContextAsync(CancellationToken token) { if (_hasStartedCreatingInitialContext) { @@ -180,17 +160,33 @@ internal async Task CreateReactContextInBackgroundAsync() ReactChoreographer.Initialize(); _hasStartedCreatingInitialContext = true; - await RecreateReactContextInBackgroundInnerAsync().ConfigureAwait(false); + return CreateReactContextCoreAsync(token); } /// - /// Recreate the React application and context. This should be called - /// if configuration has changed or the developer has requested the - /// application to be reloaded. + /// Awaits the currently initializing React context. /// - public async void RecreateReactContextInBackground() + /// A token to cancel the request. + /// + /// A task to await the React context. + /// + public async Task GetReactContextAsync(CancellationToken token) { - await RecreateReactContextInBackgroundAsync().ConfigureAwait(false); + if (!_hasStartedCreatingInitialContext) + { + throw new InvalidOperationException( + "Use the create method to start initializing the React context."); + } + + var contextInitializationTask = _contextInitializationTask; + if (contextInitializationTask != null) + { + return await contextInitializationTask; + } + else + { + return _currentReactContext; + } } /// @@ -198,8 +194,9 @@ public async void RecreateReactContextInBackground() /// if configuration has changed or the developer has requested the /// application to be reloaded. /// + /// A token to cancel the request. /// A task to await the result. - internal async Task RecreateReactContextInBackgroundAsync() + public Task RecreateReactContextAsync(CancellationToken token) { if (!_hasStartedCreatingInitialContext) { @@ -208,7 +205,7 @@ internal async Task RecreateReactContextInBackgroundAsync() "create context background call."); } - await RecreateReactContextInBackgroundInnerAsync().ConfigureAwait(false); + return CreateReactContextCoreAsync(token); } /// @@ -398,76 +395,95 @@ public IReadOnlyList CreateAllViewManagers(ReactContext reactConte } } - private async Task RecreateReactContextInBackgroundInnerAsync() + private Task CreateReactContextCoreAsync(CancellationToken token) { DispatcherHelpers.AssertOnDispatcher(); - if (_useDeveloperSupport && _jsBundleFile == null && _jsMainModuleName != null) + if (_useDeveloperSupport && _jsBundleFile == null) { - if (await _devSupportManager.HasUpToDateBundleInCacheAsync()) - { - OnJavaScriptBundleLoadedFromServer(); - } - else - { - _devSupportManager.HandleReloadJavaScript(); - } + return CreateReactContextFromDevManagerAsync(token); } else { - RecreateReactContextInBackgroundFromBundleLoader(); + return CreateReactContextFromBundleAsync(token); } } - private void RecreateReactContextInBackgroundFromBundleLoader() + private async Task CreateReactContextFromDevManagerAsync(CancellationToken token) { - RecreateReactContextInBackground( - _javaScriptExecutorFactory, - JavaScriptBundleLoader.CreateFileLoader(_jsBundleFile)); + if (await _devSupportManager.HasUpToDateBundleInCacheAsync(token)) + { + return await CreateReactContextFromCachedPackagerBundleAsync(token); + } + else + { + return await _devSupportManager.CreateReactContextFromPackagerAsync(token); + } } - private void InvokeDefaultOnBackPressed() + private Task CreateReactContextFromBundleAsync(CancellationToken token) { - DispatcherHelpers.AssertOnDispatcher(); - _defaultBackButtonHandler?.Invoke(); + return CreateReactContextAsync( + _javaScriptExecutorFactory, + JavaScriptBundleLoader.CreateFileLoader(_jsBundleFile), + token); } - private void OnReloadWithJavaScriptDebugger(Func javaScriptExecutorFactory) + private Task CreateReactContextFromCachedPackagerBundleAsync(CancellationToken token) { - RecreateReactContextInBackground( - javaScriptExecutorFactory, - JavaScriptBundleLoader.CreateRemoteDebuggerLoader( - _devSupportManager.JavaScriptBundleUrlForRemoteDebugging, - _devSupportManager.SourceUrl)); + var bundleLoader = JavaScriptBundleLoader.CreateCachedBundleFromNetworkLoader( + _devSupportManager.SourceUrl, + _devSupportManager.DownloadedJavaScriptBundleFile); + return CreateReactContextAsync(_javaScriptExecutorFactory, bundleLoader, token); } - private void OnJavaScriptBundleLoadedFromServer() + private Task CreateReactContextWithRemoteDebuggerAsync( + Func javaScriptExecutorFactory, + CancellationToken token) { - RecreateReactContextInBackground( - _javaScriptExecutorFactory, - JavaScriptBundleLoader.CreateCachedBundleFromNetworkLoader( - _devSupportManager.SourceUrl, - _devSupportManager.DownloadedJavaScriptBundleFile)); + var bundleLoader = JavaScriptBundleLoader.CreateRemoteDebuggerLoader( + _devSupportManager.JavaScriptBundleUrlForRemoteDebugging, + _devSupportManager.SourceUrl); + return CreateReactContextAsync(javaScriptExecutorFactory, bundleLoader, token); } - private void RecreateReactContextInBackground( + private async Task CreateReactContextAsync( Func jsExecutorFactory, - JavaScriptBundleLoader jsBundleLoader) + JavaScriptBundleLoader jsBundleLoader, + CancellationToken token) { - if (_contextInitializationTask == null) + var cancellationDisposable = new CancellationDisposable(); + _currentInitializationToken.Disposable = cancellationDisposable; + using (token.Register(cancellationDisposable.Dispose)) { - _contextInitializationTask = InitializeReactContextAsync(jsExecutorFactory, jsBundleLoader); - } - else - { - _pendingJsExecutorFactory = jsExecutorFactory; - _pendingJsBundleLoader = jsBundleLoader; + _pendingInitializationTasks++; + var contextInitializationTask = _contextInitializationTask ?? Task.CompletedTask; + _contextInitializationTask = contextInitializationTask.ContinueWith(async task => + { + try + { + cancellationDisposable.Token.ThrowIfCancellationRequested(); + return await InitializeReactContextAsync( + jsExecutorFactory, + jsBundleLoader, + cancellationDisposable.Token); + } + catch (OperationCanceledException) + when (cancellationDisposable.Token.IsCancellationRequested) + { + token.ThrowIfCancellationRequested(); + return null; + } + }, + TaskContinuationOptions.ExecuteSynchronously).Unwrap(); + return await _contextInitializationTask; } } - private async Task InitializeReactContextAsync( + private async Task InitializeReactContextAsync( Func jsExecutorFactory, - JavaScriptBundleLoader jsBundleLoader) + JavaScriptBundleLoader jsBundleLoader, + CancellationToken token) { var currentReactContext = _currentReactContext; if (currentReactContext != null) @@ -478,8 +494,9 @@ private async Task InitializeReactContextAsync( try { - var reactContext = await CreateReactContextAsync(jsExecutorFactory, jsBundleLoader); + var reactContext = await CreateReactContextCoreAsync(jsExecutorFactory, jsBundleLoader, token); SetupReactContext(reactContext); + return reactContext; } catch (Exception ex) { @@ -487,21 +504,13 @@ private async Task InitializeReactContextAsync( } finally { - _contextInitializationTask = null; + if (--_pendingInitializationTasks == 0) + { + _contextInitializationTask = null; + } } - if (_pendingJsExecutorFactory != null) - { - var pendingJsExecutorFactory = _pendingJsExecutorFactory; - var pendingJsBundleLoader = _pendingJsBundleLoader; - - _pendingJsExecutorFactory = null; - _pendingJsBundleLoader = null; - - RecreateReactContextInBackground( - pendingJsExecutorFactory, - pendingJsBundleLoader); - } + return null; } private void SetupReactContext(ReactContext reactContext) @@ -523,8 +532,12 @@ private void SetupReactContext(ReactContext reactContext) { AttachMeasuredRootViewToInstance(rootView, reactInstance); } + } - OnReactContextInitialized(reactContext); + private void InvokeDefaultOnBackPressed() + { + DispatcherHelpers.AssertOnDispatcher(); + _defaultBackButtonHandler?.Invoke(); } private void AttachMeasuredRootViewToInstance( @@ -572,9 +585,10 @@ private async Task TearDownReactContextAsync(ReactContext reactContext) // TODO: add memory pressure hooks } - private async Task CreateReactContextAsync( + private async Task CreateReactContextCoreAsync( Func jsExecutorFactory, - JavaScriptBundleLoader jsBundleLoader) + JavaScriptBundleLoader jsBundleLoader, + CancellationToken token) { Tracer.Write(ReactConstants.Tag, "Creating React context."); @@ -635,7 +649,7 @@ private async Task CreateReactContextAsync( using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "RunJavaScriptBundle").Start()) { - await reactInstance.InitializeBridgeAsync().ConfigureAwait(false); + await reactInstance.InitializeBridgeAsync(token).ConfigureAwait(false); } return reactContext; @@ -738,19 +752,6 @@ private void MoveToBackgroundLifecycleState() } } - private void OnReactContextInitialized(ReactContext reactContext) - { - ReactContextInitialized? - .Invoke(this, new ReactContextInitializedEventArgs(reactContext)); - } - - private void ToggleElementInspector() - { - _currentReactContext? - .GetJavaScriptModule() - .emit("toggleElementInspector", null); - } - class ReactInstanceDevCommandsHandler : IReactInstanceDevCommandsHandler { private readonly ReactInstanceManager _parent; @@ -760,24 +761,19 @@ public ReactInstanceDevCommandsHandler(ReactInstanceManager parent) _parent = parent; } - public void OnBundleFileReloadRequest() - { - _parent.RecreateReactContextInBackground(); - } - - public void OnJavaScriptBundleLoadedFromServer() + public Task CreateReactContextFromBundleAsync(CancellationToken token) { - _parent.OnJavaScriptBundleLoadedFromServer(); + return _parent.CreateReactContextFromBundleAsync(token); } - public void OnReloadWithJavaScriptDebugger(Func javaScriptExecutorFactory) + public Task CreateReactContextFromCachedPackagerBundleAsync(CancellationToken token) { - _parent.OnReloadWithJavaScriptDebugger(javaScriptExecutorFactory); + return _parent.CreateReactContextFromCachedPackagerBundleAsync(token); } - public void ToggleElementInspector() + public Task CreateReactContextWithRemoteDebuggerAsync(Func javaScriptExecutorFactory, CancellationToken token) { - _parent.ToggleElementInspector(); + return _parent.CreateReactContextWithRemoteDebuggerAsync(javaScriptExecutorFactory, token); } } } diff --git a/ReactWindows/ReactNative.Shared/ReactInstanceManagerExtensions.cs b/ReactWindows/ReactNative.Shared/ReactInstanceManagerExtensions.cs new file mode 100644 index 00000000000..40d945d80e9 --- /dev/null +++ b/ReactWindows/ReactNative.Shared/ReactInstanceManagerExtensions.cs @@ -0,0 +1,30 @@ +using ReactNative.Bridge; +using System.Threading; +using System.Threading.Tasks; + +namespace ReactNative +{ + /// + /// Extension methods for the React instance manager. + /// + public static class ReactInstanceManagerExtensions + { + /// + /// Gets or creates the React context. + /// + /// The React instance manager. + /// A token to cancel the request. + /// + /// A task to await the React context. + /// + public static Task GetOrCreateReactContextAsync(this ReactInstanceManager manager, CancellationToken token) + { + if (manager.HasStartedCreatingInitialContext) + { + return manager.GetReactContextAsync(token); + } + + return manager.CreateReactContextAsync(token); + } + } +} diff --git a/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems b/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems index 74f890a1518..0d646c7f5aa 100644 --- a/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems +++ b/ReactWindows/ReactNative.Shared/ReactNative.Shared.projitems @@ -142,8 +142,8 @@ + - diff --git a/ReactWindows/ReactNative.Shared/ReactRootView.cs b/ReactWindows/ReactNative.Shared/ReactRootView.cs index 6992b68c287..ccc3ba0178f 100644 --- a/ReactWindows/ReactNative.Shared/ReactRootView.cs +++ b/ReactWindows/ReactNative.Shared/ReactRootView.cs @@ -1,8 +1,10 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Touch; using ReactNative.UIManager; using System; +using System.Threading; +using System.Threading.Tasks; #if WINDOWS_UWP using Windows.Foundation; #else @@ -95,7 +97,7 @@ public void StartReactApplication(ReactInstanceManager reactInstanceManager, str /// /// The module name. /// The initialProps - public void StartReactApplication(ReactInstanceManager reactInstanceManager, string moduleName, JObject initialProps) + public async void StartReactApplication(ReactInstanceManager reactInstanceManager, string moduleName, JObject initialProps) { DispatcherHelpers.AssertOnDispatcher(); @@ -108,9 +110,10 @@ public void StartReactApplication(ReactInstanceManager reactInstanceManager, str _jsModuleName = moduleName; _initialProps = initialProps; + var getReactContextTask = default(Task); if (!_reactInstanceManager.HasStartedCreatingInitialContext) { - _reactInstanceManager.CreateReactContextInBackground(); + getReactContextTask = _reactInstanceManager.GetOrCreateReactContextAsync(CancellationToken.None); } // We need to wait for the initial `Measure` call, if this view has @@ -124,6 +127,11 @@ public void StartReactApplication(ReactInstanceManager reactInstanceManager, str { _attachScheduled = true; } + + if (getReactContextTask != null) + { + await getReactContextTask.ConfigureAwait(false); + } } /// diff --git a/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs index d404b32fa5b..01cf7d3fd82 100644 --- a/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs +++ b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs @@ -50,5 +50,26 @@ await RunOnDispatcherAsync(async () => await tcs.Task.ConfigureAwait(false); } + + public static async Task CallOnDispatcherAsync(Func> asyncFunc) + { + var tcs = new TaskCompletionSource(); + + await RunOnDispatcherAsync(async () => + { + try + { + var result = await asyncFunc(); + await Task.Run(() => tcs.SetResult(result)); + } + catch (Exception ex) + { + await Task.Run(() => tcs.SetException(ex)); + } + }).ConfigureAwait(false); + + return await tcs.Task.ConfigureAwait(false); + } + } } diff --git a/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs b/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs index 5cba8676108..60e6c22b6b5 100644 --- a/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs +++ b/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs @@ -1,4 +1,5 @@ using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using ReactNative.Bridge; using ReactNative.Common; using System; using System.Threading; @@ -46,40 +47,33 @@ public async Task ReactInstanceManager_ArgumentChecks() } [TestMethod] - public async Task ReactInstanceManager_CreateInBackground() + public async Task ReactInstanceManager_CreateReactContextAsync() { var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); - var waitHandle = new AutoResetEvent(false); - manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); - - await DispatcherHelpers.RunOnDispatcherAsync( - () => manager.CreateReactContextInBackground()); + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); - Assert.IsTrue(waitHandle.WaitOne()); Assert.AreEqual(jsBundleFile, manager.SourceUrl); await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); } [TestMethod] - public async Task ReactInstanceManager_CreateInBackground_EnsuresOneCall() + public async Task ReactInstanceManager_CreateReactContextAsync_EnsuresOneCall() { var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); - var waitHandle = new AutoResetEvent(false); - manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); - var caught = false; await DispatcherHelpers.RunOnDispatcherAsync(async () => { - manager.CreateReactContextInBackground(); + var task = manager.CreateReactContextAsync(CancellationToken.None); try { - await manager.CreateReactContextInBackgroundAsync(); + await manager.CreateReactContextAsync(CancellationToken.None); } catch (InvalidOperationException) { @@ -93,29 +87,28 @@ await DispatcherHelpers.RunOnDispatcherAsync(async () => } [TestMethod] - public async Task ReactInstanceManager_RecreateInBackground() + public async Task ReactInstanceManager_RecreateReactContextAsync() { var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); - var waitHandle = new AutoResetEvent(false); - manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); - - await DispatcherHelpers.RunOnDispatcherAsync(() => + var task = default(Task); + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync(async () => { - manager.CreateReactContextInBackground(); - manager.RecreateReactContextInBackground(); + task = manager.CreateReactContextAsync(CancellationToken.None); + return await manager.RecreateReactContextAsync(CancellationToken.None); }); - Assert.IsTrue(waitHandle.WaitOne()); - Assert.IsTrue(waitHandle.WaitOne()); + var initialReactContext = await task; + Assert.IsNotNull(reactContext); Assert.AreEqual(jsBundleFile, manager.SourceUrl); + Assert.AreNotEqual(initialReactContext, reactContext); await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); } [TestMethod] - public async Task ReactInstanceManager_RecreateInBackground_EnsuresCalledOnce() + public async Task ReactInstanceManager_RecreateReactContextAsync_EnsuresCalledOnce() { var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); @@ -125,7 +118,7 @@ await DispatcherHelpers.RunOnDispatcherAsync(async () => { try { - await manager.RecreateReactContextInBackgroundAsync(); + await manager.RecreateReactContextAsync(CancellationToken.None); } catch (InvalidOperationException) { @@ -155,27 +148,23 @@ await DispatcherHelpers.RunOnDispatcherAsync(() => } [TestMethod] - public async Task ReactInstanceManager_OnDestroy_CreateInBackground() + public async Task ReactInstanceManager_OnDestroy_CreateReactContextAsync() { var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); - var waitHandle = new AutoResetEvent(false); - manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); - - await DispatcherHelpers.RunOnDispatcherAsync( - () => manager.CreateReactContextInBackground()); + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); - Assert.IsTrue(waitHandle.WaitOne()); + Assert.IsNotNull(reactContext); Assert.AreEqual(jsBundleFile, manager.SourceUrl); await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); - await DispatcherHelpers.RunOnDispatcherAsync( - () => manager.CreateReactContextInBackground()); - - Assert.IsTrue(waitHandle.WaitOne()); + reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); + Assert.IsNotNull(reactContext); await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); } @@ -185,18 +174,11 @@ public async Task ReactInstanceManager_DisposeAsync_WhileBusy() var jsBundleFile = "ms-appx:///Resources/test.js"; var manager = CreateReactInstanceManager(jsBundleFile); - var initializedEvent = new AutoResetEvent(false); - manager.ReactContextInitialized += (sender, args) => initializedEvent.Set(); - - await DispatcherHelpers.CallOnDispatcherAsync(async () => - { - await manager.CreateReactContextInBackgroundAsync(); - }); + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); var e = new AutoResetEvent(false); - - initializedEvent.WaitOne(); - manager.CurrentReactContext.RunOnNativeModulesQueueThread(() => + reactContext.RunOnNativeModulesQueueThread(() => { e.WaitOne(); var x = DispatcherHelpers.CallOnDispatcherAsync(() => @@ -218,6 +200,113 @@ await DispatcherHelpers.CallOnDispatcherAsync(async () => Assert.IsTrue(task.IsCompleted); } + [TestMethod] + public async Task ReactInstanceManager_CreateReactContextAsync_Canceled() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.Cancel(); + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(cancellationTokenSource.Token)); + + Assert.IsNull(reactContext); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + [TestMethod] + public async Task ReactInstanceManager_GetReactContextAsync_Unfinished() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var initialContextTask = DispatcherHelpers.CallOnDispatcherAsync(async () => + await manager.CreateReactContextAsync(CancellationToken.None)); + var context = await DispatcherHelpers.CallOnDispatcherAsync(async () => + await manager.GetReactContextAsync(CancellationToken.None)); + var initialContext = await initialContextTask; + + Assert.AreSame(initialContext, context); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + + [TestMethod] + public async Task ReactInstanceManager_GetReactContextAsync_Finished() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var initialContext = await DispatcherHelpers.CallOnDispatcherAsync(async () => + await manager.CreateReactContextAsync(CancellationToken.None)); + var context = await DispatcherHelpers.CallOnDispatcherAsync(async () => + await manager.GetReactContextAsync(CancellationToken.None)); + + Assert.AreSame(initialContext, context); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + [TestMethod] + public async Task ReactInstanceManager_GetReactContextAsync_Fail() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var caught = false; + await DispatcherHelpers.CallOnDispatcherAsync(async () => + { + try + { + await manager.GetReactContextAsync(CancellationToken.None); + } + catch (InvalidOperationException) + { + caught = true; + } + }); + + Assert.IsTrue(caught); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + [TestMethod] + public async Task ReactInstanceManager_GetOrCreateReactContextAsync_Create() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync(() => manager.GetOrCreateReactContextAsync(CancellationToken.None)); + Assert.IsNotNull(reactContext); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + [TestMethod] + public async Task ReactInstanceManager_GetOrCreateReactContextAsync_Get() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var initialContext = default(ReactContext); + var context = default(ReactContext); + await DispatcherHelpers.CallOnDispatcherAsync(async () => + { + var initialContextTask = manager.CreateReactContextAsync(CancellationToken.None); + context = await manager.GetOrCreateReactContextAsync(CancellationToken.None); + initialContext = await initialContextTask; + }); + + Assert.AreSame(initialContext, context); + + await DispatcherHelpers.CallOnDispatcherAsync(manager.DisposeAsync); + } + + private static ReactInstanceManager CreateReactInstanceManager() { return CreateReactInstanceManager("ms-appx:///Resources/main.jsbundle"); diff --git a/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs b/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs index b81ae1e2f87..edee2199d1a 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs @@ -364,7 +364,7 @@ private static async Task CreateContextAsync(IJavaScriptExecutor e var instance = CreateReactInstance(context, executor); context.InitializeWithInstance(instance); instance.Initialize(); - await instance.InitializeBridgeAsync(); + await instance.InitializeBridgeAsync(CancellationToken.None); return instance; }); diff --git a/ReactWindows/ReactNative/ReactPage.cs b/ReactWindows/ReactNative/ReactPage.cs index 1f2be803b5e..2071b65aa38 100644 --- a/ReactWindows/ReactNative/ReactPage.cs +++ b/ReactWindows/ReactNative/ReactPage.cs @@ -4,6 +4,7 @@ using ReactNative.Modules.Core; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Windows.System; using Windows.UI.Core; @@ -173,7 +174,7 @@ protected virtual ReactRootView CreateRootView() /// /// /// - private void OnAcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEventArgs e) + private async void OnAcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEventArgs e) { if (_reactInstanceManager.DevSupportManager.IsEnabled) { @@ -192,7 +193,7 @@ private void OnAcceleratorKeyActivated(CoreDispatcher sender, AcceleratorKeyEven } else if (e.EventType == CoreAcceleratorKeyEventType.KeyUp && _isControlKeyDown && e.VirtualKey == VirtualKey.R) { - _reactInstanceManager.DevSupportManager.HandleReloadJavaScript(); + await _reactInstanceManager.DevSupportManager.CreateReactContextFromPackagerAsync(CancellationToken.None).ConfigureAwait(false); } } } diff --git a/ReactWindows/ReactNative/ReactRootViewExtensions.cs b/ReactWindows/ReactNative/ReactRootViewExtensions.cs index d575e796e41..6e17d35bb84 100644 --- a/ReactWindows/ReactNative/ReactRootViewExtensions.cs +++ b/ReactWindows/ReactNative/ReactRootViewExtensions.cs @@ -1,3 +1,4 @@ +using System.Threading; using Windows.System; using Windows.UI.Core; using Windows.UI.Xaml; @@ -26,7 +27,7 @@ private static void OnBackRequested(ReactNativeHost host, object sender, BackReq } } - private static void OnAcceleratorKeyActivated(ReactNativeHost host, CoreDispatcher sender, AcceleratorKeyEventArgs e) + private static async void OnAcceleratorKeyActivated(ReactNativeHost host, CoreDispatcher sender, AcceleratorKeyEventArgs e) { if (host.HasInstance) { @@ -48,7 +49,7 @@ private static void OnAcceleratorKeyActivated(ReactNativeHost host, CoreDispatch } else if (e.EventType == CoreAcceleratorKeyEventType.KeyUp && s_isControlKeyDown && e.VirtualKey == VirtualKey.R) { - reactInstanceManager.DevSupportManager.HandleReloadJavaScript(); + await reactInstanceManager.DevSupportManager.CreateReactContextFromPackagerAsync(CancellationToken.None).ConfigureAwait(false); } } }