Files
BadNote/lib/editor/persistence/sidecar_flush_observer.dart

59 lines
2.2 KiB
Dart
Raw Permalink Normal View History

// lib/editor/persistence/sidecar_flush_observer.dart
//
// Phase 6 / §F.3 of the file-based storage plan (docs/plans/2026-06-24-file-
// based-storage.md): app-lifecycle flush hardening.
//
// The per-file SidecarRepository debounces writes by 800 ms. That window is the
// data-loss gap on a Windows tablet: if the OS suspends or closes the app
// before the timer fires, the last strokes never reach disk. This observer
// listens for the app leaving the foreground and DRAINS every open repo's
// pending write before the process can be frozen, so "never lose the last
// strokes on app close" holds even when the editor's own dispose() doesn't run.
//
// Registered once in BadNoteApp; it delegates to
// [SidecarRepositoryRegistry.flushAll], which awaits every repo's flush().
import 'package:flutter/widgets.dart';
import 'sidecar_repository.dart';
/// A [WidgetsBindingObserver] that flushes all open sidecar repositories when
/// the app leaves the foreground (`inactive`/`paused`/`detached`/`hidden`).
class SidecarFlushObserver with WidgetsBindingObserver {
/// Whether the observer is currently registered with the binding.
bool get isAttached => _attached;
bool _attached = false;
/// Register with [WidgetsBinding.instance] so lifecycle changes are observed.
void attach() {
if (_attached) return;
WidgetsBinding.instance.addObserver(this);
_attached = true;
}
/// Stop observing lifecycle changes.
void detach() {
if (!_attached) return;
WidgetsBinding.instance.removeObserver(this);
_attached = false;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
// Any transition out of the foreground is a potential suspend/kill point:
// drain pending sidecar writes now (the editors' own dispose() may never
// run when the OS freezes the process).
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
// Fire-and-forget at the framework boundary, but each write is atomic
// and awaited inside flushAll, so a half-written sidecar is impossible.
SidecarRepositoryRegistry.flushAll();
case AppLifecycleState.resumed:
break;
}
}
}