|
| 1 | +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'dart:async'; |
| 6 | +import 'dart:js_interop'; |
| 7 | +import 'dart:typed_data'; |
| 8 | + |
| 9 | +import 'package:web/web.dart' as web; |
| 10 | + |
| 11 | +import '../web_socket.dart'; |
| 12 | +import 'utils.dart'; |
| 13 | + |
| 14 | +/// A [WebSocket] using the browser WebSocket API. |
| 15 | +/// |
| 16 | +/// Usable when targeting the browser using either JavaScript or WASM. |
| 17 | +class BrowserWebSocket implements WebSocket { |
| 18 | + final web.WebSocket _webSocket; |
| 19 | + final _events = StreamController<WebSocketEvent>(); |
| 20 | + |
| 21 | + static Future<BrowserWebSocket> connect(Uri url) async { |
| 22 | + final webSocket = web.WebSocket(url.toString())..binaryType = 'arraybuffer'; |
| 23 | + final browserSocket = BrowserWebSocket._(webSocket); |
| 24 | + final webSocketConnected = Completer<BrowserWebSocket>(); |
| 25 | + |
| 26 | + if (webSocket.readyState == web.WebSocket.OPEN) { |
| 27 | + webSocketConnected.complete(browserSocket); |
| 28 | + } else { |
| 29 | + if (webSocket.readyState == web.WebSocket.CLOSING || |
| 30 | + webSocket.readyState == web.WebSocket.CLOSED) { |
| 31 | + webSocketConnected.completeError(WebSocketException( |
| 32 | + 'Unexpected WebSocket state: ${webSocket.readyState}, ' |
| 33 | + 'expected CONNECTING (0) or OPEN (1)')); |
| 34 | + } else { |
| 35 | + // The socket API guarantees that only a single open event will be |
| 36 | + // emitted. |
| 37 | + unawaited(webSocket.onOpen.first.then((_) { |
| 38 | + webSocketConnected.complete(browserSocket); |
| 39 | + })); |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + unawaited(webSocket.onError.first.then((e) { |
| 44 | + // Unfortunately, the underlying WebSocket API doesn't expose any |
| 45 | + // specific information about the error itself. |
| 46 | + if (!webSocketConnected.isCompleted) { |
| 47 | + final error = WebSocketException('Failed to connect WebSocket'); |
| 48 | + webSocketConnected.completeError(error); |
| 49 | + } else { |
| 50 | + browserSocket._closed(1006, 'error'); |
| 51 | + } |
| 52 | + })); |
| 53 | + |
| 54 | + webSocket.onMessage.listen((e) { |
| 55 | + if (browserSocket._events.isClosed) return; |
| 56 | + |
| 57 | + final eventData = e.data!; |
| 58 | + late WebSocketEvent data; |
| 59 | + if (eventData.typeofEquals('string')) { |
| 60 | + data = TextDataReceived((eventData as JSString).toDart); |
| 61 | + } else if (eventData.typeofEquals('object') && |
| 62 | + (eventData as JSObject).instanceOfString('ArrayBuffer')) { |
| 63 | + data = BinaryDataReceived( |
| 64 | + (eventData as JSArrayBuffer).toDart.asUint8List()); |
| 65 | + } else { |
| 66 | + throw StateError('unexpected message type: ${eventData.runtimeType}'); |
| 67 | + } |
| 68 | + browserSocket._events.add(data); |
| 69 | + }); |
| 70 | + |
| 71 | + unawaited(webSocket.onClose.first.then((event) { |
| 72 | + if (!webSocketConnected.isCompleted) { |
| 73 | + webSocketConnected.complete(browserSocket); |
| 74 | + } |
| 75 | + browserSocket._closed(event.code, event.reason); |
| 76 | + })); |
| 77 | + |
| 78 | + return webSocketConnected.future; |
| 79 | + } |
| 80 | + |
| 81 | + void _closed(int? code, String? reason) { |
| 82 | + if (_events.isClosed) return; |
| 83 | + _events.add(CloseReceived(code, reason ?? '')); |
| 84 | + unawaited(_events.close()); |
| 85 | + } |
| 86 | + |
| 87 | + BrowserWebSocket._(this._webSocket); |
| 88 | + |
| 89 | + @override |
| 90 | + void sendBytes(Uint8List b) { |
| 91 | + if (_events.isClosed) { |
| 92 | + throw StateError('WebSocket is closed'); |
| 93 | + } |
| 94 | + // Silently discards the data if the connection is closed. |
| 95 | + _webSocket.send(b.jsify()!); |
| 96 | + } |
| 97 | + |
| 98 | + @override |
| 99 | + void sendText(String s) { |
| 100 | + if (_events.isClosed) { |
| 101 | + throw StateError('WebSocket is closed'); |
| 102 | + } |
| 103 | + // Silently discards the data if the connection is closed. |
| 104 | + _webSocket.send(s.jsify()!); |
| 105 | + } |
| 106 | + |
| 107 | + @override |
| 108 | + Future<void> close([int? code, String? reason]) async { |
| 109 | + if (_events.isClosed) { |
| 110 | + throw StateError('WebSocket is closed'); |
| 111 | + } |
| 112 | + |
| 113 | + checkCloseCode(code); |
| 114 | + checkCloseReason(reason); |
| 115 | + |
| 116 | + unawaited(_events.close()); |
| 117 | + if ((code, reason) case (final closeCode?, final closeReason?)) { |
| 118 | + _webSocket.close(closeCode, closeReason); |
| 119 | + } else if (code case final closeCode?) { |
| 120 | + _webSocket.close(closeCode); |
| 121 | + } else { |
| 122 | + _webSocket.close(); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + @override |
| 127 | + Stream<WebSocketEvent> get events => _events.stream; |
| 128 | +} |
0 commit comments