]> git.evergreen-ils.org Git - OpenSRF.git/blob - src/javascript/opensrf_ws_shared.js
LP#1268619: websockets: support WS via shared web workers
[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 function send_msg_to_port(ident, msg) {
31     console.debug('sending msg to port ' + ident + ' : ' + msg.action);
32     try {
33         connected_ports[ident].postMessage(msg);
34     } catch(E) {
35         // some browsers (Opera) throw an exception when messaging
36         // a disconnected port.
37         console.log('unable to send msg to port ' + ident);
38         delete connected_ports[ident];
39     }
40 }
41
42 // send a message to all listeners
43 function broadcast(msg) {
44     for (var ident in connected_ports)
45       send_msg_to_port(ident, msg);
46 }
47
48
49 // opens the websocket connection
50 // port_ident refers to the requesting port
51 function open_websocket(port_ident) {
52     var port = connected_ports[port_ident];
53
54     if (websocket) {
55         switch (websocket.readyState) {
56
57             case websocket.CONNECTING:
58                 // nothing to do.  The port will get notified on open
59                 return;
60
61             case websocket.OPEN:
62                 // websocket is already open, let the connecting.
63                 // other ports have been notified already, so
64                 // no broadcast is required.
65                 send_msg_to_port(port_ident, {action : 'socket_connected'});
66                 return;
67
68             default:
69                 // websocket is no longer connected.  We need a new socket.
70                 websocket = null;
71         }
72     }
73
74     // TODO:
75     // assume non-SSL for now.  SSL silently dies if the cert is
76     // invalid and has not been added as an exception.
77     var path = 'ws://' + location.host + ':' + 
78         WEBSOCKET_PORT + WEBSOCKET_URL_PATH
79
80     console.log('connecting websocket to ' + path);
81
82     websocket = new WebSocket(path);
83
84     websocket.onopen = function() {
85         // tell all ports the websocket is open and ready
86         console.log('websocket.onopen()');
87         broadcast({action : 'socket_connected'});
88     }
89
90     websocket.onmessage = function(evt) {
91         var message = evt.data;
92
93         // this is a hack to avoid having to run JSON2js multiple 
94         // times on the same message.  Hopefully match() is faster.
95         var thread;
96         var match = message.match(/"thread":"(.*?)"/);
97         if (!match || !(thread = match[1])) {
98             throw new Error("Websocket message malformed; no thread: " + message);
99         }
100
101         console.debug('websocket received message for thread ' + thread);
102
103         var port_msg = {action: 'message', message : message};
104         var port_ident = thread_port_map[thread];
105
106         if (port_ident) {
107             send_msg_to_port(port_ident, port_msg);
108         } else {
109             // don't know who it's for, broadcast and let the ports
110             // sort it out for themselves.
111             broadcast(port_msg);
112         }
113
114         /* poor man's memory management.  We are not cleaning up our
115          * thread_port_map as we go, because that gets messy.  Instead,
116          * after the map has reached a certain size, clear it.  If any
117          * pending messages are afield that depend on the map, they 
118          * will be broadcast to all ports on arrival (see above).  Only the 
119          * port expecting a message with the given thread will honor the 
120          * message, all other ports will drop it silently.  We could just 
121          * do that for every messsage, but this is more efficient.
122          */
123         if (Object.keys(thread_port_map).length > 1000) {
124             thread_port_map = {};
125         }
126     }
127
128     websocket.onerror = function(evt) {
129         var err = "WebSocket Error " + evt + ' : ' + evt.data;
130         // propagate to all ports so it can be logged, etc. 
131         broadcast({action : 'error', message : err});
132         throw new Error(err);
133     }
134
135     websocket.onclose = function() {
136         console.log('closing websocket');
137     }
138 }
139
140 // called when a new port (tab) is opened
141 onconnect = function(e) {
142     var port = e.ports[0];
143
144     // we have no way of identifying ports within the message handler,
145     // so we apply an identifier to each and toss that into a closer.
146     var port_ident = port_identifier++;
147     connected_ports[port_ident] = port;
148
149     // message handler
150     port.addEventListener('message', function(e) {
151         var data = e.data;
152
153         if (data.action == 'message') {
154             thread_port_map[data.thread] = port_ident;
155             websocket.send(data.message);
156             return;
157         } 
158
159         if (messsage.action == 'close') {
160             // TODO: add me to body onunload in calling pages.
161             delete connected_ports[port_ident];
162             console.log('closed port ' + port_ident + 
163                 '; ' + Object.keys(connected_ports).length + ' remaining');
164             return;
165         }
166
167     }, false);
168
169     port.start();
170
171     console.log('added port ' + port_ident + 
172       '; ' + Object.keys(connected_ports).length + ' total');
173
174     open_websocket(port_ident);
175 }
176