Repository: chriso/redback Branch: master Commit: 1529cbf2bfa3 Files: 44 Total size: 375.5 KB Directory structure: gitextract_q167ekye/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs/ │ ├── api.html │ └── index.html ├── index.js ├── lib/ │ ├── Cache.js │ ├── Channel.js │ ├── Redback.js │ ├── Structure.js │ ├── Utils.js │ ├── advanced_structures/ │ │ ├── BloomFilter.js │ │ ├── CappedList.js │ │ ├── DensitySet.js │ │ ├── KeyPair.js │ │ ├── Lock.js │ │ ├── Queue.js │ │ ├── RateLimit.js │ │ └── SocialGraph.js │ └── base_structures/ │ ├── Bitfield.js │ ├── Hash.js │ ├── List.js │ ├── Set.js │ └── SortedSet.js ├── package.json └── test/ ├── bitfield.test.js ├── bloomfilter.test.js ├── cache.test.js ├── cappedlist.test.js ├── channel.test.js ├── common.js ├── crc32.test.js ├── densityset.test.js ├── hash.test.js ├── keypair.test.js ├── list.test.js ├── lock.test.js ├── namespace.test.js ├── queue.test.js ├── set.test.js ├── socialgraph.test.js ├── sortedset.test.js └── structure.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules npm-debug.log ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js node_js: - stable services: - redis-server after_script: - npm test ================================================ FILE: LICENSE ================================================ Copyright (c) 2016 Chris O'Hara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Redback [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Downloads][downloads-image]][npm-url] A high-level Redis library. ```sh $ npm install redback ``` ## Introduction Redback provides an accessible and extensible interface to the Redis [data types](http://redis.io/topics/data-types) and allows you to create your own structures with ease. Redback comes with the following built-in structures: **List**, **Set**, **SortedSet**, **Hash**, **Channel**, **Cache** It also comes with the following advanced data structures: - **DensitySet** - A sorted set where adding an element increments its score and removing it decrements it - **KeyPair** - Uses two hash structures and an auto-incrementing key to assign an ID to each unique value - **SocialGraph** - Similar to Twitter's (following vs. followers) - **CappedList** - A list with a fixed length - **Queue** - A simple FIFO or LIFO queue - **RateLimit** - Count the number of times an event occurs over an interval. See [this introduction](https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da). - **BloomFilter** - A probabilistic structure used to test whether an an element exists in a set ## Usage ```javascript var redback = require('redback').createClient(); // or var redis = require('redis').createClient(); var redback = require('redback').use(redis); ``` ```javascript var user3 = redback.createSocialGraph(3); user3.follow(1, callback); var log = redback.createCappedList('log', 1000); log.push('Log message ...'); var user = redback.createHash('user1'); user.set({username: 'chris', password: 'foobar'}, callback); ``` ## Creating your own structures Use `addStructure(name, methods)` to create your own structure. Let's create a queue that can be either FIFO or LIFO: ```javascript redback.addStructure('SimpleQueue', { init: function (options) { options = options || {}; this.fifo = options.fifo; }, add: function (value, callback) { this.client.lpush(this.key, value, callback); }, next: function (callback) { var method = this.fifo ? 'rpop' : 'lpop'; this.client[method](this.key, callback); } }); ``` Call `createSimpleQueue(key, options)` to use the queue: ```javascript var queue = redback.createSimpleQueue('my_queue', {fifo: true}); queue.add('awesome!'); ``` Structures have access to a Redis key `this.key` and the Redis client `this.client`. If an `init()` method is defined then it is called after the structure is instantiated. Also note that `init()` receives any extra parameters from `create()`. ## Other uses **Cache backend** ```javascript var cache = redback.createCache(namespace); cache.set('foo', 'bar', callback); cache.get('foo', function (err, foo) { console.log(foo); //bar }); ``` **Pub/sub provider** ```javascript var channel = redback.createChannel('chat').subscribe(); //To received messages channel.on('message', function (msg) { console.log(msg); }); //To send messages channel.publish(msg); ``` ## Documentation See the [annotated source](http://chriso.github.io/redback/api.html). ## Tests The tests require a local redis instance running on `localhost:6379`. Note that redis database #11 will be flushed prior to each run. ```sh $ npm test ``` ## Credits - Matt Ranney for his [node_redis](https://github.com/mranney/node_redis) library. - GitHub user [sreeix](https://github.com/sreeix) for the bloom filter implementation. ## License MIT [downloads-image]: http://img.shields.io/npm/dm/redback.svg [npm-url]: https://npmjs.org/package/redback [npm-image]: http://img.shields.io/npm/v/redback.svg [travis-url]: https://travis-ci.org/chriso/redback [travis-image]: http://img.shields.io/travis/chriso/redback.svg ================================================ FILE: docs/api.html ================================================ Fork me on GitHub Redback

Redback

A high-level Redis library

BloomFilter

lib/advanced_structures/BloomFilter.js

Module dependencies.

var Structure = require('../Structure'),
    crc32 = require('../Utils').crc32;

A Simple BloomFilter. Bloomfilter is a probabilistic data structure used to determine if an element is present in a set. There may be false positives, but there cannot be false negatives.

Usage

redback.createBloomFilter(key [, size, hashes]);

Options

size - Size of the bloom filter , default is 100 bits. hashes - Number of hashes to perform. default is 2.

Reference

http://redis.io/commands#string http://en.wikipedia.org/wiki/Bloom_filter http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html

Redis Structure

(namespace:)key = string(bits)

var BloomFilter = exports.BloomFilter = Structure.new();

Adds an element to the bloom filter.

  • param: string item - Item to store into bloom filter

  • param: Function callback (optional)

  • return: this

  • api: public

BloomFilter.prototype.add = function(item, callback) {
    var multi = this.client.multi(), crc;

    for (var hash_index = 0; hash_index < this.num_hashes; hash_index++) {
        crc = crc32(item, hash_index) % (this.size+1);
        multi.setbit(this.key, crc, 1);
    }
    multi.exec(callback || function () {});
    return this;
}

Checks if the element exists in the bloom filter. This can return false positives( i.e An element does not exist but it returns true) But this can never return false negatives. (i.e an element )

  • param: string item - Item to check for existence in bloom filter

  • param: Function callback (optional)

  • return: this

  • api: public

BloomFilter.prototype.exists = function(item, callback) {
    var multi = this.client.multi(), crc;
    callback = callback || function () {};

    for (var hash_index = 0; hash_index < this.num_hashes; hash_index++) {
        crc = crc32(item, hash_index) % (this.size+1);
        multi.getbit(this.key, crc);
    }

    multi.exec(function(err, results) {
        callback(err, results.indexOf(0) === -1);
    });

    return this;
}

Resets the Bloom filter.

  • param: Function callback (optional)

  • return: this

BloomFilter.prototype.reset = function (callback) {
    this.client.set(this.key, 0, callback || function () {});
    return this;
}

CappedList

lib/advanced_structures/CappedList.js

Module dependencies.

var List = require('../base_structures/List').List;

A Redis list with a fixed length. Each command that adds a value to the list is followed by an LTRIM command.

Usage

redback.createCappedList(key [, max_length]);

Reference

http://redis.io/topics/data-types#lists http://redis.io/commands/ltrim

Redis Structure

(namespace:)key = list(values)

var CappedList = exports.CappedList = List.prototype.extend();

Insert an element before the specified pivot.

  • param: int pivot

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

CappedList.prototype.insertBefore = function (pivot, value, callback) {
    callback = callback || function () {};
    var multi = this.client.multi()
    multi.linsert(this.key, 'BEFORE', pivot, value);
    multi.ltrim(this.key, -1 * this.len, -1);
    multi.exec(callback);
    return this;
}

Insert an element after the specified pivot.

  • param: int pivot

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

CappedList.prototype.insertAfter = function (pivot, value, callback) {
    callback = callback || function () {};
    var multi = this.client.multi()
    multi.linsert(this.key, 'AFTER', pivot, value);
    multi.ltrim(this.key, -1 * this.len, -1);
    multi.exec(callback);
    return this;
}

Add one or more elements to the start of the list.

  • param: string | array value(s)

  • param: Function callback (optional)

  • return: this

  • api: public

CappedList.prototype.unshift = CappedList.prototype.lpush = function (values, callback) {
    callback = callback || function () {};
    var multi = this.client.multi();
    if (Array.isArray(values)) {
        var key = this.key;
        values.reverse().forEach(function (value) {
            multi.lpush(key, value);
        });
    } else {
        multi.lpush(this.key, values);
    }
    multi.ltrim(this.key, -1 * this.len, -1);
    multi.exec(callback);
    return this;
}

Add one or more elements to the end of the list.

  • param: string | array value(s)

  • param: Function callback (optional)

  • return: this

  • api: public

CappedList.prototype.push = CappedList.prototype.add = function (values, callback) {
    callback = callback || function () {};
    var multi = this.client.multi();
    if (Array.isArray(values)) {
        var key = this.key;
        values.forEach(function (value) {
            multi.rpush(key, value);
        });
    } else {
        multi.rpush(this.key, values);
    }
    multi.ltrim(this.key, -1 * this.len, -1);
    multi.exec(callback);
    return this;
}

DensitySet

lib/advanced_structures/DensitySet.js

Module dependencies.

var SortedSet = require('../base_structures/SortedSet').SortedSet;

The DensitySet is similar to a SortedSet but the ability to explicitly set an element's score has been removed. Instead, adding/removing an element will increment/decrement its score, e.g. DensitySet.add(['foo','foo','foo'], ..) //'foo' has a score of 3

Usage

redback.createDensitySet(key);

Reference

http://redis.io/topics/data-types#sorted-sets

Redis Structure

(namespace:)key = zset(count => element)

var DensitySet = exports.DensitySet = SortedSet.prototype.extend();

Add one or more elements to the set.

  • param: string | Array element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

DensitySet.prototype.add = function (element, callback) {
    callback = callback || function () {};
    if (Array.isArray(element)) {
        return this.addAll(element, callback);
    }
    this.client.zincrby(this.key, 1, element, callback);
    return this;
}

Remove one or more elements from the set.

  • param: string | Array element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

DensitySet.prototype.remove = function (element, callback) {
    callback = callback || function () {};
    if (Array.isArray(element)) {
        return this.removeAll(element, callback);
    }
    var self = this;
    this.client.zincrby(this.key, -1, element, function (err, removed) {
        if (err) return callback(err, null);
        self.client.zremrangebyscore(self.key, '-inf', 0, callback);
    });
    return this;
}

KeyPair

lib/advanced_structures/KeyPair.js

Module dependencies.

var Structure = require('../Structure');

The KeyPair is a structure where unique values are assigned an ID (like a table with a primary auto-incrementing key and a single unique column). Internally, the KeyPair uses two Redis hashes to provide O(1) lookup by both ID and value.

Usage

redback.createKeyPair(key);

Reference

http://redis.io/topics/data-types#hashes

Redis Structure

(namespace:)key = hash(id => value) (namespace:)key:ids = hash(value => id)

var KeyPair = exports.KeyPair = Structure.new();

Add a unique value to the KeyPair and return its id. If the value already exists, the existing id is returned.

  • param: string | Array value(s)

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.add = function (value, callback) {
    //Pass on an array of values to addAll()
    if (Array.isArray(value)) {
        return this.addAll(value, callback);
    }

    var self = this, hashed_value = this.hashValue(value);
    //Check if the value already has an id
    this.client.hget(this.idkey, value, function (err, id) {
        if (err) return callback(err, null);
        if (null !== id) {
            callback(null, id);
        } else {
            //If not, create a new id
            self.autoincrement(function (err, id) {
                if (err) return callback(err, null);

                //Set the id and value simultaneously
                var multi = self.client.multi();
                multi.hsetnx(self.idkey, hashed_value, id);
                multi.hsetnx(self.key, id, value);
                multi.exec(function(err, response) {
                    if (err) return callback(err, null);

                    //Another client may have add at exactly the same time, so do
                    //another get to get the actual stored id
                    self.client.hget(self.idkey, hashed_value, function (err, real_id) {
                        if (err) return callback(err, null);
                        if (real_id == id) {
                            return callback(null, real_id);
                        } else {
                            //Another client did beat us! remove the bad key
                            self.client.hdel(self.key, id, function (err) {
                                if (err) {
                                    callback(err, null);
                                } else {
                                    callback(null, real_id);
                                }
                            });
                        }
                    });
                });
            });
        }
    });
    return this;
}

Add multiple unique values to the KeyPair and return and object containing {value: id, ...}.

  • param: Array values

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.addAll = function (values, callback) {
    var self = this,
        remaining = values.length,
        ids = {},
        failed = false;

    values.forEach(function (value) {
        self.add(value, function (err, id) {
            if (failed) {
                return;
            } else if (err) {
                failed = true;
                return callback(err, null);
            } else {
                ids[value] = id;
                if (!--remaining) callback(null, ids);
            }
        });
    });
}

Lookup a unique value and get the associated id.

  • param: string value

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.get = function (value, callback) {
    if (typeof value === 'function') {
        callback = value;
        this.client.hgetall(this.key, callback);
    } else if (Array.isArray(value)) {
        for (var i = 0, l = value.length; i < l; i++) {
            value[i] = this.hashValue(value[i]);
        }
        this.client.hmget(this.idkey, value, callback)
    } else {
        this.client.hget(this.idkey, this.hashValue(value), callback);
    }
    return this;
}

Get the value associated with the id.

  • param: int | Array id(s)

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.getById = function (id, callback) {
	if (Array.isArray(id))
    	this.client.hmget(this.key, id, callback);
	else
    	this.client.hget(this.key, id, callback);
    return this;
}

Get an array of ids.

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.ids = function (callback) {
    this.client.hkeys(this.key, callback);
    return this;
}

Get an array of values.

  • param: string value

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.values = function (callback) {
    this.client.hvals(this.key, callback);
    return this;
}

Check whether a unique value already exists and has an associated id.

  • param: string value

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.exists = function (value, callback) {
    this.client.hexists(this.idkey, this.hashValue(value), callback);
    return this;
}

Checks whether an id exists.

  • param: string value

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.idExists = function (id, callback) {
    this.client.hexists(this.key, id, callback);
    return this;
}

Deletes a unique value and its associated id.

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

KeyPair.prototype.delete = function (value, callback) {
    callback = callback || function () {};
    var self = this, value = this.hashValue(value);
    this.client.hget(this.idkey, value, function (err, id) {
        if (err || value == null) return callback(err);
        self._delete(id, value, callback);
    });
    return this;
}

Deletes an id and its associated unique value.

  • param: int id

  • param: Function callback (optional)

  • return: this

  • api: public

KeyPair.prototype.deleteById = function (id, callback) {
    callback = callback || function () {};
    var self = this;
    this.client.hget(this.key, id, function (err, value) {
        if (err || value == null) return callback(err);
        self._delete(id, self.hashValue(value), callback);
    });
    return this;
}

Get the number of unique values.

  • param: Function callback

  • return: this

  • api: public

KeyPair.prototype.length = function (callback) {
    this.client.hlen(this.key, callback);
    return this;
}

Override this method if you need to hash the unique value in the second internal hash (i.e. if values are large).

  • param: string value

  • return: string hashed_value

  • api: public

KeyPair.prototype.hashValue = function (value) {
    return value;
}

Lock

lib/advanced_structures/Lock.js

Module dependencies.

var crypto = require('crypto');
var Structure = require('../Structure');

A distributed lock.

var Lock = exports.Lock = Structure.new();

Acquire a temporary lock on some key.

  • param: string key The unique key of the lock

  • param: number ttl The amount of time (in seconds) before the lock expires

  • param: Function callback Invoked when the process completes

  • param: Error callback.err An error that occurred, if any

  • param: string callback.token The token that was acquired if successful. If the lock was

    not acquired then this will be undefined

  • api: public

Lock.prototype.acquire = function(key, ttl, callback) {
    var client = this.client;

    _createToken(function(err, token) {
        if (err) {
            return callback(err);
        }

        client.setnx(key, token, function(err, wasSet) {
            if (err) {
                return callback(err);
            } else if (!wasSet) {
                // We did not successfully acquire the lock. Since a process can crash after it sets
                // the lock but before it sets the expiry, we need to avoid deadlocks by ensuring
                // the lock has a TTL associated to it
                _ensureTtl(client, key, ttl);
                return callback();
            }

            // Apply the expiry to the lock
            client.expire(key, ttl, function(err) {
                if (err) {
                    return callback(err);
                }

                // Return the token, which is used to release the lock
                return callback(null, token);
            });
        });
    });
};

Release a lock that was acquired with the provided key and token.

  • param: string key The key for the lock to release

  • param: string token The token that was generated for the lock acquisition

  • param: Function callback Invoked when the function completes

  • param: Error callback.err An error that occurred, if any

  • param: boolean callback.hadLock Determines whether or not we owned the lock at the time

    that we released it

  • api: public

Lock.prototype.release = function(key, token, callback) {
    var client = this.client;

    client.get(key, function(err, lockedToken) {
        if (err) {
            return callback(err);
        } else if (lockedToken !== token) {
            // The current token is not the one we acquired. It's possible we held the lock longer
            // than its expiry
            return callback(null, false);
        }

        // We have the token, simply delete the lock key
        client.del(key, function(err) {
            if (err) {
                return callback(err);
            }

            return callback(null, true);
        });
    });
};

Queue

lib/advanced_structures/Queue.js

Module dependencies.

var Structure = require('../Structure'),
    List = require('../base_structures/List').List;

A simple FIFO/LIFO queue.

Usage

redback.createQueue(key [, is_fifo]);

Reference

http://redis.io/topics/data-types#lists http://en.wikipedia.org/wiki/Queue(datastructure)

Redis Structure

(namespace:)key = list(values)

var Queue = exports.Queue = Structure.new();

Add one or more elements to the queue.

  • param: string | Array value(s)

  • param: Function callback (optional)

  • api: public

Queue.prototype.enqueue = Queue.prototype.add = function (values, callback) {
    this.list.unshift(values, callback);
    return this;
}

Remove the next element from the queue.

  • param: int wait (optional) - block for this many seconds

  • param: Function callback

  • api: public

Queue.prototype.dequeue = Queue.prototype.next = function (wait, callback) {
    this.list[this.fifo ? 'pop' : 'shift'](wait, callback);
    return this;
}

RateLimit

lib/advanced_structures/RateLimit.js

Module dependencies.

var Structure = require('../Structure');

See https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da

Count the number of times a subject performs an action over an interval in the immediate past - this can be used to rate limit the subject if the count goes over a certain threshold. For example, you could track how many times an IP (the subject) has viewed a page (the action) over a certain time frame and limit them accordingly.

Usage

redback.createRateLimit(action [, options]);

Options

bucket_interval - default is 5 seconds bucket_span - default is 10 minutes subject_expiry - default is 20 minutes

Reference

https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da http://redis.io/topics/data-types#hash

Redis Structure

(namespace:)action:<subject1> = hash(bucket => count) (namespace:)action:<subject2> = hash(bucket => count) (namespace:)action:<subjectN> = hash(bucket => count)

var RateLimit = exports.RateLimit = Structure.new();

Increment the count for the specified subject.

  • param: string subject

  • param: Function callback (optional)

  • return: this

  • api: public

RateLimit.prototype.add = function (subject, callback) {
    if (Array.isArray(subject)) {
        return this.addAll(subject, callback);
    }
    var bucket = this.getBucket(), multi = this.client.multi();
    subject = this.key + ':' + subject;

    //Increment the current bucket
    multi.hincrby(subject, bucket, 1)

    //Clear the buckets ahead
    multi.hdel(subject, (bucket + 1) % this.bucket_count)
         .hdel(subject, (bucket + 2) % this.bucket_count)

    //Renew the key TTL
    multi.expire(subject, this.subject_expiry);

    multi.exec(function (err) {
        if (!callback) return;
        if (err) return callback(err);
        callback(null);
    });

    return this;
}

Count the number of times the subject has performed an action in the last interval seconds.

  • param: string subject

  • param: int interval

  • param: Function callback

  • return: this

  • api: public

RateLimit.prototype.count = function (subject, interval, callback) {
    var bucket = this.getBucket(),
        multi = this.client.multi(),
        count = Math.floor(interval / this.bucket_interval);

    subject = this.key + ':' + subject;

    //Get the counts from the previous `count` buckets
    multi.hget(subject, bucket);
    while (count--) {
        multi.hget(subject, (--bucket + this.bucket_count) % this.bucket_count);
    }

    //Add up the counts from each bucket
    multi.exec(function (err, counts) {
        if (err) return callback(err, null);
        for (var count = 0, i = 0, l = counts.length; i &lt; l; i++) {
            if (counts[i]) {
                count += parseInt(counts[i], 10);
            }
        }
        callback(null, count);
    });

    return this;
}

An alias for ratelimit.add(subject).count(subject, interval);

  • param: string subject

  • param: int interval

  • param: Function callback

  • return: this

  • api: public

