refactor(apps): 重构数据层目录结构并新增启动预热编排器
This commit is contained in:
Vendored
+337
@@ -1,5 +1,342 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class CacheEntry<T> {
|
||||
const CacheEntry({required this.value, required this.fetchedAt});
|
||||
|
||||
final T value;
|
||||
final DateTime fetchedAt;
|
||||
}
|
||||
|
||||
abstract class CacheStore {
|
||||
Future<T?> read<T>(String key);
|
||||
Future<void> write<T>(String key, T value);
|
||||
Future<void> remove(String key);
|
||||
}
|
||||
|
||||
class MemoryCacheStore implements CacheStore {
|
||||
final Map<String, Object?> _values = <String, Object?>{};
|
||||
|
||||
@override
|
||||
Future<T?> read<T>(String key) async {
|
||||
final value = _values[key];
|
||||
if (value is T) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write<T>(String key, T value) async {
|
||||
_values[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
_values.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
class SharedPrefsCacheStore implements CacheStore {
|
||||
SharedPrefsCacheStore({required SharedPreferences prefs}) : _prefs = prefs;
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
@override
|
||||
Future<T?> read<T>(String key) async {
|
||||
final raw = _prefs.getString(key);
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return CacheCodec.decode<T>(raw);
|
||||
} catch (_) {
|
||||
await _prefs.remove(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write<T>(String key, T value) async {
|
||||
final encoded = CacheCodec.encode<T>(value);
|
||||
await _prefs.setString(key, encoded);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) {
|
||||
return _prefs.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
class PersistentCacheStore implements CacheStore {
|
||||
SharedPreferences? _prefs;
|
||||
Future<SharedPreferences>? _prefsFuture;
|
||||
final Map<String, Object?> _fallbackValues = <String, Object?>{};
|
||||
|
||||
PersistentCacheStore({SharedPreferences? prefs}) : _prefs = prefs;
|
||||
|
||||
Future<SharedPreferences?> _getPrefs() {
|
||||
if (_prefs != null) {
|
||||
return Future<SharedPreferences?>.value(_prefs);
|
||||
}
|
||||
final inFlight = _prefsFuture;
|
||||
if (inFlight != null) {
|
||||
return inFlight.then<SharedPreferences?>((prefs) => prefs);
|
||||
}
|
||||
final created = SharedPreferences.getInstance();
|
||||
_prefsFuture = created;
|
||||
return created
|
||||
.then<SharedPreferences?>((prefs) {
|
||||
_prefs = prefs;
|
||||
return prefs;
|
||||
})
|
||||
.catchError((_) {
|
||||
_prefsFuture = null;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> read<T>(String key) async {
|
||||
final prefs = await _getPrefs();
|
||||
if (prefs == null) {
|
||||
final value = _fallbackValues[key];
|
||||
if (value is T) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
final store = SharedPrefsCacheStore(prefs: prefs);
|
||||
return store.read<T>(key);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write<T>(String key, T value) async {
|
||||
final prefs = await _getPrefs();
|
||||
if (prefs == null) {
|
||||
_fallbackValues[key] = value;
|
||||
return;
|
||||
}
|
||||
final store = SharedPrefsCacheStore(prefs: prefs);
|
||||
await store.write<T>(key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> remove(String key) async {
|
||||
final prefs = await _getPrefs();
|
||||
if (prefs == null) {
|
||||
_fallbackValues.remove(key);
|
||||
return;
|
||||
}
|
||||
final store = SharedPrefsCacheStore(prefs: prefs);
|
||||
await store.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
class HybridCacheStore {
|
||||
final CacheStore memory;
|
||||
final CacheStore persistent;
|
||||
final Map<String, Future<dynamic>> _inflight = <String, Future<dynamic>>{};
|
||||
|
||||
HybridCacheStore({required this.memory, required this.persistent});
|
||||
|
||||
Future<T?> read<T>(String key) async {
|
||||
final memoryValue = await memory.read<T>(key);
|
||||
if (memoryValue != null) {
|
||||
return memoryValue;
|
||||
}
|
||||
final persistentValue = await persistent.read<T>(key);
|
||||
if (persistentValue != null) {
|
||||
await memory.write(key, persistentValue);
|
||||
}
|
||||
return persistentValue;
|
||||
}
|
||||
|
||||
Future<void> write<T>(String key, T value) async {
|
||||
await memory.write<T>(key, value);
|
||||
await persistent.write<T>(key, value);
|
||||
}
|
||||
|
||||
Future<void> remove(String key) async {
|
||||
await memory.remove(key);
|
||||
await persistent.remove(key);
|
||||
}
|
||||
|
||||
Future<T> getOrLoad<T>(String key, {required Future<T> Function() loader}) {
|
||||
final running = _inflight[key];
|
||||
if (running != null) {
|
||||
return running.then((value) => value as T);
|
||||
}
|
||||
|
||||
final future = () async {
|
||||
final cached = await read<T>(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final loaded = await loader();
|
||||
await write<T>(key, loaded);
|
||||
return loaded;
|
||||
}();
|
||||
|
||||
_inflight[key] = future;
|
||||
return future.whenComplete(() {
|
||||
_inflight.remove(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CacheInvalidator {
|
||||
final HybridCacheStore? _store;
|
||||
|
||||
CacheInvalidator({HybridCacheStore? store}) : _store = store;
|
||||
|
||||
void invalidate(String key) {
|
||||
final store = _store;
|
||||
if (store != null) {
|
||||
unawaited(store.remove(key));
|
||||
}
|
||||
}
|
||||
|
||||
void invalidateCalendarDay(DateTime date) {
|
||||
final month = '${date.year}-${date.month.toString().padLeft(2, '0')}';
|
||||
final day = '$month-${date.day.toString().padLeft(2, '0')}';
|
||||
invalidate('calendar:day:$day');
|
||||
invalidate('calendar:month:$month');
|
||||
}
|
||||
}
|
||||
|
||||
class CacheCodec {
|
||||
static String encode<T>(T value) {
|
||||
final payload = <String, Object?>{'type': '$T'};
|
||||
if (value is CacheEntry) {
|
||||
payload['entryType'] = '${value.value.runtimeType}';
|
||||
payload['fetchedAt'] = value.fetchedAt.toIso8601String();
|
||||
payload['value'] = _encodeValue(value.value);
|
||||
} else {
|
||||
payload['value'] = _encodeValue(value);
|
||||
}
|
||||
return jsonEncode(payload);
|
||||
}
|
||||
|
||||
static T decode<T>(String raw) {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const FormatException('Invalid cache payload');
|
||||
}
|
||||
|
||||
final value = decoded['value'];
|
||||
|
||||
if (T.toString().startsWith('CacheEntry<')) {
|
||||
final fetchedAtRaw = decoded['fetchedAt'];
|
||||
final fetchedAt = DateTime.parse(fetchedAtRaw as String);
|
||||
final entryType = (decoded['entryType'] as String?) ?? 'Object?';
|
||||
final decodedValue = _decodeValue(entryType, value);
|
||||
|
||||
switch (entryType) {
|
||||
case 'String':
|
||||
return CacheEntry<String>(
|
||||
value: decodedValue as String,
|
||||
fetchedAt: fetchedAt,
|
||||
)
|
||||
as T;
|
||||
case 'int':
|
||||
return CacheEntry<int>(
|
||||
value: decodedValue as int,
|
||||
fetchedAt: fetchedAt,
|
||||
)
|
||||
as T;
|
||||
case 'double':
|
||||
return CacheEntry<double>(
|
||||
value: decodedValue as double,
|
||||
fetchedAt: fetchedAt,
|
||||
)
|
||||
as T;
|
||||
case 'bool':
|
||||
return CacheEntry<bool>(
|
||||
value: decodedValue as bool,
|
||||
fetchedAt: fetchedAt,
|
||||
)
|
||||
as T;
|
||||
default:
|
||||
return CacheEntry<Object?>(value: decodedValue, fetchedAt: fetchedAt)
|
||||
as T;
|
||||
}
|
||||
}
|
||||
|
||||
final type = (decoded['type'] as String?) ?? '$T';
|
||||
return _decodeValue(type, value) as T;
|
||||
}
|
||||
|
||||
static Object? _encodeValue(Object? value) {
|
||||
if (value == null || value is String || value is num || value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is DateTime) {
|
||||
return value.toIso8601String();
|
||||
}
|
||||
if (value is List) {
|
||||
return value.map(_encodeValue).toList();
|
||||
}
|
||||
if (value is Map) {
|
||||
return value.map((k, v) => MapEntry(k.toString(), _encodeValue(v)));
|
||||
}
|
||||
|
||||
throw StateError('Unsupported cached value type: ${value.runtimeType}');
|
||||
}
|
||||
|
||||
static Object? _decodeValue(String type, Object? raw) {
|
||||
switch (type) {
|
||||
case 'String':
|
||||
return raw as String? ?? '';
|
||||
case 'int':
|
||||
if (raw is int) {
|
||||
return raw;
|
||||
}
|
||||
if (raw is num) {
|
||||
return raw.toInt();
|
||||
}
|
||||
throw const FormatException('Invalid int cache payload');
|
||||
case 'double':
|
||||
if (raw is double) {
|
||||
return raw;
|
||||
}
|
||||
if (raw is num) {
|
||||
return raw.toDouble();
|
||||
}
|
||||
throw const FormatException('Invalid double cache payload');
|
||||
case 'bool':
|
||||
return raw as bool? ?? false;
|
||||
case 'DateTime':
|
||||
return DateTime.parse(raw as String);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
final listType = _extractListType(type);
|
||||
if (listType != null) {
|
||||
if (raw is! List) {
|
||||
throw FormatException('Invalid list cache payload for type $type');
|
||||
}
|
||||
return raw
|
||||
.map((item) => _decodeValue(listType, item))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
if (raw is Map) {
|
||||
return Map<String, dynamic>.from(raw);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
static String? _extractListType(String type) {
|
||||
final listMatch = RegExp(r'^List<(.+)>$').firstMatch(type);
|
||||
if (listMatch == null) {
|
||||
return null;
|
||||
}
|
||||
return listMatch.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user