From 8f038338a5329b89b231f2b4223bca770bfce26b Mon Sep 17 00:00:00 2001 From: Kouji Matsui Date: Wed, 17 Apr 2024 11:39:48 +0900 Subject: [PATCH] Supported ArrayBuffer serialization. --- DupeNukem.Core/Internal/ByteArrayConverter.cs | 23 +- .../Internal/CancellationTokenConverter.cs | 31 ++- DupeNukem.Core/Internal/ClosureConverter.cs | 56 +++++ DupeNukem.Core/Internal/ConverterContext.cs | 117 ++++++++++ .../DeserializingRegisteredObjectRegistry.cs | 67 ------ DupeNukem.Core/Internal/Message.cs | 24 +- DupeNukem.Core/Internal/MethodDescriptors.cs | 125 ++++------- DupeNukem.Core/Internal/Utilities.cs | 7 + DupeNukem.Core/Messenger.cs | 206 ++++++++++-------- DupeNukem/Script.js | 145 +++++++----- DupeNukem/WebViewMessenger.cs | 4 + .../ViewModels/ContentPageViewModel.cs | 2 +- samples/DupeNukem.Maui/Views/MainPage.xaml | 3 +- .../ViewModels/TestFragments.cs | 49 +++-- 14 files changed, 526 insertions(+), 333 deletions(-) create mode 100644 DupeNukem.Core/Internal/ClosureConverter.cs create mode 100644 DupeNukem.Core/Internal/ConverterContext.cs delete mode 100644 DupeNukem.Core/Internal/DeserializingRegisteredObjectRegistry.cs diff --git a/DupeNukem.Core/Internal/ByteArrayConverter.cs b/DupeNukem.Core/Internal/ByteArrayConverter.cs index 3d72991..e5f0673 100644 --- a/DupeNukem.Core/Internal/ByteArrayConverter.cs +++ b/DupeNukem.Core/Internal/ByteArrayConverter.cs @@ -22,18 +22,29 @@ public override bool CanConvert(Type objectType) => objectType.Equals(type); public override object? ReadJson( - JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => - reader.ReadAsString() is { } body ? - Convert.FromBase64String(body) : - null; + JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + ConverterContext.AssertValidState(); + + var typedValue = serializer.Deserialize(reader); + if (typedValue.Type == TypedValueTypes.ByteArray) + { + var base64 = typedValue.Body.ToObject(serializer)!; + return Convert.FromBase64String(base64); + } + return null; + } public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer) { + ConverterContext.AssertValidState(); + if (value is byte[] arr) { - var body = Convert.ToBase64String(arr); - writer.WriteValue(body); + var base64 = Convert.ToBase64String(arr); + var typedValue = new TypedValue(TypedValueTypes.ByteArray, base64); + serializer.Serialize(writer, typedValue); } else { diff --git a/DupeNukem.Core/Internal/CancellationTokenConverter.cs b/DupeNukem.Core/Internal/CancellationTokenConverter.cs index 3eb7f1c..2481af4 100644 --- a/DupeNukem.Core/Internal/CancellationTokenConverter.cs +++ b/DupeNukem.Core/Internal/CancellationTokenConverter.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace DupeNukem.Internal; @@ -45,16 +46,18 @@ public Task CancelAsync() public override object? ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - var body = serializer.Deserialize(reader); - if (!string.IsNullOrEmpty(body.Scope)) + ConverterContext.AssertValidState(); + + var value = serializer.Deserialize(reader); + if (value.Type == TypedValueTypes.AbortSignal && + value.Body.ToObject(serializer) is { } body && + !string.IsNullOrEmpty(body.Scope)) { // Once the CancellationToken argument is found, the CancellationTokenProxy is generated and // make this instance visible from the JavaScript side. - // (Actually, it will be extracted and registered later from the DeserializingRegisteredObjectRegistry.) // By being called from the JavaScript side, as in "{Id}.cancel". - // CancellationTokenSource.Cancel() is called which is held internally. var ctp = new CancellationTokenProxy(); - DeserializingRegisteredObjectRegistry.TryCapture(body.Scope, ctp); + ConverterContext.Current.RegisterObject(body.Scope, ctp); // Already aborted: if (body.Aborted) @@ -62,7 +65,7 @@ public Task CancelAsync() // Cancel now. ctp.Cancel(); } - + return ctp.Token; } else @@ -74,9 +77,19 @@ public Task CancelAsync() public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer) { - var ct = value is CancellationToken c ? c : default; + ConverterContext.AssertValidState(); - // TODO: - //writer.WriteValue(tag.Name); + if (value is CancellationToken c) + { + var scope = $"abortSignal_{0}"; // TODO: + var cancellationTokenBody = new CancellationTokenBody(scope, c.IsCancellationRequested); + var body = JToken.FromObject(cancellationTokenBody); + var typedValue = new TypedValue(TypedValueTypes.AbortSignal, body); + serializer.Serialize(writer, typedValue); + } + else + { + writer.WriteNull(); + } } } diff --git a/DupeNukem.Core/Internal/ClosureConverter.cs b/DupeNukem.Core/Internal/ClosureConverter.cs new file mode 100644 index 0000000..997c173 --- /dev/null +++ b/DupeNukem.Core/Internal/ClosureConverter.cs @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////// +// +// DupeNukem - WebView attachable full-duplex asynchronous interoperable +// independent messaging library between .NET and JavaScript. +// +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Threading; +using Newtonsoft.Json; + +namespace DupeNukem.Internal; + +internal sealed class ClosureConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) => + typeof(Delegate).IsAssignableFrom(objectType); + + public override object? ReadJson( + JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + ConverterContext.AssertValidState(); + + var typedValue = serializer.Deserialize(reader); + if (typedValue.Type == TypedValueTypes.Closure) + { + var name = typedValue.Body.ToObject(serializer)!; + if (name.StartsWith("__peerClosures__.closure_$")) + { + return ConverterContext.Current.RegisterPeerClosure(name, objectType); + } + } + return null; + } + + public override void WriteJson( + JsonWriter writer, object? value, JsonSerializer serializer) + { + ConverterContext.AssertValidState(); + + if (value is Delegate dlg) + { + var name = ConverterContext.Current.RegisterHostClosure(dlg); + var typedValue = new TypedValue(TypedValueTypes.Closure, name); + serializer.Serialize(writer, typedValue); + } + else + { + writer.WriteNull(); + } + } +} diff --git a/DupeNukem.Core/Internal/ConverterContext.cs b/DupeNukem.Core/Internal/ConverterContext.cs new file mode 100644 index 0000000..ebc7d10 --- /dev/null +++ b/DupeNukem.Core/Internal/ConverterContext.cs @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////// +// +// DupeNukem - WebView attachable full-duplex asynchronous interoperable +// independent messaging library between .NET and JavaScript. +// +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Diagnostics; +using System.Threading; + +namespace DupeNukem.Internal; + +internal static class ConverterContext +{ + // JsonSerializer cannot pass application-specific context information + // to the Converter during serialization runs. + // The ConverterContext class is used to identify the corresponding Messenger instance + // during serialization. + + private sealed class MessengerContext + { + private IMessenger? messenger; + private int count; + + public IMessenger Current + { + get + { + Debug.Assert(this.messenger != null); + Debug.Assert(this.count >= 1); + return this.messenger!; + } + } + + public void Enter(IMessenger messenger) + { + if (this.count <= 0) + { + Debug.Assert(this.messenger == null); + this.messenger = messenger; + this.count = 1; + } + else + { + Debug.Assert(this.messenger == messenger); + this.count++; + } + } + + public void Exit(IMessenger messenger) + { + Debug.Assert(this.messenger == messenger); + Debug.Assert(this.count >= 1); + this.count--; + if (this.count <= 0) + { + this.messenger = null!; + this.count = 0; + } + } + } + + private static readonly ThreadLocal messengers = + new(() => new MessengerContext()); + + public static Messenger Current + { + get + { + AssertValidState(); + return (Messenger)messengers.Value!.Current; + } + } + + [Conditional("DEBUG")] + public static void AssertValidState() => + Debug.Assert( + messengers.Value!.Current is Messenger, + "Invalid state: Not called correctly."); + + public static void Enter(IMessenger messenger) => + messengers.Value!.Enter(messenger); + + public static void Exit(IMessenger messenger) => + messengers.Value!.Exit(messenger); + + public static void Run(IMessenger messenger, Action action) + { + messengers.Value!.Enter(messenger); + try + { + action(); + } + finally + { + messengers.Value!.Exit(messenger); + } + } + + public static T Run(IMessenger messenger, Func action) + { + messengers.Value!.Enter(messenger); + try + { + return action(); + } + finally + { + messengers.Value!.Exit(messenger); + } + } +} diff --git a/DupeNukem.Core/Internal/DeserializingRegisteredObjectRegistry.cs b/DupeNukem.Core/Internal/DeserializingRegisteredObjectRegistry.cs deleted file mode 100644 index 05f73df..0000000 --- a/DupeNukem.Core/Internal/DeserializingRegisteredObjectRegistry.cs +++ /dev/null @@ -1,67 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// DupeNukem - WebView attachable full-duplex asynchronous interoperable -// independent messaging library between .NET and JavaScript. -// -// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) -// -// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 -// -//////////////////////////////////////////////////////////////////////////// - -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace DupeNukem.Internal; - -internal sealed class DeserializingRegisteredObjectRegistry -{ - // HACK: During deserialization of Newtonsoft.Json, there is no way to pass an arbitrary context - // on the deserialization scope. This means that custom instances (e.g., CancellationToken) - // found during deserialization cannot be recorded after they are extracted. - // Therefore, they are kept in thread-local storage so that custom instances can be extracted - // after deserialization is complete. - // Note that this is thread-local storage, not asynchronous local storage. - // In other words, it is assumed that no asynchronous context switches occur during deserialization. - private static readonly ThreadLocal dror = - new(() => new DeserializingRegisteredObjectRegistry()); - - private readonly Dictionary objects = new(); - - private DeserializingRegisteredObjectRegistry() - { - } - - /////////////////////////////////////////////////// - - private void InternalBegin() => - this.objects.Clear(); - - private bool InternalTryCapture(string name, object value) - { - if (!this.objects.ContainsKey(name)) - { - this.objects.Add(name, value); - return true; - } - else - { - return false; - } - } - - private KeyValuePair[] InternalFinish() => - this.objects.ToArray(); - - /////////////////////////////////////////////////// - - public static void Begin() => - dror.Value!.InternalBegin(); - - public static bool TryCapture(string name, object value) => - dror.Value!.InternalTryCapture(name, value); - - public static KeyValuePair[] Finish() => - dror.Value!.InternalFinish(); -} diff --git a/DupeNukem.Core/Internal/Message.cs b/DupeNukem.Core/Internal/Message.cs index f5d2833..87804f1 100644 --- a/DupeNukem.Core/Internal/Message.cs +++ b/DupeNukem.Core/Internal/Message.cs @@ -26,7 +26,6 @@ public enum MessageTypes Succeeded, Failed, Invoke, - Closure, } [EditorBrowsable(EditorBrowsableState.Advanced)] @@ -86,6 +85,29 @@ public ExceptionBody( } } +internal enum TypedValueTypes +{ + Closure, + AbortSignal, + ByteArray, +} + +internal readonly struct TypedValue +{ + [JsonProperty("__type__")] + public readonly TypedValueTypes Type; + + [JsonProperty("__body__")] + public readonly JToken Body; + + [JsonConstructor] + public TypedValue(TypedValueTypes __type__, JToken __body__) + { + this.Type = __type__; + this.Body = __body__; + } +} + internal readonly struct CancellationTokenBody { [JsonProperty("__scope__")] diff --git a/DupeNukem.Core/Internal/MethodDescriptors.cs b/DupeNukem.Core/Internal/MethodDescriptors.cs index 0e13c5a..6afa1e0 100644 --- a/DupeNukem.Core/Internal/MethodDescriptors.cs +++ b/DupeNukem.Core/Internal/MethodDescriptors.cs @@ -10,13 +10,11 @@ //////////////////////////////////////////////////////////////////////////// using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; - using Newtonsoft.Json.Linq; namespace DupeNukem.Internal; @@ -49,72 +47,37 @@ private protected MethodDescriptor(IMessenger messenger, MethodMetadata metadata public abstract Task InvokeAsync(JToken?[] args); - protected T ToObject(JToken? arg) => - arg switch - { - // Null. - null => default!, - // Function closure comes from JavaScript. - JObject jo when - jo.ToObject(this.messenger.Serializer) is { } m && - m.Id == "descriptor" && m.Type == MessageTypes.Closure && - m.Body?.ToObject(this.messenger.Serializer) is { } name && - name.StartsWith("__peerClosures__.closure_$") && - typeof(Delegate).IsAssignableFrom(typeof(T)) => - (T)(object)this.messenger.RegisterPeerClosure(name, typeof(T))!, - // Other value types. - _ => arg.ToObject(this.messenger.Serializer)!, - }; - - protected object? ToObject(JToken? arg, Type type) => - arg switch - { - // Null. - null => default!, - // Function closure comes from JavaScript. - JObject jo when - jo.ToObject(this.messenger.Serializer) is { } m && - m.Id == "descriptor" && m.Type == MessageTypes.Closure && - m.Body?.ToObject(this.messenger.Serializer) is { } name && - name.StartsWith("__peerClosures__.closure_$") && - typeof(Delegate).IsAssignableFrom(type) => - this.messenger.RegisterPeerClosure(name, type), - // Other value types. - _ => arg.ToObject(type, this.messenger.Serializer)!, - }; - - protected void BeginCapturingArguments() => - DeserializingRegisteredObjectRegistry.Begin(); - - protected IDisposable FinishCapturingArguments() + private protected T ToObject(JToken? arg) => + arg is { } a ? + a.ToObject(this.messenger.Serializer)! : + default!; + + private protected object? ToObject(JToken? arg, Type type) => + arg is { } a ? + a.ToObject(type, this.messenger.Serializer)! : + type.IsValueType() ? + Activator.CreateInstance(type) : + null; + + private protected IDisposable BeginConverterContext() { - // Enumerate the custom instances that have occurred since the call to BeginCapturingArguments(), - // register them as temporary instances, and make them available for reference from the other side. - // And the registration is released when call to Disposer.Dispose(). - var objects = DeserializingRegisteredObjectRegistry.Finish(); - foreach (var entry in objects) - { - this.messenger.RegisterObject(entry.Key, entry.Value, false); - } - return new Disposer(this.messenger, objects); + ConverterContext.Enter(this.messenger); + return new Disposer(this.messenger); } private sealed class Disposer : IDisposable { - private readonly IMessenger messenger; - private readonly KeyValuePair[] objects; + private IMessenger? messenger; - public Disposer(IMessenger messenger, KeyValuePair[] objects) - { + public Disposer(IMessenger messenger) => this.messenger = messenger; - this.objects = objects; - } public void Dispose() { - foreach (var entry in this.objects) + if (this.messenger is { } messenger) { - this.messenger.UnregisterObject(entry.Key, entry.Value); + this.messenger = null; + ConverterContext.Exit(messenger); } } } @@ -151,9 +114,9 @@ public ActionDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); await this.action(arg0). ConfigureAwait(false); @@ -172,10 +135,10 @@ public ActionDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); await this.action(arg0, arg1). ConfigureAwait(false); @@ -194,11 +157,11 @@ public ActionDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); var arg2 = base.ToObject(args[2]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); await this.action(arg0, arg1, arg2). ConfigureAwait(false); @@ -217,12 +180,12 @@ public ActionDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); var arg2 = base.ToObject(args[2]); var arg3 = base.ToObject(args[3]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); await this.action(arg0, arg1, arg2, arg3). ConfigureAwait(false); @@ -260,9 +223,9 @@ public FuncDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); return await this.func(arg0). ConfigureAwait(false); @@ -280,10 +243,10 @@ public FuncDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); return await this.func(arg0, arg1). ConfigureAwait(false); @@ -301,11 +264,11 @@ public FuncDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); var arg2 = base.ToObject(args[2]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); return await this.func(arg0, arg1, arg2). ConfigureAwait(false); @@ -323,12 +286,12 @@ public FuncDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var arg0 = base.ToObject(args[0]); var arg1 = base.ToObject(args[1]); var arg2 = base.ToObject(args[2]); var arg3 = base.ToObject(args[3]); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); return await this.func(arg0, arg1, arg2, arg3). ConfigureAwait(false); @@ -357,11 +320,11 @@ public ObjectMethodDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var cas = args. Select((arg, index) => base.ToObject(arg, this.parameterTypes[index])). ToArray(); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); var task = (Task)this.method.Invoke(this.target, cas)!; return await TaskResultGetter.GetResultAsync(task); @@ -389,11 +352,11 @@ public DynamicMethodDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var cas = args. Select((arg, index) => base.ToObject(arg, this.parameterTypes[index])). ToArray(); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); await ((Task)this.method.DynamicInvoke(cas)!). ConfigureAwait(false); @@ -420,11 +383,11 @@ public DynamicMethodDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var cas = args. Select((arg, index) => base.ToObject(arg, this.parameterTypes[index])). ToArray(); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); var result = await ((Task)this.method.DynamicInvoke(cas)!). ConfigureAwait(false); @@ -453,11 +416,11 @@ public DynamicFunctionDescriptor( public override async Task InvokeAsync(JToken?[] args) { - base.BeginCapturingArguments(); + using var cc = base.BeginConverterContext(); var cas = args. Select((arg, index) => base.ToObject(arg, this.parameterTypes[index])). ToArray(); - using var _ = base.FinishCapturingArguments(); + cc.Dispose(); var task = (Task)this.function.DynamicInvoke(cas)!; return await TaskResultGetter.GetResultAsync(task); diff --git a/DupeNukem.Core/Internal/Utilities.cs b/DupeNukem.Core/Internal/Utilities.cs index be4a45e..c322e6a 100644 --- a/DupeNukem.Core/Internal/Utilities.cs +++ b/DupeNukem.Core/Internal/Utilities.cs @@ -87,6 +87,13 @@ internal static IEnumerable Append( /////////////////////////////////////////////////////////////////////////////// + internal static bool IsValueType(this Type type) => +#if NETSTANDARD1_3 || NETSTANDARD1_4 || NETSTANDARD1_5 || NETSTANDARD1_6 + type.GetTypeInfo().IsValueType; +#else + type.IsValueType; +#endif + internal static string GetName(Type type) { var tn = type.Name.LastIndexOf('`') is { } index && index >= 0 ? diff --git a/DupeNukem.Core/Messenger.cs b/DupeNukem.Core/Messenger.cs index fbd6918..34626bc 100644 --- a/DupeNukem.Core/Messenger.cs +++ b/DupeNukem.Core/Messenger.cs @@ -86,6 +86,7 @@ public static JsonSerializer GetDefaultJsonSerializer() serializer.Converters.Add(new StringEnumConverter(defaultNamingStrategy)); serializer.Converters.Add(new ByteArrayConverter()); serializer.Converters.Add(new CancellationTokenConverter()); + serializer.Converters.Add(new ClosureConverter()); return serializer; } @@ -110,15 +111,19 @@ public Messenger( this.timeoutTimer = new(this.ReachTimeout, null, 0, 0); this.peerClosureRegistry = new(name => { - var request = new Message( - "discard", - MessageTypes.Closure, - JToken.FromObject(name, this.Serializer)); + var bodyJson = ConverterContext.Run(this, () => + { + var request = new Message( + "discard", + MessageTypes.Control, + JToken.FromObject(name, this.Serializer)); - var tw = new StringWriter(); - this.Serializer.Serialize(tw, request); + var tw = new StringWriter(); + this.Serializer.Serialize(tw, request); + return tw.ToString(); + }); - this.SendMessageToPeer(tw.ToString()); + this.SendMessageToPeer(bodyJson); Trace.WriteLine($"DupeNukem: Sent discarded closure delegate: {name}"); }); @@ -207,6 +212,16 @@ public string[] RegisteredMethods return dlg; } + internal string RegisterHostClosure(Delegate closure) + { + var name = "closure_$" + Interlocked.Increment(ref this.id); + this.RegisterMethod( + name, + new DynamicFunctionDescriptor(closure, this), + true); + return name; + } + /////////////////////////////////////////////////////////////////////////////// [EditorBrowsable(EditorBrowsableState.Advanced)] @@ -282,31 +297,6 @@ private void ValidateArgumentType(object? arg) } } - private JToken? SerializeArgument(object? arg) - { - switch (arg) - { - case null: - return null; - case Delegate closure: - var name = "closure_$" + Interlocked.Increment(ref this.id); - this.RegisterMethod( - name, - new DynamicFunctionDescriptor(closure, this), - true); - return JToken.FromObject( - new Message( - "descriptor", - MessageTypes.Closure, - JToken.FromObject(name, this.Serializer)), - this.Serializer); - default: - return JToken.FromObject( - arg, - this.Serializer); - } - } - private void SendInvokeMessageToPeer( SuspendingDescriptor descriptor, CancellationToken ct, string functionName, object?[] args) @@ -336,19 +326,22 @@ private void SendInvokeMessageToPeer( this.suspendings.SafeAdd(id, descriptor); - var body = new InvokeBody( - functionName, - args.Select(this.SerializeArgument). - ToArray()); - var request = new Message( - id, - MessageTypes.Invoke, - JToken.FromObject(body, this.Serializer)); + var bodyJson = ConverterContext.Run(this, () => + { + var body = new InvokeBody( + functionName, + JArray.FromObject(args, this.Serializer).ToArray()); + var request = new Message( + id, + MessageTypes.Invoke, + JToken.FromObject(body, this.Serializer)); - var tw = new StringWriter(); - this.Serializer.Serialize(tw, request); + var tw = new StringWriter(); + this.Serializer.Serialize(tw, request); + return tw.ToString(); + }); - this.SendMessageToPeer(tw.ToString()); + this.SendMessageToPeer(bodyJson); } public Task InvokePeerMethodAsync( @@ -389,34 +382,61 @@ public Task InvokePeerMethodAsync( protected void SendControlMessageToPeer( string controlId, object? message) { - var request = new Message( - controlId, - MessageTypes.Control, - message != null ? - JToken.FromObject(message, this.Serializer) : - null); - var tw = new StringWriter(); - this.Serializer.Serialize(tw, request); - this.SendMessageToPeer(tw.ToString()); + var requestJson = ConverterContext.Run(this, () => + { + var request = new Message( + controlId, + MessageTypes.Control, + message != null ? + JToken.FromObject(message, this.Serializer) : + null); + + var tw = new StringWriter(); + this.Serializer.Serialize(tw, request); + return tw.ToString(); + }); + + this.SendMessageToPeer(requestJson); } protected virtual void OnReceivedControlMessage( string controlId, JToken? body) { + switch (controlId) + { + case "discard": + // Decline invalid name to avoid security attacks. + var name = ConverterContext.Run(this, () => + body!.ToObject(this.Serializer)); + if (name?.StartsWith("closure_$") ?? false) + { + this.methods.SafeRemove(name); + Trace.WriteLine($"DupeNukem: Deleted peer closure target delegate: {name}"); + } + break; + + default: + throw new InvalidOperationException( + $"Unknown control id: {controlId}"); + } } private void SendExceptionMessageToPeer( Message message, ExceptionBody responseBody) { - var response = new Message( - message.Id, - MessageTypes.Failed, - JToken.FromObject(responseBody, this.Serializer)); + var responseJson = ConverterContext.Run(this, () => + { + var response = new Message( + message.Id, + MessageTypes.Failed, + JToken.FromObject(responseBody, this.Serializer)); - var tw = new StringWriter(); - this.Serializer.Serialize(tw, response); + var tw = new StringWriter(); + this.Serializer.Serialize(tw, response); + return tw.ToString(); + }); - this.SendMessageToPeer(tw.ToString()); + this.SendMessageToPeer(responseJson); } public async void ReceivedRequest(string jsonString) @@ -448,7 +468,8 @@ public async void ReceivedRequest(string jsonString) if (this.suspendings.SafeTryGetValue(message.Id, out var failureDescriptor)) { this.suspendings.SafeRemove(message.Id); - var error = message.Body!.ToObject(this.Serializer); + var error = ConverterContext.Run(this, () => + message.Body!.ToObject(this.Serializer)); try { throw new PeerInvocationException( @@ -468,8 +489,8 @@ public async void ReceivedRequest(string jsonString) case MessageTypes.Invoke: try { - var body = message.Body!.ToObject(this.Serializer); - + var body = ConverterContext.Run(this, () => + message.Body!.ToObject(this.Serializer)); if (this.methods.SafeTryGetValue(body.Name, out var method)) { await this.SynchContext.Bind(); @@ -477,16 +498,21 @@ public async void ReceivedRequest(string jsonString) var result = await method.InvokeAsync(body.Args). ConfigureAwait(false); - var response = new Message( - message.Id, MessageTypes.Succeeded, - (result != null) ? - JToken.FromObject(result, this.Serializer) : - null); - - var tw = new StringWriter(); - this.Serializer.Serialize(tw, response); - - this.SendMessageToPeer(tw.ToString()); + var responseJson = ConverterContext.Run(this, () => + { + var response = new Message( + message.Id, + MessageTypes.Succeeded, + (result != null) ? + JToken.FromObject(result, this.Serializer) : + null); + + var tw = new StringWriter(); + this.Serializer.Serialize(tw, response); + return tw.ToString(); + }); + + this.SendMessageToPeer(responseJson); } else { @@ -501,32 +527,26 @@ public async void ReceivedRequest(string jsonString) } catch (Exception ex) { - var props = Utilities.ExtractExceptionProperties( - ex, this.MemberAccessNamingStrategy); + ConverterContext.Run(this, () => + { + var props = Utilities.ExtractExceptionProperties( + ex, this.MemberAccessNamingStrategy); - var responseBody = new ExceptionBody( - ex.GetType().FullName!, ex.Message, - this.SendExceptionWithStackTrace ? - (ex.StackTrace ?? string.Empty) : - string.Empty, - props); + var responseBody = new ExceptionBody( + ex.GetType().FullName!, ex.Message, + this.SendExceptionWithStackTrace ? + (ex.StackTrace ?? string.Empty) : + string.Empty, + props); - this.SendExceptionMessageToPeer(message, responseBody); + this.SendExceptionMessageToPeer(message, responseBody); + }); } break; - case MessageTypes.Closure: - switch (message.Id) - { - case "discard" when - // Decline invalid name to avoid security attacks. - message.Body!.ToObject(this.Serializer) is { } name && - name.StartsWith("closure_$"): - this.methods.SafeRemove(name); - Trace.WriteLine($"DupeNukem: Deleted peer closure target delegate: {name}"); - break; - } - break; + default: + throw new InvalidOperationException( + $"Unknown message type: {message.Type}"); } } catch (Exception ex) diff --git a/DupeNukem/Script.js b/DupeNukem/Script.js index 28d100d..245f090 100644 --- a/DupeNukem/Script.js +++ b/DupeNukem/Script.js @@ -21,7 +21,7 @@ var __dupeNukem_Messenger__ = this.registry__ = new FinalizationRegistry(name => { if (window.__dupeNukem_Messenger_sendToHostMessage__ != null) { window.__dupeNukem_Messenger_sendToHostMessage__( - JSON.stringify({ id: "discard", type: "closure", body: name, })); + JSON.stringify({ id: "discard", type: "control", body: name, })); this.log__("DupeNukem: Sent discarded closure function: " + name); } }); @@ -61,37 +61,71 @@ var __dupeNukem_Messenger__ = { id: message.id, type: "failed", body: exceptionBody, })) }; - this.new_cto__ = (aborted) => { - const scope = "cancellationToken_" + (this.id__++); + this.constructClosure__ = f => { + if (f.__dupeNukem_id__ === undefined) { + const closureId = "closure_$" + (this.id__++); + f.__dupeNukem_id__ = closureId; + window.__peerClosures__[closureId] = f; + const name = "__peerClosures__." + closureId; + return { + __type__: "closure", + __body__: name + }; + } else { + const closureId = f.__dupeNukem_id__; + const name = "__peerClosures__." + closureId; + return { + __type__: "closure", + __body__: name + }; + } + }; + + this.constructAbortSignal__ = signal => { + if (signal.__dupeNukem_id__ === undefined) { + const signalId = "abortSignal_$" + (this.id__++); + signal.__dupeNukem_id__ = signalId; + if (!signal.aborted) { + signal.addEventListener( + "abort", + () => window.invokeHostMethod(signalId + ".cancel")); + } + return { + __type__: "abortSignal", + __body__: { __scope__: signalId, __aborted__: signal.aborted } + }; + } else { + const signalId = signal.__dupeNukem_id__; + return { + __type__: "abortSignal", + __body__: { __scope__: signalId, __aborted__: signal.aborted } + }; + } + }; + + this.constructArray__ = arr => { + const base64 = btoa(new TextDecoder().decode(arr)); return { - __scope__: scope, - __aborted__: aborted, + __type__: "byteArray", + __body__: base64 }; }; this.normalizeObjects__ = obj => { if (obj instanceof Function) { - const baseName = "closure_$" + (this.id__++); - window.__peerClosures__[baseName] = obj; - const name = "__peerClosures__." + baseName; - return { id: "descriptor", type: "closure", body: name, }; + return this.constructClosure__(obj); } else if (obj instanceof ArrayBuffer) { - const arr = new Uint8Array(obj); - return Buffer.from(arr).toString("base64"); + return this.constructArray__(new Uint8Array(obj)); } else if (obj instanceof Uint8Array) { - return Buffer.from(obj).toString("base64"); + return this.constructArray__(obj); } else if (obj instanceof Uint8ClampedArray) { - return Buffer.from(obj).toString("base64"); + return this.constructArray__(obj); } else if (obj instanceof Array) { return obj.map(this.normalizeObjects__); } else if (obj instanceof AbortSignal) { - const cto = this.new_cto__(obj.aborted); - if (!obj.aborted) { - obj.addEventListener( - "abort", - () => window.invokeHostMethod(cto.__scope__ + ".cancel")); - } - return cto; + return this.constructAbortSignal__(obj); + } else if (obj instanceof CancellationToken) { + return this.constructAbortSignal__(obj.__ac__.signal); } else if (obj instanceof Object) { const newobj = {}; for (const [k, v] of Object.entries(obj)) { @@ -106,26 +140,31 @@ var __dupeNukem_Messenger__ = this.unnormalizeObjects__ = obj => { if (obj === undefined || obj === null) { return obj; - } else if (obj.id === "descriptor" && obj.type !== undefined && obj.body !== undefined) { - switch (obj.type) { + } else if (obj.__type__ !== undefined && obj.__body__ !== undefined) { + switch (obj.__type__) { case "closure": - if (obj.body.startsWith("closure_$")) { - const name = obj.body; + if (obj.__body__.startsWith("closure_$")) { + const closureId = obj.__body__; const cb = function () { const args = new Array(arguments.length); for (let i = 0; i < args.length; i++) { args[i] = arguments[i]; } - return window.__dupeNukem_Messenger__.invokeHostMethod__(name, args); + return window.__dupeNukem_Messenger__.invokeHostMethod__(closureId, args); }; - this.registry__.register(cb, name); + this.registry__.register(cb, closureId); return cb; } break; - case "bytearray": - if (obj.body !== null) { - const arr = Uint8Array.from(Buffer.from(obj.body, "base64")); - return arr; + case "abortSignal": + // TODO: + console.warn("DupeNukem: CancellationToken in host is not supported."); + break; + case "byteArray": + if (obj.__body__ !== null) { + const base64 = obj.__body__; + const arr = new TextEncoder().encode(atob(base64)); + return arr.buffer; } else { return null; } @@ -149,18 +188,17 @@ var __dupeNukem_Messenger__ = const message = JSON.parse(jsonString); switch (message.type) { case "succeeded": - this.log__("DupeNukem: succeeded: " + message.id); + this.log__("DupeNukem: host message: succeeded: " + message.id); const successorDescriptor = this.suspendings__.get(message.id); if (successorDescriptor !== undefined) { this.suspendings__.delete(message.id); successorDescriptor.resolve(this.unnormalizeObjects__(message.body)); - } - else { + } else { console.warn("DupeNukem: suprious message received: " + jsonString); } break; case "failed": - this.log__("DupeNukem: failed: " + message.id); + this.log__("DupeNukem: host message: failed: " + message.id); const failureDescriptor = this.suspendings__.get(message.id); if (failureDescriptor !== undefined) { this.suspendings__.delete(message.id); @@ -169,14 +207,13 @@ var __dupeNukem_Messenger__ = e.detail = message.body.detail; e.props = message.body.props; failureDescriptor.reject(e); - } - else { - console.warn("DupeNukem: suprious message received: " + jsonString); + } else { + console.warn("DupeNukem: host message: suprious message received: " + jsonString); } break; case "invoke": try { - this.log__("DupeNukem: invoke: " + message.body.name + "(...)"); + this.log__("DupeNukem: host message: invoke: " + message.body.name + "(...)"); const ne = message.body.name.split("."); const ti = ne. slice(0, ne.length - 1). @@ -187,8 +224,7 @@ var __dupeNukem_Messenger__ = this.sendExceptionToHost__(message, { name: "invalidFieldName", message: "Field \"" + n + "\" is not found.", detail: "", }); } return next; - } - else { + } else { return o; } }, window); @@ -197,21 +233,19 @@ var __dupeNukem_Messenger__ = const f = ti[fn]; if (f === undefined) { this.sendExceptionToHost__(message, { name: "invalidFunctionName", message: "Function \"" + fn + "\" is not found.", detail: "", }); - } - else { + } else { const args = message.body.args.map(this.unnormalizeObjects__); f.apply(ti, args). then(result => window.__dupeNukem_Messenger_sendToHostMessage__(JSON.stringify({ id: message.id, type: "succeeded", body: this.normalizeObjects__(result), }))). catch(e => this.sendExceptionToHost__(message, { name: e.name, message: e.message, detail: e.toString(), })); } } - } - catch (e) { + } catch (e) { this.sendExceptionToHost__(message, { name: e.name, message: e.message, detail: e.toString(), }); } break; case "control": - this.log__("DupeNukem: control: " + message.id + ": " + message.body); + this.log__("DupeNukem: host message: control: " + message.id + ": " + message.body); switch (message.id) { case "inject": this.injectProxy__(message.body); @@ -219,11 +253,6 @@ var __dupeNukem_Messenger__ = case "delete": this.deleteProxy__(message.body); break; - } - break; - case "closure": - this.log__("DupeNukem: closure: " + message.id + ": " + message.body); - switch (message.id) { case "discard": // Decline invalid name to avoid security attacks. if (message.body.startsWith("__peerClosures__.closure_$")) { @@ -232,12 +261,14 @@ var __dupeNukem_Messenger__ = this.log__("DupeNukem: Deleted peer closure target function: " + baseName); } break; + default: + throw new Error("Invalid host message id: " + message.id); } break; } } catch (e) { - console.warn("DupeNukem: unknown error: " + e.message + ": " + jsonString); + console.warn("DupeNukem: host message: unknown error: " + e.message + ": " + jsonString); } }; @@ -249,8 +280,7 @@ var __dupeNukem_Messenger__ = const descriptor = { resolve: resolve, reject: reject, }; this.suspendings__.set(id, descriptor); window.__dupeNukem_Messenger_sendToHostMessage__(JSON.stringify({ id: id, type: "invoke", body: { name: name, args: rargs, }, })); - } - catch (e) { + } catch (e) { reject(e); } }); @@ -356,8 +386,7 @@ var __dupeNukem_invokeHostMethod__ = const obsolete = entry.obsolete; if (obsolete === "obsolete") { console.warn(entry.obsoleteMessage); - } - else if (obsolete === "error") { + } else if (obsolete === "error") { throw new Error(entry.obsoleteMessage); } const args = new Array(arguments.length - 1); @@ -390,14 +419,14 @@ var delay = ////////////////////////////////////////////////// -// CancellationToken declaration. +// CancellationToken declaration (obsoleted). var CancellationToken = CancellationToken || function () { const warn = function () { console.warn("DupeNukem: CancellationToken is obsoleted, will be removed in future release. You have to switch to use AbortController and AbortSignal on ECMAScript standard instead."); } - this.__scope__ = "cancellationToken_" + (window.__dupeNukem_Messenger__.id__++); - this.cancel = () => { warn(); window.invokeHostMethod(this.__scope__ + ".cancel"); }; + this.__ac__ = new AbortController(); + this.cancel = () => { warn(); this.__ac__.abort(); }; warn(); }; diff --git a/DupeNukem/WebViewMessenger.cs b/DupeNukem/WebViewMessenger.cs index c11463c..b0ed409 100644 --- a/DupeNukem/WebViewMessenger.cs +++ b/DupeNukem/WebViewMessenger.cs @@ -127,6 +127,10 @@ protected override async void OnReceivedControlMessage( // Invoke ready event. this.Ready?.Invoke(this, EventArgs.Empty); break; + + default: + base.OnReceivedControlMessage(controlId, body); + break; } } } diff --git a/samples/DupeNukem.Maui/ViewModels/ContentPageViewModel.cs b/samples/DupeNukem.Maui/ViewModels/ContentPageViewModel.cs index 2f3424d..fdb23a2 100644 --- a/samples/DupeNukem.Maui/ViewModels/ContentPageViewModel.cs +++ b/samples/DupeNukem.Maui/ViewModels/ContentPageViewModel.cs @@ -39,7 +39,7 @@ public ContentPageViewModel() // ---- // ContentPage.Appearing: - this.Ready = Command.Factory.Create(async _ => + this.Ready = Command.Factory.Create(async () => { await this.WebViewPile.RentAsync(webView => { diff --git a/samples/DupeNukem.Maui/Views/MainPage.xaml b/samples/DupeNukem.Maui/Views/MainPage.xaml index 8883650..ad33d71 100644 --- a/samples/DupeNukem.Maui/Views/MainPage.xaml +++ b/samples/DupeNukem.Maui/Views/MainPage.xaml @@ -5,7 +5,8 @@ x:Class="DupeNukem.Maui.Views.MainPage" xmlns:epoxy="https://github.com/kekyo/Epoxy" xmlns:controls="clr-namespace:DupeNukem.Maui.Controls" - xmlns:viewmodels="clr-namespace:DupeNukem.ViewModels"> + xmlns:viewmodels="clr-namespace:DupeNukem.ViewModels" + x:DataType="viewmodels:ContentPageViewModel"> diff --git a/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs b/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs index 8169364..9d87cac 100644 --- a/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs +++ b/samples/DupeNukem.WebView2/ViewModels/TestFragments.cs @@ -73,6 +73,9 @@ private void RegisterTestObjects(WebViewMessenger messenger) messenger.RegisterFunc( "array", async keys => { await Task.Delay(100); return keys; }); + messenger.RegisterFunc( + "arrayBuffer", + async arr => { await Task.Delay(100); return arr; }); messenger.RegisterFunc>>( "callback", async (a, b, cb) => { var r = await cb(a, b); return r; }); @@ -199,6 +202,8 @@ private static void AddJavaScriptTestCode(StringBuilder script) script.AppendLine("async function js_callback(a, b, cb) { return await cb(a, b); }"); script.AppendLine("async function js_callback2(a, b, cb) { return await cb(a, b, new CancellationToken()); }"); script.AppendLine("async function js_callback3(a, b, cb) { return await cb(a, b, new AbortController().signal); }"); + script.AppendLine("async function js_arrayBuffer1(arr) { console.log('js_arrayBuffer1(' + arr + ')'); return arr; }"); + script.AppendLine("async function js_arrayBuffer2(arr) { console.log('js_arrayBuffer2(' + arr + ')'); return new Uint8Array(arr); }"); ///////////////////////////////////////////////////////// // Invoke JavaScript --> .NET methods: @@ -224,6 +229,18 @@ private static void AddJavaScriptTestCode(StringBuilder script) script.AppendLine(" const result_array = await invokeHostMethod('array', [42, 13, 27]);"); script.AppendLine(" assert(['print','enter','escape'], result_array, 'array');"); + script.AppendLine(" const arg_arrayBuffer = new Uint8Array(5);"); + script.AppendLine(" arg_arrayBuffer[0] = 1;"); + script.AppendLine(" arg_arrayBuffer[1] = 2;"); + script.AppendLine(" arg_arrayBuffer[2] = 3;"); + script.AppendLine(" arg_arrayBuffer[3] = 4;"); + script.AppendLine(" arg_arrayBuffer[4] = 5;"); + script.AppendLine(" const result_arrayBuffer1 = await invokeHostMethod('arrayBuffer', arg_arrayBuffer.buffer);"); + script.AppendLine(" assert('1,2,3,4,5', new Uint8Array(result_arrayBuffer1).toString(), 'arrayBuffer1');"); + + script.AppendLine(" const result_arrayBuffer2 = await invokeHostMethod('arrayBuffer', arg_arrayBuffer);"); + script.AppendLine(" assert('1,2,3,4,5', new Uint8Array(result_arrayBuffer2).toString(), 'arrayBuffer2');"); + script.AppendLine(" const result_callback = await invokeHostMethod('callback', 1, 2, async (a, b) => a - b);"); script.AppendLine(" assert(-1, result_callback, 'callback');"); @@ -241,28 +258,28 @@ private static void AddJavaScriptTestCode(StringBuilder script) #if WINDOWS_FORMS // Fully qualified object/function naming with `invokeHostMethod()`. script.AppendLine(" const result_fullName_calc_add = await invokeHostMethod('dupeNukem.winForms.webView2.calculator.add', 1, 2);"); - script.AppendLine(" console.log('fullName_calc.add: ' + result_fullName_calc_add);"); + script.AppendLine(" assert(3, result_fullName_calc_add, 'fullName_calc.add');"); // Able to call different fully qualified function on an object with `invokeHostMethod()`. script.AppendLine(" const result_fullName_calc_sub = await invokeHostMethod('dupeNukem.winForms.webView2.calculator.sub', 1, 2);"); - script.AppendLine(" console.log('fullName_calc.sub: ' + result_fullName_calc_sub);"); + script.AppendLine(" assert(-1, result_fullName_calc_sub, 'fullName_calc.sub');"); #else // Fully qualified object/function naming with `invokeHostMethod()`. script.AppendLine(" const result_fullName_calc_add = await invokeHostMethod('dupeNukem.viewModels.calculator.add', 1, 2);"); - script.AppendLine(" console.log('fullName_calc.add: ' + result_fullName_calc_add);"); + script.AppendLine(" assert(3, result_fullName_calc_add, 'fullName_calc.add');"); // Able to call different fully qualified function on an object with `invokeHostMethod()`. script.AppendLine(" const result_fullName_calc_sub = await invokeHostMethod('dupeNukem.viewModels.calculator.sub', 1, 2);"); - script.AppendLine(" console.log('fullName_calc.sub: ' + result_fullName_calc_sub);"); + script.AppendLine(" assert(-1, result_fullName_calc_sub, 'fullName_calc.sub');"); #endif // An object/function naming with `invokeHostMethod()`. script.AppendLine(" const result_calc_add = await invokeHostMethod('calc.add', 1, 2);"); - script.AppendLine(" console.log('calc.add: ' + result_calc_add);"); + script.AppendLine(" assert(3, result_calc_add, 'calc.add');"); // Able to call different function on an object with `invokeHostMethod()`. script.AppendLine(" const result_calc_sub = await invokeHostMethod('calc.sub', 1, 2);"); - script.AppendLine(" console.log('calc.sub: ' + result_calc_sub);"); + script.AppendLine(" assert(-1, result_calc_sub, 'calc.sub');"); // Unknown method on a object. script.AppendLine(" try {"); @@ -275,7 +292,7 @@ private static void AddJavaScriptTestCode(StringBuilder script) // CancellationToken (obsoleted) script.AppendLine(" const ct1 = new CancellationToken();"); script.AppendLine(" const result_calc_add_cancellable1 = await invokeHostMethod('calc.add_cancellable', 1, 2, ct1);"); - script.AppendLine(" console.log('calc.add_cancellable1: ' + result_calc_add_cancellable1);"); + script.AppendLine(" assert(3, result_calc_add_cancellable1, 'calc.add_cancellable1');"); script.AppendLine(" const ct2 = new CancellationToken();"); script.AppendLine(" const result_calc_add_cancellable2_p = invokeHostMethod('calc.add_cancellable', 1, 2, ct2);"); @@ -291,7 +308,7 @@ private static void AddJavaScriptTestCode(StringBuilder script) // AbortController/AbortSignal script.AppendLine(" const ac1 = new AbortController();"); script.AppendLine(" const result_calc_add_cancellable1_as = await invokeHostMethod('calc.add_cancellable', 1, 2, ac1.signal);"); - script.AppendLine(" console.log('calc.add_cancellable1_as: ' + result_calc_add_cancellable1_as);"); + script.AppendLine(" assert(3, result_calc_add_cancellable1_as, 'calc.add_cancellable1_as');"); script.AppendLine(" const ac2 = new AbortController();"); script.AppendLine(" const result_calc_add_cancellable2_p_as = invokeHostMethod('calc.add_cancellable', 1, 2, ac2.signal);"); @@ -307,7 +324,7 @@ private static void AddJavaScriptTestCode(StringBuilder script) // AbortController/AbortSignal (nested) script.AppendLine(" const ac3 = new AbortController();"); script.AppendLine(" const result_calc_nested_cancellable = await invokeHostMethod('calc.nested_cancellable', { a: 1, b: 2, ct: ac3.signal, });"); - script.AppendLine(" console.log('calc.nested_cancellable: ' + result_calc_nested_cancellable);"); + script.AppendLine(" assert(3, result_calc_nested_cancellable, 'calc.nested_cancellable');"); script.AppendLine(" const ac4 = new AbortController();"); script.AppendLine(" const result_calc_nested_cancellable_p = invokeHostMethod('calc.nested_cancellable', { a: 1, b: 2, ct: ac4.signal, });"); @@ -323,22 +340,22 @@ private static void AddJavaScriptTestCode(StringBuilder script) #if WINDOWS_FORMS // Fully qualified proxy object/function naming. script.AppendLine(" const result_fullName_proxy_calc_add = await dupeNukem.winForms.webView2.calculator.add(1, 2);"); - script.AppendLine(" console.log('fullName_proxy_calc.add: ' + result_fullName_proxy_calc_add);"); + script.AppendLine(" assert(3, result_fullName_proxy_calc_add, 'fullName_proxy_calc.add');"); #else // Fully qualified proxy object/function naming. script.AppendLine(" const result_fullName_proxy_calc_add = await dupeNukem.viewModels.calculator.add(1, 2);"); - script.AppendLine(" console.log('fullName_proxy_calc.add: ' + result_fullName_proxy_calc_add);"); + script.AppendLine(" assert(3, result_fullName_proxy_calc_add, 'fullName_proxy_calc.add');"); #endif // Proxy object/function. script.AppendLine(" const result_proxy_calc_add = await calc.add(1, 2);"); - script.AppendLine(" console.log('proxy_calc.add: ' + result_proxy_calc_add);"); + script.AppendLine(" assert(3, result_proxy_calc_add, 'proxy_calc.add');"); // Obsoleted marking. script.AppendLine(" const result_calc_add_obsoleted1 = await calc.add_obsoleted1(1, 2);"); - script.AppendLine(" console.log('calc.add_obsoleted1: ' + result_calc_add_obsoleted1);"); + script.AppendLine(" assert(3, result_calc_add_obsoleted1, 'calc.add_obsoleted1');"); script.AppendLine(" const result_calc_add_obsoleted2 = await calc.add_obsoleted2(1, 2);"); - script.AppendLine(" console.log('calc.add_obsoleted2: ' + result_calc_add_obsoleted2);"); + script.AppendLine(" assert(3, result_calc_add_obsoleted2, 'calc.add_obsoleted2');"); script.AppendLine(" try {"); script.AppendLine(" await calc.add_obsoleted3(1, 2);"); @@ -348,8 +365,8 @@ private static void AddJavaScriptTestCode(StringBuilder script) script.AppendLine(" }"); // Able to call different functions on a proxy object. - script.AppendLine(" const result_calc_mul = await calc.mul(2, 3);"); - script.AppendLine(" console.log('calc.mul: ' + result_calc_mul);"); + script.AppendLine(" const result_proxy_calc_mul = await calc.mul(2, 3);"); + script.AppendLine(" assert(6, result_proxy_calc_mul, 'proxy_calc.mul');"); // Interoperated exceptions. script.AppendLine(" try {");