Skip to content

Commit c522ca1

Browse files
author
Ruben Bridgewater
committed
Fix inconsistent command argument handling
Earlier multi.command and client.command diverged a lot in the way they accepted arguments. This is now consistent This will also fix some bugs like using multi.hmset with arrays
1 parent e24f056 commit c522ca1

13 files changed

+208
-76
lines changed

.jshintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
1717
"mocha": true,
1818

19+
// Relaxing options
20+
"boss": true, // Accept things like `while (command = keys.shift()) { ... }`
21+
1922
"overrides": {
2023
"examples/*.js": {
2124
"unused": false

index.js

Lines changed: 72 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -863,8 +863,16 @@ RedisClient.prototype.end = function () {
863863
function Multi(client, args) {
864864
this._client = client;
865865
this.queue = [["multi"]];
866+
var command, tmp_args;
866867
if (Array.isArray(args)) {
867-
this.queue = this.queue.concat(args);
868+
while (tmp_args = args.shift()) {
869+
command = tmp_args.shift();
870+
if (Array.isArray(command)) {
871+
this[command[0]].apply(this, command.slice(1).concat(tmp_args));
872+
} else {
873+
this[command].apply(this, tmp_args);
874+
}
875+
}
868876
}
869877
}
870878

@@ -878,16 +886,32 @@ commands.forEach(function (fullCommand) {
878886
return;
879887
}
880888

881-
RedisClient.prototype[command] = function (args, callback) {
882-
if (Array.isArray(args)) {
883-
return this.send_command(command, args, callback);
889+
RedisClient.prototype[command] = function (key, arg, callback) {
890+
if (Array.isArray(key)) {
891+
return this.send_command(command, key, arg);
892+
}
893+
if (Array.isArray(arg)) {
894+
arg.unshift(key);
895+
return this.send_command(command, arg, callback);
884896
}
885897
return this.send_command(command, to_array(arguments));
886898
};
887899
RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command];
888900

889-
Multi.prototype[command] = function () {
890-
this.queue.push([command].concat(to_array(arguments)));
901+
Multi.prototype[command] = function (key, arg, callback) {
902+
if (Array.isArray(key)) {
903+
if (arg) {
904+
key.push(arg);
905+
}
906+
this.queue.push([command].concat(key));
907+
} else if (Array.isArray(arg)) {
908+
if (callback) {
909+
arg.push(callback);
910+
}
911+
this.queue.push([command, key].concat(arg));
912+
} else {
913+
this.queue.push([command].concat(to_array(arguments)));
914+
}
891915
return this;
892916
};
893917
Multi.prototype[command.toUpperCase()] = Multi.prototype[command];
@@ -919,70 +943,63 @@ RedisClient.prototype.auth = RedisClient.prototype.AUTH = function (pass, callba
919943
}
920944
};
921945

922-
RedisClient.prototype.hmget = RedisClient.prototype.HMGET = function (arg1, arg2, arg3) {
923-
if (Array.isArray(arg2) && typeof arg3 === "function") {
924-
return this.send_command("hmget", [arg1].concat(arg2), arg3);
925-
} else if (Array.isArray(arg1) && typeof arg2 === "function") {
926-
return this.send_command("hmget", arg1, arg2);
927-
} else {
928-
return this.send_command("hmget", to_array(arguments));
946+
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function (key, args, callback) {
947+
var field, tmp_args;
948+
if (Array.isArray(key)) {
949+
return this.send_command("hmset", key, args);
929950
}
930-
};
931-
932-
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function (args, callback) {
933-
var tmp_args, tmp_keys, i, il, key;
934-
935951
if (Array.isArray(args)) {
936-
return this.send_command("hmset", args, callback);
937-
}
938-
939-
args = to_array(arguments);
940-
if (typeof args[args.length - 1] === "function") {
941-
callback = args[args.length - 1];
942-
args.length -= 1;
943-
} else {
944-
callback = null;
952+
return this.send_command("hmset", [key].concat(args), callback);
945953
}
946-
947-
if (args.length === 2 && (typeof args[0] === "string" || typeof args[0] === "number") && typeof args[1] === "object") {
954+
if (typeof args === "object") {
948955
// User does: client.hmset(key, {key1: val1, key2: val2})
949956
// assuming key is a string, i.e. email address
950957

951958
// if key is a number, i.e. timestamp, convert to string
952-
if (typeof args[0] === "number") {
953-
args[0] = args[0].toString();
959+
// TODO: This seems random and no other command get's the key converted => either all or none should behave like this
960+
if (typeof key !== "string") {
961+
key = key.toString();
954962
}
955-
956-
tmp_args = [ args[0] ];
957-
tmp_keys = Object.keys(args[1]);
958-
for (i = 0, il = tmp_keys.length; i < il ; i++) {
959-
key = tmp_keys[i];
960-
tmp_args.push(key);
961-
tmp_args.push(args[1][key]);
963+
tmp_args = [key];
964+
var fields = Object.keys(args);
965+
while (field = fields.shift()) {
966+
tmp_args.push(field, args[field]);
962967
}
963-
args = tmp_args;
968+
return this.send_command("hmset", tmp_args, callback);
964969
}
965-
966-
return this.send_command("hmset", args, callback);
970+
return this.send_command("hmset", to_array(arguments));
967971
};
968972

969-
Multi.prototype.hmset = Multi.prototype.HMSET = function () {
970-
var args = to_array(arguments), tmp_args;
971-
if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") {
972-
tmp_args = [ "hmset", args[0] ];
973-
Object.keys(args[1]).map(function (key) {
974-
tmp_args.push(key);
975-
tmp_args.push(args[1][key]);
976-
});
977-
if (args[2]) {
978-
tmp_args.push(args[2]);
973+
Multi.prototype.hmset = Multi.prototype.HMSET = function (key, args, callback) {
974+
var tmp_args, field;
975+
if (Array.isArray(key)) {
976+
if (args) {
977+
key.push(args);
978+
}
979+
tmp_args = ['hmset'].concat(key);
980+
} else if (Array.isArray(args)) {
981+
if (callback) {
982+
args.push(callback);
983+
}
984+
tmp_args = ['hmset', key].concat(args);
985+
} else if (typeof args === "object") {
986+
tmp_args = ["hmset", key];
987+
if (typeof key !== "string") {
988+
key = key.toString();
989+
}
990+
var fields = Object.keys(args);
991+
while (field = fields.shift()) {
992+
tmp_args.push(field);
993+
tmp_args.push(args[field]);
994+
}
995+
if (callback) {
996+
tmp_args.push(callback);
979997
}
980-
args = tmp_args;
981998
} else {
982-
args.unshift("hmset");
999+
tmp_args = to_array(arguments);
1000+
tmp_args.unshift("hmset");
9831001
}
984-
985-
this.queue.push(args);
1002+
this.queue.push(tmp_args);
9861003
return this;
9871004
};
9881005

test/commands/blpop.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ describe("The 'blpop' method", function () {
3333
});
3434
});
3535

36+
it('pops value immediately if list contains values using array notation', function (done) {
37+
bclient = redis.createClient.apply(redis.createClient, args);
38+
client.rpush(["blocking list", "initial value"], helper.isNumber(1));
39+
bclient.blpop(["blocking list", 0], function (err, value) {
40+
assert.strictEqual(value[0], "blocking list");
41+
assert.strictEqual(value[1], "initial value");
42+
return done(err);
43+
});
44+
});
45+
3646
it('waits for value if list is not yet populated', function (done) {
3747
bclient = redis.createClient.apply(redis.createClient, args);
3848
bclient.blpop("blocking list 2", 5, function (err, value) {

test/commands/client.spec.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@ describe("The 'client' method", function () {
3737
});
3838
});
3939

40+
it("lists connected clients when invoked with array syntax on client", function (done) {
41+
client.multi().client(["list"]).exec(function(err, results) {
42+
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
43+
return done();
44+
});
45+
});
46+
4047
it("lists connected clients when invoked with multi's array syntax", function (done) {
41-
client.multi().client("list").exec(function(err, results) {
48+
client.multi([
49+
['client', 'list']
50+
]).exec(function(err, results) {
4251
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
4352
return done();
4453
});

test/commands/dbsize.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe("The 'dbsize' method", function () {
7171
var oldSize;
7272

7373
beforeEach(function (done) {
74-
client.dbsize([], function (err, res) {
74+
client.dbsize(function (err, res) {
7575
helper.isType.number()(err, res);
7676
assert.strictEqual(res, 0, "Initial db size should be 0");
7777

test/commands/del.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ describe("The 'del' method", function () {
3636
client.get('apple', helper.isNull(done));
3737
});
3838

39+
it('allows multiple keys to be deleted with the array syntax', function (done) {
40+
client.mset('foo', 'bar', 'apple', 'banana');
41+
client.del(['foo', 'apple'], helper.isNumber(2));
42+
client.get('foo', helper.isNull());
43+
client.get('apple', helper.isNull(done));
44+
});
45+
46+
it('allows multiple keys to be deleted with the array syntax and no callback', function (done) {
47+
client.mset('foo', 'bar', 'apple', 'banana');
48+
client.del(['foo', 'apple']);
49+
client.get('foo', helper.isNull());
50+
client.get('apple', helper.isNull(done));
51+
});
52+
3953
afterEach(function () {
4054
client.end();
4155
});

test/commands/exits.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ describe("The 'exits' method", function () {
2424
client.EXISTS('foo', helper.isNumber(1, done));
2525
});
2626

27+
it('returns 1 if the key exists with array syntax', function (done) {
28+
client.set('foo', 'bar');
29+
client.EXISTS(['foo'], helper.isNumber(1, done));
30+
});
31+
2732
it('returns 0 if the key does not exist', function (done) {
2833
client.exists('bar', helper.isNumber(0, done));
2934
});

test/commands/expire.spec.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ describe("The 'expire' method", function () {
2020
});
2121

2222
it('expires key after timeout', function (done) {
23+
client.set(['expiry key', 'bar'], helper.isString("OK"));
24+
client.EXPIRE("expiry key", "1", helper.isNumber(1));
25+
setTimeout(function () {
26+
client.exists(["expiry key"], helper.isNumber(0, done));
27+
}, 1100);
28+
});
29+
30+
it('expires key after timeout with array syntax', function (done) {
2331
client.set(['expiry key', 'bar'], helper.isString("OK"));
2432
client.EXPIRE(["expiry key", "1"], helper.isNumber(1));
2533
setTimeout(function () {

test/commands/flushdb.spec.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,36 +58,50 @@ describe("The 'flushdb' method", function () {
5858
describe("when there is data in Redis", function () {
5959

6060
beforeEach(function (done) {
61-
var end = helper.callFuncAfter(function () {
62-
client.flushdb(helper.isString("OK", done));
63-
}, 2);
64-
client.mset(key, uuid.v4(), key2, uuid.v4(), helper.isNotError(end));
61+
client.mset(key, uuid.v4(), key2, uuid.v4(), helper.isNotError());
6562
client.dbsize([], function (err, res) {
6663
helper.isType.positiveNumber()(err, res);
6764
assert.equal(res, 2, 'Two keys should have been inserted');
68-
end();
65+
done();
6966
});
7067
});
7168

7269
it("deletes all the keys", function (done) {
73-
client.mget(key, key2, function (err, res) {
74-
assert.strictEqual(null, err, "Unexpected error returned");
75-
assert.strictEqual(true, Array.isArray(res), "Results object should be an array.");
76-
assert.strictEqual(2, res.length, "Results array should have length 2.");
77-
assert.strictEqual(null, res[0], "Redis key should have been flushed.");
78-
assert.strictEqual(null, res[1], "Redis key should have been flushed.");
79-
done(err);
70+
client.flushdb(function(err, res) {
71+
assert.equal(res, 'OK');
72+
client.mget(key, key2, function (err, res) {
73+
assert.strictEqual(null, err, "Unexpected error returned");
74+
assert.strictEqual(true, Array.isArray(res), "Results object should be an array.");
75+
assert.strictEqual(2, res.length, "Results array should have length 2.");
76+
assert.strictEqual(null, res[0], "Redis key should have been flushed.");
77+
assert.strictEqual(null, res[1], "Redis key should have been flushed.");
78+
done(err);
79+
});
8080
});
8181
});
8282

8383
it("results in a db size of zero", function (done) {
84-
client.dbsize([], function (err, res) {
85-
helper.isNotError()(err, res);
86-
helper.isType.number()(err, res);
87-
assert.strictEqual(0, res, "Flushing db should result in db size 0");
88-
done();
84+
client.flushdb(function(err, res) {
85+
client.dbsize([], function (err, res) {
86+
helper.isNotError()(err, res);
87+
helper.isType.number()(err, res);
88+
assert.strictEqual(0, res, "Flushing db should result in db size 0");
89+
done();
90+
});
8991
});
9092
});
93+
94+
it("results in a db size of zero without a callback", function (done) {
95+
client.flushdb();
96+
setTimeout(function(err, res) {
97+
client.dbsize([], function (err, res) {
98+
helper.isNotError()(err, res);
99+
helper.isType.number()(err, res);
100+
assert.strictEqual(0, res, "Flushing db should result in db size 0");
101+
done();
102+
});
103+
}, 25);
104+
});
91105
});
92106
});
93107
});

test/commands/get.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ describe("The 'get' method", function () {
7070
done(err);
7171
});
7272
});
73+
74+
it("gets the value correctly with array syntax and the callback being in the array", function (done) {
75+
client.GET([key, function (err, res) {
76+
helper.isString(value)(err, res);
77+
done(err);
78+
}]);
79+
});
80+
81+
it("should not throw on a get without callback (even if it's not useful", function (done) {
82+
client.GET(key);
83+
client.on('error', function(err) {
84+
throw err;
85+
});
86+
setTimeout(done, 50);
87+
});
7388
});
7489

7590
describe("when the key does not exist in Redis", function () {

0 commit comments

Comments
 (0)