feat(engine): P0 stroke engine + persistence
Per the full-refactor plan §9 (input-independent half of P0): - engine: canonical EditorStroke (lossless InkStroke round-trip) + stroke_geometry (single getStroke outline) + revision-gated StrokeStore - render: static/live ink painters + ink_picture_cache (revision-keyed) + annotation_layer (RepaintBoundary) - persistence: DB v6 (ink, notebook_pages) + editor_repository diff-write (UPSERT changed / DELETE removed in one txn; id-set after commit) + save_scheduler - pdf_service export now FILLS the getStroke outline (R7 hairline fix) Not yet wired into the live editor (input relocation pending pen-pressure diagnostic). 28 new tests pass.
This commit is contained in:
98
lib/editor/persistence/save_scheduler.dart
Normal file
98
lib/editor/persistence/save_scheduler.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user