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}