RateLimit.prototype.addCount = function (subject, interval, callback) {
    var bucket = this.getBucket(),
        multi = this.client.multi(),
        count = Math.floor(interval / this.bucket_interval);

    subject = this.key + ':' + subject;

    //Increment the current bucket
    multi.hincrby(subject, bucket, 1)

    //Clear the buckets ahead
    multi.hdel(subject, (bucket + 1) % this.bucket_count)
         .hdel(subject, (bucket + 2) % this.bucket_count)

    //Renew the key TTL
    multi.expire(subject, this.subject_expiry);

    //Get the counts from the previous `count` buckets
    multi.hget(subject, bucket);
    while (count--) {
        multi.hget(subject, (--bucket + this.bucket_count) % this.bucket_count);
    }

    //Add up the counts from each bucket
    multi.exec(function (err, counts) {
        if (err) return callback(err, null);
        for (var count = 0, i = 4, l = counts.length; i &lt; l; i++) {
            if (counts[i]) {
                count += parseInt(counts[i], 10);
            }
        }
        callback(null, count);
    });

    return this;
}

SocialGraph

lib/advanced_structures/SocialGraph.js

Module dependencies.

var Structure = require('../Structure');

Build a social graph similar to Twitter's. User ID can be a string or integer, as long as they're unique.

Usage

redback.createSocialGraph(id [, prefix]);

Reference

http://redis.io/topics/data-types#sets

Redis Structure

(namespace:)(prefix:)id:following = set(ids) (namespace:)(prefix:)id:followers = set(ids)

var SocialGraph = exports.SocialGraph = Structure.new();

Follow one or more users.

  • param: int | SocialGraph | Array user(s)

  • param: Function callback (optional)

  • return: this

  • api: public

SocialGraph.prototype.follow = function (users, callback) {
    var self = this,
        users = this.getKeys(arguments, 'id'),
        multi = this.client.multi();
    if (typeof users[users.length-1] === 'function') {
        callback = users.pop();
    } else {
        callback = function () {};
    }
    users.forEach(function (user) {
        multi.sadd(self.key_prefix + user + ':followers', self.id);
        multi.sadd(self.following, user);
    });
    multi.exec(callback);
    return this;
}

Unfollow one or more users.

  • param: int | SocialGraph | Array user(s)

  • param: Function callback (optional)

  • return: this

  • api: public

SocialGraph.prototype.unfollow = function (users, callback) {
    var self = this,
        users = this.getKeys(arguments, 'id'),
        multi = this.client.multi();
    if (typeof users[users.length-1] === 'function') {
        callback = users.pop();
    } else {
        callback = function () {};
    }
    users.forEach(function (user) {
        multi.srem(self.key_prefix + user + ':followers', self.id);
        multi.srem(self.following, user);
    });
    multi.exec(callback);
    return this;
}

Gets the users whom the current users follows as an array.

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getFollowing = function (callback) {
    this.client.smembers(this.following, callback);
    return this;
}

Gets an array of users who follow the current user.

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getFollowers = function (callback) {
    this.client.smembers(this.followers, callback);
    return this;
}

Count how many users the current user follows.

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.countFollowing = function (callback) {
    this.client.scard(this.following, callback);
    return this;
}

Count how many users follow the current user.

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.countFollowers = function (callback) {
    this.client.scard(this.followers, callback);
    return this;
}

Checks whether the current user follows the specified user.

  • param: string | SocialGraph user

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.isFollowing = function (user, callback) {
    user = this.getKey(user, 'id');
    this.client.sismember(this.following, user, callback);
    return this;
}

Checks whether the specified user follows the current user.

  • param: string | SocialGraph user

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.hasFollower = function (user, callback) {
    user = this.getKey(user, 'id');
    this.client.sismember(this.followers, user, callback);
    return this;
}

Gets an array of common followers for one or more users.

  • param: string | SocialGraph | Array user(s)

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getCommonFollowers = function (users, callback) {
    var users = this.getSocialKeys(arguments, 'followers');
    users.unshift(this.followers);
    this.client.sinter.apply(this.client, users);
    return this;
}

Gets an array of users who are followed by all of the specified user(s).

  • param: string | SocialGraph | Array user(s)

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getCommonFollowing = function (users, callback) {
    var users = this.getSocialKeys(arguments, 'following');
    users.unshift(this.following);
    this.client.sinter.apply(this.client, users);
    return this;
}

Gets an array of users who follow the current user but do not follow any of the other specified users.

  • param: string | SocialGraph | Array user(s)

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getDifferentFollowers = function (users, callback) {
    var users = this.getSocialKeys(arguments, 'followers');
    users.unshift(this.followers);
    this.client.sdiff.apply(this.client, users);
    return this;
}

Gets an array of users who are followed by the current user but not any of the other specified users.

  • param: string | SocialGraph | Array user(s)

  • param: Function callback

  • return: this

  • api: public

SocialGraph.prototype.getDifferentFollowing = function (users, callback) {
    var users = this.getSocialKeys(arguments, 'following');
    users.unshift(this.following);
    this.client.sdiff.apply(this.client, users);
    return this;
}

Bitfield

lib/base_structures/Bitfield.js

Module dependencies.

var Structure = require('../Structure');

Wrap the Redis bit commands.

Usage

redback.createBitfield(key);

Reference

http://redis.io/commands#string

Redis Structure

(namespace:)key = string

var Bitfield = exports.Bitfield = Structure.new();

Get a single bit

  • param: int bit

  • param: Function callback (optional)

  • return: this

  • api: public

Bitfield.prototype.get = function (bit, callback) {
    callback = callback || function () {};
    this.client.getbit(this.key, bit, callback);
    return this;
}

Set a single bit. The callback receives the previous value.

  • param: int bit

  • param: bool value

  • param: Function callback (optional)

  • return: this

  • api: public

Bitfield.prototype.set = function (bit, value, callback) {
    callback = callback || function () {};
    this.client.setbit(this.key, bit, value ? 1 : 0, callback);
    return this;
}

Hash

lib/base_structures/Hash.js

Module dependencies.

var Structure = require('../Structure');

A wrapper for the Redis hash type.

Usage

redback.createHash(key);

Reference

http://redis.io/topics/data-types#hashes

Redis Structure

(namespace:)key = hash(key => value)

var Hash = exports.Hash = Structure.new();

Get an array of hash keys.

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.keys = function (callback) {
    this.client.hkeys(this.key, callback);
    return this;
}

Get an array of hash values.

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.values = function (callback) {
    this.client.hvals(this.key, callback);
    return this;
}

Get the number of hash keys.

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.length = function (callback) {
    this.client.hlen(this.key, callback);
    return this;
}

Delete a hash key.

  • param: string hash_key

  • param: Function callback (optional)

  • return: this

  • api: public

Hash.prototype.delete = Hash.prototype.del = function (hash_key, callback) {
    callback = callback || function () {};
    this.client.hdel(this.key, hash_key, callback);
    return this;
}

Checks whether a hash key exists.

  • param: string hash_key

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.exists = function (hash_key, callback) {
    this.client.hexists(this.key, hash_key, callback);
    return this;
}

Sets one or more key/value pairs.

To set one key/value pair: hash.set('foo', 'bar', callback);

