SSE: Stopping/Cancelling implementation

This commit is contained in:
Manas Hejmadi
2025-06-26 00:36:12 +05:30
parent 97db38a42d
commit 882b393fdd
8 changed files with 119 additions and 95 deletions

View File

@@ -22,6 +22,7 @@ class RequestModel with _$RequestModel {
HttpResponseModel? httpResponseModel, HttpResponseModel? httpResponseModel,
@JsonKey(includeToJson: false) @Default(false) bool isWorking, @JsonKey(includeToJson: false) @Default(false) bool isWorking,
@JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) DateTime? sendingTime,
@JsonKey(includeToJson: false) @Default(false) bool isStreaming,
String? preRequestScript, String? preRequestScript,
String? postRequestScript, String? postRequestScript,
}) = _RequestModel; }) = _RequestModel;

View File

@@ -35,6 +35,8 @@ mixin _$RequestModel {
bool get isWorking => throw _privateConstructorUsedError; bool get isWorking => throw _privateConstructorUsedError;
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
DateTime? get sendingTime => throw _privateConstructorUsedError; DateTime? get sendingTime => throw _privateConstructorUsedError;
@JsonKey(includeToJson: false)
bool get isStreaming => throw _privateConstructorUsedError;
String? get preRequestScript => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError;
String? get postRequestScript => throw _privateConstructorUsedError; String? get postRequestScript => throw _privateConstructorUsedError;
@@ -66,6 +68,7 @@ abstract class $RequestModelCopyWith<$Res> {
HttpResponseModel? httpResponseModel, HttpResponseModel? httpResponseModel,
@JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) bool isWorking,
@JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) DateTime? sendingTime,
@JsonKey(includeToJson: false) bool isStreaming,
String? preRequestScript, String? preRequestScript,
String? postRequestScript}); String? postRequestScript});
@@ -99,6 +102,7 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel>
Object? httpResponseModel = freezed, Object? httpResponseModel = freezed,
Object? isWorking = null, Object? isWorking = null,
Object? sendingTime = freezed, Object? sendingTime = freezed,
Object? isStreaming = null,
Object? preRequestScript = freezed, Object? preRequestScript = freezed,
Object? postRequestScript = freezed, Object? postRequestScript = freezed,
}) { }) {
@@ -147,6 +151,10 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel>
? _value.sendingTime ? _value.sendingTime
: sendingTime // ignore: cast_nullable_to_non_nullable : sendingTime // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
isStreaming: null == isStreaming
? _value.isStreaming
: isStreaming // ignore: cast_nullable_to_non_nullable
as bool,
preRequestScript: freezed == preRequestScript preRequestScript: freezed == preRequestScript
? _value.preRequestScript ? _value.preRequestScript
: preRequestScript // ignore: cast_nullable_to_non_nullable : preRequestScript // ignore: cast_nullable_to_non_nullable
@@ -207,6 +215,7 @@ abstract class _$$RequestModelImplCopyWith<$Res>
HttpResponseModel? httpResponseModel, HttpResponseModel? httpResponseModel,
@JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) bool isWorking,
@JsonKey(includeToJson: false) DateTime? sendingTime, @JsonKey(includeToJson: false) DateTime? sendingTime,
@JsonKey(includeToJson: false) bool isStreaming,
String? preRequestScript, String? preRequestScript,
String? postRequestScript}); String? postRequestScript});
@@ -240,6 +249,7 @@ class __$$RequestModelImplCopyWithImpl<$Res>
Object? httpResponseModel = freezed, Object? httpResponseModel = freezed,
Object? isWorking = null, Object? isWorking = null,
Object? sendingTime = freezed, Object? sendingTime = freezed,
Object? isStreaming = null,
Object? preRequestScript = freezed, Object? preRequestScript = freezed,
Object? postRequestScript = freezed, Object? postRequestScript = freezed,
}) { }) {
@@ -287,6 +297,10 @@ class __$$RequestModelImplCopyWithImpl<$Res>
? _value.sendingTime ? _value.sendingTime
: sendingTime // ignore: cast_nullable_to_non_nullable : sendingTime // ignore: cast_nullable_to_non_nullable
as DateTime?, as DateTime?,
isStreaming: null == isStreaming
? _value.isStreaming
: isStreaming // ignore: cast_nullable_to_non_nullable
as bool,
preRequestScript: freezed == preRequestScript preRequestScript: freezed == preRequestScript
? _value.preRequestScript ? _value.preRequestScript
: preRequestScript // ignore: cast_nullable_to_non_nullable : preRequestScript // ignore: cast_nullable_to_non_nullable
@@ -315,6 +329,7 @@ class _$RequestModelImpl implements _RequestModel {
this.httpResponseModel, this.httpResponseModel,
@JsonKey(includeToJson: false) this.isWorking = false, @JsonKey(includeToJson: false) this.isWorking = false,
@JsonKey(includeToJson: false) this.sendingTime, @JsonKey(includeToJson: false) this.sendingTime,
@JsonKey(includeToJson: false) this.isStreaming = false,
this.preRequestScript, this.preRequestScript,
this.postRequestScript}); this.postRequestScript});
@@ -350,13 +365,16 @@ class _$RequestModelImpl implements _RequestModel {
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
final DateTime? sendingTime; final DateTime? sendingTime;
@override @override
@JsonKey(includeToJson: false)
final bool isStreaming;
@override
final String? preRequestScript; final String? preRequestScript;
@override @override
final String? postRequestScript; final String? postRequestScript;
@override @override
String toString() { String toString() {
return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, isStreaming: $isStreaming, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)';
} }
@override @override
@@ -382,6 +400,8 @@ class _$RequestModelImpl implements _RequestModel {
other.isWorking == isWorking) && other.isWorking == isWorking) &&
(identical(other.sendingTime, sendingTime) || (identical(other.sendingTime, sendingTime) ||
other.sendingTime == sendingTime) && other.sendingTime == sendingTime) &&
(identical(other.isStreaming, isStreaming) ||
other.isStreaming == isStreaming) &&
(identical(other.preRequestScript, preRequestScript) || (identical(other.preRequestScript, preRequestScript) ||
other.preRequestScript == preRequestScript) && other.preRequestScript == preRequestScript) &&
(identical(other.postRequestScript, postRequestScript) || (identical(other.postRequestScript, postRequestScript) ||
@@ -403,6 +423,7 @@ class _$RequestModelImpl implements _RequestModel {
httpResponseModel, httpResponseModel,
isWorking, isWorking,
sendingTime, sendingTime,
isStreaming,
preRequestScript, preRequestScript,
postRequestScript); postRequestScript);
@@ -435,6 +456,7 @@ abstract class _RequestModel implements RequestModel {
final HttpResponseModel? httpResponseModel, final HttpResponseModel? httpResponseModel,
@JsonKey(includeToJson: false) final bool isWorking, @JsonKey(includeToJson: false) final bool isWorking,
@JsonKey(includeToJson: false) final DateTime? sendingTime, @JsonKey(includeToJson: false) final DateTime? sendingTime,
@JsonKey(includeToJson: false) final bool isStreaming,
final String? preRequestScript, final String? preRequestScript,
final String? postRequestScript}) = _$RequestModelImpl; final String? postRequestScript}) = _$RequestModelImpl;
@@ -467,6 +489,9 @@ abstract class _RequestModel implements RequestModel {
@JsonKey(includeToJson: false) @JsonKey(includeToJson: false)
DateTime? get sendingTime; DateTime? get sendingTime;
@override @override
@JsonKey(includeToJson: false)
bool get isStreaming;
@override
String? get preRequestScript; String? get preRequestScript;
@override @override
String? get postRequestScript; String? get postRequestScript;

View File

@@ -27,6 +27,7 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl(
sendingTime: json['sendingTime'] == null sendingTime: json['sendingTime'] == null
? null ? null
: DateTime.parse(json['sendingTime'] as String), : DateTime.parse(json['sendingTime'] as String),
isStreaming: json['isStreaming'] as bool? ?? false,
preRequestScript: json['preRequestScript'] as String?, preRequestScript: json['preRequestScript'] as String?,
postRequestScript: json['postRequestScript'] as String?, postRequestScript: json['postRequestScript'] as String?,
); );

