| // 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; |