vapid
Version:
Vapid, a vacuous Redis implementation for connection tests, with a fully functional PubSub system for multiple clients.
416 lines (401 loc) • 17.5 kB
JavaScript
/*
* PUBSUB mix-ins.
*/
exports.commands = function () {
var keys = Object.keys
, minimatch = require( 'minimatch' )
, wmsg = function ( cmd ) {
return '-ERR wrong number of arguments for \'' + cmd + '\' command\r\n';
}
, umsg = function ( cmd ) {
return '-ERR Unknown PUBSUB subcommand or wrong number of arguments for \'' + cmd + '\'\r\n';
}
, crlf = '\r\n'
, byteLength = Buffer.byteLength
;
return {
pubsub : {
channels : function ( command, args, sock ) {
var me = this
, channels = me.pubsub.channels
, alen = args.length
, cmd = command[ 1 ]
, chan = null
, a = 0
, cnames = null
, cname = null
, clen = 0
// placeholder for multibulk
, reply = [ '*0\r\n' ]
, r = 0
, regexp = null
;
if ( alen > 1 ) return sock.write( umsg( cmd ) );
cnames = keys( channels );
clen = cnames.length;
if ( ! clen ) return sock.write( reply[ 0 ] );
// get all channels
if ( alen === 0 ) {
for ( ; a < clen; ++a ) {
cname = cnames[ a ];
chan = channels[ cname ];
if ( chan && chan.length ) reply.push( '$' + byteLength( cname ) + crlf + cname + crlf ) & ++r;
}
reply[ 0 ] = '*' + r + crlf;
return sock.write( reply.join( '' ) );
}
// match channels with pattern through minimatch
try {
regexp = minimatch.makeRe( args[ 0 ] );
} catch ( e ) {
regexp = new RegExp( args[ 0 ] );
}
for ( ; a < clen; ++a ) {
cname = cnames[ a ];
if ( ! regexp.test( cname ) ) continue;
chan = channels[ cname ];
if ( chan && chan.length ) reply.push( '$' + byteLength( cname ) + crlf + cname + crlf ) & ++r;
}
reply[ 0 ] = '*' + r + crlf;
return sock.write( reply.join( '' ) );
}
, numsub : function ( command, args, sock ) {
var me = this
, channels = me.pubsub.channels
, alen = args.length
, chan = null
, cname = null
, clen = 0
, a = 0
// placeholder for multibulk
, reply = [ '*0\r\n' ]
, r = 0
;
if ( ! alen ) return sock.write( reply[ 0 ] );
for ( ; a < alen; ++a ) {
cname = args[ a ];
chan = channels[ cname ];
// push also non existent channels name (0 subscribers). like Redis does.
clen = chan ? chan.length : 0;
reply.push( '$' + byteLength( cname ) + crlf + cname + crlf + ':' + clen + crlf );
r += 2;
}
reply[ 0 ] = '*' + r + crlf;
sock.write( reply.join( '' ) );
}
, numpat : function ( command, args, sock ) {
var me = this
, cmd = command[ 0 ]
, pubsub = me.pubsub
, patterns = pubsub.patterns
, patt = null
, p = 0
, result = 0
;
if ( args.length ) return sock.write( umsg( cmd ) );
for ( p in patterns ) {
patt = patterns[ p ];
result += patt ? patt.length : 0;
}
sock.write( ':' + result + crlf );
}
}
, publish : function ( command, args, sock ) {
var me = this
, clients = me.clients
, pubsub = me.pubsub
, channels = pubsub.channels
, patterns = pubsub.patterns
, cmd = command[ 0 ]
, name = String( args[ 0 ] )
, msg = String( args[ 1 ] )
, chan = null
, patt = null
, p = null
, c = 0
, clen = 0
, plen = 0
, ecmsg = '*3\r\n$7\r\nmessage\r\n'
, epmsg = '*4\r\n$8\r\npmessage\r\n'
, reply = null
, r = 0
;
if ( ( name === '' ) || ( msg === '' ) ) return sock.write( wmsg( cmd ) );
if ( ( chan = channels[ name ] ) && ( clen = chan.length ) ) {
reply = ecmsg + '$' + byteLength( name ) + crlf + name + crlf;
reply += '$' + byteLength( msg ) + crlf + msg + crlf;
for ( ; c < clen; ++c ) clients[ chan[ c ] ].write( reply ) & ++r;
}
for ( p in patterns ) {
patt = patterns[ p ];
if ( ! patt.regexp.test( name ) ) continue;
reply = epmsg + '$' + byteLength( p ) + crlf + p + crlf;
reply += '$' + byteLength( name ) + crlf + name + crlf;
reply += '$' + byteLength( msg ) + crlf + msg + crlf;
plen = patt.length;
c = 0;
for ( ; c < plen; ++c ) clients[ patt[ c ] ].write( reply ) & ++r;
}
sock.write( ':' + r + '\r\n' );
}
, subscribe : function ( command, args, sock ) {
var me = this
, channels = me.pubsub.channels
, schannels = null
, alen = args.length
, a = 0
, chan = null
, cname = null
, reply = null
// set lower case for command in message reply
, cmd = command[ 0 ].toLowerCase()
, csize = byteLength( cmd )
, ecmd = '*3\r\n$' + csize + crlf + cmd + crlf
;
if ( ! alen ) return sock.write( wmsg( command[ 0 ] ) );
// set pubsub property for socket if it doesn't exist
if ( ! sock.pubsub ) sock.pubsub = { channels : [], patterns : [], active : 0 };
schannels = sock.pubsub.channels;
// scan arguments
for ( ; a < alen; ++a ) {
cname = String( args[ a ] );
chan = channels[ cname ];
// NOTE: avoid '' ?
// if ( doString( cname ) !== ostr ) continue;
// create or update pubsub channels with a new subscriber
if ( chan && chan.length ) {
// check if subscriber already exists
if ( ! ~ chan.indexOf( sock.name ) ) {
chan.push( sock.name );
schannels.push( cname );
++sock.pubsub.active;
}
} else {
// create new channel and add subscriber
channels[ cname ] = [ sock.name ];
// add also to socket.pubsub property
schannels.push( cname );
++sock.pubsub.active;
}
reply = ecmd;
reply += '$' + byteLength( cname ) + crlf + cname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
}
, psubscribe : function ( command, args, sock ) {
var me = this
, patterns = me.pubsub.patterns
, spatterns = null
, alen = args.length
, a = 0
, patt = null
, pname = null
, reply = null
// set lower case for command in message reply
, cmd = command[ 0 ].toLowerCase()
, csize = byteLength( cmd )
, ecmd = '*3\r\n$' + csize + crlf + cmd + crlf
;
if ( ! alen ) return sock.write( wmsg( command[ 0 ] ) );
// set pubsub property for socket if it doesn't exist
if ( ! sock.pubsub ) sock.pubsub = { channels : [], patterns : [], active : 0 };
spatterns = sock.pubsub.patterns;
// scan arguments
for ( ; a < alen; ++a ) {
pname = String( args[ a ] );
patt = patterns[ pname ];
// NOTE: avoid '' ?
// if ( doString( pname ) !== ostr ) continue;
// create or update pubsub channels with a new subscriber
if ( patt && patt.length ) {
// check if subscriber already exists
if ( ! ~ patt.indexOf( sock.name ) ) {
patt.push( sock.name );
spatterns.push( pname );
++sock.pubsub.active;
}
} else {
// create new channel and add subscriber
patterns[ pname ] = [ sock.name ];
// set Regular Expression for this pattern
try {
patterns[ pname ].regexp = minimatch.makeRe( pname );
} catch ( e ) {
patterns[ pname ].regexp = new RegExp( pname );
}
// add also to socket.pubsub property
spatterns.push( pname );
++sock.pubsub.active;
}
reply = ecmd;
reply += '$' + byteLength( pname ) + crlf + pname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
}
/*
* NOTE: (P)UNSUBSCRIBE returns always n messages when ( n = args.length ) > 0
*/
, unsubscribe : function ( command, args, sock ) {
var me = this
, channels = me.pubsub.channels
, alen = args.length
, a = 0
, chan = null
, cname = null
, offset = -1
, soffset = -1
, schannels = null
, reply = null
// set lower case for command in message reply
, cmd = command[ 0 ].toLowerCase()
, spubsub = sock.pubsub
, csize = byteLength( cmd )
, ecmd = '*3\r\n$' + csize + crlf + cmd + crlf
;
if ( ! ( spubsub && spubsub.channels.length ) ) {
// client is not subscribed to any channel
if ( ! alen ) return sock.write( ecmd + '$-1\r\n:0\r\n' );
// send alen messages
for ( ; a < alen; ++a ) {
cname = String( args[ a ] );
reply = ecmd;
reply += '$' + byteLength( cname ) + crlf + cname + crlf;
reply += ':' + ( spubsub ? spubsub.active : 0 ) + crlf;
sock.write( reply );
}
return;
}
schannels = spubsub.channels;
if ( ! alen ) {
// no arguments, unsubscribe client from all channels
for ( ; a < schannels.length; ++a ) {
// NOTE: schannels length could change on every iteration
cname = schannels[ a ];
chan = channels[ cname ];
// check if channel exists
if ( ! chan ) continue;
// remove from pubsub
offset = chan.indexOf( sock.name );
if ( ~ offset ) {
chan.splice( offset, 1 );
// delete array property if it is empty
if ( ! chan.length ) delete channels[ cname ];
soffset = schannels.indexOf( cname );
if ( ~ soffset ) schannels.splice( soffset, 1 ) & --a;
--sock.pubsub.active;
}
reply = ecmd;
reply += '$' + byteLength( cname ) + crlf + cname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
return;
}
// scan arguments
for ( ; a < alen; ++a ) {
cname = String( args[ a ] );
chan = channels[ cname ];
// NOTE: avoid '' ?
// if ( doString( chan ) !== ostr ) continue;
// check if chan exists
if ( chan && chan.length ) {
offset = chan.indexOf( sock.name );
if ( ~ offset ) {
chan.splice( offset, 1 );
// delete array property if it is empty
if ( ! chan.length ) delete channels[ cname ];
soffset = schannels.indexOf( cname );
if ( ~ soffset ) schannels.splice( soffset, 1 ) & --a;
--sock.pubsub.active;
}
}
reply = ecmd;
reply += '$' + byteLength( cname ) + crlf + cname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
}
, punsubscribe : function ( command, args, sock ) {
var me = this
, patterns = me.pubsub.patterns
, alen = args.length
, a = 0
, patt = null
, pname = null
, offset = -1
, soffset = -1
, spatterns = null
, reply = null
// set lower case for command in message reply
, cmd = command[ 0 ].toLowerCase()
, spubsub = sock.pubsub
, csize = byteLength( cmd )
, ecmd = '*3\r\n$' + csize + crlf + cmd + crlf
;
if ( ! ( spubsub && spubsub.patterns.length ) ) {
// client is not subscribed to any pattern
if ( ! alen ) return sock.write( ecmd + '$-1\r\n:0\r\n' );
// send alen messages
for ( ; a < alen; ++a ) {
pname = String( args[ a ] );
reply = ecmd;
reply += '$' + byteLength( pname ) + crlf + pname + crlf;
reply += ':' + ( spubsub ? spubsub.active : 0 ) + crlf;
sock.write( reply );
}
return;
}
spatterns = spubsub.patterns;
if ( ! alen ) {
// no arguments, unsubscribe client from all patterns
for ( ; a < spatterns.length; ++a ) {
// NOTE: spatterns length could change on every iteration
pname = spatterns[ a ];
patt = patterns[ pname ];
// check if pattern exists
if ( ! patt ) continue;
// remove from pubsub
offset = patt.indexOf( sock.name );
if ( ~ offset ) {
patt.splice( offset, 1 );
// delete array property if it is empty
if ( ! patt.length ) delete patterns[ pname ];
soffset = spatterns.indexOf( pname );
if ( ~ soffset ) spatterns.splice( soffset, 1 ) & --a;
--sock.pubsub.active;
}
reply = ecmd;
reply += '$' + byteLength( pname ) + crlf + pname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
return;
}
// scan arguments
for ( ; a < alen; ++a ) {
pname = String( args[ a ] );
patt = patterns[ pname ];
// NOTE: avoid '' ?
// if ( doString( patt ) !== ostr ) continue;
// check if patt exists
if ( patt && patt.length ) {
offset = patt.indexOf( sock.name );
if ( ~ offset ) {
patt.splice( offset, 1 );
// delete array property if it is empty
if ( ! patt.length ) delete patterns[ pname ];
soffset = spatterns.indexOf( pname );
if ( ~ soffset ) spatterns.splice( soffset, 1 );
--sock.pubsub.active;
}
}
reply = ecmd;
reply += '$' + byteLength( pname ) + crlf + pname + crlf;
reply += ':' + sock.pubsub.active + crlf;
sock.write( reply );
}
}
};
};