From 5fca19cc65360ce6ec9fecbde226dc9836ba3701 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:03:18 +0100 Subject: [PATCH 01/16] feat(server): implement eager-loading entrypoint Introduces a custom `bin/main.dart` entrypoint to shift the application from a lazy-loading to an eager-loading model. This new entrypoint ensures that all application dependencies are initialized upfront before the Dart Frog server starts. If any initialization step fails, the server process will exit immediately, providing a robust "fail-fast" startup sequence and eliminating race conditions. --- bin/main.dart | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 bin/main.dart diff --git a/bin/main.dart b/bin/main.dart new file mode 100644 index 0000000..e37ec9f --- /dev/null +++ b/bin/main.dart @@ -0,0 +1,41 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart'; +import 'package:logging/logging.dart'; + +// Import the generated server entrypoint from the .dart_frog directory. +// We use a prefix to avoid a name collision with our own `main` function. +import '../.dart_frog/server.dart' as server; + +/// The main entrypoint for the application. +/// +/// This custom entrypoint implements an "eager loading" strategy. It ensures +/// that all critical application dependencies are initialized *before* the +/// HTTP server starts listening for requests. +/// +/// If any part of the dependency initialization fails (e.g., database +/// connection, migrations), the process will log a fatal error and exit, +/// preventing the server from running in a broken state. This is a robust, +/// "fail-fast" approach suitable for production environments. +Future main(List args) async { + // Use a local logger for startup-specific messages. + final log = Logger('EagerEntrypoint'); + + try { + log.info('EAGER_INIT: Initializing application dependencies...'); + + // Eagerly initialize all dependencies. If this fails, it will throw. + await AppDependencies.instance.init(); + + log.info('EAGER_INIT: Dependencies initialized successfully.'); + log.info('EAGER_INIT: Starting Dart Frog server...'); + + // Only if initialization succeeds, start the Dart Frog server. + await server.main(); + } catch (e, s) { + log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); + exit(1); // Exit with a non-zero code to indicate failure. + } +} From b45e8898953d608bd0b49ef3e714ced2223f9a60 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:05:22 +0100 Subject: [PATCH 02/16] refactor(server): simplify AppDependencies for eager loading Removes all lazy-loading and race-condition handling logic from the `AppDependencies` class. This includes the `_isInitialized` flag, error caching fields, and the `try-catch` block within the `init` method. With the new eager-loading entrypoint, initialization is guaranteed to happen once at startup, making this complex state management logic redundant. The class is now a simpler, more direct dependency initializer. --- lib/src/config/app_dependencies.dart | 371 +++++++++++++-------------- 1 file changed, 177 insertions(+), 194 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 3de4fdc..1cb95f3 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -40,9 +40,6 @@ class AppDependencies { /// Provides access to the singleton instance. static AppDependencies get instance => _instance; - bool _isInitialized = false; - Object? _initializationError; - StackTrace? _initializationStackTrace; final _log = Logger('AppDependencies'); // --- Late-initialized fields for all dependencies --- @@ -78,217 +75,203 @@ class AppDependencies { /// Initializes all application dependencies. /// - /// This method is idempotent; it will only run the initialization logic once. + /// This method is now designed to be called once at application startup + /// by the eager-loading entrypoint (`bin/main.dart`). It will throw an + /// exception if any part of the initialization fails, which will be caught + /// by the entrypoint to terminate the server process. Future init() async { - // If initialization previously failed, re-throw the original error. - if (_initializationError != null) { - return Future.error(_initializationError!, _initializationStackTrace); - } - - if (_isInitialized) return; - _log.info('Initializing application dependencies...'); - try { - // 1. Initialize Database Connection - _mongoDbConnectionManager = MongoDbConnectionManager(); - await _mongoDbConnectionManager.init(EnvironmentConfig.databaseUrl); - _log.info('MongoDB connection established.'); + // 1. Initialize Database Connection + _mongoDbConnectionManager = MongoDbConnectionManager(); + await _mongoDbConnectionManager.init(EnvironmentConfig.databaseUrl); + _log.info('MongoDB connection established.'); - // 2. Initialize and Run Database Migrations - databaseMigrationService = DatabaseMigrationService( - db: _mongoDbConnectionManager.db, - log: Logger('DatabaseMigrationService'), - migrations: - allMigrations, // From lib/src/database/migrations/all_migrations.dart - ); - await databaseMigrationService.init(); - _log.info('Database migrations applied.'); + // 2. Initialize and Run Database Migrations + databaseMigrationService = DatabaseMigrationService( + db: _mongoDbConnectionManager.db, + log: Logger('DatabaseMigrationService'), + migrations: + allMigrations, // From lib/src/database/migrations/all_migrations.dart + ); + await databaseMigrationService.init(); + _log.info('Database migrations applied.'); - // 3. Seed Database - // This runs AFTER migrations to ensure the schema is up-to-date. - final seedingService = DatabaseSeedingService( - db: _mongoDbConnectionManager.db, - log: Logger('DatabaseSeedingService'), - ); - await seedingService.seedInitialData(); - _log.info('Database seeding complete.'); + // 3. Seed Database + // This runs AFTER migrations to ensure the schema is up-to-date. + final seedingService = DatabaseSeedingService( + db: _mongoDbConnectionManager.db, + log: Logger('DatabaseSeedingService'), + ); + await seedingService.seedInitialData(); + _log.info('Database seeding complete.'); - // 4. Initialize Data Clients (MongoDB implementation) - final headlineClient = DataMongodb ( - connectionManager: _mongoDbConnectionManager, - modelName: 'headlines', - fromJson: Headline.fromJson, - toJson: (item) => item.toJson(), - searchableFields: ['title'], - logger: Logger('DataMongodb'), - ); - final topicClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'topics', - fromJson: Topic.fromJson, - toJson: (item) => item.toJson(), - searchableFields: ['name'], - logger: Logger('DataMongodb'), - ); - final sourceClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'sources', - fromJson: Source.fromJson, - toJson: (item) => item.toJson(), - searchableFields: ['name'], - logger: Logger('DataMongodb'), - ); - final countryClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'countries', - fromJson: Country.fromJson, - toJson: (item) => item.toJson(), - searchableFields: ['name'], - logger: Logger('DataMongodb'), - ); - final languageClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'languages', - fromJson: Language.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), - ); - final userClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'users', - fromJson: User.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), - ); - final userAppSettingsClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'user_app_settings', - fromJson: UserAppSettings.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), - ); - final userContentPreferencesClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'user_content_preferences', - fromJson: UserContentPreferences.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), - ); - final remoteConfigClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'remote_configs', - fromJson: RemoteConfig.fromJson, - toJson: (item) => item.toJson(), - logger: Logger('DataMongodb'), - ); + // 4. Initialize Data Clients (MongoDB implementation) + final headlineClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'headlines', + fromJson: Headline.fromJson, + toJson: (item) => item.toJson(), + searchableFields: ['title'], + logger: Logger('DataMongodb'), + ); + final topicClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'topics', + fromJson: Topic.fromJson, + toJson: (item) => item.toJson(), + searchableFields: ['name'], + logger: Logger('DataMongodb'), + ); + final sourceClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'sources', + fromJson: Source.fromJson, + toJson: (item) => item.toJson(), + searchableFields: ['name'], + logger: Logger('DataMongodb'), + ); + final countryClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'countries', + fromJson: Country.fromJson, + toJson: (item) => item.toJson(), + searchableFields: ['name'], + logger: Logger('DataMongodb'), + ); + final languageClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'languages', + fromJson: Language.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); + final userClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'users', + fromJson: User.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); + final userAppSettingsClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'user_app_settings', + fromJson: UserAppSettings.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); + final userContentPreferencesClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'user_content_preferences', + fromJson: UserContentPreferences.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); + final remoteConfigClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'remote_configs', + fromJson: RemoteConfig.fromJson, + toJson: (item) => item.toJson(), + logger: Logger('DataMongodb'), + ); - // 4. Initialize Repositories - headlineRepository = DataRepository(dataClient: headlineClient); - topicRepository = DataRepository(dataClient: topicClient); - sourceRepository = DataRepository(dataClient: sourceClient); - countryRepository = DataRepository(dataClient: countryClient); - languageRepository = DataRepository(dataClient: languageClient); - userRepository = DataRepository(dataClient: userClient); - userAppSettingsRepository = DataRepository( - dataClient: userAppSettingsClient, - ); - userContentPreferencesRepository = DataRepository( - dataClient: userContentPreferencesClient, - ); - remoteConfigRepository = DataRepository(dataClient: remoteConfigClient); - // Configure the HTTP client for SendGrid. - // The HttpClient's AuthInterceptor will use the tokenProvider to add - // the 'Authorization: Bearer ' header. - final sendGridApiBase = - EnvironmentConfig.sendGridApiUrl ?? 'https://api.sendgrid.com'; - final sendGridHttpClient = HttpClient( - baseUrl: '$sendGridApiBase/v3', - tokenProvider: () async => EnvironmentConfig.sendGridApiKey, - logger: Logger('EmailSendgridClient'), - ); + // 4. Initialize Repositories + headlineRepository = DataRepository(dataClient: headlineClient); + topicRepository = DataRepository(dataClient: topicClient); + sourceRepository = DataRepository(dataClient: sourceClient); + countryRepository = DataRepository(dataClient: countryClient); + languageRepository = DataRepository(dataClient: languageClient); + userRepository = DataRepository(dataClient: userClient); + userAppSettingsRepository = DataRepository( + dataClient: userAppSettingsClient, + ); + userContentPreferencesRepository = DataRepository( + dataClient: userContentPreferencesClient, + ); + remoteConfigRepository = DataRepository(dataClient: remoteConfigClient); + // Configure the HTTP client for SendGrid. + // The HttpClient's AuthInterceptor will use the tokenProvider to add + // the 'Authorization: Bearer ' header. + final sendGridApiBase = + EnvironmentConfig.sendGridApiUrl ?? 'https://api.sendgrid.com'; + final sendGridHttpClient = HttpClient( + baseUrl: '$sendGridApiBase/v3', + tokenProvider: () async => EnvironmentConfig.sendGridApiKey, + logger: Logger('EmailSendgridClient'), + ); - // Initialize the SendGrid email client with the dedicated HTTP client. - final emailClient = EmailSendGrid( - httpClient: sendGridHttpClient, - log: Logger('EmailSendgrid'), - ); + // Initialize the SendGrid email client with the dedicated HTTP client. + final emailClient = EmailSendGrid( + httpClient: sendGridHttpClient, + log: Logger('EmailSendgrid'), + ); - emailRepository = EmailRepository(emailClient: emailClient); + emailRepository = EmailRepository(emailClient: emailClient); - final localAdClient = DataMongodb( - connectionManager: _mongoDbConnectionManager, - modelName: 'local_ads', - fromJson: LocalAd.fromJson, - toJson: LocalAd.toJson, - searchableFields: ['title'], - logger: Logger('DataMongodb'), - ); - localAdRepository = DataRepository(dataClient: localAdClient); + final localAdClient = DataMongodb( + connectionManager: _mongoDbConnectionManager, + modelName: 'local_ads', + fromJson: LocalAd.fromJson, + toJson: LocalAd.toJson, + searchableFields: ['title'], + logger: Logger('DataMongodb'), + ); + localAdRepository = DataRepository(dataClient: localAdClient); - // 5. Initialize Services - tokenBlacklistService = MongoDbTokenBlacklistService( - connectionManager: _mongoDbConnectionManager, - log: Logger('MongoDbTokenBlacklistService'), - ); - authTokenService = JwtAuthTokenService( - userRepository: userRepository, - blacklistService: tokenBlacklistService, - log: Logger('JwtAuthTokenService'), - ); - verificationCodeStorageService = MongoDbVerificationCodeStorageService( - connectionManager: _mongoDbConnectionManager, - log: Logger('MongoDbVerificationCodeStorageService'), - ); - permissionService = const PermissionService(); - authService = AuthService( - userRepository: userRepository, - authTokenService: authTokenService, - verificationCodeStorageService: verificationCodeStorageService, - permissionService: permissionService, - emailRepository: emailRepository, - userAppSettingsRepository: userAppSettingsRepository, - userContentPreferencesRepository: userContentPreferencesRepository, - log: Logger('AuthService'), - ); - dashboardSummaryService = DashboardSummaryService( - headlineRepository: headlineRepository, - topicRepository: topicRepository, - sourceRepository: sourceRepository, - ); - userPreferenceLimitService = DefaultUserPreferenceLimitService( - remoteConfigRepository: remoteConfigRepository, - permissionService: permissionService, - log: Logger('DefaultUserPreferenceLimitService'), - ); - rateLimitService = MongoDbRateLimitService( - connectionManager: _mongoDbConnectionManager, - log: Logger('MongoDbRateLimitService'), - ); - countryQueryService = CountryQueryService( - countryRepository: countryRepository, - log: Logger('CountryQueryService'), - cacheDuration: EnvironmentConfig.countryServiceCacheDuration, - ); + // 5. Initialize Services + tokenBlacklistService = MongoDbTokenBlacklistService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbTokenBlacklistService'), + ); + authTokenService = JwtAuthTokenService( + userRepository: userRepository, + blacklistService: tokenBlacklistService, + log: Logger('JwtAuthTokenService'), + ); + verificationCodeStorageService = MongoDbVerificationCodeStorageService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbVerificationCodeStorageService'), + ); + permissionService = const PermissionService(); + authService = AuthService( + userRepository: userRepository, + authTokenService: authTokenService, + verificationCodeStorageService: verificationCodeStorageService, + permissionService: permissionService, + emailRepository: emailRepository, + userAppSettingsRepository: userAppSettingsRepository, + userContentPreferencesRepository: userContentPreferencesRepository, + log: Logger('AuthService'), + ); + dashboardSummaryService = DashboardSummaryService( + headlineRepository: headlineRepository, + topicRepository: topicRepository, + sourceRepository: sourceRepository, + ); + userPreferenceLimitService = DefaultUserPreferenceLimitService( + remoteConfigRepository: remoteConfigRepository, + permissionService: permissionService, + log: Logger('DefaultUserPreferenceLimitService'), + ); + rateLimitService = MongoDbRateLimitService( + connectionManager: _mongoDbConnectionManager, + log: Logger('MongoDbRateLimitService'), + ); + countryQueryService = CountryQueryService( + countryRepository: countryRepository, + log: Logger('CountryQueryService'), + cacheDuration: EnvironmentConfig.countryServiceCacheDuration, + ); - _isInitialized = true; - _log.info('Application dependencies initialized successfully.'); - } catch (e, s) { - _log.severe('Failed to initialize application dependencies', e, s); - _initializationError = e; - _initializationStackTrace = s; - rethrow; - } + _log.info('Application dependencies initialized successfully.'); } /// Disposes of resources, such as closing the database connection. Future dispose() async { - if (!_isInitialized) return; await _mongoDbConnectionManager.close(); tokenBlacklistService.dispose(); rateLimitService.dispose(); countryQueryService.dispose(); // Dispose the new service - _isInitialized = false; _log.info('Application dependencies disposed.'); } } From f5b81e3617554dc59e9fdf4a7aedced8ee8db4f6 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:07:03 +0100 Subject: [PATCH 03/16] refactor(server): remove lazy init from root middleware Updates the root middleware to remove the `AppDependencies.instance.init()` call. Since initialization is now handled eagerly at application startup by the new custom entrypoint, the middleware's responsibility is simplified to only providing the already-initialized dependencies into the request context. --- routes/_middleware.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/routes/_middleware.dart b/routes/_middleware.dart index e21a84c..97700e1 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -74,17 +74,17 @@ Handler middleware(Handler handler) { }) // --- Dependency Provider --- // This is the outermost middleware. It runs once per request, before any - // other middleware. It's responsible for initializing and providing all - // dependencies for the request. + // other middleware. It's responsible for providing all dependencies, + // which are guaranteed to be pre-initialized by the eager-loading + // entrypoint (`bin/main.dart`), to the request context. .use((handler) { return (context) async { - // 1. Ensure all dependencies are initialized (idempotent). - _log.info('Ensuring all application dependencies are initialized...'); - await AppDependencies.instance.init(); - _log.info('Dependencies are ready.'); - - // 2. Provide all dependencies to the inner handler. + // Provide all dependencies to the inner handler. + // The AppDependencies instance is a singleton that has already been + // initialized at application startup. final deps = AppDependencies.instance; + _log.finer('Providing pre-initialized dependencies to context.'); + return handler .use( provider((_) => DataOperationRegistry()), From 895be8aa92315baf4d0ff91ebbf4de7d17d36d52 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:07:51 +0100 Subject: [PATCH 04/16] refactor(server): configure custom executable entrypoint Adds the `executables` section to `pubspec.yaml` to define the new `bin/main.dart` script as a named executable. This allows the server to be started with `dart run :main`, which is the new standard for running the application with the eager-loading strategy. --- pubspec.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 0f18913..5058e82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,3 +54,7 @@ dev_dependencies: mocktail: ^1.0.3 test: ^1.25.5 very_good_analysis: ^9.0.0 + +executables: + # Defines the custom entrypoint for eager loading. + main: main From 048eaa3cefe72f4d54a60f35ef054908441fc42c Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:10:46 +0100 Subject: [PATCH 05/16] docs(readme): add eager loading architecture highlight Updates the README.md to include a new section under "Architecture & Infrastructure" that highlights the "Eager Loading & Fail-Fast Startup" pattern. This change accurately reflects the recent architectural overhaul, explaining the benefits of initializing all dependencies before the server starts, which enhances robustness and stability. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6924705..4b2a458 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,13 @@ Click on any category to explore. --- +### ✨ Eager Loading & Fail-Fast Startup +- **Robust Initialization:** The server now initializes all critical dependencies (database connection, migrations, seeding) *before* it starts accepting any requests. +- **Fail-Fast by Design:** If any part of the startup process fails (e.g., the database is down), the server will immediately exit with a clear error. +> **Your Advantage:** This eliminates startup race conditions and ensures the server is either fully operational or not running at all. It provides a highly predictable and stable production environment, preventing the server from running in a broken state. + +--- + ### 🔌 Robust Dependency Injection - **Testable & Modular:** A centralized dependency injection system makes the entire application highly modular and easy to test. - **Swappable Implementations:** Easily swap out core components—like the database (MongoDB), email provider (SendGrid), or storage services—without rewriting your business logic. From fb0795caca20d5ece70debc14eaa0a2d6fdcfbf2 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:11:48 +0100 Subject: [PATCH 06/16] docs(changelog): document eager loading refactor Updates the changelog to include the recent architectural shift to an eager-loading startup model. This is marked as a breaking change (`!`) because it alters the command used to run the server. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1c8b6..a626d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Upcoming Release +- **refactor!**: shift from lazy to eager loading for robust, fail-fast startup. + ## 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. From 22d92ea712329d9b37bf08af4a68878c3fd0a02a Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:23:47 +0100 Subject: [PATCH 07/16] refactor(server): move logger setup to eager entrypoint Relocates the global logger configuration from the root middleware (`routes/_middleware.dart`) to the new eager-loading entrypoint (`bin/main.dart`). This is an architectural improvement that ensures the logger is configured exactly once when the application process starts, rather than within a middleware that could be re-instantiated. It makes the startup sequence more robust and simplifies the root middleware's responsibilities. --- bin/main.dart | 20 ++++++++++++++++++++ routes/_middleware.dart | 26 -------------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/bin/main.dart b/bin/main.dart index e37ec9f..d3c1e9d 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -21,6 +21,26 @@ import '../.dart_frog/server.dart' as server; /// "fail-fast" approach suitable for production environments. Future main(List args) async { // Use a local logger for startup-specific messages. + // This is also the ideal place to configure the root logger for the entire + // application, as it's guaranteed to run only once at startup. + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // A more detailed logger that includes the error and stack trace. + // ignore: avoid_print + print( + '${record.level.name}: ${record.time}: ${record.loggerName}: ' + '${record.message}', + ); + if (record.error != null) { + // ignore: avoid_print + print(' ERROR: ${record.error}'); + } + if (record.stackTrace != null) { + // ignore: avoid_print + print(' STACK TRACE: ${record.stackTrace}'); + } + }); + final log = Logger('EagerEntrypoint'); try { diff --git a/routes/_middleware.dart b/routes/_middleware.dart index 97700e1..e4ef0d4 100644 --- a/routes/_middleware.dart +++ b/routes/_middleware.dart @@ -22,38 +22,12 @@ import 'package:mongo_dart/mongo_dart.dart'; // --- Middleware Definition --- final _log = Logger('RootMiddleware'); -// A flag to ensure the logger is only configured once for the application's -// entire lifecycle. -bool _loggerConfigured = false; - Handler middleware(Handler handler) { // This is the root middleware for the entire API. It's responsible for // providing all shared dependencies to the request context. // The order of `.use()` calls is important: the last one in the chain // runs first. - // This check ensures that the logger is configured only once. - if (!_loggerConfigured) { - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) { - // A more detailed logger that includes the error and stack trace. - // ignore: avoid_print - print( - '${record.level.name}: ${record.time}: ${record.loggerName}: ' - '${record.message}', - ); - if (record.error != null) { - // ignore: avoid_print - print(' ERROR: ${record.error}'); - } - if (record.stackTrace != null) { - // ignore: avoid_print - print(' STACK TRACE: ${record.stackTrace}'); - } - }); - _loggerConfigured = true; - } - return handler // --- Core Middleware --- // These run after all dependencies have been provided. From 7765ee0c5625ba91802ecfab5bb9dd0d8a6863c5 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:29:12 +0100 Subject: [PATCH 08/16] fix(server): correct entrypoint logic and linter warnings Removes the `await` from the `server.main()` call in the custom entrypoint, as the function returns `void`. Also removes redundant `// ignore: avoid_print` comments that were duplicated by a file-level ignore, resolving linter warnings. --- bin/main.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/main.dart b/bin/main.dart index d3c1e9d..493156f 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -26,17 +26,14 @@ Future main(List args) async { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { // A more detailed logger that includes the error and stack trace. - // ignore: avoid_print print( '${record.level.name}: ${record.time}: ${record.loggerName}: ' '${record.message}', ); if (record.error != null) { - // ignore: avoid_print print(' ERROR: ${record.error}'); } if (record.stackTrace != null) { - // ignore: avoid_print print(' STACK TRACE: ${record.stackTrace}'); } }); @@ -53,7 +50,9 @@ Future main(List args) async { log.info('EAGER_INIT: Starting Dart Frog server...'); // Only if initialization succeeds, start the Dart Frog server. - await server.main(); + // This function is void and handles its own async logic internally, + // so it should be called without `await`. + server.main(); } catch (e, s) { log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); exit(1); // Exit with a non-zero code to indicate failure. From 0864728fe8111b77831c24a5db09a647d206160f Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:51:03 +0100 Subject: [PATCH 09/16] feat(bin): implement hot reload for Dart Frog server - Integrate shelf_hotreload package for hot reload capability - Wrap server startup logic with withHotreload function - Preserve eager initialization of dependencies - Maintain "fail-fast" approach for production readiness - Update server startup to use createServer function from Dart Frog --- bin/main.dart | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/bin/main.dart b/bin/main.dart index 493156f..39b8aa9 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -1,11 +1,13 @@ // ignore_for_file: avoid_print import 'dart:io'; - +import 'package:dart_frog/dart_frog.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart'; import 'package:logging/logging.dart'; +import 'package:shelf_hotreload/shelf_hotreload.dart'; // Import the generated server entrypoint from the .dart_frog directory. +// This file contains the `createServer` function we need. // We use a prefix to avoid a name collision with our own `main` function. import '../.dart_frog/server.dart' as server; @@ -18,7 +20,7 @@ import '../.dart_frog/server.dart' as server; /// If any part of the dependency initialization fails (e.g., database /// connection, migrations), the process will log a fatal error and exit, /// preventing the server from running in a broken state. This is a robust, -/// "fail-fast" approach suitable for production environments. +/// "fail-fast" approach that is compatible with Dart Frog's hot reload. Future main(List args) async { // Use a local logger for startup-specific messages. // This is also the ideal place to configure the root logger for the entire @@ -40,21 +42,30 @@ Future main(List args) async { final log = Logger('EagerEntrypoint'); - try { - log.info('EAGER_INIT: Initializing application dependencies...'); + // This is our custom hot-reload-aware startup logic. + // The `withHotreload` function from `shelf_hotreload` (used by Dart Frog) + // takes a builder function that it calls whenever a reload is needed. + // We place our initialization logic inside this builder. + withHotreload( + () async { + try { + log.info('EAGER_INIT: Initializing application dependencies...'); - // Eagerly initialize all dependencies. If this fails, it will throw. - await AppDependencies.instance.init(); + // Eagerly initialize all dependencies. If this fails, it will throw. + await AppDependencies.instance.init(); - log.info('EAGER_INIT: Dependencies initialized successfully.'); - log.info('EAGER_INIT: Starting Dart Frog server...'); + log.info('EAGER_INIT: Dependencies initialized successfully.'); + log.info('EAGER_INIT: Starting Dart Frog server...'); - // Only if initialization succeeds, start the Dart Frog server. - // This function is void and handles its own async logic internally, - // so it should be called without `await`. - server.main(); - } catch (e, s) { - log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); - exit(1); // Exit with a non-zero code to indicate failure. - } + // Use the generated `createServer` function from Dart Frog. + final address = InternetAddress.anyIPv6; + const port = 8080; + return serve(server.buildRootHandler(), address, port); + } catch (e, s) { + log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); + // Exit the process if initialization fails. + exit(1); + } + }, + ); } From 7014237ea3bb23ed2faaabc1c7d02e4aca730ce1 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 07:57:00 +0100 Subject: [PATCH 10/16] refactor(loading): update eager loading description for Dart Frog compatibility - Update CHANGELOG.md to reflect the new hot reload compatibility of eager loading - Enhance the description to include Dart Frog's hot reload feature --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a626d62..4196a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## Upcoming Release -- **refactor!**: shift from lazy to eager loading for robust, fail-fast startup. +- **refactor!**: shift to eager loading for a robust, fail-fast startup that is fully compatible with Dart Frog's hot reload. ## 1.0.1 - 2025年10月17日 From 056fde1c04e4590cdfcc6645f6e0764564f418a0 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:08:42 +0100 Subject: [PATCH 11/16] refactor(server): remove unused reset logic from AppDependencies Removes the `reset()` static method and makes the `_instance` field `final` again. This logic was introduced to support hot reloading, which has since been removed from the custom entrypoint. This change cleans up the `AppDependencies` class, removing dead code and preventing confusion about the server's startup behavior. --- CHANGELOG.md | 2 +- bin/main.dart | 48 +++++++++++++++++++----------------------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4196a43..908d643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## Upcoming Release -- **refactor!**: shift to eager loading for a robust, fail-fast startup that is fully compatible with Dart Frog's hot reload. +- **refactor!**: shift to eager loading for a robust, fail-fast startup. ## 1.0.1 - 2025年10月17日 diff --git a/bin/main.dart b/bin/main.dart index 39b8aa9..8ffd888 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -2,13 +2,11 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; -import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart'; import 'package:logging/logging.dart'; -import 'package:shelf_hotreload/shelf_hotreload.dart'; -// Import the generated server entrypoint from the .dart_frog directory. -// This file contains the `createServer` function we need. -// We use a prefix to avoid a name collision with our own `main` function. +import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart'; + +// Import the generated server entrypoint to access `buildRootHandler`. import '../.dart_frog/server.dart' as server; /// The main entrypoint for the application. @@ -20,7 +18,7 @@ import '../.dart_frog/server.dart' as server; /// If any part of the dependency initialization fails (e.g., database /// connection, migrations), the process will log a fatal error and exit, /// preventing the server from running in a broken state. This is a robust, -/// "fail-fast" approach that is compatible with Dart Frog's hot reload. +/// "fail-fast" approach. Future main(List args) async { // Use a local logger for startup-specific messages. // This is also the ideal place to configure the root logger for the entire @@ -42,30 +40,22 @@ Future main(List args) async { final log = Logger('EagerEntrypoint'); - // This is our custom hot-reload-aware startup logic. - // The `withHotreload` function from `shelf_hotreload` (used by Dart Frog) - // takes a builder function that it calls whenever a reload is needed. - // We place our initialization logic inside this builder. - withHotreload( - () async { - try { - log.info('EAGER_INIT: Initializing application dependencies...'); + try { + log.info('EAGER_INIT: Initializing application dependencies...'); - // Eagerly initialize all dependencies. If this fails, it will throw. - await AppDependencies.instance.init(); + // Eagerly initialize all dependencies. If this fails, it will throw. + await AppDependencies.instance.init(); - log.info('EAGER_INIT: Dependencies initialized successfully.'); - log.info('EAGER_INIT: Starting Dart Frog server...'); + log.info('EAGER_INIT: Dependencies initialized successfully.'); + log.info('EAGER_INIT: Starting Dart Frog server...'); - // Use the generated `createServer` function from Dart Frog. - final address = InternetAddress.anyIPv6; - const port = 8080; - return serve(server.buildRootHandler(), address, port); - } catch (e, s) { - log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); - // Exit the process if initialization fails. - exit(1); - } - }, - ); + // Start the server directly without the hot reload wrapper. + final address = InternetAddress.anyIPv6; + final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080; + await serve(server.buildRootHandler(), address, port); + } catch (e, s) { + log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); + // Exit the process if initialization fails. + exit(1); + } } From 758765d6d387f2b06770ee0ba7e691cf465327e2 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:26:15 +0100 Subject: [PATCH 12/16] style: format --- bin/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/main.dart b/bin/main.dart index 8ffd888..e17331c 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -1,10 +1,10 @@ // ignore_for_file: avoid_print import 'dart:io'; -import 'package:dart_frog/dart_frog.dart'; -import 'package:logging/logging.dart'; +import 'package:dart_frog/dart_frog.dart'; import 'package:flutter_news_app_api_server_full_source_code/src/config/app_dependencies.dart'; +import 'package:logging/logging.dart'; // Import the generated server entrypoint to access `buildRootHandler`. import '../.dart_frog/server.dart' as server; From a46581e55d093a72f6f2902ebf33f3fa1bbc8225 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:34:24 +0100 Subject: [PATCH 13/16] feat(server): implement graceful shutdown and atomic logging Implements two key production-readiness features based on code review: 1. **Graceful Shutdown:** The `main.dart` entrypoint now listens for `SIGINT` and `SIGTERM` signals. Upon receiving a signal, it closes the HTTP server to stop accepting new connections, disposes of all application dependencies (like the database connection), and then exits cleanly. This prevents resource leaks. 2. **Atomic Logging:** The logger configuration in `main.dart` has been refactored to use a `StringBuffer` and a single `stdout.write()` call. This ensures that multi-line log entries are written to the console atomically, preventing interleaved output and improving log readability. A defensive check has also been added to `AppDependencies.dispose` to ensure it can be called safely even if initialization failed. --- bin/main.dart | 39 +++++++++++++++++++++------- lib/src/config/app_dependencies.dart | 8 +++++- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/bin/main.dart b/bin/main.dart index e17331c..ba9b32d 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_print +import 'dart:async'; import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; @@ -7,7 +8,7 @@ import 'package:flutter_news_app_api_server_full_source_code/src/config/app_depe import 'package:logging/logging.dart'; // Import the generated server entrypoint to access `buildRootHandler`. -import '../.dart_frog/server.dart' as server; +import '../.dart_frog/server.dart' as dart_frog; /// The main entrypoint for the application. /// @@ -25,20 +26,37 @@ Future main(List args) async { // application, as it's guaranteed to run only once at startup. Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { - // A more detailed logger that includes the error and stack trace. - print( - '${record.level.name}: ${record.time}: ${record.loggerName}: ' - '${record.message}', - ); + final message = StringBuffer() + ..write('${record.level.name}: ${record.time}: ${record.loggerName}: ') + ..writeln(record.message); + if (record.error != null) { - print(' ERROR: ${record.error}'); + message.writeln(' ERROR: ${record.error}'); } if (record.stackTrace != null) { - print(' STACK TRACE: ${record.stackTrace}'); + message.writeln(' STACK TRACE: ${record.stackTrace}'); } + + // Write the log message atomically to stdout. + stdout.write(message.toString()); }); final log = Logger('EagerEntrypoint'); + HttpServer? server; + + Future shutdown([String? signal]) async { + log.info('Received ${signal ?? 'signal'}. Shutting down gracefully...'); + // Stop accepting new connections. + await server?.close(); + // Dispose all application dependencies. + await AppDependencies.instance.dispose(); + log.info('Shutdown complete.'); + exit(0); + } + + // Listen for termination signals. + ProcessSignal.sigint.watch().listen((_) => shutdown('SIGINT')); + ProcessSignal.sigterm.watch().listen((_) => shutdown('SIGTERM')); try { log.info('EAGER_INIT: Initializing application dependencies...'); @@ -52,7 +70,10 @@ Future main(List args) async { // Start the server directly without the hot reload wrapper. final address = InternetAddress.anyIPv6; final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080; - await serve(server.buildRootHandler(), address, port); + server = await serve(dart_frog.buildRootHandler(), address, port); + log.info( + 'Server listening on http://${server.address.host}:${server.port}', + ); } catch (e, s) { log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); // Exit the process if initialization fails. diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 1cb95f3..828b9d2 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -42,6 +42,9 @@ class AppDependencies { final _log = Logger('AppDependencies'); + // A flag to track if initialization has started, for safe disposal. + bool _initStarted = false; + // --- Late-initialized fields for all dependencies --- // Database @@ -80,6 +83,7 @@ class AppDependencies { /// exception if any part of the initialization fails, which will be caught /// by the entrypoint to terminate the server process. Future init() async { + _initStarted = true; _log.info('Initializing application dependencies...'); // 1. Initialize Database Connection @@ -268,7 +272,9 @@ class AppDependencies { /// Disposes of resources, such as closing the database connection. Future dispose() async { - await _mongoDbConnectionManager.close(); + if (_initStarted) { + await _mongoDbConnectionManager.close(); + } tokenBlacklistService.dispose(); rateLimitService.dispose(); countryQueryService.dispose(); // Dispose the new service From 38a3a5d991616a3661193a87bc5cae93c4fabc0d Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:40:24 +0100 Subject: [PATCH 14/16] fix(bin): handle SIGTERM signal support on non-Windows platforms - Add a platform check before listening for SIGTERM signal - Prevent errors on Windows platforms that don't support SIGTERM --- bin/main.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/main.dart b/bin/main.dart index ba9b32d..297a281 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -56,7 +56,10 @@ Future main(List args) async { // Listen for termination signals. ProcessSignal.sigint.watch().listen((_) => shutdown('SIGINT')); - ProcessSignal.sigterm.watch().listen((_) => shutdown('SIGTERM')); + // SIGTERM is not supported on Windows. Attempting to listen to it will throw. + if (!Platform.isWindows) { + ProcessSignal.sigterm.watch().listen((_) => shutdown('SIGTERM')); + } try { log.info('EAGER_INIT: Initializing application dependencies...'); From a588d987777d71c6b9c927fd69c5b8878ca3aa08 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:47:35 +0100 Subject: [PATCH 15/16] fix(server): resolve handler type ambiguity in entrypoint Fixes a type error where `buildRootHandler()` was being inferred as `dynamic` instead of `Handler`. The fix explicitly casts the result to `Handler` before passing it to the `serve` function, resolving the static analysis error and ensuring type safety. --- bin/main.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/main.dart b/bin/main.dart index 297a281..38712e7 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -73,7 +73,10 @@ Future main(List args) async { // Start the server directly without the hot reload wrapper. final address = InternetAddress.anyIPv6; final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080; - server = await serve(dart_frog.buildRootHandler(), address, port); + + // Explicitly cast the handler to resolve the type ambiguity. + final handler = dart_frog.buildRootHandler() as Handler; + server = await serve(handler, address, port); log.info( 'Server listening on http://${server.address.host}:${server.port}', ); From 301c0e45e8c1434465fa2f69376f05841e7c4347 Mon Sep 17 00:00:00 2001 From: fulleni Date: 2025年10月19日 08:49:12 +0100 Subject: [PATCH 16/16] fix(server): ensure fatal startup logs are captured before exit Replaces the asynchronous `log.severe` call in the main catch block with a direct, synchronous write to `stderr`, followed by `stderr.flush()`. This fixes a race condition where the process could exit before the logger had a chance to write the fatal error message, ensuring that critical startup failure information is never lost. --- bin/main.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/main.dart b/bin/main.dart index 38712e7..ab59661 100644 --- a/bin/main.dart +++ b/bin/main.dart @@ -82,6 +82,10 @@ Future main(List args) async { ); } catch (e, s) { log.severe('EAGER_INIT: FATAL: Failed to start server.', e, s); + // Log directly to stderr and flush to ensure the message is captured + // before the process exits, which is crucial for debugging startup errors. + stderr.writeln('EAGER_INIT: FATAL: Failed to start server. Error: $e\nStack Trace: $s'); + await stderr.flush(); // Exit the process if initialization fails. exit(1); }

AltStyle によって変換されたページ (->オリジナル) /