Skip to content

Commit 557c420

Browse files
authored
Implement WebSocket for the browser (#1142)
1 parent 470d2c3 commit 557c420

13 files changed

+370
-16
lines changed

.github/workflows/dart.yml

Lines changed: 164 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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+
export 'src/browser_web_socket.dart' show BrowserWebSocket;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
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+
15
export 'src/io_web_socket.dart' show IOWebSocket;
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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

Comments
 (0)