Files
fin-tracker/app/lib/core/cache/response_cache.dart
T
Dmitry ffc5ed959a 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).
2026-09-19 12:40:08 +03:00

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'),
),
);