Skip to content

wallneradam/noderis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Noderis

A standalone Node.js client for Redis

Features

  • No external dependencies, it uses only standard Node.js modules.
  • Can be used with standard callback, Promises/A and async-await syntaxes.
  • Support for pipelines and transactions with MULTI-EXEC (shortcut: pmulti).
  • The callback based functions and pipelines are chainable.
  • Pipeline results emmitted to the commands callbacks.
  • MSET can accept JS objects as well.
  • Connection pools to handle more commands at the same time. If all clients in the pool are busy, commands are waiting for a free client.
  • Connection pool is transparent, it is just a proxy for the 1st available connection. So you even don't notice that you use a pool.
  • Clear, understandable, not too complicated code. Redis commands are real functions, IDEs (like PHPStorm or Webstorm) can understand it.
  • Methods and commands have a JSDoc comment with types specified.
  • Good stack trace on redis error.

Maybe it's not as fast as a C++ based parser, but still very fast though. Official Node-redis has no connection pool feature, so only one command can be run at a time (or there would be collisions). So if you have 100 users at the same time, they need to wait for each other. Connecting to Redis each time a client is connected still not a good solution beacause of the overhead of connecting. Because of overcomplicated design of Node-redis, it is hard to implement a connection pool with it (and would be much slower). So this is why we implemeted the pool feature which I think may make Noderis faster than most clients.

Cons

It is string based API, so no support for binary data (which I don't need, I store all binary data in base64). Although it should not be hard to change it to use buffer instead of utf-8 strings. If you need it, please implement it and send a pull request.

Usage

Initialize

You can start using the module in several ways

Config by global variables

The easiest (IMO) is to create global config variables:

global.REDIS_HOST = 'redis-server'; // The host of Redis server
global.REDIS_PORT = 6379; // The port of Redis server
global.REDIS_OPTIONS = {}; // Other Redis options (see in source code)
global.REDIS_POOL_SIZE = 10; // How many connections we need (5 is default)

This way we can use module level rclient and rclient_async objects to communicate with Redis. If global.REDIS_POOL_SIZE is specified, connection pool is created instead of a single RedisClient object

Calling createClient or createClientPool

createClient(REDIS_PORT, REDIS_HOST, options, function() {
    console.log('Redis client connected to %s:%i', REDIS_HOST, REDIS_PORT);
});

By creating objects

let rclient = new RedisClient(port, host, options);
let rclient_async = new RedisClientAsyncProxy(rclient);
rclient.connect(() => {
    console.log('Redis client connected.');
});

Calling commands

Callback based API

You can call any Redis commands with callRedis method like this:

rclient.callRedis('SET', 'testKey', 'testValue', (err, resp) => {
    if (err) _handleError(err);
    else {
        console.log(resp);
    }
});

Though we created preprocessors/shortcuts for some (the goal is to create for all) commands:

rclient.set('testKey', 'testValue', (err, resp) => {
    //...
});

Promise / async-await API

The same in async way:

try {
    let resp = await rclient_async.callRedis('SET', 'testKey', 'testValue');
    let resp = await rclient_async.set('testKey, testValue');
} catch(err) {
    _handleError(err);
}

Events

  • connected: Emitted when connection is ready. It is called once. If you use pool, emmitted when 1st client is connected.
  • disconnected: When the client is disconnected. In pool emitted if all clients are disconnected.
  • connect_error: When error occured while connecting, or connection timeout occured.
  • error: Errors in connection and communication with Redis server.
  • redis_error: Errors from Redis server and protocol errors.

ConnectionPool

All events are the same (because pool is transparent), but you can listen on the following client events if you want:

  • client_connected: When a client is connected in the pool.
  • client_disconnected: When any client is disconnected in the pool.

Pipeline and (MULTI-EXEC) transaction

Creating pipeline is very easy, then you can chain every command:

// Callback based
rclient.pipeline()
    .set('test', 'val')
    .get('test', (err, resp) => {
        // this is optional to have callback here
        // resp will be the emmitted result of answer from pipeline 
    })
    .send((err, resp) => {
        // Here resp is an array of all comands results 
    });

At the end of the pipeline, you need to send it. It will send all commands at once to Redis, which is faster because of no send-receive overhead.

You can specify an index to the send() method, to get only one command's result. If it is negative we can get the result from bottom:

    // ...
    .send(-1, (err, resp) => {
        // resp will be the result of the last command of the pipeline 
    });
    

Async await based example:

let resp = await rclient_async.pipeline()
    .set('test', 'val')
    .get('test', (err, resp) => {
        // this is optional to have callback here
        // resp will be the emmitted result of answer from pipeline 
    })
    .send(-1);
console.log("Result of the last command before send():", resp);

Transaction

Transactions in Redis implemented by calling MULTI, then at the end EXEC. Though it is not very useful without pipeline it can have problems as well:

  • MULTI calls cannot be nested!
  • If you start a MULTI transaction and other users start commands, it will be included in the same transaction until EXEC is called
  • If you use clientPool, it is handled there, but that client will be marked busy until EXEC is called.

So we suggest to use our pmulti() method which starts a pipeline then put a MULTI as 1st command in it. It even closes the MULTI automatically with EXEC on send() (if not disabled in options).

Example:

let resp = await rclient_async.pmulti()
    .set('test', 'val')
    .get('test', (err, resp) => {
        // this is optional to have callback here
        // resp will be the emmitted result of answer from EXEC 
    })
    .send(-1);

Our pipeline implementation uses the results of EXEC - if it is the 1st command in the pipeline -, not the whole pipeline, where every additional command returns with a "QUEUED" response. Instead we will get the real results.

Further goals

  • Implement all Redis commands
  • Create documentation for every command
  • Not blindly implement all, but if it ispossible, make it more JS and programmer friendly (e.g. SET, MSET... in source code)
  • Create benchmarks

Status

Currently the base of the module is done, but not all commands have shortcut and comments.

If you use this module and need to use commands which are not supported, or simply want to help, it is very easy to add new commands. If you extend Noderis, please send pull request!

Adding new commands

All commands should have doc comments in JSDoc standard. You need to add the implementation in the prototype of RedisClient:

RedisClient.prototype = {
    // ...
    /**
     * Command description 
     * @param {string} param1
     * @param {string} param2
     * @param {Callback=} cb resp will be ... (document what resp will be in the callback)
     * @return {RedisClient}
     */
    command(param1, param2, cb) {
        return this.callRedis('COMMAND', cb, param1, param2);
    }
    //...
}

Then you need to add it to the Promise based proxy:

RedisClientAsyncProxy.prototype = {
    // ...
    /**
     * Command description 
     * @param {string} param1
     * @param {string} param2
     * @param {Callback} cb resp will be ... (document what resp will be in the callback)
     * @return {Promise.<string>} Should be documented which type and meaning
     */
    command: proxyfy(RedisClient.prototype.command)
    // ...
}

Unit tests

Unit tests use Mocha. If you have mocha and redis server installed on your local machine you can simple run mocha. If not, you need Docker to test with dockunit-plus. Simple run npm test to start test in a docker container.

About

A standalone Node.js client for Redis

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published