From db2e8c81217dd8041ad5aae656aad4bff531e897 Mon Sep 17 00:00:00 2001 From: Ming Yau Lee Date: Fri, 26 Mar 2021 17:43:38 +0800 Subject: [PATCH] minor: Store BaseObject reference in JavaScript and use BaseObject to invoke functions in the object reference. --- ...Net.BrowserExtensionIntegrationTest.csproj | 1 + src/WebExtension.Net/BaseAPI.cs | 19 ++++ src/WebExtension.Net/BaseObject.cs | 95 +++++++++++++++++-- .../InvokeObjectReferenceOption.cs | 34 +++++++ src/WebExtension.Net/InvokeOption.cs | 13 +++ src/WebExtension.Net/WebExtensionJSRuntime.cs | 25 ++++- .../WebExtensionScripts/WebExtensionNet.js | 52 ++++++---- 7 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 src/WebExtension.Net/BaseAPI.cs create mode 100644 src/WebExtension.Net/InvokeObjectReferenceOption.cs create mode 100644 src/WebExtension.Net/InvokeOption.cs diff --git a/src/Tests/WebExtension.Net.BrowserExtensionIntegrationTest/WebExtension.Net.BrowserExtensionIntegrationTest.csproj b/src/Tests/WebExtension.Net.BrowserExtensionIntegrationTest/WebExtension.Net.BrowserExtensionIntegrationTest.csproj index 4f7b7a9..73382cf 100644 --- a/src/Tests/WebExtension.Net.BrowserExtensionIntegrationTest/WebExtension.Net.BrowserExtensionIntegrationTest.csproj +++ b/src/Tests/WebExtension.Net.BrowserExtensionIntegrationTest/WebExtension.Net.BrowserExtensionIntegrationTest.csproj @@ -8,6 +8,7 @@ + diff --git a/src/WebExtension.Net/BaseAPI.cs b/src/WebExtension.Net/BaseAPI.cs new file mode 100644 index 0000000..27dfb8e --- /dev/null +++ b/src/WebExtension.Net/BaseAPI.cs @@ -0,0 +1,19 @@ +namespace WebExtension.Net +{ + /// + /// Base API class. + /// + public class BaseAPI : BaseObject + { + /// + /// Gets the WebExtensionJsRuntime instance. + /// + protected WebExtensionJSRuntime webExtensionJSRuntime; + + internal BaseAPI(WebExtensionJSRuntime webExtensionJSRuntime, string apiNamespace) + { + this.webExtensionJSRuntime = webExtensionJSRuntime; + Initialize(webExtensionJSRuntime, "browser", apiNamespace); + } + } +} diff --git a/src/WebExtension.Net/BaseObject.cs b/src/WebExtension.Net/BaseObject.cs index 237d38c..49ba87c 100644 --- a/src/WebExtension.Net/BaseObject.cs +++ b/src/WebExtension.Net/BaseObject.cs @@ -1,21 +1,100 @@ -namespace WebExtension.Net +using System; +using System.Threading.Tasks; + +namespace WebExtension.Net { /// /// Base object returned from JavaScript. /// - public class BaseObject + public class BaseObject : IDisposable { + internal bool IsInitialized; + private WebExtensionJSRuntime webExtensionJSRuntime; + private string referenceId; + private string accessPath; + + internal void Initialize(WebExtensionJSRuntime webExtensionJSRuntime, string referenceId, string accessPath) + { + if (!IsInitialized) + { + IsInitialized = true; + this.webExtensionJSRuntime = webExtensionJSRuntime; + this.referenceId = referenceId; + this.accessPath = accessPath; + } + } + + /// + /// Initialize property if it is a base object + /// + /// + /// + protected void InitializeProperty(string propertyName, object propertyValue) + { + if (propertyValue is BaseObject baseObject && !baseObject.IsInitialized) + { + var propertyAccessPath = string.IsNullOrEmpty(accessPath) ? propertyName : $"{accessPath}.{propertyName}"; + baseObject.Initialize(webExtensionJSRuntime, referenceId, propertyAccessPath); + } + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The function to invoke. + /// JSON-serializable arguments. + /// An instance of TValue obtained by JSON-deserializing the return value. + protected ValueTask GetPropertyAsync(string propertyName, params object[] args) + { + var functionIdentifier = string.IsNullOrEmpty(accessPath) ? propertyName : $"{accessPath}.{propertyName}"; + return webExtensionJSRuntime.InvokeAsync("WebExtensionNet.InvokeOnObjectReference", new InvokeObjectReferenceOption(referenceId, functionIdentifier, false), args); + } + /// - /// Gets the WebExtensionJsRuntime instance. + /// Invokes the specified JavaScript function asynchronously. /// - protected WebExtensionJSRuntime webExtensionJSRuntime; + /// The function to invoke. + /// JSON-serializable arguments. + /// An instance of TValue obtained by JSON-deserializing the return value. + protected ValueTask InvokeAsync(string function, params object[] args) + { + var functionIdentifier = string.IsNullOrEmpty(accessPath) ? function : $"{accessPath}.{function}"; + return webExtensionJSRuntime.InvokeAsync("WebExtensionNet.InvokeOnObjectReference", new InvokeObjectReferenceOption(referenceId, functionIdentifier, true), args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The function to invoke. + /// JSON-serializable arguments. + /// A System.Threading.Tasks.ValueTask that represents the asynchronous invocation operation. + protected ValueTask InvokeVoidAsync(string function, params object[] args) + { + var functionIdentifier = string.IsNullOrEmpty(accessPath) ? function : $"{accessPath}.{function}"; + return webExtensionJSRuntime.InvokeVoidAsync("WebExtensionNet.InvokeOnObjectReference", new InvokeObjectReferenceOption(referenceId, functionIdentifier, true), args); + } + + /// + /// Dispose the object + /// + public void Dispose() + { + if (!string.IsNullOrEmpty(referenceId) && webExtensionJSRuntime != null) + { +#pragma warning disable CA2012 // Use ValueTasks correctly - Waiting is not supported in runtime + webExtensionJSRuntime.InvokeVoidAsync("WebExtensionNet.RemoveObjectReference", new InvokeObjectReferenceOption(referenceId)); +#pragma warning restore CA2012 // Use ValueTasks correctly + referenceId = null; + GC.SuppressFinalize(this); + } + } + /// - /// Sets the WebExtensionJsRuntime instance. + /// Finalizer to call Dispose() /// - /// The WebExtensionJsRuntime instance. - public void SetClient(WebExtensionJSRuntime webExtensionJSRuntime) + ~BaseObject() { - this.webExtensionJSRuntime = webExtensionJSRuntime; + Dispose(); } } } diff --git a/src/WebExtension.Net/InvokeObjectReferenceOption.cs b/src/WebExtension.Net/InvokeObjectReferenceOption.cs new file mode 100644 index 0000000..363000b --- /dev/null +++ b/src/WebExtension.Net/InvokeObjectReferenceOption.cs @@ -0,0 +1,34 @@ +namespace WebExtension.Net +{ + /// + /// Invoke JavaScript object options. + /// + public class InvokeObjectReferenceOption : InvokeOption + { + internal InvokeObjectReferenceOption(string referenceId) : this(referenceId, null) { } + + internal InvokeObjectReferenceOption(string referenceId, string targetPath) : this(referenceId, targetPath, false) { } + + internal InvokeObjectReferenceOption(string referenceId, string targetPath, bool isFunction) + { + ReferenceId = referenceId; + TargetPath = targetPath; + IsFunction = isFunction; + } + + /// + /// Reference ID of the JavaScript object. + /// + public string ReferenceId { get; set; } + + /// + /// The target path of the JavaScript object. + /// + public string TargetPath { get; set; } + + /// + /// The indicator if the target is a function. + /// + public bool IsFunction { get; set; } + } +} diff --git a/src/WebExtension.Net/InvokeOption.cs b/src/WebExtension.Net/InvokeOption.cs new file mode 100644 index 0000000..44d8838 --- /dev/null +++ b/src/WebExtension.Net/InvokeOption.cs @@ -0,0 +1,13 @@ +namespace WebExtension.Net +{ + /// + /// Invoke JavaScript options. + /// + public abstract class InvokeOption + { + /// + /// The reference ID to be used as a key for the JavaScript object returned. + /// + public string ReturnObjectReferenceId { get; set; } + } +} diff --git a/src/WebExtension.Net/WebExtensionJSRuntime.cs b/src/WebExtension.Net/WebExtensionJSRuntime.cs index cd3de98..7b7c53f 100644 --- a/src/WebExtension.Net/WebExtensionJSRuntime.cs +++ b/src/WebExtension.Net/WebExtensionJSRuntime.cs @@ -1,4 +1,6 @@ using Microsoft.JSInterop; +using System; +using System.Linq; using System.Threading.Tasks; namespace WebExtension.Net @@ -21,21 +23,36 @@ public WebExtensionJSRuntime(IJSRuntime jsRuntime) /// /// Invokes the specified JavaScript function asynchronously. /// + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The option for invocation. /// JSON-serializable arguments. /// An instance of TValue obtained by JSON-deserializing the return value. - public ValueTask InvokeAsync(params object[] args) + public async ValueTask InvokeAsync(string identifier, InvokeOption invokeOption, params object[] args) { - return jsRuntime.InvokeAsync("WebExtensionNet.Execute", args); + if (typeof(BaseObject).IsAssignableFrom(typeof(TValue))) + { + invokeOption.ReturnObjectReferenceId = Guid.NewGuid().ToString(); + } + var invokeArgs = new object[] { invokeOption }.Concat(args).ToArray(); + var result = await jsRuntime.InvokeAsync(identifier, invokeArgs); + if (!string.IsNullOrEmpty(invokeOption.ReturnObjectReferenceId) && result is BaseObject baseObject) + { + baseObject.Initialize(this, invokeOption.ReturnObjectReferenceId, null); + } + return result; } /// /// Invokes the specified JavaScript function asynchronously. /// + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The option for invocation. /// JSON-serializable arguments. /// A System.Threading.Tasks.ValueTask that represents the asynchronous invocation operation. - public ValueTask InvokeVoidAsync(params object[] args) + public ValueTask InvokeVoidAsync(string identifier, InvokeOption invokeOption, params object[] args) { - return jsRuntime.InvokeVoidAsync("WebExtensionNet.Execute", args); + var invokeArgs = new object[] { invokeOption }.Concat(args).ToArray(); + return jsRuntime.InvokeVoidAsync(identifier, invokeArgs); } } } diff --git a/src/WebExtension.Net/content/WebExtensionScripts/WebExtensionNet.js b/src/WebExtension.Net/content/WebExtensionScripts/WebExtensionNet.js index 745741e..20dd802 100644 --- a/src/WebExtension.Net/content/WebExtensionScripts/WebExtensionNet.js +++ b/src/WebExtension.Net/content/WebExtensionScripts/WebExtensionNet.js @@ -1,23 +1,43 @@ (function (global) { - global.WebExtensionNet = { - Execute: async function (api, ...args) { - let target = global.browser; - api.split(".").forEach(api => target = target?.[api]); + const objectReferences = { + }; + let objectReferencesCount = 0; + async function invokeOnObjectReference({ referenceId, targetPath, isFunction, returnObjectReferenceId }, ...args) { + let target = objectReferences[referenceId]; + if (referenceId === "browser") { + target = global.browser; + } + targetPath.split(".").forEach(functionName => target = target?.[functionName]); - if (!target) { - throw new Error(`Unable to find API browser.${api}`); - } + if (isFunction && !target) { + throw new Error(`Unable to find function ${targetPath} on object '${referenceId}'.`); + } - try { - let result = target.apply(target, args); - if (result instanceof Promise) { - result = await result; - } - return result; - } catch (error) { - console.error(api, args, target); - throw new Error(`Failed to execute browser.${api}: ${error.message}`); + try { + let result = isFunction ? target.apply(target, args) : target; + if (result instanceof Promise) { + result = await result; + } + if (returnObjectReferenceId) { + objectReferencesCount++; + objectReferences[returnObjectReferenceId] = result; } + return result; + } catch (error) { + console.error(referenceId, targetPath, args, target); + throw new Error(`Failed to execute function ${targetPath} on object '${referenceId}': ${error.message}`); } + } + async function removeObjectReference({ referenceId }) { + if (objectReferences[referenceId] !== null) { + objectReferencesCount--; + objectReferences[referenceId] = null; + } + } + global.WebExtensionNet = { + InvokeOnObjectReference: invokeOnObjectReference, + RemoveObjectReference: removeObjectReference, + GetObjectReferences: () => objectReferences, + GetObjectReferencesCount: () => objectReferencesCount }; })(globalThis); \ No newline at end of file