To set multiple: hash.set({key1:'value1', key2:'value2}, callback);

  • param: string | Object hash_key

  • param: string value (optional)

  • param: Function callback (optional)

  • return: this

  • api: public

Hash.prototype.set = function (hash_key, value, callback) {
    if (typeof hash_key === 'object') {
        callback = value || function () {};
        this.client.hmset(this.key, hash_key, callback);
    } else {
        callback = callback || function () {};
        this.client.hset(this.key, hash_key, value, callback);
    }
    return this;
}

Sets a key/value pair if the key doesn't already exist.

  • param: string hash_key

  • param: string value

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.add = function (hash_key, value, callback) {
    callback = callback || function () {};
    this.client.hsetnx(this.key, hash_key, value, callback);
    return this;
}

Gets one or more key/value pairs.

To get all key/value pairs in the hash: hash.get('foo', callback);

To get certain key/value pairs: hash.get(['foo','bar'], callback); hash.get('foo', callback);

  • param: string hash_key (optional)

  • param: Function callback

  • return: this

  • api: public

Hash.prototype.get = function (hash_key, callback) {
    if (typeof hash_key === 'function') {
        callback = hash_key;
        this.client.hgetall(this.key, callback);
    } else if (Array.isArray(hash_key)) {
        this.client.hmget(this.key, hash_key, callback)
    } else {
        this.client.hget(this.key, hash_key, callback);
    }
    return this;
}

Increment the specified hash value.

  • param: string hash_key

  • param: int amount (optional - default is 1)

  • param: Function callback (optional)

  • return: this

  • api: public

Hash.prototype.increment =
Hash.prototype.incrBy = function (hash_key, amount, callback) {
    callback = callback || function () {};
    if (typeof amount === 'function') {
        callback = amount;
        amount = 1;
    }
    this.client.hincrby(this.key, hash_key, amount, callback);
    return this;
}

Decrement the specified hash value.

  • param: string hash_key

  • param: int amount (optional - default is 1)

  • param: Function callback (optional)

  • return: this

  • api: public

Hash.prototype.decrement =
Hash.prototype.decrBy = function (hash_key, amount, callback) {
    callback = callback || function () {};
    if (typeof amount === 'function') {
        callback = amount;
        amount = 1;
    }
    this.client.hincrby(this.key, hash_key, -1 * amount, callback);
    return this;
}

List

lib/base_structures/List.js

Module dependencies.

var Structure = require('../Structure');

A wrapper for the Redis list type.

Usage

redback.createList(key);

Reference

http://redis.io/topics/data-types#lists

Redis Structure

(namespace:)key = list(values)

var List = exports.List = Structure.new();

Get the list as an array.

  • param: Function callback

  • return: this

  • api: public

List.prototype.values = function (callback) {
    this.client.lrange(this.key, 0, -1, callback);
    return this;
}

Get a range of list elements.

  • param: int start

  • param: count end (optional - defaults to the last element)

  • param: Function callback

  • return: this

  • api: public

List.prototype.range = function (start, end, callback) {
    if (typeof end === 'function') {
        callback = end;
        end = -1;
    }
    this.client.lrange(this.key, start, end, callback);
    return this;
}

Get one or more elements starting at the specified index.

  • param: int index

  • param: count count (optional - default is 1)

  • param: Function callback

  • return: this

  • api: public

List.prototype.get = function (index, count, callback) {
    if (typeof count === 'function') {
        callback = count;
        this.client.lindex(this.key, index, callback);
    } else {
        this.client.lrange(this.key, index, index + count - 1, callback);
    }
    return this;
}

Cap the length of the list.

  • param: int length

  • param: bool keep_earliest (optional - default is false)

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.cap = function (length, keep_earliest, callback) {
    callback = callback || function () {};
    var start = 0, end = -1;
    if (typeof keep_earliest === 'function') {
        //Keep the last `length` elements
        start = -1 * length;
        callback = keep_earliest;
    } else {
        //Keep the first `length` elements
        end = length - 1;
    }
    this.client.ltrim(this.key, start, end, callback);
    return this;
}

Remove one or more list elements matching the value.

  • param: string value

  • param: bool count (optional - default is 1)

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.remove = function (value, count, callback) {
    callback = callback || function () {};
    if (typeof count === 'function') {
        callback = count;
        count = 1;
    }
    this.client.lrem(this.key, count, value, callback);
    return this;
}

Trim a list to the specified bounds.

  • param: int start

  • param: int end

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.trim = function (start, end, callback) {
    callback = callback || function () {};
    this.client.ltrim(this.key, start, end, callback);
    return this;
}

Insert an element before the specified pivot.

  • param: int pivot

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.insertBefore = function (pivot, value, callback) {
    callback = callback || function () {};
    this.client.linsert(this.key, 'BEFORE', pivot, value, callback);
    return this;
}

Insert an element after the specified pivot.

  • param: int pivot

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.insertAfter = function (pivot, value, callback) {
    callback = callback || function () {};
    this.client.linsert(this.key, 'AFTER', pivot, value, callback);
    return this;
}

Set the element at the specified index.

  • param: int index

  • param: string value

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.set = function (index, value, callback) {
    callback = callback || function () {};
    this.client.lset(this.key, index, value, callback);
    return this;
}

Get the number of elements in the list.

  • param: Function callback

  • return: this

  • api: public

List.prototype.length = function (callback) {
    this.client.llen(this.key, callback);
    return this;
}

Get and remove the last element in the list. The first param can be used to block the process and wait until list elements are available. If the list is empty in both examples below, the first example will return null, while the second will wait for up to 3 seconds. If a list element becomes available during the 3 seconds it will be returned, otherwise null will be returned.

Example

list.shift(callback);

Blocking Example

list.shift(3, callback)

  • param: int wait (optional) - seconds to block

  • param: Function callback

  • return: this

  • api: public

List.prototype.shift = function (wait, callback) {
    if (typeof wait === 'function') {
        callback = wait;
        this.client.lpop(this.key, callback);
    } else {
        this.client.blpop(this.key, wait, callback);
    }
    return this;
}

Get and remove the last element in the list. The first param can be used to block the process and wait until list elements are available. If the list is empty in both examples below, the first example will return null, while the second will wait for up to 3 seconds. If a list element becomes available during the 3 seconds it will be returned, otherwise null will be returned.

Example

list.pop(callback);

Blocking Example

list.pop(3, callback)

  • param: int wait (optional) - seconds to block

  • param: Function callback

  • return: this

  • api: public

List.prototype.pop = function (wait, callback) {
    if (typeof wait === 'function') {
        callback = wait;
        this.client.rpop(this.key, callback);
    } else {
        this.client.brpop(this.key, wait, callback);
    }
    return this;
}

Add one or more elements to the start of the list.

  • param: string | array value(s)

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.unshift = List.prototype.lpush = function (values, callback) {
    callback = callback || function () {};
    if (Array.isArray(values)) {
        var multi = this.client.multi(), key = this.key;
        values.reverse().forEach(function (value) {
            multi.lpush(key, value);
        });
        multi.exec(callback);
    } else {
        this.client.lpush(this.key, values, callback);
    }
    return this;
}

Add one or more elements to the end of the list.

  • param: string | array value(s)

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.push = List.prototype.add = function (values, callback) {
    callback = callback || function () {};
    if (Array.isArray(values)) {
        var multi = this.client.multi(), key = this.key;
        values.forEach(function (value) {
            multi.rpush(key, value);
        });
        multi.exec(callback);
    } else {
        this.client.rpush(this.key, values, callback);
    }
    return this;
}

Remove the last element of the list and add it to the start of another list.

  • param: String | List list

  • param: bool wait (optional) - seconds to block while waiting

  • param: Function callback (optional)

  • return: this

  • api: public

List.prototype.popShift = function (list, wait, callback) {
    callback = callback || function () {};
    list = this.getKey(list);
    if (typeof wait === 'function') {
        callback = wait;
        this.client.rpoplpush(this.key, list, callback);
    } else {
        this.client.brpoplpush(this.key, list, wait, callback);
    }
    return this;
}

Set

lib/base_structures/Set.js

Module dependencies.

var Structure = require('../Structure');

A wrapper for the Redis set type.

Usage

redback.createSet(key);

Reference

http://redis.io/topics/data-types#sets

Redis Structure

(namespace:)key = set(elements)

var Set = exports.Set = Structure.new();

Add one or more elements to the set.

  • param: string | Array element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

Set.prototype.add = function (element, callback) {
    callback = callback || function () {};
    if (Array.isArray(element)) {
        return this.addAll(element, callback);
    }
    this.client.sadd(this.key, element, callback);
    return this;
}

Remove one or more elements from the set.

  • param: string | Array element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

Set.prototype.remove = function (element, callback) {
    callback = callback || function () {};
    if (Array.isArray(element)) {
        return this.removeAll(element, callback);
    }
    this.client.srem(this.key, element, callback);
    return this;
}

Get an array of elements in the set.

  • param: Function callback

  • return: this

  • api: public

Set.prototype.elements = Set.prototype.members = function (callback) {
    this.client.smembers(this.key, callback);
    return this;
}

Move an element to another set.

  • param: string | Set dest

  • param: string element

  • param: Function callback (optional)

  • return: this

  • api: public

Set.prototype.move = function (dest, element, callback) {
    callback = callback || function () {};
    this.client.smove(this.key, this.getKey(dest), element, callback);
    return this;
}

Check whether an element exists in the set.

  • param: string element

  • param: Function callback

  • return: this

  • api: public

Set.prototype.exists = Set.prototype.contains = function (element, callback) {
    this.client.sismember(this.key, element, callback);
    return this;
}

Get the length (cardinality) of the set.

  • param: Function callback

  • return: this

  • api: public

Set.prototype.length = Set.prototype.cardinality = function (callback) {
    this.client.scard(this.key, callback);
    return this;
}

Get a random element from the set and optionally remove it.

  • param: bool remove (optional - default is false)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.random = function (remove, callback) {
    if (typeof remove === 'function') {
        callback = remove;
        this.client.srandmember(this.key, callback);
    } else {
        this.client.spop(this.key, callback);
    }
    return this;
}

Get the intersection of one or more sets.

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.inter = function (sets, callback) {
    sets = this.getKeys(arguments);
    sets.unshift(this.key);
    this.client.sinter.apply(this.client, sets);
    return this;
}

Get the intersection of one or more sets and store it another set (dest).

  • param: string | Set dest

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.interStore = function (dest, sets, callback) {
    sets = this.getKeys(arguments);
    dest = sets.shift();
    sets.unshift(dest, this.key);
    this.client.sinterstore.apply(this.client, sets);
    return this;
}

Get the union of one or more sets.

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.union = function (sets, callback) {
    sets = this.getKeys(arguments);
    sets.unshift(this.key);
    this.client.sunion.apply(this.client, sets);
    return this;
}

Get the union of one or more sets and store it another set (dest).

  • param: string | Set dest

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.unionStore = function (dest, sets, callback) {
    sets = this.getKeys(arguments);
    dest = sets.shift();
    sets.unshift(dest, this.key);
    this.client.sunionstore.apply(this.client, sets);
    return this;
}

Get the difference of one or more sets.

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.diff = function (sets, callback) {
    sets = this.getKeys(arguments);
    sets.unshift(this.key);
    this.client.sdiff.apply(this.client, sets);
    return this;
}

Get the difference of one or more sets and store it another set (dest).

  • param: string | Set dest

  • param: string | Set | Array set(s)

  • param: Function callback

  • return: this

  • api: public

Set.prototype.diffStore = function (dest, sets, callback) {
    sets = this.getKeys(arguments);
    dest = sets.shift();
    sets.unshift(dest, this.key);
    this.client.sdiffstore.apply(this.client, sets);
    return this;
}

SortedSet

lib/base_structures/SortedSet.js

Module dependencies.

var Structure = require('../Structure');

A wrapper for the Redis sorted set (zset) type. Each element has a score which is used to rank and order all elements in the set. Elements are ranked from lowest score to highest (the lowest score has a rank of 0)

Usage

redback.createSortedSet(key);

Reference

http://redis.io/topics/data-types#sorted-sets

Redis Structure

(namespace:)key = zset(score => element)

var SortedSet = exports.SortedSet = Structure.new();

Add one or more elements to the set.

To add a single element and score: set.add(12, 'foo', callback);

To add multiple elements/scores: set.add({foo:12, bar:3}, callback);

  • param: int score (optional)

  • param: string | Object element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

SortedSet.prototype.add = function (score, element, callback) {
    callback = callback || function () {};
    if (typeof score === 'object') {
        callback = element;
        element = score;
        return this.addAll(element, callback);
    }
    this.client.zadd(this.key, score, element, callback);
    return this;
}

Remove one or more elements from the set.

  • param: string | Array element(s)

  • param: Function callback (optional)

  • return: this

  • api: public

SortedSet.prototype.remove = function (element, callback) {
    callback = callback || function () {};
    if (Array.isArray(element)) {
        return this.removeAll(element, callback);
    }
    this.client.zrem(this.key, element, callback);
    return this;
}

Get the number of elements in the set.

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.length = function (callback) {
    this.client.zcard(this.key, callback);
    return this;
}

Check whether an element exists in the set.

  • param: string element

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.exists =
SortedSet.prototype.contains = function (element, callback) {
    this.client.zscore(this.key, element, function (err, score) {
        callback(err, score != null);
    });
    return this;
}

Get the rank of the specified element.

  • param: string element

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.rank = function (element, callback) {
    this.client.zrank(this.key, element, callback)
    return this;
}

Get the score of the specified element.

  • param: string element

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.score = function (element, callback) {
    this.client.zscore(this.key, element, callback)
    return this;
}

Increment the specified element's score.

  • param: string element

  • param: int amount (optional - default is 1)

  • param: Function callback (optional)

  • return: this ;

  • api: public

SortedSet.prototype.increment =
SortedSet.prototype.incrBy = function (element, amount, callback) {
    callback = callback || function () {};
    if (typeof amount === 'function') {
        callback = amount;
        amount = 1;
    }
    this.client.zincrby(this.key, amount, element, callback);
    return this;
}

Decrement the specified element's score.

  • param: string element

  • param: int amount (optional - default is 1)

  • param: Function callback (optional)

  • return: this ;

  • api: public

SortedSet.prototype.decrement =
SortedSet.prototype.decrBy = function (element, amount, callback) {
    callback = callback || function () {};
    if (typeof amount === 'function') {
        callback = amount;
        amount = 1;
    }
    this.client.zincrby(this.key, -1 * amount, element, callback);
    return this;
}

Get all elements in the set as an object {element: score, ...}. If without_scores is true then just an array of elements is returned.

  • param: bool without_scores (optional - scores are included by default)

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.get = function (without_scores, callback) {
    if (typeof without_scores === 'function') {
        callback = without_scores;
        this.client.zrange(this.key, 0, -1, 'WITHSCORES', this.parseScores(callback));
    } else {
        this.client.zrange(this.key, 0, -1, callback);
    }
    return this;
}

Get elements with scores between the specified range. Elements are returned as an object {element: score, ..} and ordered from highest score to lowest.

Note that the start and end range is inclusive and can be an integer or the constants redback.INF to represent infinity, or redback.NINF to represent negative infinity. start must be <= end.

  • param: int start

  • param: int end

  • param: int count (optional) - the maximum number of elements to return

  • param: int offset (optional) - if using count, start at this offset

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.getScores = function (start, end, count, offset, callback) {
    if (null === start) start = '-inf';
    if (null === end) end = '+inf';
    if (typeof count === 'function') {
        callback = count;
        this.client.zrangebyscore(this.key, start, end,
            'WITHSCORES', this.parseScores(callback));
        return this;
    } else if (typeof offset === 'function') {
        callback = offset;
        offset = 0;
    }
    this.client.zrangebyscore(this.key, start, end, 'WITHSCORES',
        'LIMIT', offset, count, this.parseScores(callback));
    return this;
}

The same as getScores() but elements are ordered from lowest score to highest.

Note that end must be <= start.

  • param: int start

  • param: int end

  • param: int count (optional) - the maximum number of elements to return

  • param: int offset (optional) - if using count, start at this offset

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.getScoresReverse = function (start, end, count, offset, callback) {
    if (null === start) start = '+inf';
    if (null === end) end = '-inf';
    if (typeof count === 'function') {
        callback = count;
        this.client.zrevrangebyscore(this.key, start, end,
            'WITHSCORES', this.parseScores(callback));
        return this;
    } else if (typeof offset === 'function') {
        callback = offset;
        offset = 0;
    }
    this.client.zrevrangebyscore(this.key, start, end, 'WITHSCORES',
        'LIMIT', offset, count, this.parseScores(callback));
    return this;
}

Remove elements with scores between the specified range (inclusive).

  • param: int start

  • param: int end

  • param: Function callback (optional)

  • return: this

  • api: public

SortedSet.prototype.removeScores = function (start, end, callback) {
    callback = callback || function () {};
    if (null === start) start = '-inf';
    if (null === end) end = '+inf';
    this.client.zremrangebyscore(this.key, start, end, callback);
    return this;
}

Count the number of elements with scores between the specified range (inclusive).

  • param: int start

  • param: int end

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.countScores = function (start, end, callback) {
    if (null === start) start = '-inf';
    if (null === end) end = '+inf';
    this.client.zcount(this.key, start, end, callback);
    return this;
}

Get elements with ranks between the specified range (inclusive).

To get the first 3 elements in the set (with the highest scores): set.getRanks(0, 2, callback);

To get the last 3 elements in the set (lowest scores): set.getRanks(-3, -1, callback);

  • param: int start

  • param: int end

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.getRanks = function (start, end, callback) {
    if (null === start) start = 0;
    if (null === end) end = -1;
    this.client.zrange(this.key, start, end,
        'WITHSCORES', this.parseScores(callback));
    return this;
}

The same as getRanks() but elements are ordered from lowest score to the highest.

Note that start and end have been deliberately switched for consistency.

getScoresReverse(arg1, arg2, ..) expects arg1 >= arg2 and so does this method.

  • param: int end

  • param: int start

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.getRanksReverse = function (end, start, callback) {
    if (null === start) start = -1;
    if (null === end) end = 0;
    this.client.zrevrange(this.key, start, end,
        'WITHSCORES', this.parseScores(callback));
    return this;
}

Remove elements with ranks between the specified range (inclusive).

  • param: int start

  • param: int end

  • param: Function callback (optional)

  • return: this

  • api: public

SortedSet.prototype.removeRanks = function (start, end, callback) {
    callback = callback || function () {};
    if (null === start) start = -1;
    if (null === end) end = 0;
    this.client.zremrangebyrank(this.key, start, end, callback);
    return this;
}

Get count elements with the highest scores.

  • param: int count

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.highestScores = function (count, callback) {
    this.getRanks(-1 * count, -1, callback);
    return this;
}

Get count elements with the lowest scores.

  • param: int count

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.lowestScores = function (count, callback) {
    this.getRanks(0, count - 1, callback);
    return this;
}

Get the intersection of one or more sets. For more information on weights, aggregate functions, etc. see: http://redis.io/commands/zinterstore

  • param: int dest

  • param: string | Set | Array set(s)

  • param: int | Array weights (optional)

  • param: string aggregate (optional) - either SUM, MIN or MAX

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.inter = function (dest, sets, weights, aggregate, callback) {
    var args = [], self = this;
    args.push(this.getKey(dest));

    //weights/aggregate are optional
    if (typeof weights === 'function') {
        callback = weights;
        weights = aggregate = false;
    } else if (typeof aggregate === 'function') {
        callback = aggregate;
        aggregate = false;
    }

    //ZINTERSTORE destination numkeys key [key ...]
    //    [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
    if (Array.isArray(sets)) {
        args.push(sets.length);
        sets.forEach(function (set) {
            args.push(self.getKey(set));
        });
    } else {
        args.push(1, this.getKey(sets));
    }
    if (weights) {
        args.push('WEIGHTS');
        if (Array.isArray(weights)) {
            weights.forEach(function (weight) {
                args.push(weight);
            });
        } else {
            args.push(weights);
        }
    }
    if (aggregate) {
        args.push('AGGREGATE', aggregate);
    }
    args.push(callback);
    this.client.zinterstore.apply(this.client, args);
    return this;
}

Get the union of one or more sets. For more information on weights, aggregate functions, etc. see: http://redis.io/commands/zunionstore

  • param: int dest

  • param: string | Set | Array set(s)

  • param: int | Array weights (optional)

  • param: string aggregate (optional) - either SUM, MIN or MAX

  • param: Function callback

  • return: this

  • api: public

SortedSet.prototype.union = function (dest, sets, weights, aggregate, callback) {
    var args = [], self = this;
    args.push(this.getKey(dest));

    //weights/aggregate are optional
    if (typeof weights === 'function') {
        callback = weights;
        weights = aggregate = false;
    } else if (typeof aggregate === 'function') {
        callback = aggregate;
        aggregate = false;
    }

    //ZUNIONSTORE destination numkeys key [key ...]
    //    [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
    if (Array.isArray(sets)) {
        args.push(sets.length);
        sets.forEach(function (set) {
            args.push(self.getKey(set));
        });
    } else {
        args.push(1, this.getKey(sets));
    }
    if (weights) {
        args.push('WEIGHTS');
        if (Array.isArray(weights)) {
            weights.forEach(function (weight) {
                args.push(weight);
            });
        } else {
            args.push(weights);
        }
    }
    if (aggregate) {
        args.push('AGGREGATE', aggregate);
    }
    args.push(callback);
    this.client.zunionstore.apply(this.client, args);
    return this;
}
================================================ FILE: docs/index.html ================================================ Redback - Redis-backed persistence for Node.JS Fork me on GitHub

What is it?

A fast, high-level Redis library for Node.JS that exposes an accessible and extensible interface to the Redis data types.

Redback comes bundled with advanced Redis structures such as the Social Graph or Rate Limiter. You can also build your own Redis-backed structures with ease.

How do I get it?

npm install redback

Learn more

See the GitHub repository for more information. For an API, see the annotated source.

================================================ FILE: index.js ================================================ module.exports = require('./lib/Redback'); ================================================ FILE: lib/Cache.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Use Redis as a cache backend. * * Usage: * `redback.createCache(namespace);` * * Reference: * http://redis.io/commands#string * * Redis Structure: * `(namespace:)cache_namespace:key = string` */ var Cache = exports.Cache = function (client, namespace) { this.client = client; this.namespace = namespace; } /** * Add the namespace on to cache keys. * * @param {string} key * @return namespaced_key; * @api private */ Cache.prototype.getKey = function (key) { return this.namespace + ':' + key; } /** * Cache one or more values. * * To cache a single value by key: * `cache.set('foo', 'bar', callback);` * * To set multiple cached values by key: * `cache.set({foo:'bar', key2:'value2'}, callback);` * * @param {string} key * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.set = function (key, value, expiry, callback) { callback = callback || function () {}; if (typeof expiry === 'function') { callback = expiry; this.client.set(this.getKey(key), value, callback); } else if (typeof value === 'function') { callback = value; var i, set = []; for (i in key) { set.push(this.getKey(i)); set.push(key[i]); } set.push(callback); this.client.mset.apply(this.client, set); } else { this.client.setex(this.getKey(key), expiry, value, callback); } return this; } /** * Add one or more values to the cache, but only if the cache * key(s) do not already exist. * * @param {string|Object} key * @param {string} value (optional) * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.add = function (key, value, callback) { callback = callback || function () {}; if (typeof value === 'function') { callback = value; var i, set = []; for (i in key) { set.push(this.getKey(i)); set.push(key[i]); } set.push(callback); this.client.msetnx.apply(this.client, set); } else { this.client.setnx(this.getKey(key), value, callback); } return this; } /** * Get one or more values from the cache. * * To get a single cached value by key: * `cache.get('foo', callback);` * * To get multiple cached values by key: * `cache.get(['foo','bar'], callback);` * * To get all cached values: * `cache.get(callback);` * * @param {string} key * @param {string} value * @param {Function} callback * @return this * @api public */ Cache.prototype.get = function (key, callback) { var namespace_len = this.namespace.length + 1, self = this; var multi_get = function (keys) { var get_args = []; keys.forEach(function (key) { get_args.push(key); }) get_args.push(function (err, values) { if (err) return callback(err, null); var i, l, ret = {}; for (i = 0, l = keys.length; i < l; i++) { ret[keys[i].substr(namespace_len)] = values[i]; } callback(null, ret); }); self.client.mget.apply(self.client, get_args); } if (typeof key === 'function') { callback = key; this.keys('*', true, function (err, keys) { if (err) return callback(err, null); multi_get(keys); }); } else if (Array.isArray(key)) { if (!key.length) return callback(null, null); for (var get = [], i = 0, l = key.length; i < l; i++) { key[i] = this.getKey(key[i]); } multi_get(key); } else { this.client.get(this.getKey(key), callback); } return this; } /** * Set a cache key and return the current value. * * @param {string} key * @param {string} value * @param {Function} callback * @return this * @api public */ Cache.prototype.getSet = function (key, value, callback) { this.client.getset(this.getKey(key), value, callback); return this; } /** * Check whether a cache key exists. * * @param {string} key * @param {Function} callback * @return this * @api public */ Cache.prototype.exists = function (key, callback) { this.client.exists(this.getKey(key), callback); return this; } /** * Increment the specified cache value. * * @param {string} key * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.increment = Cache.prototype.incrBy = function (key, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.incrby(this.getKey(key), amount, callback); return this; } /** * Decrement the specified cache value. * * @param {string} key * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.decrement = Cache.prototype.decrBy = function (key, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.decrby(this.getKey(key), amount, callback); return this; } /** * Get all cache keys matching the pattern. * * @param {string} pattern (optional - default is *) * @param {bool} keep_namespace (optional - default is false) * @param {Function} callback * @return this * @api public */ Cache.prototype.keys = function (pattern, keep_namespace, callback) { if (typeof pattern === 'function') { keep_namespace = false; callback = pattern; pattern = '*'; } else if (typeof keep_namespace === 'function') { callback = keep_namespace; keep_namespace = false; } var self = this; if (keep_namespace) { this.client.keys(this.namespace + ':' + pattern, function (err, keys) { if (err) return callback(err, null); if (!keys) return callback(null, []); callback(null, keys); }); } else { var namespace_len = this.namespace.length + 1; this.client.keys(this.namespace + ':' + pattern, function (err, keys) { if (err) return callback(err, null); if (!keys) return callback(null, []); if (null == keys) return callback(null, []); for (var i = 0, l = keys.length; i < l; i++) { keys[i] = keys[i].substr(namespace_len); } callback(null, keys); }); } } /** * Flush all cache keys matching the pattern. * * @param {string} pattern (optional - default is *) * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.flush = function (pattern, callback) { callback = callback || function () {}; if (typeof pattern === 'function') { callback = pattern; pattern = '*'; } var self = this; this.keys(pattern, true, function (err, keys) { if (err) return callback(err, null); if (!keys || keys.length === 0) return callback(err, []); var error = false, remaining = keys.length, del_count = 0; keys.forEach(function (key) { self.client.del(key, function (err, deleted) { if (error) { return; } else if (err) { error = true; return callback(err, null); } del_count++; if (!--remaining) callback(err, del_count); }); }); }); } /** * Expire the cache key after a certain number of seconds. * * @param {int} ttl * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.expire = function (key, ttl, callback) { callback = callback || function () {}; this.client.expire(this.getKey(key), ttl, callback); return this; } /** * Expire the cache key at a certain date. * * @param {string} key * @param {Date} when * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.expireAt = function (key, when, callback) { callback = callback || function () {}; if (typeof when.getTime === 'function') { when = Math.round(when.getTime() / 1000); //ms => s } this.client.expireat(this.getKey(key), when, callback); return this; } /** * Get the number of seconds before the cache key expires. * * @param {string} key * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.ttl = function (key, callback) { callback = callback || function () {}; this.client.ttl(this.getKey(key), callback); return this; } /** * Checks whether a cache key has an expiry. * * @param {string} key * @param {Function} callback * @return this * @api public */ Cache.prototype.isVolatile = function (key, callback) { this.client.ttl(this.getKey(key), function (err, ttl) { if (err) return callback(err, null); callback(null, ttl != -1); }); return this; } /** * Remove the cache key's associated expiry. * * @param {string} key * @param {Function} callback (optional) * @return this * @api public */ Cache.prototype.persist = function (key, callback) { callback = callback || function () {}; this.client.persist(this.getKey(key), callback); return this; } ================================================ FILE: lib/Channel.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var EventEmitter = require('events').EventEmitter; /** * Wrap the Redis pub/sub commands. * * Usage: * `redback.createChannel(name);` * * Reference: * http://redis.io/topics/pubsub */ var Channel = exports.Channel = function (client, channel_name) { this.name = channel_name; this.setClient(client); } /** * Channel is an event emitter. */ Channel.prototype = new EventEmitter(); /** * Bind a new Redis client (e.g. if not exclusively using pub/sub mode). * * @param {Object} client * @return this * @api public */ Channel.prototype.setClient = function (client) { this.client = client; var self = this; ['message','subscribe','unsubscribe'].forEach(function (event) { self.client.on(event, function (channel, arg) { if (channel == self.name) { self.emit(event, arg); } }); }); return this; } /** * Publish a message to the channel. * * @param {string} msg * @param {Function} callback * @return this * @api public */ Channel.prototype.publish = function (msg, callback) { this.client.publish(this.name, msg, callback); return this; } /** * Subscribe to the channel. * * @param {Function} callback * @return this * @api public */ Channel.prototype.subscribe = function (callback) { this.client.subscribe(this.name); if (typeof callback === 'function') { this.on('subscribe', callback); } return this; } /** * Unsubscribe from the channel. * * @param {Function} callback * @return this * @api public */ Channel.prototype.unsubscribe = function (callback) { this.client.unsubscribe(this.name); if (typeof callback === 'function') { this.on('unsubscribe', callback); } return this; } ================================================ FILE: lib/Redback.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var redis = require('redis'), Structure = require('./Structure'), Channel = require('./Channel').Channel, Cache = require('./Cache').Cache; /** * Define the available Redis structures. */ var base = ['Hash','List','Set','SortedSet','Bitfield']; /** * Define the available advanced structures. */ var advanced = ['KeyPair','DensitySet','CappedList','SocialGraph', 'Queue', 'RateLimit', 'BloomFilter', 'Lock']; /** * The Redback object wraps the Redis client and acts as a factory * for structures. * * @param {RedisClient} client * @param {Object} options (optional) * or * @param {int} port (optional) * @param {string} host (optional) * @param {Object} options (optional) * @api public */ var Redback = exports.Redback = function (client, options) { if (typeof client === 'object' && client) { this.client = client; } else { this.client = redis.createClient.apply(this, arguments); options = arguments[1] instanceof Object ? arguments[1] : arguments[2]; } this.namespace = ''; if (typeof options === 'object' && options.namespace) { this.namespace = options.namespace; } } /** * Make a structure available to the client. * * @param {string} name * @param {Function|Object} Structure * @api private */ Redback.prototype.addStructure = function (name, obj) { if (typeof obj !== 'function') { obj = Structure.new(obj); } exports[name] = obj; Redback.prototype['create' + name] = function (key) { var init_args = Array.prototype.slice.call(arguments, 1); return new obj(this.client, key, this.namespace, init_args); } } /** * Create a new Cache. * * @param {string} namespace (optional) * @return Cache * @api public */ Redback.prototype.createCache = function (namespace) { namespace = namespace || 'cache'; if (this.namespace.length) { namespace = this.namespace + ':' + namespace; } return new Cache(this.client, namespace); } /** * Create a new Channel. * * @param {string} channel - the channel name * @return Channel * @api public */ Redback.prototype.createChannel = function (channel) { if (!channel) { throw new Error('A channel key is required'); } if (this.namespace.length) { channel = this.namespace + ':' + channel; } return new Channel(this.client, channel); } /** * Send a (BG)SAVE command to Redis. * * @param {string} background (optional - default is false) * @param {Function} callback * @return this * @api public */ Redback.prototype.save = function (background, callback) { if (typeof background === 'function') { callback = background; this.client.save(callback); } else { this.client.bgsave(callback); } return this; } /** * Close the connection to Redis. * * @return this * @api public */ Redback.prototype.quit = function () { this.client.quit(); return this; } /** * Create a new Redback client. * * @param {int} port (optional) * @param {string} host (optional) * @param {Object} options (optional) * @api public */ exports.createClient = function (port, host, options) { return new Redback(port, host, options); } /** * Wrap a Redis client with Redback. * * @param {RedisClient} client * @param {Object} options (optional) * @api public */ exports.use = function (client, options) { return new Redback(client, options); } /** * Add the Redis structures from ./base_structures */ base.forEach(function (structure) { Redback.prototype.addStructure(structure, require('./base_structures/' + structure)[structure]); }); /** * Add the advanced structures from ./advanced_structures */ advanced.forEach(function (structure) { Redback.prototype.addStructure(structure, require('./advanced_structures/' + structure)[structure]); }); /** * Redis constants. */ Redback.prototype.INF = exports.INF = '+inf'; Redback.prototype.NINF = exports.NINF = '-inf'; /** * Export prototypes so that they can be extended. */ exports.Client = redis.RedisClient; exports.Structure = Structure.Structure; exports.Cache = Cache; exports.Channel = Channel; ================================================ FILE: lib/Structure.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * All Redback structures inherit from this. */ var Structure = exports.Structure = function () {}; /** * Create a new Structure by extending the base Structure. * * @param {Object} methods (optional) * @return structure * @api public */ exports.new = function (methods) { return Structure.prototype.extend(methods); } /** * Expire the structure after a certain number of seconds. * * @param {int} ttl * @param {Function} callback (optional) * @return this * @api public */ Structure.prototype.expire = function (ttl, callback) { callback = callback || function () {}; this.client.expire(this.key, ttl, callback); return this; } /** * Expire the structure at a certain date. * * @param {Date} when * @param {Function} callback (optional) * @return this * @api public */ Structure.prototype.expireAt = function (when, callback) { callback = callback || function () {}; if (typeof when.getTime === 'function') { when = Math.round(when.getTime() / 1000); //ms => s } this.client.expireat(this.key, when, callback); return this; } /** * Get the number of seconds before the structure expires. * * @param {Function} callback * @return this * @api public */ Structure.prototype.ttl = function (callback) { this.client.ttl(this.key, callback); return this; } /** * Checks whether the structure has an expiry. * * @param {Function} callback * @return this * @api public */ Structure.prototype.isVolatile = function (callback) { this.client.ttl(this.key, function (err, ttl) { if (err) return callback(err, null); callback(null, ttl != -1); }); return this; } /** * Remove the structure's associated expiry. * * @param {Function} callback (optional) * @return this * @api public */ Structure.prototype.persist = function (callback) { callback = callback || function () {}; this.client.persist(this.key, callback); return this; } /** * Remove the structure from the Redis database. * * @param {Function} callback (optional) * @return this * @api public */ Structure.prototype.destroy = Structure.prototype.flush = function (callback) { callback = callback || function () {}; this.client.del(this.key, callback); return this; } /** * A helper for creating atomically auto-incrementing keys. * * @param {Function} callback * @return this * @api public */ Structure.prototype.autoincrement = function (callback) { var key = this.key + ':_autoinc', multi = this.client.multi(); multi.setnx(key, 1).get(key).incr(key); multi.exec(function (err, replies) { if (err) return callback(err, null); callback(null, replies[1]); }); return this; } /** * Takes a redback structure or key string and returns the key. * * @param {string|Object} key * @return {string} key * @api public */ Structure.prototype.getKey = function (key, which) { which = which || 'key'; if (typeof key[which] !== 'undefined') { return key[which]; } return key; } /** * A helper that extracts the Redis keys from many Structure or string arguments. * * @param {Array} structures * @param {Function} callback * @return this * @api public */ Structure.prototype.getKeys = function (structures, which) { var structures = Array.prototype.slice.call(structures), callback = structures.pop(), self = this, keys = []; for (var i = 0, l = structures.length; i < l; i++) { if (Array.isArray(structures[i])) { structures[i].forEach(function (structure) { keys.push(self.getKey(structure, which)); }); } else { keys.push(this.getKey(structures[i], which)); } } keys.push(callback); return keys; } /** * Add the namespace on to a key. * * @param {string} key * @return {string} namespaced_key * @api public */ Structure.prototype.namespaceKey = function (key) { key = key || ''; if (this.namespace.length) { key = this.namespace + ':' + key; } return key; } /** * Extend the structure. * * @param {Object} methods (optional) * @return this * @api public */ Structure.prototype.extend = function (methods) { var structure = function (client, key, namespace, init_args) { this.client = client; this.namespace = namespace || ''; if (!key) { throw new Error('A key is required'); } if (Array.isArray(key)) { key = key.join(':'); } this.id = key; if (this.namespace.length) { key = this.namespace + ':' + key; } this.key = key; if (typeof this.init === 'function') { this.init.apply(this, init_args); } }, ctor = function () { this.constructor = structure; } ctor.prototype = this; structure.prototype = new ctor; structure.__super__ = this; if (typeof methods === 'object') { for (var i in methods) { structure.prototype[i] = methods[i]; } } return structure; } /** * Create a random key for temporary use. * * @return {string} random_key * @api public */ Structure.prototype.randomKey = function () { return Math.random(); } /** * Get the type of the current structure. * * @param {Function} callback * @return this * @api public */ Structure.prototype.type = function (callback) { this.client.type(this.key, callback); return this; } /** * Rename the structure (change the Redis key). * * @param {string} new_key * @param {Function} callback * @return this * @api public */ Structure.prototype.rename = function (new_key, callback) { var self = this; new_key = this.namespaceKey(new_key); this.client.rename(this.key, new_key, function (err) { if (err) return callback(err, null); self.key = new_key; callback(); }); return this; } /** * Sort all elements in the structure. * * Options: * limit, offset, by, get, alpha, desc, store * * Reference: * http://redis.io/commands/sort * * @param {object} options * @param {Function} callback * @return this * @api public */ Structure.prototype.sort = function (options, callback) { var args = [this.key]; //SORT key [BY pattern] [LIMIT offset count] // [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] if (typeof options.by !== 'undefined') { args.push('BY', options.by); } if (typeof options.limit !== 'undefined') { args.push('LIMIT'); if (typeof options.offset !== 'undefined') { args.push(options.offset); } args.push(options.limit); } if (typeof options.get !== 'undefined') { if (Array.isArray(options.get)) { options.get.forEach(function (pattern) { args.push('GET', pattern); }); } else { args.push('GET', options.get); } } if (typeof options.desc !== 'undefined' && options.desc) { args.push('DESC'); } if (typeof options.alpha !== 'undefined' && options.alpha) { args.push('ALPHA'); } if (typeof options.store !== 'undefined') { args.push('STORE', options.store); } this.client.sort.apply(this.client, args); return this; } ================================================ FILE: lib/Utils.js ================================================ /* =============================================================================== Crc32 is a JavaScript function for computing the CRC32 of a string ............................................................................... Version: 1.2 - 2006/11 - http://noteslog.com/category/javascript/ ------------------------------------------------------------------------------- Copyright (c) 2006 Andrea Ercolino http://www.opensource.org/licenses/mit-license.php =============================================================================== */ exports.crc32 = function(str, crc) { if(crc == undefined) crc = 0; var n = 0; //a number between 0 and 255 var x = 0; //an hex number crc = crc ^ (-1); for( var i = 0, iTop = str.length; i < iTop; i++ ) { n = ( crc ^ str.charCodeAt( i ) ) & 0xFF; x = "0x" + table.substr( n * 9, 8 ); crc = ( crc >>> 8 ) ^ x; } return Math.abs(crc ^ (-1)); }; var table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D"; ================================================ FILE: lib/advanced_structures/BloomFilter.js ================================================ /** * Module dependencies. */ var Structure = require('../Structure'), crc32 = require('../Utils').crc32; /** * A Simple BloomFilter. Bloomfilter is a probabilistic data structure used to * determine if an element is present in a set. There may be false positives, * but there cannot be false negatives. * * Usage: * `redback.createBloomFilter(key [, size, hashes]);` * * Options: * `size` - Size of the bloom filter , default is 100 bits. * `hashes` - Number of hashes to perform. default is 2. * * Reference: * http://redis.io/commands#string * http://en.wikipedia.org/wiki/Bloom_filter * http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html * * Redis Structure: * `(namespace:)key = string(bits)` */ var BloomFilter = exports.BloomFilter = Structure.new(); /** * Initialise the bloom filter. * * @param {int} size (optional) - Size of bloom filter. * @param {int} num_hashes(optional) - Number of hashes to perform while storing. * @api private */ BloomFilter.prototype.init = function(size, num_hashes) { this.num_hashes = num_hashes || 2; this.size = size || 100; } /** * Adds an element to the bloom filter. * * @param {string} item - Item to store into bloom filter * @param {Function} callback (optional) * @return this * @api public */ BloomFilter.prototype.add = function(item, callback) { var multi = this.client.multi(), crc; for (var hash_index = 0; hash_index < this.num_hashes; hash_index++) { crc = crc32(item, hash_index) % (this.size+1); multi.setbit(this.key, crc, 1); } multi.exec(callback || function () {}); return this; } /** * Checks if the element exists in the bloom filter. * This can return false positives( i.e An element does not exist but it returns true) * But this can never return false negatives. (i.e an element ) * * @param {string} item - Item to check for existence in bloom filter * @param {Function} callback (optional) * @return this * @api public */ BloomFilter.prototype.exists = function(item, callback) { var multi = this.client.multi(), crc; callback = callback || function () {}; for (var hash_index = 0; hash_index < this.num_hashes; hash_index++) { crc = crc32(item, hash_index) % (this.size+1); multi.getbit(this.key, crc); } multi.exec(function(err, results) { callback(err, results.indexOf(0) === -1); }); return this; } /** * Resets the Bloom filter. * * @param {Function} callback (optional) * @return this */ BloomFilter.prototype.reset = function (callback) { this.client.set(this.key, 0, callback || function () {}); return this; } ================================================ FILE: lib/advanced_structures/CappedList.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var List = require('../base_structures/List').List; /** * A Redis list with a fixed length. Each command that adds a value to the * list is followed by an `LTRIM` command. * * Usage: * `redback.createCappedList(key [, max_length]);` * * Reference: * http://redis.io/topics/data-types#lists * http://redis.io/commands/ltrim * * Redis Structure: * `(namespace:)key = list(values)` */ var CappedList = exports.CappedList = List.prototype.extend(); /** * Setup the Capped List. * * @param {int} length - the maximum length of the list * @param {Function} callback * @api private */ CappedList.prototype.init = function (length) { this.len = length || 1000; } /** * Insert an element before the specified pivot. * * @param {int} pivot * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ CappedList.prototype.insertBefore = function (pivot, value, callback) { callback = callback || function () {}; var multi = this.client.multi() multi.linsert(this.key, 'BEFORE', pivot, value); multi.ltrim(this.key, -1 * this.len, -1); multi.exec(callback); return this; } /** * Insert an element after the specified pivot. * * @param {int} pivot * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ CappedList.prototype.insertAfter = function (pivot, value, callback) { callback = callback || function () {}; var multi = this.client.multi() multi.linsert(this.key, 'AFTER', pivot, value); multi.ltrim(this.key, -1 * this.len, -1); multi.exec(callback); return this; } /** * Add one or more elements to the start of the list. * * @param {string|array} value(s) * @param {Function} callback (optional) * @return this * @api public */ CappedList.prototype.unshift = CappedList.prototype.lpush = function (values, callback) { callback = callback || function () {}; var multi = this.client.multi(); if (Array.isArray(values)) { var key = this.key; values.reverse().forEach(function (value) { multi.lpush(key, value); }); } else { multi.lpush(this.key, values); } multi.ltrim(this.key, 0, this.len - 1); multi.exec(callback); return this; } /** * Add one or more elements to the end of the list. * * @param {string|array} value(s) * @param {Function} callback (optional) * @return this * @api public */ CappedList.prototype.push = CappedList.prototype.add = function (values, callback) { callback = callback || function () {}; var multi = this.client.multi(); if (Array.isArray(values)) { var key = this.key; values.forEach(function (value) { multi.rpush(key, value); }); } else { multi.rpush(this.key, values); } multi.ltrim(this.key, -1 * this.len, -1); multi.exec(callback); return this; } ================================================ FILE: lib/advanced_structures/DensitySet.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var SortedSet = require('../base_structures/SortedSet').SortedSet; /** * The DensitySet is similar to a SortedSet but the ability to explicitly * set an element's score has been removed. Instead, adding/removing * an element will increment/decrement its score, e.g. * `DensitySet.add(['foo','foo','foo'], ..)` //'foo' has a score of 3 * * Usage: * `redback.createDensitySet(key);` * * Reference: * http://redis.io/topics/data-types#sorted-sets * * Redis Structure: * `(namespace:)key = zset(count => element)` */ var DensitySet = exports.DensitySet = SortedSet.prototype.extend(); /** * Add one or more elements to the set. * * @param {string|Array} element(s) * @param {Function} callback (optional) * @return this * @api public */ DensitySet.prototype.add = function (element, callback) { callback = callback || function () {}; if (Array.isArray(element)) { return this.addAll(element, callback); } this.client.zincrby(this.key, 1, element, callback); return this; } /** * Remove one or more elements from the set. * * @param {string|Array} element(s) * @param {Function} callback (optional) * @return this * @api public */ DensitySet.prototype.remove = function (element, callback) { callback = callback || function () {}; if (Array.isArray(element)) { return this.removeAll(element, callback); } var self = this; this.client.zincrby(this.key, -1, element, function (err, removed) { if (err) return callback(err, null); self.client.zremrangebyscore(self.key, '-inf', 0, callback); }); return this; } /** * Add multiple elements to the set. * * @param {Array} elements * @param {Function} callback * @return this * @api private */ DensitySet.prototype.addAll = function (elements, callback) { var self = this, remaining = elements.length, failed = false, add_count = 0; elements.forEach(function (element) { self.client.zincrby(self.key, 1, element, function (err, added) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (added) add_count++; if (!--remaining) callback(null, add_count); } }); }); return this; } /** * Remove multiple elements from the set. * * @param {Array} elements * @param {Function} callback * @return this * @api private */ DensitySet.prototype.removeAll = function (elements, callback) { var self = this, remaining = elements.length, failed = false, rem_count = 0; elements.forEach(function (element) { self.client.zincrby(self.key, -1, element, function (err, added) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (added) rem_count++; if (!--remaining) { self.client.zremrangebyscore(self.key, '-inf', 0, function (err) { callback(err, rem_count); }); } } }); }); return this; } ================================================ FILE: lib/advanced_structures/KeyPair.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * The KeyPair is a structure where unique values are assigned an * ID (like a table with a primary auto-incrementing key and * a single unique column). Internally, the KeyPair uses two Redis * hashes to provide O(1) lookup by both ID and value. * * Usage: * `redback.createKeyPair(key);` * * Reference: * http://redis.io/topics/data-types#hashes * * Redis Structure: * `(namespace:)key = hash(id => value)` * `(namespace:)key:ids = hash(value => id)` */ var KeyPair = exports.KeyPair = Structure.new(); /** * Initialise the KeyPair. * * @api private */ KeyPair.prototype.init = function () { this.idkey = this.key + ':ids'; } /** * Add a unique value to the KeyPair and return its id. If the value already * exists, the existing id is returned. * * @param {string|Array} value(s) * @param {Function} callback * @return this * @api public */ KeyPair.prototype.add = function (value, callback) { //Pass on an array of values to addAll() if (Array.isArray(value)) { return this.addAll(value, callback); } var self = this, hashed_value = this.hashValue(value); //Check if the value already has an id this.client.hget(this.idkey, value, function (err, id) { if (err) return callback(err, null); if (null !== id) { callback(null, id); } else { //If not, create a new id self.autoincrement(function (err, id) { if (err) return callback(err, null); //Set the id and value simultaneously var multi = self.client.multi(); multi.hsetnx(self.idkey, hashed_value, id); multi.hsetnx(self.key, id, value); multi.exec(function(err, response) { if (err) return callback(err, null); //Another client may have add at exactly the same time, so do //another get to get the actual stored id self.client.hget(self.idkey, hashed_value, function (err, real_id) { if (err) return callback(err, null); if (real_id == id) { return callback(null, real_id); } else { //Another client did beat us! remove the bad key self.client.hdel(self.key, id, function (err) { if (err) { callback(err, null); } else { callback(null, real_id); } }); } }); }); }); } }); return this; } /** * Add multiple unique values to the KeyPair and return and * object containing {value: id, ...}. * * @param {Array} values * @param {Function} callback * @return this * @api public */ KeyPair.prototype.addAll = function (values, callback) { var self = this, remaining = values.length, ids = {}, failed = false; values.forEach(function (value) { self.add(value, function (err, id) { if (failed) { return; } else if (err) { failed = true; return callback(err, null); } else { ids[value] = id; if (!--remaining) callback(null, ids); } }); }); } /** * Lookup a unique value and get the associated id. * * @param {string} value * @param {Function} callback * @return this * @api public */ KeyPair.prototype.get = function (value, callback) { if (typeof value === 'function') { callback = value; this.client.hgetall(this.key, callback); } else if (Array.isArray(value)) { for (var i = 0, l = value.length; i < l; i++) { value[i] = this.hashValue(value[i]); } this.client.hmget(this.idkey, value, callback) } else { this.client.hget(this.idkey, this.hashValue(value), callback); } return this; } /** * Get the value associated with the id. * * @param {int|Array} id(s) * @param {Function} callback * @return this * @api public */ KeyPair.prototype.getById = function (id, callback) { if (Array.isArray(id)) this.client.hmget(this.key, id, callback); else this.client.hget(this.key, id, callback); return this; } /** * Get an array of ids. * * @param {Function} callback * @return this * @api public */ KeyPair.prototype.ids = function (callback) { this.client.hkeys(this.key, callback); return this; } /** * Get an array of values. * * @param {string} value * @param {Function} callback * @return this * @api public */ KeyPair.prototype.values = function (callback) { this.client.hvals(this.key, callback); return this; } /** * Check whether a unique value already exists and has an associated id. * * @param {string} value * @param {Function} callback * @return this * @api public */ KeyPair.prototype.exists = function (value, callback) { this.client.hexists(this.idkey, this.hashValue(value), callback); return this; } /** * Checks whether an id exists. * * @param {string} value * @param {Function} callback * @return this * @api public */ KeyPair.prototype.idExists = function (id, callback) { this.client.hexists(this.key, id, callback); return this; } /** * Deletes a unique value and its associated id. * * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ KeyPair.prototype.delete = function (value, callback) { callback = callback || function () {}; var self = this, value = this.hashValue(value); this.client.hget(this.idkey, value, function (err, id) { if (err || value == null) return callback(err); self._delete(id, value, callback); }); return this; } /** * Deletes an id and its associated unique value. * * @param {int} id * @param {Function} callback (optional) * @return this * @api public */ KeyPair.prototype.deleteById = function (id, callback) { callback = callback || function () {}; var self = this; this.client.hget(this.key, id, function (err, value) { if (err || value == null) return callback(err); self._delete(id, self.hashValue(value), callback); }); return this; } /** * An internal helper for simultaneously deleting an id/value pair. * * @param {int} id * @param {string} value * @param {Function} callback * @return this * @api private */ KeyPair.prototype._delete = function (id, value, callback) { var multi = this.client.multi(); multi.hdel(this.key, id); multi.hdel(this.idkey, this.hashValue(value)); multi.exec(callback); return this; } /** * Get the number of unique values. * * @param {Function} callback * @return this * @api public */ KeyPair.prototype.length = function (callback) { this.client.hlen(this.key, callback); return this; } /** * Override this method if you need to hash the unique value * in the second internal hash (i.e. if values are large). * * @param {string} value * @return {string} hashed_value * @api public */ KeyPair.prototype.hashValue = function (value) { return value; } ================================================ FILE: lib/advanced_structures/Lock.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var crypto = require('crypto'); var Structure = require('../Structure'); /** * A distributed lock. */ var Lock = exports.Lock = Structure.new(); /** * Acquire a temporary lock on some key. * * @param {string} key The unique key of the lock * @param {number} ttl The amount of time (in seconds) before the lock expires * @param {Function} callback Invoked when the process completes * @param {Error} callback.err An error that occurred, if any * @param {string} callback.token The token that was acquired if successful. If the lock was * not acquired then this will be `undefined` * @api public */ Lock.prototype.acquire = function(key, ttl, callback) { var client = this.client; _createToken(function(err, token) { if (err) { return callback(err); } client.setnx(key, token, function(err, wasSet) { if (err) { return callback(err); } else if (!wasSet) { // We did not successfully acquire the lock. Since a process can crash after it sets // the lock but before it sets the expiry, we need to avoid deadlocks by ensuring // the lock has a TTL associated to it _ensureTtl(client, key, ttl); return callback(); } // Apply the expiry to the lock client.expire(key, ttl, function(err) { if (err) { return callback(err); } // Return the token, which is used to release the lock return callback(null, token); }); }); }); }; /** * Release a lock that was acquired with the provided key and token. * * @param {string} key The key for the lock to release * @param {string} token The token that was generated for the lock acquisition * @param {Function} callback Invoked when the function completes * @param {Error} callback.err An error that occurred, if any * @param {boolean} callback.hadLock Determines whether or not we owned the lock at the time * that we released it * @api public */ Lock.prototype.release = function(key, token, callback) { var client = this.client; client.get(key, function(err, lockedToken) { if (err) { return callback(err); } else if (lockedToken !== token) { // The current token is not the one we acquired. It's possible we held the lock longer // than its expiry return callback(null, false); } // We have the token, simply delete the lock key client.del(key, function(err) { if (err) { return callback(err); } return callback(null, true); }); }); }; /** * Ensure the lock with the given key has a `ttl`. If it does not, the given expiry will be applied * to it. * * @param {RedisClient} client The Redis to use to apply the ttl * @param {string} key The key of the lock to check * @param {number} ttl If the lock does not have an expiry set, set this duration (in * seconds) * @api private */ var _ensureTtl = function(client, key, ttl) { client.ttl(key, function(err, currentTtl) { if (currentTtl === -1) { // There is no expiry set on this key, set it client.expire(key, ttl); } }); }; /** * Generate a random lock token. * * @param {Function} callback Invoked with the token when complete * @param {Error} callback.err An error that occurred, if any * @param {string} callback.token The randomly generated token * @api private */ var _createToken = function(callback) { crypto.randomBytes(16, function(err, buffer) { if (err) { return callback(err); } return callback(null, buffer.toString('base64')); }); }; ================================================ FILE: lib/advanced_structures/Queue.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'), List = require('../base_structures/List').List; /** * A simple FIFO/LIFO queue. * * Usage: * `redback.createQueue(key [, is_fifo]);` * * Reference: * http://redis.io/topics/data-types#lists * http://en.wikipedia.org/wiki/Queue_(data_structure) * * Redis Structure: * `(namespace:)key = list(values)` */ var Queue = exports.Queue = Structure.new(); /** * Setup the Queue to be either FIFO or LIFO. * * @param {bool} is_fifo * @api private */ Queue.prototype.init = function (is_fifo) { this.fifo = is_fifo; this.list = new List(this.client, this.id, this.namespace); } /** * Add one or more elements to the queue. * * @param {string|Array} value(s) * @param {Function} callback (optional) * @api public */ Queue.prototype.enqueue = Queue.prototype.add = function (values, callback) { this.list.unshift(values, callback); return this; } /** * Remove the next element from the queue. * * @param {int} wait (optional) - block for this many seconds * @param {Function} callback * @api public */ Queue.prototype.dequeue = Queue.prototype.next = function (wait, callback) { this.list[this.fifo ? 'pop' : 'shift'](wait, callback); return this; } ================================================ FILE: lib/advanced_structures/RateLimit.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * See https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da * * Count the number of times a subject performs an action over an interval * in the immediate past - this can be used to rate limit the subject if * the count goes over a certain threshold. For example, you could track * how many times an IP (the subject) has viewed a page (the action) over * a certain time frame and limit them accordingly. * * Usage: * `redback.createRateLimit(action [, options]);` * * Options: * `bucket_interval` - default is 5 seconds * `bucket_span` - default is 10 minutes * `subject_expiry` - default is 20 minutes * * Reference: * https://gist.github.com/chriso/54dd46b03155fcf555adccea822193da * http://redis.io/topics/data-types#hash * * Redis Structure: * `(namespace:)action: = hash(bucket => count)` * `(namespace:)action: = hash(bucket => count)` * `(namespace:)action: = hash(bucket => count)` */ var RateLimit = exports.RateLimit = Structure.new(); /** * Setup the RateLimit structure. * * @param {Object} options (optional) * @api private */ RateLimit.prototype.init = function (options) { options = options || {}; this.bucket_span = options.bucket_span || 600; this.bucket_interval = options.bucket_interval || 5; this.subject_expiry = options.subject_expiry || 1200; this.bucket_count = Math.round(this.bucket_span / this.bucket_interval); } /** * Get the bucket associated with the current time. * * @param {int} time (optional) - default is the current time (ms since epoch) * @return {int} bucket * @api private */ RateLimit.prototype.getBucket = function (time) { time = (time || new Date().getTime()) / 1000; return Math.floor((time % this.bucket_span) / this.bucket_interval); } /** * Increment the count for the specified subject. * * @param {string} subject * @param {Function} callback (optional) * @return this * @api public */ RateLimit.prototype.add = function (subject, callback) { if (Array.isArray(subject)) { return this.addAll(subject, callback); } var bucket = this.getBucket(), multi = this.client.multi(); subject = this.key + ':' + subject; //Increment the current bucket multi.hincrby(subject, bucket, 1) //Clear the buckets ahead multi.hdel(subject, (bucket + 1) % this.bucket_count) .hdel(subject, (bucket + 2) % this.bucket_count) //Renew the key TTL multi.expire(subject, this.subject_expiry); multi.exec(function (err) { if (!callback) return; if (err) return callback(err); callback(null); }); return this; } /** * Count the number of times the subject has performed an action * in the last `interval` seconds. * * @param {string} subject * @param {int} interval * @param {Function} callback * @return this * @api public */ RateLimit.prototype.count = function (subject, interval, callback) { var bucket = this.getBucket(), multi = this.client.multi(), count = Math.floor(interval / this.bucket_interval); subject = this.key + ':' + subject; //Get the counts from the previous `count` buckets multi.hget(subject, bucket); while (count--) { multi.hget(subject, (--bucket + this.bucket_count) % this.bucket_count); } //Add up the counts from each bucket multi.exec(function (err, counts) { if (err) return callback(err, null); for (var count = 0, i = 0, l = counts.length; i < l; i++) { if (counts[i]) { count += parseInt(counts[i], 10); } } callback(null, count); }); return this; } /** * An alias for `ratelimit.add(subject).count(subject, interval);` * * @param {string} subject * @param {int} interval * @param {Function} callback * @return this * @api public */ RateLimit.prototype.addCount = function (subject, interval, callback) { var bucket = this.getBucket(), multi = this.client.multi(), count = Math.floor(interval / this.bucket_interval); subject = this.key + ':' + subject; //Increment the current bucket multi.hincrby(subject, bucket, 1) //Clear the buckets ahead multi.hdel(subject, (bucket + 1) % this.bucket_count) .hdel(subject, (bucket + 2) % this.bucket_count) //Renew the key TTL multi.expire(subject, this.subject_expiry); //Get the counts from the previous `count` buckets multi.hget(subject, bucket); while (count--) { multi.hget(subject, (--bucket + this.bucket_count) % this.bucket_count); } //Add up the counts from each bucket multi.exec(function (err, counts) { if (err) return callback(err, null); for (var count = 0, i = 4, l = counts.length; i < l; i++) { if (counts[i]) { count += parseInt(counts[i], 10); } } callback(null, count); }); return this; } ================================================ FILE: lib/advanced_structures/SocialGraph.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * Build a social graph similar to Twitter's. User ID can be a string or * integer, as long as they're unique. * * Usage: * `redback.createSocialGraph(id [, prefix]);` * * Reference: * http://redis.io/topics/data-types#sets * * Redis Structure: * `(namespace:)(prefix:)id:following = set(ids)` * `(namespace:)(prefix:)id:followers = set(ids)` */ var SocialGraph = exports.SocialGraph = Structure.new(); /** * Initialise the SocialGraph. * * @param {string} prefix (optional) * @api private */ SocialGraph.prototype.init = function (prefix) { this.key_prefix = this.namespaceKey(); if (prefix) { this.key_prefix += prefix + ':'; } this.key = this.key_prefix + this.id; this.following = this.key + ':following'; this.followers = this.key + ':followers'; } /** * Follow one or more users. * * @param {int|SocialGraph|Array} user(s) * @param {Function} callback (optional) * @return this * @api public */ SocialGraph.prototype.follow = function (users, callback) { var self = this, users = this.getKeys(arguments, 'id'), multi = this.client.multi(); if (typeof users[users.length-1] === 'function') { callback = users.pop(); } else { callback = function () {}; } users.forEach(function (user) { multi.sadd(self.key_prefix + user + ':followers', self.id); multi.sadd(self.following, user); }); multi.exec(callback); return this; } /** * Unfollow one or more users. * * @param {int|SocialGraph|Array} user(s) * @param {Function} callback (optional) * @return this * @api public */ SocialGraph.prototype.unfollow = function (users, callback) { var self = this, users = this.getKeys(arguments, 'id'), multi = this.client.multi(); if (typeof users[users.length-1] === 'function') { callback = users.pop(); } else { callback = function () {}; } users.forEach(function (user) { multi.srem(self.key_prefix + user + ':followers', self.id); multi.srem(self.following, user); }); multi.exec(callback); return this; } /** * Gets the users whom the current users follows as an array. * * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getFollowing = function (callback) { this.client.smembers(this.following, callback); return this; } /** * Gets an array of users who follow the current user. * * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getFollowers = function (callback) { this.client.smembers(this.followers, callback); return this; } /** * Count how many users the current user follows. * * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.countFollowing = function (callback) { this.client.scard(this.following, callback); return this; } /** * Count how many users follow the current user. * * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.countFollowers = function (callback) { this.client.scard(this.followers, callback); return this; } /** * Checks whether the current user follows the specified user. * * @param {string|SocialGraph} user * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.isFollowing = function (user, callback) { user = this.getKey(user, 'id'); this.client.sismember(this.following, user, callback); return this; } /** * Checks whether the specified user follows the current user. * * @param {string|SocialGraph} user * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.hasFollower = function (user, callback) { user = this.getKey(user, 'id'); this.client.sismember(this.followers, user, callback); return this; } /** * Gets an array of common followers for one or more users. * * @param {string|SocialGraph|Array} user(s) * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getCommonFollowers = function (users, callback) { var users = this.getSocialKeys(arguments, 'followers'); users.unshift(this.followers); this.client.sinter.apply(this.client, users); return this; } /** * Gets an array of users who are followed by all of the specified user(s). * * @param {string|SocialGraph|Array} user(s) * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getCommonFollowing = function (users, callback) { var users = this.getSocialKeys(arguments, 'following'); users.unshift(this.following); this.client.sinter.apply(this.client, users); return this; } /** * Gets an array of users who follow the current user but do not follow any * of the other specified users. * * @param {string|SocialGraph|Array} user(s) * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getDifferentFollowers = function (users, callback) { var users = this.getSocialKeys(arguments, 'followers'); users.unshift(this.followers); this.client.sdiff.apply(this.client, users); return this; } /** * Gets an array of users who are followed by the current user but not any of * the other specified users. * * @param {string|SocialGraph|Array} user(s) * @param {Function} callback * @return this * @api public */ SocialGraph.prototype.getDifferentFollowing = function (users, callback) { var users = this.getSocialKeys(arguments, 'following'); users.unshift(this.following); this.client.sdiff.apply(this.client, users); return this; } /** * Grabs the specified SocialGraph key from a list of arguments. * * @param {Array} args * @param {string} key * @return {string} social_keys * @api private */ SocialGraph.prototype.getSocialKeys = function (args, key) { var users = Array.prototype.slice.call(args), callback = users.pop(), user_key, self = this, keys = []; for (var i = 0, l = users.length; i < l; i++) { if (Array.isArray(users[i])) { users[i].forEach(function (user) { if (typeof user[key] !== 'undefined') { user_key = user[key]; } else { user_key = self.key_prefix + user + ':' + key; } keys.push(user_key); }); } else { if (typeof users[i][key] !== 'undefined') { user_key = users[i][key]; } else { user_key = self.key_prefix + users[i] + ':' + key; } keys.push(user_key); } } keys.push(callback); return keys; } ================================================ FILE: lib/base_structures/Bitfield.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * Wrap the Redis bit commands. * * Usage: * `redback.createBitfield(key);` * * Reference: * http://redis.io/commands#string * * Redis Structure: * `(namespace:)key = string` */ var Bitfield = exports.Bitfield = Structure.new(); /** * Get a single bit * * @param {int} bit * @param {Function} callback (optional) * @return this * @api public */ Bitfield.prototype.get = function (bit, callback) { callback = callback || function () {}; this.client.getbit(this.key, bit, callback); return this; } /** * Set a single bit. The callback receives the previous value. * * @param {int} bit * @param {bool} value * @param {Function} callback (optional) * @return this * @api public */ Bitfield.prototype.set = function (bit, value, callback) { callback = callback || function () {}; this.client.setbit(this.key, bit, value ? 1 : 0, callback); return this; } ================================================ FILE: lib/base_structures/Hash.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * A wrapper for the Redis hash type. * * Usage: * `redback.createHash(key);` * * Reference: * http://redis.io/topics/data-types#hashes * * Redis Structure: * `(namespace:)key = hash(key => value)` */ var Hash = exports.Hash = Structure.new(); /** * Get an array of hash keys. * * @param {Function} callback * @return this * @api public */ Hash.prototype.keys = function (callback) { this.client.hkeys(this.key, callback); return this; } /** * Get an array of hash values. * * @param {Function} callback * @return this * @api public */ Hash.prototype.values = function (callback) { this.client.hvals(this.key, callback); return this; } /** * Get the number of hash keys. * * @param {Function} callback * @return this * @api public */ Hash.prototype.length = function (callback) { this.client.hlen(this.key, callback); return this; } /** * Delete a hash key. * * @param {string} hash_key * @param {Function} callback (optional) * @return this * @api public */ Hash.prototype.delete = Hash.prototype.del = function (hash_key, callback) { callback = callback || function () {}; this.client.hdel(this.key, hash_key, callback); return this; } /** * Checks whether a hash key exists. * * @param {string} hash_key * @param {Function} callback * @return this * @api public */ Hash.prototype.exists = function (hash_key, callback) { this.client.hexists(this.key, hash_key, callback); return this; } /** * Sets one or more key/value pairs. * * To set one key/value pair: * `hash.set('foo', 'bar', callback);` * * To set multiple: * `hash.set({key1:'value1', key2:'value2}, callback);` * * @param {string|Object} hash_key * @param {string} value (optional) * @param {Function} callback (optional) * @return this * @api public */ Hash.prototype.set = function (hash_key, value, callback) { if (typeof hash_key === 'object') { callback = value || function () {}; this.client.hmset(this.key, hash_key, callback); } else { callback = callback || function () {}; this.client.hset(this.key, hash_key, value, callback); } return this; } /** * Sets a key/value pair if the key doesn't already exist. * * @param {string} hash_key * @param {string} value * @param {Function} callback * @return this * @api public */ Hash.prototype.add = function (hash_key, value, callback) { callback = callback || function () {}; this.client.hsetnx(this.key, hash_key, value, callback); return this; } /** * Gets one or more key/value pairs. * * To get all key/value pairs in the hash: * `hash.get('foo', callback);` * * To get certain key/value pairs: * `hash.get(['foo','bar'], callback);` * `hash.get('foo', callback);` * * @param {string} hash_key (optional) * @param {Function} callback * @return this * @api public */ Hash.prototype.get = function (hash_key, callback) { if (typeof hash_key === 'function') { callback = hash_key; this.client.hgetall(this.key, callback); } else if (Array.isArray(hash_key)) { this.client.hmget(this.key, hash_key, callback) } else { this.client.hget(this.key, hash_key, callback); } return this; } /** * Increment the specified hash value. * * @param {string} hash_key * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this * @api public */ Hash.prototype.increment = Hash.prototype.incrBy = function (hash_key, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.hincrby(this.key, hash_key, amount, callback); return this; } /** * Decrement the specified hash value. * * @param {string} hash_key * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this * @api public */ Hash.prototype.decrement = Hash.prototype.decrBy = function (hash_key, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.hincrby(this.key, hash_key, -1 * amount, callback); return this; } ================================================ FILE: lib/base_structures/List.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * A wrapper for the Redis list type. * * Usage: * `redback.createList(key);` * * Reference: * http://redis.io/topics/data-types#lists * * Redis Structure: * `(namespace:)key = list(values)` */ var List = exports.List = Structure.new(); /** * Get the list as an array. * * @param {Function} callback * @return this * @api public */ List.prototype.values = function (callback) { this.client.lrange(this.key, 0, -1, callback); return this; } /** * Get a range of list elements. * * @param {int} start * @param {count} end (optional - defaults to the last element) * @param {Function} callback * @return this * @api public */ List.prototype.range = function (start, end, callback) { if (typeof end === 'function') { callback = end; end = -1; } this.client.lrange(this.key, start, end, callback); return this; } /** * Get one or more elements starting at the specified index. * * @param {int} index * @param {count} count (optional - default is 1) * @param {Function} callback * @return this * @api public */ List.prototype.get = function (index, count, callback) { if (typeof count === 'function') { callback = count; this.client.lindex(this.key, index, callback); } else { this.client.lrange(this.key, index, index + count - 1, callback); } return this; } /** * Cap the length of the list. * * @param {int} length * @param {bool} keep_earliest (optional - default is false) * @param {Function} callback (optional) * @return this * @api public */ List.prototype.cap = function (length, keep_earliest, callback) { callback = callback || function () {}; var start = 0, end = -1; if (typeof keep_earliest === 'function') { //Keep the last `length` elements start = -1 * length; callback = keep_earliest; } else { //Keep the first `length` elements end = length - 1; } this.client.ltrim(this.key, start, end, callback); return this; } /** * Remove one or more list elements matching the value. * * @param {string} value * @param {bool} count (optional - default is 1) * @param {Function} callback (optional) * @return this * @api public */ List.prototype.remove = function (value, count, callback) { callback = callback || function () {}; if (typeof count === 'function') { callback = count; count = 1; } this.client.lrem(this.key, count, value, callback); return this; } /** * Trim a list to the specified bounds. * * @param {int} start * @param {int} end * @param {Function} callback (optional) * @return this * @api public */ List.prototype.trim = function (start, end, callback) { callback = callback || function () {}; this.client.ltrim(this.key, start, end, callback); return this; } /** * Insert an element before the specified pivot. * * @param {int} pivot * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ List.prototype.insertBefore = function (pivot, value, callback) { callback = callback || function () {}; this.client.linsert(this.key, 'BEFORE', pivot, value, callback); return this; } /** * Insert an element after the specified pivot. * * @param {int} pivot * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ List.prototype.insertAfter = function (pivot, value, callback) { callback = callback || function () {}; this.client.linsert(this.key, 'AFTER', pivot, value, callback); return this; } /** * Set the element at the specified index. * * @param {int} index * @param {string} value * @param {Function} callback (optional) * @return this * @api public */ List.prototype.set = function (index, value, callback) { callback = callback || function () {}; this.client.lset(this.key, index, value, callback); return this; } /** * Get the number of elements in the list. * * @param {Function} callback * @return this * @api public */ List.prototype.length = function (callback) { this.client.llen(this.key, callback); return this; } /** * Get and remove the last element in the list. The first param can be used * to block the process and wait until list elements are available. If the list * is empty in both examples below, the first example will return `null`, while * the second will wait for up to 3 seconds. If a list element becomes available * during the 3 seconds it will be returned, otherwise `null` will be returned. * * Example: * `list.shift(callback);` * * Blocking Example: * `list.shift(3, callback)` * * @param {int} wait (optional) - seconds to block * @param {Function} callback * @return this * @api public */ List.prototype.shift = function (wait, callback) { if (typeof wait === 'function') { callback = wait; this.client.lpop(this.key, callback); } else { this.client.blpop(this.key, wait, callback); } return this; } /** * Get and remove the last element in the list. The first param can be used * to block the process and wait until list elements are available. If the list * is empty in both examples below, the first example will return `null`, while * the second will wait for up to 3 seconds. If a list element becomes available * during the 3 seconds it will be returned, otherwise `null` will be returned. * * Example: * `list.pop(callback);` * * Blocking Example: * `list.pop(3, callback)` * * @param {int} wait (optional) - seconds to block * @param {Function} callback * @return this * @api public */ List.prototype.pop = function (wait, callback) { if (typeof wait === 'function') { callback = wait; this.client.rpop(this.key, callback); } else { this.client.brpop(this.key, wait, callback); } return this; } /** * Add one or more elements to the start of the list. * * @param {string|array} value(s) * @param {Function} callback (optional) * @return this * @api public */ List.prototype.unshift = List.prototype.lpush = function (values, callback) { callback = callback || function () {}; if (Array.isArray(values)) { var multi = this.client.multi(), key = this.key; values.reverse().forEach(function (value) { multi.lpush(key, value); }); multi.exec(callback); } else { this.client.lpush(this.key, values, callback); } return this; } /** * Add one or more elements to the end of the list. * * @param {string|array} value(s) * @param {Function} callback (optional) * @return this * @api public */ List.prototype.push = List.prototype.add = function (values, callback) { callback = callback || function () {}; if (Array.isArray(values)) { var multi = this.client.multi(), key = this.key; values.forEach(function (value) { multi.rpush(key, value); }); multi.exec(callback); } else { this.client.rpush(this.key, values, callback); } return this; } /** * Remove the last element of the list and add it to the start * of another list. * * @param {String|List} list * @param {bool} wait (optional) - seconds to block while waiting * @param {Function} callback (optional) * @return this * @api public */ List.prototype.popShift = function (list, wait, callback) { callback = callback || function () {}; list = this.getKey(list); if (typeof wait === 'function') { callback = wait; this.client.rpoplpush(this.key, list, callback); } else { this.client.brpoplpush(this.key, list, wait, callback); } return this; } ================================================ FILE: lib/base_structures/Set.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * A wrapper for the Redis set type. * * Usage: * `redback.createSet(key);` * * Reference: * http://redis.io/topics/data-types#sets * * Redis Structure: * `(namespace:)key = set(elements)` */ var Set = exports.Set = Structure.new(); /** * Add one or more elements to the set. * * @param {string|Array} element(s) * @param {Function} callback (optional) * @return this * @api public */ Set.prototype.add = function (element, callback) { callback = callback || function () {}; if (Array.isArray(element)) { return this.addAll(element, callback); } this.client.sadd(this.key, element, callback); return this; } /** * Remove one or more elements from the set. * * @param {string|Array} element(s) * @param {Function} callback (optional) * @return this * @api public */ Set.prototype.remove = function (element, callback) { callback = callback || function () {}; if (Array.isArray(element)) { return this.removeAll(element, callback); } this.client.srem(this.key, element, callback); return this; } /** * Get an array of elements in the set. * * @param {Function} callback * @return this * @api public */ Set.prototype.elements = Set.prototype.members = function (callback) { this.client.smembers(this.key, callback); return this; } /** * Move an element to another set. * * @param {string|Set} dest * @param {string} element * @param {Function} callback (optional) * @return this * @api public */ Set.prototype.move = function (dest, element, callback) { callback = callback || function () {}; this.client.smove(this.key, this.getKey(dest), element, callback); return this; } /** * Check whether an element exists in the set. * * @param {string} element * @param {Function} callback * @return this * @api public */ Set.prototype.exists = Set.prototype.contains = function (element, callback) { this.client.sismember(this.key, element, callback); return this; } /** * Get the length (cardinality) of the set. * * @param {Function} callback * @return this * @api public */ Set.prototype.length = Set.prototype.cardinality = function (callback) { this.client.scard(this.key, callback); return this; } /** * Get a random element from the set and optionally remove it. * * @param {bool} remove (optional - default is false) * @param {Function} callback * @return this * @api public */ Set.prototype.random = function (remove, callback) { if (typeof remove === 'function') { callback = remove; this.client.srandmember(this.key, callback); } else { this.client.spop(this.key, callback); } return this; } /** * Get the intersection of one or more sets. * * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.inter = function (sets, callback) { sets = this.getKeys(arguments); sets.unshift(this.key); this.client.sinter.apply(this.client, sets); return this; } /** * Get the intersection of one or more sets and store it another * set (dest). * * @param {string|Set} dest * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.interStore = function (dest, sets, callback) { sets = this.getKeys(arguments); dest = sets.shift(); sets.unshift(dest, this.key); this.client.sinterstore.apply(this.client, sets); return this; } /** * Get the union of one or more sets. * * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.union = function (sets, callback) { sets = this.getKeys(arguments); sets.unshift(this.key); this.client.sunion.apply(this.client, sets); return this; } /** * Get the union of one or more sets and store it another * set (dest). * * @param {string|Set} dest * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.unionStore = function (dest, sets, callback) { sets = this.getKeys(arguments); dest = sets.shift(); sets.unshift(dest, this.key); this.client.sunionstore.apply(this.client, sets); return this; } /** * Get the difference of one or more sets. * * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.diff = function (sets, callback) { sets = this.getKeys(arguments); sets.unshift(this.key); this.client.sdiff.apply(this.client, sets); return this; } /** * Get the difference of one or more sets and store it another * set (dest). * * @param {string|Set} dest * @param {string|Set|Array} set(s) * @param {Function} callback * @return this * @api public */ Set.prototype.diffStore = function (dest, sets, callback) { sets = this.getKeys(arguments); dest = sets.shift(); sets.unshift(dest, this.key); this.client.sdiffstore.apply(this.client, sets); return this; } /** * Add multiple elements to the set. * * @param {Array} elements * @param {Function} callback * @return this * @api private */ Set.prototype.addAll = function (elements, callback) { var self = this, remaining = elements.length, failed = false, add_count = 0; elements.forEach(function (element) { self.client.sadd(self.key, element, function (err, added) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (added) add_count++; if (!--remaining) callback(null, add_count); } }); }); return this; } /** * Remove multiple elements from the set. * * @param {Array} elements * @param {Function} callback * @return this * @api private */ Set.prototype.removeAll = function (elements, callback) { var self = this, remaining = elements.length, failed = false, rem_count = 0; elements.forEach(function (element) { self.client.srem(self.key, element, function (err, removed) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (removed) rem_count++; if (!--remaining) callback(null, rem_count); } }); }); return this; } ================================================ FILE: lib/base_structures/SortedSet.js ================================================ /*! * Redback * Copyright(c) 2011 Chris O'Hara * MIT Licensed */ /** * Module dependencies. */ var Structure = require('../Structure'); /** * A wrapper for the Redis sorted set (zset) type. Each element has a * score which is used to rank and order all elements in the set. Elements * are ranked from lowest score to highest (the lowest score has * a rank of 0) * * Usage: * `redback.createSortedSet(key);` * * Reference: * http://redis.io/topics/data-types#sorted-sets * * Redis Structure: * `(namespace:)key = zset(score => element)` */ var SortedSet = exports.SortedSet = Structure.new(); /** * Add one or more elements to the set. * * To add a single element and score: * `set.add(12, 'foo', callback);` * * To add multiple elements/scores: * `set.add({foo:12, bar:3}, callback);` * * @param {int} score (optional) * @param {string|Object} element(s) * @param {Function} callback (optional) * @return this * @api public */ SortedSet.prototype.add = function (score, element, callback) { callback = callback || function () {}; if (typeof score === 'object') { callback = element; element = score; return this.addAll(element, callback); } this.client.zadd(this.key, score, element, callback); return this; } /** * Remove one or more elements from the set. * * @param {string|Array} element(s) * @param {Function} callback (optional) * @return this * @api public */ SortedSet.prototype.remove = function (element, callback) { callback = callback || function () {}; if (Array.isArray(element)) { return this.removeAll(element, callback); } this.client.zrem(this.key, element, callback); return this; } /** * Get the number of elements in the set. * * @param {Function} callback * @return this * @api public */ SortedSet.prototype.length = function (callback) { this.client.zcard(this.key, callback); return this; } /** * Check whether an element exists in the set. * * @param {string} element * @param {Function} callback * @return this * @api public */ SortedSet.prototype.exists = SortedSet.prototype.contains = function (element, callback) { this.client.zscore(this.key, element, function (err, score) { callback(err, score != null); }); return this; } /** * Get the rank of the specified element. * * @param {string} element * @param {Function} callback * @return this * @api public */ SortedSet.prototype.rank = function (element, callback) { this.client.zrank(this.key, element, callback) return this; } /** * Get the score of the specified element. * * @param {string} element * @param {Function} callback * @return this * @api public */ SortedSet.prototype.score = function (element, callback) { this.client.zscore(this.key, element, callback) return this; } /** * Increment the specified element's score. * * @param {string} element * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this; * @api public */ SortedSet.prototype.increment = SortedSet.prototype.incrBy = function (element, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.zincrby(this.key, amount, element, callback); return this; } /** * Decrement the specified element's score. * * @param {string} element * @param {int} amount (optional - default is 1) * @param {Function} callback (optional) * @return this; * @api public */ SortedSet.prototype.decrement = SortedSet.prototype.decrBy = function (element, amount, callback) { callback = callback || function () {}; if (typeof amount === 'function') { callback = amount; amount = 1; } this.client.zincrby(this.key, -1 * amount, element, callback); return this; } /** * Add multiple elements to the set. See `add()`. * * @param {Object} elements * @param {Function} callback * @return this * @api private */ SortedSet.prototype.addAll = function (elements, callback) { var self = this, i, remaining = 0, failed = false, add_count = 0; for (i in elements) { remaining++; this.client.zadd(this.key, elements[i], i, function (err, added) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (added) add_count++; if (!--remaining) { callback(null, add_count); } } }); } return this; } /** * Remove multiple elements from the set. See `remove()` * * @param {Array} elements * @param {Function} callback * @return this * @api private */ SortedSet.prototype.removeAll = function (elements, callback) { var self = this, remaining = elements.length, failed = false, rem_count = 0; elements.forEach(function (element) { self.client.zrem(self.key, element, function (err, added) { if (failed) { return; } else if (err) { failed = true; return callback(err); } else { if (added) rem_count++; if (!--remaining) callback(null, rem_count); } }); }); return this; } /** * Get all elements in the set as an object `{element: score, ...}`. * If `without_scores` is true then just an array of elements is returned. * * @param {bool} without_scores (optional - scores are included by default) * @param {Function} callback * @return this * @api public */ SortedSet.prototype.get = function (without_scores, callback) { if (typeof without_scores === 'function') { callback = without_scores; this.client.zrange(this.key, 0, -1, 'WITHSCORES', this.parseScores(callback)); } else { this.client.zrange(this.key, 0, -1, callback); } return this; } /** * Return a callback that parses a WITHSCORES result: * `['foo','1','bar','2'] => {foo:1, bar:2}` * * @param {Function} callback * @api private */ SortedSet.prototype.parseScores = function (callback) { return function (err, results) { if (err) return callback(err, null); if (!results || results.length < 2) return callback(null, null); var len = results.length, i = 0, ret = {}, key, value; while (true) { key = results[i++]; value = results[i++]; ret[key] = value; if (i >= len) break; } callback(null, ret); } } /** * Get elements with scores between the specified range. Elements are returned * as an object `{element: score, ..}` and ordered from highest score to lowest. * * Note that the `start` and `end` range is inclusive and can be an integer or * the constants `redback.INF` to represent infinity, or `redback.NINF` to * represent negative infinity. `start` must be <= `end`. * * @param {int} start * @param {int} end * @param {int} count (optional) - the maximum number of elements to return * @param {int} offset (optional) - if using count, start at this offset * @param {Function} callback * @return this * @api public */ SortedSet.prototype.getScores = function (start, end, count, offset, callback) { if (null === start) start = '-inf'; if (null === end) end = '+inf'; if (typeof count === 'function') { callback = count; this.client.zrangebyscore(this.key, start, end, 'WITHSCORES', this.parseScores(callback)); return this; } else if (typeof offset === 'function') { callback = offset; offset = 0; } this.client.zrangebyscore(this.key, start, end, 'WITHSCORES', 'LIMIT', offset, count, this.parseScores(callback)); return this; } /** * The same as `getScores()` but elements are ordered from lowest score to * highest. * * Note that `end` must be <= `start`. * * @param {int} start * @param {int} end * @param {int} count (optional) - the maximum number of elements to return * @param {int} offset (optional) - if using count, start at this offset * @param {Function} callback * @return this * @api public */ SortedSet.prototype.getScoresReverse = function (start, end, count, offset, callback) { if (null === start) start = '+inf'; if (null === end) end = '-inf'; if (typeof count === 'function') { callback = count; this.client.zrevrangebyscore(this.key, start, end, 'WITHSCORES', this.parseScores(callback)); return this; } else if (typeof offset === 'function') { callback = offset; offset = 0; } this.client.zrevrangebyscore(this.key, start, end, 'WITHSCORES', 'LIMIT', offset, count, this.parseScores(callback)); return this; } /** * Remove elements with scores between the specified range (inclusive). * * @param {int} start * @param {int} end * @param {Function} callback (optional) * @return this * @api public */ SortedSet.prototype.removeScores = function (start, end, callback) { callback = callback || function () {}; if (null === start) start = '-inf'; if (null === end) end = '+inf'; this.client.zremrangebyscore(this.key, start, end, callback); return this; } /** * Count the number of elements with scores between the specified * range (inclusive). * * @param {int} start * @param {int} end * @param {Function} callback * @return this * @api public */ SortedSet.prototype.countScores = function (start, end, callback) { if (null === start) start = '-inf'; if (null === end) end = '+inf'; this.client.zcount(this.key, start, end, callback); return this; } /** * Get elements with ranks between the specified range (inclusive). * * To get the first 3 elements in the set (with the highest scores): * `set.getRanks(0, 2, callback);` * * To get the last 3 elements in the set (lowest scores): * `set.getRanks(-3, -1, callback);` * * @param {int} start * @param {int} end * @param {Function} callback * @return this * @api public */ SortedSet.prototype.getRanks = function (start, end, callback) { if (null === start) start = 0; if (null === end) end = -1; this.client.zrange(this.key, start, end, 'WITHSCORES', this.parseScores(callback)); return this; } /** * The same as `getRanks()` but elements are ordered from lowest score * to the highest. * * Note that start and end have been deliberately switched for consistency. * * getScoresReverse(arg1, arg2, ..) expects arg1 >= arg2 and so does this * method. * * @param {int} end * @param {int} start * @param {Function} callback * @return this * @api public */ SortedSet.prototype.getRanksReverse = function (end, start, callback) { if (null === start) start = -1; if (null === end) end = 0; this.client.zrevrange(this.key, start, end, 'WITHSCORES', this.parseScores(callback)); return this; } /** * Remove elements with ranks between the specified range (inclusive). * * @param {int} start * @param {int} end * @param {Function} callback (optional) * @return this * @api public */ SortedSet.prototype.removeRanks = function (start, end, callback) { callback = callback || function () {}; if (null === start) start = -1; if (null === end) end = 0; this.client.zremrangebyrank(this.key, start, end, callback); return this; } /** * Get `count` elements with the highest scores. * * @param {int} count * @param {Function} callback * @return this * @api public */ SortedSet.prototype.highestScores = function (count, callback) { this.getRanks(-1 * count, -1, callback); return this; } /** * Get `count` elements with the lowest scores. * * @param {int} count * @param {Function} callback * @return this * @api public */ SortedSet.prototype.lowestScores = function (count, callback) { this.getRanks(0, count - 1, callback); return this; } /** * Get the intersection of one or more sets. For more information on weights, * aggregate functions, etc. see: http://redis.io/commands/zinterstore * * @param {int} dest * @param {string|Set|Array} set(s) * @param {int|Array} weights (optional) * @param {string} aggregate (optional) - either SUM, MIN or MAX * @param {Function} callback * @return this * @api public */ SortedSet.prototype.inter = function (dest, sets, weights, aggregate, callback) { var args = [], self = this; args.push(this.getKey(dest)); //weights/aggregate are optional if (typeof weights === 'function') { callback = weights; weights = aggregate = false; } else if (typeof aggregate === 'function') { callback = aggregate; aggregate = false; } //ZINTERSTORE destination numkeys key [key ...] // [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] if (Array.isArray(sets)) { args.push(sets.length); sets.forEach(function (set) { args.push(self.getKey(set)); }); } else { args.push(1, this.getKey(sets)); } if (weights) { args.push('WEIGHTS'); if (Array.isArray(weights)) { weights.forEach(function (weight) { args.push(weight); }); } else { args.push(weights); } } if (aggregate) { args.push('AGGREGATE', aggregate); } args.push(callback); this.client.zinterstore.apply(this.client, args); return this; } /** * Get the union of one or more sets. For more information on weights, * aggregate functions, etc. see: http://redis.io/commands/zunionstore * * @param {int} dest * @param {string|Set|Array} set(s) * @param {int|Array} weights (optional) * @param {string} aggregate (optional) - either SUM, MIN or MAX * @param {Function} callback * @return this * @api public */ SortedSet.prototype.union = function (dest, sets, weights, aggregate, callback) { var args = [], self = this; args.push(this.getKey(dest)); //weights/aggregate are optional if (typeof weights === 'function') { callback = weights; weights = aggregate = false; } else if (typeof aggregate === 'function') { callback = aggregate; aggregate = false; } //ZUNIONSTORE destination numkeys key [key ...] // [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] if (Array.isArray(sets)) { args.push(sets.length); sets.forEach(function (set) { args.push(self.getKey(set)); }); } else { args.push(1, this.getKey(sets)); } if (weights) { args.push('WEIGHTS'); if (Array.isArray(weights)) { weights.forEach(function (weight) { args.push(weight); }); } else { args.push(weights); } } if (aggregate) { args.push('AGGREGATE', aggregate); } args.push(callback); this.client.zunionstore.apply(this.client, args); return this; } ================================================ FILE: package.json ================================================ { "name" : "redback", "description" : "A high-level Redis library", "version" : "0.5.1", "homepage" : "https://github.com/chriso/redback", "author" : "Chris O'Hara ", "main" : "index", "scripts": { "test": "redis-cli -n 11 flushdb && expresso -s test/*.test.js", "gen-docs": "dox -t Redback -d 'A high-level Redis library' -r https://github.com/chriso/redback lib/**/*.js > docs/api.html" }, "bugs": { "url": "http://github.com/chriso/redback/issues" }, "repository": { "type": "git", "url": "http://github.com/chriso/redback.git" }, "dependencies": { "redis": "*" }, "devDependencies": { "expresso": "*", "dox": "0.0.5" }, "contributors": [ { "name": "Sreekanth", "github": "https://github.com/sreeix", "email": "sreeix@gmail.com" } ], "license": "MIT" } ================================================ FILE: test/bitfield.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test bitfield': function () { var bitfield = redback.createBitfield('test_bitfield'); bitfield.set(3, 1, function (err, prev) { assert.ok(!prev); bitfield.get(2, function (err, bit) { assert.ok(!bit); }); bitfield.get(3, function (err, bit) { assert.ok(bit); bitfield.set(3, 0, function (err, prev) { assert.ok(prev); }); }); }); } } ================================================ FILE: test/bloomfilter.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test bloom filter#add#single hash': function () { var bloomfilter = redback.createBloomFilter('test_addstructure_bloom_filter_test', 101, 1); bloomfilter.add('test', function(err){ bloomfilter.exists('test', function(err, value){ assert.equal(true, value); }); bloomfilter.exists('this probably does not exist', function(err, value){ assert.equal(false, value); }); }); }, 'test bloom filter#add#multiple hashs': function () { var bloomfilter = redback.createBloomFilter('test_addstructure_bloom_filter_hello', 101, 3); bloomfilter.add('hello', function(err){ bloomfilter.exists('hello', function(err, value){ assert.equal(true, value); }); bloomfilter.exists('this probably does not exist', function(err, value){ assert.equal(false, value); }); }); }, 'test bloom filter#reset': function () { var bloomfilter = redback.createBloomFilter('test_addstructure_bloom_filter_bar', 101, 3); bloomfilter.add('bar', function(err){ bloomfilter.add('baz', function(err){ bloomfilter.exists('bar', function(err, value){ assert.equal(true, value); bloomfilter.reset(function(err){ bloomfilter.exists('bar', function(err, val){ assert.equal(false, val); }); bloomfilter.exists('baz', function(err, val){ assert.equal(false, val); }); }); }); }); }); } } ================================================ FILE: test/cache.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test cache set/get': function () { var cache = redback.createCache('test_cache'); cache.set('foo', 'bar', function (err) { cache.get('foo', function (err, value) { assert.equal('bar', value); }); cache.exists('foo', function (err, exists) { assert.ok(exists); }); cache.exists('nothere', function (err, exists) { assert.ok(!exists); }); cache.add('foo', 'barbar', function (err, added) { assert.ok(!added); }); cache.add('bar', 'foofoo', function (err, added) { assert.ok(added); cache.get('bar', function (err, value) { assert.equal('foofoo', value); }); cache.get('foo', function (err, value) { assert.equal('bar', value); cache.getSet('foo', 'a', function (err, value) { assert.equal('bar', value); cache.get('foo', function (err, value) { assert.equal('a', value); }); }); }); }); }); }, 'test cache multi set/get': function () { var cache = redback.createCache('test_cache_multi'), test_obj = {a:'b',c:'d',e:'f'}; cache.set(test_obj, function (err) { cache.get(['a','c'], function (err, values) { assert.equal('b', values.a); assert.equal('d', values.c); }); cache.get(function (err, values) { for (var i in test_obj) { assert.ok(typeof values[i] !== 'undefined'); assert.equal(test_obj[i], values[i]); } }); cache.add({a:'a', g:'h'}, function (err) { cache.get('a', function (err, value) { assert.equal('b', value); }); cache.get('g', function (err, value) { //assert.equal('h', value); }); }); }); }, 'test cache increment': function () { var cache = redback.createCache('test_cache_increment'); cache.set('foo', 1, 1, function (err) { cache.increment('foo', function (err) { cache.get('foo', function (err, value) { assert.equal(2, value); cache.increment('foo', 5, function (err) { cache.get('foo', function (err, value) { assert.equal(7, value); }); }); }); }); }); }, 'test cache decrement': function () { var cache = redback.createCache('test_cache_decrement'); cache.set('foo', 10, function (err) { cache.decrement('foo', function (err) { cache.get('foo', function (err, value) { assert.equal(9, value); cache.decrement('foo', 5, function (err) { cache.get('foo', function (err, value) { assert.equal(4, value); }); }); }); }); }); }, 'test cache key select': function () { var cache = redback.createCache('test_cache_keys'); cache.set({foo: 'a', foo2: 'b', foo3: 'c', bar: 'd'}, function (err) { cache.keys(function (err, keys) { assert.equal(4, keys.length); assert.ok(keys.indexOf('foo') !== -1); assert.ok(keys.indexOf('foo2') !== -1); assert.ok(keys.indexOf('foo3') !== -1); assert.ok(keys.indexOf('bar') !== -1); }); cache.keys('f*', function (err, keys) { assert.equal(3, keys.length); assert.ok(keys.indexOf('foo') !== -1); assert.ok(keys.indexOf('foo2') !== -1); assert.ok(keys.indexOf('foo3') !== -1); assert.ok(keys.indexOf('bar') === -1); }); cache.keys('b?r', function (err, keys) { assert.equal(1, keys.length); assert.ok(keys.indexOf('foo') === -1); assert.ok(keys.indexOf('bar') !== -1); }); }); }, 'test cache key flush all': function () { var cache = redback.createCache('test_cache_flush'); cache.set({foo: 'a', foo2: 'b', foo3: 'c', bar: 'd'}, function (err) { cache.flush(function (err) { cache.keys(function (err, keys) { assert.equal(0, keys.length); }); }); }); }, 'test cache key flush by pattern': function () { var cache = redback.createCache('test_cache_flush_pattern'); cache.set({foo: 'a', foo2: 'b', foo3: 'c', bar: 'd'}, function (err) { cache.flush('f*', function (err) { cache.keys(function (err, keys) { assert.equal(1, keys.length); assert.ok(keys.indexOf('foo') === -1); assert.ok(keys.indexOf('foo2') === -1); assert.ok(keys.indexOf('foo3') === -1); assert.ok(keys.indexOf('bar') !== -1); }); }); }); }, /* 'test cache expiries': function () { var cache = redback.createCache('test_cache_expiries'); cache.set({foo: 'a', foo2: 'b', foo3: 'c', bar: 'd'}, function (err) { cache.expire('foo', 1, function (err) { cache.ttl('foo', function (err, ttl) { assert.equal(1, ttl); cache.persist('foo', function (err) { cache.ttl('foo', function (err, ttl) { assert.equal(-1, ttl); }); }); }); }); var when = new Date(); cache.expireAt('foo2', when, function (err) { cache.ttl('foo2', function (err, ttl) { assert.equal(1, ttl); }); }); }); } */ } ================================================ FILE: test/cappedlist.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test capped list keeping the latest': function() { var list = redback.createCappedList('test_cappedlist', 3); //Only the latest 3 items are kept list.push(['a','b','c','d','e','f'], function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('d', values.shift()); assert.equal('e', values.shift()); assert.equal('f', values.shift()); list.push(['g','h'], function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('f', values.shift()); assert.equal('g', values.shift()); assert.equal('h', values.shift()); }); }); }); }); }, 'test capped list length': function () { var list = redback.createCappedList('test_cappedlist_foo', 3); list.push(['a','b','c'], function (err) { for (var i = 0; i < 10; i++) { list.push(i, function (err) { list.values(function (err, values) { assert.equal(3, values.length); }); }); } }); }, 'test capped list insert before': function () { var list = redback.createCappedList('test_cappedlist_insertbefore', 3); list.push(['a','b','c'], function (err) { list.insertBefore('a', 'z', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); assert.equal('c', values.shift()); list.insertBefore('c', 'x', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('b', values.shift()); assert.equal('x', values.shift()); assert.equal('c', values.shift()); }); }); }); }); }); }, 'test capped list insert after': function () { var list = redback.createCappedList('test_cappedlist_insertafter', 3); list.push(['a','b','c'], function (err) { list.insertAfter('a', 'z', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('z', values.shift()); assert.equal('b', values.shift()); assert.equal('c', values.shift()); list.insertAfter('c', 'x', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('b', values.shift()); assert.equal('c', values.shift()); assert.equal('x', values.shift()); }); }); }); }); }); }, 'test capped list unshift': function () { var list = redback.createCappedList('test_cappedlist_unshift', 3); list.unshift(['a','b'], function (err) { list.unshift('c', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('c', values.shift()); assert.equal('a', values.shift()); assert.equal('b', values.shift()); list.unshift('z', function (err) { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('z', values.shift()); assert.equal('c', values.shift()); assert.equal('a', values.shift()); }); }); }); }); }); } } ================================================ FILE: test/channel.test.js ================================================ var redback = require('./common').createClient(), client2 = require('./common').createClient(), assert = require('assert'); //Close the Redis connection after 500ms setTimeout(function () { redback.client.quit(); client2.client.quit(); }, 500); module.exports = { 'test channels': function () { var channel = redback.createChannel('test_channel'), received = false; channel.on('message', function (msg) { assert.equal('foo', msg); if (msg != 'foo') { assert.ok(false); } received = true; }); channel.subscribe(function () { //Bind another client so it doesn't affect the other tests channel.setClient(client2.client); channel.publish('foo', function (err) { assert.ok(true); }); }); setTimeout(function () { channel.unsubscribe(function(){ assert.ok(true); }); assert.ok(received); }, 200); } } ================================================ FILE: test/common.js ================================================ var redback = require('../'); exports.createClient = function (options) { return redback.createClient('redis://localhost/11', options); }; ================================================ FILE: test/crc32.test.js ================================================ var crc32 = require('../lib/Utils').crc32, assert = require('assert'); module.exports = { 'test crc32': function () { assert.equal(1938594527, crc32('foo')); }, 'test crc32 for 123456789': function () { assert.equal(873187034, crc32('123456789')); } } ================================================ FILE: test/densityset.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test density set': function () { var zset = redback.createDensitySet('test_densityset_add'); zset.add('foo', function (err, was_new) { assert.ok(was_new); zset.length(function (err, length) { assert.equal(1, length); }); zset.contains('foo', function (err, contains) { assert.ok(contains); }); zset.contains('nothere', function (err, contains) { assert.ok(!contains); }); zset.score('foo', function (err, score) { assert.equal(1, score); zset.rank('foo', function (err, rank) { assert.equal(0, rank); zset.add('foo', function (err, was_new) { assert.ok(was_new); zset.score('foo', function (err, score) { assert.equal(2, score); zset.remove('foo', function (err) { zset.score('foo', function (err, score) { assert.equal(1, score); zset.remove('foo', function (err, removed) { assert.ok(removed); zset.score('foo', function (err, score) { assert.ok(score == null); }); }); }); }); }); }); }); }); }); }, 'test density set multi add': function () { var zset = redback.createDensitySet('test_densityset_multi_add'); zset.add(['foo','foo','foo','bar','bar','foobar'], function (err, added) { assert.equal(6, added); zset.score('foo', function (err, score) { assert.equal(3, score); }); zset.rank('foo', function (err, rank) { assert.equal(2, rank); }); zset.score('bar', function (err, score) { assert.equal(2, score); }); zset.rank('bar', function (err, rank) { assert.equal(1, rank); }); zset.score('foobar', function (err, score) { assert.equal(1, score); }); zset.rank('foobar', function (err, rank) { assert.equal(0, rank); }); zset.length(function (err, length) { assert.equal(3, length); }); }); }, 'test sorted set multi remove': function () { var zset = redback.createDensitySet('test_densityset_multi_remove'); zset.add(['foo','foo','foo','bar','bar','foobar'], function (err, added) { assert.equal(6, added); zset.remove(['foo','bar','foobar'], function (err) { zset.score('foo', function (err, score) { assert.equal(2, score); }); zset.rank('foo', function (err, rank) { assert.equal(1, rank); }); zset.score('bar', function (err, score) { assert.equal(1, score); }); zset.rank('bar', function (err, rank) { assert.equal(0, rank); }); zset.score('foobar', function (err, score) { assert.equal(null, score); }); zset.rank('foobar', function (err, rank) { assert.equal(null, rank); }); zset.length(function (err, length) { assert.equal(2, length); }); }); }); } } ================================================ FILE: test/hash.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test simple hash set/get': function() { var hash = redback.createHash('test_hash'); //Test simple get/set hash.set('foo', 'bar', function (err) { hash.get('foo', function (err, val) { assert.equal('bar', val); }); hash.exists('foo', function (err, exists) { assert.ok(exists); }); hash.exists('nonexistent', function (err, exists) { assert.ok(!exists); }); }); }, 'test multi hash set/get': function() { var hash = redback.createHash('test_hash_multi'); //Test multi set/get hash.set({a: 'b', c: 'd'}, function (err) { hash.get(['a','c'], function (err, values) { assert.equal(2, values.length); assert.equal('b', values.shift()); assert.equal('d', values.shift()); }); hash.get(function (err, values) { assert.equal('b', values.a); assert.equal('d', values.c); }); hash.values(function (err, values) { assert.equal(2, values.length); assert.equal('b', values.shift()); assert.equal('d', values.shift()); }); hash.keys(function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('c', values.shift()); }); hash.length(function (err, length) { assert.equal(2, length); }); }); }, 'test hash aggregate': function() { var hash = redback.createHash('test_hash_aggregate'); //Test multi set/get hash.set({a: 'b', c: 'd'}, function (err) { hash.values(function (err, values) { assert.equal(2, values.length); assert.equal('b', values.shift()); assert.equal('d', values.shift()); }); hash.keys(function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('c', values.shift()); }); hash.length(function (err, length) { assert.equal(2, length); }); }); }, 'test hash add': function() { var hash = redback.createHash('test_hash_add'); //Test multi set/get hash.set('foo', 'bar', function (err) { assert.equal(null, err); hash.add('foo', 'foo', function (err) { assert.equal(null, err); hash.get('foo', function (err, foo) { assert.equal('bar', foo); }); }); }); }, 'test hash add': function() { var hash = redback.createHash('test_hash_add'); //Test multi set/get hash.set('foo', 'bar', function (err) { assert.equal(null, err); //'foo' is already set.. hash.add('foo', 'foo', function (err) { assert.equal(null, err); hash.get('foo', function (err, foo) { assert.equal('bar', foo); //Try deleting a hash key hash.delete('foo', function (err) { assert.equal(null, err); hash.get('foo', function (err, foo) { assert.equal(null, foo); }); }); }); }); }); }, 'test hash increment / decrement': function() { var hash = redback.createHash('test_hash_incrdecr'); //Test increment hash.set('foo', 1, function (err) { hash.increment('foo', 5, function (err) { assert.equal(null, err); hash.get('foo', function (err, foo) { assert.equal(6, foo); //The increment amount is optional hash.increment('foo', function (err) { assert.equal(null, err); hash.get('foo', function (err, foo) { assert.equal(7, foo); }); }); }); }); }); //Test decrement hash.set('bar', 10, function (err) { hash.decrement('bar', 5, function (err) { assert.equal(null, err); hash.get('bar', function (err, foo) { assert.equal(5, foo); //The decrement amount is optional hash.decrement('bar', function (err) { assert.equal(null, err); hash.get('bar', function (err, foo) { assert.equal(4, foo); }); }); }); }); }); } } ================================================ FILE: test/keypair.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test keypair set': function () { var keypair = redback.createKeyPair('test_keypair'); keypair.add('foo', function (err, id) { assert.equal(1, id); keypair.add('bar', function (err, id) { assert.equal(2, id); keypair.add('bar', function (err, id) { assert.equal(2, id); }); keypair.length(function (err, length) { assert.equal(2, length); }); keypair.get(function (err, pairs) { assert.equal('foo', pairs[1]); assert.equal('bar', pairs[2]); }); keypair.get('bar', function (err, id) { assert.equal(2, id); }); keypair.get(['foo','bar'], function (err, ids) { assert.equal(2, ids.length); assert.equal(1, ids.shift()); assert.equal(2, ids.shift()); }); keypair.getById(2, function (err, value) { assert.equal('bar', value); }); keypair.getById([1, 2], function (err, values) { assert(Array.isArray(values)); assert.equal(values.length, 2); assert.equal('foo', values.shift()); assert.equal('bar', values.shift()); }); keypair.get('nothere', function (err, id) { assert.equal(null, id); }); keypair.values(function (err, values) { assert.equal(2, values.length); assert.equal('foo', values.shift()); assert.equal('bar', values.shift()); }); keypair.ids(function (err, ids) { assert.equal(2, ids.length); assert.equal(1, ids.shift()); assert.equal(2, ids.shift()); }); keypair.exists('bar', function (err, exists) { assert.ok(exists); }); keypair.exists('nothere', function (err, exists) { assert.ok(!exists); }); keypair.idExists(2, function (err, exists) { assert.ok(exists); }); keypair.idExists(5, function (err, exists) { assert.ok(!exists); }); }); }); }, 'test adding multiple values to a keypair': function () { var keypair = redback.createKeyPair('test_keypair_multi'); keypair.add(['a','b','c','d'], function (err, ids) { assert.equal(1, ids.a); assert.equal(2, ids.b); assert.equal(3, ids.c); assert.equal(4, ids.d); keypair.delete('a', function (err) { keypair.values(function (err, values) { assert.equal(3, values.length); assert.equal('b', values.shift()); assert.equal('c', values.shift()); assert.equal('d', values.shift()); keypair.deleteById(4, function (err) { keypair.values(function (err, values) { assert.equal(2, values.length); assert.equal('b', values.shift()); assert.equal('c', values.shift()); }); }); }); }); }); }, 'test uniqueness of keypair values': function () { var keypair = redback.createKeyPair('test_keypair_unique'), count = 50, returned = 0; //This is a test of atomicity - even when pipelining add() calls, //values should **ALWAYS** be unique. Only one 'foo' should be added while (count--) { keypair.add('foo', function () { if (returned++ == count) { keypair.length(function (err, length) { assert.equal(1, length); }); } }); } } } ================================================ FILE: test/list.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test list push / pop': function() { var list = redback.createList('test_list_pushpop'); list.push('foo', function (err) { list.push('bar', function (err) { list.values(function (err, values) { assert.equal(2, values.length); assert.equal('foo', values.shift()); assert.equal('bar', values.shift()); list.push(['a','b'], function (err) { list.values(function (err, values) { assert.equal(4, values.length); assert.equal('foo', values.shift()); assert.equal('bar', values.shift()); assert.equal('a', values.shift()); assert.equal('b', values.shift()); list.pop(function (err, value) { assert.equal('b', value); list.length(function (err, length) { assert.equal(3, length); }); }); }); }); }); }); }); }, 'test list shift': function() { var list = redback.createList('test_list_shiftunshift'); list.unshift('foo', function (err) { list.unshift('bar', function (err) { list.values(function (err, values) { assert.equal(2, values.length); assert.equal('bar', values.shift()); assert.equal('foo', values.shift()); list.unshift(['a','b'], function (err) { list.values(function (err, values) { assert.equal(4, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); assert.equal('bar', values.shift()); assert.equal('foo', values.shift()); list.shift(function (err, value) { assert.equal('a', value); list.length(function (err, length) { assert.equal(3, length); }); }); }); }); }); }); }); }, 'test list range': function () { var list = redback.createList('test_list_range'); list.unshift(['a','b','c','d','e'], function () { list.range(2, function (err, values) { assert.equal(3, values.length); assert.equal('c', values.shift()); assert.equal('d', values.shift()); assert.equal('e', values.shift()); }); list.range(0, 1, function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); }); list.range(-2, function (err, values) { assert.equal(2, values.length); assert.equal('d', values.shift()); assert.equal('e', values.shift()); }); }); }, 'test list get': function () { var list = redback.createList('test_list_get'); list.unshift(['a','b','c','d','e'], function () { list.get(1, function (err, value) { assert.equal('b', value); }); list.get(-1, function (err, value) { assert.equal('e', value); }); list.get(0, 2, function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); }); list.get(-2, 2, function (err, values) { assert.equal(2, values.length); assert.equal('d', values.shift()); assert.equal('e', values.shift()); }); }); }, 'test list cap - keep latest': function () { var list = redback.createList('test_list_cap_latest'); list.unshift(['a','b','c','d','e'], function () { list.cap(3, function () { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('c', values.shift()); assert.equal('d', values.shift()); assert.equal('e', values.shift()); }); }); }); }, 'test list cap - keep earliest': function () { var list = redback.createList('test_list_cap_earliest'); list.unshift(['a','b','c','d','e'], function () { list.cap(3, true, function () { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); assert.equal('c', values.shift()); }); }); }); }, 'test list trim': function () { var list = redback.createList('test_list_trim'); list.unshift(['a','b','c','d','e'], function () { list.trim(1, 3, function () { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('b', values.shift()); assert.equal('c', values.shift()); assert.equal('d', values.shift()); }); }); }); }, 'test list set': function () { var list = redback.createList('test_list_set'); list.unshift(['a','b','c','d','e'], function () { list.set(1, 'foo', function () { list.get(0, 3, function (err, values) { assert.equal(3, values.length); assert.equal('a', values.shift()); assert.equal('foo', values.shift()); assert.equal('c', values.shift()); }); }); }); }, 'test list insert': function () { var list = redback.createList('test_list_insert'); list.unshift(['a','b','c','d','e'], function () { list.insertAfter('a', 'foo', function (err) { list.get(0, 3, function (err, values) { assert.equal(3, values.length); assert.equal('a', values.shift()); assert.equal('foo', values.shift()); assert.equal('b', values.shift()); }); }); list.insertBefore('e', 'foo', function (err) { list.get(-3, 3, function (err, values) { assert.equal(3, values.length); assert.equal('d', values.shift()); assert.equal('foo', values.shift()); assert.equal('e', values.shift()); }); }); }); }, 'test list remove A': function () { var list = redback.createList('test_list_remove_a'); list.unshift(['a','b','a','b','c'], function () { list.remove('a', function () { list.values(function (err, values) { assert.equal(4, values.length); assert.equal('b', values.shift()); assert.equal('a', values.shift()); assert.equal('b', values.shift()); assert.equal('c', values.shift()); }); }); }); }, 'test list remove B': function () { var list = redback.createList('test_list_remove_b'); list.unshift(['a','b','a','b','c'], function () { list.remove('a', -1, function () { list.values(function (err, values) { assert.equal(4, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); assert.equal('b', values.shift()); assert.equal('c', values.shift()); }); }); }); }, 'test list remove C': function () { var list = redback.createList('test_list_remove_c'); list.unshift(['a','b','a','b','c'], function () { list.remove('b', 2, function () { list.values(function (err, values) { assert.equal(3, values.length); assert.equal('a', values.shift()); assert.equal('a', values.shift()); assert.equal('c', values.shift()); }); }); }); }, 'test popshift': function () { var src = redback.createList('test_list_popshift_a'); var dest = redback.createList('test_list_popshift_b'); src.unshift(['a','b','c'], function () { src.popShift(dest, function () { src.values(function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); }); dest.values(function (err, values) { assert.equal(1, values.length); assert.equal('c', values.shift()); }); }); }); }, 'test popshift B': function () { var src = redback.createList('test_list_popshift_c'); var dest = redback.createList('test_list_popshift_d'); src.unshift(['a','b','c'], function () { //popShift can also take the actual key string src.popShift('test_list_popshift_d', function () { src.values(function (err, values) { assert.equal(2, values.length); assert.equal('a', values.shift()); assert.equal('b', values.shift()); }); dest.values(function (err, values) { assert.equal(1, values.length); assert.equal('c', values.shift()); }); }); }); }, /* 'test blocking list operations - pop': function () { var list = redback.createList('test_list_blocking_pop'), added = false; list.pop(1, function (err, value) { if (!added) return assert.ok(false); assert.equal('foo', value); }); setTimeout(function () { list.push('foo'); added = true; }, 100); }, 'test blocking list operations - shift': function () { var list = redback.createList('test_list_blocking_shift'), added = false; list.shift(1, function (err, value) { if (!added) return assert.ok(false); assert.equal('foo', value); }); setTimeout(function () { list.push('foo'); added = true; }, 100); } */ } ================================================ FILE: test/lock.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); // Flush the DB and close the Redis connection after 2 seconds setTimeout(function () { // Ensure we completed all tests assert.strictEqual(_testsCompleted, Object.keys(module.exports).length); redback.client.quit(); }, 2000); var TEST_LOCK = 'test lock'; var _testsCompleted = 0; var _complete = function() { _testsCompleted++; }; module.exports = { 'test lock cannot be stolen': function() { var key = 'test-lock-cannot-be-stolen'; var lock = redback.createLock(TEST_LOCK); lock.acquire(key, 1, function(err, token) { assert.ok(!err); assert.ok(token); // Ensure we don't get a token the next time we try and acquire lock.acquire(key, 1, function(err, token) { assert.ok(!err); assert.ok(!token); _complete(); }); }); }, 'test lock can be re-acquired after release': function() { var key = 'test-lock-can-be-re-acquired-after-released'; var lock = redback.createLock(TEST_LOCK); lock.acquire(key, 1, function(err, token) { assert.ok(!err); assert.ok(token); lock.release(key, token, function(err, hadLock) { assert.ok(!err); assert.ok(hadLock); // Ensure we do get a token the next time we try and acquire lock.acquire(key, 1, function(err, token) { assert.ok(!err); assert.ok(token); _complete(); }); }); }); }, 'test lock release indicates when releasing with invalid token': function() { var key = 'test-lock-release-indicates-when-releasing-with-invalid-token'; var lock = redback.createLock(TEST_LOCK); lock.acquire(key, 1, function(err, token) { assert.ok(!err); assert.ok(token); // Ensure that releasing with an invalid token indicates we did not // have the lock lock.release(key, 'not the token', function(err, hadLock) { assert.ok(!err); assert.ok(!hadLock); lock.release(key, token, function(err, hadLock) { assert.ok(!err); assert.ok(hadLock); // Ensure re-release indicates something is wrong lock.release(key, token, function(err, hadLock) { assert.ok(!err); assert.ok(!hadLock); _complete(); }); }); }); }); }, 'test lock can be re-acquired after expiry': function() { var key = 'test-lock-can-be-re-acquired-after-expiry'; var lock = redback.createLock(TEST_LOCK); lock.acquire(key, 1, function(err, firstToken) { assert.ok(!err); assert.ok(firstToken); setTimeout(function() { // Ensure we can acquire the lock again lock.acquire(key, 1, function(err, secondToken) { assert.ok(!err); assert.ok(secondToken); // Ensure we cannot release it with the old token lock.release(key, firstToken, function(err, hadLock) { assert.ok(!err); assert.ok(!hadLock); // Ensure we cannot re-acquire since it is still held // by secondToken lock.acquire(key, 1, function(err, thirdToken) { assert.ok(!err); assert.ok(!thirdToken); // Ensure we can successfully release the lock with // the `secondToken` lock.release(key, secondToken, function(err, hadLock) { assert.ok(!err); assert.ok(hadLock); _complete(); }); }); }); }); }, 1250); }); }, 'test concurrent locks results in only one acquisition': function() { var key = 'test-concurrent-locks-results-in-only-one-acquisition'; var lock = redback.createLock(TEST_LOCK); var numAcquired = 0; var numCompleted = 0; var iterations = 10; var acquireComplete = function(err, token) { numCompleted++; if (token) { numAcquired++; } if (numCompleted === iterations) { assert.strictEqual(numAcquired, 1); _complete(); } }; for (var i = 0; i < iterations; i++) { lock.acquire(key, 1, acquireComplete); } } }; ================================================ FILE: test/namespace.test.js ================================================ var redback = require('./common').createClient({namespace: 'foo'}), assert = require('assert'); module.exports = { 'test namespaces': function() { assert.equal('foo', redback.namespace); var channel = redback.createChannel('channel__'); assert.equal('foo:channel__', channel.name); var cache = redback.createCache('cache__'); assert.equal('foo:cache__', cache.namespace); var list = redback.createList('list__'); assert.equal('foo', list.namespace); assert.equal('foo:bar', list.namespaceKey('bar')); redback.client.quit(); }, } ================================================ FILE: test/queue.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test lifo queue': function () { var lifo = redback.createQueue('test_lifo_queue'); lifo.add('foo', function (err) { lifo.add('bar', function (err) { lifo.next(function (err, value) { assert.equal('bar', value); lifo.next(function (err, value) { assert.equal('foo', value); lifo.next(function (err, value) { assert.equal(null, value); }); }); }); }); }); }, 'test fifo queue': function () { var fifo = redback.createQueue('test_fifo_queue', true); fifo.enqueue('foo', function (err) { fifo.enqueue('bar', function (err) { fifo.dequeue(function (err, value) { assert.equal('foo', value); fifo.dequeue(function (err, value) { assert.equal('bar', value); fifo.dequeue(function (err, value) { assert.equal(null, value); }); }); }); }); }); } } ================================================ FILE: test/set.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test set': function () { var set = redback.createSet('test_set'); set.add('foo', function (err, added) { assert.ok(added); set.add('bar', function (err, added) { assert.ok(added); set.elements(function (err, values) { assert.equal(2, values.length); values = values.sort(function (a, b) { return a > b ? -1 : 1; }); assert.equal('foo', values.shift()); assert.equal('bar', values.shift()); }); set.add('foo', function (err, added) { assert.ok(!added); }); set.exists('foo', function (err, exists) { assert.ok(exists); }); set.exists('nothere', function (err, exists) { assert.ok(!exists); }); set.remove('nothere', function (err, removed) { assert.ok(!removed); }); for (var i = 0; i < 5; i++) { set.random(function (err, element) { assert.ok(['foo','bar'].indexOf(element) !== -1); }); } set.length(function (err, length) { assert.equal(2, length); }); }); }); }, 'test multi add/remove': function () { var set = redback.createSet('test_set_multi'); set.add(['a','b','c','d'], function (err, added) { set.elements(function (err, values) { assert.equal(4, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); assert.ok(values.indexOf('d') !== -1); set.remove('a', function (err, removed) { assert.ok(removed); set.elements(function (err, values) { assert.equal(3, values.length); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); assert.ok(values.indexOf('d') !== -1); set.remove(['b','c'], function (err, removed) { assert.ok(removed); set.elements(function (err, values) { assert.equal(1, values.length); assert.equal('d', values[0]); }); }); }); }); }); }); }, 'test popping a set element': function () { var set = redback.createSet('test_set_pop'), values = ['a','b','c']; set.add(values, function (err, added) { assert.ok(added); set.random(true, function (err, element) { assert.ok(values.indexOf(element) !== -1); set.random(true, function (err, element) { assert.ok(values.indexOf(element) !== -1); set.random(true, function (err, element) { assert.ok(values.indexOf(element) !== -1); set.random(true, function (err, element) { assert.ok(!element); }); }); }); }); }); }, 'test set move': function () { var src = redback.createSet('test_set_move_src'), dest = redback.createSet('test_set_move_dest'); src.add(['a','b','c'], function (err, added) { assert.ok(added); src.move(dest, 'a', function (err, moved) { assert.ok(moved); src.length(function (err, length) { assert.equal(2, length); }); dest.elements(function (err, values) { assert.equal(1, values.length); assert.equal('a', values[0]); }); }); }); }, 'test set intersect': function () { var set1 = redback.createSet('test_set_inter1'), set2 = redback.createSet('test_set_inter2'), set3 = redback.createSet('test_set_inter3'); set1.add(['a','b','c','d'], function (err, added) { assert.ok(added); set2.add(['b','c','d','e'], function (err, added) { assert.ok(added); set3.add(['1','2','3','a','b','c'], function (err, added) { assert.ok(added); set1.inter(set2, set3, function (err, values) { assert.equal(2, values.length); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); }); set1.interStore('test_set_inter_store', set2, set3, function (err, stored) { assert.ok(stored); redback.createSet('test_set_inter_store').elements(function (err, values) { assert.equal(2, values.length); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); }); }); }); }); }); }, 'test set union': function () { var set1 = redback.createSet('test_set_union1'), set2 = redback.createSet('test_set_union2'), set3 = redback.createSet('test_set_union3'); set1.add(['a','b','c'], function (err, added) { assert.ok(added); set2.add(['b','c','d'], function (err, added) { assert.ok(added); set3.add(['1','2','3'], function (err, added) { assert.ok(added); set1.union(set2, set3, function (err, values) { assert.equal(7, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); assert.ok(values.indexOf('d') !== -1); assert.ok(values.indexOf('1') !== -1); assert.ok(values.indexOf('2') !== -1); assert.ok(values.indexOf('3') !== -1); }); set1.unionStore('test_set_union_store', set2, set3, function (err, stored) { assert.ok(stored); redback.createSet('test_set_union_store').elements(function (err, values) { assert.equal(7, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('b') !== -1); assert.ok(values.indexOf('c') !== -1); assert.ok(values.indexOf('d') !== -1); assert.ok(values.indexOf('1') !== -1); assert.ok(values.indexOf('2') !== -1); assert.ok(values.indexOf('3') !== -1); }); }); }); }); }); }, 'test set diff': function () { var set1 = redback.createSet('test_set_diff1'), set2 = redback.createSet('test_set_diff2'), set3 = redback.createSet('test_set_diff3'); set1.add(['a','b','c','d','e','f'], function (err, added) { assert.ok(added); set2.add(['b','c','d'], function (err, added) { assert.ok(added); set3.add(['c','d','e'], function (err, added) { assert.ok(added); set1.diff(set2, set3, function (err, values) { assert.equal(2, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('f') !== -1); }); set1.diff([set2, set3], function (err, values) { assert.equal(2, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('f') !== -1); }); set1.diffStore('test_set_diff_store', set2, set3, function (err, stored) { assert.ok(stored); redback.createSet('test_set_diff_store').elements(function (err, values) { assert.equal(2, values.length); assert.ok(values.indexOf('a') !== -1); assert.ok(values.indexOf('f') !== -1); }); }); }); }); }); } } ================================================ FILE: test/socialgraph.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test social graph': function () { var user1 = redback.createSocialGraph(1, 'test_social_graph'), user2 = redback.createSocialGraph(2, 'test_social_graph'), user3 = redback.createSocialGraph(3, 'test_social_graph'); user1.follow(user2, user3, function (err) { user3.follow(1, 2, function (err) { user2.follow([1], function (err) { user1.getFollowers(function (err, followers) { var expected = [2,3], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user1.countFollowers(function (err, count) { assert.equal(2, count); }); user1.getFollowing(function (err, followers) { var expected = [2,3], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user1.countFollowing(function (err, count) { assert.equal(2, count); }); user2.getFollowers(function (err, followers) { var expected = [1,3], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user2.countFollowers(function (err, count) { assert.equal(2, count); }); user2.getFollowing(function (err, followers) { var expected = [1], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user2.countFollowing(function (err, count) { assert.equal(1, count); }); user3.getFollowers(function (err, followers) { var expected = [1], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user3.countFollowers(function (err, count) { assert.equal(1, count); }); user3.getFollowing(function (err, followers) { var expected = [1,2], l = expected.length; while (l--) assert.equal(expected.shift(), followers.shift()); }); user3.countFollowing(function (err, count) { assert.equal(2, count); }); user3.hasFollower(1, function (err, has) { assert.ok(has); }); user3.hasFollower(2, function (err, has) { assert.ok(!has); }); user3.isFollowing(1, function (err, following) { assert.ok(following); }); user3.isFollowing(2, function (err, following) { assert.ok(following); }); user3.isFollowing(4, function (err, following) { assert.ok(!following); }); }); }); }); }, 'test social graph unfollow': function () { var user1 = redback.createSocialGraph(1, 'test_social_graph_rem'), user2 = redback.createSocialGraph(2, 'test_social_graph_rem'), user3 = redback.createSocialGraph(3, 'test_social_graph_rem'); user1.follow([2,3,4,5,6,7], function (err) { user3.follow(1, 2, 4, 5, 6, function (err) { user2.follow([1], function (err) { user2.unfollow(1, function (err) { user2.getFollowing(function (err, following) { assert.equal(0, following); }); }); user3.unfollow(4, 5, 6, function (err) { user3.getFollowing(function (err, following) { var expected = [1,2], l = expected.length; while (l--) assert.equal(expected.shift(), following.shift()); user1.unfollow([user2, user3, 4, 5], function (err) { user1.getFollowing(function (err, following) { var expected = [6,7], l = expected.length; while (l--) assert.equal(expected.shift(), following.shift()); user2.getFollowers(function (err, followers) { assert.equal(1, followers.length); assert.equal(3, followers[0]); }); }); }); }); }); }); }); }); }, 'test social graph get commong followers/following': function () { var user1 = redback.createSocialGraph(1, 'test_social_graph_common'), user2 = redback.createSocialGraph(2, 'test_social_graph_common'), user3 = redback.createSocialGraph(3, 'test_social_graph_common'), user8 = redback.createSocialGraph(8, 'test_social_graph_common'), user9 = redback.createSocialGraph(9, 'test_social_graph_common'); user1.follow([2,3,4,5,6,7,8,9,10], function (err) { user2.follow([1,3,5,7,9], function (err) { user3.follow([2,5,6,7,8], function (err) { user1.getCommonFollowing(user2, function (err, common) { var expected = [3,5,7], l = expected.length; while (l--) assert.equal(expected.shift(), common.shift()); }); user1.getCommonFollowing([user2, 3], function (err, common) { var expected = [5,7], l = expected.length; while (l--) assert.equal(expected.shift(), common.shift()); }); user1.getCommonFollowers(9, function (err, common) { var expected = [2], l = expected.length; while (l--) assert.equal(expected.shift(), common.shift()); }); user2.getDifferentFollowing(user3, function (err, diff) { var expected = [1,3], l = expected.length; while (l--) assert.equal(expected.shift(), diff.shift()); }); user2.getDifferentFollowers(9, function (err, diff) { var expected = [3], l = expected.length; while (l--) assert.equal(expected.shift(), diff.shift()); }); }); }); }); } } ================================================ FILE: test/sortedset.test.js ================================================ var redback = require('./common').createClient(), assert = require('assert'); //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); module.exports = { 'test sorted set': function () { var zset = redback.createSortedSet('test_zset_add'); zset.add(12, 'foo', function (err, was_new) { assert.ok(was_new); zset.length(function (err, length) { assert.equal(1, length); }); zset.contains('foo', function (err, contains) { assert.ok(contains); }); zset.contains('nothere', function (err, contains) { assert.ok(!contains); }); zset.score('foo', function (err, score) { assert.equal(12, score); zset.rank('foo', function (err, rank) { assert.equal(0, rank); zset.add(3, 'foo', function (err, was_new) { assert.ok(!was_new); zset.score('foo', function (err, score) { assert.equal(3, score); zset.remove('foo', function (err, removed) { assert.ok(removed); zset.score('foo', function (err, score) { assert.ok(score == null); }); zset.rank('foo', function (err, rank) { assert.ok(rank == null); }); }); }); }); }); }); }); }, 'test sorted set multi add': function () { var zset = redback.createSortedSet('test_zset_multi_add'); zset.add({foo:1, bar:2, foobar:3}, function (err) { zset.score('foo', function (err, score) { assert.equal(1, score); }); zset.rank('foo', function (err, rank) { assert.equal(0, rank); }); zset.score('bar', function (err, score) { assert.equal(2, score); }); zset.rank('bar', function (err, rank) { assert.equal(1, rank); }); zset.score('foobar', function (err, score) { assert.equal(3, score); }); zset.rank('foobar', function (err, rank) { assert.equal(2, rank); }); zset.length(function (err, length) { assert.equal(3, length); }); }); }, 'test sorted set multi remove': function () { var zset = redback.createSortedSet('test_zset_multi_remove'); zset.add({foo:1, bar:2, foobar:3}, function (err, added) { assert.equal(3, added); zset.remove(['foo','bar'], function (err) { zset.score('foo', function (err, score) { assert.ok(score == null); }); zset.rank('foo', function (err, rank) { assert.ok(rank == null); }); zset.score('bar', function (err, score) { assert.ok(score == null); }); zset.rank('bar', function (err, rank) { assert.ok(rank == null); }); zset.score('foobar', function (err, score) { assert.equal(3, score); }); zset.rank('foobar', function (err, rank) { assert.equal(0, rank); }); zset.length(function (err, length) { assert.equal(1, length); }); }); }); }, 'test sorted set increment decrement': function () { var zset = redback.createSortedSet('test_zset_incdec'); zset.add({foo:1, bar:2, foobar:3, barfoo: 4}, function (err) { zset.increment('foo', function (err) { zset.score('foo', function (err, score) { assert.equal(2, score); }); }); zset.increment('bar', 7, function (err) { zset.score('bar', function (err, score) { assert.equal(9, score); }); }); zset.decrement('foobar', function (err) { zset.score('foobar', function (err, score) { assert.equal(2, score); }); }); zset.decrement('barfoo', 2, function (err) { zset.score('barfoo', function (err, score) { assert.equal(2, score); }); }); }); }, 'test sorted set get scores / ranks': function () { var zset = redback.createSortedSet('test_zset_scores'); zset.add({foo:1, bar:2, foobar:3}, function (err) { zset.get(function (err, set) { var expected = ['foo','bar','foobar'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(3, set.foobar); assert.equal(2, set.bar); assert.equal(1, set.foo); }); zset.get(true, function (err, set) { var expected = ['foo','bar','foobar'], i, l = 3; while (--l) { assert.equal(expected.shift(), set.shift()); } }); zset.getScores(redback.NINF, 2, function (err, set) { var expected = ['foo','bar'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(1, set.foo); assert.equal(2, set.bar); }); zset.getScores(null, 2, 1, function (err, set) { for (var i in set) { assert.equal('foo', i); } assert.equal(1, set.foo); }); zset.getScores(null, 2, 1, 1, function (err, set) { for (var i in set) { assert.equal('bar', i); } assert.equal(2, set.bar); }); zset.getScoresReverse(2, null, 1, function (err, set) { for (var i in set) { assert.equal('bar', i); } assert.equal(2, set.bar); }); zset.getScoresReverse(2, null, 1, 1, function (err, set) { for (var i in set) { assert.equal('foo', i); } assert.equal(1, set.foo); }); zset.getScoresReverse(2, redback.NINF, function (err, set) { var expected = ['bar','foo'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(1, set.foo); assert.equal(2, set.bar); }); zset.getScoresReverse(3, 2, function (err, set) { var expected = ['foobar','bar'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(3, set.foobar); assert.equal(2, set.bar); }); zset.countScores(1, 3, function (err, count) { assert.equal(3, count); }); zset.countScores(5, null, function (err, count) { assert.equal(0, count); }); zset.getScores(2, 3, function (err, set) { var expected = ['bar','foobar'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(3, set.foobar); assert.equal(2, set.bar); }); zset.getRanks(0, 1, function (err, set) { var expected = ['foo','bar'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(2, set.bar); assert.equal(1, set.foo); }); zset.getRanksReverse(2, 0, function (err, set) { var expected = ['foobar','bar','foo'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(3, set.foobar); assert.equal(2, set.bar); assert.equal(1, set.foo); }); }); }, 'test sorted set remove by score and rank': function () { var zset = redback.createSortedSet('test_zset_remove'); zset.add({a:1,b:2,c:3,d:4,e:5,f:6,g:7,h:8,i:9}, function (err) { zset.get(true, function (err, values) { var expected = ['a','b','c','d','e','f','g','h','i']; for (var i = 0, l = expected.length; i < l; i++) { assert.equal(expected.shift(), values.shift()); } zset.removeScores(1, 3, function (err) { zset.get(true, function (err, values) { var expected = ['d','e','f','g','h','i']; for (var i = 0, l = expected.length; i < l; i++) { assert.equal(expected.shift(), values.shift()); } zset.removeRanks(-3, -1, function (err) { zset.get(true, function (err, values) { var expected = ['d','e','f']; for (var i = 0, l = expected.length; i < l; i++) { assert.equal(expected.shift(), values.shift()); } zset.highestScores(2, function (err, set) { var expected = ['e','f'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(5, set.e); assert.equal(6, set.f); }); zset.lowestScores(2, function (err, set) { var expected = ['d','e'], i; for (i in set) { assert.equal(expected.shift(), i); } assert.equal(4, set.d); assert.equal(5, set.e); }); }); }); }); }); }); }); } } ================================================ FILE: test/structure.test.js ================================================ var redback = require('../'), assert = require('assert'), Structure = redback.Structure, Hash = redback.Hash, List = redback.List; redback = require('./common').createClient(), //Flush the DB and close the Redis connection after 500ms setTimeout(function () { redback.client.flushdb(function (err) { redback.client.quit(); }); }, 500); var hash = new Hash(null, 'hash'), list = new List(null, 'list'), structure = new Structure(null, 'structure'); module.exports = { 'test structure prototypal inheritance': function() { assert.ok(typeof structure.ttl === 'function'); //These methods are inherited from Structure assert.ok(typeof hash.ttl === 'function'); assert.ok(typeof list.ttl === 'function'); }, 'test modifying the base prototype': function () { assert.ok(typeof structure.foo === 'undefined'); assert.ok(typeof list.foo === 'undefined'); assert.ok(typeof list.foo === 'undefined'); //All structures inherit from Structure Structure.prototype.foo = function() { return 'foo'; }; //..so all children will inherit its methods assert.equal('foo', hash.foo()); assert.equal('foo', list.foo()); }, 'test modifying a childs prototype': function () { assert.ok(typeof structure.bar === 'undefined'); assert.ok(typeof list.bar === 'undefined'); assert.ok(typeof hash.bar === 'undefined'); //Adding to a structure's prototype should not affect other structures Hash.prototype.bar = function() { return 'bar'; } assert.equal('bar', hash.bar()); assert.ok(typeof structure.bar === 'undefined'); assert.ok(typeof list.bar === 'undefined'); }, 'test creating a structure with no key': function () { var structures = [ 'List','Hash','Set','SortedSet','CappedList','DensitySet', 'Channel','KeyPair','SocialGraph' ]; structures.forEach(function (structure) { assert.throws(function () { redback['create' + structure](); }); }); //Cache doesn't require a key.. redback.createCache(); }, 'test adding a custom structure': function () { redback.addStructure('TestQueue', { init: function (is_fifo) { this.fifo = is_fifo; }, add: function (value, callback) { this.client.lpush(this.key, value, callback); }, next: function (callback) { var method = this.fifo ? 'rpop' : 'lpop'; this.client[method](this.key, callback); } }); var lifo = redback.createTestQueue('test_addstructure_lifo_queue'); lifo.add('foo', function (err) { lifo.add('bar', function (err) { lifo.next(function (err, value) { assert.equal('bar', value); lifo.next(function (err, value) { assert.equal('foo', value); lifo.next(function (err, value) { assert.equal(null, value); }); }); }); }); }); var fifo = redback.createTestQueue('test_addstructure_fifo_queue', true); fifo.add('foo', function (err) { fifo.add('bar', function (err) { fifo.next(function (err, value) { assert.equal('foo', value); fifo.next(function (err, value) { assert.equal('bar', value); fifo.next(function (err, value) { assert.equal(null, value); }); }); }); }); }); }, 'test structure (bg)save and destroy': function () { var hash = redback.createHash('test_structure_destroy'); redback.save(function (err) { assert.ok(!err); }); redback.save(true, function (err) { assert.ok(!err); }); hash.set('foo', 'bar', function (err) { hash.get('foo', function (err, value) { assert.equal('bar', value); hash.destroy(function (err) { hash.get('foo', function (err, value) { assert.equal(null, value); }); }); }); }); }, 'test an array key': function () { var hash = redback.createHash(['user', 1]); assert.equal('user:1', hash.key); }, 'test get type': function () { var hash = redback.createHash('test_get_type'); hash.type(function (err, type) { assert.equal('none', type); hash.set('foo', 'bar', function (err) { hash.type(function (err, type) { assert.equal('hash', type); }); }); }); }, 'test renaming a structure': function () { var hash = redback.createHash('test_rename_hash1'), hash2 = redback.createHash('test_rename_hash2'); hash.set('foo', 'bar', function (err) { hash.rename('test_rename_hash2', function (err) { hash2.get('foo', function (err, value) { assert.equal('bar', value); }); }); }); } /* 'test structure expiries': function () { var hash = redback.createHash('test_structure_expiry'); hash.expire(1, function (err) { hash.ttl(function (err, ttl) { assert.equal(1, ttl); hash.persist(function (err) { hash.ttl(function (err, ttl) { assert.equal(-1, ttl); }); }); }); }); var set = redback.createSet('test_structure_expiry2'); var when = new Date(); set.expireAt(when, function (err) { set.ttl(function (err, ttl) { assert.equal(1, ttl); }); }); } */ }