feat(app): offline-кэш GET-запросов на дашборде — фаза 5
CacheInterceptor кэширует каждый успешный GET по путь+параметры в drift (sqlite нативно, wasm+OPFS в браузере) и подменяет им сетевую ошибку; провайдер отдаёт Cached<T>, экран показывает баннер «данные на …». Контракт для остальных экранов — docs/ai/offline-cache.md. flake.nix: libsecret/pkg-config для линуксовой сборки, jq/curl для just app-web-assets (сборка sqlite3.wasm + drift_worker.js).
This commit is contained in:
+71
@@ -0,0 +1,71 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
|
||||
part 'response_cache.g.dart';
|
||||
|
||||
/// The last successful body of one GET, keyed by endpoint+params exactly as
|
||||
/// [CacheInterceptor] builds the key. See `docs/ai/offline-cache.md` for the
|
||||
/// whole contract this table is part of.
|
||||
class CachedResponses extends Table {
|
||||
TextColumn get requestKey => text()();
|
||||
TextColumn get body => text()();
|
||||
DateTimeColumn get fetchedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {requestKey};
|
||||
}
|
||||
|
||||
/// A cached body plus when it was fetched — what [CacheInterceptor] needs to
|
||||
/// rebuild a synthetic [Response] when the network itself is unreachable.
|
||||
///
|
||||
/// Not named `CachedResponse`: drift already generates a row class with that
|
||||
/// name for the `CachedResponses` table above.
|
||||
class CacheEntry {
|
||||
const CacheEntry(this.body, this.fetchedAt);
|
||||
final Object? body;
|
||||
final DateTime fetchedAt;
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [CachedResponses])
|
||||
class ResponseCacheDatabase extends _$ResponseCacheDatabase {
|
||||
ResponseCacheDatabase() : super(_openConnection());
|
||||
ResponseCacheDatabase.forTesting(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
/// Replaces whatever was cached for [requestKey] — one row per endpoint+params,
|
||||
/// never a history.
|
||||
Future<void> put(String requestKey, Object? body) => into(cachedResponses).insertOnConflictUpdate(
|
||||
CachedResponsesCompanion.insert(
|
||||
requestKey: requestKey,
|
||||
body: jsonEncode(body),
|
||||
fetchedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
/// The body last stored for [requestKey], or null if this endpoint+params was
|
||||
/// never fetched successfully on this device.
|
||||
Future<CacheEntry?> get(String requestKey) async {
|
||||
final row = await (select(
|
||||
cachedResponses,
|
||||
)..where((t) => t.requestKey.equals(requestKey))).getSingleOrNull();
|
||||
if (row == null) return null;
|
||||
return CacheEntry(jsonDecode(row.body), row.fetchedAt);
|
||||
}
|
||||
}
|
||||
|
||||
/// One sqlite file via native FFI on Android/iOS/desktop; wasm+OPFS (falling
|
||||
/// back to IndexedDB) on web — `driftDatabase` from `package:drift_flutter`
|
||||
/// picks the right backend, `web/sqlite3.wasm` and `web/drift_worker.js` are
|
||||
/// the compiled assets it loads there (regenerate with `just app-web-assets`
|
||||
/// whenever the `drift`/`sqlite3` package versions change).
|
||||
QueryExecutor _openConnection() => driftDatabase(
|
||||
name: 'response_cache',
|
||||
web: DriftWebOptions(
|
||||
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
|
||||
driftWorker: Uri.parse('drift_worker.js'),
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user