declare var module ; declare var require ; declare var Object ; import { SubscriptionInterface } from '../types/subscription' ; import { DriverInterface } from '../types/driver' ; import { OptionsInterface } from '../types/options' ; import { EventsInterface } from '../types/events' ; import { FireLoopData } from '../types/fireloop-data' ; import { RealTimeLog } from '../logger' ; import * as async from 'async' ; module author Jonathan Casarrubias <t:@johncasarrubias, gh:github.com/mean-expert-official>
 *  license description export class FireLoop { property static UNAUTHORIZED : string = '401 Unauthorized Event' ; property static driver : DriverInterface ; property static options : OptionsInterface ; property static events : EventsInterface = { readings : [ 'value' , 'change' , 'child_added' , 'child_updated' , 'child_removed' , 'stats' ] , writings : [ 'create' , 'upsert' , 'remove' ] , } ; property static contexts : { [ id ] : { [ id ] : Object ; } ; } = { } ; method param driver param options description constructor ( driver , options ) { RealTimeLog . log ( `FireLoop server enabled using ${ options . driver . name } driver.` ) ; FireLoop . driver = driver ; FireLoop . options = options ; FireLoop . setup ( ) ; } method description static setup ( ) { FireLoop . setupHooks ( ) ; Object . keys ( FireLoop . options . app . models ) . forEach ( ( modelName ) => { FireLoop . events . readings . forEach ( ( event ) => { FireLoop . setupServerBroadcast ( { modelName } , event ) ; if ( ! FireLoop . options . app . models [ modelName ] . sharedClass . ctor . relations ) return ; Object . keys ( FireLoop . options . app . models [ modelName ] . sharedClass . ctor . relations ) . forEach ( ( scope ) => { FireLoop . setupServerBroadcast ( { modelName : `${ modelName }.${ scope }` } , event ) ; } ) ; } ) ; } ) ; FireLoop . driver . onConnection ( ( socket ) => { socket . connContextId = FireLoop . buildId ( ) ; FireLoop . contexts [ socket . connContextId ] = { } ; Object . keys ( FireLoop . options . app . models ) . forEach ( ( modelName ) => FireLoop . getReference ( modelName , null , ( Model ) => { socket . on ( `Subscribe.${ modelName }` , ( subscription ) => { FireLoop . contexts [ socket . connContextId ] [ subscription . id ] = { id : subscription . id , modelName , Model , socket , subscription } ; FireLoop . setupRemoteMethods ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] ) ; FireLoop . events . writings . forEach ( ( event ) => { FireLoop . setupModelWritings ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] , event ) ; FireLoop . setupScopeWritings ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] , event ) ; } ) ; FireLoop . events . readings . forEach ( ( event ) => { FireLoop . setupModelReadings ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] , event ) ; FireLoop . setupScopeReadings ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] , event ) ; } ) ; FireLoop . setupDisposeReference ( FireLoop . contexts [ socket . connContextId ] [ subscription . id ] ) ; } ) ; } ) ) ; socket . on ( 'disconnect' , ( ) => { RealTimeLog . log ( `FireLoop is releasing context tree with id ${ socket . connContextId } from memory` ) ; delete FireLoop . contexts [ socket . connContextId ] ; } ) ; } ) ; } method description static setupDisposeReference ( ctx ) { ctx . socket . on ( `${ ctx . modelName }.dispose.${ ctx . subscription . id }` , ( input ) => { RealTimeLog . log ( `FireLoop is releasing model subscription tree with id ${ ctx . subscription . id } from memory` ) ; ctx . socket . removeAllListeners ( `${ ctx . modelName }.remote.${ ctx . subscription . id }` ) ; FireLoop . disposeModelWritings ( ctx ) ; FireLoop . disposeScopeWritings ( ctx ) ; FireLoop . disposeModelReadings ( ctx ) ; FireLoop . disposeScopeReadings ( ctx ) ; process . nextTick ( ( ) => { if ( FireLoop . contexts [ ctx . socket . connContextId ] && FireLoop . contexts [ ctx . socket . connContextId ] [ ctx . id ] ) delete FireLoop . contexts [ ctx . socket . connContextId ] [ ctx . id ] ; } ) ; } ) ; } method description static getUserConnection ( userId ) { FireLoop . driver . getUserConnection ( userId ) ; } method description static setupHooks ( ) { FireLoop . driver . internal . on ( 'create-hook' , ( event ) => { FireLoop . publish ( { modelName : event . modelName , err : null , input : null , data : event . data , created : event . created } ) ; } ) ; FireLoop . driver . internal . on ( 'delete-hook' , ( event ) => { FireLoop . publish ( { modelName : event . modelName , err : null , input : null , removed : event . data } ) ; } ) ; } method description static setupRemoteMethods ( ctx ) { ctx . socket . on ( `${ ctx . modelName }.remote.${ ctx . subscription . id }` , ( input ) => FireLoop . remote ( ctx , input ) ) ; } method description static setupModelWritings ( ctx , event ) { ctx . socket . on ( `${ ctx . modelName }.${ event }.${ ctx . subscription . id }` , ( input ) => ( < any > FireLoop ) [ event ] ( ctx , input ) ) ; } method description static disposeModelWritings ( ctx ) { FireLoop . events . writings . forEach ( ( event ) => { ctx . socket . removeAllListeners ( `${ ctx . modelName }.${ event }.${ ctx . subscription . id }` ) ; } ) ; } method description static setupModelReadings ( ctx , event ) { ctx . socket . on ( `${ ctx . modelName }.${ event }.pull.request.${ ctx . subscription . id }` , ( request ) => { let _request = Object . assign ( { } , request ) ; RealTimeLog . log ( `FireLoop model pull request received: ${ JSON . stringify ( _request . filter ) }` ) ; let emit = ( err , data ) => { if ( err ) RealTimeLog . log ( `FireLoop server error: ${ JSON . stringify ( err ) }` ) ; ctx . socket . emit ( `${ ctx . modelName }.${ event }.pull.requested.${ ctx . subscription . id }` , err ? { error : err } : data ) ; } ; let remoteEvent ; if ( event . match ( 'stats' ) ) { remoteEvent = 'stats' ; } else { remoteEvent = 'find' ; } FireLoop . checkAccess ( ctx , ctx . Model , remoteEvent , ctx . input , ( err , hasAccess ) => { if ( err ) { emit ( err ) ; } else if ( ! hasAccess ) { emit ( FireLoop . UNAUTHORIZED ) ; } else { switch ( remoteEvent ) { case 'find' : ctx . Model . find ( _request . filter , { accessToken : ctx . socket . token } , emit ) ; break ; case 'stats' : ctx . Model . stats ( _request . filter . range || 'monthly' , _request . filter . custom , _request . filter . where || { } , _request . filter . groupBy , emit ) ; break ; } } } ) ; } ) ; FireLoop . setupClientBroadcast ( ctx , event ) ; } method description static disposeModelReadings ( ctx ) { FireLoop . events . readings . forEach ( ( event ) => { ctx . socket . removeAllListeners ( `${ ctx . modelName }.${ event }.pull.request.${ ctx . subscription . id }` ) ; } ) ; } method description static setupScopeWritings ( ctx , event ) { if ( ! FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) return ; Object . keys ( FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) . forEach ( ( scope ) => { let relation = FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations [ scope ] ; let _ctx = Object . assign ( { } , { modelName : `${ ctx . modelName }.${ scope }` } , ctx ) ; _ctx . modelName = `${ ctx . modelName }.${ scope }` ; RealTimeLog . log ( `FireLoop setting write relation: ${ ctx . modelName }.${ scope }.${ event }.${ ctx . subscription . id }` ) ; ctx . socket . on ( `${ _ctx . modelName }.${ event }.${ ctx . subscription . id }` , ( input ) => { _ctx . input = input ; RealTimeLog . log ( `FireLoop relation operation: ${ _ctx . modelName }.${ event }.${ ctx . subscription . id }: ${ JSON . stringify ( input ) }` ) ; ( < any > FireLoop ) [ event ] ( _ctx , input ) ; } ) ; } ) ; } method description static disposeScopeWritings ( ctx ) { if ( ! FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) return ; FireLoop . events . writings . forEach ( ( event ) => { Object . keys ( FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) . forEach ( ( scope ) => { ctx . socket . removeAllListeners ( `${ ctx . modelName }.${ scope }.${ event }.${ ctx . subscription . id }` ) ; } ) ; } ) ; } method description static setupScopeReadings ( ctx , event ) { if ( ! FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) return ; Object . keys ( FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) . forEach ( ( scope ) => { let relation = FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations [ scope ] ; let _ctx = Object . assign ( { } , { modelName : `${ ctx . modelName }.${ scope }` } , ctx ) ; _ctx . modelName = `${ ctx . modelName }.${ scope }` ; RealTimeLog . log ( `FireLoop setting read relation: ${ _ctx . modelName }.${ event }.pull.request.${ ctx . subscription . id }` ) ; ctx . socket . on ( `${ _ctx . modelName }.${ event }.pull.request.${ ctx . subscription . id }` , ( request ) => { _ctx . input = request ; FireLoop . setupClientBroadcast ( _ctx , event ) ; let _filter = Object . assign ( { } , request . filter ) ; RealTimeLog . log ( `FireLoop scope pull request received: ${ JSON . stringify ( _filter ) }` ) ; let emit = ( err , data ) => { if ( err ) RealTimeLog . log ( `FireLoop server error: ${ JSON . stringify ( err ) }` ) ; ctx . socket . emit ( `${ _ctx . modelName }.${ event }.pull.requested.${ ctx . subscription . id }` , err ? { error : err } : data ) ; } ; switch ( event ) { case 'value' : case 'change' : FireLoop . getReference ( _ctx . modelName , request , ( ref ) => { if ( ! ref ) { const error = { error : `${ ctx . modelName } Model reference was not found.` } ; RealTimeLog . log ( error ) ; emit ( error ) ; } else { ref ( _filter , emit ) ; } } ) ; break ; case 'stats' : RealTimeLog . log ( 'Stats are currently only for root models' ) ; break ; } } ) ; } ) ; } method description static disposeScopeReadings ( ctx ) { if ( ! FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) return ; FireLoop . events . readings . forEach ( ( event ) => { Object . keys ( FireLoop . options . app . models [ ctx . modelName ] . sharedClass . ctor . relations ) . forEach ( ( scope ) => { ctx . socket . removeAllListeners ( `${ ctx . modelName }.${ scope }.${ event }.pull.request.${ ctx . subscription . id }` ) ; } ) ; } ) ; } method description static getReference ( modelName , input , next ) { let ref ; if ( modelName . match ( /\./g ) ) { if ( ! input || ! input . parent ) return next ( null ) ; let segments = modelName . split ( '.' ) ; let parent = FireLoop . options . app . models [ segments [ 0 ] ] || null ; if ( ! parent ) return next ( null ) ; let idName = parent . getIdName ( ) ; let filter = { where : { } } ; filter . where [ idName ] = input . parent [ idName ] ; return parent . findOne ( filter , ( err , instance ) => { ref = instance ? instance [ segments [ 1 ] ] || null : null ; next ( ref ) ; } ) ; } else { ref = FireLoop . options . app . models [ modelName ] || null ; next ( ref ) ; } } method description static remote ( ctx , input ) { RealTimeLog . log ( `FireLoop starting remote: ${ ctx . modelName }: ${ JSON . stringify ( input ) }` ) ; async . waterfall ( [ ( next ) => { if ( ctx . modelName . match ( /\./g ) ) { FireLoop . getReference ( ctx . modelName , input , ( ref ) => { if ( ! ref ) { next ( { error : `${ ctx . modelName } Model reference was not found.` } ) ; } else { next ( null , ref ) ; } } ) ; } else { next ( null , ctx . Model ) ; } } , ( ref , next ) => FireLoop . checkAccess ( ctx , ref , input . data . method , input , next ) , ( ref , next ) => { let method = ref [ input . data . method ] ; if ( ! method ) { return next ( `ERROR: Remote method ${ input . data . method } was not found` ) ; } if ( Array . isArray ( input . data . params ) && input . data . params . length > 0 ) { input . data . params . push ( next ) ; method . apply ( method , input . data . params ) ; } else { method ( next ) ; } } ] , ( err , data ) => FireLoop . publish ( Object . assign ( { err , input , data , created : true } , ctx ) ) ) ; } method description static create ( ctx , input ) { RealTimeLog . log ( `FireLoop starting create: ${ ctx . modelName }: ${ JSON . stringify ( input ) }` ) ; let isScoped = ctx . modelName . match ( /\./g ) ; async . waterfall ( [ ( next ) => { if ( isScoped ) { FireLoop . getReference ( ctx . modelName , input , ( ref ) => { if ( ! ref ) { next ( { error : `${ ctx . modelName } Model reference was not found.` } ) ; } else { next ( null , ref ) ; } } ) ; } else { next ( null , ctx . Model ) ; } } , ( ref , next ) => FireLoop . checkAccess ( ctx , ref , 'create' , input , next ) , ( ref , next ) => ref . create ( input . data , { accessToken : ctx . socket . token } , next ) ] , ( err , data ) => { const resultContext = Object . assign ( { err , input , data , created : true } , ctx ) ; FireLoop . response ( resultContext ) ; if ( isScoped ) { FireLoop . publish ( resultContext ) ; } } ) ; } method description static upsert ( ctx , input ) { let created ; RealTimeLog . log ( `FireLoop starting upsert: ${ ctx . modelName }: ${ JSON . stringify ( input ) }` ) ; let isScoped = ctx . modelName . match ( /\./g ) ; async . waterfall ( [ ( next ) => { if ( isScoped ) { FireLoop . getReference ( ctx . modelName , input , ( ref ) => { if ( ! ref ) { next ( { error : `${ ctx . modelName } Model reference was not found.` } ) ; } else { next ( null , ref ) ; } } ) ; } else { next ( null , ctx . Model ) ; } } , ( ref , next ) => FireLoop . checkAccess ( ctx , ref , 'create' , input , next ) , ( ref , next ) => { if ( input . data . id ) { ref . findById ( input . data . id , ( err , inst ) => next ( err , ref , inst ) ) ; } else { next ( null , ref , null ) ; } } , ( ref , inst , next ) => { if ( inst ) { created = false ; Object . keys ( input . data ) . forEach ( ( key ) => { if ( typeof inst [ key ] !== 'function' ) { inst [ key ] = input . data [ key ] ; } } ) ; inst . save ( { accessToken : ctx . socket . token } , next ) ; } else { created = true ; ref . create ( input . data , { accessToken : ctx . socket . token } , next ) ; } } ] , ( err , data ) => { const resultContext = Object . assign ( { err , input , data , created : true } , ctx ) ; FireLoop . response ( resultContext ) ; if ( isScoped ) { FireLoop . publish ( resultContext ) ; } } ) ; } method description static remove ( ctx , input ) { RealTimeLog . log ( `FireLoop starting remove: ${ ctx . modelName }: ${ JSON . stringify ( input ) }` ) ; let isScoped = ctx . modelName . match ( /\./g ) ; async . waterfall ( [ ( next ) => { if ( ctx . modelName . match ( /\./g ) ) { FireLoop . getReference ( ctx . modelName , input , ( ref ) => { if ( ! ref ) { next ( { error : `${ ctx . modelName } Model reference was not found.` } ) ; } else { next ( null , ref ) ; } } ) ; } else { next ( null , ctx . Model ) ; } } , ( ref , next ) => FireLoop . checkAccess ( ctx , ref , ref . destroy ? 'destroy' : 'removeById' , input , next ) , ( ref , next ) => ref . destroy ? ref . destroy ( input . data . id , { accessToken : ctx . socket . token } , next ) : ref . removeById ( input . data . id , { accessToken : ctx . socket . token } , next ) ] , ( err ) => { const resultContext = Object . assign ( { err , input , removed : input . data } , ctx ) ; FireLoop . response ( resultContext ) ; if ( isScoped ) { FireLoop . publish ( resultContext ) ; } } ) ; } method description static response ( ctx ) { if ( ctx . subscription && ctx . subscription . id ) { ctx . socket . emit ( `${ ctx . modelName }.value.result.${ ctx . subscription . id }` , ctx . err ? { error : ctx . err } : ctx . data || ctx . removed ) ; } } method description static publish ( ctx ) { if ( ctx . err ) { return ; } if ( ctx . input && ctx . input . data && ctx . input . data . method ) { if ( ctx . input && ctx . input . data && ctx . input . data . method && ctx . input . data . broadcast ) { FireLoop . broadcast ( ctx , 'remote' ) ; } return ; } if ( ctx . data ) { FireLoop . broadcast ( ctx , 'value' ) ; if ( ctx . created ) { FireLoop . broadcast ( ctx , 'child_added' ) ; } else { FireLoop . broadcast ( ctx , 'child_changed' ) ; } } else if ( ctx . removed ) { FireLoop . broadcast ( ctx , 'child_removed' ) ; } FireLoop . broadcast ( ctx , 'change' ) ; FireLoop . broadcast ( ctx , 'stats' ) ; } method description static broadcast ( ctx , event ) { RealTimeLog . log ( `FireLoop ${ event } broadcasting` ) ; FireLoop . driver . server . to ( 'flint' ) . emit ( `${ ctx . modelName }.${ event }.broadcast` , ctx . data || ctx . removed ) ; ctx = null ; } method description static setupServerBroadcast ( ctx , event ) { FireLoop . driver . internal . on ( `${ ctx . modelName }.${ event }.broadcast` , ( payload ) => { Object . keys ( FireLoop . contexts ) . forEach ( ( connContextId ) => { Object . keys ( FireLoop . contexts [ connContextId ] ) . forEach ( ( contextId ) => { let context = FireLoop . contexts [ connContextId ] [ contextId ] ; let scopeModelName = context . modelName . match ( /\./g ) ? context . modelName . split ( '.' ) . shift ( ) : context . modelName ; if ( context . modelName === ctx . modelName || ctx . modelName . match ( new RegExp ( `\\b${ scopeModelName }\.${ context . subscription . relationship }\\b` , 'g' ) ) ) { if ( event . match ( /(child_changed|child_removed|remote)/ ) ) { context . socket . emit ( `${ ctx . modelName }.${ event }.broadcast.${ context . subscription . id }` , payload ) ; } if ( event !== 'remote' ) { context . socket . emit ( `${ ctx . modelName }.${ event }.broadcast.announce.${ context . subscription . id }` , 1 ) ; } } } ) ; } ) ; } ) ; } method description static setupClientBroadcast ( ctx , event ) { if ( ! event . match ( /(value|change|stats|child_added)/ ) ) { return ; } RealTimeLog . log ( `FireLoop setting up: ${ ctx . modelName }.${ event }.broadcast.request.${ ctx . subscription . id }` ) ; ctx . socket . on ( `${ ctx . modelName }.${ event }.broadcast.request.${ ctx . subscription . id }` , ( request ) => { let _request = Object . assign ( { } , request ) ; RealTimeLog . log ( `FireLoop ${ event } broadcast request received: ${ JSON . stringify ( _request . filter ) }` ) ; let broadcast = ( err , data ) => { if ( err ) RealTimeLog . log ( `FireLoop server error: ${ JSON . stringify ( err ) }` ) ; if ( event . match ( /(value|change|stats)/ ) ) { RealTimeLog . log ( `FireLoop ${ event } broadcasting: ${ JSON . stringify ( data ) }` ) ; ctx . socket . emit ( `${ ctx . modelName }.${ event }.broadcast.${ ctx . subscription . id }` , err ? { error : err } : data ) ; } else { data . forEach ( ( d ) => ctx . socket . emit ( `${ ctx . modelName }.${ event }.broadcast.${ ctx . subscription . id }` , err ? { error : err } : d ) ) ; } } ; let remoteEvent ; if ( event . match ( 'stats' ) ) { remoteEvent = 'stats' ; } else { remoteEvent = 'find' ; } async . waterfall ( [ ( next ) => { if ( ctx . modelName . match ( /\./g ) ) { FireLoop . getReference ( ctx . modelName , ctx . input , ( ref ) => { if ( ! ref ) { next ( { error : `${ ctx . modelName } Model reference was not found.` } ) ; } else { next ( null , ref ) ; } } ) ; } else { next ( null , ctx . Model ) ; } } , ( ref , next ) => FireLoop . checkAccess ( ctx , ref , remoteEvent , ctx . input , ( err , hasAccess ) => next ( err , hasAccess , ref ) ) , ( hasAccess , ref , next ) => { if ( ! hasAccess ) { broadcast ( FireLoop . UNAUTHORIZED ) ; next ( ) ; } else { switch ( remoteEvent ) { case 'find' : if ( ctx . modelName . match ( /\./g ) ) { ref ( _request . filter , broadcast ) ; } else { ref . find ( _request . filter , { accessToken : ctx . socket . token } , broadcast ) ; } break ; case 'stats' : ref . stats ( _request . filter . range || 'monthly' , _request . filter . custom , _request . filter . where || { } , _request . filter . groupBy , broadcast ) ; break ; } } } ] ) ; } ) ; } method param ctx param ref param event param input param next description static checkAccess ( ctx , ref , event , input , next ) { if ( ref . checkAccess ) { ref . checkAccess ( ctx . socket . token , input && input . parent ? input . parent . id : null , { name : event , aliases : [ ] } , { } , function ( err , access ) { if ( access ) { next ( null , ref ) ; } else { next ( FireLoop . UNAUTHORIZED , ref ) ; } } ) ; } else if ( ctx . subscription && FireLoop . options . app . models [ ctx . subscription . scope ] . checkAccess ) { const relationName = ctx . modelName . split ( '.' ) . pop ( ) ; let methodName = '' ; switch ( event ) { case 'create' : case 'upsert' : methodName = `__create__${ relationName }` ; break ; case 'remove' : methodName = `__delete__${ relationName }` ; break ; default : methodName = `__get__${ relationName }` ; } ctx . Model . checkAccess ( ctx . socket . token , input && input . parent ? input . parent . id : null , { name : methodName , aliases : [ ] } , { } , function ( err , access ) { if ( access ) { next ( null , ref ) ; } else { next ( FireLoop . UNAUTHORIZED , ref ) ; } } ) ; } else { RealTimeLog . log ( `Reference not found for: ${ ctx . subscription . scope }` ) ; next ( FireLoop . UNAUTHORIZED , ref ) ; } } method description static buildId ( ) { let id = Date . now ( ) + Math . floor ( Math . random ( ) * 100800 ) * Math . floor ( Math . random ( ) * 100700 ) * Math . floor ( Math . random ( ) * 198500 ) ; if ( FireLoop . contexts [ id ] ) { return FireLoop . buildId ( ) ; } else { return id ; } } }