Skip to content

Commit d3bdc02

Browse files
Doug Borgclaude
authored andcommitted
Parallelize independent init steps
Restore Future.wait() for independent init steps. Each step is wrapped in _guardInit() try-catch so a failure in one doesn't prevent others from completing. Settings init remains sequential (required by all steps). Race condition analysis: safe in Dart's single-threaded event loop. Each step operates on independent state, SharedPrefs keys don't overlap, and additive cache writes don't conflict. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9371ac7 commit d3bdc02

1 file changed

Lines changed: 60 additions & 39 deletions

File tree

lib/app_state.dart

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -213,51 +213,26 @@ class AppState extends ChangeNotifier {
213213
// Settings must init first — other modules read its values
214214
await _settingsState.init();
215215

216-
// Initialize changelog service
217-
await ChangelogService().init();
218-
219216
// Fire-and-forget tile preview fetch (existing pattern)
220217
_fetchMissingTilePreviews();
221218

222-
// Check if we should add default profiles (first launch OR no profiles of each type exist)
223-
const firstLaunchKey = 'profiles_defaults_initialized';
224-
final prefsForProfiles = await SharedPreferences.getInstance();
225-
final isFirstLaunch = !(prefsForProfiles.getBool(firstLaunchKey) ?? false);
226-
227-
final existingOperatorProfiles = await OperatorProfileService().load();
228-
final existingNodeProfiles = await ProfileService().load();
229-
230-
final shouldAddOperatorDefaults = isFirstLaunch || existingOperatorProfiles.isEmpty;
231-
final shouldAddNodeDefaults = isFirstLaunch || existingNodeProfiles.isEmpty;
232-
233-
await _operatorProfileState.init(addDefaults: shouldAddOperatorDefaults);
234-
await _profileState.init(addDefaults: shouldAddNodeDefaults);
235-
236-
// Set up callback to clear stale sessions when profiles are deleted
219+
// Run independent init steps in parallel.
220+
// Each step is wrapped in try-catch so a failure in one doesn't
221+
// prevent the others from completing via Future.wait.
222+
await Future.wait([
223+
_guardInit('changelog', () => ChangelogService().init()),
224+
_initProfiles(),
225+
_guardInit('suspected locations', () => _suspectedLocationState.initLocal()),
226+
_guardInit('upload queue', () => _uploadQueueState.init()),
227+
_guardInit('auth', () => _authState.init(_settingsState.uploadMode)),
228+
_initOfflineData(),
229+
]);
230+
231+
// Post-parallel setup (depends on results above)
237232
_profileState.setProfileDeletedCallback(_onProfileDeleted);
238-
239-
if (isFirstLaunch) {
240-
await prefsForProfiles.setBool(firstLaunchKey, true);
241-
}
242-
243-
// Local-only init for suspected locations (no network)
244-
await _suspectedLocationState.initLocal();
245-
await _uploadQueueState.init();
246-
// Local-only auth init (no network)
247-
await _authState.init(_settingsState.uploadMode);
248-
249-
// Set up callback to repopulate pending nodes after cache clears
250233
NodeProviderWithCache.instance.setOnCacheClearedCallback(() {
251234
_uploadQueueState.repopulateCacheFromQueue();
252235
});
253-
254-
// Initialize OfflineAreaService to ensure offline areas are loaded
255-
await OfflineAreaService().ensureInitialized();
256-
257-
// Preload offline nodes into cache for immediate display
258-
await NodeDataManager().preloadOfflineNodes();
259-
260-
// Start uploader if conditions are met
261236
_startUploader();
262237

263238
_isInitialized = true;
@@ -288,9 +263,55 @@ class AppState extends ChangeNotifier {
288263
}
289264
}
290265

266+
/// Load profiles and handle first-launch defaults.
267+
Future<void> _initProfiles() async {
268+
try {
269+
final prefs = await SharedPreferences.getInstance();
270+
const firstLaunchKey = 'profiles_defaults_initialized';
271+
final isFirstLaunch = !(prefs.getBool(firstLaunchKey) ?? false);
272+
273+
final existingOperatorProfiles = await OperatorProfileService().load();
274+
final existingNodeProfiles = await ProfileService().load();
275+
276+
final shouldAddOperatorDefaults = isFirstLaunch || existingOperatorProfiles.isEmpty;
277+
final shouldAddNodeDefaults = isFirstLaunch || existingNodeProfiles.isEmpty;
278+
279+
await _operatorProfileState.init(addDefaults: shouldAddOperatorDefaults);
280+
await _profileState.init(addDefaults: shouldAddNodeDefaults);
281+
282+
if (isFirstLaunch) {
283+
await prefs.setBool(firstLaunchKey, true);
284+
}
285+
} catch (e, stackTrace) {
286+
debugPrint('[AppState] Error initializing profiles: $e');
287+
debugPrint('[AppState] Stack trace: $stackTrace');
288+
}
289+
}
290+
291+
/// Initialize offline area service and preload offline nodes.
292+
Future<void> _initOfflineData() async {
293+
try {
294+
await OfflineAreaService().ensureInitialized();
295+
await NodeDataManager().preloadOfflineNodes();
296+
} catch (e, stackTrace) {
297+
debugPrint('[AppState] Error initializing offline data: $e');
298+
debugPrint('[AppState] Stack trace: $stackTrace');
299+
}
300+
}
301+
302+
/// Run an init step inside try-catch so it can't break Future.wait.
303+
Future<void> _guardInit(String label, Future<void> Function() fn) async {
304+
try {
305+
await fn();
306+
} catch (e, stackTrace) {
307+
debugPrint('[AppState] Error initializing $label: $e');
308+
debugPrint('[AppState] Stack trace: $stackTrace');
309+
}
310+
}
311+
291312
void _startMessageCheckTimer() {
292313
_messageCheckTimer?.cancel();
293-
314+
294315
// Check messages every 10 minutes when logged in
295316
_messageCheckTimer = Timer.periodic(
296317
const Duration(minutes: 10),

0 commit comments

Comments
 (0)