Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Upcoming Release

- fix: prevent race condition during concurrent dependency initialization.

## 1.0.1 - 2025-10-17

- **chore**: A new migration ensures that existing user preference documents are updated to include the savedFilters field, initialized as an empty array.
Expand Down
53 changes: 39 additions & 14 deletions lib/src/config/app_dependencies.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// ignore_for_file: public_member_api_docs

import 'dart:async';
import 'package:core/core.dart';
import 'package:data_mongodb/data_mongodb.dart';
import 'package:data_repository/data_repository.dart';
Expand Down Expand Up @@ -40,11 +41,12 @@ class AppDependencies {
/// Provides access to the singleton instance.
static AppDependencies get instance => _instance;

bool _isInitialized = false;
Object? _initializationError;
StackTrace? _initializationStackTrace;
final _log = Logger('AppDependencies');

// A Completer to manage the one-time asynchronous initialization.
// This ensures the initialization logic runs only once.
Completer<void>? _initCompleter;

// --- Late-initialized fields for all dependencies ---

// Database
Expand Down Expand Up @@ -80,15 +82,29 @@ class AppDependencies {
///
/// This method is idempotent; it will only run the initialization logic once.
Future<void> init() async {
// If initialization previously failed, re-throw the original error.
if (_initializationError != null) {
return Future.error(_initializationError!, _initializationStackTrace);
// If _initCompleter is not null, it means initialization is either in
// progress or has already completed. Return its future to allow callers
// to await the single, shared initialization process.
if (_initCompleter != null) {
return _initCompleter!.future;
}

if (_isInitialized) return;
// This is the first call to init(). Create the completer and start the
// initialization process.
_initCompleter = Completer<void>();
_log.info('Starting application dependency initialization...');
// We intentionally don't await this future here. The completer's future,
// which is returned below, is what callers will await.
unawaited(_initializeDependencies());

_log.info('Initializing application dependencies...');
// Return the future from the completer.
return _initCompleter!.future;
}

/// The core logic for initializing all dependencies.
/// This method is private and should only be called once by [init].
Future<void> _initializeDependencies() async {
_log.info('Initializing application dependencies...');
try {
// 1. Initialize Database Connection
_mongoDbConnectionManager = MongoDbConnectionManager();
Expand Down Expand Up @@ -271,24 +287,33 @@ class AppDependencies {
cacheDuration: EnvironmentConfig.countryServiceCacheDuration,
);

_isInitialized = true;
_log.info('Application dependencies initialized successfully.');
// Signal that initialization has completed successfully.
_initCompleter!.complete();
} catch (e, s) {
_log.severe('Failed to initialize application dependencies', e, s);
_initializationError = e;
_initializationStackTrace = s;
// Signal that initialization has failed.
_initCompleter!.completeError(e, s);
rethrow;
}
}

/// Disposes of resources, such as closing the database connection.
Future<void> dispose() async {
if (!_isInitialized) return;
// Only attempt to dispose if initialization has been started.
if (_initCompleter == null) {
_log.info('Dispose called, but dependencies were never initialized.');
return;
}

_log.info('Disposing application dependencies...');
await _mongoDbConnectionManager.close();
tokenBlacklistService.dispose();
rateLimitService.dispose();
countryQueryService.dispose(); // Dispose the new service
_isInitialized = false;
_log.info('Application dependencies disposed.');

// Reset the completer to allow for re-initialization (e.g., in tests).
_initCompleter = null;
_log.info('Application dependencies disposed and state reset.');
}
}
Loading