using System.Collections.Concurrent; using System.Collections.Generic; namespace UnityTcp.Editor.Helpers { /// /// Thread-safe index of every in-flight runner job, keyed by job id and by the native request /// id that started it. The request-id index is what lets a client detach a job it has not yet /// received a job id for — it always knows the request id of the call it is stuck waiting on. /// /// Runners register on enroll (and re-registers restored jobs /// after a domain reload) and unregister when the job leaves their registry. The handle a /// runner registers is what makes cross-thread detach possible: its /// touches only thread-safe state (the /// delivery gate, , the native response /// queue), never the runner's main-thread-only job list. /// public static class JobRegistry { /// A registered in-flight job, with the thread-safe detach entry point. public interface IJobHandle { string JobId { get; } ulong RequestId { get; } string Name { get; } /// Owning runner: "step", "task", or "coroutine". string Runner { get; } bool Detached { get; } /// /// Detaches the job: answers its original request with a detach ack carrying the job /// id, after which the outcome lands in for polling. /// Callable from any thread. Returns false (with an explanation) when the job was /// already detached or already answered. /// bool TryDetach(string reason, out string error); } private static readonly ConcurrentDictionary _byJobId = new ConcurrentDictionary(); private static readonly ConcurrentDictionary _jobIdByRequestId = new ConcurrentDictionary(); internal static void Register(IJobHandle handle) { if (handle == null || string.IsNullOrEmpty(handle.JobId)) return; _byJobId[handle.JobId] = handle; if (handle.RequestId != 0) _jobIdByRequestId[handle.RequestId] = handle.JobId; } internal static void Unregister(string jobId) { if (string.IsNullOrEmpty(jobId)) return; if (_byJobId.TryRemove(jobId, out var handle) && handle.RequestId != 0) { // Only drop the request mapping if it still points at this job — a handed-off job // re-registers under the same request id before the old runner unregisters. if (_jobIdByRequestId.TryGetValue(handle.RequestId, out string mapped) && mapped == jobId) _jobIdByRequestId.TryRemove(handle.RequestId, out _); } } public static bool TryGet(string jobId, out IJobHandle handle) { handle = null; return !string.IsNullOrEmpty(jobId) && _byJobId.TryGetValue(jobId, out handle); } public static bool TryGetByRequestId(ulong requestId, out IJobHandle handle) { handle = null; return requestId != 0 && _jobIdByRequestId.TryGetValue(requestId, out string jobId) && _byJobId.TryGetValue(jobId, out handle); } /// Snapshot of every in-flight job, for manage_job.list. public static List Snapshot() { var result = new List(); foreach (var kv in _byJobId) result.Add(kv.Value); return result; } public static int Count => _byJobId.Count; internal static void Clear() { _byJobId.Clear(); _jobIdByRequestId.Clear(); } } }