mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 21:53:52 +01:00
The IndexedDB stores are the desktop device's primary copy of the food log and the *only* copy of its photos -- photos never sync -- and their wrapper-mirror fallback is what makes a wiped Chrome profile recoverable. All of that was invisible to the suite: the files import `idb_browser.dart`, which cannot be imported outside a web compile, so they were simply absent from the VM test run rather than covered. Splitting the store classes from the browser-factory `open…()` functions lets them run against `idb_shim`'s in-memory backend on the plain VM, which is what the flutter-desktop-to-chrome skill recommends the shim for. Twelve tests now pin the round-trip, the mirror write, the recover-from-mirror path, and the no-wrapper-running case (a plain browser tab, where the app must still work). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.1 KiB
Dart
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 _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');
|
|
}
|