-
Notifications
You must be signed in to change notification settings - Fork 9
/
index.js
355 lines (308 loc) · 11 KB
/
index.js
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
$(document).ready(function() {
var ReadyState = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
};
var LogMessageType = {
TEXT: 'text',
BINARY: 'binary'
};
var LogMessageSender = {
SYSTEM: 'system',
LOCAL: 'local',
REMOTE: 'remote',
CONSOLE: 'console'
};
var MAX_LOG_SIZE = 500;
var closeCodeToString = function(code) {
code = Number(code);
if (code >= 0 && code <= 999) {
return 'UNKNOWN_UNUSED_CODE';
}
if (code >= 1016 && code <= 1999) {
return 'UNKNOWN_WEBSOCKET_CODE';
}
if (code >= 2000 && code <= 2999) {
return 'UNKNOWN_EXTENSION_CODE';
}
if (code >= 3000 && code <= 3999) {
return 'UNKNOWN_FRAMEWORK_CODE';
}
if (code >= 4000 && code <= 4999) {
return 'UNKNOWN_APPLICATION_CODE';
}
switch (code) {
case 1000: return 'NORMAL';
case 1001: return 'GOING_AWAY';
case 1002: return 'PROTOCOL_ERROR';
case 1003: return 'UNSUPPORTED';
case 1014: // fall through
case 1004: return 'UNKNOWN_RESERVED_CODE';
case 1005: return 'NO_STATUS_RECVD';
case 1006: return 'ABNORMAL';
case 1007: return 'UNSUPPORTED_DATA';
case 1008: return 'POLICY_VIOLATION';
case 1009: return 'TOO_LARGE';
case 1010: return 'MISSING_EXTENSION';
case 1011: return 'INTERNAL_ERROR';
case 1012: return 'RESTARTING';
case 1013: return 'TRY_AGAIN_LATER';
case 1015: return 'TLS_HANDSHAKE';
default:
break;
}
return 'UNKNOWN';
};
var socket = null;
var transition = function() {
$(".controls").removeClass("open closed closing connecting");
$(".controls input").removeAttr("disabled");
$(".controls textarea").removeAttr("disabled");
$(".controls select").removeAttr("disabled");
if (socket == null || socket.readyState == ReadyState.CLOSED) {
$(".controls").addClass("closed");
$(".controls #btn_close").attr("disabled", "disabled");
$(".controls #close_status").attr("disabled", "disabled");
$(".controls #close_reason").attr("disabled", "disabled");
$(".controls #btn_send").attr("disabled", "disabled");
$(".controls #message_text").attr("disabled", "disabled");
}
else if (socket.readyState == ReadyState.CONNECTING) {
$(".controls").addClass("connecting");
$(".controls #endpoint").attr("disabled", "disabled");
$(".controls #protocols").attr("disabled", "disabled");
$(".controls #reconnect").attr("disabled", "disabled");
$(".controls #btn_connect").attr("disabled", "disabled");
$(".controls #btn_send").attr("disabled", "disabled");
$(".controls #message_text").attr("disabled", "disabled");
$(".controls #close_status").attr("disabled", "disabled");
$(".controls #close_reason").attr("disabled", "disabled");
}
else if (socket.readyState == ReadyState.CLOSING) {
$(".controls").addClass("closing");
$(".controls #endpoint").attr("disabled", "disabled");
$(".controls #protocols").attr("disabled", "disabled");
$(".controls #reconnect").attr("disabled", "disabled");
$(".controls #btn_connect").attr("disabled", "disabled");
$(".controls #btn_close").attr("disabled", "disabled");
$(".controls #close_status").attr("disabled", "disabled");
$(".controls #close_reason").attr("disabled", "disabled");
$(".controls #btn_send").attr("disabled", "disabled");
$(".controls #message_text").attr("disabled", "disabled");
}
else if (socket.readyState == ReadyState.OPEN) {
$(".controls").addClass("open");
$(".controls #endpoint").attr("disabled", "disabled");
$(".controls #protocols").attr("disabled", "disabled");
$(".controls #reconnect").attr("disabled", "disabled");
$(".controls #btn_connect").attr("disabled", "disabled");
}
else {
window.console.log("Invalid socket ready state.", socket);
throw new Error("Invalid socket ready state.");
}
};
var logIsScrolledToBottom = function() {
var j = $(".log");
var e = j[0];
return e.scrollTop + j.height() + 10 /* padding */ >= e.scrollHeight - 10 /* some tolerance */;
};
var scrollLogToBottom = function() {
var e = $(".log")[0];
e.scrollTop = e.scrollHeight;
};
var pruneLog = function() {
var e = $(".log .entries");
if (e.length == 0) {
return;
}
// Prune the oldest entry.
if (e[0].children.length == MAX_LOG_SIZE) {
e[0].children[0].remove();
}
};
var clearLog = function() {
$(".log .entries").empty();
};
var addLogEntry = function(sender, type, data) {
pruneLog();
if (type == LogMessageType.BINARY) {
data = '(BINARY MESSAGE: ' + data.size + ' bytes)\n' + blobToHex(data);
} else {
data = data || '(empty message)';
}
var entry = $("<div>").addClass('entry');
var publisher = $("<div>").addClass('publisher').addClass(sender);
var content = $("<div>").addClass('content').addClass(type).text(data);
var timestamp = $("<div>").addClass('timestamp').text('just now');
entry.attr('timestamp', '' + Date.now());
publisher.append(timestamp);
entry.append(publisher);
entry.append(content);
var scroll = logIsScrolledToBottom();
$(".log .entries").append(entry);
if (scroll) {
scrollLogToBottom();
}
};
var maybeReconnect = function() {
if ($("#reconnect").is(":checked") &&
!socket.__isClientClose) {
connect($("#endpoint").val(), $("#protocols").val());
}
};
var blobToHex = function(blob) {
// TODO: Implement (seems to be non-trivial).
return '';
};
var connect = function(url, protocols) {
protocols = parseProtocols(protocols);
try {
socket = new WebSocket(url, protocols);
} catch (err) {
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
'Unable to open connection: ' + err);
}
socket.binaryType = "blob";
socket.__openTime = Date.now();
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
"Attempt to connect to '" + socket.url + "'...");
transition();
socket.onclose = function(event) {
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
"The connection was terminated " + (event.wasClean ? 'cleanly' : 'uncleanly')+ " with status " +
"code " + event.code + " (" + closeCodeToString(event.code) + ").\n" +
(event.reason ? 'The reason provided was: ' + event.reason + '\n' : ''));
transition();
maybeReconnect();
};
socket.onerror = function(event) {
// No way to access error message via JS API... Lame.
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
"An error occured with the connection - refer to the Chrome developer console for detailed error info.");
transition();
};
socket.onmessage = function(event) {
if (typeof event.data == 'string') {
addLogEntry(LogMessageSender.REMOTE,
LogMessageType.TEXT,
event.data);
}
else if (event.data instanceof Blob) {
addLogEntry(LogMessageSender.REMOTE,
LogMessageType.BINARY,
event.data);
}
else {
window.console.log('Bad data type: ', event.data);
throw new Error('Bad data type received.');
}
};
socket.onopen = function(event) {
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
"The connection was established successfully (in " + (Date.now() - socket.__openTime)+ " ms).\n" +
(socket.extensions ? 'Negotiated Extensions: ' + socket.extensions : '') +
(socket.protocol ? 'Negotiated Protocol: ' + socket.protocol : ''));
transition();
};
};
var sendText = function(text) {
addLogEntry(LogMessageSender.LOCAL,
LogMessageType.TEXT,
text);
socket.send(text);
};
var sendBytes = function(bytes) {
// TODO: Implement.
// Same as sendText but pass a Blob or ArrayBuffer into send().
};
var parseProtocols = function(s) {
var p = s.split(",")
.map(function(x) { return x.trim(); })
.filter(function(x) { return x.length > 0; });
if (p.length > 0) {
return p;
}
return undefined;
};
var close = function(code, reason) {
socket.__isClientClose = true; // Prevent re-connects.
try {
socket.close(Number(code), reason);
} catch (err) {
socket.__isClientClose = false;
addLogEntry(LogMessageSender.SYSTEM,
LogMessageType.TEXT,
'Unable to close connection: ' + err);
}
};
var updateCloseText = function() {
$("#close_status_text").text(closeCodeToString($("#close_status").val()));
}
$("#btn_connect").on('click', function(event) {
event.preventDefault();
connect($("#endpoint").val(), $("#protocols").val());
});
$("#btn_send").on('click', function(event) {
event.preventDefault();
sendText($("#message_text").val());
scrollLogToBottom();
});
$("#btn_close").on('click', function(event) {
event.preventDefault();
close($("#close_status").val(), $("#close_reason").val());
});
$("#btn_clear_log").on('click', function(event) {
event.preventDefault();
clearLog();
});
$("#close_status").on('change', function(event) {
updateCloseText();
});
transition();
updateCloseText();
addLogEntry(LogMessageSender.CONSOLE,
LogMessageType.TEXT,
'Welcome! You may initiate and manage a web socket connection using the controls above. Messages sent and received will appear in this log.');
var SECOND = 1000;
var MINUTE = SECOND * 60;
var HOUR = MINUTE * 60;
var DAY = HOUR * 24;
var MONTH = DAY * 30;
var YEAR = MONTH * 12;
var formatTimeDifference = function(now, then) {
var difference = Math.abs(now - then); // in ms
if (difference < 30 * SECOND) {
return 'just now';
}
if (difference < MINUTE) {
return '< 1 min ago';
}
if (difference < HOUR) {
return Math.round(difference / MINUTE) + ' min ago';
}
if (difference < DAY) {
return Math.round(difference / HOUR) + ' hr ago';
}
if (difference < MONTH) {
return Math.round(difference / DAY) + ' day ago';
}
return Math.round(difference / YEAR) + ' yr ago';
};
// NOTE: O(n) w.r.t. number of log entries (capped at MAX_LOG_SIZE).
var updateTimestamps = function() {
var entries = $(".log .entries .entry");
var now = Date.now();
for (var i = 0; i < entries.length; i++) {
$(entries[i]).find(".publisher .timestamp").text(formatTimeDifference(now, Number($(entries[i]).attr('timestamp'))));
}
};
window.setInterval(updateTimestamps, 15 * SECOND);
});