1 /* -----------------------------------------------------------------------
2 * Copyright (C) 2014 Equinox Software, Inc.
3 * Bill Erickson <berick@esilibrary.com>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * ----------------------------------------------------------------------- */
18 * Shared WebSocket communication layer. Each browser tab registers with
19 * this code all inbound / outbound messages are delivered through a
20 * single websocket connection managed within.
22 * Messages take the form : {action : my_action, message : my_message}
23 * actions for tab-generated messages may be "message" or "close".
24 * actions for messages generated within may be "message" or "error"
27 var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
28 var WEBSOCKET_PORT = 7680; // TODO: remove. all traffic should use SSL.
29 var WEBSOCKET_PORT_SSL = 7682;
30 var WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE = 1000;
33 * Collection of shared ports (browser tabs)
35 var connected_ports = {};
38 * Each port gets a local identifier so we have an easy way to refer to
41 var port_identifier = 0;
43 // maps osrf message threads to a port index in connected_ports.
44 // this is how we know which browser tab to deliver messages to.
45 var thread_port_map = {};
48 * Browser-global, shared websocket connection.
53 * Pending messages awaiting a successful websocket connection
55 * instead of asking the caller to pass messages after a connection
56 * is made, queue the messages for the caller and deliver them
57 * after the connection is established.
59 var pending_ws_messages = [];
62 * Deliver the message blob to the specified port (tab)
64 function send_msg_to_port(ident, msg) {
65 console.debug('sending msg to port ' + ident + ' : ' + msg.action);
67 connected_ports[ident].postMessage(msg);
69 // some browsers (Opera) throw an exception when messaging
70 // a disconnected port.
71 console.debug('unable to send msg to port ' + ident);
72 delete connected_ports[ident];
77 * Send a message blob to all ports (tabs)
79 function broadcast(msg) {
80 for (var ident in connected_ports)
81 send_msg_to_port(ident, msg);
86 * Opens the websocket connection.
88 * If our global socket is already open, use it. Otherwise, queue the
89 * message for delivery after the socket is open.
91 function send_to_websocket(message) {
93 if (websocket && websocket.readyState == websocket.OPEN) {
94 // websocket connection is viable. send our message now.
95 websocket.send(message);
99 // no viable connection. queue our outbound messages for future delivery.
100 pending_ws_messages.push(message);
102 if (websocket && websocket.readyState == websocket.CONNECTING) {
103 // we are already in the middle of a setup call.
104 // our queued message will be delivered after setup completes.
108 // we have no websocket or an invalid websocket. build a new one.
111 // assume non-SSL for now. SSL silently dies if the cert is
112 // invalid and has not been added as an exception. need to
113 // explain / document / avoid this better.
114 var path = 'ws://' + location.host + ':' + WEBSOCKET_PORT + WEBSOCKET_URL_PATH;
116 console.debug('connecting websocket to ' + path);
119 websocket = new WebSocket(path);
121 console.log('Error creating WebSocket for path ' + path + ' : ' + E);
125 websocket.onopen = function() {
126 console.debug('websocket.onopen()');
127 // deliver any queued messages
129 while ( (msg = pending_ws_messages.shift()) )
133 websocket.onmessage = function(evt) {
134 var message = evt.data;
136 // this is sort of a hack to avoid having to run JSON2js
137 // multiple times on the same message. Hopefully match() is
138 // faster. Note: We can't use JSON_v1 within a shared worker
139 // for marshalling messages, because it has no knowledge of
140 // application-level class hints in this environment.
142 var match = message.match(/"thread":"(.*?)"/);
143 if (!match || !(thread = match[1])) {
144 throw new Error("Websocket message malformed; no thread: " + message);
147 console.debug('websocket received message for thread ' + thread);
149 var port_msg = {action: 'message', message : message};
150 var port_ident = thread_port_map[thread];
153 send_msg_to_port(port_ident, port_msg);
155 // don't know who it's for, broadcast and let the ports
156 // sort it out for themselves.
160 /* poor man's memory management. We are not cleaning up our
161 * thread_port_map as we go, because that would require parsing
162 * and analyzing every message to look for opensrf statuses.
163 * parsing messages adds overhead (see also above comments about
164 * JSON_v1.js). So, instead, after the map has reached a certain
165 * size, clear it. If any pending messages are afield that depend
166 * on the map, they will be broadcast to all ports on arrival
167 * (see above). Only the port expecting a message with the given
168 * thread will honor the message, all other ports will drop it
169 * silently. We could just broadcastfor every messsage, but this
170 * is presumably more efficient.
172 * If for some reason this fails to work as expected, we could add
173 * a new tab->ws message type for marking a thread as complete.
174 * My hunch is this will be faster, since it will require a lot
175 * fewer cross-tab messages overall.
177 if (Object.keys(thread_port_map).length >
178 WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE) {
179 console.debug('resetting thread_port_map');
180 thread_port_map = {};
185 * Websocket error handler. This type of error indicates a probelem
186 * with the connection. I.e. it's not port-specific.
187 * Broadcast to all ports.
189 websocket.onerror = function(evt) {
190 var err = "WebSocket Error " + evt + ' : ' + evt.data;
191 broadcast({action : 'error', message : err});
192 websocket.close(); // connection is no good; reset.
193 throw new Error(err);
197 * Called when the websocket connection is closed.
199 * Once a websocket is closed, it will be re-opened the next time
200 * a message delivery attempt is made. Clean up and prepare to reconnect.
202 websocket.onclose = function() {
203 console.debug('closing websocket');
205 thread_port_map = {};
210 * New port (tab) opened handler
212 * Apply the port identifier and message handlers.
214 onconnect = function(e) {
215 var port = e.ports[0];
217 // we have no way of identifying ports within the message handler,
218 // so we apply an identifier to each and toss that into a closer.
219 var port_ident = port_identifier++;
220 connected_ports[port_ident] = port;
223 port.addEventListener('message', function(e) {
226 if (data.action == 'message') {
227 thread_port_map[data.thread] = port_ident;
228 send_to_websocket(data.message);
232 if (messsage.action == 'close') {
233 // TODO: all browser tabs need an onunload handler which sends
234 // a action=close message, so that the port may be removed from
235 // the conected_ports collection.
236 delete connected_ports[port_ident];
237 console.debug('closed port ' + port_ident +
238 '; ' + Object.keys(connected_ports).length + ' remaining');
246 console.debug('added port ' + port_ident +
247 '; ' + Object.keys(connected_ports).length + ' total');