191 lines
5.3 KiB
Dart
191 lines
5.3 KiB
Dart
import 'dart:collection';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:hive/hive.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
class HiveTileCache {
|
|
HiveTileCache._();
|
|
|
|
static const String _boxName = 'map_tile_cache_v1';
|
|
static const int _maxEntries = 6000;
|
|
static const int _maxMemoryEntries = 1024;
|
|
static const Duration _staleAfter = Duration(days: 14);
|
|
static const Duration _prefetchCooldown = Duration(seconds: 25);
|
|
static final Map<String, Future<Uint8List?>> _inFlight =
|
|
<String, Future<Uint8List?>>{};
|
|
static final LinkedHashMap<String, Uint8List> _memoryCache =
|
|
LinkedHashMap<String, Uint8List>();
|
|
static final Map<String, int> _recentPrefetches = <String, int>{};
|
|
|
|
static Future<void> init() async {
|
|
if (!Hive.isBoxOpen(_boxName)) {
|
|
await Hive.openBox(_boxName);
|
|
}
|
|
}
|
|
|
|
static Box? get _boxOrNull {
|
|
try {
|
|
if (!Hive.isBoxOpen(_boxName)) return null;
|
|
return Hive.box(_boxName);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Uint8List? peek(String url) {
|
|
final inMemory = _memoryCache.remove(url);
|
|
if (inMemory != null) {
|
|
_memoryCache[url] = inMemory;
|
|
return inMemory;
|
|
}
|
|
|
|
final box = _boxOrNull;
|
|
if (box == null) return null;
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
final cached = _freshCachedBytes(box, url, now);
|
|
if (cached == null || cached.isEmpty) return null;
|
|
_rememberInMemory(url, cached);
|
|
return cached;
|
|
}
|
|
|
|
static Future<Uint8List?> getOrFetch(String url) async {
|
|
final immediate = peek(url);
|
|
if (immediate != null) return immediate;
|
|
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
final box = _boxOrNull;
|
|
if (box == null) {
|
|
final bytes = await _fetchWithoutCache(url);
|
|
if (bytes != null && bytes.isNotEmpty) {
|
|
_rememberInMemory(url, bytes);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
final bytes = await _fetchAndStore(box, url, now);
|
|
if (bytes != null && bytes.isNotEmpty) {
|
|
_rememberInMemory(url, bytes);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
static void prefetchUrls(Iterable<String> urls, {int maxCount = 48}) {
|
|
final box = _boxOrNull;
|
|
if (box == null) return;
|
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
_recentPrefetches.removeWhere(
|
|
(_, ts) => now - ts > _prefetchCooldown.inMilliseconds,
|
|
);
|
|
var queued = 0;
|
|
for (final url in urls) {
|
|
if (queued >= maxCount) break;
|
|
if (_memoryCache.containsKey(url)) continue;
|
|
if (box.containsKey(url)) continue;
|
|
final lastPrefetchTs = _recentPrefetches[url];
|
|
if (lastPrefetchTs != null &&
|
|
now - lastPrefetchTs < _prefetchCooldown.inMilliseconds) {
|
|
continue;
|
|
}
|
|
if (_inFlight.containsKey(url)) continue;
|
|
_recentPrefetches[url] = now;
|
|
queued += 1;
|
|
_fetchAndStore(box, url, now).then((bytes) {
|
|
if (bytes != null && bytes.isNotEmpty) {
|
|
_rememberInMemory(url, bytes);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
static Uint8List? _freshCachedBytes(Box box, String url, int now) {
|
|
final cached = box.get(url);
|
|
if (cached is! Map) return null;
|
|
final ts = cached['ts'];
|
|
final bytes = cached['bytes'];
|
|
if (ts is int &&
|
|
bytes is Uint8List &&
|
|
now - ts <= _staleAfter.inMilliseconds) {
|
|
return bytes;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static Future<Uint8List?> _fetchAndStore(Box box, String url, int now) {
|
|
final inFlight = _inFlight[url];
|
|
if (inFlight != null) return inFlight;
|
|
|
|
final future = _fetchAndStoreInternal(box, url, now);
|
|
_inFlight[url] = future;
|
|
future.whenComplete(() {
|
|
_inFlight.remove(url);
|
|
});
|
|
return future;
|
|
}
|
|
|
|
static Future<Uint8List?> _fetchAndStoreInternal(
|
|
Box box,
|
|
String url,
|
|
int now,
|
|
) async {
|
|
try {
|
|
final response = await http.get(Uri.parse(url));
|
|
if (response.statusCode != 200) return null;
|
|
final bytes = response.bodyBytes;
|
|
await box.put(url, <String, dynamic>{'ts': now, 'bytes': bytes});
|
|
_rememberInMemory(url, bytes);
|
|
await _pruneIfNeeded(box);
|
|
return bytes;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<Uint8List?> _fetchWithoutCache(String url) async {
|
|
try {
|
|
final response = await http.get(Uri.parse(url));
|
|
if (response.statusCode != 200) return null;
|
|
return response.bodyBytes;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<void> _pruneIfNeeded(Box box) async {
|
|
final overflow = box.length - _maxEntries;
|
|
if (overflow <= 0) return;
|
|
|
|
final entries = <MapEntry<dynamic, dynamic>>[];
|
|
for (final key in box.keys) {
|
|
entries.add(MapEntry(key, box.get(key)));
|
|
}
|
|
|
|
entries.sort((a, b) {
|
|
final ats = (a.value is Map && a.value['ts'] is int)
|
|
? a.value['ts'] as int
|
|
: 0;
|
|
final bts = (b.value is Map && b.value['ts'] is int)
|
|
? b.value['ts'] as int
|
|
: 0;
|
|
return ats.compareTo(bts);
|
|
});
|
|
|
|
for (var i = 0; i < overflow; i++) {
|
|
final key = entries[i].key;
|
|
await box.delete(key);
|
|
if (key is String) {
|
|
_memoryCache.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _rememberInMemory(String url, Uint8List bytes) {
|
|
if (bytes.isEmpty) return;
|
|
_memoryCache.remove(url);
|
|
_memoryCache[url] = bytes;
|
|
while (_memoryCache.length > _maxMemoryEntries) {
|
|
_memoryCache.remove(_memoryCache.keys.first);
|
|
}
|
|
}
|
|
}
|