using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace UnityTcp.Editor.Helpers { /// /// Manages long-running asynchronous operation states (compilation, UPM, baking, etc.) /// Provides operation ID generation, status tracking, and timeout management. /// public static class AsyncOperationTracker { /// /// Job status enum matching MCP protocol. /// public enum JobStatus { Pending, Complete, Error } /// /// Job type enum for categorizing operations. /// public enum JobType { Compilation, UpmPackage, NavMeshBake, LightingBake, Custom } /// /// Represents a tracked job/operation. /// public class Job { public string OpId { get; set; } public JobType Type { get; set; } public JobStatus Status { get; set; } public DateTime CreatedAt { get; set; } public DateTime? CompletedAt { get; set; } public string Message { get; set; } public object Data { get; set; } public string ErrorMessage { get; set; } public float Progress { get; set; } // 0.0 to 1.0 } // Job storage private static readonly Dictionary _jobs = new Dictionary(); private static readonly object _jobsLock = new object(); // Default timeout in seconds private const int DefaultTimeoutSeconds = 300; /// /// Creates a new job with a unique op_id and registers it. /// public static Job CreateJob(JobType type, string message = null) { var job = new Job { OpId = GenerateOpId(), Type = type, Status = JobStatus.Pending, CreatedAt = DateTime.UtcNow, Message = message ?? $"{type} operation started", Progress = 0.0f }; lock (_jobsLock) { _jobs[job.OpId] = job; } return job; } /// /// Gets a job by op_id. /// public static Job GetJob(string opId) { lock (_jobsLock) { return _jobs.TryGetValue(opId, out var job) ? job : null; } } /// /// Updates job status to Complete. /// public static void CompleteJob(string opId, string message = null, object data = null) { lock (_jobsLock) { if (_jobs.TryGetValue(opId, out var job)) { job.Status = JobStatus.Complete; job.CompletedAt = DateTime.UtcNow; job.Progress = 1.0f; if (message != null) job.Message = message; if (data != null) job.Data = data; } } } /// /// Updates job status to Error. /// public static void FailJob(string opId, string errorMessage) { lock (_jobsLock) { if (_jobs.TryGetValue(opId, out var job)) { job.Status = JobStatus.Error; job.CompletedAt = DateTime.UtcNow; job.ErrorMessage = errorMessage; job.Message = $"Operation failed: {errorMessage}"; } } } /// /// Updates job progress (0.0 to 1.0). /// public static void UpdateProgress(string opId, float progress, string message = null) { lock (_jobsLock) { if (_jobs.TryGetValue(opId, out var job)) { job.Progress = Mathf.Clamp01(progress); if (message != null) job.Message = message; } } } /// /// Removes a job from tracking. /// public static void RemoveJob(string opId) { lock (_jobsLock) { _jobs.Remove(opId); } } /// /// Gets all pending jobs of a specific type. /// public static List GetPendingJobs(JobType? type = null) { lock (_jobsLock) { return _jobs.Values .Where(j => j.Status == JobStatus.Pending && (!type.HasValue || j.Type == type.Value)) .ToList(); } } /// /// Cleans up old jobs that have been completed or timed out. /// Should be called periodically. /// public static void CleanupOldJobs(int maxAgeSeconds = 3600) { var cutoff = DateTime.UtcNow.AddSeconds(-maxAgeSeconds); lock (_jobsLock) { var toRemove = _jobs .Where(kv => kv.Value.CompletedAt.HasValue && kv.Value.CompletedAt.Value < cutoff) .Select(kv => kv.Key) .ToList(); foreach (var opId in toRemove) { _jobs.Remove(opId); } if (toRemove.Count > 0) { Debug.Log($"[AsyncOperationTracker] Cleaned up {toRemove.Count} old jobs"); } } } /// /// Checks if a job has timed out. /// public static bool IsJobTimedOut(string opId, int timeoutSeconds = DefaultTimeoutSeconds) { lock (_jobsLock) { if (_jobs.TryGetValue(opId, out var job)) { if (job.Status == JobStatus.Pending) { var elapsed = (DateTime.UtcNow - job.CreatedAt).TotalSeconds; return elapsed > timeoutSeconds; } } } return false; } /// /// Creates a Pending async operation response for a job. /// Protocol: status/poll_interval/op_id /// public static object CreatePendingResponse(Job job, object stateDelta = null) { var response = new Dictionary { ["status"] = "pending", ["poll_interval"] = 1.0, // Poll every 1 second ["op_id"] = job.OpId, ["success"] = true, ["message"] = job.Message, ["data"] = new { type = job.Type.ToString(), progress = job.Progress, createdAt = job.CreatedAt.ToString("o") } }; // Add operations state delta showing the new pending operation var opDelta = StateComposer.CreateOperationsDelta(new[] { new { id = job.OpId, type = job.Type.ToString(), progress = job.Progress, message = job.Message } }); response["state_delta"] = stateDelta != null ? StateComposer.MergeStateDeltas(opDelta, stateDelta) : opDelta; return response; } /// /// Creates a Complete async operation response for a job. /// Protocol: status/op_id /// public static object CreateCompleteResponse(Job job, object stateDelta = null) { var response = new Dictionary { ["status"] = "complete", ["op_id"] = job.OpId, ["success"] = true, ["message"] = job.Message, ["data"] = job.Data }; // Include state_delta if provided if (stateDelta != null) { response["state_delta"] = stateDelta; } return response; } /// /// Creates an Error async operation response for a job. /// Protocol: status/op_id /// public static object CreateErrorResponse(Job job, object stateDelta = null) { var response = new Dictionary { ["status"] = "error", ["op_id"] = job.OpId, ["success"] = false, ["message"] = job.Message, ["error"] = job.ErrorMessage }; // Include state_delta if provided if (stateDelta != null) { response["state_delta"] = stateDelta; } return response; } /// /// Generates a unique operation ID. /// private static string GenerateOpId() { return Guid.NewGuid().ToString("N"); } /// /// Gets count of all jobs by status. /// public static Dictionary GetJobCounts() { lock (_jobsLock) { return _jobs.Values .GroupBy(j => j.Status) .ToDictionary(g => g.Key, g => g.Count()); } } } }