using System; using System.Collections; using System.Threading; using Codely.Newtonsoft.Json; namespace UnityTcp.Editor.Helpers { /// /// A tracked async job: the native to reply on, plus an optional /// outcome that gets serialized when the job finishes. /// /// Created via a runner's CreateJob(requestId, name). A coroutine reports its outcome with /// / (it cannot return a value). A Task may /// return a Response from its result; otherwise the AsyncTaskRunner fills the context. /// /// Result and Error are Response objects and may be null. When null, /// synthesizes a Response from the job name/id. /// /// The original request is answered exactly once, guarded by a single-delivery gate /// (): either by the runner when the job finishes, or by /// a mid-flight detach (, possibly from a non-main thread) that /// early-returns the job id — whichever claims the gate first. The loser routes the outcome /// to instead, so a detach racing a completion never double-answers /// and never loses the result. /// public sealed class JobContext { /// Native request id — passed to NativeUnityTcpBridgeHost.EnqueueResponse. public ulong RequestId { get; } /// Human-readable job name (e.g. the command type), echoed in the response. public string Name { get; } /// Stable id for correlating a detached job across the client's poll requests. public string JobId { get; } // 1 once the original request has been (or is being) answered — by the handler's early // pending response for a job launched detached, by a mid-flight detach ack, or by the // finish path. Interlocked, because detach may claim it from a non-main thread while the // runner finishes the job on the main thread. private int _requestAnswered; /// /// When true, the finished job is memorized (see ) instead of /// being sent on : the client early-returned with /// and polls for the result later. Set at launch, or by a mid-flight detach (see /// ). /// public bool Detached => Volatile.Read(ref _detached); private bool _detached; internal JobContext(ulong requestId, string name, bool detached) { RequestId = requestId; Name = name; _detached = detached; // A job launched detached is answered by the handler itself (ToPendingResponse), so // the gate starts claimed and the finish path goes straight to DetachedJobs. _requestAnswered = detached ? 1 : 0; JobId = Guid.NewGuid().ToString("N"); } /// /// Claims the one-and-only delivery on the original request. Returns true exactly once /// per context; thread-safe. The finish path answers the request only when it wins this /// claim — otherwise the result goes to . /// internal bool TryClaimRequestDelivery() => Interlocked.CompareExchange(ref _requestAnswered, 1, 0) == 0; /// Success Response to send, or null to synthesize from the job name. internal object Result { get; private set; } /// Error Response to send, or null to synthesize from the job name. internal object Error { get; private set; } internal bool Failed { get; private set; } /// True once or has been called. internal bool HasOutcome { get; private set; } /// /// Records the success Response. Pass null to let build a /// default success from the job name. /// public void SetResult(object response = null) { Failed = false; Error = null; Result = response; HasOutcome = true; } /// /// Records the error Response. Pass null to let build a /// default error from the job name. /// public void SetError(object response = null) { Failed = true; Result = null; Error = response; HasOutcome = true; } /// Builds the response JSON the runner enqueues to the client when finished. internal string ToResponseJson() { var jobData = new { job = Name, id = JobId }; object response = Failed ? (Error ?? Response.Error(Name ?? "Operation failed.", jobData)) : (Result ?? Response.Success(Name ?? "Operation completed.", jobData)); return JsonConvert.SerializeObject(response); } /// /// Early-return response for a detached job: tells the client the job started and to poll /// for the result later using . /// public object ToPendingResponse() => Response.Success( $"Job started. Use the manage_job tool (action 'status', job_id '{JobId}') to query the job's status and collect its result.", new { job = Name, id = JobId, status = "pending" }); /// /// Flips an attached job to detached after launch. Callable from any thread — the caller /// must have claimed first (see /// ). /// internal void MarkDetached() => Volatile.Write(ref _detached, true); /// /// Ack sent on the original request when the job is detached mid-flight: tells the caller /// to stop waiting, and how to poll for the eventual result. /// internal string ToDetachedResponseJson(string reason) => JsonConvert.SerializeObject(Response.Success( $"Job detached. Use the manage_job tool (action 'status', job_id '{JobId}') to query the job's status and collect its result.", new { job = Name, id = JobId, status = "detached", reason = reason ?? "Detached by request." })); /// True when looks like a payload. internal static bool IsResponse(object value) => value is IDictionary d && d.Contains("success"); } }