| // Copyright (c) 2016, 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:async/async.dart'; |
| import 'package:stream_channel/stream_channel.dart'; |
| import 'package:web/web.dart'; |
| |
| import 'src/channel.dart'; |
| import 'src/exception.dart'; |
| |
| /// A [WebSocketChannel] that communicates using a `dart:html` [WebSocket]. |
| class HtmlWebSocketChannel extends StreamChannelMixin |
| implements WebSocketChannel { |
| /// The underlying `dart:html` [WebSocket]. |
| final WebSocket innerWebSocket; |
| |
| @override |
| String? get protocol => innerWebSocket.protocol; |
| |
| @override |
| int? get closeCode => _closeCode; |
| int? _closeCode; |
| |
| @override |
| String? get closeReason => _closeReason; |
| String? _closeReason; |
| |
| /// The number of bytes of data that have been queued but not yet transmitted |
| /// to the network. |
| int? get bufferedAmount => innerWebSocket.bufferedAmount; |
| |
| /// The close code set by the local user. |
| /// |
| /// To ensure proper ordering, this is stored until we get a done event on |
| /// [_controller.local.stream]. |
| int? _localCloseCode; |
| |
| /// The close reason set by the local user. |
| /// |
| /// To ensure proper ordering, this is stored until we get a done event on |
| /// [_controller.local.stream]. |
| String? _localCloseReason; |
| |
| /// Completer for [ready]. |
| late Completer<void> _readyCompleter; |
| |
| @override |
| Future<void> get ready => _readyCompleter.future; |
| |
| @override |
| Stream get stream => _controller.foreign.stream; |
| |
| final _controller = |
| StreamChannelController<Object?>(sync: true, allowForeignErrors: false); |
| |
| @override |
| late final WebSocketSink sink = _HtmlWebSocketSink(this); |
| |
| /// Creates a new WebSocket connection. |
| /// |
| /// Connects to [url] using [WebSocket.new] and returns a channel that can be |
| /// used to communicate over the resulting socket. The [url] may be either a |
| /// [String] or a [Uri]. The [protocols] parameter is the same as for |
| /// [WebSocket.new]. |
| /// |
| /// The [binaryType] parameter controls what type is used for binary messages |
| /// received by this socket. It defaults to [BinaryType.list], which causes |
| /// binary messages to be delivered as [Uint8List]s. If it's |
| /// [BinaryType.blob], they're delivered as [Blob]s instead. |
| HtmlWebSocketChannel.connect(Object url, |
| {Iterable<String>? protocols, BinaryType? binaryType}) |
| : this( |
| WebSocket( |
| url.toString(), |
| protocols?.map((e) => e.toJS).toList().toJS ?? JSArray(), |
| )..binaryType = (binaryType ?? BinaryType.list).value, |
| ); |
| |
| /// Creates a channel wrapping [webSocket]. |
| /// |
| /// The parameter [webSocket] should be either a dart:html `WebSocket` |
| /// instance or a package:web [WebSocket] instance. |
| HtmlWebSocketChannel(Object /*WebSocket*/ webSocket) |
| : innerWebSocket = webSocket as WebSocket { |
| _readyCompleter = Completer(); |
| if (innerWebSocket.readyState == WebSocket.OPEN) { |
| _readyCompleter.complete(); |
| _listen(); |
| } else { |
| if (innerWebSocket.readyState == WebSocket.CLOSING || |
| innerWebSocket.readyState == WebSocket.CLOSED) { |
| _readyCompleter.completeError(WebSocketChannelException( |
| 'WebSocket state error: ${innerWebSocket.readyState}')); |
| } |
| // The socket API guarantees that only a single open event will be |
| // emitted. |
| innerWebSocket.onOpen.first.then((_) { |
| _readyCompleter.complete(); |
| _listen(); |
| }); |
| } |
| |
| // The socket API guarantees that only a single error event will be emitted, |
| // and that once it is no open or message events will be emitted. |
| innerWebSocket.onError.first.then((_) { |
| // Unfortunately, the underlying WebSocket API doesn't expose any |
| // specific information about the error itself. |
| final error = WebSocketChannelException('WebSocket connection failed.'); |
| if (!_readyCompleter.isCompleted) { |
| _readyCompleter.completeError(error); |
| } |
| _controller.local.sink.addError(error); |
| _controller.local.sink.close(); |
| }); |
| |
| innerWebSocket.onMessage.listen(_innerListen); |
| |
| // The socket API guarantees that only a single error event will be emitted, |
| // and that once it is no other events will be emitted. |
| innerWebSocket.onClose.first.then((event) { |
| _closeCode = event.code; |
| _closeReason = event.reason; |
| _controller.local.sink.close(); |
| }); |
| } |
| |
| void _innerListen(MessageEvent event) { |
| // Event data will be ArrayBuffer, Blob, or String. |
| final eventData = event.data; |
| final Object? data; |
| if (eventData.typeofEquals('string')) { |
| data = (eventData as JSString).toDart; |
| } else if (eventData.typeofEquals('object') && |
| (eventData as JSObject).instanceOfString('ArrayBuffer')) { |
| data = (eventData as JSArrayBuffer).toDart.asUint8List(); |
| } else { |
| // Blobs are passed directly. |
| data = eventData; |
| } |
| _controller.local.sink.add(data); |
| } |
| |
| /// Pipes user events to [innerWebSocket]. |
| void _listen() { |
| _controller.local.stream.listen((obj) => innerWebSocket.send(obj!.jsify()!), |
| onDone: () { |
| // On Chrome and possibly other browsers, `null` can't be passed as the |
| // default here. The actual arity of the function call must be correct or |
| // it will fail. |
| if ((_localCloseCode, _localCloseReason) |
| case (final closeCode?, final closeReason?)) { |
| innerWebSocket.close(closeCode, closeReason); |
| } else if (_localCloseCode case final closeCode?) { |
| innerWebSocket.close(closeCode); |
| } else { |
| innerWebSocket.close(); |
| } |
| }); |
| } |
| } |
| |
| /// A [WebSocketSink] that tracks the close code and reason passed to [close]. |
| class _HtmlWebSocketSink extends DelegatingStreamSink implements WebSocketSink { |
| /// The channel to which this sink belongs. |
| final HtmlWebSocketChannel _channel; |
| |
| _HtmlWebSocketSink(HtmlWebSocketChannel channel) |
| : _channel = channel, |
| super(channel._controller.foreign.sink); |
| |
| @override |
| Future close([int? closeCode, String? closeReason]) { |
| _channel._localCloseCode = closeCode; |
| _channel._localCloseReason = closeReason; |
| return super.close(); |
| } |
| } |
| |
| /// An enum for choosing what type [HtmlWebSocketChannel] emits for binary |
| /// messages. |
| class BinaryType { |
| /// Tells the channel to emit binary messages as [Blob]s. |
| static const blob = BinaryType._('blob', 'blob'); |
| |
| /// Tells the channel to emit binary messages as [Uint8List]s. |
| static const list = BinaryType._('list', 'arraybuffer'); |
| |
| /// The name of the binary type, which matches its variable name. |
| final String name; |
| |
| /// The value as understood by the underlying [WebSocket] API. |
| final String value; |
| |
| const BinaryType._(this.name, this.value); |
| |
| @override |
| String toString() => name; |
| } |