diet-guard/app/lib/services/document_store_indexeddb.dart
Krzysztof kuhy Rudnicki 77468b1efd
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Bring the Flutter app's dependencies up to latest stable
Split out from the free-day change only by sequence -- diet-guard's
pre-push CI mirror runs the freshness gate over --all-files, so these
were already blocking a push before that work started.

permission_handler 13.0.1 -> 13.0.2 and workmanager 0.10.9 -> 0.10.10
are patch bumps. very_good_analysis ^10.3.0 -> ^11.0.0 is the major one,
and it newly flags 8 async_return_with_no_await sites across the desktop
wrapper and the sync/Firebase services.

Each is now `return await`, not a suppression. The rule is right in every
one of these cases: without the await the future escapes the enclosing
async function, so a rejection surfaces at the caller with a stack trace
that no longer points at the code that made the call.
background_check_service.dart:33 is the clearest example -- its own
comment says the push "is retried with backoff rather than silently
dropped", which is only true if the failure happens inside the function.

Nothing in CI or pre-commit runs `flutter analyze`, so this would not
have failed anything. Leaving a newly-red analyzer for the next person to
find is how a repo stops being analyzable at all.

flutter analyze clean, 706 Flutter tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:25:33 +02:00

88 lines
3.1 KiB
Dart

/// IndexedDB-backed [DocumentStore], independent of how the database was
/// opened.
///
/// Split from `document_store_web.dart` so a plain VM test can drive it
/// against `idb_shim`'s in-memory backend: the browser factory
/// (`idb_browser.dart`) cannot be imported outside a web compile, and this is
/// the code a wiped Chrome profile depends on for recovery.
library;
import 'package:diet_guard_app/services/desktop_wrapper.dart';
import 'package:diet_guard_app/services/document_store.dart';
import 'package:http/http.dart' as http;
import 'package:idb_shim/idb_shim.dart';
/// [DocumentStore] backed by IndexedDB, mirrored to the desktop wrapper's
/// on-disk copy.
///
/// IndexedDB rather than `localStorage`: the latter caps at roughly 5-10MB per
/// origin and is evicted more eagerly, and these documents are the desktop
/// device's primary copy of the food log.
///
/// The wrapper mirror is the recovery path for a wiped Chrome profile. GitHub
/// sync would eventually restore the log too, but only what other devices have
/// already seen -- a meal logged on this desktop and never pushed exists
/// nowhere else. Reads fall back to the mirror when IndexedDB is empty; writes
/// to it are best-effort so the app still works opened in a plain browser tab
/// with no wrapper running.
class IndexedDbDocumentStore implements DocumentStore {
/// Creates a store over an already-open [database], mirroring to [baseUrl].
IndexedDbDocumentStore(
this._database, {
this.baseUrl = desktopWrapperOrigin,
http.Client? httpClient,
}) : _client = httpClient ?? http.Client();
final Database _database;
final http.Client _client;
/// Origin of the desktop wrapper.
final String baseUrl;
/// Object store holding one entry per document name.
static const storeName = 'documents';
/// IndexedDB database name.
static const databaseName = 'diet_guard';
@override
Future<String?> read(String name) async {
final txn = _database.transaction(storeName, idbModeReadOnly);
final value = await txn.objectStore(storeName).getObject(name);
await txn.completed;
if (value is String) return value;
// Empty IndexedDB: either a first run or a cleared profile. Recover from
// the wrapper's disk copy rather than silently starting from nothing.
return await _readMirror(name);
}
@override
Future<void> write(String name, String contents) async {
final txn = _database.transaction(storeName, idbModeReadWrite);
await txn.objectStore(storeName).put(contents, name);
await txn.completed;
await _writeMirror(name, contents);
}
Future<String?> _readMirror(String name) async {
try {
final response = await _client.get(_uriFor(name));
if (response.statusCode != 200 || response.body.isEmpty) return null;
return response.body;
} on Exception {
return null;
}
}
Future<void> _writeMirror(String name, String contents) async {
try {
await _client.post(_uriFor(name), body: contents);
} on Exception {
// Best-effort by design; see the class docs.
}
}
Uri _uriFor(String name) =>
Uri.parse('$baseUrl${WrapperPaths.documents}$name');
}