-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathclient.dart
More file actions
223 lines (193 loc) · 8.21 KB
/
Copy pathclient.dart
File metadata and controls
223 lines (193 loc) · 8.21 KB
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2017 Workiva Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async';
import 'dart:js';
import 'dart:js_util' as js_util;
import 'package:js/js.dart';
import 'package:opentelemetry/api.dart';
import 'package:w_common/disposable_browser.dart';
import 'package:sockjs_client_wrapper/src/events.dart';
import 'package:sockjs_client_wrapper/src/js_interop.dart' as js_interop;
/// Error thrown when the required `sockjs.js` library has not been loaded.
class MissingSockJSLibError extends Error {
@override
String toString() =>
'Missing SockJS Library: sockjs.js or sockjs_prod.js must be loaded '
'(details: https://goo.gl/VGM6Pr).';
}
/// A SockJS Client that acts and looks like a browser WebSocket object.
///
/// See https://github.com/sockjs/sockjs-client for more information on SockJS
/// in general.
///
/// All of the SockJS implementation logic lives in the native SockJS library.
/// This [SockJSClient] class is simply a wrapper that provides a typed API
/// accessible from Dart.
class SockJSClient extends Disposable {
@override
String get disposableTypeName => 'SockJSClient';
// The native SockJS client object.
late js_interop.SockJS _jsClient;
// Event stream controllers.
final StreamController<SockJSCloseEvent> _onCloseController =
StreamController<SockJSCloseEvent>.broadcast();
final StreamController<SockJSMessageEvent> _onMessageController =
StreamController<SockJSMessageEvent>.broadcast();
final StreamController<SockJSOpenEvent> _onOpenController =
StreamController<SockJSOpenEvent>.broadcast();
/// Context used to capture open and close events while establishing the
/// initial connection.
///
/// This value is set to the current context during construction, then set to
/// root after the first open or close event. This is to correlate events
/// related to initial connection with the construction of this object without
/// creating long running traces due to events from much further along in the
/// sessions lifetime.
Context _context;
/// Constructs a new [SockJSClient] that will attempt to connect to a SockJS
/// server at the given [uri].
///
/// Additional configuration can be provided via [options]:
///
/// - `SockJSOptions.server` - string to append to url for actual data
/// connection. Defaults to a random 4 digit number.
/// - `SockJSOptions.transports` - list of transports that may be used by
/// SockJS. By default, all available transports will be used.
///
/// For example, the following would create a client with a whitelist of three
/// transport protocols:
///
/// final uri = Uri.parse('ws://example.org/echo');
/// final options = new SockJSOptions(
/// transports: ['websocket', 'xhr-streaming', 'xhr-polling']);
/// final client = new SockJSClient(uri, options: options);
SockJSClient(Uri uri, {SockJSOptions? options}) : _context = Context.current {
try {
_jsClient = js_interop.SockJS(uri.toString(), null, options?._toJs());
// ignore: avoid_catches_without_on_clauses
} catch (e) {
if (!js_interop.hasSockJS) {
throw MissingSockJSLibError();
} else {
rethrow;
}
}
manageStreamController(_onCloseController);
manageStreamController(_onMessageController);
manageStreamController(_onOpenController);
_addManagedEventListenerToJSClient('close', _onClose);
_addManagedEventListenerToJSClient('message', _onMessage);
_addManagedEventListenerToJSClient('open', _onOpen);
// Automatically dispose if this client closes. If this close event is
// emitted in response to a call to dispose(), then this will effectively be
// a no-op.
listenToStream<SockJSCloseEvent>(onClose, (_) => dispose());
}
/// A stream that can be listened to in order to know when this SockJS Client
/// has closed.
///
/// Only a single [SockJSCloseEvent] will be emitted from this stream.
///
/// The event will include the close code and reason, if available.
Stream<SockJSCloseEvent> get onClose => _onCloseController.stream;
/// A stream of message events received from the server.
///
/// Each [SockJSMessageEvent] will include the [String] payload.
Stream<SockJSMessageEvent> get onMessage => _onMessageController.stream;
/// A stream that can be listened to in order to know when this SockJS Client
/// has successfully connected to the server.
///
/// Only a single [SockJSOpenEvent] will be emitted from this stream unless a
/// connection cannot be made, in which case no events will be emitted at all.
///
/// The event will include the selected transport as well as the server URL.
Stream<SockJSOpenEvent> get onOpen => _onOpenController.stream;
num get timeout => js_util.getProperty(_jsClient, '_timeout');
/// Close this client.
///
/// Optionally, a [closeCode] and [reason] can be provided.
void close([int? closeCode, String? reason]) {
_jsClient.close(closeCode, reason);
}
/// Send data to the server.
void send(String data) {
_jsClient.send(data);
}
@override
Future<Null> onWillDispose() async {
// If the client is already closed, there is nothing to do.
if (_jsClient.readyState == 3 /* closed */) {
return;
}
// Close this client. If already closing, this will be a no-op.
close();
// Wait for the event to be emitted on the `onClose` stream. This ensures
// that the underlying StreamController is not disposed too early.
await onClose.first;
}
void _addManagedEventListenerToJSClient(String eventName, Function callback) {
final interopAllowedCallback = allowInterop(callback);
_jsClient.addEventListener(eventName, interopAllowedCallback);
getManagedDisposer(() async {
_jsClient.removeEventListener(eventName, interopAllowedCallback);
});
}
void _onClose(js_interop.SockJSCloseEvent event) {
spanFromContext(_context).addEvent('sockjs.close', attributes: [
Attribute.fromString('sockjs.transport', _jsClient.transport),
Attribute.fromDouble('sockjs.timeout', timeout.toDouble()),
Attribute.fromInt('sockjs.close.code', event.code),
Attribute.fromString('sockjs.close.reason', event.reason),
]);
_context = Context.root; // Reset context to root for future events.
_onCloseController.add(SockJSCloseEvent(
event.code,
event.reason,
wasClean: event.wasClean,
));
}
void _onMessage(js_interop.SockJSMessageEvent event) {
_onMessageController.add(SockJSMessageEvent(event.data));
}
void _onOpen(_) {
spanFromContext(_context).addEvent('sockjs.open', attributes: [
Attribute.fromString('sockjs.transport', _jsClient.transport),
Attribute.fromDouble('sockjs.timeout', timeout.toDouble()),
]);
_context = Context.root; // Reset context to root for future events.
_onOpenController.add(SockJSOpenEvent(
_jsClient.transport,
Uri.parse(_jsClient.url),
Uri.parse(_jsClient.transportUrl ?? ''),
));
}
}
/// A configuration object to be used when constructing a [SockJSClient].
class SockJSOptions {
/// String to append to url for actual data connection.
///
/// Defaults to a random 4 digit number.
final String? server;
/// A list of transports that may be used by SockJS.
///
/// By default, all available transports will be used. Specifying a whitelist
/// can be useful if you need to disable certain fallback transports.
final List<String>? transports;
final num? timeout;
/// Construct a [SockJSOptions] instance to be passed to the [SockJSClient]
/// constructor.
SockJSOptions({this.server, this.transports, this.timeout});
js_interop.SockJSOptions _toJs() => js_interop.SockJSOptions(
server: server, transports: transports, timeout: timeout);
}