Use Fetch API in SSE Client (#66)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 763f390..26bfc33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.1.2-dev
+
+- Send `fetch` requests instead of `XHR` requests.
+
 ## 4.1.1
 
 - Apply `keepAlive` logic to `SocketException`s.
diff --git a/lib/client/sse_client.dart b/lib/client/sse_client.dart
index 3335346..15f85ed 100644
--- a/lib/client/sse_client.dart
+++ b/lib/client/sse_client.dart
@@ -6,6 +6,7 @@
 import 'dart:convert';
 import 'dart:html';
 
+import 'package:js/js.dart';
 import 'package:logging/logging.dart';
 import 'package:pool/pool.dart';
 import 'package:stream_channel/stream_channel.dart';
@@ -64,13 +65,7 @@
         // By default the SSE client uses keep-alive.
         // Allow for a retry to connect before giving up.
         _errorTimer = Timer(const Duration(seconds: 5), () {
-          _incomingController.addError(error);
-          close();
-          if (!_onConnected.isCompleted) {
-            // This call must happen after the call to close() which checks
-            // whether the completer was completed earlier.
-            _onConnected.completeError(error);
-          }
+          _closeWithError(error);
         });
       }
     });
@@ -103,6 +98,16 @@
     _outgoingController.close();
   }
 
+  void _closeWithError(Object error) {
+    _incomingController.addError(error);
+    close();
+    if (!_onConnected.isCompleted) {
+      // This call must happen after the call to close() which checks
+      // whether the completer was completed earlier.
+      _onConnected.completeError(error);
+    }
+  }
+
   void _onIncomingControlMessage(Event message) {
     var data = (message as MessageEvent).data;
     if (data == 'close') {
@@ -133,12 +138,45 @@
         _logger.warning('Invalid argument: $e');
       }
       try {
-        await HttpRequest.request('$_serverUrl&messageId=${++_lastMessageId}',
-            method: 'POST', sendData: encodedMessage, withCredentials: true);
-      } catch (e) {
-        _logger.severe('Failed to send $message:\n $e');
-        close();
+        final url = '$_serverUrl&messageId=${++_lastMessageId}';
+        await _fetch(
+            url,
+            _FetchOptions(
+                method: 'POST',
+                body: encodedMessage,
+                credentialsOptions:
+                    _CredentialsOptions(credentials: 'include')));
+      } catch (error) {
+        final augmentedError = 'SSE client failed to send $message:\n $error';
+        _logger.severe(augmentedError);
+        _closeWithError(augmentedError);
       }
     });
   }
 }
+
+// Custom implementation of Fetch API until Dart supports GET vs. POST,
+// credentials, etc. See https://github.com/dart-lang/http/issues/595.
+@JS('fetch')
+external Object _nativeJsFetch(String resourceUrl, _FetchOptions options);
+
+Future<dynamic> _fetch(String resourceUrl, _FetchOptions options) =>
+    promiseToFuture(_nativeJsFetch(resourceUrl, options));
+
+@JS()
+@anonymous
+class _FetchOptions {
+  external factory _FetchOptions({
+    required String method, // e.g., 'GET', 'POST'
+    required _CredentialsOptions credentialsOptions,
+    required String? body,
+  });
+}
+
+@JS()
+@anonymous
+class _CredentialsOptions {
+  external factory _CredentialsOptions({
+    required String credentials, // e.g., 'omit', 'same-origin', 'include'
+  });
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 263f336..3de73be 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: sse
-version: 4.1.1
+version: 4.1.2-dev
 description: >-
   Provides client and server functionality for setting up bi-directional
   communication through Server Sent Events (SSE) and corresponding POST