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 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 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 clear() => delete(cachedResponses).go(); /// The body last stored for [requestKey], or null if this endpoint+params was /// never fetched successfully on this device. Future 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'), ), );