From a60f92c56de4346205fc533f2f4a5b2add46ada8 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Tue, 20 Jan 2026 13:11:58 -0500 Subject: [PATCH] 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 --- lib/providers/providers.dart | 170 ++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 2 deletions(-) diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 5d80eda..3b3587f 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -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((ref) { + // Default to HTTP for safety during transition + return ApiModeConfig.development; +}); + +// ============================================================================ +// Core Service Providers +// ============================================================================ + final authServiceProvider = Provider((ref) { return AuthService(config: AuthConfig.development); }); @@ -65,7 +159,9 @@ final authTokenProvider = FutureProvider((ref) async { return await authService.getToken(); }); -final deliveryRoutesProvider = FutureProvider>((ref) async { +/// Internal HTTP-based delivery routes provider. +/// Use [deliveryRoutesProvider] instead, which respects the API mode configuration. +final _httpDeliveryRoutesProvider = FutureProvider>((ref) async { final authService = ref.watch(authServiceProvider); final isAuthenticated = await authService.isAuthenticated(); @@ -95,6 +191,39 @@ final deliveryRoutesProvider = FutureProvider>((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>((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, int>((ref, return [...deliveries, Delivery.createWarehouseDelivery()]; }); -final deliveriesProvider = FutureProvider.family, int>((ref, routeFragmentId) async { +/// Internal HTTP-based deliveries provider. +/// Use [deliveriesProvider] instead, which respects the API mode configuration. +final _httpDeliveriesProvider = FutureProvider.family, int>((ref, routeFragmentId) async { final authService = ref.watch(authServiceProvider); final isAuthenticated = await authService.isAuthenticated(); @@ -213,6 +344,41 @@ final deliveriesProvider = FutureProvider.family, 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, 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>((ref) async { final routes = await ref.read(deliveryRoutesProvider.future);