UNPKG

hopjs

Version:

A RESTful declarative API framework, with stub generators for Shell, and Android

482 lines (403 loc) 12.9 kB
/** @module Hop @submodule Event **/ var Hop = require('./api'); var uuid = require('node-uuid'); if(!Array.remove){ Array.remove = function(array,from, to) { var rest = array.slice((to || from) + 1 || array.length); array.length = from < 0 ? array.length + from : from; return array.push.apply(array, rest); }; } Hop.Event={}; Hop.Event.sockets={}; Hop.Event.Channels=[]; Hop.Event.Channel = function(channel){ this.channel=channel; this.regexp = new RegExp(this.channel.replace(/:([^\/]+)/g,"([^\/]+)")); this.params = this.channel.match(/:([^\/]+)/g)||[]; this.params = this.params.map(function(i){ return i.replace(/^:/,""); }); this.subscribers=[]; Hop.Event.Channels.push(this); } Hop.Event.Channel.create=function(channelPath){ for(var i in Hop.Event.Channels){ var channel = Hop.Event.Channels[i]; if(channel.channel == channelPath) return channel; } return new Hop.Event.Channel(channelPath); } Hop.Event.Channel.prototype.toString=function(){ return { channel: this.channel, subscribers: this.subscribers.length }; } Hop.Event.Channel.prototype.matches=function(path){ var match = path.match(this.regexp); if(match){ match.shift(); var input={}; for(var i in this.params){ input[this.params[i]] = match[i]; } return input; } else { return null; } } Hop.Event.Channel.prototype.unsubscribeByKey=function(key){ Hop.log("Subscribers",this.subscribers); for(var i in this.subscribers){ var subscriber = this.subscribers[i]; if(subscriber.key==key){ Hop.log("Attempting to delete:",i); Array.remove(this.subscribers,i); this.unsubscribeByKey(key); } } } Hop.Event.Channel.prototype.unsubscribe=function(id){ for(var i in this.subscribers){ var subscriber = this.subscribers[i]; if(subscriber.id==id){ Array.remove(this.subscribers,i); } } } Hop.Event.Channel.prototype.subscribe=function(url,key,onComplete){ var m = this.matches(url); if(m){ var id = uuid(); this.subscribers.push({ id: id, key: key, url:url, }); return onComplete(null,id); } else { return false; } } Hop.Event.Channel.prototype.notify=function(url,message,source,onComplete){ onComplete=onComplete||function(){}; var m = this.matches(url); var self=this; if(m){ Hop.log("Notifying channel",this.toString()); for(var i in this.subscribers){ var subscriber = this.subscribers[i]; if(subscriber.url==url){ try { var req = { channel: self.channel, url: url, source: source, key: subscriber.key, id: subscriber.id, params: m } Hop.log("Notifying subsscriber",req,message); (function(req,message){ Hop.Event.Channel.notify(req,message,function(err,res){ if(res==0){ Hop.log("Deleting all subscriptions for key: ",req.key); self.unsubscribeByKey(req.key); } else { Hop.log("There are listeners",res); } //FIXME This needs to keep track of the IDs to unsubscribe }); })(req,message); } catch(e){ Hop.warn(e); } } } return true; } else { return false; } } Hop.Event.Channel.Listeners={}; Hop.Event.Channel.removeListener=function(key,index){ Hop.log("Removing",key,index); if(Hop.Event.Channel.Listeners[key]){ var listeners = Hop.Event.Channel.Listeners[key]; Array.remove(listeners.callbacks,index); } } Hop.Event.Channel.notify=function(req,message,onComplete){ var badIDs=[]; Hop.log("LISTENERS",Hop.Event.Channel.Listeners); if(Hop.Event.Channel.Listeners[req.key]){ var listeners = Hop.Event.Channel.Listeners[req.key]; var k = listeners.callbacks.length; var notify=function(){ if(k>0){ k--; Hop.log("Notifying listener",listeners.callbacks[k].toString()); try { listeners.callbacks[k](req,message,function(err,res){ if(err || res===false){ Hop.Event.Channel.removeListener(req.key,k); } notify(); }); } catch(e){ Hop.error(e); Hop.Event.Channel.removeListener(req.key,k); } } else { var numListeners = listeners.callbacks.length; if(numListeners==0){ delete Hop.Event.Channel.Listeners[req.key]; } return onComplete(null,numListeners); } } notify(); } else { return onComplete(null,0); } } Hop.Event.Channel.listen=function(key,onNotify){ if(!Hop.Event.Channel.Listeners[key]){ Hop.Event.Channel.Listeners[key]={ callbacks:[], when: new Date().getTime() }; } Hop.log("Adding call back"); Hop.Event.Channel.Listeners[key].callbacks.push(onNotify); } Hop.Event.Channel.find=function(path){ for(var i in Hop.Event.Channels){ var channel = Hop.Event.Channels[i]; if(channel.matches(path)){ return channel; } } return null; } Hop.EventHelper={}; Hop.EventHelper.getKey=function(input,onComplete,req){ if(req.session){ if(!req.session.eventKey){ req.session.eventKey=uuid(); } return onComplete(null,req.session.eventKey); } else { return onComplete("Session support is required for event support"); } } Hop.EventHelper.listen=function(input,onComplete,req){ Hop.log("CHANNELS",typeof input.channels); if(input.channels){ if(input.channels instanceof String){ try { input.channels = JSON.parse(input.channels); } catch(e){ } } if(!input.channels instanceof Array) return onComplete("Invalid type for channels, array is expected"); } else { input.channels = []; } var subscribe=function(){ if(input.channels.length>0){ var channelPath = input.channels.pop(); var channel = Hop.Event.Channel.find(channelPath); if(channel){ Hop.log(input,"Found channel",channel.toString()); //We need to see if this listener can actually subscribe to the channel channel.subscribe(channelPath,input.key,function(){}); } process.nextTick(subscribe); } else { Hop.log("Listinging on key",input.key); Hop.Event.Channel.listen(input.key,function(req,message,onDone){ Hop.log("Attempting to respond to listener"); try { onComplete(null,{ channel: req.channel, url: req.url, message:message, source: req.source}); } catch(e){ return onDone(e,false); } return onDone(null,false); }); } } subscribe(); }; Hop.EventHelper.subscribe=function(input,onComplete,req){ Hop.log("SUBSCRIBE"); //Find a matching channel var channel = Hop.Event.Channel.find(input.channel); if(channel){ Hop.log(input,"Found channel",channel.toString()); //We need to see if this listener can actually subscribe to the channel channel.subscribe(input.channel,input.key,onComplete); } else return onComplete(null,null); } Hop.EventHelper.listChannels=function(input,onComplete,req){ var channels = []; for(var i in Hop.Event.Channels){ var channel = Hop.Event.Channels[i]; channels.push(channel.toString()); } return onComplete(null,channels); } Hop.EventHelper.unsubscribe=function(input,onComplete,req){ var channel = Hop.Event.Channel.find(input.channel); if(channel){ var res = channel.unsubscribe(input.id); return onComplete(null,res); } else return onComplete(null,null); } Hop.EventHelper.emit=function(req,input,onComplete){ var source = { method: "Event.emit" }; if(req.session && req.session.user && req.session.user.name){ source.user=req.session.user.name; } var message = { channel: input.channel, message: input.message, source: source }; //FIXME should try to force addition of user to event data Hop.Event.Bus.emit(message); return onComplete(null,true); } Hop.defineClass("Hop.Event",Hop.EventHelper,function(api){ api.get("getKey","/event/key"); api.get("listen","/event/listen").demand("key").optional("channels").noCache(); api.post("emit","/event/").demand("channel").demand("message"); api.post("subscribe","/event/channel").demand("channel").demand("key"); api.del("unsubscribe","/event/channel").demand("key").demand("id"); api.get("listChannels","/event/channel"); }); Hop.EventEmitter=function(channel,source){ this.channel=channel; this.source = source } Hop.EventEmitter.prototype.emit=function(message){ //FIXME should try to force addition of user to event data Hop.Event.Bus.emit({ channel: this.channel,message: message, source:this.source},function(){}); }; Hop.EventEmitter.resolvePath=function(path,req,input){ var request=req; return path.replace(/:([^\/]+)/g,function(m,s){ if(s){ try { with(input){ var v = eval(s); if(v==undefined){ throw "Undefined value found in cache path: "+s; } return v.toString(); } } catch(e){ throw "Error in cache path: "+s+" is "+e; } } }); } /** Emit an event prior to calling this method * The channel will have variables provided by the input parameters substituted into it. @param {string} channel The channel to emit the event on @param {function} onEmit The function which determines what is emitted @param {object} onEmit.req The Exbeforess/HTTP request object @param {object} onEmit.input The input object @example api.post("send","/message/send").emitBefore("/user/:to",function(req,input){ //Emit an event to the specified channel this.emit({msg: input.msg, from: input.from}); }).demand("msg").demand("from").demand("to"); @method emitBefore @for Hop.Method @chainable **/ Hop.Method.prototype.emitBefore=function(channel,onEmit){ Hop.Event.Channel.create(channel); var self=this; self.addPreCall(function(req,input,onComplete){ var path = Hop.EventEmitter.resolvePath(channel,req,input); var source = { method: self.getMethod() }; if(req.session && req.session.user && req.session.user.name){ source.user=req.session.user.name; } var emitter = new Hop.EventEmitter(path,source); try { onEmit.apply(emitter,[req,input]); } catch(e){ Hop.error("Error while calling event emitter on method "+self.className+"."+self.name+" "+e.toString()); } onComplete(); },"event"); return this; } /** Emit an event after calling this method * The channel will have variables provided by the input parameters substituted into it. @param {string} channel The channel to emit the event on @param {function} onEmit The function which determines what is emitted @param {object} onEmit.req The Express/HTTP request object @param {object} onEmit.input The input object @param {object} onEmit.err The error resulting from calling the method @param {object} onEmit.result The result of calling the method @example api.post("processFile","/process/").emitAfter("/user/:userId",function(req,input,err,result){ //Emit an event to the specified channel this.emit({err: err, result:result}); }).demand("userId"); @method emitAfter @for Hop.Method @users Hop.Cache @chainable **/ Hop.Method.prototype.emitAfter=function(channel,onEmit){ new Hop.Event.Channel(channel); var self=this; self.addPostCall(function(req,input,err,result,onComplete){ var path = Hop.EventEmitter.resolvePath(channel,req,input); var source = { method: self.getMethod() }; if(req.session && req.session.user && req.session.user.name){ source.user=req.session.user.name; } var emitter = new Hop.EventEmitter(path,source); try { onEmit.apply(emitter,[req,input,err,result]); } catch(e){ Hop.error("Error while calling event emitter on method "+self.className+"."+self.name+" "+e.toString()); } onComplete(); },"event"); return this; } /** Alias to emitAFter @method emit @for Hop.Method @chainable **/ Hop.Method.prototype.emit=Hop.Method.prototype.emitAfter; Hop.Event.Bus={}; Hop.addAfterTemplate("JavaScript","event/preJSHop.comb"); Hop.Event.Bus.localEmit=function(message,onComplete){ var channel = Hop.Event.Channel.find(message.channel); if(channel){ Hop.log("localEmit",message); channel.notify(message.channel,message.message,message.source); } } Hop.Event.Bus.emit=function(message,onComplete){ } /* Hop.defineChannel("/user/:userID/messages",function(c){ c.onConnect(function(req,input,onAllowConnect){ if(req.session.user.name==input.userID || req.session.user.userID=input.userID){ return onAllowConnect(true); } else return onAllowConnect(false); }); }); */ module.exports=Hop;