Repository navigation
Expand file tree
/
Copy pathUsageHistoryStore.cs
More file actions
102 lines (91 loc) · 3.32 KB
/
Copy pathUsageHistoryStore.cs
File metadata and controls
102 lines (91 loc) · 3.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System.Text.Json;
namespace CodexUsageTray;
internal sealed record UsageHistoryPoint(
DateTimeOffset RecordedAt,
int RemainingPercent);
internal sealed class UsageHistoryStore
{
private static readonly TimeSpan Retention = TimeSpan.FromDays(7);
private readonly string filePath;
private readonly List<UsageHistoryPoint> points;
/// <summary>
/// Creates the weekly usage-history store at the standard local application-data path.
/// </summary>
/// <param name="filePath">An optional file path used by self-tests.</param>
internal UsageHistoryStore(string? filePath = null)
{
this.filePath = filePath ?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"CodexUsageTray",
"usage-history.json");
points = Load(this.filePath);
Prune(DateTimeOffset.Now);
}
/// <summary>
/// Gets the retained weekly usage samples.
/// </summary>
internal IReadOnlyList<UsageHistoryPoint> Points => points;
/// <summary>
/// Records one available weekly remaining-percentage sample.
/// </summary>
/// <param name="reading">The normalized weekly limit reading.</param>
/// <param name="recordedAt">The sample timestamp.</param>
internal void Record(LimitReading reading, DateTimeOffset recordedAt)
{
if (reading.State != LimitState.Available
|| reading.RemainingPercent is not int remainingPercent)
{
return;
}
points.Add(new UsageHistoryPoint(recordedAt, Math.Clamp(remainingPercent, 0, 100)));
Prune(recordedAt);
Save();
}
/// <summary>
/// Removes samples older than the retention period.
/// </summary>
/// <param name="now">The timestamp defining the retention window.</param>
private void Prune(DateTimeOffset now)
{
DateTimeOffset cutoff = now - Retention;
points.RemoveAll(point => point.RecordedAt < cutoff);
points.Sort((left, right) => left.RecordedAt.CompareTo(right.RecordedAt));
}
/// <summary>
/// Loads persisted samples, treating missing or invalid state as empty.
/// </summary>
/// <param name="path">The usage-history JSON file.</param>
/// <returns>The persisted samples.</returns>
private static List<UsageHistoryPoint> Load(string path)
{
try
{
return File.Exists(path)
? JsonSerializer.Deserialize<List<UsageHistoryPoint>>(File.ReadAllText(path)) ?? []
: [];
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
{
return [];
}
}
/// <summary>
/// Persists the retained samples to local application data.
/// </summary>
private void Save()
{
try
{
string? directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(filePath, JsonSerializer.Serialize(points));
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// History persistence must not hide otherwise valid usage data.
}
}
}