Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/core/motion/ticker_gated_polling.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'dart:async';

import 'package:flutter/widgets.dart';

/// Starts/stops a [Timer.periodic] based on [TickerMode] (keep-alive morph).
///
/// Call from [State.build] or [State.didChangeDependencies]. When the ancestor
/// [TickerMode] is disabled (inactive [QueryaSwitchingBody] /
/// [QueryaCrossFadeStack] layer), the timer is cancelled so network polls do
/// not run offscreen. Re-enabling restarts the timer when [shouldRun] is true.
Timer? syncTickerGatedPeriodicTimer({
required BuildContext context,
required Timer? timer,
required bool shouldRun,
required Duration interval,
required void Function() onTick,
}) {
final active = TickerMode.valuesOf(context).enabled && shouldRun;
if (!active) {
timer?.cancel();
return null;
}
return timer ?? Timer.periodic(interval, (_) => onTick());
}
29 changes: 20 additions & 9 deletions lib/features/extensions/extension_stats_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/extensions/extension_driver_session.dart';
import 'package:querya_desktop/core/extensions/models/extension_server_stats.dart';
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/core/util/deep_collection_equals.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';
Expand Down Expand Up @@ -97,21 +98,31 @@ class _ExtensionStatsViewState extends material.State<ExtensionStatsView> {
}
}

