143 lines
4.8 KiB
C#
143 lines
4.8 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace TermRemoteCtl.Agent.History;
|
|
|
|
public sealed class SessionIoJournalStore
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
};
|
|
private static readonly UTF8Encoding Utf8WithoutBom = new(false);
|
|
|
|
private readonly string _sessionRoot;
|
|
|
|
public SessionIoJournalStore(string rootPath)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
|
|
|
_sessionRoot = Path.Combine(rootPath, "sessions");
|
|
Directory.CreateDirectory(_sessionRoot);
|
|
}
|
|
|
|
public async Task AppendAsync(SessionIoEvent ioEvent, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(ioEvent);
|
|
|
|
var filePath = Path.Combine(_sessionRoot, $"{ioEvent.SessionId}.io.jsonl");
|
|
var line = JsonSerializer.Serialize(ioEvent, SerializerOptions) + Environment.NewLine;
|
|
await File.AppendAllTextAsync(filePath, line, Utf8WithoutBom, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task<SessionJournalPage> ReadAsync(
|
|
string sessionId,
|
|
long? beforeSequence,
|
|
long? afterSequence,
|
|
int limit,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit);
|
|
|
|
if (beforeSequence.HasValue && afterSequence.HasValue)
|
|
{
|
|
throw new ArgumentException("beforeSequence and afterSequence cannot both be specified.");
|
|
}
|
|
|
|
var items = await ReadAllAsync(sessionId, cancellationToken).ConfigureAwait(false);
|
|
var currentSequence = items.Count == 0 ? 0 : items[^1].Sequence;
|
|
|
|
if (afterSequence.HasValue)
|
|
{
|
|
var newerItems = items
|
|
.Where(item => item.Sequence > afterSequence.Value)
|
|
.ToList();
|
|
var pageItems = newerItems.Take(limit).ToArray();
|
|
|
|
return new SessionJournalPage(
|
|
sessionId,
|
|
pageItems,
|
|
HasMoreBefore: afterSequence.Value > 0,
|
|
HasMoreAfter: newerItems.Count > pageItems.Length,
|
|
CurrentSequence: currentSequence);
|
|
}
|
|
|
|
if (beforeSequence.HasValue)
|
|
{
|
|
var olderItems = items
|
|
.Where(item => item.Sequence < beforeSequence.Value)
|
|
.ToList();
|
|
var skipCount = Math.Max(0, olderItems.Count - limit);
|
|
var pageItems = olderItems.Skip(skipCount).ToArray();
|
|
|
|
return new SessionJournalPage(
|
|
sessionId,
|
|
pageItems,
|
|
HasMoreBefore: skipCount > 0,
|
|
HasMoreAfter: beforeSequence.Value <= currentSequence,
|
|
CurrentSequence: currentSequence);
|
|
}
|
|
|
|
var latestSkipCount = Math.Max(0, items.Count - limit);
|
|
var latestItems = items.Skip(latestSkipCount).ToArray();
|
|
return new SessionJournalPage(
|
|
sessionId,
|
|
latestItems,
|
|
HasMoreBefore: latestSkipCount > 0,
|
|
HasMoreAfter: false,
|
|
CurrentSequence: currentSequence);
|
|
}
|
|
|
|
public Task DeleteAsync(string sessionId, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var filePath = Path.Combine(_sessionRoot, $"{sessionId}.io.jsonl");
|
|
if (File.Exists(filePath))
|
|
{
|
|
File.Delete(filePath);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task<List<SessionIoEvent>> ReadAllAsync(string sessionId, CancellationToken cancellationToken)
|
|
{
|
|
var filePath = Path.Combine(_sessionRoot, $"{sessionId}.io.jsonl");
|
|
if (!File.Exists(filePath))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var items = new List<SessionIoEvent>();
|
|
await using var stream = new FileStream(
|
|
filePath,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.ReadWrite,
|
|
4096,
|
|
FileOptions.Asynchronous);
|
|
using var reader = new StreamReader(stream, Utf8WithoutBom, detectEncodingFromByteOrderMarks: false);
|
|
while (!reader.EndOfStream)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var item = JsonSerializer.Deserialize<SessionIoEvent>(line, SerializerOptions);
|
|
if (item is not null)
|
|
{
|
|
items.Add(item);
|
|
}
|
|
}
|
|
|
|
items.Sort(static (left, right) => left.Sequence.CompareTo(right.Sequence));
|
|
return items;
|
|
}
|
|
}
|