Files
Qlyra/lib/core/cache/info_cache.dart
T

344 lines
9.5 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../backend/api.dart';
import '../../models/bot_info.dart';
import '../../models/chat_info.dart';
import '../../models/contact_info.dart';
import '../protocol/opcode_map.dart';
import '../storage/chat_members_store.dart';
Api? _api;
void attachInfoCacheApi(Api api) {
_api = api;
}
class _Entry<T> {
T? value;
DateTime? fetchedAt;
DateTime? failedAt;
Future<T?>? inFlight;
}
class InfoCache<T> {
final Duration ttl;
final Duration failureBackoff;
final Future<T?> Function(int id) fetcher;
final Map<int, _Entry<T>> _entries = {};
InfoCache({
required this.ttl,
required this.fetcher,
this.failureBackoff = const Duration(seconds: 10),
});
bool _isFresh(_Entry<T> e) {
if (e.fetchedAt == null) return false;
return DateTime.now().difference(e.fetchedAt!) < ttl;
}
bool _isInFailureBackoff(_Entry<T> e) {
if (e.failedAt == null) return false;
return DateTime.now().difference(e.failedAt!) < failureBackoff;
}
Future<T?> get(int id, {bool forceRefresh = false}) {
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
if (!forceRefresh && _isFresh(entry)) {
return Future.value(entry.value);
}
if (!forceRefresh && _isInFailureBackoff(entry)) {
return Future.value(null);
}
if (entry.inFlight != null) return entry.inFlight!;
final future = _runFetch(entry, id);
entry.inFlight = future;
return future;
}
Future<T?> _runFetch(_Entry<T> entry, int id) async {
try {
final result = await fetcher(id);
entry.value = result;
entry.fetchedAt = DateTime.now();
entry.failedAt = null;
return result;
} catch (_) {
entry.failedAt = DateTime.now();
return null;
} finally {
entry.inFlight = null;
}
}
T? peek(int id) {
final entry = _entries[id];
if (entry == null || !_isFresh(entry)) return null;
return entry.value;
}
void invalidate(int id) => _entries.remove(id);
void clear() => _entries.clear();
void putValue(int id, T value, {DateTime? at}) {
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
entry.value = value;
entry.fetchedAt = at ?? DateTime.now();
entry.failedAt = null;
}
void markFailed(int id, {DateTime? at}) {
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
entry.failedAt = at ?? DateTime.now();
}
}
class ContactInfoFetch {
static final _cache = InfoCache<ContactInfo>(
ttl: const Duration(minutes: 5),
fetcher: _fetch,
);
static Future<ContactInfo?> get(int id, {bool forceRefresh = false}) =>
_cache.get(id, forceRefresh: forceRefresh);
static ContactInfo? peek(int id) => _cache.peek(id);
static void invalidate(int id) => _cache.invalidate(id);
static void clear() => _cache.clear();
static void putContact(int id, Map<dynamic, dynamic> contact) {
_cache.putValue(id, ContactInfo.fromMap(Map<String, dynamic>.from(contact)));
}
static Future<Map<int, ContactInfo>> getMany(
List<int> ids, {
bool forceRefresh = false,
}) async {
final result = <int, ContactInfo>{};
final missing = <int>[];
for (final id in ids) {
if (!forceRefresh) {
final cached = _cache.peek(id);
if (cached != null) {
result[id] = cached;
continue;
}
}
missing.add(id);
}
if (missing.isEmpty) return result;
final api = _api;
if (api == null || api.state != SessionState.online) return result;
try {
final resp = await api.sendRequest(Opcode.contactInfo, {
'contactIds': missing,
});
final data = resp.payload;
final contacts = data is Map ? data['contacts'] : null;
if (contacts is List) {
final now = DateTime.now();
for (final c in contacts.whereType<Map>()) {
final id = c['id'];
if (id is! int) continue;
final info = ContactInfo.fromMap(Map<String, dynamic>.from(c));
_cache.putValue(id, info, at: now);
result[id] = info;
}
}
} catch (_) {}
return result;
}
static Future<ContactInfo?> _fetch(int id) async {
final api = _api;
if (api == null || api.state != SessionState.online) return null;
final resp = await api.sendRequest(Opcode.contactInfo, {
'contactIds': [id],
});
final data = resp.payload;
if (data is! Map) return null;
final contacts = data['contacts'];
if (contacts is! List || contacts.isEmpty) return null;
final first = contacts.first;
if (first is! Map) return null;
return ContactInfo.fromMap(Map<String, dynamic>.from(first));
}
}
class PresenceFetch {
static final _cache = InfoCache<Map<String, dynamic>>(
ttl: const Duration(seconds: 60),
fetcher: _fetch,
);
static Future<Map<String, dynamic>?> get(
int id, {
bool forceRefresh = false,
}) => _cache.get(id, forceRefresh: forceRefresh);
static Map<String, dynamic>? peek(int id) => _cache.peek(id);
static final Map<int, Map<String, dynamic>> _live = {};
static final ValueNotifier<int> revision = ValueNotifier<int>(0);
static Map<String, dynamic>? live(int id) => _live[id] ?? _cache.peek(id);
static bool isOnline(int id) => (live(id)?['status'] as int?) == 1;
static void apply(int id, Map<String, dynamic> presence) {
if (id <= 0) return;
_live[id] = presence;
_cache.putValue(id, presence);
revision.value++;
}
static void invalidate(int id) => _cache.invalidate(id);
static void clear() {
_cache.clear();
_live.clear();
revision.value++;
}
static void primeAll(Map<dynamic, dynamic> presence) {
final now = DateTime.now();
presence.forEach((key, value) {
if (value is! Map) return;
final id = key is int ? key : int.tryParse(key.toString());
if (id == null) return;
final map = Map<String, dynamic>.from(value);
_cache.putValue(id, map, at: now);
_live[id] = map;
});
revision.value++;
}
static Future<Map<String, dynamic>?> _fetch(int id) async {
final results = await _fetchBatch([id]);
return results[id];
}
static Future<Map<int, Map<String, dynamic>>> getMany(
List<int> ids, {
bool forceRefresh = false,
}) async {
final result = <int, Map<String, dynamic>>{};
final missing = <int>[];
for (final id in ids) {
if (!forceRefresh) {
final cached = _cache.peek(id);
if (cached != null) {
result[id] = cached;
continue;
}
}
missing.add(id);
}
if (missing.isNotEmpty) {
final fetched = await _fetchBatch(missing);
final now = DateTime.now();
for (final id in missing) {
final value = fetched[id];
if (value != null) {
_cache.putValue(id, value, at: now);
result[id] = value;
} else {
_cache.markFailed(id, at: now);
}
}
}
return result;
}
static Future<Map<int, Map<String, dynamic>>> _fetchBatch(
List<int> ids,
) async {
final api = _api;
if (api == null || api.state != SessionState.online || ids.isEmpty) {
return const {};
}
final resp = await api.sendRequest(Opcode.contactPresence, {
'contactIds': ids,
});
final data = resp.payload;
if (data is! Map) return const {};
final presence = data['presence'];
if (presence is! Map) return const {};
final out = <int, Map<String, dynamic>>{};
for (final id in ids) {
final entry = presence[id.toString()] ?? presence[id];
if (entry is Map) {
out[id] = Map<String, dynamic>.from(entry);
}
}
return out;
}
}
class BotInfoFetch {
static final _cache = InfoCache<BotInfo>(
ttl: const Duration(minutes: 30),
fetcher: _fetch,
);
static Future<BotInfo?> get(int botId, {bool forceRefresh = false}) =>
_cache.get(botId, forceRefresh: forceRefresh);
static BotInfo? peek(int botId) => _cache.peek(botId);
static List<BotCommand> commandsOf(int botId) =>
_cache.peek(botId)?.commands ?? const [];
static void invalidate(int botId) => _cache.invalidate(botId);
static void clear() => _cache.clear();
static Future<BotInfo?> _fetch(int botId) async {
final api = _api;
if (api == null || api.state != SessionState.online) return null;
final resp = await api.sendRequest(Opcode.botInfo, {'botId': botId});
final data = resp.payload;
if (data is! Map) return null;
final info = BotInfo.fromPayload(botId, Map<String, dynamic>.from(data));
final contact = info.contact;
if (contact != null) ContactInfoFetch.putContact(botId, contact.raw);
return info;
}
}
class ChatInfoFetch {
static final _cache = InfoCache<ChatInfo>(
ttl: const Duration(minutes: 5),
fetcher: _fetch,
);
static Future<ChatInfo?> get(int id, {bool forceRefresh = false}) =>
_cache.get(id, forceRefresh: forceRefresh);
static ChatInfo? peek(int id) => _cache.peek(id);
static void invalidate(int id) => _cache.invalidate(id);
static void clear() => _cache.clear();
static Future<ChatInfo?> _fetch(int id) async {
final api = _api;
if (api == null || api.state != SessionState.online) return null;
final resp = await api.sendRequest(Opcode.chatInfo, {
'chatIds': [id],
});
final data = resp.payload;
if (data is! Map) return null;
final chats = data['chats'];
if (chats is! List || chats.isEmpty) return null;
final first = chats.first;
if (first is! Map) return null;
ChatMembersStore.instance.applyChatPayload(first);
return ChatInfo.fromMap(Map<String, dynamic>.from(first));
}
}