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? value; DateTime? fetchedAt; DateTime? failedAt; Future? inFlight; } class InfoCache { final Duration ttl; final Duration failureBackoff; final Future Function(int id) fetcher; final Map> _entries = {}; InfoCache({ required this.ttl, required this.fetcher, this.failureBackoff = const Duration(seconds: 10), }); bool _isFresh(_Entry e) { if (e.fetchedAt == null) return false; return DateTime.now().difference(e.fetchedAt!) < ttl; } bool _isInFailureBackoff(_Entry e) { if (e.failedAt == null) return false; return DateTime.now().difference(e.failedAt!) < failureBackoff; } Future get(int id, {bool forceRefresh = false}) { final entry = _entries.putIfAbsent(id, () => _Entry()); 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 _runFetch(_Entry 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()); entry.value = value; entry.fetchedAt = at ?? DateTime.now(); entry.failedAt = null; } void markFailed(int id, {DateTime? at}) { final entry = _entries.putIfAbsent(id, () => _Entry()); entry.failedAt = at ?? DateTime.now(); } } class ContactInfoFetch { static final _cache = InfoCache( ttl: const Duration(minutes: 5), fetcher: _fetch, ); static Future 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 contact) { _cache.putValue( id, ContactInfo.fromMap(Map.from(contact)), ); } static Future> getMany( List ids, { bool forceRefresh = false, }) async { final result = {}; final missing = []; 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()) { final id = c['id']; if (id is! int) continue; final info = ContactInfo.fromMap(Map.from(c)); _cache.putValue(id, info, at: now); result[id] = info; } } } catch (_) {} return result; } static Future _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.from(first)); } } class PresenceFetch { static final _cache = InfoCache>( ttl: const Duration(seconds: 60), fetcher: _fetch, ); static Future?> get( int id, { bool forceRefresh = false, }) => _cache.get(id, forceRefresh: forceRefresh); static Map? peek(int id) => _cache.peek(id); static final Map> _live = {}; static final ValueNotifier revision = ValueNotifier(0); static Map? 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 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 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.from(value); _cache.putValue(id, map, at: now); _live[id] = map; }); revision.value++; } static Future?> _fetch(int id) async { final results = await _fetchBatch([id]); return results[id]; } static Future>> getMany( List ids, { bool forceRefresh = false, }) async { final result = >{}; final missing = []; 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(); var changed = false; for (final id in missing) { final value = fetched[id]; if (value != null) { _cache.putValue(id, value, at: now); _live[id] = value; result[id] = value; changed = true; } else { _cache.markFailed(id, at: now); } } if (changed) revision.value++; } return result; } static const _batchSize = 100; static Future ensureFor(Iterable ids) async { final wanted = {}; for (final id in ids) { if (id <= 0) continue; if (_cache.peek(id) != null) continue; wanted.add(id); } if (wanted.isEmpty) return; final list = wanted.toList(); for (var i = 0; i < list.length; i += _batchSize) { final chunk = list.sublist( i, i + _batchSize > list.length ? list.length : i + _batchSize, ); await getMany(chunk); } } static Future>> _fetchBatch( List 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 = >{}; for (final id in ids) { final entry = presence[id.toString()] ?? presence[id]; if (entry is Map) { out[id] = Map.from(entry); } } return out; } } class BotInfoFetch { static final _cache = InfoCache( ttl: const Duration(minutes: 30), fetcher: _fetch, ); static Future get(int botId, {bool forceRefresh = false}) => _cache.get(botId, forceRefresh: forceRefresh); static BotInfo? peek(int botId) => _cache.peek(botId); static List commandsOf(int botId) => _cache.peek(botId)?.commands ?? const []; static void invalidate(int botId) => _cache.invalidate(botId); static void clear() => _cache.clear(); static Future _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.from(data)); final contact = info.contact; if (contact != null) ContactInfoFetch.putContact(botId, contact.raw); return info; } } class ChatInfoFetch { static final _cache = InfoCache( ttl: const Duration(minutes: 5), fetcher: _fetch, ); static Future 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 _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.from(first)); } }