Skip to content

Commit c5c9419

Browse files
committed
Allows the conversion of primses to tasks when pasing an async function as argument
Fixes: #2194
1 parent 215d1cd commit c5c9419

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

Jint.Tests/Runtime/AsyncTests.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,72 @@ public void ShouldTaskAwaitCurrentStack()
155155
Assert.Equal("123", asyncTestClass.StringToAppend);
156156
}
157157

158+
[Fact]
159+
public void ShouldCompleteWithAsyncTaskCallbacks()
160+
{
161+
Engine engine = new(options =>
162+
{
163+
options.ExperimentalFeatures = ExperimentalFeature.TaskInterop;
164+
options.Constraints.PromiseTimeout = TimeSpan.FromMilliseconds(-1);
165+
});
166+
engine.SetValue("asyncTestMethod", new Func<Func<Task>, Task<string>>(async callback => { await Task.Delay(100); await callback(); return "Hello World"; }));
167+
engine.SetValue("asyncWork", new Func<Task>(() => Task.Delay(100)));
168+
engine.SetValue("assert", new Action<bool>(Assert.True));
169+
var result = engine.Evaluate("async function hello() {return await asyncTestMethod(async () =>{ await asyncWork(); })} hello();");
170+
result = result.UnwrapIfPromise();
171+
Assert.Equal("Hello World", result);
172+
}
173+
174+
[Fact]
175+
public void ShouldFromAsyncTaskCallbacks()
176+
{
177+
Engine engine = new(options =>
178+
{
179+
options.ExperimentalFeatures = ExperimentalFeature.TaskInterop;
180+
options.Constraints.PromiseTimeout = TimeSpan.FromMilliseconds(-1);
181+
});
182+
engine.SetValue("asyncTestMethod", new Func<Func<Task<string>>, Task<string>>(async callback => { await Task.Delay(100); return await callback(); }));
183+
engine.SetValue("asyncWork", new Func<Task<string>>(async () => { await Task.Delay(100); return "Hello World"; }));
184+
engine.SetValue("assert", new Action<bool>(Assert.True));
185+
var result = engine.Evaluate("async function hello() {return await asyncTestMethod(async () =>{ return await asyncWork(); })} hello();");
186+
result = result.UnwrapIfPromise();
187+
Assert.Equal("Hello World", result);
188+
}
189+
158190
#if NETFRAMEWORK == false
191+
192+
[Fact]
193+
public void ShouldCompleteWithAsyncValueTaskCallbacks()
194+
{
195+
Engine engine = new(options =>
196+
{
197+
options.ExperimentalFeatures = ExperimentalFeature.TaskInterop;
198+
options.Constraints.PromiseTimeout = TimeSpan.FromMilliseconds(-1);
199+
});
200+
engine.SetValue("asyncTestMethod", new Func<Func<ValueTask>, Task<string>>(async callback => { await Task.Delay(100); await callback(); return "Hello World"; }));
201+
engine.SetValue("asyncWork", new Func<Task>(() => Task.Delay(100)));
202+
engine.SetValue("assert", new Action<bool>(Assert.True));
203+
var result = engine.Evaluate("async function hello() {return await asyncTestMethod(async () =>{ await asyncWork(); })} hello();");
204+
result = result.UnwrapIfPromise();
205+
Assert.Equal("Hello World", result);
206+
}
207+
208+
[Fact]
209+
public void ShouldFromAsyncValueTaskCallbacks()
210+
{
211+
Engine engine = new(options =>
212+
{
213+
options.ExperimentalFeatures = ExperimentalFeature.TaskInterop;
214+
options.Constraints.PromiseTimeout = TimeSpan.FromMilliseconds(-1);
215+
});
216+
engine.SetValue("asyncTestMethod", new Func<Func<ValueTask<string>>, Task<string>>(async callback => { await Task.Delay(100); return await callback(); }));
217+
engine.SetValue("asyncWork", new Func<ValueTask<string>>(async () => { await Task.Delay(100); return "Hello World"; }));
218+
engine.SetValue("assert", new Action<bool>(Assert.True));
219+
var result = engine.Evaluate("async function hello() {return await asyncTestMethod(async () =>{ return await asyncWork(); })} hello();");
220+
result = result.UnwrapIfPromise();
221+
Assert.Equal("Hello World", result);
222+
}
223+
159224
[Fact]
160225
public void ShouldValueTaskConvertedToPromiseInJS()
161226
{

Jint/Native/Object/ObjectInstance.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,18 @@ private object ToObject(ObjectTraverseStack stack)
10221022
case ObjectClass.Arguments:
10231023
case ObjectClass.Object:
10241024

1025+
if ((Engine.Options.ExperimentalFeatures & ExperimentalFeature.TaskInterop) != ExperimentalFeature.None)
1026+
{
1027+
if (this is JsPromise asPromise)
1028+
{
1029+
var promsiseResult = asPromise.UnwrapIfPromise(Engine.Options.Constraints.PromiseTimeout);
1030+
1031+
converted = promsiseResult is ObjectInstance oi
1032+
? oi.ToObject(stack)
1033+
: promsiseResult.ToObject();
1034+
break;
1035+
}
1036+
}
10251037
if (this is JsArray arrayInstance)
10261038
{
10271039
var result = new object?[arrayInstance.GetLength()];

Jint/Runtime/Interop/DefaultTypeConverter.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Concurrent;
23
using System.Collections.ObjectModel;
34
using System.Diagnostics.CodeAnalysis;
@@ -25,12 +26,21 @@ public class DefaultTypeConverter : ITypeConverter
2526
private readonly record struct TypeConversionKey(Type Source, Type Target);
2627

2728
private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo?> _knownCastOperators = new();
29+
private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo?> _knownFromResultGenerics = new();
2830

2931
private static readonly Type intType = typeof(int);
3032
private static readonly Type iCallableType = typeof(JsCallDelegate);
3133
private static readonly Type jsValueType = typeof(JsValue);
3234
private static readonly Type objectType = typeof(object);
3335
private static readonly Type engineType = typeof(Engine);
36+
private static readonly Type taskType = typeof(Task);
37+
private static readonly Type genTaskType = typeof(Task<>);
38+
private static readonly MethodInfo taskFromResultInfo = taskType.GetMethod("FromResult")!;
39+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
40+
private static readonly Type valueTaskType = typeof(ValueTask);
41+
private static readonly Type genValueTaskType = typeof(ValueTask<>);
42+
private static readonly MethodInfo valueTaskFromResultInfo = valueTaskType.GetMethod("FromResult")!;
43+
#endif
3444

3545
private static readonly MethodInfo changeTypeIfConvertible = typeof(DefaultTypeConverter).GetMethod(
3646
nameof(ChangeTypeOnlyIfConvertible), BindingFlags.NonPublic | BindingFlags.Static)!;
@@ -390,12 +400,58 @@ private LambdaExpression BuildDelegate(
390400
[return: NotNullIfNotNull(nameof(value))]
391401
private static object? ChangeTypeOnlyIfConvertible(object? value, Type conversionType, IFormatProvider? provider)
392402
{
403+
if (conversionType == taskType)
404+
{
405+
return Task.CompletedTask;
406+
}
407+
408+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
409+
if (conversionType == valueTaskType)
410+
{
411+
return default(ValueTask);
412+
}
413+
#endif
414+
415+
if (conversionType.IsGenericType && conversionType.GetGenericTypeDefinition() == genTaskType)
416+
{
417+
var key = new TypeConversionKey(conversionType.GetGenericArguments()[0], genTaskType);
418+
var fromResultMethod = _knownFromResultGenerics.GetOrAdd(key, GetFromResultMethod);
419+
if (fromResultMethod != null)
420+
{
421+
return fromResultMethod.Invoke(null, [value]);
422+
}
423+
}
424+
425+
#if NETCOREAPP
426+
if (conversionType.IsGenericType && conversionType.GetGenericTypeDefinition() == genValueTaskType)
427+
{
428+
var key = new TypeConversionKey(conversionType.GetGenericArguments()[0], genValueTaskType);
429+
var fromResultMethod = _knownFromResultGenerics.GetOrAdd(key, GetFromResultMethod);
430+
if (fromResultMethod != null)
431+
{
432+
return fromResultMethod.Invoke(null, [value]);
433+
}
434+
}
435+
#endif
436+
393437
if (value == null || value is IConvertible)
394438
return System.Convert.ChangeType(value, conversionType, provider);
395439

396440
return value;
397441
}
398442

443+
private static MethodInfo? GetFromResultMethod(TypeConversionKey key)
444+
{
445+
var (target, taskType) = key;
446+
#if NETCOREAPP
447+
if (taskType == genValueTaskType)
448+
{
449+
return valueTaskFromResultInfo.MakeGenericMethod(target);
450+
}
451+
#endif
452+
return taskFromResultInfo.MakeGenericMethod(target);
453+
}
454+
399455
private static bool TryCastWithOperators(object value, Type type, Type valueType, [NotNullWhen(true)] out object? converted)
400456
{
401457
var key = new TypeConversionKey(valueType, type);

0 commit comments

Comments
 (0)