├── .github └── workflows │ ├── dart.yml │ └── publish.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── cas.dart ├── command.dart ├── connection.dart ├── exceptions.dart ├── lazystream.dart ├── pubsub.dart ├── redis.dart ├── redisparser.dart ├── redisserialise.dart └── transaction.dart ├── pubspec.yaml └── test ├── basic_test.dart ├── binary_parser_test.dart ├── cas_test.dart ├── close_test.dart ├── conversion_test.dart ├── docker ├── Readme.txt ├── docker-compose.yaml └── run_podman.sh ├── error_test.dart ├── lua_test.dart ├── main.dart ├── performance.dart ├── pubsub_test.dart ├── transactions_test.dart └── unicode_test.dart /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Dart 7 | 8 | on: 9 | push: 10 | branches: [ master ] 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | sdk: [stable, dev, 2.12.0] 20 | 21 | env: 22 | REDIS_URL: localhost 23 | REDIS_PORT: 6379 24 | 25 | services: 26 | redis: 27 | image: redis 28 | ports: ['6379:6379'] 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | # Note: This workflow uses the latest stable version of the Dart SDK. 34 | # You can specify other versions if desired, see documentation here: 35 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 36 | # - uses: dart-lang/setup-dart@v1 37 | - uses: dart-lang/setup-dart@v1 38 | with: 39 | sdk: ${{ matrix.sdk }} 40 | 41 | - name: Install dependencies 42 | run: dart pub get 43 | 44 | # Uncomment this step to verify the use of 'dart format' on each commit. 45 | # - name: Verify formatting 46 | # run: dart format --output=none --set-exit-if-changed . 47 | 48 | # Consider passing '--fatal-infos' for slightly stricter analysis. 49 | - name: Analyze project source 50 | run: dart analyze --fatal-infos 51 | 52 | # Your project will need to have tests in test/ and a dependency on 53 | # package:test for this step to succeed. Note that Flutter projects will 54 | # want to change this to 'flutter test'. 55 | - name: Run tests 56 | run: dart test 57 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | Publish: 10 | permissions: 11 | id-token: write 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: dart-lang/setup-dart@v1 17 | 18 | - run: dart pub get 19 | - run: dart test 20 | - run: dart analyze 21 | - run: dart pub publish --force 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # dotenv environment variables file 15 | .env* 16 | 17 | # Avoid committing generated Javascript files: 18 | *.dart.js 19 | *.info.json # Produced by the --dump-info flag. 20 | *.js # When generated by dart2js. Don't specify *.js if your 21 | # project includes source files written in JavaScript. 22 | *.js_ 23 | *.js.deps 24 | *.js.map 25 | 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | [README.md](README.md) 3 | 4 | ### 4.0.0 5 | - Change licencse to Mit License [Issue #82](https://github.com/ra1u/redis-dart/issues/82) 6 | 7 | ### 3.1.0 8 | - PubSub can be closed and errors can be caught as per PubSub example in [README.md](README.md) 9 | 10 | ### 3.0.0 11 | - Feature: Binary data parsing, [Issue #56](https://github.com/ra1u/redis-dart/issues/56) 12 | - we upgraded to new major version due to minor backward incompatibility. 13 | Incompatibility is in publicly exposed parser class. Most users should not notice incompatibility. 14 | 15 | ### 2.2.0 16 | - Bugfix: Handle socket exception on close [Issue #49](https://github.com/ra1u/redis-dart/issues/49) 17 | 18 | ### 2.1.0 19 | - Bugfix: Try to recover after received RedisError [Issue #48](https://github.com/ra1u/redis-dart/issues/48) 20 | 21 | ### 2.0.0 22 | - Migration on nullsafety and dart >= 2.12. Thanks to [@ArnaudHeywear](https://github.com/ArnaudHeywear) 23 | 24 | ### 1.4.0 25 | - Tls and custom Socket support. Thanks to [@Derrick56007](https://github.com/Derrick56007) 26 | 27 | ### 1.3.0 28 | - Improved error handling [Issue #15](https://github.com/ra1u/redis-dart/issues/15) 29 | - Experimental transaction discard [Issue #11](https://github.com/ra1u/redis-dart/issues/11) 30 | - Minor fixes 31 | 32 | ### 1.2.0 33 | - Received redis errors throws exception. Thanks to [@eknoes](https://github.com/eknoes) for pull request. 34 | - Integers in array get auto converted to strings. Author [@eknoes](https://github.com/eknoes). 35 | - Improve transaction handling errors. Patch from [@eknoes](https://github.com/eknoes) 36 | - Testing migrated on dart.test. Patch from [@eknoes](https://github.com/eknoes) 37 | 38 | ### 1.1.0 39 | - Performance tweaks and simplified code 40 | 41 | ### 1.0.0 42 | - Dart 2.0 support 43 | 44 | ### 0.4.5 45 | - Unicode bugfix -> https://github.com/ra1u/redis-dart/issues/4 46 | - Update PubSub doc 47 | - Improve tests 48 | 49 | ### 0.4.4 50 | - bugfix for subscribe -> https://github.com/ra1u/redis-dart/issues/3 51 | - performance improvement 52 | - add PubSub class (simpler/shorter/faster? PubSubCommand) 53 | - doc update and example of EVAL 54 | 55 | ### 0.4.3 56 | - Cas helper 57 | - Improved unit tests 58 | 59 | ### 0.4.2 60 | - Improved performance by 10% 61 | - Pubsub interface uses Stream 62 | - Better test coverage 63 | - Improved documentation 64 | 65 | ### 0.4.1 66 | - Command raise error if used during transaction. 67 | 68 | ### 0.4.0 69 | - PubSub interface is made simpler but backward incompatible :( 70 | - README is updated 71 | 72 | [README.md](README.md) 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luka Rahne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redis client for Dart 2 | ===================== 3 | 4 | [![test master](https://github.com/ra1u/redis-dart/actions/workflows/dart.yml/badge.svg)](https://github.com/ra1u/redis-dart/actions/workflows/dart.yml?query=event%3Apush+branch%3Amaster) 5 | 6 | [Redis](http://redis.io/) protocol parser and client for [Dart](https://www.dartlang.org) 7 | 8 | Fast and simple by design. It requires no external package to run. 9 | 10 | ### Supported features: 11 | 12 | * [transactions](#transactions) and [CAS](#cas) (check-and-set) pattern 13 | * [pubsub](#pubsub) 14 | * [unicode](#unicode) 15 | * [performance](#fast) and [simplicity](#Simple) 16 | * [tls](#Tls) 17 | 18 | ## Simple 19 | 20 | **redis** is simple serializer and deserializer of the [redis protocol](http://redis.io/topics/protocol) with additional helper functions and classes. 21 | 22 | Redis protocol is a composition of array, strings (and bulk) and integers. 23 | 24 | For example a [SET](http://redis.io/commands/set) command might look like this: 25 | 26 | ```dart 27 | Future f = command.send_object(["SET","key","value"]); 28 | ``` 29 | 30 | This enables sending any command. Before sending commands one needs to open a 31 | connection to Redis. 32 | 33 | In the following example we will open a connection to a Redis server running on 34 | port 6379, execute the command 'SET key 0' and print the result. 35 | 36 | ```dart 37 | import 'package:redis/redis.dart'; 38 | ... 39 | final conn = RedisConnection(); 40 | conn.connect('localhost', 6379).then((Command command){ 41 | command.send_object(["SET","key","0"]).then((var response) 42 | print(response); 43 | ) 44 | } 45 | ``` 46 | 47 | Due to the simple implementation, it is possible to execute commands in various 48 | ways. In the following example we execute one after the other. 49 | 50 | ```dart 51 | final conn = RedisConnection(); 52 | conn.connect('localhost', 6379).then((Command command){ 53 | command.send_object(["SET","key","0"]) 54 | .then((var response){ 55 | assert(response == 'OK'); 56 | return command.send_object(["INCR","key"]); 57 | }) 58 | .then((var response){ 59 | assert(response == 1); 60 | return command.send_object(["INCR","key"]); 61 | }) 62 | .then((var response){ 63 | assert(response == 2); 64 | return command.send_object(["INCR","key"]); 65 | }) 66 | .then((var response){ 67 | assert(response == 3); 68 | return command.send_object(["GET","key"]); 69 | }) 70 | .then((var response){ 71 | return print(response); // 3 72 | }); 73 | }); 74 | ``` 75 | 76 | Another way is to execute commands without waiting for the previous command to 77 | complete, and we can still be sure that the response handled by `Future` will be 78 | completed in the correct order. 79 | 80 | ```dart 81 | final conn = RedisConnection(); 82 | conn.connect('localhost',6379).then((Command command){ 83 | command.send_object(["SET","key","0"]) 84 | .then((var response){ 85 | assert(response == 'OK'); 86 | }); 87 | command.send_object(["INCR","key"]) 88 | .then((var response){ 89 | assert(response == 1); 90 | }); 91 | command.send_object(["INCR","key"]) 92 | .then((var response){ 93 | assert(response == 2); 94 | }); 95 | command.send_object(["INCR","key"]) 96 | .then((var response){ 97 | assert(response == 3); 98 | }); 99 | command.send_object(["GET","key"]) 100 | .then((var response){ 101 | print(response); // 3 102 | }); 103 | }); 104 | ``` 105 | 106 | Difference is that there are five commands in last examples 107 | and only one in the previous example. 108 | 109 | ### Generic 110 | 111 | Redis responses and requests can be arbitrarily nested. 112 | 113 | Mapping 114 | 115 | | Redis | Dart | 116 | | ------------- |:-------------:| 117 | | String | String | 118 | | Integer | Integer | 119 | | Array | List | 120 | | Error | RedisError | 121 | | Bulk | String or Binary | 122 | 123 | \* Both simple string and bulk string from Redis are serialized to Dart string. 124 | Strings from Dart to Redis are converted to bulk string. UTF8 encoding is used 125 | in both directions. 126 | 127 | New feature since 3.0: Support for converting received data as [binary data](#Binary data). 128 | 129 | Lists can be nested. This is useful when executing the [EVAL](http://redis.io/commands/EVAL) command. 130 | 131 | ```dart 132 | command.send_object(["EVAL","return {KEYS[1],{KEYS[2],{ARGV[1]},ARGV[2]},2}","2","key1","key2","first","second"]) 133 | .then((response){ 134 | print(response); 135 | }); 136 | ``` 137 | 138 | results in 139 | 140 | ```dart 141 | [key1, [key2, [first], second], 2] 142 | ``` 143 | 144 | ## Tls 145 | 146 | Secure ssl/tls with `RedisConnection.connectSecure(host,port)` 147 | 148 | ```dart 149 | final conn = RedisConnection(); 150 | conn.connectSecure('localhost', 6379).then((Command command) { 151 | command 152 | .send_object(["AUTH", "username", "password"]).then((var response) { 153 | print(response); 154 | command.send_object(["SET", "key", "0"]).then( 155 | (var response) => print(response)); 156 | }); 157 | }); 158 | ``` 159 | 160 | or by passing any other [`Socket`](https://api.dart.dev/stable/dart-io/Socket-class.html) to 161 | `RedisConnection.connectWithSocket(Socket s)` in similar fashion. 162 | 163 | ## Fast 164 | 165 | Tested on a laptop, we can execute and process 180K INCR operations per second. 166 | 167 | Example 168 | 169 | ```dart 170 | const int N = 200000; 171 | int start; 172 | final conn = RedisConnection(); 173 | conn.connect('localhost',6379).then((Command command){ 174 | print("test started, please wait ..."); 175 | start = DateTime.now().millisecondsSinceEpoch; 176 | command.pipe_start(); 177 | command.send_object(["SET","test","0"]); 178 | for(int i=1;i<=N;i++){ 179 | command.send_object(["INCR","test"]) 180 | .then((v){ 181 | if(i != v) 182 | throw("wrong received value, we got $v"); 183 | }); 184 | } 185 | //last command will be executed and then processed last 186 | command.send_object(["GET","test"]).then((v){ 187 | print(v); 188 | double diff = (new DateTime.now().millisecondsSinceEpoch - start)/1000.0; 189 | double perf = N/diff; 190 | print("$N operations done in $diff s\nperformance $perf/s"); 191 | }); 192 | command.pipe_end(); 193 | }); 194 | ``` 195 | 196 | We are not just sending 200K commands here, but also checking result of every send command. 197 | 198 | Using `command.pipe_start();` and `command.pipe_end();` does nothing more 199 | than enabling and disabling the [Nagle's algorhitm](https://en.wikipedia.org/wiki/Nagle%27s_algorithm) on socket. By default it is disabled to achieve shortest 200 | possible latency at the expense of more TCP packets and extra overhead. Enabling 201 | Nagle's algorithm during transactions can achieve greater data throughput and 202 | less overhead. 203 | 204 | ## [Transactions](http://redis.io/topics/transactions) 205 | 206 | Transactions by redis protocol are started by MULTI command and completed with 207 | EXEC command. `.multi()`, `.exec()` and `class Transaction` are implemented as 208 | helpers for checking the result of each command executed during transaction. 209 | 210 | ```dart 211 | Future Command.multi(); 212 | ``` 213 | 214 | Executing `multi()` returns a `Future`. This class should be used 215 | to execute commands by calling `.send_object`. It returns a `Future` that is 216 | called after calling `.exec()`. 217 | 218 | ```dart 219 | import 'package:redis/redis.dart'; 220 | ... 221 | 222 | final conn = RedisConnection(); 223 | conn.connect('localhost',6379).then((Command command){ 224 | command.multi().then((Transaction trans){ 225 | trans.send_object(["SET","val","0"]); 226 | for(int i=0;i<200000;++i){ 227 | trans.send_object(["INCR","val"]).then((v){ 228 | assert(i==v); 229 | }); 230 | } 231 | trans.send_object(["GET","val"]).then((v){ 232 | print("number is now $v"); 233 | }); 234 | trans.exec(); 235 | }); 236 | }); 237 | ``` 238 | 239 | ### [CAS](http://redis.io/topics/transactions#cas) 240 | 241 | It's impossible to write code that depends on the result of the previous command 242 | during a transaction, because all commands are executed at once. To overcome 243 | this, user should use the [CAS](http://redis.io/topics/transactions#cas). 244 | 245 | `Cas` requires a `Command` as a constructor argument. It implements two methods: 246 | `watch` and `multiAndExec`. 247 | 248 | `watch` takes two arguments: a list of keys to watch and a handler to call and 249 | to proceed with CAS. 250 | 251 | Example: 252 | 253 | ```dart 254 | cas.watch(["key1,key2,key3"],(){ 255 | //body of CAS 256 | }); 257 | ``` 258 | 259 | Failure happens if the watched key is modified outside of the transaction. When 260 | this happens the handler is called until final transaction completes. 261 | 262 | `multiAndExec` is used to complete a transaction with a handler where 263 | the argument is `Transaction`. 264 | 265 | Example: 266 | 267 | ```dart 268 | //last part in body of CAS 269 | cas.multiAndExec((Transaction trans){ 270 | trans.send_object(["SET","key1",v1]); 271 | trans.send_object(["SET","key2",v2]); 272 | trans.send_object(["SET","key2",v2]); 273 | }); 274 | ``` 275 | 276 | Lets imagine we need to atomically increment the value of a key by 1 (and that 277 | Redis does not have the [INCR](http://redis.io/commands/incr) command). 278 | 279 | ```dart 280 | Cas cas = new Cas(command); 281 | cas.watch(["key"], (){ 282 | command.send_object(["GET","key"]).then((String val){ 283 | int i = int.parse(val); 284 | i++; 285 | cas.multiAndExec((Transaction trans){ 286 | trans.send_object(["SET","key",i.toString()]); 287 | }); 288 | }); 289 | }); 290 | ``` 291 | 292 | ## Unicode 293 | 294 | By default UTF8 encoding/decoding for string is used. Each string is converted 295 | in binary array using UTF8 encoding. This makes ascii string compatible in both 296 | direction. 297 | 298 | ## Binary data 299 | 300 | Default conversion response from Redis of Bulk data is converted to utf-8 string. 301 | In case when binary interpretation is needed, there is option to request such parsing. 302 | 303 | ```dart 304 | final conn = RedisConnection(); 305 | Command cmd = await conn.connect('localhost',6379); 306 | Command cmd_bin = Command.from(cmd).setParser(RedisParserBulkBinary()); 307 | List d = [1,2,3,4,5,6,7,8,9]; 308 | // send binary 309 | await cmd_bin.send_object(["SET", key, RedisBulk(d)]); 310 | // receive binary from binary command handler 311 | var r = await cmd_bin.send_object(["GET", key]) 312 | // r is now same as d 313 | ``` 314 | 315 | 316 | 317 | ## [PubSub](http://redis.io/topics/pubsub) 318 | 319 | PubSub is a helper for dispatching received messages. First, create a new 320 | `PubSub` from an existing `Command` 321 | 322 | ```dart 323 | final pubsub = PubSub(command); 324 | ``` 325 | 326 | Once `PubSub` is created, `Command` is invalidated and should not be used 327 | on the same connection. `PubSub` have the following commands 328 | 329 | ```dart 330 | void subscribe(List channels) 331 | void psubscribe(List channels) 332 | void unsubscribe(List channels) 333 | void punsubscribe(List channels) 334 | ``` 335 | 336 | and additional `Stream getStream()` 337 | 338 | `getStream` returns [`Stream`](https://api.dartlang.org/stable/dart-async/Stream-class.html) 339 | 340 | Example for receiving and printing messages 341 | 342 | ```dart 343 | import 'dart:async'; 344 | import 'package:redis/redis.dart'; 345 | 346 | Future rx() async { 347 | Command cmd = await RedisConnection().connect('localhost', 6379); 348 | final pubsub = PubSub(cmd); 349 | pubsub.subscribe(["monkey"]); 350 | final stream = pubsub.getStream(); 351 | var streamWithoutErrors = stream.handleError((e) => print("error $e")); 352 | await for (final msg in streamWithoutErrors) { 353 | var kind = msg[0]; 354 | var food = msg[2]; 355 | if (kind == "message") { 356 | print("monkey got ${food}"); 357 | if (food == "cucumber") { 358 | print("monkey does not like cucumber"); 359 | cmd.get_connection().close(); 360 | } 361 | } 362 | else { 363 | print("received non-message ${msg}"); 364 | } 365 | } 366 | } 367 | 368 | Future tx() async { 369 | Command cmd = await RedisConnection().connect('localhost', 6379); 370 | await cmd.send_object(["PUBLISH", "monkey", "banana"]); 371 | await cmd.send_object(["PUBLISH", "monkey", "apple"]); 372 | await cmd.send_object(["PUBLISH", "monkey", "peanut"]); 373 | await cmd.send_object(["PUBLISH", "monkey", "cucumber"]); 374 | cmd.get_connection().close(); 375 | } 376 | 377 | void main() async { 378 | var frx = rx(); 379 | var ftx = tx(); 380 | await ftx; 381 | await frx; 382 | } 383 | ``` 384 | 385 | Sending messages can be done from different connection for example 386 | 387 | ```dart 388 | command.send_object(["PUBLISH","monkey","banana"]); 389 | ``` 390 | 391 | ## Todo 392 | In the near future: 393 | 394 | - Better documentation 395 | - Implement all "generic commands" with named commands 396 | - Better error handling - that is ability to recover from error 397 | - Spell check code 398 | 399 | ## Changes 400 | 401 | [CHANGELOG.md](CHANGELOG.md) 402 | -------------------------------------------------------------------------------- /lib/cas.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | class Cas { 13 | Command _cmd; 14 | late Completer _completer_bool; 15 | 16 | /// Class for CAS check-and-set pattern 17 | /// Construct with Command and call 18 | /// watch() and multiAndExec() 19 | Cas(this._cmd) {} 20 | 21 | /// watch takes list of watched keys and 22 | /// function that is executed 23 | /// during CAS opertion 24 | /// 25 | Future watch(List watching_keys, func()) { 26 | //return _cmd.send_object(["TRANS"]); 27 | List watchcmd = ["WATCH"]; 28 | watchcmd.addAll(watching_keys); 29 | return Future.doWhile(() { 30 | _completer_bool = Completer(); 31 | _cmd.send_object(watchcmd).then((_) { 32 | func(); 33 | }); 34 | return _completer_bool.future; 35 | }); 36 | } 37 | 38 | /// multiAndExec takes function 39 | /// to complete CAS as Transaction 40 | /// passed function takes Transaction 41 | /// as only parameter and should be used to 42 | /// complete transaction 43 | /// 44 | /// !!! DO NOT call exec() on Transaction 45 | 46 | Future multiAndExec(Future func(Transation)) { 47 | return _cmd.multi().then((Transaction _trans) { 48 | func(_trans); 49 | return _trans.exec().then((var resp) { 50 | if (resp == "OK") { 51 | _completer_bool.complete(false); //terminate Future.doWhile 52 | } else { 53 | // exec completes only with valid response 54 | _completer_bool.completeError( 55 | RedisError("exec response is not expected, but is $resp")); 56 | } 57 | }).catchError((e) { 58 | // dont do anything 59 | _completer_bool.complete(true); // retry 60 | }, test: (e) => e is TransactionError); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/command.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | class Command { 13 | /*RedisConnection*/ var _connection; 14 | // parser is somthing that transfer data from redis database to object 15 | Parser parser = Parser(); 16 | // serializer is somehing that transform object to redis 17 | Serializer serializer = Serializer(); 18 | 19 | Command(this._connection) {} 20 | Command.from(Command other) { 21 | this._connection = other._connection; 22 | this.parser = other.parser; 23 | this.serializer = other.serializer; 24 | } 25 | 26 | Command setParser(Parser p) { 27 | this.parser = p; 28 | return this; 29 | } 30 | 31 | Command setSerializer(Serializer s) { 32 | this.serializer = s; 33 | return this; 34 | } 35 | 36 | /// Serialize and send data to server 37 | /// 38 | /// Data can be any object recognised by Redis 39 | /// List, integer, Bulk, null and composite of those 40 | /// Redis command is List 41 | /// 42 | /// example SET: 43 | /// send_object(["SET","key","value"]); 44 | Future send_object(Object obj) { 45 | try { 46 | return _connection._sendraw(parser, serializer.serialize(obj)).then((v) { 47 | // turn RedisError into exception 48 | if (v is RedisError) { 49 | return Future.error(v); 50 | } else { 51 | return v; 52 | } 53 | }); 54 | } catch (e) { 55 | return Future.error(e); 56 | } 57 | } 58 | 59 | /// return future that completes when 60 | /// all prevous packets are processed 61 | Future? send_nothing() => _connection._getdummy(); 62 | 63 | /// Set socket settings for sending transations 64 | /// 65 | /// This is optimisation and not requrement. 66 | void pipe_start() => 67 | _connection.disable_nagle(false); //we want to use sockets buffering 68 | /// Requred to be called after last piping command 69 | void pipe_end() => _connection.disable_nagle(true); 70 | 71 | /// Set String value given a key 72 | Future set(String key, String value) => send_object(["SET", key, value]); 73 | 74 | /// Get value given a key 75 | Future get(String key) => send_object(["GET", key]); 76 | 77 | /// Transations are started with multi and completed with exec() 78 | Future multi() { 79 | //multi retun transation as future 80 | return send_object(["MULTI"]).then((_) => Transaction(this)); 81 | } 82 | 83 | RedisConnection get_connection() { 84 | return _connection; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/connection.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | /// Class for server connection on server 13 | class RedisConnection { 14 | Socket? _socket; 15 | LazyStream? _stream; 16 | Future _future = Future.value(); 17 | RedisParser parser = RedisParser(); 18 | 19 | /// connect on Redis server as client 20 | Future connect(host, port) { 21 | return Socket.connect(host, port).then((Socket sock) { 22 | _socket = sock; 23 | disable_nagle(true); 24 | _stream = LazyStream.fromstream(_socket!); 25 | return Command(this); 26 | }); 27 | } 28 | 29 | /// connect on Redis server as client 30 | Future connectSecure(host, port) { 31 | return SecureSocket.connect(host, port).then((SecureSocket sock) { 32 | _socket = sock; 33 | disable_nagle(true); 34 | _stream = LazyStream.fromstream(_socket!); 35 | return Command(this); 36 | }); 37 | } 38 | 39 | // connect with custom socket 40 | Future connectWithSocket(Socket s) async { 41 | _socket = s; 42 | disable_nagle(true); 43 | _stream = LazyStream.fromstream(_socket!); 44 | return Command(this); 45 | } 46 | 47 | /// close connection to Redis server 48 | Future close() { 49 | _stream!.close(); 50 | return _socket!.close(); 51 | } 52 | 53 | //this doesnt send anything 54 | //it just wait something to come from socket 55 | //it parse it and execute future 56 | Future _senddummy(Parser parser) { 57 | _future = _future.then((_) { 58 | return parser.parse(_stream!); 59 | }); 60 | return _future; 61 | } 62 | 63 | // return future that complets 64 | // when all prevous _future finished 65 | // ignore: unused_element 66 | Future _getdummy() { 67 | _future = _future.then((_) { 68 | return "dummy data"; 69 | }); 70 | return _future; 71 | } 72 | 73 | // ignore: unused_element 74 | Future _sendraw(Parser parser, List data) { 75 | _socket!.add(data); 76 | return _senddummy(parser); 77 | } 78 | 79 | void disable_nagle(bool v) { 80 | _socket!.setOption(SocketOption.tcpNoDelay, v); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/exceptions.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | // this class is returned when redis response is type error 13 | class RedisError { 14 | String e; 15 | RedisError(this.e); 16 | String toString() { 17 | return "RedisError($e)"; 18 | } 19 | 20 | String get error => e; 21 | } 22 | 23 | // thiss class is returned when parsing in client side (aka this libraray) 24 | // get error 25 | class RedisRuntimeError { 26 | String e; 27 | RedisRuntimeError(this.e); 28 | String toString() { 29 | return "RedisRuntimeError($e)"; 30 | } 31 | 32 | String get error => e; 33 | } 34 | 35 | class TransactionError { 36 | String e; 37 | TransactionError(this.e); 38 | String toString() { 39 | return "TranscationError($e)"; 40 | } 41 | 42 | String get error => e; 43 | } 44 | -------------------------------------------------------------------------------- /lib/lazystream.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | //our parser was designed for lazy stream that is consumable 11 | //unfortnatly redis socket streams doest work that way 12 | //this class implements minimum requrement for redisparser 13 | 14 | //currently parser requrement is take_n and take_while methods 15 | 16 | part of redis; 17 | 18 | // like Stream but has method next for simple reading 19 | class StreamNext { 20 | late Queue>> _queue; 21 | late int _nfut; 22 | late int _npack; 23 | late bool done; 24 | late Socket socket; 25 | StreamNext.fromstream(Socket _socket) { 26 | _queue = Queue>>(); 27 | _nfut = 0; 28 | _npack = 0; 29 | done = false; 30 | socket = _socket; 31 | socket.listen(onData, onError: this.onError, onDone: this.onDone); 32 | } 33 | 34 | void onData(List event) { 35 | if (_nfut >= 1) { 36 | Completer c = _queue.removeFirst(); 37 | c.complete(event); 38 | _nfut -= 1; 39 | } else { 40 | Completer> c = Completer>(); 41 | c.complete(event); 42 | _queue.addLast(c); 43 | _npack += 1; 44 | } 45 | } 46 | 47 | void onError(error) { 48 | done = true; 49 | // close socket on error 50 | // follow bug https://github.com/ra1u/redis-dart/issues/49 51 | // and bug https://github.com/dart-lang/sdk/issues/47538 52 | socket.close().then((_) { 53 | if (_nfut >= 1) { 54 | _nfut = 0; 55 | for (Completer> e in _queue) { 56 | e.completeError(error); 57 | } 58 | } 59 | }); 60 | } 61 | 62 | void onDone() { 63 | onError("stream is closed"); 64 | } 65 | 66 | Future> next() { 67 | if (_npack == 0) { 68 | if (done) { 69 | return Future>.error("stream closed"); 70 | } 71 | _nfut += 1; 72 | _queue.addLast(Completer>()); 73 | return _queue.last.future; 74 | } else { 75 | Completer> c = _queue.removeFirst(); 76 | _npack -= 1; 77 | return c.future; 78 | } 79 | } 80 | } 81 | 82 | // it 83 | class LazyStream { 84 | late StreamNext _stream; 85 | late List _remainder; 86 | late List _return; 87 | late Iterator _iter; 88 | LazyStream.fromstream(Socket socket) { 89 | _stream = StreamNext.fromstream(socket); 90 | _return = []; 91 | _remainder = []; 92 | _iter = _remainder.iterator; 93 | } 94 | 95 | void close() { 96 | _stream.onDone(); 97 | } 98 | 99 | Future> take_n(int n) { 100 | _return = []; 101 | return __take_n(n); 102 | } 103 | 104 | Future> __take_n(int n) { 105 | int rest = _take_n_helper(n); 106 | if (rest == 0) { 107 | return Future>.value(_return); 108 | } else { 109 | return _stream.next().then>((List pack) { 110 | _remainder = pack; 111 | _iter = _remainder.iterator; 112 | return __take_n(rest); 113 | }); 114 | } 115 | } 116 | 117 | // return remining n 118 | int _take_n_helper(int n) { 119 | while (n > 0 && _iter.moveNext()) { 120 | _return.add(_iter.current); 121 | n--; 122 | } 123 | return n; 124 | } 125 | 126 | Future> take_while(bool Function(int) pred) { 127 | _return = []; 128 | return __take_while(pred); 129 | } 130 | 131 | Future> __take_while(bool Function(int) pred) { 132 | if (_take_while_helper(pred)) { 133 | return Future>.value(_return); 134 | } else { 135 | return _stream.next().then>((List rem) { 136 | _remainder = rem; 137 | _iter = _remainder.iterator; 138 | return __take_while(pred); 139 | }); 140 | } 141 | } 142 | 143 | // return true when exaused (when predicate returns false) 144 | bool _take_while_helper(bool Function(int) pred) { 145 | while (_iter.moveNext()) { 146 | if (pred(_iter.current)) { 147 | _return.add(_iter.current); 148 | } else { 149 | return true; 150 | } 151 | } 152 | return false; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/pubsub.dart: -------------------------------------------------------------------------------- 1 | part of redis; 2 | 3 | class _WarrningPubSubInProgress extends RedisConnection { 4 | RedisConnection _connection; 5 | _WarrningPubSubInProgress(this._connection) {} 6 | 7 | _err() => throw "PubSub on this connaction in progress" 8 | "It is not allowed to issue commands trough this handler"; 9 | 10 | // swap this relevant methods in Conenction with exception 11 | // ignore: unused_element 12 | Future _sendraw(Parser parser, List data) => _err(); 13 | 14 | // ignore: unused_element 15 | Future _getdummy() => _err(); 16 | Future _senddummy(Parser parser) => _err(); 17 | 18 | // this fake PubSub connection can be closed 19 | Future close() { 20 | return this._connection.close(); 21 | } 22 | } 23 | 24 | class PubSub { 25 | late Command _command; 26 | StreamController _stream_controler = StreamController(); 27 | 28 | PubSub(Command command) { 29 | _command = Command.from(command); 30 | command.send_nothing()!.then((_) { 31 | //override socket with warrning 32 | command._connection = _WarrningPubSubInProgress(_command._connection); 33 | // listen and process forever 34 | return Future.doWhile(() { 35 | return _command._connection 36 | ._senddummy(_command.parser) 37 | .then((var data) { 38 | try { 39 | _stream_controler.add(data); 40 | return true; // run doWhile more 41 | } catch (e) { 42 | try { 43 | _stream_controler.addError(e); 44 | } catch (_) { 45 | // we could not notfy stream that we have eror 46 | } 47 | // stop doWhile() 48 | _stream_controler.close(); 49 | return false; 50 | } 51 | }).catchError((e) { 52 | try { 53 | _stream_controler.addError(e); 54 | } catch (_) { 55 | // we could not notfy stream that we have eror 56 | } 57 | // stop doWhile() 58 | _stream_controler.close(); 59 | return false; 60 | }); 61 | }); 62 | }); 63 | } 64 | 65 | Stream getStream() { 66 | return _stream_controler.stream; 67 | } 68 | 69 | void subscribe(List s) { 70 | _sendcmd_and_list("SUBSCRIBE", s); 71 | } 72 | 73 | void psubscribe(List s) { 74 | _sendcmd_and_list("PSUBSCRIBE", s); 75 | } 76 | 77 | void unsubscribe(List s) { 78 | _sendcmd_and_list("UNSUBSCRIBE", s); 79 | } 80 | 81 | void punsubscribe(List s) { 82 | _sendcmd_and_list("PUNSUBSCRIBE", s); 83 | } 84 | 85 | void _sendcmd_and_list(String cmd, List s) { 86 | List list = [cmd]; 87 | list.addAll(s); 88 | _command._connection._socket.add(_command.serializer.serialize(list)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/redis.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | library redis; 11 | 12 | import 'dart:io'; 13 | import 'dart:async'; 14 | import 'dart:convert'; 15 | import 'dart:collection'; 16 | 17 | part './redisserialise.dart'; 18 | part './connection.dart'; 19 | part './lazystream.dart'; 20 | part './transaction.dart'; 21 | part './redisparser.dart'; 22 | part './command.dart'; 23 | part './pubsub.dart'; 24 | part './cas.dart'; 25 | part './exceptions.dart'; 26 | -------------------------------------------------------------------------------- /lib/redisparser.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | class RedisParser extends Parser {} 13 | 14 | class RedisParserBulkBinary extends Parser { 15 | Future parseBulk(LazyStream s) { 16 | return parseInt(s).then((i) { 17 | //get len 18 | if (i == -1) //null 19 | return null; 20 | if (i >= 0) { 21 | //i of bulk data 22 | return s 23 | .take_n(i) 24 | .then((lst) => takeCRLF(s, lst)); //consume CRLF and return list 25 | } else { 26 | return Future.error( 27 | RedisRuntimeError("cant process buld data less than -1")); 28 | } 29 | }); 30 | } 31 | } 32 | 33 | class Parser { 34 | static final UTF8 = const Utf8Codec(); 35 | static const int CR = 13; 36 | static const int LF = 10; 37 | 38 | static const int TYPE_SS = 43; //+ 39 | static const int TYPE_ERROR = 45; //- 40 | static const int TYPE_INT = 58; //: 41 | static const int TYPE_BULK = 36; //$ 42 | static const int TYPE_ARRAY = 42; //* 43 | 44 | //read untill it finds CR and LF 45 | //by protocol it is enough to find just CR and LF folows 46 | //this method can be used only on types that complies with such rule 47 | //it consumes both CR and LF from stream, but is not returned 48 | Future read_simple(LazyStream s) { 49 | return s.take_while((c) => (c != CR)).then((list) { 50 | //takeWile consumed CR from stream, 51 | //now check for LF 52 | return s.take_n(1).then((lf) { 53 | if (lf[0] != LF) { 54 | return Future.error(RedisRuntimeError("received element is not LF")); 55 | } 56 | return list; 57 | }); 58 | }); 59 | } 60 | 61 | //return Future if next two elemets are CRLF 62 | //or thows if failed 63 | Future takeCRLF(LazyStream s, r) { 64 | return s.take_n(2).then((data) { 65 | if (data[0] == CR && data[1] == LF) { 66 | return r; 67 | } else { 68 | return Future.error(RedisRuntimeError("expeting CRLF")); 69 | } 70 | }); 71 | } 72 | 73 | Future parse(LazyStream s) { 74 | return parseredisresponse(s); 75 | } 76 | 77 | Future parseredisresponse(LazyStream s) { 78 | return s.take_n(1).then((list) { 79 | int cmd = list[0]; 80 | switch (cmd) { 81 | case TYPE_SS: 82 | return parseSimpleString(s); 83 | case TYPE_INT: 84 | return parseInt(s); 85 | case TYPE_ARRAY: 86 | return parseArray(s); 87 | case TYPE_BULK: 88 | return parseBulk(s); 89 | case TYPE_ERROR: 90 | return parseError(s); 91 | default: 92 | return Future.error( 93 | RedisRuntimeError("got element that cant not be parsed")); 94 | } 95 | }); 96 | } 97 | 98 | Future parseSimpleString(LazyStream s) { 99 | return read_simple(s).then((v) { 100 | return UTF8.decode(v); 101 | }); 102 | } 103 | 104 | Future parseError(LazyStream s) { 105 | return parseSimpleString(s).then((str) => RedisError(str)); 106 | } 107 | 108 | Future parseInt(LazyStream s) { 109 | return read_simple(s).then((v) => _ParseIntRaw(v)); 110 | } 111 | 112 | Future parseBulk(LazyStream s) { 113 | return parseInt(s).then((i) { 114 | //get len 115 | if (i == -1) //null 116 | return null; 117 | if (i >= 0) { 118 | //i of bulk data 119 | return s.take_n(i).then((lst) => takeCRLF( 120 | s, UTF8.decode(lst))); //consume CRLF and return decoded list 121 | } else { 122 | return Future.error( 123 | RedisRuntimeError("cant process buld data less than -1")); 124 | } 125 | }); 126 | } 127 | 128 | //it first consume array as N and then 129 | //consume N elements with parseredisresponse function 130 | Future parseArray(LazyStream s) { 131 | //closure 132 | Future consumeList(LazyStream s, int len, List lst) { 133 | assert(len >= 0); 134 | if (len == 0) { 135 | return Future.value(lst); 136 | } 137 | return parseredisresponse(s).then((resp) { 138 | lst.add(resp); 139 | return consumeList(s, len - 1, lst); 140 | }); 141 | } 142 | 143 | //end of closure 144 | return parseInt(s).then((i) { 145 | //get len 146 | if (i == -1) //null 147 | return [null]; 148 | if (i >= 0) { 149 | //i of array data 150 | List a = []; 151 | return consumeList(s, i, a); 152 | } else { 153 | return Future.error( 154 | RedisRuntimeError("cant process array data less than -1")); 155 | } 156 | }); 157 | } 158 | 159 | //maualy parse int from raw data (faster) 160 | static int _ParseIntRaw(Iterable arr) { 161 | int sign = 1; 162 | var v = arr.fold(0, (dynamic a, b) { 163 | if (b == 45) { 164 | if (a != 0) throw RedisRuntimeError("cannot parse int"); 165 | sign = -1; 166 | return 0; 167 | } else if ((b >= 48) && (b < 58)) { 168 | return a * 10 + b - 48; 169 | } else { 170 | throw RedisRuntimeError("cannot parse int"); 171 | } 172 | }); 173 | return v * sign; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/redisserialise.dart: -------------------------------------------------------------------------------- 1 | part of redis; 2 | /* 3 | * Free software licenced under 4 | * MIT License 5 | * 6 | * Check for document LICENCE forfull licence text 7 | * 8 | * Luka Rahne 9 | */ 10 | 11 | Utf8Encoder RedisSerializeEncoder = Utf8Encoder(); 12 | 13 | class RedisBulk { 14 | Iterable iterable; 15 | 16 | /// This clase enables sending Iterable 17 | /// as bulk data on redis 18 | /// it can be used when sending files for example 19 | RedisBulk(this.iterable) {} 20 | } 21 | 22 | typedef Consumer = void Function(Iterable s); 23 | 24 | class Serializer { 25 | List serialize(Object? object) { 26 | return RedisSerialize.Serialize(object); 27 | } 28 | } 29 | 30 | class RedisSerialize { 31 | static final ASCII = const AsciiCodec(); 32 | static final UTF8 = const Utf8Codec(); 33 | static final _dollar = ASCII.encode("\$"); 34 | static final _star = ASCII.encode("\*"); 35 | static final _semicol = ASCII.encode(":"); 36 | static final _linesep = ASCII.encode("\r\n"); 37 | static final _dollarminus1 = ASCII.encode("\$-1"); 38 | 39 | static List Serialize(Object? object) { 40 | final s = []; 41 | SerializeConsumable(object, (v) => s.addAll(v)); 42 | return s; 43 | } 44 | 45 | static void SerializeConsumable(Object? object, Consumer consumer) { 46 | if (object is String) { 47 | var data = UTF8.encode(object); 48 | consumer(_dollar); 49 | consumer(_IntToRaw(data.length)); 50 | consumer(_linesep); 51 | consumer(data); 52 | consumer(_linesep); 53 | } else if (object is Iterable) { 54 | int len = object.length; 55 | consumer(_star); 56 | consumer(_IntToRaw(len)); 57 | consumer(_linesep); 58 | object.forEach( 59 | (v) => SerializeConsumable(v is int ? v.toString() : v, consumer)); 60 | } else if (object is int) { 61 | consumer(_semicol); 62 | consumer(_IntToRaw(object)); 63 | consumer(_linesep); 64 | } else if (object is RedisBulk) { 65 | consumer(_dollar); 66 | consumer(_IntToRaw(object.iterable.length)); 67 | consumer(_linesep); 68 | consumer(object.iterable); 69 | consumer(_linesep); 70 | } else if (object == null) { 71 | consumer(_dollarminus1); //null bulk 72 | } else { 73 | throw ("cant serialize such type"); 74 | } 75 | } 76 | 77 | static Iterable _IntToRaw(int n) { 78 | //if(i>=0 && i < _ints.length) 79 | // return _ints[i]; 80 | return ASCII.encode(n.toString()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/transaction.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | part of redis; 11 | 12 | class _WarningConnection { 13 | noSuchMethod(_) => throw RedisRuntimeError("Transaction in progress. " 14 | "Please complete Transaction with .exec"); 15 | } 16 | 17 | class Transaction extends Command { 18 | Queue _queue = Queue(); 19 | late Command _overrided_command; 20 | bool transaction_completed = false; 21 | 22 | Transaction(Command command) : super(command._connection) { 23 | _overrided_command = command; 24 | //we override his _connection, during transaction 25 | //it is best to point out where problem is 26 | command._connection = _WarningConnection(); 27 | } 28 | 29 | Future send_object(object) { 30 | if (transaction_completed) { 31 | return Future.error(RedisRuntimeError("Transaction already completed.")); 32 | } 33 | 34 | Completer c = Completer(); 35 | _queue.add(c); 36 | super.send_object(object).then((msg) { 37 | if (msg.toString().toLowerCase() != "queued") { 38 | c.completeError( 39 | RedisError("Could not enqueue command: " + msg.toString())); 40 | } 41 | }).catchError((error) { 42 | c.completeError(error); 43 | }); 44 | return c.future; 45 | } 46 | 47 | Future? discard() { 48 | _overrided_command._connection = this._connection; 49 | transaction_completed = true; 50 | return super.send_object(["DISCARD"]); 51 | } 52 | 53 | Future exec() { 54 | _overrided_command._connection = this._connection; 55 | transaction_completed = true; 56 | return super.send_object(["EXEC"]).then((list) { 57 | if (list == null || (list.length == 1 && list[0] == null)) { 58 | //we got explicit error from redis 59 | while (_queue.isNotEmpty) { 60 | _queue.removeFirst(); 61 | } 62 | // return Future.error(TransactionError("transaction error ")); 63 | throw TransactionError("transaction error "); 64 | //return null; 65 | } else { 66 | if (list.length != _queue.length) { 67 | int? diff = list.length - _queue.length; 68 | //return 69 | throw RedisRuntimeError( 70 | "There was $diff command(s) executed during transcation," 71 | "not going trough Transation handler"); 72 | } 73 | int len = list.length; 74 | for (int i = 0; i < len; ++i) { 75 | Completer c = _queue.removeFirst(); 76 | c.complete(list[i]); 77 | } 78 | return "OK"; 79 | } 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: redis 2 | version: 4.0.0 3 | description: Redis database (https://redis.io/) client, with both simplicity and performance as primary goals. 4 | homepage: https://github.com/ra1u/redis-dart 5 | repository: https://github.com/ra1u/redis-dart 6 | issue_tracker: https://github.com/ra1u/redis-dart/issues 7 | 8 | environment: 9 | sdk: '>=2.12.0 <3.0.0' 10 | 11 | dev_dependencies: 12 | test: ^1.6.5 13 | -------------------------------------------------------------------------------- /test/basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | void main() { 7 | group("Basic Redis Functionality test", () { 8 | String key = "key1b1"; 9 | 10 | test("One by One", () async { 11 | Command cmd = await generate_connect(); 12 | 13 | expect(await cmd.send_object(["SET", key, 0]), equals("OK")); 14 | 15 | for (int i = 0; i < 100; i++) { 16 | expect(await cmd.send_object(["INCR", key]), equals(i + 1)); 17 | } 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/binary_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | import 'dart:math'; 6 | 7 | List _gen_rnd_array(Random rng) { 8 | int len = 1 + rng.nextInt(1000); 9 | List r = List.filled(len, 0); 10 | for (int i = 0; i < len; ++i) { 11 | r[i] = rng.nextInt(255); 12 | } 13 | return r; 14 | } 15 | 16 | void main() { 17 | group("Test for sending and parsing binary data", () { 18 | String key = "keyBinary"; 19 | 20 | test("binary", () async { 21 | Command _cmd = await generate_connect(); 22 | Command cmd_bin = Command.from(_cmd).setParser(RedisParserBulkBinary()); 23 | 24 | List d = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 25 | var r = await cmd_bin.send_object(["SET", key, RedisBulk(d)]); 26 | expect(r, equals("OK")); 27 | expect(await cmd_bin.send_object(["GET", key]), equals(d)); 28 | }); 29 | 30 | test("binary with randomly generated data", () async { 31 | Command _cmd = await generate_connect(); 32 | Command cmd_bin = Command.from(_cmd).setParser(RedisParserBulkBinary()); 33 | var rng = Random(); 34 | 35 | for (int i = 0; i < 1000; ++i) { 36 | List d = _gen_rnd_array(rng); 37 | var r = await cmd_bin.send_object(["SET", key, RedisBulk(d)]); 38 | expect(r, equals("OK")); 39 | expect(await cmd_bin.send_object(["GET", key]), equals(d)); 40 | } 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/cas_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:redis/redis.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'main.dart'; 8 | 9 | void main() { 10 | test("Test Incr CAS Multiple", () async { 11 | Command cmd = await generate_connect(); 12 | 13 | cmd.send_object(["SET", "key", "0"]); 14 | Queue q = Queue(); 15 | int N = 100; 16 | for (int i = 0; i < N; i++) { 17 | q.add(testincrcas()); 18 | } 19 | 20 | await Future.wait(q); 21 | var val = await cmd.send_object(["GET", "key"]); 22 | return expect(val, equals(N.toString())); 23 | }); 24 | } 25 | 26 | Future testincrcas() { 27 | return generate_connect().then((Command command) { 28 | Cas cas = Cas(command); 29 | return cas.watch(["key"], () { 30 | command.send_object(["GET", "key"]).then((val) { 31 | int i = int.parse(val); 32 | i++; 33 | return cas.multiAndExec((trans) { 34 | return trans.send_object(["SET", "key", i.toString()]); 35 | }); 36 | }); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/close_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | main() { 7 | group("Close test", () { 8 | test("Expect error when sending Garbage", () async { 9 | Command cmd = await generate_connect(); 10 | await cmd.send_object(["SET", "test", "0"]); 11 | for (int i = 1; i <= 100000; i++) { 12 | cmd.send_object(["INCR", "test"]).then((v) { 13 | if (i != v) { 14 | throw ("wrong received value, we got $v"); 15 | } 16 | }).catchError((e) { 17 | // stream closed 18 | }); 19 | } 20 | await cmd.get_connection().close(); 21 | //expect(cmd.send_object("GARBAGE"), throwsA(isRedisError)); 22 | }); 23 | 24 | test("Open/Close in loop", () async { 25 | for (int i = 0; i < 1000; ++i) { 26 | Command cmd = await generate_connect(); 27 | await cmd.send_object(["SET", "test", "0"]); 28 | await cmd.get_connection().close(); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | const Matcher isRedisError = TypeMatcher(); 35 | -------------------------------------------------------------------------------- /test/conversion_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | import 'main.dart'; 4 | 5 | void main() { 6 | group("Redis Type Conversion", () { 7 | test("Expect Integer conversion to Bulk String", () async { 8 | Command cmd = await generate_connect(); 9 | 10 | expect(cmd.send_object(["SADD", "test_list", 1]), completion(isA())); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/docker/Readme.txt: -------------------------------------------------------------------------------- 1 | Simple test that can be run inside container 2 | we use this for local testing 3 | 4 | Requrements: 5 | - docker 6 | - docker-compose 7 | 8 | to run this test execute 9 | 10 | docker-compose up --abort-on-container-exit && echo ok 11 | 12 | In case of funny errors remove pubspec.lock in top dir 13 | 14 | Extra: 15 | 16 | We also provide script for podman that can be used for rootless execution. 17 | In that case docker is not needed, podman only. 18 | 19 | ./run_podman.sh 20 | -------------------------------------------------------------------------------- /test/docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2.3" 2 | 3 | services: 4 | redis: 5 | image: redis 6 | 7 | dart: 8 | image: google/dart:2.12 # least supported version 9 | depends_on: 10 | - redis 11 | entrypoint: ["/bin/sh", "-c"] 12 | volumes: 13 | - "../../:/workdir" 14 | environment: 15 | - REDIS_URL=redis 16 | - REDIS_PORT=6379 17 | command: 18 | - | 19 | set -e #exit on falure 20 | sleep 1 #todo use better approach 21 | cd /workdir 22 | dart --version 23 | dart pub get 24 | dart analyze || true 25 | dart test 26 | dart test/performance.dart 27 | -------------------------------------------------------------------------------- /test/docker/run_podman.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # run tests localy with podman instead of docker 4 | # can be executed rootless 5 | 6 | set -e 7 | 8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 9 | 10 | #use cache 11 | LOCAL_CACHE_DIR="${HOME}/.pub-cache" 12 | mkdir -p ${LOCAL_CACHE_DIR} 13 | 14 | #make pod 15 | POD=$(podman pod create) 16 | 17 | echo "POD ${POD}" 18 | 19 | # cleanup on script exit 20 | trap "podman pod rm -f ${POD}" EXIT 21 | 22 | # run redis in pod 23 | REDIS_IMAGE=redis 24 | podman run \ 25 | --detach \ 26 | --rm \ 27 | --pod ${POD} \ 28 | ${REDIS_IMAGE} 29 | 30 | # run dart in pod 31 | for TAG in "2.12" "stable" "beta" 32 | do 33 | DART_IMAGE=docker.io/dart:${TAG} 34 | echo "image ${DART_IMAGE}" 35 | podman run \ 36 | --rm \ 37 | --pod ${POD} \ 38 | --entrypoint "/bin/sh" \ 39 | --env REDIS_URL=127.0.0.1 \ 40 | --env REDIS_PORT=6379 \ 41 | --volume "${SCRIPT_DIR}/../../:/workdir" \ 42 | --volume "${LOCAL_CACHE_DIR}:/root/.pub-cache" \ 43 | ${DART_IMAGE} \ 44 | -c "set -e 45 | sleep 1 46 | cd /workdir 47 | dart --version 48 | dart pub get 49 | dart analyze 50 | #dart analyze --fatal-infos 51 | dart test 52 | dart test/performance.dart" 53 | done 54 | -------------------------------------------------------------------------------- /test/error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | import 'dart:io'; 6 | 7 | Future generate_connect_broken() { 8 | return RedisConnection().connect("localhost", 2); 9 | } 10 | 11 | main() { 12 | group("Throw received Redis Errors", () { 13 | test("Expect error when sending Garbage", () async { 14 | Command cmd = await generate_connect(); 15 | expect(() => cmd.send_object("GARBAGE"), throwsA(isRedisError)); 16 | }); 17 | }); 18 | 19 | group("Recover after received Redis Errors", () { 20 | test("Expect error when sending Garbage 2", () async { 21 | Command cmd = await generate_connect(); 22 | expect(() => cmd.send_object(["GARBAGE"]), throwsA(isRedisError)); 23 | // next two commands over same connection should be fine 24 | var ok = await cmd.send_object(["SET", "garbage_test", "grb"]); 25 | expect(ok, equals("OK")); 26 | var v = await cmd.send_object(["GET", "garbage_test"]); 27 | expect(v, equals("grb")); 28 | }); 29 | }); 30 | 31 | group("Handle low lewel error", () { 32 | test("handle error that is out of our contoll", () { 33 | expect(generate_connect_broken, throwsA(isA())); 34 | }); 35 | }); 36 | } 37 | 38 | const Matcher isRedisError = TypeMatcher(); 39 | -------------------------------------------------------------------------------- /test/lua_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | void main() { 7 | test("Test Lua Native", () async { 8 | Command cmd = await generate_connect(); 9 | 10 | expect( 11 | (await cmd.send_object([ 12 | "EVAL", 13 | "return {KEYS[1],{KEYS[2],{ARGV[1]},ARGV[2]},2}", 14 | "2", 15 | "key1", 16 | "key2", 17 | "first", 18 | "2" 19 | ])) 20 | .toString(), 21 | equals("[key1, [key2, [first], 2], 2]")); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/main.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Free software licenced under 3 | * MIT License 4 | * 5 | * Check for document LICENCE forfull licence text 6 | * 7 | * Luka Rahne 8 | */ 9 | 10 | import 'dart:async'; 11 | 12 | import 'package:redis/redis.dart'; 13 | import 'package:test/test.dart'; 14 | import 'dart:io' show Platform; 15 | 16 | bool g_redis_initialsed = false; 17 | String g_db_uri = ""; 18 | int g_db_port = 0; 19 | 20 | void init_db_vars() { 21 | // read REDIS_URL and REDIS_PORT from ENV and store in globals for faster retreival 22 | if (g_redis_initialsed) return; 23 | Map envVars = Platform.environment; 24 | g_db_uri = envVars["REDIS_URL"] ?? "localhost"; 25 | String port = envVars["REDIS_PORT"] ?? "6379"; 26 | g_db_port = int.tryParse(port) ?? 6379; 27 | g_redis_initialsed = true; 28 | } 29 | 30 | Future generate_connect() { 31 | init_db_vars(); 32 | return RedisConnection().connect(g_db_uri, g_db_port); 33 | } 34 | 35 | void main() { 36 | setUpAll(() => generate_connect().then((cmd) => cmd.send_object("FLUSHALL"))); 37 | } 38 | -------------------------------------------------------------------------------- /test/performance.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:redis/redis.dart'; 5 | 6 | import 'main.dart'; 7 | 8 | void main() { 9 | print("Performance TEST and REPORT"); 10 | testing_performance( 11 | test_muliconnections_con(100) as Future Function(int), 12 | "Multiple connections", 13 | 200000) 14 | .then( 15 | (_) => testing_performance(test_pubsub_performance, "PubSub", 200000)) 16 | .then((_) => 17 | testing_performance(test_performance, "Raw Performance", 200000)) 18 | .then((_) => test_long_running(20000)) 19 | .then((_) => print("Finished Performance Tests")) 20 | .then((_) => exit(0)); 21 | } 22 | 23 | Future testing_performance( 24 | Future Function(int) fun, String title, int iterations) { 25 | int start = DateTime.now().millisecondsSinceEpoch; 26 | return fun(iterations).then((_) { 27 | int end = DateTime.now().millisecondsSinceEpoch; 28 | 29 | double diff = (end - start) / 1000.0; 30 | int perf = (iterations / diff).round(); 31 | print(title + 32 | ": " + 33 | perf.toString() + 34 | " op/s\t (Iterations: " + 35 | iterations.toString() + 36 | ")"); 37 | 38 | return perf; 39 | }); 40 | } 41 | 42 | Future test_pubsub_performance(int N) { 43 | late Command command; //on conn1 tosend commands 44 | late Stream pubsubstream; //on conn2 to rec c 45 | return generate_connect().then((Command cmd) { 46 | command = cmd; 47 | return generate_connect(); 48 | }).then((Command cmd) { 49 | PubSub pubsub = PubSub(cmd); 50 | pubsub.subscribe(["monkey"]); 51 | pubsubstream = pubsub.getStream(); 52 | return pubsubstream; 53 | }).then((_) { 54 | //bussy wait for prevous to be subscibed 55 | return Future.doWhile(() { 56 | return command 57 | .send_object(["PUBSUB", "NUMSUB", "monkey"]).then((v) => v[1] == 0); 58 | }).then((_) { 59 | //at thuis point one is subscribed 60 | for (int i = 0; i < N; ++i) { 61 | command.send_object(["PUBLISH", "monkey", "banana"]); 62 | } 63 | }); 64 | }).then((_) { 65 | int counter = 0; 66 | //var expected = ["message", "monkey", "banana"]; 67 | late var subscription; 68 | Completer comp = Completer(); 69 | subscription = pubsubstream.listen((var data) { 70 | counter++; 71 | if (counter == N) { 72 | subscription.cancel(); 73 | comp.complete("OK"); 74 | } 75 | }); 76 | return comp.future; 77 | }); 78 | } 79 | 80 | Future test_performance(int n, [bool piping = true]) { 81 | int N = n; 82 | //int rec = 0; 83 | //int start; 84 | return generate_connect().then((Command command) { 85 | //start = DateTime.now().millisecondsSinceEpoch; 86 | if (piping) { 87 | command.pipe_start(); 88 | } 89 | command.send_object(["SET", "test", "0"]); 90 | for (int i = 1; i <= N; i++) { 91 | command.send_object(["INCR", "test"]).then((v) { 92 | if (i != v) { 93 | throw ("wrong received value, we got $v"); 94 | } 95 | }); 96 | } 97 | //last command will be executed and then processed last 98 | Future r = command.send_object(["GET", "test"]).then((v) { 99 | if (N.toString() != v.toString()) { 100 | throw ("wrong received value, we got $v instead of $N"); 101 | } 102 | return true; 103 | }); 104 | if (piping) { 105 | command.pipe_end(); 106 | } 107 | return r; 108 | }); 109 | } 110 | 111 | Function test_muliconnections_con(int conn) { 112 | return (int cmd) => test_muliconnections(cmd, conn); 113 | } 114 | 115 | Future test_muliconnections(int commands, int connections) { 116 | int N = commands; 117 | int K = connections; 118 | int c = 0; 119 | 120 | Completer completer = Completer(); 121 | generate_connect().then((Command command) { 122 | return command.set("var", "0"); 123 | }).then((_) { 124 | for (int j = 0; j < K; j++) { 125 | RedisConnection(); 126 | generate_connect().then((Command command) { 127 | command.pipe_start(); 128 | for (int i = j; i < N; i += K) { 129 | command.send_object(["INCR", "var"]).then((v) { 130 | c++; 131 | if (c == N) { 132 | command.get("var").then((v) { 133 | assert(v == N.toString()); 134 | completer.complete("ok"); 135 | }); 136 | } 137 | }); 138 | } 139 | command.pipe_end(); 140 | }); 141 | } 142 | }); 143 | return completer.future; 144 | } 145 | 146 | //this one employs doWhile to allow numerous 147 | //commands wihout "memory leaking" 148 | //next command is executed after prevous commands completes 149 | //performance of this test depends on packet roundtrip time 150 | Future test_long_running(int n) { 151 | int start = DateTime.now().millisecondsSinceEpoch; 152 | int update_period = 2000; 153 | int timeout = start + update_period; 154 | //const String key = "keylr"; 155 | return generate_connect().then((Command command) { 156 | int N = n; 157 | int c = 0; 158 | print(" started long running test of $n commands"); 159 | return Future.doWhile(() { 160 | c++; 161 | if (c >= N) { 162 | print(" done"); 163 | int now = DateTime.now().millisecondsSinceEpoch; 164 | double diff = (now - start) / 1000.0; 165 | double perf = c / diff; 166 | print(" ping-pong test performance ${perf.round()} ops/s"); 167 | return false; 168 | } 169 | if (c % 40000 == 0) { 170 | int now = DateTime.now().millisecondsSinceEpoch; 171 | if (now > timeout) { 172 | timeout += update_period; 173 | double diff = (now - start) / 1000.0; 174 | double perf = c / diff; 175 | print( 176 | " ping-pong test running ${((N - c) / perf).round()}s to complete , performance ${perf.round()} ops/s"); 177 | } 178 | } 179 | return command.send_object(["PING"]).then((v) { 180 | if (v != "PONG") { 181 | throw "expeted $c but got $v"; 182 | } 183 | return true; 184 | }); 185 | }); 186 | }); 187 | } 188 | 189 | //this one employs doWhile to allow numerous 190 | //commands wihout "memory leaking" 191 | //it uses multiple connections 192 | Future test_long_running2(int n, int k) { 193 | int start = DateTime.now().millisecondsSinceEpoch; 194 | int timeout = start + 5000; 195 | const String key = "keylr"; 196 | Completer completer = Completer(); 197 | generate_connect().then((Command command) { 198 | int N = n; 199 | int c = 0; 200 | print(" started long running test of $n commands and $k connections"); 201 | command.send_object(["SET", key, "0"]).then((_) { 202 | for (int i = 0; i < k; i++) { 203 | generate_connect().then((Command command) { 204 | Future.doWhile(() { 205 | c++; 206 | if (c >= N) { 207 | if (c == N) { 208 | print(" done"); 209 | completer.complete("OK"); 210 | } 211 | return Future(() => false); 212 | } 213 | 214 | int now = DateTime.now().millisecondsSinceEpoch; 215 | if (now > timeout) { 216 | timeout += 5000; 217 | double diff = (now - start) / 1000.0; 218 | double perf = c / diff; 219 | print( 220 | " ping-pong test running ${((N - c) / perf).round()}s to complete , performance ${perf.round()} ops/s"); 221 | } 222 | return command.send_object(["INCR", key]).then((v) { 223 | return true; 224 | }); 225 | }); 226 | }); 227 | } 228 | }); 229 | }); 230 | return completer.future; 231 | } 232 | -------------------------------------------------------------------------------- /test/pubsub_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | void main() async { 7 | Command cmdP = await generate_connect(); 8 | Command cmdS = await generate_connect(); 9 | 10 | group("Test Redis Pub-Sub", () { 11 | PubSub subscriber = PubSub(cmdS); 12 | 13 | test("Publishing to channel before subscription", () { 14 | expect(cmdP.send_object(["PUBLISH", "test", "hello"]), 15 | completion(equals(0))); 16 | }); 17 | 18 | test("Subscribe to channel", () { 19 | expect(() => subscriber.subscribe(["test"]), returnsNormally, 20 | reason: "No error should be thrown when subscribing to channel."); 21 | 22 | expect(cmdP.send_object(["PUBSUB", "NUMSUB", "test"]), 23 | completion(equals(["test", 1])), 24 | reason: "Number of subscribers should be 1 after subscription"); 25 | 26 | expect( 27 | () => cmdS.send_object("PING"), 28 | throwsA(equals("PubSub on this connaction in progress" 29 | "It is not allowed to issue commands trough this handler")), 30 | reason: "After subscription, command should not be able to send"); 31 | }); 32 | 33 | test("Publishing to channel", () { 34 | expect(cmdP.send_object(["PUBLISH", "test", "goodbye"]), 35 | completion(equals(1))); 36 | 37 | expect( 38 | subscriber.getStream(), 39 | emitsInOrder([ 40 | ["subscribe", "test", 1], 41 | ["message", "test", "goodbye"] 42 | ]), 43 | reason: "After subscribing, the message should be received."); 44 | }); 45 | 46 | test("Unsubscribe channel", () { 47 | expect(() => subscriber.unsubscribe(["test"]), returnsNormally, 48 | reason: "No error should be thrown when subscribing to channel."); 49 | 50 | expect(cmdP.send_object(["PUBSUB", "NUMSUB", "test"]), 51 | completion(equals(["test", 0])), 52 | reason: "Number of subscribers should be 0 after unsubscribe"); 53 | 54 | expect(cmdP.send_object(["PUBLISH", "test", "goodbye"]), 55 | completion(equals(0)), 56 | reason: 57 | "Publishing a message after unsubscribe should be received by zero clients."); 58 | 59 | // TODO: Multiple channels, Pattern (un)subscribe 60 | }); 61 | 62 | test("Test close", () async { 63 | // test that we can close connection 64 | // creates new connection as prevously used in test 65 | // does not expect errors 66 | Command cmdClose = await generate_connect(); 67 | PubSub ps_c = PubSub(cmdClose); 68 | cmdClose.get_connection().close(); 69 | expect(ps_c.getStream(), emitsError(anything), // todo catch CloseError 70 | reason: "Number of subscribers should be 0 after unsubscribe"); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /test/transactions_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:redis/redis.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | import 'main.dart'; 8 | 9 | void main() { 10 | test("Basic Transaction Test", () async { 11 | Command cmd1 = await generate_connect(); 12 | Command cmd2 = await generate_connect(); 13 | 14 | const String key = "transaction_key"; 15 | int n = 2; 16 | 17 | // Start Transaction 18 | Transaction trans = await cmd1.multi(); 19 | trans.send_object(["SET", key, "0"]); 20 | 21 | cmd2.send_object(["SET", key, "10"]); 22 | 23 | for (int i = 1; i <= n; ++i) { 24 | trans.send_object(["INCR", key]).then((v) { 25 | expect(v == i, true, 26 | reason: 27 | "Transaction value should not be interfered by actions outside of transaction"); 28 | }).catchError((e) { 29 | print("got test error $e"); 30 | expect(e, TypeMatcher()); 31 | }); 32 | 33 | // Increase value out of transaction 34 | cmd2.send_object(["INCR", key]); 35 | } 36 | 37 | expect(trans.send_object(["GET", key]), completion(equals(n.toString())), 38 | reason: "Transaction value should be final value $n"); 39 | 40 | //Test using command fail during transaction 41 | expect(() => cmd1.send_object(['SET', key, 0]), 42 | throwsA(TypeMatcher()), 43 | reason: "Command should not be usable during transaction"); 44 | 45 | expect(trans.exec(), completion(equals("OK")), 46 | reason: "Transaction should be executed."); 47 | 48 | expect(cmd1.send_object(["GET", key]), completion(equals(n.toString())), 49 | reason: "Value should be final value $n after transaction complete"); 50 | 51 | expect(() => trans.send_object(["GET", key]), 52 | throwsA(TypeMatcher()), 53 | reason: 54 | "Transaction object should not be usable after finishing transaction"); 55 | }); 56 | 57 | group("Fake CAS", () { 58 | test("Transaction Fake CAS", () { 59 | expect(() => test_incr_fakecas(), returnsNormally); 60 | }); 61 | 62 | test("Transaction Fake CAS Multiple", () { 63 | expect(() => test_incr_fakecas_multiple(10), returnsNormally); 64 | }); 65 | }); 66 | } 67 | 68 | //this doesnt use Cas class, but does same functionality 69 | Future test_incr_fakecas() { 70 | RedisConnection(); 71 | String key = "keycaswewe"; 72 | return generate_connect().then((Command cmd) { 73 | cmd.send_object(["SETNX", key, "1"]); 74 | return Future.doWhile(() { 75 | cmd.send_object(["WATCH", key]); 76 | return cmd.send_object(["GET", key]).then((val) { 77 | int i = int.parse(val); 78 | ++i; 79 | return cmd.multi().then((Transaction trans) { 80 | trans.send_object(["SET", key, i.toString()]); 81 | return trans.exec().then((var res) { 82 | return false; //terminate doWhile 83 | }).catchError((e) { 84 | return true; // try again 85 | }); 86 | }); 87 | }); 88 | }); 89 | }); 90 | } 91 | 92 | Future test_incr_fakecas_multiple(int n) { 93 | Queue q = Queue(); 94 | for (int i = 0; i < n; ++i) { 95 | q.add(test_incr_fakecas()); 96 | } 97 | return Future.wait(q); 98 | } 99 | -------------------------------------------------------------------------------- /test/unicode_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:redis/redis.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | void main() { 7 | group("Test Unicode Support", () { 8 | test("Set and Get Unicode Value", () async { 9 | Command cmd = await generate_connect(); 10 | String unicodeString = "中华人民共和😊👍📱😀😬"; 11 | 12 | expect(cmd.send_object(["SET", "unicode_test", unicodeString]), 13 | completion(equals("OK"))); 14 | 15 | expect(cmd.send_object(["GET", "unicode_test"]), 16 | completion(equals(unicodeString))); 17 | }); 18 | }); 19 | } 20 | --------------------------------------------------------------------------------