using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
/// <summary>A single-threaded, explicitly pumped browser task queue; never sleeps or starts host threads.</summary>
internal sealed class BrowserEventLoop
{
private sealed record Task(int Id, JsValue Callback, JsValue[] Arguments, long Due, long Interval, bool Animation);
private readonly Engine engine;
private readonly BrowserLimits limits;
private readonly Dictionary<int, Task> tasks = [];
private readonly Func<JsValue, JsValue[], JsValue> isCallable;
private int nextId;
private int pendingMicrotasks;
public BrowserEventLoop(Engine engine, BrowserLimits limits)
{
this.engine = engine;
this.limits = limits;
isCallable = (Func<JsValue, JsValue[], JsValue>)engine.Evaluate("(function(value){return typeof value==='function';})").ToObject()!;
Install("setTimeout", args => Schedule(args, false, false));
Install("setInterval", args => Schedule(args, true, false));
Install("requestAnimationFrame", args => Schedule(args, false, true));
foreach (var name in new[] { "clearTimeout", "clearInterval", "cancelAnimationFrame" })
Install(name, args => { tasks.Remove(Integer(Value(args, 0))); return JsValue.Undefined; });
Install("__xfeMicrotaskEnter", _ =>
{
if (++pendingMicrotasks > limits.MaximumPendingTasks)
{
--pendingMicrotasks;
throw new BrowserLimitException("The browser microtask queue is full.");
}
return JsValue.Undefined;
});
Install("__xfeMicrotaskLeave", _ => { if (pendingMicrotasks > 0) --pendingMicrotasks; return JsValue.Undefined; });
_ = new BrowserPerformance(engine, limits, () => Now);
engine.Execute("""
(function(enter,leave){
// A page/polyfill may replace Promise and Promise.prototype.then.
// Keep the host's own native reaction queue independent of that
// replacement, including the hidden anchor's species constructor.
const NativePromise=Promise,anchor=NativePromise.resolve();
Object.defineProperty(anchor,'constructor',{value:{[Symbol.species]:NativePromise}});
const enqueue=Function.prototype.call.bind(NativePromise.prototype.then,anchor);
globalThis.queueMicrotask = function(callback) {
if (typeof callback !== 'function') throw new TypeError('queueMicrotask requires a function');
enter();
enqueue(function() { try { callback(); } finally { leave(); } });
};
delete globalThis.__xfeMicrotaskEnter;delete globalThis.__xfeMicrotaskLeave;})(__xfeMicrotaskEnter,__xfeMicrotaskLeave);
""");
}
public long Now { get; private set; }
public int PendingCount => tasks.Count + pendingMicrotasks;
public void Release(){tasks.Clear();pendingMicrotasks=0;}
public int Pump(long monotonicMs, int maximumCallbacks)
{
if (monotonicMs < 0) throw new ArgumentOutOfRangeException(nameof(monotonicMs));
Now = Math.Max(Now, monotonicMs);
var maximum = Math.Clamp(maximumCallbacks, 1, limits.MaximumEventCallbacksPerPump);
// Snapshot: tasks queued by a callback wait for the next pump (RAF never recursively runs in one frame).
var due = tasks.Values.Where(task => task.Due <= Now).OrderBy(task => task.Due).ThenBy(task => task.Id)
.Take(maximum).ToArray();
var executed = 0;
foreach (var task in due)
{
if (!tasks.Remove(task.Id)) continue;
if (task.Interval > 0) tasks.Add(task.Id, task with { Due = checked(Now + task.Interval) });
engine.Invoke(task.Callback, engine.Global, task.Animation ? [(JsValue)(double)Now] : task.Arguments);
// Evaluate drains Jint's actual promise job queue under statement/time/memory constraints.
engine.Evaluate("void 0");
++executed;
}
return executed;
}
private JsValue Schedule(JsValue[] args, bool repeat, bool animation)
{
var callback = Value(args, 0);
if (!isCallable(JsValue.Undefined, [callback]).AsBoolean())
throw new ArgumentException("Browser timers require a function; source-string timers are disabled.");
if (tasks.Count + pendingMicrotasks >= limits.MaximumPendingTasks)
throw new BrowserLimitException("The browser task queue is full.");
var delay = animation ? 1 : Math.Clamp(Number(Value(args, 1)), 0, int.MaxValue);
var interval = repeat ? Math.Max(1, (long)delay) : 0;
var id = nextId == int.MaxValue ? nextId = 1 : ++nextId;
while (tasks.ContainsKey(id)) id = ++nextId;
tasks.Add(id, new(id, callback, animation ? [] : args.Skip(2).ToArray(), checked(Now + (long)delay), interval, animation));
return id;
}
private void Install(string name, Func<JsValue[], JsValue> callback) => engine.SetValue(name,
new ClrFunction(engine, name, (_, args) => callback(args)));
private static JsValue Value(JsValue[] args, int index) => index < args.Length ? args[index] : JsValue.Undefined;
private static double Number(JsValue value) => value.IsNumber() && double.IsFinite(value.AsNumber()) ? value.AsNumber() : 0;
private static int Integer(JsValue value) => (int)Math.Clamp(Number(value), int.MinValue, int.MaxValue);
}
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
/// <summary>A single-threaded, explicitly pumped browser task queue; never sleeps or starts host threads.</summary>
internal sealed class BrowserEventLoop
{
private sealed record Task(int Id, JsValue Callback, JsValue[] Arguments, long Due, long Interval, bool Animation);
private readonly Engine engine;
private readonly BrowserLimits limits;
private readonly Dictionary<int, Task> tasks = [];
private readonly Func<JsValue, JsValue[], JsValue> isCallable;
private int nextId;
private int pendingMicrotasks;
public BrowserEventLoop(Engine engine, BrowserLimits limits)
{
this.engine = engine;
this.limits = limits;
isCallable = (Func<JsValue, JsValue[], JsValue>)engine.Evaluate("(function(value){return typeof value==='function';})").ToObject()!;
Install("setTimeout", args => Schedule(args, false, false));
Install("setInterval", args => Schedule(args, true, false));
Install("requestAnimationFrame", args => Schedule(args, false, true));
foreach (var name in new[] { "clearTimeout", "clearInterval", "cancelAnimationFrame" })
Install(name, args => { tasks.Remove(Integer(Value(args, 0))); return JsValue.Undefined; });
Install("__xfeMicrotaskEnter", _ =>
{
if (++pendingMicrotasks > limits.MaximumPendingTasks)
{
--pendingMicrotasks;
throw new BrowserLimitException("The browser microtask queue is full.");
}
return JsValue.Undefined;
});
Install("__xfeMicrotaskLeave", _ => { if (pendingMicrotasks > 0) --pendingMicrotasks; return JsValue.Undefined; });
_ = new BrowserPerformance(engine, limits, () => Now);
engine.Execute("""
(function(enter,leave){
// A page/polyfill may replace Promise and Promise.prototype.then.
// Keep the host's own native reaction queue independent of that
// replacement, including the hidden anchor's species constructor.
const NativePromise=Promise,anchor=NativePromise.resolve();
Object.defineProperty(anchor,'constructor',{value:{[Symbol.species]:NativePromise}});
const enqueue=Function.prototype.call.bind(NativePromise.prototype.then,anchor);
globalThis.queueMicrotask = function(callback) {
if (typeof callback !== 'function') throw new TypeError('queueMicrotask requires a function');
enter();
enqueue(function() { try { callback(); } finally { leave(); } });
};
delete globalThis.__xfeMicrotaskEnter;delete globalThis.__xfeMicrotaskLeave;})(__xfeMicrotaskEnter,__xfeMicrotaskLeave);
""");
}
public long Now { get; private set; }
public int PendingCount => tasks.Count + pendingMicrotasks;
public void Release(){tasks.Clear();pendingMicrotasks=0;}
public int Pump(long monotonicMs, int maximumCallbacks)
{
if (monotonicMs < 0) throw new ArgumentOutOfRangeException(nameof(monotonicMs));
Now = Math.Max(Now, monotonicMs);
var maximum = Math.Clamp(maximumCallbacks, 1, limits.MaximumEventCallbacksPerPump);
// Snapshot: tasks queued by a callback wait for the next pump (RAF never recursively runs in one frame).
var due = tasks.Values.Where(task => task.Due <= Now).OrderBy(task => task.Due).ThenBy(task => task.Id)
.Take(maximum).ToArray();
var executed = 0;
foreach (var task in due)
{
if (!tasks.Remove(task.Id)) continue;
if (task.Interval > 0) tasks.Add(task.Id, task with { Due = checked(Now + task.Interval) });
engine.Invoke(task.Callback, engine.Global, task.Animation ? [(JsValue)(double)Now] : task.Arguments);
// Evaluate drains Jint's actual promise job queue under statement/time/memory constraints.
engine.Evaluate("void 0");
++executed;
}
return executed;
}
private JsValue Schedule(JsValue[] args, bool repeat, bool animation)
{
var callback = Value(args, 0);
if (!isCallable(JsValue.Undefined, [callback]).AsBoolean())
throw new ArgumentException("Browser timers require a function; source-string timers are disabled.");
if (tasks.Count + pendingMicrotasks >= limits.MaximumPendingTasks)
throw new BrowserLimitException("The browser task queue is full.");
var delay = animation ? 1 : Math.Clamp(Number(Value(args, 1)), 0, int.MaxValue);
var interval = repeat ? Math.Max(1, (long)delay) : 0;
var id = nextId == int.MaxValue ? nextId = 1 : ++nextId;
while (tasks.ContainsKey(id)) id = ++nextId;
tasks.Add(id, new(id, callback, animation ? [] : args.Skip(2).ToArray(), checked(Now + (long)delay), interval, animation));
return id;
}
private void Install(string name, Func<JsValue[], JsValue> callback) => engine.SetValue(name,
new ClrFunction(engine, name, (_, args) => callback(args)));
private static JsValue Value(JsValue[] args, int index) => index < args.Length ? args[index] : JsValue.Undefined;
private static double Number(JsValue value) => value.IsNumber() && double.IsFinite(value.AsNumber()) ? value.AsNumber() : 0;
private static int Integer(JsValue value) => (int)Math.Clamp(Number(value), int.MinValue, int.MaxValue);
}