From 8476937dbd2d45538fccc1c76b7b6f7e804eabb7 Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Mon, 31 Mar 2025 02:04:37 +0530 Subject: [PATCH 1/8] commiting --- lib/providers/collection_providers.dart | 201 ++++++++++--- .../details_card/response_pane.dart | 242 +++++++++++++++- packages/apidash_core/lib/apidash_core.dart | 2 +- packages/apidash_core/lib/consts.dart | 1 + .../lib/models/http_sse_event_model.dart | 68 +++++ .../models/http_sse_event_model.freezed.dart | 271 ++++++++++++++++++ .../lib/models/http_sse_event_model.g.dart | 27 ++ packages/apidash_core/lib/models/models.dart | 1 + .../lib/services/http_service.dart | 127 +++++++- .../lib/services/http_sse_service.dart | 113 ++++++++ .../apidash_core/lib/services/services.dart | 1 + 11 files changed, 989 insertions(+), 65 deletions(-) create mode 100644 packages/apidash_core/lib/models/http_sse_event_model.dart create mode 100644 packages/apidash_core/lib/models/http_sse_event_model.freezed.dart create mode 100644 packages/apidash_core/lib/models/http_sse_event_model.g.dart create mode 100644 packages/apidash_core/lib/services/http_sse_service.dart diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 35bc4aa0c..c6b061854 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -20,6 +22,39 @@ final selectedRequestModelProvider = StateProvider<RequestModel?>((ref) { } }); +class SSEFramesNotifier extends StateNotifier<Map<String, List<dynamic>>> { + SSEFramesNotifier() : super({}); + + void addFrame(String requestId, dynamic frame) { + state = { + ...state, + requestId: [...(state[requestId] ?? []), SSEEventModel.fromRawSSE(frame)], + }; + } + + void clearFrames(String requestId) { + if (state.containsKey(requestId)) { + state = { + ...state, + requestId: [], + }; + } + } + + void removeRequest(String requestId) { + final newState = {...state}; + newState.remove(requestId); + state = newState; + } +} + +// Provide it globally +final sseFramesProvider = + StateNotifierProvider<SSEFramesNotifier, Map<String, List<dynamic>>>( + (ref) => SSEFramesNotifier(), +); + + final requestSequenceProvider = StateProvider<List<String>>((ref) { var ids = hiveHandler.getIds(); return ids ?? []; @@ -289,58 +324,138 @@ class CollectionStateNotifier state = map; bool noSSL = ref.read(settingsProvider).isSSLDisabled; - var responseRec = await sendHttpRequest( + + state = map; + var responseRec = await sendSSERequest( requestId, apiType, substitutedHttpRequestModel, defaultUriScheme: defaultUriScheme, noSSL: noSSL, - ); - - late final RequestModel newRequestModel; - if (responseRec.$1 == null) { - newRequestModel = requestModel.copyWith( - responseStatus: -1, - message: responseRec.$3, - isWorking: false, - ); - } else { - final httpResponseModel = baseHttpResponseModel.fromResponse( - response: responseRec.$1!, - time: responseRec.$2!, - ); - int statusCode = responseRec.$1!.statusCode; - newRequestModel = requestModel.copyWith( + onConnect: (statusCode, headers) { + map = {...state!}; + map[requestId] = requestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], - httpResponseModel: httpResponseModel, - isWorking: false, - ); - String newHistoryId = getNewUuid(); - HistoryRequestModel model = HistoryRequestModel( - historyId: newHistoryId, - metaData: HistoryMetaModel( - historyId: newHistoryId, - requestId: requestId, - apiType: requestModel.apiType, - name: requestModel.name, - url: substitutedHttpRequestModel.url, - method: substitutedHttpRequestModel.method, - responseStatus: statusCode, - timeStamp: DateTime.now(), - ), - httpRequestModel: substitutedHttpRequestModel, - httpResponseModel: httpResponseModel, + httpResponseModel: null, + isWorking: true, ); - ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); - } - - // update state with response data - map = {...state!}; - map[requestId] = newRequestModel; - state = map; + + + }, + onData: (response) { + ref.read(sseFramesProvider.notifier).addFrame(requestId, response); + }, + ); - unsave(); + late final RequestModel newRequestModel; + // if (responseRec.$1 == null && responseRec.$4 == null) { + // print("inside first if"); + + // newRequestModel = requestModel.copyWith( + // responseStatus: -1, + // message: responseRec.$3, + // isWorking: false, + // ); + // } else if(responseRec.$1 != null){ + // print("inside second if"); + // final httpResponseModel = baseHttpResponseModel.fromResponse( + // response: responseRec.$1!, + // time: responseRec.$2!, + // ); + // // if(responseRec.$1!.headers['content-type']?.contains('text/event-stream') ?? false){ + + // // Stream.value(responseRec.$1!.bodyBytes as List<int>).byteStream.transform(utf8.decoder).transform(const LineSplitter()); + + // // await for (final event in utf8Stream) { + // // if (event.isNotEmpty) { + // // yield SSEEventModel.fromRawSSE(event); // ✅ Emit each event lazily + // // } + // // } + // // } + // // } + // int statusCode = responseRec.$1!.statusCode; + // newRequestModel = requestModel.copyWith( + // responseStatus: statusCode, + // message: kResponseCodeReasons[statusCode], + // httpResponseModel: httpResponseModel, + // isWorking: false, + // ); + // String newHistoryId = getNewUuid(); + // HistoryRequestModel model = HistoryRequestModel( + // historyId: newHistoryId, + // metaData: HistoryMetaModel( + // historyId: newHistoryId, + // requestId: requestId, + // apiType: requestModel.apiType, + // name: requestModel.name, + // url: substitutedHttpRequestModel.url, + // method: substitutedHttpRequestModel.method, + // responseStatus: statusCode, + // timeStamp: DateTime.now(), + // ), + // httpRequestModel: substitutedHttpRequestModel, + // httpResponseModel: httpResponseModel, + // ); + // ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); + // }else if(responseRec.$4 != null){ + // print("inside else if"); + + // // final httpResponseModel = baseHttpResponseModel.fromResponse( + // // response: responseRec.$4!, + // // time: responseRec.$2!, + // // ); + // // if(responseRec.$1!.headers['content-type']?.contains('text/event-stream') ?? false){ + + // // Stream.value(responseRec.$1!.bodyBytes as List<int>).byteStream.transform(utf8.decoder).transform(const LineSplitter()); + + // // await for (final event in utf8Stream) { + // // if (event.isNotEmpty) { + // // yield SSEEventModel.fromRawSSE(event); // ✅ Emit each event lazily + // // } + // // } + // // } + // // } + // // var streamedResponse = responseRec.$4!; + // // final stream = streamedResponse.stream + // // .transform(utf8.decoder) + // // .transform(const LineSplitter()); + + // // stream.listen( + // // (event) { + // // if (event.isNotEmpty) { + // // print('🔹 SSE Event Received: $event'); + // // print(event.toString()); + // // // final parsedEvent = SSEEventModel.fromRawSSE(event); + // // // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); + + // // } + // // }, + // // onError: (error) { + // // // ref.read(sseFramesProvider.notifier).update((state) { + // // // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; + // // // }); + // // // finalizeRequestModel(requestId); + // // }, + // // onDone: () { + // // // finalizeRequestModel(requestId); + // // }, + // // ); + + // // // newRequestModel = requestModel.copyWith( + // // // responseStatus: 200, + // // // message: kResponseCodeReasons[200], + // // httpResponseModel: httpResponseModel, + // // isWorking: false, + // // ); + // } + + // // update state with response data + // // map = {...state!}; + // // map[requestId] = newRequestModel; + // // state = map; + + // unsave(); } void cancelRequest() { diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 367bb4128..ab4350f60 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:apidash_core/apidash_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -5,11 +7,12 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -class ResponsePane extends ConsumerWidget { +class ResponsePane extends ConsumerWidget{ const ResponsePane({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider)!; final isWorking = ref.watch( selectedRequestModelProvider.select((value) => value?.isWorking)) ?? false; @@ -19,16 +22,20 @@ class ResponsePane extends ConsumerWidget { selectedRequestModelProvider.select((value) => value?.responseStatus)); final message = ref .watch(selectedRequestModelProvider.select((value) => value?.message)); + final sseFramesNotifier = ref.watch(sseFramesProvider.select((state) => state[selectedId] ?? [])); - if (isWorking) { + + if (isWorking && sseFramesNotifier.isEmpty) { return SendingWidget( startSendingTime: startSendingTime, ); + } - if (responseStatus == null) { + + if (responseStatus == null && sseFramesNotifier.isEmpty) { return const NotSentWidget(); } - if (responseStatus == -1) { + if (responseStatus == -1 && sseFramesNotifier.isEmpty) { return message == kMsgRequestCancelled ? ErrorMessage( message: message, @@ -54,7 +61,6 @@ class ResponseDetails extends ConsumerWidget { .watch(selectedRequestModelProvider.select((value) => value?.message)); final responseModel = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel)); - return Column( children: [ ResponsePaneHeader( @@ -78,12 +84,22 @@ class ResponseTabs extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedId = ref.watch(selectedIdStateProvider); + final selectedId = ref.watch(selectedIdStateProvider)!; + final apiType = ref + .watch(selectedRequestModelProvider.select((value) => value?.apiType)); + return ResponseTabView( selectedId: selectedId, - children: const [ - ResponseBodyTab(), - ResponseHeadersTab(), + children: [ + if (apiType == APIType.rest || apiType == APIType.graphql) ...[ + SSEView(requestId: selectedId ), + const ResponseHeadersTab(), + ] + // ] else if (apiType == APIType.rest) ...const [ + // WebsocketResponseView(), + // ResponseHeadersTab(), + // ], + ], ); } @@ -106,15 +122,211 @@ class ResponseHeadersTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final requestHeaders = ref.watch(selectedRequestModelProvider + final requestHttpHeaders = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel?.requestHeaders)) ?? {}; - final responseHeaders = ref.watch(selectedRequestModelProvider + final responseHttpHeaders = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel?.headers)) ?? {}; - return ResponseHeaders( - responseHeaders: responseHeaders, - requestHeaders: requestHeaders, - ); + // final requestWebSocketHeaders = ref.watch(selectedRequestModelProvider + // .select((value) => value?.webSocketResponseModel?.requestHeaders)) ?? + // {}; + // final responseWebSocketHeaders = ref.watch(selectedRequestModelProvider + // .select((value) => value?.webSocketResponseModel?.headers)) ?? + // {}; + final apiType = ref + .watch(selectedRequestModelProvider.select((value) => value?.apiType)); + switch (apiType!) { + // case APIType.rest || APIType.graphql: + // return ResponseHeaders( + // responseHeaders: responseHttpHeaders, + // requestHeaders: requestHttpHeaders, + // ); + case APIType.rest: + return ResponseHeaders( + responseHeaders: responseHttpHeaders, + requestHeaders: requestHttpHeaders, + ); + case _: + return ResponseHeaders( + responseHeaders: responseHttpHeaders, + requestHeaders: requestHttpHeaders, + ); + } + } } + + + +class SSEView extends ConsumerStatefulWidget { + final String requestId; + const SSEView({super.key, required this.requestId}); + + @override + ConsumerState<SSEView> createState() => _SSEViewState(); +} + +class _SSEViewState extends ConsumerState<SSEView> { + final ScrollController _controller = ScrollController(); + bool _isAtBottom = true; + List<SSEEventModel> _pausedFrames = []; + + @override + void initState() { + super.initState(); + + _controller.addListener(() { + if (_controller.hasClients) { + final isAtBottom = _controller.position.pixels >= _controller.position.maxScrollExtent; + setState(() { + _isAtBottom = isAtBottom; + }); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final frames = ref.watch(sseFramesProvider.select((state) => state[widget.requestId] ?? [])); + + if (_isAtBottom) { + _pausedFrames = List.from(frames); + } + final displayFrames = _isAtBottom ? frames : _pausedFrames; + + return Scaffold( + appBar: AppBar(title: Text("SSE Stream - ${widget.requestId}")), + body: Column( + children: [ + Expanded( + child: ListView.builder( + controller: _controller, + itemCount: displayFrames.length, + itemBuilder: (context, index) { + final event = displayFrames[displayFrames.length - index - 1]; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (event.event.isEmpty) _buildTag("Event", "message", Colors.blue), + _buildData(event.data), + if (event.id != null && event.id!.isNotEmpty) _buildTag("ID", event.id!, Colors.orange), + if (event.retry != null) _buildTag("Retry", "${event.retry} ms", Colors.purple), + if (event.customFields != null && event.customFields!.isNotEmpty) + ...event.customFields!.entries.map( + (e) => _buildTag(e.key, e.value, Colors.grey), + ), + ], + ), + ), + ), + ); + }, + ), + ), + if (!_isAtBottom) + ElevatedButton( + onPressed: () { + _controller.animateTo( + _controller.position.maxScrollExtent, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOut, + ); + }, + child: const Text("Scroll to Bottom"), + ), + ], + ), + ); + } + + Widget _buildTag(String title, String value, Color color) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + title, + style: TextStyle(color: color, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 8), + Expanded(child: Text(value, style: const TextStyle(fontSize: 14))), + ], + ), + ), + const Divider(height: 10, thickness: 0.5), // Add a subtle separator line + ], + ); + } + + Widget _buildData(String data) { + bool _isExpanded = false; + + return StatefulBuilder( + builder: (context, setState) { + try { + final jsonData = json.decode(data); + final formattedJson = const JsonEncoder.withIndent(' ').convert(jsonData); + final bool shouldCollapse = formattedJson.length > 2; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text("Data", style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green)), + const Spacer(), + if (shouldCollapse) + IconButton( + icon: Icon(_isExpanded ? Icons.expand_less : Icons.expand_more), + onPressed: () => setState(() => _isExpanded = !_isExpanded), + iconSize: 18, + ), + ], + ), + Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + _isExpanded || !shouldCollapse ? formattedJson : "${formattedJson.substring(0, 100)}...", + style: const TextStyle(fontFamily: 'Courier', fontSize: 12), + ), + ), + const Divider(height: 10, thickness: 0.5), + ], + ); + } catch (_) { + return _buildTag("Data", data, Colors.green); + } + }, + ); + } +} \ No newline at end of file diff --git a/packages/apidash_core/lib/apidash_core.dart b/packages/apidash_core/lib/apidash_core.dart index 305bb5c81..a26b11b8f 100644 --- a/packages/apidash_core/lib/apidash_core.dart +++ b/packages/apidash_core/lib/apidash_core.dart @@ -4,7 +4,7 @@ export 'consts.dart'; export 'extensions/extensions.dart'; export 'models/models.dart'; export 'utils/utils.dart'; -export 'services/services.dart'; +export 'services/services.dart' hide StreamedResponse; export 'import_export/import_export.dart'; // Export 3rd party packages diff --git a/packages/apidash_core/lib/consts.dart b/packages/apidash_core/lib/consts.dart index 1d7044c74..26a24879d 100644 --- a/packages/apidash_core/lib/consts.dart +++ b/packages/apidash_core/lib/consts.dart @@ -3,6 +3,7 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), graphql("GraphQL", "GQL"); +// sse("SSE", "SSE"); const APIType(this.label, this.abbr); final String label; diff --git a/packages/apidash_core/lib/models/http_sse_event_model.dart b/packages/apidash_core/lib/models/http_sse_event_model.dart new file mode 100644 index 000000000..64aa58a3e --- /dev/null +++ b/packages/apidash_core/lib/models/http_sse_event_model.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'http_sse_event_model.freezed.dart'; +part 'http_sse_event_model.g.dart'; + +@freezed +class SSEEventModel with _$SSEEventModel { + const SSEEventModel._(); + + @JsonSerializable( + explicitToJson: true, + anyMap: true, + ) + const factory SSEEventModel({ + @Default("") String event, // Custom event name + @Default("") String data, // Event data (main payload) + String? id, // Last event ID for reconnection + int? retry, // Reconnect interval in milliseconds + Map<String, String>? customFields, // Additional metadata + }) = _SSEEventModel; + + factory SSEEventModel.fromJson(Map<String, Object?> json) => + _$SSEEventModelFromJson(json); + + /// Parses raw SSE data into an SSEEventModel + static SSEEventModel fromRawSSE(String rawEvent) { + final Map<String, String> fields = {}; + String? event, data, id; + int? retry; + + for (var line in LineSplitter.split(rawEvent)) { + if (line.startsWith(":")) { + continue; // Ignore comments + } + final parts = line.split(": "); + if (parts.length < 2) continue; + + final key = parts[0].trim(); + final value = parts.sublist(1).join(": ").trim(); + + switch (key) { + case "event": + event = value; + break; + case "data": + data = (data ?? "") + value + "\n"; // Multi-line data support + break; + case "id": + id = value; + break; + case "retry": + retry = int.tryParse(value); + break; + default: + fields[key] = value; // Store unknown fields as custom metadata + } + } + + return SSEEventModel( + event: event ?? "", + data: data?.trim() ?? "", + id: id, + retry: retry, + customFields: fields.isNotEmpty ? fields : null, + ); + } +} \ No newline at end of file diff --git a/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart b/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart new file mode 100644 index 000000000..d35f2c256 --- /dev/null +++ b/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart @@ -0,0 +1,271 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'http_sse_event_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity<T>(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +SSEEventModel _$SSEEventModelFromJson(Map<String, dynamic> json) { + return _SSEEventModel.fromJson(json); +} + +/// @nodoc +mixin _$SSEEventModel { + String get event => throw _privateConstructorUsedError; // Custom event name + String get data => + throw _privateConstructorUsedError; // Event data (main payload) + String? get id => + throw _privateConstructorUsedError; // Last event ID for reconnection + int? get retry => + throw _privateConstructorUsedError; // Reconnect interval in milliseconds + Map<String, String>? get customFields => throw _privateConstructorUsedError; + + /// Serializes this SSEEventModel to a JSON map. + Map<String, dynamic> toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SSEEventModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SSEEventModelCopyWith<SSEEventModel> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SSEEventModelCopyWith<$Res> { + factory $SSEEventModelCopyWith( + SSEEventModel value, $Res Function(SSEEventModel) then) = + _$SSEEventModelCopyWithImpl<$Res, SSEEventModel>; + @useResult + $Res call( + {String event, + String data, + String? id, + int? retry, + Map<String, String>? customFields}); +} + +/// @nodoc +class _$SSEEventModelCopyWithImpl<$Res, $Val extends SSEEventModel> + implements $SSEEventModelCopyWith<$Res> { + _$SSEEventModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SSEEventModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? event = null, + Object? data = null, + Object? id = freezed, + Object? retry = freezed, + Object? customFields = freezed, + }) { + return _then(_value.copyWith( + event: null == event + ? _value.event + : event // ignore: cast_nullable_to_non_nullable + as String, + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as int?, + customFields: freezed == customFields + ? _value.customFields + : customFields // ignore: cast_nullable_to_non_nullable + as Map<String, String>?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SSEEventModelImplCopyWith<$Res> + implements $SSEEventModelCopyWith<$Res> { + factory _$$SSEEventModelImplCopyWith( + _$SSEEventModelImpl value, $Res Function(_$SSEEventModelImpl) then) = + __$$SSEEventModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String event, + String data, + String? id, + int? retry, + Map<String, String>? customFields}); +} + +/// @nodoc +class __$$SSEEventModelImplCopyWithImpl<$Res> + extends _$SSEEventModelCopyWithImpl<$Res, _$SSEEventModelImpl> + implements _$$SSEEventModelImplCopyWith<$Res> { + __$$SSEEventModelImplCopyWithImpl( + _$SSEEventModelImpl _value, $Res Function(_$SSEEventModelImpl) _then) + : super(_value, _then); + + /// Create a copy of SSEEventModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? event = null, + Object? data = null, + Object? id = freezed, + Object? retry = freezed, + Object? customFields = freezed, + }) { + return _then(_$SSEEventModelImpl( + event: null == event + ? _value.event + : event // ignore: cast_nullable_to_non_nullable + as String, + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as String, + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + retry: freezed == retry + ? _value.retry + : retry // ignore: cast_nullable_to_non_nullable + as int?, + customFields: freezed == customFields + ? _value._customFields + : customFields // ignore: cast_nullable_to_non_nullable + as Map<String, String>?, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$SSEEventModelImpl extends _SSEEventModel { + const _$SSEEventModelImpl( + {this.event = "", + this.data = "", + this.id, + this.retry, + final Map<String, String>? customFields}) + : _customFields = customFields, + super._(); + + factory _$SSEEventModelImpl.fromJson(Map<String, dynamic> json) => + _$$SSEEventModelImplFromJson(json); + + @override + @JsonKey() + final String event; +// Custom event name + @override + @JsonKey() + final String data; +// Event data (main payload) + @override + final String? id; +// Last event ID for reconnection + @override + final int? retry; +// Reconnect interval in milliseconds + final Map<String, String>? _customFields; +// Reconnect interval in milliseconds + @override + Map<String, String>? get customFields { + final value = _customFields; + if (value == null) return null; + if (_customFields is EqualUnmodifiableMapView) return _customFields; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + String toString() { + return 'SSEEventModel(event: $event, data: $data, id: $id, retry: $retry, customFields: $customFields)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SSEEventModelImpl && + (identical(other.event, event) || other.event == event) && + (identical(other.data, data) || other.data == data) && + (identical(other.id, id) || other.id == id) && + (identical(other.retry, retry) || other.retry == retry) && + const DeepCollectionEquality() + .equals(other._customFields, _customFields)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, event, data, id, retry, + const DeepCollectionEquality().hash(_customFields)); + + /// Create a copy of SSEEventModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SSEEventModelImplCopyWith<_$SSEEventModelImpl> get copyWith => + __$$SSEEventModelImplCopyWithImpl<_$SSEEventModelImpl>(this, _$identity); + + @override + Map<String, dynamic> toJson() { + return _$$SSEEventModelImplToJson( + this, + ); + } +} + +abstract class _SSEEventModel extends SSEEventModel { + const factory _SSEEventModel( + {final String event, + final String data, + final String? id, + final int? retry, + final Map<String, String>? customFields}) = _$SSEEventModelImpl; + const _SSEEventModel._() : super._(); + + factory _SSEEventModel.fromJson(Map<String, dynamic> json) = + _$SSEEventModelImpl.fromJson; + + @override + String get event; // Custom event name + @override + String get data; // Event data (main payload) + @override + String? get id; // Last event ID for reconnection + @override + int? get retry; // Reconnect interval in milliseconds + @override + Map<String, String>? get customFields; + + /// Create a copy of SSEEventModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SSEEventModelImplCopyWith<_$SSEEventModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/apidash_core/lib/models/http_sse_event_model.g.dart b/packages/apidash_core/lib/models/http_sse_event_model.g.dart new file mode 100644 index 000000000..b85ec871d --- /dev/null +++ b/packages/apidash_core/lib/models/http_sse_event_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'http_sse_event_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SSEEventModelImpl _$$SSEEventModelImplFromJson(Map json) => + _$SSEEventModelImpl( + event: json['event'] as String? ?? "", + data: json['data'] as String? ?? "", + id: json['id'] as String?, + retry: (json['retry'] as num?)?.toInt(), + customFields: (json['customFields'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + ); + +Map<String, dynamic> _$$SSEEventModelImplToJson(_$SSEEventModelImpl instance) => + <String, dynamic>{ + 'event': instance.event, + 'data': instance.data, + 'id': instance.id, + 'retry': instance.retry, + 'customFields': instance.customFields, + }; diff --git a/packages/apidash_core/lib/models/models.dart b/packages/apidash_core/lib/models/models.dart index c50ec988f..71dda0ae9 100644 --- a/packages/apidash_core/lib/models/models.dart +++ b/packages/apidash_core/lib/models/models.dart @@ -1,3 +1,4 @@ export 'environment_model.dart'; export 'http_request_model.dart'; export 'http_response_model.dart'; +export 'http_sse_event_model.dart'; \ No newline at end of file diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index ad06a21d4..4457ebafb 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -9,15 +9,19 @@ import '../utils/utils.dart'; import 'http_client_manager.dart'; typedef HttpResponse = http.Response; +typedef StreamedResponse = http.StreamedResponse; final httpClientManager = HttpClientManager(); -Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( +Future<(HttpResponse?, Duration?, String?,StreamedResponse?)> sendHttpRequest( String requestId, APIType apiType, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, + Function(String event)? onData, + Function(Object error, StackTrace stackTrace)? onError, + Function()? onDone, }) async { final client = httpClientManager.createClient(requestId, noSSL: noSSL); @@ -35,6 +39,78 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( try { Stopwatch stopwatch = Stopwatch()..start(); if (apiType == APIType.rest) { + print("inside sse http serviec"); + // ✅ SSE Handling (POST, PUT, etc. with Body) + var request = http.Request(requestModel.method.name.toUpperCase(), requestUrl); + + request.headers.addAll(headers); + request.headers["Accept"] = "text/event-stream"; + request.headers["Cache-Control"] = "no-cache"; + request.headers["Connection"] = "keep-alive"; + + if (body != null) request.body = body; + + http.StreamedResponse streamedResponse = await client.send(request); + print("inside streamed response"); + final stream = streamedResponse.stream + .transform(utf8.decoder) + .transform(const LineSplitter()); + try{ stream.listen( + (event) { + onData?.call(event); + if (event.isNotEmpty) { + print('🔹 SSE Event Received: $event'); + print(event.toString()); + // final parsedEvent = SSEEventModel.fromRawSSE(event); + // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); + + } + }, + onError: (error) { + (error, stackTrace) => onError?.call(error, stackTrace); + print('🔹 SSE Error: $error'); + // ref.read(sseFramesProvider.notifier).update((state) { + // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; + // }); + // finalizeRequestModel(requestId); + }, + onDone: () { + onDone?.call(); + print('🔹 SSE Stream Done'); + // finalizeRequestModel(requestId); + }, + );}catch (e, stackTrace) { + print('🔹 Error connecting to SSE: $e'); + onError?.call(e, stackTrace); + //_reconnect(onData, onError, onDone); + } + // print("streamedResponse"+streamedResponse.headers.toString()); + // // var buffer = await convertStreamedResponse(streamedResponse); + // // print(buffer.body.toString()); + // if(streamedResponse.headers['content-type']?.contains('text/event-stream') == true){ + // print("has the content type"); + // } + // stopwatch.stop(); + // Stream<String>? utf8Stream; + // if (streamedResponse.statusCode == 200) { + + // utf8Stream = await streamedResponse.stream + // .transform(utf8.decoder) + // .transform(const LineSplitter()); + // print("utf8stream"+utf8Stream.toString()); + + // await for (final event in utf8Stream) { + // print("inside eventloop"); + // if (event.isNotEmpty) { + // print('🔹 SSE Event Received: $event'); + // } + // } + // } + // print("just before return ing null"); + return (null, stopwatch.elapsed, null,streamedResponse); + } + if (apiType == APIType.rest) { + print("iside second rest"); var isMultiPartRequest = requestModel.bodyContentType == ContentType.formdata; @@ -75,7 +151,7 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( stopwatch.stop(); http.Response convertedMultiPartResponse = await convertStreamedResponse(multiPartResponse); - return (convertedMultiPartResponse, stopwatch.elapsed, null); + return (convertedMultiPartResponse, stopwatch.elapsed, null,null); } } switch (requestModel.method) { @@ -122,20 +198,59 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( ); } stopwatch.stop(); - return (response, stopwatch.elapsed, null); + return (response, stopwatch.elapsed, null,null); } catch (e) { + print("inside catch"); if (httpClientManager.wasRequestCancelled(requestId)) { - return (null, null, kMsgRequestCancelled); + return (null, null, kMsgRequestCancelled,null); } - return (null, null, e.toString()); + return (null, null, e.toString(),null); } finally { + print("inside finallyy"); httpClientManager.closeClient(requestId); } } else { - return (null, null, uriRec.$2); + return (null, null, uriRec.$2,null); } } void cancelHttpRequest(String? requestId) { httpClientManager.cancelRequest(requestId); } + + +// void startSSE() async { +// var url = Uri.parse('https://sse.dev/test'); +// var client = HttpClient(); + +// try { +// var request = await client.getUrl(url); + + +// var response = await request.close(); + +// // Ensure it's a successful SSE connection +// if (response.statusCode == 200) { +// print("✅ Connected to SSE stream"); + +// response.transform(utf8.decoder).listen( +// (data) { +// print("🔹 SSE Event Received: $data"); +// }, +// onError: (error) { +// print("❌ SSE Error: $error"); +// }, +// onDone: () { +// print("🔌 SSE Stream Closed"); +// }, +// cancelOnError: true, +// ); +// } else { +// print("❌ Failed to connect: ${response.statusCode}"); +// } +// } catch (e) { +// print("❌ Exception: $e"); +// } +// } + + diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart new file mode 100644 index 000000000..3a0b9777d --- /dev/null +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -0,0 +1,113 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../consts.dart'; +import '../models/models.dart'; +import '../utils/utils.dart'; +import 'http_service.dart'; + + +Future<void> sendSSERequest( + String requestId, + APIType apiType, + HttpRequestModel requestModel, { + SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, + bool noSSL = false, + Function(int statusCode, Map<String, String> headers)? onConnect, + Function(String event)? onData, + Function(Object error, StackTrace stackTrace)? onError, + Function()? onDone, +}) async { + final client = httpClientManager.createClient(requestId, noSSL: noSSL); + + (Uri?, String?) uriRec = getValidRequestUri( + requestModel.url, + requestModel.enabledParams, + defaultUriScheme: defaultUriScheme, + ); + + if (uriRec.$1 != null) { + Uri requestUrl = uriRec.$1!; + Map<String, String> headers = requestModel.enabledHeadersMap; + HttpResponse? response; + String? body; + + Stopwatch stopwatch = Stopwatch()..start(); + if (apiType == APIType.rest) { + print("inside sse http serviec"); + // ✅ SSE Handling (POST, PUT, etc. with Body) + var request = http.Request(requestModel.method.name.toUpperCase(), requestUrl); + + request.headers.addAll(headers); + request.headers["Accept"] = "text/event-stream"; + request.headers["Cache-Control"] = "no-cache"; + request.headers["Connection"] = "keep-alive"; + + if (body != null) request.body = body; + + http.StreamedResponse streamedResponse = await client.send(request); + final int statusCode = streamedResponse.statusCode; + final Map<String, String> responseHeaders = streamedResponse.headers; + + onConnect?.call(statusCode, responseHeaders); + + print("inside streamed response"); + final stream = streamedResponse.stream + .transform(utf8.decoder) + .transform(const LineSplitter()); + try{ stream.listen( + (event) { + onData?.call(event); + if (event.isNotEmpty) { + print('🔹 SSE Event Received: $event'); + print(event.toString()); + // final parsedEvent = SSEEventModel.fromRawSSE(event); + // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); + + } + }, + onError: (error) { + (error, stackTrace) => onError?.call(error, stackTrace); + print('🔹 SSE Error: $error'); + // ref.read(sseFramesProvider.notifier).update((state) { + // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; + // }); + // finalizeRequestModel(requestId); + }, + onDone: () { + onDone?.call(); + print('🔹 SSE Stream Done'); + // finalizeRequestModel(requestId); + }, + );}catch (e, stackTrace) { + print('🔹 Error connecting to SSE: $e'); + onError?.call(e, stackTrace); + //_reconnect(onData, onError, onDone); + } + // print("streamedResponse"+streamedResponse.headers.toString()); + // // var buffer = await convertStreamedResponse(streamedResponse); + // // print(buffer.body.toString()); + // if(streamedResponse.headers['content-type']?.contains('text/event-stream') == true){ + // print("has the content type"); + // } + // stopwatch.stop(); + // Stream<String>? utf8Stream; + // if (streamedResponse.statusCode == 200) { + + // utf8Stream = await streamedResponse.stream + // .transform(utf8.decoder) + // .transform(const LineSplitter()); + // print("utf8stream"+utf8Stream.toString()); + + // await for (final event in utf8Stream) { + // print("inside eventloop"); + // if (event.isNotEmpty) { + // print('🔹 SSE Event Received: $event'); + // } + // } + // } + // print("just before return ing null"); + } + } + +} \ No newline at end of file diff --git a/packages/apidash_core/lib/services/services.dart b/packages/apidash_core/lib/services/services.dart index d155e9c70..a5bb7fbd0 100644 --- a/packages/apidash_core/lib/services/services.dart +++ b/packages/apidash_core/lib/services/services.dart @@ -1,2 +1,3 @@ export 'http_client_manager.dart'; export 'http_service.dart'; +export 'http_sse_service.dart'; \ No newline at end of file From 7aa25c71a38bb9a746666dc3a47877fcabfa66d2 Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Tue, 1 Apr 2025 01:08:27 +0530 Subject: [PATCH 2/8] corrected sse service --- lib/providers/collection_providers.dart | 233 +++++++++--------- .../request_pane/request_pane.dart | 2 +- .../details_card/response_pane.dart | 27 +- .../home_page/editor_pane/url_card.dart | 6 +- lib/utils/ui_utils.dart | 1 + lib/widgets/texts.dart | 1 + packages/apidash_core/lib/consts.dart | 4 +- .../lib/models/http_response_model.dart | 2 + .../models/http_response_model.freezed.dart | 44 +++- .../lib/models/http_response_model.g.dart | 5 + .../lib/services/http_client_manager.dart | 15 +- .../lib/services/http_service.dart | 173 ++----------- .../lib/services/http_sse_service.dart | 213 +++++++++------- .../lib/utils/http_request_utils.dart | 2 +- packages/apidash_core/pubspec.yaml | 1 + .../lib/tokens/colors.dart | 2 + pubspec.lock | 8 + 17 files changed, 349 insertions(+), 390 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index c6b061854..c65cd814e 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -22,10 +22,10 @@ final selectedRequestModelProvider = StateProvider<RequestModel?>((ref) { } }); -class SSEFramesNotifier extends StateNotifier<Map<String, List<dynamic>>> { +class SSEFramesNotifier extends StateNotifier<Map<String, List<SSEEventModel>>> { SSEFramesNotifier() : super({}); - void addFrame(String requestId, dynamic frame) { + void addFrame(String requestId, String frame) { state = { ...state, requestId: [...(state[requestId] ?? []), SSEEventModel.fromRawSSE(frame)], @@ -50,7 +50,7 @@ class SSEFramesNotifier extends StateNotifier<Map<String, List<dynamic>>> { // Provide it globally final sseFramesProvider = - StateNotifierProvider<SSEFramesNotifier, Map<String, List<dynamic>>>( + StateNotifierProvider<SSEFramesNotifier, Map<String, List<SSEEventModel>>>( (ref) => SSEFramesNotifier(), ); @@ -297,6 +297,7 @@ class CollectionStateNotifier unsave(); } + Future<void> sendRequest() async { final requestId = ref.read(selectedIdStateProvider); ref.read(codePaneVisibleStateProvider.notifier).state = false; @@ -324,140 +325,132 @@ class CollectionStateNotifier state = map; bool noSSL = ref.read(settingsProvider).isSSLDisabled; - - state = map; - var responseRec = await sendSSERequest( - requestId, - apiType, - substitutedHttpRequestModel, - defaultUriScheme: defaultUriScheme, - noSSL: noSSL, - onConnect: (statusCode, headers) { + if(apiType == APIType.sse){ + print("inside sendrequest apitype,sse"); + List<SSEEventModel> frames; + await sendSSERequest( + requestId, + apiType, + substitutedHttpRequestModel, + defaultUriScheme: defaultUriScheme, + noSSL: noSSL, + onConnect: (statusCode, responseHeaders, requestHeaders,duration) { + print("inside sse onconnect"); + ref.read(sseFramesProvider.notifier).clearFrames(requestId); map = {...state!}; + print(statusCode); + print(responseHeaders); + print(requestHeaders); map[requestId] = requestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], - httpResponseModel: null, + httpResponseModel: baseHttpResponseModel.copyWith( + statusCode: statusCode, + headers: responseHeaders, + requestHeaders: requestHeaders, + time: duration, + ), isWorking: true, ); + state = map; }, onData: (response) { ref.read(sseFramesProvider.notifier).addFrame(requestId, response); }, + onError: (error, stackTrace) { + frames = ref.read(sseFramesProvider)[requestId] ?? []; + map = {...state!}; + map[requestId] = requestModel.copyWith( + responseStatus: 500, + message: error.toString(), + httpResponseModel: baseHttpResponseModel.copyWith( + sseEvents: frames, + ), + isWorking: false, + ); + state = map; + }, + onDone: () { + frames = ref.read(sseFramesProvider)[requestId] ?? []; + map = {...state!}; + map[requestId] = requestModel.copyWith( + httpResponseModel: baseHttpResponseModel.copyWith( + sseEvents: frames, + ), + isWorking: false, + ); + state = map; + }, + onCancel: () { + frames = ref.read(sseFramesProvider)[requestId] ?? []; + map = {...state!}; + map[requestId] = requestModel.copyWith( + responseStatus: -1, + message: kMsgRequestCancelled, + isWorking: false, + ); + state = map; + }, + ); + return; + } + var responseRec = await sendHttpRequest( + requestId, + apiType, + substitutedHttpRequestModel, + defaultUriScheme: defaultUriScheme, + noSSL: noSSL, ); late final RequestModel newRequestModel; - // if (responseRec.$1 == null && responseRec.$4 == null) { - // print("inside first if"); - - // newRequestModel = requestModel.copyWith( - // responseStatus: -1, - // message: responseRec.$3, - // isWorking: false, - // ); - // } else if(responseRec.$1 != null){ - // print("inside second if"); - // final httpResponseModel = baseHttpResponseModel.fromResponse( - // response: responseRec.$1!, - // time: responseRec.$2!, - // ); - // // if(responseRec.$1!.headers['content-type']?.contains('text/event-stream') ?? false){ - - // // Stream.value(responseRec.$1!.bodyBytes as List<int>).byteStream.transform(utf8.decoder).transform(const LineSplitter()); - - // // await for (final event in utf8Stream) { - // // if (event.isNotEmpty) { - // // yield SSEEventModel.fromRawSSE(event); // ✅ Emit each event lazily - // // } - // // } - // // } - // // } - // int statusCode = responseRec.$1!.statusCode; - // newRequestModel = requestModel.copyWith( - // responseStatus: statusCode, - // message: kResponseCodeReasons[statusCode], - // httpResponseModel: httpResponseModel, - // isWorking: false, - // ); - // String newHistoryId = getNewUuid(); - // HistoryRequestModel model = HistoryRequestModel( - // historyId: newHistoryId, - // metaData: HistoryMetaModel( - // historyId: newHistoryId, - // requestId: requestId, - // apiType: requestModel.apiType, - // name: requestModel.name, - // url: substitutedHttpRequestModel.url, - // method: substitutedHttpRequestModel.method, - // responseStatus: statusCode, - // timeStamp: DateTime.now(), - // ), - // httpRequestModel: substitutedHttpRequestModel, - // httpResponseModel: httpResponseModel, - // ); - // ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); - // }else if(responseRec.$4 != null){ - // print("inside else if"); - - // // final httpResponseModel = baseHttpResponseModel.fromResponse( - // // response: responseRec.$4!, - // // time: responseRec.$2!, - // // ); - // // if(responseRec.$1!.headers['content-type']?.contains('text/event-stream') ?? false){ - - // // Stream.value(responseRec.$1!.bodyBytes as List<int>).byteStream.transform(utf8.decoder).transform(const LineSplitter()); - - // // await for (final event in utf8Stream) { - // // if (event.isNotEmpty) { - // // yield SSEEventModel.fromRawSSE(event); // ✅ Emit each event lazily - // // } - // // } - // // } - // // } - // // var streamedResponse = responseRec.$4!; - // // final stream = streamedResponse.stream - // // .transform(utf8.decoder) - // // .transform(const LineSplitter()); - - // // stream.listen( - // // (event) { - // // if (event.isNotEmpty) { - // // print('🔹 SSE Event Received: $event'); - // // print(event.toString()); - // // // final parsedEvent = SSEEventModel.fromRawSSE(event); - // // // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); - - // // } - // // }, - // // onError: (error) { - // // // ref.read(sseFramesProvider.notifier).update((state) { - // // // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; - // // // }); - // // // finalizeRequestModel(requestId); - // // }, - // // onDone: () { - // // // finalizeRequestModel(requestId); - // // }, - // // ); - - // // // newRequestModel = requestModel.copyWith( - // // // responseStatus: 200, - // // // message: kResponseCodeReasons[200], - // // httpResponseModel: httpResponseModel, - // // isWorking: false, - // // ); - // } - - // // update state with response data - // // map = {...state!}; - // // map[requestId] = newRequestModel; - // // state = map; - - // unsave(); + if (responseRec.$1 == null) { + newRequestModel = requestModel.copyWith( + responseStatus: -1, + message: responseRec.$3, + isWorking: false, + ); + } else { + final httpResponseModel = baseHttpResponseModel.fromResponse( + response: responseRec.$1!, + time: responseRec.$2!, + ); + int statusCode = responseRec.$1!.statusCode; + newRequestModel = requestModel.copyWith( + responseStatus: statusCode, + message: kResponseCodeReasons[statusCode], + httpResponseModel: httpResponseModel, + isWorking: false, + ); + String newHistoryId = getNewUuid(); + HistoryRequestModel model = HistoryRequestModel( + historyId: newHistoryId, + metaData: HistoryMetaModel( + historyId: newHistoryId, + requestId: requestId, + apiType: requestModel.apiType, + name: requestModel.name, + url: substitutedHttpRequestModel.url, + method: substitutedHttpRequestModel.method, + responseStatus: statusCode, + timeStamp: DateTime.now(), + ), + httpRequestModel: substitutedHttpRequestModel, + httpResponseModel: httpResponseModel, + ); + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); + } + + // update state with response data + map = {...state!}; + map[requestId] = newRequestModel; + state = map; + + unsave(); } + void cancelRequest() { final id = ref.read(selectedIdStateProvider); cancelHttpRequest(id); diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index 9c852c71b..c15deab83 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -15,7 +15,7 @@ class EditRequestPane extends ConsumerWidget { final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); return switch (apiType) { - APIType.rest => const EditRestRequestPane(), + APIType.rest || APIType.sse => const EditRestRequestPane(), APIType.graphql => const EditGraphQLRequestPane(), _ => kSizedBoxEmpty, }; diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index ab4350f60..14440d92d 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -26,6 +26,7 @@ class ResponsePane extends ConsumerWidget{ if (isWorking && sseFramesNotifier.isEmpty) { + print("inside sending widget"+sseFramesNotifier.isEmpty.toString()); return SendingWidget( startSendingTime: startSendingTime, ); @@ -91,14 +92,13 @@ class ResponseTabs extends ConsumerWidget { return ResponseTabView( selectedId: selectedId, children: [ - if (apiType == APIType.rest || apiType == APIType.graphql) ...[ - SSEView(requestId: selectedId ), - const ResponseHeadersTab(), + if (apiType == APIType.rest || apiType == APIType.graphql) ...const[ + ResponseBodyTab(), + ResponseHeadersTab(), + ]else ...[ + SSEView(requestId: selectedId), + const ResponseHeadersTab(), ] - // ] else if (apiType == APIType.rest) ...const [ - // WebsocketResponseView(), - // ResponseHeadersTab(), - // ], ], ); @@ -128,20 +128,11 @@ class ResponseHeadersTab extends ConsumerWidget { final responseHttpHeaders = ref.watch(selectedRequestModelProvider .select((value) => value?.httpResponseModel?.headers)) ?? {}; - // final requestWebSocketHeaders = ref.watch(selectedRequestModelProvider - // .select((value) => value?.webSocketResponseModel?.requestHeaders)) ?? - // {}; - // final responseWebSocketHeaders = ref.watch(selectedRequestModelProvider - // .select((value) => value?.webSocketResponseModel?.headers)) ?? - // {}; + final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); switch (apiType!) { - // case APIType.rest || APIType.graphql: - // return ResponseHeaders( - // responseHeaders: responseHttpHeaders, - // requestHeaders: requestHttpHeaders, - // ); + case APIType.rest: return ResponseHeaders( responseHeaders: responseHttpHeaders, diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 829bc5c97..a5828d0f7 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -33,12 +33,12 @@ class EditorPaneRequestURLCard extends ConsumerWidget { ? Row( children: [ switch (apiType) { - APIType.rest => const DropdownButtonHTTPMethod(), + APIType.rest || APIType.sse => const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, null => kSizedBoxEmpty, }, switch (apiType) { - APIType.rest => kHSpacer5, + APIType.rest || APIType.sse => kHSpacer5, _ => kHSpacer8, }, const Expanded( @@ -49,7 +49,7 @@ class EditorPaneRequestURLCard extends ConsumerWidget { : Row( children: [ switch (apiType) { - APIType.rest => const DropdownButtonHTTPMethod(), + APIType.rest || APIType.sse=> const DropdownButtonHTTPMethod(), APIType.graphql => kSizedBoxEmpty, null => kSizedBoxEmpty, }, diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 4f72a1a3f..50458f72a 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -36,6 +36,7 @@ Color getAPIColor( method, ), APIType.graphql => kColorGQL, + APIType.sse => kColorSSE, }; if (brightness == Brightness.dark) { col = getDarkModeColor(col); diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index c64a71b27..834936c56 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -20,6 +20,7 @@ class SidebarRequestCardTextBox extends StatelessWidget { switch (apiType) { APIType.rest => method.abbr, APIType.graphql => apiType.abbr, + APIType.sse => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/packages/apidash_core/lib/consts.dart b/packages/apidash_core/lib/consts.dart index 26a24879d..6bb48bcbf 100644 --- a/packages/apidash_core/lib/consts.dart +++ b/packages/apidash_core/lib/consts.dart @@ -2,8 +2,8 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), - graphql("GraphQL", "GQL"); -// sse("SSE", "SSE"); + graphql("GraphQL", "GQL"), + sse("SSE", "SSE"); const APIType(this.label, this.abbr); final String label; diff --git a/packages/apidash_core/lib/models/http_response_model.dart b/packages/apidash_core/lib/models/http_response_model.dart index 914aaa577..5e49a4f9e 100644 --- a/packages/apidash_core/lib/models/http_response_model.dart +++ b/packages/apidash_core/lib/models/http_response_model.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:apidash_core/apidash_core.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:collection/collection.dart' show mergeMaps; import 'package:http/http.dart'; @@ -56,6 +57,7 @@ class HttpResponseModel with _$HttpResponseModel { String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, @DurationConverter() Duration? time, + List<SSEEventModel>? sseEvents, }) = _HttpResponseModel; factory HttpResponseModel.fromJson(Map<String, Object?> json) => diff --git a/packages/apidash_core/lib/models/http_response_model.freezed.dart b/packages/apidash_core/lib/models/http_response_model.freezed.dart index 45071986a..68fe1923b 100644 --- a/packages/apidash_core/lib/models/http_response_model.freezed.dart +++ b/packages/apidash_core/lib/models/http_response_model.freezed.dart @@ -29,6 +29,7 @@ mixin _$HttpResponseModel { Uint8List? get bodyBytes => throw _privateConstructorUsedError; @DurationConverter() Duration? get time => throw _privateConstructorUsedError; + List<SSEEventModel>? get sseEvents => throw _privateConstructorUsedError; /// Serializes this HttpResponseModel to a JSON map. Map<String, dynamic> toJson() => throw _privateConstructorUsedError; @@ -53,7 +54,8 @@ abstract class $HttpResponseModelCopyWith<$Res> { String? body, String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, - @DurationConverter() Duration? time}); + @DurationConverter() Duration? time, + List<SSEEventModel>? sseEvents}); } /// @nodoc @@ -78,6 +80,7 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseEvents = freezed, }) { return _then(_value.copyWith( statusCode: freezed == statusCode @@ -108,6 +111,10 @@ class _$HttpResponseModelCopyWithImpl<$Res, $Val extends HttpResponseModel> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseEvents: freezed == sseEvents + ? _value.sseEvents + : sseEvents // ignore: cast_nullable_to_non_nullable + as List<SSEEventModel>?, ) as $Val); } } @@ -127,7 +134,8 @@ abstract class _$$HttpResponseModelImplCopyWith<$Res> String? body, String? formattedBody, @Uint8ListConverter() Uint8List? bodyBytes, - @DurationConverter() Duration? time}); + @DurationConverter() Duration? time, + List<SSEEventModel>? sseEvents}); } /// @nodoc @@ -150,6 +158,7 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> Object? formattedBody = freezed, Object? bodyBytes = freezed, Object? time = freezed, + Object? sseEvents = freezed, }) { return _then(_$HttpResponseModelImpl( statusCode: freezed == statusCode @@ -180,6 +189,10 @@ class __$$HttpResponseModelImplCopyWithImpl<$Res> ? _value.time : time // ignore: cast_nullable_to_non_nullable as Duration?, + sseEvents: freezed == sseEvents + ? _value._sseEvents + : sseEvents // ignore: cast_nullable_to_non_nullable + as List<SSEEventModel>?, )); } } @@ -195,9 +208,11 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { this.body, this.formattedBody, @Uint8ListConverter() this.bodyBytes, - @DurationConverter() this.time}) + @DurationConverter() this.time, + final List<SSEEventModel>? sseEvents}) : _headers = headers, _requestHeaders = requestHeaders, + _sseEvents = sseEvents, super._(); factory _$HttpResponseModelImpl.fromJson(Map<String, dynamic> json) => @@ -235,10 +250,19 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { @override @DurationConverter() final Duration? time; + final List<SSEEventModel>? _sseEvents; + @override + List<SSEEventModel>? get sseEvents { + final value = _sseEvents; + if (value == null) return null; + if (_sseEvents is EqualUnmodifiableListView) return _sseEvents; + // 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, sseEvents: $sseEvents)'; } @override @@ -255,7 +279,9 @@ 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._sseEvents, _sseEvents)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -268,7 +294,8 @@ class _$HttpResponseModelImpl extends _HttpResponseModel { body, formattedBody, const DeepCollectionEquality().hash(bodyBytes), - time); + time, + const DeepCollectionEquality().hash(_sseEvents)); /// Create a copy of HttpResponseModel /// with the given fields replaced by the non-null parameter values. @@ -295,7 +322,8 @@ abstract class _HttpResponseModel extends HttpResponseModel { final String? body, final String? formattedBody, @Uint8ListConverter() final Uint8List? bodyBytes, - @DurationConverter() final Duration? time}) = _$HttpResponseModelImpl; + @DurationConverter() final Duration? time, + final List<SSEEventModel>? sseEvents}) = _$HttpResponseModelImpl; const _HttpResponseModel._() : super._(); factory _HttpResponseModel.fromJson(Map<String, dynamic> json) = @@ -317,6 +345,8 @@ abstract class _HttpResponseModel extends HttpResponseModel { @override @DurationConverter() Duration? get time; + @override + List<SSEEventModel>? get sseEvents; /// Create a copy of HttpResponseModel /// with the given fields replaced by the non-null parameter values. diff --git a/packages/apidash_core/lib/models/http_response_model.g.dart b/packages/apidash_core/lib/models/http_response_model.g.dart index 186fd152f..f3e174018 100644 --- a/packages/apidash_core/lib/models/http_response_model.g.dart +++ b/packages/apidash_core/lib/models/http_response_model.g.dart @@ -20,6 +20,10 @@ _$HttpResponseModelImpl _$$HttpResponseModelImplFromJson(Map json) => bodyBytes: const Uint8ListConverter().fromJson(json['bodyBytes'] as List<int>?), time: const DurationConverter().fromJson((json['time'] as num?)?.toInt()), + sseEvents: (json['sseEvents'] as List<dynamic>?) + ?.map((e) => + SSEEventModel.fromJson(Map<String, Object?>.from(e as Map))) + .toList(), ); Map<String, dynamic> _$$HttpResponseModelImplToJson( @@ -32,4 +36,5 @@ Map<String, dynamic> _$$HttpResponseModelImplToJson( 'formattedBody': instance.formattedBody, 'bodyBytes': const Uint8ListConverter().toJson(instance.bodyBytes), 'time': const DurationConverter().toJson(instance.time), + 'sseEvents': instance.sseEvents?.map((e) => e.toJson()).toList(), }; diff --git a/packages/apidash_core/lib/services/http_client_manager.dart b/packages/apidash_core/lib/services/http_client_manager.dart index bec23214f..2dc13d821 100644 --- a/packages/apidash_core/lib/services/http_client_manager.dart +++ b/packages/apidash_core/lib/services/http_client_manager.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http/io_client.dart'; @@ -15,7 +14,7 @@ class HttpClientManager { static final HttpClientManager _instance = HttpClientManager._internal(); static const int _maxCancelledRequests = 100; final Map<String, http.Client> _clients = {}; - final Queue<String> _cancelledRequests = Queue(); + final Set<String> _cancelledRequests = {}; factory HttpClientManager() { return _instance; @@ -38,9 +37,9 @@ class HttpClientManager { _clients[requestId]?.close(); _clients.remove(requestId); - _cancelledRequests.addLast(requestId); - while (_cancelledRequests.length > _maxCancelledRequests) { - _cancelledRequests.removeFirst(); + _cancelledRequests.add(requestId); + if (_cancelledRequests.length > _maxCancelledRequests) { + _cancelledRequests.remove(_cancelledRequests.first); } } } @@ -49,6 +48,10 @@ class HttpClientManager { return _cancelledRequests.contains(requestId); } + void removeCancelledRequest(String requestId) { + _cancelledRequests.remove(requestId); + } + void closeClient(String requestId) { if (_clients.containsKey(requestId)) { _clients[requestId]?.close(); @@ -59,4 +62,4 @@ class HttpClientManager { bool hasActiveClient(String requestId) { return _clients.containsKey(requestId); } -} +} \ No newline at end of file diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index 4457ebafb..6f5488aad 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -9,20 +9,19 @@ import '../utils/utils.dart'; import 'http_client_manager.dart'; typedef HttpResponse = http.Response; -typedef StreamedResponse = http.StreamedResponse; final httpClientManager = HttpClientManager(); -Future<(HttpResponse?, Duration?, String?,StreamedResponse?)> sendHttpRequest( +Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( String requestId, APIType apiType, HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, - Function(String event)? onData, - Function(Object error, StackTrace stackTrace)? onError, - Function()? onDone, }) async { + if (httpClientManager.wasRequestCancelled(requestId)) { + httpClientManager.removeCancelledRequest(requestId); + } final client = httpClientManager.createClient(requestId, noSSL: noSSL); (Uri?, String?) uriRec = getValidRequestUri( @@ -39,78 +38,6 @@ Future<(HttpResponse?, Duration?, String?,StreamedResponse?)> sendHttpRequest( try { Stopwatch stopwatch = Stopwatch()..start(); if (apiType == APIType.rest) { - print("inside sse http serviec"); - // ✅ SSE Handling (POST, PUT, etc. with Body) - var request = http.Request(requestModel.method.name.toUpperCase(), requestUrl); - - request.headers.addAll(headers); - request.headers["Accept"] = "text/event-stream"; - request.headers["Cache-Control"] = "no-cache"; - request.headers["Connection"] = "keep-alive"; - - if (body != null) request.body = body; - - http.StreamedResponse streamedResponse = await client.send(request); - print("inside streamed response"); - final stream = streamedResponse.stream - .transform(utf8.decoder) - .transform(const LineSplitter()); - try{ stream.listen( - (event) { - onData?.call(event); - if (event.isNotEmpty) { - print('🔹 SSE Event Received: $event'); - print(event.toString()); - // final parsedEvent = SSEEventModel.fromRawSSE(event); - // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); - - } - }, - onError: (error) { - (error, stackTrace) => onError?.call(error, stackTrace); - print('🔹 SSE Error: $error'); - // ref.read(sseFramesProvider.notifier).update((state) { - // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; - // }); - // finalizeRequestModel(requestId); - }, - onDone: () { - onDone?.call(); - print('🔹 SSE Stream Done'); - // finalizeRequestModel(requestId); - }, - );}catch (e, stackTrace) { - print('🔹 Error connecting to SSE: $e'); - onError?.call(e, stackTrace); - //_reconnect(onData, onError, onDone); - } - // print("streamedResponse"+streamedResponse.headers.toString()); - // // var buffer = await convertStreamedResponse(streamedResponse); - // // print(buffer.body.toString()); - // if(streamedResponse.headers['content-type']?.contains('text/event-stream') == true){ - // print("has the content type"); - // } - // stopwatch.stop(); - // Stream<String>? utf8Stream; - // if (streamedResponse.statusCode == 200) { - - // utf8Stream = await streamedResponse.stream - // .transform(utf8.decoder) - // .transform(const LineSplitter()); - // print("utf8stream"+utf8Stream.toString()); - - // await for (final event in utf8Stream) { - // print("inside eventloop"); - // if (event.isNotEmpty) { - // print('🔹 SSE Event Received: $event'); - // } - // } - // } - // print("just before return ing null"); - return (null, stopwatch.elapsed, null,streamedResponse); - } - if (apiType == APIType.rest) { - print("iside second rest"); var isMultiPartRequest = requestModel.bodyContentType == ContentType.formdata; @@ -147,37 +74,27 @@ Future<(HttpResponse?, Duration?, String?,StreamedResponse?)> sendHttpRequest( } } http.StreamedResponse multiPartResponse = - await multiPartRequest.send(); + await client.send(multiPartRequest); + stopwatch.stop(); http.Response convertedMultiPartResponse = await convertStreamedResponse(multiPartResponse); - return (convertedMultiPartResponse, stopwatch.elapsed, null,null); + return (convertedMultiPartResponse, stopwatch.elapsed, null); } } - switch (requestModel.method) { - case HTTPVerb.get: - response = await client.get(requestUrl, headers: headers); - break; - case HTTPVerb.head: - response = await client.head(requestUrl, headers: headers); - break; - case HTTPVerb.post: - response = - await client.post(requestUrl, headers: headers, body: body); - break; - case HTTPVerb.put: - response = - await client.put(requestUrl, headers: headers, body: body); - break; - case HTTPVerb.patch: - response = - await client.patch(requestUrl, headers: headers, body: body); - break; - case HTTPVerb.delete: - response = - await client.delete(requestUrl, headers: headers, body: body); - break; - } + response = switch (requestModel.method) { + HTTPVerb.get => await client.get(requestUrl, headers: headers), + HTTPVerb.head => response = + await client.head(requestUrl, headers: headers), + HTTPVerb.post => response = + await client.post(requestUrl, headers: headers, body: body), + HTTPVerb.put => response = + await client.put(requestUrl, headers: headers, body: body), + HTTPVerb.patch => response = + await client.patch(requestUrl, headers: headers, body: body), + HTTPVerb.delete => response = + await client.delete(requestUrl, headers: headers, body: body), + }; } if (apiType == APIType.graphql) { var requestBody = getGraphQLBody(requestModel); @@ -198,59 +115,21 @@ Future<(HttpResponse?, Duration?, String?,StreamedResponse?)> sendHttpRequest( ); } stopwatch.stop(); - return (response, stopwatch.elapsed, null,null); + return (response, stopwatch.elapsed, null); } catch (e) { - print("inside catch"); if (httpClientManager.wasRequestCancelled(requestId)) { - return (null, null, kMsgRequestCancelled,null); + return (null, null, kMsgRequestCancelled); } - return (null, null, e.toString(),null); + return (null, null, e.toString()); } finally { - print("inside finallyy"); httpClientManager.closeClient(requestId); } } else { - return (null, null, uriRec.$2,null); + return (null, null, uriRec.$2); } } void cancelHttpRequest(String? requestId) { + print("cancelHttpRequest: $requestId"); httpClientManager.cancelRequest(requestId); -} - - -// void startSSE() async { -// var url = Uri.parse('https://sse.dev/test'); -// var client = HttpClient(); - -// try { -// var request = await client.getUrl(url); - - -// var response = await request.close(); - -// // Ensure it's a successful SSE connection -// if (response.statusCode == 200) { -// print("✅ Connected to SSE stream"); - -// response.transform(utf8.decoder).listen( -// (data) { -// print("🔹 SSE Event Received: $data"); -// }, -// onError: (error) { -// print("❌ SSE Error: $error"); -// }, -// onDone: () { -// print("🔌 SSE Stream Closed"); -// }, -// cancelOnError: true, -// ); -// } else { -// print("❌ Failed to connect: ${response.statusCode}"); -// } -// } catch (e) { -// print("❌ Exception: $e"); -// } -// } - - +} \ No newline at end of file diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart index 3a0b9777d..2055d9bb2 100644 --- a/packages/apidash_core/lib/services/http_sse_service.dart +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -1,11 +1,37 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:http_interceptor/http_interceptor.dart'; +import 'package:seed/consts.dart'; import '../consts.dart'; import '../models/models.dart'; import '../utils/utils.dart'; import 'http_service.dart'; +class LoggingInterceptor implements InterceptorContract { + Map<String, String>? interceptedHeaders; + + @override + FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async { + interceptedHeaders = request.headers; + print("Headers: \${request.headers}"); + return request; + } + + @override + Future<BaseResponse> interceptResponse({required BaseResponse response}) async { + print("Response: \${response.statusCode}"); + print("Headers: \${response.headers}"); + return response; + } + + @override + FutureOr<bool> shouldInterceptRequest() => true; + + @override + FutureOr<bool> shouldInterceptResponse() => true; +} Future<void> sendSSERequest( String requestId, @@ -13,101 +39,118 @@ Future<void> sendSSERequest( HttpRequestModel requestModel, { SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, - Function(int statusCode, Map<String, String> headers)? onConnect, + Function(int statusCode, Map<String, String> headers, Map<String, String>? interceptedHeaders, Duration?)? onConnect, Function(String event)? onData, Function(Object error, StackTrace stackTrace)? onError, Function()? onDone, + Function()? onCancel, }) async { - final client = httpClientManager.createClient(requestId, noSSL: noSSL); + if (httpClientManager.wasRequestCancelled(requestId)) { + httpClientManager.removeCancelledRequest(requestId); + } + final interceptor = LoggingInterceptor(); + final baseClient = httpClientManager.createClient(requestId, noSSL: noSSL); + final client = InterceptedClient.build(interceptors: [interceptor], client: baseClient); - (Uri?, String?) uriRec = getValidRequestUri( - requestModel.url, - requestModel.enabledParams, - defaultUriScheme: defaultUriScheme, - ); + try { + (Uri?, String?) uriRec = getValidRequestUri( + requestModel.url, + requestModel.enabledParams, + defaultUriScheme: defaultUriScheme, + ); + if (uriRec.$1 == null) return; - if (uriRec.$1 != null) { Uri requestUrl = uriRec.$1!; - Map<String, String> headers = requestModel.enabledHeadersMap; - HttpResponse? response; - String? body; - - Stopwatch stopwatch = Stopwatch()..start(); - if (apiType == APIType.rest) { - print("inside sse http serviec"); - // ✅ SSE Handling (POST, PUT, etc. with Body) - var request = http.Request(requestModel.method.name.toUpperCase(), requestUrl); + var isMultiPartRequest = requestModel.bodyContentType == ContentType.formdata; + var stopwatch = Stopwatch()..start(); + late http.StreamedResponse streamedResponse; + if (apiType == APIType.sse) { + print("Inside SSE HTTP service"); + + if (isMultiPartRequest) { + var multiPartRequest = http.MultipartRequest( + requestModel.method.name.toUpperCase(), + requestUrl, + ); + multiPartRequest.headers.addAll(requestModel.enabledHeadersMap); + + for (var formData in requestModel.formDataList) { + if (formData.type == FormDataType.text) { + multiPartRequest.fields[formData.name] = formData.value; + } else { + multiPartRequest.files.add(await http.MultipartFile.fromPath(formData.name, formData.value)); + } + } + + streamedResponse = await multiPartRequest.send(); + stopwatch.stop(); + - request.headers.addAll(headers); - request.headers["Accept"] = "text/event-stream"; - request.headers["Cache-Control"] = "no-cache"; - request.headers["Connection"] = "keep-alive"; - - if (body != null) request.body = body; + + } else { + var request = http.Request(requestModel.method.name.toUpperCase(), requestUrl); + request.headers.addAll(requestModel.enabledHeadersMap); - http.StreamedResponse streamedResponse = await client.send(request); - final int statusCode = streamedResponse.statusCode; - final Map<String, String> responseHeaders = streamedResponse.headers; - - onConnect?.call(statusCode, responseHeaders); - - print("inside streamed response"); - final stream = streamedResponse.stream - .transform(utf8.decoder) - .transform(const LineSplitter()); - try{ stream.listen( - (event) { - onData?.call(event); - if (event.isNotEmpty) { - print('🔹 SSE Event Received: $event'); - print(event.toString()); - // final parsedEvent = SSEEventModel.fromRawSSE(event); - // ref.read(sseFramesProvider.notifier).addFrame(requestId, parsedEvent); + if (kMethodsWithBody.contains(requestModel.method) && requestModel.body != null) { + request.body = requestModel.body!; + request.headers[HttpHeaders.contentTypeHeader] = requestModel.bodyContentType.header; + } + + streamedResponse = await client.send(request); + stopwatch.stop(); + + } - }, - onError: (error) { - (error, stackTrace) => onError?.call(error, stackTrace); - print('🔹 SSE Error: $error'); - // ref.read(sseFramesProvider.notifier).update((state) { - // return {...state, requestId: [...(state[requestId] ?? []), 'Error: $error']}; - // }); - // finalizeRequestModel(requestId); - }, - onDone: () { - onDone?.call(); - print('🔹 SSE Stream Done'); - // finalizeRequestModel(requestId); - }, - );}catch (e, stackTrace) { - print('🔹 Error connecting to SSE: $e'); - onError?.call(e, stackTrace); - //_reconnect(onData, onError, onDone); + print("before connect"); + print("status code: ${streamedResponse.statusCode}"); + print("headers: ${streamedResponse.headers}"); + onConnect?.call( + streamedResponse.statusCode, streamedResponse.headers, interceptor.interceptedHeaders, stopwatch.elapsed + ); + print("after connect"); + final stream = streamedResponse.stream + .transform(utf8.decoder) + .transform(const LineSplitter()); + stream.listen( + (event) { + onData?.call(event); + if (event.isNotEmpty) { + print('🔹 SSE Event Received: $event'); + print(event.toString()); + + } + }, + onError: (error) { + if (httpClientManager.wasRequestCancelled(requestId)) { + print("inside cancelled"); + onCancel?.call(); + return; + } + (error, stackTrace) => onError?.call(error, stackTrace); + print('🔹 SSE Error: $error'); + }, + onDone: () { + if (httpClientManager.wasRequestCancelled(requestId)) { + print("inside cancelled"); + onCancel?.call(); + return; + } + onDone?.call(); + print('🔹 SSE Stream Done'); + } + ); } - // print("streamedResponse"+streamedResponse.headers.toString()); - // // var buffer = await convertStreamedResponse(streamedResponse); - // // print(buffer.body.toString()); - // if(streamedResponse.headers['content-type']?.contains('text/event-stream') == true){ - // print("has the content type"); - // } - // stopwatch.stop(); - // Stream<String>? utf8Stream; - // if (streamedResponse.statusCode == 200) { - - // utf8Stream = await streamedResponse.stream - // .transform(utf8.decoder) - // .transform(const LineSplitter()); - // print("utf8stream"+utf8Stream.toString()); + } catch (e, stackTrace) { + print("inside catch"); + onError?.call(e, stackTrace); + } + // finally{ + // print("finally"); + // if (httpClientManager.wasRequestCancelled(requestId)) { + // onCancel?.call(); + // } + // httpClientManager.closeClient(requestId); + // } +} - // await for (final event in utf8Stream) { - // print("inside eventloop"); - // if (event.isNotEmpty) { - // print('🔹 SSE Event Received: $event'); - // } - // } - // } - // print("just before return ing null"); - } - } - -} \ No newline at end of file diff --git a/packages/apidash_core/lib/utils/http_request_utils.dart b/packages/apidash_core/lib/utils/http_request_utils.dart index b5eab42cc..24bc36753 100644 --- a/packages/apidash_core/lib/utils/http_request_utils.dart +++ b/packages/apidash_core/lib/utils/http_request_utils.dart @@ -93,7 +93,7 @@ List<NameValueModel>? getEnabledRows( String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { return switch (type) { - APIType.rest => + APIType.rest || APIType.sse => (httpRequestModel.hasJsonData || httpRequestModel.hasTextData) ? httpRequestModel.body : null, diff --git a/packages/apidash_core/pubspec.yaml b/packages/apidash_core/pubspec.yaml index 13aaea079..344a20aac 100644 --- a/packages/apidash_core/pubspec.yaml +++ b/packages/apidash_core/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: path: ../postman seed: ^0.0.3 xml: ^6.3.0 + http_interceptor: ^2.0.0 dev_dependencies: flutter_test: diff --git a/packages/apidash_design_system/lib/tokens/colors.dart b/packages/apidash_design_system/lib/tokens/colors.dart index 11943bd32..92b8c061f 100644 --- a/packages/apidash_design_system/lib/tokens/colors.dart +++ b/packages/apidash_design_system/lib/tokens/colors.dart @@ -26,6 +26,8 @@ final kColorHttpMethodDelete = Colors.red.shade800; final kColorGQL = Colors.pink.shade600; +final kColorSSE = Colors.purple.shade600; + const kHintOpacity = 0.6; const kForegroundOpacity = 0.05; const kOverlayBackgroundOpacity = 0.5; diff --git a/pubspec.lock b/pubspec.lock index b16f69dd4..c4a0694fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -783,6 +783,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + http_interceptor: + dependency: transitive + description: + name: http_interceptor + sha256: "288c6ded4a2c66de2730a16b30cbd29d05d042a5e61304d9b4be0e16378f4082" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http_multi_server: dependency: transitive description: From 6123091f0026e07f13dca7a26a0b2e4774c009d0 Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Tue, 1 Apr 2025 01:19:31 +0530 Subject: [PATCH 3/8] removed debug prints --- .../lib/services/http_sse_service.dart | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart index 2055d9bb2..c411a0b8c 100644 --- a/packages/apidash_core/lib/services/http_sse_service.dart +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -15,14 +15,12 @@ class LoggingInterceptor implements InterceptorContract { @override FutureOr<BaseRequest> interceptRequest({required BaseRequest request}) async { interceptedHeaders = request.headers; - print("Headers: \${request.headers}"); return request; } @override Future<BaseResponse> interceptResponse({required BaseResponse response}) async { - print("Response: \${response.statusCode}"); - print("Headers: \${response.headers}"); + return response; } @@ -102,28 +100,20 @@ Future<void> sendSSERequest( } - print("before connect"); - print("status code: ${streamedResponse.statusCode}"); - print("headers: ${streamedResponse.headers}"); onConnect?.call( streamedResponse.statusCode, streamedResponse.headers, interceptor.interceptedHeaders, stopwatch.elapsed ); - print("after connect"); + final stream = streamedResponse.stream .transform(utf8.decoder) .transform(const LineSplitter()); stream.listen( (event) { onData?.call(event); - if (event.isNotEmpty) { - print('🔹 SSE Event Received: $event'); - print(event.toString()); - - } }, onError: (error) { if (httpClientManager.wasRequestCancelled(requestId)) { - print("inside cancelled"); + onCancel?.call(); return; } @@ -137,20 +127,20 @@ Future<void> sendSSERequest( return; } onDone?.call(); - print('🔹 SSE Stream Done'); + } ); } } catch (e, stackTrace) { - print("inside catch"); + onError?.call(e, stackTrace); } - // finally{ - // print("finally"); - // if (httpClientManager.wasRequestCancelled(requestId)) { - // onCancel?.call(); - // } - // httpClientManager.closeClient(requestId); - // } + finally{ + + if (httpClientManager.wasRequestCancelled(requestId)) { + onCancel?.call(); + } + + } } From 7e633f5ab15aa4ac70510565b74cc0ff837fd0d8 Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Tue, 1 Apr 2025 11:01:24 +0530 Subject: [PATCH 4/8] working on cancellation --- .../home_page/editor_pane/details_card/response_pane.dart | 7 +++---- packages/apidash_core/lib/services/http_service.dart | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 14440d92d..3def68ca4 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -193,7 +193,6 @@ class _SSEViewState extends ConsumerState<SSEView> { final displayFrames = _isAtBottom ? frames : _pausedFrames; return Scaffold( - appBar: AppBar(title: Text("SSE Stream - ${widget.requestId}")), body: Column( children: [ Expanded( @@ -213,8 +212,8 @@ class _SSEViewState extends ConsumerState<SSEView> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (event.event.isEmpty) _buildTag("Event", "message", Colors.blue), - _buildData(event.data), + if (event.event.isNotEmpty) _buildTag("Event", "message", Colors.blue), + if (event.data.isNotEmpty) _buildData(event.data), if (event.id != null && event.id!.isNotEmpty) _buildTag("ID", event.id!, Colors.orange), if (event.retry != null) _buildTag("Retry", "${event.retry} ms", Colors.purple), if (event.customFields != null && event.customFields!.isNotEmpty) @@ -282,7 +281,7 @@ class _SSEViewState extends ConsumerState<SSEView> { try { final jsonData = json.decode(data); final formattedJson = const JsonEncoder.withIndent(' ').convert(jsonData); - final bool shouldCollapse = formattedJson.length > 2; + final bool shouldCollapse = formattedJson.length > 100; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index 6f5488aad..436fb2a8c 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -130,6 +130,5 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( } void cancelHttpRequest(String? requestId) { - print("cancelHttpRequest: $requestId"); httpClientManager.cancelRequest(requestId); } \ No newline at end of file From 64601b1daad28cac78f61e4b1721087f77f6bdef Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Tue, 1 Apr 2025 20:29:15 +0530 Subject: [PATCH 5/8] removed print statements --- .../lib/models/http_sse_event_model.dart | 18 ++++--- .../models/http_sse_event_model.freezed.dart | 49 ++++++++++++------- .../lib/models/http_sse_event_model.g.dart | 2 + .../lib/services/http_sse_service.dart | 1 - 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/packages/apidash_core/lib/models/http_sse_event_model.dart b/packages/apidash_core/lib/models/http_sse_event_model.dart index 64aa58a3e..f633d3baa 100644 --- a/packages/apidash_core/lib/models/http_sse_event_model.dart +++ b/packages/apidash_core/lib/models/http_sse_event_model.dart @@ -13,11 +13,12 @@ class SSEEventModel with _$SSEEventModel { anyMap: true, ) const factory SSEEventModel({ - @Default("") String event, // Custom event name - @Default("") String data, // Event data (main payload) - String? id, // Last event ID for reconnection - int? retry, // Reconnect interval in milliseconds - Map<String, String>? customFields, // Additional metadata + @Default("") String event, + @Default("") String data, + @Default("") String comment, + String? id, + int? retry, + Map<String, String>? customFields, }) = _SSEEventModel; factory SSEEventModel.fromJson(Map<String, Object?> json) => @@ -26,12 +27,12 @@ class SSEEventModel with _$SSEEventModel { /// Parses raw SSE data into an SSEEventModel static SSEEventModel fromRawSSE(String rawEvent) { final Map<String, String> fields = {}; - String? event, data, id; + String? event, data, id ,comment; int? retry; for (var line in LineSplitter.split(rawEvent)) { if (line.startsWith(":")) { - continue; // Ignore comments + comment = line;// Ignore comments } final parts = line.split(": "); if (parts.length < 2) continue; @@ -44,7 +45,7 @@ class SSEEventModel with _$SSEEventModel { event = value; break; case "data": - data = (data ?? "") + value + "\n"; // Multi-line data support + data = (data ?? "") + value; break; case "id": id = value; @@ -60,6 +61,7 @@ class SSEEventModel with _$SSEEventModel { return SSEEventModel( event: event ?? "", data: data?.trim() ?? "", + comment: comment ?? "", id: id, retry: retry, customFields: fields.isNotEmpty ? fields : null, diff --git a/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart b/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart index d35f2c256..2e16d7079 100644 --- a/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart +++ b/packages/apidash_core/lib/models/http_sse_event_model.freezed.dart @@ -20,13 +20,11 @@ SSEEventModel _$SSEEventModelFromJson(Map<String, dynamic> json) { /// @nodoc mixin _$SSEEventModel { - String get event => throw _privateConstructorUsedError; // Custom event name - String get data => - throw _privateConstructorUsedError; // Event data (main payload) - String? get id => - throw _privateConstructorUsedError; // Last event ID for reconnection - int? get retry => - throw _privateConstructorUsedError; // Reconnect interval in milliseconds + String get event => throw _privateConstructorUsedError; + String get data => throw _privateConstructorUsedError; + String get comment => throw _privateConstructorUsedError; + String? get id => throw _privateConstructorUsedError; + int? get retry => throw _privateConstructorUsedError; Map<String, String>? get customFields => throw _privateConstructorUsedError; /// Serializes this SSEEventModel to a JSON map. @@ -48,6 +46,7 @@ abstract class $SSEEventModelCopyWith<$Res> { $Res call( {String event, String data, + String comment, String? id, int? retry, Map<String, String>? customFields}); @@ -70,6 +69,7 @@ class _$SSEEventModelCopyWithImpl<$Res, $Val extends SSEEventModel> $Res call({ Object? event = null, Object? data = null, + Object? comment = null, Object? id = freezed, Object? retry = freezed, Object? customFields = freezed, @@ -83,6 +83,10 @@ class _$SSEEventModelCopyWithImpl<$Res, $Val extends SSEEventModel> ? _value.data : data // ignore: cast_nullable_to_non_nullable as String, + comment: null == comment + ? _value.comment + : comment // ignore: cast_nullable_to_non_nullable + as String, id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable @@ -110,6 +114,7 @@ abstract class _$$SSEEventModelImplCopyWith<$Res> $Res call( {String event, String data, + String comment, String? id, int? retry, Map<String, String>? customFields}); @@ -130,6 +135,7 @@ class __$$SSEEventModelImplCopyWithImpl<$Res> $Res call({ Object? event = null, Object? data = null, + Object? comment = null, Object? id = freezed, Object? retry = freezed, Object? customFields = freezed, @@ -143,6 +149,10 @@ class __$$SSEEventModelImplCopyWithImpl<$Res> ? _value.data : data // ignore: cast_nullable_to_non_nullable as String, + comment: null == comment + ? _value.comment + : comment // ignore: cast_nullable_to_non_nullable + as String, id: freezed == id ? _value.id : id // ignore: cast_nullable_to_non_nullable @@ -166,6 +176,7 @@ class _$SSEEventModelImpl extends _SSEEventModel { const _$SSEEventModelImpl( {this.event = "", this.data = "", + this.comment = "", this.id, this.retry, final Map<String, String>? customFields}) @@ -178,19 +189,17 @@ class _$SSEEventModelImpl extends _SSEEventModel { @override @JsonKey() final String event; -// Custom event name @override @JsonKey() final String data; -// Event data (main payload) + @override + @JsonKey() + final String comment; @override final String? id; -// Last event ID for reconnection @override final int? retry; -// Reconnect interval in milliseconds final Map<String, String>? _customFields; -// Reconnect interval in milliseconds @override Map<String, String>? get customFields { final value = _customFields; @@ -202,7 +211,7 @@ class _$SSEEventModelImpl extends _SSEEventModel { @override String toString() { - return 'SSEEventModel(event: $event, data: $data, id: $id, retry: $retry, customFields: $customFields)'; + return 'SSEEventModel(event: $event, data: $data, comment: $comment, id: $id, retry: $retry, customFields: $customFields)'; } @override @@ -212,6 +221,7 @@ class _$SSEEventModelImpl extends _SSEEventModel { other is _$SSEEventModelImpl && (identical(other.event, event) || other.event == event) && (identical(other.data, data) || other.data == data) && + (identical(other.comment, comment) || other.comment == comment) && (identical(other.id, id) || other.id == id) && (identical(other.retry, retry) || other.retry == retry) && const DeepCollectionEquality() @@ -220,7 +230,7 @@ class _$SSEEventModelImpl extends _SSEEventModel { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, event, data, id, retry, + int get hashCode => Object.hash(runtimeType, event, data, comment, id, retry, const DeepCollectionEquality().hash(_customFields)); /// Create a copy of SSEEventModel @@ -243,6 +253,7 @@ abstract class _SSEEventModel extends SSEEventModel { const factory _SSEEventModel( {final String event, final String data, + final String comment, final String? id, final int? retry, final Map<String, String>? customFields}) = _$SSEEventModelImpl; @@ -252,13 +263,15 @@ abstract class _SSEEventModel extends SSEEventModel { _$SSEEventModelImpl.fromJson; @override - String get event; // Custom event name + String get event; + @override + String get data; @override - String get data; // Event data (main payload) + String get comment; @override - String? get id; // Last event ID for reconnection + String? get id; @override - int? get retry; // Reconnect interval in milliseconds + int? get retry; @override Map<String, String>? get customFields; diff --git a/packages/apidash_core/lib/models/http_sse_event_model.g.dart b/packages/apidash_core/lib/models/http_sse_event_model.g.dart index b85ec871d..17c1ccdf6 100644 --- a/packages/apidash_core/lib/models/http_sse_event_model.g.dart +++ b/packages/apidash_core/lib/models/http_sse_event_model.g.dart @@ -10,6 +10,7 @@ _$SSEEventModelImpl _$$SSEEventModelImplFromJson(Map json) => _$SSEEventModelImpl( event: json['event'] as String? ?? "", data: json['data'] as String? ?? "", + comment: json['comment'] as String? ?? "", id: json['id'] as String?, retry: (json['retry'] as num?)?.toInt(), customFields: (json['customFields'] as Map?)?.map( @@ -21,6 +22,7 @@ Map<String, dynamic> _$$SSEEventModelImplToJson(_$SSEEventModelImpl instance) => <String, dynamic>{ 'event': instance.event, 'data': instance.data, + 'comment': instance.comment, 'id': instance.id, 'retry': instance.retry, 'customFields': instance.customFields, diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart index c411a0b8c..1fe885d8f 100644 --- a/packages/apidash_core/lib/services/http_sse_service.dart +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -63,7 +63,6 @@ Future<void> sendSSERequest( var stopwatch = Stopwatch()..start(); late http.StreamedResponse streamedResponse; if (apiType == APIType.sse) { - print("Inside SSE HTTP service"); if (isMultiPartRequest) { var multiPartRequest = http.MultipartRequest( From 8f68aaf30f5e1516ef817dd5723e1bc3b50c9e7f Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Tue, 1 Apr 2025 21:59:08 +0530 Subject: [PATCH 6/8] working on response --- lib/providers/collection_providers.dart | 6 ++--- .../details_card/response_pane.dart | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index c65cd814e..b308ca548 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -335,12 +335,10 @@ class CollectionStateNotifier defaultUriScheme: defaultUriScheme, noSSL: noSSL, onConnect: (statusCode, responseHeaders, requestHeaders,duration) { - print("inside sse onconnect"); + ref.read(sseFramesProvider.notifier).clearFrames(requestId); map = {...state!}; - print(statusCode); - print(responseHeaders); - print(requestHeaders); + map[requestId] = requestModel.copyWith( responseStatus: statusCode, message: kResponseCodeReasons[statusCode], diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 3def68ca4..f37cab1f8 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -23,20 +23,29 @@ class ResponsePane extends ConsumerWidget{ final message = ref .watch(selectedRequestModelProvider.select((value) => value?.message)); final sseFramesNotifier = ref.watch(sseFramesProvider.select((state) => state[selectedId] ?? [])); - + final apiType = ref.watch(selectedRequestModelProvider.select((value) => value?.apiType)); - if (isWorking && sseFramesNotifier.isEmpty) { - print("inside sending widget"+sseFramesNotifier.isEmpty.toString()); + if (isWorking && apiType == APIType.sse && sseFramesNotifier.isEmpty) { + + return SendingWidget( + startSendingTime: startSendingTime, + ); + + } + + + if (isWorking && apiType != APIType.sse ) { + return SendingWidget( startSendingTime: startSendingTime, ); } - if (responseStatus == null && sseFramesNotifier.isEmpty) { + if (responseStatus == null) { return const NotSentWidget(); } - if (responseStatus == -1 && sseFramesNotifier.isEmpty) { + if (responseStatus == -1) { return message == kMsgRequestCancelled ? ErrorMessage( message: message, @@ -187,7 +196,10 @@ class _SSEViewState extends ConsumerState<SSEView> { Widget build(BuildContext context) { final frames = ref.watch(sseFramesProvider.select((state) => state[widget.requestId] ?? [])); - if (_isAtBottom) { + // if (_isAtBottom) { + // _pausedFrames = List.from(frames); + // } + if (_isAtBottom || frames.isEmpty) { _pausedFrames = List.from(frames); } final displayFrames = _isAtBottom ? frames : _pausedFrames; From 29f93ed42bee826fdfdbe2c181e477ae0d7633cf Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Fri, 4 Apr 2025 00:28:40 +0530 Subject: [PATCH 7/8] corrected sse transform --- lib/providers/collection_providers.dart | 3 +- .../lib/services/http_sse_service.dart | 37 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index b308ca548..5c7bd8ba1 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -26,6 +26,7 @@ class SSEFramesNotifier extends StateNotifier<Map<String, List<SSEEventModel>>> SSEFramesNotifier() : super({}); void addFrame(String requestId, String frame) { + print(frame+"\n"); state = { ...state, requestId: [...(state[requestId] ?? []), SSEEventModel.fromRawSSE(frame)], @@ -326,7 +327,7 @@ class CollectionStateNotifier bool noSSL = ref.read(settingsProvider).isSSLDisabled; if(apiType == APIType.sse){ - print("inside sendrequest apitype,sse"); + List<SSEEventModel> frames; await sendSSERequest( requestId, diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart index 1fe885d8f..dfa1e8ee8 100644 --- a/packages/apidash_core/lib/services/http_sse_service.dart +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -9,6 +9,29 @@ import '../models/models.dart'; import '../utils/utils.dart'; import 'http_service.dart'; + + + + +class SSETransformer extends StreamTransformerBase<String, String> { + const SSETransformer(); + + @override + Stream<String> bind(Stream<String> stream) async* { + String buffer = ""; + await for (final chunk in stream) { + buffer += chunk; + List<String> frames = buffer.split("\n\n"); + + + buffer = frames.removeLast(); + + for (final frame in frames) { + yield frame.trim(); + } + } + } +} class LoggingInterceptor implements InterceptorContract { Map<String, String>? interceptedHeaders; @@ -105,10 +128,14 @@ Future<void> sendSSERequest( final stream = streamedResponse.stream .transform(utf8.decoder) - .transform(const LineSplitter()); + .transform(const SSETransformer()); stream.listen( - (event) { - onData?.call(event); + (frame) { + print("Received frames: $frame"); + + onData?.call(frame); + + }, onError: (error) { if (httpClientManager.wasRequestCancelled(requestId)) { @@ -117,11 +144,11 @@ Future<void> sendSSERequest( return; } (error, stackTrace) => onError?.call(error, stackTrace); - print('🔹 SSE Error: $error'); + }, onDone: () { if (httpClientManager.wasRequestCancelled(requestId)) { - print("inside cancelled"); + onCancel?.call(); return; } From 129a4b671a673f8ff80fbb192d3ef2cece8681f5 Mon Sep 17 00:00:00 2001 From: Clasherzz <govindkm20044@gmail.com> Date: Fri, 4 Apr 2025 01:06:25 +0530 Subject: [PATCH 8/8] final commit --- .../details_card/response_pane.dart | 2 +- .../lib/services/http_client_manager.dart | 3 +-- .../lib/services/http_service.dart | 1 + .../lib/services/http_sse_service.dart | 18 ++++-------------- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index f37cab1f8..d6563cac0 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -212,7 +212,7 @@ class _SSEViewState extends ConsumerState<SSEView> { controller: _controller, itemCount: displayFrames.length, itemBuilder: (context, index) { - final event = displayFrames[displayFrames.length - index - 1]; + final event = displayFrames[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), diff --git a/packages/apidash_core/lib/services/http_client_manager.dart b/packages/apidash_core/lib/services/http_client_manager.dart index 2dc13d821..e72d7ac9a 100644 --- a/packages/apidash_core/lib/services/http_client_manager.dart +++ b/packages/apidash_core/lib/services/http_client_manager.dart @@ -34,10 +34,9 @@ class HttpClientManager { void cancelRequest(String? requestId) { if (requestId != null && _clients.containsKey(requestId)) { + _cancelledRequests.add(requestId); _clients[requestId]?.close(); _clients.remove(requestId); - - _cancelledRequests.add(requestId); if (_cancelledRequests.length > _maxCancelledRequests) { _cancelledRequests.remove(_cancelledRequests.first); } diff --git a/packages/apidash_core/lib/services/http_service.dart b/packages/apidash_core/lib/services/http_service.dart index 436fb2a8c..0d673758b 100644 --- a/packages/apidash_core/lib/services/http_service.dart +++ b/packages/apidash_core/lib/services/http_service.dart @@ -130,5 +130,6 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( } void cancelHttpRequest(String? requestId) { + print("Cancelling request: $requestId"); httpClientManager.cancelRequest(requestId); } \ No newline at end of file diff --git a/packages/apidash_core/lib/services/http_sse_service.dart b/packages/apidash_core/lib/services/http_sse_service.dart index dfa1e8ee8..f7422f411 100644 --- a/packages/apidash_core/lib/services/http_sse_service.dart +++ b/packages/apidash_core/lib/services/http_sse_service.dart @@ -131,16 +131,13 @@ Future<void> sendSSERequest( .transform(const SSETransformer()); stream.listen( (frame) { - print("Received frames: $frame"); - - onData?.call(frame); - - + onData?.call(frame); }, onError: (error) { + if (httpClientManager.wasRequestCancelled(requestId)) { - - onCancel?.call(); + onCancel?.call(); + return; } (error, stackTrace) => onError?.call(error, stackTrace); @@ -148,7 +145,6 @@ Future<void> sendSSERequest( }, onDone: () { if (httpClientManager.wasRequestCancelled(requestId)) { - onCancel?.call(); return; } @@ -161,12 +157,6 @@ Future<void> sendSSERequest( onError?.call(e, stackTrace); } - finally{ - - if (httpClientManager.wasRequestCancelled(requestId)) { - onCancel?.call(); - } - } }