using System.Text.Json; using System.Text.Json.Serialization; namespace IronNotify.Client; /// /// File-based offline queue for notification events. /// Persists failed sends to disk and retries with exponential backoff. /// public class NotifyOfflineQueue : IDisposable { private readonly string _queueFilePath; private readonly int _maxQueueSize; private readonly object _fileLock = new(); private readonly SemaphoreSlim _retrySemaphore = new(1, 1); private readonly Timer? _retryTimer; private readonly Func> _sendFunc; private int _retryAttempt; private bool _disposed; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false }; /// /// Creates a new offline queue. /// /// Function to send an event to server. Returns true on success. /// Directory to store queue file. Defaults to app data. /// Maximum items to store. Oldest items dropped when exceeded. /// Whether to automatically retry sending queued items. public NotifyOfflineQueue( Func> sendFunc, string? queueDirectory = null, int maxQueueSize = 500, bool enableAutoRetry = true) { _sendFunc = sendFunc; _maxQueueSize = maxQueueSize; var directory = queueDirectory ?? GetDefaultQueueDirectory(); Directory.CreateDirectory(directory); _queueFilePath = Path.Combine(directory, "notify_queue.json"); if (enableAutoRetry) { // Start retry timer - initial delay 30 seconds, then every 60 seconds _retryTimer = new Timer( _ => _ = RetryQueuedItemsAsync(), null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(60)); } } /// /// Number of items currently in the queue. /// public int Count { get { lock (_fileLock) { var items = LoadQueue(); return items.Count; } } } /// /// Enqueue an event that failed to send. /// public void Enqueue(NotifyEventRequest request) { lock (_fileLock) { var queue = LoadQueue(); queue.Add(new QueuedEvent { QueuedAt = DateTime.UtcNow, Request = request }); // Trim to max size (remove oldest) while (queue.Count > _maxQueueSize) { queue.RemoveAt(0); } SaveQueue(queue); } } /// /// Try to send all queued items. /// public async Task RetryQueuedItemsAsync() { if (!await _retrySemaphore.WaitAsync(0)) { // Already retrying return 0; } int sentCount = 0; try { List items; lock (_fileLock) { items = LoadQueue(); } if (items.Count == 0) { _retryAttempt = 0; return 0; } var remaining = new List(); foreach (var item in items) { var success = await _sendFunc(item); if (success) { sentCount++; } else { remaining.Add(item); } } // Save remaining items lock (_fileLock) { SaveQueue(remaining); } if (remaining.Count == 0) { _retryAttempt = 0; } else { _retryAttempt++; } return sentCount; } catch { _retryAttempt++; return 0; } finally { _retrySemaphore.Release(); } } /// /// Clear all queued items without sending. /// public void Clear() { lock (_fileLock) { SaveQueue(new List()); } } /// /// Get all queued items (for display/export). /// public List GetQueuedItems() { lock (_fileLock) { return LoadQueue(); } } private List LoadQueue() { try { if (!File.Exists(_queueFilePath)) { return new List(); } var json = File.ReadAllText(_queueFilePath); return JsonSerializer.Deserialize>(json, JsonOptions) ?? new List(); } catch { return new List(); } } private void SaveQueue(List items) { try { var json = JsonSerializer.Serialize(items, JsonOptions); File.WriteAllText(_queueFilePath, json); } catch { // Ignore write errors } } private static string GetDefaultQueueDirectory() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "IronNotify", "Queue"); } public void Dispose() { if (!_disposed) { _retryTimer?.Dispose(); _retrySemaphore.Dispose(); _disposed = true; } } } /// /// A queued notification event with metadata. /// public class QueuedEvent { public DateTime QueuedAt { get; set; } public int RetryCount { get; set; } public NotifyEventRequest Request { get; set; } = new(); }