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); operation.FinishWorker(); }
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ operation.Fail(BoundedText.Safe(error.Message, 512), 0); operation.FinishWorker(); }
return operation;
}
private void Prepare(Operation operation)
{
BrowserService? ownedPage=null;EditorPreparationResult? completed=null;bool transferred=false;
try
{
completed=PrepareCore(operation,browser=>ownedPage=browser);
if(completed is not null)transferred=operation.Complete(completed);
}
catch(Exception error)when(error is not OutOfMemoryException and not StackOverflowException)
{
if(operation.Cancelled)operation.Report("Cancelled",ThreadId);
else operation.Fail(BoundedText.Safe(error.Message,512),ThreadId);
}
// Common explicit owner cleanup also covers every cancelled early
// return, failed initialization, and cancelled publication CAS.
if(!transferred)
{
if(completed?.Page is BackgroundBrowserEditorSession background)background.RetireOnOwner();
else ownedPage?.Release();
}
operation.FinishWorker();
}
private EditorPreparationResult? PrepareCore(Operation operation,Action<BrowserService> ownPage)
{
int workerThread = ThreadId;
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
operation.Report("WorkBegin", workerThread);
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.");
ownPage(browser);
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 null; }
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 null; }
operation.Report("FrameReady", workerThread, initializationPumps);
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
// 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 null; }
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 null; }
var session = new BackgroundBrowserEditorSession(browser, scheduler, request,
() => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled);
operation.Report("Prepared", workerThread);
return 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.
}
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 sealed record Publication(EditorPreparationResult? Result,bool Cancelled=false,bool Finished=false);
private Publication publication=new(null);
public EditorPreparationRequest Request => request;
public bool Cancelled => Volatile.Read(ref publication).Cancelled;
public bool IsRetired
{get{var state=Volatile.Read(ref publication);return state.Finished&&(state.Result?.Page is not IBackgroundEditorSession background||background.IsRetired);}}
public void Cancel()
{
while(true)
{
var state=Volatile.Read(ref publication);if(state.Cancelled)break;
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Cancelled=true},state),state))break;
}
if(Volatile.Read(ref publication).Result?.Page is IBackgroundEditorSession background)background.Release();
}
public void AdvanceRetirement(long monotonicMilliseconds)
{
var state=Volatile.Read(ref publication);if(!state.Cancelled)return;
if(state.Result?.Page is IBackgroundEditorSession background)
{
background.Release();background.AdvanceTime(monotonicMilliseconds);
if(!background.IsRetired)return;
Interlocked.CompareExchange(ref publication,state with{Result=null},state);
}
}
public bool TryTakeResult(out EditorPreparationResult completed)
{
while(true)
{
var state=Volatile.Read(ref publication);
if(state.Cancelled||state.Result is null){completed=null!;return false;}
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=null},state),state))
{completed=state.Result;return true;}
}
}
public bool Complete(EditorPreparationResult completed)
{
while(true)
{
var state=Volatile.Read(ref publication);if(state.Cancelled)return false;
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=completed},state),state))return true;
}
}
public void FinishWorker()
{while(true){var state=Volatile.Read(ref publication);if(state.Finished||ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Finished=true},state),state))return;}}
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); operation.FinishWorker(); }
}
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ operation.Fail(BoundedText.Safe(error.Message, 512), 0); operation.FinishWorker(); }
return operation;
}
private void Prepare(Operation operation)
{
BrowserService? ownedPage=null;EditorPreparationResult? completed=null;bool transferred=false;
try
{
completed=PrepareCore(operation,browser=>ownedPage=browser);
if(completed is not null)transferred=operation.Complete(completed);
}
catch(Exception error)when(error is not OutOfMemoryException and not StackOverflowException)
{
if(operation.Cancelled)operation.Report("Cancelled",ThreadId);
else operation.Fail(BoundedText.Safe(error.Message,512),ThreadId);
}
// Common explicit owner cleanup also covers every cancelled early
// return, failed initialization, and cancelled publication CAS.
if(!transferred)
{
if(completed?.Page is BackgroundBrowserEditorSession background)background.RetireOnOwner();
else ownedPage?.Release();
}
operation.FinishWorker();
}
private EditorPreparationResult? PrepareCore(Operation operation,Action<BrowserService> ownPage)
{
int workerThread = ThreadId;
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
operation.Report("WorkBegin", workerThread);
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.");
ownPage(browser);
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 null; }
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 null; }
operation.Report("FrameReady", workerThread, initializationPumps);
if (operation.Cancelled) { operation.Report("Cancelled", workerThread); return null; }
// 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 null; }
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 null; }
var session = new BackgroundBrowserEditorSession(browser, scheduler, request,
() => Clock(request, 136), () => ThreadId, trace, () => operation.Cancelled);
operation.Report("Prepared", workerThread);
return 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.
}
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 sealed record Publication(EditorPreparationResult? Result,bool Cancelled=false,bool Finished=false);
private Publication publication=new(null);
public EditorPreparationRequest Request => request;
public bool Cancelled => Volatile.Read(ref publication).Cancelled;
public bool IsRetired
{get{var state=Volatile.Read(ref publication);return state.Finished&&(state.Result?.Page is not IBackgroundEditorSession background||background.IsRetired);}}
public void Cancel()
{
while(true)
{
var state=Volatile.Read(ref publication);if(state.Cancelled)break;
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Cancelled=true},state),state))break;
}
if(Volatile.Read(ref publication).Result?.Page is IBackgroundEditorSession background)background.Release();
}
public void AdvanceRetirement(long monotonicMilliseconds)
{
var state=Volatile.Read(ref publication);if(!state.Cancelled)return;
if(state.Result?.Page is IBackgroundEditorSession background)
{
background.Release();background.AdvanceTime(monotonicMilliseconds);
if(!background.IsRetired)return;
Interlocked.CompareExchange(ref publication,state with{Result=null},state);
}
}
public bool TryTakeResult(out EditorPreparationResult completed)
{
while(true)
{
var state=Volatile.Read(ref publication);
if(state.Cancelled||state.Result is null){completed=null!;return false;}
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=null},state),state))
{completed=state.Result;return true;}
}
}
public bool Complete(EditorPreparationResult completed)
{
while(true)
{
var state=Volatile.Read(ref publication);if(state.Cancelled)return false;
if(ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Result=completed},state),state))return true;
}
}
public void FinishWorker()
{while(true){var state=Volatile.Read(ref publication);if(state.Finished||ReferenceEquals(Interlocked.CompareExchange(ref publication,state with{Finished=true},state),state))return;}}
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");
}
}