using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// A page and its Jint engine belong exclusively to one worker for their entire
/// lifetime. Only immutable paint/model snapshots are published to the desktop.
/// The scheduler and isolated real-font canvas are explicit platform capabilities.
/// </summary>
public sealed class BrowserEditorPreparationService(
IInteractiveBrowserFactory factory, IEditorBackgroundWorkScheduler scheduler,
IEditorPreparationCanvasFactory canvasFactory, Action<string>? trace = null,
Func<int>? threadIdentity = null, Func<long>? monotonicMilliseconds = null) : IEditorPreparationService
{
private int ThreadId => threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId;
public IEditorPreparationOperation Begin(EditorPreparationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.Source);
ArgumentNullException.ThrowIfNull(request.Path);
var operation = new Operation(request, ThreadId, trace);
operation.Report("Begin", ThreadId);
try
{
if (!scheduler.TryQueue(() => Prepare(operation)))
operation.Fail("The verified background editor worker is unavailable.", 0);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ operation.Fail(BoundedText.Safe(error.Message, 512), 0); }
return operation;
}
private void Prepare(Operation operation)
{
int workerThread = ThreadId;
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
operation.Report("WorkBegin", workerThread);
try
{
var request = operation.Request;
if (request.ViewportWidth is < 1 or > 100_000 || request.ViewportHeight is < 1 or > 100_000)
throw new ArgumentOutOfRangeException(nameof(request), "Invalid editor viewport.");
// Both page construction and private font setup can be substantial;
// neither is performed by Begin or by the desktop's polling loop.
var canvas = canvasFactory.CreateEditorPreparationCanvas();
if (canvas is not IDesktopWebCanvas)
throw new NotSupportedException("Editor preparation requires actual text metrics and rasterization.");
var page = factory.CreatePageSession();
if (page is not BrowserService browser)
throw new NotSupportedException("Background editor pages require the actual managed browser engine.");
browser.SetOperationCancellation(() => operation.Cancelled);
page.SetCanvas(canvas);
page.ResizeViewport(request.ViewportWidth, request.ViewportHeight);
page.NavigateEditor(request.Source, request.Path);
BrowserDisplayList? layout = null;
int initializationPumps = 0;
for (; initializationPumps <= 8; initializationPumps++)
{
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
layout = browser.ReadyEditorLayout(request.Source, request.Path, request.ViewportWidth, request.ViewportHeight);
if (layout is not null) break;
if (initializationPumps == 8) throw new IOException("Monaco did not produce the actual source view before the initialization limit.");
// The desktop and worker use the SAME clock domain. A fixed
// 17..136 clock left long-delayed initialization callbacks for
// the UI's first 20,000+ ms pump, freezing the handoff there.
page.AdvanceTime(Clock(request, (initializationPumps + 1) * 17));
}
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
operation.Report("FrameReady", workerThread, initializationPumps);
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
// Caret/RAF/observer work is perpetual. Readiness means a real model
// and frame exist, not that all browser timers can become silent.
// A SECOND private canvas is delivered to the UI. The continuing
// worker must never share its mutable fonts/caches with scanout.
var deliveryCanvas = canvasFactory.CreateEditorPreparationCanvas();
if (deliveryCanvas is IEditorPreparationCanvasHandoff cache)
foreach (var command in layout!.Commands)
{
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && command.Text.Length != 0 &&
command.Bounds.Intersect(new(0, 0, request.ViewportWidth, request.ViewportHeight)) is { Width: > 0, Height: > 0 })
cache.PrepareEditorText(command.Text, command.Font.Size, command.Font.Family, command.Font.Weight);
}
if (page.ReadEditorText() != request.Source)
throw new IOException("The prepared Monaco model did not preserve the source text.");
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
var session = new BackgroundBrowserEditorSession(browser, scheduler, request,
() => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled);
if (operation.Cancelled) { session.Cancel(); operation.Report("Cancelled", workerThread); return; }
operation.Report("Prepared", workerThread);
operation.Complete(new(session, null, workerThread, deliveryCanvas));
// Only the proxy crosses into the UI. The actual page remains
// worker-owned for timers, input, model readback and future layout.
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if (operation.Cancelled) operation.Report("Cancelled", workerThread);
else operation.Fail(BoundedText.Safe(error.Message, 512), workerThread);
}
}
private long Clock(EditorPreparationRequest request, int fallbackAdvance) =>
Math.Max(request.MonotonicMilliseconds, monotonicMilliseconds?.Invoke() ?? checked(request.MonotonicMilliseconds + fallbackAdvance));
private sealed class Operation(EditorPreparationRequest request, int uiThread, Action<string>? trace) : IEditorPreparationOperation
{
private EditorPreparationResult? result;
private int cancelled;
public EditorPreparationRequest Request => request;
public bool Cancelled => Volatile.Read(ref cancelled) != 0;
public void Cancel() => Volatile.Write(ref cancelled, 1);
public bool TryTakeResult(out EditorPreparationResult completed)
{
completed = Interlocked.Exchange(ref result, null)!;
return completed is not null;
}
public void Complete(EditorPreparationResult completed)
{
if (!Cancelled) Volatile.Write(ref result, completed);
}
public void Fail(string message, int workerThread)
{
Report("Failed", workerThread);
Complete(new(null, message, workerThread));
}
public void Report(string state, int workerThread, int initializationPumps = -1) => trace?.Invoke("XFE_EDITOR_BACKGROUND|" + state +
"|Window|" + request.WindowId + "|Generation|" + request.Generation + "|UiThread|" + uiThread +
"|Thread|" + workerThread + "|SourceCharacters|" + request.Source.Length +
(initializationPumps < 0 ? "" : "|InitializationPumps|" + initializationPumps) + "\n");
}
}
using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// A page and its Jint engine belong exclusively to one worker for their entire
/// lifetime. Only immutable paint/model snapshots are published to the desktop.
/// The scheduler and isolated real-font canvas are explicit platform capabilities.
/// </summary>
public sealed class BrowserEditorPreparationService(
IInteractiveBrowserFactory factory, IEditorBackgroundWorkScheduler scheduler,
IEditorPreparationCanvasFactory canvasFactory, Action<string>? trace = null,
Func<int>? threadIdentity = null, Func<long>? monotonicMilliseconds = null) : IEditorPreparationService
{
private int ThreadId => threadIdentity?.Invoke() ?? Environment.CurrentManagedThreadId;
public IEditorPreparationOperation Begin(EditorPreparationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(request.Source);
ArgumentNullException.ThrowIfNull(request.Path);
var operation = new Operation(request, ThreadId, trace);
operation.Report("Begin", ThreadId);
try
{
if (!scheduler.TryQueue(() => Prepare(operation)))
operation.Fail("The verified background editor worker is unavailable.", 0);
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ operation.Fail(BoundedText.Safe(error.Message, 512), 0); }
return operation;
}
private void Prepare(Operation operation)
{
int workerThread = ThreadId;
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
operation.Report("WorkBegin", workerThread);
try
{
var request = operation.Request;
if (request.ViewportWidth is < 1 or > 100_000 || request.ViewportHeight is < 1 or > 100_000)
throw new ArgumentOutOfRangeException(nameof(request), "Invalid editor viewport.");
// Both page construction and private font setup can be substantial;
// neither is performed by Begin or by the desktop's polling loop.
var canvas = canvasFactory.CreateEditorPreparationCanvas();
if (canvas is not IDesktopWebCanvas)
throw new NotSupportedException("Editor preparation requires actual text metrics and rasterization.");
var page = factory.CreatePageSession();
if (page is not BrowserService browser)
throw new NotSupportedException("Background editor pages require the actual managed browser engine.");
browser.SetOperationCancellation(() => operation.Cancelled);
page.SetCanvas(canvas);
page.ResizeViewport(request.ViewportWidth, request.ViewportHeight);
page.NavigateEditor(request.Source, request.Path);
BrowserDisplayList? layout = null;
int initializationPumps = 0;
for (; initializationPumps <= 8; initializationPumps++)
{
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
layout = browser.ReadyEditorLayout(request.Source, request.Path, request.ViewportWidth, request.ViewportHeight);
if (layout is not null) break;
if (initializationPumps == 8) throw new IOException("Monaco did not produce the actual source view before the initialization limit.");
// The desktop and worker use the SAME clock domain. A fixed
// 17..136 clock left long-delayed initialization callbacks for
// the UI's first 20,000+ ms pump, freezing the handoff there.
page.AdvanceTime(Clock(request, (initializationPumps + 1) * 17));
}
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
operation.Report("FrameReady", workerThread, initializationPumps);
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
// Caret/RAF/observer work is perpetual. Readiness means a real model
// and frame exist, not that all browser timers can become silent.
// A SECOND private canvas is delivered to the UI. The continuing
// worker must never share its mutable fonts/caches with scanout.
var deliveryCanvas = canvasFactory.CreateEditorPreparationCanvas();
if (deliveryCanvas is IEditorPreparationCanvasHandoff cache)
foreach (var command in layout!.Commands)
{
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && command.Text.Length != 0 &&
command.Bounds.Intersect(new(0, 0, request.ViewportWidth, request.ViewportHeight)) is { Width: > 0, Height: > 0 })
cache.PrepareEditorText(command.Text, command.Font.Size, command.Font.Family, command.Font.Weight);
}
if (page.ReadEditorText() != request.Source)
throw new IOException("The prepared Monaco model did not preserve the source text.");
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return; }
var session = new BackgroundBrowserEditorSession(browser, scheduler, request,
() => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled);
if (operation.Cancelled) { session.Cancel(); operation.Report("Cancelled", workerThread); return; }
operation.Report("Prepared", workerThread);
operation.Complete(new(session, null, workerThread, deliveryCanvas));
// Only the proxy crosses into the UI. The actual page remains
// worker-owned for timers, input, model readback and future layout.
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
if (operation.Cancelled) operation.Report("Cancelled", workerThread);
else operation.Fail(BoundedText.Safe(error.Message, 512), workerThread);
}
}
private long Clock(EditorPreparationRequest request, int fallbackAdvance) =>
Math.Max(request.MonotonicMilliseconds, monotonicMilliseconds?.Invoke() ?? checked(request.MonotonicMilliseconds + fallbackAdvance));
private sealed class Operation(EditorPreparationRequest request, int uiThread, Action<string>? trace) : IEditorPreparationOperation
{
private EditorPreparationResult? result;
private int cancelled;
public EditorPreparationRequest Request => request;
public bool Cancelled => Volatile.Read(ref cancelled) != 0;
public void Cancel() => Volatile.Write(ref cancelled, 1);
public bool TryTakeResult(out EditorPreparationResult completed)
{
completed = Interlocked.Exchange(ref result, null)!;
return completed is not null;
}
public void Complete(EditorPreparationResult completed)
{
if (!Cancelled) Volatile.Write(ref result, completed);
}
public void Fail(string message, int workerThread)
{
Report("Failed", workerThread);
Complete(new(null, message, workerThread));
}
public void Report(string state, int workerThread, int initializationPumps = -1) => trace?.Invoke("XFE_EDITOR_BACKGROUND|" + state +
"|Window|" + request.WindowId + "|Generation|" + request.Generation + "|UiThread|" + uiThread +
"|Thread|" + workerThread + "|SourceCharacters|" + request.Source.Length +
(initializationPumps < 0 ? "" : "|InitializationPumps|" + initializationPumps) + "\n");
}
}