From 882b393fdddad0a1601e0aff8de775521326fbcf Mon Sep 17 00:00:00 2001 From: Manas Hejmadi Date: Thu, 26 Jun 2025 00:36:12 +0530 Subject: [PATCH] SSE: Stopping/Cancelling implementation --- lib/models/request_model.dart | 1 + lib/models/request_model.freezed.dart | 27 ++- lib/models/request_model.g.dart | 1 + lib/providers/collection_providers.dart | 165 +++++++++--------- .../home_page/editor_pane/url_card.dart | 3 + lib/widgets/button_send.dart | 12 +- .../lib/services/http_service.dart | 2 - test/widgets/button_send_test.dart | 3 + 8 files changed, 119 insertions(+), 95 deletions(-) diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 4d63c2ad..b7052f81 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -22,6 +22,7 @@ class RequestModel with _$RequestModel { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) @Default(false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) @Default(false) bool isStreaming, String? preRequestScript, String? postRequestScript, }) = _RequestModel; diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 72f607bd..3ba8979b 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -35,6 +35,8 @@ mixin _$RequestModel { bool get isWorking => throw _privateConstructorUsedError; @JsonKey(includeToJson: false) DateTime? get sendingTime => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false) + bool get isStreaming => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError; @@ -66,6 +68,7 @@ abstract class $RequestModelCopyWith<$Res> { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript}); @@ -99,6 +102,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, }) { @@ -147,6 +151,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + isStreaming: null == isStreaming + ? _value.isStreaming + : isStreaming // ignore: cast_nullable_to_non_nullable + as bool, preRequestScript: freezed == preRequestScript ? _value.preRequestScript : preRequestScript // ignore: cast_nullable_to_non_nullable @@ -207,6 +215,7 @@ abstract class _$$RequestModelImplCopyWith<$Res> HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @JsonKey(includeToJson: false) bool isStreaming, String? preRequestScript, String? postRequestScript}); @@ -240,6 +249,7 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? isStreaming = null, Object? preRequestScript = freezed, Object? postRequestScript = freezed, }) { @@ -287,6 +297,10 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable as DateTime?, + isStreaming: null == isStreaming + ? _value.isStreaming + : isStreaming // ignore: cast_nullable_to_non_nullable + as bool, preRequestScript: freezed == preRequestScript ? _value.preRequestScript : preRequestScript // ignore: cast_nullable_to_non_nullable @@ -315,6 +329,7 @@ class _$RequestModelImpl implements _RequestModel { this.httpResponseModel, @JsonKey(includeToJson: false) this.isWorking = false, @JsonKey(includeToJson: false) this.sendingTime, + @JsonKey(includeToJson: false) this.isStreaming = false, this.preRequestScript, this.postRequestScript}); @@ -350,13 +365,16 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) final DateTime? sendingTime; @override + @JsonKey(includeToJson: false) + final bool isStreaming; + @override final String? preRequestScript; @override final String? postRequestScript; @override String toString() { - return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; } @override @@ -382,6 +400,8 @@ class _$RequestModelImpl implements _RequestModel { other.isWorking == isWorking) && (identical(other.sendingTime, sendingTime) || other.sendingTime == sendingTime) && + (identical(other.isStreaming, isStreaming) || + other.isStreaming == isStreaming) && (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || @@ -403,6 +423,7 @@ class _$RequestModelImpl implements _RequestModel { httpResponseModel, isWorking, sendingTime, + isStreaming, preRequestScript, postRequestScript); @@ -435,6 +456,7 @@ abstract class _RequestModel implements RequestModel { final HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) final bool isWorking, @JsonKey(includeToJson: false) final DateTime? sendingTime, + @JsonKey(includeToJson: false) final bool isStreaming, final String? preRequestScript, final String? postRequestScript}) = _$RequestModelImpl; @@ -467,6 +489,9 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) DateTime? get sendingTime; @override + @JsonKey(includeToJson: false) + bool get isStreaming; + @override String? get preRequestScript; @override String? get postRequestScript; diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index d4f6ec20..8e0a5a68 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -27,6 +27,7 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( sendingTime: json['sendingTime'] == null ? null : DateTime.parse(json['sendingTime'] as String), + isStreaming: json['isStreaming'] as bool? ?? false, preRequestScript: json['preRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?, ); diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 6fe27f0e..5ce32dd5 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -270,19 +270,16 @@ class CollectionStateNotifier Future sendRequest() async { final requestId = ref.read(selectedIdStateProvider); ref.read(codePaneVisibleStateProvider.notifier).state = false; + + if (requestId == null || state == null) return; + + RequestModel? requestModel = state![requestId]; + if (requestModel?.httpRequestModel == null) return; + final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; final EnvironmentModel? originalEnvironmentModel = ref.read(selectedEnvironmentModelProvider); - if (requestId == null || state == null) { - return; - } - RequestModel? requestModel = state![requestId]; - - if (requestModel?.httpRequestModel == null) { - return; - } - if (requestModel != null && !requestModel.preRequestScript.isNullOrEmpty()) { requestModel = await handlePreRequestScript( @@ -303,23 +300,18 @@ class CollectionStateNotifier APIType apiType = requestModel!.apiType; HttpRequestModel substitutedHttpRequestModel = getSubstitutedHttpRequestModel(requestModel.httpRequestModel!); - - // set current model's isWorking to true and update state - var map = {...state!}; - map[requestId] = requestModel.copyWith( - isWorking: true, - sendingTime: DateTime.now(), - ); - state = map; - bool noSSL = ref.read(settingsProvider).isSSLDisabled; - (Response?, Duration?, String?) responseRec; - HttpResponseModel? respModel; - HistoryRequestModel? historyM; - RequestModel? newRequestModel; + // Set model to working and streaming + state = { + ...state!, + requestId: requestModel.copyWith( + isWorking: true, + isStreaming: true, + sendingTime: DateTime.now(), + ), + }; - responseRec = (null, null, null); final stream = await streamHttpRequest( requestId, apiType, @@ -328,101 +320,98 @@ class CollectionStateNotifier noSSL: noSSL, ); - StreamSubscription? sub; - final completer = Completer(); - + HttpResponseModel? respModel; + HistoryRequestModel? historyM; + RequestModel newRequestModel = requestModel; + final completer = Completer<(Response?, Duration?, String?)>(); bool isTextStream = false; + StreamSubscription? sub; sub = stream.listen((d) async { if (d == null) return; - isTextStream = ((d.$1 == null && isTextStream) || - (d.$1 == 'text/event-stream' || d.$1 == 'application/x-ndjson')); + final contentType = d.$1; + isTextStream = isTextStream || + contentType == 'text/event-stream' || + contentType == 'application/x-ndjson'; - responseRec = (d.$2, d.$3, d.$4); + final response = d.$2; + final duration = d.$3; + final errorMessage = d.$4; if (!isTextStream) { - if (completer.isCompleted) return; - completer.complete(responseRec); - await Future.delayed(Duration(milliseconds: 100)); + if (!completer.isCompleted) { + completer.complete((response, duration, errorMessage)); + } + return; } - 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, - ); - } + respModel = respModel?.copyWith( + sseOutput: [ + ...(respModel?.sseOutput ?? []), + if (response != null) response.body, + ], + ); + + newRequestModel = newRequestModel.copyWith( + httpResponseModel: respModel, + isStreaming: true, + ); + state = { + ...state!, + requestId: newRequestModel, + }; + unsave(); - //----------- 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!, - ); + historyM = historyM!.copyWith(httpResponseModel: respModel!); ref .read(historyMetaStateNotifier.notifier) .editHistoryRequest(historyM!); } - //----------- MAKE CHANGES -------------- - if (completer.isCompleted) return; - completer.complete(responseRec); + if (!completer.isCompleted) { + completer.complete((response, duration, errorMessage)); + } }, onDone: () { sub?.cancel(); + state = { + ...state!, + requestId: newRequestModel.copyWith(isStreaming: false), + }; + unsave(); }, onError: (e) { - print('err: $e'); + print('Stream error: $e'); }); - responseRec = await completer.future; - if (responseRec.$1 == null) { - newRequestModel = requestModel.copyWith( + final (response, duration, errorMessage) = await completer.future; + + if (response == null) { + newRequestModel = newRequestModel.copyWith( responseStatus: -1, - message: responseRec.$3, + message: errorMessage, isWorking: false, + isStreaming: false, ); } else { + final statusCode = response.statusCode; respModel = baseHttpResponseModel.fromResponse( - response: responseRec.$1!, - time: responseRec.$2!, + response: response, + time: duration, ); - int statusCode = responseRec.$1!.statusCode; - newRequestModel = requestModel.copyWith( + + newRequestModel = newRequestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], httpResponseModel: respModel, isWorking: false, ); - String newHistoryId = getNewUuid(); + + final historyId = getNewUuid(); historyM = HistoryRequestModel( - historyId: newHistoryId, + historyId: historyId, metaData: HistoryMetaModel( - historyId: newHistoryId, + historyId: historyId, requestId: requestId, apiType: requestModel.apiType, name: requestModel.name, @@ -452,13 +441,15 @@ class CollectionStateNotifier }, ); } + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(historyM!); } - // update state with response data - map = {...state!}; - map[requestId] = newRequestModel; - state = map; + // Final state update + state = { + ...state!, + requestId: newRequestModel, + }; unsave(); } diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 829bc5c9..5aa1ce02 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -129,8 +129,11 @@ class SendRequestButton extends ConsumerWidget { ref.watch(selectedIdStateProvider); final isWorking = ref.watch( selectedRequestModelProvider.select((value) => value?.isWorking)); + final isStreaming = ref.watch( + selectedRequestModelProvider.select((value) => value?.isStreaming)); return SendButton( + isStreaming: isStreaming ?? false, isWorking: isWorking ?? false, onTap: () { onTap?.call(); diff --git a/lib/widgets/button_send.dart b/lib/widgets/button_send.dart index 67e78908..8ffb7aa7 100644 --- a/lib/widgets/button_send.dart +++ b/lib/widgets/button_send.dart @@ -5,11 +5,13 @@ import 'package:apidash/consts.dart'; class SendButton extends StatelessWidget { const SendButton({ super.key, + required this.isStreaming, required this.isWorking, required this.onTap, this.onCancel, }); + final bool isStreaming; final bool isWorking; final void Function() onTap; final void Function()? onCancel; @@ -17,13 +19,13 @@ class SendButton extends StatelessWidget { @override Widget build(BuildContext context) { return ADFilledButton( - onPressed: isWorking ? onCancel : onTap, - isTonal: isWorking ? true : false, - items: isWorking - ? const [ + onPressed: (isWorking || isStreaming) ? onCancel : onTap, + isTonal: (isWorking || isStreaming), + items: (isWorking || isStreaming) + ? [ kHSpacer8, Text( - kLabelCancel, + isStreaming ? 'Stop' : kLabelCancel, style: kTextStyleButton, ), kHSpacer6, diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index 266b4235..b91eac00 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -247,8 +247,6 @@ streamHttpRequest( final streamedResponse = await client.send(multipart); final stream = streamTextResponse(streamedResponse); - print(streamedResponse.headers['content-type']); - subscription = stream.listen( (data) => controller.add(( streamedResponse.headers['content-type'].toString(), diff --git a/test/widgets/button_send_test.dart b/test/widgets/button_send_test.dart index c9bee3c7..8b3f1ee1 100644 --- a/test/widgets/button_send_test.dart +++ b/test/widgets/button_send_test.dart @@ -17,6 +17,7 @@ void main() { home: Scaffold( body: SendButton( isWorking: false, + isStreaming: false, onTap: () => sendPressed = true, onCancel: () => cancelPressed = true, ), @@ -46,6 +47,7 @@ void main() { home: Scaffold( body: SendButton( isWorking: true, + isStreaming: false, onTap: () => sendPressed = true, onCancel: () => cancelPressed = true, ), @@ -74,6 +76,7 @@ void main() { builder: (context, setState) { return Scaffold( body: SendButton( + isStreaming: false, isWorking: isWorking, onTap: () => setState(() => isWorking = true), onCancel: () => setState(() => isWorking = false),