call<TInput, TOutput> method

Future<TOutput> call<TInput, TOutput>(
  1. BloomRpcContract<TInput, TOutput> contract,
  2. TInput input, {
  3. Map<String, dynamic>? pathParams,
  4. Map<String, dynamic>? queryParameters,
  5. Map<String, String>? headers,
  6. Duration? timeout,
  7. BloomRpcCancelToken? cancelToken,
})

Executes a typed contract with input, returning the decoded TOutput.

  • pathParams: Map of path parameter substitutions for tokens in the contract path template.
  • queryParameters: Additional query parameters appended to the URL.
  • headers: Custom headers merged into request headers for this call.
  • timeout: Custom timeout duration overriding BloomRpcClient.timeout.
  • cancelToken: Token used to abort the request while in-flight.

Throws:

final user = await client.call(
  getUserContract,
  null,
  pathParams: {'id': '42'},
);

Implementation

Future<TOutput> call<TInput, TOutput>(
  BloomRpcContract<TInput, TOutput> contract,
  TInput input, {
  Map<String, dynamic>? pathParams,
  Map<String, dynamic>? queryParameters,
  Map<String, String>? headers,
  Duration? timeout,
  BloomRpcCancelToken? cancelToken,
}) async {
  cancelToken?.throwIfCancelled(contract);

  // 1. Resolve path template
  final String resolvedPath;
  try {
    resolvedPath =
        contract.resolvePath(pathParams: pathParams, input: input);
  } catch (e) {
    if (e is BloomRpcException) rethrow;
    throw BloomRpcEncodeException(cause: e, contract: contract);
  }

  // 2. Prepare merged headers
  final mergedHeaders = <String, String>{
    ...defaultHeaders,
    if (headers != null) ...headers,
  };

  // 3. Prepare payload and query parameters based on HTTP method
  final isGetStyle = contract.method == BloomHttpMethod.get ||
      contract.method == BloomHttpMethod.head ||
      contract.method == BloomHttpMethod.options;

  final mergedQueryParams = <String, dynamic>{};
  if (queryParameters != null) {
    mergedQueryParams.addAll(queryParameters);
  }

  dynamic bodyPayload;

  if (isGetStyle) {
    if (input != null) {
      dynamic encoded;
      if (contract.encodeInput != null) {
        try {
          encoded = contract.encodeInput!(input);
        } catch (e, st) {
          throw BloomRpcEncodeException(
              cause: e, contract: contract, stackTrace: st);
        }
      } else {
        encoded = input;
      }

      if (encoded is Map) {
        final pathKeys = contract.pathParameters.toSet();
        for (final entry in encoded.entries) {
          final keyStr = entry.key.toString();
          if (!pathKeys.contains(keyStr)) {
            mergedQueryParams[keyStr] = entry.value;
          }
        }
      }
    }
  } else {
    if (input != null) {
      if (contract.encodeInput != null) {
        try {
          bodyPayload = contract.encodeInput!(input);
        } catch (e, st) {
          throw BloomRpcEncodeException(
              cause: e, contract: contract, stackTrace: st);
        }
      } else {
        bodyPayload = input;
      }
    }
  }

  final effectiveTimeout = timeout ?? this.timeout;

  // 4. Dispatch request
  Future<dynamic> requestFuture;
  switch (contract.method) {
    case BloomHttpMethod.get:
    case BloomHttpMethod.head:
    case BloomHttpMethod.options:
      requestFuture = httpClient.get(
        resolvedPath,
        headers: mergedHeaders,
        queryParameters:
            mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
      );
      break;
    case BloomHttpMethod.post:
      requestFuture = httpClient.post(
        resolvedPath,
        body: bodyPayload,
        headers: mergedHeaders,
        queryParameters:
            mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
      );
      break;
    case BloomHttpMethod.put:
      requestFuture = httpClient.put(
        resolvedPath,
        body: bodyPayload,
        headers: mergedHeaders,
        queryParameters:
            mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
      );
      break;
    case BloomHttpMethod.patch:
      requestFuture = httpClient.patch(
        resolvedPath,
        body: bodyPayload,
        headers: mergedHeaders,
        queryParameters:
            mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
      );
      break;
    case BloomHttpMethod.delete:
      requestFuture = httpClient.delete(
        resolvedPath,
        body: bodyPayload,
        headers: mergedHeaders,
        queryParameters:
            mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
      );
      break;
  }

  // 5. Wrap with cancellation token and timeout
  final dynamic rawResponse;
  try {
    rawResponse = await _raceWithCancellation(
      requestFuture.timeout(effectiveTimeout),
      cancelToken,
      contract,
    );
  } on TimeoutException {
    throw BloomRpcTimeoutException(
      timeout: effectiveTimeout,
      contract: contract,
    );
  } on BloomRpcException {
    rethrow;
  } on http.ClientException catch (e, st) {
    // Inspect message for HTTP status pattern "HTTP <statusCode>: <body>"
    final httpMatch =
        RegExp(r'^HTTP\s+(\d{3}):\s*(.*)$', dotAll: true).firstMatch(e.message);
    if (httpMatch != null) {
      final statusCode = int.parse(httpMatch.group(1)!);
      final rawBody = httpMatch.group(2) ?? '';
      dynamic decodedBody;
      try {
        decodedBody = jsonDecode(rawBody);
      } catch (_) {
        decodedBody = rawBody;
      }

      throw BloomRpcHttpException(
        statusCode: statusCode,
        responseBody: decodedBody,
        contract: contract,
        uri: e.uri,
      );
    }

    throw BloomRpcTransportException(
      cause: e,
      contract: contract,
      uri: e.uri,
      stackTrace: st,
    );
  } catch (e, st) {
    throw BloomRpcTransportException(
      cause: e,
      contract: contract,
      stackTrace: st,
    );
  }

  // 6. Decode output
  try {
    // A void/nullable output with no body decodes to null. `TOutput == void`
    // is not valid Dart; `null is TOutput` is the correct way to ask whether
    // the output type admits null.
    if (rawResponse == null && null is TOutput) {
      return null as TOutput;
    }
    final decode = contract.decodeOutput ?? _defaultDecode<TOutput>;
    return decode(rawResponse);
  } catch (e, st) {
    throw BloomRpcDecodeException(
      cause: e,
      rawData: rawResponse,
      contract: contract,
      stackTrace: st,
    );
  }
}