| 1 | using System.Text.Json; |
| 2 | using System.Text.Json.Serialization; |
| 3 | |
| 4 | namespace CascadeIDE.Features.Intercom.Transport; |
| 5 | |
| 6 | /// <summary>Offline outbox: <c>.cascade-ide/intercom-transport-outbox.ndjson</c> (ADR 0144 §9).</summary> |
| 7 | public sealed class IntercomTransportOutboundQueue |
| 8 | { |
| 9 | private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); |
| 10 | |
| 11 | private string? _workspaceRoot; |
| 12 | |
| 13 | public void SetWorkspaceRoot(string? workspaceRoot) => _workspaceRoot = workspaceRoot; |
| 14 | |
| 15 | public async Task EnqueueAsync(IntercomOutboundQueueEntry entry, CancellationToken ct = default) |
| 16 | { |
| 17 | var path = OutboxPath(); |
| 18 | if (path is null) |
| 19 | return; |
| 20 | |
| 21 | Directory.CreateDirectory(Path.GetDirectoryName(path)!); |
| 22 | var line = JsonSerializer.Serialize(entry, Json); |
| 23 | await File.AppendAllTextAsync(path, line + Environment.NewLine, ct).ConfigureAwait(false); |
| 24 | } |
| 25 | |
| 26 | public async Task<IReadOnlyList<IntercomOutboundQueueEntry>> ReadAllAsync(CancellationToken ct = default) |
| 27 | { |
| 28 | var path = OutboxPath(); |
| 29 | if (path is null || !File.Exists(path)) |
| 30 | return []; |
| 31 | |
| 32 | var list = new List<IntercomOutboundQueueEntry>(); |
| 33 | await using var stream = File.OpenRead(path); |
| 34 | using var reader = new StreamReader(stream); |
| 35 | while (true) |
| 36 | { |
| 37 | ct.ThrowIfCancellationRequested(); |
| 38 | var line = await reader.ReadLineAsync(ct).ConfigureAwait(false); |
| 39 | if (line is null) |
| 40 | break; |
| 41 | if (string.IsNullOrWhiteSpace(line)) |
| 42 | continue; |
| 43 | try |
| 44 | { |
| 45 | var entry = JsonSerializer.Deserialize<IntercomOutboundQueueEntry>(line, Json); |
| 46 | if (entry is not null) |
| 47 | list.Add(entry); |
| 48 | } |
| 49 | catch (JsonException) |
| 50 | { |
| 51 | // skip corrupt line |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | return list; |
| 56 | } |
| 57 | |
| 58 | public async Task ReplaceAllAsync(IReadOnlyList<IntercomOutboundQueueEntry> remaining, CancellationToken ct = default) |
| 59 | { |
| 60 | var path = OutboxPath(); |
| 61 | if (path is null) |
| 62 | return; |
| 63 | |
| 64 | if (remaining.Count == 0) |
| 65 | { |
| 66 | if (File.Exists(path)) |
| 67 | File.Delete(path); |
| 68 | return; |
| 69 | } |
| 70 | |
| 71 | Directory.CreateDirectory(Path.GetDirectoryName(path)!); |
| 72 | var lines = remaining.Select(e => JsonSerializer.Serialize(e, Json)); |
| 73 | await File.WriteAllTextAsync(path, string.Join(Environment.NewLine, lines) + Environment.NewLine, ct) |
| 74 | .ConfigureAwait(false); |
| 75 | } |
| 76 | |
| 77 | private string? OutboxPath() |
| 78 | { |
| 79 | if (string.IsNullOrWhiteSpace(_workspaceRoot)) |
| 80 | return null; |
| 81 | return Path.Combine(_workspaceRoot, ".cascade-ide", "intercom-transport-outbox.ndjson"); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | public sealed record IntercomOutboundQueueEntry( |
| 86 | [property: JsonPropertyName("topic_id")] string TopicId, |
| 87 | [property: JsonPropertyName("request")] IntercomAppendEventRequestDto Request, |
| 88 | [property: JsonPropertyName("attempts")] int Attempts = 0); |
| 89 | |