blob: af390d24dbf766f9be94a52269bc0db37924499b [file] [log] [blame]
// 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:convert';
import 'dart:html';
import 'dart:typed_data';
import 'package:async/async.dart';
import 'package:stream_channel/stream_channel.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 _webSocket;
String get protocol => _webSocket.protocol;
int get closeCode => _closeCode;
int _closeCode;
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 => _webSocket.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;
Stream get stream => _controller.foreign.stream;
final _controller = new StreamChannelController(
sync: true, allowForeignErrors: false);
WebSocketSink get sink => _sink;
WebSocketSink _sink;
/// Creates a new WebSocket connection.
///
/// Connects to [url] using [new WebSocket] 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
/// [new WebSocket].
///
/// 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(url, {Iterable<String> protocols,
BinaryType binaryType})
: this(new WebSocket(url.toString(), protocols)
..binaryType = (binaryType ?? BinaryType.list).value);
/// Creates a channel wrapping [webSocket].
HtmlWebSocketChannel(this._webSocket){
_sink = new _HtmlWebSocketSink(this);
if (_webSocket.readyState == WebSocket.OPEN) {
_listen();
} else {
// The socket API guarantees that only a single open event will be
// emitted.
_webSocket.onOpen.first.then((_) {
_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.
_webSocket.onError.first.then((_) {
_controller.local.sink.addError(
new WebSocketChannelException("WebSocket connection failed."));
_controller.local.sink.close();
});
_webSocket.onMessage.listen((event) {
var data = event.data;
if (data is ByteBuffer) data = data.asUint8List();
_controller.local.sink.add(data);
});
// The socket API guarantees that only a single error event will be emitted,
// and that once it is no other events will be emitted.
_webSocket.onClose.first.then((event) {
_closeCode = event.code;
_closeReason = event.reason;
_controller.local.sink.close();
});
}
/// Pipes user events to [_webSocket].
void _listen() {
_controller.local.stream.listen((message) => _webSocket.send(message),
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 != null && _localCloseReason != null) {
_webSocket.close(_localCloseCode, _localCloseReason);
} else if (_localCloseCode != null) {
_webSocket.close(_localCloseCode);
} else {
_webSocket.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);
void addUtf8Text(List<int> bytes) => super.add(UTF8.decode(bytes));
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 = const BinaryType._("blob", "blob");
/// Tells the channel to emit binary messages as [Uint8List]s.
static const list = const 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);
String toString() => name;
}