BSD and expact license modified
[KiwiIRC.git] / server / websocketrpc.js
CommitLineData
d99e7823 1/*
b42bd8f3 2 TODO:
d99e7823 3 Create a document explaining the protocol
b42bd8f3 4 Some way to expire unused callbacks? TTL? expireCallback() function?
d99e7823
D
5*/
6
d718a0f9
D
7/**
8 * Wrapper around creating a new WebsocketRpcCaller
9 * This lets us use the WebsocketRpc object as a function
10 */
d99e7823 11function WebsocketRpc(eio_socket) {
d718a0f9
D
12 var caller = new WebsocketRpcCaller(eio_socket);
13 var ret = function WebsocketRpcInstance() {
14 return ret.makeCall.apply(ret, arguments);
15 };
16
17 for(var prop in caller){
18 ret[prop] = caller[prop];
19 }
d99e7823 20
d718a0f9
D
21 ret._mixinEmitter();
22 ret._bindSocketListeners();
23
c7cb4d54
D
24 // Keep a reference to the main Rpc object so namespaces can find calling functions
25 ret._rpc = ret;
26
d718a0f9
D
27 return ret;
28}
29
30
31function WebsocketRpcCaller(eio_socket) {
d99e7823 32 this._next_id = 0;
f621d0ff 33 this._rpc_callbacks = {};
d99e7823 34 this._socket = eio_socket;
c7cb4d54
D
35
36 this._rpc = this;
37 this._namespace = '';
38 this._namespaces = [];
d99e7823
D
39}
40
41
d718a0f9 42WebsocketRpcCaller.prototype._bindSocketListeners = function() {
d99e7823
D
43 var self = this;
44
45 // Proxy the onMessage listener
46 this._onMessageProxy = function rpcOnMessageBoundFunction(){
47 self._onMessage.apply(self, arguments);
48 };
49 this._socket.on('message', this._onMessageProxy);
50};
51
52
53
d718a0f9 54WebsocketRpcCaller.prototype.dispose = function() {
d99e7823 55 if (this._onMessageProxy) {
0fe7c000 56 this._socket.removeListener('message', this._onMessageProxy);
d99e7823
D
57 delete this._onMessageProxy;
58 }
0fe7c000 59
c7cb4d54
D
60 // Clean up any namespaces
61 for (var idx in this._namespaces) {
62 this._namespaces[idx].dispose();
63 }
64
0fe7c000 65 this.removeAllListeners();
d99e7823
D
66};
67
68
69
c7cb4d54
D
70WebsocketRpcCaller.prototype.namespace = function(namespace_name) {
71 var complete_namespace, namespace;
72
73 if (this._namespace) {
74 complete_namespace = this._namespace + '.' + namespace_name;
75 } else {
76 complete_namespace = namespace_name;
77 }
78
79 namespace = new this._rpc.Namespace(this._rpc, complete_namespace);
80 this._rpc._namespaces.push(namespace);
81
82 return namespace;
83};
84
85
86
87// Find all namespaces that either matches or starts with namespace_name
88WebsocketRpcCaller.prototype._findRelevantNamespaces = function(namespace_name) {
89 var found_namespaces = [];
90
91 for(var idx in this._namespaces) {
92 if (this._namespaces[idx]._namespace === namespace_name) {
93 found_namespaces.push(this._namespaces[idx]);
94 }
95
96 if (this._namespaces[idx]._namespace.indexOf(namespace_name + '.') === 0) {
97 found_namespaces.push(this._namespaces[idx]);
98 }
99 }
100
101 return found_namespaces;
102};
103
104
d99e7823
D
105
106/**
107 * The engine.io socket already has an emitter mixin so steal it from there
108 */
c7cb4d54 109WebsocketRpcCaller.prototype._mixinEmitter = function(target_obj) {
d99e7823
D
110 var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
111
c7cb4d54
D
112 target_obj = target_obj || this;
113
d99e7823 114 for (var i=0; i<funcs.length; i++) {
a9cd4622 115 if (typeof this._socket[funcs[i]] === 'function')
c7cb4d54 116 target_obj[funcs[i]] = this._socket[funcs[i]];
d99e7823
D
117 }
118};
119
120
121/**
122 * Check if a packet is a valid RPC call
123 */
d718a0f9 124WebsocketRpcCaller.prototype._isCall = function(packet) {
d99e7823
D
125 return (typeof packet.method !== 'undefined' &&
126 typeof packet.params !== 'undefined');
127};
128
129
130/**
131 * Check if a packet is a valid RPC response
132 */
d718a0f9 133WebsocketRpcCaller.prototype._isResponse = function(packet) {
d99e7823
D
134 return (typeof packet.id !== 'undefined' &&
135 typeof packet.response !== 'undefined');
136};
137
138
139
140/**
141 * Make an RPC call
142 * First argument must be the method name to call
143 * If the last argument is a function, it is used as a callback
144 * All other arguments are passed to the RPC method
d718a0f9 145 * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn)
d99e7823 146 */
d718a0f9 147WebsocketRpcCaller.prototype.makeCall = function(method) {
d99e7823
D
148 var params, callback, packet;
149
b42bd8f3 150 // Get a normal array of passed in arguments
d99e7823
D
151 params = Array.prototype.slice.call(arguments, 1, arguments.length);
152
b42bd8f3 153 // If the last argument is a function, take it as a callback and strip it out
d99e7823
D
154 if (typeof params[params.length-1] === 'function') {
155 callback = params[params.length-1];
156 params = params.slice(0, params.length-1);
157 }
158
159 packet = {
160 method: method,
161 params: params
162 };
163
164 if (typeof callback === 'function') {
165 packet.id = this._next_id;
166
167 this._next_id++;
f621d0ff 168 this._rpc_callbacks[packet.id] = callback;
d99e7823
D
169 }
170
171 this.send(packet);
172};
173
174
175/**
176 * Encode the packet into JSON and send it over the websocket
177 */
d718a0f9 178WebsocketRpcCaller.prototype.send = function(packet) {
d99e7823
D
179 if (this._socket)
180 this._socket.send(JSON.stringify(packet));
181};
182
183
184/**
185 * Handler for the websocket `message` event
186 */
d718a0f9 187WebsocketRpcCaller.prototype._onMessage = function(message_raw) {
d99e7823
D
188 var self = this,
189 packet,
a6b1c062 190 returnFn,
c7cb4d54
D
191 callback,
192 namespace, namespaces, idx;
d99e7823
D
193
194 try {
195 packet = JSON.parse(message_raw);
196 if (!packet) throw 'Corrupt packet';
197 } catch(err) {
198 return;
199 }
200
201 if (this._isResponse(packet)) {
b42bd8f3 202 // If we have no callback waiting for this response, don't do anything
f621d0ff 203 if (typeof this._rpc_callbacks[packet.id] !== 'function')
b42bd8f3
D
204 return;
205
a6b1c062
D
206 // Delete the callback before calling it. If any exceptions accur within the callback
207 // we don't have to worry about the delete not happening
208 callback = this._rpc_callbacks[packet.id];
f621d0ff 209 delete this._rpc_callbacks[packet.id];
d99e7823 210
a6b1c062
D
211 callback.apply(this, packet.response);
212
d99e7823
D
213 } else if (this._isCall(packet)) {
214 // Calls with an ID may be responded to
215 if (typeof packet.id !== 'undefined') {
b42bd8f3 216 returnFn = this._createReturnCallFn(packet.id);
d99e7823 217 } else {
b42bd8f3 218 returnFn = this._noop;
d99e7823
D
219 }
220
0ca6adac 221 this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params));
d99e7823 222 this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
c7cb4d54
D
223
224 if (packet.method.indexOf('.') > 0) {
225 namespace = packet.method.substring(0, packet.method.lastIndexOf('.'));
226 namespaces = this._findRelevantNamespaces(namespace);
227 for(idx in namespaces){
228 packet.method = packet.method.replace(namespaces[idx]._namespace + '.', '');
229 namespaces[idx].emit.apply(namespaces[idx], [packet.method, returnFn].concat(packet.params));
230 }
231 }
d99e7823
D
232 }
233};
234
235
b42bd8f3
D
236/**
237 * Returns a function used as a callback when responding to a call
238 */
d718a0f9 239WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) {
b42bd8f3
D
240 var self = this;
241
242 return function returnCallFn() {
243 var value = Array.prototype.slice.call(arguments, 0);
244
245 var ret_packet = {
246 id: packet_id,
247 response: value
248 };
249
250 self.send(ret_packet);
251 };
252};
253
254
255
d718a0f9 256WebsocketRpcCaller.prototype._noop = function() {};
b42bd8f3
D
257
258
d99e7823 259
c7cb4d54
D
260WebsocketRpcCaller.prototype.Namespace = function(rpc, namespace) {
261 var ret = function WebsocketRpcNamespaceInstance() {
262 if (typeof arguments[0] === 'undefined') {
263 return;
264 }
265
266 arguments[0] = ret._namespace + '.' + arguments[0];
267 return ret._rpc.apply(ret._rpc, arguments);
268 };
269
270 ret._rpc = rpc;
271 ret._namespace = namespace;
272
273 ret.dispose = function() {
274 ret.removeAllListeners();
275 ret._rpc = null;
276 };
277
278 rpc._mixinEmitter(ret);
279
280 return ret;
281};
282
283
284
d99e7823
D
285
286// If running a node module, set the exports
287if (typeof module === 'object' && typeof module.exports !== 'undefined') {
288 module.exports = WebsocketRpc;
289}