Skip to content

Commit

Permalink
minor: Store BaseObject reference in JavaScript and use BaseObject to…
Browse files Browse the repository at this point in the history
… invoke functions in the object reference.
  • Loading branch information
mingyaulee committed Mar 26, 2021
1 parent 3110a0f commit db2e8c8
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Blazor.BrowserExtension" Version="0.1.0" />
<PackageReference Include="WebExtension.Net" Version="1.0.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.3" PrivateAssets="all" />
Expand Down
19 changes: 19 additions & 0 deletions src/WebExtension.Net/BaseAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace WebExtension.Net
{
/// <summary>
/// Base API class.
/// </summary>
public class BaseAPI : BaseObject
{
/// <summary>
/// Gets the WebExtensionJsRuntime instance.
/// </summary>
protected WebExtensionJSRuntime webExtensionJSRuntime;

internal BaseAPI(WebExtensionJSRuntime webExtensionJSRuntime, string apiNamespace)
{
this.webExtensionJSRuntime = webExtensionJSRuntime;
Initialize(webExtensionJSRuntime, "browser", apiNamespace);
}
}
}
95 changes: 87 additions & 8 deletions src/WebExtension.Net/BaseObject.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,100 @@
namespace WebExtension.Net
using System;
using System.Threading.Tasks;

namespace WebExtension.Net
{
/// <summary>
/// Base object returned from JavaScript.
/// </summary>
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;
}
}

/// <summary>
/// Initialize property if it is a base object
/// </summary>
/// <param name="propertyName"></param>
/// <param name="propertyValue"></param>
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);
}
}

/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <param name="propertyName">The function to invoke.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of TValue obtained by JSON-deserializing the return value.</returns>
protected ValueTask<TValue> GetPropertyAsync<TValue>(string propertyName, params object[] args)
{
var functionIdentifier = string.IsNullOrEmpty(accessPath) ? propertyName : $"{accessPath}.{propertyName}";
return webExtensionJSRuntime.InvokeAsync<TValue>("WebExtensionNet.InvokeOnObjectReference", new InvokeObjectReferenceOption(referenceId, functionIdentifier, false), args);
}

/// <summary>
/// Gets the WebExtensionJsRuntime instance.
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
protected WebExtensionJSRuntime webExtensionJSRuntime;
/// <param name="function">The function to invoke.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of TValue obtained by JSON-deserializing the return value.</returns>
protected ValueTask<TValue> InvokeAsync<TValue>(string function, params object[] args)
{
var functionIdentifier = string.IsNullOrEmpty(accessPath) ? function : $"{accessPath}.{function}";
return webExtensionJSRuntime.InvokeAsync<TValue>("WebExtensionNet.InvokeOnObjectReference", new InvokeObjectReferenceOption(referenceId, functionIdentifier, true), args);
}

/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <param name="function">The function to invoke.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>A System.Threading.Tasks.ValueTask that represents the asynchronous invocation operation.</returns>
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);
}

/// <summary>
/// Dispose the object
/// </summary>
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);
}
}

/// <summary>
/// Sets the WebExtensionJsRuntime instance.
/// Finalizer to call Dispose()
/// </summary>
/// <param name="webExtensionJSRuntime">The WebExtensionJsRuntime instance.</param>
public void SetClient(WebExtensionJSRuntime webExtensionJSRuntime)
~BaseObject()
{
this.webExtensionJSRuntime = webExtensionJSRuntime;
Dispose();
}
}
}
34 changes: 34 additions & 0 deletions src/WebExtension.Net/InvokeObjectReferenceOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace WebExtension.Net
{
/// <summary>
/// Invoke JavaScript object options.
/// </summary>
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;
}

/// <summary>
/// Reference ID of the JavaScript object.
/// </summary>
public string ReferenceId { get; set; }

/// <summary>
/// The target path of the JavaScript object.
/// </summary>
public string TargetPath { get; set; }

/// <summary>
/// The indicator if the target is a function.
/// </summary>
public bool IsFunction { get; set; }
}
}
13 changes: 13 additions & 0 deletions src/WebExtension.Net/InvokeOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace WebExtension.Net
{
/// <summary>
/// Invoke JavaScript options.
/// </summary>
public abstract class InvokeOption
{
/// <summary>
/// The reference ID to be used as a key for the JavaScript object returned.
/// </summary>
public string ReturnObjectReferenceId { get; set; }
}
}
25 changes: 21 additions & 4 deletions src/WebExtension.Net/WebExtensionJSRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.JSInterop;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace WebExtension.Net
Expand All @@ -21,21 +23,36 @@ public WebExtensionJSRuntime(IJSRuntime jsRuntime)
/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <param name="identifier">An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction.</param>
/// <param name="invokeOption">The option for invocation.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>An instance of TValue obtained by JSON-deserializing the return value.</returns>
public ValueTask<TValue> InvokeAsync<TValue>(params object[] args)
public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, InvokeOption invokeOption, params object[] args)
{
return jsRuntime.InvokeAsync<TValue>("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<TValue>(identifier, invokeArgs);
if (!string.IsNullOrEmpty(invokeOption.ReturnObjectReferenceId) && result is BaseObject baseObject)
{
baseObject.Initialize(this, invokeOption.ReturnObjectReferenceId, null);
}
return result;
}

/// <summary>
/// Invokes the specified JavaScript function asynchronously.
/// </summary>
/// <param name="identifier">An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction.</param>
/// <param name="invokeOption">The option for invocation.</param>
/// <param name="args">JSON-serializable arguments.</param>
/// <returns>A System.Threading.Tasks.ValueTask that represents the asynchronous invocation operation.</returns>
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);
}
}
}
52 changes: 36 additions & 16 deletions src/WebExtension.Net/content/WebExtensionScripts/WebExtensionNet.js
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit db2e8c8

Please sign in to comment.