From 29be6a67e65f8965406cbb23ab9d6d0a9850fcd4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 11:04:41 +0000 Subject: [PATCH] fix(cdn): defer artifact refresh UI until app is foregrounded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2252 fixed the appBundle/translation ordering race on resume, but _refreshIfStale() began calling _handlePendingUpdate() whenever config was set—even when the refresh was triggered from a paused/inactive lifecycle callback. That contradicts the background fetch contract ("update cache but DON'T fire events") and can leave translations stale: _refreshTranslationsAtRuntime() skips when no BuildContext exists, yet _fireManifestRefreshEvent() still runs and clears _hasPendingUpdate, so resume never retries i18n refresh. Only deliver refresh events immediately when lifecycle is resumed (or unknown during cold start). Otherwise set _hasPendingUpdate for the existing resume handler. Co-authored-by: Sharjeel Yunus --- .../definition_providers/cdn_provider.dart | 34 ++++++++++++++----- modules/ensemble/test/cdn_provider_test.dart | 29 ++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart index 5ce6c2e7d..c05c8b1cf 100644 --- a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart @@ -488,6 +488,15 @@ class CdnDefinitionProvider extends DefinitionProvider { } } + /// Whether a CDN artifact refresh should update appBundle and fire UI events + /// immediately. Background/inactive lifecycle must defer until [resumed] so + /// [_handlePendingUpdate] runs when a BuildContext exists for i18n refresh. + @visibleForTesting + bool shouldDeliverArtifactRefreshImmediately() => + cdnShouldDeliverArtifactRefreshImmediately( + WidgetsBinding.instance.lifecycleState, + ); + /// Check for updates and update cache if available /// Sets _hasPendingUpdate flag if updates were fetched Future _refreshIfStale() async { @@ -513,15 +522,16 @@ class CdnDefinitionProvider extends DefinitionProvider { // Save to persistent cache await _saveCachedState(jsonString); - // If artifact refresh is enabled and app is already initialized, - // immediately update appBundle and fire refresh event. - // This handles the cold start scenario where background refresh - // completes after initial render. - if (isArtifactRefreshEnabled() && Ensemble().getConfig() != null) { - await _handlePendingUpdate(); - } else { - // Mark for later refresh on next resume - _hasPendingUpdate = true; + // Foreground/cold-start: sync appBundle + translations before refresh event. + // Background: defer until resume (see onAppLifecycleStateChanged) so i18n + // refresh is not skipped when context is unavailable. + if (isArtifactRefreshEnabled()) { + if (shouldDeliverArtifactRefreshImmediately() && + Ensemble().getConfig() != null) { + await _handlePendingUpdate(); + } else { + _hasPendingUpdate = true; + } } } catch (e) { if (kDebugMode) { @@ -947,3 +957,9 @@ class CdnDefinitionProvider extends DefinitionProvider { } } } + +/// True when CDN artifact refresh events may be delivered immediately. +@visibleForTesting +bool cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState? state) { + return state == null || state == AppLifecycleState.resumed; +} diff --git a/modules/ensemble/test/cdn_provider_test.dart b/modules/ensemble/test/cdn_provider_test.dart index 97d3df78d..5550df70a 100644 --- a/modules/ensemble/test/cdn_provider_test.dart +++ b/modules/ensemble/test/cdn_provider_test.dart @@ -9,6 +9,35 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { + group('cdnShouldDeliverArtifactRefreshImmediately', () { + test('allows delivery when lifecycle is unknown or resumed', () { + expect(cdnShouldDeliverArtifactRefreshImmediately(null), isTrue); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.resumed), + isTrue, + ); + }); + + test('defers delivery while app is backgrounded or inactive', () { + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.paused), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.inactive), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.hidden), + isFalse, + ); + expect( + cdnShouldDeliverArtifactRefreshImmediately(AppLifecycleState.detached), + isFalse, + ); + }); + }); + group('CDN cache invalidation', () { test('resets freshness metadata when persisted manifest is invalid', () async {