mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Group contacts by last seen
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MeshMapNode {
|
||||
final int type;
|
||||
@@ -33,25 +35,35 @@ class MeshMapNode {
|
||||
class MeshMapNodesService {
|
||||
static const String _nodesEndpoint =
|
||||
'https://api.meshcore.nz/api/v1/map/nodes';
|
||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
||||
static const Duration traceCacheTtl = Duration(minutes: 10);
|
||||
static const Duration _cacheTtl = Duration(hours: 24);
|
||||
static const Duration traceCacheTtl = _cacheTtl;
|
||||
static const Duration traceTimeout = Duration(seconds: 30);
|
||||
static const String _cacheKey = 'mesh_map_nodes_cache_v1';
|
||||
static const String _cacheTimestampKey = 'mesh_map_nodes_cache_timestamp_v1';
|
||||
static List<MeshMapNode>? _cachedNodes;
|
||||
static DateTime? _cachedAt;
|
||||
static Future<void>? _ongoingSync;
|
||||
|
||||
static Future<List<MeshMapNode>> fetchNodes({
|
||||
bool forceRefresh = false,
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
bool allowNetwork = true,
|
||||
http.Client? client,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
if (!forceRefresh &&
|
||||
_cachedNodes != null &&
|
||||
cached.isNotEmpty &&
|
||||
_cachedAt != null &&
|
||||
now.difference(_cachedAt!) < cacheTtl) {
|
||||
return _cachedNodes!;
|
||||
return cached;
|
||||
}
|
||||
|
||||
final response = await http
|
||||
if (!allowNetwork) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final response = await (client ?? http.Client())
|
||||
.get(Uri.parse(_nodesEndpoint))
|
||||
.timeout(traceTimeout);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
@@ -69,8 +81,131 @@ class MeshMapNodesService {
|
||||
)
|
||||
.toList();
|
||||
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = now;
|
||||
await _storeCache(nodes, cachedAt: now);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
static Future<List<MeshMapNode>> loadCachedNodes({
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
if (_cachedNodes != null &&
|
||||
_cachedAt != null &&
|
||||
now.difference(_cachedAt!) < cacheTtl) {
|
||||
return _cachedNodes!;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||
final cachedJson = prefs.getString(_cacheKey);
|
||||
if (cachedAtMs == null || cachedJson == null) {
|
||||
_cachedNodes = null;
|
||||
_cachedAt = null;
|
||||
return const [];
|
||||
}
|
||||
|
||||
final cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||
if (now.difference(cachedAt) >= cacheTtl) {
|
||||
_cachedNodes = null;
|
||||
_cachedAt = cachedAt;
|
||||
return const [];
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(cachedJson) as List<dynamic>;
|
||||
final nodes = decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(MeshMapNode.fromJson)
|
||||
.where(
|
||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
||||
)
|
||||
.toList();
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = cachedAt;
|
||||
return nodes;
|
||||
}
|
||||
|
||||
static Future<bool> hasFreshCache({Duration cacheTtl = _cacheTtl}) async {
|
||||
final nodes = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
return nodes.isNotEmpty;
|
||||
}
|
||||
|
||||
static Future<void> syncInBackgroundIfStale({
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
http.Client? client,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
if (_cachedAt != null && now.difference(_cachedAt!) < cacheTtl) {
|
||||
return;
|
||||
}
|
||||
|
||||
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
if (cached.isNotEmpty && _cachedAt != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ongoingSync != null) {
|
||||
return _ongoingSync!;
|
||||
}
|
||||
|
||||
_ongoingSync = () async {
|
||||
try {
|
||||
await fetchNodes(
|
||||
forceRefresh: true,
|
||||
cacheTtl: cacheTtl,
|
||||
allowNetwork: true,
|
||||
client: client,
|
||||
);
|
||||
} catch (_) {
|
||||
// Background refresh is best-effort only.
|
||||
} finally {
|
||||
_ongoingSync = null;
|
||||
}
|
||||
}();
|
||||
|
||||
return _ongoingSync!;
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_cacheKey);
|
||||
await prefs.remove(_cacheTimestampKey);
|
||||
_cachedNodes = null;
|
||||
_cachedAt = null;
|
||||
}
|
||||
|
||||
static Future<DateTime?> cachedAt() async {
|
||||
if (_cachedAt != null) return _cachedAt;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||
if (cachedAtMs == null) return null;
|
||||
_cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||
return _cachedAt;
|
||||
}
|
||||
|
||||
static Future<void> _storeCache(
|
||||
List<MeshMapNode> nodes, {
|
||||
required DateTime cachedAt,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_cacheKey,
|
||||
jsonEncode(
|
||||
nodes
|
||||
.map(
|
||||
(node) => {
|
||||
'type': node.type,
|
||||
'name': node.name,
|
||||
'public_key': node.publicKey,
|
||||
'latitude': node.latitude,
|
||||
'longitude': node.longitude,
|
||||
'updated_at': node.updatedAtMs,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
await prefs.setInt(_cacheTimestampKey, cachedAt.millisecondsSinceEpoch);
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = cachedAt;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user