UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

413 lines (362 loc) 13.9 kB
#uuid = require('node-uuid') #$q = require('node-promise') #lru = require('lru') $q = Q #debug = process.env['DEBUG'] opts = max: 1000 maxAgeInMilliseconds: 1000 * 60 * 60 * 24 * 4 # 4 days timeout of objects no matter what debug = true uuid = UUID4 class Chillman @underwayCache: new LRUCache() @callbackCache: new LRUCache() @lookup: (key, type, resolveFunc) => #console.log 'Chillman.lookup for '+key+' '+type q = $q.defer() underway = Chillman.underwayCache.get(key+'_'+type) if underway callbacks = Chillman.callbackCache.get(key+'_'+type) or [] callbacks.push q Chillman.callbackCache.set(key+'_'+type, callbacks) else Chillman.underwayCache.set(key+'_'+type, true) Chillman._doLookup(key, type, resolveFunc, q) return q.promise @_doLookup: (key, type, resolveFunc, q) => #console.log 'Chillman._doLookup for '+key+' '+type resolveFunc(key, type).then (result) => # console.log 'Chillman._doLookup got reply from resoolvefunc for '+key+' '+type Chillman.underwayCache.remove(key+'_'+type) callbacks = Chillman.callbackCache.get(key+'_'+type) or [] cbcount = callbacks.length callbacks.forEach (_q) => #console.log 'calling callback '+cbcount+' for '+key+' and '+type _q.resolve(result) if --cbcount == 0 #console.log 'removing callback cache entry for '+key+'_'+type Chillman.callbackCache.remove(key+'_'+type) q.resolve(result) class spinpolymer constructor: (@dbUrl) -> @open = false @subscribers = {} @objsubscribers = [] @popsubscribers = {} @populationsubscribers = {} @objectsSubscribedTo = [] @onsubscribers = {} @outstandingMessages = [] @modelcache = [] @seenMessages = [] @sessionId = null @objects = new LRUCache(opts) @failure = false @failureMessage = '' @savedMessagesInCaseOfRetries = new LRUCache({max:1000, maxAgeInMilliseconds: 5000}) if debug then console.log 'polymer-spincycle dbUrl = ' + @dbUrl @subscribers['OBJECT_UPDATE'] = [(obj) => #console.log 'spinpolymer +++++++++ obj update message router got obj '+obj.id+' of type '+obj.type #console.dir(obj); #console.dir(@objsubscribers) objsubs = @objsubscribers[obj.id] or [] for k,v of objsubs #console.log 'updating subscriber to @objects updates on id '+k if not @objects.get(obj.id) @objects.set(obj.id, obj) else o = @objects.get(obj.id) for prop, val of obj o[prop] = val v obj ] @subscribers['POPULATION_UPDATE'] = [(update) => #console.log 'spinpolymer +++++++++ population update message router got update' #console.dir update obj = update.added or update.removed if obj objsubs = @populationsubscribers[obj.type] or {} for k,v of objsubs if v.cb then v.cb update ] @setup() on: (id,type,cb, onlyupdates)-> if typeof id == 'object' then xyzzy() if not onlyupdates then @get(type,id).then (o)->cb(o) @_registerObjectSubscriber({id:id,type:type,cb:cb}) get: (type, id)-> if typeof id == 'object' then xyzzy() d = $q.defer() o = @objects.get(id) if not o #console.log '******************X get calling server for obj '+id+' of type '+type Chillman.lookup(id, type, @_doGet).then (oo)-> d.resolve(oo) else #console.log 'get found obj for '+id+' in cache of type '+type d.resolve(o) return d.promise _doGet: (k,t)=> #console.log '******************* _doGet calling server for obj '+k+' of type '+t dd = $q.defer() @emitMessage( {target: '_get'+t, type: t, obj: {id: k, type: t}} ).then (_oo)=> #console.log '******************* server replied for obj '+k+' of type '+t @objects.set(_oo.id, _oo) dd.resolve(_oo) return dd.promise save: (o) -> @objects.set(o.id, o) @emitMessage( { target: '_update'+o.type, obj: o } ).then (sres)->console.log('saved obj result: '+sres) failed: (msg)-> console.log 'spinclient message failed!! ' + JSON.toString(msg) if @onFailure then @onFailure msg.info setSessionId: (id) -> if(id) console.log '++++++++++++++++++++++++++++++++++++++ spinclient setting session id to ' + id @sessionId = id dumpOutstanding: ()-> console.log '-------------------------------- ' + @outstandingMessages.length + ' outstanding messages ---------------------------------' @outstandingMessages.forEach (os)-> console.log os.messageId + ' -> ' + os.target + ' - ' + os.d console.log '-----------------------------------------------------------------------------------------' emit: (message) => @_emit(message) _emit:(message)=> #console.log 'emitting message '+message.target #console.dir message @savedMessagesInCaseOfRetries.set(message.messageId, message) @socket.emit('message', JSON.stringify(message)) setup: () => console.log '.....connecting to "' + @dbUrl + "'" @socket = io(@dbUrl) @socket.on 'connect', ()=> @emit({target:'listcommands'}) @socket.on 'message', (reply) => #console.log '***** got message ******' #console.dir reply status = reply.status message = reply.payload info = reply.info isNew = not @hasSeenThisMessage reply.messageId isPopulationUpdate = (reply.info == 'POPULATION_UPDATE') #console.log 'info = '+info if info == 'list of available targets' console.log 'Spincycle server channel is up and awake' @open = true else if message and message.error and message.error == 'ERRCHILLMAN' oldmsg = @savedMessagesInCaseOfRetries[reply.messageId] if oldmsg console.log 'got ERRCHILLMAN from spinycle service, preparing to retry sending message...' setTimeout( ()=> console.log 'resending message '+oldmsg.messageId+' due to target endpoint not open yet' @emit(oldmsg) ,250 ) else if isNew @savedMessagesInCaseOfRetries.remove(reply.messageId) if reply.messageId and reply.messageId isnt 'undefined' then @seenMessages.push(reply.messageId) if @seenMessages.length > 10 then @seenMessages.shift() index = -1 if reply.messageId i = 0 while i < @outstandingMessages.length index = i detail = @outstandingMessages[i] if detail and not detail.delivered and detail.messageId == reply.messageId if reply.status == 'FAILURE' or reply.status == 'NOT_ALLOWED' console.log 'spinclient message FAILURE' console.dir reply @failure = true @failureMessage = reply.info console.log '--- initial message was' console.dir detail @failed(reply) detail.d.reject reply break else #console.log 'delivering message '+message+' reply to '+detail.target+' to '+reply.messageId detail.d.resolve(message) break detail.delivered = true i++ if index > -1 @outstandingMessages.splice index, 1 else subs = @subscribers[info] if subs subs.forEach (listener) -> listener message else if debug then console.log 'no subscribers for message ' + message if debug then console.dir reply else if debug then console.log '-- skipped resent message ' + reply.messageId hasSeenThisMessage: (messageId) => @seenMessages.some (mid) -> messageId == mid registerListener: (detail) => #console.log 'spinclient::registerListener called for ' + detail.message subs = @subscribers[detail.message] or [] subs.push detail.callback @subscribers[detail.message] = subs deRegisterPopulationChangesSubscriber: (detail) => sid = detail.listenerid type = detail.type localsubs = @populationsubscribers[type] if localsubs[sid] console.log 'deregistering local updates for model type ' + type delete localsubs[sid] count = 0 for k,v in localsubs count++ if count == 1 # only remotesid property left @_deRegisterPopulationChangesSubscriber('remotesid', type) _deRegisterPopulationChangesSubscriber: (sid, type) => subs = @popsubscribers[type] or [] if subs and subs[sid] delete subs[sid] @popsubscribers[type] = subs @emitMessage({target: 'deRegisterForPopulationChangesFor', type: type, listenerid: sid}).then (reply)-> console.log 'deregistering server updates for population changes for '+type registerPopulationChangeSubscriber: (detail) => #console.log 'registerPopulationChangeSubscriber called for '+detail.type d = $q.defer() sid = uuid.generate() localsubs = @populationsubscribers[detail.type] if not localsubs localsubs = {} @_registerPopulationSubscriber( { type: detail.type cb: (updatedobj) => lsubs = @populationsubscribers[detail.type] for k,v of lsubs if (v.cb) v.cb updatedobj }).then( (remotesid) => localsubs['remotesid'] = remotesid localsubs[sid] = detail @populationsubscribers[detail.type] = localsubs d.resolve(sid) ,(rejection)=> console.log 'spinpolymer registerPopulationSubscriber rejection: '+rejection console.dir rejection ) else localsubs[sid] = detail return d.promise _registerPopulationSubscriber: (detail) => d = $q.defer() subs = @popsubscribers[detail.type] or {} #console.log '_registerPopulationChangeSubscriber called for '+detail.type @emitMessage({target: 'registerForPopulationChangesFor', type: detail.type}).then( (reply)=> subs[reply] = detail.cb @popsubscribers[detail.type] = subs d.resolve(reply) , (reply)=> @failed(reply) ) return d.promise registerObjectSubscriber: (detail) => d = $q.defer() sid = uuid.generate() localsubs = @objectsSubscribedTo[detail.id] if not localsubs localsubs = [] @_registerObjectSubscriber({ id: detail.id, type: detail.type, cb: (updatedobj) => lsubs = @objectsSubscribedTo[detail.id] for k,v of lsubs if (v.cb) v.cb updatedobj }).then( (remotesid) => localsubs['remotesid'] = remotesid localsubs[sid] = detail @objectsSubscribedTo[detail.id] = localsubs #console.log 'spinclient registered observer for object type '+detail.type+' id '+detail.id d.resolve(sid) ,(rejection)=> console.log 'spinpolymer registerObjectSubscriber rejection: '+rejection console.dir rejection ) else localsubs[sid] = detail return d.promise _registerObjectSubscriber: (detail) => d = $q.defer() subs = @objsubscribers[detail.id] or [] @emitMessage({target: 'registerForUpdatesOn', obj: {id: detail.id, type: detail.type}}).then( (reply)=> subs[reply] = detail.cb @objsubscribers[detail.id] = subs d.resolve(reply) , (reply)=> @failed(reply) ) return d.promise deRegisterObjectsSubscriber: (sid, o) => localsubs = @objectsSubscribedTo[o.id] or [] if localsubs[sid] #console.log 'deregistering local updates for @objects ' + o.id delete localsubs[sid] count = 0 for k,v in localsubs count++ if count == 1 # only remotesid property left @_deRegisterObjectsSubscriber('remotesid', o) _deRegisterObjectsSubscriber: (sid, o) => subs = @objsubscribers[o.id] or [] if subs and subs[sid] delete subs[sid] @objsubscribers[o.id] = subs @emitMessage({target: 'deRegisterForUpdatesOn', id: o.id, type: o.type, listenerid: sid}).then (reply)-> #console.log 'deregistering server updates for @objects ' + o.id emitMessage: (detail) => #if debug then console.log 'emitMessage called' #if debug then console.dir detail d = $q.defer() detail.messageId = uuid.generate() detail.sessionId = detail.sessionId or @sessionId detail.d = d #console.log '------------------> EmitMessage sessionId = '+detail.sessionId @outstandingMessages.push detail #if debug then console.log 'saving outstanding reply to messageId ' + detail.messageId + ' and @sessionId ' + detail.sessionId @emit detail return d.promise # ------------------------------------------------------------------------------------------------------------------ getModelFor: (type) => d = $q.defer() if @modelcache[type] #console.log 'getModelFor found model in cache..' d.resolve(@modelcache[type]) else @emitMessage({target: 'getModelFor', modelname: type}).then((model)=> #console.log 'getModelFor got model from server' #console.dir model @modelcache[type] = model #console.log 'getModelFor resolving.....' d.resolve(model) ,(rejection)=> console.log '+++++++++++++++++ spinpolymer getModelFor rejection: '+rejection console.dir rejection ) return d.promise listTargets: () => d = $q.defer() @emitMessage({target: 'listcommands'}).then((targets)-> d.resolve(targets) ,(rejection)-> console.log 'spinpolymer listTargets rejection: '+rejection console.dir rejection ) return d.promise flattenModel: (model) => rv = {} for k,v of model if angular.isArray(v) rv[k] = v.map (e) -> e.id else rv[k] = v return rv window.SpinClient = spinpolymer