channel:join + channel:leave plugin events
[KiwiIRC.git] / server / websocketrpc.js
index e0a49f03c1a8706e88eddb9870ed3352d89beb83..a8d287b98d8c82e223efbaddb13e0aa5d1443f7d 100644 (file)
@@ -1,20 +1,45 @@
 /*
+    TODO:
     Create a document explaining the protocol
+    Some way to expire unused callbacks? TTL? expireCallback() function?
 */
 
+/**
+ * Wrapper around creating a new WebsocketRpcCaller
+ * This lets us use the WebsocketRpc object as a function
+ */
 function WebsocketRpc(eio_socket) {
-    var self = this;
+    var caller = new WebsocketRpcCaller(eio_socket);
+    var ret = function WebsocketRpcInstance() {
+        return ret.makeCall.apply(ret, arguments);
+    };
+
+    for(var prop in caller){
+        ret[prop] = caller[prop];
+    }
+
+    ret._mixinEmitter();
+    ret._bindSocketListeners();
+
+    // Keep a reference to the main Rpc object so namespaces can find calling functions
+    ret._rpc = ret;
 
+    return ret;
+}
+
+
+function WebsocketRpcCaller(eio_socket) {
     this._next_id = 0;
-    this._callbacks = {};
+    this._rpc_callbacks = {};
     this._socket = eio_socket;
 
-    this._mixinEmitter();
-    this._bindSocketListeners();
+    this._rpc = this;
+    this._namespace = '';
+    this._namespaces = [];
 }
 
 
