Skip to content

Commit

Permalink
Implement WebSocket for the browser (#1142)
Browse files Browse the repository at this point in the history
  • Loading branch information
brianquinlan authored Mar 4, 2024
1 parent 470d2c3 commit 557c420
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 16 deletions.
172 changes: 164 additions & 8 deletions .github/workflows/dart.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkgs/web_socket/lib/browser_web_socket.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 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.

export 'src/browser_web_socket.dart' show BrowserWebSocket;
4 changes: 4 additions & 0 deletions pkgs/web_socket/lib/io_web_socket.dart
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
// 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.

export 'src/io_web_socket.dart' show IOWebSocket;
128 changes: 128 additions & 0 deletions pkgs/web_socket/lib/src/browser_web_socket.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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>();

static Future<BrowserWebSocket> connect(Uri url) async {
final webSocket = web.WebSocket(url.toString())..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 StateError('WebSocket is closed');
}
// Silently discards the data if the connection is closed.
_webSocket.send(b.jsify()!);
}

@override
void sendText(String s) {
if (_events.isClosed) {
throw StateError('WebSocket is closed');
}
// 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 StateError('WebSocket is closed');
}

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;
}
Loading

0 comments on commit 557c420

Please sign in to comment.