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();
}