mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 06:13:48 +00:00
Improved workspace performance (#680)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
721d5c9070
commit
906d9ba058
16 files changed
+994
-494
No files matched your search
@@ -12,21 +12,73 @@ namespace AIStudio.Tools;
|
||||
|
||||
public static class WorkspaceBehaviour
|
||||
{
|
||||
private sealed class WorkspaceChatCacheEntry
|
||||
{
|
||||
public Guid WorkspaceId { get; init; }
|
||||
|
||||
public Guid ChatId { get; init; }
|
||||
|
||||
public string ChatPath { get; init; } = string.Empty;
|
||||
|
||||
public string ChatName { get; set; } = string.Empty;
|
||||
|
||||
public DateTimeOffset LastEditTime { get; set; }
|
||||
|
||||
public bool IsTemporary { get; init; }
|
||||
}
|
||||
|
||||
private sealed class WorkspaceCacheEntry
|
||||
{
|
||||
public Guid WorkspaceId { get; init; }
|
||||
|
||||
public string WorkspacePath { get; init; } = string.Empty;
|
||||
|
||||
public string WorkspaceName { get; set; } = string.Empty;
|
||||
|
||||
public bool ChatsLoaded { get; set; }
|
||||
|
||||
public List<WorkspaceChatCacheEntry> Chats { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class WorkspaceTreeCacheState
|
||||
{
|
||||
public Dictionary<Guid, WorkspaceCacheEntry> Workspaces { get; } = [];
|
||||
|
||||
public List<Guid> WorkspaceOrder { get; } = [];
|
||||
|
||||
public List<WorkspaceChatCacheEntry> TemporaryChats { get; set; } = [];
|
||||
|
||||
public bool IsShellLoaded { get; set; }
|
||||
}
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(WorkspaceBehaviour));
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> CHAT_STORAGE_SEMAPHORES = new();
|
||||
private static readonly SemaphoreSlim WORKSPACE_TREE_CACHE_SEMAPHORE = new(1, 1);
|
||||
private static readonly WorkspaceTreeCacheState WORKSPACE_TREE_CACHE = new();
|
||||
|
||||
private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6);
|
||||
private static volatile bool WORKSPACE_TREE_CACHE_INVALIDATED = true;
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(WorkspaceBehaviour).Namespace, nameof(WorkspaceBehaviour));
|
||||
|
||||
/// <summary>
|
||||
/// Semaphores for synchronizing chat storage operations per chat.
|
||||
/// This prevents race conditions when multiple threads try to write
|
||||
/// the same chat file simultaneously.
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> CHAT_STORAGE_SEMAPHORES = new();
|
||||
public static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for acquiring the chat storage semaphore.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan SEMAPHORE_TIMEOUT = TimeSpan.FromSeconds(6);
|
||||
private static readonly TimeSpan PREFETCH_DELAY_DURATION = TimeSpan.FromMilliseconds(45);
|
||||
|
||||
private static string WorkspaceRootDirectory => Path.Join(SettingsManager.DataDirectory, "workspaces");
|
||||
|
||||
private static string TemporaryChatsRootDirectory => Path.Join(SettingsManager.DataDirectory, "tempChats");
|
||||
|
||||
private static SemaphoreSlim GetChatSemaphore(Guid workspaceId, Guid chatId)
|
||||
{
|
||||
@@ -34,13 +86,6 @@ public static class WorkspaceBehaviour
|
||||
return CHAT_STORAGE_SEMAPHORES.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to acquire the chat storage semaphore within the configured timeout.
|
||||
/// </summary>
|
||||
/// <param name="workspaceId">The workspace ID.</param>
|
||||
/// <param name="chatId">The chat ID.</param>
|
||||
/// <param name="callerName">The name of the calling method for logging purposes.</param>
|
||||
/// <returns>A tuple containing whether the semaphore was acquired and the semaphore instance.</returns>
|
||||
private static async Task<(bool Acquired, SemaphoreSlim Semaphore)> TryAcquireChatSemaphoreAsync(Guid workspaceId, Guid chatId, string callerName)
|
||||
{
|
||||
var semaphore = GetChatSemaphore(workspaceId, chatId);
|
||||
@@ -56,18 +101,357 @@ public static class WorkspaceBehaviour
|
||||
return (acquired, semaphore);
|
||||
}
|
||||
|
||||
public static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
private static WorkspaceTreeChat ToPublicChat(WorkspaceChatCacheEntry chat) => new(chat.WorkspaceId, chat.ChatId, chat.ChatPath, chat.ChatName, chat.LastEditTime, chat.IsTemporary);
|
||||
|
||||
private static WorkspaceTreeWorkspace ToPublicWorkspace(WorkspaceCacheEntry workspace) => new(workspace.WorkspaceId,
|
||||
workspace.WorkspacePath,
|
||||
workspace.WorkspaceName,
|
||||
workspace.ChatsLoaded,
|
||||
workspace.Chats.Select(ToPublicChat).ToList());
|
||||
|
||||
private static async Task<string> ReadNameOrDefaultAsync(string nameFilePath, string fallbackName)
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
try
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
||||
if (!File.Exists(nameFilePath))
|
||||
return fallbackName;
|
||||
|
||||
var name = await File.ReadAllTextAsync(nameFilePath, Encoding.UTF8);
|
||||
return string.IsNullOrWhiteSpace(name) ? fallbackName : name;
|
||||
}
|
||||
};
|
||||
catch
|
||||
{
|
||||
return fallbackName;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<WorkspaceChatCacheEntry>> ReadWorkspaceChatsCoreAsync(Guid workspaceId, string workspacePath)
|
||||
{
|
||||
var chats = new List<WorkspaceChatCacheEntry>();
|
||||
if (!Directory.Exists(workspacePath))
|
||||
return chats;
|
||||
|
||||
foreach (var chatPath in Directory.EnumerateDirectories(workspacePath))
|
||||
{
|
||||
if (!Guid.TryParse(Path.GetFileName(chatPath), out var chatId))
|
||||
continue;
|
||||
|
||||
var chatName = await ReadNameOrDefaultAsync(Path.Join(chatPath, "name"), TB("Unnamed chat"));
|
||||
var chatThreadPath = Path.Join(chatPath, "thread.json");
|
||||
var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue;
|
||||
chats.Add(new WorkspaceChatCacheEntry
|
||||
{
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = chatId,
|
||||
ChatPath = chatPath,
|
||||
ChatName = chatName,
|
||||
LastEditTime = lastEditTime,
|
||||
IsTemporary = false,
|
||||
});
|
||||
}
|
||||
|
||||
return chats.OrderByDescending(x => x.LastEditTime).ToList();
|
||||
}
|
||||
|
||||
private static async Task<List<WorkspaceChatCacheEntry>> ReadTemporaryChatsCoreAsync()
|
||||
{
|
||||
var chats = new List<WorkspaceChatCacheEntry>();
|
||||
Directory.CreateDirectory(TemporaryChatsRootDirectory);
|
||||
|
||||
foreach (var tempChatPath in Directory.EnumerateDirectories(TemporaryChatsRootDirectory))
|
||||
{
|
||||
if (!Guid.TryParse(Path.GetFileName(tempChatPath), out var chatId))
|
||||
continue;
|
||||
|
||||
var chatName = await ReadNameOrDefaultAsync(Path.Join(tempChatPath, "name"), TB("Unnamed chat"));
|
||||
var chatThreadPath = Path.Join(tempChatPath, "thread.json");
|
||||
var lastEditTime = File.Exists(chatThreadPath) ? File.GetLastWriteTimeUtc(chatThreadPath) : DateTimeOffset.MinValue;
|
||||
chats.Add(new WorkspaceChatCacheEntry
|
||||
{
|
||||
WorkspaceId = Guid.Empty,
|
||||
ChatId = chatId,
|
||||
ChatPath = tempChatPath,
|
||||
ChatName = chatName,
|
||||
LastEditTime = lastEditTime,
|
||||
IsTemporary = true,
|
||||
});
|
||||
}
|
||||
|
||||
return chats.OrderByDescending(x => x.LastEditTime).ToList();
|
||||
}
|
||||
|
||||
private static async Task EnsureTreeShellLoadedCoreAsync()
|
||||
{
|
||||
if (!WORKSPACE_TREE_CACHE_INVALIDATED && WORKSPACE_TREE_CACHE.IsShellLoaded)
|
||||
return;
|
||||
|
||||
WORKSPACE_TREE_CACHE.Workspaces.Clear();
|
||||
WORKSPACE_TREE_CACHE.WorkspaceOrder.Clear();
|
||||
|
||||
Directory.CreateDirectory(WorkspaceRootDirectory);
|
||||
foreach (var workspacePath in Directory.EnumerateDirectories(WorkspaceRootDirectory))
|
||||
{
|
||||
if (!Guid.TryParse(Path.GetFileName(workspacePath), out var workspaceId))
|
||||
continue;
|
||||
|
||||
var workspaceName = await ReadNameOrDefaultAsync(Path.Join(workspacePath, "name"), TB("Unnamed workspace"));
|
||||
WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry
|
||||
{
|
||||
WorkspaceId = workspaceId,
|
||||
WorkspacePath = workspacePath,
|
||||
WorkspaceName = workspaceName,
|
||||
ChatsLoaded = false,
|
||||
Chats = [],
|
||||
};
|
||||
|
||||
WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId);
|
||||
}
|
||||
|
||||
WORKSPACE_TREE_CACHE.TemporaryChats = await ReadTemporaryChatsCoreAsync();
|
||||
WORKSPACE_TREE_CACHE.IsShellLoaded = true;
|
||||
WORKSPACE_TREE_CACHE_INVALIDATED = false;
|
||||
}
|
||||
|
||||
private static void UpsertChatInCache(List<WorkspaceChatCacheEntry> chats, WorkspaceChatCacheEntry chat)
|
||||
{
|
||||
var existingIndex = chats.FindIndex(existing => existing.ChatId == chat.ChatId);
|
||||
if (existingIndex >= 0)
|
||||
chats[existingIndex] = chat;
|
||||
else
|
||||
chats.Add(chat);
|
||||
|
||||
chats.Sort((a, b) => b.LastEditTime.CompareTo(a.LastEditTime));
|
||||
}
|
||||
|
||||
private static void DeleteChatFromCache(List<WorkspaceChatCacheEntry> chats, Guid chatId)
|
||||
{
|
||||
var existingIndex = chats.FindIndex(existing => existing.ChatId == chatId);
|
||||
if (existingIndex >= 0)
|
||||
chats.RemoveAt(existingIndex);
|
||||
}
|
||||
|
||||
private static async Task UpdateCacheAfterChatStored(Guid workspaceId, Guid chatId, string chatDirectory, string chatName, DateTimeOffset lastEditTime)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED)
|
||||
return;
|
||||
|
||||
var chatCacheEntry = new WorkspaceChatCacheEntry
|
||||
{
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = chatId,
|
||||
ChatPath = chatDirectory,
|
||||
ChatName = string.IsNullOrWhiteSpace(chatName) ? TB("Unnamed chat") : chatName,
|
||||
LastEditTime = lastEditTime,
|
||||
IsTemporary = workspaceId == Guid.Empty,
|
||||
};
|
||||
|
||||
if (workspaceId == Guid.Empty)
|
||||
{
|
||||
UpsertChatInCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatCacheEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded)
|
||||
UpsertChatInCache(workspace.Chats, chatCacheEntry);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateCacheAfterChatDeleted(Guid workspaceId, Guid chatId)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED)
|
||||
return;
|
||||
|
||||
if (workspaceId == Guid.Empty)
|
||||
{
|
||||
DeleteChatFromCache(WORKSPACE_TREE_CACHE.TemporaryChats, chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && workspace.ChatsLoaded)
|
||||
DeleteChatFromCache(workspace.Chats, chatId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static void InvalidateWorkspaceTreeCache()
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_INVALIDATED = true;
|
||||
}
|
||||
|
||||
public static async Task ForceReloadWorkspaceTreeAsync()
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_INVALIDATED = false;
|
||||
WORKSPACE_TREE_CACHE.IsShellLoaded = false;
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<WorkspaceTreeCacheSnapshot> GetOrLoadWorkspaceTreeShellAsync()
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
var workspaces = WORKSPACE_TREE_CACHE.WorkspaceOrder
|
||||
.Where(workspaceId => WORKSPACE_TREE_CACHE.Workspaces.ContainsKey(workspaceId))
|
||||
.Select(workspaceId => ToPublicWorkspace(WORKSPACE_TREE_CACHE.Workspaces[workspaceId]))
|
||||
.ToList();
|
||||
var temporaryChats = WORKSPACE_TREE_CACHE.TemporaryChats.Select(ToPublicChat).ToList();
|
||||
return new WorkspaceTreeCacheSnapshot(workspaces, temporaryChats);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyList<WorkspaceTreeChat>> GetWorkspaceChatsAsync(Guid workspaceId, bool forceRefresh = false)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
if (!WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
|
||||
return [];
|
||||
|
||||
if (forceRefresh || !workspace.ChatsLoaded)
|
||||
{
|
||||
workspace.Chats = await ReadWorkspaceChatsCoreAsync(workspaceId, workspace.WorkspacePath);
|
||||
workspace.ChatsLoaded = true;
|
||||
}
|
||||
|
||||
return workspace.Chats.Select(ToPublicChat).ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task TryPrefetchRemainingChatsAsync(Func<Guid, Task>? onWorkspaceUpdated = null, CancellationToken token = default)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
Guid? workspaceToPrefetch = null;
|
||||
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
foreach (var workspaceId in WORKSPACE_TREE_CACHE.WorkspaceOrder)
|
||||
{
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace) && !workspace.ChatsLoaded)
|
||||
{
|
||||
workspaceToPrefetch = workspaceId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
|
||||
if (workspaceToPrefetch is null)
|
||||
return;
|
||||
|
||||
await GetWorkspaceChatsAsync(workspaceToPrefetch.Value);
|
||||
if (onWorkspaceUpdated is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await onWorkspaceUpdated(workspaceToPrefetch.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogWarning(ex, "Failed to process callback after prefetching workspace '{WorkspaceId}'.", workspaceToPrefetch.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(PREFETCH_DELAY_DURATION, token);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task AddWorkspaceToCacheAsync(Guid workspaceId, string workspacePath, string workspaceName)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
|
||||
{
|
||||
workspace.WorkspaceName = workspaceName;
|
||||
return;
|
||||
}
|
||||
|
||||
WORKSPACE_TREE_CACHE.Workspaces[workspaceId] = new WorkspaceCacheEntry
|
||||
{
|
||||
WorkspaceId = workspaceId,
|
||||
WorkspacePath = workspacePath,
|
||||
WorkspaceName = workspaceName,
|
||||
Chats = [],
|
||||
ChatsLoaded = false,
|
||||
};
|
||||
WORKSPACE_TREE_CACHE.WorkspaceOrder.Add(workspaceId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task UpdateWorkspaceNameInCacheAsync(Guid workspaceId, string workspaceName)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
|
||||
workspace.WorkspaceName = workspaceName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task RemoveWorkspaceFromCacheAsync(Guid workspaceId)
|
||||
{
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!WORKSPACE_TREE_CACHE.IsShellLoaded || WORKSPACE_TREE_CACHE_INVALIDATED)
|
||||
return;
|
||||
|
||||
WORKSPACE_TREE_CACHE.Workspaces.Remove(workspaceId);
|
||||
WORKSPACE_TREE_CACHE.WorkspaceOrder.Remove(workspaceId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsChatExisting(LoadChat loadChat)
|
||||
{
|
||||
@@ -78,31 +462,28 @@ public static class WorkspaceBehaviour
|
||||
return Directory.Exists(chatPath);
|
||||
}
|
||||
|
||||
public static async Task StoreChat(ChatThread chat)
|
||||
public static async Task StoreChatAsync(ChatThread chat)
|
||||
{
|
||||
// Try to acquire the semaphore for this specific chat to prevent concurrent writes to the same file:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChat));
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(chat.WorkspaceId, chat.ChatId, nameof(StoreChatAsync));
|
||||
if (!acquired)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
var chatDirectory = chat.WorkspaceId == Guid.Empty
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(chatDirectory);
|
||||
|
||||
// Save the chat name:
|
||||
var chatNamePath = Path.Join(chatDirectory, "name");
|
||||
await File.WriteAllTextAsync(chatNamePath, chat.Name);
|
||||
|
||||
// Save the thread as thread.json:
|
||||
var chatPath = Path.Join(chatDirectory, "thread.json");
|
||||
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
|
||||
|
||||
var lastEditTime = File.GetLastWriteTimeUtc(chatPath);
|
||||
await UpdateCacheAfterChatStored(chat.WorkspaceId, chat.ChatId, chatDirectory, chat.Name, lastEditTime);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -110,10 +491,9 @@ public static class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<ChatThread?> LoadChat(LoadChat loadChat)
|
||||
public static async Task<ChatThread?> LoadChatAsync(LoadChat loadChat)
|
||||
{
|
||||
// Try to acquire the semaphore for this specific chat to prevent concurrent read/writes to the same file:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChat));
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(loadChat.WorkspaceId, loadChat.ChatId, nameof(LoadChatAsync));
|
||||
if (!acquired)
|
||||
return null;
|
||||
|
||||
@@ -123,12 +503,11 @@ public static class WorkspaceBehaviour
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", loadChat.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", loadChat.WorkspaceId.ToString(), loadChat.ChatId.ToString());
|
||||
|
||||
if(!Directory.Exists(chatPath))
|
||||
if (!Directory.Exists(chatPath))
|
||||
return null;
|
||||
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
|
||||
return chat;
|
||||
return JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -140,51 +519,68 @@ public static class WorkspaceBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> LoadWorkspaceName(Guid workspaceId)
|
||||
public static async Task<string> LoadWorkspaceNameAsync(Guid workspaceId)
|
||||
{
|
||||
if(workspaceId == Guid.Empty)
|
||||
if (workspaceId == Guid.Empty)
|
||||
return string.Empty;
|
||||
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
|
||||
|
||||
await WORKSPACE_TREE_CACHE_SEMAPHORE.WaitAsync();
|
||||
try
|
||||
{
|
||||
// If the name file does not exist or is empty, self-heal with a default name.
|
||||
if (!File.Exists(workspaceNamePath))
|
||||
await EnsureTreeShellLoadedCoreAsync();
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var cachedWorkspace) && !string.IsNullOrWhiteSpace(cachedWorkspace.WorkspaceName))
|
||||
return cachedWorkspace.WorkspaceName;
|
||||
|
||||
// Not in cache — read from disk and update cache in the same semaphore scope
|
||||
// to avoid a second semaphore acquisition via UpdateWorkspaceNameInCacheAsync:
|
||||
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
string workspaceName;
|
||||
|
||||
try
|
||||
{
|
||||
var defaultName = TB("Unnamed workspace");
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8);
|
||||
return defaultName;
|
||||
if (!File.Exists(workspaceNamePath))
|
||||
{
|
||||
workspaceName = TB("Unnamed workspace");
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
}
|
||||
else
|
||||
{
|
||||
workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
{
|
||||
workspaceName = TB("Unnamed workspace");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var name = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
catch
|
||||
{
|
||||
var defaultName = TB("Unnamed workspace");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, defaultName, Encoding.UTF8);
|
||||
return defaultName;
|
||||
workspaceName = TB("Unnamed workspace");
|
||||
}
|
||||
|
||||
return name;
|
||||
|
||||
// Update the cache directly (we already hold the semaphore):
|
||||
if (WORKSPACE_TREE_CACHE.Workspaces.TryGetValue(workspaceId, out var workspace))
|
||||
workspace.WorkspaceName = workspaceName;
|
||||
|
||||
return workspaceName;
|
||||
}
|
||||
catch
|
||||
finally
|
||||
{
|
||||
// On any error, return a localized default without throwing.
|
||||
return TB("Unnamed workspace");
|
||||
WORKSPACE_TREE_CACHE_SEMAPHORE.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task DeleteChat(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true)
|
||||
public static async Task DeleteChatAsync(IDialogService dialogService, Guid workspaceId, Guid chatId, bool askForConfirmation = true)
|
||||
{
|
||||
var chat = await LoadChat(new(workspaceId, chatId));
|
||||
var chat = await LoadChatAsync(new(workspaceId, chatId));
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await LoadWorkspaceName(chat.WorkspaceId);
|
||||
var workspaceName = await LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{
|
||||
@@ -202,20 +598,20 @@ public static class WorkspaceBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
var chatDirectory = chat.WorkspaceId == Guid.Empty
|
||||
? Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString())
|
||||
: Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
// Try to acquire the semaphore to prevent deleting while another thread is writing:
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChat));
|
||||
var (acquired, semaphore) = await TryAcquireChatSemaphoreAsync(workspaceId, chatId, nameof(DeleteChatAsync));
|
||||
if (!acquired)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(chatDirectory, true);
|
||||
if (Directory.Exists(chatDirectory))
|
||||
Directory.Delete(chatDirectory, true);
|
||||
|
||||
await UpdateCacheAfterChatDeleted(workspaceId, chatId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -225,16 +621,14 @@ public static class WorkspaceBehaviour
|
||||
|
||||
private static async Task EnsureWorkspace(Guid workspaceId, string workspaceName)
|
||||
{
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
var workspacePath = Path.Join(WorkspaceRootDirectory, workspaceId.ToString());
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
|
||||
if(!Path.Exists(workspacePath))
|
||||
if (!Path.Exists(workspacePath))
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
|
||||
try
|
||||
{
|
||||
// When the name file is missing or empty, write it (self-heal).
|
||||
// Otherwise, keep the existing name:
|
||||
if (!File.Exists(workspaceNamePath))
|
||||
{
|
||||
await File.WriteAllTextAsync(workspaceNamePath, workspaceName, Encoding.UTF8);
|
||||
@@ -250,9 +644,11 @@ public static class WorkspaceBehaviour
|
||||
{
|
||||
// Ignore IO issues to avoid interrupting background initialization.
|
||||
}
|
||||
|
||||
await AddWorkspaceToCacheAsync(workspaceId, workspacePath, workspaceName);
|
||||
}
|
||||
|
||||
|
||||
public static async Task EnsureBiasWorkspace() => await EnsureWorkspace(KnownWorkspaces.BIAS_WORKSPACE_ID, "Bias of the Day");
|
||||
|
||||
|
||||
public static async Task EnsureERIServerWorkspace() => await EnsureWorkspace(KnownWorkspaces.ERI_SERVER_WORKSPACE_ID, "ERI Servers");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user