Future<void> _onPollTick() async {
try {
final stats = await ExtensionDriverSession.instance
.getServerStats(widget.connectionRow);
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
}

void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(_pollInterval, (_) async {
try {
final stats = await ExtensionDriverSession.instance
.getServerStats(widget.connectionRow);
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
});
_timer = Timer.periodic(_pollInterval, (_) => unawaited(_onPollTick()));
}

@override
material.Widget build(material.BuildContext context) {
_timer = syncTickerGatedPeriodicTimer(
context: context,
timer: _timer,
shouldRun: !_loading && _error == null,
interval: _pollInterval,
onTick: () => unawaited(_onPollTick()),
);

final cs = Theme.of(context).colorScheme;
final width = MediaQuery.sizeOf(context).width;

Expand Down
17 changes: 16 additions & 1 deletion lib/features/mongodb/mongo_stats_view.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';
import 'package:querya_desktop/core/util/deep_collection_equals.dart';
import 'package:querya_desktop/core/database/mongodb_connection.dart';
import 'package:querya_desktop/core/database/mongodb_service.dart';
Expand Down Expand Up @@ -174,7 +175,7 @@ class _MongoStatsViewState extends material.State<MongoStatsView> {
_timer?.cancel();
final interval = _autoRefreshInterval;
if (interval == null) return;
_timer = Timer.periodic(interval, (_) => _pollTick());
_timer = Timer.periodic(interval, (_) => unawaited(_pollTick()));
}

String _formatClock(DateTime t) {
Expand All @@ -184,6 +185,20 @@ class _MongoStatsViewState extends material.State<MongoStatsView> {

@override
material.Widget build(material.BuildContext context) {
final interval = _autoRefreshInterval;
if (interval != null) {
_timer = syncTickerGatedPeriodicTimer(
context: context,
timer: _timer,
shouldRun: !_loading && _connection != null,
interval: interval,
onTick: () => unawaited(_pollTick()),
);
} else {
_timer?.cancel();
_timer = null;
}

final cs = Theme.of(context).colorScheme;
final width = MediaQuery.sizeOf(context).width;

Expand Down
32 changes: 22 additions & 10 deletions lib/features/mysql/mysql_stats_view.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';
import 'package:querya_desktop/core/util/deep_collection_equals.dart';
import 'package:querya_desktop/core/database/mysql_service.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
Expand Down Expand Up @@ -108,22 +109,33 @@ class _MysqlStatsViewState extends material.State<MysqlStatsView> {
}
}

Future<void> _onPollTick() async {
final conn = _lease?.connection;
if (conn == null || !conn.isConnected) return;
try {
final stats = await conn.serverStats();
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
}

void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(_pollInterval, (_) async {
final conn = _lease?.connection;
if (conn == null || !conn.isConnected) return;
try {
final stats = await conn.serverStats();
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
});
_timer = Timer.periodic(_pollInterval, (_) => unawaited(_onPollTick()));
}

@override
material.Widget build(material.BuildContext context) {
final conn = _lease?.connection;
_timer = syncTickerGatedPeriodicTimer(
context: context,
timer: _timer,
shouldRun: conn != null && conn.isConnected && !_loading,
interval: _pollInterval,
onTick: () => unawaited(_onPollTick()),
);

final cs = Theme.of(context).colorScheme;
final width = material.MediaQuery.sizeOf(context).width;

Expand Down
32 changes: 22 additions & 10 deletions lib/features/postgresql/postgres_stats_view.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';
import 'package:querya_desktop/core/util/deep_collection_equals.dart';
import 'package:querya_desktop/core/database/postgres_connection.dart';
import 'package:querya_desktop/core/database/postgres_service.dart';
Expand Down Expand Up @@ -121,22 +122,33 @@ class _PostgresStatsViewState extends material.State<PostgresStatsView> {
}
}

Future<void> _onPollTick() async {
final c = _connection;
if (c == null || !c.isConnected) return;
try {
final stats = await c.serverStats();
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
}

void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(_pollInterval, (_) async {
final c = _connection;
if (c == null || !c.isConnected) return;
try {
final stats = await c.serverStats();
if (!mounted) return;
if (!replaceIfChanged(_stats, stats, (v) => _stats = v)) return;
setState(() {});
} catch (_) {}
});
_timer = Timer.periodic(_pollInterval, (_) => unawaited(_onPollTick()));
}

@override
material.Widget build(material.BuildContext context) {
final c = _connection;
_timer = syncTickerGatedPeriodicTimer(
context: context,
timer: _timer,
shouldRun: c != null && c.isConnected && !_loading,
interval: _pollInterval,
onTick: () => unawaited(_onPollTick()),
);

final cs = Theme.of(context).colorScheme;
final width = MediaQuery.sizeOf(context).width;

Expand Down
34 changes: 23 additions & 11 deletions lib/features/redis/redis_view.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';
import 'package:querya_desktop/core/util/deep_collection_equals.dart';
import 'package:querya_desktop/core/database/redis_connection.dart';
import 'package:querya_desktop/core/database/redis_info.dart';
Expand Down Expand Up @@ -147,23 +148,34 @@ class _RedisViewState extends material.State<RedisView> {
setState(() => _loading = false);
}

Future<void> _onPollTick() async {
final c = _connection;
if (c == null || !c.isConnected) return;
try {
final raw = await c.info();
final info = parseRedisInfo(raw);
if (!mounted) return;
if (!replaceIfChanged(_info, info, (v) => _info = v)) return;
setState(() {});
} catch (_) {}
}

void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(_pollInterval, (_) async {
final c = _connection;
if (c == null || !c.isConnected) return;
try {
final raw = await c.info();
final info = parseRedisInfo(raw);
if (!mounted) return;
if (!replaceIfChanged(_info, info, (v) => _info = v)) return;
setState(() {});
} catch (_) {}
});
_timer = Timer.periodic(_pollInterval, (_) => unawaited(_onPollTick()));
}

@override
material.Widget build(material.BuildContext context) {
final c = _connection;
_timer = syncTickerGatedPeriodicTimer(
context: context,
timer: _timer,
shouldRun: c != null && c.isConnected && !_loading,
interval: _pollInterval,
onTick: () => unawaited(_onPollTick()),
);

final cs = Theme.of(context).colorScheme;
final width = MediaQuery.sizeOf(context).width;

Expand Down
55 changes: 55 additions & 0 deletions test/core/motion/ticker_gated_polling_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/motion/ticker_gated_polling.dart';

void main() {
testWidgets('cancels timer when TickerMode is disabled', (tester) async {
Timer? timer;
var ticks = 0;

await tester.pumpWidget(
TickerMode(
enabled: true,
child: Builder(
builder: (context) {
timer = syncTickerGatedPeriodicTimer(
context: context,
timer: timer,
shouldRun: true,
interval: const Duration(milliseconds: 20),
onTick: () => ticks++,
);
return const SizedBox.shrink();
},
),
),
);
await tester.pump(const Duration(milliseconds: 50));
expect(ticks, greaterThan(0));
expect(timer, isNotNull);

await tester.pumpWidget(
TickerMode(
enabled: false,
child: Builder(
builder: (context) {
timer = syncTickerGatedPeriodicTimer(
context: context,
timer: timer,
shouldRun: true,
interval: const Duration(milliseconds: 20),
onTick: () => ticks++,
);
return const SizedBox.shrink();
},
),
),
);
final afterDisable = ticks;
await tester.pump(const Duration(milliseconds: 60));
expect(ticks, afterDisable);
expect(timer, isNull);
});
}
Loading