networkResponse<T> method

Future<NyResponse<T>> networkResponse<T>({
  1. required dynamic request(
    1. Dio api
    ),
  2. dynamic handleSuccess(
    1. NyResponse<T> response
    )?,
  3. dynamic handleFailure(
    1. NyResponse<T> response
    )?,
  4. String? bearerToken,
  5. String? baseUrl,
  6. bool useUndefinedResponse = true,
  7. bool shouldRetry = true,
  8. bool? shouldSetAuthHeaders,
  9. int? retry,
  10. Duration? retryDelay,
  11. bool retryIf(
    1. DioException dioException
    )?,
  12. Duration? connectionTimeout,
  13. Duration? receiveTimeout,
  14. Duration? sendTimeout,
  15. Duration? cacheDuration,
  16. String? cacheKey,
  17. CachePolicy? cachePolicy,
  18. bool? checkConnectivity,
  19. Map<String, dynamic>? headers,
})

Implementation

Future<NyResponse<T>> networkResponse<T>({
  required Function(Dio api) request,
  Function(NyResponse<T> response)? handleSuccess,
  Function(NyResponse<T> response)? handleFailure,
  String? bearerToken,
  String? baseUrl,
  bool useUndefinedResponse = true,
  bool shouldRetry = true,
  bool? shouldSetAuthHeaders,
  int? retry,
  Duration? retryDelay,
  bool Function(DioException dioException)? retryIf,
  Duration? connectionTimeout,
  Duration? receiveTimeout,
  Duration? sendTimeout,
  Duration? cacheDuration,
  String? cacheKey,
  CachePolicy? cachePolicy,
  bool? checkConnectivity,
  Map<String, dynamic>? headers,
}) async {
  headers ??= {};
  Stopwatch stopwatch = Stopwatch();
  String? requestTime;

  // Resolve cache policy - default to networkOnly if no cache settings
  CachePolicy effectivePolicy = cachePolicy ?? CachePolicy.networkOnly;
  String? cacheKeyRequest = cacheKey ?? _cacheKey;
  Duration? cacheDurationRequest = cacheDuration ?? _cacheDuration;

  // If cache key/duration provided but no policy, use cacheFirst for backward compatibility
  if (cachePolicy == null &&
      (cacheKeyRequest != null || cacheDurationRequest != null)) {
    effectivePolicy = CachePolicy.cacheFirst;
  }

  try {
    // Compute per-request headers
    Map<String, dynamic> newValuesToAddToHeader = {};
    if (headers.isNotEmpty) {
      for (var header in headers.entries) {
        if (!_api.options.headers.containsKey(header.key)) {
          newValuesToAddToHeader.addAll({header.key: header.value});
        }
      }
    }
    if (await shouldRefreshToken()) {
      await refreshToken(Dio());
    }

    if (bearerToken != null) {
      newValuesToAddToHeader.addAll({"Authorization": "Bearer $bearerToken"});
    } else {
      if ((shouldSetAuthHeaders ?? this.shouldSetAuthHeaders) == true) {
        newValuesToAddToHeader.addAll(await setAuthHeaders(headers));
      }
    }

    // Create a per-request Dio instance to avoid shared mutable state
    // across concurrent requests (prevents header/auth token leakage).
    Dio requestDio = _createRequestDio(
      additionalHeaders: newValuesToAddToHeader,
      baseUrl: baseUrl,
      connectTimeout: connectionTimeout,
      receiveTimeout: receiveTimeout,
      sendTimeout: sendTimeout,
    );

    // Handle cache-first policies
    if (effectivePolicy.shouldTryCacheFirst && cacheKeyRequest != null) {
      final cachedData = await _tryGetFromCache(cacheKeyRequest);
      if (cachedData != null) {
        printDebug('');
        printDebug('╔╣ Cache hit: $cacheKeyRequest');
        printDebug('╚╣ Policy: ${effectivePolicy.description}');

        // For staleWhileRevalidate, trigger background refresh
        if (effectivePolicy.shouldRevalidateInBackground) {
          _revalidateInBackground(
            request: request,
            cacheKey: cacheKeyRequest,
            cacheDuration: cacheDurationRequest,
            requestDio: requestDio,
          );
        }

        return _createResponseFromCache<T>(cachedData, handleSuccess);
      } else if (effectivePolicy == CachePolicy.cacheOnly) {
        throw DioException(
          requestOptions: RequestOptions(path: ''),
          type: DioExceptionType.unknown,
          message: 'No cached data available for key: $cacheKeyRequest',
        );
      }
    }

    // Check connectivity before making request if enabled
    bool shouldCheckConnectivity =
        checkConnectivity ?? checkConnectivityBeforeRequest;
    if (shouldCheckConnectivity && await NyConnectivity.isOffline()) {
      // For networkFirst, try cache on offline
      if (effectivePolicy.shouldFallbackToCache && cacheKeyRequest != null) {
        final cachedData = await _tryGetFromCache(cacheKeyRequest);
        if (cachedData != null) {
          printDebug('Offline - using cached data for: $cacheKeyRequest');
          return _createResponseFromCache<T>(cachedData, handleSuccess);
        }
      }
      throw DioException(
        requestOptions: RequestOptions(path: ''),
        type: DioExceptionType.connectionError,
        message: 'No network connection',
      );
    }

    Response? response;

    // Make the network request using the per-request Dio
    stopwatch.start();
    requestDio.options.extra['timestamp'] =
        DateTime.now().microsecondsSinceEpoch;
    response = await request(requestDio);
    stopwatch.stop();
    requestTime = "${stopwatch.elapsedMilliseconds}ms";
    // Cache the response if caching is enabled
    if (cacheKeyRequest != null && effectivePolicy.shouldTryNetwork) {
      await _saveToCache(cacheKeyRequest, response!, cacheDurationRequest);
      printDebug('Cached response: $cacheKeyRequest');
    }

    NyResponse<T> apiResponse = handleResponse<T>(
      response!,
      handleSuccess: handleSuccess,
    );

    _recordApiResponse(apiResponse, requestTime);

    if (apiResponse.data == null && useUndefinedResponse) {
      onUndefinedResponse(apiResponse.data, response);
    }
    if (_onSuccessEvent != null) {
      _onSuccessEvent!(response, apiResponse.data);
    }

    return apiResponse;
  } on DioException catch (dioException) {
    NyResponse response = NyResponse(
      response: dioException.response ?? null,
      data: null,
      rawData: dioException.response?.data,
    );
    stopwatch.stop();
    requestTime = "${stopwatch.elapsedMilliseconds}ms";
    _recordApiResponse(response, requestTime);

    int nyRetries = retry ?? this.retry;
    Duration nyRetryDelay = retryDelay ?? this.retryDelay;
    bool Function(DioException dioException)? retryIfFinal = this.retryIf;
    if (retryIf != null) {
      retryIfFinal = retryIf;
    }
    if (retryIfFinal != null) {
      shouldRetry = retryIfFinal(dioException);
    }
    if (shouldRetry == true && nyRetries > 0) {
      for (var i = 0; i < nyRetries; i++) {
        await Future.delayed(nyRetryDelay);
        NyLogger.debug("[${i + 1}] Retrying request...");
        dynamic response = await networkResponse(
          request: request,
          handleSuccess: handleSuccess,
          handleFailure: handleFailure,
          bearerToken: bearerToken,
          baseUrl: baseUrl,
          useUndefinedResponse: useUndefinedResponse,
          shouldSetAuthHeaders: shouldSetAuthHeaders,
          connectionTimeout: connectionTimeout,
          receiveTimeout: receiveTimeout,
          sendTimeout: sendTimeout,
          headers: headers,
          shouldRetry: false,
        );
        if (response != null) {
          return response;
        }
      }
    }

    NyLogger.error(dioException.toString());
    error(dioException);

    if (handleFailure != null) {
      NyResponse<T> errorResponse = _createErrorResponse<T>(dioException);
      return handleFailure(errorResponse);
    }

    if (_onErrorEvent != null) {
      _onErrorEvent!(dioException);
    }

    // Create an error response from the DioException
    return _createErrorResponse<T>(dioException);
  } on Exception catch (e) {
    NyLogger.error(e.toString());
    return _createGenericErrorResponse<T>(e);
  } finally {
    _api.options.queryParameters = {};
    _cacheDuration = null;
    _cacheKey = null;
  }
}