146 lines
4.7 KiB
C#
146 lines
4.7 KiB
C#
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Threading;
|
|
using WeldingCommon.Data.Services.History;
|
|
|
|
namespace WeldingCommon.Data.History
|
|
{
|
|
public class SavedHistoryService<TKey, TIn, TView> : HistoryService<TKey, TView>, IHistoryWriter<TKey, TIn>
|
|
{
|
|
private readonly Func<TIn, TView> _projector;
|
|
|
|
private readonly string _filePath;
|
|
private readonly Func<TKey, string> _keyToString;
|
|
private readonly Func<string, TKey> _stringToKey;
|
|
|
|
private readonly DispatcherTimer _saveTimer;
|
|
private volatile bool _dirty;
|
|
|
|
public SavedHistoryService(string filePath, Func<TIn, TView> projector, int maxCount = 50, int pumpMs = 100, int saveDebounceMs = 500, Func<TKey, string> keyToString = null, Func<string, TKey> stringToKey = null)
|
|
: base(maxCount, pumpMs)
|
|
{
|
|
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
|
_projector = projector ?? throw new ArgumentNullException(nameof(projector));
|
|
|
|
_keyToString = keyToString ?? (k => k.ToString());
|
|
_stringToKey = stringToKey ?? DefaultStringToKey;
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_filePath));
|
|
|
|
LoadFromDisk(); // base.Seed 사용
|
|
|
|
_saveTimer = new DispatcherTimer(DispatcherPriority.Background) {
|
|
Interval = TimeSpan.FromMilliseconds(saveDebounceMs)
|
|
};
|
|
_saveTimer.Tick += SaveTimer_Tick;
|
|
}
|
|
public void Append(TKey key, TIn snapshot)
|
|
{
|
|
var view = _projector(snapshot);
|
|
base.Append(key, view); // 큐에 들어감 → Pump 후 컬렉션 반영
|
|
}
|
|
// Clear는 base 구현 그대로 사용(= TView 기준)
|
|
public override void Clear(TKey key)
|
|
{
|
|
base.Clear(key);
|
|
MarkDirtyAndScheduleSave();
|
|
}
|
|
|
|
protected override void OnChanged()
|
|
{
|
|
MarkDirtyAndScheduleSave();
|
|
}
|
|
|
|
private void MarkDirtyAndScheduleSave()
|
|
{
|
|
_dirty = true;
|
|
if (!_saveTimer.IsEnabled)
|
|
_saveTimer.Start();
|
|
}
|
|
|
|
private void SaveTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
if (!_dirty) {
|
|
_saveTimer.Stop();
|
|
return;
|
|
}
|
|
|
|
SaveToDisk();
|
|
_dirty = false;
|
|
_saveTimer.Stop();
|
|
}
|
|
|
|
private void LoadFromDisk()
|
|
{
|
|
if (!File.Exists(_filePath))
|
|
return;
|
|
|
|
try {
|
|
var json = File.ReadAllText(_filePath, Encoding.UTF8);
|
|
var data = JsonConvert.DeserializeObject<Dictionary<string, List<TView>>>(json);
|
|
if (data == null) return;
|
|
|
|
foreach (var kv in data) {
|
|
var key = _stringToKey(kv.Key);
|
|
var list = kv.Value ?? new List<TView>();
|
|
|
|
// 저장 포맷을 "최신이 앞"으로 가정
|
|
Seed(key, list);
|
|
}
|
|
} catch {
|
|
// 파일 손상/버전 문제면 무시하고 시작
|
|
}
|
|
}
|
|
|
|
private void SaveToDisk()
|
|
{
|
|
Dictionary<string, List<TView>> snapshot;
|
|
|
|
lock (_lock) {
|
|
snapshot = _collections.ToDictionary(
|
|
kv => _keyToString(kv.Key),
|
|
kv => kv.Value.Take(500000).ToList()); // 여기서 maxCount 대신 큰 수 쓰지 말고, RollingCollection이 max를 유지하면 그냥 ToList도 OK
|
|
}
|
|
|
|
var json = JsonConvert.SerializeObject(snapshot, Formatting.Indented);
|
|
|
|
var temp = _filePath + ".tmp";
|
|
File.WriteAllText(temp, json, Encoding.UTF8);
|
|
|
|
if (File.Exists(_filePath)) {
|
|
var backup = _filePath + ".bak";
|
|
File.Replace(temp, _filePath, backup, ignoreMetadataErrors: true);
|
|
}
|
|
else {
|
|
File.Move(temp, _filePath);
|
|
}
|
|
}
|
|
|
|
private TKey DefaultStringToKey(string s)
|
|
{
|
|
var t = typeof(TKey);
|
|
if (t.IsEnum) return (TKey)Enum.Parse(t, s);
|
|
if (t == typeof(string)) return (TKey)(object)s;
|
|
|
|
throw new InvalidOperationException(
|
|
$"TKey가 enum/string이 아니면 stringToKey 델리게이트를 제공해야 합니다. TKey={t.FullName}");
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
_saveTimer.Stop();
|
|
_saveTimer.Tick -= SaveTimer_Tick;
|
|
|
|
if (_dirty)
|
|
SaveToDisk();
|
|
|
|
base.Dispose();
|
|
}
|
|
}
|
|
}
|