Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 2 additions & 3 deletions modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,8 @@ class NavigateViewGroupAction extends EnsembleAction {
} else if (viewIndex != null) {
final pageGroup = context.findAncestorWidgetOfExactType<PageGroup>();
final menuLen = pageGroup?.menu.menuItems.length ?? 0;
final resolvedIndex = menuLen > 0
? safeViewGroupPayloadIndex(viewIndex, menuLen)
: viewIndex;
final resolvedIndex =
resolveNavigateViewGroupTabIndex(viewIndex, menuLen);
if (payload != null) {
// TODO: this is wrong. Can't mutate the scope like this
scopeManager.dataContext.addDataContext(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,26 @@ class CdnDefinitionProvider extends DefinitionProvider {
}
}

/// Whether a stale CDN refresh should apply immediately (cold start after init)
/// instead of deferring until the next app resume.
@visibleForTesting
bool shouldApplyCdnStaleRefreshImmediately({
required bool artifactRefreshEnabled,
required bool hasEnsembleConfig,
}) =>
artifactRefreshEnabled && hasEnsembleConfig;

Future<void> _applyStaleRefreshOutcome() async {
if (shouldApplyCdnStaleRefreshImmediately(
artifactRefreshEnabled: isArtifactRefreshEnabled(),
hasEnsembleConfig: Ensemble().getConfig() != null,
)) {
await _handlePendingUpdate();
} else {
_hasPendingUpdate = true;
}
}

/// Handles pending CDN updates when app resumes.
/// CRITICAL: Must update appBundle and translations BEFORE firing refresh event
/// to ensure screens rebuild with the new resources (fixes race condition).
Expand Down Expand Up @@ -513,16 +533,7 @@ 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;
}
await _applyStaleRefreshOutcome();
} catch (e) {
if (kDebugMode) {
debugPrint('⚠️ CDN Provider: Refresh failed: $e');
Expand Down Expand Up @@ -925,6 +936,10 @@ class CdnDefinitionProvider extends DefinitionProvider {
@visibleForTesting
Future<void> handlePendingUpdateForTesting() => _handlePendingUpdate();

@visibleForTesting
Future<void> applyStaleRefreshOutcomeForTesting() =>
_applyStaleRefreshOutcome();

@visibleForTesting
bool get hasPendingUpdateForTesting => _hasPendingUpdate;

Expand Down
7 changes: 7 additions & 0 deletions modules/ensemble/lib/framework/view/page_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,13 @@ int safeViewGroupPayloadIndex(int index, int payloadLength) {
return index;
}

/// Resolves the tab index for [NavigateViewGroupAction] before
/// [PageController.jumpToPage]. Clamps when the menu has items; otherwise
/// preserves [viewIndex] (the [PageGroup] ancestor may not be mounted yet).
@visibleForTesting
int resolveNavigateViewGroupTabIndex(int viewIndex, int menuLen) =>
menuLen > 0 ? safeViewGroupPayloadIndex(viewIndex, menuLen) : viewIndex;

class ViewGroupNotifier extends ChangeNotifier {
int _viewIndex = 0;
Map<String, dynamic>? _payload;
Expand Down
75 changes: 75 additions & 0 deletions modules/ensemble/test/cdn_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,81 @@ void main() {
});
});

group('CDN stale refresh outcome', () {
test('shouldApplyCdnStaleRefreshImmediately requires both flags', () {
final provider = CdnDefinitionProvider('test-app');

expect(
provider.shouldApplyCdnStaleRefreshImmediately(
artifactRefreshEnabled: true,
hasEnsembleConfig: true,
),
isTrue,
);
expect(
provider.shouldApplyCdnStaleRefreshImmediately(
artifactRefreshEnabled: false,
hasEnsembleConfig: true,
),
isFalse,
);
expect(
provider.shouldApplyCdnStaleRefreshImmediately(
artifactRefreshEnabled: true,
hasEnsembleConfig: false,
),
isFalse,
);
});

test(
'applyStaleRefreshOutcome syncs bundle immediately when refresh enabled',
() async {
final provider = CdnDefinitionProvider('test-app');
final config = EnsembleConfig(definitionProvider: provider);
Ensemble().setEnsembleConfig(config);

await provider.applyRuntimeManifestForTesting(
_manifestWithArtifactRefresh(_manifestWithResourceVersion('v1')),
);
await config.updateAppBundle();

provider.rebuildManifestCacheForTesting(
_manifestWithArtifactRefresh(_manifestWithResourceVersion('v2')),
);

await provider.applyStaleRefreshOutcomeForTesting();

expect(provider.hasPendingUpdateForTesting, isFalse);
expect(
config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'],
'v2',
);
});

test('applyStaleRefreshOutcome defers when artifact refresh is disabled',
() async {
final provider = CdnDefinitionProvider('test-app');
final config = EnsembleConfig(definitionProvider: provider);
Ensemble().setEnsembleConfig(config);

await provider.applyRuntimeManifestForTesting(
_manifestWithResourceVersion('v1'),
);
await config.updateAppBundle();

provider.rebuildManifestCacheForTesting(_manifestWithResourceVersion('v2'));

await provider.applyStaleRefreshOutcomeForTesting();

expect(provider.hasPendingUpdateForTesting, isTrue);
expect(
config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'],
'v1',
);
});
});

group('CDN pending update ordering', () {
test('handlePendingUpdate syncs app bundle from CDN cache and fires refresh',
() async {
Expand Down
13 changes: 13 additions & 0 deletions modules/ensemble/test/safe_view_group_payload_index_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,17 @@ void main() {
expect(safeViewGroupPayloadIndex(5, 0), 0);
});
});

group('resolveNavigateViewGroupTabIndex', () {
test('clamps out-of-range indices when the menu has items', () {
expect(resolveNavigateViewGroupTabIndex(5, 3), 2);
expect(resolveNavigateViewGroupTabIndex(-1, 3), 0);
expect(resolveNavigateViewGroupTabIndex(1, 3), 1);
});

test('preserves index when menu length is zero', () {
expect(resolveNavigateViewGroupTabIndex(5, 0), 5);
expect(resolveNavigateViewGroupTabIndex(-1, 0), -1);
});
});
}
Loading