| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file | |
| // 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:async'; | |
| import 'dart:js_interop'; | |
| import 'dart:typed_data'; | |
| import 'package:web/web.dart' as web; | |
| import '../web_socket.dart'; | |
| import 'utils.dart'; | |
| /// A [WebSocket] using the browser WebSocket API. | |
| /// | |
| /// Usable when targeting the browser using either JavaScript or WASM. | |
| class BrowserWebSocket implements WebSocket { | |
| final web.WebSocket _webSocket; | |
| final _events = StreamController<WebSocketEvent>(); | |
| /// Create a new WebSocket connection using the JavaScript WebSocket API. | |
| /// | |
| /// The URL supplied in [url] must use the scheme ws or wss. | |
| /// | |
| /// If provided, the [protocols] argument indicates that subprotocols that | |
| /// the peer is able to select. See | |
| /// [RFC-6455 1.9](https://datatracker.ietf.org/doc/html/rfc6455#section-1.9). | |
| static Future<BrowserWebSocket> connect(Uri url, | |
| {Iterable<String>? protocols}) async { | |
| if (!url.isScheme('ws') && !url.isScheme('wss')) { | |
| throw ArgumentError.value( | |
| url, 'url', 'only ws: and wss: schemes are supported'); | |
| } | |
| final webSocket = web.WebSocket(url.toString(), | |
| protocols?.map((e) => e.toJS).toList().toJS ?? JSArray()) | |
| ..binaryType = 'arraybuffer'; | |
| final browserSocket = BrowserWebSocket._(webSocket); | |
| final webSocketConnected = Completer<BrowserWebSocket>(); | |
| if (webSocket.readyState == web.WebSocket.OPEN) { | |
| webSocketConnected.complete(browserSocket); | |
| } else { | |
| if (webSocket.readyState == web.WebSocket.CLOSING || | |
| webSocket.readyState == web.WebSocket.CLOSED) { | |
| webSocketConnected.completeError(WebSocketException( | |
| 'Unexpected WebSocket state: ${webSocket.readyState}, ' | |
| 'expected CONNECTING (0) or OPEN (1)')); | |
| } else { | |
| // The socket API guarantees that only a single open event will be | |
| // emitted. | |
| unawaited(webSocket.onOpen.first.then((_) { | |
| webSocketConnected.complete(browserSocket); | |
| })); | |
| } | |
| } | |
| unawaited(webSocket.onError.first.then((e) { | |
| // Unfortunately, the underlying WebSocket API doesn't expose any | |
| // specific information about the error itself. | |
| if (!webSocketConnected.isCompleted) { | |
| final error = WebSocketException('Failed to connect WebSocket'); | |
| webSocketConnected.completeError(error); | |
| } else { | |
| browserSocket._closed(1006, 'error'); | |
| } | |
| })); | |
| webSocket.onMessage.listen((e) { | |
| if (browserSocket._events.isClosed) return; | |
| final eventData = e.data!; | |
| late WebSocketEvent data; | |
| if (eventData.typeofEquals('string')) { | |
| data = TextDataReceived((eventData as JSString).toDart); | |
| } else if (eventData.typeofEquals('object') && | |
| (eventData as JSObject).instanceOfString('ArrayBuffer')) { | |
| data = BinaryDataReceived( | |
| (eventData as JSArrayBuffer).toDart.asUint8List()); | |
| } else { | |
| throw StateError('unexpected message type: ${eventData.runtimeType}'); | |
| } | |
| browserSocket._events.add(data); | |
| }); | |
| unawaited(webSocket.onClose.first.then((event) { | |
| if (!webSocketConnected.isCompleted) { | |
| webSocketConnected.complete(browserSocket); | |
| } | |
| browserSocket._closed(event.code, event.reason); | |
| })); | |
| return webSocketConnected.future; | |
| } | |
| void _closed(int? code, String? reason) { | |
| if (_events.isClosed) return; | |
| _events.add(CloseReceived(code, reason ?? '')); | |
| unawaited(_events.close()); | |
| } | |
| BrowserWebSocket._(this._webSocket); | |
| @override | |
| void sendBytes(Uint8List b) { | |
| if (_events.isClosed) { | |
| throw WebSocketConnectionClosed(); | |
| } | |
| // Silently discards the data if the connection is closed. | |
| _webSocket.send(b.jsify()!); | |
| } | |
| @override | |
| void sendText(String s) { | |
| if (_events.isClosed) { | |
| throw WebSocketConnectionClosed(); | |
| } | |
| // Silently discards the data if the connection is closed. | |
| _webSocket.send(s.jsify()!); | |
| } | |
| @override | |
| Future<void> close([int? code, String? reason]) async { | |
| if (_events.isClosed) { | |
| throw WebSocketConnectionClosed(); | |
| } | |
| checkCloseCode(code); | |
| checkCloseReason(reason); | |
| unawaited(_events.close()); | |
| if ((code, reason) case (final closeCode?, final closeReason?)) { | |
| _webSocket.close(closeCode, closeReason); | |
| } else if (code case final closeCode?) { | |
| _webSocket.close(closeCode); | |
| } else { | |
| _webSocket.close(); | |
| } | |
| } | |
| @override | |
| Stream<WebSocketEvent> get events => _events.stream; | |
| @override | |
| String get protocol => _webSocket.protocol; | |
| } | |
| const connect = BrowserWebSocket.connect; |