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).
72 lines
2.5 KiB
Dart
72 lines
2.5 KiB
Dart
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'),
|
|
),
|
|
);
|