Require Dart 3.2, use pkg:web (#92)

diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index e6e4909..73bf502 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -47,7 +47,7 @@
       matrix:
         # Add macos-latest and/or windows-latest if relevant for this package.
         os: [ubuntu-latest]
-        sdk: [3.1.0, dev]
+        sdk: [3.2.0, dev]
     steps:
       - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
       - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36822cf..fef1e31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
-## 4.1.3-dev
+## 4.1.3
 
-- Update the minimum Dart SDK version to `3.1.0`.
+- Update the minimum Dart SDK version to `3.2.0`.
 
 ## 4.1.2
 
diff --git a/lib/client/sse_client.dart b/lib/client/sse_client.dart
index 86dac10..96cd1f3 100644
--- a/lib/client/sse_client.dart
+++ b/lib/client/sse_client.dart
@@ -4,12 +4,12 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:html';
+import 'dart:js_interop';
 
-import 'package:js/js.dart';
 import 'package:logging/logging.dart';
 import 'package:pool/pool.dart';
 import 'package:stream_channel/stream_channel.dart';
+import 'package:web/helpers.dart';
 
 import '../src/util/uuid.dart';
 
@@ -52,14 +52,15 @@
             ? generateUuidV4()
             : '$debugKey-${generateUuidV4()}' {
     _serverUrl = '$serverUrl?sseClientId=$_clientId';
-    _eventSource = EventSource(_serverUrl, withCredentials: true);
+    _eventSource =
+        EventSource(_serverUrl, EventSourceInit(withCredentials: true));
     _eventSource.onOpen.first.whenComplete(() {
       _onConnected.complete();
       _outgoingController.stream
           .listen(_onOutgoingMessage, onDone: _onOutgoingDone);
     });
-    _eventSource.addEventListener('message', _onIncomingMessage);
-    _eventSource.addEventListener('control', _onIncomingControlMessage);
+    _eventSource.addEventListener('message', _onIncomingMessage.toJS);
+    _eventSource.addEventListener('control', _onIncomingControlMessage.toJS);
 
     _eventSource.onOpen.listen((_) {
       _errorTimer?.cancel();
@@ -114,7 +115,7 @@
 
   void _onIncomingControlMessage(Event message) {
     var data = (message as MessageEvent).data;
-    if (data == 'close') {
+    if (data.dartify() == 'close') {
       close();
     } else {
       throw UnsupportedError('[$_clientId] Illegal Control Message "$data"');
@@ -147,8 +148,10 @@
         final url = '$_serverUrl&messageId=${++_lastMessageId}';
         await _fetch(
             url,
-            _FetchOptions(
-                method: 'POST', body: encodedMessage, credentials: 'include'));
+            RequestInit(
+                method: 'POST',
+                body: encodedMessage?.toJS,
+                credentials: 'include'));
       } catch (error) {
         final augmentedError =
             '[$_clientId] SSE client failed to send $message:\n $error';
@@ -159,20 +162,5 @@
   }
 }
 
-// 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 String credentials, // e.g., 'omit', 'same-origin', 'include'
-    required String? body,
-  });
-}
+Future<void> _fetch(String resourceUrl, RequestInit options) =>
+    window.fetch(resourceUrl.toJS, options).toDart;
diff --git a/lib/src/server/sse_handler.dart b/lib/src/server/sse_handler.dart
index d4b7e06..c235c20 100644
--- a/lib/src/server/sse_handler.dart
+++ b/lib/src/server/sse_handler.dart
@@ -158,7 +158,7 @@
       // period.
       // If the connection comes back, this will be cancelled and all messages
       // left in the queue tried again.
-      _keepAliveTimer = Timer(_keepAlive!, _close);
+      _keepAliveTimer = Timer(_keepAlive, _close);
     }
   }
 
diff --git a/pubspec.yaml b/pubspec.yaml
index 1258266..8581f8e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: sse
-version: 4.1.3-dev
+version: 4.1.3
 description: >-
   Provides client and server functionality for setting up bi-directional
   communication through Server Sent Events (SSE) and corresponding POST
@@ -7,7 +7,7 @@
 repository: https://github.com/dart-lang/sse
 
 environment:
-  sdk: ^3.1.0
+  sdk: ^3.2.0
 
 dependencies:
   async: ^2.0.8
@@ -17,6 +17,7 @@
   pool: ^1.5.0
   shelf: ^1.1.0
   stream_channel: ^2.0.0
+  web: '>=0.3.0 <0.5.0'
 
 dev_dependencies:
   dart_flutter_team_lints: ^2.0.0
diff --git a/test/web/index.dart b/test/web/index.dart
index e149372..fadb147 100644
--- a/test/web/index.dart
+++ b/test/web/index.dart
@@ -2,9 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:html';
-
 import 'package:sse/client/sse_client.dart';
+import 'package:web/helpers.dart';
 
 void main() {
   var channel = SseClient('/test');