-WebsocketRpc.prototype._bindSocketListeners = function() {
+WebsocketRpcCaller.prototype._bindSocketListeners = function() {
     var self = this;
 
     // Proxy the onMessage listener
@@ -26,26 +51,69 @@ WebsocketRpc.prototype._bindSocketListeners = function() {
 
 
 
-WebsocketRpc.prototype.dispose = function() {
+WebsocketRpcCaller.prototype.dispose = function() {
     if (this._onMessageProxy) {
         this._socket.removeListener('message', this._onMessageProxy);
         delete this._onMessageProxy;
     }
 
+    // Clean up any namespaces
+    for (var idx in this._namespaces) {
+        this._namespaces[idx].dispose();
+    }
+
     this.removeAllListeners();
 };
 
 
 
+WebsocketRpcCaller.prototype.namespace = function(namespace_name) {
+    var complete_namespace, namespace;
+
+    if (this._namespace) {
+        complete_namespace = this._namespace + '.' + namespace_name;
+    } else {
+        complete_namespace = namespace_name;
+    }
+
+    namespace = new this._rpc.Namespace(this._rpc, complete_namespace);
+    this._rpc._namespaces.push(namespace);
+
+    return namespace;
+};
+
+
+
+// Find all namespaces that either matches or starts with namespace_name
+WebsocketRpcCaller.prototype._findRelevantNamespaces = function(namespace_name) {
+    var found_namespaces = [];
+
+    for(var idx in this._namespaces) {
+        if (this._namespaces[idx]._namespace === namespace_name) {
+            found_namespaces.push(this._namespaces[idx]);
+        }
+
+        if (this._namespaces[idx]._namespace.indexOf(namespace_name + '.') === 0) {
+            found_namespaces.push(this._namespaces[idx]);
+        }
+    }
+
+    return found_namespaces;
+};
+
+
 
 /**
  * The engine.io socket already has an emitter mixin so steal it from there
  */
-WebsocketRpc.prototype._mixinEmitter = function() {
+WebsocketRpcCaller.prototype._mixinEmitter = function(target_obj) {
     var funcs = ['on', 'once', 'off', 'removeListener', 'removeAllListeners', 'emit', 'listeners', 'hasListeners'];
 
+    target_obj = target_obj || this;
+
     for (var i=0; i<funcs.length; i++) {
-        this[funcs[i]] = this._socket[funcs[i]];
+        if (typeof this._socket[funcs[i]] === 'function')
+            target_obj[funcs[i]] = this._socket[funcs[i]];
     }
 };
 
@@ -53,7 +121,7 @@ WebsocketRpc.prototype._mixinEmitter = function() {
 /**
  * Check if a packet is a valid RPC call
  */
-WebsocketRpc.prototype._isCall = function(packet) {
+WebsocketRpcCaller.prototype._isCall = function(packet) {
     return (typeof packet.method !== 'undefined' &&
             typeof packet.params !== 'undefined');
 };
@@ -62,7 +130,7 @@ WebsocketRpc.prototype._isCall = function(packet) {
 /**
  * Check if a packet is a valid RPC response
  */
-WebsocketRpc.prototype._isResponse = function(packet) {
+WebsocketRpcCaller.prototype._isResponse = function(packet) {
     return (typeof packet.id !== 'undefined' &&
             typeof packet.response !== 'undefined');
 };
@@ -74,15 +142,15 @@ WebsocketRpc.prototype._isResponse = function(packet) {
  * First argument must be the method name to call
  * If the last argument is a function, it is used as a callback
  * All other arguments are passed to the RPC method
- * Eg. Rpc.call('namespace.method_name', 1, 2, 3, callbackFn)
+ * Eg. Rpc.makeCall('namespace.method_name', 1, 2, 3, callbackFn)
  */
-WebsocketRpc.prototype.call = function(method) {
+WebsocketRpcCaller.prototype.makeCall = function(method) {
     var params, callback, packet;
 
-    // Get a normal array of passed in params
+    // Get a normal array of passed in arguments
     params = Array.prototype.slice.call(arguments, 1, arguments.length);
 
-    // If the last param is a function, take it as a callback and strip it out
+    // If the last argument is a function, take it as a callback and strip it out
     if (typeof params[params.length-1] === 'function') {
         callback = params[params.length-1];
         params = params.slice(0, params.length-1);
@@ -97,7 +165,7 @@ WebsocketRpc.prototype.call = function(method) {
         packet.id = this._next_id;
 
         this._next_id++;
-        this._callbacks[packet.id] = callback;
+        this._rpc_callbacks[packet.id] = callback;
     }
 
     this.send(packet);
@@ -107,7 +175,7 @@ WebsocketRpc.prototype.call = function(method) {
 /**
  * Encode the packet into JSON and send it over the websocket
  */
-WebsocketRpc.prototype.send = function(packet) {
+WebsocketRpcCaller.prototype.send = function(packet) {
     if (this._socket)
         this._socket.send(JSON.stringify(packet));
 };
@@ -116,10 +184,12 @@ WebsocketRpc.prototype.send = function(packet) {
 /**
  * Handler for the websocket `message` event
  */
-WebsocketRpc.prototype._onMessage = function(message_raw) {
+WebsocketRpcCaller.prototype._onMessage = function(message_raw) {
     var self = this,
         packet,
-        returnFn;
+        returnFn,
+        callback,
+        namespace, namespaces, idx;
 
     try {
         packet = JSON.parse(message_raw);
@@ -129,32 +199,88 @@ WebsocketRpc.prototype._onMessage = function(message_raw) {
     }
 
     if (this._isResponse(packet)) {
-        this._callbacks[packet.id].apply(this, packet.response);
-        delete this._callbacks[packet.id];
+        // If we have no callback waiting for this response, don't do anything
+        if (typeof this._rpc_callbacks[packet.id] !== 'function')
+            return;
+
+        // Delete the callback before calling it. If any exceptions accur within the callback
+        // we don't have to worry about the delete not happening
+        callback = this._rpc_callbacks[packet.id];
+        delete this._rpc_callbacks[packet.id];
+
+        callback.apply(this, packet.response);
 
     } else if (this._isCall(packet)) {
         // Calls with an ID may be responded to
         if (typeof packet.id !== 'undefined') {
-            returnFn = function returnCallFn() {
-                var value = Array.prototype.slice.call(arguments, 0);
-
-                var ret_packet = {
-                    id: packet.id,
-                    response: value
-                };
-
-                self.send(ret_packet);
-            };
-
+            returnFn = this._createReturnCallFn(packet.id);
         } else {
-            returnFn = function noop(){};
+            returnFn = this._noop;
         }
 
+        this.emit.apply(this, ['all', packet.method, returnFn].concat(packet.params));
         this.emit.apply(this, [packet.method, returnFn].concat(packet.params));
+
+        if (packet.method.indexOf('.') > 0) {
+            namespace = packet.method.substring(0, packet.method.lastIndexOf('.'));
+            namespaces = this._findRelevantNamespaces(namespace);
+            for(idx in namespaces){
+                packet.method = packet.method.replace(namespaces[idx]._namespace + '.', '');
+                namespaces[idx].emit.apply(namespaces[idx], [packet.method, returnFn].concat(packet.params));
+            }
+        }
     }
 };
 
 
+/**
+ * Returns a function used as a callback when responding to a call
+ */
+WebsocketRpcCaller.prototype._createReturnCallFn = function(packet_id) {
+    var self = this;
+
+    return function returnCallFn() {
+        var value = Array.prototype.slice.call(arguments, 0);
+
+        var ret_packet = {
+            id: packet_id,
+            response: value
+        };
+
+        self.send(ret_packet);
+    };
+};
+
+
+
+WebsocketRpcCaller.prototype._noop = function() {};
+
+
+
+WebsocketRpcCaller.prototype.Namespace = function(rpc, namespace) {
+    var ret = function WebsocketRpcNamespaceInstance() {
+        if (typeof arguments[0] === 'undefined') {
+            return;
+        }
+
+        arguments[0] = ret._namespace + '.' + arguments[0];
+        return ret._rpc.apply(ret._rpc, arguments);
+    };
+
+    ret._rpc = rpc;
+    ret._namespace = namespace;
+
+    ret.dispose = function() {
+        ret.removeAllListeners();
+        ret._rpc = null;
+    };
+
+    rpc._mixinEmitter(ret);
+
+    return ret;
+};
+
+
 
 
 // If running a node module, set the exports