├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── index.js ├── package.json ├── test ├── ranaly.amount.test.js ├── ranaly.data_list.test.js └── ranaly.realtime.test.js └── type ├── amount.js ├── data_list.js └── realtime.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.8" 4 | - "0.6" 5 | services: 6 | - redis-server 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = $(shell find test/ -name '*.test.js') 2 | 3 | run-tests: 4 | @./node_modules/.bin/mocha --timeout 3000 $(TESTS) 5 | 6 | test: 7 | @$(MAKE) NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests 8 | 9 | .PHONY: test 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ranaly - a node.js ranaly client 2 | [![Build Status](https://travis-ci.org/luin/node_ranaly.png?branch=master)](https://travis-ci.org/luin/node_ranaly) 3 | 4 | Ranaly可以非常简单地统计项目中的各种数据,本项目是ranaly的node.js客户端。想要了解更多关于ranaly的介绍请访问[ranaly项目主页](https://github.com/luin/ranaly)。 5 | 6 | ## 安装 7 | 8 | npm install node_ranaly 9 | 10 | ## 使用方法 11 | 首先加载ranaly库: 12 | 13 | var ranaly = require('node_ranaly'); 14 | 15 | 而后创建ranaly连接: 16 | 17 | var client = ranaly.createClient(port, host, keyPrefix); 18 | 19 | 其中`post`和`host`是Redis数据库的端口号和主机地址,`keyPrefix`用来指定ranaly向Redis加入的键的前缀以防止命名冲突,默认值是`RANALY:`。 20 | 21 | 如果程序中已经使用[node_redis](https://github.com/mranney/node_redis)库建立了到Redis的连接,也可以将该实例传入createClient函数: 22 | 23 | var redis = require('redis').createClient(); 24 | var client = ranaly.createClient(redis, keyPrefix); 25 | 26 | Ranaly支持3种数据类型,分别是Amount、Realtime和DataList。 27 | ## Amount 28 | 创建一个Amount实例: 29 | 30 | var users = new client.Amount('Users'); 31 | 32 | ### incr 33 | `incr`方法用来增加实例的值,如每当有新用户注册时可以通过如下方法增加用户数量: 34 | 35 | users.incr(function (err, total) { 36 | console.log('用户总数为:' + total + '个'); 37 | }); 38 | 39 | `incr`函数的定义是: 40 | 41 | incr([increment, [when, [callback]]) 42 | 43 | 其中`increment`指增加的数量,默认为1。`when`指增长发生的时间,`Date`类型,默认为`new Date()`,即当前时间。`callback`的第二个参数返回增长后的总数。 44 | 45 | ### get 46 | `get`方法用来获取实例在若干个时间的数量,如: 47 | 48 | users.get(['20130218', '20130219'], function (err, result) { 49 | console.log(result); 50 | }); 51 | 52 | 第一个参数是时间的数组,时间的表示方法为`YYYYMMDD`或`YYYYMMDDHH`。如想获取今天和当前小时的注册用户数量: 53 | 54 | var now = moment(); // 需要使用moment库 55 | users.get([now.format('YYYYMMDD'), now.format('YYYYMMDDHH')], function (err, results) { 56 | console.log('今天新注册的用户数量:' + results[0]); 57 | console.log('当前小时新注册的用户数量:' + results[1]); 58 | }); 59 | 60 | ### sum 61 | `sum`方法用来获取实例在若干个时间内总共的数量,使用方法和`get`一样,不再赘述。特例是当第一个参数为空时,`sum`会返回该Amount实例的总数。如: 62 | 63 | users.sum([], function (err, result) { 64 | console.log('用户总数为:' + total + '个'); 65 | }); 66 | 67 | ## Realtime 68 | 创建一个Realtime实例: 69 | 70 | var memory = new client.Realtime('Memory'); 71 | 72 | ### incr 73 | `incr`方法用来递增实例的值,如增加当前内存占用的空间: 74 | 75 | memory.incr(1, function (err, result) { 76 | console.log('当前内存占用为:' + result); 77 | }); 78 | 79 | 其中第一个参数表示增加的数量,如果省略则默认为1。 80 | 81 | ### set 82 | `set`方法用来设置实例的值,如: 83 | 84 | memory.set(20); 85 | 86 | ### get 87 | `get`方法用来获得实例的值,如: 88 | 89 | memory.get(function (err, result) { 90 | console.log('当前内存占用为:' + result); 91 | }); 92 | 93 | ### 实时通知 94 | 当修改了某个Realtime实例的值后,ranaly会使用Redis的`PUBLISH`命令派发通知,`channel`可以通过实例的`channel`属性获得,如: 95 | 96 | var sub = redis.createClient(); 97 | sub.subscribe(memory.channel); 98 | sub.on('message', function (channel, message) { 99 | if (channel === memory.channel) { 100 | console.log('当前内存占用为:' + message); 101 | } 102 | }); 103 | 104 | ## DataList 105 | 创建一个DataList实例: 106 | 107 | var userAvatars = new client.DataList('Avatars'); 108 | 109 | ### push 110 | `push`方法用来向实例加入一个元素,可以是字符串、数组、数组或对象类型,如: 111 | 112 | userAvatars.push({ 113 | url: 'http://demo.com/avatar.png', 114 | userID: 17 115 | }, 50, function (err, length) { 116 | }); 117 | 118 | 其中第二个参数表示保留的记录数量,默认为100。 119 | 120 | ### len 121 | `len`方法用来获得实例的大小,如: 122 | 123 | userAvatars.len(function (err, length) { 124 | }); 125 | 126 | ### range 127 | `range`方法用来获得队列中的某个片段,第一个参数表示起始元素索引,第二个元素表示末尾元素索引。索引从0开始,支持负索引,即-1表示队列中最后一个元素。如: 128 | 129 | userAvatars.range(0, -1, function (err, avatars) { 130 | avatars.forEach(function (avatar) { 131 | console.log(avatar.url); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | 3 | exports.createClient = function (port, host, prefix) { 4 | if (typeof port === 'object') { 5 | exports.redisClient = port; 6 | prefix = host; 7 | } else { 8 | exports.redisClient = redis.createClient(port, host); 9 | } 10 | exports.prefix = prefix || 'Ranaly:'; 11 | 12 | return { 13 | Amount: require('./type/amount')(exports), 14 | Realtime: require('./type/realtime')(exports), 15 | DataList: require('./type/data_list')(exports) 16 | }; 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node_ranaly", 3 | "version": "0.1.1", 4 | "description": "Ranaly client library", 5 | "keywords": ["ranaly"], 6 | "author": "Zihua Li (http://zihua.li)", 7 | "main": "./index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/luin/node_ranaly.git" 11 | }, 12 | "scripts": { 13 | "test": "make test" 14 | }, 15 | "dependencies": { 16 | "redis": "0.8.2", 17 | "moment": "2.0.0" 18 | }, 19 | "devDependencies": { 20 | "mocha": "*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/ranaly.amount.test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef:false */ 2 | var assert = require('assert'); 3 | var moment = require('moment'); 4 | var redis = require('redis').createClient(); 5 | var ranaly = require('../index'); 6 | var client = ranaly.createClient(redis); 7 | 8 | var Amount = client.Amount; 9 | 10 | var aDay = 24 * 3600000; 11 | var yesterday = new Date(Date.now() - aDay); 12 | describe('Amount', function () { 13 | var bucket = new Amount('test'); 14 | redis.del(bucket.key); 15 | 16 | describe('basic flow', function () { 17 | it('should return 0 if the key do not exists', function (done) { 18 | bucket.get(['20130101', '2013010203'], function (err, results) { 19 | assert.equal(results.length, 2); 20 | assert.equal(results[0], 0); 21 | assert.equal(results[1], 0); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should have default increment and unixTime', function (done) { 27 | bucket.incr(); 28 | bucket.get([moment().format('YYYYMMDD'), moment().format('YYYYMMDDHH')], function (err, results) { 29 | assert.equal(results.length, 2); 30 | assert.equal(results[0], 1); 31 | assert.equal(results[1], 1); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should accept custom increment', function (done) { 37 | bucket.incr(2); 38 | bucket.get([moment().format('YYYYMMDD'), moment().format('YYYYMMDDHH')], function (err, results) { 39 | assert.equal(results.length, 2); 40 | assert.equal(results[0], 3); 41 | assert.equal(results[1], 3); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should accept custom date', function (done) { 47 | bucket.incr(null, yesterday); 48 | bucket.incr(10, yesterday); 49 | bucket.get([moment(yesterday).format('YYYYMMDD'), moment(yesterday).format('YYYYMMDDHH')], function (err, results) { 50 | assert.equal(results.length, 2); 51 | assert.equal(results[0], 11); 52 | assert.equal(results[1], 11); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should sum up the amount correctly', function (done) { 58 | bucket.sum([moment().format('YYYYMMDD'), moment(yesterday).format('YYYYMMDDHH')], function (err, sum) { 59 | assert.equal(typeof sum, 'number'); 60 | assert.equal(sum, 14); 61 | done(); 62 | }); 63 | }); 64 | 65 | }); 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /test/ranaly.data_list.test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef:false */ 2 | var assert = require('assert'); 3 | var redis = require('redis').createClient(); 4 | var ranaly = require('../index'); 5 | var client = ranaly.createClient(redis); 6 | 7 | var DataList = client.DataList; 8 | 9 | describe('DataList', function () { 10 | var bucket = new DataList('test'); 11 | redis.del(bucket.key); 12 | 13 | describe('basic flow', function () { 14 | it('should have a length of 0', function (done) { 15 | bucket.len(function (err, result) { 16 | assert.equal(typeof result, 'number'); 17 | assert.equal(result, 0); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should be able to push a string', function (done) { 23 | bucket.push('hi', function (err, result) { 24 | assert.equal(typeof result, 'number'); 25 | assert.equal(result, 1); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should be able to push a object', function (done) { 31 | bucket.push({text: 'hi'}, function (err, result) { 32 | assert.equal(typeof result, 'number'); 33 | assert.equal(result, 2); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should return the correct length', function (done) { 39 | bucket.len(function (err, result) { 40 | assert.equal(typeof result, 'number'); 41 | assert.equal(result, 2); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should return the data correctly', function (done) { 47 | bucket.range(0, -1, function (err, results) { 48 | assert.equal(typeof results[0], 'object'); 49 | assert.equal(results[0].text, 'hi'); 50 | assert.equal(results[1], 'hi'); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should trim correctly', function (done) { 56 | bucket.push(1, 3); 57 | bucket.push(2, 3); 58 | bucket.push(3, 3); 59 | bucket.len(function (err, result) { 60 | assert.equal(typeof result, 'number'); 61 | assert.equal(result, 3); 62 | done(); 63 | }); 64 | }); 65 | 66 | }); 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /test/ranaly.realtime.test.js: -------------------------------------------------------------------------------- 1 | /*jshint undef:false */ 2 | var assert = require('assert'); 3 | var redis = require('redis').createClient(); 4 | var sub = require('redis').createClient(); 5 | var ranaly = require('../index'); 6 | var client = ranaly.createClient(redis); 7 | 8 | var Realtime = client.Realtime; 9 | 10 | describe('Realtime', function () { 11 | var bucket = new Realtime('test'); 12 | redis.del(bucket.key); 13 | 14 | describe('basic flow', function () { 15 | it('should return 0 if the key do not exists', function (done) { 16 | bucket.get(function (err, result) { 17 | assert.equal(typeof result, 'number'); 18 | assert.equal(result, 0); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should have default increment', function (done) { 24 | bucket.incr(); 25 | bucket.get(function (err, result) { 26 | assert.equal(typeof result, 'number'); 27 | assert.equal(result, 1); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should accept custom increment', function (done) { 33 | bucket.incr(2); 34 | bucket.get(function (err, result) { 35 | assert.equal(typeof result, 'number'); 36 | assert.equal(result, 3); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('should set correctly', function (done) { 42 | bucket.set('20'); 43 | bucket.get(function (err, result) { 44 | assert.equal(typeof result, 'number'); 45 | assert.equal(result, 20); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should publish messages when the value has been changed', function (done) { 51 | sub.subscribe(bucket.key); 52 | sub.on('message', function (channel, message) { 53 | assert.equal(channel, bucket.key); 54 | assert.equal(message, '1'); 55 | done(); 56 | }); 57 | bucket.set(1); 58 | }); 59 | }); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /type/amount.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var ZMSCORE = ' \ 3 | local result = {} \ 4 | local length = #ARGV \ 5 | for i = 1, length do \ 6 | local score = 0 \ 7 | if #ARGV[i] == 8 then \ 8 | for j = 0, 23 do \ 9 | local k = tostring(j) \ 10 | if #k == 1 then \ 11 | k = "0" .. k \ 12 | end \ 13 | local r = redis.call("zscore", KEYS[1], ARGV[i] .. k) \ 14 | if r then \ 15 | score = score + r \ 16 | end \ 17 | end \ 18 | else \ 19 | score = redis.call("zscore", KEYS[1], ARGV[i]) \ 20 | if not score then \ 21 | score = 0 \ 22 | else \ 23 | score = tonumber(score) \ 24 | end \ 25 | end \ 26 | result[i] = score \ 27 | end \ 28 | return result \ 29 | '; 30 | 31 | var ZSUMSCORE = ' \ 32 | local length = #ARGV \ 33 | local score = 0 \ 34 | if #ARGV >= 1 then \ 35 | for i = 1, length do \ 36 | if #ARGV[i] == 8 then \ 37 | for j = 0, 23 do \ 38 | local k = tostring(j) \ 39 | if #k == 1 then \ 40 | k = "0" .. k \ 41 | end \ 42 | local result = redis.call("zscore", KEYS[1], ARGV[i] .. k) \ 43 | if result then \ 44 | score = score + result \ 45 | end \ 46 | end \ 47 | else \ 48 | local result = redis.call("zscore", KEYS[1], ARGV[i]) \ 49 | if result then \ 50 | score = score + result \ 51 | end \ 52 | end \ 53 | end \ 54 | else \ 55 | local result = redis.call("get", KEYS[1] .. ":TOTAL") \ 56 | if result then \ 57 | score = tonumber(result) \ 58 | end \ 59 | end \ 60 | return score \ 61 | '; 62 | module.exports = function (ranaly) { 63 | var db = ranaly.redisClient; 64 | 65 | var Amount = function (bucket) { 66 | this.bucket = bucket; 67 | this.key = ranaly.prefix + 'AMOUNT' + ':' + this.bucket; 68 | }; 69 | 70 | Amount.prototype.incr = function (increment, when, callback) { 71 | if (typeof increment === 'function') { 72 | callback = increment; 73 | increment = void 0; 74 | } else if (typeof when === 'function') { 75 | callback = when; 76 | when = void 0; 77 | } 78 | if (typeof increment !== 'number') { 79 | increment = 1; 80 | } 81 | when = moment(when); 82 | db.multi() 83 | .incrby(this.key + ':TOTAL', increment) 84 | .zincrby(this.key, increment,when.format('YYYYMMDDHH')) 85 | .exec(function (err, result) { 86 | if (typeof callback === 'function') { 87 | callback(err, Array.isArray(result) ? result[0] : result); 88 | } 89 | }); 90 | }; 91 | 92 | Amount.prototype.get = function (timeList, callback) { 93 | var next = function (err, result) { 94 | callback(err, result); 95 | }; 96 | db['eval'].apply(db, [ZMSCORE].concat(1).concat(this.key).concat(timeList).concat(next)); 97 | }; 98 | 99 | Amount.prototype.sum = function (timeList, callback) { 100 | var next = function (err, result) { 101 | callback(err, result); 102 | }; 103 | var tl = [ZSUMSCORE].concat(1).concat(this.key); 104 | if (Array.isArray(timeList) && timeList.length > 0) { 105 | tl = tl.concat(timeList).concat(next); 106 | } else { 107 | tl = tl.concat(next); 108 | } 109 | db['eval'].apply(db, tl); 110 | }; 111 | 112 | return Amount; 113 | }; 114 | 115 | -------------------------------------------------------------------------------- /type/data_list.js: -------------------------------------------------------------------------------- 1 | module.exports = function (ranaly) { 2 | var db = ranaly.redisClient; 3 | 4 | var DataList = function (bucket) { 5 | this.bucket = bucket; 6 | this.key = ranaly.prefix + 'DATALIST' + ':' + this.bucket; 7 | }; 8 | 9 | DataList.prototype.push = function (data, trim, callback) { 10 | if (typeof trim === 'function') { 11 | callback = trim; 12 | trim = void 0; 13 | } 14 | if (typeof trim === 'string') { 15 | trim = parseInt(trim, 10); 16 | } 17 | if (typeof trim !== 'number') { 18 | trim = 100; 19 | } 20 | 21 | db.multi() 22 | .lpush(this.key, JSON.stringify(data)) 23 | .ltrim(this.key, 0, trim - 1) 24 | .exec(function (err, result) { 25 | if (typeof callback === 'function') { 26 | if (!err) result = result[0]; 27 | callback(err, result); 28 | } 29 | }); 30 | }; 31 | 32 | DataList.prototype.range = function (start, stop, callback) { 33 | db.lrange(this.key, start, stop, function (err, result) { 34 | if (!err && Array.isArray(result)) { 35 | result = result.map(function (r) { 36 | return JSON.parse(r); 37 | }); 38 | } 39 | callback(err, result); 40 | }); 41 | }; 42 | 43 | DataList.prototype.len = function (callback) { 44 | db.llen(this.key, callback); 45 | }; 46 | 47 | return DataList; 48 | }; 49 | -------------------------------------------------------------------------------- /type/realtime.js: -------------------------------------------------------------------------------- 1 | module.exports = function (ranaly) { 2 | var db = ranaly.redisClient; 3 | 4 | var Realtime = function (bucket) { 5 | this.bucket = bucket; 6 | this.channel = this.key = ranaly.prefix + 'REALTIME' + ':' + this.bucket; 7 | }; 8 | 9 | Realtime.prototype.incr = function (increment, callback) { 10 | if (typeof increment === 'function') { 11 | callback = increment; 12 | increment = void 0; 13 | } 14 | if (typeof increment !== 'number') { 15 | increment = 1; 16 | } 17 | 18 | db.incrby(this.key, increment, function (err, result) { 19 | if (!err) { 20 | db.publish(this.channel, result); 21 | } 22 | if (typeof callback === 'function') { 23 | callback(err, result); 24 | } 25 | }); 26 | }; 27 | 28 | Realtime.prototype.get = function (callback) { 29 | db.get(this.key, function (err, result) { 30 | callback(err, parseInt(result, 10) || 0); 31 | }); 32 | }; 33 | 34 | Realtime.prototype.set = function (value, callback) { 35 | value = parseInt(value, 10) || 0; 36 | db.set(this.key, value, callback); 37 | db.publish(this.channel, value); 38 | }; 39 | 40 | return Realtime; 41 | }; 42 | --------------------------------------------------------------------------------