├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── composer.json ├── composer.lock ├── docker-compose.yml ├── package-lock.json ├── package.json ├── phpstan.neon ├── phpunit.xml ├── psalm.xml ├── src ├── Exceptions │ ├── MessageToLongException.php │ ├── QueueAlreadyExistsException.php │ ├── QueueNotFoundException.php │ └── QueueParametersValidationException.php ├── ExecutorInterface.php ├── Message.php ├── QueueAttributes.php ├── QueueWorker.php ├── RSMQClient.php ├── RSMQClientInterface.php ├── WorkerSleepProvider.php ├── functions.php └── functions_include.php └── tests ├── FunctionsTest.php ├── QueueWorkerTest.php └── RSMQTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | coverage/ 4 | .phpunit.result.cache 5 | coverage.xml 6 | clover.xml 7 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | services: 3 | - redis 4 | 5 | php: 6 | - 7.4 7 | - 8.0 8 | - nightly 9 | 10 | matrix: 11 | allow_failures: 12 | - php: nightly 13 | fast_finish: true 14 | 15 | install: 16 | - composer install -n 17 | 18 | script: 19 | - composer test -- --coverage-clover=clover.xml 20 | - composer phpstan 21 | - composer psalm 22 | 23 | cache: 24 | directories: 25 | - $HOME/.composer/cache/files 26 | 27 | after_success: 28 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 eislambey 4 | Copyright (c) 2020 Andrew Breksa 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redis Simple Message Queue 2 | -------------------------- 3 | [![Build Status](https://travis-ci.com/abreksa4/php-rsmq.svg?branch=master)](https://travis-ci.com/abreksa4/php-rsmq) 4 | [![codecov](https://codecov.io/gh/abreksa4/php-rsmq/branch/master/graph/badge.svg)](https://codecov.io/gh/abreksa4/php-rsmq) 5 | [![License](https://poser.pugx.org/andrewbreksa/rsmq/license)](//packagist.org/packages/andrewbreksa/rsmq) 6 | [![GitHub issues](https://img.shields.io/github/issues/abreksa4/php-rsmq)](https://github.com/abreksa4/php-rsmq/issues) 7 | [![Latest Stable Version](https://poser.pugx.org/andrewbreksa/rsmq/v)](//packagist.org/packages/andrewbreksa/rsmq) 8 | [![Latest Unstable Version](https://poser.pugx.org/andrewbreksa/rsmq/v/unstable)](//packagist.org/packages/andrewbreksa/rsmq) 9 | [![composer.lock](https://poser.pugx.org/andrewbreksa/rsmq/composerlock)](//packagist.org/packages/andrewbreksa/rsmq) 10 | [![Total Downloads](https://poser.pugx.org/andrewbreksa/rsmq/downloads)](//packagist.org/packages/andrewbreksa/rsmq) 11 | [![GitHub stars](https://img.shields.io/github/stars/abreksa4/php-rsmq)](https://github.com/abreksa4/php-rsmq/stargazers) 12 | [![Dependents](https://poser.pugx.org/andrewbreksa/rsmq/dependents)](//packagist.org/packages/andrewbreksa/rsmq) 13 | 14 | A lightweight message queue for PHP that requires no dedicated queue server. Just a Redis server. See 15 | [smrchy/rsmq](https://github.com/smrchy/rsmq) for more information. 16 | 17 | This is a fork of [eislambey/php-rsmq](https://github.com/eislambey/php-rsmq) with the following changes: 18 | 19 | - Uses [predis](https://github.com/nrk/predis) instead of the Redis extension 20 | - Has some OO wrappers for QueueAttributes and Message 21 | - Provides a simple [QueueWorker](./src/QueueWorker.php) 22 | 23 | # Table of Contents 24 | 25 | 26 | 27 | - [Installation](#installation) 28 | - [Methods](#methods) 29 | * [Construct](#construct) 30 | * [Queue](#queue) 31 | + [createQueue](#createqueue) 32 | + [listQueues](#listqueues) 33 | + [deleteQueue](#deletequeue) 34 | + [getQueueAttributes](#getqueueattributes) 35 | + [setQueueAttributes](#setqueueattributes) 36 | * [Messages](#messages) 37 | + [sendMessage](#sendmessage) 38 | + [receiveMessage](#receivemessage) 39 | + [deleteMessage](#deletemessage) 40 | + [popMessage](#popmessage) 41 | + [changeMessageVisibility](#changemessagevisibility) 42 | * [Realtime](#realtime) 43 | - [QueueWorker](#queueworker) 44 | - [LICENSE](#license) 45 | 46 | 47 | 48 | # Installation 49 | 50 | composer require andrewbreksa/rsmq 51 | 52 | # Methods 53 | 54 | ## Construct 55 | 56 | Creates a new instance of RSMQ. 57 | 58 | Parameters: 59 | 60 | * `$predis` (\Predis\ClientInterface): *required The Predis instance 61 | * `$ns` (string): *optional (Default: "rsmq")* The namespace prefix used for all keys created by RSMQ 62 | * `$realtime` (Boolean): *optional (Default: false)* Enable realtime PUBLISH of new messages 63 | 64 | Example: 65 | 66 | ```php 67 | '127.0.0.1', 74 | 'port' => 6379 75 | ] 76 | ); 77 | $this->rsmq = new RSMQClient($predis); 78 | ``` 79 | 80 | ## Queue 81 | 82 | ### createQueue 83 | 84 | Create a new queue. 85 | 86 | Parameters: 87 | 88 | * `$name` (string): The Queue name. Maximum 160 characters; alphanumeric characters, hyphens (-), and underscores (_) 89 | are allowed. 90 | * `$vt` (int): *optional* *(Default: 30)* The length of time, in seconds, that a message received from a queue will be 91 | invisible to other receiving components when they ask to receive messages. Allowed values: 0-9999999 (around 115 days) 92 | * `$delay` (int): *optional* *(Default: 0)* The time in seconds that the delivery of all new messages in the queue will 93 | be delayed. Allowed values: 0-9999999 (around 115 days) 94 | * `$maxsize` (int): *optional* *(Default: 65536)* The maximum message size in bytes. Allowed values: 1024-65536 and -1 95 | (for unlimited size) 96 | 97 | Returns: 98 | 99 | * `true` (Bool) 100 | 101 | Throws: 102 | 103 | * `\AndrewBreksa\RSMQ\Exceptions\QueueAlreadyExistsException` 104 | 105 | Example: 106 | 107 | ```php 108 | createQueue('myqueue'); 114 | ``` 115 | 116 | ### listQueues 117 | 118 | List all queues 119 | 120 | Returns an array: 121 | 122 | * `["qname1", "qname2"]` 123 | 124 | Example: 125 | 126 | ```php 127 | listQueues(); 133 | ``` 134 | 135 | ### deleteQueue 136 | 137 | Deletes a queue and all messages. 138 | 139 | Parameters: 140 | 141 | * `$name` (string): The Queue name. 142 | 143 | Returns: 144 | 145 | * `true` (Bool) 146 | 147 | Throws: 148 | 149 | * `\AndrewBreksa\RSMQ\Exceptions\QueueNotFoundException` 150 | 151 | Example: 152 | 153 | ```php 154 | deleteQueue('myqueue'); 160 | ``` 161 | 162 | ### getQueueAttributes 163 | 164 | Get queue attributes, counter and stats 165 | 166 | Parameters: 167 | 168 | * `$queue` (string): The Queue name. 169 | 170 | Returns a `\AndrewBreksa\RSMQ\QueueAttributes` object with the following properties: 171 | 172 | * `vt` (int): The visibility timeout for the queue in seconds 173 | * `delay` (int): The delay for new messages in seconds 174 | * `maxSize` (int): The maximum size of a message in bytes 175 | * `totalReceived` (int): Total number of messages received from the queue 176 | * `totalSent` (int): Total number of messages sent to the queue 177 | * `created` (float): Timestamp (epoch in seconds) when the queue was created 178 | * `modified` (float): Timestamp (epoch in seconds) when the queue was last modified with `setQueueAttributes` 179 | * `messageCount` (int): Current number of messages in the queue 180 | * `hiddenMessageCount` (int): Current number of hidden / not visible messages. A message can be hidden while "in flight" 181 | due to a `vt` parameter or when sent with a `delay` 182 | 183 | Example: 184 | 185 | ```php 186 | getQueueAttributes('myqueue'); 192 | echo "visibility timeout: ", $attributes->getVt(), "\n"; 193 | echo "delay for new messages: ", $attributes->getDelay(), "\n"; 194 | echo "max size in bytes: ", $attributes->getMaxSize(), "\n"; 195 | echo "total received messages: ", $attributes->getTotalReceived(), "\n"; 196 | echo "total sent messages: ", $attributes->getTotalSent(), "\n"; 197 | echo "created: ", $attributes->getCreated(), "\n"; 198 | echo "last modified: ", $attributes->getModified(), "\n"; 199 | echo "current n of messages: ", $attributes->getMessageCount(), "\n"; 200 | echo "hidden messages: ", $attributes->getHiddenMessageCount(), "\n"; 201 | ``` 202 | 203 | ### setQueueAttributes 204 | 205 | Sets queue parameters. 206 | 207 | Parameters: 208 | 209 | * `$queue` (string): The Queue name. 210 | * `$vt` (int): *optional* * The length of time, in seconds, that a message received from a queue will be invisible to 211 | other receiving components when they ask to receive messages. Allowed values: 0-9999999 (around 115 days) 212 | * `$delay` (int): *optional* The time in seconds that the delivery of all new messages in the queue will be delayed. 213 | Allowed values: 0-9999999 (around 115 days) 214 | * `$maxsize` (int): *optional* The maximum message size in bytes. Allowed values: 1024-65536 and -1 (for unlimited size) 215 | 216 | Note: At least one attribute (vt, delay, maxsize) must be supplied. Only attributes that are supplied will be modified. 217 | 218 | Returns a `\AndrewBreksa\RSMQ\QueueAttributes` object with the following properties: 219 | 220 | * `vt` (int): The visibility timeout for the queue in seconds 221 | * `delay` (int): The delay for new messages in seconds 222 | * `maxSize` (int): The maximum size of a message in bytes 223 | * `totalReceived` (int): Total number of messages received from the queue 224 | * `totalSent` (int): Total number of messages sent to the queue 225 | * `created` (float): Timestamp (epoch in seconds) when the queue was created 226 | * `modified` (float): Timestamp (epoch in seconds) when the queue was last modified with `setQueueAttributes` 227 | * `messageCount` (int): Current number of messages in the queue 228 | * `hiddenMessageCount` (int): Current number of hidden / not visible messages. A message can be hidden while "in flight" 229 | due to a `vt` parameter or when sent with a `delay` 230 | 231 | Throws: 232 | 233 | * `\AndrewBreksa\RSMQ\QueueAttributes` 234 | * `\AndrewBreksa\RSMQ\QueueParametersValidationException` 235 | * `\AndrewBreksa\RSMQ\QueueNotFoundException` 236 | 237 | Example: 238 | 239 | ```php 240 | setQueueAttributes($queue, $vt, $delay, $maxsize); 250 | ``` 251 | 252 | ## Messages 253 | 254 | ### sendMessage 255 | 256 | Sends a new message. 257 | 258 | Parameters: 259 | 260 | * `$queue` (string) 261 | * `$message` (string) 262 | * `$delay` (int): *optional* *(Default: queue settings)* The time in seconds that the delivery of the message will be 263 | delayed. Allowed values: 0-9999999 (around 115 days) 264 | 265 | Returns: 266 | 267 | * `$id` (string): The internal message id. 268 | 269 | Throws: 270 | 271 | * `\AndrewBreksa\RSMQ\Exceptions\MessageToLongException` 272 | * `\AndrewBreksa\RSMQ\Exceptions\QueueNotFoundException` 273 | * `\AndrewBreksa\RSMQ\Exceptions\QueueParametersValidationException` 274 | 275 | Example: 276 | 277 | ```php 278 | sendMessage('myqueue', 'a message'); 284 | echo "Message Sent. ID: ", $id; 285 | ``` 286 | 287 | ### receiveMessage 288 | 289 | Receive the next message from the queue. 290 | 291 | Parameters: 292 | 293 | * `$queue` (string): The Queue name. 294 | * `$vt` (int): *optional* *(Default: queue settings)* The length of time, in seconds, that the received message will be 295 | invisible to others. Allowed values: 0-9999999 (around 115 days) 296 | 297 | Returns a `\AndrewBreksa\RSMQ\Message` object with the following properties: 298 | 299 | * `message` (string): The message's contents. 300 | * `id` (string): The internal message id. 301 | * `sent` (int): Timestamp of when this message was sent / created. 302 | * `firstReceived` (int): Timestamp of when this message was first received. 303 | * `receiveCount` (int): Number of times this message was received. 304 | 305 | Note: Will return an empty array if no message is there 306 | 307 | Throws: 308 | 309 | * `\AndrewBreksa\RSMQ\Exceptions\QueueNotFoundException` 310 | * `\AndrewBreksa\RSMQ\Exceptions\QueueParametersValidationException` 311 | 312 | Example: 313 | 314 | ```php 315 | receiveMessage('myqueue'); 321 | echo "Message ID: ", $message->getId(); 322 | echo "Message: ", $message->getMessage(); 323 | ``` 324 | 325 | ### deleteMessage 326 | 327 | Parameters: 328 | 329 | * `$queue` (string): The Queue name. 330 | * `$id` (string): message id to delete. 331 | 332 | Returns: 333 | 334 | * `true` if successful, `false` if the message was not found (bool). 335 | 336 | Throws: 337 | 338 | * `\AndrewBreksa\RSMQ\Exceptions\QueueParametersValidationException` 339 | 340 | Example: 341 | 342 | ```php 343 | sendMessage('queue', 'a message'); 349 | $rsmq->deleteMessage('queue', $id); 350 | ``` 351 | 352 | ### popMessage 353 | 354 | Receive the next message from the queue **and delete it**. 355 | 356 | **Important:** This method deletes the message it receives right away. There is no way to receive the message again if 357 | something goes wrong while working on the message. 358 | 359 | Parameters: 360 | 361 | * `$queue` (string): The Queue name. 362 | 363 | Returns a `\AndrewBreksa\RSMQ\Message` object with the following properties: 364 | 365 | * `message` (string): The message's contents. 366 | * `id` (string): The internal message id. 367 | * `sent` (int): Timestamp of when this message was sent / created. 368 | * `firstReceived` (int): Timestamp of when this message was first received. 369 | * `receiveCount` (int): Number of times this message was received. 370 | 371 | Note: Will return an empty object if no message is there 372 | 373 | Throws: 374 | 375 | * `\AndrewBreksa\RSMQ\Exceptions\QueueNotFoundException` 376 | * `\AndrewBreksa\RSMQ\Exceptions\QueueParametersValidationException` 377 | 378 | Example: 379 | 380 | ```php 381 | popMessage('myqueue'); 387 | echo "Message ID: ", $message->getId(); 388 | echo "Message: ", $message->getMessage(); 389 | ``` 390 | 391 | ### changeMessageVisibility 392 | 393 | Change the visibility timer of a single message. The time when the message will be visible again is calculated from the 394 | current time (now) + `vt`. 395 | 396 | Parameters: 397 | 398 | * `qname` (string): The Queue name. 399 | * `id` (string): The message id. 400 | * `vt` (int): The length of time, in seconds, that this message will not be visible. Allowed values: 0-9999999 (around 401 | 115 days) 402 | 403 | Returns: 404 | 405 | * `true` if successful, `false` if the message was not found (bool). 406 | 407 | Throws: 408 | 409 | * `\AndrewBreksa\RSMQ\Exceptions\QueueParametersValidationException` 410 | * `\AndrewBreksa\RSMQ\Exceptions\QueueNotFoundException` 411 | 412 | Example: 413 | 414 | ```php 415 | sendMessage($queue, 'a message'); 422 | if($rsmq->changeMessageVisibility($queue, $id, 60)) { 423 | echo "Message hidden for 60 secs"; 424 | } 425 | ``` 426 | 427 | ## Realtime 428 | 429 | When creating an instance of `AndrewBreksa\RSMQ\RSMQClient`, you can enable the realtime `PUBLISH` for new messages by 430 | passing `true` for the `$realtime` argument of `\AndrewBreksa\RSMQ\RSMQClient::__construct`. On every new message that 431 | is sent via `sendMessage`, a Redis `PUBLISH` will be issued to `{rsmq.ns}:rt:{qname}`. 432 | 433 | Example for RSMQ with default settings: 434 | 435 | * The queue `testQueue` already contains 5 messages. 436 | * A new message is being sent to the queue `testQueue`. 437 | * The following Redis command will be issued: `PUBLISH rsmq:rt:testQueue 6` 438 | 439 | The realtime option enables sending a `PUBLISH` when a new message is sent to RSMQ, however no further functionality is 440 | built on this feature. Your app could use the Redis `SUBSCRIBE` command to be notified of new messages and then attempt 441 | to poll from the queue, however due to how the Redis pub/sub system works, 442 | [all listeners will be notified of the new message](https://redis.io/docs/manual/pubsub/), this method doesn't lend 443 | itself to driving message handling in environments with more than one subscribed process. 444 | 445 | # QueueWorker 446 | 447 | The QueueWorker class provides an easy way to consume RSMQ messages, to use it: 448 | 449 | ```php 450 | work(); // here we can optionally pass true to only process one message 481 | ``` 482 | 483 | # LICENSE 484 | 485 | The MIT LICENSE. See [LICENSE](./LICENSE) 486 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "andrewbreksa/rsmq", 3 | "description": "Redis Simple Message Queue.", 4 | "type": "library", 5 | "license": "MIT", 6 | "version": "2.0.2", 7 | "authors": [ 8 | { 9 | "name": "emre can islambey", 10 | "email": "eislambey@gmail.com" 11 | }, 12 | { 13 | "name": "Andrew Breksa", 14 | "email": "andrew@andrewbreksa.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.4|^8.0", 19 | "ext-mbstring": "*", 20 | "predis/predis": "^1.1" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "AndrewBreksa\\RSMQ\\": "src/" 25 | }, 26 | "files": [ 27 | "./src/functions_include.php" 28 | ] 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^8.3 || ^9.0", 32 | "phpstan/phpstan": "^0.11.12 || ^0.12.0 || ^1.0.0", 33 | "roave/security-advisories": "dev-master", 34 | "vimeo/psalm": "^3.12 || ^4.0", 35 | "squizlabs/php_codesniffer": "^3.5", 36 | "mockery/mockery": "^1.3" 37 | }, 38 | "autoload-dev": { 39 | "classmap": [ 40 | "tests/" 41 | ] 42 | }, 43 | "scripts": { 44 | "test": "XDEBUG_MODE=coverage phpunit", 45 | "local-test": "XDEBUG_MODE=coverage php ./vendor/bin/phpunit --coverage-text --coverage-clover coverage.xml --coverage-html ./coverage", 46 | "phpstan": "phpstan analyse -c phpstan.neon", 47 | "cbf": "php ./vendor/bin/phpcbf tests src", 48 | "psalm": "php ./vendor/bin/psalm --show-info=true", 49 | "toc": "node ./node_modules/markdown-toc/cli.js README.md -i", 50 | "all-the-things": [ 51 | "composer local-test", 52 | "composer phpstan", 53 | "composer cbf", 54 | "composer psalm", 55 | "composer toc" 56 | ] 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true 60 | } 61 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | redis: 4 | image: redis:4 5 | ports: 6 | - 6379:6379 -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-rsmq", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "markdown-toc": "^1.2.0" 9 | } 10 | }, 11 | "node_modules/ansi-red": { 12 | "version": "0.1.1", 13 | "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", 14 | "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", 15 | "dev": true, 16 | "dependencies": { 17 | "ansi-wrap": "0.1.0" 18 | }, 19 | "engines": { 20 | "node": ">=0.10.0" 21 | } 22 | }, 23 | "node_modules/ansi-wrap": { 24 | "version": "0.1.0", 25 | "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", 26 | "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", 27 | "dev": true, 28 | "engines": { 29 | "node": ">=0.10.0" 30 | } 31 | }, 32 | "node_modules/argparse": { 33 | "version": "1.0.10", 34 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 35 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 36 | "dev": true, 37 | "dependencies": { 38 | "sprintf-js": "~1.0.2" 39 | } 40 | }, 41 | "node_modules/autolinker": { 42 | "version": "0.28.1", 43 | "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", 44 | "integrity": "sha1-BlK0kYgYefB3XazgzcoyM5QqTkc=", 45 | "dev": true, 46 | "dependencies": { 47 | "gulp-header": "^1.7.1" 48 | } 49 | }, 50 | "node_modules/buffer-from": { 51 | "version": "1.1.1", 52 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 53 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 54 | "dev": true 55 | }, 56 | "node_modules/coffee-script": { 57 | "version": "1.12.7", 58 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 59 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 60 | "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", 61 | "dev": true, 62 | "bin": { 63 | "cake": "bin/cake", 64 | "coffee": "bin/coffee" 65 | }, 66 | "engines": { 67 | "node": ">=0.8.0" 68 | } 69 | }, 70 | "node_modules/concat-stream": { 71 | "version": "1.6.2", 72 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 73 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 74 | "dev": true, 75 | "engines": [ 76 | "node >= 0.8" 77 | ], 78 | "dependencies": { 79 | "buffer-from": "^1.0.0", 80 | "inherits": "^2.0.3", 81 | "readable-stream": "^2.2.2", 82 | "typedarray": "^0.0.6" 83 | } 84 | }, 85 | "node_modules/concat-with-sourcemaps": { 86 | "version": "1.1.0", 87 | "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", 88 | "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", 89 | "dev": true, 90 | "dependencies": { 91 | "source-map": "^0.6.1" 92 | } 93 | }, 94 | "node_modules/core-util-is": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 97 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 98 | "dev": true 99 | }, 100 | "node_modules/diacritics-map": { 101 | "version": "0.1.0", 102 | "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", 103 | "integrity": "sha1-bfwP+dAQAKLt8oZTccrDFulJd68=", 104 | "dev": true, 105 | "engines": { 106 | "node": ">=0.8.0" 107 | } 108 | }, 109 | "node_modules/esprima": { 110 | "version": "4.0.1", 111 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 112 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 113 | "dev": true, 114 | "bin": { 115 | "esparse": "bin/esparse.js", 116 | "esvalidate": "bin/esvalidate.js" 117 | }, 118 | "engines": { 119 | "node": ">=4" 120 | } 121 | }, 122 | "node_modules/expand-range": { 123 | "version": "1.8.2", 124 | "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", 125 | "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", 126 | "dev": true, 127 | "dependencies": { 128 | "fill-range": "^2.1.0" 129 | }, 130 | "engines": { 131 | "node": ">=0.10.0" 132 | } 133 | }, 134 | "node_modules/extend-shallow": { 135 | "version": "2.0.1", 136 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 137 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 138 | "dev": true, 139 | "dependencies": { 140 | "is-extendable": "^0.1.0" 141 | }, 142 | "engines": { 143 | "node": ">=0.10.0" 144 | } 145 | }, 146 | "node_modules/fill-range": { 147 | "version": "2.2.4", 148 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", 149 | "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", 150 | "dev": true, 151 | "dependencies": { 152 | "is-number": "^2.1.0", 153 | "isobject": "^2.0.0", 154 | "randomatic": "^3.0.0", 155 | "repeat-element": "^1.1.2", 156 | "repeat-string": "^1.5.2" 157 | }, 158 | "engines": { 159 | "node": ">=0.10.0" 160 | } 161 | }, 162 | "node_modules/for-in": { 163 | "version": "1.0.2", 164 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 165 | "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", 166 | "dev": true, 167 | "engines": { 168 | "node": ">=0.10.0" 169 | } 170 | }, 171 | "node_modules/gray-matter": { 172 | "version": "2.1.1", 173 | "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", 174 | "integrity": "sha1-MELZrewqHe1qdwep7SOA+KF6Qw4=", 175 | "dev": true, 176 | "dependencies": { 177 | "ansi-red": "^0.1.1", 178 | "coffee-script": "^1.12.4", 179 | "extend-shallow": "^2.0.1", 180 | "js-yaml": "^3.8.1", 181 | "toml": "^2.3.2" 182 | }, 183 | "engines": { 184 | "node": ">=0.10.0" 185 | } 186 | }, 187 | "node_modules/gulp-header": { 188 | "version": "1.8.12", 189 | "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", 190 | "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", 191 | "deprecated": "Removed event-stream from gulp-header", 192 | "dev": true, 193 | "dependencies": { 194 | "concat-with-sourcemaps": "*", 195 | "lodash.template": "^4.4.0", 196 | "through2": "^2.0.0" 197 | } 198 | }, 199 | "node_modules/inherits": { 200 | "version": "2.0.4", 201 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 202 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 203 | "dev": true 204 | }, 205 | "node_modules/is-buffer": { 206 | "version": "1.1.6", 207 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 208 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", 209 | "dev": true 210 | }, 211 | "node_modules/is-extendable": { 212 | "version": "0.1.1", 213 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 214 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", 215 | "dev": true, 216 | "engines": { 217 | "node": ">=0.10.0" 218 | } 219 | }, 220 | "node_modules/is-number": { 221 | "version": "2.1.0", 222 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", 223 | "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", 224 | "dev": true, 225 | "dependencies": { 226 | "kind-of": "^3.0.2" 227 | }, 228 | "engines": { 229 | "node": ">=0.10.0" 230 | } 231 | }, 232 | "node_modules/is-plain-object": { 233 | "version": "2.0.4", 234 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 235 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 236 | "dev": true, 237 | "dependencies": { 238 | "isobject": "^3.0.1" 239 | }, 240 | "engines": { 241 | "node": ">=0.10.0" 242 | } 243 | }, 244 | "node_modules/is-plain-object/node_modules/isobject": { 245 | "version": "3.0.1", 246 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 247 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 248 | "dev": true, 249 | "engines": { 250 | "node": ">=0.10.0" 251 | } 252 | }, 253 | "node_modules/isarray": { 254 | "version": "1.0.0", 255 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 256 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 257 | "dev": true 258 | }, 259 | "node_modules/isobject": { 260 | "version": "2.1.0", 261 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 262 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 263 | "dev": true, 264 | "dependencies": { 265 | "isarray": "1.0.0" 266 | }, 267 | "engines": { 268 | "node": ">=0.10.0" 269 | } 270 | }, 271 | "node_modules/js-yaml": { 272 | "version": "3.14.0", 273 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 274 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 275 | "dev": true, 276 | "dependencies": { 277 | "argparse": "^1.0.7", 278 | "esprima": "^4.0.0" 279 | }, 280 | "bin": { 281 | "js-yaml": "bin/js-yaml.js" 282 | } 283 | }, 284 | "node_modules/kind-of": { 285 | "version": "3.2.2", 286 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 287 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 288 | "dev": true, 289 | "dependencies": { 290 | "is-buffer": "^1.1.5" 291 | }, 292 | "engines": { 293 | "node": ">=0.10.0" 294 | } 295 | }, 296 | "node_modules/lazy-cache": { 297 | "version": "2.0.2", 298 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", 299 | "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", 300 | "dev": true, 301 | "dependencies": { 302 | "set-getter": "^0.1.0" 303 | }, 304 | "engines": { 305 | "node": ">=0.10.0" 306 | } 307 | }, 308 | "node_modules/list-item": { 309 | "version": "1.1.1", 310 | "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", 311 | "integrity": "sha1-DGXQDih8tmPMs8s4Sad+iewmilY=", 312 | "dev": true, 313 | "dependencies": { 314 | "expand-range": "^1.8.1", 315 | "extend-shallow": "^2.0.1", 316 | "is-number": "^2.1.0", 317 | "repeat-string": "^1.5.2" 318 | }, 319 | "engines": { 320 | "node": ">=0.10.0" 321 | } 322 | }, 323 | "node_modules/lodash._reinterpolate": { 324 | "version": "3.0.0", 325 | "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", 326 | "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", 327 | "dev": true 328 | }, 329 | "node_modules/lodash.template": { 330 | "version": "4.5.0", 331 | "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", 332 | "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", 333 | "dev": true, 334 | "dependencies": { 335 | "lodash._reinterpolate": "^3.0.0", 336 | "lodash.templatesettings": "^4.0.0" 337 | } 338 | }, 339 | "node_modules/lodash.templatesettings": { 340 | "version": "4.2.0", 341 | "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", 342 | "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", 343 | "dev": true, 344 | "dependencies": { 345 | "lodash._reinterpolate": "^3.0.0" 346 | } 347 | }, 348 | "node_modules/markdown-link": { 349 | "version": "0.1.1", 350 | "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", 351 | "integrity": "sha1-MsXGUZmmRXMWMi0eQinRNAfIx88=", 352 | "dev": true, 353 | "engines": { 354 | "node": ">=0.10.0" 355 | } 356 | }, 357 | "node_modules/markdown-toc": { 358 | "version": "1.2.0", 359 | "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz", 360 | "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==", 361 | "dev": true, 362 | "dependencies": { 363 | "concat-stream": "^1.5.2", 364 | "diacritics-map": "^0.1.0", 365 | "gray-matter": "^2.1.0", 366 | "lazy-cache": "^2.0.2", 367 | "list-item": "^1.1.1", 368 | "markdown-link": "^0.1.1", 369 | "minimist": "^1.2.0", 370 | "mixin-deep": "^1.1.3", 371 | "object.pick": "^1.2.0", 372 | "remarkable": "^1.7.1", 373 | "repeat-string": "^1.6.1", 374 | "strip-color": "^0.1.0" 375 | }, 376 | "bin": { 377 | "markdown-toc": "cli.js" 378 | }, 379 | "engines": { 380 | "node": ">=0.10.0" 381 | } 382 | }, 383 | "node_modules/math-random": { 384 | "version": "1.0.4", 385 | "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", 386 | "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", 387 | "dev": true 388 | }, 389 | "node_modules/minimist": { 390 | "version": "1.2.6", 391 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 392 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", 393 | "dev": true 394 | }, 395 | "node_modules/mixin-deep": { 396 | "version": "1.3.2", 397 | "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", 398 | "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", 399 | "dev": true, 400 | "dependencies": { 401 | "for-in": "^1.0.2", 402 | "is-extendable": "^1.0.1" 403 | }, 404 | "engines": { 405 | "node": ">=0.10.0" 406 | } 407 | }, 408 | "node_modules/mixin-deep/node_modules/is-extendable": { 409 | "version": "1.0.1", 410 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", 411 | "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", 412 | "dev": true, 413 | "dependencies": { 414 | "is-plain-object": "^2.0.4" 415 | }, 416 | "engines": { 417 | "node": ">=0.10.0" 418 | } 419 | }, 420 | "node_modules/object.pick": { 421 | "version": "1.3.0", 422 | "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", 423 | "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", 424 | "dev": true, 425 | "dependencies": { 426 | "isobject": "^3.0.1" 427 | }, 428 | "engines": { 429 | "node": ">=0.10.0" 430 | } 431 | }, 432 | "node_modules/object.pick/node_modules/isobject": { 433 | "version": "3.0.1", 434 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 435 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 436 | "dev": true, 437 | "engines": { 438 | "node": ">=0.10.0" 439 | } 440 | }, 441 | "node_modules/process-nextick-args": { 442 | "version": "2.0.1", 443 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 444 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 445 | "dev": true 446 | }, 447 | "node_modules/randomatic": { 448 | "version": "3.1.1", 449 | "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", 450 | "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", 451 | "dev": true, 452 | "dependencies": { 453 | "is-number": "^4.0.0", 454 | "kind-of": "^6.0.0", 455 | "math-random": "^1.0.1" 456 | }, 457 | "engines": { 458 | "node": ">= 0.10.0" 459 | } 460 | }, 461 | "node_modules/randomatic/node_modules/is-number": { 462 | "version": "4.0.0", 463 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", 464 | "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", 465 | "dev": true, 466 | "engines": { 467 | "node": ">=0.10.0" 468 | } 469 | }, 470 | "node_modules/randomatic/node_modules/kind-of": { 471 | "version": "6.0.3", 472 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 473 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 474 | "dev": true, 475 | "engines": { 476 | "node": ">=0.10.0" 477 | } 478 | }, 479 | "node_modules/readable-stream": { 480 | "version": "2.3.7", 481 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 482 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 483 | "dev": true, 484 | "dependencies": { 485 | "core-util-is": "~1.0.0", 486 | "inherits": "~2.0.3", 487 | "isarray": "~1.0.0", 488 | "process-nextick-args": "~2.0.0", 489 | "safe-buffer": "~5.1.1", 490 | "string_decoder": "~1.1.1", 491 | "util-deprecate": "~1.0.1" 492 | } 493 | }, 494 | "node_modules/remarkable": { 495 | "version": "1.7.4", 496 | "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", 497 | "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", 498 | "dev": true, 499 | "dependencies": { 500 | "argparse": "^1.0.10", 501 | "autolinker": "~0.28.0" 502 | }, 503 | "bin": { 504 | "remarkable": "bin/remarkable.js" 505 | }, 506 | "engines": { 507 | "node": ">= 0.10.0" 508 | } 509 | }, 510 | "node_modules/repeat-element": { 511 | "version": "1.1.3", 512 | "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", 513 | "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", 514 | "dev": true, 515 | "engines": { 516 | "node": ">=0.10.0" 517 | } 518 | }, 519 | "node_modules/repeat-string": { 520 | "version": "1.6.1", 521 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 522 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", 523 | "dev": true, 524 | "engines": { 525 | "node": ">=0.10" 526 | } 527 | }, 528 | "node_modules/safe-buffer": { 529 | "version": "5.1.2", 530 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 531 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 532 | "dev": true 533 | }, 534 | "node_modules/set-getter": { 535 | "version": "0.1.1", 536 | "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", 537 | "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", 538 | "dev": true, 539 | "dependencies": { 540 | "to-object-path": "^0.3.0" 541 | }, 542 | "engines": { 543 | "node": ">=0.10.0" 544 | } 545 | }, 546 | "node_modules/source-map": { 547 | "version": "0.6.1", 548 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 549 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 550 | "dev": true, 551 | "engines": { 552 | "node": ">=0.10.0" 553 | } 554 | }, 555 | "node_modules/sprintf-js": { 556 | "version": "1.0.3", 557 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 558 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 559 | "dev": true 560 | }, 561 | "node_modules/string_decoder": { 562 | "version": "1.1.1", 563 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 564 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 565 | "dev": true, 566 | "dependencies": { 567 | "safe-buffer": "~5.1.0" 568 | } 569 | }, 570 | "node_modules/strip-color": { 571 | "version": "0.1.0", 572 | "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", 573 | "integrity": "sha1-EG9l09PmotlAHKwOsM6LinArT3s=", 574 | "dev": true, 575 | "engines": { 576 | "node": ">=0.10.0" 577 | } 578 | }, 579 | "node_modules/through2": { 580 | "version": "2.0.5", 581 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", 582 | "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", 583 | "dev": true, 584 | "dependencies": { 585 | "readable-stream": "~2.3.6", 586 | "xtend": "~4.0.1" 587 | } 588 | }, 589 | "node_modules/to-object-path": { 590 | "version": "0.3.0", 591 | "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", 592 | "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", 593 | "dev": true, 594 | "dependencies": { 595 | "kind-of": "^3.0.2" 596 | }, 597 | "engines": { 598 | "node": ">=0.10.0" 599 | } 600 | }, 601 | "node_modules/toml": { 602 | "version": "2.3.6", 603 | "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz", 604 | "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==", 605 | "dev": true 606 | }, 607 | "node_modules/typedarray": { 608 | "version": "0.0.6", 609 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 610 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 611 | "dev": true 612 | }, 613 | "node_modules/util-deprecate": { 614 | "version": "1.0.2", 615 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 616 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 617 | "dev": true 618 | }, 619 | "node_modules/xtend": { 620 | "version": "4.0.2", 621 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 622 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 623 | "dev": true, 624 | "engines": { 625 | "node": ">=0.4" 626 | } 627 | } 628 | }, 629 | "dependencies": { 630 | "ansi-red": { 631 | "version": "0.1.1", 632 | "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", 633 | "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", 634 | "dev": true, 635 | "requires": { 636 | "ansi-wrap": "0.1.0" 637 | } 638 | }, 639 | "ansi-wrap": { 640 | "version": "0.1.0", 641 | "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", 642 | "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", 643 | "dev": true 644 | }, 645 | "argparse": { 646 | "version": "1.0.10", 647 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 648 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 649 | "dev": true, 650 | "requires": { 651 | "sprintf-js": "~1.0.2" 652 | } 653 | }, 654 | "autolinker": { 655 | "version": "0.28.1", 656 | "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", 657 | "integrity": "sha1-BlK0kYgYefB3XazgzcoyM5QqTkc=", 658 | "dev": true, 659 | "requires": { 660 | "gulp-header": "^1.7.1" 661 | } 662 | }, 663 | "buffer-from": { 664 | "version": "1.1.1", 665 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 666 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 667 | "dev": true 668 | }, 669 | "coffee-script": { 670 | "version": "1.12.7", 671 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 672 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 673 | "dev": true 674 | }, 675 | "concat-stream": { 676 | "version": "1.6.2", 677 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 678 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 679 | "dev": true, 680 | "requires": { 681 | "buffer-from": "^1.0.0", 682 | "inherits": "^2.0.3", 683 | "readable-stream": "^2.2.2", 684 | "typedarray": "^0.0.6" 685 | } 686 | }, 687 | "concat-with-sourcemaps": { 688 | "version": "1.1.0", 689 | "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", 690 | "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", 691 | "dev": true, 692 | "requires": { 693 | "source-map": "^0.6.1" 694 | } 695 | }, 696 | "core-util-is": { 697 | "version": "1.0.2", 698 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 699 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 700 | "dev": true 701 | }, 702 | "diacritics-map": { 703 | "version": "0.1.0", 704 | "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", 705 | "integrity": "sha1-bfwP+dAQAKLt8oZTccrDFulJd68=", 706 | "dev": true 707 | }, 708 | "esprima": { 709 | "version": "4.0.1", 710 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 711 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 712 | "dev": true 713 | }, 714 | "expand-range": { 715 | "version": "1.8.2", 716 | "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", 717 | "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", 718 | "dev": true, 719 | "requires": { 720 | "fill-range": "^2.1.0" 721 | } 722 | }, 723 | "extend-shallow": { 724 | "version": "2.0.1", 725 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 726 | "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", 727 | "dev": true, 728 | "requires": { 729 | "is-extendable": "^0.1.0" 730 | } 731 | }, 732 | "fill-range": { 733 | "version": "2.2.4", 734 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", 735 | "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", 736 | "dev": true, 737 | "requires": { 738 | "is-number": "^2.1.0", 739 | "isobject": "^2.0.0", 740 | "randomatic": "^3.0.0", 741 | "repeat-element": "^1.1.2", 742 | "repeat-string": "^1.5.2" 743 | } 744 | }, 745 | "for-in": { 746 | "version": "1.0.2", 747 | "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", 748 | "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", 749 | "dev": true 750 | }, 751 | "gray-matter": { 752 | "version": "2.1.1", 753 | "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", 754 | "integrity": "sha1-MELZrewqHe1qdwep7SOA+KF6Qw4=", 755 | "dev": true, 756 | "requires": { 757 | "ansi-red": "^0.1.1", 758 | "coffee-script": "^1.12.4", 759 | "extend-shallow": "^2.0.1", 760 | "js-yaml": "^3.8.1", 761 | "toml": "^2.3.2" 762 | } 763 | }, 764 | "gulp-header": { 765 | "version": "1.8.12", 766 | "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", 767 | "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", 768 | "dev": true, 769 | "requires": { 770 | "concat-with-sourcemaps": "*", 771 | "lodash.template": "^4.4.0", 772 | "through2": "^2.0.0" 773 | } 774 | }, 775 | "inherits": { 776 | "version": "2.0.4", 777 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 778 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 779 | "dev": true 780 | }, 781 | "is-buffer": { 782 | "version": "1.1.6", 783 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 784 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", 785 | "dev": true 786 | }, 787 | "is-extendable": { 788 | "version": "0.1.1", 789 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 790 | "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", 791 | "dev": true 792 | }, 793 | "is-number": { 794 | "version": "2.1.0", 795 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", 796 | "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", 797 | "dev": true, 798 | "requires": { 799 | "kind-of": "^3.0.2" 800 | } 801 | }, 802 | "is-plain-object": { 803 | "version": "2.0.4", 804 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 805 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 806 | "dev": true, 807 | "requires": { 808 | "isobject": "^3.0.1" 809 | }, 810 | "dependencies": { 811 | "isobject": { 812 | "version": "3.0.1", 813 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 814 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 815 | "dev": true 816 | } 817 | } 818 | }, 819 | "isarray": { 820 | "version": "1.0.0", 821 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 822 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 823 | "dev": true 824 | }, 825 | "isobject": { 826 | "version": "2.1.0", 827 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", 828 | "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", 829 | "dev": true, 830 | "requires": { 831 | "isarray": "1.0.0" 832 | } 833 | }, 834 | "js-yaml": { 835 | "version": "3.14.0", 836 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 837 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 838 | "dev": true, 839 | "requires": { 840 | "argparse": "^1.0.7", 841 | "esprima": "^4.0.0" 842 | } 843 | }, 844 | "kind-of": { 845 | "version": "3.2.2", 846 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 847 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 848 | "dev": true, 849 | "requires": { 850 | "is-buffer": "^1.1.5" 851 | } 852 | }, 853 | "lazy-cache": { 854 | "version": "2.0.2", 855 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", 856 | "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", 857 | "dev": true, 858 | "requires": { 859 | "set-getter": "^0.1.0" 860 | } 861 | }, 862 | "list-item": { 863 | "version": "1.1.1", 864 | "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", 865 | "integrity": "sha1-DGXQDih8tmPMs8s4Sad+iewmilY=", 866 | "dev": true, 867 | "requires": { 868 | "expand-range": "^1.8.1", 869 | "extend-shallow": "^2.0.1", 870 | "is-number": "^2.1.0", 871 | "repeat-string": "^1.5.2" 872 | } 873 | }, 874 | "lodash._reinterpolate": { 875 | "version": "3.0.0", 876 | "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", 877 | "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", 878 | "dev": true 879 | }, 880 | "lodash.template": { 881 | "version": "4.5.0", 882 | "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", 883 | "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", 884 | "dev": true, 885 | "requires": { 886 | "lodash._reinterpolate": "^3.0.0", 887 | "lodash.templatesettings": "^4.0.0" 888 | } 889 | }, 890 | "lodash.templatesettings": { 891 | "version": "4.2.0", 892 | "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", 893 | "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", 894 | "dev": true, 895 | "requires": { 896 | "lodash._reinterpolate": "^3.0.0" 897 | } 898 | }, 899 | "markdown-link": { 900 | "version": "0.1.1", 901 | "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", 902 | "integrity": "sha1-MsXGUZmmRXMWMi0eQinRNAfIx88=", 903 | "dev": true 904 | }, 905 | "markdown-toc": { 906 | "version": "1.2.0", 907 | "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz", 908 | "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==", 909 | "dev": true, 910 | "requires": { 911 | "concat-stream": "^1.5.2", 912 | "diacritics-map": "^0.1.0", 913 | "gray-matter": "^2.1.0", 914 | "lazy-cache": "^2.0.2", 915 | "list-item": "^1.1.1", 916 | "markdown-link": "^0.1.1", 917 | "minimist": "^1.2.0", 918 | "mixin-deep": "^1.1.3", 919 | "object.pick": "^1.2.0", 920 | "remarkable": "^1.7.1", 921 | "repeat-string": "^1.6.1", 922 | "strip-color": "^0.1.0" 923 | } 924 | }, 925 | "math-random": { 926 | "version": "1.0.4", 927 | "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", 928 | "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", 929 | "dev": true 930 | }, 931 | "minimist": { 932 | "version": "1.2.6", 933 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 934 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", 935 | "dev": true 936 | }, 937 | "mixin-deep": { 938 | "version": "1.3.2", 939 | "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", 940 | "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", 941 | "dev": true, 942 | "requires": { 943 | "for-in": "^1.0.2", 944 | "is-extendable": "^1.0.1" 945 | }, 946 | "dependencies": { 947 | "is-extendable": { 948 | "version": "1.0.1", 949 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", 950 | "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", 951 | "dev": true, 952 | "requires": { 953 | "is-plain-object": "^2.0.4" 954 | } 955 | } 956 | } 957 | }, 958 | "object.pick": { 959 | "version": "1.3.0", 960 | "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", 961 | "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", 962 | "dev": true, 963 | "requires": { 964 | "isobject": "^3.0.1" 965 | }, 966 | "dependencies": { 967 | "isobject": { 968 | "version": "3.0.1", 969 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 970 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", 971 | "dev": true 972 | } 973 | } 974 | }, 975 | "process-nextick-args": { 976 | "version": "2.0.1", 977 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 978 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 979 | "dev": true 980 | }, 981 | "randomatic": { 982 | "version": "3.1.1", 983 | "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", 984 | "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", 985 | "dev": true, 986 | "requires": { 987 | "is-number": "^4.0.0", 988 | "kind-of": "^6.0.0", 989 | "math-random": "^1.0.1" 990 | }, 991 | "dependencies": { 992 | "is-number": { 993 | "version": "4.0.0", 994 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", 995 | "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", 996 | "dev": true 997 | }, 998 | "kind-of": { 999 | "version": "6.0.3", 1000 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 1001 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 1002 | "dev": true 1003 | } 1004 | } 1005 | }, 1006 | "readable-stream": { 1007 | "version": "2.3.7", 1008 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1009 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1010 | "dev": true, 1011 | "requires": { 1012 | "core-util-is": "~1.0.0", 1013 | "inherits": "~2.0.3", 1014 | "isarray": "~1.0.0", 1015 | "process-nextick-args": "~2.0.0", 1016 | "safe-buffer": "~5.1.1", 1017 | "string_decoder": "~1.1.1", 1018 | "util-deprecate": "~1.0.1" 1019 | } 1020 | }, 1021 | "remarkable": { 1022 | "version": "1.7.4", 1023 | "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", 1024 | "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", 1025 | "dev": true, 1026 | "requires": { 1027 | "argparse": "^1.0.10", 1028 | "autolinker": "~0.28.0" 1029 | } 1030 | }, 1031 | "repeat-element": { 1032 | "version": "1.1.3", 1033 | "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", 1034 | "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", 1035 | "dev": true 1036 | }, 1037 | "repeat-string": { 1038 | "version": "1.6.1", 1039 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1040 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", 1041 | "dev": true 1042 | }, 1043 | "safe-buffer": { 1044 | "version": "5.1.2", 1045 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1046 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1047 | "dev": true 1048 | }, 1049 | "set-getter": { 1050 | "version": "0.1.1", 1051 | "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", 1052 | "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", 1053 | "dev": true, 1054 | "requires": { 1055 | "to-object-path": "^0.3.0" 1056 | } 1057 | }, 1058 | "source-map": { 1059 | "version": "0.6.1", 1060 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1061 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1062 | "dev": true 1063 | }, 1064 | "sprintf-js": { 1065 | "version": "1.0.3", 1066 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1067 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1068 | "dev": true 1069 | }, 1070 | "string_decoder": { 1071 | "version": "1.1.1", 1072 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1073 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1074 | "dev": true, 1075 | "requires": { 1076 | "safe-buffer": "~5.1.0" 1077 | } 1078 | }, 1079 | "strip-color": { 1080 | "version": "0.1.0", 1081 | "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", 1082 | "integrity": "sha1-EG9l09PmotlAHKwOsM6LinArT3s=", 1083 | "dev": true 1084 | }, 1085 | "through2": { 1086 | "version": "2.0.5", 1087 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", 1088 | "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", 1089 | "dev": true, 1090 | "requires": { 1091 | "readable-stream": "~2.3.6", 1092 | "xtend": "~4.0.1" 1093 | } 1094 | }, 1095 | "to-object-path": { 1096 | "version": "0.3.0", 1097 | "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", 1098 | "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", 1099 | "dev": true, 1100 | "requires": { 1101 | "kind-of": "^3.0.2" 1102 | } 1103 | }, 1104 | "toml": { 1105 | "version": "2.3.6", 1106 | "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz", 1107 | "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==", 1108 | "dev": true 1109 | }, 1110 | "typedarray": { 1111 | "version": "0.0.6", 1112 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1113 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1114 | "dev": true 1115 | }, 1116 | "util-deprecate": { 1117 | "version": "1.0.2", 1118 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1119 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1120 | "dev": true 1121 | }, 1122 | "xtend": { 1123 | "version": "4.0.2", 1124 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1125 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 1126 | "dev": true 1127 | } 1128 | } 1129 | } 1130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "markdown-toc": "^1.2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 1 3 | paths: 4 | - src 5 | - tests 6 | 7 | reportUnmatchedIgnoredErrors: false 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | src 8 | 9 | 10 | ./src/functions_include.php 11 | 12 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Exceptions/MessageToLongException.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class MessageToLongException extends Exception 16 | { 17 | 18 | } -------------------------------------------------------------------------------- /src/Exceptions/QueueAlreadyExistsException.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class QueueAlreadyExistsException extends Exception 16 | { 17 | 18 | } -------------------------------------------------------------------------------- /src/Exceptions/QueueNotFoundException.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class QueueNotFoundException extends Exception 16 | { 17 | 18 | } -------------------------------------------------------------------------------- /src/Exceptions/QueueParametersValidationException.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class QueueParametersValidationException extends Exception 16 | { 17 | 18 | } -------------------------------------------------------------------------------- /src/ExecutorInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ExecutorInterface 13 | { 14 | 15 | /** 16 | * Handle the message, retuning true will "ack" the message, false will not ack (causing the message to become 17 | * visible as per the queue's vt setting) 18 | * 19 | * @param Message $message 20 | * @return bool 21 | */ 22 | public function __invoke(Message $message): bool; 23 | } 24 | -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Message 13 | { 14 | 15 | /** 16 | * @var string 17 | */ 18 | protected $id; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $message; 24 | 25 | /** 26 | * @var int 27 | */ 28 | protected $receiveCount; 29 | 30 | /** 31 | * @var int 32 | */ 33 | protected $firstReceived; 34 | 35 | /** 36 | * @var float 37 | */ 38 | protected $sent; 39 | 40 | /** 41 | * Message constructor. 42 | * 43 | * @param string $id 44 | * @param string $message 45 | * @param int $receiveCount 46 | * @param int $firstReceived 47 | * @param float $sent 48 | */ 49 | public function __construct(string $id, string $message, int $receiveCount, int $firstReceived, float $sent) 50 | { 51 | $this->id = $id; 52 | $this->message = $message; 53 | $this->receiveCount = $receiveCount; 54 | $this->firstReceived = $firstReceived; 55 | $this->sent = $sent; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getId(): string 62 | { 63 | return $this->id; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getMessage(): string 70 | { 71 | return $this->message; 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getReceiveCount(): int 78 | { 79 | return $this->receiveCount; 80 | } 81 | 82 | /** 83 | * @return int 84 | */ 85 | public function getFirstReceived(): int 86 | { 87 | return $this->firstReceived; 88 | } 89 | 90 | /** 91 | * @return float 92 | */ 93 | public function getSent(): float 94 | { 95 | return $this->sent; 96 | } 97 | } -------------------------------------------------------------------------------- /src/QueueAttributes.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class QueueAttributes 13 | { 14 | 15 | /** 16 | * @var int 17 | */ 18 | protected $vt; 19 | 20 | /** 21 | * @var int 22 | */ 23 | protected $delay; 24 | 25 | /** 26 | * @var int 27 | */ 28 | protected $maxSize; 29 | 30 | /** 31 | * @var int 32 | */ 33 | protected $totalReceived; 34 | 35 | /** 36 | * @var int 37 | */ 38 | protected $totalSent; 39 | 40 | /** 41 | * @var int 42 | */ 43 | protected $created; 44 | 45 | /** 46 | * @var int 47 | */ 48 | protected $modified; 49 | 50 | /** 51 | * @var int 52 | */ 53 | protected $messageCount; 54 | 55 | /** 56 | * @var int 57 | */ 58 | protected $hiddenMessageCount; 59 | 60 | /** 61 | * QueueAttributes constructor. 62 | * 63 | * @param int $vt 64 | * @param int $delay 65 | * @param int $maxSize 66 | * @param int $totalReceived 67 | * @param int $totalSent 68 | * @param int $created 69 | * @param int $modified 70 | * @param int $messageCount 71 | * @param int $hiddenMessageCount 72 | */ 73 | public function __construct( 74 | int $vt, 75 | int $delay, 76 | int $maxSize, 77 | int $totalReceived, 78 | int $totalSent, 79 | int $created, 80 | int $modified, 81 | int $messageCount, 82 | int $hiddenMessageCount 83 | ) { 84 | $this->vt = $vt; 85 | $this->delay = $delay; 86 | $this->maxSize = $maxSize; 87 | $this->totalReceived = $totalReceived; 88 | $this->totalSent = $totalSent; 89 | $this->created = $created; 90 | $this->modified = $modified; 91 | $this->messageCount = $messageCount; 92 | $this->hiddenMessageCount = $hiddenMessageCount; 93 | } 94 | 95 | /** 96 | * @return int 97 | */ 98 | public function getVt(): int 99 | { 100 | return $this->vt; 101 | } 102 | 103 | /** 104 | * @return int 105 | */ 106 | public function getDelay(): int 107 | { 108 | return $this->delay; 109 | } 110 | 111 | /** 112 | * @return int 113 | */ 114 | public function getMaxSize(): int 115 | { 116 | return $this->maxSize; 117 | } 118 | 119 | /** 120 | * @return int 121 | */ 122 | public function getTotalReceived(): int 123 | { 124 | return $this->totalReceived; 125 | } 126 | 127 | /** 128 | * @return int 129 | */ 130 | public function getTotalSent(): int 131 | { 132 | return $this->totalSent; 133 | } 134 | 135 | /** 136 | * @return int 137 | */ 138 | public function getCreated(): int 139 | { 140 | return $this->created; 141 | } 142 | 143 | /** 144 | * @return int 145 | */ 146 | public function getModified(): int 147 | { 148 | return $this->modified; 149 | } 150 | 151 | /** 152 | * @return int 153 | */ 154 | public function getMessageCount(): int 155 | { 156 | return $this->messageCount; 157 | } 158 | 159 | /** 160 | * @return int 161 | */ 162 | public function getHiddenMessageCount(): int 163 | { 164 | return $this->hiddenMessageCount; 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /src/QueueWorker.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class QueueWorker 13 | { 14 | 15 | /** 16 | * @var RSMQClientInterface 17 | */ 18 | protected $rsmq; 19 | 20 | /** 21 | * @var ExecutorInterface 22 | */ 23 | protected $executor; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $queue; 29 | 30 | /** 31 | * @var WorkerSleepProvider 32 | */ 33 | protected $sleepProvider; 34 | 35 | /** 36 | * @var int 37 | */ 38 | protected $received = 0; 39 | 40 | /** 41 | * @var int 42 | */ 43 | protected $failed = 0; 44 | 45 | /** 46 | * @var int 47 | */ 48 | protected $successful = 0; 49 | 50 | /** 51 | * QueueWorker constructor. 52 | * 53 | * @param RSMQClientInterface $rsmq 54 | * @param ExecutorInterface $executor 55 | * @param WorkerSleepProvider $sleepProvider 56 | * @param string $queue 57 | */ 58 | public function __construct( 59 | RSMQClientInterface $rsmq, 60 | ExecutorInterface $executor, 61 | WorkerSleepProvider $sleepProvider, 62 | string $queue 63 | ) { 64 | $this->rsmq = $rsmq; 65 | $this->executor = $executor; 66 | $this->sleepProvider = $sleepProvider; 67 | $this->queue = $queue; 68 | } 69 | 70 | 71 | /** 72 | * @param bool $processOne 73 | * @throws Exceptions\QueueNotFoundException 74 | * @throws Exceptions\QueueParametersValidationException 75 | */ 76 | public function work(bool $processOne = false): void 77 | { 78 | while (true) { 79 | $sleep = $this->sleepProvider->getSleep(); 80 | if ($sleep === null || $sleep < 0) { 81 | break; 82 | } 83 | $message = $this->rsmq->receiveMessage($this->queue); 84 | if (!($message instanceof Message)) { 85 | sleep($sleep); 86 | continue; 87 | } 88 | $this->received++; 89 | $result = $this->executor->__invoke($message); 90 | if ($result === true) { 91 | $this->successful++; 92 | $this->rsmq->deleteMessage($this->queue, $message->getId()); 93 | } else { 94 | $this->failed++; 95 | } 96 | if ($processOne && $this->getProcessedCount() === 1) { 97 | break; 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * @return int 104 | */ 105 | public function getProcessedCount(): int 106 | { 107 | return $this->successful + $this->failed; 108 | } 109 | 110 | /** 111 | * @return int 112 | */ 113 | public function getReceived(): int 114 | { 115 | return $this->received; 116 | } 117 | 118 | /** 119 | * @return int 120 | */ 121 | public function getFailed(): int 122 | { 123 | return $this->failed; 124 | } 125 | 126 | /** 127 | * @return int 128 | */ 129 | public function getSuccessful(): int 130 | { 131 | return $this->successful; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/RSMQClient.php: -------------------------------------------------------------------------------- 1 | 17 | * @author emre can islambey 18 | */ 19 | class RSMQClient implements RSMQClientInterface 20 | { 21 | const MAX_DELAY = 9999999; 22 | const MIN_MESSAGE_SIZE = 1024; 23 | const MAX_PAYLOAD_SIZE = 65536; 24 | 25 | /** 26 | * @var ClientInterface 27 | */ 28 | private ClientInterface $predis; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private string $ns; 34 | 35 | /** 36 | * @var bool 37 | */ 38 | private bool $realtime; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private string $receiveMessageSha1; 44 | 45 | /** 46 | * @var string 47 | */ 48 | private string $popMessageSha1; 49 | 50 | /** 51 | * @var string 52 | */ 53 | private string $changeMessageVisibilitySha1; 54 | 55 | /** 56 | * RSMQ constructor. 57 | * 58 | * @param ClientInterface $predis 59 | * @param string $ns 60 | * @param bool $realtime 61 | */ 62 | public function __construct(ClientInterface $predis, string $ns = 'rsmq', bool $realtime = false) 63 | { 64 | $this->predis = $predis; 65 | $this->ns = "$ns:"; 66 | $this->realtime = $realtime; 67 | 68 | $this->initScripts(); 69 | } 70 | 71 | 72 | private function initScripts(): void 73 | { 74 | $receiveMessageScript = 'local msg = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", KEYS[2], "LIMIT", "0", "1") 75 | if #msg == 0 then 76 | return {} 77 | end 78 | redis.call("ZADD", KEYS[1], KEYS[3], msg[1]) 79 | redis.call("HINCRBY", KEYS[1] .. ":Q", "totalrecv", 1) 80 | local mbody = redis.call("HGET", KEYS[1] .. ":Q", msg[1]) 81 | local rc = redis.call("HINCRBY", KEYS[1] .. ":Q", msg[1] .. ":rc", 1) 82 | local o = {msg[1], mbody, rc} 83 | if rc==1 then 84 | redis.call("hset", KEYS[1] .. ":Q", msg[1] .. ":fr", KEYS[2]) 85 | table.insert(o, KEYS[2]) 86 | else 87 | local fr = redis.call("HGET", KEYS[1] .. ":Q", msg[1] .. ":fr") 88 | table.insert(o, fr) 89 | end 90 | return o'; 91 | 92 | $popMessageScript = 'local msg = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", KEYS[2], "LIMIT", "0", "1") 93 | if #msg == 0 then 94 | return {} 95 | end 96 | redis.call("HINCRBY", KEYS[1] .. ":Q", "totalrecv", 1) 97 | local mbody = redis.call("HGET", KEYS[1] .. ":Q", msg[1]) 98 | local rc = redis.call("HINCRBY", KEYS[1] .. ":Q", msg[1] .. ":rc", 1) 99 | local o = {msg[1], mbody, rc} 100 | if rc==1 then 101 | table.insert(o, KEYS[2]) 102 | else 103 | local fr = redis.call("HGET", KEYS[1] .. ":Q", msg[1] .. ":fr") 104 | table.insert(o, fr) 105 | end 106 | redis.call("zrem", KEYS[1], msg[1]) 107 | redis.call("hdel", KEYS[1] .. ":Q", msg[1], msg[1] .. ":rc", msg[1] .. ":fr") 108 | return o'; 109 | 110 | $changeMessageVisibilityScript = 'local msg = redis.call("ZSCORE", KEYS[1], KEYS[2]) 111 | if not msg then 112 | return 0 113 | end 114 | redis.call("ZADD", KEYS[1], KEYS[3], KEYS[2]) 115 | return 1'; 116 | 117 | $this->receiveMessageSha1 = $this->predis->script('load', $receiveMessageScript); 118 | $this->popMessageSha1 = $this->predis->script('load', $popMessageScript); 119 | $this->changeMessageVisibilitySha1 = $this->predis->script('load', $changeMessageVisibilityScript); 120 | } 121 | 122 | /** 123 | * @param string $name 124 | * @param int $vt 125 | * @param int $delay 126 | * @param int $maxSize 127 | * @return bool 128 | * @throws QueueAlreadyExistsException 129 | */ 130 | public function createQueue(string $name, int $vt = 30, int $delay = 0, int $maxSize = 65536): bool 131 | { 132 | $this->validate( 133 | [ 134 | 'queue' => $name, 135 | 'vt' => $vt, 136 | 'delay' => $delay, 137 | 'maxsize' => $maxSize, 138 | ] 139 | ); 140 | 141 | $key = "{$this->ns}$name:Q"; 142 | 143 | $resp = $this->predis->time(); 144 | $this->predis->multi(); 145 | $this->predis->hsetnx($key, 'vt', (string)$vt); 146 | $this->predis->hsetnx($key, 'delay', (string)$delay); 147 | $this->predis->hsetnx($key, 'maxsize', (string)$maxSize); 148 | $this->predis->hsetnx($key, 'created', $resp[0]); 149 | $this->predis->hsetnx($key, 'modified', $resp[0]); 150 | $resp = $this->predis->exec() ?? []; 151 | 152 | if (!$resp[0]) { 153 | throw new QueueAlreadyExistsException('Queue already exists.'); 154 | } 155 | 156 | return (bool)$this->predis->sadd("{$this->ns}QUEUES", [$name]); 157 | } 158 | 159 | /** 160 | * @param array $params 161 | * @throws QueueParametersValidationException 162 | */ 163 | public function validate(array $params): void 164 | { 165 | if (isset($params['queue']) && !preg_match('/^([a-zA-Z0-9_-]){1,160}$/', (string)$params['queue'])) { 166 | throw new QueueParametersValidationException('Invalid queue name'); 167 | } 168 | 169 | if (isset($params['id']) && !preg_match('/^([a-zA-Z0-9:]){32}$/', (string)$params['id'])) { 170 | throw new QueueParametersValidationException('Invalid message id'); 171 | } 172 | 173 | if (isset($params['vt']) && ($params['vt'] < 0 || $params['vt'] > self::MAX_DELAY)) { 174 | throw new QueueParametersValidationException('Visibility time must be between 0 and ' . self::MAX_DELAY); 175 | } 176 | 177 | if (isset($params['delay']) && ($params['delay'] < 0 || $params['delay'] > self::MAX_DELAY)) { 178 | throw new QueueParametersValidationException('Delay must be between 0 and ' . self::MAX_DELAY); 179 | } 180 | 181 | if (isset($params['maxsize']) 182 | && $params['maxsize'] !== -1 && ($params['maxsize'] < self::MIN_MESSAGE_SIZE || $params['maxsize'] > self::MAX_PAYLOAD_SIZE) 183 | ) { 184 | $message = "Maximum message size must be between %d and %d"; 185 | throw new QueueParametersValidationException( 186 | sprintf( 187 | $message, self::MIN_MESSAGE_SIZE, 188 | self::MAX_PAYLOAD_SIZE 189 | ) 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * @return array 196 | */ 197 | public function listQueues(): array 198 | { 199 | return $this->predis->smembers("{$this->ns}QUEUES"); 200 | } 201 | 202 | /** 203 | * @param string $name 204 | * @throws QueueNotFoundException 205 | */ 206 | public function deleteQueue(string $name): void 207 | { 208 | $this->validate( 209 | [ 210 | 'queue' => $name, 211 | ] 212 | ); 213 | 214 | $key = "{$this->ns}$name"; 215 | $this->predis->multi(); 216 | $this->predis->del(["$key:Q", $key]); 217 | $this->predis->srem("{$this->ns}QUEUES", $name); 218 | $resp = $this->predis->exec() ?? []; 219 | 220 | if (!$resp[0]) { 221 | throw new QueueNotFoundException('Queue not found.'); 222 | } 223 | } 224 | 225 | /** 226 | * @param string $queue 227 | * @param int|null $vt 228 | * @param int|null $delay 229 | * @param int|null $maxSize 230 | * @return QueueAttributes 231 | * @throws QueueParametersValidationException 232 | * @throws QueueNotFoundException 233 | */ 234 | public function setQueueAttributes( 235 | string $queue, 236 | int $vt = null, 237 | int $delay = null, 238 | int $maxSize = null 239 | ): QueueAttributes { 240 | $this->validate( 241 | [ 242 | 'vt' => $vt, 243 | 'delay' => $delay, 244 | 'maxsize' => $maxSize, 245 | ] 246 | ); 247 | $this->getQueue($queue); 248 | 249 | $time = $this->predis->time(); 250 | $this->predis->multi(); 251 | 252 | $this->predis->hset("{$this->ns}$queue:Q", 'modified', $time[0]); 253 | if ($vt !== null) { 254 | $this->predis->hset("{$this->ns}$queue:Q", 'vt', (string)$vt); 255 | } 256 | 257 | if ($delay !== null) { 258 | $this->predis->hset("{$this->ns}$queue:Q", 'delay', (string)$delay); 259 | } 260 | 261 | if ($maxSize !== null) { 262 | $this->predis->hset("{$this->ns}$queue:Q", 'maxsize', (string)$maxSize); 263 | } 264 | 265 | $this->predis->exec(); 266 | 267 | return $this->getQueueAttributes($queue); 268 | } 269 | 270 | /** 271 | * @param string $name 272 | * @param bool $generateUid 273 | * @return array|int[] 274 | * @throws QueueNotFoundException 275 | */ 276 | private function getQueue(string $name, bool $generateUid = false): array 277 | { 278 | $this->validate( 279 | [ 280 | 'queue' => $name, 281 | ] 282 | ); 283 | 284 | /** 285 | * @psalm-suppress UndefinedMagicMethod 286 | */ 287 | $transaction = $this->predis->transaction(); 288 | $transaction->hmget("{$this->ns}$name:Q", ['vt', 'delay', 'maxsize']); 289 | $transaction->time(); 290 | $resp = $transaction->execute(); 291 | 292 | if (!isset($resp[0][0])) { 293 | throw new QueueNotFoundException('Queue not found.'); 294 | } 295 | 296 | $ms = formatZeroPad((int)$resp[1][1], 6); 297 | 298 | 299 | $queue = [ 300 | 'vt' => (int)$resp[0][0], 301 | 'delay' => (int)$resp[0][1], 302 | 'maxsize' => (int)$resp[0][2], 303 | 'ts' => (int)($resp[1][0] . substr($ms, 0, 3)), 304 | ]; 305 | 306 | if ($generateUid) { 307 | $queue['uid'] = base_convert(($resp[1][0] . $ms), 10, 36) . makeID(22); 308 | } 309 | 310 | return $queue; 311 | } 312 | 313 | /** 314 | * @param string $queue 315 | * @return QueueAttributes 316 | * @throws QueueNotFoundException 317 | * @throws QueueParametersValidationException 318 | */ 319 | public function getQueueAttributes(string $queue): QueueAttributes 320 | { 321 | $this->validate( 322 | [ 323 | 'queue' => $queue, 324 | ] 325 | ); 326 | 327 | $key = "{$this->ns}$queue"; 328 | $resp = $this->predis->time(); 329 | 330 | /** 331 | * @psalm-suppress UndefinedMagicMethod 332 | */ 333 | $transaction = $this->predis->transaction(); 334 | $transaction->hmget("$key:Q", ['vt', 'delay', 'maxsize', 'totalrecv', 'totalsent', 'created', 'modified']); 335 | $transaction->zcard($key); 336 | $transaction->zcount($key, $resp[0] . '0000', "+inf"); 337 | $resp = $transaction->execute(); 338 | 339 | if (!isset($resp[0][0])) { 340 | throw new QueueNotFoundException('Queue not found.'); 341 | } 342 | 343 | return new QueueAttributes( 344 | (int)$resp[0][0], 345 | (int)$resp[0][1], 346 | (int)$resp[0][2], 347 | (int)$resp[0][3], 348 | (int)$resp[0][4], 349 | (int)$resp[0][5], 350 | (int)$resp[0][6], 351 | (int)$resp[1], 352 | (int)$resp[2] 353 | ); 354 | } 355 | 356 | /** 357 | * @param string $queue 358 | * @param string $message 359 | * @param int|null $delay 360 | * @return string 361 | * @throws MessageToLongException 362 | * @throws QueueNotFoundException 363 | * @throws QueueParametersValidationException 364 | */ 365 | public function sendMessage(string $queue, string $message, int $delay = null): string 366 | { 367 | $this->validate( 368 | [ 369 | 'queue' => $queue, 370 | ] 371 | ); 372 | 373 | $q = $this->getQueue($queue, true); 374 | if ($delay === null) { 375 | $delay = $q['delay']; 376 | } 377 | 378 | if ($q['maxsize'] !== -1 && mb_strlen($message) > $q['maxsize']) { 379 | throw new MessageToLongException('Message too long'); 380 | } 381 | 382 | $key = "{$this->ns}$queue"; 383 | 384 | $this->predis->multi(); 385 | $this->predis->zadd($key, [$q['uid'] => $q['ts'] + $delay * 1000]); 386 | $this->predis->hset("$key:Q", $q['uid'], $message); 387 | $this->predis->hincrby("$key:Q", 'totalsent', 1); 388 | 389 | if ($this->realtime) { 390 | $this->predis->zcard($key); 391 | } 392 | 393 | $resp = $this->predis->exec() ?? []; 394 | 395 | if ($this->realtime) { 396 | $this->predis->publish("{$this->ns}rt:$$queue", $resp[3]); 397 | } 398 | 399 | return $q['uid']; 400 | } 401 | 402 | /** 403 | * @param string $queue 404 | * @param array $options 405 | * @return Message|null 406 | * @throws QueueNotFoundException 407 | * @throws QueueParametersValidationException 408 | */ 409 | public function receiveMessage(string $queue, array $options = []): ?Message 410 | { 411 | $this->validate( 412 | [ 413 | 'queue' => $queue, 414 | ] 415 | ); 416 | 417 | $q = $this->getQueue($queue); 418 | $vt = $options['vt'] ?? $q['vt']; 419 | 420 | $resp = $this->predis->evalsha( 421 | $this->receiveMessageSha1, 422 | 3, 423 | "{$this->ns}$queue", $q['ts'], 424 | $q['ts'] + $vt * 1000 425 | ); 426 | if (empty($resp)) { 427 | return null; 428 | } 429 | 430 | return new Message( 431 | (string)$resp[0], 432 | (string)$resp[1], 433 | (int)$resp[2], 434 | (int)$resp[3], 435 | (float)base_convert(substr($resp[0], 0, 10), 36, 10) / 1000 436 | ); 437 | } 438 | 439 | /** 440 | * @param string $queue 441 | * @return Message|null 442 | * @throws QueueNotFoundException 443 | * @throws QueueParametersValidationException 444 | */ 445 | public function popMessage(string $queue): ?Message 446 | { 447 | $this->validate( 448 | [ 449 | 'queue' => $queue, 450 | ] 451 | ); 452 | 453 | $q = $this->getQueue($queue); 454 | 455 | $resp = $this->predis->evalsha($this->popMessageSha1, 2, "{$this->ns}$queue", $q['ts']); 456 | if (empty($resp)) { 457 | return null; 458 | } 459 | return new Message( 460 | (string)$resp[0], 461 | (string)$resp[1], 462 | (int)$resp[2], 463 | (int)$resp[3], 464 | (float)base_convert(substr($resp[0], 0, 10), 36, 10) / 1000 465 | ); 466 | } 467 | 468 | /** 469 | * @param string $queue 470 | * @param string $id 471 | * @return bool 472 | * @throws QueueParametersValidationException 473 | */ 474 | public function deleteMessage(string $queue, string $id): bool 475 | { 476 | $this->validate( 477 | [ 478 | 'queue' => $queue, 479 | 'id' => $id, 480 | ] 481 | ); 482 | 483 | $key = "{$this->ns}$queue"; 484 | $this->predis->multi(); 485 | $this->predis->zrem($key, $id); 486 | $this->predis->hdel("$key:Q", [$id, "$id:rc", "$id:fr"]); 487 | $resp = $this->predis->exec() ?? []; 488 | 489 | return $resp[0] === 1 && $resp[1] > 0; 490 | } 491 | 492 | /** 493 | * @param string $queue 494 | * @param string $id 495 | * @param int $vt 496 | * @return bool 497 | * @throws QueueParametersValidationException 498 | * @throws QueueNotFoundException 499 | */ 500 | public function changeMessageVisibility(string $queue, string $id, int $vt): bool 501 | { 502 | $this->validate( 503 | [ 504 | 'queue' => $queue, 505 | 'id' => $id, 506 | 'vt' => $vt, 507 | ] 508 | ); 509 | 510 | $q = $this->getQueue($queue, true); 511 | 512 | $resp = $this->predis->evalsha( 513 | $this->changeMessageVisibilitySha1, 514 | 3, 515 | "{$this->ns}$queue", 516 | $id, 517 | $q['ts'] + $vt * 1000 518 | ); 519 | 520 | return (bool)$resp; 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/RSMQClientInterface.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface RSMQClientInterface 19 | { 20 | 21 | /** 22 | * @param string $name 23 | * @param int $vt 24 | * @param int $delay 25 | * @param int $maxSize 26 | * @return bool 27 | * @throws QueueAlreadyExistsException 28 | */ 29 | public function createQueue(string $name, int $vt = 30, int $delay = 0, int $maxSize = 65536): bool; 30 | 31 | /** 32 | * @param string $queue 33 | * @param array $options 34 | * @return Message|null 35 | * @throws QueueNotFoundException 36 | * @throws QueueParametersValidationException 37 | */ 38 | public function receiveMessage(string $queue, array $options = []): ?Message; 39 | 40 | /** 41 | * @param string $queue 42 | * @param string $id 43 | * @param int $vt 44 | * @return bool 45 | * @throws QueueParametersValidationException 46 | * @throws QueueNotFoundException 47 | */ 48 | public function changeMessageVisibility(string $queue, string $id, int $vt): bool; 49 | 50 | /** 51 | * @param string $queue 52 | * @return QueueAttributes 53 | * @throws QueueNotFoundException 54 | * @throws QueueParametersValidationException 55 | */ 56 | public function getQueueAttributes(string $queue): QueueAttributes; 57 | 58 | /** 59 | * @param string $queue 60 | * @param int|null $vt 61 | * @param int|null $delay 62 | * @param int|null $maxSize 63 | * @return QueueAttributes 64 | * @throws QueueParametersValidationException 65 | * @throws QueueNotFoundException 66 | */ 67 | public function setQueueAttributes( 68 | string $queue, 69 | int $vt = null, 70 | int $delay = null, 71 | int $maxSize = null 72 | ): QueueAttributes; 73 | 74 | /** 75 | * @param string $name 76 | * @throws QueueNotFoundException 77 | */ 78 | public function deleteQueue(string $name): void; 79 | 80 | /** 81 | * @param string $queue 82 | * @return Message|null 83 | * @throws QueueNotFoundException 84 | * @throws QueueParametersValidationException 85 | */ 86 | public function popMessage(string $queue): ?Message; 87 | 88 | /** 89 | * @param array $params 90 | * @throws QueueParametersValidationException 91 | */ 92 | public function validate(array $params): void; 93 | 94 | /** 95 | * @param string $queue 96 | * @param string $id 97 | * @return bool 98 | * @throws QueueParametersValidationException 99 | */ 100 | public function deleteMessage(string $queue, string $id): bool; 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function listQueues(): array; 106 | 107 | /** 108 | * @param string $queue 109 | * @param string $message 110 | * @param int|null $delay 111 | * @return string 112 | * @throws MessageToLongException 113 | * @throws QueueNotFoundException 114 | * @throws QueueParametersValidationException 115 | */ 116 | public function sendMessage(string $queue, string $message, int $delay = null): string; 117 | 118 | } -------------------------------------------------------------------------------- /src/WorkerSleepProvider.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface WorkerSleepProvider 13 | { 14 | 15 | /** 16 | * Return the number of seconds that the worker should sleep for before grabbing the next message. 17 | * Returning null or a value less than zero will cause the worker to exit. 18 | * 19 | * Note: this method is called _before_ the receiveMessage method is called. 20 | * 21 | * @return positive-int|null 22 | */ 23 | public function getSleep(): ?int; 24 | } 25 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | assertSame($size, strlen(makeID($size))); 13 | } 14 | 15 | /** 16 | * @param string $expected 17 | * @param int $num 18 | * @param int $count 19 | * @dataProvider providerFormatZeroPad 20 | */ 21 | public function testFormatZeroPad($expected, $num, $count): void 22 | { 23 | $this->assertSame($expected, formatZeroPad($num, $count)); 24 | } 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function providerFormatZeroPad(): array 30 | { 31 | return [ 32 | ['01', 1, 2], 33 | ['001', 1, 3], 34 | ['0001', 1, 4], 35 | ['00001', 1, 5], 36 | ['000001', 1, 6], 37 | ['000451', 451, 6], 38 | ['123456', 123456, 6], 39 | ['0000123456', 123456, 10], 40 | ]; 41 | } 42 | } -------------------------------------------------------------------------------- /tests/QueueWorkerTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class QueueWorkerTest extends TestCase 16 | { 17 | 18 | 19 | public function testEjectFromSleepProvider() 20 | { 21 | $rsmq = Mockery::mock(RSMQClientInterface::class); 22 | $executor = Mockery::mock(ExecutorInterface::class); 23 | $sleepProvider = Mockery::mock(WorkerSleepProvider::class); 24 | $sleepProvider->shouldReceive('getSleep') 25 | ->andReturn(null) 26 | ->once(); 27 | $worker = new QueueWorker($rsmq, $executor, $sleepProvider, 'test'); 28 | 29 | $worker->work(true); 30 | self::assertEquals(0, $worker->getProcessedCount()); 31 | } 32 | 33 | public function testProcessOneAvilableFailed() 34 | { 35 | $message = Mockery::mock(Message::class); 36 | $rsmq = Mockery::mock(RSMQClientInterface::class); 37 | $rsmq->shouldReceive('receiveMessage') 38 | ->with('test') 39 | ->andReturn($message) 40 | ->once(); 41 | $executor = Mockery::mock(ExecutorInterface::class); 42 | $executor->shouldReceive('__invoke') 43 | ->with($message) 44 | ->andReturn(false) 45 | ->once(); 46 | $sleepProvider = Mockery::mock(WorkerSleepProvider::class); 47 | $sleepProvider->shouldReceive('getSleep') 48 | ->andReturn(0) 49 | ->once(); 50 | $worker = new QueueWorker($rsmq, $executor, $sleepProvider, 'test'); 51 | 52 | $worker->work(true); 53 | self::assertEquals(1, $worker->getProcessedCount()); 54 | self::assertEquals(0, $worker->getSuccessful()); 55 | self::assertEquals(1, $worker->getFailed()); 56 | } 57 | 58 | public function testProcessOneNotAvilableSuccessful() 59 | { 60 | $message = Mockery::mock(Message::class); 61 | $message->shouldReceive('getId') 62 | ->andReturn('test_id') 63 | ->once(); 64 | $rsmq = Mockery::mock(RSMQClientInterface::class); 65 | $rsmq->shouldReceive('receiveMessage') 66 | ->with('test') 67 | ->andReturn(null, $message) 68 | ->twice(); 69 | $rsmq->shouldReceive('deleteMessage') 70 | ->with('test', 'test_id') 71 | ->once(); 72 | $executor = Mockery::mock(ExecutorInterface::class); 73 | $executor->shouldReceive('__invoke') 74 | ->with($message) 75 | ->andReturn(true) 76 | ->once(); 77 | $sleepProvider = Mockery::mock(WorkerSleepProvider::class); 78 | $sleepProvider->shouldReceive('getSleep') 79 | ->andReturn(0) 80 | ->twice(); 81 | $worker = new QueueWorker($rsmq, $executor, $sleepProvider, 'test'); 82 | 83 | $worker->work(true); 84 | self::assertEquals(1, $worker->getProcessedCount()); 85 | self::assertEquals(1, $worker->getSuccessful()); 86 | self::assertEquals(0, $worker->getFailed()); 87 | } 88 | 89 | public function testProcessThreeAndExit() 90 | { 91 | $message = Mockery::mock(Message::class); 92 | $message->shouldReceive('getId') 93 | ->andReturn('test_id') 94 | ->twice(); 95 | $rsmq = Mockery::mock(RSMQClientInterface::class); 96 | $rsmq->shouldReceive('receiveMessage') 97 | ->with('test') 98 | ->andReturn($message, $message, $message) 99 | ->times(3); 100 | $rsmq->shouldReceive('deleteMessage') 101 | ->with('test', 'test_id') 102 | ->twice(); 103 | $executor = Mockery::mock(ExecutorInterface::class); 104 | $executor->shouldReceive('__invoke') 105 | ->with($message) 106 | ->andReturn(false, true, true) 107 | ->times(3); 108 | $sleepProvider = Mockery::mock(WorkerSleepProvider::class); 109 | $sleepProvider->shouldReceive('getSleep') 110 | ->andReturn(0, 0, 0, null) 111 | ->times(4); 112 | $worker = new QueueWorker($rsmq, $executor, $sleepProvider, 'test'); 113 | 114 | $worker->work(); 115 | self::assertEquals(3, $worker->getProcessedCount()); 116 | self::assertEquals(2, $worker->getSuccessful()); 117 | self::assertEquals(1, $worker->getFailed()); 118 | self::assertEquals(3, $worker->getReceived()); 119 | } 120 | 121 | public function tearDown(): void 122 | { 123 | Mockery::close(); 124 | parent::tearDown(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/RSMQTest.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 20 | 'port' => 6379 21 | ] 22 | ); 23 | $this->rsmq = new RSMQClient($redis); 24 | } 25 | 26 | public function testScriptsShouldInitialized(): void 27 | { 28 | $reflection = new ReflectionClass($this->rsmq); 29 | 30 | $recvMsgRef = $reflection->getProperty('receiveMessageSha1'); 31 | $recvMsgRef->setAccessible(true); 32 | 33 | $this->assertSame(40, strlen($recvMsgRef->getValue($this->rsmq))); 34 | 35 | $popMsgRef = $reflection->getProperty('popMessageSha1'); 36 | $popMsgRef->setAccessible(true); 37 | 38 | $this->assertSame(40, strlen($popMsgRef->getValue($this->rsmq))); 39 | } 40 | 41 | public function testCreateQueue(): void 42 | { 43 | $this->assertTrue($this->rsmq->createQueue('foo')); 44 | } 45 | 46 | public function testCreateQueueWithInvalidName(): void 47 | { 48 | $this->expectException(QueueParametersValidationException::class); 49 | $this->expectExceptionMessage('Invalid queue name'); 50 | $this->rsmq->createQueue(' sad'); 51 | } 52 | 53 | public function testCreateQueueWithBigVt(): void 54 | { 55 | $this->expectException(QueueParametersValidationException::class); 56 | $this->expectExceptionMessage('Visibility time must be between'); 57 | $this->rsmq->createQueue('foo', PHP_INT_MAX); 58 | } 59 | 60 | public function testCreateQueueWithNegativeVt(): void 61 | { 62 | $this->expectException(QueueParametersValidationException::class); 63 | $this->expectExceptionMessage('Visibility time must be between'); 64 | $this->rsmq->createQueue('foo', -1); 65 | } 66 | 67 | public function testCreateQueueWithBigDelay(): void 68 | { 69 | $this->expectException(QueueParametersValidationException::class); 70 | $this->expectExceptionMessage('Delay must be between'); 71 | $this->rsmq->createQueue('foo', 30, PHP_INT_MAX); 72 | } 73 | 74 | public function testCreateQueueWithNegativeDelay(): void 75 | { 76 | $this->expectException(QueueParametersValidationException::class); 77 | $this->expectExceptionMessage('Delay must be between'); 78 | $this->rsmq->createQueue('foo', 30, -1); 79 | } 80 | 81 | public function testCreateQueueWithBigMaxSize(): void 82 | { 83 | $this->expectException(QueueParametersValidationException::class); 84 | $this->expectExceptionMessage('Maximum message size must be between'); 85 | $this->rsmq->createQueue('foo', 30, 0, PHP_INT_MAX); 86 | } 87 | 88 | public function testCreateQueueWithSmallMaxSize(): void 89 | { 90 | $this->expectException(QueueParametersValidationException::class); 91 | $this->expectExceptionMessage('Maximum message size must be between'); 92 | $this->rsmq->createQueue('foo', 30, 0, 1023); 93 | } 94 | 95 | public function testGetQueueAttributes(): void 96 | { 97 | $vt = 40; 98 | $delay = 60; 99 | $maxSize = 1024; 100 | $this->rsmq->createQueue('foo', $vt, $delay, $maxSize); 101 | 102 | $attributes = $this->rsmq->getQueueAttributes('foo'); 103 | 104 | $this->assertSame($vt, $attributes->getVt()); 105 | $this->assertSame($delay, $attributes->getDelay()); 106 | $this->assertSame($maxSize, $attributes->getMaxSize()); 107 | $this->assertSame(0, $attributes->getMessageCount()); 108 | $this->assertSame(0, $attributes->getHiddenMessageCount()); 109 | $this->assertSame(0, $attributes->getTotalReceived()); 110 | $this->assertSame(0, $attributes->getTotalSent()); 111 | $this->assertNotEmpty($attributes->getCreated()); 112 | $this->assertNotEmpty($attributes->getModified()); 113 | } 114 | 115 | public function testGetQueueAttributesThatDoesNotExists(): void 116 | { 117 | $this->expectExceptionMessage('Queue not found.'); 118 | $this->rsmq->getQueueAttributes('not_existent_queue'); 119 | } 120 | 121 | public function testCreateQueueMustThrowExceptionWhenQueueExists(): void 122 | { 123 | $this->expectException(Exception::class); 124 | $this->expectExceptionMessage('Queue already exists.'); 125 | 126 | $this->rsmq->createQueue('foo'); 127 | $this->rsmq->createQueue('foo'); 128 | } 129 | 130 | public function testListQueues(): void 131 | { 132 | $this->assertEmpty($this->rsmq->listQueues()); 133 | 134 | $this->rsmq->createQueue('foo'); 135 | $this->assertSame(['foo'], $this->rsmq->listQueues()); 136 | } 137 | 138 | public function testValidateWithInvalidQueueName(): void 139 | { 140 | $this->expectExceptionMessage('Invalid queue name'); 141 | $this->invokeMethod( 142 | $this->rsmq, 'validate', [ 143 | ['queue' => ' foo'] 144 | ] 145 | ); 146 | 147 | } 148 | 149 | /** 150 | * @param object $object 151 | * @param string $methodName 152 | * @param array $parameters 153 | * @return mixed 154 | * @throws ReflectionException 155 | */ 156 | public function invokeMethod(object &$object, string $methodName, array $parameters = array()) 157 | { 158 | $reflection = new ReflectionClass(get_class($object)); 159 | $method = $reflection->getMethod($methodName); 160 | $method->setAccessible(true); 161 | 162 | return $method->invokeArgs($object, $parameters); 163 | } 164 | 165 | public function testValidateWithInvalidVt(): void 166 | { 167 | $this->expectExceptionMessage('Visibility time must be'); 168 | $this->invokeMethod( 169 | $this->rsmq, 'validate', [ 170 | ['vt' => '-1'] 171 | ] 172 | ); 173 | } 174 | 175 | public function testValidateWithInvalidId(): void 176 | { 177 | $this->expectExceptionMessage('Invalid message id'); 178 | $this->invokeMethod( 179 | $this->rsmq, 'validate', [ 180 | ['id' => '123456'] 181 | ] 182 | ); 183 | } 184 | 185 | public function testValidateWithInvalidDelay(): void 186 | { 187 | $this->expectExceptionMessage('Delay must be'); 188 | $this->invokeMethod( 189 | $this->rsmq, 'validate', [ 190 | ['delay' => 99999999] 191 | ] 192 | ); 193 | } 194 | 195 | public function testValidateWithInvalidMaxSize(): void 196 | { 197 | $this->expectExceptionMessage('Maximum message size must be'); 198 | $this->invokeMethod( 199 | $this->rsmq, 200 | 'validate', 201 | [ 202 | ['maxsize' => 512] 203 | ] 204 | ); 205 | } 206 | 207 | public function testSendMessage(): void 208 | { 209 | $this->rsmq->createQueue('foo'); 210 | $id = $this->rsmq->sendMessage('foo', 'foobar'); 211 | $this->assertSame(32, strlen($id)); 212 | $attributes = $this->rsmq->getQueueAttributes('foo'); 213 | $this->assertSame(1, $attributes->getMessageCount()); 214 | $this->assertSame(0, $attributes->getHiddenMessageCount()); 215 | $this->assertSame(0, $attributes->getTotalReceived()); 216 | $this->assertSame(1, $attributes->getTotalSent()); 217 | } 218 | 219 | public function testSendMessageRealtime(): void 220 | { 221 | $rsmq = new RSMQClient(new Client(['host' => '127.0.0.1', 'port' => 6379]), 'rsmq', true); 222 | $rsmq->createQueue('foo'); 223 | $id = $rsmq->sendMessage('foo', 'foobar'); 224 | $this->assertSame(32, strlen($id)); 225 | } 226 | 227 | public function testSendMessageWithBigMessage(): void 228 | { 229 | $this->rsmq->createQueue('foo'); 230 | $bigStr = str_repeat(bin2hex(random_bytes(512)), 100); 231 | 232 | $this->expectExceptionMessage('Message too long'); 233 | $this->rsmq->sendMessage('foo', $bigStr); 234 | } 235 | 236 | public function testDeleteMessage(): void 237 | { 238 | $this->rsmq->createQueue('foo'); 239 | $id = $this->rsmq->sendMessage('foo', 'bar'); 240 | $this->assertTrue($this->rsmq->deleteMessage('foo', $id)); 241 | } 242 | 243 | public function testReceiveMessage(): void 244 | { 245 | $queue = 'foo'; 246 | $message = 'Hello World'; 247 | $this->rsmq->createQueue($queue); 248 | $id = $this->rsmq->sendMessage($queue, $message); 249 | $received = $this->rsmq->receiveMessage($queue); 250 | 251 | $this->assertSame($message, $received->getMessage()); 252 | $this->assertSame($id, $received->getId()); 253 | $this->assertNotEmpty($received->getFirstReceived()); 254 | $this->assertNotEmpty($received->getSent()); 255 | $this->assertSame(1, $received->getReceiveCount()); 256 | } 257 | 258 | public function testReceiveMessageWhenNoMessageExists(): void 259 | { 260 | $queue = 'foo'; 261 | $this->rsmq->createQueue($queue); 262 | $received = $this->rsmq->receiveMessage($queue); 263 | 264 | $this->assertEmpty($received); 265 | } 266 | 267 | public function testChangeMessageVisibility(): void 268 | { 269 | $queue = 'foo'; 270 | $this->rsmq->createQueue($queue); 271 | $id = $this->rsmq->sendMessage($queue, 'bar'); 272 | $this->assertTrue($this->rsmq->changeMessageVisibility($queue, $id, 60)); 273 | 274 | } 275 | 276 | public function testGetQueue(): void 277 | { 278 | $queueName = 'foo'; 279 | $vt = 30; 280 | $delay = 0; 281 | $maxSize = 65536; 282 | $this->rsmq->createQueue($queueName, $vt, $delay, $maxSize); 283 | $queue = $this->invokeMethod($this->rsmq, 'getQueue', [$queueName, true]); 284 | 285 | $this->assertSame($vt, $queue['vt']); 286 | $this->assertSame($delay, $queue['delay']); 287 | $this->assertSame($maxSize, $queue['maxsize']); 288 | $this->assertArrayHasKey('uid', $queue); 289 | $this->assertSame(32, strlen($queue['uid'])); 290 | } 291 | 292 | public function testGetQueueNotFound(): void 293 | { 294 | $this->expectExceptionMessage('Queue not found'); 295 | $this->invokeMethod($this->rsmq, 'getQueue', ['notfound']); 296 | } 297 | 298 | public function testPopMessage(): void 299 | { 300 | $queue = 'foo'; 301 | $message = 'bar'; 302 | $this->rsmq->createQueue($queue); 303 | 304 | $id = $this->rsmq->sendMessage($queue, $message); 305 | $received = $this->rsmq->popMessage($queue); 306 | 307 | $this->assertSame($id, $received->getId()); 308 | $this->assertSame($message, $received->getMessage()); 309 | } 310 | 311 | public function testPopMessageWhenNoMessageExists(): void 312 | { 313 | $queue = 'foo'; 314 | $this->rsmq->createQueue($queue); 315 | 316 | $received = $this->rsmq->popMessage($queue); 317 | 318 | $this->assertEmpty($received); 319 | 320 | } 321 | 322 | public function testSetQueueAttributes(): void 323 | { 324 | $queue = 'foo'; 325 | $vt = 100; 326 | $delay = 10; 327 | $maxsize = 2048; 328 | $this->rsmq->createQueue($queue); 329 | $attrs = $this->rsmq->setQueueAttributes($queue, $vt, $delay, $maxsize); 330 | 331 | $this->assertSame($vt, $attrs->getVt()); 332 | $this->assertSame($delay, $attrs->getDelay()); 333 | $this->assertSame($maxsize, $attrs->getMaxSize()); 334 | } 335 | 336 | public function tearDown(): void 337 | { 338 | try { 339 | $this->rsmq->deleteQueue('foo'); 340 | } catch (Exception $_) { 341 | 342 | } 343 | 344 | } 345 | } --------------------------------------------------------------------------------