blob: fc66628b7a5854fb17b2888374b9d3e4fa39de39 [file] [log] [blame]
// 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;