]> git.evergreen-ils.org Git - OpenSRF.git/blob - src/javascript/opensrf_ws_shared.js
LP#1268619: websockets: detect connectedness of JS default sockets
[OpenSRF.git] / src / javascript / opensrf_ws_shared.js
1 /* -----------------------------------------------------------------------
2  * Copyright (C) 2014  Equinox Software, Inc.
3  * Bill Erickson <berick@esilibrary.com>
4  *  
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.
9  * 
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  * ----------------------------------------------------------------------- */
15
16
17 /**
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.
21  *
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"
25  */
26
27 var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
28 var WEBSOCKET_PORT_SSL = 7682;
29 var WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE = 1000;
30
31 /**
32  * Collection of shared ports (browser tabs)
33  */
34 var connected_ports = {};
35
36 /**
37  * Each port gets a local identifier so we have an easy way to refer to 
38  * it later.
39  */
40 var port_identifier = 0;
41
42 // maps osrf message threads to a port index in connected_ports.
43 // this is how we know which browser tab to deliver messages to.
44 var thread_port_map = {}; 
45
46 /**
47  * Browser-global, shared websocket connection.
48  */
49 var websocket;
50
51 /** 
52  * Pending messages awaiting a successful websocket connection
53  *
54  * instead of asking the caller to pass messages after a connection
55  * is made, queue the messages for the caller and deliver them
56  * after the connection is established.
57  */
58 var pending_ws_messages = [];
59
60 /** 
61  * Deliver the message blob to the specified port (tab)
62  */
63 function send_msg_to_port(ident, msg) {
64     console.debug('sending msg to port ' + ident + ' : ' + msg.action);
65     try {
66         connected_ports[ident].postMessage(msg);
67     } catch(E) {
68         // some browsers (Opera) throw an exception when messaging
69         // a disconnected port.
70         console.debug('unable to send msg to port ' + ident);
71         delete connected_ports[ident];
72     }
73 }
74
75 /**
76  * Send a message blob to all ports (tabs)
77  */
78 function broadcast(msg) {
79     for (var ident in connected_ports)
80       send_msg_to_port(ident, msg);
81 }
82
83
84 /**
85  * Opens the websocket connection.
86  *
87  * If our global socket is already open, use it.  Otherwise, queue the 
88  * message for delivery after the socket is open.
89  */
90 function send_to_websocket(message) {
91
92     if (websocket && websocket.readyState == websocket.OPEN) {
93         // websocket connection is viable.  send our message now.
94         websocket.send(message);
95         return;
96     }
97
98     // no viable connection. queue our outbound messages for future delivery.
99     pending_ws_messages.push(message);
100
101     if (websocket && websocket.readyState == websocket.CONNECTING) {
102         // we are already in the middle of a setup call.  
103         // our queued message will be delivered after setup completes.
104         return;
105     }
106
107     // we have no websocket or an invalid websocket.  build a new one.
108
109     // TODO:
110     // assume non-SSL for now.  SSL silently dies if the cert is
111     // invalid and has not been added as an exception.  need to
112     // explain / document / avoid this better.
113     var path = 'wss://' + location.host + ':' + 
114         WEBSOCKET_PORT_SSL + WEBSOCKET_URL_PATH;
115
116     console.debug('connecting websocket to ' + path);
117
118     try {
119         websocket = new WebSocket(path);
120     } catch(E) {
121         console.log('Error creating WebSocket for path ' + path + ' : ' + E);
122         throw new Error(E);
123     }
124
125     websocket.onopen = function() {
126         console.debug('websocket.onopen()');
127         // deliver any queued messages
128         var msg;
129         while ( (msg = pending_ws_messages.shift()) )
130             websocket.send(msg);
131
132     }
133
134     websocket.onmessage = function(evt) {
135         var message = evt.data;
136
137         // this is sort of a hack to avoid having to run JSON2js
138         // multiple times on the same message.  Hopefully match() is
139         // faster.  Note: We can't use JSON_v1 within a shared worker
140         // for marshalling messages, because it has no knowledge of
141         // application-level class hints in this environment.
142         var thread;
143         var match = message.match(/"thread":"(.*?)"/);
144         if (!match || !(thread = match[1])) {
145             throw new Error("Websocket message malformed; no thread: " + message);
146         }
147
148         console.debug('websocket received message for thread ' + thread);
149
150         var port_msg = {action: 'message', message : message};
151         var port_ident = thread_port_map[thread];
152
153         if (port_ident) {
154             send_msg_to_port(port_ident, port_msg);
155         } else {
156             // don't know who it's for, broadcast and let the ports
157             // sort it out for themselves.
158             broadcast(port_msg);
159         }
160
161         /* poor man's memory management.  We are not cleaning up our
162          * thread_port_map as we go, because that would require parsing
163          * and analyzing every message to look for opensrf statuses.  
164          * parsing messages adds overhead (see also above comments about
165          * JSON_v1.js).  So, instead, after the map has reached a certain 
166          * size, clear it.  If any pending messages are afield that depend 
167          * on the map, they will be broadcast to all ports on arrival 
168          * (see above).  Only the port expecting a message with the given 
169          * thread will honor the message, all other ports will drop it 
170          * silently.  We could just broadcastfor every messsage, but this 
171          * is presumably more efficient.
172          *
173          * If for some reason this fails to work as expected, we could add
174          * a new tab->ws message type for marking a thread as complete.
175          * My hunch is this will be faster, since it will require a lot
176          * fewer cross-tab messages overall.
177          */
178         if (Object.keys(thread_port_map).length > 
179                 WEBSOCKET_MAX_THREAD_PORT_CACHE_SIZE) {
180             console.debug('resetting thread_port_map');
181             thread_port_map = {};
182         }
183     }
184
185     /**
186      * Websocket error handler.  This type of error indicates a probelem
187      * with the connection.  I.e. it's not port-specific. 
188      * Broadcast to all ports.
189      */
190     websocket.onerror = function(evt) {
191         var err = "WebSocket Error " + evt;
192         console.error(err);
193         broadcast({action : 'event', type : 'onerror', message : err});
194         websocket.close(); // connection is no good; reset.
195     }
196
197     /**
198      * Called when the websocket connection is closed.
199      *
200      * Once a websocket is closed, it will be re-opened the next time
201      * a message delivery attempt is made.  Clean up and prepare to reconnect.
202      */
203     websocket.onclose = function() {
204         console.debug('closing websocket');
205         websocket = null;
206         thread_port_map = {};
207         broadcast({action : 'event', type : 'onclose'});
208     }
209 }
210
211 /**
212  * New port (tab) opened handler
213  *
214  * Apply the port identifier and message handlers.
215  */
216 onconnect = function(e) {
217     var port = e.ports[0];
218
219     // we have no way of identifying ports within the message handler,
220     // so we apply an identifier to each and toss that into a closer.
221     var port_ident = port_identifier++;
222     connected_ports[port_ident] = port;
223
224     // message handler
225     port.addEventListener('message', function(e) {
226         var data = e.data;
227
228         if (data.action == 'message') {
229             thread_port_map[data.thread] = port_ident;
230             send_to_websocket(data.message);
231             return;
232         } 
233
234         if (messsage.action == 'close') {
235             // TODO: all browser tabs need an onunload handler which sends
236             // a action=close message, so that the port may be removed from
237             // the conected_ports collection.
238             delete connected_ports[port_ident];
239             console.debug('closed port ' + port_ident + 
240                 '; ' + Object.keys(connected_ports).length + ' remaining');
241             return;
242         }
243
244     }, false);
245
246     port.start();
247
248     console.debug('added port ' + port_ident + 
249       '; ' + Object.keys(connected_ports).length + ' total');
250 }
251