316 lines
9.7 KiB
C#
316 lines
9.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
|
|
namespace UnityTcp.Editor.Helpers
|
|
{
|
|
/// <summary>
|
|
/// Manages long-running asynchronous operation states (compilation, UPM, baking, etc.)
|
|
/// Provides operation ID generation, status tracking, and timeout management.
|
|
/// </summary>
|
|
public static class AsyncOperationTracker
|
|
{
|
|
/// <summary>
|
|
/// Job status enum matching MCP protocol.
|
|
/// </summary>
|
|
public enum JobStatus
|
|
{
|
|
Pending,
|
|
Complete,
|
|
Error
|
|
}
|
|
|
|
/// <summary>
|
|
/// Job type enum for categorizing operations.
|
|
/// </summary>
|
|
public enum JobType
|
|
{
|
|
Compilation,
|
|
UpmPackage,
|
|
NavMeshBake,
|
|
LightingBake,
|
|
Custom
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a tracked job/operation.
|
|
/// </summary>
|
|
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<string, Job> _jobs = new Dictionary<string, Job>();
|
|
private static readonly object _jobsLock = new object();
|
|
|
|
// Default timeout in seconds
|
|
private const int DefaultTimeoutSeconds = 300;
|
|
|
|
/// <summary>
|
|
/// Creates a new job with a unique op_id and registers it.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a job by op_id.
|
|
/// </summary>
|
|
public static Job GetJob(string opId)
|
|
{
|
|
lock (_jobsLock)
|
|
{
|
|
return _jobs.TryGetValue(opId, out var job) ? job : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates job status to Complete.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates job status to Error.
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates job progress (0.0 to 1.0).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a job from tracking.
|
|
/// </summary>
|
|
public static void RemoveJob(string opId)
|
|
{
|
|
lock (_jobsLock)
|
|
{
|
|
_jobs.Remove(opId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all pending jobs of a specific type.
|
|
/// </summary>
|
|
public static List<Job> GetPendingJobs(JobType? type = null)
|
|
{
|
|
lock (_jobsLock)
|
|
{
|
|
return _jobs.Values
|
|
.Where(j => j.Status == JobStatus.Pending && (!type.HasValue || j.Type == type.Value))
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up old jobs that have been completed or timed out.
|
|
/// Should be called periodically.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a job has timed out.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Pending async operation response for a job.
|
|
/// Protocol: status/poll_interval/op_id
|
|
/// </summary>
|
|
public static object CreatePendingResponse(Job job, object stateDelta = null)
|
|
{
|
|
var response = new Dictionary<string, object>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Complete async operation response for a job.
|
|
/// Protocol: status/op_id
|
|
/// </summary>
|
|
public static object CreateCompleteResponse(Job job, object stateDelta = null)
|
|
{
|
|
var response = new Dictionary<string, object>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an Error async operation response for a job.
|
|
/// Protocol: status/op_id
|
|
/// </summary>
|
|
public static object CreateErrorResponse(Job job, object stateDelta = null)
|
|
{
|
|
var response = new Dictionary<string, object>
|
|
{
|
|
["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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a unique operation ID.
|
|
/// </summary>
|
|
private static string GenerateOpId()
|
|
{
|
|
return Guid.NewGuid().ToString("N");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets count of all jobs by status.
|
|
/// </summary>
|
|
public static Dictionary<JobStatus, int> GetJobCounts()
|
|
{
|
|
lock (_jobsLock)
|
|
{
|
|
return _jobs.Values
|
|
.GroupBy(j => j.Status)
|
|
.ToDictionary(g => g.Key, g => g.Count());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|