feat: add static DashboardCache - file-timestamp-based cache invalidation for near-zero cost refreshes
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
using Disco.Plugins.ServiceTracker.Models;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Disco.Plugins.ServiceTracker.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Static in-memory cache for the dashboard model.
|
||||
/// Instead of running full EF queries + JSON reads + Google Sheet fetches on every page load,
|
||||
/// this checks file modification timestamps to detect changes.
|
||||
/// If nothing changed, returns the cached model instantly (near-zero CPU/memory cost).
|
||||
/// </summary>
|
||||
public static class DashboardCache
|
||||
{
|
||||
private static DashboardViewModel _cachedModel;
|
||||
private static DateTime _cacheBuiltAt = DateTime.MinValue;
|
||||
private static DateTime _ticketsFileTime = DateTime.MinValue;
|
||||
private static DateTime _externalFileTime = DateTime.MinValue;
|
||||
private static DateTime _sheetCacheTime = DateTime.MinValue;
|
||||
private static DateTime _configFileTime = DateTime.MinValue;
|
||||
private static string _lastSortBy;
|
||||
private static string _lastFilter;
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age of cache in seconds before forced rebuild, even if no files changed.
|
||||
/// Acts as a safety net for database-only changes (e.g. Disco job opened/closed externally).
|
||||
/// </summary>
|
||||
public static int MaxCacheAgeSeconds = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Returns cached model if still valid, or null if rebuild is needed.
|
||||
/// A rebuild is needed when:
|
||||
/// - Cache doesn't exist yet
|
||||
/// - Any data file has been modified since last build
|
||||
/// - Cache is older than MaxCacheAgeSeconds
|
||||
/// - Sort/filter parameters changed
|
||||
/// </summary>
|
||||
public static DashboardViewModel GetIfValid(string dataDirectory, string sortBy, string filterKey)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cachedModel == null) return null;
|
||||
|
||||
// Different sort/filter = must rebuild
|
||||
if (_lastSortBy != sortBy || _lastFilter != filterKey) return null;
|
||||
|
||||
// Check age
|
||||
var age = (DateTime.Now - _cacheBuiltAt).TotalSeconds;
|
||||
if (age > MaxCacheAgeSeconds) return null;
|
||||
|
||||
// Check if any data files have been modified
|
||||
if (HasFileChanged(Path.Combine(dataDirectory, "tickets.json"), ref _ticketsFileTime)) return null;
|
||||
if (HasFileChanged(Path.Combine(dataDirectory, "external_tickets.json"), ref _externalFileTime)) return null;
|
||||
if (HasFileChanged(Path.Combine(dataDirectory, "sheet_cache.csv"), ref _sheetCacheTime)) return null;
|
||||
if (HasFileChanged(Path.Combine(dataDirectory, "config.json"), ref _configFileTime)) return null;
|
||||
|
||||
return _cachedModel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store a freshly built model in the cache.
|
||||
/// </summary>
|
||||
public static void Store(DashboardViewModel model, string dataDirectory, string sortBy, string filterKey)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cachedModel = model;
|
||||
_cacheBuiltAt = DateTime.Now;
|
||||
_lastSortBy = sortBy;
|
||||
_lastFilter = filterKey;
|
||||
|
||||
// Snapshot current file times
|
||||
_ticketsFileTime = GetFileTime(Path.Combine(dataDirectory, "tickets.json"));
|
||||
_externalFileTime = GetFileTime(Path.Combine(dataDirectory, "external_tickets.json"));
|
||||
_sheetCacheTime = GetFileTime(Path.Combine(dataDirectory, "sheet_cache.csv"));
|
||||
_configFileTime = GetFileTime(Path.Combine(dataDirectory, "config.json"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force invalidate the cache (called after writes if needed).
|
||||
/// </summary>
|
||||
public static void Invalidate()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cachedModel = null;
|
||||
_cacheBuiltAt = DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasFileChanged(string path, ref DateTime lastKnownTime)
|
||||
{
|
||||
var currentTime = GetFileTime(path);
|
||||
if (currentTime != lastKnownTime)
|
||||
{
|
||||
lastKnownTime = currentTime;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static DateTime GetFileTime(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path)) return File.GetLastWriteTimeUtc(path);
|
||||
}
|
||||
catch { }
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user