View File

@@ -270,19 +270,16 @@ class CollectionStateNotifier
Future<void> sendRequest() async { Future<void> sendRequest() async {
final requestId = ref.read(selectedIdStateProvider); final requestId = ref.read(selectedIdStateProvider);
ref.read(codePaneVisibleStateProvider.notifier).state = false; ref.read(codePaneVisibleStateProvider.notifier).state = false;
if (requestId == null || state == null) return;
RequestModel? requestModel = state![requestId];
if (requestModel?.httpRequestModel == null) return;
final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme;
final EnvironmentModel? originalEnvironmentModel = final EnvironmentModel? originalEnvironmentModel =
ref.read(selectedEnvironmentModelProvider); ref.read(selectedEnvironmentModelProvider);
if (requestId == null || state == null) {
return;
}
RequestModel? requestModel = state![requestId];
if (requestModel?.httpRequestModel == null) {
return;
}
if (requestModel != null && if (requestModel != null &&
!requestModel.preRequestScript.isNullOrEmpty()) { !requestModel.preRequestScript.isNullOrEmpty()) {
requestModel = await handlePreRequestScript( requestModel = await handlePreRequestScript(
@@ -303,23 +300,18 @@ class CollectionStateNotifier
APIType apiType = requestModel!.apiType; APIType apiType = requestModel!.apiType;
HttpRequestModel substitutedHttpRequestModel = HttpRequestModel substitutedHttpRequestModel =
getSubstitutedHttpRequestModel(requestModel.httpRequestModel!); getSubstitutedHttpRequestModel(requestModel.httpRequestModel!);
// set current model's isWorking to true and update state
var map = {...state!};
map[requestId] = requestModel.copyWith(
isWorking: true,
sendingTime: DateTime.now(),
);
state = map;
bool noSSL = ref.read(settingsProvider).isSSLDisabled; bool noSSL = ref.read(settingsProvider).isSSLDisabled;
(Response?, Duration?, String?) responseRec; // Set model to working and streaming
HttpResponseModel? respModel; state = {
HistoryRequestModel? historyM; ...state!,
RequestModel? newRequestModel; requestId: requestModel.copyWith(
isWorking: true,
isStreaming: true,
sendingTime: DateTime.now(),
),
};
responseRec = (null, null, null);
final stream = await streamHttpRequest( final stream = await streamHttpRequest(
requestId, requestId,
apiType, apiType,
@@ -328,101 +320,98 @@ class CollectionStateNotifier
noSSL: noSSL, noSSL: noSSL,
); );
StreamSubscription? sub; HttpResponseModel? respModel;
final completer = Completer(); HistoryRequestModel? historyM;
RequestModel newRequestModel = requestModel;
final completer = Completer<(Response?, Duration?, String?)>();
bool isTextStream = false; bool isTextStream = false;
StreamSubscription? sub;
sub = stream.listen((d) async { sub = stream.listen((d) async {
if (d == null) return; if (d == null) return;
isTextStream = ((d.$1 == null && isTextStream) || final contentType = d.$1;
(d.$1 == 'text/event-stream' || d.$1 == 'application/x-ndjson')); isTextStream = isTextStream ||
contentType == 'text/event-stream' ||
contentType == 'application/x-ndjson';
responseRec = (d.$2, d.$3, d.$4); final response = d.$2;
final duration = d.$3;
final errorMessage = d.$4;
if (!isTextStream) { if (!isTextStream) {
if (completer.isCompleted) return; if (!completer.isCompleted) {
completer.complete(responseRec); completer.complete((response, duration, errorMessage));
await Future.delayed(Duration(milliseconds: 100)); }
return;
} }
if (responseRec.$1 != null) { respModel = respModel?.copyWith(
responseRec = ( sseOutput: [
HttpResponse(
responseRec.$1!.body,
responseRec.$1!.statusCode,
request: responseRec.$1!.request,
headers: {
...(responseRec.$1?.headers ?? {}),
'content-type': 'text/event-stream'
},
isRedirect: responseRec.$1!.isRedirect,
reasonPhrase: responseRec.$1!.reasonPhrase,
persistentConnection: responseRec.$1!.persistentConnection,
),
responseRec.$2,
responseRec.$3,
);
}
//----------- MAKE CHANGES --------------
respModel = respModel?.copyWith(sseOutput: [
...(respModel?.sseOutput ?? []), ...(respModel?.sseOutput ?? []),
responseRec.$1!.body, if (response != null) response.body,
]); ],
if (respModel != null) { );
final nRM = newRequestModel!.copyWith(
newRequestModel = newRequestModel.copyWith(
httpResponseModel: respModel, httpResponseModel: respModel,
isStreaming: true,
); );
map = {...state!}; state = {
map[requestId] = nRM; ...state!,
state = map; requestId: newRequestModel,
};
unsave(); unsave();
}
//Changing History
if (historyM != null && respModel != null) { if (historyM != null && respModel != null) {
historyM = historyM!.copyWith( historyM = historyM!.copyWith(httpResponseModel: respModel!);
httpResponseModel: respModel!,
);
ref ref
.read(historyMetaStateNotifier.notifier) .read(historyMetaStateNotifier.notifier)
.editHistoryRequest(historyM!); .editHistoryRequest(historyM!);
} }
//----------- MAKE CHANGES --------------
if (completer.isCompleted) return; if (!completer.isCompleted) {
completer.complete(responseRec); completer.complete((response, duration, errorMessage));
}
}, onDone: () { }, onDone: () {
sub?.cancel(); sub?.cancel();
state = {
...state!,
requestId: newRequestModel.copyWith(isStreaming: false),
};
unsave();
}, onError: (e) { }, onError: (e) {
print('err: $e'); print('Stream error: $e');
}); });
responseRec = await completer.future;
if (responseRec.$1 == null) { final (response, duration, errorMessage) = await completer.future;
newRequestModel = requestModel.copyWith(
if (response == null) {
newRequestModel = newRequestModel.copyWith(
responseStatus: -1, responseStatus: -1,
message: responseRec.$3, message: errorMessage,
isWorking: false, isWorking: false,
isStreaming: false,
); );
} else { } else {
final statusCode = response.statusCode;
respModel = baseHttpResponseModel.fromResponse( respModel = baseHttpResponseModel.fromResponse(
response: responseRec.$1!, response: response,
time: responseRec.$2!, time: duration,
); );
int statusCode = responseRec.$1!.statusCode;
newRequestModel = requestModel.copyWith( newRequestModel = newRequestModel.copyWith(
responseStatus: statusCode, responseStatus: statusCode,
message: kResponseCodeReasons[statusCode], message: kResponseCodeReasons[statusCode],
httpResponseModel: respModel, httpResponseModel: respModel,
isWorking: false, isWorking: false,
); );
String newHistoryId = getNewUuid();
final historyId = getNewUuid();
historyM = HistoryRequestModel( historyM = HistoryRequestModel(
historyId: newHistoryId, historyId: historyId,
metaData: HistoryMetaModel( metaData: HistoryMetaModel(
historyId: newHistoryId, historyId: historyId,
requestId: requestId, requestId: requestId,
apiType: requestModel.apiType, apiType: requestModel.apiType,
name: requestModel.name, name: requestModel.name,
@@ -452,13 +441,15 @@ class CollectionStateNotifier
}, },
); );
} }
ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(historyM!); ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(historyM!);
} }
// update state with response data // Final state update
map = {...state!}; state = {
map[requestId] = newRequestModel; ...state!,
state = map; requestId: newRequestModel,
};
unsave(); unsave();
} }

View File

@@ -129,8 +129,11 @@ class SendRequestButton extends ConsumerWidget {
ref.watch(selectedIdStateProvider); ref.watch(selectedIdStateProvider);
final isWorking = ref.watch( final isWorking = ref.watch(
selectedRequestModelProvider.select((value) => value?.isWorking)); selectedRequestModelProvider.select((value) => value?.isWorking));
final isStreaming = ref.watch(
selectedRequestModelProvider.select((value) => value?.isStreaming));
return SendButton( return SendButton(
isStreaming: isStreaming ?? false,
isWorking: isWorking ?? false, isWorking: isWorking ?? false,
onTap: () { onTap: () {
onTap?.call(); onTap?.call();

View File

@@ -5,11 +5,13 @@ import 'package:apidash/consts.dart';
class SendButton extends StatelessWidget { class SendButton extends StatelessWidget {
const SendButton({ const SendButton({
super.key, super.key,
required this.isStreaming,
required this.isWorking, required this.isWorking,
required this.onTap, required this.onTap,
this.onCancel, this.onCancel,
}); });
final bool isStreaming;
final bool isWorking; final bool isWorking;
final void Function() onTap; final void Function() onTap;
final void Function()? onCancel; final void Function()? onCancel;
@@ -17,13 +19,13 @@ class SendButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ADFilledButton( return ADFilledButton(
onPressed: isWorking ? onCancel : onTap, onPressed: (isWorking || isStreaming) ? onCancel : onTap,
isTonal: isWorking ? true : false, isTonal: (isWorking || isStreaming),
items: isWorking items: (isWorking || isStreaming)
? const [ ? [
kHSpacer8, kHSpacer8,
Text( Text(
kLabelCancel, isStreaming ? 'Stop' : kLabelCancel,
style: kTextStyleButton, style: kTextStyleButton,
), ),
kHSpacer6, kHSpacer6,

View File

@@ -247,8 +247,6 @@ streamHttpRequest(
final streamedResponse = await client.send(multipart); final streamedResponse = await client.send(multipart);
final stream = streamTextResponse(streamedResponse); final stream = streamTextResponse(streamedResponse);
print(streamedResponse.headers['content-type']);
subscription = stream.listen( subscription = stream.listen(
(data) => controller.add(( (data) => controller.add((
streamedResponse.headers['content-type'].toString(), streamedResponse.headers['content-type'].toString(),

View File

@@ -17,6 +17,7 @@ void main() {
home: Scaffold( home: Scaffold(
body: SendButton( body: SendButton(
isWorking: false, isWorking: false,
isStreaming: false,
onTap: () => sendPressed = true, onTap: () => sendPressed = true,
onCancel: () => cancelPressed = true, onCancel: () => cancelPressed = true,
), ),
@@ -46,6 +47,7 @@ void main() {
home: Scaffold( home: Scaffold(
body: SendButton( body: SendButton(
isWorking: true, isWorking: true,
isStreaming: false,
onTap: () => sendPressed = true, onTap: () => sendPressed = true,
onCancel: () => cancelPressed = true, onCancel: () => cancelPressed = true,
), ),
@@ -74,6 +76,7 @@ void main() {
builder: (context, setState) { builder: (context, setState) {
return Scaffold( return Scaffold(
body: SendButton( body: SendButton(
isStreaming: false,
isWorking: isWorking, isWorking: isWorking,
onTap: () => setState(() => isWorking = true), onTap: () => setState(() => isWorking = true),
onCancel: () => setState(() => isWorking = false), onCancel: () => setState(() => isWorking = false),