diff --git a/specs/WorkerPostMessage.md b/specs/WorkerPostMessage.md new file mode 100644 index 00000000..f969e3df --- /dev/null +++ b/specs/WorkerPostMessage.md @@ -0,0 +1,682 @@ +Post message support for Dedicated and Service Workers +=== + +# Background +Currently, if developers want to post a message to or from a worker, they must +first post a message to the main thread and then post a message to the worker +from the main thread. This leads to increased load on the main thread. + +The WebView2 team is introducing APIs that allow messages to be posted directly +from the Host app to dedicated and service workers, and vice versa. This +eliminates the need for the main thread to act as an intermediary, thereby +improving app performance and responsiveness. + +# Description +We propose the following APIs: + +**PostWebMessageAsJson**: This API gives ability to post a message to +dedicated/service worker. The worker receives the message by subscribing to +message event of `self.chrome.webview`. + +**PostWebMessageAsString**: This API gives ability to post a message that is +a simple string rather than a string representation of a JSON object to +dedicated/service worker. The worker receives the message by subscribing to +message event of `self.chrome.webview`. This can be used to communicate using +simple strings rather than JSON objects. + +**WebMessageReceived**: This event gives Host app the ability to receive +message sent from worker using `self.chrome.webview.postMessage`. + +# Examples +## Dedicated Worker +## Posting messages to and from a dedicated worker +### main.js +```JS +if (window.Worker) { + const worker = new Worker('dedicated_worker.js'); +} +``` + +### dedicated_worker.js +```JS +//! [chromeWebView] +self.chrome.webview.addEventListener('message', (e) => { + const first = e.data.first; + const second = e.data.second; + switch (e.data.command) { + case 'ADD': { + result = first + second; + break; + } + case 'SUB': { + result = first - second; + break; + } + case 'MUL': { + result = first * second; + break; + } + case 'DIV': { + if (second === 0) { + result = 'Error: Division by zero'; + break; + } + + result = first / second; + break; + } + default: { + result = 'Failed to process the command'; + } + } + self.chrome.webview.postMessage('Result: ' + result.toString()); +}); +//! [chromeWebView] +``` + +### C++ Sample +```cpp +using namespace Microsoft::WRL; + +AppWindow* m_appWindow; +wil::com_ptr m_webView2_26; +EventRegistrationToken m_dedicatedWorkerCreatedToken = {}; + +ScenarioDedicatedWorkerPostMessage::ScenarioDedicatedWorkerPostMessage(AppWindow* appWindow) + : m_appWindow(appWindow) +{ + //! [DedicatedWorkerCreated] + m_appWindow->GetWebView()->QueryInterface(IID_PPV_ARGS(&m_webView2_26)); + CHECK_FEATURE_RETURN_EMPTY(m_webView2_26); + + CHECK_FAILURE(m_webView2_26->add_DedicatedWorkerCreated( + Callback( + [this]( + ICoreWebView2* sender, + ICoreWebView2DedicatedWorkerCreatedEventArgs* args) + { + wil::com_ptr dedicatedWorker; + CHECK_FAILURE(args->get_Worker(&dedicatedWorker)); + + wil::unique_cotaskmem_string scriptUri; + CHECK_FAILURE(dedicatedWorker->get_ScriptUri(&scriptUri)); + + std::wstring scriptUriStr(scriptUri.get()); + m_appWindow->AsyncMessageBox(scriptUriStr, L"Dedicated worker is created"); + + SetupEventsOnDedicatedWorker(dedicatedWorker); + ComputeWithDedicatedWorker(dedicatedWorker); + + return S_OK; + }) + .Get(), + &m_dedicatedWorkerCreatedToken)); + //! [DedicatedWorkerCreated] +} + +void ScenarioDedicatedWorkerPostMessage::SetupEventsOnDedicatedWorker( + wil::com_ptr dedicatedWorker) +{ + //! [WebMessageReceived] + dedicatedWorker->add_WebMessageReceived( + Callback( + [this]( + ICoreWebView2DedicatedWorker* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string scriptUri; + CHECK_FAILURE(args->get_Source(&scriptUri)); + + wil::unique_cotaskmem_string messageRaw; + CHECK_FAILURE(args->TryGetWebMessageAsString(&messageRaw)); + std::wstring messageFromWorker = messageRaw.get(); + + std::wstringstream message{}; + message << L"Dedicated Worker: " << std::endl << scriptUri.get() << std::endl; + message << std::endl; + message << L"Message: " << std::endl << messageFromWorker << std::endl; + m_appWindow->AsyncMessageBox(message.str(), L"Message from Dedicated Worker"); + + return S_OK; + }) + .Get(), + nullptr); + //! [WebMessageReceived] +} + +void ScenarioDedicatedWorkerPostMessage::ComputeWithDedicatedWorker( + wil::com_ptr dedicatedWorker) +{ + TextInputDialog dialog( + m_appWindow->GetMainWindow(), L"Post Web Message JSON", L"Web message JSON", + L"Enter the web message as JSON."); + // Ex: {"command":"ADD","first":2,"second":3} + if (dialog.confirmed) + { + //! [PostWebMessageAsJson] + dedicatedWorker->PostWebMessageAsJson(dialog.input.c_str()); + //! [PostWebMessageAsJson] + } +} +``` + +### .NET/WinRT +```c# +void DedicatedWorkerPostMessageExecuted(object target, ExecutedRoutedEventArgs e) +{ + _iWebView2.CoreWebView2.DedicatedWorkerCreated += + DedicatedWorker_PostMessage_DedicatedWorkerCreated; +} + +void DedicatedWorker_PostMessage_DedicatedWorkerCreated(object sender, + CoreWebView2DedicatedWorkerCreatedEventArgs args) +{ + CoreWebView2DedicatedWorker dedicatedWorker = args.Worker; + MessageBox.Show("Dedicated worker is created" , "Dedicated Worker Message"); + DedicatedWorker_PostMessage_SetupEventsOnDedicatedWorker(dedicatedWorker); + DedicatedWorker_PostMessage_ComputeWithDedicatedWorker(dedicatedWorker); +} + +void DedicatedWorker_PostMessage_SetupEventsOnDedicatedWorker( + CoreWebView2DedicatedWorker dedicatedWorker) +{ + dedicatedWorker.WebMessageReceived += (sender, args) => + { + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.AppendLine($"Dedicated Worker: \n{args.Source} "); + messageBuilder.AppendLine($"\nMessage: \n{args.TryGetWebMessageAsString()} "); + MessageBox.Show(messageBuilder.ToString(), "Message from Dedicated Worker", + MessageBoxButton.OK); + }; +} + +void DedicatedWorker_PostMessage_ComputeWithDedicatedWorker( + CoreWebView2DedicatedWorker dedicatedWorker) +{ + var dialog = new TextInputDialog( + title: "Post Web Message JSON", + description: "Enter the web message as JSON.", + defaultInput: ""); + // Ex: {"command":"MUL","first":2,"second":3} + if (dialog.ShowDialog() == true) + { + dedicatedWorker.PostWebMessageAsJson(dialog.Input.Text); + } +} +``` + +## Service Worker +## Posting messages to and from a service worker +### main.js +```JS +if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("sw.js"); +} + +``` +### sw.js +```JS +'use strict'; + +const CACHE_NAME = 'sw_post_message_cache'; +const CACHE_LIST = ['style.css']; + +const cacheFirst = async (request) => { + // First try to get the resource from the cache + const responseFromCache = await caches.match(request); + if (responseFromCache) { + console.log('Cache hit for request: ', request.url); + // Notify the app about the cache hit. + //! [chromeWebView] + self.chrome.webview.postMessage('Cache hit for resource: ' + request.url); + //! [chromeWebView] + return responseFromCache; + } + + // Next try to get the resource from the network + try { + console.log('Cache miss for request: ', request.url); + const responseFromNetwork = await fetch(request); + const cache = await caches.open(CACHE_NAME); + console.log('Cache new resource: ', request.url); + await cache.put(request, responseFromNetwork.clone()); + return responseFromNetwork; + } catch (error) { + return new Response('Network error happened', { + status: 408, + headers: { 'Content-Type': 'text/plain' }, + }); + } +}; + +const addToCache = async (url) => { + console.log('Add to cache: ', url); + const cache = await caches.open(CACHE_NAME); + cache.add(url); + //! [chromeWebView] + chrome.webview.postMessage('Added to cache: ' + url); + //! [chromeWebView] +}; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches + .open(CACHE_NAME) + .then((cache) => cache.addAll(CACHE_LIST)) + .then(self.skipWaiting()) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(clients.claim()); +}); + +self.addEventListener('fetch', (event) => { + event.respondWith(cacheFirst(event.request)); +}); + +//! [chromeWebView] +self.chrome.webview.addEventListener('message', (event) => { + if (event.data.command === 'ADD_TO_CACHE') { + addToCache(event.data.url); + } +}); +//! [chromeWebView] +``` + +### C++ Sample +```cpp +using namespace Microsoft::WRL; + +AppWindow* m_appWindow; +wil::com_ptr m_webView; +wil::com_ptr m_serviceWorkerManager; +EventRegistrationToken m_serviceWorkerRegisteredToken = {}; + +ScenarioServiceWorkerPostMessage::ScenarioServiceWorkerPostMessage(AppWindow* appWindow) + : m_appWindow(appWindow), m_webView(appWindow->GetWebView()) +{ + CreateServiceWorkerManager(); + SetupEventsOnWebview(); +} + +void ScenarioServiceWorkerPostMessage::CreateServiceWorkerManager() +{ + //! [ServiceWorkerManager] + auto webView2_13 = m_webView.try_query(); + CHECK_FEATURE_RETURN_EMPTY(webView2_13); + + wil::com_ptr webView2Profile; + CHECK_FAILURE(webView2_13->get_Profile(&webView2Profile)); + auto webViewProfile3 = webView2Profile.try_query(); + CHECK_FEATURE_RETURN_EMPTY(webViewProfile3); + CHECK_FAILURE(webViewProfile3->get_ServiceWorkerManager(&m_serviceWorkerManager)); + //! [ServiceWorkerManager] +} + +void ScenarioServiceWorkerPostMessage::SetupEventsOnWebview() +{ + if (!m_serviceWorkerManager) + { + return; + } + + //! [ServiceWorkerRegistered] + CHECK_FAILURE(m_serviceWorkerManager->add_ServiceWorkerRegistered( + Callback( + [this]( + ICoreWebView2ServiceWorkerManager* sender, + ICoreWebView2ServiceWorkerRegisteredEventArgs* args) + { + wil::com_ptr + serviceWorkerRegistration; + CHECK_FAILURE(args->get_ServiceWorkerRegistration(&serviceWorkerRegistration)); + + if (serviceWorkerRegistration) + { + wil::unique_cotaskmem_string scopeUri; + CHECK_FAILURE(serviceWorkerRegistration->get_ScopeUri(&scopeUri)); + std::wstring scopeUriStr(scopeUri.get()); + + wil::com_ptr serviceWorker; + CHECK_FAILURE( + serviceWorkerRegistration->get_ActiveServiceWorker(&serviceWorker)); + + if (serviceWorker) + { + SetupEventsOnServiceWorker(serviceWorker); + AddToCache(L"img.jpg", serviceWorker); + } + else + { + CHECK_FAILURE(serviceWorkerRegistration->add_ServiceWorkerActivated( + Callback( + [this]( + ICoreWebView2ServiceWorkerRegistration* sender, + ICoreWebView2ServiceWorkerActivatedEventArgs* args) + -> HRESULT + { + wil::com_ptr + serviceWorker; + CHECK_FAILURE( + args->get_ActiveServiceWorker(&serviceWorker)); + + SetupEventsOnServiceWorker(serviceWorker); + AddToCache(L"img.jpg", serviceWorker); + + return S_OK; + }) + .Get(), + nullptr)); + } + + m_appWindow->AsyncMessageBox(scopeUriStr, L"Service worker is registered"); + } + + return S_OK; + }) + .Get(), + &m_serviceWorkerRegisteredToken)); + //! [ServiceWorkerRegistered] +} + +void ScenarioServiceWorkerPostMessage::SetupEventsOnServiceWorker( + wil::com_ptr serviceWorker) +{ + //! [WebMessageReceived] + serviceWorker->add_WebMessageReceived( + Callback( + [this]( + ICoreWebView2ServiceWorker* sender, + ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT + { + wil::unique_cotaskmem_string scriptUri; + CHECK_FAILURE(args->get_Source(&scriptUri)); + + wil::unique_cotaskmem_string messageRaw; + CHECK_FAILURE(args->TryGetWebMessageAsString(&messageRaw)); + std::wstring messageFromWorker = messageRaw.get(); + + std::wstringstream message{}; + message << L"Service Worker: " << std::endl << scriptUri.get() << std::endl; + message << std::endl; + message << L"Message: " << std::endl << messageFromWorker << std::endl; + m_appWindow->AsyncMessageBox(message.str(), L"Message from Service Worker"); + + return S_OK; + }) + .Get(), + nullptr); + //! [WebMessageReceived] +} + +void ScenarioServiceWorkerPostMessage::AddToCache( + std::wstring url, wil::com_ptr serviceWorker) +{ + std::wstring msg = L"{\"command\":\"ADD_TO_CACHE\",\"url\":\"" + url + L"\"}"; + //! [PostWebMessageAsJson] + serviceWorker->PostWebMessageAsJson(msg.c_str()); + //! [PostWebMessageAsJson] +} +``` + +### .NET/WinRT +```c# +CoreWebView2ServiceWorkerManager PostMessage_ServiceWorkerManager_; + +void ServiceWorkerPostMessageExecuted(object target, ExecutedRoutedEventArgs e) +{ + PostMessage_ServiceWorkerManager_ = WebViewProfile.ServiceWorkerManager; + + PostMessage_ServiceWorkerManager_.ServiceWorkerRegistered += + ServiceWorker_PostMessage_ServiceWorkerRegistered; +} + +void ServiceWorker_PostMessage_ServiceWorkerRegistered(object sender, + CoreWebView2ServiceWorkerRegisteredEventArgs args) +{ + CoreWebView2ServiceWorkerRegistration serviceWorkerRegistration = + args.ServiceWorkerRegistration; + MessageBox.Show("Service worker is registered for " + serviceWorkerRegistration.ScopeUri, + "Service Worker Registration Message"); + + CoreWebView2ServiceWorker serviceWorker = serviceWorkerRegistration.ActiveServiceWorker; + if (serviceWorker != null) + { + ServiceWorker_PostMessage_SetupEventsOnServiceWorker(serviceWorker); + ServiceWorker_PostMessage_AddToCache(serviceWorker, "img.jpg"); + } + else + { + serviceWorkerRegistration.ServiceWorkerActivated += (sender1, args1) => + { + ServiceWorker_PostMessage_SetupEventsOnServiceWorker(serviceWorker); + ServiceWorker_PostMessage_AddToCache(serviceWorker, "img.jpg"); + }; + } +} + +void ServiceWorker_PostMessage_SetupEventsOnServiceWorker(CoreWebView2ServiceWorker serviceWorker) +{ + serviceWorker.WebMessageReceived += (sender, args) => + { + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.AppendLine($"Service Worker: \n{args.Source} "); + messageBuilder.AppendLine($"\nMessage: \n{args.TryGetWebMessageAsString()} "); + MessageBox.Show(messageBuilder.ToString(), "Message from Service Worker", + MessageBoxButton.OK); + }; +} + +void ServiceWorker_PostMessage_AddToCache(CoreWebView2ServiceWorker serviceWorker, string url) +{ + string msg = "{\"command\":\"ADD_TO_CACHE\",\"url\":\"" + url + "\"}"; + serviceWorker.PostWebMessageAsJson(msg); +} +``` + +# API Details +## C++ +``` +/// Receives `WebMessageReceived` events. +[uuid(b366218b-0bb8-58a3-ac33-f40a2235366e), object, pointer_default(unique)] +interface ICoreWebView2DedicatedWorkerWebMessageReceivedEventHandler : IUnknown { + /// Provides the event args for the corresponding event. + HRESULT Invoke( + [in] ICoreWebView2DedicatedWorker* sender, + [in] ICoreWebView2WebMessageReceivedEventArgs* args); +} + +[uuid(66833876-edba-5a60-8508-7da64504a9d2), object, pointer_default(unique)] +interface ICoreWebView2DedicatedWorker : IUnknown { + /// Adds an event handler for the `WebMessageReceived` event. + /// Add an event handler for the `WebMessageReceived` event. + /// `WebMessageReceived` can be subscribed to, when the + /// `ICoreWebView2Settings::IsWebMessageEnabled` setting is set TRUE and the + /// worker runs + /// `self.chrome.webview.postMessage`. The `postMessage` function is + /// `void postMessage(object)` where object is any object supported by JSON + /// conversion. + /// + /// If the worker calls `postMessage` multiple times, the corresponding + /// `WebMessageReceived` events are guaranteed to be fired in the same order. + HRESULT add_WebMessageReceived( + [in] ICoreWebView2DedicatedWorkerWebMessageReceivedEventHandler* eventHandler, + [out] EventRegistrationToken* token); + + /// Removes an event handler previously added with `add_WebMessageReceived`. + HRESULT remove_WebMessageReceived( + [in] EventRegistrationToken token); + + + /// Post the specific message to this worker. + /// The worker receives the message by subscribing to the message event of the + /// `self.chrome.webview` of the worker. + /// + /// self.chrome.webview.addEventListener('message', handler) + /// self.chrome.webview.removeEventListener('message', handler) + /// + /// The event args is an instance of `MessageEvent`. The + /// `ICoreWebView2Settings::IsWebMessageEnabled` setting must be `TRUE` or + /// the web message will not be sent. The data property of the event arg is the + /// message string parameter parsed as a JSON string into a JS object. + /// The message is delivered asynchronously + /// If the worker is terminated or destroyed before the message is posted, + /// the message is discarded. + HRESULT PostWebMessageAsJson( + [in] LPCWSTR messageAsJson + ); + + /// Posts a message that is a simple string rather than a string + /// representation of a JSON object. This behaves exactly the same + /// manner as `PostWebMessageAsJson`, but the `data` property of the event + /// arg of the worker's `self.chrome.webview` message is a string with the same + /// value as `messageAsString`. Use this instead of + /// `PostWebMessageAsJson` if you want to communicate using simple strings + /// rather than JSON objects. + HRESULT PostWebMessageAsString( + [in] LPCWSTR messageAsString + ); +} + +/// Receives `WebMessageReceived` events. +[uuid(8765d114-94f4-5d90-b833-b0c8cc05a4dc), object, pointer_default(unique)] +interface ICoreWebView2ServiceWorkerWebMessageReceivedEventHandler : IUnknown { + /// Provides the event args for the corresponding event. + HRESULT Invoke( + [in] ICoreWebView2ServiceWorker* sender, + [in] ICoreWebView2WebMessageReceivedEventArgs* args); +} + +[uuid(f115648d-56e3-5570-8d69-be999e769fd8), object, pointer_default(unique)] +interface ICoreWebView2ServiceWorker : IUnknown { + /// Adds an event handler for the `WebMessageReceived` event. + /// Add an event handler for the `WebMessageReceived` event. + /// `WebMessageReceived` is fired, when the + /// `ICoreWebView2Settings::IsWebMessageEnabled` setting is set and the + /// worker runs + /// `self.chrome.webview.postMessage`. The `postMessage` function is + /// `void postMessage(object)` where object is any object supported by JSON + /// conversion. + /// + /// If the worker calls `postMessage` multiple times, the corresponding + /// `WebMessageReceived` events are guaranteed to be fired in the same order. + HRESULT add_WebMessageReceived( + [in] ICoreWebView2ServiceWorkerWebMessageReceivedEventHandler* eventHandler, + [out] EventRegistrationToken* token); + + /// Removes an event handler previously added with `add_WebMessageReceived`. + HRESULT remove_WebMessageReceived( + [in] EventRegistrationToken token); + + /// Post the specific message to this worker. + /// The worker receives the message by subscribing to the message event of the + /// `self.chrome.webview` of the worker. + /// + /// self.chrome.webview.addEventListener('message', handler) + /// self.chrome.webview.removeEventListener('message', handler) + /// + /// The event args is an instance of `MessageEvent`. The + /// `ICoreWebView2Settings::IsWebMessageEnabled` setting must be `TRUE` or + /// the web message will not be sent. The data property of the event arg is the + /// message string parameter parsed as a JSON string into a JS object. + /// The message is delivered asynchronously + /// If the worker is terminated or destroyed before the message is posted, + /// the message is discarded. + HRESULT PostWebMessageAsJson( + [in] LPCWSTR messageAsJson + ); + + /// Posts a message that is a simple string rather than a string + /// representation of a JSON object. This behaves exactly the same + /// manner as `PostWebMessageAsJson`, but the `data` property of the event + /// arg of the worker's `self.chrome.webview` message is a string with the same + /// value as `messageAsString`. Use this instead of + /// `PostWebMessageAsJson` if you want to communicate using simple strings + /// rather than JSON objects. + HRESULT PostWebMessageAsString( + [in] LPCWSTR messageAsString + ); +} +``` + +## C# +```c# +namespace Microsoft.Web.WebView2.Core +{ + runtimeclass CoreWebView2DedicatedWorker + { + event Windows.Foundation.TypedEventHandler WebMessageReceived; + + void PostWebMessageAsJson(String messageAsJson); + + void PostWebMessageAsString(String messageAsString); + } + + runtimeclass CoreWebView2ServiceWorker + { + event Windows.Foundation.TypedEventHandler WebMessageReceived; + + void PostWebMessageAsJson(String messageAsJson); + + void PostWebMessageAsString(String messageAsString); + } +} +``` + +## JavaScript +```JS +interface WorkerGlobalScope { + chrome: Chrome; +} + +interface Chrome { + /** + * Makes the JavaScript APIs in WebView2 available to the worker. + */ + webview: WebView; +} + +/** + * `self.chrome.webview` is the interface to access the WebView2-specific APIs + * that are available to the worker running within WebView2 Runtime. + */ +interface WebView extends EventTarget { + /** + * When the worker calls `postMessage`, the `message` parameter is converted + * to JSON and is posted asynchronously to the WebView2 host process. + * This will result in either the + * `ICoreWebView2DedicatedWorker.WebMessageReceived` event or the + * `ICoreWebView2ServiceWorker.WebMessageReceived` event being raised, + * depending on if `postMessage` is called from the dedicated worker or + * service worker. + * @param message The message to send to the WebView2 host. This can be any + * object that can be serialized to JSON. + */ + postMessage(message: any) : void; + + /** + * The standard `EventTarget.addEventListener` method. Use it to subscribe + * to the `message` event. + * The `message` event receives messages posted from the WebView2 host via + * `PostWebMessageAsJson` or `PostWebMessageAsString`. + * + * @param type The name of the event to subscribe to. + */ + addEventListener(type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions): void; + /** + * The standard `EventTarget.removeEventListener` method. Use it to + * unsubscribe to the `message` event. + * + * @param type The name of the event to unsubscribe from. + */ + + removeEventListener(type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions): void; +} +```