From 97db38a42d665ef193b4560033a6f87492bf664a Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Wed, 25 Jun 2025 20:50:27 +0530 Subject: [PATCH] Added SSE ability to HTTPS method (fusion) --- lib/providers/collection_providers.dart | 94 +++++++++++++++++-- lib/providers/history_providers.dart | 15 +++ lib/widgets/response_body.dart | 18 ++++ .../lib/models/http_response_model.dart | 1 + .../models/http_response_model.freezed.dart | 36 ++++++- .../lib/models/http_response_model.g.dart | 4 + .../lib/services/http_service.dart | 50 ++++++++-- 7 files changed, 202 insertions(+), 16 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 1bb9fba2..6fe27f0e 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -311,7 +313,14 @@ class CollectionStateNotifier state = map; bool noSSL = ref.read(settingsProvider).isSSLDisabled; - var responseRec = await sendHttpRequest( + + (Response?, Duration?, String?) responseRec; + HttpResponseModel? respModel; + HistoryRequestModel? historyM; + RequestModel? newRequestModel; + + responseRec = (null, null, null); + final stream = await streamHttpRequest( requestId, apiType, substitutedHttpRequestModel, @@ -319,7 +328,78 @@ class CollectionStateNotifier noSSL: noSSL, ); - late RequestModel newRequestModel; + StreamSubscription? sub; + final completer = Completer(); + + bool isTextStream = false; + + sub = stream.listen((d) async { + if (d == null) return; + + isTextStream = ((d.$1 == null && isTextStream) || + (d.$1 == 'text/event-stream' || d.$1 == 'application/x-ndjson')); + + responseRec = (d.$2, d.$3, d.$4); + + if (!isTextStream) { + if (completer.isCompleted) return; + completer.complete(responseRec); + await Future.delayed(Duration(milliseconds: 100)); + } + + if (responseRec.$1 != null) { + responseRec = ( + HttpResponse( + responseRec.$1!.body, + responseRec.$1!.statusCode, + request: responseRec.$1!.request, + headers: { + ...(responseRec.$1?.headers ?? {}), + 'content-type': 'text/event-stream' + }, + isRedirect: responseRec.$1!.isRedirect, + reasonPhrase: responseRec.$1!.reasonPhrase, + persistentConnection: responseRec.$1!.persistentConnection, + ), + responseRec.$2, + responseRec.$3, + ); + } + + //----------- MAKE CHANGES -------------- + respModel = respModel?.copyWith(sseOutput: [ + ...(respModel?.sseOutput ?? []), + responseRec.$1!.body, + ]); + if (respModel != null) { + final nRM = newRequestModel!.copyWith( + httpResponseModel: respModel, + ); + map = {...state!}; + map[requestId] = nRM; + state = map; + unsave(); + } + //Changing History + if (historyM != null && respModel != null) { + historyM = historyM!.copyWith( + httpResponseModel: respModel!, + ); + ref + .read(historyMetaStateNotifier.notifier) + .editHistoryRequest(historyM!); + } + //----------- MAKE CHANGES -------------- + + if (completer.isCompleted) return; + completer.complete(responseRec); + }, onDone: () { + sub?.cancel(); + }, onError: (e) { + print('err: $e'); + }); + responseRec = await completer.future; + if (responseRec.$1 == null) { newRequestModel = requestModel.copyWith( responseStatus: -1, @@ -327,7 +407,7 @@ class CollectionStateNotifier isWorking: false, ); } else { - final httpResponseModel = baseHttpResponseModel.fromResponse( + respModel = baseHttpResponseModel.fromResponse( response: responseRec.$1!, time: responseRec.$2!, ); @@ -335,11 +415,11 @@ class CollectionStateNotifier newRequestModel = requestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], - httpResponseModel: httpResponseModel, + httpResponseModel: respModel, isWorking: false, ); String newHistoryId = getNewUuid(); - HistoryRequestModel model = HistoryRequestModel( + historyM = HistoryRequestModel( historyId: newHistoryId, metaData: HistoryMetaModel( historyId: newHistoryId, @@ -352,7 +432,7 @@ class CollectionStateNotifier timeStamp: DateTime.now(), ), httpRequestModel: substitutedHttpRequestModel, - httpResponseModel: httpResponseModel, + httpResponseModel: respModel!, preRequestScript: requestModel.preRequestScript, postRequestScript: requestModel.postRequestScript, ); @@ -372,7 +452,7 @@ class CollectionStateNotifier }, ); } - ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(historyM!); } // update state with response data diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart index cc7a587c..f44edc6e 100644 --- a/lib/providers/history_providers.dart +++ b/lib/providers/history_providers.dart @@ -90,6 +90,21 @@ class HistoryMetaStateNotifier await loadHistoryRequest(id); } + void editHistoryRequest(HistoryRequestModel model) async { + final id = model.historyId; + state = { + ...state ?? {}, + id: model.metaData, + }; + final existingKeys = state?.keys.toList() ?? []; + if (!existingKeys.contains(id)) { + hiveHandler.setHistoryIds([...existingKeys, id]); + } + hiveHandler.setHistoryMeta(id, model.metaData.toJson()); + await hiveHandler.setHistoryRequest(id, model.toJson()); + await loadHistoryRequest(id); + } + Future clearAllHistory() async { await hiveHandler.clearAllHistory(); ref.read(selectedHistoryIdStateProvider.notifier).state = null; diff --git a/lib/widgets/response_body.dart b/lib/widgets/response_body.dart index 561d9271..5e9a2806 100644 --- a/lib/widgets/response_body.dart +++ b/lib/widgets/response_body.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash/models/models.dart'; @@ -54,6 +56,22 @@ class ResponseBody extends StatelessWidget { options.remove(ResponseBodyView.code); } + // print('reM -> ${responseModel.sseOutput}'); + + if (responseModel.sseOutput?.isNotEmpty ?? false) { + final modifiedBody = responseModel.sseOutput!.join('\n\n'); + print(modifiedBody); + return ResponseBodySuccess( + key: Key("${selectedRequestModel!.id}-response"), + mediaType: mediaType, + options: options, + bytes: utf8.encode(modifiedBody), + body: modifiedBody, + formattedBody: modifiedBody, + highlightLanguage: highlightLanguage, + ); + } + return ResponseBodySuccess( key: Key("${selectedRequestModel!.id}-response"), mediaType: mediaType, diff --git a/packages/better_networking/lib/models/http_response_model.dart b/packages/better_networking/lib/models/http_response_model.dart index b3effc76..683cab87 100644 --- a/packages/better_networking/lib/models/http_response_model.dart +++ b/packages/better_networking/lib/models/http_response_model.dart @@ -53,6 +53,7 @@ class HttpResponseModel with _$HttpResponseModel { String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }) = _HttpResponseModel; factory HttpResponseModel.fromJson(Map json) => diff --git a/packages/better_networking/lib/models/http_response_model.freezed.dart b/packages/better_networking/lib/models/http_response_model.freezed.dart index b4474f4e..3650c122 100644 --- a/packages/better_networking/lib/models/http_response_model.freezed.dart +++ b/packages/better_networking/lib/models/http_response_model.freezed.dart @@ -30,6 +30,7 @@ mixin _$HttpResponseModel { Uint8List? get bodyBytes => throw _privateConstructorUsedError; @DurationConverter() Duration? get time => throw _privateConstructorUsedError; + List? get sseOutput => throw _privateConstructorUsedError; /// Serializes this HttpResponseModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -56,6 +57,7 @@ abstract class $HttpResponseModelCopyWith<$Res> { String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }); } @@ -81,6 +83,7 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseOutput = freezed, }) { return _then( _value.copyWith( @@ -112,6 +115,10 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseOutput: freezed == sseOutput + ? _value.sseOutput + : sseOutput // ignore: cast_nullable_to_non_nullable + as List?, ) as $Val, ); @@ -135,6 +142,7 @@ abstract class _$$HttpResponseModelImplCopyWith<$Res> String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List? sseOutput, }); } @@ -159,6 +167,7 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseOutput = freezed, }) { return _then( _$HttpResponseModelImpl( @@ -190,6 +199,10 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseOutput: freezed == sseOutput + ? _value._sseOutput + : sseOutput // ignore: cast_nullable_to_non_nullable + as List?, ), ); } @@ -207,8 +220,10 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { this.formattedBody, @Uint8ListConverter() this.bodyBytes, @DurationConverter() this.time, + final List? sseOutput, }) : _headers = headers, _requestHeaders = requestHeaders, + _sseOutput = sseOutput, super._(); factory _$HttpResponseModelImpl.fromJson(Map json) => @@ -246,10 +261,19 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { @override @DurationConverter() final Duration? time; + final List? _sseOutput; + @override + List? get sseOutput { + final value = _sseOutput; + if (value == null) return null; + if (_sseOutput is EqualUnmodifiableListView) return _sseOutput; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } @override String toString() { - return 'HttpResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, bodyBytes: $bodyBytes, time: $time)'; + return 'HttpResponseModel(statusCode: $statusCode, headers: $headers, requestHeaders: $requestHeaders, body: $body, formattedBody: $formattedBody, bodyBytes: $bodyBytes, time: $time, sseOutput: $sseOutput)'; } @override @@ -268,7 +292,11 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { (identical(other.formattedBody, formattedBody) || other.formattedBody == formattedBody) && const DeepCollectionEquality().equals(other.bodyBytes, bodyBytes) && - (identical(other.time, time) || other.time == time)); + (identical(other.time, time) || other.time == time) && + const DeepCollectionEquality().equals( + other._sseOutput, + _sseOutput, + )); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -282,6 +310,7 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { formattedBody, const DeepCollectionEquality().hash(bodyBytes), time, + const DeepCollectionEquality().hash(_sseOutput), ); /// Create a copy of HttpResponseModel @@ -310,6 +339,7 @@ abstract class _HttpResponseModel extends HttpResponseModel { final String? formattedBody, @Uint8ListConverter() final Uint8List? bodyBytes, @DurationConverter() final Duration? time, + final List? sseOutput, }) = _$HttpResponseModelImpl; const _HttpResponseModel._() : super._(); @@ -332,6 +362,8 @@ abstract class _HttpResponseModel extends HttpResponseModel { @override @DurationConverter() Duration? get time; + @override + List? get sseOutput; /// Create a copy of HttpResponseModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/better_networking/lib/models/http_response_model.g.dart b/packages/better_networking/lib/models/http_response_model.g.dart index e197be09..1ce28977 100644 --- a/packages/better_networking/lib/models/http_response_model.g.dart +++ b/packages/better_networking/lib/models/http_response_model.g.dart @@ -21,6 +21,9 @@ _$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) => json['bodyBytes'] as List?, ), time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), + sseOutput: (json['sseOutput'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$$HttpResponseModelImplToJson( @@ -33,4 +36,5 @@ Map _$$HttpResponseModelImplToJson( 'formattedBody': instance.formattedBody, 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), 'time': const DurationConverter().toJson(instance.time), + 'sseOutput': instance.sseOutput, }; diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index 31c7ef06..266b4235 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -165,14 +165,18 @@ http.Request prepareHttpRequest({ return request; } -Future> streamHttpRequest( +Future> +streamHttpRequest( String requestId, APIType apiType, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, }) async { - final controller = StreamController<(String?, Duration?, String?)?>(); + final controller = + StreamController< + (String? cT, HttpResponse? resp, Duration? dur, String? err)? + >(); StreamSubscription? subscription; final stopwatch = Stopwatch()..start(); @@ -186,10 +190,10 @@ Future> streamHttpRequest( Future handleError(dynamic error) async { await Future.microtask(() {}); if (httpClientManager.wasRequestCancelled(requestId)) { - controller.add((null, null, kMsgRequestCancelled)); + controller.add((null, null, null, kMsgRequestCancelled)); httpClientManager.removeCancelledRequest(requestId); } else { - controller.add((null, null, error.toString())); + controller.add((null, null, null, error.toString())); } await cleanup(); } @@ -200,7 +204,7 @@ Future> streamHttpRequest( }; if (httpClientManager.wasRequestCancelled(requestId)) { - controller.add((null, null, kMsgRequestCancelled)); + controller.add((null, null, null, kMsgRequestCancelled)); httpClientManager.removeCancelledRequest(requestId); controller.close(); return controller.stream; @@ -243,8 +247,25 @@ Future> streamHttpRequest( final streamedResponse = await client.send(multipart); final stream = streamTextResponse(streamedResponse); + print(streamedResponse.headers['content-type']); + subscription = stream.listen( - (data) => controller.add((data, stopwatch.elapsed, null)), + (data) => controller.add(( + streamedResponse.headers['content-type'].toString(), + data == null + ? null + : HttpResponse.bytes( + utf8.encode(data), + streamedResponse.statusCode, + request: streamedResponse.request, + headers: streamedResponse.headers, + isRedirect: streamedResponse.isRedirect, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + ), + stopwatch.elapsed, + null, + )), onDone: () => cleanup(), onError: handleError, ); @@ -279,7 +300,22 @@ Future> streamHttpRequest( subscription = stream.listen( (data) { if (!controller.isClosed) { - controller.add((data, stopwatch.elapsed, null)); + controller.add(( + streamedResponse.headers['content-type'].toString(), + data == null + ? null + : HttpResponse.bytes( + utf8.encode(data), + streamedResponse.statusCode, + request: streamedResponse.request, + headers: streamedResponse.headers, + isRedirect: streamedResponse.isRedirect, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + ), + stopwatch.elapsed, + null, + )); } }, onDone: () => cleanup(),