mirror of
https://github.com/amark/gun.git
synced 2025-06-09 15:46:44 +00:00
2024 lines
52 KiB
JavaScript
2024 lines
52 KiB
JavaScript
"use strict";
|
|
|
|
|
|
//// Error replies.
|
|
|
|
var ERROR = function ( message )
|
|
{
|
|
this.getError = function () { return message; };
|
|
this.toString = function () { return "<ERROR<" + message + ">>"; };
|
|
};
|
|
|
|
var BAD_TYPE = new ERROR ( 'Operation against a key holding the wrong kind of value' );
|
|
var BAD_KEY = new ERROR ( 'no such key' );
|
|
var BAD_INT = new ERROR ( 'value is not an integer or out of range' );
|
|
var BAD_FLOAT = new ERROR ( 'value is not a valid float' );
|
|
var BAD_ARGS = new ERROR ( 'wrong number of arguments' );
|
|
var BAD_SYNTAX = new ERROR ( 'syntax error' );
|
|
var BAD_INDEX = new ERROR ( 'index out of range' );
|
|
var BAD_SORT = new ERROR ( 'One or more scores can\'t be converted into double' );
|
|
|
|
var BAD_BIT1 = new ERROR ( 'bit offset is not an integer or out of range' );
|
|
var BAD_BIT2 = new ERROR ( 'bit is not an integer or out of range' );
|
|
var BAD_SETEX = new ERROR ( 'invalid expire time in SETEX' );
|
|
var BAD_ZUIS = new ERROR ( 'at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE' );
|
|
|
|
|
|
|
|
//// Status replies.
|
|
|
|
var STATUS = function ( message )
|
|
{
|
|
this.getStatus = function () { return message; };
|
|
this.toString = function () { return "<STATUS<" + message + ">>"; };
|
|
};
|
|
|
|
var OK = new STATUS ( 'OK' );
|
|
var PONG = new STATUS ( 'PONG' );
|
|
var NONE = new STATUS ( 'none' );
|
|
|
|
|
|
|
|
//// Redis types.
|
|
|
|
var VALID_TYPE = function () {};
|
|
var TYPE = function ( type, makePrimitive )
|
|
{
|
|
var Constr = function ( value )
|
|
{
|
|
if ( !( this instanceof VALID_TYPE ) )
|
|
return new Constr ( value );
|
|
if ( !value )
|
|
value = makePrimitive ();
|
|
|
|
this.value = value;
|
|
};
|
|
|
|
Constr.getStatus = function () { return type; };
|
|
Constr.prototype = new VALID_TYPE;
|
|
Constr.prototype.toString = function () { return "<TYPE<" + type + ">>"; };
|
|
Constr.prototype.TYPE = Constr;
|
|
return Constr;
|
|
};
|
|
|
|
var EMPTY_STR = { toString : function () { return ""; }, length : 0, copy : function () {} };
|
|
var STRING = TYPE ( "string", function () { return EMPTY_STR; } );
|
|
var LIST = TYPE ( "list", function () { return []; } );
|
|
var HASH = TYPE ( "hash", function () { return {}; } );
|
|
var SET = TYPE ( "set", function () { return {}; } );
|
|
var ZSET = TYPE ( "zset", function () { return {}; } );
|
|
|
|
|
|
|
|
//// Utils.
|
|
|
|
var arr = function ( obj )
|
|
{
|
|
var i, n = obj.length, out = [];
|
|
for ( i = 0; i < n; i ++ )
|
|
out [ i ] = obj [ i ];
|
|
|
|
return out;
|
|
};
|
|
|
|
var range = function ( min, max )
|
|
{
|
|
var xlo, xhi;
|
|
|
|
if (( xlo = min.substr ( 0, 1 ) === '(' ))
|
|
min = str2float ( min.substr ( 1 ) );
|
|
else
|
|
min = str2float ( min );
|
|
|
|
if ( min instanceof ERROR )
|
|
return min;
|
|
|
|
if (( xhi = max.substr ( 0, 1 ) === '(' ))
|
|
max = str2float ( max.substr ( 1 ) );
|
|
else
|
|
max = str2float ( max );
|
|
|
|
if ( max instanceof ERROR )
|
|
return max;
|
|
|
|
return function ( num )
|
|
{
|
|
return !( ( xlo && num <= min ) || ( num < min ) || ( xhi && num >= max ) || ( num > max ) );
|
|
};
|
|
};
|
|
|
|
var slice = function ( arr, start, stop, asCount )
|
|
{
|
|
start = str2int ( start );
|
|
stop = str2int ( stop );
|
|
if ( start instanceof ERROR ) return start;
|
|
if ( stop instanceof ERROR ) return stop;
|
|
|
|
if ( arr.slice )
|
|
{
|
|
var n = arr.length;
|
|
if ( asCount )
|
|
{
|
|
if ( start < 0 )
|
|
{
|
|
start = 0; // Redis is inconsistent about this, ZRANGEBYSCORE will return an empty multibulk on negative offset
|
|
stop = 0; // whilst SORT will return as if the offset was 0. Best to lint these away with client-side errors.
|
|
}
|
|
else if ( stop < 0 ) stop = n;
|
|
else stop += start;
|
|
}
|
|
else
|
|
{
|
|
if ( start < 0 ) start = n + start;
|
|
if ( stop < 0 ) stop = n + stop;
|
|
stop ++;
|
|
}
|
|
|
|
if ( start >= stop )
|
|
return [];
|
|
else
|
|
return arr.slice ( start < 0 ? 0 : start, stop > n ? n : stop );
|
|
}
|
|
|
|
else
|
|
return arr;
|
|
};
|
|
|
|
var str2float = function ( string )
|
|
{
|
|
var value = Number ( string );
|
|
if ( typeof string !== 'string' ) throw new Error ( "WOOT! str2float: '" + string + "' not a string." );
|
|
if ( string === '+inf' ) value = Number.POSITIVE_INFINITY;
|
|
else if ( string === '-inf' ) value = Number.NEGATIVE_INFINITY;
|
|
else if ( !string || ( !value && value !== 0 ) ) return BAD_FLOAT;
|
|
return value;
|
|
};
|
|
|
|
var str2int = function ( string )
|
|
{
|
|
var value = str2float ( string );
|
|
if ( value instanceof ERROR || value % 1 !== 0 ) return BAD_INT;
|
|
return value;
|
|
};
|
|
|
|
var pattern = function ( string )
|
|
{
|
|
string = string.replace ( /[+{($^|.\\]/g, '\\' + '$0' );
|
|
string = string.replace ( /(^|[^\\])([*?])/g, '$1.$2' );
|
|
string = '^' + string + '$';
|
|
|
|
var pattern = new RegExp ( string );
|
|
return pattern.test.bind ( pattern );
|
|
};
|
|
|
|
|
|
|
|
//// Keyspace and pubsub.
|
|
|
|
exports.Backend = function ()
|
|
{
|
|
var state,
|
|
dbs = {},
|
|
delrev = {},
|
|
rev = 0,
|
|
|
|
subs = [],
|
|
call = [],
|
|
tick = false;
|
|
|
|
|
|
// Select.
|
|
// Selected keyspace is NOT relevant to pubsub.
|
|
this.selectDB = function (id) {
|
|
if (typeof id !== "number" || id % 1 !== 0)
|
|
throw new Error("Invalid database id: " + id);
|
|
|
|
// Select or instantiate.
|
|
var db = dbs[id] || (dbs[id] = {});
|
|
state = db;
|
|
};
|
|
|
|
// Connections start in database 0.
|
|
this.selectDB(0);
|
|
|
|
|
|
//// Typed getKey.
|
|
|
|
this.getKey = function ( type, key, make )
|
|
{
|
|
var entry = state [ key ];
|
|
|
|
if ( type && !type.getStatus )
|
|
throw new Error ( "WOOT! type param for getKey is not a valid type." );
|
|
if ( key === undefined )
|
|
throw new Error ( "WOOT! key param for getKey is undefined." );
|
|
|
|
if ( !entry || entry.expire < Date.now () )
|
|
{
|
|
delete state [ key ];
|
|
delrev [ key ] = ++ rev;
|
|
entry = null;
|
|
}
|
|
else if ( !( entry.value instanceof VALID_TYPE ) )
|
|
throw new Error ( "WOOT! keyspace entry value is not a valid type." );
|
|
|
|
if ( type )
|
|
{
|
|
if ( entry && !( entry.value instanceof type ) )
|
|
return BAD_TYPE;
|
|
if ( !entry && make )
|
|
return new type;
|
|
}
|
|
|
|
return ( entry && entry.value ) || null;
|
|
};
|
|
|
|
this.setKey = function ( key, value )
|
|
{
|
|
if ( value )
|
|
{
|
|
if ( !( value instanceof VALID_TYPE ) )
|
|
throw new Error ( "WOOT! Value doesn't have a valid type." );
|
|
|
|
rev ++;
|
|
state [ key ] = { value : value };
|
|
state [ key ].rev = rev;
|
|
delete delrev [ key ];
|
|
|
|
this.pub ( this.UPDATE, key );
|
|
|
|
return 1;
|
|
}
|
|
|
|
else if ( state [ key ] )
|
|
{
|
|
rev ++;
|
|
delrev [ key ] = rev;
|
|
delete state [ key ];
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
this.upsetKey = function ( key, value )
|
|
{
|
|
if ( !value )
|
|
throw new Error ( "WOOT! Update key with a falsy value." );
|
|
if ( !( value instanceof VALID_TYPE ) )
|
|
throw new Error ( "WOOT! Value doesn't have a valid type." );
|
|
|
|
if ( state [ key ] && state [ key ].expire >= Date.now () )
|
|
{
|
|
if ( state [ key ].value !== value )
|
|
throw new Error ( "WOOT! Chaning value containers during upsetKey." );
|
|
|
|
rev ++;
|
|
state [ key ].value = value;
|
|
state [ key ].rev = rev;
|
|
|
|
this.pub ( this.UPDATE, key );
|
|
}
|
|
|
|
else
|
|
this.setKey ( key, value );
|
|
};
|
|
|
|
this.getExpire = function ( key )
|
|
{
|
|
var entry = state [ key ];
|
|
|
|
if ( !entry || entry.expire < Date.now () )
|
|
{
|
|
delete state [ key ];
|
|
return null;
|
|
}
|
|
|
|
return entry.expire;
|
|
};
|
|
|
|
this.setExpire = function ( key, expire )
|
|
{
|
|
var entry = state [ key ];
|
|
|
|
if ( !entry || entry.expire < Date.now () )
|
|
{
|
|
delete state [ key ];
|
|
return 0;
|
|
}
|
|
|
|
else if ( expire )
|
|
{
|
|
entry.expire = expire;
|
|
return 1;
|
|
}
|
|
|
|
else if ( entry.expire )
|
|
{
|
|
delete entry.expire;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
this.getKeys = function ()
|
|
{
|
|
var keys = [],
|
|
key;
|
|
|
|
for ( key in state )
|
|
if ( this.getKey ( null, key ) )
|
|
keys.push ( key );
|
|
|
|
return keys;
|
|
};
|
|
|
|
this.renameKey = function ( keyA, keyB )
|
|
{
|
|
if ( !this.getKey ( null, keyA ) )
|
|
return false;
|
|
|
|
rev ++;
|
|
state [ keyB ] = state [ keyA ];
|
|
state [ keyB ].rev = rev ++;
|
|
delete state [ keyA ];
|
|
|
|
this.pub ( this.UPDATE, keyB );
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
//// Keyspace change event.
|
|
|
|
this.UPDATE = new STATUS ( "Key value updated." );
|
|
|
|
|
|
//// For implementing watch and stuff.
|
|
|
|
this.getRevision = function ( key )
|
|
{
|
|
this.getKey ( null, key );
|
|
return ( state [ key ] && state [ key ].rev ) || delrev [ key ] || 0;
|
|
};
|
|
|
|
|
|
//// Publish / subscribe backend.
|
|
|
|
this.pub = function ( channel, message )
|
|
{
|
|
if ( !channel && channel !== '' ) throw new Error ( "WOOT! Publishing to a falsy, non-string channel : [" + channel + '] ' + message );
|
|
if ( !message && message !== '' ) throw new Error ( "WOOT! Publishing a falsy, non-string message : [" + channel + '] ' + message );
|
|
|
|
var i, n = subs.length, sub, x = 0;
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
sub = subs [ i ];
|
|
|
|
if ( sub.channel === channel || ( sub.pattern !== null && sub.channel ( channel ) ) )
|
|
{
|
|
if ( sub.pattern !== null )
|
|
call.push ( sub.client.pushMessage.bind ( sub.client, 'pmessage', sub.pattern, channel, message ) );
|
|
else
|
|
call.push ( sub.client.pushMessage.bind ( sub.client, 'message', channel, message ) );
|
|
|
|
x ++;
|
|
if ( !tick )
|
|
{
|
|
tick = true;
|
|
process.nextTick ( function ()
|
|
{
|
|
var c, func;
|
|
tick = false;
|
|
c = call.splice ( 0, call.length );
|
|
while (( func = c.shift () )) func ();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return x;
|
|
};
|
|
|
|
//// p - true/false
|
|
//// channel - string
|
|
//// client { push ( pattern, channel, message ) }
|
|
|
|
this.sub = function ( p, channel, client )
|
|
{
|
|
if ( !channel && channel !== '' ) throw new Error ( "WOOT! Subscribing to a falsy, non-string channel : [" + channel + ']' );
|
|
if ( !client || !client.pushMessage ) throw new Error ( "WOOT! Subscribing an invalid client : " + client );
|
|
if ( typeof channel === 'function' ) throw new Error ( "WOOT! Subscribing to a function : " + channel );
|
|
|
|
var i, n = subs.length, sub, found = false;
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
sub = subs [ i ];
|
|
if ( sub.client === client && ( ( p && sub.pattern === channel ) || ( !p && sub.channel === channel ) ) )
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var x = this.numSubs ( client );
|
|
|
|
if ( !found )
|
|
{
|
|
x ++;
|
|
|
|
subs.push ({ pattern : p ? channel : null, channel : p ? pattern ( channel ) : channel, client : client });
|
|
process.nextTick ( client.pushMessage.bind ( client, p ? 'psubscribe' : 'subscribe', channel, x ) );
|
|
}
|
|
|
|
return x;
|
|
};
|
|
|
|
this.unsub = function ( p, channel, client )
|
|
{
|
|
if ( !channel && channel !== '' && channel !== null ) throw new Error ( "WOOT! Unsubscribing from a falsy, non-string, non-null channel : [" + channel + ']' );
|
|
if ( !client || !client.pushMessage ) throw new Error ( "WOOT! Unsubscribing an invalid client : " + client );
|
|
|
|
var x = this.numSubs ( client );
|
|
|
|
var i, n = subs.length, sub;
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
sub = subs [ i ];
|
|
if ( sub.client !== client )
|
|
continue;
|
|
|
|
if ( ( p && sub.pattern !== null && ( channel === null || sub.pattern === channel ) ) || ( !p && sub.pattern === null && ( channel === null || sub.channel === channel ) ) )
|
|
{
|
|
x --;
|
|
subs.splice ( i, 1 );
|
|
process.nextTick ( client.pushMessage.bind ( client, p ? 'punsubscribe' : 'unsubscribe', p ? sub.pattern : sub.channel, x ) );
|
|
i --; n --;
|
|
}
|
|
}
|
|
|
|
return x;
|
|
};
|
|
|
|
this.numSubs = function ( client )
|
|
{
|
|
var i, n = subs.length, x = 0;
|
|
for ( i = 0; i < n; i ++ )
|
|
if ( subs [ i ].client === client )
|
|
x ++;
|
|
|
|
return x;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
//// Redis commands.
|
|
|
|
exports.Backend.prototype =
|
|
{
|
|
|
|
|
|
//// Keys.
|
|
|
|
DEL : function ()
|
|
{
|
|
var i, n = arguments.length, x = 0;
|
|
if ( !n ) return BAD_ARGS;
|
|
for ( i = 0; i < n; i ++ )
|
|
if ( this.setKey ( arguments [ i ], null ) ) x ++;
|
|
|
|
return x;
|
|
},
|
|
|
|
EXISTS : function ( key )
|
|
{
|
|
return this.getKey ( null, key ) ? 1 : 0;
|
|
},
|
|
|
|
PEXPIREAT : function ( key, time )
|
|
{
|
|
time = str2int ( time );
|
|
if ( time instanceof ERROR ) return time;
|
|
return this.setExpire ( key, time );
|
|
},
|
|
|
|
EXPIREAT : function ( key, time )
|
|
{
|
|
time = str2int ( time );
|
|
if ( time instanceof ERROR ) return time;
|
|
return this.setExpire ( key, time * 1000 );
|
|
},
|
|
|
|
PEXPIRE : function ( key, time )
|
|
{
|
|
time = str2int ( time );
|
|
if ( time instanceof ERROR ) return time;
|
|
return this.setExpire ( key, time + Date.now () );
|
|
},
|
|
|
|
EXPIRE : function ( key, time )
|
|
{
|
|
time = str2int ( time );
|
|
if ( time instanceof ERROR ) return time;
|
|
return this.setExpire ( key, time * 1000 + Date.now () );
|
|
},
|
|
|
|
PERSIST : function ( key )
|
|
{
|
|
return this.PEXPIREAT ( key, "0" );
|
|
},
|
|
|
|
PTTL : function ( key )
|
|
{
|
|
var ttl = this.getExpire ( key );
|
|
if ( ttl ) return ttl - Date.now ();
|
|
else return -1;
|
|
},
|
|
|
|
RANDOMKEY : function ( key )
|
|
{
|
|
var keys = this.getKeys (), n = keys && keys.length;
|
|
if ( n ) return keys [ Math.floor ( Math.random () * n ) ];
|
|
else return null;
|
|
},
|
|
|
|
RENAME : function ( key, newkey )
|
|
{
|
|
return this.renameKey ( key, newkey ) ? OK : BAD_KEY;
|
|
},
|
|
|
|
RENAMENX : function ( key, newkey )
|
|
{
|
|
if ( !this.EXISTS ( key ) ) return BAD_KEY;
|
|
if ( this.EXISTS ( newkey ) ) return 0;
|
|
if ( !this.renameKey ( key, newkey ) ) throw new Error ( "WOOT! Couldn't rename." );
|
|
return 1;
|
|
},
|
|
|
|
TTL : function ( key )
|
|
{
|
|
var ttl = this.getExpire ( key );
|
|
if ( ttl ) return Math.ceil ( ( ttl - Date.now () ) / 1000 );
|
|
else return -1;
|
|
},
|
|
|
|
TYPE : function ( key )
|
|
{
|
|
var K = this.getKey ( null, key );
|
|
return K ? K.TYPE : NONE;
|
|
},
|
|
|
|
KEYS : function ( pat )
|
|
{
|
|
var keys = this.getKeys ().filter ( pattern ( pat ) );
|
|
keys.sort ();
|
|
return keys;
|
|
},
|
|
|
|
|
|
|
|
//// String setters.
|
|
|
|
SET : function ( key, value )
|
|
{
|
|
var buf = new Buffer ( Buffer.byteLength ( value ) );
|
|
buf.write ( value );
|
|
|
|
this.setKey ( key, new STRING ( buf ) );
|
|
return OK;
|
|
},
|
|
|
|
sIncrBy : function ( parse, key, incr )
|
|
{
|
|
var K = this.getKey ( STRING, key, true );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
incr = parse ( incr );
|
|
if ( incr instanceof ERROR ) return incr;
|
|
var value = parse ( K.value.toString () || "0" );
|
|
if ( value instanceof ERROR ) return value;
|
|
|
|
value = ( value + incr ).toString ();
|
|
var buf = new Buffer ( Buffer.byteLength ( value ) );
|
|
buf.write ( value );
|
|
|
|
K.value = value;
|
|
this.upsetKey ( key, K );
|
|
return value;
|
|
},
|
|
|
|
sFit : function ( key, length )
|
|
{
|
|
var K = this.getKey ( STRING, key, true );
|
|
if ( K instanceof ERROR ) return ERROR;
|
|
|
|
if ( K.value.length < length )
|
|
{
|
|
var buf = new Buffer ( length );
|
|
buf.fill ( 0 );
|
|
|
|
K.value.copy ( buf );
|
|
K.value = buf;
|
|
}
|
|
|
|
return K;
|
|
},
|
|
|
|
SETBIT : function ( key, offset, state )
|
|
{
|
|
var offset = str2int ( offset );
|
|
if ( !( offset > -1 ) ) return BAD_BIT1;
|
|
var state = str2int ( state );
|
|
if ( !( state === 0 || state === 1 ) ) return BAD_BIT2;
|
|
|
|
var x = Math.floor ( offset / 8 );
|
|
var K = this.sFit ( key, x + 1 );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var mask = 1 << ( 7 - ( offset % 8 ) );
|
|
var current = K.value [ x ];
|
|
var old = current & mask ? 1 : 0;
|
|
|
|
if ( state && !old )
|
|
K.value [ x ] = current | mask;
|
|
else if ( !state && old )
|
|
K.value [ x ] = current & ~mask;
|
|
|
|
this.upsetKey ( key, K );
|
|
return old;
|
|
},
|
|
|
|
SETRANGE : function ( key, offset, value )
|
|
{
|
|
var offset = str2int ( offset );
|
|
if ( !( offset > -1 ) ) return BAD_BIT1;
|
|
|
|
var K = this.sFit ( key, offset + Buffer.byteLength ( value ) );
|
|
K.value.write ( value, offset );
|
|
|
|
this.upsetKey ( key, K );
|
|
return this.STRLEN ( key );
|
|
},
|
|
|
|
//// String getters.
|
|
|
|
GET : function ( key )
|
|
{
|
|
var K = this.getKey ( STRING, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
return K ? K.value.toString () : null;
|
|
},
|
|
|
|
STRLEN : function ( key )
|
|
{
|
|
var K = this.getKey ( STRING, key );
|
|
if ( K instanceof ERROR ) return ERROR;
|
|
return K ? K.value.length : 0;
|
|
},
|
|
|
|
GETBIT : function ( key, offset )
|
|
{
|
|
var K = this.getKey ( STRING, key );
|
|
if ( K instanceof ERROR ) return ERROR;
|
|
|
|
var offset = str2int ( offset );
|
|
if ( !( offset > -1 ) ) return BAD_BIT1;
|
|
var x = Math.floor ( offset / 8 );
|
|
if ( !K || K.length < x + 1 ) return 0;
|
|
|
|
var mask = 1 << ( 7 - ( offset % 8 ) );
|
|
return ( K.value [ x ] & mask ) ? 1 : 0;
|
|
},
|
|
|
|
GETRANGE : function ( key, start, stop )
|
|
{
|
|
var K = this.getKey ( STRING, key );
|
|
if ( K instanceof ERROR ) return ERROR;
|
|
if ( !K ) return "";
|
|
|
|
var out = slice ( K.value, start, stop );
|
|
if ( out instanceof ERROR ) return out;
|
|
return out.toString ();
|
|
},
|
|
|
|
//// String ops.
|
|
|
|
APPEND : function ( key, value )
|
|
{
|
|
var strlen = this.STRLEN ( key );
|
|
if ( strlen instanceof ERROR ) return strlen;
|
|
return this.SETRANGE ( key, strlen.toString (), value );
|
|
},
|
|
|
|
DECR : function ( key )
|
|
{
|
|
return this.DECRBY ( key, "1" );
|
|
},
|
|
|
|
DECRBY : function ( key, decr )
|
|
{
|
|
var value = str2int ( decr );
|
|
if ( value instanceof ERROR ) return value;
|
|
return this.INCRBY ( key, ( - value ).toString () );
|
|
},
|
|
|
|
GETSET : function ( key, value )
|
|
{
|
|
var old = this.GET ( key );
|
|
if ( old instanceof ERROR ) return old;
|
|
this.SET ( key, value );
|
|
return old;
|
|
},
|
|
|
|
INCR : function ( key )
|
|
{
|
|
return this.INCRBY ( key, "1" );
|
|
},
|
|
|
|
INCRBY : function ( key, incr )
|
|
{
|
|
return this.sIncrBy ( str2int, key, incr );
|
|
},
|
|
|
|
INCRBYFLOAT : function ( key, incr )
|
|
{
|
|
return this.sIncrBy ( str2float, key, incr );
|
|
},
|
|
|
|
MGET : function ()
|
|
{
|
|
var out = [], i, n = arguments.length;
|
|
if ( !n ) return BAD_ARGS;
|
|
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
var value = this.GET ( arguments [ i ] );
|
|
out [ i ] = value instanceof ERROR ? null : value;
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
MSET : function ()
|
|
{
|
|
var key, value, i, n = arguments.length;
|
|
if ( !n || n % 2 ) return BAD_ARGS;
|
|
|
|
for ( i = 0; i < n; i += 2 )
|
|
{
|
|
key = arguments [ i ];
|
|
value = arguments [ i + 1 ];
|
|
this.SET ( key, value );
|
|
}
|
|
|
|
return OK;
|
|
},
|
|
|
|
MSETNX : function ()
|
|
{
|
|
var i, n = arguments.length;
|
|
for ( i = 0; i < n; i += 2 )
|
|
if ( this.EXISTS ( arguments [ i ] ) ) return 0;
|
|
|
|
this.MSET.apply ( this, arguments );
|
|
return 1;
|
|
},
|
|
|
|
PSETEX : function ( key, timediff, value )
|
|
{
|
|
if ( !( str2int ( timediff ) > 0 ) )
|
|
return BAD_SETEX;
|
|
|
|
this.SET ( key, value );
|
|
this.PEXPIRE ( key, timediff );
|
|
return OK;
|
|
},
|
|
|
|
SETEX : function ( key, timediff, value )
|
|
{
|
|
if ( !( str2int ( timediff ) > 0 ) )
|
|
return BAD_SETEX;
|
|
|
|
this.SET ( key, value );
|
|
this.EXPIRE ( key, timediff );
|
|
return OK;
|
|
},
|
|
|
|
SETNX : function ( key, value )
|
|
{
|
|
if ( this.EXISTS ( key ) ) return 0;
|
|
this.SET ( key, value );
|
|
return 1;
|
|
},
|
|
|
|
|
|
|
|
//// Lists, non-blocking.
|
|
|
|
lStore : function ( key, values )
|
|
{
|
|
//// Only used in SORT.
|
|
|
|
if ( values.length )
|
|
return this.setKey ( key, new LIST ( values ) );
|
|
else
|
|
return this.setKey ( key, null );
|
|
},
|
|
|
|
LINDEX : function ( key, index )
|
|
{
|
|
var K = this.getKey ( LIST, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
index = str2int ( index );
|
|
if ( index instanceof ERROR )
|
|
return index;
|
|
|
|
return ( K && K.value [ index < 0 ? K.value.length + index : index ] ) || null;
|
|
},
|
|
|
|
upsetList : function ( key, K )
|
|
{
|
|
if ( K.value.length ) this.upsetKey ( key, K );
|
|
else this.setKey ( key, null );
|
|
},
|
|
|
|
LINSERT : function ( key, relpos, pivot, value )
|
|
{
|
|
var K = this.getKey ( LIST, key ), x;
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
relpos = relpos.toUpperCase ();
|
|
if ( relpos !== 'BEFORE' && relpos !== 'AFTER' ) return BAD_SYNTAX;
|
|
if ( !K ) return 0;
|
|
if ( ( x = K.value.indexOf ( pivot ) ) < 0 ) return 0;
|
|
|
|
K.value.splice ( relpos === 'AFTER' ? x + 1 : x, 0, value );
|
|
this.upsetList ( key, K );
|
|
return 1;
|
|
},
|
|
|
|
LLEN : function ( key )
|
|
{
|
|
var K = this.getKey ( LIST, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
return ( K && K.value && K.value.length ) || 0;
|
|
},
|
|
|
|
lPopMany : function ( left, keys )
|
|
{
|
|
var K = [], value, i, n = keys.length;
|
|
if ( !n ) return BAD_ARGS;
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
K [ i ] = this.getKey ( LIST, keys [ i ] );
|
|
if ( K [ i ] instanceof ERROR ) return K [ i ];
|
|
}
|
|
for ( i = 0; i < n; i ++ )
|
|
if ( K [ i ] && K [ i ].value && K [ i ].value.length )
|
|
{
|
|
value = left ? K [ i ].value.shift () : K [ i ].value.pop ();
|
|
this.upsetList ( keys [ i ], K [ i ] );
|
|
return [ keys [ i ], value ];
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
lPop : function ( left, key )
|
|
{
|
|
var out = this.lPopMany ( left, [ key ] );
|
|
return out && out.length ? out [ 1 ] : out;
|
|
},
|
|
|
|
LPOP : function ( key )
|
|
{
|
|
return this.lPop ( true, key );
|
|
},
|
|
|
|
RPOP : function ( key )
|
|
{
|
|
return this.lPop ( false, key );
|
|
},
|
|
|
|
lPush : function ( left, make, args )
|
|
{
|
|
var i, n = args.length, key = args [ 0 ];
|
|
var K = this.getKey ( LIST, key, make );
|
|
if ( K instanceof ERROR ) return K;
|
|
if ( n < 2 ) return BAD_ARGS;
|
|
if ( !K ) return 0;
|
|
|
|
if ( left ) for ( i = 1; i < n; i ++ )
|
|
K.value.unshift ( args [ i ] );
|
|
else
|
|
K.value.push.apply ( K.value, args.slice ( 1 ) );
|
|
|
|
this.upsetList ( key, K );
|
|
return K.value.length;
|
|
},
|
|
|
|
LPUSH : function ()
|
|
{
|
|
return this.lPush ( true, true, arr ( arguments ) );
|
|
},
|
|
|
|
LPUSHX : function ()
|
|
{
|
|
return this.lPush ( true, false, arr ( arguments ) );
|
|
},
|
|
|
|
RPUSH : function ()
|
|
{
|
|
return this.lPush ( false, true, arr ( arguments ) );
|
|
},
|
|
|
|
RPUSHX : function ()
|
|
{
|
|
return this.lPush ( false, false, arr ( arguments ) );
|
|
},
|
|
|
|
RPOPLPUSH : function ( source, destination )
|
|
{
|
|
var dest = this.getKey ( LIST, destination );
|
|
if ( dest && dest instanceof ERROR ) return dest;
|
|
var value = this.RPOP ( source );
|
|
if ( value === null || value instanceof ERROR ) return value;
|
|
|
|
var len = this.LPUSH ( destination, value );
|
|
if ( !len || len instanceof ERROR ) throw new Error ( "WOOT! LPUSH failed in RPOPLPUSH." );
|
|
|
|
return value;
|
|
},
|
|
|
|
LRANGE : function ( key, start, stop )
|
|
{
|
|
var K = this.getKey ( LIST, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
if ( !K ) return [];
|
|
|
|
return slice ( K.value, start, stop );
|
|
},
|
|
|
|
LREM : function ( key, count, value )
|
|
{
|
|
var K = this.getKey ( LIST, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
var count = str2int ( count );
|
|
if ( count instanceof ERROR ) return count;
|
|
if( !K ) return 0;
|
|
|
|
var i, n = K.value.length, x = 0;
|
|
if ( count < 0 )
|
|
{
|
|
count *= -1;
|
|
for ( i = n - 1; i >= 0; i -- )
|
|
if ( K.value [ i ] === value && (!count || x < count) )
|
|
{
|
|
K.value.splice ( i, 1 );
|
|
x ++;
|
|
}
|
|
}
|
|
else for ( i = 0; i < n; i ++ )
|
|
if ( K.value [ i ] === value && (!count || x < count) )
|
|
{
|
|
K.value.splice ( i, 1 );
|
|
i --; n --; x ++;
|
|
}
|
|
|
|
if ( x > 0 ) this.upsetList ( key, K );
|
|
return x;
|
|
},
|
|
|
|
LSET : function ( key, index, value )
|
|
{
|
|
var K = this.getKey ( LIST, key );
|
|
if ( !K ) return BAD_KEY;
|
|
if ( K instanceof ERROR ) return K;
|
|
var index = str2int ( index );
|
|
if ( index instanceof ERROR ) return index;
|
|
if ( index < 0 || index > K.value.length ) return BAD_INDEX;
|
|
|
|
K.value [ index ] = value;
|
|
this.upsetList ( key, K );
|
|
return OK;
|
|
},
|
|
|
|
LTRIM : function ( key, start, stop )
|
|
{
|
|
var range = this.LRANGE ( key, start, stop );
|
|
if ( !range.join )
|
|
return range;
|
|
|
|
var K = this.getKey ( LIST, key );
|
|
if ( K )
|
|
{
|
|
K.value = range;
|
|
this.upsetList ( key, K );
|
|
}
|
|
|
|
return OK;
|
|
},
|
|
|
|
//// Blocking list commands.
|
|
//// The blocking part happens at the connection level,
|
|
//// where in case the response is null the connection subscribes to the keyspace change event for the key and waits to retry.
|
|
|
|
//// So this only validates the parameter.
|
|
|
|
bArgs : function ( args )
|
|
{
|
|
args = arr ( args );
|
|
var timeout = str2int ( args.pop () || "FAIL" );
|
|
if ( timeout instanceof ERROR ) return timeout;
|
|
if ( timeout < 0 ) return BAD_INT;
|
|
return args;
|
|
},
|
|
|
|
BLPOP : function ()
|
|
{
|
|
var a = this.bArgs ( arguments );
|
|
if ( a instanceof ERROR ) return a;
|
|
return this.lPopMany ( true, a );
|
|
},
|
|
|
|
BRPOP : function ()
|
|
{
|
|
var a = this.bArgs ( arguments );
|
|
if ( a instanceof ERROR ) return a;
|
|
return this.lPopMany ( false, a );
|
|
},
|
|
|
|
BRPOPLPUSH : function ()
|
|
{
|
|
var a = this.bArgs ( arguments );
|
|
if ( a instanceof ERROR ) return a;
|
|
return this.RPOPLPUSH.apply ( this, a );
|
|
},
|
|
|
|
|
|
|
|
//// Hashes.
|
|
|
|
structPut : function ( type, validate, revArgs, args )
|
|
{
|
|
var key = args [ 0 ], i, n = args.length, x = 0;
|
|
|
|
if ( n < 3 || ( ( n - 1 ) % 2 ) ) return BAD_ARGS;
|
|
var K = this.getKey ( type, key, true );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
for ( i = 1; i < n; i += 2 )
|
|
{
|
|
var member = args [ revArgs ? i + 1 : i ],
|
|
value = validate ( args [ revArgs ? i : i + 1 ] );
|
|
if ( value instanceof ERROR ) return value;
|
|
if ( !( member in K.value ) ) x ++;
|
|
K.value [ member ] = value;
|
|
}
|
|
|
|
if ( x ) this.upsetKey ( key, K );
|
|
return x;
|
|
},
|
|
|
|
structDel : function ( type, args )
|
|
{
|
|
var key = args [ 0 ],
|
|
i, n = args.length,
|
|
x;
|
|
|
|
if ( n < 2 ) return BAD_ARGS;
|
|
var K = this.getKey ( type, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
if ( !K ) return 0;
|
|
|
|
x = 0;
|
|
for ( i = 1; i < n; i ++ )
|
|
{
|
|
if ( args [ i ] in K.value ) x ++;
|
|
delete K.value [ args [ i ] ];
|
|
}
|
|
|
|
//// Remove the set if empty, upset otherwise.
|
|
|
|
var member;
|
|
for ( member in K.value )
|
|
{
|
|
if ( x ) this.upsetKey ( key, K );
|
|
return x;
|
|
}
|
|
|
|
this.setKey ( key, null );
|
|
return x;
|
|
},
|
|
|
|
structGet : function ( type, key, member )
|
|
{
|
|
var K = this.getKey ( type, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
if ( !K || !( member in K.value ) ) return null;
|
|
return K.value [ member ];
|
|
},
|
|
|
|
HDEL : function ()
|
|
{
|
|
return this.structDel ( HASH, arguments );
|
|
},
|
|
|
|
HEXISTS : function ( key, field )
|
|
{
|
|
var fields = this.HKEYS ( key );
|
|
if ( fields.indexOf ) return fields.indexOf ( field ) >= 0 ? 1 : 0;
|
|
return fields;
|
|
},
|
|
|
|
HGET : function ( key, field )
|
|
{
|
|
return this.structGet ( HASH, key, field );
|
|
},
|
|
|
|
HGETALL : function ( key )
|
|
{
|
|
var fields = this.HKEYS ( key );
|
|
var i, n = fields.length, out = [];
|
|
for ( i = 0; i < n; i ++ )
|
|
out.push ( fields [ i ], this.HGET ( key, fields [ i ] ) );
|
|
|
|
return out;
|
|
},
|
|
|
|
hIncrBy : function ( parse, key, field, incr )
|
|
{
|
|
var K = this.getKey ( HASH, key, true );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
incr = parse ( incr );
|
|
if ( incr instanceof ERROR ) return incr;
|
|
var value = parse ( K.value [ field ] || "0" );
|
|
if ( value instanceof ERROR ) return value;
|
|
|
|
K.value [ field ] = ( value + incr ).toString ();
|
|
this.upsetKey ( key, K );
|
|
return K.value [ field ];
|
|
},
|
|
|
|
HINCRBY : function ( key, field, incr )
|
|
{
|
|
return this.hIncrBy ( str2int, key, field, incr );
|
|
},
|
|
|
|
HINCRBYFLOAT : function ( key, field, incr )
|
|
{
|
|
return this.hIncrBy ( str2float, key, field, incr );
|
|
},
|
|
|
|
HKEYS : function ( key )
|
|
{
|
|
var K = this.getKey ( HASH, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var fields = [], field;
|
|
if ( K ) for ( field in K.value )
|
|
fields.push ( field );
|
|
|
|
fields.sort ();
|
|
return fields;
|
|
},
|
|
|
|
HLEN : function ( key )
|
|
{
|
|
var fields = this.HKEYS ( key );
|
|
if ( fields.indexOf ) return fields.length;
|
|
return fields;
|
|
},
|
|
|
|
HMGET : function ()
|
|
{
|
|
var K = this.getKey ( HASH, arguments [ 0 ] );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var i, n = arguments.length, values = [];
|
|
if ( n < 2 ) return BAD_ARGS;
|
|
for ( i = 1; i < n; i ++ )
|
|
values.push ( K && arguments [ i ] in K.value ? K.value [ arguments [ i ] ] : null );
|
|
|
|
return values;
|
|
},
|
|
|
|
HMSET : function ()
|
|
{
|
|
var x = this.structPut ( HASH, String, false, arguments );
|
|
return x instanceof ERROR ? x : OK;
|
|
},
|
|
|
|
HSET : function ( key, field, value )
|
|
{
|
|
return this.structPut ( HASH, String, false, arguments );
|
|
},
|
|
|
|
HSETNX : function ( key, field, value )
|
|
{
|
|
var exists = this.HEXISTS ( key, field );
|
|
if ( exists instanceof ERROR ) return exists;
|
|
if ( exists ) return 0;
|
|
return this.HSET ( key, field, value );
|
|
},
|
|
|
|
HVALS : function ( key )
|
|
{
|
|
var out = this.HKEYS ( key ), self = this;
|
|
if ( out instanceof ERROR ) return out;
|
|
|
|
if ( out.map )
|
|
out = out.map ( function ( field )
|
|
{
|
|
return self.HGET ( key, field );
|
|
});
|
|
|
|
out.sort ();
|
|
return out;
|
|
},
|
|
|
|
|
|
|
|
//// Sets.
|
|
|
|
SADD : function ()
|
|
{
|
|
var key = arguments [ 0 ],
|
|
i, n = arguments.length,
|
|
x = 0;
|
|
|
|
if ( n < 2 ) return BAD_ARGS;
|
|
var K = this.getKey ( SET, key, true );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
for ( i = 1; i < n; i ++ )
|
|
if ( !K.value [ arguments [ i ] ] )
|
|
{
|
|
K.value [ arguments [ i ] ] = true;
|
|
x ++;
|
|
}
|
|
|
|
if ( x ) this.upsetKey ( key, K );
|
|
return x;
|
|
},
|
|
|
|
SCARD : function ( key )
|
|
{
|
|
var members = this.SMEMBERS ( key );
|
|
return members.join ? members.length : members;
|
|
},
|
|
|
|
SISMEMBER : function ( key, member )
|
|
{
|
|
var members = this.SMEMBERS ( key );
|
|
return members.indexOf ? members.indexOf ( member ) >= 0 ? 1 : 0 : members;
|
|
},
|
|
|
|
SMEMBERS : function ( key )
|
|
{
|
|
return this.SUNION ( key );
|
|
},
|
|
|
|
SPOP : function ( key )
|
|
{
|
|
var member = this.SRANDMEMBER ( key );
|
|
if ( typeof member === 'string' )
|
|
this.SREM ( key, member );
|
|
|
|
return member;
|
|
},
|
|
|
|
SRANDMEMBER : function ( key )
|
|
{
|
|
var members = this.SMEMBERS ( key ),
|
|
n = members.length, member;
|
|
if ( !n )
|
|
return n === 0 ? null : members;
|
|
|
|
member = members [ Math.floor ( Math.random () * n ) ];
|
|
return member;
|
|
},
|
|
|
|
SREM : function ()
|
|
{
|
|
return this.structDel ( SET, arguments );
|
|
},
|
|
|
|
//// Set multikey ops.
|
|
//// Set members come out sorted lexicographically to facilitate testing.
|
|
|
|
SMOVE : function ( source, destination, member )
|
|
{
|
|
var removed = this.SREM ( source, member );
|
|
if ( removed === 1 )
|
|
return this.SADD ( destination, member );
|
|
else
|
|
return removed;
|
|
},
|
|
|
|
SUNION : function ()
|
|
{
|
|
var i, n = arguments.length, out = [];
|
|
if ( !n ) return BAD_ARGS;
|
|
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
var K = this.getKey ( SET, arguments [ i ] );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var member;
|
|
if ( K ) for ( member in K.value )
|
|
if ( out.indexOf ( member ) < 0 )
|
|
out.push ( member );
|
|
}
|
|
|
|
out.sort ();
|
|
return out;
|
|
},
|
|
|
|
sCombine : function ( diff, args )
|
|
{
|
|
var i, n = args.length;
|
|
if ( !n )
|
|
return BAD_ARGS;
|
|
|
|
var out = this.SUNION ( args [ 0 ] );
|
|
if ( out instanceof ERROR ) return out;
|
|
for ( i = 1; i < n; i ++ )
|
|
{
|
|
var K = this.getKey ( SET, args [ i ] );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var j, m = out.length;
|
|
if ( K ) for ( j = 0; j < m; j ++ )
|
|
if ( ( diff && K.value [ out [ j ] ] ) || ( !diff && !K.value [ out [ j ] ] ) )
|
|
{
|
|
out.splice ( j, 1 );
|
|
j --;
|
|
m --;
|
|
}
|
|
}
|
|
|
|
out.sort ();
|
|
return out;
|
|
},
|
|
|
|
SDIFF : function ()
|
|
{
|
|
return this.sCombine ( true, arr ( arguments ) );
|
|
},
|
|
|
|
SINTER : function ()
|
|
{
|
|
return this.sCombine ( false, arr ( arguments ) );
|
|
},
|
|
|
|
sStore : function ( key, members )
|
|
{
|
|
var K, i, n = members.length;
|
|
if ( n ) K = new SET ({});
|
|
for ( i = 0; i < n; i ++ )
|
|
K.value [ members [ i ] ] = true;
|
|
|
|
return this.setKey ( key, K || null );
|
|
},
|
|
|
|
sStoreOp : function ( op, args )
|
|
{
|
|
if ( !args.length )
|
|
return BAD_ARGS;
|
|
|
|
var key = args.shift (),
|
|
members = op.apply ( this, args );
|
|
|
|
if ( members.join )
|
|
{
|
|
this.sStore ( key, members );
|
|
return members.length;
|
|
}
|
|
|
|
return members;
|
|
},
|
|
|
|
SDIFFSTORE : function ()
|
|
{
|
|
return this.sStoreOp ( this.SDIFF, arr ( arguments ) );
|
|
},
|
|
|
|
SINTERSTORE : function ()
|
|
{
|
|
return this.sStoreOp ( this.SINTER, arr ( arguments ) );
|
|
},
|
|
|
|
SUNIONSTORE : function ()
|
|
{
|
|
return this.sStoreOp ( this.SUNION, arr ( arguments ) );
|
|
},
|
|
|
|
|
|
|
|
//// Sorted sets.
|
|
|
|
ZADD : function ()
|
|
{
|
|
return this.structPut ( ZSET, str2float, true, arguments );
|
|
},
|
|
|
|
ZCARD : function ( key )
|
|
{
|
|
return this.ZCOUNT ( key, '-inf', '+inf' );
|
|
},
|
|
|
|
ZCOUNT : function ( key, min, max )
|
|
{
|
|
var members = this.ZRANGEBYSCORE ( key, min, max );
|
|
return members.join ? members.length : members;
|
|
},
|
|
|
|
ZINCRBY : function ( key, incr, member )
|
|
{
|
|
var K = this.getKey ( ZSET, key, true );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var value = str2float ( incr );
|
|
if ( value instanceof ERROR ) return value;
|
|
value += Number ( K.value [ member ] || 0 );
|
|
|
|
K.value [ member ] = value;
|
|
this.upsetKey ( key, K );
|
|
return value;
|
|
},
|
|
|
|
//// Sort set queries.
|
|
|
|
zSort : function ( rev, key, min, max )
|
|
{
|
|
var K = this.getKey ( ZSET, key );
|
|
if ( K instanceof ERROR ) return K;
|
|
if ( !K ) return [];
|
|
|
|
var R = range ( min, max ), member, out = [];
|
|
if ( R instanceof ERROR ) return R;
|
|
|
|
for ( member in K.value )
|
|
if ( R ( K.value [ member ] ) )
|
|
out.push ({ member : member, score : K.value [ member ] });
|
|
|
|
//// First by score,
|
|
//// then in lexicographic order.
|
|
|
|
if ( rev )
|
|
out.sort ( function ( b, a )
|
|
{
|
|
return ( a.score - b.score ) || ( a.member < b.member ? -1 : 1 );
|
|
});
|
|
|
|
else
|
|
out.sort ( function ( a, b )
|
|
{
|
|
return ( a.score - b.score ) || ( a.member < b.member ? -1 : 1 );
|
|
});
|
|
|
|
return out;
|
|
},
|
|
|
|
zUnwrap : function ( range, scores )
|
|
{
|
|
var i, n = range.length, out = n ? [] : range;
|
|
if ( n )
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
out.push ( range [ i ].member );
|
|
if ( scores )
|
|
out.push ( range [ i ].score );
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
zGetRange : function ( rev, args )
|
|
{
|
|
var key = args [ 0 ], start = args [ 1 ], stop = args [ 2 ], scores = args [ 3 ];
|
|
|
|
if ( args.length < 3 || args.length > 4 )
|
|
return BAD_ARGS;
|
|
if ( scores && scores.toUpperCase () !== 'WITHSCORES' )
|
|
return BAD_SYNTAX;
|
|
|
|
var range = this.zSort ( rev, key, '-inf', '+inf' );
|
|
|
|
return this.zUnwrap ( slice ( range, start, stop ), scores );
|
|
},
|
|
|
|
zGetRangeByScore : function ( rev, args )
|
|
{
|
|
var key = args [ 0 ], min = args [ rev ? 2 : 1 ], max = args [ rev ? 1 : 2 ],
|
|
scores, limit, offset, count;
|
|
|
|
if ( args.length < 3 )
|
|
return BAD_ARGS;
|
|
|
|
else if ( args.length === 4 )
|
|
scores = args [ 3 ];
|
|
|
|
else if ( args.length === 6 )
|
|
{
|
|
limit = args [ 3 ];
|
|
offset = args [ 4 ];
|
|
count = args [ 5 ];
|
|
}
|
|
|
|
else if ( args.length === 7 )
|
|
{
|
|
scores = args [ 3 ];
|
|
limit = args [ 4 ];
|
|
offset = args [ 5 ];
|
|
count = args [ 6 ];
|
|
}
|
|
|
|
if ( scores && scores.toUpperCase () !== 'WITHSCORES' )
|
|
return BAD_SYNTAX;
|
|
if ( limit && limit.toUpperCase () !== 'LIMIT' )
|
|
return BAD_SYNTAX;
|
|
|
|
var range = this.zSort ( rev, key, min, max );
|
|
if ( limit )
|
|
range = slice ( range, offset, count, true );
|
|
|
|
return this.zUnwrap ( range, scores );
|
|
},
|
|
|
|
ZRANGE : function ()
|
|
{
|
|
return this.zGetRange ( false, arr ( arguments ) );
|
|
},
|
|
|
|
ZREVRANGE : function ()
|
|
{
|
|
return this.zGetRange ( true, arr ( arguments ) );
|
|
},
|
|
|
|
ZRANGEBYSCORE : function ()
|
|
{
|
|
return this.zGetRangeByScore ( false, arr ( arguments ) );
|
|
},
|
|
|
|
ZREVRANGEBYSCORE : function ()
|
|
{
|
|
return this.zGetRangeByScore ( true, arr ( arguments ) );
|
|
},
|
|
|
|
ZRANK : function ( key, member )
|
|
{
|
|
var out = this.zSort ( false, key, '-inf', '+inf' ),
|
|
i, n = out.length;
|
|
|
|
for ( i = 0; i < n; i ++ )
|
|
if ( out [ i ].member === member )
|
|
return i;
|
|
|
|
return n || n === 0 ? null : out;
|
|
},
|
|
|
|
ZREVRANK : function ( key, member )
|
|
{
|
|
var out = this.zSort ( false, key, '-inf', '+inf' ),
|
|
i, n = out.length;
|
|
|
|
for ( i = n - 1; i >= 0; i -- )
|
|
if ( out [ i ].member === member )
|
|
return n - i - 1;
|
|
|
|
return n || n === 0 ? null : out;
|
|
},
|
|
|
|
ZSCORE : function ( key, member )
|
|
{
|
|
return this.structGet ( ZSET, key, member );
|
|
},
|
|
|
|
ZREM : function ()
|
|
{
|
|
return this.structDel ( ZSET, arguments );
|
|
},
|
|
|
|
ZREMRANGEBYRANK : function ( key, start, stop )
|
|
{
|
|
var members = this.ZRANGE ( key, start, stop ), n = members.length;
|
|
if ( n )
|
|
n = this.ZREM.apply ( this, [ key ].concat ( members ) );
|
|
|
|
return n || n === 0 ? n : members;
|
|
},
|
|
|
|
ZREMRANGEBYSCORE : function ( key, min, max )
|
|
{
|
|
var members = this.ZRANGEBYSCORE ( key, min, max ), n = members.length;
|
|
if ( n )
|
|
n = this.ZREM.apply ( this, [ key ].concat ( members ) );
|
|
|
|
return n || n === 0 ? n : members;
|
|
},
|
|
|
|
//// Sorted set multikey ops.
|
|
|
|
zOpStore : function ( union, key, keys, weights, aggregate )
|
|
{
|
|
var K = this.getKey ( ZSET, keys [ 0 ] );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
var out = {}, member, x = 0, weight = ( weights === null ? 1 : weights [ 0 ] );
|
|
if ( K ) for ( member in K.value )
|
|
{
|
|
out [ member ] = K.value [ member ] * weight;
|
|
x ++;
|
|
}
|
|
|
|
var i, n = keys.length;
|
|
for ( i = 1; i < n; i ++ )
|
|
{
|
|
K = this.getKey ( ZSET, keys [ i ] );
|
|
if ( K instanceof ERROR ) return K;
|
|
|
|
weight = ( weights !== null ? weights [ i ] : 1 );
|
|
if ( !union )
|
|
{
|
|
if ( !K )
|
|
{
|
|
out = {};
|
|
x = 0;
|
|
}
|
|
|
|
else for ( member in out ) if ( !( member in K.value ) )
|
|
{
|
|
delete out [ member ];
|
|
x --;
|
|
}
|
|
}
|
|
|
|
if ( K ) for ( member in K.value )
|
|
if ( union || member in out )
|
|
{
|
|
if ( !( member in out ) )
|
|
{
|
|
x ++;
|
|
out [ member ] = K.value [ member ] * weight;
|
|
}
|
|
|
|
else
|
|
out [ member ] = aggregate ( K.value [ member ] * weight, out [ member ] );
|
|
}
|
|
}
|
|
|
|
if ( x ) this.setKey ( key, new ZSET ( out ) );
|
|
return x;
|
|
},
|
|
|
|
zsum : function ( a, b ) { return a + b; },
|
|
zmin : function ( a, b ) { return a < b ? a : b; },
|
|
zmax : function ( a, b ) { return a > b ? a : b; },
|
|
|
|
zParseOpStore : function ( union, args )
|
|
{
|
|
var key = args [ 0 ], N = str2int ( args [ 1 ] );
|
|
if ( N instanceof ERROR ) return N;
|
|
if ( N < 1 ) return BAD_ZUIS;
|
|
if ( args.length < N + 2 ) return BAD_ARGS;
|
|
|
|
var keys = args.splice ( 2, N ), weigh = ( args [ 2 ] || '' ).toUpperCase () === 'WEIGHTS', weights;
|
|
if ( weigh )
|
|
{
|
|
if ( args.length < N + 3 ) return BAD_ARGS;
|
|
weights = args.splice ( 3, N );
|
|
if ( weights.map ( str2float ).some ( function ( w ) { return w instanceof ERROR; } ) ) return BAD_FLOAT;
|
|
args.splice ( 2, 1 );
|
|
}
|
|
|
|
var aggregate = ( args [ 2 ] || '' ).toUpperCase () === 'AGGREGATE' ? ( args [ 3 ] || '' ).toLowerCase () : null;
|
|
if ( aggregate )
|
|
{
|
|
if ( aggregate !== 'sum' && aggregate !== 'min' && aggregate !== 'max' ) return BAD_SYNTAX;
|
|
aggregate = this [ 'z' + aggregate ];
|
|
if ( typeof aggregate !== 'function' )
|
|
throw new Error ( "WOOT! Can't find the aggregate function for " + args [ 3 ] );
|
|
args.splice ( 2, 2 );
|
|
}
|
|
|
|
if ( args.length !== 2 )
|
|
return BAD_ARGS;
|
|
|
|
return this.zOpStore ( union, key, keys, weights || null, aggregate || this.zsum );
|
|
},
|
|
|
|
ZINTERSTORE : function ()
|
|
{
|
|
return this.zParseOpStore ( false, arr ( arguments ) );
|
|
},
|
|
|
|
ZUNIONSTORE : function ()
|
|
{
|
|
return this.zParseOpStore ( true, arr ( arguments ) );
|
|
},
|
|
|
|
|
|
|
|
//// Sort.
|
|
|
|
sortSelect : function ( pat, key )
|
|
{
|
|
var select = /^((?:.)*?)(?:->(.*))?$/.exec ( pat ),
|
|
key = select [ 1 ].replace ( /\*/, key ), // no g flag, so only first occurence is replaced
|
|
field = select [ 2 ];
|
|
|
|
if ( typeof field === 'string' )
|
|
return this.HGET ( key, field );
|
|
else
|
|
return this.GET ( key );
|
|
},
|
|
|
|
SORT : function ()
|
|
{
|
|
var self = this, args = arr ( arguments ), n = args.length;
|
|
if ( !n ) return new BAD_ARGS;
|
|
|
|
//// Parse.
|
|
//// SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
|
|
|
|
var key = args.shift (),
|
|
by, limit, offset, count, get, pat, desc, alpha, store;
|
|
|
|
if ( /^by$/i.test ( args [ 0 ] ) )
|
|
{
|
|
by = args [ 1 ];
|
|
if ( typeof by !== 'string' ) return BAD_SYNTAX;
|
|
args.splice ( 0, 2 );
|
|
}
|
|
|
|
if ( /^limit$/i.test ( args [ 0 ] ) )
|
|
{
|
|
limit = true;
|
|
if ( args.length < 3 ) return BAD_ARGS;
|
|
offset = args [ 1 ]; // integer validation happens in slice()
|
|
count = args [ 2 ];
|
|
args.splice ( 0, 3 );
|
|
}
|
|
|
|
while ( /^get$/i.test ( args [ 0 ] ) )
|
|
{
|
|
pat = args [ 1 ];
|
|
if ( typeof pat !== 'string' ) return BAD_SYNTAX;
|
|
if ( !get ) get = [];
|
|
get.push ( pat );
|
|
args.splice ( 0, 2 );
|
|
}
|
|
|
|
if ( /^asc|desc$/i.test ( args [ 0 ] ) )
|
|
{
|
|
desc = /^desc$/i.test ( args [ 0 ] );
|
|
args.splice ( 0, 1 );
|
|
}
|
|
|
|
if ( /^alpha$/i.test ( args [ 0 ] ) )
|
|
{
|
|
alpha = true;
|
|
args.splice ( 0, 1 );
|
|
}
|
|
|
|
if ( /^store$/i.test ( args [ 0 ] ) )
|
|
{
|
|
store = args [ 1 ];
|
|
if ( typeof store !== 'string' ) return BAD_SYNTAX;
|
|
args.splice ( 0, 2 );
|
|
}
|
|
|
|
//// Redis appears to accept params in any order,
|
|
//// needs some tests before allowing this here.
|
|
|
|
if ( args.length ) return BAD_SYNTAX;
|
|
|
|
//// Collect data.
|
|
|
|
var type = this.TYPE ( key ), data, scoreFail = false;
|
|
|
|
if ( type === NONE )
|
|
data = [];
|
|
else if ( type === LIST )
|
|
data = this.LRANGE ( key, '0', '-1' );
|
|
else if ( type === SET )
|
|
data = this.SMEMBERS ( key );
|
|
else if ( type === ZSET )
|
|
data = this.ZRANGE ( key, '0', '-1' );
|
|
else
|
|
return BAD_TYPE;
|
|
|
|
data = data.map ( function ( id )
|
|
{
|
|
var entry = { id : id };
|
|
if ( by )
|
|
{
|
|
entry.by = self.sortSelect ( by, id );
|
|
if ( !alpha )
|
|
entry.num = str2float ( entry.by || '0' );
|
|
}
|
|
else if ( !alpha )
|
|
entry.num = str2float ( id );
|
|
else
|
|
entry.num = 0;
|
|
|
|
if ( entry.num instanceof ERROR )
|
|
scoreFail = true;
|
|
|
|
if ( get )
|
|
entry.get = get.map ( function ( get )
|
|
{
|
|
if ( get === '#' ) return id;
|
|
return self.sortSelect ( get, id );
|
|
});
|
|
|
|
return entry;
|
|
});
|
|
|
|
if ( scoreFail ) return BAD_SORT;
|
|
|
|
//// Sort.
|
|
|
|
data.sort ( function ( a, b )
|
|
{
|
|
var d = a.num - b.num;
|
|
if ( !d && by ) d = a.by < b.by ? -1 : a.by > b.by ? 1 : 0;
|
|
if ( !d ) d = a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
return desc ? -d : d;
|
|
});
|
|
|
|
//// Limit.
|
|
|
|
if ( parseInt ( offset ) < 0 )
|
|
offset = '0'; // SORT treats negative offset limit differently from other redis commands.
|
|
|
|
if ( limit ) data = slice ( data, offset, count, true );
|
|
|
|
//// Format.
|
|
|
|
var out = [], i;
|
|
n = data.length;
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
if ( get ) out.push.apply ( out, data [ i ].get );
|
|
else out [ i ] = data [ i ].id;
|
|
}
|
|
|
|
//// Store or return.
|
|
|
|
if ( store )
|
|
{
|
|
this.lStore ( store, out );
|
|
return this.LLEN ( store );
|
|
}
|
|
else
|
|
return out;
|
|
},
|
|
|
|
|
|
|
|
//// Pubsub.
|
|
|
|
PUBLISH : function ( channel, message )
|
|
{
|
|
return this.pub ( channel, message );
|
|
},
|
|
|
|
|
|
|
|
//// Connection.
|
|
//// Quit and select could be implemented on the connection object.
|
|
|
|
PING : function ()
|
|
{
|
|
if ( arguments.length )
|
|
return BAD_ARGS;
|
|
|
|
return PONG;
|
|
},
|
|
|
|
ECHO : function ( message )
|
|
{
|
|
return message;
|
|
},
|
|
|
|
|
|
|
|
//// Server.
|
|
//// FLUSHALL can be implemented on the connection object.
|
|
|
|
DBSIZE : function ()
|
|
{
|
|
return this.getKeys ().length;
|
|
},
|
|
|
|
FLUSHDB : function ()
|
|
{
|
|
var keys = this.getKeys (), i, n = keys.length;
|
|
for ( i = 0; i < n; i ++ )
|
|
this.setKey ( keys [ i ], null );
|
|
|
|
return OK;
|
|
},
|
|
|
|
TIME : function ()
|
|
{
|
|
var time = Date.now (),
|
|
sec = Math.round ( time / 1000 ),
|
|
msec = ( time % 1000 ) * 1000 + Math.floor ( Math.random () * 1000 );
|
|
|
|
return [ sec, msec ];
|
|
},
|
|
|
|
|
|
|
|
//// Helper commands.
|
|
/*
|
|
FAKE_MISS : function ()
|
|
{
|
|
var implemented = this;
|
|
|
|
return require ( "../lib/commands" ).filter ( function ( command )
|
|
{
|
|
return !( command.toUpperCase () in implemented );
|
|
});
|
|
},
|
|
|
|
FAKE_AVAIL : function ()
|
|
{
|
|
var implemented = this;
|
|
|
|
return require ( "../lib/commands" ).filter ( function ( command )
|
|
{
|
|
return ( command.toUpperCase () in implemented );
|
|
});
|
|
},
|
|
*/
|
|
FAKE_DUMP : function ( pattern )
|
|
{
|
|
var keys = this.KEYS ( pattern ), i, n = keys.length, out = [], key, type;
|
|
|
|
for ( i = 0; i < n; i ++ )
|
|
{
|
|
key = keys [ i ];
|
|
type = this.TYPE ( key );
|
|
out.push ( key, this.TTL ( key ), type.getStatus () );
|
|
|
|
if ( type === STRING )
|
|
out.push ( this.GET ( key ) );
|
|
else if ( type === LIST )
|
|
out.push ( this.LRANGE ( key, '0', '-1' ) );
|
|
else if ( type === HASH )
|
|
out.push ( this.HGETALL ( key ) );
|
|
else if ( type === SET )
|
|
out.push ( this.SMEMBERS ( key ) );
|
|
else if ( type === ZSET )
|
|
out.push ( this.ZRANGE ( key, '0', '-1', 'withscores' ) );
|
|
else
|
|
throw new Error ( "WOOT! Key type is " + type );
|
|
}
|
|
|
|
return out;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
//// These don't have an effect on the dataset, so dummies are safe for tests.
|
|
|
|
exports.Backend.prototype.AUTH =
|
|
exports.Backend.prototype.BGREWRITEAOF =
|
|
exports.Backend.prototype.SAVE =
|
|
exports.Backend.prototype.BGSAVE = function () { return OK; };
|
|
|
|
|
|
|
|
//// All of these are implemented at the connection level.
|
|
|
|
exports.Backend.prototype.QUIT =
|
|
|
|
exports.Backend.prototype.SUBSCRIBE =
|
|
exports.Backend.prototype.PSUBSCRIBE =
|
|
exports.Backend.prototype.UNSUBSCRIBE =
|
|
exports.Backend.prototype.PUNSUBSCRIBE =
|
|
|
|
exports.Backend.prototype.MULTI =
|
|
exports.Backend.prototype.EXEC =
|
|
exports.Backend.prototype.WATCH =
|
|
exports.Backend.prototype.UNWATCH =
|
|
exports.Backend.prototype.SELECT =
|
|
exports.Backend.prototype.DISCARD = function () { throw new Error ( "WOOT! This command shouldn't have reached the backend." ); };
|
|
|
|
|
|
|