Files
Dmitry a559d6de3e feat(app): смена сервера API и очистка данных устройства при выходе
Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
2026-09-19 22:14:14 +03:00

76 lines
2.7 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(),
),
);
/// Drops every cached body — on sign-out, so the next account (or nobody) never sees them.
Future<void> clear() => delete(cachedResponses).go();
/// 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'),
),
);