diff --git a/Jint.Tests/Jint.Tests.csproj b/Jint.Tests/Jint.Tests.csproj index 13d7ef1cf1..15be8accce 100644 --- a/Jint.Tests/Jint.Tests.csproj +++ b/Jint.Tests/Jint.Tests.csproj @@ -1,7 +1,6 @@  net6.0 - $(TargetFrameworks);net461 ..\Jint\Jint.snk true false diff --git a/Jint.Tests/Runtime/SchedulingTests.cs b/Jint.Tests/Runtime/SchedulingTests.cs new file mode 100644 index 0000000000..b70a0fc1d3 --- /dev/null +++ b/Jint.Tests/Runtime/SchedulingTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Jint.Tests.Runtime +{ + public class SchedulingTests + { + class Context + { + public string Result { get; set; } + } + + [Fact] + public async Task CanRunAsyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action(async (callback, delay) => + { + using (var task = engine.CreateTask()) + { + await Task.Delay(delay); + + task.Invoke(callback); + } + })); + + await engine.ExecuteAsync(@" + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } + [Fact] + public async Task CanRunNestedAsyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action(async (callback, delay) => + { + using (var task = engine.CreateTask()) + { + await Task.Delay(delay); + + task.Invoke(callback); + } + })); + + await engine.ExecuteAsync(@" + setTimeout(function () { + setTimeout(function () { + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + }, 100); + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } + + [Fact] + public async Task CanRunSyncCode() + { + var engine = new Engine(); + + var ctx = new Context(); + + engine.SetValue("ctx", ctx); + engine.SetValue("setTimeout", new Action((callback, delay) => + { + using (var task = engine.CreateTask()) + { + task.Invoke(callback); + } + })); + + await engine.ExecuteAsync(@" + setTimeout(function () { + ctx.Result = 'Hello World'; + }, 100); + "); + + Assert.Equal("Hello World", ctx.Result); + } + } +} diff --git a/Jint/Engine.cs b/Jint/Engine.cs index ad5935bcfa..af0371e9ee 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Esprima; using Esprima.Ast; using Jint.Native; @@ -20,6 +21,7 @@ using Jint.Runtime.Interpreter; using Jint.Runtime.Interpreter.Expressions; using Jint.Runtime.References; +using Jint.Scheduling; namespace Jint { @@ -36,6 +38,7 @@ public partial class Engine internal EvaluationContext _activeEvaluationContext; private readonly EventLoop _eventLoop = new(); + private readonly Stack _schedulers = new Stack(); // lazy properties private DebugHandler _debugHandler; @@ -51,6 +54,7 @@ public partial class Engine internal readonly JsValueArrayPool _jsValueArrayPool; internal readonly ExtensionMethodCache _extensionMethods; + public ITypeConverter ClrTypeConverter { get; internal set; } // cache of types used when resolving CLR type names @@ -253,7 +257,53 @@ public Engine Execute(string source) public Engine Execute(string source, ParserOptions parserOptions) => Execute(new JavaScriptParser(source, parserOptions).ParseScript()); + public Task ExecuteAsync(string source) + => ExecuteAsync(source, DefaultParserOptions); + + public Task ExecuteAsync(string source, ParserOptions parserOptions) + => ExecuteAsync(new JavaScriptParser(source, parserOptions).ParseScript()); + public Engine Execute(Script script) + { + using (var scheduler = new Scheduler(false)) + { + try + { + _schedulers.Push(scheduler); + + ExecuteWithScheduler(scheduler, script); + } + finally + { + _schedulers.Pop(); + } + } + + return this; + } + + public async Task ExecuteAsync(Script script) + { + using (var scheduler = new Scheduler(true)) + { + try + { + _schedulers.Push(scheduler); + + ExecuteWithScheduler(scheduler, script); + + await scheduler.Completion; + } + finally + { + _schedulers.Pop(); + } + } + + return this; + } + + private void ExecuteWithScheduler(Scheduler scheduler, Script script) { Engine DoInvoke() { @@ -290,10 +340,23 @@ Engine DoInvoke() return this; } - var strict = _isStrict || script.Strict; - ExecuteWithConstraints(strict, DoInvoke); + var task = scheduler.CreateTask(); - return this; + task.Invoke(() => + { + var strict = _isStrict || script.Strict; + ExecuteWithConstraints(strict, DoInvoke); + }); + } + + public IDeferredTask CreateTask() + { + if (_schedulers.Count == 0) + { + throw new InvalidOperationException("Not within a script."); + } + + return _schedulers.Peek().CreateTask(); } /// diff --git a/Jint/Jint.csproj b/Jint/Jint.csproj index 6b3b23e88d..1097671b69 100644 --- a/Jint/Jint.csproj +++ b/Jint/Jint.csproj @@ -1,7 +1,7 @@  en-US - net461;netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1 Jint.snk true latest @@ -11,5 +11,6 @@ + diff --git a/Jint/Scheduling/DeferredTask.cs b/Jint/Scheduling/DeferredTask.cs new file mode 100644 index 0000000000..5b0884b743 --- /dev/null +++ b/Jint/Scheduling/DeferredTask.cs @@ -0,0 +1,44 @@ +using System; + +namespace Jint.Scheduling +{ + internal class DeferredTask : IDeferredTask + { + private readonly Scheduler _scheduler; + private bool _isCompleted; + + public DeferredTask(Scheduler scheduler) + { + _scheduler = scheduler; + } + + public void Dispose() + { + Cancel(); + } + + public void Cancel() + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + + _scheduler.Cancel(this); + } + + public void Invoke(Action action) + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + + _scheduler.Invoke(this, action); + } + } +} diff --git a/Jint/Scheduling/IDeferredTask.cs b/Jint/Scheduling/IDeferredTask.cs new file mode 100644 index 0000000000..5d6eacf664 --- /dev/null +++ b/Jint/Scheduling/IDeferredTask.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jint.Scheduling +{ + public interface IDeferredTask : IDisposable + { + void Invoke(Action action); + + void Cancel(); + } +} diff --git a/Jint/Scheduling/Scheduler.cs b/Jint/Scheduling/Scheduler.cs new file mode 100644 index 0000000000..630957e32c --- /dev/null +++ b/Jint/Scheduling/Scheduler.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Jint.Scheduling +{ + internal sealed class Scheduler : IDisposable + { + private readonly TaskCompletionSource _taskCompletionSource = new TaskCompletionSource(); + private readonly HashSet _pendingTasks = new HashSet(); + private readonly Queue _inlinedTasks = new Queue(); + private bool _isRunning; + private bool _isDisposed; + private bool _isMainFlow = true; + private bool _allowAsyncFlow; + + public Scheduler(bool allowAsyncFlow) + { + _allowAsyncFlow = allowAsyncFlow; + } + + public Task Completion + { + get => _taskCompletionSource.Task; + } + + public void Dispose() + { + _isDisposed = true; + } + + public IDeferredTask CreateTask() + { + var task = new DeferredTask(this); + + _pendingTasks.Add(task); + + return task; + } + + public void Invoke(DeferredTask task, Action action) + { + if (_isDisposed) + { + return; + } + + _pendingTasks.Remove(task); + + if (_isRunning) + { + _inlinedTasks.Enqueue(action); + return; + } + + if (!_allowAsyncFlow) + { + throw new InvalidOperationException("You can only run async task when using ExecuteAsync()"); + } + + _isRunning = true; + try + { + action(); + + RunAvailableContinuations(); + + TryComplete(); + } + catch (Exception ex) when (!_isMainFlow) + { + _taskCompletionSource.TrySetException(ex); + } + finally + { + _isMainFlow = false; + _isRunning = false; + } + } + + internal void Cancel(DeferredTask deferredTask) + { + _pendingTasks.Remove(deferredTask); + + TryComplete(); + } + + private void TryComplete() + { + if (_pendingTasks.Count == 0) + { + _taskCompletionSource.TrySetResult(true); + } + } + + private void RunAvailableContinuations() + { + var queue = _inlinedTasks; + + while (true) + { + if (queue.Count == 0) + { + return; + } + + var nextContinuation = queue.Dequeue(); + + // note that continuation can enqueue new events + nextContinuation(); + } + } + } +}