2025-09-07 23:17:26 +05:30
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
/// A simple activity-based watchdog.
|
|
|
|
|
///
|
|
|
|
|
/// Call [ping] whenever activity occurs. If no activity happens
|
|
|
|
|
/// within [window], [onTimeout] fires. Optionally, an [absoluteCap]
|
|
|
|
|
/// enforces a maximum total duration regardless of activity.
|
2025-12-02 16:15:16 +05:30
|
|
|
///
|
|
|
|
|
/// The [onTimeout] callback can be sync or async - if async, it will be
|
|
|
|
|
/// awaited before the watchdog considers itself fully stopped.
|
2025-09-07 23:17:26 +05:30
|
|
|
class InactivityWatchdog {
|
|
|
|
|
InactivityWatchdog({
|
|
|
|
|
required Duration window,
|
|
|
|
|
required this.onTimeout,
|
|
|
|
|
Duration? absoluteCap,
|
2025-09-24 12:00:49 +05:30
|
|
|
}) : _window = window,
|
|
|
|
|
_absoluteCap = absoluteCap;
|
2025-09-07 23:17:26 +05:30
|
|
|
|
2025-12-02 16:15:16 +05:30
|
|
|
final FutureOr<void> Function() onTimeout;
|
2025-09-07 23:17:26 +05:30
|
|
|
|
|
|
|
|
Duration _window;
|
|
|
|
|
Duration? _absoluteCap;
|
|
|
|
|
Timer? _timer;
|
|
|
|
|
Timer? _absoluteTimer;
|
|
|
|
|
bool _started = false;
|
2025-12-02 16:15:16 +05:30
|
|
|
bool _firing = false;
|
2025-09-07 23:17:26 +05:30
|
|
|
|
|
|
|
|
Duration get window => _window;
|
|
|
|
|
|
2025-12-02 16:15:16 +05:30
|
|
|
/// Whether the timeout callback is currently executing.
|
|
|
|
|
bool get isFiring => _firing;
|
|
|
|
|
|
2025-09-07 23:17:26 +05:30
|
|
|
void setWindow(Duration newWindow) {
|
|
|
|
|
_window = newWindow;
|
|
|
|
|
if (_started) {
|
|
|
|
|
// Restart timer with new window
|
|
|
|
|
_restart();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void setAbsoluteCap(Duration? cap) {
|
|
|
|
|
_absoluteCap = cap;
|
|
|
|
|
if (_started) {
|
|
|
|
|
_absoluteTimer?.cancel();
|
|
|
|
|
if (_absoluteCap != null) {
|
|
|
|
|
_absoluteTimer = Timer(_absoluteCap!, _fire);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void start() {
|
|
|
|
|
if (_started) return;
|
2025-12-02 16:15:16 +05:30
|
|
|
// Prevent restart while callback is still executing to avoid double-fire
|
|
|
|
|
if (_firing) return;
|
2025-09-07 23:17:26 +05:30
|
|
|
_started = true;
|
|
|
|
|
_restart();
|
|
|
|
|
if (_absoluteCap != null) {
|
|
|
|
|
_absoluteTimer = Timer(_absoluteCap!, _fire);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ping() {
|
2025-12-02 16:15:16 +05:30
|
|
|
// Prevent restart while callback is still executing to avoid double-fire
|
|
|
|
|
if (_firing) return;
|
2025-09-07 23:17:26 +05:30
|
|
|
if (!_started) {
|
|
|
|
|
start();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_restart();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void stop() {
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
_timer = null;
|
|
|
|
|
_absoluteTimer?.cancel();
|
|
|
|
|
_absoluteTimer = null;
|
|
|
|
|
_started = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void dispose() => stop();
|
|
|
|
|
|
|
|
|
|
void _restart() {
|
|
|
|
|
_timer?.cancel();
|
|
|
|
|
_timer = Timer(_window, _fire);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 16:15:16 +05:30
|
|
|
/// Synchronous entry point called by Timer. Kicks off async work.
|
2025-09-07 23:17:26 +05:30
|
|
|
void _fire() {
|
2025-12-02 16:15:16 +05:30
|
|
|
if (_firing) return; // Prevent re-entry
|
|
|
|
|
_firing = true;
|
2025-09-07 23:17:26 +05:30
|
|
|
stop();
|
2025-12-02 16:15:16 +05:30
|
|
|
// Execute the callback asynchronously. We don't await because Timer
|
|
|
|
|
// expects a sync callback, but the async work will complete in background.
|
|
|
|
|
_executeCallback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Executes the timeout callback asynchronously.
|
|
|
|
|
Future<void> _executeCallback() async {
|
2025-09-07 23:17:26 +05:30
|
|
|
try {
|
2025-12-02 16:15:16 +05:30
|
|
|
await onTimeout();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Swallow errors to prevent unhandled exceptions
|
|
|
|
|
} finally {
|
|
|
|
|
_firing = false;
|
|
|
|
|
}
|
2025-09-07 23:17:26 +05:30
|
|
|
}
|
|
|
|
|
}
|