WebsocketRpc: Use instance as a function instead of confusing .call function
[KiwiIRC.git] / server / websocketrpc.js
1 /*
2 TODO:
3 Create a document explaining the protocol
4 Some way to expire unused callbacks? TTL? expireCallback() function?
5 */
6
7 /**
8 * Wrapper around creating a new WebsocketRpcCaller
9 * This lets us use the WebsocketRpc object as a function
10 */
11 function WebsocketRpc(eio_socket) {
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 }
20
21 ret._mixinEmitter();
22 ret._bindSocketListeners();
23
24 return ret;
25 }
26
27
28 function WebsocketRpcCaller(eio_socket) {
29 this._next_id = 0;
30 this._rpc_callbacks = {};
31 this._socket = eio_socket;
32 }
33
34
35 WebsocketRpcCaller.prototype._bindSocketListeners = function() {
36 var self = this;
37
38 // Proxy the onMessage listener
39 this._onMessageProxy = function rpcOnMessageBoundFunction(){
40 self._onMessage.apply(self, arguments);
41 };
42 this._socket.on('message', this._onMessageProxy);
43 };
44
45
46
47 WebsocketRpcCaller.prototype.dispose = function() {
48 if (this._onMessageProxy) {
49 this._socket.removeListener('message', this._onMessageProxy);
50 delete this._onMessageProxy;
51 }
52
53 this.removeAllListeners();
54 };
55
56
57
58
59 /**
60 * The engine.io socket already has an emitter mixin so steal it from there
61 */
62 WebsocketRpcCaller.prototype._mixinEmitter = function() {
63 var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
64
65 for (var i=0; i<funcs.length; i++) {
66 if (typeof this._socket[funcs[i]] === 'function')
67 this[funcs[i]] = this._socket[funcs[i]];
68 }
69 };
70
71
72 /**
73 * Check if a packet is a valid RPC call
74 */
75 WebsocketRpcCaller.prototype._isCall = function(packet) {
76 return (typeof packet.method !== 'undefined' &&
77 typeof packet.params !== 'undefined');
78 };
79
80
81 /**
82 * Check if a packet is a valid RPC response
83 */
84 WebsocketRpcCaller.prototype._isResponse = function(packet) {
85 return (typeof packet.id !== 'undefined' &&
86 typeof packet.response !== 'undefined');
87 };
88
89
90
91 /**
92 * Make an RPC call
93 * First argument must be the method name to call
94 * If the last argument is a function, it is used as a callback
95 * All other arguments are passed to the RPC method
96 * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn)
97 */
98 WebsocketRpcCaller.prototype.makeCall = function(method) {
99 var params, callback, packet;
100
101 // Get a normal array of passed in arguments
102 params = Array.prototype.slice.call(arguments, 1, arguments.length);
103
104 // If the last argument is a function, take it as a callback and strip it out
105 if (typeof params[params.length-1] === 'function') {
106 callback = params[params.length-1];
107 params = params.slice(0, params.length-1);
108 }
109
110 packet = {
111 method: method,
112 params: params
113 };
114
115 if (typeof callback === 'function') {
116 packet.id = this._next_id;
117
118 this._next_id++;
119 this._rpc_callbacks[packet.id] = callback;
120 }
121
122 this.send(packet);
123 };
124
125
126 /**
127 * Encode the packet into JSON and send it over the websocket
128 */
129 WebsocketRpcCaller.prototype.send = function(packet) {
130 if (this._socket)
131 this._socket.send(JSON.stringify(packet));
132 };
133
134
135 /**
136 * Handler for the websocket `message` event
137 */
138 WebsocketRpcCaller.prototype._onMessage = function(message_raw) {
139 var self = this,
140 packet,
141 returnFn,
142 callback;
143
144 try {
145 packet = JSON.parse(message_raw);
146 if (!packet) throw 'Corrupt packet';
147 } catch(err) {
148 return;
149 }
150
151 if (this._isResponse(packet)) {
152 // If we have no callback waiting for this response, don't do anything
153 if (typeof this._rpc_callbacks[packet.id] !== 'function')
154 return;
155
156 // Delete the callback before calling it. If any exceptions accur within the callback
157 // we don't have to worry about the delete not happening
158 callback = this._rpc_callbacks[packet.id];
159 delete this._rpc_callbacks[packet.id];
160
161 callback.apply(this, packet.response);
162
163 } else if (this._isCall(packet)) {
164 // Calls with an ID may be responded to
165 if (typeof packet.id !== 'undefined') {
166 returnFn = this._createReturnCallFn(packet.id);
167 } else {
168 returnFn = this._noop;
169 }
170
171 this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params));
172 this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
173 }
174 };
175
176
177 /**
178 * Returns a function used as a callback when responding to a call
179 */
180 WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) {
181 var self = this;
182
183 return function returnCallFn() {
184 var value = Array.prototype.slice.call(arguments, 0);
185
186 var ret_packet = {
187 id: packet_id,
188 response: value
189 };
190
191 self.send(ret_packet);
192 };
193 };
194
195
196
197 WebsocketRpcCaller.prototype._noop = function() {};
198
199
200
201
202 // If running a node module, set the exports
203 if (typeof module === 'object' && typeof module.exports !== 'undefined') {
204 module.exports = WebsocketRpc;
205 }