Skip to content

Commit 9860121

Browse files
committed
fix(api): implement correct server entrypoint with initialization gate
Refactors the server startup logic into a custom `run` function in `lib/src/config/server.dart` to align with the `dart_frog dev` lifecycle. This change resolves a `LateInitializationError` caused by a race condition where requests could be processed before asynchronous dependency setup was complete. - All async setup (DB connection, seeding, service creation) is now completed within the `run` function. - A `Completer`-based "initialization gate" middleware is used to hold all incoming requests until the server is fully initialized and the `DependencyContainer` is populated. - Deletes the unused `bin/server.dart` file.
1 parent 60d9563 commit 9860121

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed

lib/src/config/server.dart

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:dart_frog/dart_frog.dart';
5+
import 'package:ht_api/src/config/dependency_container.dart';
6+
import 'package:ht_api/src/config/environment_config.dart';
7+
import 'package:ht_api/src/rbac/permission_service.dart';
8+
import 'package:ht_api/src/services/auth_service.dart';
9+
import 'package:ht_api/src/services/auth_token_service.dart';
10+
import 'package:ht_api/src/services/dashboard_summary_service.dart';
11+
import 'package:ht_api/src/services/database_seeding_service.dart';
12+
import 'package:ht_api/src/services/default_user_preference_limit_service.dart';
13+
import 'package:ht_api/src/services/jwt_auth_token_service.dart';
14+
import 'package:ht_api/src/services/token_blacklist_service.dart';
15+
import 'package:ht_api/src/services/user_preference_limit_service.dart';
16+
import 'package:ht_api/src/services/verification_code_storage_service.dart';
17+
import 'package:ht_data_client/ht_data_client.dart';
18+
import 'package:ht_data_postgres/ht_data_postgres.dart';
19+
import 'package:ht_data_repository/ht_data_repository.dart';
20+
import 'package:ht_email_inmemory/ht_email_inmemory.dart';
21+
import 'package:ht_email_repository/ht_email_repository.dart';
22+
import 'package:ht_shared/ht_shared.dart';
23+
import 'package:logging/logging.dart';
24+
import 'package:postgres/postgres.dart';
25+
import 'package:uuid/uuid.dart';
26+
27+
/// Global logger instance.
28+
final _log = Logger('ht_api');
29+
30+
/// Global PostgreSQL connection instance.
31+
late final Connection _connection;
32+
33+
/// Creates a data repository for a given type [T].
34+
HtDataRepository<T> _createRepository<T>({
35+
required String tableName,
36+
required FromJson<T> fromJson,
37+
required ToJson<T> toJson,
38+
}) {
39+
return HtDataRepository<T>(
40+
dataClient: HtDataPostgresClient<T>(
41+
connection: _connection,
42+
tableName: tableName,
43+
fromJson: fromJson,
44+
toJson: toJson,
45+
log: _log,
46+
),
47+
);
48+
}
49+
50+
/// The main entry point for the server, used by `dart_frog dev`.
51+
///
52+
/// This function is responsible for the entire server startup sequence:
53+
/// 1. **Gating Requests:** It immediately sets up a "gate" using a `Completer`
54+
/// to hold all incoming requests until initialization is complete.
55+
/// 2. **Async Initialization:** It performs all necessary asynchronous setup,
56+
/// including logging, database connection, and data seeding.
57+
/// 3. **Dependency Injection:** It initializes all repositories and services
58+
/// and populates the `DependencyContainer`.
59+
/// 4. **Server Start:** It starts the HTTP server with the gated handler.
60+
/// 5. **Opening the Gate:** Once the server is listening, it completes the
61+
/// `Completer`, allowing the gated requests to be processed.
62+
/// 6. **Graceful Shutdown:** It sets up a listener for `SIGINT` to close
63+
/// resources gracefully.
64+
Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
65+
final initCompleter = Completer<void>();
66+
67+
// This middleware wraps the main handler. It awaits the completer's future,
68+
// effectively pausing the request until `initCompleter.complete()` is called.
69+
final gatedHandler = handler.use(
70+
(innerHandler) {
71+
return (context) async {
72+
await initCompleter.future;
73+
return innerHandler(context);
74+
};
75+
},
76+
);
77+
78+
// 1. Setup Logger
79+
Logger.root.level = Level.ALL;
80+
Logger.root.onRecord.listen((record) {
81+
// ignore: avoid_print
82+
print(
83+
'${record.level.name}: ${record.time}: '
84+
'${record.loggerName}: ${record.message}',
85+
);
86+
});
87+
88+
// 2. Establish Database Connection
89+
_log.info('Connecting to PostgreSQL database...');
90+
final dbUri = Uri.parse(EnvironmentConfig.databaseUrl);
91+
String? username;
92+
String? password;
93+
if (dbUri.userInfo.isNotEmpty) {
94+
final parts = dbUri.userInfo.split(':');
95+
username = Uri.decodeComponent(parts.first);
96+
if (parts.length > 1) {
97+
password = Uri.decodeComponent(parts.last);
98+
}
99+
}
100+
101+
_connection = await Connection.open(
102+
Endpoint(
103+
host: dbUri.host,
104+
port: dbUri.port,
105+
database: dbUri.path.substring(1), // Remove leading '/'
106+
username: username,
107+
password: password,
108+
),
109+
settings: const ConnectionSettings(sslMode: SslMode.require),
110+
);
111+
_log.info('PostgreSQL database connection established.');
112+
113+
// 3. Initialize and run database seeding
114+
final seedingService = DatabaseSeedingService(
115+
connection: _connection,
116+
log: _log,
117+
);
118+
await seedingService.createTables();
119+
await seedingService.seedGlobalFixtureData();
120+
await seedingService.seedInitialAdminAndConfig();
121+
122+
// 4. Initialize Repositories
123+
final headlineRepository = _createRepository<Headline>(
124+
tableName: 'headlines',
125+
fromJson: Headline.fromJson,
126+
toJson: (h) => h.toJson(),
127+
);
128+
final categoryRepository = _createRepository<Category>(
129+
tableName: 'categories',
130+
fromJson: Category.fromJson,
131+
toJson: (c) => c.toJson(),
132+
);
133+
final sourceRepository = _createRepository<Source>(
134+
tableName: 'sources',
135+
fromJson: Source.fromJson,
136+
toJson: (s) => s.toJson(),
137+
);
138+
final countryRepository = _createRepository<Country>(
139+
tableName: 'countries',
140+
fromJson: Country.fromJson,
141+
toJson: (c) => c.toJson(),
142+
);
143+
final userRepository = _createRepository<User>(
144+
tableName: 'users',
145+
fromJson: User.fromJson,
146+
toJson: (u) => u.toJson(),
147+
);
148+
final userAppSettingsRepository = _createRepository<UserAppSettings>(
149+
tableName: 'user_app_settings',
150+
fromJson: UserAppSettings.fromJson,
151+
toJson: (s) => s.toJson(),
152+
);
153+
final userContentPreferencesRepository =
154+
_createRepository<UserContentPreferences>(
155+
tableName: 'user_content_preferences',
156+
fromJson: UserContentPreferences.fromJson,
157+
toJson: (p) => p.toJson(),
158+
);
159+
final appConfigRepository = _createRepository<AppConfig>(
160+
tableName: 'app_config',
161+
fromJson: AppConfig.fromJson,
162+
toJson: (c) => c.toJson(),
163+
);
164+
165+
// 5. Initialize Services
166+
const emailRepository = HtEmailRepository(
167+
emailClient: HtEmailInMemoryClient(),
168+
);
169+
final tokenBlacklistService = InMemoryTokenBlacklistService();
170+
final authTokenService = JwtAuthTokenService(
171+
userRepository: userRepository,
172+
blacklistService: tokenBlacklistService,
173+
uuidGenerator: const Uuid(),
174+
);
175+
final verificationCodeStorageService =
176+
InMemoryVerificationCodeStorageService();
177+
final authService = AuthService(
178+
userRepository: userRepository,
179+
authTokenService: authTokenService,
180+
verificationCodeStorageService: verificationCodeStorageService,
181+
emailRepository: emailRepository,
182+
userAppSettingsRepository: userAppSettingsRepository,
183+
userContentPreferencesRepository: userContentPreferencesRepository,
184+
uuidGenerator: const Uuid(),
185+
);
186+
final dashboardSummaryService = DashboardSummaryService(
187+
headlineRepository: headlineRepository,
188+
categoryRepository: categoryRepository,
189+
sourceRepository: sourceRepository,
190+
);
191+
const permissionService = PermissionService();
192+
final userPreferenceLimitService = DefaultUserPreferenceLimitService(
193+
appConfigRepository: appConfigRepository,
194+
);
195+
196+
// 6. Populate the DependencyContainer
197+
DependencyContainer.instance.init(
198+
headlineRepository: headlineRepository,
199+
categoryRepository: categoryRepository,
200+
sourceRepository: sourceRepository,
201+
countryRepository: countryRepository,
202+
userRepository: userRepository,
203+
userAppSettingsRepository: userAppSettingsRepository,
204+
userContentPreferencesRepository: userContentPreferencesRepository,
205+
appConfigRepository: appConfigRepository,
206+
emailRepository: emailRepository,
207+
tokenBlacklistService: tokenBlacklistService,
208+
authTokenService: authTokenService,
209+
verificationCodeStorageService: verificationCodeStorageService,
210+
authService: authService,
211+
dashboardSummaryService: dashboardSummaryService,
212+
permissionService: permissionService,
213+
userPreferenceLimitService: userPreferenceLimitService,
214+
);
215+
216+
// 7. Start the server with the gated handler
217+
final server = await serve(gatedHandler, ip, port);
218+
_log.info('Server listening on port ${server.port}');
219+
220+
// 8. Open the gate now that the server is ready.
221+
initCompleter.complete();
222+
223+
// 9. Handle graceful shutdown
224+
ProcessSignal.sigint.watch().listen((_) async {
225+
_log.info('Received SIGINT. Shutting down...');
226+
await _connection.close();
227+
_log.info('Database connection closed.');
228+
await server.close(force: true);
229+
_log.info('Server shut down.');
230+
exit(0);
231+
});
232+
233+
return server;
234+
}

0 commit comments

Comments
 (0)