Files
daluandou/.codely.packages/cn.tuanjie.codely.bridge@1.0.85-47f31100/Editor/Bridge/Helpers/JobContext.cs
T
huadexuan 13d0c80335 ```
chore(bridge): 删除桥接包的提交记录和Git属性配置

删除了 .codely-bridge-commit 文件中存储的提交哈希值
移除了 .gitattributes 文件中的行结束符配置规则
```
2026-09-24 21:41:14 +08:00

146 lines
6.8 KiB
C#

using System;
using System.Collections;
using System.Threading;
using Codely.Newtonsoft.Json;
namespace UnityTcp.Editor.Helpers
{
/// <summary>
/// A tracked async job: the native <see cref="RequestId"/> to reply on, plus an optional
/// <see cref="Response"/> outcome that gets serialized when the job finishes.
///
/// Created via a runner's CreateJob(requestId, name). A coroutine reports its outcome with
/// <see cref="SetResult"/> / <see cref="SetError"/> (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, <see cref="ToResponseJson"/>
/// synthesizes a Response from the job name/id.
///
/// The original request is answered exactly once, guarded by a single-delivery gate
/// (<see cref="TryClaimRequestDelivery"/>): either by the runner when the job finishes, or by
/// a mid-flight detach (<see cref="JobControl"/>, possibly from a non-main thread) that
/// early-returns the job id — whichever claims the gate first. The loser routes the outcome
/// to <see cref="DetachedJobs"/> instead, so a detach racing a completion never double-answers
/// and never loses the result.
/// </summary>
public sealed class JobContext
{
/// <summary>Native request id — passed to NativeUnityTcpBridgeHost.EnqueueResponse.</summary>
public ulong RequestId { get; }
/// <summary>Human-readable job name (e.g. the command type), echoed in the response.</summary>
public string Name { get; }
/// <summary>Stable id for correlating a detached job across the client's poll requests.</summary>
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;
/// <summary>
/// When true, the finished job is memorized (see <see cref="DetachedJobs"/>) instead of
/// being sent on <see cref="RequestId"/>: the client early-returned with <see cref="JobId"/>
/// and polls for the result later. Set at launch, or by a mid-flight detach (see
/// <see cref="JobControl"/>).
/// </summary>
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");
}
/// <summary>
/// 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 <see cref="DetachedJobs"/>.
/// </summary>
internal bool TryClaimRequestDelivery()
=> Interlocked.CompareExchange(ref _requestAnswered, 1, 0) == 0;
/// <summary>Success Response to send, or null to synthesize from the job name.</summary>
internal object Result { get; private set; }
/// <summary>Error Response to send, or null to synthesize from the job name.</summary>
internal object Error { get; private set; }
internal bool Failed { get; private set; }
/// <summary>True once <see cref="SetResult"/> or <see cref="SetError"/> has been called.</summary>
internal bool HasOutcome { get; private set; }
/// <summary>
/// Records the success Response. Pass null to let <see cref="ToResponseJson"/> build a
/// default success from the job name.
/// </summary>
public void SetResult(object response = null)
{
Failed = false;
Error = null;
Result = response;
HasOutcome = true;
}
/// <summary>
/// Records the error Response. Pass null to let <see cref="ToResponseJson"/> build a
/// default error from the job name.
/// </summary>
public void SetError(object response = null)
{
Failed = true;
Result = null;
Error = response;
HasOutcome = true;
}
/// <summary>Builds the response JSON the runner enqueues to the client when finished.</summary>
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);
}
/// <summary>
/// Early-return response for a detached job: tells the client the job started and to poll
/// for the result later using <see cref="JobId"/>.
/// </summary>
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" });
/// <summary>
/// Flips an attached job to detached after launch. Callable from any thread — the caller
/// must have claimed <see cref="TryClaimRequestDelivery"/> first (see
/// <see cref="JobControl"/>).
/// </summary>
internal void MarkDetached() => Volatile.Write(ref _detached, true);
/// <summary>
/// Ack sent on the original request when the job is detached mid-flight: tells the caller
/// to stop waiting, and how to poll <see cref="JobId"/> for the eventual result.
/// </summary>
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." }));
/// <summary>True when <paramref name="value"/> looks like a <see cref="Response"/> payload.</summary>
internal static bool IsResponse(object value)
=> value is IDictionary d && d.Contains("success");
}
}