Prechádzať zdrojové kódy

Add ValueTask and ValueTask<T> to Promise conversion (#1744)

* Support ValueTask/ValueTask<T> in modern targets

Adds new `IsAwaitable` and `ConvertAwaitableToPromise` methods.

`IsAwaitable` checks if a result is a `Task`. For .NET Standard 2.1/.NET targets, the method also checks if a result is `ValueTask` or `ValueTask<T>`.

If a result is awaitable, `Call` now calls `ConvertAwaitableToPromise`. This method makes use of the existing `ConvertTaskToPromise` method for `Task`-based results. For .NET Standard 2.1/.NET targets, it also handles conversion of `ValueTask`/`ValueTask<T>` results into `Task`/`Task<T>` before calling `ConvertTaskToPromise`.

* Add ValueTask/ValueTask<T> tests
Graham Watts 1 rok pred
rodič
commit
45fca0420e

+ 70 - 0
Jint.Tests/Runtime/AwaitTests.cs

@@ -78,4 +78,74 @@ public class AsyncTests
         engine.Execute("async function hello() {await myAsyncMethod();myAsyncMethod2();} hello();");
         Assert.Equal("12", log);
     }
+
+#if NETFRAMEWORK == false
+    [Fact]
+    public void ShouldValueTaskConvertedToPromiseInJS()
+    {
+        Engine engine = new();
+        engine.SetValue("callable", Callable);
+        var result = engine.Evaluate("callable().then(x=>x*2)");
+        result = result.UnwrapIfPromise();
+        Assert.Equal(2, result);
+
+        static async ValueTask<int> Callable()
+        {
+            await Task.Delay(10);
+            Assert.True(true);
+            return 1;
+        }
+    }
+
+    [Fact]
+    public void ShouldValueTaskCatchWhenCancelled()
+    {
+        Engine engine = new();
+        CancellationTokenSource cancel = new();
+        cancel.Cancel();
+        engine.SetValue("token", cancel.Token);
+        engine.SetValue("callable", Callable);
+        engine.SetValue("assert", new Action<bool>(Assert.True));
+        var result = engine.Evaluate("callable(token).then(_ => assert(false)).catch(_ => assert(true))");
+        result = result.UnwrapIfPromise();
+        static async ValueTask Callable(CancellationToken token)
+        {
+            await ValueTask.FromCanceled(token);
+        }
+    }
+
+    [Fact]
+    public void ShouldValueTaskCatchWhenThrowError()
+    {
+        Engine engine = new();
+        engine.SetValue("callable", Callable);
+        engine.SetValue("assert", new Action<bool>(Assert.True));
+        var result = engine.Evaluate("callable().then(_ => assert(false)).catch(_ => assert(true))");
+
+        static async ValueTask Callable()
+        {
+            await Task.Delay(10);
+            throw new Exception();
+        }
+    }
+
+    [Fact]
+    public void ShouldValueTaskAwaitCurrentStack()
+    {
+        //https://github.com/sebastienros/jint/issues/514#issuecomment-1507127509
+        Engine engine = new();
+        string log = "";
+        engine.SetValue("myAsyncMethod", new Func<ValueTask>(async () =>
+        {
+            await Task.Delay(1000);
+            log += "1";
+        }));
+        engine.SetValue("myAsyncMethod2", new Action(() =>
+        {
+            log += "2";
+        }));
+        engine.Execute("async function hello() {await myAsyncMethod();myAsyncMethod2();} hello();");
+        Assert.Equal("12", log);
+    }
+#endif
 }

+ 56 - 2
Jint/Runtime/Interop/DelegateWrapper.cs

@@ -131,11 +131,11 @@ namespace Jint.Runtime.Interop
             try
             {
                 var result = _d.DynamicInvoke(parameters);
-                if (result is not Task task)
+                if (!IsAwaitable(result))
                 {
                     return FromObject(Engine, result);
                 }
-                return ConvertTaskToPromise(task);
+                return ConvertAwaitableToPromise(result!);
             }
             catch (TargetInvocationException exception)
             {
@@ -144,6 +144,59 @@ namespace Jint.Runtime.Interop
             }
         }
 
+        private static bool IsAwaitable(object? obj)
+        {
+            if (obj is null)
+            {
+                return false;
+            }
+            if (obj is Task)
+            {
+                return true;
+            }
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
+            if (obj is ValueTask)
+            {
+                return true;
+            }
+
+            // ValueTask<T> is not derived from ValueTask, so we need to check for it explicitly
+            var type = obj.GetType();
+            if (!type.IsGenericType)
+            {
+                return false;
+            }
+
+            return type.GetGenericTypeDefinition() == typeof(ValueTask<>);
+#else
+            return false;
+#endif
+        }
+
+        private JsValue ConvertAwaitableToPromise(object obj)
+        {
+            if (obj is Task task)
+            {
+                return ConvertTaskToPromise(task);
+            }
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
+            if (obj is ValueTask valueTask)
+            {
+                return ConvertTaskToPromise(valueTask.AsTask());
+            }
+
+            // ValueTask<T>
+            var asTask = obj.GetType().GetMethod(nameof(ValueTask<object>.AsTask));
+            if (asTask is not null)
+            {
+                return ConvertTaskToPromise((Task) asTask.Invoke(obj, parameters: null)!);
+            }
+#endif
+
+            return FromObject(Engine, JsValue.Undefined);
+        }
+
         private JsValue ConvertTaskToPromise(Task task)
         {
             var (promise, resolve, reject) = Engine.RegisterPromise();
@@ -190,5 +243,6 @@ namespace Jint.Runtime.Interop
 
             return promise;
         }
+
     }
 }