Add feature flag to switch between HTTP and gRPC transports

Adds ApiModeConfig class with support for selecting API transport mode:
- ApiMode enum (http, grpc)
- Static configurations: development (HTTP), developmentGrpc, production, productionGrpc
- fallbackToHttpOnError option for graceful degradation

Creates unified deliveryRoutesProvider and deliveriesProvider that:
- Respect apiModeConfigProvider for transport selection
- Automatically delegate to gRPC or HTTP providers
- Fall back to HTTP on gRPC failures when configured

To enable gRPC, override apiModeConfigProvider with ApiModeConfig.developmentGrpc
in the ProviderScope.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan 2026-01-20 13:11:58 -05:00
parent 4a9377e0a9
commit a60f92c56d

View File

@ -11,6 +11,100 @@ import '../models/user_profile.dart';
import '../models/delivery_route.dart';
import '../models/delivery.dart';
// ============================================================================
// API Mode Configuration - Feature Flag for HTTP/gRPC Transport
// ============================================================================
/// Enum representing the available API transport modes.
enum ApiMode {
/// Use HTTP/REST-based CQRS API client
http,
/// Use gRPC-based CQRS API client
grpc,
}
/// Configuration for API transport mode selection.
///
/// This class allows switching between HTTP and gRPC transports
/// for API calls. Following the pattern from [ApiClientConfig].
///
/// Example usage:
/// ```dart
/// // To switch to gRPC, override the apiModeConfigProvider:
/// ProviderScope(
/// overrides: [
/// apiModeConfigProvider.overrideWithValue(ApiModeConfig.developmentGrpc),
/// ],
/// child: MyApp(),
/// )
/// ```
class ApiModeConfig {
/// The transport mode to use for API calls.
final ApiMode mode;
/// Whether to fall back to HTTP on gRPC failures.
/// Only applicable when [mode] is [ApiMode.grpc].
final bool fallbackToHttpOnError;
const ApiModeConfig({
required this.mode,
this.fallbackToHttpOnError = true,
});
/// Development configuration - defaults to HTTP for stability.
/// Use this for safe development when gRPC backend may be unavailable.
static const ApiModeConfig development = ApiModeConfig(
mode: ApiMode.http,
fallbackToHttpOnError: true,
);
/// gRPC-first development configuration for testing gRPC integration.
/// Use this when actively developing/testing gRPC functionality.
static const ApiModeConfig developmentGrpc = ApiModeConfig(
mode: ApiMode.grpc,
fallbackToHttpOnError: true,
);
/// Production configuration - uses HTTP until gRPC is verified stable.
static const ApiModeConfig production = ApiModeConfig(
mode: ApiMode.http,
fallbackToHttpOnError: false,
);
/// Production gRPC configuration for when gRPC is production-ready.
static const ApiModeConfig productionGrpc = ApiModeConfig(
mode: ApiMode.grpc,
fallbackToHttpOnError: false,
);
/// Whether the current mode is gRPC.
bool get isGrpc => mode == ApiMode.grpc;
/// Whether the current mode is HTTP.
bool get isHttp => mode == ApiMode.http;
}
/// Provider for API mode configuration.
///
/// Override this provider to switch between HTTP and gRPC:
/// ```dart
/// ProviderScope(
/// overrides: [
/// apiModeConfigProvider.overrideWithValue(ApiModeConfig.developmentGrpc),
/// ],
/// child: MyApp(),
/// )
/// ```
final apiModeConfigProvider = Provider<ApiModeConfig>((ref) {
// Default to HTTP for safety during transition
return ApiModeConfig.development;
});
// ============================================================================
// Core Service Providers
// ============================================================================
final authServiceProvider = Provider<AuthService>((ref) {
return AuthService(config: AuthConfig.development);
});
@ -65,7 +159,9 @@ final authTokenProvider = FutureProvider<String?>((ref) async {
return await authService.getToken();
});
final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
/// Internal HTTP-based delivery routes provider.
/// Use [deliveryRoutesProvider] instead, which respects the API mode configuration.
final _httpDeliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
@ -95,6 +191,39 @@ final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
return result.whenSuccess((routes) => routes) ?? [];
});
/// Unified delivery routes provider that respects the API mode configuration.
///
/// Automatically switches between HTTP and gRPC based on [apiModeConfigProvider].
/// When gRPC mode is enabled and [ApiModeConfig.fallbackToHttpOnError] is true,
/// falls back to HTTP on gRPC failures.
///
/// Example usage:
/// ```dart
/// final routes = ref.watch(deliveryRoutesProvider);
/// routes.when(
/// data: (data) => displayRoutes(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final deliveryRoutesProvider = FutureProvider<List<DeliveryRoute>>((ref) async {
final apiModeConfig = ref.watch(apiModeConfigProvider);
if (apiModeConfig.isGrpc) {
try {
return await ref.watch(grpcDeliveryRoutesProvider.future);
} catch (e) {
if (apiModeConfig.fallbackToHttpOnError) {
debugPrint('gRPC failed, falling back to HTTP: $e');
return await ref.watch(_httpDeliveryRoutesProvider.future);
}
rethrow;
}
}
return await ref.watch(_httpDeliveryRoutesProvider.future);
});
/// Provider for delivery routes using gRPC.
///
/// This is the gRPC-based alternative to [deliveryRoutesProvider].
@ -173,7 +302,9 @@ final grpcDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref,
return [...deliveries, Delivery.createWarehouseDelivery()];
});
final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
/// Internal HTTP-based deliveries provider.
/// Use [deliveriesProvider] instead, which respects the API mode configuration.
final _httpDeliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final authService = ref.watch(authServiceProvider);
final isAuthenticated = await authService.isAuthenticated();
@ -213,6 +344,41 @@ final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, rout
return [...deliveries, Delivery.createWarehouseDelivery()];
});
/// Unified deliveries provider that respects the API mode configuration.
///
/// Automatically switches between HTTP and gRPC based on [apiModeConfigProvider].
/// When gRPC mode is enabled and [ApiModeConfig.fallbackToHttpOnError] is true,
/// falls back to HTTP on gRPC failures.
///
/// Takes a [routeFragmentId] parameter to fetch deliveries for a specific route.
///
/// Example usage:
/// ```dart
/// final deliveries = ref.watch(deliveriesProvider(routeFragmentId));
/// deliveries.when(
/// data: (data) => displayDeliveries(data),
/// loading: () => showLoading(),
/// error: (error, stack) => showError(error),
/// );
/// ```
final deliveriesProvider = FutureProvider.family<List<Delivery>, int>((ref, routeFragmentId) async {
final apiModeConfig = ref.watch(apiModeConfigProvider);
if (apiModeConfig.isGrpc) {
try {
return await ref.watch(grpcDeliveriesProvider(routeFragmentId).future);
} catch (e) {
if (apiModeConfig.fallbackToHttpOnError) {
debugPrint('gRPC failed for route $routeFragmentId, falling back to HTTP: $e');
return await ref.watch(_httpDeliveriesProvider(routeFragmentId).future);
}
rethrow;
}
}
return await ref.watch(_httpDeliveriesProvider(routeFragmentId).future);
});
/// Provider to get all deliveries from all routes
final allDeliveriesProvider = FutureProvider<List<Delivery>>((ref) async {
final routes = await ref.read(deliveryRoutesProvider.future);