fix(di): robust all-or-nothing init so the social layer never greys out
The idempotency guard used SocialService as its sentinel, but that was registered mid-cascade, so a run that crashed part-way could leave a half-built container the next run would reuse (or hit 'already registered' on) — intermittently falling back to the inventory-only look with market/chat/profile greyed out. - Guard on a dedicated _DepsReady marker registered LAST, so the sentinel means the WHOLE container is wired, never a partial one. - Reset a half-wired container (crashed prior run) before rebuilding, instead of reusing it. Safe: main aborts before runApp on a partial run. - Make the social key derivation non-fatal: on failure the app opens on inventory (social hidden) rather than blanking. main tolerates an absent SocialService.
This commit is contained in:
parent
97b9223cb2
commit
606861bbfa
2 changed files with 46 additions and 9 deletions
|
|
@ -40,15 +40,33 @@ import '../services/social_settings.dart';
|
||||||
/// in the tree.
|
/// in the tree.
|
||||||
final GetIt getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
|
/// End-of-init sentinel: registered last, so `isRegistered<_DepsReady>()` means
|
||||||
|
/// the container is fully wired (see [configureDependencies]). Distinct from
|
||||||
|
/// [SocialService], which is optional and absent in inventory-only mode.
|
||||||
|
class _DepsReady {
|
||||||
|
const _DepsReady();
|
||||||
|
}
|
||||||
|
|
||||||
/// Wires the encrypted DB, keystore and repositories. Call once from `main`
|
/// Wires the encrypted DB, keystore and repositories. Call once from `main`
|
||||||
/// before `runApp`; the DB key must exist before the DB opens.
|
/// before `runApp`; the DB key must exist before the DB opens.
|
||||||
///
|
///
|
||||||
/// Idempotent: a hot restart (or an Android Activity restart that keeps the
|
/// Idempotent AND all-or-nothing: a hot restart (or an Android Activity restart
|
||||||
/// isolate) re-runs `main` while GetIt still holds the singletons. Without this
|
/// that keeps the isolate) re-runs `main` while GetIt still holds the singletons.
|
||||||
/// guard the re-registration throws, `main` aborts before rebuilding `TaneApp`
|
///
|
||||||
/// with the social layer, and the app falls back to the "inventory only" look.
|
/// A dedicated [_DepsReady] marker is registered LAST and used as the "fully
|
||||||
|
/// wired" sentinel, so `isRegistered<_DepsReady>()` means the WHOLE container is
|
||||||
|
/// ready — never a half-built one. If a prior run crashed part-way through init
|
||||||
|
/// (leaving some singletons registered but not the marker), we reset the
|
||||||
|
/// container and rebuild from scratch rather than early-returning on a partial
|
||||||
|
/// container or throwing "already registered" mid-cascade. That partial state is
|
||||||
|
/// safe to reset because `main` aborts before `runApp` on a partial run, so no
|
||||||
|
/// live UI holds these singletons. Without this, the app intermittently fell
|
||||||
|
/// back to the "inventory only" look (market/chat/profile greyed out).
|
||||||
Future<void> configureDependencies() async {
|
Future<void> configureDependencies() async {
|
||||||
if (getIt.isRegistered<SocialService>()) return;
|
if (getIt.isRegistered<_DepsReady>()) return; // fully wired already
|
||||||
|
// A prior run half-wired the container (crashed after some registrations but
|
||||||
|
// before the sentinel). Clear it so we rebuild cleanly.
|
||||||
|
if (getIt.isRegistered<AppDatabase>()) await getIt.reset();
|
||||||
|
|
||||||
final secretStore = FlutterSecretStore();
|
final secretStore = FlutterSecretStore();
|
||||||
final keyStore = SecureKeyStore(store: secretStore);
|
final keyStore = SecureKeyStore(store: secretStore);
|
||||||
|
|
@ -66,8 +84,15 @@ Future<void> configureDependencies() async {
|
||||||
|
|
||||||
// Block 2 social identity: a secp256k1 Nostr key derived (one-way) from the
|
// Block 2 social identity: a secp256k1 Nostr key derived (one-way) from the
|
||||||
// SAME root seed, so it needs no extra backup. Cheap + offline; no relay is
|
// SAME root seed, so it needs no extra backup. Cheap + offline; no relay is
|
||||||
// contacted here (local-first — the social layer only enriches).
|
// contacted here (local-first — the social layer only enriches). Non-fatal: if
|
||||||
final socialService = await SocialService.fromRootSeedHex(rootSeedHex);
|
// the derivation ever fails, the app still opens on inventory (market/chat/
|
||||||
|
// profile stay hidden) rather than blanking the whole app.
|
||||||
|
SocialService? socialService;
|
||||||
|
try {
|
||||||
|
socialService = await SocialService.fromRootSeedHex(rootSeedHex);
|
||||||
|
} catch (e, s) {
|
||||||
|
debugPrint('Social identity derivation failed; inventory-only mode: $e\n$s');
|
||||||
|
}
|
||||||
|
|
||||||
// Seed the bundled species catalog before the UI opens — but only once per
|
// Seed the bundled species catalog before the UI opens — but only once per
|
||||||
// catalog version. Parsing the ~1.5 MB catalog and scanning the species table
|
// catalog version. Parsing the ~1.5 MB catalog and scanning the species table
|
||||||
|
|
@ -108,7 +133,6 @@ Future<void> configureDependencies() async {
|
||||||
..registerSingleton<LabelTextExtractor>(labelExtractor)
|
..registerSingleton<LabelTextExtractor>(labelExtractor)
|
||||||
..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
|
..registerSingleton<OnboardingStore>(OnboardingStore(secretStore))
|
||||||
..registerSingleton<LocaleStore>(LocaleStore(secretStore))
|
..registerSingleton<LocaleStore>(LocaleStore(secretStore))
|
||||||
..registerSingleton<SocialService>(socialService)
|
|
||||||
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
||||||
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
||||||
..registerSingleton<MessageStore>(MessageStore(secretStore))
|
..registerSingleton<MessageStore>(MessageStore(secretStore))
|
||||||
|
|
@ -139,6 +163,17 @@ Future<void> configureDependencies() async {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: absent only if the derivation above failed — then the app runs
|
||||||
|
// inventory-only, by design (market/chat/profile hidden).
|
||||||
|
if (socialService != null) {
|
||||||
|
getIt.registerSingleton<SocialService>(socialService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registered LAST: the "fully wired" sentinel the guard above checks. Anything
|
||||||
|
// that throws before here leaves the marker unregistered, so the next run
|
||||||
|
// rebuilds cleanly instead of reusing a half-built container.
|
||||||
|
getIt.registerSingleton<_DepsReady>(const _DepsReady());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Directory> _backupsDir() async {
|
Future<Directory> _backupsDir() async {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ Future<void> main() async {
|
||||||
repository: getIt<VarietyRepository>(),
|
repository: getIt<VarietyRepository>(),
|
||||||
species: getIt<SpeciesRepository>(),
|
species: getIt<SpeciesRepository>(),
|
||||||
onboarding: onboarding,
|
onboarding: onboarding,
|
||||||
social: getIt<SocialService>(),
|
social: getIt.isRegistered<SocialService>()
|
||||||
|
? getIt<SocialService>()
|
||||||
|
: null,
|
||||||
socialSettings: getIt<SocialSettings>(),
|
socialSettings: getIt<SocialSettings>(),
|
||||||
location: const GeolocatorCoarseLocation(),
|
location: const GeolocatorCoarseLocation(),
|
||||||
outbox: getIt<OfferOutbox>(),
|
outbox: getIt<OfferOutbox>(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue