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
39 changes: 39 additions & 0 deletions lib/core/database/redis_connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,45 @@ class RedisConnection {
return result is int ? result : int.tryParse(result.toString()) ?? -1;
}

/// Pipelined TYPE + TTL for a SCAN batch.
///
/// Writes all commands before awaiting replies (redis-dart FIFO parse
/// queue + optional Nagle via [Command.pipe_start]), so a batch of N keys
/// costs ~1 RTT instead of ~2N sequential round-trips.
Future<List<({String type, int ttl})>> typesAndTtls(List<String> keys) async {
if (keys.isEmpty) return const [];
if (!isConnected) {
throw StateError('Not connected to Redis');
}

final cmd = _command;
cmd?.pipe_start();
try {
final typeFutures = <Future<String>>[
for (final key in keys)
sendCommand(['TYPE', key]).then(
(v) => v?.toString() ?? 'none',
onError: (_) => 'unknown',
),
];
final ttlFutures = <Future<int>>[
for (final key in keys)
sendCommand(['TTL', key]).then(
(v) => v is int ? v : int.tryParse(v.toString()) ?? -1,
onError: (_) => -1,
),
];
final types = await Future.wait(typeFutures);
final ttls = await Future.wait(ttlFutures);
return [
for (var i = 0; i < keys.length; i++)
(type: types[i], ttl: ttls[i]),
];
} finally {
cmd?.pipe_end();
}
}

/// GET (string).
Future<String?> get(String key) async {
final result = await sendCommand(['GET', key]);
Expand Down
29 changes: 18 additions & 11 deletions lib/features/redis/redis_keys_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,24 @@ class _RedisKeysViewState extends material.State<RedisKeysView> {
count: 100,
);

// Fetch type and TTL for each key concurrently
final futures = keyNames.map((name) async {
try {
final type = await widget.connection.keyType(name);
final ttl = await widget.connection.ttl(name);
return _KeyInfo(name: name, type: type, ttl: ttl);
} catch (_) {
return _KeyInfo(name: name, type: 'unknown', ttl: -1);
}
});
final infos = await Future.wait(futures);
// One pipelined burst of TYPE+TTL (not N× Future.wait round-trips).
List<_KeyInfo> infos;
try {
final metas = await widget.connection.typesAndTtls(keyNames);
infos = [
for (var i = 0; i < keyNames.length; i++)
_KeyInfo(
name: keyNames[i],
type: metas[i].type,
ttl: metas[i].ttl,
),
];
} catch (_) {
infos = [
for (final name in keyNames)
_KeyInfo(name: name, type: 'unknown', ttl: -1),
];
}

if (!mounted) return;
setState(() {
Expand Down
43 changes: 43 additions & 0 deletions test/core/database/redis_types_and_ttls_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/database/redis_connection.dart';

/// Counts outbound commands to prove [typesAndTtls] fires TYPE+TTL without
/// awaiting between keys (true pipeline enqueue).
class _CountingRedisFake extends RedisConnectionTestFake {
_CountingRedisFake() : super(firstScanKeys: const []);

final List<String> ops = [];

@override
Future<dynamic> sendCommand(List<dynamic> args) async {
ops.add(args.first.toString().toUpperCase());
// Delay so overlapping awaits would change order if callers awaited per key.
await Future<void>.delayed(Duration.zero);
return super.sendCommand(args);
}
}

void main() {
test('typesAndTtls enqueues all TYPE then all TTL before settling', () async {
final fake = _CountingRedisFake();
await fake.connect();

final metas = await fake.typesAndTtls(['a', 'b', 'c']);

expect(metas, hasLength(3));
expect(metas.map((m) => m.type), everyElement('string'));
expect(metas.map((m) => m.ttl), everyElement(-1));

// All TYPE writes precede all TTL writes (single burst, not TYPE+TTL per key).
expect(
fake.ops,
['TYPE', 'TYPE', 'TYPE', 'TTL', 'TTL', 'TTL'],
);
});

test('typesAndTtls returns empty for empty keys', () async {
final fake = RedisConnectionTestFake();
await fake.connect();
expect(await fake.typesAndTtls(const []), isEmpty);
});
}
Loading