Main coves client
1import 'dart:async';
2
3/// Returns the input if it's a String, otherwise returns null.
4String? ifString<V>(V v) => v is String ? v : null;
5
6/// Extracts the MIME type from Content-Type header.
7///
8/// Example: "application/json; charset=utf-8" -> "application/json"
9String? contentMime(Map<String, String> headers) {
10 final contentType = headers['content-type'];
11 if (contentType == null) return null;
12 return contentType.split(';')[0].trim();
13}
14
15/// Event detail map for custom event handling.
16///
17/// This is a simplified version of TypeScript's CustomEvent pattern,
18/// adapted for Dart using StreamController and typed events.
19///
20/// Example:
21/// ```dart
22/// final target = CustomEventTarget();
23/// final subscription = target.addEventListener('myEvent', (String detail) {
24/// print('Received: $detail');
25/// });
26///
27/// // Later, to remove the listener:
28/// subscription.cancel();
29/// ```
30class CustomEventTarget<EventDetailMap> {
31 final Map<String, StreamController<dynamic>> _controllers = {};
32
33 /// Add an event listener for a specific event type.
34 ///
35 /// Returns a [StreamSubscription] that can be cancelled to remove the listener.
36 ///
37 /// Throws [TypeError] if an event type is already registered with a different type parameter.
38 ///
39 /// Example:
40 /// ```dart
41 /// final subscription = target.addEventListener('event', (detail) => print(detail));
42 /// subscription.cancel(); // Remove this specific listener
43 /// ```
44 StreamSubscription<T> addEventListener<T>(
45 String type,
46 void Function(T detail) callback,
47 ) {
48 final existingController = _controllers[type];
49
50 // Check if a controller already exists with a different type
51 if (existingController != null &&
52 existingController is! StreamController<T>) {
53 throw TypeError();
54 }
55
56 final controller =
57 _controllers.putIfAbsent(type, () => StreamController<T>.broadcast())
58 as StreamController<T>;
59
60 return controller.stream.listen(callback);
61 }
62
63 /// Dispatch a custom event with detail data.
64 ///
65 /// Returns true if the event was dispatched successfully.
66 bool dispatchCustomEvent<T>(String type, T detail) {
67 final controller = _controllers[type];
68 if (controller == null) return false;
69
70 (controller as StreamController<T>).add(detail);
71 return true;
72 }
73
74 /// Dispose of all stream controllers.
75 ///
76 /// Call this when the event target is no longer needed to prevent memory leaks.
77 void dispose() {
78 for (final controller in _controllers.values) {
79 controller.close();
80 }
81 _controllers.clear();
82 }
83}
84
85/// Combines multiple cancellation tokens into a single cancellable operation.
86///
87/// This is a Dart adaptation of the TypeScript combineSignals function.
88/// Since Dart doesn't have AbortSignal/AbortController, we use CancellationToken
89/// pattern with StreamController.
90///
91/// The returned controller will be cancelled if any of the input tokens are cancelled.
92class CombinedCancellationToken {
93 final StreamController<void> _controller = StreamController<void>.broadcast();
94 final List<StreamSubscription<void>> _subscriptions = [];
95 bool _isCancelled = false;
96 Object? _reason;
97
98 CombinedCancellationToken(List<CancellationToken?> tokens) {
99 for (final token in tokens) {
100 if (token != null) {
101 if (token.isCancelled) {
102 cancel(Exception('Operation was cancelled: ${token.reason}'));
103 return;
104 }
105
106 final subscription = token.stream.listen((_) {
107 cancel(Exception('Operation was cancelled: ${token.reason}'));
108 });
109 _subscriptions.add(subscription);
110 }
111 }
112 }
113
114 /// Whether this operation has been cancelled.
115 bool get isCancelled => _isCancelled;
116
117 /// The reason for cancellation, if any.
118 Object? get reason => _reason;
119
120 /// Stream that emits when the operation is cancelled.
121 Stream<void> get stream => _controller.stream;
122
123 /// Cancel the operation with an optional reason.
124 void cancel([Object? reason]) {
125 if (_isCancelled) return;
126
127 _isCancelled = true;
128 _reason = reason ?? Exception('Operation was cancelled');
129
130 _controller.add(null);
131 dispose();
132 }
133
134 /// Clean up resources.
135 void dispose() {
136 for (final subscription in _subscriptions) {
137 subscription.cancel();
138 }
139 _subscriptions.clear();
140 _controller.close();
141 }
142}
143
144/// Represents a cancellable operation.
145///
146/// This is a Dart equivalent of AbortSignal in JavaScript.
147class CancellationToken {
148 final StreamController<void> _controller = StreamController<void>.broadcast();
149 bool _isCancelled = false;
150 Object? _reason;
151
152 CancellationToken();
153
154 /// Whether this operation has been cancelled.
155 bool get isCancelled => _isCancelled;
156
157 /// The reason for cancellation, if any.
158 Object? get reason => _reason;
159
160 /// Stream that emits when the operation is cancelled.
161 Stream<void> get stream => _controller.stream;
162
163 /// Cancel the operation with an optional reason.
164 void cancel([Object? reason]) {
165 if (_isCancelled) return;
166
167 _isCancelled = true;
168 _reason = reason ?? Exception('Operation was cancelled');
169
170 // Only add to stream if not already closed
171 if (!_controller.isClosed) {
172 _controller.add(null);
173 }
174 }
175
176 /// Throw an exception if the operation has been cancelled.
177 void throwIfCancelled() {
178 if (_isCancelled) {
179 throw _reason ?? Exception('Operation was cancelled');
180 }
181 }
182
183 /// Dispose of the stream controller.
184 void dispose() {
185 _controller.close();
186 }
187}
188
189/// Combines multiple cancellation tokens into a single token.
190///
191/// If any of the input tokens are cancelled, the returned token will also be cancelled.
192/// The returned token should be disposed when no longer needed.
193CombinedCancellationToken combineSignals(List<CancellationToken?> signals) {
194 return CombinedCancellationToken(signals);
195}