This repository was archived by the owner on Feb 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 111
/
Copy pathhtml.dart
204 lines (173 loc) · 6.88 KB
/
html.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// 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/helpers.dart';
import 'src/channel.dart';
import 'src/exception.dart';
import 'src/web_helpers.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.onOpenX.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.onErrorX.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.onMessageX.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.onCloseX.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;
}