using System.Collections.Concurrent; using System.Diagnostics; using Autodesk.Revit.UI; namespace RevitBridge { /// /// Thrown by queued work when no Revit document is open. /// The bridge maps this to HTTP 409 with hasActiveDocument = false. /// internal sealed class NoActiveDocumentException : InvalidOperationException { public NoActiveDocumentException() : base("No active Revit document is open.") { } } /// /// Schedules work onto the Revit API thread via an ExternalEvent and returns /// a Task that completes with the result (standard ExternalEvent + /// TaskCompletionSource dispatch). /// internal sealed class CommandQueue : IExternalEventHandler { private sealed class WorkItem { public required Func Action { get; init; } public required TaskCompletionSource Completion { get; init; } /// Stopwatch timestamp after which the item must not start; 0 = no deadline. public long DeadlineTimestamp { get; init; } } private readonly ConcurrentQueue _queue = new(); /// Set once from Application.OnStartup (a valid Revit API context). public ExternalEvent? ExternalEvent { get; set; } public Task RunAsync(Func action, TimeSpan? timeout = null) { var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _queue.Enqueue(new WorkItem { Action = app => action(app), Completion = completion, DeadlineTimestamp = timeout is { } budget ? Stopwatch.GetTimestamp() + (long)(budget.TotalSeconds * Stopwatch.Frequency) : 0, }); var externalEvent = ExternalEvent; if (externalEvent is null) { completion.TrySetException(new InvalidOperationException("Revit bridge command queue is not initialized.")); } else { externalEvent.Raise(); } return Await(completion.Task); } private static async Task Await(Task task) => (T)(await task.ConfigureAwait(false))!; public void Execute(UIApplication app) { while (_queue.TryDequeue(out var item)) { // The HTTP caller abandons the request at the same deadline, so running // the item late would mutate the model behind the agent's back. if (item.DeadlineTimestamp != 0 && Stopwatch.GetTimestamp() > item.DeadlineTimestamp) { item.Completion.TrySetException(new TimeoutException( "Tool call expired before execution; no work was performed. Revit stayed busy past the timeout_ms deadline; retry, optionally with a larger timeout_ms.")); continue; } try { item.Completion.TrySetResult(item.Action(app)); } catch (Exception ex) { item.Completion.TrySetException(ex); } } } public string GetName() => "Revit Bridge Command Queue"; } }