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,
@JsonKey(includeToJson: false) @Default(false) bool isWorking,
@JsonKey(includeToJson: false) DateTime? sendingTime,
@JsonKey(includeToJson: false) @Default(false) bool isStreaming,
String? preRequestScript,
String? postRequestScript,
}) = _RequestModel;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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