99 lines
3.1 KiB
Dart
99 lines
3.1 KiB
Dart
|
|
// lib/editor/persistence/save_scheduler.dart
|
||
|
|
//
|
||
|
|
// Debounced save scheduler. The snapshot/JSON is captured SYNCHRONOUSLY by
|
||
|
|
// the schedule() call before any await, so callers do not need to worry about
|
||
|
|
// the in-memory state mutating between schedule() and the actual write.
|
||
|
|
|
||
|
|
import 'dart:async';
|
||
|
|
|
||
|
|
import '../engine/stroke_model.dart';
|
||
|
|
import 'editor_repository.dart';
|
||
|
|
|
||
|
|
/// Debounced save scheduler that batches rapid successive changes to a host
|
||
|
|
/// into a single [EditorRepository.saveHost] call.
|
||
|
|
///
|
||
|
|
/// Usage:
|
||
|
|
/// ```dart
|
||
|
|
/// final scheduler = SaveScheduler(repository);
|
||
|
|
/// scheduler.schedule('page', hostId, List.from(strokes));
|
||
|
|
/// // …later, on dispose / navigate away:
|
||
|
|
/// await scheduler.flush();
|
||
|
|
/// scheduler.dispose();
|
||
|
|
/// ```
|
||
|
|
class SaveScheduler {
|
||
|
|
SaveScheduler(
|
||
|
|
this._repository, {
|
||
|
|
Duration debounce = const Duration(milliseconds: 800),
|
||
|
|
}) : _debounce = debounce;
|
||
|
|
|
||
|
|
final EditorRepository _repository;
|
||
|
|
final Duration _debounce;
|
||
|
|
|
||
|
|
// One pending timer + captured snapshot per host.
|
||
|
|
final Map<String, Timer> _timers = {};
|
||
|
|
final Map<String, _PendingWrite> _pending = {};
|
||
|
|
|
||
|
|
bool _disposed = false;
|
||
|
|
|
||
|
|
// ── Public API ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/// Schedule a save for ([hostKind], [hostId]).
|
||
|
|
///
|
||
|
|
/// [strokes] is captured synchronously (defensive copy via the caller's
|
||
|
|
/// `List.from(…)` convention or equivalent) so mutations after this call
|
||
|
|
/// do not affect what is written.
|
||
|
|
void schedule(
|
||
|
|
String hostKind,
|
||
|
|
String hostId,
|
||
|
|
List<EditorStroke> strokes,
|
||
|
|
) {
|
||
|
|
if (_disposed) return;
|
||
|
|
|
||
|
|
// Capture the snapshot synchronously before any async gap.
|
||
|
|
_pending[hostId] = _PendingWrite(hostKind: hostKind, strokes: strokes);
|
||
|
|
|
||
|
|
_timers[hostId]?.cancel();
|
||
|
|
_timers[hostId] = Timer(_debounce, () => _fire(hostId));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Force-write all pending saves immediately and wait for them to complete.
|
||
|
|
Future<void> flush() async {
|
||
|
|
final hosts = List<String>.from(_pending.keys);
|
||
|
|
for (final hostId in hosts) {
|
||
|
|
_timers[hostId]?.cancel();
|
||
|
|
_timers.remove(hostId);
|
||
|
|
await _fire(hostId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Cancel all pending timers and release resources.
|
||
|
|
///
|
||
|
|
/// Call [flush] first if you need pending writes to complete.
|
||
|
|
void dispose() {
|
||
|
|
_disposed = true;
|
||
|
|
for (final timer in _timers.values) {
|
||
|
|
timer.cancel();
|
||
|
|
}
|
||
|
|
_timers.clear();
|
||
|
|
_pending.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Internal ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
Future<void> _fire(String hostId) async {
|
||
|
|
final write = _pending.remove(hostId);
|
||
|
|
_timers.remove(hostId);
|
||
|
|
if (write == null) return;
|
||
|
|
await _repository.saveHost(write.hostKind, hostId, write.strokes);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
class _PendingWrite {
|
||
|
|
const _PendingWrite({required this.hostKind, required this.strokes});
|
||
|
|
|
||
|
|
final String hostKind;
|
||
|
|
final List<EditorStroke> strokes;
|
||
|
|
}
|