Skip to content

Commit

Permalink
feat: use native Promise instead of Bluebird, and allow users to swit…
Browse files Browse the repository at this point in the history
…ch back.

BREAKING CHANGE:
This change makes all the code that rely on the features provided by Bluebird not working
anymore. For example, `redis.get('foo').timeout(500)` now should be failed since the native
Promise doesn't support the `timeout` method.

You can switch back to the Bluebird implementation by setting `Redis.Promise`:

```
const Redis = require('ioredis')
Redis.Promise = require('bluebird')

const redis = new Redis()

// Use bluebird
assert.equal(redis.get().constructor, require('bluebird'))

// You can change the Promise implementation at any time:
Redis.Promise = global.Promise
assert.equal(redis.get().constructor, global.Promise)
```
  • Loading branch information
luin committed Jun 23, 2018
1 parent 229f264 commit da60b8b
Show file tree
Hide file tree
Showing 20 changed files with 185 additions and 102 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ used in the world's biggest online commerce company [Alibaba](http://www.alibaba

0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](http://redis.io/topics/sentinel), [Pipelining](http://redis.io/topics/pipelining) and of course [Lua scripting](http://redis.io/commands/eval) & [Pub/Sub](http://redis.io/topics/pubsub) (with the support of binary messages).
0. High performance.
0. Delightful API. It works with Node callbacks and [Bluebird promises](https://github.com/petkaantonov/bluebird).
0. Delightful API. It works with Node callbacks and Native promises.
0. Transformation of command arguments and replies.
0. Transparent key prefixing.
0. Abstraction for Lua scripting, allowing you to define custom commands.
Expand Down Expand Up @@ -944,6 +944,24 @@ var redis = new Redis();
redis.set('foo');
```

# Plugging in your own Promises Library
If you're an advanced user, you may want to plug in your own promise library like [bluebird](https://www.npmjs.com/package/bluebird). Just set Redis.Promise to your favorite ES6-style promise constructor and ioredis will use it.

```javascript
const Redis = require('ioredis')
Redis.Promise = require('bluebird')

const redis = new Redis()

// Use bluebird
assert.equal(redis.get().constructor, require('bluebird'))

// You can change the Promise implementation at any time:
Redis.Promise = global.Promise
assert.equal(redis.get().constructor, global.Promise)
```


# Running tests

Start a Redis server on 127.0.0.1:6379, and then:
Expand Down
10 changes: 10 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ exports.Promise = require('bluebird');
exports.Cluster = require('./lib/cluster');
exports.Command = require('./lib/command');

var PromiseContainer = require('./lib/promise_container');
Object.defineProperty(exports, 'Promise', {
get: function() {
return PromiseContainer.get();
},
set: function(lib) {
PromiseContainer.set(lib);
}
});

exports.print = function (err, reply) {
if (err) {
console.log('Error: ' + err);
Expand Down
2 changes: 1 addition & 1 deletion lib/cluster/connection_pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ConnectionPool.prototype.findOrCreate = function (node, readOnly) {
readOnly = Boolean(readOnly);

if (this.specifiedOptions[node.key]) {
_.assign(node, this.specifiedOptions[node.key]);
Object.assign(node, this.specifiedOptions[node.key]);
} else {
this.specifiedOptions[node.key] = node;
}
Expand Down
23 changes: 15 additions & 8 deletions lib/cluster/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict';

var Promise = require('bluebird');
var Deque = require('denque');
var Redis = require('../redis');
var utils = require('../utils');
Expand All @@ -12,8 +11,10 @@ var ScanStream = require('../scan_stream');
var Commander = require('../commander');
var Command = require('../command');
var commands = require('redis-commands');
var asCallback = require('standard-as-callback');
var ConnectionPool = require('./connection_pool');
var DelayQueue = require('./delay_queue');
var PromiseContainer = require('../promise_container');

/**
* Creates a Redis Cluster instance
Expand Down Expand Up @@ -112,7 +113,7 @@ Cluster.defaultOptions = {
};

util.inherits(Cluster, EventEmitter);
_.assign(Cluster.prototype, Commander.prototype);
Object.assign(Cluster.prototype, Commander.prototype);

Cluster.prototype.resetOfflineQueue = function () {
this.offlineQueue = new Deque();
Expand Down Expand Up @@ -141,6 +142,7 @@ Cluster.prototype.connect = function () {
this.resetNodesRefreshInterval();
}

var Promise = PromiseContainer.get();
return new Promise(function (resolve, reject) {
if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') {
reject(new Error('Redis is already connecting/connected'));
Expand Down Expand Up @@ -271,8 +273,10 @@ Cluster.prototype.quit = function (callback) {
clearInterval(this.slotsTimer);
this.slotsTimer = null;
}

var Promise = PromiseContainer.get();
if (status === 'wait') {
var ret = Promise.resolve('OK').nodeify(callback);
var ret = asCallback(Promise.resolve('OK'), callback)

// use setImmediate to make sure "close" event
// being emitted after quit() is returned
Expand All @@ -283,11 +287,14 @@ Cluster.prototype.quit = function (callback) {

return ret;
}
return Promise.all(this.nodes().map(function (node) {
return node.quit();
})).then(function () {
return 'OK';
}).nodeify(callback);
return asCallback(
Promise.all(this.nodes().map(function (node) {
return node.quit();
})).then(function () {
return 'OK';
}),
callback
);
};

/**
Expand Down
10 changes: 7 additions & 3 deletions lib/command.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use strict';

var _ = require('./utils/lodash');
var Promise = require('bluebird');
var fbuffer = require('flexbuffer');
var utils = require('./utils');
var commands = require('redis-commands');
var calculateSlot = require('cluster-key-slot');
var PromiseContainer = require('./promise_container');
var asCallback = require('standard-as-callback');

/**
* Command instance
Expand Down Expand Up @@ -60,7 +61,8 @@ function Command(name, args, options, callback) {

Command.prototype.initPromise = function () {
var _this = this;
this.promise = new Promise(function (resolve, reject) {
var Promise = PromiseContainer.get();
var promise = new Promise(function (resolve, reject) {
if (!_this.transformed) {
_this.transformed = true;
var transformer = Command._transformer.argument[_this.name];
Expand All @@ -78,7 +80,9 @@ Command.prototype.initPromise = function () {
} else {
_this.reject = reject;
}
}).nodeify(this.callback);
});

this.promise = asCallback(promise, this.callback);
};

Command.prototype.getSlot = function () {
Expand Down
13 changes: 10 additions & 3 deletions lib/commander.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
var _ = require('./utils/lodash');
var Command = require('./command');
var Script = require('./script');
var Promise = require('bluebird');
var PromiseContainer = require('./promise_container');
var asCallback = require('standard-as-callback');

var DROP_BUFFER_SUPPORT_ERROR = '*Buffer methods are not available ' +
'because "dropBufferSupport" option is enabled.' +
Expand Down Expand Up @@ -114,7 +115,10 @@ function generateFunction(_commandName, _encoding) {
var options;
if (this.options.dropBufferSupport) {
if (!_encoding) {
return Promise.reject(new Error(DROP_BUFFER_SUPPORT_ERROR)).nodeify(callback);
return asCallback(
PromiseContainer.get().reject(new Error(DROP_BUFFER_SUPPORT_ERROR)),
callback
)
}
options = { replyEncoding: null };
} else {
Expand Down Expand Up @@ -150,7 +154,10 @@ function generateScriptingFunction(_script, _encoding) {
var options;
if (this.options.dropBufferSupport) {
if (!_encoding) {
return Promise.reject(new Error(DROP_BUFFER_SUPPORT_ERROR)).nodeify(callback);
return asCallback(
PromiseContainer.get().reject(new Error(DROP_BUFFER_SUPPORT_ERROR)),
callback
)
}
options = { replyEncoding: null };
} else {
Expand Down
2 changes: 1 addition & 1 deletion lib/connectors/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Connector.prototype.connect = function (callback) {
connectionOptions = _.pick(this.options, ['port', 'host', 'family']);
}
if (this.options.tls) {
_.assign(connectionOptions, this.options.tls);
Object.assign(connectionOptions, this.options.tls);
}

var _this = this;
Expand Down
11 changes: 7 additions & 4 deletions lib/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ var _ = require('./utils/lodash');
var Commander = require('./commander');
var Command = require('./command');
var fbuffer = require('flexbuffer');
var Promise = require('bluebird');
var util = require('util');
var asCallback = require('standard-as-callback');
var commands = require('redis-commands');
var calculateSlot = require('cluster-key-slot');
var PromiseContainer = require('./promise_container');

function Pipeline(redis) {
Commander.call(this);
Expand All @@ -20,13 +21,14 @@ function Pipeline(redis) {
this._shaToScript = {};

var _this = this;
_.keys(redis.scriptsSet).forEach(function (name) {
Object.keys(redis.scriptsSet).forEach(function (name) {
var script = redis.scriptsSet[name];
_this._shaToScript[script.sha] = script;
_this[name] = redis[name];
_this[name + 'Buffer'] = redis[name + 'Buffer'];
});

var Promise = PromiseContainer.get()
this.promise = new Promise(function (resolve, reject) {
_this.resolve = resolve;
_this.reject = reject;
Expand All @@ -39,7 +41,7 @@ function Pipeline(redis) {
});
}

_.assign(Pipeline.prototype, Commander.prototype);
Object.assign(Pipeline.prototype, Commander.prototype);

Pipeline.prototype.fillResult = function (value, position) {
var i;
Expand Down Expand Up @@ -213,7 +215,7 @@ Pipeline.prototype.exec = function (callback) {
}
if (!this.nodeifiedPromise) {
this.nodeifiedPromise = true;
this.promise.nodeify(callback);
asCallback(this.promise, callback);
}
if (_.isEmpty(this._queue)) {
this.resolve([]);
Expand Down Expand Up @@ -273,6 +275,7 @@ Pipeline.prototype.exec = function (callback) {
pending.push(scripts[i]);
}
}
var Promise = PromiseContainer.get()
return Promise.all(pending.map(function (script) {
return _this.redis.script('load', script.lua);
}));
Expand Down
19 changes: 19 additions & 0 deletions lib/promise_container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
exports.isPromise = function (obj) {
return !!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof obj.then === 'function'
}

let promise = global.Promise

exports.get = function () {
return promise
}

exports.set = function (lib) {
if (typeof lib !== 'function') {
throw new Error(`Provided Promise must be a function, got ${lib}`)
}

promise = lib
}
30 changes: 19 additions & 11 deletions lib/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
var _ = require('./utils/lodash');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Promise = require('bluebird');
var Deque = require('denque');
var Command = require('./command');
var Commander = require('./commander');
var utils = require('./utils');
var asCallback = require('standard-as-callback');
var eventHandler = require('./redis/event_handler');
var debug = require('./utils/debug')('ioredis:redis');
var Connector = require('./connectors/connector');
var SentinelConnector = require('./connectors/sentinel_connector');
var ScanStream = require('./scan_stream');
var commands = require('redis-commands');
var PromiseContainer = require('./promise_container');

/**
* Creates a Redis instance
Expand Down Expand Up @@ -131,7 +132,7 @@ function Redis() {
}

util.inherits(Redis, EventEmitter);
_.assign(Redis.prototype, Commander.prototype);
Object.assign(Redis.prototype, Commander.prototype);

/**
* Create a Redis instance
Expand Down Expand Up @@ -248,7 +249,8 @@ Redis.prototype.setStatus = function (status, arg) {
* @public
*/
Redis.prototype.connect = function (callback) {
return new Promise(function (resolve, reject) {
var Promise = PromiseContainer.get();
var promise = new Promise(function (resolve, reject) {
if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') {
reject(new Error('Redis is already connecting/connected'));
return;
Expand Down Expand Up @@ -315,7 +317,9 @@ Redis.prototype.connect = function (callback) {
}, function (type, err) {
_this.silentEmit(type, err);
});
}.bind(this)).nodeify(callback);
}.bind(this))

return asCallback(promise, callback)
};

/**
Expand Down Expand Up @@ -362,7 +366,7 @@ Redis.prototype.end = function () {
* @public
*/
Redis.prototype.duplicate = function (override) {
return new Redis(_.assign(_.cloneDeep(this.options), override || {}));
return new Redis(Object.assign(_.cloneDeep(this.options), override || {}));
};

/**
Expand Down Expand Up @@ -511,11 +515,15 @@ Redis.prototype.monitor = function (callback) {
lazyConnect: false
});

return new Promise(function (resolve) {
monitorInstance.once('monitoring', function () {
resolve(monitorInstance);
});
}).nodeify(callback);
var Promise = PromiseContainer.get();
return asCallback(
new Promise(function (resolve) {
monitorInstance.once('monitoring', function () {
resolve(monitorInstance);
});
}),
callback
);
};

require('./transaction').addTransactionSupport(Redis.prototype);
Expand Down Expand Up @@ -636,6 +644,6 @@ Redis.prototype.sendCommand = function (command, stream) {
};
});

_.assign(Redis.prototype, require('./redis/parser'));
Object.assign(Redis.prototype, require('./redis/parser'));

module.exports = Redis;
Loading

0 comments on commit da60b8b

Please sign in to comment.