From c905f53c998ba7723e41a0c18e56268cfce5ad6a Mon Sep 17 00:00:00 2001 From: Umut Akkaya Date: Thu, 12 Oct 2023 07:58:18 +0300 Subject: [PATCH] Add experimental support for Task to Promise conversion (#1567) This commit fixes an issue where an `async Task` (be it a method or a delegate) would end up being marshalled directly to JS, giving a `Task` to the user, instead of `undefined`, which is what is returned for "synchronous tasks", i.e. any Task-returning invokable function that does not generate an async state machine of its own (that is to say, any function that returns `Task`, not `async Task`. This commit fixes the issue by checking if a Task's result is equal to `VoidTaskResult`, which is an internal type used by the runtime to indicate a void-returning Task, such as that from an `async Task` method/delegate --------- Co-authored-by: Velvet Toroyashi <42438262+VelvetToroyashi@users.noreply.github.com> --- Jint.Tests/Runtime/AwaitTests.cs | 68 +++++++++++++++++++++++++ Jint/Runtime/Interop/DelegateWrapper.cs | 56 +++++++++++++++++++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/Jint.Tests/Runtime/AwaitTests.cs b/Jint.Tests/Runtime/AwaitTests.cs index c1fa20d28e..f3a840e3cd 100644 --- a/Jint.Tests/Runtime/AwaitTests.cs +++ b/Jint.Tests/Runtime/AwaitTests.cs @@ -10,4 +10,72 @@ public void AwaitPropagationAgainstPrimitiveValue() result = result.UnwrapIfPromise(); Assert.Equal("1", result); } + + [Fact] + public void ShouldTaskConvertedToPromiseInJS() + { + 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 Task Callable() + { + await Task.Delay(10); + Assert.True(true); + return 1; + } + } + + [Fact] + public void ShouldTaskCatchWhenCancelled() + { + Engine engine = new(); + CancellationTokenSource cancel = new(); + cancel.Cancel(); + engine.SetValue("token", cancel.Token); + engine.SetValue("callable", Callable); + engine.SetValue("assert", new Action(Assert.True)); + var result = engine.Evaluate("callable(token).then(_ => assert(false)).catch(_ => assert(true))"); + result = result.UnwrapIfPromise(); + static async Task Callable(CancellationToken token) + { + await Task.FromCanceled(token); + } + } + + [Fact] + public void ShouldTaskCatchWhenThrowError() + { + Engine engine = new(); + engine.SetValue("callable", Callable); + engine.SetValue("assert", new Action(Assert.True)); + var result = engine.Evaluate("callable().then(_ => assert(false)).catch(_ => assert(true))"); + + static async Task Callable() + { + await Task.Delay(10); + throw new Exception(); + } + } + + [Fact] + public void ShouldTaskAwaitCurrentStack() + { + //https://github.com/sebastienros/jint/issues/514#issuecomment-1507127509 + Engine engine = new(); + string log = ""; + engine.SetValue("myAsyncMethod", new Func(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); + } } diff --git a/Jint/Runtime/Interop/DelegateWrapper.cs b/Jint/Runtime/Interop/DelegateWrapper.cs index 4a33e324a0..36bd49b898 100644 --- a/Jint/Runtime/Interop/DelegateWrapper.cs +++ b/Jint/Runtime/Interop/DelegateWrapper.cs @@ -130,13 +130,65 @@ protected internal override JsValue Call(JsValue thisObject, JsValue[] jsArgumen try { - return FromObject(Engine, _d.DynamicInvoke(parameters)); + var result = _d.DynamicInvoke(parameters); + if (result is not Task task) + { + return FromObject(Engine, result); + } + return ConvertTaskToPromise(task); } catch (TargetInvocationException exception) { - ExceptionHelper.ThrowMeaningfulException(_engine, exception); + ExceptionHelper.ThrowMeaningfulException(Engine, exception); throw; } } + + private JsValue ConvertTaskToPromise(Task task) + { + var (promise, resolve, reject) = Engine.RegisterPromise(); + task = task.ContinueWith(continuationAction => + { + if (continuationAction.IsFaulted) + { + reject(FromObject(Engine, continuationAction.Exception)); + } + else if (continuationAction.IsCanceled) + { + reject(FromObject(Engine, new ExecutionCanceledException())); + } + else + { + // Special case: Marshal `async Task` as undefined, as this is `Task` at runtime + // See https://github.com/sebastienros/jint/pull/1567#issuecomment-1681987702 + if (Task.CompletedTask.Equals(continuationAction)) + { + resolve(FromObject(Engine, JsValue.Undefined)); + return; + } + + var result = continuationAction.GetType().GetProperty(nameof(Task.Result)); + if (result is not null) + { + resolve(FromObject(Engine, result.GetValue(continuationAction))); + } + else + { + resolve(FromObject(Engine, JsValue.Undefined)); + } + } + }); + + Engine.AddToEventLoop(() => + { + if (!task.IsCompleted) + { + // Task.Wait has the potential of inlining the task's execution on the current thread; avoid this. + ((IAsyncResult) task).AsyncWaitHandle.WaitOne(); + } + }); + + return promise; + } } }