LP#1268619: websocket JS libs: reconnect and auto-connect
[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 var WEBSOCKET_URL_PATH = '/osrf-websocket-translator';
17 var WEBSOCKET_PORT = 7680;
18 var WEBSOCKET_PORT_SSL = 7682;
19
20 // set of shared ports (i.e. browser tabs)
21 var connected_ports = {};
22 var port_identifier = 0;
23
24 // maps osrf message threads to a port index in connected_ports
25 var thread_port_map = {}; 
26
27 // our shared websocket
28 var websocket;
29
30 // pending messages awaiting a successful websocket connection
31 var pending_ws_messages = [];
32
33 function send_msg_to_port(ident, msg) {
34     console.debug('sending msg to port ' + ident + ' : ' + msg.action);
35     try {
36         connected_ports[ident].postMessage(msg);
37     } catch(E) {
38         // some browsers (Opera) throw an exception when messaging
39         // a disconnected port.
40         console.debug('unable to send msg to port ' + ident);
41         delete connected_ports[ident];
42     }
43 }
44
45 // send a message to all listeners
46 function broadcast(msg) {
47     for (var ident in connected_ports)
48       send_msg_to_port(ident, msg);
49 }
50
51
52 // opens the websocket connection
53 // port_ident refers to the requesting port
54 function send_to_websocket(message) {
55
56     if (websocket && websocket.readyState == websocket.OPEN) {
57         // websocket connection is viable.  send our message.
58         websocket.send(message);
59         return;
60     }
61
62     // no viable connection. queue our outbound messages for future delivery.
63     pending_ws_messages.push(message);
64
65     if (websocket && websocket.readyState == websocket.CONNECTING) {
66         // we are already in the middle of a setup call.  
67         // our queued message will be delivered after setup completes.
68         return;
69     }
70
71     // we have no websocket or an invalid websocket.  build a new one.
72
73     // TODO:
74     // assume non-SSL for now.  SSL silently dies if the cert is
75     // invalid and has not been added as an exception.  need to
76     // explain / document / avoid this better.
77     var path = 'ws://' + location.host + ':' + WEBSOCKET_PORT + WEBSOCKET_URL_PATH;
78
79     console.debug('connecting websocket to ' + path);
80
81     try {
82         websocket = new WebSocket(path);
83     } catch(E) {
84         console.log('Error creating WebSocket for path ' + path + ' : ' + E);
85         throw new Error(E);
86     }
87
88     websocket.onopen = function() {
89         console.debug('websocket.onopen()');
90         // deliver any queued messages
91         var msg;
92         while ( (msg = pending_ws_messages.shift()) )
93             websocket.send(msg);
94     }
95
96     websocket.onmessage = function(evt) {
97         var message = evt.data;
98
99         // this is a hack to avoid having to run JSON2js multiple 
100         // times on the same message.  Hopefully match() is faster.
101         // We can't use JSON_v1 within a shared worker for marshalling
102         // messages, because it has no knowledge of application-level
103         // class hints in this environment.
104         var thread;
105         var match = message.match(/"thread":"(.*?)"/);
106         if (!match || !(thread = match[1])) {
107             throw new Error("Websocket message malformed; no thread: " + message);
108         }
109
110         console.debug('websocket received message for thread ' + thread);
111
112         var port_msg = {action: 'message', message : message};
113         var port_ident = thread_port_map[thread];
114
115         if (port_ident) {
116             send_msg_to_port(port_ident, port_msg);
117         } else {
118             // don't know who it's for, broadcast and let the ports
119             // sort it out for themselves.
120             broadcast(port_msg);
121         }
122
123         /* poor man's memory management.  We are not cleaning up our
124          * thread_port_map as we go, because that would require parsing
125          * and analyzing every message to look for opensrf statuses.  
126          * parsing messages adds overhead (see also above comments about
127          * JSON_v1.js).  So, instead, after the map has reached a certain 
128          * size, clear it.  If any pending messages are afield that depend 
129          * on the map, they will be broadcast to all ports on arrival 
130          * (see above).  Only the port expecting a message with the given 
131          * thread will honor the message, all other ports will drop it 
132          * silently.  We could just broadcastfor every messsage, but this 
133          * is more efficient.
134          */
135         if (Object.keys(thread_port_map).length > 1000) 
136             thread_port_map = {};
137     }
138
139     websocket.onerror = function(evt) {
140         var err = "WebSocket Error " + evt + ' : ' + evt.data;
141         // propagate to all ports so it can be logged, etc. 
142         broadcast({action : 'error', message : err});
143         throw new Error(err);
144     }
145
146     websocket.onclose = function() {
147         console.debug('closing websocket');
148         websocket = null;
149     }
150 }
151
152 // called when a new port (tab) is opened
153 onconnect = function(e) {
154     var port = e.ports[0];
155
156     // we have no way of identifying ports within the message handler,
157     // so we apply an identifier to each and toss that into a closer.
158     var port_ident = port_identifier++;
159     connected_ports[port_ident] = port;
160
161     // message handler
162     port.addEventListener('message', function(e) {
163         var data = e.data;
164
165         if (data.action == 'message') {
166             thread_port_map[data.thread] = port_ident;
167             send_to_websocket(data.message);
168             return;
169         } 
170
171         if (messsage.action == 'close') {
172             // TODO: add me to body onunload in calling pages.
173             delete connected_ports[port_ident];
174             console.debug('closed port ' + port_ident + 
175                 '; ' + Object.keys(connected_ports).length + ' remaining');
176             return;
177         }
178
179     }, false);
180
181     port.start();
182
183     console.debug('added port ' + port_ident + 
184       '; ' + Object.keys(connected_ports).length + ' total');
185 }
186