├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── bankersbox.js ├── package.json ├── test.html └── tests ├── adapter_file_system.test.coffee ├── adapter_localstorage.test.coffee ├── adapter_null.test.coffee ├── bb_exceptions.test.coffee ├── bb_key.test.coffee ├── bb_list.test.coffee ├── bb_misc.test.coffee ├── bb_set.test.coffee └── bb_string.test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *~ 3 | node_modules/ 4 | bankersbox.min.js 5 | src-tmp/ 6 | src-min-tmp/ 7 | src-cov/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012 Twilio Inc. 4 | 5 | Authors: Chad Etzel 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: min 3 | 4 | clean: 5 | rm -rf src-tmp 6 | rm -rf src-min-tmp 7 | rm -rf src-cov 8 | rm -f bankersbox.min.js 9 | 10 | bankersbox.min.js: bankersbox.js 11 | @echo "Sending to Google Closure compiler..." 12 | @curl -vvv -d output_format=text -d output_info=compiled_code -d compilation_level=SIMPLE_OPTIMIZATIONS --data-urlencode js_code@bankersbox.js http://closure-compiler.appspot.com/compile > bankersbox.min.js 2> /dev/null 13 | @echo "Done." 14 | 15 | min: bankersbox.min.js 16 | 17 | test: 18 | @echo "Testing bankersbox.js:" 19 | @NODE_PATH=`pwd` expresso -s tests/* 20 | 21 | testmin: min 22 | @mkdir -p src-min-tmp 23 | @rm -f src-min-tmp/bankersbox.js 24 | @ln -s ../bankersbox.min.js src-min-tmp/bankersbox.js 25 | @echo "Testing MINIFIED file:" 26 | @NODE_PATH=`pwd`/src-min-tmp expresso -s tests/* 27 | 28 | testall: test testmin 29 | 30 | coverage: 31 | mkdir -p src-tmp 32 | rm -f src-tmp/bankersbox.js 33 | ln -s ../bankersbox.js src-tmp/bankersbox.js 34 | rm -rf src-cov 35 | node-jscoverage src-tmp src-cov 36 | NODE_PATH=`pwd`/src-cov expresso -s tests/* 37 | 38 | .PHONY: all clean min test testmin testall coverage -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BankersBox 2 | 3 | [![Build Status](https://secure.travis-ci.org/twilio/BankersBox.png)](http://travis-ci.org/twilio/BankersBox) 4 | 5 | A redis-like wrapper for javascript data storage using localStorage as 6 | the default persistent data-store. 7 | 8 | ## Motivation 9 | 10 | Modern browsers provide a native javascript **localStorage** object 11 | which acts as a simple client-side key-value store per 12 | origin. Currently, storage is limited to 5MB and can only store 13 | strings as values (integers, floats, and booleans are coerced into 14 | strings upon save). 15 | 16 | [Redis](http://redis.io) is an insanely popular key-value server on 17 | steroids. It provides many more capabilities than just simple key-value 18 | storage. 19 | 20 | BankersBox aims to bring redis-like APIs and behavior to the 21 | client-side using localStorage as the default data backing 22 | mechanism. You can store strings, arrays, and javscript objects in 23 | BankersBox. Behind the scenes, BankersBox transparently uses 24 | JSON.stringify and JSON.parse to save and restore non-string values to 25 | their original types. 26 | 27 | BankersBox uses a pluggable storage adapter system so that other 28 | methods may be used to persist and restore data other than with 29 | localStorage. See the Storage Adapters section below. 30 | 31 | BankersBox is **not** a redis client. It is only meant to be a 32 | storage abstraction with redis-like APIs and behaviors for those 33 | familiar with the redis way. If you are looking for a javascript redis 34 | client (e.g. for use with node.js) please turn back now. 35 | 36 | ## Usage 37 | 38 | Like redis, BankersBox has the concept of "databases" which are just 39 | stores with different index numbers. BankersBox takes the id of the 40 | store you wish to use in the constructor. Each id maps to a different 41 | store, so if you set "foo" in two separate stores, they will retain 42 | their separate values. 43 | 44 | ```js 45 | var bb = new BankersBox(1); 46 | 47 | bb.set("foo", "bar"); 48 | bb.get("foo"); // returns "bar" 49 | 50 | bb.set("count", 10); 51 | bb.incr("count"); // sets "count" to 11, returns 11 52 | 53 | bb.incr("newcount"); // sets "newcount" to 1, returns 1 54 | 55 | bb.lpush("mylist", "hello"); 56 | bb.lrange("mylist", 0, -1); // returns ["hello"] 57 | 58 | bb.rpush("mylist", "world"); 59 | bb.lrange("mylist", 0, -1); // returns ["hello", "world"] 60 | 61 | bb.sadd("myset", "apple"); 62 | bb.sadd("myset", "oragne"); 63 | bb.smembers("myset"); // returns ["apple", "orange"] 64 | bb.sismember("myset", "apple"); // returns true 65 | bb.sismember("myset", "lemon"); // returns false 66 | bb.scard("myset"); // returns 2 67 | 68 | ``` 69 | 70 | ## Notes on BankersBox data types 71 | 72 | ### Keys 73 | 74 | Keys must be strings. 75 | 76 | ### Simple key-value pairs 77 | 78 | In redis, key-value values must also be strings, but with BankersBox you can also store numbers, booleans, arrays, and objects. 79 | 80 | When dealing with operations that involve numeric values (incr, decr, etc) the values should be numbers (duh). 81 | 82 | ### LISTS 83 | 84 | Values stored in lists can also be strings, numbers, booleans, arrays, and objects. 85 | 86 | ### SETS 87 | 88 | Values stored in sets should be strings. When storing arrays or objects in sets, the behavior is undefined. 89 | 90 | ## Implemented Functions 91 | 92 | * [KEYS](http://redis.io/commands#generic) 93 | * keys 94 | * del 95 | * exists 96 | * type 97 | * [STRINGS](http://redis.io/commands#string) 98 | * append 99 | * decr 100 | * decrby 101 | * incr 102 | * incrby 103 | * get 104 | * getset 105 | * set 106 | * setnx 107 | * strlen 108 | * [HASHES](http://redis.io/commands#hash) 109 | * none, see TODO.md 110 | * [LISTS](http://redis.io/commands#list) 111 | * lindex 112 | * llen 113 | * lpop 114 | * lpush 115 | * lpushx 116 | * lrange 117 | * lrem 118 | * lset 119 | * ltrim 120 | * rpop 121 | * rpoplpush 122 | * rpush 123 | * rpushx 124 | * [SETS](http://redis.io/commands#set) 125 | * sadd 126 | * scard 127 | * sismember 128 | * smembers 129 | * smove 130 | * spop 131 | * srandmember 132 | * srem 133 | * [SORTED SETS](http://redis.io/commands#sorted_set) (ZSET) 134 | * none, see TODO.md 135 | * [CONNECTION](http://redis.io/commands#connection) 136 | * select 137 | 138 | ## Storage Adapters 139 | 140 | By default, BankersBox will use localStorage to persist data between sessions. 141 | 142 | The main bankersbox.js file comes with two adapters built in: 143 | 144 | * BankersBoxLocalStorageAdapter - default adapter for using localStorage 145 | * BankersBoxNullAdapter - adapter which does not persist or restore data anywhere, good for testing 146 | 147 | To specify your desired storage adapter, pass it into the constructor in the options hash: 148 | 149 | ```js 150 | bb = new BankersBox(1, {adapter: new BankersBoxNullAdapter()}); 151 | ``` 152 | 153 | To create your own adapter, you must create an object which has the following three functions: 154 | 155 | * ```storeItem(key, value)``` 156 | * ```stores a value in the data-store associated with a key``` 157 | * ```param: key - a string``` 158 | * ```param: value - a string``` 159 | * ```returns: void``` 160 | * ```getItem(key)``` 161 | * ```retrieves a value from the data-store for the associated key``` 162 | * ```param: key - a string``` 163 | * ```returns: string - the value represented by key``` 164 | * ```removeItem(key)``` 165 | * ```deletes the value in the data-store associated with the key``` 166 | * ```param: key - a string``` 167 | * ```returns: void``` 168 | 169 | If you create your own adapters, please add appropriate unit tests 170 | following the examples in the tests/adapter_*.test.coffee files. 171 | 172 | ## API Usage 173 | 174 | BankersBox has tried to adhere as closely as possible to the Redis 175 | command API and return values associated with each command. 176 | 177 | You can read the full [Redis Command Documentation](http://redis.io/commands). 178 | 179 | In the cases where Redis would normally return a ```nil``` value, 180 | BankersBox will return the javascript ```null``` value. In the cases 181 | where Redis would return an error, BankersBox will throw a 182 | ```BankersBoxException``` or ```BankersBoxKeyException``` depending on 183 | the situation. 184 | 185 | ## Usage On Your Webpage 186 | 187 | Simply copy bankersbox.js (or bankersbox.min.js if you have minified) 188 | to your site's static assets folder and link to it inside your page 189 | source: 190 | 191 | ``` 192 | 193 | ``` 194 | 195 | BankersBox depends on using a global JSON object which must provide 196 | ```parse``` and ```stringify``` functions. Make sure to include a JSON 197 | library in the page as well or BankersBox will throw an exception when 198 | you try to instantiate. 199 | 200 | ## Development 201 | 202 | To develop on BankersBox, simply hack on bankersbox.js. 203 | 204 | To install the node modules needed for testing, simply: 205 | 206 | ```npm install``` 207 | 208 | ## Testing 209 | 210 | BankersBox has a large suite of unit tests. If you add new 211 | functionality, please update (or add) the appropriate 212 | tests/bb_*.test.coffee file with new tests. You can run the unit tests 213 | with: 214 | 215 | ```make test``` 216 | 217 | ## Minify 218 | 219 | BankersBox is minified using the Google Closure javascript compression tool. This is handled in the Makefile with: 220 | 221 | ```make min``` 222 | 223 | This will output bankersbox.min.js. To test the minified version as well, simply: 224 | 225 | ```make testall``` 226 | 227 | ## License 228 | 229 | MIT License - see LICENSE for details 230 | 231 | ## Misc 232 | 233 | Pull requests welcome, please contribute! -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Error checking around correct number of arguments 4 | 5 | * Allow functions to take variable arguments where appropriate (e.g. lpush, etc) 6 | 7 | * There currenlty exists no concept of key expiry as redis has. Is this even wanted, needed? 8 | 9 | * Privatize some of the internal functions so they are not exposed to the public (done in the privatize branch) 10 | 11 | * Async version of the API so that new storage adapters could work on an async basis (e.g. AJAX posting to server storage)? 12 | 13 | * Allow SETs to store aribrary data-type values 14 | 15 | ## Unimplemented Functions ## 16 | 17 | Functions that are missing which might prove useful. All expiry-based functions are omitted since that is a larger question. 18 | 19 | * [KEYS](http://redis.io/commands#generic) 20 | * keys (mostly implemented, only returns "*" at the moment) 21 | * move 22 | * randomkey 23 | * rename 24 | * renamenx 25 | * sort 26 | * [STRINGS](http://redis.io/commands#string) 27 | * getbit 28 | * getrange 29 | * mget 30 | * mset 31 | * msetnx 32 | * setbit 33 | * setrange 34 | * [HASHES](http://redis.io/commands#hash) 35 | * all functions 36 | * [LISTS](http://redis.io/commands#list) 37 | * blpop, brpop, brpoplpush (seem unnecessary as js is single-threaded?) 38 | * linsert 39 | * [SETS](http://redis.io/commands#set) 40 | * sdiff 41 | * sdiffstore 42 | * sinter 43 | * sinterstore 44 | * sunion 45 | * sunionstore 46 | * [SORTED SETS](http://redis.io/commands#sorted_set) (ZSET) 47 | * all functions 48 | -------------------------------------------------------------------------------- /bankersbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT License 3 | * 4 | * Copyright (c) 2012 Twilio Inc. 5 | * 6 | * Authors: Chad Etzel 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | (function(ctx) { 29 | 30 | if (typeof(window) === 'undefined') { 31 | window = {}; 32 | } 33 | 34 | if (typeof(window.localStorage) === 'undefined' && ctx !== window) { 35 | // fake out localStorage functionality, mostly for testing purposes 36 | window.localStorage = {}; 37 | window.localStorage.store = {}; 38 | window.localStorage.setItem = function(k, v) { 39 | window.localStorage.store[k] = v; 40 | }; 41 | window.localStorage.getItem = function(k) { 42 | var ret; 43 | ret = window.localStorage.store[k]; 44 | if (ret === undefined) { 45 | return null; 46 | } 47 | return ret; 48 | }; 49 | window.localStorage.removeItem = function(k) { 50 | delete window.localStorage.store[k]; 51 | }; 52 | window.localStorage.clear = function() { 53 | window.localStorage.store = {}; 54 | }; 55 | } 56 | 57 | // Array Remove - By John Resig (MIT Licensed) 58 | var arr_remove = function(array, from, to) { 59 | var rest = array.slice((to || from) + 1 || array.length); 60 | array.length = from < 0 ? array.length + from : from; 61 | return array.push.apply(array, rest); 62 | }; 63 | 64 | var _log = function(m) { 65 | if (console && console.log) { 66 | console.log(m); 67 | } 68 | }; 69 | 70 | var BB = function(dbi, opts) { 71 | 72 | if (isNaN(parseInt(dbi, 10))) { 73 | throw(new BankersBoxException("db index must be an integer")); 74 | } 75 | dbi = parseInt(dbi, 10); 76 | 77 | opts = opts || {}; 78 | 79 | var self = this; 80 | 81 | var db = dbi; 82 | var adapter = opts.adapter; 83 | 84 | if (adapter === undefined) { 85 | adapter = new BankersBoxLocalStorageAdapter(); 86 | } else if (adapter === null) { 87 | adapter = new BankersBoxNullAdapter(); 88 | } 89 | 90 | var prefix = "bb:" + db.toString() + ":"; 91 | var keyskey = "bb:" + db.toString() + "k:___keys___"; 92 | var store = {}; 93 | 94 | this.toString = function() { 95 | return "bb:" + db.toString(); 96 | }; 97 | 98 | if (typeof(JSON) == 'undefined' && !(window.JSON && window.JSON.parse && window.JSON.stringify)) { 99 | throw("No JSON support detected. Please include a JSON module with 'parse' and 'stringify' functions."); 100 | } 101 | 102 | var exists_raw = function(k) { 103 | var ret = store[k] || adapter.getItem(k); 104 | return ret ? true : false; 105 | }; 106 | 107 | var get_raw = function(k, t) { 108 | var ret = store[k]; 109 | if (ret !== undefined) { 110 | return ret; 111 | } 112 | ret = adapter.getItem(k); 113 | var obj = ret; 114 | try { 115 | obj = JSON.parse(ret); 116 | } catch (e) { 117 | } finally { 118 | store[k] = obj; 119 | } 120 | return obj; 121 | }; 122 | 123 | var set_raw = function(k, v, t) { 124 | store[k] = v; 125 | adapter.storeItem(k, JSON.stringify(v)); 126 | }; 127 | 128 | var del_raw = function(k) { 129 | delete store[k]; 130 | adapter.removeItem(k); 131 | }; 132 | 133 | var get_raw_value = function(k, t) { 134 | var val = get_raw(k, t); 135 | if (val === null) { 136 | return null; 137 | } 138 | return val.v; 139 | }; 140 | 141 | var get_raw_meta = function(k, meta, t) { 142 | var val = get_raw(k, t); 143 | if (val === null) { 144 | return null; 145 | } 146 | return val.m[meta]; 147 | }; 148 | 149 | var set_raw_value = function(k, v, t) { 150 | var val = get_raw(k, t); 151 | if (val === undefined || val === null) { 152 | val = {}; 153 | val.m = {}; 154 | } 155 | val.v = v; 156 | if (t !== undefined) { 157 | val.m.t = t; 158 | } 159 | set_raw(k, val, t); 160 | }; 161 | 162 | var set_raw_meta = function(k, meta, v) { 163 | var val = store[k]; 164 | if (val === undefined || val === null) { 165 | return; 166 | } 167 | val.m[meta] = v; 168 | set_raw(k, val); 169 | }; 170 | 171 | var exists_bbkey = function(k) { 172 | return exists_raw(prefix + k); 173 | }; 174 | 175 | var set_bbkey = function(k, v, t) { 176 | set_raw_value(prefix + k, v, t); 177 | if (t !== undefined) { 178 | set_bbkeytype(k, t); 179 | } 180 | keystore[k] = 1; 181 | set_raw_value(keyskey, keystore, "set"); 182 | }; 183 | 184 | var get_bbkey = function(k, t) { 185 | return get_raw_value(prefix + k, t); 186 | }; 187 | 188 | var del_bbkey = function(k) { 189 | del_raw(prefix + k); 190 | delete keystore[k]; 191 | set_raw_value(keyskey, keystore, "set"); 192 | }; 193 | 194 | var set_bbkeymeta = function(k, meta, v) { 195 | set_raw_meta(prefix + k, meta, v); 196 | }; 197 | 198 | var get_bbkeymeta = function(k, meta) { 199 | return get_raw_meta(prefix + k, meta); 200 | }; 201 | 202 | var set_bbkeytype = function(k, v) { 203 | set_bbkeymeta(k, "t", v); 204 | }; 205 | 206 | var get_bbkeytype = function(k) { 207 | return get_bbkeymeta(k, "t"); 208 | }; 209 | 210 | var validate_key = function(k, checktype) { 211 | var keytype = self.type(k); 212 | var tmap = {}; 213 | tmap["get"] = "string"; 214 | tmap["set"] = "string"; 215 | tmap["strlen"] = "string"; 216 | tmap["setnx"] = "string"; 217 | tmap["append"] = "string"; 218 | tmap["incr"] = "string"; 219 | tmap["incrby"] = "string"; 220 | tmap["getset"] = "string"; 221 | tmap["lpush"] = "list"; 222 | tmap["lpushx"] = "list"; 223 | tmap["lpop"] = "list"; 224 | tmap["rpush"] = "list"; 225 | tmap["rpushx"] = "list"; 226 | tmap["rpop"] = "list"; 227 | tmap["rpoplpush"] = "list"; 228 | tmap["llen"] = "list"; 229 | tmap["lindex"] = "list"; 230 | tmap["lrange"] = "list"; 231 | tmap["lrem"] = "list"; 232 | tmap["lset"] = "list"; 233 | tmap["ltrim"] = "list"; 234 | tmap["sadd"] = "set"; 235 | tmap["scard"] = "set"; 236 | tmap["sismember"] = "set"; 237 | tmap["smembers"] = "set"; 238 | tmap["srem"] = "set"; 239 | tmap["smove"] = "set"; 240 | tmap["spop"] = "set"; 241 | tmap["srandmember"] = "set"; 242 | 243 | if (tmap[checktype] === undefined) { 244 | throw new BankersBoxException("unknown key operation in validate_key"); 245 | } 246 | 247 | if (keytype === undefined || keytype === null || tmap[checktype] === undefined || tmap[checktype] == keytype) { 248 | return true; 249 | } 250 | throw(new BankersBoxKeyException("invalid operation on key type: " + keytype)); 251 | }; 252 | 253 | /* ---- PRIVILEGED METHODS ---- */ 254 | 255 | /* ---- KEY ---- */ 256 | 257 | this.del = function(k) { 258 | var ret = 0; 259 | if (get_bbkey(k)) { 260 | ret = 1; 261 | } 262 | del_bbkey(k); 263 | return ret; 264 | }; 265 | 266 | this.exists = function(k) { 267 | return exists_bbkey(k); 268 | }; 269 | 270 | this.type = function(k) { 271 | return get_bbkeytype(k); 272 | }; 273 | 274 | 275 | /* ---- STRING ---- */ 276 | 277 | this.get = function(k) { 278 | validate_key(k, "get"); 279 | return get_bbkey(k); 280 | }; 281 | 282 | this.getset = function(k, v) { 283 | validate_key(k, "getset"); 284 | var val = self.get(k); 285 | self.set(k, v); 286 | return val; 287 | }; 288 | 289 | this.append = function(k, v) { 290 | validate_key(k, "append"); 291 | var val = self.get(k); 292 | if (val !== null) { 293 | self.set(k, val + v); 294 | return (val + v).length; 295 | } 296 | self.set(k, v); 297 | return v.toString().length; 298 | }; 299 | 300 | this.decr = function(k) { 301 | return self.incrby(k, -1); 302 | }; 303 | 304 | this.decrby = function(k, i) { 305 | return self.incrby(k, 0 - i); 306 | }; 307 | 308 | this.incr = function(k) { 309 | return self.incrby(k, 1); 310 | }; 311 | 312 | this.incrby = function(k, i) { 313 | validate_key(k, "incrby"); 314 | var val = self.get(k); 315 | if (val !== null) { 316 | if (isNaN(parseInt(val, 10))) { 317 | throw(new BankersBoxKeyException("key is not parsable as an integer")); 318 | } 319 | self.set(k, val + i); 320 | return val + i; 321 | } 322 | self.set(k, i); 323 | return i; 324 | }; 325 | 326 | this.set = function(k, v) { 327 | validate_key(k, "set"); 328 | set_bbkey(k, v); 329 | set_bbkeytype(k, "string"); 330 | return "OK"; 331 | }; 332 | 333 | this.setnx = function(k, v) { 334 | validate_key(k, "setnx"); 335 | var val = self.get(k); 336 | if (val !== null) { 337 | return 0; 338 | } 339 | self.set(k, v); 340 | return 1; 341 | }; 342 | 343 | this.strlen = function(k) { 344 | validate_key(k, "strlen"); 345 | var v = self.get(k); 346 | if (v !== null) { 347 | return v.toString().length; 348 | } 349 | return 0; 350 | }; 351 | 352 | /* ---- LIST ---- */ 353 | 354 | this.llen = function(k) { 355 | validate_key(k, "llen"); 356 | var val = get_bbkey(k, "list"); 357 | if (val === null) { 358 | return 0; 359 | } 360 | return val.length; 361 | }; 362 | 363 | this.lindex = function(k, i) { 364 | validate_key(k, "lindex"); 365 | var val = get_bbkey(k, "list"); 366 | if (val !== null) { 367 | if (i < 0) { 368 | i = val.length + i; 369 | } 370 | var ret = val[i]; 371 | if (ret === undefined) { 372 | ret = null; 373 | } 374 | return ret; 375 | } 376 | return null; 377 | }; 378 | 379 | this.lpop = function(k) { 380 | validate_key(k, "lpop"); 381 | var val = get_bbkey(k, "list"); 382 | if (val === null) { 383 | return null; 384 | } 385 | var ret = val.shift(); 386 | if (val.length === 0) { 387 | self.del(k); 388 | } else { 389 | set_bbkey(k, val, "list"); 390 | } 391 | return ret; 392 | }; 393 | 394 | this.lpush = function(k, v) { 395 | validate_key(k, "lpush"); 396 | var val = get_bbkey(k, "list"); 397 | if (val === null) { 398 | val = []; 399 | } 400 | val.unshift(v); 401 | set_bbkey(k, val, "list"); 402 | return val.length; 403 | }; 404 | 405 | this.lpushx = function(k, v) { 406 | validate_key(k, "lpushx"); 407 | var val = get_bbkey(k, "list"); 408 | if (val !== null) { 409 | return self.lpush(k, v); 410 | } 411 | return 0; 412 | }; 413 | 414 | this.lrange = function(k, start, end) { 415 | validate_key(k, "lrange"); 416 | var val = get_bbkey(k, "list"); 417 | if (val === null) { 418 | return []; 419 | } 420 | if (end === -1) { 421 | return val.slice(start); 422 | } 423 | return val.slice(start, end + 1); 424 | }; 425 | 426 | this.lrem = function(k, count, v) { 427 | validate_key(k, "lrem"); 428 | var val = get_bbkey(k, "list"); 429 | if (val === null) { 430 | return 0; 431 | } 432 | var ret = 0; 433 | var to_remove = []; 434 | for (var i = 0; i < val.length; i++) { 435 | if (val[i] == v) { 436 | to_remove.push(i); 437 | ret++; 438 | } 439 | } 440 | 441 | if (count > 0) { 442 | to_remove = to_remove.slice(0, count); 443 | } else if (count < 0) { 444 | to_remove = to_remove.slice(count); 445 | } 446 | 447 | while(to_remove.length) { 448 | var el = to_remove.pop(); 449 | arr_remove(val, el); 450 | } 451 | 452 | if (val.length === 0) { 453 | self.del(k); 454 | } else { 455 | set_bbkey(k, val, "list"); 456 | } 457 | if (count == 0) { 458 | return ret; 459 | } else { 460 | return Math.min(ret, Math.abs(count)); 461 | } 462 | }; 463 | 464 | this.lset = function(k, i, v) { 465 | validate_key(k, "lset"); 466 | var val = get_bbkey(k, "list"); 467 | if (val === null) { 468 | throw(new BankersBoxKeyException("no such key")); 469 | } 470 | if (i < 0) { 471 | i = val.length + i; 472 | } 473 | if (i < 0 || i >= val.length) { 474 | throw(new BankersBoxException("index out of range")); 475 | } 476 | val[i] = v; 477 | set_bbkey(k, val, "list"); 478 | return "OK"; 479 | }; 480 | 481 | this.ltrim = function(k, start, end) { 482 | validate_key(k, "ltrim"); 483 | var val = get_bbkey(k, "list"); 484 | if (val === null) { 485 | return "OK"; 486 | } 487 | if (end === -1) { 488 | val = val.slice(start); 489 | } else { 490 | val = val.slice(start, end + 1); 491 | } 492 | if (val.length === 0) { 493 | self.del(k); 494 | } else { 495 | set_bbkey(k, val, "list"); 496 | } 497 | return "OK"; 498 | }; 499 | 500 | this.rpop = function(k) { 501 | validate_key(k, "rpop"); 502 | var val = get_bbkey(k, "list"); 503 | if (val === null) { 504 | return null; 505 | } 506 | var ret = val.pop(); 507 | if (val.length === 0) { 508 | self.del(k); 509 | } else { 510 | set_bbkey(k, val, "list"); 511 | } 512 | return ret; 513 | }; 514 | 515 | this.rpush = function(k, v) { 516 | validate_key(k, "rpush"); 517 | var val = get_bbkey(k); 518 | if (val === null) { 519 | val = []; 520 | } 521 | val.push(v); 522 | set_bbkey(k, val, "list"); 523 | return val.length; 524 | }; 525 | 526 | this.rpushx = function(k, v) { 527 | validate_key(k, "rpushx"); 528 | var val = get_bbkey(k, "list"); 529 | if (val !== null) { 530 | return self.rpush(k, v); 531 | } 532 | return 0; 533 | }; 534 | 535 | this.rpoplpush = function(src, dest) { 536 | validate_key(src, "rpoplpush"); 537 | validate_key(dest, "rpoplpush"); 538 | 539 | var srcval = get_bbkey(src, "list"); 540 | var destval = get_bbkey(dest, "list"); 541 | 542 | if (srcval === null) { 543 | return null; 544 | } 545 | 546 | var val = self.rpop(src); 547 | self.lpush(dest, val); 548 | return val; 549 | }; 550 | 551 | 552 | /* ---- SET ---- */ 553 | 554 | this.sadd = function(k, v) { 555 | validate_key(k, "sadd"); 556 | var val = get_bbkey(k, "set"); 557 | var scard; 558 | var ret = 0; 559 | if (val === null) { 560 | val = {}; 561 | scard = 0; 562 | } else { 563 | scard = parseInt(get_bbkeymeta(k, "card"), 10); 564 | } 565 | if (val[v] !== 1) { 566 | ret = 1; 567 | scard = scard + 1; 568 | } 569 | val[v] = 1; 570 | set_bbkey(k, val, "set"); 571 | set_bbkeymeta(k, "card", scard); 572 | return ret; 573 | }; 574 | 575 | this.scard = function(k) { 576 | validate_key(k, "scard"); 577 | if (self.exists(k)) { 578 | return parseInt(get_bbkeymeta(k, "card"), 10); 579 | }; 580 | return 0; 581 | }; 582 | 583 | this.sismember = function(k, v) { 584 | validate_key(k, "sismember"); 585 | var val = get_bbkey(k, "set"); 586 | if (val === null) { 587 | return false; 588 | } 589 | if (val[v] === 1) { 590 | return true; 591 | } 592 | return false; 593 | }; 594 | 595 | this.smembers = function(k) { 596 | validate_key(k, "smembers"); 597 | var val = get_bbkey(k, "set"); 598 | if (val === null) { 599 | return []; 600 | } 601 | var ret = []; 602 | for (var v in val) { 603 | if (val.hasOwnProperty(v)) { 604 | ret.push(v); 605 | } 606 | } 607 | return ret; 608 | }; 609 | 610 | this.smove = function(src, dest, v) { 611 | validate_key(src, "smove"); 612 | validate_key(dest, "smove"); 613 | var srcval = get_bbkey(src, "set"); 614 | if (srcval === null) { 615 | return 0; 616 | } 617 | var ret = self.srem(src, v); 618 | if (ret) { 619 | self.sadd(dest, v); 620 | } 621 | return ret; 622 | }; 623 | 624 | this.spop = function(k) { 625 | validate_key(k, "spop"); 626 | var member = self.srandmember(k); 627 | if (member !== null) { 628 | self.srem(k, member); 629 | } 630 | return member; 631 | }; 632 | 633 | this.srandmember = function(k) { 634 | validate_key(k, "srandmember"); 635 | var val = get_bbkey(k, "set"); 636 | if (val === null) { 637 | return null; 638 | } 639 | var members = self.smembers(k); 640 | var i = Math.floor(Math.random() * members.length); 641 | var ret = members[i]; 642 | return ret; 643 | }; 644 | 645 | this.srem = function(k, v) { 646 | validate_key(k, "srem"); 647 | var val = get_bbkey(k, "set"); 648 | if (val === null) { 649 | return 0; 650 | } 651 | var ret = 0; 652 | if (val[v] === 1) { 653 | ret = 1; 654 | delete val[v]; 655 | var scard = parseInt(get_bbkeymeta(k, "card"), 10) - 1; 656 | if (scard === 0) { 657 | self.del(k); 658 | } else { 659 | set_bbkey(k, val, "set"); 660 | set_bbkeymeta(k, "card", scard); 661 | } 662 | } 663 | return ret; 664 | }; 665 | 666 | /* ---- SERVER ---- */ 667 | 668 | this.keys = function(filter) { 669 | // TODO: implement filter.. for now just return * 670 | var ret = []; 671 | for (var k in keystore) { 672 | if (keystore.hasOwnProperty(k)) { 673 | ret.push(k); 674 | } 675 | } 676 | return ret; 677 | }; 678 | 679 | this.flushdb = function() { 680 | var keys = self.keys("*"); 681 | for (var i in keys) { 682 | self.del(keys[i]); 683 | } 684 | del_raw(keyskey); 685 | return "OK"; 686 | }; 687 | 688 | this.select = function(i) { 689 | if (isNaN(parseInt(i, 10))) { 690 | throw(new BankersBoxException("db index must be an integer")); 691 | } 692 | db = i; 693 | prefix = "bb:" + i.toString() + ":"; 694 | keyskey = "bb:" + i.toString() + "k:___keys___"; 695 | keystore = get_raw_value(keyskey, "set") || {}; 696 | }; 697 | 698 | var keystore = get_raw_value(keyskey, "set") || {}; 699 | 700 | }; /* end constructor */ 701 | 702 | 703 | BB.toString = function() { 704 | return "[object BankersBox]"; 705 | }; 706 | 707 | 708 | var BankersBoxException = function(msg) { 709 | this.type = "BankersBoxException"; 710 | this.toString = function() { 711 | return this.type + ": " + msg.toString(); 712 | }; 713 | }; 714 | 715 | var BankersBoxKeyException = function(msg) { 716 | BankersBoxException.call(this, msg); 717 | this.type = "BankersBoxKeyException"; 718 | }; 719 | 720 | var BankersBoxLocalStorageAdapter = function() { 721 | 722 | if (typeof(window) === 'undefined' || typeof(window.localStorage) === 'undefined') { 723 | throw("window.localStorage is undefined, consider a different storage adapter"); 724 | } 725 | 726 | this.getItem = function(k) { 727 | return window.localStorage.getItem(k); 728 | }; 729 | 730 | this.storeItem = function(k, v) { 731 | try { 732 | window.localStorage.setItem(k, v); 733 | } catch (e) { 734 | if (e == QUOTA_EXCEEDED_ERR) { 735 | // TODO: properly handle quota exceeded behavior 736 | } 737 | throw(e); 738 | } 739 | }; 740 | 741 | this.removeItem = function(k) { 742 | window.localStorage.removeItem(k); 743 | }; 744 | 745 | this.clear = function() { 746 | window.localStorage.clear(); 747 | }; 748 | }; 749 | 750 | var BankersBoxNullAdapter = function() { 751 | 752 | this.getItem = function(k) { 753 | return null; 754 | }; 755 | 756 | this.storeItem = function(k, v) { 757 | }; 758 | 759 | this.removeItem = function(k) { 760 | }; 761 | 762 | this.clear = function() { 763 | }; 764 | }; 765 | 766 | var BankersBoxFileSystemAdapter = function(filename) { 767 | if (!(typeof(module) !== 'undefined' && module && module.exports)) { 768 | throw("this does not appear to be a server context, consider a different storage adapter"); 769 | } 770 | var store = {}; 771 | var fs = require('fs'); 772 | 773 | var init = function() { 774 | if (fs.existsSync(filename)) { 775 | var data = fs.readFileSync(filename, {encoding: 'utf8'}); 776 | if (data) { 777 | store = JSON.parse(data); 778 | } 779 | } 780 | }; 781 | 782 | var persist = function() { 783 | fs.writeFileSync(filename, JSON.stringify(store), {encoding: 'utf8'}); 784 | }; 785 | 786 | init(); 787 | 788 | this.getItem = function(k) { 789 | return store[k] || null; 790 | }; 791 | 792 | this.storeItem = function(k, v) { 793 | store[k] = v; 794 | persist(); 795 | }; 796 | 797 | this.removeItem = function(k) { 798 | delete store[k]; 799 | persist(); 800 | }; 801 | 802 | this.clear = function() { 803 | store = {}; 804 | fs.unlinkSync(filename); 805 | }; 806 | }; 807 | 808 | ctx.BankersBox = BB; 809 | ctx.BankersBoxException = BankersBoxException; 810 | ctx.BankersBoxKeyException = BankersBoxKeyException; 811 | ctx.BankersBoxLocalStorageAdapter = BankersBoxLocalStorageAdapter; 812 | ctx.BankersBoxNullAdapter = BankersBoxNullAdapter; 813 | ctx.BankersBoxFileSystemAdapter = BankersBoxFileSystemAdapter; 814 | 815 | if (ctx !== window) { 816 | ctx.mock_window = window; 817 | } 818 | 819 | })(typeof(module) !== 'undefined' && module && module.exports ? module.exports : window); 820 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BankersBox", 3 | "version": "0.1.0", 4 | "author": "Chad Etzel ", 5 | "description": "A redis-like API wrapper for data storage in javascript, using localStorage as a default.", 6 | "scripts": { 7 | "test": "NODE_PATH=`pwd` expresso -s tests/*" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/twilio/BankersBox.git" 12 | }, 13 | "dependencies": { 14 | "coffee-script": "*", 15 | "expresso": "*" 16 | }, 17 | "license": "MIT", 18 | "engine": { 19 | "node": ">=0.6" 20 | } 21 | } -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/adapter_file_system.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | fs = require "fs" 3 | { BankersBox, BankersBoxFileSystemAdapter } = require "bankersbox" 4 | 5 | exports.testFileSystemAdapter =-> 6 | adapter = new BankersBoxFileSystemAdapter("./bb_file_adapter.dat") 7 | assert.equal adapter.getItem("foo"), null, "test getItem" 8 | assert.equal adapter.storeItem("foo", "bar"), undefined, "test storeItem" 9 | assert.equal adapter.getItem("foo"), "bar", "test getItem after storeItem" 10 | assert.equal adapter.removeItem("foo"), undefined, "test removeItem" 11 | assert.equal adapter.getItem("foo"), null, "test getItem after removeItem" 12 | assert.equal adapter.clear(), undefined, "test clear" 13 | 14 | exports.testBankersBoxWithFileSystemAdapter =-> 15 | filename = "./bb_file_test.dat" 16 | adapter = new BankersBoxFileSystemAdapter(filename) 17 | bb = new BankersBox(1, {adapter: adapter}) 18 | bb.flushdb() 19 | assert.equal bb.set("foo", "bar"), "OK", "test set" 20 | assert.equal bb.get("foo"), "bar", "test get" 21 | assert.equal fs.existsSync(filename), true, "backing file should exist" 22 | adapter.clear() 23 | assert.equal fs.existsSync(filename), false, "backing file should not exist" 24 | -------------------------------------------------------------------------------- /tests/adapter_localstorage.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { mock_window, BankersBoxLocalStorageAdapter } = require "bankersbox" 3 | 4 | exports.testBBSAStoreItem = -> 5 | adapter = new BankersBoxLocalStorageAdapter() 6 | adapter.clear() 7 | assert.equal adapter.storeItem("foo", "bar"), undefined, "test storeItem return" 8 | assert.equal mock_window.localStorage.store["foo"], "bar", "test localStorage value" 9 | 10 | exports.testBBSAGetItem = -> 11 | adapter = new BankersBoxLocalStorageAdapter() 12 | adapter.clear() 13 | adapter.storeItem "foo", "bar" 14 | assert.equal adapter.getItem("foo"), "bar", "test getItem" 15 | 16 | exports.testBBSARemoveItem = -> 17 | adapter = new BankersBoxLocalStorageAdapter() 18 | adapter.clear() 19 | adapter.storeItem "foo", "bar" 20 | assert.equal adapter.removeItem("foo", "bar"), undefined, "test removeItem return" 21 | assert.equal adapter.getItem("foo"), null, "test removeItem - getItem after remove" 22 | assert.equal mock_window.localStorage.store["foo"], undefined, "test removeItem - localStorage value" 23 | 24 | exports.testBBSAClear = -> 25 | adapter = new BankersBoxLocalStorageAdapter() 26 | adapter.clear() 27 | adapter.storeItem "foo", "bar" 28 | adapter.storeItem "baz", "qux" 29 | adapter.storeItem "abc", "def" 30 | assert.equal adapter.clear(), undefined, "test clear return" 31 | assert.equal adapter.getItem("foo"), null, "test clear - getItem after clear 1" 32 | assert.equal adapter.getItem("baz"), null, "test clear - getItem after clear 2" 33 | assert.equal adapter.getItem("abc"), null, "test clear - getItem after clear 3" 34 | assert.eql mock_window.localStorage.store, {}, "test clear - localStorage value" 35 | -------------------------------------------------------------------------------- /tests/adapter_null.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox, BankersBoxNullAdapter } = require "bankersbox" 3 | 4 | exports.testNullAdapter =-> 5 | adapter = new BankersBoxNullAdapter() 6 | assert.equal adapter.getItem("foo"), undefined, "test getItem" 7 | assert.equal adapter.storeItem("foo", "bar"), undefined, "test storeItem" 8 | assert.equal adapter.removeItem("foo"), undefined, "test removeItem" 9 | assert.equal adapter.clear(), undefined, "test clear" 10 | 11 | exports.testBankersBoxWithNullAdapter =-> 12 | adapter = new BankersBoxNullAdapter() 13 | bb = new BankersBox(1, {adapter: adapter}) 14 | bb.flushdb() 15 | assert.equal bb.set("foo", "bar"), "OK", "test set" 16 | assert.equal bb.get("foo"), "bar", "test get" 17 | -------------------------------------------------------------------------------- /tests/bb_exceptions.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox, BankersBoxException, BankersBoxKeyException } = require "bankersbox" 3 | 4 | exports.testNewBankersBox = -> 5 | assert.throws (-> bb = new BankersBox()), BankersBoxException 6 | assert.throws (-> bb = new BankersBox("foo")), BankersBoxException 7 | 8 | # I tried to write a test for no JSON object, but it blows up the 9 | # testing framework, so I have to leave it out... boo 10 | #exports.testNoJSON = -> 11 | # assert.throws (-> 12 | # `JSON = undefined;` 13 | # bb = new BankersBox(1)) 14 | 15 | exports.testWrongKeyTypes = -> 16 | bb = new BankersBox(1) 17 | bb.flushdb() 18 | bb.set "foo", "bar" 19 | assert.throws (-> bb.sadd("foo", "qux")), BankersBoxKeyException 20 | # reset 21 | bb.del "foo" 22 | bb.lpush "foo", "bar" 23 | assert.throws (-> bb.get("foo")), BankersBoxKeyException 24 | # reset 25 | bb.del "foo" 26 | bb.lpush "foo", "bar" 27 | assert.throws (-> bb.smembers("foo")), BankersBoxKeyException 28 | 29 | exports.testIncrNonInt = -> 30 | bb = new BankersBox(1) 31 | bb.flushdb() 32 | bb.set "foo", "bar" 33 | assert.throws (-> bb.incr("foo")), BankersBoxKeyException 34 | assert.throws (-> bb.incrby("foo", 5)), BankersBoxKeyException 35 | assert.throws (-> bb.decr("foo")), BankersBoxKeyException 36 | assert.throws (-> bb.decrby("foo", 5)), BankersBoxKeyException 37 | 38 | exports.testLsetNoSuchKey = -> 39 | bb = new BankersBox(1) 40 | bb.flushdb() 41 | assert.throws (-> bb.lset("foo", 0, "bar")), BankersBoxKeyException 42 | 43 | exports.testLsetIndexOutOfRange = -> 44 | bb = new BankersBox(1) 45 | bb.flushdb() 46 | bb.lpush "foo", "bar" 47 | bb.lpush "foo", "baz" 48 | assert.throws (-> bb.lset("foo", 2, "qux")), BankersBoxException 49 | 50 | exports.testBBExceptionToString = -> 51 | bb = new BankersBox(1) 52 | bb.flushdb() 53 | try 54 | bb.lset "foo", 0, "bar" 55 | assert.equal true, false, "test exception toString - should not reach here" 56 | catch e 57 | assert.equal e.toString(), "BankersBoxKeyException: no such key", "test exception toString" -------------------------------------------------------------------------------- /tests/bb_key.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox } = require "bankersbox" 3 | 4 | ### KEY TESTS ### 5 | 6 | exports.testExistsFalse = -> 7 | bb = new BankersBox(1) 8 | bb.flushdb() 9 | assert.equal bb.exists("foo"), false, "key exists is false" 10 | 11 | exports.testExistsTrue = -> 12 | bb = new BankersBox(1) 13 | bb.flushdb() 14 | bb.set "foo", "bar" 15 | assert.equal bb.exists("foo"), true, "key exists is true" 16 | 17 | exports.testDelKey = -> 18 | bb = new BankersBox(1) 19 | bb.flushdb() 20 | bb.set "foo", "bar" 21 | assert.equal bb.exists("foo"), true, "test del key exists" 22 | bb.del "foo" 23 | assert.equal bb.get("foo"), null, "test del key get" 24 | assert.equal bb.exists("foo"), false, "test del key exists" 25 | 26 | exports.testDelReturn = -> 27 | bb = new BankersBox(1) 28 | bb.flushdb() 29 | bb.set "foo", "bar" 30 | assert.equal bb.del("foo"), 1, "test del return is 1" 31 | assert.equal bb.del("blah"), 0, "test del return is 0" -------------------------------------------------------------------------------- /tests/bb_list.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox } = require "bankersbox" 3 | 4 | ### LIST TESTS ### 5 | 6 | exports.testTypeList = -> 7 | bb = new BankersBox(1) 8 | bb.flushdb() 9 | bb.lpush "mylist", "a" 10 | assert.equal bb.type("mylist"), "list", "test list type" 11 | 12 | exports.testLpushNewKey = -> 13 | bb = new BankersBox(1) 14 | bb.flushdb() 15 | assert.eql bb.lpush("mylist", "apple"), 1, "test lpush new key" 16 | 17 | exports.testLpushExistingKey = -> 18 | bb = new BankersBox(1) 19 | bb.flushdb() 20 | bb.lpush "mylist", "apple" 21 | assert.eql bb.lpush("mylist", "banana"), 2, "test lpush existing key" 22 | 23 | exports.testRpushNewKey = -> 24 | bb = new BankersBox(1) 25 | bb.flushdb() 26 | assert.eql bb.rpush("mylist", "apple"), 1, "test rpush new key" 27 | 28 | exports.testRpushExistingKey = -> 29 | bb = new BankersBox(1) 30 | bb.flushdb() 31 | bb.rpush "mylist", "apple" 32 | assert.eql bb.rpush("mylist", "banana"), 2, "test rpush existing key" 33 | 34 | exports.testLrangeNonKey = -> 35 | bb = new BankersBox(1) 36 | bb.flushdb() 37 | assert.eql bb.lrange("keydoesnotexist", 0, -1), [], "test lrange non existant key" 38 | 39 | exports.testLrangeKey = -> 40 | bb = new BankersBox(1) 41 | bb.flushdb() 42 | bb.lpush "mylist", "a" 43 | bb.lpush "mylist", "b" 44 | bb.rpush "mylist", "c" 45 | bb.rpush "mylist", "d" 46 | assert.eql bb.lrange("mylist", 0, -1), ["b", "a", "c", "d"], "test lrange 1" 47 | assert.eql bb.lrange("mylist", 0, 1), ["b", "a"], "test lrange 2" 48 | assert.eql bb.lrange("mylist", -2, -1), ["c", "d"], "test lrange 3" 49 | assert.eql bb.lrange("mylist", 0, 0), ["b"], "test lrange 4" 50 | assert.eql bb.lrange("mylist", 0, 3), ["b", "a", "c", "d"], "test lrange 5" 51 | assert.eql bb.lrange("mylist", 1, -1), ["a", "c", "d"], "test lrange 6" 52 | assert.eql bb.lrange("mylist", 5, 10), [], "test lrange 7" 53 | 54 | exports.testLlenNonKey = -> 55 | bb = new BankersBox(1) 56 | bb.flushdb() 57 | assert.equal bb.llen("mylist"), 0, "test llen non existent key" 58 | 59 | exports.testLlen = -> 60 | bb = new BankersBox(1) 61 | bb.flushdb() 62 | assert.equal bb.llen("mylist"), 0, "test llen 0" 63 | bb.lpush "mylist", "a" 64 | assert.equal bb.llen("mylist"), 1, "test llen 1" 65 | bb.lpush "mylist", "b" 66 | assert.equal bb.llen("mylist"), 2, "test llen 2" 67 | bb.rpush "mylist", "c" 68 | assert.equal bb.llen("mylist"), 3, "test llen 3" 69 | bb.rpush "mylist", "d" 70 | assert.equal bb.llen("mylist"), 4, "test llen 4" 71 | 72 | exports.testLpushxNonKey = -> 73 | bb = new BankersBox(1) 74 | bb.flushdb() 75 | assert.equal bb.lpushx("mylist", "foo"), 0, "test lpushx non existent key" 76 | 77 | exports.testLpushxKey = -> 78 | bb = new BankersBox(1) 79 | bb.flushdb() 80 | bb.lpush "mylist", "apple" 81 | assert.equal bb.lpushx("mylist", "banana"), 2, "test lpushx on existing key" 82 | 83 | exports.testRpushxNonKey = -> 84 | bb = new BankersBox(1) 85 | bb.flushdb() 86 | assert.equal bb.rpushx("mylist", "foo"), 0, "test rpushx non existent key" 87 | 88 | exports.testRpushxKey = -> 89 | bb = new BankersBox(1) 90 | bb.flushdb() 91 | bb.lpush "mylist", "apple" 92 | assert.equal bb.rpushx("mylist", "banana"), 2, "test rpushx on existing key" 93 | 94 | exports.testLindexNonKey = -> 95 | bb = new BankersBox(1) 96 | bb.flushdb() 97 | assert.equal bb.lindex("mylist", 0), null, "test lindex non existent key" 98 | 99 | exports.testLindex = -> 100 | bb = new BankersBox(1) 101 | bb.flushdb() 102 | bb.lpush "mylist", "a" 103 | bb.lpush "mylist", "b" 104 | bb.rpush "mylist", "c" 105 | bb.rpush "mylist", "d" 106 | assert.equal bb.lindex("mylist", 0), "b", "test lindex 0" 107 | assert.equal bb.lindex("mylist", 1), "a", "test lindex 1" 108 | assert.equal bb.lindex("mylist", 2), "c", "test lindex 2" 109 | assert.equal bb.lindex("mylist", 3), "d", "test lindex 3" 110 | assert.equal bb.lindex("mylist", -4), "b", "test lindex -4" 111 | assert.equal bb.lindex("mylist", -3), "a", "test lindex -3" 112 | assert.equal bb.lindex("mylist", -2), "c", "test lindex -2" 113 | assert.equal bb.lindex("mylist", -1), "d", "test lindex -1" 114 | assert.equal bb.lindex("mylist", 4), null, "test lindex 4" 115 | assert.equal bb.lindex("mylist", -5), null, "test lindex -5" 116 | 117 | exports.testLpopNonKey = -> 118 | bb = new BankersBox(1) 119 | bb.flushdb() 120 | assert.equal bb.lpop("mylist"), null, "test lpop non existent key" 121 | 122 | exports.testLpopKey = -> 123 | bb = new BankersBox(1) 124 | bb.flushdb() 125 | bb.lpush "mylist", "a" 126 | bb.lpush "mylist", "b" 127 | bb.rpush "mylist", "c" 128 | bb.rpush "mylist", "d" 129 | assert.equal bb.lpop("mylist"), "b", "test lpop return" 130 | assert.eql bb.lrange("mylist", 0, -1), ["a", "c", "d"], "test lpop remaining" 131 | 132 | exports.testRpopNonKey = -> 133 | bb = new BankersBox(1) 134 | bb.flushdb() 135 | assert.equal bb.rpop("mylist"), null, "test rpop non existent key" 136 | 137 | exports.testRpopKey = -> 138 | bb = new BankersBox(1) 139 | bb.flushdb() 140 | bb.lpush "mylist", "a" 141 | bb.lpush "mylist", "b" 142 | bb.rpush "mylist", "c" 143 | bb.rpush "mylist", "d" 144 | assert.equal bb.rpop("mylist"), "d", "test rpop return" 145 | assert.eql bb.lrange("mylist", 0, -1), ["b", "a", "c"], "test rpop remaining" 146 | 147 | exports.testLset = -> 148 | bb = new BankersBox(1) 149 | bb.flushdb() 150 | bb.lpush "mylist", "a" 151 | bb.lpush "mylist", "b" 152 | bb.rpush "mylist", "c" 153 | bb.rpush "mylist", "d" 154 | assert.equal bb.lset("mylist", 0, "f"), "OK", "test lset return 1" 155 | assert.eql bb.lrange("mylist", 0, 0), ["f"], "test lset remaining 1" 156 | # test negative index 157 | assert.equal bb.lset("mylist", -1, "g"), "OK", "test lset return 2" 158 | assert.eql bb.lrange("mylist", -1, -1), ["g"], "test lset remaining 2" 159 | 160 | exports.testLtrimNonKey = -> 161 | bb = new BankersBox(1) 162 | bb.flushdb() 163 | assert.equal bb.ltrim("mylist", 1, -1), "OK", "test ltrim non existent key" 164 | 165 | exports.testLtrim = -> 166 | bb = new BankersBox(1) 167 | bb.flushdb() 168 | bb.lpush "mylist", "a" 169 | bb.lpush "mylist", "b" 170 | bb.rpush "mylist", "c" 171 | bb.rpush "mylist", "d" 172 | assert.equal bb.ltrim("mylist", 1, -1), "OK", "test ltrim return 1" 173 | assert.eql bb.lrange("mylist", 0, -1), ["a", "c", "d"], "test ltrim remaining 1" 174 | # test non -1 second index 175 | assert.equal bb.ltrim("mylist", 0, 1), "OK", "test ltrim return 2" 176 | assert.eql bb.lrange("mylist", 0, -1), ["a", "c"], "test ltrim reamining 2" 177 | # test other negative second index 178 | bb.rpush "mylist", "e" 179 | bb.rpush "mylist", "f" 180 | assert.eql bb.lrange("mylist", 0, -1), ["a", "c", "e", "f"], "test ltrim sanity check" 181 | assert.equal bb.ltrim("mylist", 0, -2), "OK", "test ltrim return 3" 182 | assert.eql bb.lrange("mylist", 0, -1), ["a", "c", "e"], "test ltrim remaining 3" 183 | # test out of range returns - end out of range 184 | assert.equal bb.ltrim("mylist", 1, 10), "OK", "test ltrim return 4" 185 | assert.eql bb.lrange("mylist", 0, -1), ["c", "e"], "test ltrim remaining 4" 186 | # test out of range return - start > length 187 | assert.equal bb.ltrim("mylist", 5, 10), "OK", "test ltrim return 5" 188 | assert.eql bb.lrange("mylist", 0, -1), [], "test ltrim remaining 5" 189 | # test out of range return - start > end 190 | bb.rpush "mylist", "a" 191 | bb.rpush "mylist", "b" 192 | bb.rpush "mylist", "c" 193 | bb.rpush "mylist", "d" 194 | assert.eql bb.lrange("mylist", 0, -1), ["a", "b", "c", "d"], "test ltrim sanity check" 195 | assert.equal bb.ltrim("mylist", 1, 0), "OK", "test ltrim return 6" 196 | assert.eql bb.lrange("mylist", 0, -1), [], "test ltrim remaining 6" 197 | 198 | exports.testLremNonKey = -> 199 | bb = new BankersBox(1) 200 | bb.flushdb() 201 | assert.equal bb.lrem("mylist", 0, "a"), 0, "test lrem non existent key" 202 | 203 | exports.testLrem = -> 204 | bb = new BankersBox(1) 205 | bb.flushdb() 206 | bb.rpush "mylist", "a" 207 | bb.rpush "mylist", "b" 208 | bb.rpush "mylist", "a" 209 | bb.rpush "mylist", "c" 210 | bb.rpush "mylist", "a" 211 | bb.rpush "mylist", "d" 212 | bb.rpush "mylist", "a" 213 | assert.equal bb.lrem("mylist", 0, "a"), 4, "test lrem 0" 214 | assert.eql bb.lrange("mylist", 0, -1), ["b", "c", "d"], "test lrem 0 remaining" 215 | bb.del "mylist" 216 | bb.rpush "mylist", "a" 217 | bb.rpush "mylist", "b" 218 | bb.rpush "mylist", "a" 219 | bb.rpush "mylist", "c" 220 | bb.rpush "mylist", "a" 221 | bb.rpush "mylist", "d" 222 | bb.rpush "mylist", "a" 223 | assert.equal bb.lrem("mylist", 2, "a"), 2, "test lrem 2" 224 | assert.eql bb.lrange("mylist", 0, -1), ["b", "c", "a", "d", "a"], "test lrem 2 remaining" 225 | bb.del "mylist" 226 | bb.rpush "mylist", "a" 227 | bb.rpush "mylist", "b" 228 | bb.rpush "mylist", "a" 229 | bb.rpush "mylist", "c" 230 | bb.rpush "mylist", "a" 231 | bb.rpush "mylist", "d" 232 | bb.rpush "mylist", "a" 233 | assert.equal bb.lrem("mylist", -2, "a"), 2, "test lrem -2" 234 | assert.eql bb.lrange("mylist", 0, -1), ["a", "b", "a", "c", "d"], "test lrem -2 remaining" 235 | 236 | exports.testRpoplpush = -> 237 | bb = new BankersBox(1) 238 | bb.flushdb() 239 | # src exists, dst does not 240 | bb.rpush "mylist", "a" 241 | bb.rpush "mylist", "b" 242 | assert.equal bb.rpoplpush("mylist", "newlist"), "b", "test rpoplpush" 243 | assert.eql bb.lrange("mylist", 0, -1), ["a"], "test rpoplpush mylist remaining" 244 | assert.eql bb.lrange("newlist", 0, -1), ["b"], "test rpoplpush newlist remaining" 245 | # reset 246 | bb.del "mylist" 247 | bb.del "newlist" 248 | # src exists, dst exists 249 | bb.rpush "mylist", "a" 250 | bb.rpush "mylist", "b" 251 | bb.rpush "mylist", "c" 252 | bb.rpush "newlist", "d" 253 | bb.rpush "newlist", "e" 254 | bb.rpush "newlist", "f" 255 | assert.equal bb.rpoplpush("mylist", "newlist"), "c", "test rpoplpush" 256 | assert.eql bb.lrange("mylist", 0, -1), ["a", "b"], "test rpoplpush mylist remaining" 257 | assert.eql bb.lrange("newlist", 0, -1), ["c", "d", "e", "f"], "test rpoplpush newlist remaining" 258 | # reset 259 | bb.del "mylist" 260 | bb.del "newlist" 261 | # src not exist, dst does 262 | bb.rpush "newlist", "d" 263 | bb.rpush "newlist", "e" 264 | bb.rpush "newlist", "f" 265 | assert.equal bb.rpoplpush("mylist", "newlist"), null, "test rpoplpush" 266 | assert.eql bb.lrange("newlist", 0, -1), ["d", "e", "f"], "test rpoplpush newlist remaining" 267 | # reset 268 | bb.del "mylist" 269 | bb.del "newlist" 270 | # src is dst 271 | bb.rpush "mylist", "a" 272 | bb.rpush "mylist", "b" 273 | bb.rpush "mylist", "c" 274 | assert.equal bb.rpoplpush("mylist", "mylist"), "c", "test rpoplpush" 275 | assert.eql bb.lrange("mylist", 0, -1), ["c", "a", "b"], "test rpoplpush mylist remaining" 276 | 277 | exports.testLpopEmptyListRemoved = -> 278 | bb = new BankersBox(1) 279 | bb.flushdb() 280 | bb.lpush "foo", "a" 281 | bb.lpush "foo", "b" 282 | bb.lpop "foo" 283 | bb.lpop "foo" 284 | assert.equal bb.exists("foo"), false, "test lpop empty list becomes deleted" 285 | 286 | exports.testRpopEmptyListRemoved = -> 287 | bb = new BankersBox(1) 288 | bb.flushdb() 289 | bb.rpush "foo", "a" 290 | bb.rpush "foo", "b" 291 | bb.rpop "foo" 292 | bb.rpop "foo" 293 | assert.equal bb.exists("foo"), false, "test rpop empty list becomes deleted" 294 | 295 | exports.testRpoplpushEmptyListRemoved = -> 296 | bb = new BankersBox(1) 297 | bb.flushdb() 298 | bb.lpush "foo", "a" 299 | bb.lpush "foo", "b" 300 | bb.rpop "foo" 301 | bb.rpoplpush "foo", "bar" 302 | assert.equal bb.exists("foo"), false, "test rpoplpush empty list becomes deleted" 303 | 304 | exports.testLtrimEmptyListRemoved = -> 305 | bb = new BankersBox(1) 306 | bb.flushdb() 307 | bb.lpush "mylist", "a" 308 | bb.lpush "mylist", "b" 309 | bb.lpush "mylist", "c" 310 | bb.lpush "mylist", "d" 311 | # out of range - start > length 312 | bb.ltrim "mylist", 5, 10 313 | assert.equal bb.exists("mylist"), false, "test ltrim out of range deletes key 1" 314 | # reset 315 | bb.lpush "mylist", "a" 316 | bb.lpush "mylist", "b" 317 | bb.lpush "mylist", "c" 318 | bb.lpush "mylist", "d" 319 | # out of range - start > end 320 | bb.ltrim "mylist", 1, 0 321 | assert.equal bb.exists("mylist"), false, "test ltrim out of range deletes key 2" 322 | 323 | exports.testLremEmptyListRemoved = -> 324 | bb = new BankersBox(1) 325 | bb.flushdb() 326 | bb.lpush "mylist", "a" 327 | bb.lpush "mylist", "a" 328 | bb.lpush "mylist", "a" 329 | bb.lpush "mylist", "a" 330 | bb.lrem "mylist", 0, "a" 331 | assert.equal bb.exists("mylist"), false, "test lrem empty list becomes deleted" -------------------------------------------------------------------------------- /tests/bb_misc.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox } = require "bankersbox" 3 | 4 | exports.testBBToString = -> 5 | assert.equal BankersBox.toString(), "[object BankersBox]", "test BankersBox toString" 6 | 7 | exports.testBBInstanceToString = -> 8 | bb = new BankersBox(1) 9 | bb.flushdb() 10 | assert.equal bb.toString(), "bb:1", "test BankersBox instance toString" 11 | 12 | exports.testSelect = -> 13 | bb = new BankersBox(1) 14 | bb.flushdb() 15 | bb.set "foo", "bar" 16 | bb.select 2 17 | bb.set "foo", "qux" 18 | bb.select 1 19 | assert.equal bb.get("foo"), "bar", "test select 1" 20 | bb.select 2 21 | assert.equal bb.get("foo"), "qux", "test select 2" 22 | 23 | exports.testFlushdb = -> 24 | bb = new BankersBox(1) 25 | bb.flushdb() 26 | bb.set "foo", "bar" 27 | bb.set "baz", "qux" 28 | assert.equal bb.flushdb(), "OK", "test flushdb" 29 | assert.eql bb.keys("*"), [], "test flushdb keys remaining" 30 | 31 | exports.testKyesAll = -> 32 | bb = new BankersBox(1) 33 | bb.flushdb() 34 | bb.set "foo", "bar" 35 | bb.set "baz", "qux" 36 | assert.includes bb.keys(), "foo", "test keys * foo" 37 | assert.includes bb.keys(), "baz", "test keys * bar" 38 | assert.equal bb.keys().length, 2, "test keys * length" -------------------------------------------------------------------------------- /tests/bb_set.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox } = require "bankersbox" 3 | 4 | exports.testTypeSet = -> 5 | bb = new BankersBox(1) 6 | bb.flushdb() 7 | bb.sadd "myset", "foo" 8 | assert.equal bb.type("myset"), "set", "test set type" 9 | 10 | exports.testSaddNewMember = -> 11 | bb = new BankersBox(1) 12 | bb.flushdb() 13 | assert.equal bb.sadd("foo", "bar"), 1, "test sadd new member" 14 | 15 | exports.testSaddExistingMember = -> 16 | bb = new BankersBox(1) 17 | bb.flushdb() 18 | bb.sadd "foo", "bar" 19 | assert.equal bb.sadd("foo", "bar"), 0, "test sadd existing member" 20 | 21 | exports.testScardNonKey = -> 22 | bb = new BankersBox(1) 23 | bb.flushdb() 24 | assert.equal bb.scard("myset"), 0, "test scard non existent key" 25 | 26 | exports.testScard = -> 27 | bb = new BankersBox(1) 28 | bb.flushdb() 29 | bb.sadd "myset", "a" 30 | bb.sadd "myset", "b" 31 | bb.sadd "myset", "c" 32 | assert.equal bb.scard("myset"), 3, "test scard" 33 | 34 | exports.testSisimemberNonKey = -> 35 | bb = new BankersBox(1) 36 | bb.flushdb() 37 | assert.equal bb.sismember("myset", "foo"), false, "test sismember non existent key" 38 | 39 | exports.testSismemberTrue = -> 40 | bb = new BankersBox(1) 41 | bb.flushdb() 42 | bb.sadd "myset", "foo" 43 | bb.sadd "myset", "bar" 44 | assert.equal bb.sismember("myset", "foo"), true, "test sismember true 1" 45 | assert.equal bb.sismember("myset", "bar"), true, "test sismember true 2" 46 | 47 | exports.testSismemberTrue = -> 48 | bb = new BankersBox(1) 49 | bb.flushdb() 50 | bb.sadd "myset", "foo" 51 | bb.sadd "myset", "bar" 52 | assert.equal bb.sismember("myset", "baz"), false, "test sismember false 1" 53 | assert.equal bb.sismember("myset", "qux"), false, "test sismember false 2" 54 | 55 | exports.testSmembersNonKey = -> 56 | bb = new BankersBox(1) 57 | bb.flushdb() 58 | assert.eql bb.smembers("myset"), [], "test smemebers non existent key" 59 | 60 | exports.testSmembers = -> 61 | bb = new BankersBox(1) 62 | bb.flushdb() 63 | bb.sadd "myset", "a" 64 | bb.sadd "myset", "b" 65 | bb.sadd "myset", "c" 66 | ret = bb.smembers "myset" 67 | assert.equal ret.length, 3, "test smembers return length" 68 | assert.includes ret, "a", "test smembers contains a" 69 | assert.includes ret, "b", "test smembers contains b" 70 | assert.includes ret, "c", "test smembers contains c" 71 | 72 | exports.testSmove = -> 73 | bb = new BankersBox(1) 74 | bb.flushdb() 75 | # src and dst don't exists 76 | assert.equal bb.smove("myset1", "myset2", "foo"), 0, "test smove 1" 77 | assert.equal bb.exists("myset1"), false, "test smove 1 myset1 exists false" 78 | assert.equal bb.exists("myset2"), false, "test smove 1 myset2 exists false" 79 | # src exists, dst doesn't exist 80 | bb.sadd "myset1", "a" 81 | bb.sadd "myset1", "b" 82 | assert.equal bb.smove("myset1", "myset2", "a"), 1, "test smove 2" 83 | assert.eql bb.smembers("myset1"), ["b"], "test smove 2 myset1 remaining" 84 | assert.eql bb.smembers("myset2"), ["a"], "test smove 2 myset2 remaining" 85 | # reset 86 | bb.del "myset1" 87 | bb.del "myset2" 88 | # src exists, dst doesn't exist - move non existent member 89 | bb.sadd "myset1", "a" 90 | bb.sadd "myset1", "b" 91 | assert.equal bb.smove("myset1", "myset2", "c"), 0, "test smove 3" 92 | assert.includes bb.smembers("myset1"), "a", "test smove 3 myset1 remaining a" 93 | assert.includes bb.smembers("myset1"), "b", "test smove 3 myset1 remaining b" 94 | assert.equal bb.exists("myset2"), false, "test smove 3 myset2 exists false" 95 | # reset 96 | bb.del "myset1" 97 | # src and dst exist 98 | bb.sadd "myset1", "a" 99 | bb.sadd "myset1", "b" 100 | bb.sadd "myset2", "c" 101 | bb.sadd "myset2", "d" 102 | assert.equal bb.smove("myset1", "myset2", "a"), 1, "test smove 4" 103 | assert.equal bb.scard("myset1"), 1, "test smove 4 myset1 remaining length" 104 | assert.includes bb.smembers("myset1"), "b", "test smove 4 myset1 remianing b" 105 | assert.equal bb.scard("myset2"), 3, "test smove 4 myset2 remaining length" 106 | assert.includes bb.smembers("myset2"), "a", "test smove 4 myset2 remaining a" 107 | assert.includes bb.smembers("myset2"), "c", "test smove 4 myset2 remaining c" 108 | assert.includes bb.smembers("myset2"), "d", "test smove 4 myset2 remaining d" 109 | # reset 110 | bb.del "myset1" 111 | bb.del "myset2" 112 | # src and dst exist - move non existent member 113 | bb.sadd "myset1", "a" 114 | bb.sadd "myset1", "b" 115 | bb.sadd "myset2", "c" 116 | bb.sadd "myset2", "d" 117 | assert.equal bb.smove("myset1", "myset2", "e"), 0, "test smove 4" 118 | assert.equal bb.scard("myset1"), 2, "test smove 4 myset1 remaining length" 119 | assert.includes bb.smembers("myset1"), "a", "test smove 4 myset1 remianing a" 120 | assert.includes bb.smembers("myset1"), "b", "test smove 4 myset1 remianing b" 121 | assert.equal bb.scard("myset2"), 2, "test smove 4 myset2 remaining length" 122 | assert.includes bb.smembers("myset2"), "c", "test smove 4 myset2 remaining c" 123 | assert.includes bb.smembers("myset2"), "d", "test smove 4 myset2 remaining d" 124 | 125 | exports.testSpopNonKey = -> 126 | bb = new BankersBox(1) 127 | bb.flushdb() 128 | assert.equal bb.spop("myset"), null, "test spop non existent key" 129 | 130 | exports.testSpop = -> 131 | bb = new BankersBox(1) 132 | bb.flushdb() 133 | bb.sadd "myset", "a" 134 | bb.sadd "myset", "b" 135 | bb.sadd "myset", "c" 136 | ret = bb.spop "myset" 137 | assert.includes ["a", "b", "c"], ret, "test spop ret was in myset" 138 | assert.equal bb.sismember("myset", ret), false, "test spop ret is no longer in myset" 139 | assert.equal bb.scard("myset"), 2, "test spop myset remaining length" 140 | 141 | exports.testSrandmemberNonKey = -> 142 | bb = new BankersBox(1) 143 | bb.flushdb() 144 | assert.equal bb.srandmember("myset"), null, "test srandmember non existent key" 145 | 146 | exports.testSrandmember = -> 147 | bb = new BankersBox(1) 148 | bb.flushdb() 149 | bb.sadd "myset", "a" 150 | bb.sadd "myset", "b" 151 | bb.sadd "myset", "c" 152 | ret = bb.srandmember "myset" 153 | assert.includes ["a", "b", "c"], ret, "test spop ret was in myset" 154 | assert.equal bb.sismember("myset", ret), true, "test srandmember ret is still in myset" 155 | assert.equal bb.scard("myset"), 3, "test srandmember myset remaining length" 156 | 157 | exports.testSremNonKey = -> 158 | bb = new BankersBox(1) 159 | bb.flushdb() 160 | assert.equal bb.srem("myset", "a"), 0, "test srem non existent key" 161 | 162 | exports.testSrem = -> 163 | bb = new BankersBox(1) 164 | bb.flushdb() 165 | bb.sadd "myset", "a" 166 | bb.sadd "myset", "b" 167 | bb.sadd "myset", "c" 168 | # remove non existent member 169 | assert.equal bb.srem("myset", "d"), 0, "test srem non existent member" 170 | assert.equal bb.scard("myset"), 3, "test srem remaining length" 171 | # remove existing member 172 | assert.equal bb.srem("myset", "b"), 1, "test srem existing member" 173 | assert.equal bb.scard("myset"), 2, "test srem remaining length" 174 | assert.equal bb.sismember("myset", "b"), false, "test srem member is not in set" 175 | 176 | 177 | exports.testSremEmptySetRemovesKey = -> 178 | bb = new BankersBox(1) 179 | bb.flushdb() 180 | bb.sadd "foo", "a" 181 | bb.sadd "foo", "b" 182 | bb.srem "foo", "a" 183 | bb.srem "foo", "b" 184 | assert.equal bb.exists("foo"), false, "test srem empty set deletes key" 185 | 186 | exports.testSpopEmptySetRemovesKey = -> 187 | bb = new BankersBox(1) 188 | bb.flushdb() 189 | bb.sadd "foo", "a" 190 | bb.sadd "foo", "b" 191 | bb.srem "foo", "a" 192 | bb.spop "foo" 193 | assert.equal bb.exists("foo"), false, "test spop empty set deletes key" 194 | 195 | exports.testSmoveEmptySetRemovesKey = -> 196 | bb = new BankersBox(1) 197 | bb.flushdb() 198 | bb.sadd "foo", "a" 199 | bb.sadd "foo", "b" 200 | bb.srem "foo", "a" 201 | bb.smove "foo", "bar", "b" 202 | assert.equal bb.exists("foo"), false, "test smove empty set deletes key" 203 | 204 | ### 205 | * [SETS](http://redis.io/commands#set 206 | x* sadd 207 | x* scard 208 | x* sismember 209 | x* smembers 210 | x* smove 211 | x* spop 212 | x* srandmember 213 | x* srem 214 | ### -------------------------------------------------------------------------------- /tests/bb_string.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require "assert" 2 | { BankersBox } = require "bankersbox" 3 | 4 | ### STRING TESTS ### 5 | 6 | exports.testSet = -> 7 | bb = new BankersBox(1) 8 | bb.flushdb() 9 | assert.equal bb.set("foo", "bar"), "OK", "test set" 10 | 11 | exports.testSetAndGet = -> 12 | bb = new BankersBox(1) 13 | bb.flushdb() 14 | bb.set "foo", "bar" 15 | assert.equal bb.get("foo"), "bar", "test get" 16 | 17 | exports.testGetNull = -> 18 | bb = new BankersBox(1) 19 | bb.flushdb() 20 | assert.equal bb.get("asdf"), null, "test get non-existing key" 21 | 22 | exports.testTypeString = -> 23 | bb = new BankersBox(1) 24 | bb.flushdb() 25 | bb.set "baz", "qwer" 26 | assert.equal bb.type("baz"), "string", "test string type" 27 | 28 | exports.testSetnxSuccess = -> 29 | bb = new BankersBox(1) 30 | bb.flushdb() 31 | assert.equal bb.setnx("foo", "bar"), 1, "test setnx success" 32 | 33 | exports.testSetnxFail = -> 34 | bb = new BankersBox(1) 35 | bb.flushdb() 36 | bb.set "foo", "bar" 37 | assert.equal bb.setnx("foo", "qwer"), 0, "test setnx fail" 38 | 39 | exports.testGetSet = -> 40 | bb = new BankersBox(1) 41 | bb.flushdb() 42 | bb.set "foo", "hello" 43 | assert.equal bb.getset("foo", "goodbye"), "hello", "test getset return" 44 | assert.equal bb.get("foo"), "goodbye", "test getset new value" 45 | 46 | exports.testStrlenNonKey = -> 47 | bb = new BankersBox(1) 48 | bb.flushdb() 49 | assert.equal bb.strlen("foo"), 0, "test strlen non existent key" 50 | 51 | exports.testStrlen = -> 52 | bb = new BankersBox(1) 53 | bb.flushdb() 54 | bb.set "foo", "123456789" 55 | assert.equal bb.strlen("foo"), 9, "test strlen" 56 | 57 | exports.testIncrNewKey = -> 58 | bb = new BankersBox(1) 59 | bb.flushdb() 60 | assert.equal bb.incr("counter"), 1, "test incr new key" 61 | 62 | exports.testIncrExistingKey = -> 63 | bb = new BankersBox(1) 64 | bb.flushdb() 65 | bb.set "counter2", 10 66 | assert.equal bb.incr("counter2"), 11, "test incr existing key" 67 | 68 | exports.testIncrbyNewKey = -> 69 | bb = new BankersBox(1) 70 | bb.flushdb() 71 | assert.equal bb.incrby("counter3", 5), 5, "test incrby new key" 72 | 73 | exports.testIncrbyExistingKey = -> 74 | bb = new BankersBox(1) 75 | bb.flushdb() 76 | bb.set "counter4", 10 77 | assert.equal bb.incrby("counter4", 7), 17, "test incrby existing key" 78 | 79 | exports.testDecrNewKey = -> 80 | bb = new BankersBox(1) 81 | bb.flushdb() 82 | assert.equal bb.decr("counter5"), -1, "test decr new key" 83 | 84 | exports.testDecrExistingKey = -> 85 | bb = new BankersBox(1) 86 | bb.flushdb() 87 | bb.set "counter6", -6 88 | assert.equal bb.decr("counter6"), -7, "test decr existing key" 89 | 90 | exports.testDecrbyNewKey = -> 91 | bb = new BankersBox(1) 92 | bb.flushdb() 93 | assert.equal bb.decrby("counter7", 8), -8, "test decrby new key" 94 | 95 | exports.testDecrbyExistingKey = -> 96 | bb = new BankersBox(1) 97 | bb.flushdb() 98 | bb.set "counter8", 10 99 | assert.equal bb.decrby("counter8", 7), 3, "test decrby existing key" 100 | 101 | exports.testAppendNewKey = -> 102 | bb = new BankersBox(1) 103 | bb.flushdb() 104 | assert.equal bb.append("zxcv", "Hello"), 5, "test append new key return" 105 | assert.equal bb.get("zxcv"), "Hello", "test append new key" 106 | 107 | exports.testAppendExistingKey = -> 108 | bb = new BankersBox(1) 109 | bb.flushdb() 110 | bb.set "zxcv", "Hello" 111 | assert.equal bb.append("zxcv", " World"), 11, "test append new key return" 112 | assert.equal bb.get("zxcv"), "Hello World", "test append new key" 113 | 114 | exports.testStringRepresentations = -> 115 | bb = new BankersBox(1) 116 | bb.flushdb() 117 | bb.set "a", "a string" 118 | bb.set "b", 3 119 | bb.set "b2", 3.14159 120 | bb.set "c", true 121 | bb.set "d", [1, 2, 3] 122 | bb.set "e", {foo: "hello", bar: "goodbye"} 123 | bb.set "f", JSON.stringify({foo: "hello", bar: "goodbye"}) 124 | bb.set "g", JSON.stringify({foo: "hello", bar: "goodbye", v: "uh oh"}) 125 | cc = new BankersBox(1) 126 | assert.equal cc.get("a"), "a string" 127 | assert.equal cc.get("b"), 3 128 | assert.equal cc.get("b2"), 3.14159 129 | assert.equal cc.get("c"), true 130 | assert.eql cc.get("d"), [1, 2, 3] 131 | assert.eql cc.get("e"), {foo: "hello", bar: "goodbye"} 132 | assert.equal cc.get("f"), JSON.stringify({foo: "hello", bar: "goodbye"}) 133 | assert.equal cc.get("g"), JSON.stringify({foo: "hello", bar: "goodbye", v: "uh oh"}) --------------------------------------------------------------------------------