feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+1377
View File
@@ -0,0 +1,1377 @@
## [4.16.5](https://github.com/OptimalBits/bull/compare/v4.16.4...v4.16.5) (2024-12-18)
### Bug Fixes
* upgrade cron-parser dependency for Luxon CVE-2023-22467 ([e45698e](https://github.com/OptimalBits/bull/commit/e45698eb91b91a4676f9bafdb7bdb35043d2316d))
## [4.16.4](https://github.com/OptimalBits/bull/compare/v4.16.3...v4.16.4) (2024-11-01)
### Bug Fixes
* **deps:** bump msgpackr to 1.1.2 to resolve ERR_BUFFER_OUT_OF_BOUNDS error ([#2783](https://github.com/OptimalBits/bull/issues/2783)) fixes [#2782](https://github.com/OptimalBits/bull/issues/2782) ([bc0ae0a](https://github.com/OptimalBits/bull/commit/bc0ae0ae5b7fb2b067de94c3e51ce1bdd609806d))
## [4.16.3](https://github.com/OptimalBits/bull/compare/v4.16.2...v4.16.3) (2024-09-10)
### Bug Fixes
* **metrics:** differentiate points in different minutes to be more accurate ([#2770](https://github.com/OptimalBits/bull/issues/2770)) ([fbf2fa3](https://github.com/OptimalBits/bull/commit/fbf2fa397c865e6620aac39b291704e486f424d2))
## [4.16.2](https://github.com/OptimalBits/bull/compare/v4.16.1...v4.16.2) (2024-09-05)
### Performance Improvements
* **metrics:** save zeros as much as max data points ([#2767](https://github.com/OptimalBits/bull/issues/2767)) ([3a09840](https://github.com/OptimalBits/bull/commit/3a098406a86573150ad13d1487d16211030c4200))
## [4.16.1](https://github.com/OptimalBits/bull/compare/v4.16.0...v4.16.1) (2024-08-28)
### Bug Fixes
* **metrics:** use batches include when collecting metrics ([#2765](https://github.com/OptimalBits/bull/issues/2765)) fixes [#2764](https://github.com/OptimalBits/bull/issues/2764) [#2763](https://github.com/OptimalBits/bull/issues/2763) ([8276f72](https://github.com/OptimalBits/bull/commit/8276f72b30a7abc2f3fb5509f980d931027d0f0c))
# [4.16.0](https://github.com/OptimalBits/bull/compare/v4.15.1...v4.16.0) (2024-07-30)
### Features
* **job:** support debouncing ([#2760](https://github.com/OptimalBits/bull/issues/2760)) ([603befe](https://github.com/OptimalBits/bull/commit/603befe439fb4e318137b88550ab4d0c6c2bbcb3))
## [4.15.1](https://github.com/OptimalBits/bull/compare/v4.15.0...v4.15.1) (2024-07-04)
### Bug Fixes
* **job:** check jobKey when saving stacktrace ([#2755](https://github.com/OptimalBits/bull/issues/2755)) ([96675f5](https://github.com/OptimalBits/bull/commit/96675f51d1a9e5a30df0a0fe24a40a2d14078203))
# [4.15.0](https://github.com/OptimalBits/bull/compare/v4.14.0...v4.15.0) (2024-06-30)
### Features
* **queue:** emit internal duplicated event ([#2754](https://github.com/OptimalBits/bull/issues/2754)) ([021ab7f](https://github.com/OptimalBits/bull/commit/021ab7fd8d6c89b01767572b0e53422f34f7bc04))
# [4.14.0](https://github.com/OptimalBits/bull/compare/v4.13.1...v4.14.0) (2024-06-25)
### Features
* **queue:** add global:duplicated event when a duplicated is added ([#2749](https://github.com/OptimalBits/bull/issues/2749)) ([d632ac1](https://github.com/OptimalBits/bull/commit/d632ac1010d7d7faaa8f731eafdea67d6129e111))
## [4.13.1](https://github.com/OptimalBits/bull/compare/v4.13.0...v4.13.1) (2024-06-21)
### Bug Fixes
* **priority:** consider paused state when calling getCountsPerPriority ([#2748](https://github.com/OptimalBits/bull/issues/2748)) ([6c2719a](https://github.com/OptimalBits/bull/commit/6c2719a9292bf0a11136d575d333600be0b9a422))
# [4.13.0](https://github.com/OptimalBits/bull/compare/v4.12.9...v4.13.0) (2024-06-12)
### Features
* **queue:** add getCountsPerPriority method ([#2746](https://github.com/OptimalBits/bull/issues/2746)) ([0376dcc](https://github.com/OptimalBits/bull/commit/0376dcc128d9af13ecbd658d8ea7ff19fce56915))
## [4.12.9](https://github.com/OptimalBits/bull/compare/v4.12.8...v4.12.9) (2024-05-24)
### Bug Fixes
* **retry-job:** throw error when job is not in active state ([#2741](https://github.com/OptimalBits/bull/issues/2741)) ([c29e3b0](https://github.com/OptimalBits/bull/commit/c29e3b061a168c3d40a336a733c382bab14caa57))
## [4.12.8](https://github.com/OptimalBits/bull/compare/v4.12.7...v4.12.8) (2024-05-22)
### Bug Fixes
* **move-to-finished:** throw error when job is not in active state ([#2739](https://github.com/OptimalBits/bull/issues/2739)) ([7b12be1](https://github.com/OptimalBits/bull/commit/7b12be13ea39e309d6a6a39a2e56f646f06fdfa8))
## [4.12.7](https://github.com/OptimalBits/bull/compare/v4.12.6...v4.12.7) (2024-05-21)
### Bug Fixes
* **scripts:** throw error when moving non-active job to delayed ([#2740](https://github.com/OptimalBits/bull/issues/2740)) ([63636b1](https://github.com/OptimalBits/bull/commit/63636b181d0a166b1702059e5abb53ce1589d90f))
## [4.12.6](https://github.com/OptimalBits/bull/compare/v4.12.5...v4.12.6) (2024-05-18)
### Bug Fixes
* **stalled:** take in count removeOnFail option ([#2734](https://github.com/OptimalBits/bull/issues/2734)) ([2112269](https://github.com/OptimalBits/bull/commit/21122697461b551055192ff3b9c02c6f37cb331e))
## [4.12.5](https://github.com/OptimalBits/bull/compare/v4.12.4...v4.12.5) (2024-05-17)
### Bug Fixes
* **job:** validate job existence when adding log ([#2738](https://github.com/OptimalBits/bull/issues/2738)) ([1fb1562](https://github.com/OptimalBits/bull/commit/1fb15628dc5912e83220d430aa14e19e90d1cc53))
## [4.12.4](https://github.com/OptimalBits/bull/compare/v4.12.3...v4.12.4) (2024-05-15)
### Bug Fixes
* **retry-job:** consider priority ([#2737](https://github.com/OptimalBits/bull/issues/2737)) fixes [#1755](https://github.com/OptimalBits/bull/issues/1755) ([09ce146](https://github.com/OptimalBits/bull/commit/09ce146563871519cda638bafa82ce6af34bdd25))
## [4.12.3](https://github.com/OptimalBits/bull/compare/v4.12.2...v4.12.3) (2024-05-10)
### Bug Fixes
* **job:** validate jobKey in updateProgress and update ([#2730](https://github.com/OptimalBits/bull/issues/2730)) ([6d84156](https://github.com/OptimalBits/bull/commit/6d8415696606d3b7ec891f7fca9ab0508923c321))
### Performance Improvements
* **scripts:** remove token after moving to wait or delayed ([#2731](https://github.com/OptimalBits/bull/issues/2731)) ([7ee8f74](https://github.com/OptimalBits/bull/commit/7ee8f7430a68492c9ce768e7108443592f49d74c))
## [4.12.2](https://github.com/OptimalBits/bull/compare/v4.12.1...v4.12.2) (2024-01-17)
### Bug Fixes
* **dependencies:** upgrade msgpackr ([cc83ae2](https://github.com/OptimalBits/bull/commit/cc83ae297e96344f94039401be5097d7f05ab10b))
## [4.12.1](https://github.com/OptimalBits/bull/compare/v4.12.0...v4.12.1) (2024-01-15)
### Bug Fixes
* **deps:** bump msgpackr from 1.5.2 to 1.10.1 ([#2697](https://github.com/OptimalBits/bull/issues/2697)) ([b27c90d](https://github.com/OptimalBits/bull/commit/b27c90d8b106cc5319e712df2386ddc35946ec3d))
# [4.12.0](https://github.com/OptimalBits/bull/compare/v4.11.5...v4.12.0) (2023-12-18)
### Features
* add missing extendLock definition ([14432ff](https://github.com/OptimalBits/bull/commit/14432ff8a5e743d97c82a360c9d6a92204b1a684))
## [4.11.5](https://github.com/OptimalBits/bull/compare/v4.11.4...v4.11.5) (2023-11-11)
### Bug Fixes
* pass redis string as opts into queue ([e94f568](https://github.com/OptimalBits/bull/commit/e94f568085de079fc42b876233a060ba11ec946e))
## [4.11.4](https://github.com/OptimalBits/bull/compare/v4.11.3...v4.11.4) (2023-10-14)
### Bug Fixes
* catch pause errors when closing ([ccb6cc7](https://github.com/OptimalBits/bull/commit/ccb6cc7ecca8c726fc14509536831f665ac49701))
## [4.11.3](https://github.com/OptimalBits/bull/compare/v4.11.2...v4.11.3) (2023-08-11)
### Bug Fixes
* **types:** make repeat option key optional ([934ec98](https://github.com/OptimalBits/bull/commit/934ec9875c6e04c3e771b6c6ba212e3693d3a25b))
## [4.11.2](https://github.com/OptimalBits/bull/compare/v4.11.1...v4.11.2) (2023-08-08)
### Bug Fixes
* **worker:** better client name support ([5910f44](https://github.com/OptimalBits/bull/commit/5910f44b03a264d979c8ade54d64d13fdc908b51))
## [4.11.1](https://github.com/OptimalBits/bull/compare/v4.11.0...v4.11.1) (2023-08-08)
### Bug Fixes
* **queue:** deep clone opts ([#2634](https://github.com/OptimalBits/bull/issues/2634)) fixes [#2633](https://github.com/OptimalBits/bull/issues/2633) ([35f1da3](https://github.com/OptimalBits/bull/commit/35f1da3cf631bee97e96a774d9f1127466e7a66a))
# [4.11.0](https://github.com/OptimalBits/bull/compare/v4.10.4...v4.11.0) (2023-08-08)
### Bug Fixes
* add mising getMetrics type ([#2640](https://github.com/OptimalBits/bull/issues/2640)) ([a217a7d](https://github.com/OptimalBits/bull/commit/a217a7d56d52385eb56ffe386b7503eca9a24604))
* remove deprecated debuglog ([4ce36fe](https://github.com/OptimalBits/bull/commit/4ce36febe3a63a45198e2fe24b46fc371ee3f6e5))
* **types:** add missing keys to repeat opts ([e4e6457](https://github.com/OptimalBits/bull/commit/e4e64572a3ad259d9cb90d5dec81e8565eeadca1))
* **types:** rename strategyOptions to options to reflect js file ([bae6427](https://github.com/OptimalBits/bull/commit/bae6427ce9d9fac26b198402068bd84647fd8208))
* **typings:** return type of getJobCountByTypes ([#2622](https://github.com/OptimalBits/bull/issues/2622)) ([47722ed](https://github.com/OptimalBits/bull/commit/47722ed791429b087128ce5f35847663b2d8fc9c))
* **worker:** high-memory-usage-when-providing-float-to-concurrency ([#2620](https://github.com/OptimalBits/bull/issues/2620)) ([dcca1e8](https://github.com/OptimalBits/bull/commit/dcca1e8c39b121fb01ac299bec30a3d011059c1f))
* change option name to match ts declaration ([909a07e](https://github.com/OptimalBits/bull/commit/909a07e27075a63b9ca178a3074b0b5c80d86355))
* ts declaration metrics option and getMetrics function ([11331b7](https://github.com/OptimalBits/bull/commit/11331b718a8e534ac6822917a536eab32b10446b))
### Features
* upgrade ioredis to 5.3.2 ([e1883f0](https://github.com/OptimalBits/bull/commit/e1883f01c2cb23a51b5485ef8048c4268ee968ea))
## [4.10.4](https://github.com/OptimalBits/bull/compare/v4.10.3...v4.10.4) (2023-02-09)
### Bug Fixes
* **retry:** handle pause queue status ([9f945d6](https://github.com/OptimalBits/bull/commit/9f945d60c69e8b5d7b46f58189a1c49a83897099))
## [4.10.3](https://github.com/OptimalBits/bull/compare/v4.10.2...v4.10.3) (2023-02-03)
### Bug Fixes
* don't reschedule delay timer if closing ([#2535](https://github.com/OptimalBits/bull/issues/2535)) ([8a0292a](https://github.com/OptimalBits/bull/commit/8a0292a574df82a62d718e13d8995800fd8529d0))
## [4.10.2](https://github.com/OptimalBits/bull/compare/v4.10.1...v4.10.2) (2022-11-24)
### Bug Fixes
* **queue:** throw error when needed instead of hiding it in a closure ([8a742c1](https://github.com/OptimalBits/bull/commit/8a742c1176e7147e2069602f18089d8becb4cb15))
## [4.10.1](https://github.com/OptimalBits/bull/compare/v4.10.0...v4.10.1) (2022-10-13)
### Bug Fixes
* support for instantiation using redisUrl ([6288f7d](https://github.com/OptimalBits/bull/commit/6288f7de9b82e712e480510eb10c03bd4d1cd24e))
# [4.10.0](https://github.com/OptimalBits/bull/compare/v4.9.0...v4.10.0) (2022-09-29)
### Features
* **types:** add typescript types to package ([e793f8d](https://github.com/OptimalBits/bull/commit/e793f8d1502bc5ed4a2e15087dc048c18e5e8644))
# [4.9.0](https://github.com/OptimalBits/bull/compare/v4.8.5...v4.9.0) (2022-09-05)
### Features
* support .cjs files ([75e6775](https://github.com/OptimalBits/bull/commit/75e6775fc3720563aac9cd9a07d1722dfbdfa177))
## [4.8.5](https://github.com/OptimalBits/bull/compare/v4.8.4...v4.8.5) (2022-07-27)
### Performance Improvements
* **clean:** use ZRANGEBYSCORE to improve performance ([#2363](https://github.com/OptimalBits/bull/issues/2363)) ([3331188](https://github.com/OptimalBits/bull/commit/3331188bce510e0bb4749d92cb63f4c73203d076))
## [4.8.4](https://github.com/OptimalBits/bull/compare/v4.8.3...v4.8.4) (2022-06-16)
### Bug Fixes
* **worker:** better disconnect when blocking connection ([b9ea7f4](https://github.com/OptimalBits/bull/commit/b9ea7f4780948d4556548e6bf13e2c3271939d12))
## [4.8.3](https://github.com/OptimalBits/bull/compare/v4.8.2...v4.8.3) (2022-05-12)
### Bug Fixes
* **stalled-jobs:** move stalled jobs to wait in batches ([2f1fb6c](https://github.com/OptimalBits/bull/commit/2f1fb6cdc1329f98b2dd30e847b5a79839db0346))
## [4.8.2](https://github.com/OptimalBits/bull/compare/v4.8.1...v4.8.2) (2022-04-21)
### Bug Fixes
* unlock job when moving it to delayed ([#2329](https://github.com/OptimalBits/bull/issues/2329)) ([11eae6b](https://github.com/OptimalBits/bull/commit/11eae6b960c83b47dadb22991b2b3e239c177508))
## [4.8.1](https://github.com/OptimalBits/bull/compare/v4.8.0...v4.8.1) (2022-03-21)
### Performance Improvements
* speed up clean operation ([#2326](https://github.com/OptimalBits/bull/issues/2326)) ([ef5f471](https://github.com/OptimalBits/bull/commit/ef5f4717258940042fa11f980622034cde765860))
# [4.8.0](https://github.com/OptimalBits/bull/compare/v4.7.0...v4.8.0) (2022-03-19)
### Features
* have Queue#clean consult job.{finishedOn,processedOn,timestamp} ([#2309](https://github.com/OptimalBits/bull/issues/2309)) ([b7058e6](https://github.com/OptimalBits/bull/commit/b7058e6e8f4bb56febebaf9452f993883240fa9d))
# [4.7.0](https://github.com/OptimalBits/bull/compare/v4.6.2...v4.7.0) (2022-03-02)
### Features
* **metrics:** add support for collecting queue metrics ([886d764](https://github.com/OptimalBits/bull/commit/886d7643819dcf52902d2e92394267dbd495c71b))
## [4.6.2](https://github.com/OptimalBits/bull/compare/v4.6.1...v4.6.2) (2022-02-23)
### Bug Fixes
* better handling of maxRetriesPerRequest ([d3b9138](https://github.com/OptimalBits/bull/commit/d3b91386e30d7205efdc19bcd18fe1e5fefa3542))
## [4.6.1](https://github.com/OptimalBits/bull/compare/v4.6.0...v4.6.1) (2022-02-21)
### Bug Fixes
* **sandbox:** better error reporting broken processor file ([10db479](https://github.com/OptimalBits/bull/commit/10db479731bcb8ba27c3a0e2dd4094c8e9ff1c57))
# [4.6.0](https://github.com/OptimalBits/bull/compare/v4.5.6...v4.6.0) (2022-02-21)
### Features
* handle redis uri queries ([54e5463](https://github.com/OptimalBits/bull/commit/54e5463bffee1b8b56b460460c79d8751142d859))
## [4.5.6](https://github.com/OptimalBits/bull/compare/v4.5.5...v4.5.6) (2022-02-20)
### Bug Fixes
* **sandbox:** wait for result of sending start command ([232ed85](https://github.com/OptimalBits/bull/commit/232ed85d4c980d94dee5d9e4c5b6f8758dbb82d9))
## [4.5.5](https://github.com/OptimalBits/bull/compare/v4.5.4...v4.5.5) (2022-02-16)
### Bug Fixes
* **worker:** better closing when disconnected ([41b9404](https://github.com/OptimalBits/bull/commit/41b940457b3447619c3c2887674a8cebf1508b07))
## [4.5.4](https://github.com/OptimalBits/bull/compare/v4.5.3...v4.5.4) (2022-02-14)
### Bug Fixes
* **queue:** return correct workers with getWorkers() ([193644c](https://github.com/OptimalBits/bull/commit/193644c5ed290901448f8c35ede99e1063a90f4a))
## [4.5.3](https://github.com/OptimalBits/bull/compare/v4.5.2...v4.5.3) (2022-02-14)
### Bug Fixes
* **commands:** do not wait for redis to load commands ([ad7b647](https://github.com/OptimalBits/bull/commit/ad7b6474db426b4970a0d1d3ddb8a032a22c481b))
## [4.5.2](https://github.com/OptimalBits/bull/compare/v4.5.1...v4.5.2) (2022-02-14)
### Bug Fixes
* **scripts:** make it easier for tools like vercel to find the .lua scripts ([8ab5b1d](https://github.com/OptimalBits/bull/commit/8ab5b1d1c3eecf41b20d97f464030377ece01640))
## [4.5.1](https://github.com/OptimalBits/bull/compare/v4.5.0...v4.5.1) (2022-02-06)
### Bug Fixes
* **sandbox:** broken processor files should fail jobs ([dd0b853](https://github.com/OptimalBits/bull/commit/dd0b853a516c32baac535395377a19e361964dc8))
# [4.5.0](https://github.com/OptimalBits/bull/compare/v4.4.0...v4.5.0) (2022-02-01)
### Features
* **queue:** add retryJobs for failed status ([501b2cc](https://github.com/OptimalBits/bull/commit/501b2cc49ccd0d7de82fea50144b52fa9879d1f5))
# [4.4.0](https://github.com/OptimalBits/bull/compare/v4.3.0...v4.4.0) (2022-01-26)
### Features
* add support for removeOn based on time ([90f040c](https://github.com/OptimalBits/bull/commit/90f040c052325da302c99f17111a12d1afbe88bd))
# [4.3.0](https://github.com/OptimalBits/bull/compare/v4.2.1...v4.3.0) (2022-01-26)
### Features
* upgrade cron version enabling new cron expressions ([79337a3](https://github.com/OptimalBits/bull/commit/79337a30758ea7e6d6b1536eb0edebcd0b3c8274))
## [4.2.1](https://github.com/OptimalBits/bull/compare/v4.2.0...v4.2.1) (2022-01-17)
### Bug Fixes
* **sandbox:** exit if uncaughtException ([43dc2e6](https://github.com/OptimalBits/bull/commit/43dc2e69dfa0cbaf960ce6f1bd6c4125b1052ff9))
# [4.2.0](https://github.com/OptimalBits/bull/compare/v4.1.4...v4.2.0) (2021-12-21)
### Features
* **queue:** enabled queues to share childPool instance ([#2237](https://github.com/OptimalBits/bull/issues/2237)) ([16fdbe9](https://github.com/OptimalBits/bull/commit/16fdbe90a05ae2e2bcb134a550a97dc84dfe573e))
## [4.1.4](https://github.com/OptimalBits/bull/compare/v4.1.3...v4.1.4) (2021-12-14)
### Bug Fixes
* **queue:** check redisOptions is available fixes [#2186](https://github.com/OptimalBits/bull/issues/2186) ([071c51d](https://github.com/OptimalBits/bull/commit/071c51d16db58ed9f71138058dbbc25f73383e56))
## [4.1.3](https://github.com/OptimalBits/bull/compare/v4.1.2...v4.1.3) (2021-12-14)
### Bug Fixes
* typo on url ([#2195](https://github.com/OptimalBits/bull/issues/2195)) ([7e7d9cb](https://github.com/OptimalBits/bull/commit/7e7d9cb58d7cb5c1a92020f7c4333932526d8e98))
## [4.1.2](https://github.com/OptimalBits/bull/compare/v4.1.1...v4.1.2) (2021-12-14)
### Performance Improvements
* speed up performance of queue.clean when called with a limit ([#2205](https://github.com/OptimalBits/bull/issues/2205)) ([c20e469](https://github.com/OptimalBits/bull/commit/c20e469dcd71fd13e23e922f2720f55450311d22))
## [4.1.1](https://github.com/OptimalBits/bull/compare/v4.1.0...v4.1.1) (2021-11-16)
### Bug Fixes
* **emit:** protect emit calls fixes [#2213](https://github.com/OptimalBits/bull/issues/2213) ([4978a2b](https://github.com/OptimalBits/bull/commit/4978a2b40ee840ba91e0939e86e1e3b15e8b16e9))
# [4.1.0](https://github.com/OptimalBits/bull/compare/v4.0.0...v4.1.0) (2021-10-31)
### Features
* emit event on job lock extend failure ([7247b3b](https://github.com/OptimalBits/bull/commit/7247b3bb9741c5eb18ce4027ea14349cbc8504c5))
# [4.0.0](https://github.com/OptimalBits/bull/compare/v3.29.3...v4.0.0) (2021-10-27)
### Bug Fixes
* force options to guarantee correct reconnects ([3ade8e6](https://github.com/OptimalBits/bull/commit/3ade8e6727d7b906a30b09bccb6dc10d76ed1b5f))
### BREAKING CHANGES
* If redis opts are missing:
{ maxRetriesPerRequest: null,
enableReadyCheck: false }
then a exception will be thrown.
## [3.29.3](https://github.com/OptimalBits/bull/compare/v3.29.2...v3.29.3) (2021-10-13)
### Bug Fixes
* **name-processors:** wait for all processors when closing fixes [#1618](https://github.com/OptimalBits/bull/issues/1618) ([79ce013](https://github.com/OptimalBits/bull/commit/79ce013af695f96ff57106b213982647e0783d3f))
## [3.29.2](https://github.com/OptimalBits/bull/compare/v3.29.1...v3.29.2) (2021-09-08)
### Bug Fixes
* **connection:** fail only if redis connection does not recover ([0ca4c6b](https://github.com/OptimalBits/bull/commit/0ca4c6b4d57efa78e5ca484cb8bed2a6961646a3))
## [3.29.1](https://github.com/OptimalBits/bull/compare/v3.29.0...v3.29.1) (2021-08-26)
### Bug Fixes
* protect getJob with isReady, fixes [#1386](https://github.com/OptimalBits/bull/issues/1386) ([2f27faa](https://github.com/OptimalBits/bull/commit/2f27faa410f70504f24df2ebd6bb7831df21660d))
# [3.29.0](https://github.com/OptimalBits/bull/compare/v3.28.1...v3.29.0) (2021-08-20)
### Features
* **jobs:** add extendLock method ([30d5959](https://github.com/OptimalBits/bull/commit/30d59590c34cb664e3b9a62695c4092c9b1ae3f3))
## [3.28.1](https://github.com/OptimalBits/bull/compare/v3.28.0...v3.28.1) (2021-08-06)
### Bug Fixes
* **queue:** changed user prop to username for redisOptions ([71baea9](https://github.com/OptimalBits/bull/commit/71baea91de702d9bd1d5516f09b22599b7f13045))
# [3.28.0](https://github.com/OptimalBits/bull/compare/v3.27.0...v3.28.0) (2021-08-05)
### Features
* **queue:** handle redis url containing username ([a245fc4](https://github.com/OptimalBits/bull/commit/a245fc403827fd22b08dbe1499fe843eb1ad633a))
# [3.27.0](https://github.com/OptimalBits/bull/compare/v3.26.0...v3.27.0) (2021-07-27)
### Features
* support job.discard function in sandboxed processors ([5adcf2c](https://github.com/OptimalBits/bull/commit/5adcf2ceed263d46089cceefcdcaa658151c53b7))
# [3.26.0](https://github.com/OptimalBits/bull/compare/v3.25.2...v3.26.0) (2021-07-16)
### Features
* **repeatable:** store key in repeat options ([dab0d82](https://github.com/OptimalBits/bull/commit/dab0d8266174f1d25ec914cff8450594d85db511))
## [3.25.2](https://github.com/OptimalBits/bull/compare/v3.25.1...v3.25.2) (2021-07-16)
### Bug Fixes
* **repeatable:** honor endDate fixes [#1573](https://github.com/OptimalBits/bull/issues/1573) ([7f0db0e](https://github.com/OptimalBits/bull/commit/7f0db0e293367752971be8a1bf7b6c8cf4190350))
## [3.25.1](https://github.com/OptimalBits/bull/compare/v3.25.0...v3.25.1) (2021-07-16)
### Bug Fixes
* error when .lua scripts missing in built bundle ([85307c3](https://github.com/OptimalBits/bull/commit/85307c3a8e48b910590b2682700c5062c84d32b0))
# [3.25.0](https://github.com/OptimalBits/bull/compare/v3.24.0...v3.25.0) (2021-07-15)
### Features
* pass clientName to createClient function ([2a29569](https://github.com/OptimalBits/bull/commit/2a295691b88318ff64beaa7b83e03487854a7fe4))
# [3.24.0](https://github.com/OptimalBits/bull/compare/v3.23.3...v3.24.0) (2021-07-15)
### Features
* **backoff:** add option to specify options for custom backoff strategy ([e573010](https://github.com/OptimalBits/bull/commit/e5730107688a41c7268a717f16302c5959eaf6f6))
## [3.23.3](https://github.com/OptimalBits/bull/compare/v3.23.2...v3.23.3) (2021-07-15)
### Bug Fixes
* **delayed:** do not get lock twice fixes [#2033](https://github.com/OptimalBits/bull/issues/2033) ([2800cf8](https://github.com/OptimalBits/bull/commit/2800cf8923ccac52dcd4da4e7fd19b3404c68fe8))
## [3.23.2](https://github.com/OptimalBits/bull/compare/v3.23.1...v3.23.2) (2021-07-15)
### Bug Fixes
* **job:** add default err object fixes [#2029](https://github.com/OptimalBits/bull/issues/2029) ([39684e9](https://github.com/OptimalBits/bull/commit/39684e9f941e2ea4191809fdc2aa52b3d7b267ae))
## [3.23.1](https://github.com/OptimalBits/bull/compare/v3.23.0...v3.23.1) (2021-07-15)
### Bug Fixes
* wait in queue to be ready in getNextJob fixes [#1852](https://github.com/OptimalBits/bull/issues/1852) ([4e224e5](https://github.com/OptimalBits/bull/commit/4e224e5533f729b9781b1db81e1875b1bd50afb0))
# [3.23.0](https://github.com/OptimalBits/bull/compare/v3.22.12...v3.23.0) (2021-07-13)
### Features
* support job.update function in sandboxed processors ([ff79fb4](https://github.com/OptimalBits/bull/commit/ff79fb494ea3e123256b35d18b46b24fbb9b8365)), closes [#1279](https://github.com/OptimalBits/bull/issues/1279) [#1608](https://github.com/OptimalBits/bull/issues/1608) [#1056](https://github.com/OptimalBits/bull/issues/1056)
## [3.22.12](https://github.com/OptimalBits/bull/compare/v3.22.11...v3.22.12) (2021-07-13)
### Bug Fixes
* remove stalled job when finishing fixes [#1600](https://github.com/OptimalBits/bull/issues/1600) ([90763fd](https://github.com/OptimalBits/bull/commit/90763fd66404d8bc5a47ff8555cbe2da776c030d))
## [3.22.11](https://github.com/OptimalBits/bull/compare/v3.22.10...v3.22.11) (2021-07-08)
### Bug Fixes
* **close:** clear timers after waiting jobs fixes [#1415](https://github.com/OptimalBits/bull/issues/1415) ([77b319d](https://github.com/OptimalBits/bull/commit/77b319da6acaa93351830f0a4e38b5ad1d3d8cf5))
## [3.22.10](https://github.com/OptimalBits/bull/compare/v3.22.9...v3.22.10) (2021-07-01)
### Bug Fixes
* deep clone options ([#2083](https://github.com/OptimalBits/bull/issues/2083)) ([1e00a90](https://github.com/OptimalBits/bull/commit/1e00a90d1216083cdf33bc78a9753ece1acdddb2))
## [3.22.9](https://github.com/OptimalBits/bull/compare/v3.22.8...v3.22.9) (2021-06-22)
### Bug Fixes
* **reprocess:** do not store job.id in added list ([3fbc506](https://github.com/OptimalBits/bull/commit/3fbc506aee4c36fd612ed18fc3a1619c0c8069d8))
## [3.22.8](https://github.com/OptimalBits/bull/compare/v3.22.7...v3.22.8) (2021-06-09)
### Bug Fixes
* upgrade ioredis fixes [#1445](https://github.com/OptimalBits/bull/issues/1445) ([f6a2364](https://github.com/OptimalBits/bull/commit/f6a23648146f5a53bb2e4084f8de9b783b782bed))
## [3.22.7](https://github.com/OptimalBits/bull/compare/v3.22.6...v3.22.7) (2021-05-31)
### Bug Fixes
* **obliterate:** remove job logs fixes [#2050](https://github.com/OptimalBits/bull/issues/2050) ([6ccf2b9](https://github.com/OptimalBits/bull/commit/6ccf2b90892bf16b4edb0095d6e770f978bc660a))
## [3.22.6](https://github.com/OptimalBits/bull/compare/v3.22.5...v3.22.6) (2021-05-17)
### Bug Fixes
* **job:** fix job log pagination, fixes [#2031](https://github.com/OptimalBits/bull/issues/2031) ([30aa0a9](https://github.com/OptimalBits/bull/commit/30aa0a99acb4a04a12f988840bc8ccc7a014fed3))
## [3.22.5](https://github.com/OptimalBits/bull/compare/v3.22.4...v3.22.5) (2021-05-11)
### Bug Fixes
* emit failed event when stalled job fails ([f68da41](https://github.com/OptimalBits/bull/commit/f68da4176658b9935cf4b63b218130008619b25b))
## [3.22.4](https://github.com/OptimalBits/bull/compare/v3.22.3...v3.22.4) (2021-04-27)
### Bug Fixes
* also populate retriedOn when loading from id with excludeData ([0964b39](https://github.com/OptimalBits/bull/commit/0964b390d9190510f0d016f4eb2a3f1bc6cdb4e5))
## [3.22.3](https://github.com/OptimalBits/bull/compare/v3.22.2...v3.22.3) (2021-04-23)
### Bug Fixes
* **delayed:** re-schedule updateDelay in case of error fixes [#2015](https://github.com/OptimalBits/bull/issues/2015) ([16bbfad](https://github.com/OptimalBits/bull/commit/16bbfadb270bc6c7d6df9cf5ab30b7f66028b2b3))
## [3.22.2](https://github.com/OptimalBits/bull/compare/v3.22.1...v3.22.2) (2021-04-23)
### Bug Fixes
* **obliterate:** obliterate many jobs fixes [#2016](https://github.com/OptimalBits/bull/issues/2016) ([7a923b4](https://github.com/OptimalBits/bull/commit/7a923b468d5299bbdfe06d1ee7447fd810e2779b))
## v.3.22.1
- fix(obliterate): remove repeatable jobs fixes #2012
[Changes](https://github.com/OptimalBits/bull/compare/v3.22.0...v3.22.1)
## v.3.22.0
- feat: do not rely on comma to encode jobid in progress fixes #2003
[Changes](https://github.com/OptimalBits/bull/compare/v3.21.1...v3.22.0)
## v.3.21.1
- fix: safer implementation of obliterate.
Note: If you want to use the new method "obliterate" it is highly recommended
that you upgrade to this version, since previous version is not safe to use
if using the colon ':' character in your queue names.
[Changes](https://github.com/OptimalBits/bull/compare/v3.21.0...v3.21.1)
## v.3.21.0
- feat: add a method to "obliterate" a queue
- fix: upgrade lodash fixes #1996
[Changes](https://github.com/OptimalBits/bull/compare/v3.20.1...v3.21.0)
## v.3.20.1
- fix(queue): possible infinite loop when disconnect fixes #1746
- fix(clean-priority): remove job from priority set on clean (#1405)
- fix(sandbox): job update (#1957)
- fix: use async version of process.send for progress and log (#1948)
- fix: promote jobs to the right "list" when paused
[Changes](https://github.com/OptimalBits/bull/compare/v3.20.0...v3.20.1)
## v.3.20.0
- feat(job): implement Job#retriedOn (#1868)
- fix: job default opts fixes #1904
[Changes](https://github.com/OptimalBits/bull/compare/v3.19.1...v3.20.0)
## v.3.19.1
- fix(getters): properly zip hmget result
[Changes](https://github.com/OptimalBits/bull/compare/v3.19.0...v3.19.1)
## v.3.19.0
- feat: add option to exclude data on getters (#1910)
- fix: lock ioredis to 4.18.0 to avoid breakage with newer 4.19.0+.
[Changes](https://github.com/OptimalBits/bull/compare/v3.18.1...v3.19.0)
## v.3.18.1
- fix(repeat): remove last delayed job.
- fix(rate-limiter): increment jobCounter only when a job is processed. fixes #1875.
- fix(sandbox): clear dangling timeout.
[Changes](https://github.com/OptimalBits/bull/compare/v3.18.0...v3.18.1)
## v.3.18.0
- feat: make pause forward compatible with bullmq (#1818) (@manast)
[Changes](https://github.com/OptimalBits/bull/compare/v3.17.0...v3.18.0)
## v.3.17.0
- feat: better rate limiter (#1816) (@manast)
- feat(sandbox): kill child workers gracefully (#1802) (@GabrielCastro)
[Changes](https://github.com/OptimalBits/bull/compare/v3.16.0...v3.17.0)
## v.3.16.0
- feat(rate-limiter): add grouping support.
[Changes](https://github.com/OptimalBits/bull/compare/v3.15.0...v3.16.0)
## v.3.15.0
- feat: add isPaused fixes #1274
- fix: emit waiting event when adding a priority job (#1134)
[Changes](https://github.com/OptimalBits/bull/compare/v3.14.0...v3.15.0)
## v.3.14.0
- feat(queue): add removeJobs function
- fix: clamp negative job delay values to 0 to prevent thrashing
- fix: use DEFAULT_JOB_NAME (#1585)
- fix: remove the lazy client error handler on close (#1605)
- fix: prevent exceeding the maximum stack call size when emptying large queues (#1660)
[Changes](https://github.com/OptimalBits/bull/compare/v3.13.0...v3.14.0)
## v.3.13.0
- feat: add "preventParsingData" job option to prevent data parsing
- fix: queue.clean clean job logs as well
- fix: whenCurrentJobsFinished should wait for all jobs
[Changes](https://github.com/OptimalBits/bull/compare/v3.12.1...v3.13.0)
## v.3.12.1
- fix: catch errors parsing invalid progress data
- fix(pause): don't initialize bclient if not waiting for jobs to finish
[Changes](https://github.com/OptimalBits/bull/compare/v3.12.0...v3.12.1)
## v.3.12.0
- feat: support async custom backoffs.
- feat(sandbox): emulate job.progress function.
- fix: queue.pause(true, true) doesn't pause queue.
[Changes](https://github.com/OptimalBits/bull/compare/v3.11.0...v3.12.0)
## v.3.11.0
- feat(queue): basic support for bulk adding jobs.
- feat(job): save data on job instance when updated.
- fix(queue): whenCurrentJobsFinished shouldn't initialize bclient. Fixes #1346.
- fix(queue): unhandled promise warning in updateDelayTimer.
- fix(sandbox): if the child process is killed, remove it from the pool.
[Changes](https://github.com/OptimalBits/bull/compare/v3.10.0...v3.11.0)
## v.3.10.0
- fix: remove logs automtically when using autoremove fixes #1330
- feat: add support for keeping a specified number of jobs when using auto-remove.
- feat: add support for node 12
- fix: fix check for supported file types in sandboxed processors #1311
- ci: drop support for node 6
[Changes](https://github.com/OptimalBits/bull/compare/v3.9.1...v3.10.0)
## v.3.9.1
- fix: add log to job wrapper
[Changes](https://github.com/OptimalBits/bull/compare/v3.9.0...v3.9.1)
## v.3.9.0
- feat: add job.log #1165
[Changes](https://github.com/OptimalBits/bull/compare/v3.8.1...v3.9.0)
## v.3.8.1
- fix: wait for ready in cleanJobsInSet fixes #1298
[Changes](https://github.com/OptimalBits/bull/compare/v3.8.0...v3.8.1)
## v.3.8.0
- fix: improve delay logic fixing #1226, #1222
- feat: store finishedOn on the job instance
- fix: return every in getRepeatableJobs #1284
- fix: remove broken child processes #1098
- feat: update sandbox exit handler to log signals #1252
- fix: Ignore unknown command client error #1240
[Changes](https://github.com/OptimalBits/bull/compare/v3.7.0...v3.8.0)
## v.3.7.0
- perf: improve update delay set logic.
- feat: consider priority when promoting a job #1205.
- fix: better delay for rate limited jobs.
- feat: update port selection mechanism for child node process inspect flag.
[Changes](https://github.com/OptimalBits/bull/compare/v3.6.0...v3.7.0)
## v.3.6.0
- feat: add function to remove repeatable jobs by key.
- fix: properly remove sandbox events. Fixes #1179.
- fix: make progress functions in sandbox consistently return a promise.
[Changes](https://github.com/OptimalBits/bull/compare/v3.5.3...v3.6.0)
## v.3.5.3
- chore: upgrade ioredis to ^4.5.1.
- fix: fix #1044 support for typescript processors.
- chore: remove bluebird.
- chore: use lockKey method instead of private property.
- fix(queue): convert delay setting to number.
[Changes](https://github.com/OptimalBits/bull/compare/v3.5.2...v3.5.3)
## v.3.5.2
- chore(queue): remove bluebird config from the codebase.
[Changes](https://github.com/OptimalBits/bull/compare/v3.5.1...v3.5.2)
## v.3.5.1
- chore(yarn): updated yarn.lock
[Changes](https://github.com/OptimalBits/bull/compare/v3.5.0...v3.5.1)
## v.3.5.0
- fix(delayed): pause delayed jobs #1087
- fix(lua): correct numJobs fetch in moveToActive
- perf(moveToActive): used local var for rate limiter
- perf(queue): replace bluebird by native promises where possible
- chore(queue): fix typo in forcedReconnection variable
- feat(child-processes): catch sub process crashes
- fix(jobs): reset 'failedReason', 'finishedOn' and 'processedOn' fields on job retry
- fix(queue): fix Warning: cancellation is disabled
- fix(queue): remove the correct listener in isRedisReady
- feat(jobs): allow cancelling of retries when using custom backoff strategy
- feat(rate-limiter): add discard config for rate-limiter
- feat(jobs): make job progress accepts variant types
- fix(repeatable): Fixed wrong repeatable count updates
- fix(jobs): fix copy paste mistake for stacktrace in job toData
- feat(child-processes): Propagate stack traces
- feat(repeatable): add ability for cron repeatable job with startDate
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.8...v3.5.0)
## v.3.4.8
- emit waiting event when waking up sleep jobs fixing #792
- throw error if missing processor file fixing #954
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.7...v3.4.8)
## v.3.4.7
- Fixes to deal with removing correctly in priority queues #984
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.6...v3.4.7)
## v.3.4.6
- Reverted use of arrow function that was incompatible with older versions of node.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.5...v3.4.6)
## v.3.4.5
- Fixed Unhandled promise rejections #1012.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.4...v3.4.5)
## v.3.4.4
- Partially fixed #845. When call queue.close() bull throws Error: Connection is closed.
- Fixed #998. Check for existence of rate limiter options.
- Fixed #1003. Fixed fixed repeatable jobs duplication using every.
- Feature/provide error to custom backoff strategy.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.3...v3.4.4)
## v.3.4.3
- Fixed #994 queue.getJobs() race condition.
- Fixed #966 Race conditions reviving repeatable jobs.
- Fixed getters: Update types array to include paused.
- Fixed #958 job.finished slowdown.
- Fixed #949 TypeError: job.queue.client.isFinished is not a function.
- Fixed #870 TypeError when retrying jobs.
- Fixed #942 Support for milliseconds intervals in repeatable jobs.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.2...v3.4.3)
## v.3.4.2
- Fixed #903 Globally paused queue cannot receive job (or not shown in Arena untill queue is globally resumed).
- Workaround for #911 Seperate process worker fails to launch when Node is started with --inspect flag
- added missing retain on reused child job #908.
- added more tests for child jobs.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.1...v3.4.2)
## v.3.4.1
- Better check for closing in moveUnlockedJobsToWait, possibly fixing #806.
[Changes](https://github.com/OptimalBits/bull/compare/v3.4.0...v3.4.1)
## v.3.4.0
- Added support for prioritized delayed jobs.
- Added ability to process all named jobs from one process function.
- Fixed #893, warning 'a promise was rejected with a non-error' for external queues in case of an error.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.10...v3.4.0)
## v.3.3.10
- Faster next job fetch #868
- Added global default options for jobs. Fixes #706.
- Added a limit for repeatable jobs. #854.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.9...v3.3.10)
## v.3.3.9
- Support custom backoff strategies.
- Fixed #786. Handling of ES6 default export.
- Fixed #782. Better handling of "isReady".
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.8...v3.3.9)
## v.3.3.8
- Fixed #812. External process doesn't terminate on `queue.close()`.
- Fixed #830. Named Process Sent to Wrong Processor.
- Fixed #572. Do not close external connections.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.7...v3.3.8)
## v.3.3.7
- Fixed #807.
- Adding ability to limit by stacktrace. #798.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.6...v3.3.7)
## v.3.3.6
- Fixed #766, #781, #716.
- Correctly accept DB in redis connection string.
- Fixed global progress event.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.5...v3.3.6)
## v.3.3.5
- Fixed #764, #762, #759.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.4...v3.3.5)
## v.3.3.4
- Fixed #748.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.3...v3.3.4)
## v.3.3.3
- Re-fixed #739.
- Possibly fixed for #747.
- Fixed removeRepeatable (missing file)
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.2...v3.3.3)
## v.3.3.2
- Fixed #721. SyntaxError: Unexpected token u in JSON at position 0.
- Fixed #739. childs are not added to the retained set.
- Fixed #734. fixed Promise warnings.
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.1...v3.3.2)
## v.3.3.1
- Fixed #714
[Changes](https://github.com/OptimalBits/bull/compare/v3.3.0...v3.3.1)
## v.3.3.0
- Added a method `Queue##removeRepeatable` to remove repeatable jobs.
- Now also emits drained as a global event.
- Fixed #518, #624
[Changes](https://github.com/OptimalBits/bull/compare/v3.2.0...v3.3.0)
## v.3.2.0
- Added support for running jobs in child processes #488
[Changes](https://github.com/OptimalBits/bull/compare/v3.1.0...v3.2.0)
## v.3.1.0
- Added rate limiter support.
- Added method to update jobs data.
- Implemented stalled as global event.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0...v3.1.0)
## v.3.0.0
- No changes.
## v.3.0.0-rc.10
- Fixed #666.
- Small improvements in the repeat code.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.9...v3.0.0-rc.10)
## v.3.0.0-rc.9
- Fixed #672.
- Fixed #670
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.8...v3.0.0-rc.9)
## v.3.0.0-rc.8
- Enhanced job fetching #651 (faster and more reliable).
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.7...v3.0.0-rc.8)
## v.3.0.0-rc.7
- Fixed #659
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.6...v3.0.0-rc.7)
## v.3.0.0-rc.6
- Fixed #645.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.5...v3.0.0-rc.6)
## v.3.0.0-rc.5
- Improved performance, specially when having many concurrent workers.
- Fixed #609 using zsets for storing repeatable jobs.
- Fixed #608 Event chaining no longer works.
- Improved getters.
- Fixed #601 Add multiple repeatable jobs with the same cron pattern.
[Changes](https://github.com/OptimalBits/bull/compare/3.0.0-rc.4...v3.0.0-rc.5)
## v.3.0.0-rc.4
- Added support for naming workers in redis connections #530.
- Lazy instantiation of redis clients. Fixes #526.
- job.finished captures result from queue process. #588.
- Caches LUA scripts to avoid reading files in every queue instantiation. #591.
- Emit 'drain' event when queue is empty. #596.
- store finished and processed timestamps. #594, #606.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.3...3.0.0-rc.4)
## v.3.0.0-rc.3
- Fixed #579.
- Lazy subscription to events for better performance.
- Corrected calculation of next repeat job. #563.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.2...v3.0.0-rc.3)
## v.3.0.0-rc.2
- Improved performance of moveToActive #550.
- Fixed issue with cancelable promise #546.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-rc.1...v3.0.0-rc.2)
## v.3.0.0-rc.1
- Improved error and lock handling for failed jobs #499, #539.
- Corrected instantiation from urls #538.
- Return jobs in proper order in jobs getters.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-alpha.4...v3.0.0-rc.1)
## v.3.0.0-alpha.4
- Implemented repeatable jobs. #252.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-alpha.3...v3.0.0-alpha.4)
## v.3.0.0-alpha.3
- Simplified global events #501.
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-alpha.2...v3.0.0-alpha.3)
## v.3.0.0-alpha.2
- Eliminated possible memory leak #503
[Changes](https://github.com/OptimalBits/bull/compare/v3.0.0-alpha.1...v3.0.0-alpha.2)
## v.3.0.0-alpha.1
- improved job fetch mechanism. #480.
- job.jobId changed to job.id.
- refactored error messages into separate error module.
- refactored lua scripts into separate files, and preloaded.
- many atomizations and clean ups.
- completed and failed job states are now represented in ZSETs. #190.
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.6...v3.0.0-alpha.1)
## v.2.2.6
- Persisted failedReason when storing job data.
- added queue##isReady()
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.5...v2.2.6)
## v.2.2.5
- Fixed so that redis key prefix works properly.
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.4...v2.2.5)
## v.2.2.4
- Allow reusing certain redis connections.
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.3...v2.2.4)
## v.2.2.3
- Added getJobCounts.
- Fixed global events #394.
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.2...v2.2.3)
## v.2.2.2
- Fixed redis script cache gets bloated after update to bull 2.0 #426
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.1...v2.2.2)
## v.2.2.1
- Re-added createClient option that was removed by mistake.
- Corrected getJobCountByTypes, fixes #419 and #401
[Changes](https://github.com/OptimalBits/bull/compare/v2.2.0...v2.2.1)
## v.2.2.0
- Much improved priority queues, simpler, faster and more reliable.
- Fixed issue where lua scripts where leaking memory.
- Improvements in local pause, fixing #446 and #447.
- Fix to increase delay time over 24 days #244
[Changes](https://github.com/OptimalBits/bull/compare/v2.1.2...v2.2.0)
## v.2.1.2
- Fixed Error renewing lock LockError: Exceeded 0 attempts to lock the resource #437
- Fixed Unable to renew nonexisting lock on job fail #441
[Changes](https://github.com/OptimalBits/bull/compare/v2.1.1...v2.1.2)
## v.2.1.1
- Catch errors produced in timers. Related to #441
[Changes](https://github.com/OptimalBits/bull/compare/v2.1.0...v2.1.1)
## v.2.1.0
- Fixed #397, Error: Unable to renew nonexisting lock
- Fixed #402, Job.prototype.finished contains error in promise
- Fixed #371, "Unexpected token u in JSON at position 0" while processing job
- New feature #363, "method to permanently fail a job"
- Fix job.progress() to return the correct progress
[Changes](https://github.com/OptimalBits/bull/compare/v2.0.0...v2.1.0)
## v.2.0.0
- Changed redis module to ioredis fixing many issues along the way, see changes.
[Changes](https://github.com/OptimalBits/bull/compare/v1.1.3...v2.0.0)
## v.1.1.3
- fixed "Broken constructor pattern from recent commit" #384
- fixed "Queue.prototype.getWaiting() returns empty list if Queue is paused" #342
[Changes](https://github.com/OptimalBits/bull/compare/v1.1.2...v1.1.3)
## v1.1.2
- regained backwards compatibility in events by using disturbed 1.0.6
[Changes](https://github.com/OptimalBits/bull/compare/v1.1.1...v1.1.2)
## v1.1.1
- Returned this in queue##on and queue##once for backwards compatibility.
- [Fixes PriorityQueue Events and Local Worker Pause/Resume](https://github.com/OptimalBits/bull/pull/341)
[Changes](https://github.com/OptimalBits/bull/compare/v1.1.0...v1.1.1)
## v1.1.0
- Fixed [job corruption issue](https://github.com/OptimalBits/bull/pull/359)
- The job id can be [overridden](https://github.com/OptimalBits/bull/pull/335) to implement job throttling behavior
- Added [`removeOnComplete` job option](https://github.com/OptimalBits/bull/pull/361)
- [More robust job retry](https://github.com/OptimalBits/bull/pull/318)
- Events are [now broadcast to all workers](https://github.com/OptimalBits/bull/commit/d55ad1c8f44f86be9b4e9f4fa9a3fc8a16c6e02d)
[Changes](https://github.com/OptimalBits/bull/compare/v1.0.0...v1.1.0)
## v1.0.0
- improvements in clean (fixes and performance).
[Changes](https://github.com/OptimalBits/bull/compare/v1.0.0-rc4...v1.0.0)
## v1.0.0-rc4
- fixed lock renew logic.
- atomized code for getting stalled jobs.
[Changes](https://github.com/OptimalBits/bull/compare/v1.0.0-rc3...v1.0.0-rc4)
## v1.0.0-rc3
- smaller fixes.
[Changes](https://github.com/OptimalBits/bull/compare/v1.0.0-rc2...v1.0.0-rc3)
## v1.0.0-rc2
- Improved locking when removing and processing stalled jobs.
- Fixed #302 EVALSHA failure.
- Fixed #295 error with redis 3.2.
- Correctly allows the specification of the db
- Honor start/end range for complete/failed jobs.
- Fixed #277 Memory Leaks With Large Queue.
- Support for custom key prefix for redis keys.
[Changes](https://github.com/OptimalBits/bull/compare/v1.0.0-rc1...v1.0.0-rc2)
## v1.0.0-rc1
- Removed all potential dangerous hazards by atomizing many operations using
cached LUA scripts.
- Improved performance around 400% compared to previous version.
- Better pause/resume (#266), and added pause for local workers.
- Fixed #272, #271, #261, #253, #240, #239
[Changes](https://github.com/OptimalBits/bull/compare/v0.7.2...v1.0.0-rc1)
## v0.7.2
- Added local pause/resume functionality
- fixed memory leaks present in the run promise chain.
- fixed "Illegal access to a strict mode caller function".
[Changes](https://github.com/OptimalBits/bull/compare/v0.7.1...v0.7.2)
## v0.7.1
- fixed storing of stacktraces
[Changes](https://github.com/OptimalBits/bull/compare/v0.7.0...v0.7.1)
## v0.7.0
- store the return value from the job handlers.
- store stacktraces.
- improvements in delayed jobs.
[Changes](https://github.com/OptimalBits/bull/compare/v0.6.0...v0.7.0)
## v0.4.0
- added a Queue##clean method
[Changes](https://github.com/OptimalBits/bull/compare/v0.3.0...v0.4.0)
## v0.3.0
- added support for custom clients.
- added test support for node 0.12.
- timeout improvements.
- unit test improvements.
- added timeout to queue pop blocking call.
- removed when dependency.
[Changes](https://github.com/OptimalBits/bull/compare/v0.2.7...v0.3.0)
## v0.2.7
[Changes](https://github.com/OptimalBits/bull/compare/v0.2.6...v0.2.7)
## v0.2.6
- [Fix] #103 When a queue start it do not process delayed job.
[Changes](https://github.com/OptimalBits/bull/compare/v0.2.5...v0.2.6)
## v0.2.5
- [upgrade] Upgraded node redis to version 0.12.x
- [improvement] eslinted all code.
- [fix] added missing token when calling takeLock on jobs.
[Changes](https://github.com/OptimalBits/bull/compare/v0.2.4...v0.2.5)
## v0.2.4
[Changes](https://github.com/OptimalBits/bull/compare/v0.2.3...v0.2.4)
## v0.2.3
[Changes](https://github.com/OptimalBits/bull/compare/v0.1.9...v0.2.3)
## v0.1.9
- [Improvement] Faster job removal. (manast)
## v0.1.8
- [Improvement] Better promisification of redis methods. (manast)
## v0.1.7
- [Feature] Added a convenience method for getting a job. (loginx)
- [Fix] Only set a redis db from options if defined. (jboga)
- [Fix] Fixed issue #52. (manast)
## v0.1.6
- [Fix] Improved and corrected job's getters.
- [Fix] Automatically restart queues in the event of redis disconnections.
- [Feature] Added support for adding jobs in a LIFO fashion.
+13
View File
@@ -0,0 +1,13 @@
License
-------
(The MIT License)
Copyright &copy; 2013-2018 Manuel Astudillo <manuel@optimalbits.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+514
View File
@@ -0,0 +1,514 @@
<div align="center">
<br/>
<img src="./support/logo@2x.png" width="300" />
<br/>
<br/>
<p>
The fastest, most reliable, Redis-based queue for Node. <br/>
Carefully written for rock solid stability and atomicity.
</p>
<br/>
<p>
<a href="#-sponsors-"><strong>Sponsors</strong></a> ·
<a href="#bull-features"><strong>Features</strong></a> ·
<a href="#uis"><strong>UIs</strong></a> ·
<a href="#install"><strong>Install</strong></a> ·
<a href="#quick-guide"><strong>Quick Guide</strong></a> ·
<a href="#documentation"><strong>Documentation</strong></a>
</p>
<p>Check the new <a href="https://optimalbits.github.io/bull/"><strong>Guide!</strong></p>
<br/>
<p>
<a href="https://gitter.im/OptimalBits/bull">
<img src="https://badges.gitter.im/Join%20Chat.svg"/>
</a>
<a href="https://join.slack.com/t/bullmq/shared_invite/zt-1nbtpk6mv-TItWpF9jf3k4yrCaS0PPZA">
<img src="https://img.shields.io/badge/Slack-4A154B"/>
</a>
<a href="http://badge.fury.io/js/bull">
<img src="https://badge.fury.io/js/bull.svg"/>
</a>
<a href="https://coveralls.io/github/OptimalBits/bull?branch=master">
<img src="https://coveralls.io/repos/github/OptimalBits/bull/badge.svg?branch=master"/>
</a>
<a href="http://isitmaintained.com/project/OptimalBits/bull">
<img src="http://isitmaintained.com/badge/open/optimalbits/bull.svg"/>
</a>
<a href="http://isitmaintained.com/project/OptimalBits/bull">
<img src="http://isitmaintained.com/badge/resolution/optimalbits/bull.svg"/>
</a>
<a href="https://twitter.com/manast">
<img src="https://img.shields.io/twitter/follow/manast?label=Stay%20updated&style=social"/>
</a>
</p>
</div>
### 🚀 Sponsors 🚀
<table cellspacing="0" cellpadding="0" border="0">
<tr>
<td>
<a href="https://www.dragonflydb.io/">
<img src="https://raw.githubusercontent.com/dragonflydb/dragonfly/main/.github/images/logo-full.svg" width=550 alt="Dragonfly" />
</a>
</td>
<td>
Dragonfly is a new Redis™ drop-in replacement that is fully compatible with BullMQ and brings some important advantages over Redis™ such as massive
better performance by utilizing all CPU cores available and faster and more memory efficient data structures. Read more <a href="https://www.dragonflydb.io/docs/integrations/bullmq">here</a> on how to use it with BullMQ.
</td>
</tr>
</table>
### 📻 News and updates
Bull is currently in maintenance mode, we are only fixing bugs. For new features check [BullMQ](https://github.com/taskforcesh/bullmq), a modern rewritten
implementation in Typescript. You are still very welcome to use Bull if it suits your needs, which is a safe, battle tested library.
Follow me on [Twitter](http://twitter.com/manast) for other important news and updates.
### 🛠 Tutorials
You can find tutorials and news in this blog: https://blog.taskforce.sh/
---
### Used by
Bull is popular among large and small organizations, like the following ones:
<table cellspacing="0" cellpadding="0">
<tr>
<td valign="center">
<a href="https://github.com/atlassian/github-for-jira">
<img
src="https://876297641-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LUuDmt_xXMfG66Rn1GA%2Fuploads%2FevsJCF6F1tx1ScZwDQOd%2FAtlassian-horizontal-blue-rgb.webp?alt=media&token=2fcd0528-e8bb-4bdd-af35-9d20e313d1a8"
width="150"
alt="Atlassian"
/></a>
</td>
<td valign="center">
<a href="https://github.com/Autodesk">
<img
src="https://876297641-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LUuDmt_xXMfG66Rn1GA%2Fuploads%2FvpTe02RdOhUJBA8TdHEE%2Fautodesk-logo-white.png?alt=media&token=326961b4-ea4f-4ded-89a4-e05692eec8ee"
width="150"
alt="Autodesk"
/></a>
</td>
<td valign="center">
<a href="https://github.com/common-voice/common-voice">
<img
src="https://876297641-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LUuDmt_xXMfG66Rn1GA%2Fuploads%2F4zPSrubNJKViAzUIftIy%2Fmozilla-logo-bw-rgb.png?alt=media&token=9f93aae2-833f-4cc4-8df9-b7fea0ad5cb5"
width="150"
alt="Mozilla"
/></a>
</td>
<td valign="center">
<a href="https://github.com/nestjs/bull">
<img
src="https://876297641-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LUuDmt_xXMfG66Rn1GA%2Fuploads%2FfAcGye182utFUtPKdLqJ%2FScreenshot%202022-02-15%20at%2011.32.39.png?alt=media&token=29feb550-f0bc-467d-a290-f700701d7d15"
width="150"
alt="Nest"
/></a>
</td>
<td valign="center">
<a href="https://github.com/salesforce/refocus">
<img
src="https://876297641-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LUuDmt_xXMfG66Rn1GA%2Fuploads%2FZNnYNuL5qJ6ZoBh7JJEW%2Fsalesforce-logo.png?alt=media&token=ddcae63b-08c0-4dd4-8496-3b29a9bf977d"
width="100"
alt="Salesforce"
/></a>
</td>
</tr>
</table>
---
---
### Official FrontEnd
[<img src="http://taskforce.sh/assets/logo_square.png" width="100" alt="Taskforce.sh, Inc" style="padding: 100px"/>](https://taskforce.sh)
Supercharge your queues with a professional front end:
- Get a complete overview of all your queues.
- Inspect jobs, search, retry, or promote delayed jobs.
- Metrics and statistics.
- and many more features.
Sign up at [Taskforce.sh](https://taskforce.sh)
---
### Bull Features
- [x] Minimal CPU usage due to a polling-free design.
- [x] Robust design based on Redis.
- [x] Delayed jobs.
- [x] Schedule and repeat jobs according to a cron specification.
- [x] Rate limiter for jobs.
- [x] Retries.
- [x] Priority.
- [x] Concurrency.
- [x] Pause/resume—globally or locally.
- [x] Multiple job types per queue.
- [x] Threaded (sandboxed) processing functions.
- [x] Automatic recovery from process crashes.
And coming up on the roadmap...
- [ ] Job completion acknowledgement (you can use the message queue [pattern](https://github.com/OptimalBits/bull/blob/develop/PATTERNS.md#returning-job-completions) in the meantime).
- [ ] Parent-child jobs relationships.
---
### UIs
There are a few third-party UIs that you can use for monitoring:
**BullMQ**
- [Taskforce](https://taskforce.sh)
**Bull v3**
- [Taskforce](https://taskforce.sh)
- [bull-board](https://github.com/vcapretz/bull-board)
- [bull-repl](https://github.com/darky/bull-repl)
- [bull-monitor](https://github.com/s-r-x/bull-monitor)
- [Monitoro](https://github.com/AbhilashJN/monitoro)
**Bull <= v2**
- [Matador](https://github.com/ShaneK/Matador)
- [react-bull](https://github.com/kfatehi/react-bull)
- [Toureiro](https://github.com/Epharmix/Toureiro)
---
### Monitoring & Alerting
- With Prometheus [Bull Queue Exporter](https://github.com/UpHabit/bull_exporter)
---
### Feature Comparison
Since there are a few job queue solutions, here is a table comparing them:
| Feature | [BullMQ-Pro](https://bullmq.io/#bullmq-pro) | [BullMQ](https://bullmq.io) | Bull | Kue | Bee | Agenda |
| :------------------------ | :-------------: | :-------------: | :-------------: | :---: | -------- | ------ |
| Backend | redis | redis | redis | redis | redis | mongo |
| Observables | ✓ | | | | | |
| Group Rate Limit | ✓ | | | | | |
| Group Support | ✓ | | | | | |
| Batches Support | ✓ | | | | | |
| Parent/Child Dependencies | ✓ | ✓ | | | | |
| Priorities | ✓ | ✓ | ✓ | ✓ | | ✓ |
| Concurrency | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Delayed jobs | ✓ | ✓ | ✓ | ✓ | | ✓ |
| Global events | ✓ | ✓ | ✓ | ✓ | | |
| Rate Limiter | ✓ | ✓ | ✓ | | | |
| Pause/Resume | ✓ | ✓ | ✓ | ✓ | | |
| Sandboxed worker | ✓ | ✓ | ✓ | | | |
| Repeatable jobs | ✓ | ✓ | ✓ | | | ✓ |
| Atomic ops | ✓ | ✓ | ✓ | | ✓ | |
| Persistence | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| UI | ✓ | ✓ | ✓ | ✓ | | ✓ |
| Optimized for | Jobs / Messages | Jobs / Messages | Jobs / Messages | Jobs | Messages | Jobs |
### Install
```bash
npm install bull --save
```
or
```bash
yarn add bull
```
_**Requirements:** Bull requires a Redis version greater than or equal to `2.8.18`._
### Typescript Definitions
```bash
npm install @types/bull --save-dev
```
```bash
yarn add --dev @types/bull
```
Definitions are currently maintained in the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/bull) repo.
## Contributing
We welcome all types of contributions, either code fixes, new features or doc improvements.
Code formatting is enforced by [prettier](https://prettier.io/).
For commits please follow conventional [commits convention](https://www.conventionalcommits.org/en/v1.0.0-beta.2/).
All code must pass lint rules and test suites before it can be merged into develop.
---
### Quick Guide
#### Basic Usage
```js
const Queue = require('bull');
const videoQueue = new Queue('video transcoding', 'redis://127.0.0.1:6379');
const audioQueue = new Queue('audio transcoding', { redis: { port: 6379, host: '127.0.0.1', password: 'foobared' } }); // Specify Redis connection using object
const imageQueue = new Queue('image transcoding');
const pdfQueue = new Queue('pdf transcoding');
videoQueue.process(function (job, done) {
// job.data contains the custom data passed when the job was created
// job.id contains id of this job.
// transcode video asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { framerate: 29.5 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
audioQueue.process(function (job, done) {
// transcode audio asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { samplerate: 48000 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
imageQueue.process(function (job, done) {
// transcode image asynchronously and report progress
job.progress(42);
// call done when finished
done();
// or give an error if error
done(new Error('error transcoding'));
// or pass it a result
done(null, { width: 1280, height: 720 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
});
pdfQueue.process(function (job) {
// Processors can also return promises instead of using the done callback
return pdfAsyncProcessor();
});
videoQueue.add({ video: 'http://example.com/video1.mov' });
audioQueue.add({ audio: 'http://example.com/audio1.mp3' });
imageQueue.add({ image: 'http://example.com/image1.tiff' });
```
#### Using promises
Alternatively, you can return promises instead of using the `done` callback:
```javascript
videoQueue.process(function (job) { // don't forget to remove the done callback!
// Simply return a promise
return fetchVideo(job.data.url).then(transcodeVideo);
// Handles promise rejection
return Promise.reject(new Error('error transcoding'));
// Passes the value the promise is resolved with to the "completed" event
return Promise.resolve({ framerate: 29.5 /* etc... */ });
// If the job throws an unhandled exception it is also handled correctly
throw new Error('some unexpected error');
// same as
return Promise.reject(new Error('some unexpected error'));
});
```
#### Separate processes
The process function can also be run in a separate process. This has several advantages:
- The process is sandboxed so if it crashes it does not affect the worker.
- You can run blocking code without affecting the queue (jobs will not stall).
- Much better utilization of multi-core CPUs.
- Less connections to redis.
In order to use this feature just create a separate file with the processor:
```js
// processor.js
module.exports = function (job) {
// Do some heavy work
return Promise.resolve(result);
}
```
And define the processor like this:
```js
// Single process:
queue.process('/path/to/my/processor.js');
// You can use concurrency as well:
queue.process(5, '/path/to/my/processor.js');
// and named processors:
queue.process('my processor', 5, '/path/to/my/processor.js');
```
#### Repeated jobs
A job can be added to a queue and processed repeatedly according to a cron specification:
```js
paymentsQueue.process(function (job) {
// Check payments
});
// Repeat payment job once every day at 3:15 (am)
paymentsQueue.add(paymentsData, { repeat: { cron: '15 3 * * *' } });
```
As a tip, check your expressions here to verify they are correct:
[cron expression generator](https://crontab.cronhub.io)
#### Pause / Resume
A queue can be paused and resumed globally (pass `true` to pause processing for
just this worker):
```js
queue.pause().then(function () {
// queue is paused now
});
queue.resume().then(function () {
// queue is resumed now
})
```
#### Events
A queue emits some useful events, for example...
```js
.on('completed', function (job, result) {
// Job completed with output result!
})
```
For more information on events, including the full list of events that are fired, check out the [Events reference](./REFERENCE.md#events)
#### Queues performance
Queues are cheap, so if you need many of them just create new ones with different
names:
```javascript
const userJohn = new Queue('john');
const userLisa = new Queue('lisa');
.
.
.
```
However every queue instance will require new redis connections, check how to [reuse connections](https://github.com/OptimalBits/bull/blob/master/PATTERNS.md#reusing-redis-connections) or you can also use [named processors](https://github.com/OptimalBits/bull/blob/master/REFERENCE.md#queueprocess) to achieve a similar result.
#### Cluster support
NOTE: From version 3.2.0 and above it is recommended to use threaded processors instead.
Queues are robust and can be run in parallel in several threads or processes
without any risk of hazards or queue corruption. Check this simple example
using cluster to parallelize jobs across processes:
```js
const Queue = require('bull');
const cluster = require('cluster');
const numWorkers = 8;
const queue = new Queue('test concurrent queue');
if (cluster.isMaster) {
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('online', function (worker) {
// Let's create a few jobs for the queue workers
for (let i = 0; i < 500; i++) {
queue.add({ foo: 'bar' });
};
});
cluster.on('exit', function (worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
} else {
queue.process(function (job, jobDone) {
console.log('Job done by worker', cluster.worker.id, job.id);
jobDone();
});
}
```
---
### Documentation
For the full documentation, check out the reference and common patterns:
- [Guide](https://optimalbits.github.io/bull/) — Your starting point for developing with Bull.
- [Reference](./REFERENCE.md) — Reference document with all objects and methods available.
- [Patterns](./PATTERNS.md) — a set of examples for common patterns.
- [License](./LICENSE.md) — the Bull license—it's MIT.
If you see anything that could use more docs, please submit a pull request!
---
### Important Notes
The queue aims for an "at least once" working strategy. This means that in some situations, a job
could be processed more than once. This mostly happens when a worker fails to keep a lock
for a given job during the total duration of the processing.
When a worker is processing a job it will keep the job "locked" so other workers can't process it.
It's important to understand how locking works to prevent your jobs from losing their lock - becoming _stalled_ -
and being restarted as a result. Locking is implemented internally by creating a lock for `lockDuration` on interval
`lockRenewTime` (which is usually half `lockDuration`). If `lockDuration` elapses before the lock can be renewed,
the job will be considered stalled and is automatically restarted; it will be __double processed__. This can happen when:
1. The Node process running your job processor unexpectedly terminates.
2. Your job processor was too CPU-intensive and stalled the Node event loop, and as a result, Bull couldn't renew the job lock (see [#488](https://github.com/OptimalBits/bull/issues/488) for how we might better detect this). You can fix this by breaking your job processor into smaller parts so that no single part can block the Node event loop. Alternatively, you can pass a larger value for the `lockDuration` setting (with the tradeoff being that it will take longer to recognize a real stalled job).
As such, you should always listen for the `stalled` event and log this to your error monitoring system, as this means your jobs are likely getting double-processed.
As a safeguard so problematic jobs won't get restarted indefinitely (e.g. if the job processor always crashes its Node process), jobs will be recovered from a stalled state a maximum of `maxStalledCount` times (default: `1`).
+1136
View File
@@ -0,0 +1,1136 @@
// Type definitions for bull 3.15
// Project: https://github.com/OptimalBits/bull
// Definitions by: Bruno Grieder <https://github.com/bgrieder>
// Cameron Crothers <https://github.com/JProgrammer>
// Marshall Cottrell <https://github.com/marshall007>
// Weeco <https://github.com/weeco>
// Oleg Repin <https://github.com/iamolegga>
// David Koblas <https://github.com/koblas>
// Bond Akinmade <https://github.com/bondz>
// Wuha Team <https://github.com/wuha-team>
// Alec Brunelle <https://github.com/aleccool213>
// Dan Manastireanu <https://github.com/danmana>
// Kjell-Morten Bratsberg Thorsen <https://github.com/kjellmorten>
// Christian D. <https://github.com/pc-jedi>
// Silas Rech <https://github.com/lenovouser>
// DoYoung Ha <https://github.com/hados99>
// Borys Kupar <https://github.com/borys-kupar>
// Remko Klein <https://github.com/remko79>
// Levi Bostian <https://github.com/levibostian>
// Todd Dukart <https://github.com/tdukart>
// Mix <https://github.com/mnixry>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
import * as Redis from 'ioredis';
import { EventEmitter } from 'events';
/**
* This is the Queue constructor.
* It creates a new Queue that is persisted in Redis.
* Everytime the same queue is instantiated it tries to process all the old jobs that may exist from a previous unfinished session.
*/
declare const Bull: {
/* tslint:disable:no-unnecessary-generics unified-signatures */
<T = any>(queueName: string, opts?: Bull.QueueOptions): Bull.Queue<T>;
<T = any>(
queueName: string,
url: string,
opts?: Bull.QueueOptions
): Bull.Queue<T>;
new <T = any>(queueName: string, opts?: Bull.QueueOptions): Bull.Queue<T>;
new <T = any>(
queueName: string,
url: string,
opts?: Bull.QueueOptions
): Bull.Queue<T>;
/* tslint:enable:no-unnecessary-generics unified-signatures */
};
declare namespace Bull {
interface RateLimiter {
/** Max numbers of jobs processed */
max: number;
/** Per duration in milliseconds */
duration: number;
/** When jobs get rate limited, they stay in the waiting queue and are not moved to the delayed queue */
bounceBack?: boolean | undefined;
/** Groups jobs with the specified key from the data object passed to the Queue#add ex. "network.handle" */
groupKey?: string | undefined;
}
interface QueueOptions {
/**
* Options passed into the `ioredis` constructor's `options` parameter.
* `connectionName` is overwritten with `Queue.clientName()`. other properties are copied
*/
redis?: Redis.RedisOptions | string | undefined;
/**
* When specified, the `Queue` will use this function to create new `ioredis` client connections.
* This is useful if you want to re-use connections or connect to a Redis cluster.
*/
createClient?(
type: 'client' | 'subscriber' | 'bclient',
redisOpts?: Redis.RedisOptions
): Redis.Redis | Redis.Cluster;
/**
* Prefix to use for all redis keys
*/
prefix?: string | undefined;
settings?: AdvancedSettings | undefined;
limiter?: RateLimiter | undefined;
defaultJobOptions?: JobOptions | undefined;
metrics?: MetricsOpts; // Configure metrics
}
interface MetricsOpts {
maxDataPoints?: number; // Max number of data points to collect, granularity is fixed at one minute.
}
interface AdvancedSettings {
/**
* Key expiration time for job locks
*/
lockDuration?: number | undefined;
/**
* Interval in milliseconds on which to acquire the job lock.
*/
lockRenewTime?: number | undefined;
/**
* How often check for stalled jobs (use 0 for never checking)
*/
stalledInterval?: number | undefined;
/**
* Max amount of times a stalled job will be re-processed
*/
maxStalledCount?: number | undefined;
/**
* Poll interval for delayed jobs and added jobs
*/
guardInterval?: number | undefined;
/**
* Delay before processing next job in case of internal error
*/
retryProcessDelay?: number | undefined;
/**
* Define a custom backoff strategy
*/
backoffStrategies?:
| {
[key: string]: (
attemptsMade: number,
err: Error,
strategyOptions?: any
) => number;
}
| undefined;
/**
* A timeout for when the queue is in `drained` state (empty waiting for jobs).
* It is used when calling `queue.getNextJob()`, which will pass it to `.brpoplpush` on the Redis client.
*/
drainDelay?: number | undefined;
}
type DoneCallback = (error?: Error | null, value?: any) => void;
type JobId = number | string;
type ProcessCallbackFunction<T> = (job: Job<T>, done: DoneCallback) => void;
type ProcessPromiseFunction<T> = (job: Job<T>) => Promise<void>;
interface Job<T = any> {
id: JobId;
/**
* The custom data passed when the job was created
*/
data: T;
/**
* Options of the job
*/
opts: JobOptions;
/**
* How many attempts where made to run this job
*/
attemptsMade: number;
/**
* When this job was started (unix milliseconds)
*/
processedOn?: number | undefined;
/**
* When this job was completed (unix milliseconds)
*/
finishedOn?: number | undefined;
/**
* Which queue this job was part of
*/
queue: Queue<T>;
timestamp: number;
/**
* The named processor name
*/
name: string;
/**
* The stacktrace for any errors
*/
stacktrace: string[];
returnvalue: any;
failedReason?: string | undefined;
/**
* Get progress on a job
*/
progress(): any;
/**
* Report progress on a job
*/
progress(value: any): Promise<void>;
/**
* Logs one row of log data.
*
* @param row String with log data to be logged.
*/
log(row: string): Promise<any>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is completed
*/
isCompleted(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is failed
*/
isFailed(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is delayed
*/
isDelayed(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is active
*/
isActive(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is wait
*/
isWaiting(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is paused
*/
isPaused(): Promise<boolean>;
/**
* Returns a promise resolving to a boolean which, if true, current job's state is stuck
*/
isStuck(): Promise<boolean>;
/**
* Returns a promise resolving to the current job's status.
* Please take note that the implementation of this method is not very efficient, nor is
* it atomic. If your queue does have a very large quantity of jobs, you may want to
* avoid using this method.
*/
getState(): Promise<JobStatus | 'stuck'>;
/**
* Update a specific job's data. Promise resolves when the job has been updated.
*/
update(data: T): Promise<void>;
/**
* Removes a job from the queue and from any lists it may be included in.
* The returned promise resolves when the job has been removed.
*/
remove(): Promise<void>;
/**
* Re-run a job that has failed. The returned promise resolves when the job
* has been scheduled for retry.
*/
retry(): Promise<void>;
/**
* Ensure this job is never ran again even if attemptsMade is less than job.attempts.
*/
discard(): Promise<void>;
/**
* Returns a promise that resolves to the returned data when the job has been finished.
* TODO: Add a watchdog to check if the job has finished periodically.
* since pubsub does not give any guarantees.
*/
finished(): Promise<any>;
/**
* Moves a job to the `completed` queue. Pulls a job from 'waiting' to 'active'
* and returns a tuple containing the next jobs data and id. If no job is in the `waiting` queue, returns null.
*/
moveToCompleted(
returnValue?: string,
ignoreLock?: boolean,
notFetch?: boolean
): Promise<[any, JobId] | null>;
/**
* Moves a job to the `failed` queue. Pulls a job from 'waiting' to 'active'
* and returns a tuple containing the next jobs data and id. If no job is in the `waiting` queue, returns null.
*/
moveToFailed(
errorInfo: { message: string },
ignoreLock?: boolean
): Promise<[any, JobId] | null>;
/**
* Promotes a job that is currently "delayed" to the "waiting" state and executed as soon as possible.
*/
promote(): Promise<void>;
/**
* Return a unique key representing a lock for this Job
*/
lockKey(): string;
/**
* Releases the lock on the job. Only locks owned by the queue instance can be released.
*/
releaseLock(): Promise<void>;
/**
* Takes a lock for this job so that no other queue worker can process it at the same time.
*/
takeLock(): Promise<number | false>;
/**
* Extend the lock for this job.
*
* @param duration lock duration in milliseconds
*/
extendLock(duration: number): Promise<number>
/**
* Get job properties as Json Object
*/
toJSON(): {
id: JobId;
name: string;
data: T;
opts: JobOptions;
progress: number;
delay: number;
timestamp: number;
attemptsMade: number;
failedReason: any;
stacktrace: string[] | null;
returnvalue: any;
finishedOn: number | null;
processedOn: number | null;
};
}
type JobStatus =
| 'completed'
| 'waiting'
| 'active'
| 'delayed'
| 'failed'
| 'paused';
type JobStatusClean =
| 'completed'
| 'wait'
| 'active'
| 'delayed'
| 'failed'
| 'paused';
interface BackoffOptions {
/**
* Backoff type, which can be either `fixed` or `exponential`
*/
type: string;
/**
* Backoff delay, in milliseconds
*/
delay?: number | undefined;
/**
* Options for custom strategies
*/
options?: any;
}
interface RepeatOptions {
/**
* Timezone
*/
tz?: string | undefined;
/**
* End date when the repeat job should stop repeating
*/
endDate?: Date | string | number | undefined;
/**
* Number of times the job should repeat at max.
*/
limit?: number | undefined;
/**
* The start value for the repeat iteration count.
*/
count?: number | undefined;
}
interface CronRepeatOptions extends RepeatOptions {
/**
* Cron pattern specifying when the job should execute
*/
cron: string;
/**
* Start date when the repeat job should start repeating (only with cron).
*/
startDate?: Date | string | number | undefined;
}
interface EveryRepeatOptions extends RepeatOptions {
/**
* Repeat every millis (cron setting cannot be used together with this setting.)
*/
every: number;
}
interface DebounceOptions {
/**
* ttl in milliseconds
*/
ttl?: number;
/**
* Identifier
*/
id: string;
}
interface JobOptions {
/**
* Debounce options.
*/
debounce?: DebounceOptions;
/**
* Optional priority value. ranges from 1 (highest priority) to MAX_INT (lowest priority).
* Note that using priorities has a slight impact on performance, so do not use it if not required
*/
priority?: number | undefined;
/**
* An amount of miliseconds to wait until this job can be processed.
* Note that for accurate delays, both server and clients should have their clocks synchronized. [optional]
*/
delay?: number | undefined;
/**
* The total number of attempts to try the job until it completes
*/
attempts?: number | undefined;
/**
* Repeat job according to a cron specification
*/
repeat?:
| ((CronRepeatOptions | EveryRepeatOptions) & {
/**
* The key for the repeatable job metadata in Redis.
*/
readonly key?: string;
})
| undefined;
/**
* Backoff setting for automatic retries if the job fails
*/
backoff?: number | BackoffOptions | undefined;
/**
* A boolean which, if true, adds the job to the right
* of the queue instead of the left (default false)
*/
lifo?: boolean | undefined;
/**
* The number of milliseconds after which the job should be fail with a timeout error
*/
timeout?: number | undefined;
/**
* Override the job ID - by default, the job ID is a unique
* integer, but you can use this setting to override it.
* If you use this option, it is up to you to ensure the
* jobId is unique. If you attempt to add a job with an id that
* already exists, it will not be added.
*/
jobId?: JobId | undefined;
/**
* A boolean which, if true, removes the job when it successfully completes.
* When a number, it specifies the amount of jobs to keep.
* Default behavior is to keep the job in the completed set.
* See KeepJobsOptions if using that interface instead.
*/
removeOnComplete?: boolean | number | KeepJobsOptions | undefined;
/**
* A boolean which, if true, removes the job when it fails after all attempts.
* When a number, it specifies the amount of jobs to keep.
* Default behavior is to keep the job in the failed set.
* See KeepJobsOptions if using that interface instead.
*/
removeOnFail?: boolean | number | KeepJobsOptions | undefined;
/**
* Limits the amount of stack trace lines that will be recorded in the stacktrace.
*/
stackTraceLimit?: number | undefined;
/**
* Prevents JSON data from being parsed.
*/
preventParsingData?: boolean | undefined;
}
/**
* Specify which jobs to keep after finishing processing this job.
* If both age and count are specified, then the jobs kept will be the ones that satisfies both properties.
*/
interface KeepJobsOptions {
/**
* Maximum age in *seconds* for job to be kept.
*/
age?: number | undefined;
/**
* Maximum count of jobs to be kept.
*/
count?: number | undefined;
}
interface JobCounts {
active: number;
completed: number;
failed: number;
delayed: number;
waiting: number;
}
interface JobInformation {
key: string;
name: string;
id?: string | undefined;
endDate?: number | undefined;
tz?: string | undefined;
cron: string;
every: number;
next: number;
}
interface Queue<T = any> extends EventEmitter {
/**
* The name of the queue
*/
name: string;
/**
* Queue client (used to add jobs, pause queues, etc);
*/
client: Redis.Redis;
/**
* Returns a promise that resolves when Redis is connected and the queue is ready to accept jobs.
* This replaces the `ready` event emitted on Queue in previous verisons.
*/
isReady(): Promise<this>;
/* tslint:disable:unified-signatures */
/**
* Defines a processing function for the jobs placed into a given Queue.
*
* The callback is called everytime a job is placed in the queue.
* It is passed an instance of the job as first argument.
*
* If the callback signature contains the second optional done argument,
* the callback will be passed a done callback to be called after the job has been completed.
* The done callback can be called with an Error instance, to signal that the job did not complete successfully,
* or with a result as second argument (e.g.: done(null, result);) when the job is successful.
* Errors will be passed as a second argument to the "failed" event; results, as a second argument to the "completed" event.
*
* If, however, the callback signature does not contain the done argument,
* a promise must be returned to signal job completion.
* If the promise is rejected, the error will be passed as a second argument to the "failed" event.
* If it is resolved, its value will be the "completed" event's second argument.
*/
process(callback: ProcessCallbackFunction<T>): Promise<void>;
process(callback: ProcessPromiseFunction<T>): Promise<void>;
process(callback: string): Promise<void>;
/**
* Defines a processing function for the jobs placed into a given Queue.
*
* The callback is called everytime a job is placed in the queue.
* It is passed an instance of the job as first argument.
*
* If the callback signature contains the second optional done argument,
* the callback will be passed a done callback to be called after the job has been completed.
* The done callback can be called with an Error instance, to signal that the job did not complete successfully,
* or with a result as second argument (e.g.: done(null, result);) when the job is successful.
* Errors will be passed as a second argument to the "failed" event; results, as a second argument to the "completed" event.
*
* If, however, the callback signature does not contain the done argument,
* a promise must be returned to signal job completion.
* If the promise is rejected, the error will be passed as a second argument to the "failed" event.
* If it is resolved, its value will be the "completed" event's second argument.
*
* @param concurrency Bull will then call your handler in parallel respecting this maximum value.
*/
process(
concurrency: number,
callback: ProcessCallbackFunction<T>
): Promise<void>;
process(
concurrency: number,
callback: ProcessPromiseFunction<T>
): Promise<void>;
process(concurrency: number, callback: string): Promise<void>;
/**
* Defines a processing function for the jobs placed into a given Queue.
*
* The callback is called everytime a job is placed in the queue.
* It is passed an instance of the job as first argument.
*
* If the callback signature contains the second optional done argument,
* the callback will be passed a done callback to be called after the job has been completed.
* The done callback can be called with an Error instance, to signal that the job did not complete successfully,
* or with a result as second argument (e.g.: done(null, result);) when the job is successful.
* Errors will be passed as a second argument to the "failed" event; results, as a second argument to the "completed" event.
*
* If, however, the callback signature does not contain the done argument,
* a promise must be returned to signal job completion.
* If the promise is rejected, the error will be passed as a second argument to the "failed" event.
* If it is resolved, its value will be the "completed" event's second argument.
*
* @param name Bull will only call the handler if the job name matches
*/
process(name: string, callback: ProcessCallbackFunction<T>): Promise<void>;
process(name: string, callback: ProcessPromiseFunction<T>): Promise<void>;
process(name: string, callback: string): Promise<void>;
/**
* Defines a processing function for the jobs placed into a given Queue.
*
* The callback is called everytime a job is placed in the queue.
* It is passed an instance of the job as first argument.
*
* If the callback signature contains the second optional done argument,
* the callback will be passed a done callback to be called after the job has been completed.
* The done callback can be called with an Error instance, to signal that the job did not complete successfully,
* or with a result as second argument (e.g.: done(null, result);) when the job is successful.
* Errors will be passed as a second argument to the "failed" event; results, as a second argument to the "completed" event.
*
* If, however, the callback signature does not contain the done argument,
* a promise must be returned to signal job completion.
* If the promise is rejected, the error will be passed as a second argument to the "failed" event.
* If it is resolved, its value will be the "completed" event's second argument.
*
* @param name Bull will only call the handler if the job name matches
* @param concurrency Bull will then call your handler in parallel respecting this maximum value.
*/
process(
name: string,
concurrency: number,
callback: ProcessCallbackFunction<T>
): Promise<void>;
process(
name: string,
concurrency: number,
callback: ProcessPromiseFunction<T>
): Promise<void>;
process(name: string, concurrency: number, callback: string): Promise<void>;
/* tslint:enable:unified-signatures */
/**
* Creates a new job and adds it to the queue.
* If the queue is empty the job will be executed directly,
* otherwise it will be placed in the queue and executed as soon as possible.
*/
add(data: T, opts?: JobOptions): Promise<Job<T>>;
/**
* Creates a new named job and adds it to the queue.
* If the queue is empty the job will be executed directly,
* otherwise it will be placed in the queue and executed as soon as possible.
*/
add(name: string, data: T, opts?: JobOptions): Promise<Job<T>>;
/**
* Adds an array of jobs to the queue.
* If the queue is empty the jobs will be executed directly,
* otherwise they will be placed in the queue and executed as soon as possible.
* 'repeat' option is not supported in addBulk https://github.com/OptimalBits/bull/issues/1731
*/
addBulk(
jobs: Array<{
name?: string | undefined;
data: T;
opts?: Omit<JobOptions, 'repeat'> | undefined;
}>
): Promise<Array<Job<T>>>;
/**
* Returns a promise that resolves when the queue is paused.
*
* A paused queue will not process new jobs until resumed, but current jobs being processed will continue until
* they are finalized. The pause can be either global or local. If global, all workers in all queue instances
* for a given queue will be paused. If local, just this worker will stop processing new jobs after the current
* lock expires. This can be useful to stop a worker from taking new jobs prior to shutting down.
*
* If doNotWaitActive is true, pause will not wait for any active jobs to finish before resolving. Otherwise, pause
* will wait for active jobs to finish. See Queue#whenCurrentJobsFinished for more information.
*
* Pausing a queue that is already paused does nothing.
*/
pause(isLocal?: boolean, doNotWaitActive?: boolean): Promise<void>;
/**
* Returns a promise that resolves when the queue is resumed after being paused.
*
* The resume can be either local or global. If global, all workers in all queue instances for a given queue
* will be resumed. If local, only this worker will be resumed. Note that resuming a queue globally will not
* resume workers that have been paused locally; for those, resume(true) must be called directly on their
* instances.
*
* Resuming a queue that is not paused does nothing.
*/
resume(isLocal?: boolean): Promise<void>;
/**
* Returns a promise that resolves with a boolean if queue is paused
*/
isPaused(isLocal?: boolean): Promise<boolean>;
/**
* Returns a promise that returns the number of jobs in the queue, waiting or paused.
* Since there may be other processes adding or processing jobs, this value may be true only for a very small amount of time.
*/
count(): Promise<number>;
/**
* Empties a queue deleting all the input lists and associated jobs.
*/
empty(): Promise<void>;
/**
* Closes the underlying redis client. Use this to perform a graceful shutdown.
*
* `close` can be called from anywhere, with one caveat:
* if called from within a job handler the queue won't close until after the job has been processed
*/
close(doNotWaitJobs?: boolean): Promise<void>;
/**
* Returns the number of jobs per priority.
*/
getCountsPerPriority(priorities: number[]): Promise<{
[index: string]: number;
}>;
/**
* Returns a promise that will return the job instance associated with the jobId parameter.
* If the specified job cannot be located, the promise callback parameter will be set to null.
*/
getJob(jobId: JobId): Promise<Job<T> | null>;
/**
* Returns a promise that will return an array with the waiting jobs between start and end.
*/
getWaiting(start?: number, end?: number): Promise<Array<Job<T>>>;
/**
* Returns a promise that will return an array with the active jobs between start and end.
*/
getActive(start?: number, end?: number): Promise<Array<Job<T>>>;
/**
* Returns a promise that will return an array with the delayed jobs between start and end.
*/
getDelayed(start?: number, end?: number): Promise<Array<Job<T>>>;
/**
* Returns a promise that will return an array with the completed jobs between start and end.
*/
getCompleted(start?: number, end?: number): Promise<Array<Job<T>>>;
/**
* Returns a promise that will return an array with the failed jobs between start and end.
*/
getFailed(start?: number, end?: number): Promise<Array<Job<T>>>;
/**
* Returns JobInformation of repeatable jobs (ordered descending). Provide a start and/or an end
* index to limit the number of results. Start defaults to 0, end to -1 and asc to false.
*/
getRepeatableJobs(
start?: number,
end?: number,
asc?: boolean
): Promise<JobInformation[]>;
/**
* ???
*/
nextRepeatableJob(
name: string,
data: any,
opts: JobOptions
): Promise<Job<T>>;
/**
* Removes a given repeatable job. The RepeatOptions and JobId needs to be the same as the ones
* used for the job when it was added.
*/
removeRepeatable(
repeat: (CronRepeatOptions | EveryRepeatOptions) & {
jobId?: JobId | undefined;
}
): Promise<void>;
/**
* Removes a given repeatable job. The RepeatOptions and JobId needs to be the same as the ones
* used for the job when it was added.
*
* name: The name of the to be removed job
*/
removeRepeatable(
name: string,
repeat: (CronRepeatOptions | EveryRepeatOptions) & {
jobId?: JobId | undefined;
}
): Promise<void>;
/**
* Removes a given repeatable job by key.
*/
removeRepeatableByKey(key: string): Promise<void>;
/**
* Returns a promise that will return an array of job instances of the given job statuses.
* Optional parameters for range and ordering are provided.
*/
getJobs(
types: JobStatus[],
start?: number,
end?: number,
asc?: boolean
): Promise<Array<Job<T>>>;
/**
* Returns a promise that resolves to a Metrics object.
*/
getMetrics(type: 'completed' | 'failed', start?: number, end?: number): Promise<{
meta: {
count: number;
prevTS: number;
prevCount: number;
};
data: number[];
count: number;
}>
/**
* Returns a promise that resolves to the next job in queue.
*/
getNextJob(): Promise<Job<T> | undefined>;
/**
* Returns a object with the logs according to the start and end arguments. The returned count
* value is the total amount of logs, useful for implementing pagination.
*/
getJobLogs(
jobId: JobId,
start?: number,
end?: number
): Promise<{ logs: string[]; count: number }>;
/**
* Returns a promise that resolves with the job counts for the given queue.
*/
getJobCounts(): Promise<JobCounts>;
/**
* Returns a promise that resolves with the job counts for the given queue of the given job statuses.
*/
getJobCountByTypes(types: JobStatus[] | JobStatus): Promise<number>;
/**
* Returns a promise that resolves with the quantity of completed jobs.
*/
getCompletedCount(): Promise<number>;
/**
* Returns a promise that resolves with the quantity of failed jobs.
*/
getFailedCount(): Promise<number>;
/**
* Returns a promise that resolves with the quantity of delayed jobs.
*/
getDelayedCount(): Promise<number>;
/**
* Returns a promise that resolves with the quantity of waiting jobs.
*/
getWaitingCount(): Promise<number>;
/**
* Returns a promise that resolves with the quantity of paused jobs.
*/
getPausedCount(): Promise<number>;
/**
* Returns a promise that resolves with the quantity of active jobs.
*/
getActiveCount(): Promise<number>;
/**
* Returns a promise that resolves to the quantity of repeatable jobs.
*/
getRepeatableCount(): Promise<number>;
/**
* Tells the queue remove all jobs created outside of a grace period in milliseconds.
* You can clean the jobs with the following states: completed, wait (typo for waiting), active, delayed, and failed.
* @param grace Grace period in milliseconds.
* @param status Status of the job to clean. Values are completed, wait, active, delayed, and failed. Defaults to completed.
* @param limit Maximum amount of jobs to clean per call. If not provided will clean all matching jobs.
*/
clean(
grace: number,
status?: JobStatusClean,
limit?: number
): Promise<Array<Job<T>>>;
/**
* Returns a promise that resolves to a Metrics object.
* @param type Job metric type either 'completed' or 'failed'
* @param start Start point of the metrics, where 0 is the newest point to be returned.
* @param end End point of the metrics, where -1 is the oldest point to be returned.
* @returns - Returns an object with queue metrics.
*/
getMetrics(
type: 'completed' | 'failed',
start?: number,
end?: number
): Promise<{
meta: {
count: number;
prevTS: number;
prevCount: number;
};
data: number[];
count: number;
}>;
/**
* Returns a promise that marks the start of a transaction block.
*/
multi(): Redis.Pipeline;
/**
* Returns the queue specific key.
*/
toKey(queueType: string): string;
/**
* Completely destroys the queue and all of its contents irreversibly.
* @param ops.force Obliterate the queue even if there are active jobs
*/
obliterate(ops?: { force: boolean }): Promise<void>;
/**
* Listens to queue events
*/
on(event: string, callback: (...args: any[]) => void): this;
/**
* An error occured
*/
on(event: 'error', callback: ErrorEventCallback): this;
/**
* A Job is waiting to be processed as soon as a worker is idling.
*/
on(event: 'waiting', callback: WaitingEventCallback): this;
/**
* A job has started. You can use `jobPromise.cancel()` to abort it
*/
on(event: 'active', callback: ActiveEventCallback<T>): this;
/**
* A job has been marked as stalled.
* This is useful for debugging job workers that crash or pause the event loop.
*/
on(event: 'stalled', callback: StalledEventCallback<T>): this;
/**
* A job's progress was updated
*/
on(event: 'progress', callback: ProgressEventCallback<T>): this;
/**
* A job successfully completed with a `result`
*/
on(event: 'completed', callback: CompletedEventCallback<T>): this;
/**
* A job failed with `err` as the reason
*/
on(event: 'failed', callback: FailedEventCallback<T>): this;
/**
* The queue has been paused
*/
on(event: 'paused', callback: EventCallback): this;
/**
* The queue has been resumed
*/
on(event: 'resumed', callback: EventCallback): this; // tslint:disable-line unified-signatures
/**
* A job successfully removed.
*/
on(event: 'removed', callback: RemovedEventCallback<T>): this;
/**
* Old jobs have been cleaned from the queue.
* `jobs` is an array of jobs that were removed, and `type` is the type of those jobs.
*
* @see Queue#clean() for details
*/
on(event: 'cleaned', callback: CleanedEventCallback<T>): this;
/**
* Emitted every time the queue has processed all the waiting jobs
* (even if there can be some delayed jobs not yet processed)
*/
on(event: 'drained', callback: EventCallback): this; // tslint:disable-line unified-signatures
/**
* Array of Redis clients the queue uses
*/
clients: Redis.Redis[];
/**
* Set clientName to Redis.client
*/
setWorkerName(): Promise<any>;
/**
* Returns array of workers that are currently working on this queue.
*/
getWorkers(): Promise<{ [index: string]: string }[]>;
/**
* Returns Queue name in base64 encoded format
*/
base64Name(): string;
/**
* Returns Queue name with keyPrefix (default: 'bull')
*/
clientName(): string;
/**
* Returns Redis clients array which belongs to current Queue from string with all redis clients
*
* @param list String with all redis clients
*/
parseClientList(list: string): { [index: string]: string }[][];
/**
* Returns a promise that resolves when active jobs are finished
*/
whenCurrentJobsFinished(): Promise<void>;
/**
* Removes all the jobs which jobId matches the given pattern. The pattern must follow redis glob-style pattern
* (syntax)[redis.io/commands/keys]
*/
removeJobs(pattern: string): Promise<void>;
}
type EventCallback = () => void;
type ErrorEventCallback = (error: Error) => void;
interface JobPromise {
/**
* Abort this job
*/
cancel(): void;
}
type ActiveEventCallback<T = any> = (
job: Job<T>,
jobPromise?: JobPromise
) => void;
type StalledEventCallback<T = any> = (job: Job<T>) => void;
type ProgressEventCallback<T = any> = (job: Job<T>, progress: any) => void;
type CompletedEventCallback<T = any> = (job: Job<T>, result: any) => void;
type FailedEventCallback<T = any> = (job: Job<T>, error: Error) => void;
type CleanedEventCallback<T = any> = (
jobs: Array<Job<T>>,
status: JobStatusClean
) => void;
type RemovedEventCallback<T = any> = (job: Job<T>) => void;
type WaitingEventCallback = (jobId: JobId) => void;
}
export = Bull;
+5
View File
@@ -0,0 +1,5 @@
'use strict';
module.exports = require('./lib/queue');
module.exports.Job = require('./lib/job');
module.exports.utils = require('./lib/utils');
+56
View File
@@ -0,0 +1,56 @@
'use strict';
const _ = require('lodash');
const builtinStrategies = {
fixed(delay) {
return function() {
return delay;
};
},
exponential(delay) {
return function(attemptsMade) {
return Math.round((Math.pow(2, attemptsMade) - 1) * delay);
};
}
};
function lookupStrategy(backoff, customStrategies) {
if (backoff.type in customStrategies) {
return customStrategies[backoff.type];
} else if (backoff.type in builtinStrategies) {
return builtinStrategies[backoff.type](backoff.delay);
} else {
throw new Error(
'Unknown backoff strategy ' +
backoff.type +
'. If a custom backoff strategy is used, specify it when the queue is created.'
);
}
}
module.exports = {
normalize(backoff) {
if (_.isFinite(backoff)) {
return {
type: 'fixed',
delay: backoff
};
} else if (backoff) {
return backoff;
}
},
calculate(backoff, attemptsMade, customStrategies, err, strategyOptions) {
if (backoff) {
const strategy = lookupStrategy(
backoff,
customStrategies,
strategyOptions
);
return strategy(attemptsMade, err, strategyOptions);
}
}
};
+112
View File
@@ -0,0 +1,112 @@
--[[
Adds a job to the queue by doing the following:
- Increases the job counter if needed.
- Creates a new job key with the job data.
- if delayed:
- computes timestamp.
- adds to delayed zset.
- Emits a global event 'delayed' if the job is delayed.
- if not delayed
- Adds the jobId to the wait/paused list in one of three ways:
- LIFO
- FIFO
- prioritized.
- Adds the job to the "added" list so that workers gets notified.
Input:
KEYS[1] 'wait',
KEYS[2] 'paused'
KEYS[3] 'meta-paused'
KEYS[4] 'id'
KEYS[5] 'delayed'
KEYS[6] 'priority'
ARGV[1] key prefix,
ARGV[2] custom id (will not generate one automatically)
ARGV[3] name
ARGV[4] data (json stringified job data)
ARGV[5] opts (json stringified job opts)
ARGV[6] timestamp
ARGV[7] delay
ARGV[8] delayedTimestamp
ARGV[9] priority
ARGV[10] LIFO
ARGV[11] token
ARGV[12] debounce key
ARGV[13] debounceId
ARGV[14] debounceTtl
]]
local jobId
local jobIdKey
local rcall = redis.call
-- Includes
--- @include "includes/addJobWithPriority"
--- @include "includes/debounceJob"
--- @include "includes/getTargetQueueList"
local jobCounter = rcall("INCR", KEYS[4])
if ARGV[2] == "" then
jobId = jobCounter
jobIdKey = ARGV[1] .. jobId
else
jobId = ARGV[2]
jobIdKey = ARGV[1] .. jobId
if rcall("EXISTS", jobIdKey) == 1 then
rcall("PUBLISH", ARGV[1] .. "duplicated@" .. ARGV[11], jobId)
return jobId .. "" -- convert to string
end
end
local debounceKey = ARGV[12]
local opts = cmsgpack.unpack(ARGV[5])
local debouncedJobId = debounceJob(ARGV[1], ARGV[13], ARGV[14],
jobId, debounceKey, ARGV[11])
if debouncedJobId then
return debouncedJobId
end
local debounceId = ARGV[13]
local optionalValues = {}
if debounceId ~= "" then
table.insert(optionalValues, "deid")
table.insert(optionalValues, debounceId)
end
-- Store the job.
rcall("HMSET", jobIdKey, "name", ARGV[3], "data", ARGV[4], "opts", opts, "timestamp",
ARGV[6], "delay", ARGV[7], "priority", ARGV[9], unpack(optionalValues))
-- Check if job is delayed
local delayedTimestamp = tonumber(ARGV[8])
if(delayedTimestamp ~= 0) then
local timestamp = delayedTimestamp * 0x1000 + bit.band(jobCounter, 0xfff)
rcall("ZADD", KEYS[5], timestamp, jobId)
rcall("PUBLISH", KEYS[5], delayedTimestamp)
else
local target
-- Whe check for the meta-paused key to decide if we are paused or not
-- (since an empty list and !EXISTS are not really the same)
local target, paused = getTargetQueueList(KEYS[3], KEYS[1], KEYS[2])
-- Standard or priority add
local priority = tonumber(ARGV[9])
if priority == 0 then
-- LIFO or FIFO
rcall(ARGV[10], target, jobId)
else
addJobWithPriority(KEYS[6], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[1] .. "ing@" .. ARGV[11], jobId)
end
return jobId .. "" -- convert to string
+30
View File
@@ -0,0 +1,30 @@
--[[
Add job log
Input:
KEYS[1] job id key
KEYS[2] job logs key
ARGV[1] id
ARGV[2] log
ARGV[3] keepLogs
Output:
-1 - Missing job.
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then -- // Make sure job exists
local logCount = rcall("RPUSH", KEYS[2], ARGV[2])
if ARGV[3] ~= '' then
local keepLogs = tonumber(ARGV[3])
rcall("LTRIM", KEYS[2], -keepLogs, -1)
return math.min(keepLogs, logCount)
end
return logCount
else
return -1
end
+146
View File
@@ -0,0 +1,146 @@
--[[
Remove jobs from the specific set.
Input:
KEYS[1] set key,
KEYS[2] priority key
KEYS[3] rate limiter key
ARGV[1] prefix key
ARGV[2] maxTimestamp
ARGV[3] limit the number of jobs to be removed. 0 is unlimited
ARGV[4] set name, can be any of 'wait', 'active', 'paused', 'delayed', 'completed', or 'failed'
]]
local setKey = KEYS[1]
local priorityKey = KEYS[2]
local rateLimiterKey = KEYS[3]
local prefixKey = ARGV[1]
local maxTimestamp = ARGV[2]
local limitStr = ARGV[3]
local setName = ARGV[4]
local isList = false
local rcall = redis.call
-- Includes
--- @include "includes/removeDebounceKey"
if setName == "wait" or setName == "active" or setName == "paused" then
isList = true
end
-- We use ZRANGEBYSCORE to make the case where we're deleting a limited number
-- of items in a sorted set only run a single iteration. If we simply used
-- ZRANGE, we may take a long time traversing through jobs that are within the
-- grace period.
local function shouldUseZRangeByScore(isList, limit)
return not isList and limit > 0
end
local function getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
if isList then
return rcall("LRANGE", setKey, rangeStart, rangeEnd)
elseif shouldUseZRangeByScore(isList, limit) then
return rcall("ZRANGEBYSCORE", setKey, 0, maxTimestamp, "LIMIT", 0, limit)
else
return rcall("ZRANGE", setKey, rangeStart, rangeEnd)
end
end
local limit = tonumber(limitStr)
local rangeStart = 0
local rangeEnd = -1
-- If we're only deleting _n_ items, avoid retrieving all items
-- for faster performance
--
-- Start from the tail of the list, since that's where oldest elements
-- are generally added for FIFO lists
if limit > 0 then
rangeStart = -1 - limit + 1
rangeEnd = -1
end
local jobIds = getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
local deleted = {}
local deletedCount = 0
local jobTS
-- Run this loop:
-- - Once, if limit is -1 or 0
-- - As many times as needed if limit is positive
while ((limit <= 0 or deletedCount < limit) and next(jobIds, nil) ~= nil) do
local jobIdsLen = #jobIds
for i, jobId in ipairs(jobIds) do
if limit > 0 and deletedCount >= limit then
break
end
local jobKey = prefixKey .. jobId
if (rcall("EXISTS", jobKey .. ":lock") == 0) then
-- Find the right timestamp of the job to compare to maxTimestamp:
-- * finishedOn says when the job was completed, but it isn't set unless the job has actually completed
-- * processedOn represents when the job was last attempted, but it doesn't get populated until the job is first tried
-- * timestamp is the original job submission time
-- Fetch all three of these (in that order) and use the first one that is set so that we'll leave jobs that have been active within the grace period:
for _, ts in ipairs(rcall("HMGET", jobKey, "finishedOn", "processedOn", "timestamp")) do
if (ts) then
jobTS = ts
break
end
end
if (not jobTS or jobTS < maxTimestamp) then
if isList then
-- Job ids can't be the empty string. Use the empty string as a
-- deletion marker. The actual deletion will occur at the end of the
-- script.
rcall("LSET", setKey, rangeEnd - jobIdsLen + i, "")
else
rcall("ZREM", setKey, jobId)
end
rcall("ZREM", priorityKey, jobId)
if setName ~= "completed" and setName ~= "failed" then
removeDebounceKey(prefixKey, jobKey)
end
rcall("DEL", jobKey)
rcall("DEL", jobKey .. ":logs")
-- delete keys related to rate limiter
-- NOTE: this code is unncessary for other sets than wait, paused and delayed.
local limiterIndexTable = rateLimiterKey .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
deletedCount = deletedCount + 1
table.insert(deleted, jobId)
end
end
end
-- If we didn't have a limit or used the single-iteration ZRANGEBYSCORE
-- function, return immediately. We should have deleted all the jobs we can
if limit <= 0 or shouldUseZRangeByScore(isList, limit) then
break
end
if deletedCount < limit then
-- We didn't delete enough. Look for more to delete
rangeStart = rangeStart - limit
rangeEnd = rangeEnd - limit
jobIds = getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
end
end
if isList then
rcall("LREM", setKey, 0, "")
end
return deleted
+22
View File
@@ -0,0 +1,22 @@
--[[
Extend lock and removes the job from the stalled set.
Input:
KEYS[1] 'lock',
KEYS[2] 'stalled'
ARGV[1] token
ARGV[2] lock duration in milliseconds
ARGV[3] jobid
Output:
"1" if lock extended succesfully.
]]
local rcall = redis.call
if rcall("GET", KEYS[1]) == ARGV[1] then
if rcall("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) then
rcall("SREM", KEYS[2], ARGV[3])
return 1
end
end
return 0
+37
View File
@@ -0,0 +1,37 @@
--[[
Get counts per provided states
Input:
KEYS[1] wait key
KEYS[2] paused key
KEYS[3] meta-paused key
KEYS[4] priority key
ARGV[1...] priorities
]]
local rcall = redis.call
local results = {}
local prioritizedKey = KEYS[4]
-- Includes
--- @include "includes/getTargetQueueList"
for i = 1, #ARGV do
local priority = tonumber(ARGV[i])
if priority == 0 then
local target = getTargetQueueList(KEYS[3], KEYS[1], KEYS[2])
local count = rcall("LLEN", target) - rcall("ZCARD", prioritizedKey)
if count < 0 then
-- considering when last waiting job is moved to active before
-- removing priority reference
results[#results+1] = 0
else
results[#results+1] = count
end
else
results[#results+1] = rcall("ZCOUNT", prioritizedKey,
priority, priority)
end
end
return results
+16
View File
@@ -0,0 +1,16 @@
--[[
Function to add job considering priority.
]]
local function addJobWithPriority(priorityKey, priority, jobId, targetKey)
rcall("ZADD", priorityKey, priority, jobId)
local count = rcall("ZCOUNT", priorityKey, 0, priority)
local len = rcall("LLEN", targetKey)
local id = rcall("LINDEX", targetKey, len - (count - 1))
if id then
rcall("LINSERT", targetKey, "BEFORE", id, jobId)
else
rcall("RPUSH", targetKey, jobId)
end
end
+18
View File
@@ -0,0 +1,18 @@
--[[
Function to loop in batches.
Just a bit of warning, some commands as ZREM
could receive a maximum of 7000 parameters per call.
]]
local function batches(n, batchSize)
local i = 0
return function()
local from = i * batchSize + 1
i = i + 1
if (from <= n) then
local to = math.min(from + batchSize - 1, n)
return from, to
end
end
end
+46
View File
@@ -0,0 +1,46 @@
--[[
Functions to collect metrics based on a current and previous count of jobs.
Granualarity is fixed at 1 minute.
]]
-- Includes
--- @include "batches"
local function collectMetrics(metaKey, dataPointsList, maxDataPoints, timestamp)
-- Increment current count
local count = rcall("HINCRBY", metaKey, "count", 1) - 1
-- Compute how many data points we need to add to the list, N.
local prevTS = rcall("HGET", metaKey, "prevTS")
if not prevTS then
-- If prevTS is nil, set it to the current timestamp
rcall("HSET", metaKey, "prevTS", timestamp, "prevCount", 0)
return
end
local N = math.min(math.floor(timestamp / 60000) - math.floor(prevTS / 60000), tonumber(maxDataPoints))
if N > 0 then
local delta = count - rcall("HGET", metaKey, "prevCount")
-- If N > 1, add N-1 zeros to the list
if N > 1 then
local points = {}
points[1] = delta
for i = 2, N do points[i] = 0 end
for from, to in batches(#points, 7000) do
rcall("LPUSH", dataPointsList, unpack(points, from, to))
end
else
-- LPUSH delta to the list
rcall("LPUSH", dataPointsList, delta)
end
-- LTRIM to keep list to its max size
rcall("LTRIM", dataPointsList, 0, maxDataPoints - 1)
-- update prev count with current count
rcall("HSET", metaKey, "prevCount", count, "prevTS", timestamp)
end
end
+20
View File
@@ -0,0 +1,20 @@
--[[
Function to debounce a job.
]]
local function debounceJob(prefixKey, debounceId, ttl, jobId, debounceKey, token)
if debounceId ~= "" then
local debounceKeyExists
if ttl ~= "" then
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'PX', ttl, 'NX')
else
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'NX')
end
if debounceKeyExists then
local currentDebounceJobId = rcall('GET', debounceKey)
rcall("PUBLISH", prefixKey .. "debounced@" .. token, currentDebounceJobId)
return currentDebounceJobId
end
end
end
+12
View File
@@ -0,0 +1,12 @@
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
+12
View File
@@ -0,0 +1,12 @@
--[[
Function to remove debounce key.
]]
local function removeDebounceKey(prefixKey, jobKey)
local debounceId = rcall("HGET", jobKey, "deid")
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
rcall("DEL", debounceKey)
end
end
@@ -0,0 +1,14 @@
--[[
Function to remove debounce key if needed.
]]
local function removeDebounceKeyIfNeeded(prefixKey, debounceId)
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
local pttl = rcall("PTTL", debounceKey)
if pttl == 0 or pttl == -1 then
rcall("DEL", debounceKey)
end
end
end
+19
View File
@@ -0,0 +1,19 @@
local function removeLock(jobKey, stalledKey, token, jobId)
if token ~= "0" then
local lockKey = jobKey .. ':lock'
local lockToken = rcall("GET", lockKey)
if lockToken == token then
rcall("DEL", lockKey)
rcall("SREM", stalledKey, jobId)
else
if lockToken then
-- Lock exists but token does not match
return -6
else
-- Lock is missing completely
return -2
end
end
end
return 0
end
+22
View File
@@ -0,0 +1,22 @@
--[[
Checks if a job is finished (.i.e. is in the completed or failed set)
Input:
KEYS[1] completed key
KEYS[2] failed key
ARGV[1] job id
Output:
0 - not finished.
1 - completed.
2 - failed.
]]
if redis.call("ZSCORE", KEYS[1], ARGV[1]) ~= false then
return 1
end
if redis.call("ZSCORE", KEYS[2], ARGV[1]) ~= false then
return 2
end
return redis.call("ZSCORE", KEYS[2], ARGV[1])
+20
View File
@@ -0,0 +1,20 @@
--[[
Checks if job is in a given list.
Input:
KEYS[1]
ARGV[1]
Output:
1 if element found in the list.
]]
local function item_in_list (list, item)
for _, v in pairs(list) do
if v == item then
return 1
end
end
return nil
end
local items = redis.call("LRANGE", KEYS[1] , 0, -1)
return item_in_list(items, ARGV[1])
+137
View File
@@ -0,0 +1,137 @@
--[[
Move stalled jobs to wait.
Input:
KEYS[1] 'stalled' (SET)
KEYS[2] 'wait', (LIST)
KEYS[3] 'active', (LIST)
KEYS[4] 'failed', (ZSET)
KEYS[5] 'stalled-check', (KEY)
KEYS[6] 'meta-paused', (KEY)
KEYS[7] 'paused', (LIST)
ARGV[1] Max stalled job count
ARGV[2] queue.toKey('')
ARGV[3] timestamp
ARGV[4] max check time
Events:
'stalled' with stalled job id.
]]
local rcall = redis.call
-- Includes
--- @include "includes/batches"
--- @include "includes/getTargetQueueList"
--- @include "includes/removeDebounceKeyIfNeeded"
local function removeJob(jobId, baseKey)
local jobKey = baseKey .. jobId
rcall("DEL", jobKey, jobKey .. ':logs')
end
local function removeJobsByMaxAge(timestamp, maxAge, targetSet, prefix)
local start = timestamp - maxAge * 1000
local jobIds = rcall("ZREVRANGEBYSCORE", targetSet, start, "-inf")
for i, jobId in ipairs(jobIds) do
removeJob(jobId, prefix)
end
rcall("ZREMRANGEBYSCORE", targetSet, "-inf", start)
end
local function removeJobsByMaxCount(maxCount, targetSet, prefix)
local start = maxCount
local jobIds = rcall("ZREVRANGE", targetSet, start, -1)
for i, jobId in ipairs(jobIds) do
removeJob(jobId, prefix)
end
rcall("ZREMRANGEBYRANK", targetSet, 0, -(maxCount + 1))
end
-- Check if we need to check for stalled jobs now.
if rcall("EXISTS", KEYS[5]) == 1 then
return {{}, {}}
end
rcall("SET", KEYS[5], ARGV[3], "PX", ARGV[4])
-- Move all stalled jobs to wait
local stalling = rcall('SMEMBERS', KEYS[1])
local stalled = {}
local failed = {}
if(#stalling > 0) then
rcall('DEL', KEYS[1])
local MAX_STALLED_JOB_COUNT = tonumber(ARGV[1])
-- Remove from active list
for i, jobId in ipairs(stalling) do
local jobKey = ARGV[2] .. jobId
-- Check that the lock is also missing, then we can handle this job as really stalled.
if(rcall("EXISTS", jobKey .. ":lock") == 0) then
-- Remove from the active queue.
local removed = rcall("LREM", KEYS[3], 1, jobId)
if(removed > 0) then
-- If this job has been stalled too many times, such as if it crashes the worker, then fail it.
local stalledCount = rcall("HINCRBY", jobKey, "stalledCounter", 1)
if(stalledCount > MAX_STALLED_JOB_COUNT) then
local jobAttributes = rcall("HMGET", jobKey, "opts", "deid")
local opts = cjson.decode(jobAttributes[1])
local removeOnFailType = type(opts["removeOnFail"])
rcall("ZADD", KEYS[4], ARGV[3], jobId)
rcall("HMSET", jobKey, "failedReason", "job stalled more than allowable limit",
"finishedOn", ARGV[3])
removeDebounceKeyIfNeeded(ARGV[2], jobAttributes[2])
rcall("PUBLISH", KEYS[4], '{"jobId":"' .. jobId .. '", "val": "job stalled more than maxStalledCount"}')
if removeOnFailType == "number" then
removeJobsByMaxCount(opts["removeOnFail"],
KEYS[4], ARGV[2])
elseif removeOnFailType == "boolean" then
if opts["removeOnFail"] then
removeJob(jobId, ARGV[2])
rcall("ZREM", KEYS[4], jobId)
end
elseif removeOnFailType ~= "nil" then
local maxAge = opts["removeOnFail"]["age"]
local maxCount = opts["removeOnFail"]["count"]
if maxAge ~= nil then
removeJobsByMaxAge(ARGV[3], maxAge,
KEYS[4], ARGV[2])
end
if maxCount ~= nil and maxCount > 0 then
removeJobsByMaxCount(maxCount, KEYS[4],
ARGV[2])
end
end
table.insert(failed, jobId)
else
local target = getTargetQueueList(KEYS[6], KEYS[2], KEYS[7])
-- Move the job back to the wait queue, to immediately be picked up by a waiting worker.
rcall("RPUSH", target, jobId)
rcall('PUBLISH', KEYS[1] .. '@', jobId)
table.insert(stalled, jobId)
end
end
end
end
end
-- Mark potentially stalled jobs
local active = rcall('LRANGE', KEYS[3], 0, -1)
if (#active > 0) then
for from, to in batches(#active, 7000) do
rcall('SADD', KEYS[1], unpack(active, from, to))
end
end
return {failed, stalled}
+149
View File
@@ -0,0 +1,149 @@
--[[
Move next job to be processed to active, lock it and fetch its data. The job
may be delayed, in that case we need to move it to the delayed set instead.
This operation guarantees that the worker owns the job during the locks
expiration time. The worker is responsible of keeping the lock fresh
so that no other worker picks this job again.
Input:
KEYS[1] wait key
KEYS[2] active key
KEYS[3] priority key
KEYS[4] active event key
KEYS[5] stalled key
-- Rate limiting
KEYS[6] rate limiter key
KEYS[7] delayed key
--
KEYS[8] drained key
ARGV[1] key prefix
ARGV[2] lock token
ARGV[3] lock duration in milliseconds
ARGV[4] timestamp
ARGV[5] optional jobid
ARGV[6] optional jobs per time unit (rate limiter)
ARGV[7] optional time unit (rate limiter)
ARGV[8] optional do not do anything with job if rate limit hit
ARGV[9] optional rate limit by key
]]
local rcall = redis.call
local rateLimit = function(jobId, maxJobs)
local rateLimiterKey = KEYS[6];
local limiterIndexTable = rateLimiterKey .. ":index"
-- Rate limit by group?
if(ARGV[9]) then
local group = string.match(jobId, "[^:]+$")
if group ~= nil then
rateLimiterKey = rateLimiterKey .. ":" .. group
end
end
-- -- key for storing rate limited jobs
-- When a job has been previously rate limited it should be part of this set
-- if the job is back here means that the delay time for this job has passed and now we should
-- be able to process it again.
local limitedSetKey = rateLimiterKey .. ":limited"
local delay = 0
-- -- Check if job was already limited
local isLimited = rcall("SISMEMBER", limitedSetKey, jobId);
if isLimited == 1 then
-- Remove from limited zset since we are going to try to process it
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
else
-- If not, check if there are any limited jobs
-- If the job has not been rate limited, we should check if there are any other rate limited jobs, because if that
-- is the case we do not want to process this job, just calculate a delay for it and put it to "sleep".
local numLimitedJobs = rcall("SCARD", limitedSetKey)
if numLimitedJobs > 0 then
-- Note, add some slack to compensate for drift.
delay = ((numLimitedJobs * ARGV[7] * 1.1) / maxJobs) + tonumber(rcall("PTTL", rateLimiterKey))
end
end
local jobCounter = tonumber(rcall("GET", rateLimiterKey))
if(jobCounter == nil) then
jobCounter = 0
end
-- check if rate limit hit
if (delay == 0) and (jobCounter >= maxJobs) then
-- Seems like there are no current rated limited jobs, but the jobCounter has exceeded the number of jobs for this unit of time so we need to rate limit this job.
local exceedingJobs = jobCounter - maxJobs
delay = tonumber(rcall("PTTL", rateLimiterKey)) + ((exceedingJobs) * ARGV[7]) / maxJobs
end
if delay > 0 then
local bounceBack = ARGV[8]
if bounceBack == 'false' then
local timestamp = delay + tonumber(ARGV[4])
-- put job into delayed queue
rcall("ZADD", KEYS[7], timestamp * 0x1000 + bit.band(jobCounter, 0xfff), jobId)
rcall("PUBLISH", KEYS[7], timestamp)
rcall("SADD", limitedSetKey, jobId)
-- store index so that we can delete rate limited data
rcall("HSET", limiterIndexTable, jobId, limitedSetKey)
end
-- remove from active queue
rcall("LREM", KEYS[2], 1, jobId)
return true
else
-- false indicates not rate limited
-- increment jobCounter only when a job is not rate limited
if (jobCounter == 0) then
rcall("PSETEX", rateLimiterKey, ARGV[7], 1)
else
rcall("INCR", rateLimiterKey)
end
return false
end
end
local jobId = ARGV[5]
if jobId ~= '' then
-- clean stalled key
rcall("SREM", KEYS[5], jobId)
else
-- move from wait to active
jobId = rcall("RPOPLPUSH", KEYS[1], KEYS[2])
end
if jobId then
-- Check if we need to perform rate limiting.
local maxJobs = tonumber(ARGV[6])
if maxJobs then
if rateLimit(jobId, maxJobs) then
return
end
end
-- get a lock
local jobKey = ARGV[1] .. jobId
local lockKey = jobKey .. ':lock'
rcall("SET", lockKey, ARGV[2], "PX", ARGV[3])
-- remove from priority
rcall("ZREM", KEYS[3], jobId)
rcall("PUBLISH", KEYS[4], jobId)
rcall("HSET", jobKey, "processedOn", ARGV[4])
return {rcall("HGETALL", jobKey), jobId} -- get job data
else
rcall("PUBLISH", KEYS[8], "")
end
+43
View File
@@ -0,0 +1,43 @@
--[[
Moves job from active to delayed set.
Input:
KEYS[1] active key
KEYS[2] delayed key
KEYS[3] job key
KEYS[4] stalled key
ARGV[1] delayedTimestamp
ARGV[2] the id of the job
ARGV[3] queue token
Output:
0 - OK
-1 - Missing job.
-2 - Job is locked.
Events:
- delayed key.
]]
local rcall = redis.call
-- Includes
--- @include "includes/removeLock"
if rcall("EXISTS", KEYS[3]) == 1 then
local errorCode = removeLock(KEYS[3], KEYS[4], ARGV[3], ARGV[2])
if errorCode < 0 then
return errorCode
end
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[2])
if numRemovedElements < 1 then return -3 end
local score = tonumber(ARGV[1])
rcall("ZADD", KEYS[2], score, ARGV[2])
rcall("PUBLISH", KEYS[2], (score / 0x1000))
return 0
else
return -1
end
+134
View File
@@ -0,0 +1,134 @@
--[[
Move job from active to a finished status (completed or failed)
A job can only be moved to completed if it was active.
The job must be locked before it can be moved to a finished status,
and the lock must be released in this script.
Input:
KEYS[1] active key
KEYS[2] completed/failed key
KEYS[3] jobId key
KEYS[4] wait key
KEYS[5] priority key
KEYS[6] active event key
KEYS[7] delayed key
KEYS[8] stalled key
KEYS[9] metrics key
ARGV[1] jobId
ARGV[2] timestamp
ARGV[3] msg property
ARGV[4] return value / failed reason
ARGV[5] token
ARGV[6] shouldRemove
ARGV[7] event data (? maybe just send jobid).
ARGV[8] should fetch next job
ARGV[9] base key
ARGV[10] lock token
ARGV[11] lock duration in milliseconds
ARGV[12] maxMetricsSize
Output:
0 OK
-1 Missing key.
-2 Missing lock.
-3 - Job not in active set.
Events:
'completed/failed'
]]
local rcall = redis.call
-- Includes
--- @include "includes/collectMetrics"
--- @include "includes/removeLock"
--- @include "includes/removeDebounceKeyIfNeeded"
if rcall("EXISTS", KEYS[3]) == 1 then -- // Make sure job exists
local errorCode = removeLock(KEYS[3], KEYS[8], ARGV[5], ARGV[1])
if errorCode < 0 then
return errorCode
end
-- Remove from active list (if not active we shall return error)
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[1])
if numRemovedElements < 1 then return -3 end
local debounceId = rcall("HGET", KEYS[3], "deid")
removeDebounceKeyIfNeeded(ARGV[9], debounceId)
-- Remove job?
local keepJobs = cmsgpack.unpack(ARGV[6])
local maxCount = keepJobs['count']
local maxAge = keepJobs['age']
local targetSet = KEYS[2]
local timestamp = ARGV[2]
if maxCount ~= 0 then
-- Add to complete/failed set
rcall("ZADD", targetSet, timestamp, ARGV[1])
rcall("HMSET", KEYS[3], ARGV[3], ARGV[4], "finishedOn", timestamp) -- "returnvalue" / "failedReason" and "finishedOn"
local function removeJobs(jobIds)
for i, jobId in ipairs(jobIds) do
local jobKey = ARGV[9] .. jobId
local jobLogKey = jobKey .. ':logs'
rcall("DEL", jobKey, jobLogKey)
end
end
-- Remove old jobs?
if maxAge ~= nil then
local start = timestamp - maxAge * 1000
local jobIds = rcall("ZREVRANGEBYSCORE", targetSet, start, "-inf")
removeJobs(jobIds)
rcall("ZREMRANGEBYSCORE", targetSet, "-inf", start)
end
if maxCount ~= nil and maxCount > 0 then
local start = maxCount
local jobIds = rcall("ZREVRANGE", targetSet, start, -1)
removeJobs(jobIds)
rcall("ZREMRANGEBYRANK", targetSet, 0, -(maxCount + 1));
end
else
local jobLogKey = KEYS[3] .. ':logs'
rcall("DEL", KEYS[3], jobLogKey)
end
-- Collect metrics
if ARGV[12] ~= "" then
collectMetrics(KEYS[9], KEYS[9]..':data', ARGV[12], timestamp)
end
rcall("PUBLISH", targetSet, ARGV[7])
-- Try to get next job to avoid an extra roundtrip if the queue is not closing,
-- and not rate limited.
if (ARGV[8] == "1") then
-- move from wait to active
local jobId = rcall("RPOPLPUSH", KEYS[4], KEYS[1])
if jobId then
local jobKey = ARGV[9] .. jobId
local lockKey = jobKey .. ':lock'
-- get a lock
rcall("SET", lockKey, ARGV[11], "PX", ARGV[10])
rcall("ZREM", KEYS[5], jobId) -- remove from priority
rcall("PUBLISH", KEYS[6], jobId)
rcall("HSET", jobKey, "processedOn", ARGV[2])
return {rcall("HGETALL", jobKey), jobId} -- get job data
end
end
return 0
else
return -1
end
+108
View File
@@ -0,0 +1,108 @@
--[[
Completely obliterates a queue and all of its contents
Input:
KEYS[1] meta-paused
KEYS[2] base
ARGV[1] count
ARGV[2] force
]]
-- This command completely destroys a queue including all of its jobs, current or past
-- leaving no trace of its existence. Since this script needs to iterate to find all the job
-- keys, consider that this call may be slow for very large queues.
-- The queue needs to be "paused" or it will return an error
-- If the queue has currently active jobs then the script by default will return error,
-- however this behaviour can be overrided using the 'force' option.
local maxCount = tonumber(ARGV[1])
local baseKey = KEYS[2]
local rcall = redis.call
-- Includes
--- @include "includes/removeDebounceKey"
local function getListItems(keyName, max)
return rcall('LRANGE', keyName, 0, max - 1)
end
local function getZSetItems(keyName, max)
return rcall('ZRANGE', keyName, 0, max - 1)
end
local function removeJobs(baseKey, keys)
for i, key in ipairs(keys) do
local jobKey = baseKey .. key
rcall("DEL", jobKey, jobKey .. ':logs')
removeDebounceKey(baseKey, jobKey)
end
maxCount = maxCount - #keys
end
local function removeListJobs(keyName, max)
local jobs = getListItems(keyName, max)
removeJobs(baseKey, jobs)
rcall("LTRIM", keyName, #jobs, -1)
end
local function removeZSetJobs(keyName, max)
local jobs = getZSetItems(keyName, max)
removeJobs(baseKey, jobs)
if (#jobs > 0) then rcall("ZREM", keyName, unpack(jobs)) end
end
local function removeLockKeys(keys)
for i, key in ipairs(keys) do rcall("DEL", baseKey .. key .. ':lock') end
end
-- 1) Check if paused, if not return with error.
if rcall("EXISTS", KEYS[1]) ~= 1 then
return -1 -- Error, NotPaused
end
-- 2) Check if there are active jobs, if there are and not "force" return error.
local activeKey = baseKey .. 'active'
local activeJobs = getListItems(activeKey, maxCount)
if (#activeJobs > 0) then
if (ARGV[2] == "") then
return -2 -- Error, ExistsActiveJobs
end
end
removeLockKeys(activeJobs)
removeJobs(baseKey, activeJobs)
rcall("LTRIM", activeKey, #activeJobs, -1)
if (maxCount <= 0) then return 1 end
local waitKey = baseKey .. 'paused'
removeListJobs(waitKey, maxCount)
if (maxCount <= 0) then return 1 end
local delayedKey = baseKey .. 'delayed'
removeZSetJobs(delayedKey, maxCount)
if (maxCount <= 0) then return 1 end
local completedKey = baseKey .. 'completed'
removeZSetJobs(completedKey, maxCount)
if (maxCount <= 0) then return 1 end
local failedKey = baseKey .. 'failed'
removeZSetJobs(failedKey, maxCount)
if (maxCount <= 0) then return 1 end
if (maxCount > 0) then
rcall("DEL", baseKey .. 'priority')
rcall("DEL", baseKey .. 'stalled-check')
rcall("DEL", baseKey .. 'stalled')
rcall("DEL", baseKey .. 'meta-paused')
rcall("DEL", baseKey .. 'meta')
rcall("DEL", baseKey .. 'id')
rcall("DEL", baseKey .. 'repeat')
rcall("DEL", baseKey .. 'metrics:completed')
rcall("DEL", baseKey .. 'metrics:completed:data')
rcall("DEL", baseKey .. 'metrics:failed')
rcall("DEL", baseKey .. 'metrics:failed:data')
return 0
else
return 1
end
+35
View File
@@ -0,0 +1,35 @@
--[[
Pauses or resumes a queue globably.
Input:
KEYS[1] 'wait' or 'paused''
KEYS[2] 'paused' or 'wait'
KEYS[3] 'meta-paused'
KEYS[4] 'paused' o 'resumed' event.
KEYS[5] 'meta' this key is only used in BullMQ and above.
ARGV[1] 'paused' or 'resumed'
Event:
publish paused or resumed event.
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then
rcall("RENAME", KEYS[1], KEYS[2])
end
if ARGV[1] == "paused" then
rcall("SET", KEYS[3], 1)
-- for forwards compatibility
rcall("HSET", KEYS[5], "paused", 1)
else
rcall("DEL", KEYS[3])
-- for forwards compatibility
rcall("HDEL", KEYS[5], "paused")
end
rcall("PUBLISH", KEYS[4], ARGV[1])
+45
View File
@@ -0,0 +1,45 @@
--[[
Promotes a job that is currently "delayed" to the "waiting" state
Input:
KEYS[1] 'delayed'
KEYS[2] 'wait'
KEYS[3] 'paused'
KEYS[4] 'meta-paused'
KEYS[5] 'priority'
ARGV[1] queue.toKey('')
ARGV[2] jobId
ARGV[3] queue token
Events:
'waiting'
]]
local rcall = redis.call;
local jobId = ARGV[2]
-- Includes
--- @include "includes/addJobWithPriority"
--- @include "includes/getTargetQueueList"
if rcall("ZREM", KEYS[1], jobId) == 1 then
local priority = tonumber(rcall("HGET", ARGV[1] .. jobId, "priority")) or 0
local target = getTargetQueueList(KEYS[4], KEYS[2], KEYS[3])
if priority == 0 then
-- LIFO or FIFO
rcall("LPUSH", target, jobId)
else
addJobWithPriority(KEYS[5], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[2] .. "ing@" .. ARGV[3], jobId)
rcall("HSET", ARGV[1] .. jobId, "delay", 0)
return 0
else
return -1
end
+19
View File
@@ -0,0 +1,19 @@
--[[
Release lock
Input:
KEYS[1] 'lock',
ARGV[1] token
ARGV[2] lock duration in milliseconds
Output:
"OK" if lock extented succesfully.
]]
local rcall = redis.call
if rcall("GET", KEYS[1]) == ARGV[1] then
return rcall("DEL", KEYS[1])
else
return 0
end
+59
View File
@@ -0,0 +1,59 @@
--[[
Remove a job from all the queues it may be in as well as all its data.
In order to be able to remove a job, it must be unlocked.
Input:
KEYS[1] 'active',
KEYS[2] 'wait',
KEYS[3] 'delayed',
KEYS[4] 'paused',
KEYS[5] 'completed',
KEYS[6] 'failed',
KEYS[7] 'priority',
KEYS[8] jobId key
KEYS[9] job logs
KEYS[10] rate limiter index table
KEYS[11] prefix key
ARGV[1] jobId
ARGV[2] lock token
Events:
'removed'
]]
-- TODO PUBLISH global event 'removed'
local rcall = redis.call
-- Includes
--- @include "includes/removeDebounceKey"
local lockKey = KEYS[8] .. ':lock'
local lock = rcall("GET", lockKey)
if not lock then -- or (lock == ARGV[2])) then
local jobId = ARGV[1]
rcall("LREM", KEYS[1], 0, jobId)
rcall("LREM", KEYS[2], 0, jobId)
rcall("ZREM", KEYS[3], jobId)
rcall("LREM", KEYS[4], 0, jobId)
rcall("ZREM", KEYS[5], jobId)
rcall("ZREM", KEYS[6], jobId)
rcall("ZREM", KEYS[7], jobId)
removeDebounceKey(KEYS[11], KEYS[8])
rcall("DEL", KEYS[8])
rcall("DEL", KEYS[9])
-- delete keys related to rate limiter
local limiterIndexTable = KEYS[10] .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
return 1
else
return 0
end
+61
View File
@@ -0,0 +1,61 @@
--[[
Remove all jobs matching a given pattern from all the queues they may be in as well as all its data.
In order to be able to remove any job, they must be unlocked.
Input:
KEYS[1] 'active',
KEYS[2] 'wait',
KEYS[3] 'delayed',
KEYS[4] 'paused',
KEYS[5] 'completed',
KEYS[6] 'failed',
KEYS[7] 'priority',
KEYS[8] 'rate-limiter'
ARGV[1] prefix
ARGV[2] pattern
ARGV[3] cursor
Events:
'removed'
]]
-- TODO PUBLISH global events 'removed'
local rcall = redis.call
local result = rcall("SCAN", ARGV[3], "MATCH", ARGV[1] .. ARGV[2])
local cursor = result[1];
local jobKeys = result[2];
local removed = {}
local prefixLen = string.len(ARGV[1]) + 1
for i, jobKey in ipairs(jobKeys) do
local keyTypeResp = rcall("TYPE", jobKey)
if keyTypeResp["ok"] == "hash" then
local jobId = string.sub(jobKey, prefixLen)
local lockKey = jobKey .. ':lock'
local lock = redis.call("GET", lockKey)
if not lock then
rcall("LREM", KEYS[1], 0, jobId)
rcall("LREM", KEYS[2], 0, jobId)
rcall("ZREM", KEYS[3], jobId)
rcall("LREM", KEYS[4], 0, jobId)
rcall("ZREM", KEYS[5], jobId)
rcall("ZREM", KEYS[6], jobId)
rcall("ZREM", KEYS[7], jobId)
rcall("DEL", jobKey)
rcall("DEL", jobKey .. ':logs')
-- delete keys related to rate limiter
local limiterIndexTable = KEYS[8] .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
table.insert(removed, jobId)
end
end
end
return {cursor, removed}
+22
View File
@@ -0,0 +1,22 @@
--[[
Removes a repeatable job
Input:
KEYS[1] repeat jobs key
KEYS[2] delayed jobs key
ARGV[1] repeat job id
ARGV[2] repeat job key
ARGV[3] queue key
]]
local millis = redis.call("ZSCORE", KEYS[1], ARGV[2])
if(millis) then
-- Delete next programmed job.
local repeatJobId = ARGV[1] .. millis
if(redis.call("ZREM", KEYS[2], repeatJobId) == 1) then
redis.call("DEL", ARGV[3] .. repeatJobId)
end
end
redis.call("ZREM", KEYS[1], ARGV[2]);
+52
View File
@@ -0,0 +1,52 @@
--[[
Attempts to reprocess a job
Input:
KEYS[1] job key
KEYS[2] job lock key
KEYS[3] job state
KEYS[4] wait key
KEYS[5] meta-pause
KEYS[6] paused key
ARGV[1] job.id,
ARGV[2] (job.opts.lifo ? 'R' : 'L') + 'PUSH'
ARGV[3] token
ARGV[4] timestamp
Output:
1 means the operation was a success
0 means the job does not exist
-1 means the job is currently locked and can't be retried.
-2 means the job was not found in the expected set.
]]
local rcall = redis.call;
if (rcall("EXISTS", KEYS[1]) == 1) then
if (rcall("EXISTS", KEYS[2]) == 0) then
rcall("HDEL", KEYS[1], "finishedOn", "processedOn", "failedReason")
rcall("HSET", KEYS[1], "retriedOn", ARGV[4])
if (rcall("ZREM", KEYS[3], ARGV[1]) == 1) then
local target
if rcall("EXISTS", KEYS[5]) ~= 1 then
target = KEYS[4]
else
target = KEYS[6]
end
rcall(ARGV[2], target, ARGV[1])
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[4] .. "ing@" .. ARGV[3], ARGV[1])
return 1
else
return -2
end
else
return -1
end
else
return 0
end
+56
View File
@@ -0,0 +1,56 @@
--[[
Retries a failed job by moving it back to the wait queue.
Input:
KEYS[1] 'active',
KEYS[2] 'wait'
KEYS[3] jobId key
KEYS[4] 'meta-paused'
KEYS[5] 'paused'
KEYS[6] stalled key
KEYS[7] 'priority'
ARGV[1] pushCmd
ARGV[2] jobId
ARGV[3] token
Events:
'prefix:added'
Output:
0 - OK
-1 - Missing key
-2 - Job Not locked
-3 - Job not in active set
]]
local rcall = redis.call
-- Includes
--- @include "includes/addJobWithPriority"
--- @include "includes/getTargetQueueList"
--- @include "includes/removeLock"
if rcall("EXISTS", KEYS[3]) == 1 then
local errorCode = removeLock(KEYS[3], KEYS[6], ARGV[3], ARGV[2])
if errorCode < 0 then
return errorCode
end
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[2])
if numRemovedElements < 1 then return -3 end
local target = getTargetQueueList(KEYS[4], KEYS[2], KEYS[5])
local priority = tonumber(rcall("HGET", KEYS[3], "priority")) or 0
if priority == 0 then
-- LIFO or FIFO
rcall(ARGV[1], target, ARGV[2])
else
addJobWithPriority(KEYS[7], priority, ARGV[2], target)
end
return 0
else
return -1
end
+54
View File
@@ -0,0 +1,54 @@
--[[
Attempts to retry all failed jobs
Input:
KEYS[1] base key
KEYS[2] failed state key
KEYS[3] wait state key
KEYS[4] 'meta-paused'
KEYS[5] 'paused'
ARGV[1] count
Output:
1 means the operation is not completed
0 means the operation is completed
]]
local baseKey = KEYS[1]
local maxCount = tonumber(ARGV[1])
local rcall = redis.call;
-- Includes
--- @include "includes/batches"
local function getZSetItems(keyName, max)
return rcall('ZRANGE', keyName, 0, max - 1)
end
local jobs = getZSetItems(KEYS[2], maxCount)
if (#jobs > 0) then
for i, key in ipairs(jobs) do
local jobKey = baseKey .. key
rcall("HDEL", jobKey, "finishedOn", "processedOn", "failedReason")
end
local target
if rcall("EXISTS", KEYS[4]) ~= 1 then
target = KEYS[3]
else
target = KEYS[5]
end
for from, to in batches(#jobs, 7000) do
rcall("ZREM", KEYS[2], unpack(jobs, from, to))
rcall("LPUSH", target, unpack(jobs, from, to))
end
end
maxCount = maxCount - #jobs
if (maxCount <= 0) then return 1 end
return 0
+24
View File
@@ -0,0 +1,24 @@
--[[
Save stacktrace and failedReason.
Input:
KEYS[1] job key
ARGV[1] stacktrace
ARGV[2] failedReason
ARGV[3] attemptsMade
Output:
0 - OK
-1 - Missing key
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then
rcall("HMSET", KEYS[1], "stacktrace", ARGV[1], "failedReason", ARGV[2],
"attemptsMade", ARGV[3])
return 0
else
return -1
end
+17
View File
@@ -0,0 +1,17 @@
--[[
Takes a lock
Input:
KEYS[1] 'lock',
ARGV[1] token
ARGV[2] lock duration in milliseconds
Output:
"OK" if lock taken successfully.
]]
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
+20
View File
@@ -0,0 +1,20 @@
--[[
Update job data
Input:
KEYS[1] Job id key
ARGV[1] data
Output:
0 - OK
-1 - Missing job.
]]
local rcall = redis.call
if rcall("EXISTS",KEYS[1]) == 1 then -- // Make sure job exists
rcall("HSET", KEYS[1], "data", ARGV[1])
return 0
else
return -1
end
+59
View File
@@ -0,0 +1,59 @@
--[[
Updates the delay set, by picking a delayed job that should
be processed now.
Input:
KEYS[1] 'delayed'
KEYS[2] 'active'
KEYS[3] 'wait'
KEYS[4] 'priority'
KEYS[5] 'paused'
KEYS[6] 'meta-paused'
ARGV[1] queue.toKey('')
ARGV[2] delayed timestamp
ARGV[3] queue token
Events:
'removed'
]]
local rcall = redis.call;
-- Includes
--- @include "includes/addJobWithPriority"
--- @include "includes/getTargetQueueList"
-- Try to get as much as 1000 jobs at once
local jobs = rcall("ZRANGEBYSCORE", KEYS[1], 0, tonumber(ARGV[2]) * 0x1000, "LIMIT", 0, 1000)
if(#jobs > 0) then
rcall("ZREM", KEYS[1], unpack(jobs))
-- check if we need to use push in paused instead of waiting
local target = getTargetQueueList(KEYS[6], KEYS[3], KEYS[5])
for _, jobId in ipairs(jobs) do
-- Is this really needed?
rcall("LREM", KEYS[2], 0, jobId)
local priority = tonumber(rcall("HGET", ARGV[1] .. jobId, "priority")) or 0
if priority == 0 then
-- LIFO or FIFO
rcall("LPUSH", target, jobId)
else
addJobWithPriority(KEYS[4], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[3] .. "ing@" .. ARGV[3], jobId)
rcall("HSET", ARGV[1] .. jobId, "delay", 0)
end
end
local nextTimestamp = rcall("ZRANGE", KEYS[1], 0, 0, "WITHSCORES")[2]
if(nextTimestamp ~= nil) then
rcall("PUBLISH", KEYS[1], nextTimestamp / 0x1000)
end
return nextTimestamp
+21
View File
@@ -0,0 +1,21 @@
--[[
Update job progress
Input:
KEYS[1] Job id key
KEYS[2] progress event key
ARGV[1] progress
ARGV[2] event data
Event:
progress(jobId, progress)
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then -- // Make sure job exists
rcall("HSET", KEYS[1], "progress", ARGV[1])
rcall("PUBLISH", KEYS[2], ARGV[2])
return 0
else
return -1
end
+11
View File
@@ -0,0 +1,11 @@
'use strict';
module.exports.Messages = {
RETRY_JOB_NOT_EXIST: "Couldn't retry job: The job doesn't exist",
RETRY_JOB_IS_LOCKED: "Couldn't retry job: The job is locked",
RETRY_JOB_NOT_FAILED:
"Couldn't retry job: The job has been already retried or has not failed",
MISSING_REDIS_OPTS: `Using a redis instance with enableReadyCheck or maxRetriesPerRequest for bclient/subscriber is not permitted.
see https://github.com/OptimalBits/bull/issues/1873
`
};
+273
View File
@@ -0,0 +1,273 @@
'use strict';
const _ = require('lodash');
const Job = require('./job');
const scripts = require('./scripts');
module.exports = function(Queue) {
Queue.prototype.getJob = async function(jobId) {
await this.isReady();
return Job.fromId(this, jobId);
};
Queue.prototype.getCountsPerPriority = async function(priorities) {
const uniquePriorities = [...new Set(priorities)];
const responses = await scripts.getCountsPerPriority(
this,
uniquePriorities
);
const counts = {};
responses.forEach((res, index) => {
counts[`${uniquePriorities[index]}`] = res || 0;
});
return counts;
};
Queue.prototype._commandByType = function(types, count, callback) {
return _.map(types, type => {
type = type === 'waiting' ? 'wait' : type; // alias
const key = this.toKey(type);
switch (type) {
case 'completed':
case 'failed':
case 'delayed':
case 'repeat':
return callback(key, count ? 'zcard' : 'zrange');
case 'active':
case 'wait':
case 'paused':
return callback(key, count ? 'llen' : 'lrange');
}
});
};
/**
Returns the number of jobs waiting to be processed.
*/
Queue.prototype.count = function() {
return this.getJobCountByTypes('wait', 'paused', 'delayed');
};
// Job counts by type
// Queue#getJobCountByTypes('completed') => completed count
// Queue#getJobCountByTypes('completed,failed') => completed + failed count
// Queue#getJobCountByTypes('completed', 'failed') => completed + failed count
// Queue#getJobCountByTypes('completed,waiting', 'failed') => completed + waiting + failed count
Queue.prototype.getJobCountByTypes = function() {
return this.getJobCounts.apply(this, arguments).then(result => {
return _.chain(result)
.values()
.sum()
.value();
});
};
/**
* Returns the job counts for each type specified or every list/set in the queue by default.
*
*/
Queue.prototype.getJobCounts = function() {
const types = parseTypeArg(arguments);
const multi = this.multi();
this._commandByType(types, true, (key, command) => {
multi[command](key);
});
return multi.exec().then(res => {
const counts = {};
res.forEach((res, index) => {
counts[types[index]] = res[1] || 0;
});
return counts;
});
};
Queue.prototype.getCompletedCount = function() {
return this.getJobCountByTypes('completed');
};
Queue.prototype.getFailedCount = function() {
return this.getJobCountByTypes('failed');
};
Queue.prototype.getDelayedCount = function() {
return this.getJobCountByTypes('delayed');
};
Queue.prototype.getActiveCount = function() {
return this.getJobCountByTypes('active');
};
Queue.prototype.getWaitingCount = function() {
return this.getJobCountByTypes('wait', 'paused');
};
/**
*
* @returns the potential stalled jobs. Only useful for tests.
*/
Queue.prototype.getStalledCount = function() {
const key = this.toKey('stalled');
return this.client.scard(key);
};
// TO BE DEPRECATED --->
Queue.prototype.getPausedCount = function() {
return this.getJobCountByTypes('paused');
};
// <-----
Queue.prototype.getWaiting = function(start, end, opts) {
return this.getJobs(['wait', 'paused'], start, end, true, opts);
};
Queue.prototype.getActive = function(start, end, opts) {
return this.getJobs('active', start, end, true, opts);
};
Queue.prototype.getDelayed = function(start, end, opts) {
return this.getJobs('delayed', start, end, true, opts);
};
Queue.prototype.getCompleted = function(start, end, opts) {
return this.getJobs('completed', start, end, false, opts);
};
Queue.prototype.getFailed = function(start, end, opts) {
return this.getJobs('failed', start, end, false, opts);
};
Queue.prototype.getRanges = function(types, start, end, asc) {
start = _.isUndefined(start) ? 0 : start;
end = _.isUndefined(end) ? -1 : end;
const multi = this.multi();
const multiCommands = [];
this._commandByType(parseTypeArg(types), false, (key, command) => {
switch (command) {
case 'lrange':
if (asc) {
multiCommands.push('lrange');
multi.lrange(key, -(end + 1), -(start + 1));
} else {
multi.lrange(key, start, end);
}
break;
case 'zrange':
multiCommands.push('zrange');
if (asc) {
multi.zrange(key, start, end);
} else {
multi.zrevrange(key, start, end);
}
break;
}
});
return multi.exec().then(responses => {
let results = [];
responses.forEach((response, index) => {
const result = response[1] || [];
if (asc && multiCommands[index] === 'lrange') {
results = results.concat(result.reverse());
} else {
results = results.concat(result);
}
});
return results;
});
};
Queue.prototype.getJobs = function(types, start, end, asc, opts) {
return this.getRanges(types, start, end, asc).then(jobIds => {
return Promise.all(jobIds.map(jobId => this.getJobFromId(jobId, opts)));
});
};
Queue.prototype.getJobLogs = function(jobId, start, end, asc = true) {
start = _.isUndefined(start) ? 0 : start;
end = _.isUndefined(end) ? -1 : end;
const multi = this.multi();
const logsKey = this.toKey(jobId + ':logs');
if (asc) {
multi.lrange(logsKey, start, end);
} else {
multi.lrange(logsKey, -(end + 1), -(start + 1));
}
multi.llen(logsKey);
return multi.exec().then(result => {
if (!asc) {
result[0][1].reverse();
}
return {
logs: result[0][1],
count: result[1][1]
};
});
};
/**
* Get queue metrics related to the queue.
*
* This method returns the gathered metrics for the queue.
* The metrics are represented as an array of job counts
* per unit of time (1 minute).
*
* @param start - Start point of the metrics, where 0
* is the newest point to be returned.
* @param end - End poinf of the metrics, where -1 is the
* oldest point to be returned.
*
* @returns - Returns an object with queue metrics.
*/
Queue.prototype.getMetrics = async function(type, start = 0, end = -1) {
const metricsKey = this.toKey(`metrics:${type}`);
const dataKey = `${metricsKey}:data`;
const multi = this.multi();
multi.hmget(metricsKey, 'count', 'prevTS', 'prevCount');
multi.lrange(dataKey, start, end);
multi.llen(dataKey);
const [hmget, range, len] = await multi.exec();
const [err, [count, prevTS, prevCount]] = hmget;
const [err2, data] = range;
const [err3, numPoints] = len;
if (err || err2) {
throw err || err2 || err3;
}
return {
meta: {
count: parseInt(count || '0', 10),
prevTS: parseInt(prevTS || '0', 10),
prevCount: parseInt(prevCount || '0', 10)
},
data,
count: numPoints
};
};
};
function parseTypeArg(args) {
const types = _.chain([])
.concat(args)
.join(',')
.split(/\s*,\s*/g)
.compact()
.value();
return types.length
? types
: ['waiting', 'active', 'completed', 'failed', 'delayed', 'paused'];
}
+673
View File
@@ -0,0 +1,673 @@
'use strict';
const _ = require('lodash');
const utils = require('./utils');
const scripts = require('./scripts');
const debuglog = require('util').debuglog('bull');
const errors = require('./errors');
const backoffs = require('./backoffs');
const FINISHED_WATCHDOG = 5000;
const DEFAULT_JOB_NAME = '__default__';
/**
interface JobOptions
{
priority: Priority;
attempts: number;
delay: number;
}
*/
const jobFields = [
'opts',
'name',
'id',
'progress',
'delay',
'timestamp',
'finishedOn',
'processedOn',
'retriedOn',
'failedReason',
'attemptsMade',
'stacktrace',
'returnvalue'
];
// queue: Queue, data: {}, opts: JobOptions
const Job = function(queue, name, data, opts) {
if (typeof name !== 'string') {
opts = data;
data = name;
name = DEFAULT_JOB_NAME;
}
// defaults
this.opts = setDefaultOpts(opts);
this.name = name;
this.queue = queue;
this.data = data;
this._progress = 0;
this.delay = this.opts.delay < 0 ? 0 : this.opts.delay;
this.timestamp = this.opts.timestamp;
this.stacktrace = [];
this.returnvalue = null;
this.attemptsMade = 0;
this.toKey = _.bind(queue.toKey, queue);
this.debounceId = this.opts.debounce ? this.opts.debounce.id : undefined;
};
function setDefaultOpts(opts) {
const _opts = Object.assign({}, opts);
_opts.attempts = typeof _opts.attempts == 'undefined' ? 1 : _opts.attempts;
_opts.delay = typeof _opts.delay == 'undefined' ? 0 : Number(_opts.delay);
_opts.timestamp =
typeof _opts.timestamp == 'undefined' ? Date.now() : _opts.timestamp;
_opts.attempts = parseInt(_opts.attempts);
_opts.backoff = backoffs.normalize(_opts.backoff);
return _opts;
}
Job.DEFAULT_JOB_NAME = DEFAULT_JOB_NAME;
function addJob(queue, client, job) {
const opts = job.opts;
const jobData = job.toData();
return scripts.addJob(client, queue, jobData, {
lifo: opts.lifo,
customJobId: opts.jobId,
priority: opts.priority,
debounce: opts.debounce
});
}
Job.create = function(queue, name, data, opts) {
const job = new Job(queue, name, data, opts);
return queue
.isReady()
.then(() => {
return addJob(queue, queue.client, job);
})
.then(jobId => {
job.id = jobId;
debuglog('Job added', jobId);
return job;
});
};
Job.createBulk = function(queue, jobs) {
jobs = jobs.map(job => new Job(queue, job.name, job.data, job.opts));
return queue
.isReady()
.then(() => {
const multi = queue.client.multi();
for (const job of jobs) {
addJob(queue, multi, job);
}
return multi.exec();
})
.then(res => {
res.forEach((res, index) => {
jobs[index].id = res[1];
debuglog('Job added', res[1]);
});
return jobs;
});
};
Job.fromId = async function(queue, jobId, opts) {
// jobId can be undefined if moveJob returns undefined
if (!jobId) {
return Promise.resolve();
}
const jobKey = queue.toKey(jobId);
let rawJob;
if (opts && opts.excludeData) {
rawJob = _.zipObject(
jobFields,
await queue.client.hmget(jobKey, jobFields)
);
} else {
rawJob = await queue.client.hgetall(jobKey);
}
return _.isEmpty(rawJob) ? null : Job.fromJSON(queue, rawJob, jobId);
};
Job.remove = async function(queue, pattern) {
await queue.isReady();
const removed = await scripts.removeWithPattern(queue, pattern);
removed.forEach(jobId => queue.emit('removed', jobId));
};
Job.prototype.progress = function(progress) {
if (_.isUndefined(progress)) {
return this._progress;
}
this._progress = progress;
return scripts.updateProgress(this, progress);
};
Job.prototype.update = async function(data) {
this.data = data;
const code = await scripts.updateData(this, data);
if (code < 0) {
throw scripts.finishedErrors(code, this.id, 'updateData');
}
};
Job.prototype.toJSON = function() {
const opts = Object.assign({}, this.opts);
return {
id: this.id,
name: this.name,
data: this.data || {},
opts: opts,
progress: this._progress,
delay: this.delay, // Move to opts
timestamp: this.timestamp,
attemptsMade: this.attemptsMade,
failedReason: this.failedReason,
stacktrace: this.stacktrace || null,
returnvalue: this.returnvalue || null,
debounceId: this.debounceId || null,
finishedOn: this.finishedOn || null,
processedOn: this.processedOn || null
};
};
Job.prototype.toData = function() {
const json = this.toJSON();
json.data = JSON.stringify(json.data);
json.opts = JSON.stringify(json.opts);
json.stacktrace = JSON.stringify(json.stacktrace);
json.failedReason = JSON.stringify(json.failedReason);
json.returnvalue = JSON.stringify(json.returnvalue);
return json;
};
/**
Return a unique key representing a lock for this Job
*/
Job.prototype.lockKey = function() {
return this.toKey(this.id) + ':lock';
};
/**
Takes a lock for this job so that no other queue worker can process it at the
same time.
*/
Job.prototype.takeLock = function() {
return scripts.takeLock(this.queue, this).then(lock => {
return lock || false;
});
};
/**
Releases the lock. Only locks owned by the queue instance can be released.
*/
Job.prototype.releaseLock = function() {
return scripts.releaseLock(this.queue, this.id).then(unlocked => {
if (unlocked != 1) {
throw new Error('Could not release lock for job ' + this.id);
}
});
};
/**
* Extend the lock for this job.
*
* @param duration lock duration in milliseconds
*/
Job.prototype.extendLock = function(duration) {
return scripts.extendLock(this.queue, this.id, duration);
};
/**
* Moves a job to the completed queue.
* Returned job to be used with Queue.prototype.nextJobFromJobData.
* @param returnValue {string} The jobs success message.
* @param ignoreLock {boolean} True when wanting to ignore the redis lock on this job.
* @param notFetch {boolean} True when should not fetch next job from queue.
* @returns {Promise} Returns the jobData of the next job in the waiting queue.
*/
Job.prototype.moveToCompleted = function(
returnValue,
ignoreLock,
notFetch = false
) {
return this.queue.isReady().then(() => {
this.returnvalue = returnValue || 0;
returnValue = utils.tryCatch(JSON.stringify, JSON, [returnValue]);
if (returnValue === utils.errorObject) {
const err = utils.errorObject.value;
return Promise.reject(err);
}
this.finishedOn = Date.now();
return scripts.moveToCompleted(
this,
returnValue,
this.opts.removeOnComplete,
ignoreLock,
notFetch
);
});
};
Job.prototype.discard = function() {
this._discarded = true;
};
/**
* Moves a job to the failed queue.
* @param err {string} The jobs error message.
* @param ignoreLock {boolean} True when wanting to ignore the redis lock on this job.
* @returns void
*/
Job.prototype.moveToFailed = async function(err, ignoreLock) {
err = err || { message: 'Unknown reason' };
this.failedReason = err.message;
await this.queue.isReady();
let command;
const multi = this.queue.client.multi();
this._saveAttempt(multi, err);
// Check if an automatic retry should be performed
let moveToFailed = false;
if (this.attemptsMade < this.opts.attempts && !this._discarded) {
// Check if backoff is needed
const delay = await backoffs.calculate(
this.opts.backoff,
this.attemptsMade,
this.queue.settings.backoffStrategies,
err,
_.get(this, 'opts.backoff.options', null)
);
if (delay === -1) {
// If delay is -1, we should no continue retrying
moveToFailed = true;
} else if (delay) {
// If so, move to delayed (need to unlock job in this case!)
const args = scripts.moveToDelayedArgs(
this.queue,
this.id,
Date.now() + delay,
ignoreLock
);
multi.moveToDelayed(args);
command = 'delayed';
} else {
// If not, retry immediately
multi.retryJob(scripts.retryJobArgs(this, ignoreLock));
command = 'retry';
}
} else {
// If not, move to failed
moveToFailed = true;
}
if (moveToFailed) {
this.finishedOn = Date.now();
const args = scripts.moveToFailedArgs(
this,
err.message,
this.opts.removeOnFail,
ignoreLock
);
multi.moveToFinished(args);
command = 'failed';
}
const results = await multi.exec();
const code = _.last(results)[1];
if (code < 0) {
throw scripts.finishedErrors(code, this.id, command, 'active');
}
};
Job.prototype.moveToDelayed = function(timestamp, ignoreLock) {
return scripts.moveToDelayed(this.queue, this.id, timestamp, ignoreLock);
};
Job.prototype.promote = function() {
const queue = this.queue;
const jobId = this.id;
return queue.isReady().then(() =>
scripts.promote(queue, jobId).then(result => {
if (result === -1) {
throw new Error('Job ' + jobId + ' is not in a delayed state');
}
})
);
};
/**
* Attempts to retry the job. Only a job that has failed can be retried.
*
* @return {Promise} If resolved and return code is 1, then the queue emits a waiting event
* otherwise the operation was not a success and throw the corresponding error. If the promise
* rejects, it indicates that the script failed to execute
*/
Job.prototype.retry = function() {
return this.queue.isReady().then(() => {
this.failedReason = null;
this.finishedOn = null;
this.processedOn = null;
this.retriedOn = Date.now();
return scripts.reprocessJob(this, { state: 'failed' }).then(result => {
if (result === 1) {
return;
} else if (result === 0) {
throw new Error(errors.Messages.RETRY_JOB_NOT_EXIST);
} else if (result === -1) {
throw new Error(errors.Messages.RETRY_JOB_IS_LOCKED);
} else if (result === -2) {
throw new Error(errors.Messages.RETRY_JOB_NOT_FAILED);
}
});
});
};
/**
* Logs one row of log data.
*
* @params logRow: string String with log data to be logged.
*
*/
Job.prototype.log = function(logRow) {
return scripts.addLog(this.queue, this.id, logRow);
};
Job.prototype.isCompleted = function() {
return this._isDone('completed');
};
Job.prototype.isFailed = function() {
return this._isDone('failed');
};
Job.prototype.isDelayed = function() {
return this._isDone('delayed');
};
Job.prototype.isActive = function() {
return this._isInList('active');
};
Job.prototype.isWaiting = function() {
return this._isInList('wait');
};
Job.prototype.isPaused = function() {
return this._isInList('paused');
};
Job.prototype.isStuck = function() {
return this.getState().then(state => {
return state === 'stuck';
});
};
Job.prototype.isDiscarded = function() {
return this._discarded;
};
Job.prototype.getState = function() {
const fns = [
{ fn: 'isCompleted', state: 'completed' },
{ fn: 'isFailed', state: 'failed' },
{ fn: 'isDelayed', state: 'delayed' },
{ fn: 'isActive', state: 'active' },
{ fn: 'isWaiting', state: 'waiting' },
{ fn: 'isPaused', state: 'paused' }
];
return fns
.reduce((result, fn) => {
return result.then(state => {
if (state) {
return state;
}
return this[fn.fn]().then(result => {
return result ? fn.state : null;
});
});
}, Promise.resolve())
.then(result => {
return result ? result : 'stuck';
});
};
Job.prototype.remove = function() {
const queue = this.queue;
const job = this;
return queue.isReady().then(() => {
return scripts.remove(queue, job.id).then(removed => {
if (removed) {
queue.emit('removed', job);
} else {
throw new Error('Could not remove job ' + job.id);
}
});
});
};
/**
* Returns a promise the resolves when the job has finished. (completed or failed).
*/
Job.prototype.finished = async function() {
await Promise.all([
this.queue._registerEvent('global:completed'),
this.queue._registerEvent('global:failed')
]);
await this.queue.isReady();
const status = await scripts.isFinished(this);
const finished = status > 0;
if (finished) {
const job = await Job.fromId(this.queue, this.id);
if (status == 2) {
throw new Error(job.failedReason);
} else {
return job.returnvalue;
}
} else {
return new Promise((resolve, reject) => {
const onCompleted = (jobId, resultValue) => {
if (String(jobId) === String(this.id)) {
let result = void 0;
try {
if (typeof resultValue === 'string') {
result = JSON.parse(resultValue);
}
} catch (err) {
//swallow exception because the resultValue got corrupted somehow.
debuglog('corrupted resultValue: ' + resultValue, err);
}
resolve(result);
removeListeners();
}
};
const onFailed = (jobId, failedReason) => {
if (String(jobId) === String(this.id)) {
reject(new Error(failedReason));
removeListeners();
}
};
this.queue.on('global:completed', onCompleted);
this.queue.on('global:failed', onFailed);
const removeListeners = () => {
clearInterval(interval);
this.queue.removeListener('global:completed', onCompleted);
this.queue.removeListener('global:failed', onFailed);
};
//
// Watchdog
//
const interval = setInterval(() => {
if (this._isQueueClosing()) {
removeListeners();
// TODO(manast) maybe we would need a more graceful way to get out of this interval.
reject(
new Error('cannot check if job is finished in a closing queue.')
);
} else {
scripts.isFinished(this).then(status => {
const finished = status > 0;
if (finished) {
Job.fromId(this.queue, this.id).then(job => {
removeListeners();
if (status == 2) {
reject(new Error(job.failedReason));
} else {
resolve(job.returnvalue);
}
});
}
});
}
}, FINISHED_WATCHDOG);
});
}
};
// -----------------------------------------------------------------------------
// Private methods
// -----------------------------------------------------------------------------
Job.prototype._isQueueClosing = function() {
return this.queue.closing;
};
Job.prototype._isDone = function(list) {
return this.queue.client
.zscore(this.queue.toKey(list), this.id)
.then(score => {
return score !== null;
});
};
Job.prototype._isInList = function(list) {
return scripts.isJobInList(
this.queue.client,
this.queue.toKey(list),
this.id
);
};
Job.prototype._saveAttempt = function(multi, err) {
this.attemptsMade++;
this.stacktrace = this.stacktrace || [];
if (err && err.stack) {
this.stacktrace.push(err.stack);
if (this.opts.stackTraceLimit) {
this.stacktrace = this.stacktrace.slice(-this.opts.stackTraceLimit);
}
}
const args = scripts.saveStacktraceArgs(
this,
JSON.stringify(this.stacktrace),
err && err.message,
);
multi.saveStacktrace(args);
};
Job.fromJSON = function(queue, json, jobId) {
const opts = JSON.parse(json.opts || '{}');
const data = opts.preventParsingData
? json.data
: JSON.parse(json.data || '{}');
const job = new Job(queue, json.name || Job.DEFAULT_JOB_NAME, data, opts);
job.id = json.id || jobId;
try {
job._progress = JSON.parse(json.progress || 0);
} catch (err) {
console.error(
`Error parsing progress ${json.progress} with ${err.message}`
);
}
job.delay = parseInt(json.delay);
job.timestamp = parseInt(json.timestamp);
if (json.finishedOn) {
job.finishedOn = parseInt(json.finishedOn);
}
if (json.processedOn) {
job.processedOn = parseInt(json.processedOn);
}
if (json.retriedOn) {
job.retriedOn = parseInt(json.retriedOn);
}
job.failedReason = json.failedReason;
job.attemptsMade = parseInt(json.attemptsMade || 0);
job.stacktrace = getTraces(json.stacktrace);
if (typeof json.returnvalue === 'string') {
job.returnvalue = getReturnValue(json.returnvalue);
}
if (json.deid) {
job.debounceId = json.deid;
}
return job;
};
function getTraces(stacktrace) {
const _traces = utils.tryCatch(JSON.parse, JSON, [stacktrace]);
if (_traces === utils.errorObject || !(_traces instanceof Array)) {
return [];
} else {
return _traces;
}
}
function getReturnValue(_value) {
const value = utils.tryCatch(JSON.parse, JSON, [_value]);
if (value !== utils.errorObject) {
return value;
} else {
debuglog('corrupted returnvalue: ' + _value, value);
}
}
module.exports = Job;
+84
View File
@@ -0,0 +1,84 @@
// Extracted from p-timeout https://github.com/sindresorhus/p-timeout
// as it is not commonjs compatible. This is version 5.0.2
'use strict';
class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'TimeoutError';
}
}
module.exports.TimeoutError = TimeoutError;
module.exports.pTimeout = function pTimeout(
promise,
milliseconds,
fallback,
options
) {
let timer;
const cancelablePromise = new Promise((resolve, reject) => {
if (typeof milliseconds !== 'number' || Math.sign(milliseconds) !== 1) {
throw new TypeError(
`Expected \`milliseconds\` to be a positive number, got \`${milliseconds}\``
);
}
if (milliseconds === Number.POSITIVE_INFINITY) {
resolve(promise);
return;
}
options = {
customTimers: { setTimeout, clearTimeout },
...options
};
timer = options.customTimers.setTimeout.call(
undefined,
() => {
if (typeof fallback === 'function') {
try {
resolve(fallback());
} catch (error) {
reject(error);
}
return;
}
const message =
typeof fallback === 'string'
? fallback
: `Promise timed out after ${milliseconds} milliseconds`;
const timeoutError =
fallback instanceof Error ? fallback : new TimeoutError(message);
if (typeof promise.cancel === 'function') {
promise.cancel();
}
reject(timeoutError);
},
milliseconds
);
(async () => {
try {
resolve(await promise);
} catch (error) {
reject(error);
} finally {
options.customTimers.clearTimeout.call(undefined, timer);
}
})();
});
cancelablePromise['clear'] = () => {
clearTimeout(timer);
timer = undefined;
};
return cancelablePromise;
};
+144
View File
@@ -0,0 +1,144 @@
'use strict';
const fork = require('child_process').fork;
const path = require('path');
const _ = require('lodash');
const getPort = require('get-port');
const { killAsync } = require('./utils');
const CHILD_KILL_TIMEOUT = 30000;
const ChildPool = function ChildPool() {
if (!(this instanceof ChildPool)) {
return new ChildPool();
}
this.retained = {};
this.free = {};
};
const convertExecArgv = function(execArgv) {
const standard = [];
const promises = [];
_.forEach(execArgv, arg => {
if (arg.indexOf('--inspect') === -1) {
standard.push(arg);
} else {
const argName = arg.split('=')[0];
promises.push(
getPort().then(port => {
return `${argName}=${port}`;
})
);
}
});
return Promise.all(promises).then(convertedArgs => {
return standard.concat(convertedArgs);
});
};
ChildPool.prototype.retain = function(processFile) {
const _this = this;
let child = _this.getFree(processFile).pop();
if (child) {
_this.retained[child.pid] = child;
return Promise.resolve(child);
}
return convertExecArgv(process.execArgv).then(execArgv => {
child = fork(path.join(__dirname, './master.js'), {
execArgv
});
child.processFile = processFile;
_this.retained[child.pid] = child;
child.on('exit', _this.remove.bind(_this, child));
return initChild(child, child.processFile)
.then(() => {
return child;
})
.catch(err => {
this.remove(child);
throw err;
});
});
};
ChildPool.prototype.release = function(child) {
delete this.retained[child.pid];
this.getFree(child.processFile).push(child);
};
ChildPool.prototype.remove = function(child) {
delete this.retained[child.pid];
const free = this.getFree(child.processFile);
const childIndex = free.indexOf(child);
if (childIndex > -1) {
free.splice(childIndex, 1);
}
};
ChildPool.prototype.kill = function(child, signal) {
this.remove(child);
return killAsync(child, signal || 'SIGKILL', CHILD_KILL_TIMEOUT);
};
ChildPool.prototype.clean = function() {
const children = _.values(this.retained).concat(this.getAllFree());
this.retained = {};
this.free = {};
const allKillPromises = [];
children.forEach(child => {
allKillPromises.push(this.kill(child, 'SIGTERM'));
});
return Promise.all(allKillPromises).then(() => {});
};
ChildPool.prototype.getFree = function(id) {
return (this.free[id] = this.free[id] || []);
};
ChildPool.prototype.getAllFree = function() {
return _.flatten(_.values(this.free));
};
async function initChild(child, processFile) {
const onComplete = new Promise((resolve, reject) => {
const onMessageHandler = msg => {
if (msg.cmd === 'init-complete') {
resolve();
} else if (msg.cmd === 'error') {
reject(msg.error);
}
child.off('message', onMessageHandler);
};
child.on('message', onMessageHandler);
});
await new Promise(resolve =>
child.send({ cmd: 'init', value: processFile }, resolve)
);
await onComplete;
}
function ChildPoolSingleton(isSharedChildPool = false) {
if (isSharedChildPool === false) {
return new ChildPool();
} else if (
!(this instanceof ChildPool) &&
ChildPoolSingleton.instance === undefined
) {
ChildPoolSingleton.instance = new ChildPool();
}
return ChildPoolSingleton.instance;
}
module.exports = ChildPoolSingleton;
+200
View File
@@ -0,0 +1,200 @@
/**
* Master of child processes. Handles communication between the
* processor and the main process.
*
*/
'use strict';
let status;
let processor;
let currentJobPromise;
const { promisify } = require('util');
const { asyncSend } = require('./utils');
// https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify
if (!('toJSON' in Error.prototype)) {
Object.defineProperty(Error.prototype, 'toJSON', {
value: function() {
const alt = {};
Object.getOwnPropertyNames(this).forEach(function(key) {
alt[key] = this[key];
}, this);
return alt;
},
configurable: true,
writable: true
});
}
async function waitForCurrentJobAndExit() {
status = 'TERMINATING';
try {
await currentJobPromise;
} finally {
// it's an exit handler
// eslint-disable-next-line no-process-exit
process.exit(process.exitCode || 0);
}
}
process.on('SIGTERM', waitForCurrentJobAndExit);
process.on('SIGINT', waitForCurrentJobAndExit);
process.on('message', msg => {
switch (msg.cmd) {
case 'init':
try {
processor = require(msg.value);
} catch (err) {
status = 'Errored';
err.message = `Error loading process file ${msg.value}. ${err.message}`;
return process.send({
cmd: 'error',
error: err
});
}
if (processor.default) {
// support es2015 module.
processor = processor.default;
}
if (processor.length > 1) {
processor = promisify(processor);
} else {
const origProcessor = processor;
processor = function() {
try {
return Promise.resolve(origProcessor.apply(null, arguments));
} catch (err) {
return Promise.reject(err);
}
};
}
status = 'IDLE';
process.send({
cmd: 'init-complete'
});
break;
case 'start':
if (status !== 'IDLE') {
return process.send({
cmd: 'error',
err: new Error('cannot start a not idling child process')
});
}
status = 'STARTED';
currentJobPromise = (async () => {
try {
const result = (await processor(wrapJob(msg.job))) || {};
await asyncSend(process, {
cmd: 'completed',
value: result
});
} catch (err) {
if (!err.message) {
// eslint-disable-next-line no-ex-assign
err = new Error(err);
}
await asyncSend(process, {
cmd: 'failed',
value: err
});
} finally {
status = 'IDLE';
currentJobPromise = null;
}
})();
break;
case 'stop':
break;
}
});
/*eslint no-process-exit: "off"*/
process.on('uncaughtException', err => {
if (!err.message) {
err = new Error(err);
}
process.send({
cmd: 'failed',
value: err
});
// An uncaughException leaves this process in a potentially undetermined state so
// we must exit
process.exit(-1);
});
/**
* Enhance the given job argument with some functions
* that can be called from the sandboxed job processor.
*
* Note, the `job` argument is a JSON deserialized message
* from the main node process to this forked child process,
* the functions on the original job object are not in tact.
* The wrapped job adds back some of those original functions.
*/
function wrapJob(job) {
/*
* Emulate the real job `progress` function.
* If no argument is given, it behaves as a sync getter.
* If an argument is given, it behaves as an async setter.
*/
let progressValue = job.progress;
job.progress = function(progress) {
if (progress) {
// Locally store reference to new progress value
// so that we can return it from this process synchronously.
progressValue = progress;
// Send message to update job progress.
return asyncSend(process, {
cmd: 'progress',
value: progress
});
} else {
// Return the last known progress value.
return progressValue;
}
};
/**
* Update job info
*/
job.update = function(data) {
process.send({
cmd: 'update',
value: data
});
};
/*
* Emulate the real job `log` function.
*/
job.log = function(row) {
return asyncSend(process, {
cmd: 'log',
value: row
});
};
/*
* Emulate the real job `update` function.
*/
job.update = function(data) {
process.send({
cmd: 'update',
value: data
});
job.data = data;
};
/*
* Emulate the real job `discard` function.
*/
job.discard = function() {
process.send({
cmd: 'discard'
});
};
return job;
}
+68
View File
@@ -0,0 +1,68 @@
'use strict';
const { asyncSend } = require('./utils');
module.exports = function(processFile, childPool) {
return function process(job) {
return childPool.retain(processFile).then(async child => {
let msgHandler;
let exitHandler;
await asyncSend(child, {
cmd: 'start',
job: job
});
const done = new Promise((resolve, reject) => {
msgHandler = function(msg) {
switch (msg.cmd) {
case 'completed':
resolve(msg.value);
break;
case 'failed':
case 'error': {
const err = new Error();
Object.assign(err, msg.value);
reject(err);
break;
}
case 'progress':
job.progress(msg.value);
break;
case 'update':
job.update(msg.value);
break;
case 'discard':
job.discard();
break;
case 'log':
job.log(msg.value);
break;
}
};
exitHandler = (exitCode, signal) => {
reject(
new Error(
'Unexpected exit code: ' + exitCode + ' signal: ' + signal
)
);
};
child.on('message', msgHandler);
child.on('exit', exitHandler);
});
return done.finally(() => {
child.removeListener('message', msgHandler);
child.removeListener('exit', exitHandler);
if (child.exitCode !== null || /SIG.*/.test(child.signalCode)) {
childPool.remove(child);
} else {
childPool.release(child);
}
});
});
};
};
+70
View File
@@ -0,0 +1,70 @@
'use strict';
function hasProcessExited(child) {
return !!(child.exitCode !== null || child.signalCode);
}
function onExitOnce(child) {
return new Promise(resolve => {
child.once('exit', () => resolve());
});
}
/**
* Sends a kill signal to a child resolving when the child has exited,
* resorting to SIGKILL if the given timeout is reached
*
* @param {ChildProcess} child
* @param {'SIGTERM' | 'SIGKILL'} [signal] initial signal to use
* @param {number} [timeoutMs] time to wait until sending SIGKILL
*
* @returns {Promise<void>} the killed child
*/
function killAsync(child, signal, timeoutMs) {
if (hasProcessExited(child)) {
return Promise.resolve(child);
}
// catch any new on exit
let onExit = onExitOnce(child);
child.kill(signal || 'SIGKILL');
if (timeoutMs === 0 || isFinite(timeoutMs)) {
const timeout = setTimeout(() => {
if (!hasProcessExited(child)) {
child.kill('SIGKILL');
}
}, timeoutMs);
onExit = onExit.then(() => {
clearTimeout(timeout);
});
}
return onExit;
}
/*
asyncSend
Same as process.send but waits until the send is complete
the async version is used below because otherwise
the termination handler may exit before the parent
process has recived the messages it requires
*/
const asyncSend = (proc, msg) => {
return new Promise((resolve, reject) => {
proc.send(msg, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
module.exports = {
killAsync,
asyncSend
};
Generated Vendored Executable
+1417
View File
@@ -0,0 +1,1417 @@
'use strict';
const Redis = require('ioredis');
const EventEmitter = require('events');
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const util = require('util');
const url = require('url');
const Job = require('./job');
const scripts = require('./scripts');
const errors = require('./errors');
const utils = require('./utils');
const TimerManager = require('./timer-manager');
const { promisify } = require('util');
const { pTimeout } = require('./p-timeout');
const semver = require('semver');
const debuglog = require('util').debuglog('bull');
const uuid = require('uuid');
const commands = require('./scripts/');
/**
Gets or creates a new Queue with the given name.
The Queue keeps 6 data structures:
- wait (list)
- active (list)
- delayed (zset)
- priority (zset)
- completed (zset)
- failed (zset)
--> priorities -- > completed
/ | /
job -> wait -> active
\ ^ \
v | -- > failed
delayed
*/
/**
Delayed jobs are jobs that cannot be executed until a certain time in
ms has passed since they were added to the queue.
The mechanism is simple, a delayedTimestamp variable holds the next
known timestamp that is on the delayed set (or MAX_TIMEOUT_MS if none).
When the current job has finalized the variable is checked, if
no delayed job has to be executed yet a setTimeout is set so that a
delayed job is processed after timing out.
*/
const MINIMUM_REDIS_VERSION = '2.8.18';
/*
interface QueueOptions {
prefix?: string = 'bull',
limiter?: RateLimiter,
redis : RedisOpts, // ioredis defaults,
createClient?: (type: enum('client', 'subscriber'), redisOpts?: RedisOpts) => redisClient,
defaultJobOptions?: JobOptions,
// Advanced settings
settings?: QueueSettings {
lockDuration?: number = 30000,
lockRenewTime?: number = lockDuration / 2,
stalledInterval?: number = 30000,
maxStalledCount?: number = 1, // The maximum number of times a job can be recovered from the 'stalled' state
guardInterval?: number = 5000,
retryProcessDelay?: number = 5000,
drainDelay?: number = 5
isSharedChildPool?: boolean = false
}
}
interface RateLimiter {
max: number, // Number of jobs
duration: number, // per duration milliseconds
}
*/
// Queue(name: string, url?, opts?)
const Queue = function Queue(name, url, opts) {
if (!(this instanceof Queue)) {
return new Queue(name, url, opts);
}
if (_.isString(url)) {
const clonedOpts = _.cloneDeep(opts || {});
opts = {
...clonedOpts,
redis: {
...redisOptsFromUrl(url),
...clonedOpts.redis
}
};
} else {
opts = _.cloneDeep(url || {});
}
if (!_.isObject(opts)) {
throw TypeError('Options must be a valid object');
}
if (opts.limiter) {
if (opts.limiter.max && opts.limiter.duration) {
this.limiter = opts.limiter;
} else {
throw new TypeError('Limiter requires `max` and `duration` options');
}
}
if (opts.defaultJobOptions) {
this.defaultJobOptions = opts.defaultJobOptions;
}
this.name = name;
this.token = uuid.v4();
opts.redis = {
enableReadyCheck: false,
...(_.isString(opts.redis)
? { ...redisOptsFromUrl(opts.redis) }
: opts.redis)
};
_.defaults(opts.redis, {
port: 6379,
host: '127.0.0.1',
db: opts.redis.db || opts.redis.DB,
retryStrategy: function(times) {
return Math.min(Math.exp(times), 20000);
}
});
this.keyPrefix = opts.redis.keyPrefix || opts.prefix || 'bull';
//
// We cannot use ioredis keyPrefix feature since we
// create keys dynamically in lua scripts.
//
delete opts.redis.keyPrefix;
this.clients = [];
const loadCommands = (providedScripts, client) => {
const finalScripts = providedScripts || scripts;
for (const property in finalScripts) {
// Only define the command if not already defined
if (!client[finalScripts[property].name]) {
client.defineCommand(finalScripts[property].name, {
numberOfKeys: finalScripts[property].keys,
lua: finalScripts[property].content
});
}
}
};
const lazyClient = redisClientGetter(this, opts, (type, client) => {
// bubble up Redis error events
const handler = this.emit.bind(this, 'error');
client.on('error', handler);
this.once('close', () => client.removeListener('error', handler));
if (type === 'client') {
this._initializing = (async () => loadCommands(commands, client))().then(
() => {
debuglog(name + ' queue ready');
},
err => {
this.emit('error', new Error('Error initializing Lua scripts'));
throw err;
}
);
this._initializing.catch((/*err*/) => {});
}
});
Object.defineProperties(this, {
//
// Queue client (used to add jobs, pause queues, etc);
//
client: {
get: lazyClient('client')
},
//
// Event subscriber client (receive messages from other instance of the queue)
//
eclient: {
get: lazyClient('subscriber')
},
bclient: {
get: lazyClient('bclient')
}
});
if (opts.skipVersionCheck !== true) {
getRedisVersion(this.client)
.then(version => {
if (semver.lt(version, MINIMUM_REDIS_VERSION)) {
this.emit(
'error',
new Error(
'Redis version needs to be greater than ' +
MINIMUM_REDIS_VERSION +
'. Current: ' +
version
)
);
}
})
.catch((/*err*/) => {
// Ignore this error.
});
}
this.handlers = {};
this.delayTimer;
this.processing = [];
this.retrieving = 0;
this.drained = true;
this.settings = _.defaults(opts.settings, {
lockDuration: 30000,
stalledInterval: 30000,
maxStalledCount: 1,
guardInterval: 5000,
retryProcessDelay: 5000,
drainDelay: 5,
backoffStrategies: {},
isSharedChildPool: false
});
this.metrics = opts.metrics;
this.settings.lockRenewTime =
this.settings.lockRenewTime || this.settings.lockDuration / 2;
this.on('error', () => {
// Dummy handler to avoid process to exit with an unhandled exception.
});
// keeps track of active timers. used by close() to
// ensure that disconnect() is deferred until all
// scheduled redis commands have been executed
this.timers = new TimerManager();
// Bind these methods to avoid constant rebinding and/or creating closures
// in processJobs etc.
this.moveUnlockedJobsToWait = this.moveUnlockedJobsToWait.bind(this);
this.processJob = this.processJob.bind(this);
this.getJobFromId = Job.fromId.bind(null, this);
const keys = {};
_.each(
[
'',
'active',
'wait',
'waiting',
'paused',
'resumed',
'meta-paused',
'active',
'id',
'delayed',
'priority',
'stalled-check',
'completed',
'failed',
'stalled',
'repeat',
'limiter',
'drained',
'duplicated',
'progress',
'de' // debounce key
],
key => {
keys[key] = this.toKey(key);
}
);
this.keys = keys;
};
function redisClientGetter(queue, options, initCallback) {
const createClient = _.isFunction(options.createClient)
? options.createClient
: function(type, config) {
if (['bclient', 'subscriber'].includes(type)) {
return new Redis({ ...config, maxRetriesPerRequest: null });
} else {
return new Redis(config);
}
};
const connections = {};
return function(type) {
return function() {
// Memoized connection
if (connections[type] != null) {
return connections[type];
}
const clientOptions = _.assign({}, options.redis);
const client = (connections[type] = createClient(type, clientOptions));
const opts = client.options.redisOptions || client.options;
if (
['bclient', 'subscriber'].includes(type) &&
(opts.enableReadyCheck || opts.maxRetriesPerRequest)
) {
throw new Error(errors.Messages.MISSING_REDIS_OPTS);
}
// Since connections are lazily initialized, we can't check queue.client
// without initializing a connection. So expose a boolean we can safely
// query.
queue[type + 'Initialized'] = true;
if (!options.createClient) {
queue.clients.push(client);
}
return initCallback(type, client), client;
};
};
}
function redisOptsFromUrl(urlString) {
let redisOpts = {};
try {
const redisUrl = url.parse(urlString, true, true);
redisOpts.port = parseInt(redisUrl.port || '6379', 10);
redisOpts.host = redisUrl.hostname;
redisOpts.db = redisUrl.pathname ? redisUrl.pathname.split('/')[1] : 0;
if (redisUrl.auth) {
const columnIndex = redisUrl.auth.indexOf(':');
redisOpts.password = redisUrl.auth.slice(columnIndex + 1);
if (columnIndex > 0) {
redisOpts.username = redisUrl.auth.slice(0, columnIndex);
}
}
if (redisUrl.query) {
redisOpts = { ...redisOpts, ...redisUrl.query };
}
} catch (e) {
throw new Error(e.message);
}
return redisOpts;
}
util.inherits(Queue, EventEmitter);
//
// Extend Queue with "aspects"
//
require('./getters')(Queue);
require('./worker')(Queue);
require('./repeatable')(Queue);
// --
Queue.prototype.off = Queue.prototype.removeListener;
const _on = Queue.prototype.on;
Queue.prototype.on = function(eventName) {
this._registerEvent(eventName);
return _on.apply(this, arguments);
};
const _once = Queue.prototype.once;
Queue.prototype.once = function(eventName) {
this._registerEvent(eventName);
return _once.apply(this, arguments);
};
Queue.prototype._initProcess = function() {
if (!this._initializingProcess) {
//
// Only setup listeners if .on/.addEventListener called, or process function defined.
//
this.delayedTimestamp = Number.MAX_VALUE;
this._initializingProcess = this.isReady()
.then(() => {
return this._registerEvent('delayed');
})
.then(() => {
return this.updateDelayTimer();
});
this.errorRetryTimer = {};
}
return this._initializingProcess;
};
Queue.prototype._setupQueueEventListeners = function() {
/*
if(eventName !== 'cleaned' && eventName !== 'error'){
args[0] = Job.fromJSON(this, args[0]);
}
*/
const activeKey = this.keys.active;
const stalledKey = this.keys.stalled;
const progressKey = this.keys.progress;
const delayedKey = this.keys.delayed;
const pausedKey = this.keys.paused;
const resumedKey = this.keys.resumed;
const waitingKey = this.keys.waiting;
const completedKey = this.keys.completed;
const failedKey = this.keys.failed;
const drainedKey = this.keys.drained;
const duplicatedKey = this.keys.duplicated;
const debouncedKey = this.keys.de + 'bounced';
const pmessageHandler = (pattern, channel, message) => {
const keyAndToken = channel.split('@');
const key = keyAndToken[0];
const token = keyAndToken[1];
switch (key) {
case activeKey:
utils.emitSafe(this, 'global:active', message, 'waiting');
break;
case waitingKey:
if (this.token === token) {
utils.emitSafe(this, 'waiting', message, null);
}
token && utils.emitSafe(this, 'global:waiting', message, null);
break;
case stalledKey:
if (this.token === token) {
utils.emitSafe(this, 'stalled', message);
}
utils.emitSafe(this, 'global:stalled', message);
break;
case duplicatedKey:
if (this.token === token) {
utils.emitSafe(this, 'duplicated', message);
}
utils.emitSafe(this, 'global:duplicated', message);
break;
case debouncedKey:
if (this.token === token) {
utils.emitSafe(this, 'debounced', message);
}
utils.emitSafe(this, 'global:debounced', message);
break;
}
};
const messageHandler = (channel, message) => {
const key = channel.split('@')[0];
switch (key) {
case progressKey: {
// New way to send progress message data
try {
const { progress, jobId } = JSON.parse(message);
utils.emitSafe(this, 'global:progress', jobId, progress);
} catch (err) {
// If we fail we should try to parse the data using the deprecated method
const commaPos = message.indexOf(',');
const jobId = message.substring(0, commaPos);
const progress = message.substring(commaPos + 1);
utils.emitSafe(this, 'global:progress', jobId, JSON.parse(progress));
}
break;
}
case delayedKey: {
const newDelayedTimestamp = _.ceil(message);
if (newDelayedTimestamp < this.delayedTimestamp) {
// The new delayed timestamp is before the currently newest known delayed timestamp
// Assume this is the new delayed timestamp and call `updateDelayTimer()` to process any delayed jobs
// This will also update the `delayedTimestamp`
this.delayedTimestamp = newDelayedTimestamp;
this.updateDelayTimer();
}
break;
}
case pausedKey:
case resumedKey:
utils.emitSafe(this, 'global:' + message);
break;
case completedKey: {
const data = JSON.parse(message);
utils.emitSafe(
this,
'global:completed',
data.jobId,
data.val,
'active'
);
break;
}
case failedKey: {
const data = JSON.parse(message);
utils.emitSafe(this, 'global:failed', data.jobId, data.val, 'active');
break;
}
case drainedKey:
utils.emitSafe(this, 'global:drained');
break;
}
};
this.eclient.on('pmessage', pmessageHandler);
this.eclient.on('message', messageHandler);
this.once('close', () => {
this.eclient.removeListener('pmessage', pmessageHandler);
this.eclient.removeListener('message', messageHandler);
});
};
Queue.prototype._registerEvent = function(eventName) {
const internalEvents = ['waiting', 'delayed', 'duplicated', 'debounced'];
if (
eventName.startsWith('global:') ||
internalEvents.indexOf(eventName) !== -1
) {
if (!this.registeredEvents) {
this._setupQueueEventListeners();
this.registeredEvents = this.registeredEvents || {};
}
const _eventName = eventName.replace('global:', '');
if (!this.registeredEvents[_eventName]) {
return utils
.isRedisReady(this.eclient)
.then(() => {
const channel = this.toKey(_eventName);
if (['active', 'waiting', 'stalled', 'duplicated', 'debounced'].indexOf(_eventName) !== -1) {
return (this.registeredEvents[_eventName] = this.eclient.psubscribe(
channel + '*'
));
} else {
return (this.registeredEvents[_eventName] = this.eclient.subscribe(
channel
));
}
})
.then(() => {
utils.emitSafe(this, 'registered:' + eventName);
});
} else {
return this.registeredEvents[_eventName];
}
}
return Promise.resolve();
};
Queue.ErrorMessages = errors.Messages;
Queue.prototype.isReady = async function() {
await this._initializing;
return this;
};
async function redisClientDisconnect(client) {
if (client.status !== 'end') {
let _resolve, _reject;
return new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
client.once('end', _resolve);
pTimeout(
client.quit().catch(err => {
if (err.message !== 'Connection is closed.') {
throw err;
}
}),
500
)
.catch(() => {
// Ignore timeout error
})
.finally(() => {
client.once('error', _reject);
client.disconnect();
if (['connecting', 'reconnecting'].includes(client.status)) {
resolve();
}
});
}).finally(() => {
client.removeListener('end', _resolve);
client.removeListener('error', _reject);
});
}
}
Queue.prototype.disconnect = async function() {
await Promise.all(
this.clients.map(client =>
client.blocked ? client.disconnect() : redisClientDisconnect(client)
)
);
};
Queue.prototype.removeJobs = function(pattern) {
return Job.remove(this, pattern);
};
Queue.prototype.close = function(doNotWaitJobs) {
let isReady = true;
if (this.closing) {
return this.closing;
}
return (this.closing = this.isReady()
.then(this._initializingProcess)
.catch(() => {
isReady = false;
})
.then(() => isReady && this.pause(true, doNotWaitJobs))
.catch(() => void 0) // Ignore possible error from pause
.finally(() => this._clearTimers())
.then(() => {
if (!this.childPool) {
return;
}
const cleanPromise = this.childPool.clean().catch(() => {
// Ignore this error and try to close anyway.
});
if (doNotWaitJobs) {
return;
}
return cleanPromise;
})
.then(
async () => this.disconnect(),
err => console.error(err)
)
.finally(() => {
this.closed = true;
utils.emitSafe(this, 'close');
}));
};
Queue.prototype._clearTimers = function() {
_.each(this.errorRetryTimer, timer => {
clearTimeout(timer);
});
clearTimeout(this.delayTimer);
clearInterval(this.guardianTimer);
clearInterval(this.moveUnlockedJobsToWaitInterval);
this.timers.clearAll();
return this.timers.whenIdle();
};
/**
Processes a job from the queue. The callback is called for every job that
is dequeued.
@method process
*/
Queue.prototype.process = function(name, concurrency, handler) {
switch (arguments.length) {
case 1:
handler = name;
concurrency = 1;
name = Job.DEFAULT_JOB_NAME;
break;
case 2: // (string, function) or (string, string) or (number, function) or (number, string)
handler = concurrency;
if (typeof name === 'string') {
concurrency = 1;
} else {
concurrency = name;
name = Job.DEFAULT_JOB_NAME;
}
break;
}
this.setHandler(name, handler);
return this._initProcess().then(() => {
return this.start(concurrency, name);
});
};
Queue.prototype.start = function(concurrency, name) {
return this.run(concurrency, name).catch(err => {
utils.emitSafe(this, 'error', err, 'error running queue');
throw err;
});
};
Queue.prototype.setHandler = function(name, handler) {
if (!handler) {
throw new Error('Cannot set an undefined handler');
}
if (this.handlers[name]) {
throw new Error('Cannot define the same handler twice ' + name);
}
this.setWorkerName();
if (typeof handler === 'string') {
const supportedFileTypes = ['.js', '.ts', '.flow', '.cjs'];
const processorFile =
handler +
(supportedFileTypes.includes(path.extname(handler)) ? '' : '.js');
if (!fs.existsSync(processorFile)) {
throw new Error('File ' + processorFile + ' does not exist');
}
const isSharedChildPool = this.settings.isSharedChildPool;
this.childPool =
this.childPool || require('./process/child-pool')(isSharedChildPool);
const sandbox = require('./process/sandbox');
this.handlers[name] = sandbox(handler, this.childPool).bind(this);
} else {
handler = handler.bind(this);
if (handler.length > 1) {
this.handlers[name] = promisify(handler);
} else {
this.handlers[name] = function() {
try {
return Promise.resolve(handler.apply(null, arguments));
} catch (err) {
return Promise.reject(err);
}
};
}
}
};
/**
interface JobOptions
{
attempts: number;
repeat: {
tz?: string,
endDate?: Date | string | number
},
preventParsingData: boolean;
}
*/
/**
Adds a job to the queue.
@method add
@param data: {} Custom data to store for this job. Should be JSON serializable.
@param opts: JobOptions Options for this job.
*/
Queue.prototype.add = function(name, data, opts) {
if (typeof name !== 'string') {
opts = data;
data = name;
name = Job.DEFAULT_JOB_NAME;
}
opts = _.cloneDeep({ ...this.defaultJobOptions, ...opts });
opts.jobId = jobIdForGroup(this.limiter, opts, data);
if (opts.repeat) {
return this.isReady().then(() => {
return this.nextRepeatableJob(name, data, opts, true);
});
} else {
return Job.create(this, name, data, opts);
}
};
/**
* Retry all the failed jobs.
*
* @param opts.count - number to limit how many jobs will be moved to wait status per iteration
* @returns
*/
Queue.prototype.retryJobs = async function(opts = {}) {
let cursor = 0;
do {
cursor = await scripts.retryJobs(this, opts.count);
} while (cursor);
};
/**
* Removes a debounce key.
*
* @param id - identifier
*/
Queue.prototype.removeDebounceKey = (id) => {
return this.client.del(`${this.keys.de}:${id}`);
}
/**
Adds an array of jobs to the queue.
@method add
@param jobs: [] The array of jobs to add to the queue. Each job is defined by 3 properties, 'name', 'data' and 'opts'. They follow the same signature as 'Queue.add'.
*/
Queue.prototype.addBulk = function(jobs) {
const decoratedJobs = jobs.map(job => {
const jobId = jobIdForGroup(this.limiter, job.opts, job.data);
return {
...job,
name: typeof job.name !== 'string' ? Job.DEFAULT_JOB_NAME : job.name,
opts: {
...this.defaultJobOptions,
...job.opts,
jobId
}
};
});
return Job.createBulk(this, decoratedJobs);
};
/**
Empties the queue.
Returns a promise that is resolved after the operation has been completed.
Note that if some other process is adding jobs at the same time as emptying,
the queues may not be really empty after this method has executed completely.
Also, if the method does error between emptying the lists and removing all the
jobs, there will be zombie jobs left in redis.
TODO: Use EVAL to make this operation fully atomic.
*/
Queue.prototype.empty = function() {
const queueKeys = this.keys;
let multi = this.multi();
multi.lrange(queueKeys.wait, 0, -1);
multi.lrange(queueKeys.paused, 0, -1);
multi.keys(this.toKey('*:limited'));
multi.del(
queueKeys.wait,
queueKeys.paused,
queueKeys['meta-paused'],
queueKeys.delayed,
queueKeys.priority,
queueKeys.limiter,
`${queueKeys.limiter}:index`
);
return multi.exec().then(res => {
let [waiting, paused, limited] = res;
waiting = waiting[1];
paused = paused[1];
limited = limited[1];
const jobKeys = paused.concat(waiting).map(this.toKey, this);
if (jobKeys.length || limited.length) {
multi = this.multi();
for (let i = 0; i < jobKeys.length; i += 10000) {
multi.del.apply(multi, jobKeys.slice(i, i + 10000));
}
for (let i = 0; i < limited.length; i += 10000) {
multi.del.apply(multi, limited.slice(i, i + 10000));
}
return multi.exec();
}
});
};
/**
Pauses the processing of this queue, locally if true passed, otherwise globally.
For global pause, we use an atomic RENAME operation on the wait queue. Since
we have blocking calls with BRPOPLPUSH on the wait queue, as long as the queue
is renamed to 'paused', no new jobs will be processed (the current ones
will run until finalized).
Adding jobs requires a LUA script to check first if the paused list exist
and in that case it will add it there instead of the wait list.
*/
Queue.prototype.pause = function(isLocal, doNotWaitActive) {
return this.isReady()
.then(() => {
if (isLocal) {
if (!this.paused) {
this.paused = new Promise(resolve => {
this.resumeLocal = function() {
this.paused = null; // Allow pause to be checked externally for paused state.
resolve();
};
});
}
if (!this.bclientInitialized) {
// bclient not yet initialized, so no jobs to wait for
return;
}
if (doNotWaitActive) {
// Force reconnection of blocking connection to abort blocking redis call immediately.
return redisClientDisconnect(this.bclient).then(() =>
this.bclient.connect()
);
}
return this.whenCurrentJobsFinished();
} else {
return scripts.pause(this, true);
}
})
.then(() => {
return utils.emitSafe(this, 'paused');
});
};
Queue.prototype.resume = function(isLocal /* Optional */) {
return this.isReady()
.then(() => {
if (isLocal) {
if (this.resumeLocal) {
this.resumeLocal();
}
} else {
return scripts.pause(this, false);
}
})
.then(() => {
utils.emitSafe(this, 'resumed');
});
};
Queue.prototype.isPaused = async function(isLocal) {
if (isLocal) {
return !!this.paused;
} else {
await this.isReady();
const multi = this.multi();
multi.exists(this.keys['meta-paused']);
// For forward compatibility with BullMQ.
multi.hexists(this.toKey('meta'), 'paused');
const [[, isPaused], [, isPausedNew]] = await multi.exec();
return !!(isPaused || isPausedNew);
}
};
Queue.prototype.run = function(concurrency, handlerName) {
if (!Number.isInteger(concurrency)) {
throw new Error('Cannot set Float as concurrency');
}
const promises = [];
return this.isReady()
.then(() => {
return this.moveUnlockedJobsToWait();
})
.then(() => {
return utils.isRedisReady(this.bclient);
})
.then(() => {
while (concurrency--) {
promises.push(
new Promise(resolve => {
this.processJobs(`${handlerName}:${concurrency}`, resolve);
})
);
}
this.startMoveUnlockedJobsToWait();
return Promise.all(promises);
});
};
// ---------------------------------------------------------------------
// Private methods
// ---------------------------------------------------------------------
/**
This function updates the delay timer, which is a timer that timeouts
at the next known delayed job.
*/
Queue.prototype.updateDelayTimer = function() {
if (this.closing) {
return Promise.resolve();
}
return scripts
.updateDelaySet(this, Date.now())
.then(nextTimestamp => {
this.delayedTimestamp = nextTimestamp
? nextTimestamp / 4096
: Number.MAX_VALUE;
// Clear any existing update delay timer
if (this.delayTimer) {
clearTimeout(this.delayTimer);
}
// Delay for the next update of delay set
const delay = _.min([
this.delayedTimestamp - Date.now(),
this.settings.guardInterval
]);
// Schedule next processing of the delayed jobs
if (delay <= 0) {
// Next set of jobs are due right now, process them also
this.updateDelayTimer();
} else {
// Update the delay set when the next job is due
// or the next guard time
this.delayTimer = setTimeout(() => this.updateDelayTimer(), delay);
}
// Silence warnings about promise created but not returned.
// This isn't an issue since we emit errors.
// See http://bluebirdjs.com/docs/warning-explanations.html#warning-a-promise-was-created-in-a-handler-but-was-not-returned-from-it
return null;
})
.catch(err => {
utils.emitSafe(this, 'error', err, 'Error updating the delay timer');
if (this.delayTimer) {
clearTimeout(this.delayTimer);
}
this.delayTimer = setTimeout(
() => this.updateDelayTimer(),
this.settings.guardInterval
);
});
};
/**
* Process jobs that have been added to the active list but are not being
* processed properly. This can happen due to a process crash in the middle
* of processing a job, leaving it in 'active' but without a job lock.
*/
Queue.prototype.moveUnlockedJobsToWait = function() {
if (this.closing) {
return Promise.resolve();
}
return scripts
.moveUnlockedJobsToWait(this)
.then(([failed, stalled]) => {
const handleFailedJobs = failed.map(jobId => {
return this.getJobFromId(jobId).then(job => {
utils.emitSafe(
this,
'failed',
job,
new Error('job stalled more than allowable limit'),
'active'
);
return null;
});
});
const handleStalledJobs = stalled.map(jobId => {
return this.getJobFromId(jobId).then(job => {
// Do not emit the event if the job was completed by another worker
if (job !== null) {
utils.emitSafe(this, 'stalled', job);
}
return null;
});
});
return Promise.all(handleFailedJobs.concat(handleStalledJobs));
})
.catch(err => {
utils.emitSafe(
this,
'error',
err,
'Failed to handle unlocked job in active'
);
});
};
Queue.prototype.startMoveUnlockedJobsToWait = function() {
clearInterval(this.moveUnlockedJobsToWaitInterval);
if (this.settings.stalledInterval > 0 && !this.closing) {
this.moveUnlockedJobsToWaitInterval = setInterval(
this.moveUnlockedJobsToWait,
this.settings.stalledInterval
);
}
};
/*
Process jobs. Note last argument 'job' is optional.
*/
Queue.prototype.processJobs = function(index, resolve, job) {
const processJobs = this.processJobs.bind(this, index, resolve);
process.nextTick(() => {
this._processJobOnNextTick(processJobs, index, resolve, job);
});
};
Queue.prototype._processJobOnNextTick = function(
processJobs,
index,
resolve,
job
) {
if (!this.closing) {
(this.paused || Promise.resolve())
.then(() => {
const gettingNextJob = job ? Promise.resolve(job) : this.getNextJob();
return (this.processing[index] = gettingNextJob
.then(this.processJob)
.then(processJobs, err => {
if (!(this.closing && err.message === 'Connection is closed.')) {
utils.emitSafe(this, 'error', err, 'Error processing job');
//
// Wait before trying to process again.
//
clearTimeout(this.errorRetryTimer[index]);
this.errorRetryTimer[index] = setTimeout(() => {
processJobs();
}, this.settings.retryProcessDelay);
}
return null;
}));
})
.catch(err => {
utils.emitSafe(this, 'error', err, 'Error processing job');
});
} else {
resolve(this.closing);
}
};
Queue.prototype.processJob = function(job, notFetch = false) {
let lockRenewId;
let timerStopped = false;
if (!job) {
return Promise.resolve();
}
//
// There are two cases to take into consideration regarding locks.
// 1) The lock renewer fails to renew a lock, this should make this job
// unable to complete, since some other worker is also working on it.
// 2) The lock renewer is called more seldom than the check for stalled
// jobs, so we can assume the job has been stalled and is already being processed
// by another worker. See #308
//
const lockExtender = () => {
lockRenewId = this.timers.set(
'lockExtender',
this.settings.lockRenewTime,
() => {
scripts
.extendLock(this, job.id, this.settings.lockDuration)
.then(lock => {
if (lock && !timerStopped) {
lockExtender();
}
})
.catch(err => {
utils.emitSafe(this, 'lock-extension-failed', job, err);
});
}
);
};
const timeoutMs = job.opts.timeout;
const stopTimer = () => {
timerStopped = true;
this.timers.clear(lockRenewId);
};
const handleCompleted = result => {
return job.moveToCompleted(result, undefined, notFetch).then(jobData => {
utils.emitSafe(this, 'completed', job, result, 'active');
return jobData ? this.nextJobFromJobData(jobData[0], jobData[1]) : null;
});
};
const handleFailed = err => {
const error = err;
return job.moveToFailed(err).then(jobData => {
utils.emitSafe(this, 'failed', job, error, 'active');
return jobData ? this.nextJobFromJobData(jobData[0], jobData[1]) : null;
});
};
lockExtender();
const handler = this.handlers[job.name] || this.handlers['*'];
if (!handler) {
return handleFailed(
new Error('Missing process handler for job type ' + job.name)
);
} else {
let jobPromise = handler(job);
if (timeoutMs) {
jobPromise = pTimeout(jobPromise, timeoutMs);
}
// Local event with jobPromise so that we can cancel job.
utils.emitSafe(this, 'active', job, jobPromise, 'waiting');
return jobPromise
.then(handleCompleted)
.catch(handleFailed)
.finally(() => {
stopTimer();
});
}
};
Queue.prototype.multi = function() {
return this.client.multi();
};
/**
Returns a promise that resolves to the next job in queue.
*/
Queue.prototype.getNextJob = async function() {
if (this.closing) {
return Promise.resolve();
}
if (this.drained) {
//
// Waiting for new jobs to arrive
//
try {
this.bclient.blocked = true;
const jobId = await this.bclient.brpoplpush(
this.keys.wait,
this.keys.active,
this.settings.drainDelay
);
this.bclient.blocked = false;
if (jobId) {
return this.moveToActive(jobId);
}
} catch (err) {
// Swallow error if locally paused since we did force a disconnection
if (!(this.paused && err.message === 'Connection is closed.')) {
throw err;
}
}
} else {
return this.moveToActive();
}
};
Queue.prototype.moveToActive = async function(jobId) {
// For manual retrieving jobs we need to wait for the queue to be ready.
await this.isReady();
return scripts.moveToActive(this, jobId).then(([jobData, jobId]) => {
return this.nextJobFromJobData(jobData, jobId);
});
};
Queue.prototype.nextJobFromJobData = function(jobData, jobId) {
if (jobData) {
this.drained = false;
const job = Job.fromJSON(this, jobData, jobId);
if (job.opts.repeat) {
return this.nextRepeatableJob(job.name, job.data, job.opts).then(() => {
return job;
});
}
return job;
} else {
this.drained = true;
utils.emitSafe(this, 'drained');
return null;
}
};
Queue.prototype.retryJob = function(job) {
return job.retry();
};
Queue.prototype.toKey = function(queueType) {
return [this.keyPrefix, this.name, queueType].join(':');
};
/*@function clean
*
* Cleans jobs from a queue. Similar to remove but keeps jobs within a certain
* grace period.
*
* @param {int} grace - The grace period
* @param {string} [type=completed] - The type of job to clean. Possible values are completed, wait, active, paused, delayed, failed. Defaults to completed.
* @param {int} The max number of jobs to clean
*/
Queue.prototype.clean = function(grace, type, limit) {
return this.isReady().then(() => {
if (grace === undefined || grace === null) {
throw new Error('You must define a grace period.');
}
if (!type) {
type = 'completed';
}
if (
_.indexOf(
['completed', 'wait', 'active', 'paused', 'delayed', 'failed'],
type
) === -1
) {
throw new Error('Cannot clean unknown queue type ' + type);
}
return scripts
.cleanJobsInSet(this, type, Date.now() - grace, limit)
.then(jobs => {
utils.emitSafe(this, 'cleaned', jobs, type);
return jobs;
})
.catch(err => {
utils.emitSafe(this, 'error', err);
throw err;
});
});
};
/* @method obliterate
*
* Completely destroys the queue and all of its contents irreversibly.
* This method will the *pause* the queue and requires that there are no
* active jobs. It is possible to bypass this requirement, i.e. not
* having active jobs using the "force" option.
*
* Note: This operation requires to iterate on all the jobs stored in the queue
* and can be slow for very large queues.
*
* @param { { force: boolean, count: number }} opts. Use force = true to force obliteration even
* with active jobs in the queue. Use count with the maximun number of deleted keys per iteration,
* 1000 is the default.
*/
Queue.prototype.obliterate = async function(opts) {
await this.pause();
let cursor = 0;
do {
cursor = await scripts.obliterate(this, {
force: false,
count: 1000,
...opts
});
} while (cursor);
};
/**
* Returns a promise that resolves when active jobs are finished
*
* @returns {Promise}
*/
Queue.prototype.whenCurrentJobsFinished = function() {
if (!this.bclientInitialized) {
// bclient not yet initialized, so no jobs to wait for
return Promise.resolve();
}
//
// Force reconnection of blocking connection to abort blocking redis call immediately.
//
const forcedReconnection = redisClientDisconnect(this.bclient).then(() => {
return this.bclient.connect();
});
return Promise.all(Object.values(this.processing)).then(
() => forcedReconnection
);
};
//
// Private local functions
//
function getRedisVersion(client) {
return client.info().then(doc => {
const prefix = 'redis_version:';
const lines = doc.split('\r\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].indexOf(prefix) === 0) {
return lines[i].substr(prefix.length);
}
}
});
}
function jobIdForGroup(limiter, opts, data) {
const jobId = opts && opts.jobId;
const groupKey = _.get(limiter, 'groupKey');
if (groupKey) {
return `${jobId || uuid.v4()}:${_.get(data, groupKey)}`;
}
return jobId;
}
module.exports = Queue;
+237
View File
@@ -0,0 +1,237 @@
'use strict';
const _ = require('lodash');
const parser = require('cron-parser');
const crypto = require('crypto');
const Job = require('./job');
module.exports = function(Queue) {
Queue.prototype.nextRepeatableJob = function(
name,
data,
opts,
skipCheckExists
) {
const client = this.client;
const repeat = opts.repeat;
const prevMillis = opts.prevMillis || 0;
if (!prevMillis && opts.jobId) {
repeat.jobId = opts.jobId;
}
const currentCount = repeat.count ? repeat.count + 1 : 1;
if (!_.isUndefined(repeat.limit) && currentCount > repeat.limit) {
return Promise.resolve();
}
let now = Date.now();
if (!_.isUndefined(repeat.endDate) && now > new Date(repeat.endDate)) {
return Promise.resolve();
}
now = prevMillis < now ? now : prevMillis;
const nextMillis = getNextMillis(now, repeat);
if (nextMillis) {
const jobId = repeat.jobId ? repeat.jobId + ':' : ':';
const repeatKey = getRepeatKey(name, repeat, jobId);
const createNextJob = () => {
return client.zadd(this.keys.repeat, nextMillis, repeatKey).then(() => {
//
// Generate unique job id for this iteration.
//
const customId = getRepeatJobId(
name,
jobId,
nextMillis,
md5(repeatKey)
);
now = Date.now();
const delay = nextMillis - now;
return Job.create(
this,
name,
data,
_.defaultsDeep(
{
repeat: {
count: currentCount,
key: repeatKey
},
jobId: customId,
delay: delay < 0 ? 0 : delay,
timestamp: now,
prevMillis: nextMillis
},
opts
)
);
});
};
if (skipCheckExists) {
return createNextJob();
}
// Check that the repeatable job hasn't been removed
// TODO: a lua script would be better here
return client
.zscore(this.keys.repeat, repeatKey)
.then(repeatableExists => {
// The job could have been deleted since this check
if (repeatableExists) {
return createNextJob();
}
return Promise.resolve();
});
} else {
return Promise.resolve();
}
};
Queue.prototype.removeRepeatable = function(name, repeat) {
if (typeof name !== 'string') {
repeat = name;
name = Job.DEFAULT_JOB_NAME;
}
return this.isReady().then(() => {
const jobId = repeat.jobId ? repeat.jobId + ':' : ':';
const repeatJobKey = getRepeatKey(name, repeat, jobId);
const repeatJobId = getRepeatJobId(name, jobId, '', md5(repeatJobKey));
const queueKey = this.keys[''];
return this.client.removeRepeatable(
this.keys.repeat,
this.keys.delayed,
repeatJobId,
repeatJobKey,
queueKey
);
});
};
Queue.prototype.removeRepeatableByKey = function(repeatJobKey) {
const repeatMeta = this._keyToData(repeatJobKey);
const queueKey = this.keys[''];
const jobId = repeatMeta.id ? repeatMeta.id + ':' : ':';
const repeatJobId = getRepeatJobId(
repeatMeta.name || Job.DEFAULT_JOB_NAME,
jobId,
'',
md5(repeatJobKey)
);
return this.isReady().then(() => {
return this.client.removeRepeatable(
this.keys.repeat,
this.keys.delayed,
repeatJobId,
repeatJobKey,
queueKey
);
});
};
Queue.prototype._keyToData = function(key) {
const data = key.split(':');
return {
key: key,
name: data[0],
id: data[1] || null,
endDate: parseInt(data[2]) || null,
tz: data[3] || null,
cron: data[4]
};
};
Queue.prototype.getRepeatableJobs = function(start, end, asc) {
const key = this.keys.repeat;
start = start || 0;
end = end || -1;
return (asc
? this.client.zrange(key, start, end, 'WITHSCORES')
: this.client.zrevrange(key, start, end, 'WITHSCORES')
).then(result => {
const jobs = [];
for (let i = 0; i < result.length; i += 2) {
const data = this._keyToData(result[i]);
jobs.push({
key: data.key,
name: data.name,
id: data.id,
endDate: data.endDate,
tz: data.cron ? data.tz : null,
cron: data.cron || null,
every: !data.cron ? parseInt(data.tz) : null,
next: parseInt(result[i + 1])
});
}
return jobs;
});
};
Queue.prototype.getRepeatableCount = function() {
return this.client.zcard(this.toKey('repeat'));
};
function getRepeatJobId(name, jobId, nextMillis, namespace) {
return 'repeat:' + md5(name + jobId + namespace) + ':' + nextMillis;
}
function getRepeatKey(name, repeat, jobId) {
const endDate = repeat.endDate
? new Date(repeat.endDate).getTime() + ':'
: ':';
const tz = repeat.tz ? repeat.tz + ':' : ':';
const suffix = repeat.cron ? tz + repeat.cron : String(repeat.every);
return name + ':' + jobId + endDate + suffix;
}
function getNextMillis(millis, opts) {
if (opts.cron && opts.every) {
throw new Error(
'Both .cron and .every options are defined for this repeatable job'
);
}
if (opts.every) {
return Math.floor(millis / opts.every) * opts.every + opts.every;
}
const currentDate =
opts.startDate && new Date(opts.startDate) > new Date(millis)
? new Date(opts.startDate)
: new Date(millis);
const interval = parser.parseExpression(
opts.cron,
_.defaults(
{
currentDate
},
opts
)
);
try {
return interval.next().getTime();
} catch (e) {
// Ignore error
}
}
function md5(str) {
return crypto
.createHash('md5')
.update(str)
.digest('hex');
}
};
+625
View File
@@ -0,0 +1,625 @@
/**
* Includes all the scripts needed by the queue and jobs.
*/
'use strict';
const _ = require('lodash');
const msgpackr = require('msgpackr');
const packer = new msgpackr.Packr({
useRecords: false,
encodeUndefinedAsNil: true
});
const pack = packer.pack;
const scripts = {
isJobInList(client, listKey, jobId) {
return client.isJobInList([listKey, jobId]).then(result => {
return result === 1;
});
},
addJob(client, queue, job, opts) {
const queueKeys = queue.keys;
let keys = [
queueKeys.wait,
queueKeys.paused,
queueKeys['meta-paused'],
queueKeys.id,
queueKeys.delayed,
queueKeys.priority
];
const args = [
queueKeys[''],
_.isUndefined(opts.customJobId) ? '' : opts.customJobId,
job.name,
job.data,
pack(job.opts),
job.timestamp,
job.delay,
job.delay ? job.timestamp + job.delay : 0,
opts.priority || 0,
opts.lifo ? 'RPUSH' : 'LPUSH',
queue.token,
job.debounceId ? `${queueKeys.de}:${job.debounceId}` : null,
opts.debounce ? opts.debounce.id : null,
opts.debounce ? opts.debounce.ttl : null,
];
keys = keys.concat(args);
return client.addJob(keys);
},
pause(queue, pause) {
let src = 'wait',
dst = 'paused';
if (!pause) {
src = 'paused';
dst = 'wait';
}
const keys = _.map(
[src, dst, 'meta-paused', pause ? 'paused' : 'resumed', 'meta'],
name => {
return queue.toKey(name);
}
);
return queue.client.pause(keys.concat([pause ? 'paused' : 'resumed']));
},
async addLog(queue, jobId, logRow, keepLogs) {
const client = await queue.client;
const keys = [queue.toKey(jobId), queue.toKey(jobId) + ':logs'];
const result = await client.addLog(
keys.concat([jobId, logRow, keepLogs ? keepLogs : ''])
);
if (result < 0) {
throw scripts.finishedErrors(result, jobId, 'addLog');
}
return result;
},
getCountsPerPriorityArgs(queue, priorities) {
const keys = [
queue.keys.wait,
queue.keys.paused,
queue.keys['meta-paused'],
queue.keys.priority
];
const args = priorities;
return keys.concat(args);
},
async getCountsPerPriority(queue, priorities) {
const client = await queue.client;
const args = this.getCountsPerPriorityArgs(queue, priorities);
return client.getCountsPerPriority(args);
},
moveToActive(queue, jobId) {
const queueKeys = queue.keys;
const keys = [queueKeys.wait, queueKeys.active, queueKeys.priority];
keys[3] = keys[1] + '@' + queue.token;
keys[4] = queueKeys.stalled;
keys[5] = queueKeys.limiter;
keys[6] = queueKeys.delayed;
keys[7] = queueKeys.drained;
const args = [
queueKeys[''],
queue.token,
queue.settings.lockDuration,
Date.now(),
jobId
];
if (queue.limiter) {
args.push(
queue.limiter.max,
queue.limiter.duration,
!!queue.limiter.bounceBack
);
queue.limiter.groupKey && args.push(true);
}
return queue.client.moveToActive(keys.concat(args)).then(raw2jobData);
},
updateProgress(job, progress) {
const queue = job.queue;
const keys = [job.id, 'progress'].map(name => {
return queue.toKey(name);
});
const progressJson = JSON.stringify(progress);
return queue.client
.updateProgress(keys, [
progressJson,
JSON.stringify({ jobId: job.id, progress })
])
.then(code => {
if (code < 0) {
throw scripts.finishedErrors(code, job.id, 'updateProgress');
}
queue.emit('progress', job, progress);
});
},
updateData(job, data) {
const queue = job.queue;
const keys = [job.id].map(name => {
return queue.toKey(name);
});
const dataJson = JSON.stringify(data);
return queue.client.updateData(keys, [dataJson]);
},
saveStacktraceArgs(
job,
stacktrace,
failedReason
) {
const queue = job.queue;
const keys = [queue.toKey(job.id)];
return keys.concat([stacktrace, failedReason, job.attemptsMade]);
},
retryJobsArgs(queue, count) {
const keys = [
queue.toKey(''),
queue.toKey('failed'),
queue.toKey('wait'),
queue.toKey('meta-paused'),
queue.toKey('paused')
];
const args = [count];
return keys.concat(args);
},
async retryJobs(queue, count = 1000) {
const client = await queue.client;
const args = this.retryJobsArgs(queue, count);
return client.retryJobs(args);
},
moveToFinishedArgs(
job,
val,
propVal,
shouldRemove,
target,
ignoreLock,
notFetch
) {
const queue = job.queue;
const queueKeys = queue.keys;
const metricsKey = queue.toKey(`metrics:${target}`);
const keys = [
queueKeys.active,
queueKeys[target],
queue.toKey(job.id),
queueKeys.wait,
queueKeys.priority,
queueKeys.active + '@' + queue.token,
queueKeys.delayed,
queueKeys.stalled,
metricsKey
];
const keepJobs = pack(
typeof shouldRemove === 'object'
? shouldRemove
: typeof shouldRemove === 'number'
? { count: shouldRemove }
: { count: shouldRemove ? 0 : -1 }
);
const args = [
job.id,
job.finishedOn,
propVal,
_.isUndefined(val) ? 'null' : val,
ignoreLock ? '0' : queue.token,
keepJobs,
JSON.stringify({ jobId: job.id, val: val }),
notFetch || queue.paused || queue.closing || queue.limiter ? 0 : 1,
queueKeys[''],
queue.settings.lockDuration,
queue.token,
queue.metrics && queue.metrics.maxDataPoints
];
return keys.concat(args);
},
moveToFinished(
job,
val,
propVal,
shouldRemove,
target,
ignoreLock,
notFetch = false
) {
const args = scripts.moveToFinishedArgs(
job,
val,
propVal,
shouldRemove,
target,
ignoreLock,
notFetch,
job.queue.toKey('')
);
return job.queue.client.moveToFinished(args).then(result => {
if (result < 0) {
throw scripts.finishedErrors(result, job.id, 'finished', 'active');
} else if (result) {
return raw2jobData(result);
}
return 0;
});
},
finishedErrors(code, jobId, command, state) {
switch (code) {
case -1:
return new Error('Missing key for job ' + jobId + ' ' + command);
case -2:
return new Error('Missing lock for job ' + jobId + ' ' + command);
case -3:
return new Error(
`Job ${jobId} is not in the ${state} state. ${command}`
);
case -6:
return new Error(
`Lock mismatch for job ${jobId}. Cmd ${command} from ${state}`
);
}
},
// TODO: add a retention argument for completed and finished jobs (in time).
moveToCompleted(
job,
returnvalue,
removeOnComplete,
ignoreLock,
notFetch = false
) {
return scripts.moveToFinished(
job,
returnvalue,
'returnvalue',
removeOnComplete,
'completed',
ignoreLock,
notFetch
);
},
moveToFailedArgs(job, failedReason, removeOnFailed, ignoreLock) {
return scripts.moveToFinishedArgs(
job,
failedReason,
'failedReason',
removeOnFailed,
'failed',
ignoreLock,
true
);
},
moveToFailed(job, failedReason, removeOnFailed, ignoreLock) {
const args = scripts.moveToFailedArgs(
job,
failedReason,
removeOnFailed,
ignoreLock
);
return scripts.moveToFinished(args);
},
isFinished(job) {
const keys = _.map(['completed', 'failed'], key => {
return job.queue.toKey(key);
});
return job.queue.client.isFinished(keys.concat([job.id]));
},
moveToDelayedArgs(queue, jobId, timestamp, ignoreLock) {
//
// Bake in the job id first 12 bits into the timestamp
// to guarantee correct execution order of delayed jobs
// (up to 4096 jobs per given timestamp or 4096 jobs apart per timestamp)
//
// WARNING: Jobs that are so far apart that they wrap around will cause FIFO to fail
//
timestamp = _.isUndefined(timestamp) ? 0 : timestamp;
timestamp = +timestamp || 0;
timestamp = timestamp < 0 ? 0 : timestamp;
if (timestamp > 0) {
timestamp = timestamp * 0x1000 + (jobId & 0xfff);
}
const keys = _.map(['active', 'delayed', jobId, 'stalled'], name => {
return queue.toKey(name);
});
return keys.concat([
JSON.stringify(timestamp),
jobId,
ignoreLock ? '0' : queue.token
]);
},
moveToDelayed(queue, jobId, timestamp, ignoreLock) {
const args = scripts.moveToDelayedArgs(queue, jobId, timestamp, ignoreLock);
return queue.client.moveToDelayed(args).then(result => {
switch (result) {
case -1:
throw new Error(
'Missing Job ' +
jobId +
' when trying to move from active to delayed'
);
case -2:
throw new Error(
'Job ' +
jobId +
' was locked when trying to move from active to delayed'
);
}
});
},
remove(queue, jobId) {
const keys = [
queue.keys.active,
queue.keys.wait,
queue.keys.delayed,
queue.keys.paused,
queue.keys.completed,
queue.keys.failed,
queue.keys.priority,
queue.toKey(jobId),
queue.toKey(`${jobId}:logs`),
queue.keys.limiter,
queue.toKey(''),
];
return queue.client.removeJob(keys.concat([jobId, queue.token]));
},
async removeWithPattern(queue, pattern) {
const keys = [
queue.keys.active,
queue.keys.wait,
queue.keys.delayed,
queue.keys.paused,
queue.keys.completed,
queue.keys.failed,
queue.keys.priority,
queue.keys.limiter
];
const allRemoved = [];
let cursor = '0',
removed;
do {
[cursor, removed] = await queue.client.removeJobs(
keys.concat([queue.toKey(''), pattern, cursor])
);
allRemoved.push.apply(allRemoved, removed);
} while (cursor !== '0');
return allRemoved;
},
extendLock(queue, jobId, duration) {
return queue.client.extendLock([
queue.toKey(jobId) + ':lock',
queue.keys.stalled,
queue.token,
duration,
jobId
]);
},
releaseLock(queue, jobId) {
return queue.client.releaseLock([
queue.toKey(jobId) + ':lock',
queue.token
]);
},
takeLock(queue, job) {
return queue.client.takeLock([
job.lockKey(),
queue.token,
queue.settings.lockDuration
]);
},
/**
It checks if the job in the top of the delay set should be moved back to the
top of the wait queue (so that it will be processed as soon as possible)
*/
updateDelaySet(queue, delayedTimestamp) {
const keys = [
queue.keys.delayed,
queue.keys.active,
queue.keys.wait,
queue.keys.priority,
queue.keys.paused,
queue.keys['meta-paused']
];
const args = [queue.toKey(''), delayedTimestamp, queue.token];
return queue.client.updateDelaySet(keys.concat(args));
},
promote(queue, jobId) {
const keys = [
queue.keys.delayed,
queue.keys.wait,
queue.keys.paused,
queue.keys['meta-paused'],
queue.keys.priority
];
const args = [queue.toKey(''), jobId, queue.token];
return queue.client.promote(keys.concat(args));
},
/**
* Looks for unlocked jobs in the active queue.
*
* The job was being worked on, but the worker process died and it failed to renew the lock.
* We call these jobs 'stalled'. This is the most common case. We resolve these by moving them
* back to wait to be re-processed. To prevent jobs from cycling endlessly between active and wait,
* (e.g. if the job handler keeps crashing), we limit the number stalled job recoveries to settings.maxStalledCount.
*/
moveUnlockedJobsToWait(queue) {
const keys = [
queue.keys.stalled,
queue.keys.wait,
queue.keys.active,
queue.keys.failed,
queue.keys['stalled-check'],
queue.keys['meta-paused'],
queue.keys.paused
];
const args = [
queue.settings.maxStalledCount,
queue.toKey(''),
Date.now(),
queue.settings.stalledInterval
];
return queue.client.moveStalledJobsToWait(keys.concat(args));
},
cleanJobsInSet(queue, set, ts, limit) {
return queue.client.cleanJobsInSet([
queue.toKey(set),
queue.toKey('priority'),
queue.keys.limiter,
queue.toKey(''),
ts,
limit || 0,
set
]);
},
retryJobArgs(job, ignoreLock) {
const queue = job.queue;
const jobId = job.id;
const keys = _.map(
['active', 'wait', jobId, 'meta-paused', 'paused', 'stalled', 'priority'],
name => {
return queue.toKey(name);
}
);
const pushCmd = (job.opts.lifo ? 'R' : 'L') + 'PUSH';
return keys.concat([pushCmd, jobId, ignoreLock ? '0' : job.queue.token]);
},
/**
* Attempts to reprocess a job
*
* @param {Job} job
* @param {Object} options
* @param {String} options.state The expected job state. If the job is not found
* on the provided state, then it's not reprocessed. Supported states: 'failed', 'completed'
*
* @return {Promise<Number>} Returns a promise that evaluates to a return code:
* 1 means the operation was a success
* 0 means the job does not exist
* -1 means the job is currently locked and can't be retried.
* -2 means the job was not found in the expected set
*/
reprocessJob(job, options) {
const queue = job.queue;
const keys = [
queue.toKey(job.id),
queue.toKey(job.id) + ':lock',
queue.toKey(options.state),
queue.toKey('wait'),
queue.toKey('meta-paused'),
queue.toKey('paused')
];
const args = [
job.id,
(job.opts.lifo ? 'R' : 'L') + 'PUSH',
queue.token,
Date.now()
];
return queue.client.reprocessJob(keys.concat(args));
},
obliterate(queue, opts) {
const client = queue.client;
const keys = [queue.keys['meta-paused'], queue.toKey('')];
const args = [opts.count, opts.force ? 'force' : null];
return client.obliterate(keys.concat(args)).then(result => {
if (result < 0) {
switch (result) {
case -1:
throw new Error('Cannot obliterate non-paused queue');
case -2:
throw new Error('Cannot obliterate queue with active jobs');
}
}
return result;
});
}
};
module.exports = scripts;
function array2obj(arr) {
const obj = {};
for (let i = 0; i < arr.length; i += 2) {
obj[arr[i]] = arr[i + 1];
}
return obj;
}
function raw2jobData(raw) {
if (raw) {
const jobData = raw[0];
if (jobData.length) {
const job = array2obj(jobData);
return [job, raw[1]];
}
}
return [];
}
+141
View File
@@ -0,0 +1,141 @@
'use strict';
const content = `--[[
Adds a job to the queue by doing the following:
- Increases the job counter if needed.
- Creates a new job key with the job data.
- if delayed:
- computes timestamp.
- adds to delayed zset.
- Emits a global event 'delayed' if the job is delayed.
- if not delayed
- Adds the jobId to the wait/paused list in one of three ways:
- LIFO
- FIFO
- prioritized.
- Adds the job to the "added" list so that workers gets notified.
Input:
KEYS[1] 'wait',
KEYS[2] 'paused'
KEYS[3] 'meta-paused'
KEYS[4] 'id'
KEYS[5] 'delayed'
KEYS[6] 'priority'
ARGV[1] key prefix,
ARGV[2] custom id (will not generate one automatically)
ARGV[3] name
ARGV[4] data (json stringified job data)
ARGV[5] opts (json stringified job opts)
ARGV[6] timestamp
ARGV[7] delay
ARGV[8] delayedTimestamp
ARGV[9] priority
ARGV[10] LIFO
ARGV[11] token
ARGV[12] debounce key
ARGV[13] debounceId
ARGV[14] debounceTtl
]]
local jobId
local jobIdKey
local rcall = redis.call
-- Includes
--[[
Function to add job considering priority.
]]
local function addJobWithPriority(priorityKey, priority, jobId, targetKey)
rcall("ZADD", priorityKey, priority, jobId)
local count = rcall("ZCOUNT", priorityKey, 0, priority)
local len = rcall("LLEN", targetKey)
local id = rcall("LINDEX", targetKey, len - (count - 1))
if id then
rcall("LINSERT", targetKey, "BEFORE", id, jobId)
else
rcall("RPUSH", targetKey, jobId)
end
end
--[[
Function to debounce a job.
]]
local function debounceJob(prefixKey, debounceId, ttl, jobId, debounceKey, token)
if debounceId ~= "" then
local debounceKeyExists
if ttl ~= "" then
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'PX', ttl, 'NX')
else
debounceKeyExists = not rcall('SET', debounceKey, jobId, 'NX')
end
if debounceKeyExists then
local currentDebounceJobId = rcall('GET', debounceKey)
rcall("PUBLISH", prefixKey .. "debounced@" .. token, currentDebounceJobId)
return currentDebounceJobId
end
end
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
local jobCounter = rcall("INCR", KEYS[4])
if ARGV[2] == "" then
jobId = jobCounter
jobIdKey = ARGV[1] .. jobId
else
jobId = ARGV[2]
jobIdKey = ARGV[1] .. jobId
if rcall("EXISTS", jobIdKey) == 1 then
rcall("PUBLISH", ARGV[1] .. "duplicated@" .. ARGV[11], jobId)
return jobId .. "" -- convert to string
end
end
local debounceKey = ARGV[12]
local opts = cmsgpack.unpack(ARGV[5])
local debouncedJobId = debounceJob(ARGV[1], ARGV[13], ARGV[14],
jobId, debounceKey, ARGV[11])
if debouncedJobId then
return debouncedJobId
end
local debounceId = ARGV[13]
local optionalValues = {}
if debounceId ~= "" then
table.insert(optionalValues, "deid")
table.insert(optionalValues, debounceId)
end
-- Store the job.
rcall("HMSET", jobIdKey, "name", ARGV[3], "data", ARGV[4], "opts", opts, "timestamp",
ARGV[6], "delay", ARGV[7], "priority", ARGV[9], unpack(optionalValues))
-- Check if job is delayed
local delayedTimestamp = tonumber(ARGV[8])
if(delayedTimestamp ~= 0) then
local timestamp = delayedTimestamp * 0x1000 + bit.band(jobCounter, 0xfff)
rcall("ZADD", KEYS[5], timestamp, jobId)
rcall("PUBLISH", KEYS[5], delayedTimestamp)
else
local target
-- Whe check for the meta-paused key to decide if we are paused or not
-- (since an empty list and !EXISTS are not really the same)
local target, paused = getTargetQueueList(KEYS[3], KEYS[1], KEYS[2])
-- Standard or priority add
local priority = tonumber(ARGV[9])
if priority == 0 then
-- LIFO or FIFO
rcall(ARGV[10], target, jobId)
else
addJobWithPriority(KEYS[6], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[1] .. "ing@" .. ARGV[11], jobId)
end
return jobId .. "" -- convert to string
`;
module.exports = {
name: 'addJob',
content,
keys: 6,
};
+30
View File
@@ -0,0 +1,30 @@
'use strict';
const content = `--[[
Add job log
Input:
KEYS[1] job id key
KEYS[2] job logs key
ARGV[1] id
ARGV[2] log
ARGV[3] keepLogs
Output:
-1 - Missing job.
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then -- // Make sure job exists
local logCount = rcall("RPUSH", KEYS[2], ARGV[2])
if ARGV[3] ~= '' then
local keepLogs = tonumber(ARGV[3])
rcall("LTRIM", KEYS[2], -keepLogs, -1)
return math.min(keepLogs, logCount)
end
return logCount
else
return -1
end
`;
module.exports = {
name: 'addLog',
content,
keys: 2,
};
+139
View File
@@ -0,0 +1,139 @@
'use strict';
const content = `--[[
Remove jobs from the specific set.
Input:
KEYS[1] set key,
KEYS[2] priority key
KEYS[3] rate limiter key
ARGV[1] prefix key
ARGV[2] maxTimestamp
ARGV[3] limit the number of jobs to be removed. 0 is unlimited
ARGV[4] set name, can be any of 'wait', 'active', 'paused', 'delayed', 'completed', or 'failed'
]]
local setKey = KEYS[1]
local priorityKey = KEYS[2]
local rateLimiterKey = KEYS[3]
local prefixKey = ARGV[1]
local maxTimestamp = ARGV[2]
local limitStr = ARGV[3]
local setName = ARGV[4]
local isList = false
local rcall = redis.call
-- Includes
--[[
Function to remove debounce key.
]]
local function removeDebounceKey(prefixKey, jobKey)
local debounceId = rcall("HGET", jobKey, "deid")
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
rcall("DEL", debounceKey)
end
end
if setName == "wait" or setName == "active" or setName == "paused" then
isList = true
end
-- We use ZRANGEBYSCORE to make the case where we're deleting a limited number
-- of items in a sorted set only run a single iteration. If we simply used
-- ZRANGE, we may take a long time traversing through jobs that are within the
-- grace period.
local function shouldUseZRangeByScore(isList, limit)
return not isList and limit > 0
end
local function getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
if isList then
return rcall("LRANGE", setKey, rangeStart, rangeEnd)
elseif shouldUseZRangeByScore(isList, limit) then
return rcall("ZRANGEBYSCORE", setKey, 0, maxTimestamp, "LIMIT", 0, limit)
else
return rcall("ZRANGE", setKey, rangeStart, rangeEnd)
end
end
local limit = tonumber(limitStr)
local rangeStart = 0
local rangeEnd = -1
-- If we're only deleting _n_ items, avoid retrieving all items
-- for faster performance
--
-- Start from the tail of the list, since that's where oldest elements
-- are generally added for FIFO lists
if limit > 0 then
rangeStart = -1 - limit + 1
rangeEnd = -1
end
local jobIds = getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
local deleted = {}
local deletedCount = 0
local jobTS
-- Run this loop:
-- - Once, if limit is -1 or 0
-- - As many times as needed if limit is positive
while ((limit <= 0 or deletedCount < limit) and next(jobIds, nil) ~= nil) do
local jobIdsLen = #jobIds
for i, jobId in ipairs(jobIds) do
if limit > 0 and deletedCount >= limit then
break
end
local jobKey = prefixKey .. jobId
if (rcall("EXISTS", jobKey .. ":lock") == 0) then
-- Find the right timestamp of the job to compare to maxTimestamp:
-- * finishedOn says when the job was completed, but it isn't set unless the job has actually completed
-- * processedOn represents when the job was last attempted, but it doesn't get populated until the job is first tried
-- * timestamp is the original job submission time
-- Fetch all three of these (in that order) and use the first one that is set so that we'll leave jobs that have been active within the grace period:
for _, ts in ipairs(rcall("HMGET", jobKey, "finishedOn", "processedOn", "timestamp")) do
if (ts) then
jobTS = ts
break
end
end
if (not jobTS or jobTS < maxTimestamp) then
if isList then
-- Job ids can't be the empty string. Use the empty string as a
-- deletion marker. The actual deletion will occur at the end of the
-- script.
rcall("LSET", setKey, rangeEnd - jobIdsLen + i, "")
else
rcall("ZREM", setKey, jobId)
end
rcall("ZREM", priorityKey, jobId)
if setName ~= "completed" and setName ~= "failed" then
removeDebounceKey(prefixKey, jobKey)
end
rcall("DEL", jobKey)
rcall("DEL", jobKey .. ":logs")
-- delete keys related to rate limiter
-- NOTE: this code is unncessary for other sets than wait, paused and delayed.
local limiterIndexTable = rateLimiterKey .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
deletedCount = deletedCount + 1
table.insert(deleted, jobId)
end
end
end
-- If we didn't have a limit or used the single-iteration ZRANGEBYSCORE
-- function, return immediately. We should have deleted all the jobs we can
if limit <= 0 or shouldUseZRangeByScore(isList, limit) then
break
end
if deletedCount < limit then
-- We didn't delete enough. Look for more to delete
rangeStart = rangeStart - limit
rangeEnd = rangeEnd - limit
jobIds = getJobs(setKey, isList, rangeStart, rangeEnd, maxTimestamp, limit)
end
end
if isList then
rcall("LREM", setKey, 0, "")
end
return deleted
`;
module.exports = {
name: 'cleanJobsInSet',
content,
keys: 3,
};
+26
View File
@@ -0,0 +1,26 @@
'use strict';
const content = `--[[
Extend lock and removes the job from the stalled set.
Input:
KEYS[1] 'lock',
KEYS[2] 'stalled'
ARGV[1] token
ARGV[2] lock duration in milliseconds
ARGV[3] jobid
Output:
"1" if lock extended succesfully.
]]
local rcall = redis.call
if rcall("GET", KEYS[1]) == ARGV[1] then
if rcall("SET", KEYS[1], ARGV[1], "PX", ARGV[2]) then
rcall("SREM", KEYS[2], ARGV[3])
return 1
end
end
return 0
`;
module.exports = {
name: 'extendLock',
content,
keys: 2,
};
+49
View File
@@ -0,0 +1,49 @@
'use strict';
const content = `--[[
Get counts per provided states
Input:
KEYS[1] wait key
KEYS[2] paused key
KEYS[3] meta-paused key
KEYS[4] priority key
ARGV[1...] priorities
]]
local rcall = redis.call
local results = {}
local prioritizedKey = KEYS[4]
-- Includes
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
for i = 1, #ARGV do
local priority = tonumber(ARGV[i])
if priority == 0 then
local target = getTargetQueueList(KEYS[3], KEYS[1], KEYS[2])
local count = rcall("LLEN", target) - rcall("ZCARD", prioritizedKey)
if count < 0 then
-- considering when last waiting job is moved to active before
-- removing priority reference
results[#results+1] = 0
else
results[#results+1] = count
end
else
results[#results+1] = rcall("ZCOUNT", prioritizedKey,
priority, priority)
end
end
return results
`;
module.exports = {
name: 'getCountsPerPriority',
content,
keys: 4,
};
+29
View File
@@ -0,0 +1,29 @@
'use strict';
module.exports = {
["addJob-6"]: require('./addJob-6'),
["addLog-2"]: require('./addLog-2'),
["cleanJobsInSet-3"]: require('./cleanJobsInSet-3'),
["extendLock-2"]: require('./extendLock-2'),
["getCountsPerPriority-4"]: require('./getCountsPerPriority-4'),
["isFinished-2"]: require('./isFinished-2'),
["isJobInList-1"]: require('./isJobInList-1'),
["moveStalledJobsToWait-7"]: require('./moveStalledJobsToWait-7'),
["moveToActive-8"]: require('./moveToActive-8'),
["moveToDelayed-4"]: require('./moveToDelayed-4'),
["moveToFinished-9"]: require('./moveToFinished-9'),
["obliterate-2"]: require('./obliterate-2'),
["pause-5"]: require('./pause-5'),
["promote-5"]: require('./promote-5'),
["releaseLock-1"]: require('./releaseLock-1'),
["removeJob-11"]: require('./removeJob-11'),
["removeJobs-8"]: require('./removeJobs-8'),
["removeRepeatable-2"]: require('./removeRepeatable-2'),
["reprocessJob-6"]: require('./reprocessJob-6'),
["retryJob-7"]: require('./retryJob-7'),
["retryJobs-5"]: require('./retryJobs-5'),
["saveStacktrace-1"]: require('./saveStacktrace-1'),
["takeLock-1"]: require('./takeLock-1'),
["updateData-1"]: require('./updateData-1'),
["updateDelaySet-6"]: require('./updateDelaySet-6'),
["updateProgress-2"]: require('./updateProgress-2'),
}
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const content = `--[[
Checks if a job is finished (.i.e. is in the completed or failed set)
Input:
KEYS[1] completed key
KEYS[2] failed key
ARGV[1] job id
Output:
0 - not finished.
1 - completed.
2 - failed.
]]
if redis.call("ZSCORE", KEYS[1], ARGV[1]) ~= false then
return 1
end
if redis.call("ZSCORE", KEYS[2], ARGV[1]) ~= false then
return 2
end
return redis.call("ZSCORE", KEYS[2], ARGV[1])
`;
module.exports = {
name: 'isFinished',
content,
keys: 2,
};
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const content = `--[[
Checks if job is in a given list.
Input:
KEYS[1]
ARGV[1]
Output:
1 if element found in the list.
]]
local function item_in_list (list, item)
for _, v in pairs(list) do
if v == item then
return 1
end
end
return nil
end
local items = redis.call("LRANGE", KEYS[1] , 0, -1)
return item_in_list(items, ARGV[1])
`;
module.exports = {
name: 'isJobInList',
content,
keys: 1,
};
+156
View File
@@ -0,0 +1,156 @@
'use strict';
const content = `--[[
Move stalled jobs to wait.
Input:
KEYS[1] 'stalled' (SET)
KEYS[2] 'wait', (LIST)
KEYS[3] 'active', (LIST)
KEYS[4] 'failed', (ZSET)
KEYS[5] 'stalled-check', (KEY)
KEYS[6] 'meta-paused', (KEY)
KEYS[7] 'paused', (LIST)
ARGV[1] Max stalled job count
ARGV[2] queue.toKey('')
ARGV[3] timestamp
ARGV[4] max check time
Events:
'stalled' with stalled job id.
]]
local rcall = redis.call
-- Includes
--[[
Function to loop in batches.
Just a bit of warning, some commands as ZREM
could receive a maximum of 7000 parameters per call.
]]
local function batches(n, batchSize)
local i = 0
return function()
local from = i * batchSize + 1
i = i + 1
if (from <= n) then
local to = math.min(from + batchSize - 1, n)
return from, to
end
end
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
--[[
Function to remove debounce key if needed.
]]
local function removeDebounceKeyIfNeeded(prefixKey, debounceId)
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
local pttl = rcall("PTTL", debounceKey)
if pttl == 0 or pttl == -1 then
rcall("DEL", debounceKey)
end
end
end
local function removeJob(jobId, baseKey)
local jobKey = baseKey .. jobId
rcall("DEL", jobKey, jobKey .. ':logs')
end
local function removeJobsByMaxAge(timestamp, maxAge, targetSet, prefix)
local start = timestamp - maxAge * 1000
local jobIds = rcall("ZREVRANGEBYSCORE", targetSet, start, "-inf")
for i, jobId in ipairs(jobIds) do
removeJob(jobId, prefix)
end
rcall("ZREMRANGEBYSCORE", targetSet, "-inf", start)
end
local function removeJobsByMaxCount(maxCount, targetSet, prefix)
local start = maxCount
local jobIds = rcall("ZREVRANGE", targetSet, start, -1)
for i, jobId in ipairs(jobIds) do
removeJob(jobId, prefix)
end
rcall("ZREMRANGEBYRANK", targetSet, 0, -(maxCount + 1))
end
-- Check if we need to check for stalled jobs now.
if rcall("EXISTS", KEYS[5]) == 1 then
return {{}, {}}
end
rcall("SET", KEYS[5], ARGV[3], "PX", ARGV[4])
-- Move all stalled jobs to wait
local stalling = rcall('SMEMBERS', KEYS[1])
local stalled = {}
local failed = {}
if(#stalling > 0) then
rcall('DEL', KEYS[1])
local MAX_STALLED_JOB_COUNT = tonumber(ARGV[1])
-- Remove from active list
for i, jobId in ipairs(stalling) do
local jobKey = ARGV[2] .. jobId
-- Check that the lock is also missing, then we can handle this job as really stalled.
if(rcall("EXISTS", jobKey .. ":lock") == 0) then
-- Remove from the active queue.
local removed = rcall("LREM", KEYS[3], 1, jobId)
if(removed > 0) then
-- If this job has been stalled too many times, such as if it crashes the worker, then fail it.
local stalledCount = rcall("HINCRBY", jobKey, "stalledCounter", 1)
if(stalledCount > MAX_STALLED_JOB_COUNT) then
local jobAttributes = rcall("HMGET", jobKey, "opts", "deid")
local opts = cjson.decode(jobAttributes[1])
local removeOnFailType = type(opts["removeOnFail"])
rcall("ZADD", KEYS[4], ARGV[3], jobId)
rcall("HMSET", jobKey, "failedReason", "job stalled more than allowable limit",
"finishedOn", ARGV[3])
removeDebounceKeyIfNeeded(ARGV[2], jobAttributes[2])
rcall("PUBLISH", KEYS[4], '{"jobId":"' .. jobId .. '", "val": "job stalled more than maxStalledCount"}')
if removeOnFailType == "number" then
removeJobsByMaxCount(opts["removeOnFail"],
KEYS[4], ARGV[2])
elseif removeOnFailType == "boolean" then
if opts["removeOnFail"] then
removeJob(jobId, ARGV[2])
rcall("ZREM", KEYS[4], jobId)
end
elseif removeOnFailType ~= "nil" then
local maxAge = opts["removeOnFail"]["age"]
local maxCount = opts["removeOnFail"]["count"]
if maxAge ~= nil then
removeJobsByMaxAge(ARGV[3], maxAge,
KEYS[4], ARGV[2])
end
if maxCount ~= nil and maxCount > 0 then
removeJobsByMaxCount(maxCount, KEYS[4],
ARGV[2])
end
end
table.insert(failed, jobId)
else
local target = getTargetQueueList(KEYS[6], KEYS[2], KEYS[7])
-- Move the job back to the wait queue, to immediately be picked up by a waiting worker.
rcall("RPUSH", target, jobId)
rcall('PUBLISH', KEYS[1] .. '@', jobId)
table.insert(stalled, jobId)
end
end
end
end
end
-- Mark potentially stalled jobs
local active = rcall('LRANGE', KEYS[3], 0, -1)
if (#active > 0) then
for from, to in batches(#active, 7000) do
rcall('SADD', KEYS[1], unpack(active, from, to))
end
end
return {failed, stalled}
`;
module.exports = {
name: 'moveStalledJobsToWait',
content,
keys: 7,
};
+130
View File
@@ -0,0 +1,130 @@
'use strict';
const content = `--[[
Move next job to be processed to active, lock it and fetch its data. The job
may be delayed, in that case we need to move it to the delayed set instead.
This operation guarantees that the worker owns the job during the locks
expiration time. The worker is responsible of keeping the lock fresh
so that no other worker picks this job again.
Input:
KEYS[1] wait key
KEYS[2] active key
KEYS[3] priority key
KEYS[4] active event key
KEYS[5] stalled key
-- Rate limiting
KEYS[6] rate limiter key
KEYS[7] delayed key
--
KEYS[8] drained key
ARGV[1] key prefix
ARGV[2] lock token
ARGV[3] lock duration in milliseconds
ARGV[4] timestamp
ARGV[5] optional jobid
ARGV[6] optional jobs per time unit (rate limiter)
ARGV[7] optional time unit (rate limiter)
ARGV[8] optional do not do anything with job if rate limit hit
ARGV[9] optional rate limit by key
]]
local rcall = redis.call
local rateLimit = function(jobId, maxJobs)
local rateLimiterKey = KEYS[6];
local limiterIndexTable = rateLimiterKey .. ":index"
-- Rate limit by group?
if(ARGV[9]) then
local group = string.match(jobId, "[^:]+$")
if group ~= nil then
rateLimiterKey = rateLimiterKey .. ":" .. group
end
end
-- -- key for storing rate limited jobs
-- When a job has been previously rate limited it should be part of this set
-- if the job is back here means that the delay time for this job has passed and now we should
-- be able to process it again.
local limitedSetKey = rateLimiterKey .. ":limited"
local delay = 0
-- -- Check if job was already limited
local isLimited = rcall("SISMEMBER", limitedSetKey, jobId);
if isLimited == 1 then
-- Remove from limited zset since we are going to try to process it
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
else
-- If not, check if there are any limited jobs
-- If the job has not been rate limited, we should check if there are any other rate limited jobs, because if that
-- is the case we do not want to process this job, just calculate a delay for it and put it to "sleep".
local numLimitedJobs = rcall("SCARD", limitedSetKey)
if numLimitedJobs > 0 then
-- Note, add some slack to compensate for drift.
delay = ((numLimitedJobs * ARGV[7] * 1.1) / maxJobs) + tonumber(rcall("PTTL", rateLimiterKey))
end
end
local jobCounter = tonumber(rcall("GET", rateLimiterKey))
if(jobCounter == nil) then
jobCounter = 0
end
-- check if rate limit hit
if (delay == 0) and (jobCounter >= maxJobs) then
-- Seems like there are no current rated limited jobs, but the jobCounter has exceeded the number of jobs for this unit of time so we need to rate limit this job.
local exceedingJobs = jobCounter - maxJobs
delay = tonumber(rcall("PTTL", rateLimiterKey)) + ((exceedingJobs) * ARGV[7]) / maxJobs
end
if delay > 0 then
local bounceBack = ARGV[8]
if bounceBack == 'false' then
local timestamp = delay + tonumber(ARGV[4])
-- put job into delayed queue
rcall("ZADD", KEYS[7], timestamp * 0x1000 + bit.band(jobCounter, 0xfff), jobId)
rcall("PUBLISH", KEYS[7], timestamp)
rcall("SADD", limitedSetKey, jobId)
-- store index so that we can delete rate limited data
rcall("HSET", limiterIndexTable, jobId, limitedSetKey)
end
-- remove from active queue
rcall("LREM", KEYS[2], 1, jobId)
return true
else
-- false indicates not rate limited
-- increment jobCounter only when a job is not rate limited
if (jobCounter == 0) then
rcall("PSETEX", rateLimiterKey, ARGV[7], 1)
else
rcall("INCR", rateLimiterKey)
end
return false
end
end
local jobId = ARGV[5]
if jobId ~= '' then
-- clean stalled key
rcall("SREM", KEYS[5], jobId)
else
-- move from wait to active
jobId = rcall("RPOPLPUSH", KEYS[1], KEYS[2])
end
if jobId then
-- Check if we need to perform rate limiting.
local maxJobs = tonumber(ARGV[6])
if maxJobs then
if rateLimit(jobId, maxJobs) then
return
end
end
-- get a lock
local jobKey = ARGV[1] .. jobId
local lockKey = jobKey .. ':lock'
rcall("SET", lockKey, ARGV[2], "PX", ARGV[3])
-- remove from priority
rcall("ZREM", KEYS[3], jobId)
rcall("PUBLISH", KEYS[4], jobId)
rcall("HSET", jobKey, "processedOn", ARGV[4])
return {rcall("HGETALL", jobKey), jobId} -- get job data
else
rcall("PUBLISH", KEYS[8], "")
end
`;
module.exports = {
name: 'moveToActive',
content,
keys: 8,
};
+59
View File
@@ -0,0 +1,59 @@
'use strict';
const content = `--[[
Moves job from active to delayed set.
Input:
KEYS[1] active key
KEYS[2] delayed key
KEYS[3] job key
KEYS[4] stalled key
ARGV[1] delayedTimestamp
ARGV[2] the id of the job
ARGV[3] queue token
Output:
0 - OK
-1 - Missing job.
-2 - Job is locked.
Events:
- delayed key.
]]
local rcall = redis.call
-- Includes
local function removeLock(jobKey, stalledKey, token, jobId)
if token ~= "0" then
local lockKey = jobKey .. ':lock'
local lockToken = rcall("GET", lockKey)
if lockToken == token then
rcall("DEL", lockKey)
rcall("SREM", stalledKey, jobId)
else
if lockToken then
-- Lock exists but token does not match
return -6
else
-- Lock is missing completely
return -2
end
end
end
return 0
end
if rcall("EXISTS", KEYS[3]) == 1 then
local errorCode = removeLock(KEYS[3], KEYS[4], ARGV[3], ARGV[2])
if errorCode < 0 then
return errorCode
end
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[2])
if numRemovedElements < 1 then return -3 end
local score = tonumber(ARGV[1])
rcall("ZADD", KEYS[2], score, ARGV[2])
rcall("PUBLISH", KEYS[2], (score / 0x1000))
return 0
else
return -1
end
`;
module.exports = {
name: 'moveToDelayed',
content,
keys: 4,
};
+196
View File
@@ -0,0 +1,196 @@
'use strict';
const content = `--[[
Move job from active to a finished status (completed or failed)
A job can only be moved to completed if it was active.
The job must be locked before it can be moved to a finished status,
and the lock must be released in this script.
Input:
KEYS[1] active key
KEYS[2] completed/failed key
KEYS[3] jobId key
KEYS[4] wait key
KEYS[5] priority key
KEYS[6] active event key
KEYS[7] delayed key
KEYS[8] stalled key
KEYS[9] metrics key
ARGV[1] jobId
ARGV[2] timestamp
ARGV[3] msg property
ARGV[4] return value / failed reason
ARGV[5] token
ARGV[6] shouldRemove
ARGV[7] event data (? maybe just send jobid).
ARGV[8] should fetch next job
ARGV[9] base key
ARGV[10] lock token
ARGV[11] lock duration in milliseconds
ARGV[12] maxMetricsSize
Output:
0 OK
-1 Missing key.
-2 Missing lock.
-3 - Job not in active set.
Events:
'completed/failed'
]]
local rcall = redis.call
-- Includes
--[[
Functions to collect metrics based on a current and previous count of jobs.
Granualarity is fixed at 1 minute.
]]
-- Includes
--[[
Function to loop in batches.
Just a bit of warning, some commands as ZREM
could receive a maximum of 7000 parameters per call.
]]
local function batches(n, batchSize)
local i = 0
return function()
local from = i * batchSize + 1
i = i + 1
if (from <= n) then
local to = math.min(from + batchSize - 1, n)
return from, to
end
end
end
local function collectMetrics(metaKey, dataPointsList, maxDataPoints, timestamp)
-- Increment current count
local count = rcall("HINCRBY", metaKey, "count", 1) - 1
-- Compute how many data points we need to add to the list, N.
local prevTS = rcall("HGET", metaKey, "prevTS")
if not prevTS then
-- If prevTS is nil, set it to the current timestamp
rcall("HSET", metaKey, "prevTS", timestamp, "prevCount", 0)
return
end
local N = math.min(math.floor(timestamp / 60000) - math.floor(prevTS / 60000), tonumber(maxDataPoints))
if N > 0 then
local delta = count - rcall("HGET", metaKey, "prevCount")
-- If N > 1, add N-1 zeros to the list
if N > 1 then
local points = {}
points[1] = delta
for i = 2, N do points[i] = 0 end
for from, to in batches(#points, 7000) do
rcall("LPUSH", dataPointsList, unpack(points, from, to))
end
else
-- LPUSH delta to the list
rcall("LPUSH", dataPointsList, delta)
end
-- LTRIM to keep list to its max size
rcall("LTRIM", dataPointsList, 0, maxDataPoints - 1)
-- update prev count with current count
rcall("HSET", metaKey, "prevCount", count, "prevTS", timestamp)
end
end
local function removeLock(jobKey, stalledKey, token, jobId)
if token ~= "0" then
local lockKey = jobKey .. ':lock'
local lockToken = rcall("GET", lockKey)
if lockToken == token then
rcall("DEL", lockKey)
rcall("SREM", stalledKey, jobId)
else
if lockToken then
-- Lock exists but token does not match
return -6
else
-- Lock is missing completely
return -2
end
end
end
return 0
end
--[[
Function to remove debounce key if needed.
]]
local function removeDebounceKeyIfNeeded(prefixKey, debounceId)
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
local pttl = rcall("PTTL", debounceKey)
if pttl == 0 or pttl == -1 then
rcall("DEL", debounceKey)
end
end
end
if rcall("EXISTS", KEYS[3]) == 1 then -- // Make sure job exists
local errorCode = removeLock(KEYS[3], KEYS[8], ARGV[5], ARGV[1])
if errorCode < 0 then
return errorCode
end
-- Remove from active list (if not active we shall return error)
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[1])
if numRemovedElements < 1 then return -3 end
local debounceId = rcall("HGET", KEYS[3], "deid")
removeDebounceKeyIfNeeded(ARGV[9], debounceId)
-- Remove job?
local keepJobs = cmsgpack.unpack(ARGV[6])
local maxCount = keepJobs['count']
local maxAge = keepJobs['age']
local targetSet = KEYS[2]
local timestamp = ARGV[2]
if maxCount ~= 0 then
-- Add to complete/failed set
rcall("ZADD", targetSet, timestamp, ARGV[1])
rcall("HMSET", KEYS[3], ARGV[3], ARGV[4], "finishedOn", timestamp) -- "returnvalue" / "failedReason" and "finishedOn"
local function removeJobs(jobIds)
for i, jobId in ipairs(jobIds) do
local jobKey = ARGV[9] .. jobId
local jobLogKey = jobKey .. ':logs'
rcall("DEL", jobKey, jobLogKey)
end
end
-- Remove old jobs?
if maxAge ~= nil then
local start = timestamp - maxAge * 1000
local jobIds = rcall("ZREVRANGEBYSCORE", targetSet, start, "-inf")
removeJobs(jobIds)
rcall("ZREMRANGEBYSCORE", targetSet, "-inf", start)
end
if maxCount ~= nil and maxCount > 0 then
local start = maxCount
local jobIds = rcall("ZREVRANGE", targetSet, start, -1)
removeJobs(jobIds)
rcall("ZREMRANGEBYRANK", targetSet, 0, -(maxCount + 1));
end
else
local jobLogKey = KEYS[3] .. ':logs'
rcall("DEL", KEYS[3], jobLogKey)
end
-- Collect metrics
if ARGV[12] ~= "" then
collectMetrics(KEYS[9], KEYS[9]..':data', ARGV[12], timestamp)
end
rcall("PUBLISH", targetSet, ARGV[7])
-- Try to get next job to avoid an extra roundtrip if the queue is not closing,
-- and not rate limited.
if (ARGV[8] == "1") then
-- move from wait to active
local jobId = rcall("RPOPLPUSH", KEYS[4], KEYS[1])
if jobId then
local jobKey = ARGV[9] .. jobId
local lockKey = jobKey .. ':lock'
-- get a lock
rcall("SET", lockKey, ARGV[11], "PX", ARGV[10])
rcall("ZREM", KEYS[5], jobId) -- remove from priority
rcall("PUBLISH", KEYS[6], jobId)
rcall("HSET", jobKey, "processedOn", ARGV[2])
return {rcall("HGETALL", jobKey), jobId} -- get job data
end
end
return 0
else
return -1
end
`;
module.exports = {
name: 'moveToFinished',
content,
keys: 9,
};
+106
View File
@@ -0,0 +1,106 @@
'use strict';
const content = `--[[
Completely obliterates a queue and all of its contents
Input:
KEYS[1] meta-paused
KEYS[2] base
ARGV[1] count
ARGV[2] force
]]
-- This command completely destroys a queue including all of its jobs, current or past
-- leaving no trace of its existence. Since this script needs to iterate to find all the job
-- keys, consider that this call may be slow for very large queues.
-- The queue needs to be "paused" or it will return an error
-- If the queue has currently active jobs then the script by default will return error,
-- however this behaviour can be overrided using the 'force' option.
local maxCount = tonumber(ARGV[1])
local baseKey = KEYS[2]
local rcall = redis.call
-- Includes
--[[
Function to remove debounce key.
]]
local function removeDebounceKey(prefixKey, jobKey)
local debounceId = rcall("HGET", jobKey, "deid")
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
rcall("DEL", debounceKey)
end
end
local function getListItems(keyName, max)
return rcall('LRANGE', keyName, 0, max - 1)
end
local function getZSetItems(keyName, max)
return rcall('ZRANGE', keyName, 0, max - 1)
end
local function removeJobs(baseKey, keys)
for i, key in ipairs(keys) do
local jobKey = baseKey .. key
rcall("DEL", jobKey, jobKey .. ':logs')
removeDebounceKey(baseKey, jobKey)
end
maxCount = maxCount - #keys
end
local function removeListJobs(keyName, max)
local jobs = getListItems(keyName, max)
removeJobs(baseKey, jobs)
rcall("LTRIM", keyName, #jobs, -1)
end
local function removeZSetJobs(keyName, max)
local jobs = getZSetItems(keyName, max)
removeJobs(baseKey, jobs)
if (#jobs > 0) then rcall("ZREM", keyName, unpack(jobs)) end
end
local function removeLockKeys(keys)
for i, key in ipairs(keys) do rcall("DEL", baseKey .. key .. ':lock') end
end
-- 1) Check if paused, if not return with error.
if rcall("EXISTS", KEYS[1]) ~= 1 then
return -1 -- Error, NotPaused
end
-- 2) Check if there are active jobs, if there are and not "force" return error.
local activeKey = baseKey .. 'active'
local activeJobs = getListItems(activeKey, maxCount)
if (#activeJobs > 0) then
if (ARGV[2] == "") then
return -2 -- Error, ExistsActiveJobs
end
end
removeLockKeys(activeJobs)
removeJobs(baseKey, activeJobs)
rcall("LTRIM", activeKey, #activeJobs, -1)
if (maxCount <= 0) then return 1 end
local waitKey = baseKey .. 'paused'
removeListJobs(waitKey, maxCount)
if (maxCount <= 0) then return 1 end
local delayedKey = baseKey .. 'delayed'
removeZSetJobs(delayedKey, maxCount)
if (maxCount <= 0) then return 1 end
local completedKey = baseKey .. 'completed'
removeZSetJobs(completedKey, maxCount)
if (maxCount <= 0) then return 1 end
local failedKey = baseKey .. 'failed'
removeZSetJobs(failedKey, maxCount)
if (maxCount <= 0) then return 1 end
if (maxCount > 0) then
rcall("DEL", baseKey .. 'priority')
rcall("DEL", baseKey .. 'stalled-check')
rcall("DEL", baseKey .. 'stalled')
rcall("DEL", baseKey .. 'meta-paused')
rcall("DEL", baseKey .. 'meta')
rcall("DEL", baseKey .. 'id')
rcall("DEL", baseKey .. 'repeat')
rcall("DEL", baseKey .. 'metrics:completed')
rcall("DEL", baseKey .. 'metrics:completed:data')
rcall("DEL", baseKey .. 'metrics:failed')
rcall("DEL", baseKey .. 'metrics:failed:data')
return 0
else
return 1
end
`;
module.exports = {
name: 'obliterate',
content,
keys: 2,
};
+33
View File
@@ -0,0 +1,33 @@
'use strict';
const content = `--[[
Pauses or resumes a queue globably.
Input:
KEYS[1] 'wait' or 'paused''
KEYS[2] 'paused' or 'wait'
KEYS[3] 'meta-paused'
KEYS[4] 'paused' o 'resumed' event.
KEYS[5] 'meta' this key is only used in BullMQ and above.
ARGV[1] 'paused' or 'resumed'
Event:
publish paused or resumed event.
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then
rcall("RENAME", KEYS[1], KEYS[2])
end
if ARGV[1] == "paused" then
rcall("SET", KEYS[3], 1)
-- for forwards compatibility
rcall("HSET", KEYS[5], "paused", 1)
else
rcall("DEL", KEYS[3])
-- for forwards compatibility
rcall("HDEL", KEYS[5], "paused")
end
rcall("PUBLISH", KEYS[4], ARGV[1])
`;
module.exports = {
name: 'pause',
content,
keys: 5,
};
+65
View File
@@ -0,0 +1,65 @@
'use strict';
const content = `--[[
Promotes a job that is currently "delayed" to the "waiting" state
Input:
KEYS[1] 'delayed'
KEYS[2] 'wait'
KEYS[3] 'paused'
KEYS[4] 'meta-paused'
KEYS[5] 'priority'
ARGV[1] queue.toKey('')
ARGV[2] jobId
ARGV[3] queue token
Events:
'waiting'
]]
local rcall = redis.call;
local jobId = ARGV[2]
-- Includes
--[[
Function to add job considering priority.
]]
local function addJobWithPriority(priorityKey, priority, jobId, targetKey)
rcall("ZADD", priorityKey, priority, jobId)
local count = rcall("ZCOUNT", priorityKey, 0, priority)
local len = rcall("LLEN", targetKey)
local id = rcall("LINDEX", targetKey, len - (count - 1))
if id then
rcall("LINSERT", targetKey, "BEFORE", id, jobId)
else
rcall("RPUSH", targetKey, jobId)
end
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
if rcall("ZREM", KEYS[1], jobId) == 1 then
local priority = tonumber(rcall("HGET", ARGV[1] .. jobId, "priority")) or 0
local target = getTargetQueueList(KEYS[4], KEYS[2], KEYS[3])
if priority == 0 then
-- LIFO or FIFO
rcall("LPUSH", target, jobId)
else
addJobWithPriority(KEYS[5], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[2] .. "ing@" .. ARGV[3], jobId)
rcall("HSET", ARGV[1] .. jobId, "delay", 0)
return 0
else
return -1
end
`;
module.exports = {
name: 'promote',
content,
keys: 5,
};
+22
View File
@@ -0,0 +1,22 @@
'use strict';
const content = `--[[
Release lock
Input:
KEYS[1] 'lock',
ARGV[1] token
ARGV[2] lock duration in milliseconds
Output:
"OK" if lock extented succesfully.
]]
local rcall = redis.call
if rcall("GET", KEYS[1]) == ARGV[1] then
return rcall("DEL", KEYS[1])
else
return 0
end
`;
module.exports = {
name: 'releaseLock',
content,
keys: 1,
};
+65
View File
@@ -0,0 +1,65 @@
'use strict';
const content = `--[[
Remove a job from all the queues it may be in as well as all its data.
In order to be able to remove a job, it must be unlocked.
Input:
KEYS[1] 'active',
KEYS[2] 'wait',
KEYS[3] 'delayed',
KEYS[4] 'paused',
KEYS[5] 'completed',
KEYS[6] 'failed',
KEYS[7] 'priority',
KEYS[8] jobId key
KEYS[9] job logs
KEYS[10] rate limiter index table
KEYS[11] prefix key
ARGV[1] jobId
ARGV[2] lock token
Events:
'removed'
]]
-- TODO PUBLISH global event 'removed'
local rcall = redis.call
-- Includes
--[[
Function to remove debounce key.
]]
local function removeDebounceKey(prefixKey, jobKey)
local debounceId = rcall("HGET", jobKey, "deid")
if debounceId then
local debounceKey = prefixKey .. "de:" .. debounceId
rcall("DEL", debounceKey)
end
end
local lockKey = KEYS[8] .. ':lock'
local lock = rcall("GET", lockKey)
if not lock then -- or (lock == ARGV[2])) then
local jobId = ARGV[1]
rcall("LREM", KEYS[1], 0, jobId)
rcall("LREM", KEYS[2], 0, jobId)
rcall("ZREM", KEYS[3], jobId)
rcall("LREM", KEYS[4], 0, jobId)
rcall("ZREM", KEYS[5], jobId)
rcall("ZREM", KEYS[6], jobId)
rcall("ZREM", KEYS[7], jobId)
removeDebounceKey(KEYS[11], KEYS[8])
rcall("DEL", KEYS[8])
rcall("DEL", KEYS[9])
-- delete keys related to rate limiter
local limiterIndexTable = KEYS[10] .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
return 1
else
return 0
end
`;
module.exports = {
name: 'removeJob',
content,
keys: 11,
};
+60
View File
@@ -0,0 +1,60 @@
'use strict';
const content = `--[[
Remove all jobs matching a given pattern from all the queues they may be in as well as all its data.
In order to be able to remove any job, they must be unlocked.
Input:
KEYS[1] 'active',
KEYS[2] 'wait',
KEYS[3] 'delayed',
KEYS[4] 'paused',
KEYS[5] 'completed',
KEYS[6] 'failed',
KEYS[7] 'priority',
KEYS[8] 'rate-limiter'
ARGV[1] prefix
ARGV[2] pattern
ARGV[3] cursor
Events:
'removed'
]]
-- TODO PUBLISH global events 'removed'
local rcall = redis.call
local result = rcall("SCAN", ARGV[3], "MATCH", ARGV[1] .. ARGV[2])
local cursor = result[1];
local jobKeys = result[2];
local removed = {}
local prefixLen = string.len(ARGV[1]) + 1
for i, jobKey in ipairs(jobKeys) do
local keyTypeResp = rcall("TYPE", jobKey)
if keyTypeResp["ok"] == "hash" then
local jobId = string.sub(jobKey, prefixLen)
local lockKey = jobKey .. ':lock'
local lock = redis.call("GET", lockKey)
if not lock then
rcall("LREM", KEYS[1], 0, jobId)
rcall("LREM", KEYS[2], 0, jobId)
rcall("ZREM", KEYS[3], jobId)
rcall("LREM", KEYS[4], 0, jobId)
rcall("ZREM", KEYS[5], jobId)
rcall("ZREM", KEYS[6], jobId)
rcall("ZREM", KEYS[7], jobId)
rcall("DEL", jobKey)
rcall("DEL", jobKey .. ':logs')
-- delete keys related to rate limiter
local limiterIndexTable = KEYS[8] .. ":index"
local limitedSetKey = rcall("HGET", limiterIndexTable, jobId)
if limitedSetKey then
rcall("SREM", limitedSetKey, jobId)
rcall("HDEL", limiterIndexTable, jobId)
end
table.insert(removed, jobId)
end
end
end
return {cursor, removed}
`;
module.exports = {
name: 'removeJobs',
content,
keys: 8,
};
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const content = `--[[
Removes a repeatable job
Input:
KEYS[1] repeat jobs key
KEYS[2] delayed jobs key
ARGV[1] repeat job id
ARGV[2] repeat job key
ARGV[3] queue key
]]
local millis = redis.call("ZSCORE", KEYS[1], ARGV[2])
if(millis) then
-- Delete next programmed job.
local repeatJobId = ARGV[1] .. millis
if(redis.call("ZREM", KEYS[2], repeatJobId) == 1) then
redis.call("DEL", ARGV[3] .. repeatJobId)
end
end
redis.call("ZREM", KEYS[1], ARGV[2]);
`;
module.exports = {
name: 'removeRepeatable',
content,
keys: 2,
};
+51
View File
@@ -0,0 +1,51 @@
'use strict';
const content = `--[[
Attempts to reprocess a job
Input:
KEYS[1] job key
KEYS[2] job lock key
KEYS[3] job state
KEYS[4] wait key
KEYS[5] meta-pause
KEYS[6] paused key
ARGV[1] job.id,
ARGV[2] (job.opts.lifo ? 'R' : 'L') + 'PUSH'
ARGV[3] token
ARGV[4] timestamp
Output:
1 means the operation was a success
0 means the job does not exist
-1 means the job is currently locked and can't be retried.
-2 means the job was not found in the expected set.
]]
local rcall = redis.call;
if (rcall("EXISTS", KEYS[1]) == 1) then
if (rcall("EXISTS", KEYS[2]) == 0) then
rcall("HDEL", KEYS[1], "finishedOn", "processedOn", "failedReason")
rcall("HSET", KEYS[1], "retriedOn", ARGV[4])
if (rcall("ZREM", KEYS[3], ARGV[1]) == 1) then
local target
if rcall("EXISTS", KEYS[5]) ~= 1 then
target = KEYS[4]
else
target = KEYS[6]
end
rcall(ARGV[2], target, ARGV[1])
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[4] .. "ing@" .. ARGV[3], ARGV[1])
return 1
else
return -2
end
else
return -1
end
else
return 0
end
`;
module.exports = {
name: 'reprocessJob',
content,
keys: 6,
};
+93
View File
@@ -0,0 +1,93 @@
'use strict';
const content = `--[[
Retries a failed job by moving it back to the wait queue.
Input:
KEYS[1] 'active',
KEYS[2] 'wait'
KEYS[3] jobId key
KEYS[4] 'meta-paused'
KEYS[5] 'paused'
KEYS[6] stalled key
KEYS[7] 'priority'
ARGV[1] pushCmd
ARGV[2] jobId
ARGV[3] token
Events:
'prefix:added'
Output:
0 - OK
-1 - Missing key
-2 - Job Not locked
-3 - Job not in active set
]]
local rcall = redis.call
-- Includes
--[[
Function to add job considering priority.
]]
local function addJobWithPriority(priorityKey, priority, jobId, targetKey)
rcall("ZADD", priorityKey, priority, jobId)
local count = rcall("ZCOUNT", priorityKey, 0, priority)
local len = rcall("LLEN", targetKey)
local id = rcall("LINDEX", targetKey, len - (count - 1))
if id then
rcall("LINSERT", targetKey, "BEFORE", id, jobId)
else
rcall("RPUSH", targetKey, jobId)
end
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
local function removeLock(jobKey, stalledKey, token, jobId)
if token ~= "0" then
local lockKey = jobKey .. ':lock'
local lockToken = rcall("GET", lockKey)
if lockToken == token then
rcall("DEL", lockKey)
rcall("SREM", stalledKey, jobId)
else
if lockToken then
-- Lock exists but token does not match
return -6
else
-- Lock is missing completely
return -2
end
end
end
return 0
end
if rcall("EXISTS", KEYS[3]) == 1 then
local errorCode = removeLock(KEYS[3], KEYS[6], ARGV[3], ARGV[2])
if errorCode < 0 then
return errorCode
end
local numRemovedElements = rcall("LREM", KEYS[1], -1, ARGV[2])
if numRemovedElements < 1 then return -3 end
local target = getTargetQueueList(KEYS[4], KEYS[2], KEYS[5])
local priority = tonumber(rcall("HGET", KEYS[3], "priority")) or 0
if priority == 0 then
-- LIFO or FIFO
rcall(ARGV[1], target, ARGV[2])
else
addJobWithPriority(KEYS[7], priority, ARGV[2], target)
end
return 0
else
return -1
end
`;
module.exports = {
name: 'retryJob',
content,
keys: 7,
};
+63
View File
@@ -0,0 +1,63 @@
'use strict';
const content = `--[[
Attempts to retry all failed jobs
Input:
KEYS[1] base key
KEYS[2] failed state key
KEYS[3] wait state key
KEYS[4] 'meta-paused'
KEYS[5] 'paused'
ARGV[1] count
Output:
1 means the operation is not completed
0 means the operation is completed
]]
local baseKey = KEYS[1]
local maxCount = tonumber(ARGV[1])
local rcall = redis.call;
-- Includes
--[[
Function to loop in batches.
Just a bit of warning, some commands as ZREM
could receive a maximum of 7000 parameters per call.
]]
local function batches(n, batchSize)
local i = 0
return function()
local from = i * batchSize + 1
i = i + 1
if (from <= n) then
local to = math.min(from + batchSize - 1, n)
return from, to
end
end
end
local function getZSetItems(keyName, max)
return rcall('ZRANGE', keyName, 0, max - 1)
end
local jobs = getZSetItems(KEYS[2], maxCount)
if (#jobs > 0) then
for i, key in ipairs(jobs) do
local jobKey = baseKey .. key
rcall("HDEL", jobKey, "finishedOn", "processedOn", "failedReason")
end
local target
if rcall("EXISTS", KEYS[4]) ~= 1 then
target = KEYS[3]
else
target = KEYS[5]
end
for from, to in batches(#jobs, 7000) do
rcall("ZREM", KEYS[2], unpack(jobs, from, to))
rcall("LPUSH", target, unpack(jobs, from, to))
end
end
maxCount = maxCount - #jobs
if (maxCount <= 0) then return 1 end
return 0
`;
module.exports = {
name: 'retryJobs',
content,
keys: 5,
};
+26
View File
@@ -0,0 +1,26 @@
'use strict';
const content = `--[[
Save stacktrace and failedReason.
Input:
KEYS[1] job key
ARGV[1] stacktrace
ARGV[2] failedReason
ARGV[3] attemptsMade
Output:
0 - OK
-1 - Missing key
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then
rcall("HMSET", KEYS[1], "stacktrace", ARGV[1], "failedReason", ARGV[2],
"attemptsMade", ARGV[3])
return 0
else
return -1
end
`;
module.exports = {
name: 'saveStacktrace',
content,
keys: 1,
};
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const content = `--[[
Takes a lock
Input:
KEYS[1] 'lock',
ARGV[1] token
ARGV[2] lock duration in milliseconds
Output:
"OK" if lock taken successfully.
]]
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
`;
module.exports = {
name: 'takeLock',
content,
keys: 1,
};
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const content = `--[[
Update job data
Input:
KEYS[1] Job id key
ARGV[1] data
Output:
0 - OK
-1 - Missing job.
]]
local rcall = redis.call
if rcall("EXISTS",KEYS[1]) == 1 then -- // Make sure job exists
rcall("HSET", KEYS[1], "data", ARGV[1])
return 0
else
return -1
end
`;
module.exports = {
name: 'updateData',
content,
keys: 1,
};
+76
View File
@@ -0,0 +1,76 @@
'use strict';
const content = `--[[
Updates the delay set, by picking a delayed job that should
be processed now.
Input:
KEYS[1] 'delayed'
KEYS[2] 'active'
KEYS[3] 'wait'
KEYS[4] 'priority'
KEYS[5] 'paused'
KEYS[6] 'meta-paused'
ARGV[1] queue.toKey('')
ARGV[2] delayed timestamp
ARGV[3] queue token
Events:
'removed'
]]
local rcall = redis.call;
-- Includes
--[[
Function to add job considering priority.
]]
local function addJobWithPriority(priorityKey, priority, jobId, targetKey)
rcall("ZADD", priorityKey, priority, jobId)
local count = rcall("ZCOUNT", priorityKey, 0, priority)
local len = rcall("LLEN", targetKey)
local id = rcall("LINDEX", targetKey, len - (count - 1))
if id then
rcall("LINSERT", targetKey, "BEFORE", id, jobId)
else
rcall("RPUSH", targetKey, jobId)
end
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, waitKey, pausedKey)
if rcall("EXISTS", queueMetaKey) ~= 1 then
return waitKey, false
else
return pausedKey, true
end
end
-- Try to get as much as 1000 jobs at once
local jobs = rcall("ZRANGEBYSCORE", KEYS[1], 0, tonumber(ARGV[2]) * 0x1000, "LIMIT", 0, 1000)
if(#jobs > 0) then
rcall("ZREM", KEYS[1], unpack(jobs))
-- check if we need to use push in paused instead of waiting
local target = getTargetQueueList(KEYS[6], KEYS[3], KEYS[5])
for _, jobId in ipairs(jobs) do
-- Is this really needed?
rcall("LREM", KEYS[2], 0, jobId)
local priority = tonumber(rcall("HGET", ARGV[1] .. jobId, "priority")) or 0
if priority == 0 then
-- LIFO or FIFO
rcall("LPUSH", target, jobId)
else
addJobWithPriority(KEYS[4], priority, jobId, target)
end
-- Emit waiting event (wait..ing@token)
rcall("PUBLISH", KEYS[3] .. "ing@" .. ARGV[3], jobId)
rcall("HSET", ARGV[1] .. jobId, "delay", 0)
end
end
local nextTimestamp = rcall("ZRANGE", KEYS[1], 0, 0, "WITHSCORES")[2]
if(nextTimestamp ~= nil) then
rcall("PUBLISH", KEYS[1], nextTimestamp / 0x1000)
end
return nextTimestamp
`;
module.exports = {
name: 'updateDelaySet',
content,
keys: 6,
};
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const content = `--[[
Update job progress
Input:
KEYS[1] Job id key
KEYS[2] progress event key
ARGV[1] progress
ARGV[2] event data
Event:
progress(jobId, progress)
]]
local rcall = redis.call
if rcall("EXISTS", KEYS[1]) == 1 then -- // Make sure job exists
rcall("HSET", KEYS[1], "progress", ARGV[1])
rcall("PUBLISH", KEYS[2], ARGV[2])
return 0
else
return -1
end
`;
module.exports = {
name: 'updateProgress',
content,
keys: 2,
};
+142
View File
@@ -0,0 +1,142 @@
'use strict';
const _ = require('lodash');
const uuid = require('uuid');
/**
Timer Manager
Keep track of timers to ensure that disconnect() is
only called (via close()) at a time when it's safe
to do so.
Queues currently use two timers:
- The first one is used for delayed jobs and is
preemptible i.e. it is possible to close a queue
while delayed jobs are still pending (they will
be processed when the queue is resumed). This timer
is cleared by close() and is not managed here.
- The second one is used to lock Redis while
processing jobs. These timers are short-lived,
and there can be more than one active at a
time.
The lock timer executes Redis commands, which
means we can't close queues while it's active i.e.
this won't work:
queue.process(function (job, jobDone) {
handle(job);
queue.disconnect().then(jobDone);
})
The disconnect() call closes the Redis connections; then, when
a queue tries to perform the scheduled Redis commands,
they block until a Redis connection becomes available...
The solution is to close the Redis connections when there are no
active timers i.e. when the queue is idle. This helper class keeps
track of the active timers and executes any queued listeners
whenever that count goes to zero.
Since disconnect() simply can't work if there are active handles,
its close() wrapper postpones closing the Redis connections
until the next idle state. This means that close() can safely
be called from anywhere at any time, even from within a job
handler:
queue.process(function (job, jobDone) {
handle(job);
queue.close();
jobDone();
})
*/
function TimerManager() {
this.idle = true;
this.listeners = [];
this.timers = {};
}
/**
Create a new timer (setTimeout).
Expired timers are automatically cleared
@param {String} name - Name of a timer key. Used only for debugging.
@param {Number} delay - delay of timeout
@param {Function} fn - Function to execute after delay
@returns {Number} id - The timer id. Used to clear the timer
*/
TimerManager.prototype.set = function(name, delay, fn) {
const id = uuid.v4();
const timer = setTimeout(
(timerInstance, timeoutId) => {
timerInstance.clear(timeoutId);
try {
fn();
} catch (err) {
console.error(err);
}
},
delay,
this,
id
);
// XXX only the timer is used, but the
// other fields are useful for
// troubleshooting/debugging
this.timers[id] = {
name,
timer
};
this.idle = false;
return id;
};
/**
Clear a timer (clearTimeout).
Queued listeners are executed if there are no
remaining timers
*/
TimerManager.prototype.clear = function(id) {
const timers = this.timers;
const timer = timers[id];
if (!timer) {
return;
}
clearTimeout(timer.timer);
delete timers[id];
if (!this.idle && _.size(timers) === 0) {
while (this.listeners.length) {
this.listeners.pop()();
}
this.idle = true;
}
};
TimerManager.prototype.clearAll = function() {
_.each(this.timers, (timer, id) => {
this.clear(id);
});
};
/**
* Returns a promise that resolves when there are no active timers.
*/
TimerManager.prototype.whenIdle = function() {
return new Promise(resolve => {
if (this.idle) {
resolve();
} else {
this.listeners.unshift(resolve);
}
});
};
module.exports = TimerManager;
+70
View File
@@ -0,0 +1,70 @@
'use strict';
const errorObject = { value: null };
function tryCatch(fn, ctx, args) {
try {
return fn.apply(ctx, args);
} catch (e) {
errorObject.value = e;
return errorObject;
}
}
/**
* Waits for a redis client to be ready.
* @param {Redis} redis client
*/
function isRedisReady(client) {
return new Promise((resolve, reject) => {
if (client.status === 'ready') {
resolve();
} else {
function handleReady() {
client.removeListener('end', handleEnd);
client.removeListener('error', handleError);
resolve();
}
let lastError;
function handleError(err) {
lastError = err;
}
function handleEnd() {
client.removeListener('ready', handleReady);
client.removeListener('error', handleError);
reject(lastError);
}
client.once('ready', handleReady);
client.on('error', handleError);
client.once('end', handleEnd);
}
});
}
module.exports.errorObject = errorObject;
module.exports.tryCatch = tryCatch;
module.exports.isRedisReady = isRedisReady;
module.exports.emitSafe = function(emitter, event, ...args) {
try {
return emitter.emit(event, ...args);
} catch (err) {
try {
return emitter.emit('error', err);
} catch (err) {
// We give up if the error event also throws an exception.
console.error(err);
}
}
};
module.exports.MetricsTime = {
ONE_MINUTE: 1,
FIVE_MINUTES: 5,
FIFTEEN_MINUTES: 15,
THIRTY_MINUTES: 30,
ONE_HOUR: 60,
ONE_WEEK: 60 * 24 * 7,
TWO_WEEKS: 60 * 24 * 7 * 2,
ONE_MONTH: 60 * 24 * 7 * 2 * 4
};
+68
View File
@@ -0,0 +1,68 @@
'use strict';
const utils = require('./utils');
const clientCommandMessageReg = /ERR unknown command ['`]\s*client\s*['`]/;
module.exports = function(Queue) {
// IDEA, How to store metadata associated to a worker.
// create a key from the worker ID associated to the given name.
// We keep a hash table bull:myqueue:workers where every worker is a hash key workername:workerId with json holding
// metadata of the worker. The worker key gets expired every 30 seconds or so, we renew the worker metadata.
//
Queue.prototype.setWorkerName = function() {
return utils
.isRedisReady(this.client)
.then(() => {
const connectionName = this.clientName();
this.bclient.options.connectionName = connectionName;
return this.bclient.client('setname', connectionName);
})
.catch(err => {
if (!clientCommandMessageReg.test(err.message)) throw err;
});
};
Queue.prototype.getWorkers = function() {
return utils
.isRedisReady(this.client)
.then(() => {
return this.client.client('list');
})
.then(clients => {
return this.parseClientList(clients);
})
.catch(err => {
if (!clientCommandMessageReg.test(err.message)) throw err;
});
};
Queue.prototype.base64Name = function() {
return Buffer.from(this.name).toString('base64');
};
Queue.prototype.clientName = function() {
return this.keyPrefix + ':' + this.base64Name();
};
Queue.prototype.parseClientList = function(list) {
const lines = list.split('\n');
const clients = [];
lines.forEach(line => {
const client = {};
const keyValues = line.split(' ');
keyValues.forEach(keyValue => {
const index = keyValue.indexOf('=');
const key = keyValue.substring(0, index);
const value = keyValue.substring(index + 1);
client[key] = value;
});
const name = client['name'];
if (name && name.startsWith(this.clientName())) {
client['name'] = this.name;
clients.push(client);
}
});
return clients;
};
};
+1
View File
@@ -0,0 +1 @@
../uuid/dist/bin/uuid
+229
View File
@@ -0,0 +1,229 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [8.3.2](https://github.com/uuidjs/uuid/compare/v8.3.1...v8.3.2) (2020-12-08)
### Bug Fixes
- lazy load getRandomValues ([#537](https://github.com/uuidjs/uuid/issues/537)) ([16c8f6d](https://github.com/uuidjs/uuid/commit/16c8f6df2f6b09b4d6235602d6a591188320a82e)), closes [#536](https://github.com/uuidjs/uuid/issues/536)
### [8.3.1](https://github.com/uuidjs/uuid/compare/v8.3.0...v8.3.1) (2020-10-04)
### Bug Fixes
- support expo>=39.0.0 ([#515](https://github.com/uuidjs/uuid/issues/515)) ([c65a0f3](https://github.com/uuidjs/uuid/commit/c65a0f3fa73b901959d638d1e3591dfacdbed867)), closes [#375](https://github.com/uuidjs/uuid/issues/375)
## [8.3.0](https://github.com/uuidjs/uuid/compare/v8.2.0...v8.3.0) (2020-07-27)
### Features
- add parse/stringify/validate/version/NIL APIs ([#479](https://github.com/uuidjs/uuid/issues/479)) ([0e6c10b](https://github.com/uuidjs/uuid/commit/0e6c10ba1bf9517796ff23c052fc0468eedfd5f4)), closes [#475](https://github.com/uuidjs/uuid/issues/475) [#478](https://github.com/uuidjs/uuid/issues/478) [#480](https://github.com/uuidjs/uuid/issues/480) [#481](https://github.com/uuidjs/uuid/issues/481) [#180](https://github.com/uuidjs/uuid/issues/180)
## [8.2.0](https://github.com/uuidjs/uuid/compare/v8.1.0...v8.2.0) (2020-06-23)
### Features
- improve performance of v1 string representation ([#453](https://github.com/uuidjs/uuid/issues/453)) ([0ee0b67](https://github.com/uuidjs/uuid/commit/0ee0b67c37846529c66089880414d29f3ae132d5))
- remove deprecated v4 string parameter ([#454](https://github.com/uuidjs/uuid/issues/454)) ([88ce3ca](https://github.com/uuidjs/uuid/commit/88ce3ca0ba046f60856de62c7ce03f7ba98ba46c)), closes [#437](https://github.com/uuidjs/uuid/issues/437)
- support jspm ([#473](https://github.com/uuidjs/uuid/issues/473)) ([e9f2587](https://github.com/uuidjs/uuid/commit/e9f2587a92575cac31bc1d4ae944e17c09756659))
### Bug Fixes
- prepare package exports for webpack 5 ([#468](https://github.com/uuidjs/uuid/issues/468)) ([8d6e6a5](https://github.com/uuidjs/uuid/commit/8d6e6a5f8965ca9575eb4d92e99a43435f4a58a8))
## [8.1.0](https://github.com/uuidjs/uuid/compare/v8.0.0...v8.1.0) (2020-05-20)
### Features
- improve v4 performance by reusing random number array ([#435](https://github.com/uuidjs/uuid/issues/435)) ([bf4af0d](https://github.com/uuidjs/uuid/commit/bf4af0d711b4d2ed03d1f74fd12ad0baa87dc79d))
- optimize V8 performance of bytesToUuid ([#434](https://github.com/uuidjs/uuid/issues/434)) ([e156415](https://github.com/uuidjs/uuid/commit/e156415448ec1af2351fa0b6660cfb22581971f2))
### Bug Fixes
- export package.json required by react-native and bundlers ([#449](https://github.com/uuidjs/uuid/issues/449)) ([be1c8fe](https://github.com/uuidjs/uuid/commit/be1c8fe9a3206c358e0059b52fafd7213aa48a52)), closes [ai/nanoevents#44](https://github.com/ai/nanoevents/issues/44#issuecomment-602010343) [#444](https://github.com/uuidjs/uuid/issues/444)
## [8.0.0](https://github.com/uuidjs/uuid/compare/v7.0.3...v8.0.0) (2020-04-29)
### ⚠ BREAKING CHANGES
- For native ECMAScript Module (ESM) usage in Node.js only named exports are exposed, there is no more default export.
```diff
-import uuid from 'uuid';
-console.log(uuid.v4()); // -> 'cd6c3b08-0adc-4f4b-a6ef-36087a1c9869'
+import { v4 as uuidv4 } from 'uuid';
+uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
```
- Deep requiring specific algorithms of this library like `require('uuid/v4')`, which has been deprecated in `uuid@7`, is no longer supported.
Instead use the named exports that this module exports.
For ECMAScript Modules (ESM):
```diff
-import uuidv4 from 'uuid/v4';
+import { v4 as uuidv4 } from 'uuid';
uuidv4();
```
For CommonJS:
```diff
-const uuidv4 = require('uuid/v4');
+const { v4: uuidv4 } = require('uuid');
uuidv4();
```
### Features
- native Node.js ES Modules (wrapper approach) ([#423](https://github.com/uuidjs/uuid/issues/423)) ([2d9f590](https://github.com/uuidjs/uuid/commit/2d9f590ad9701d692625c07ed62f0a0f91227991)), closes [#245](https://github.com/uuidjs/uuid/issues/245) [#419](https://github.com/uuidjs/uuid/issues/419) [#342](https://github.com/uuidjs/uuid/issues/342)
- remove deep requires ([#426](https://github.com/uuidjs/uuid/issues/426)) ([daf72b8](https://github.com/uuidjs/uuid/commit/daf72b84ceb20272a81bb5fbddb05dd95922cbba))
### Bug Fixes
- add CommonJS syntax example to README quickstart section ([#417](https://github.com/uuidjs/uuid/issues/417)) ([e0ec840](https://github.com/uuidjs/uuid/commit/e0ec8402c7ad44b7ef0453036c612f5db513fda0))
### [7.0.3](https://github.com/uuidjs/uuid/compare/v7.0.2...v7.0.3) (2020-03-31)
### Bug Fixes
- make deep require deprecation warning work in browsers ([#409](https://github.com/uuidjs/uuid/issues/409)) ([4b71107](https://github.com/uuidjs/uuid/commit/4b71107d8c0d2ef56861ede6403fc9dc35a1e6bf)), closes [#408](https://github.com/uuidjs/uuid/issues/408)
### [7.0.2](https://github.com/uuidjs/uuid/compare/v7.0.1...v7.0.2) (2020-03-04)
### Bug Fixes
- make access to msCrypto consistent ([#393](https://github.com/uuidjs/uuid/issues/393)) ([8bf2a20](https://github.com/uuidjs/uuid/commit/8bf2a20f3565df743da7215eebdbada9d2df118c))
- simplify link in deprecation warning ([#391](https://github.com/uuidjs/uuid/issues/391)) ([bb2c8e4](https://github.com/uuidjs/uuid/commit/bb2c8e4e9f4c5f9c1eaaf3ea59710c633cd90cb7))
- update links to match content in readme ([#386](https://github.com/uuidjs/uuid/issues/386)) ([44f2f86](https://github.com/uuidjs/uuid/commit/44f2f86e9d2bbf14ee5f0f00f72a3db1292666d4))
### [7.0.1](https://github.com/uuidjs/uuid/compare/v7.0.0...v7.0.1) (2020-02-25)
### Bug Fixes
- clean up esm builds for node and browser ([#383](https://github.com/uuidjs/uuid/issues/383)) ([59e6a49](https://github.com/uuidjs/uuid/commit/59e6a49e7ce7b3e8fb0f3ee52b9daae72af467dc))
- provide browser versions independent from module system ([#380](https://github.com/uuidjs/uuid/issues/380)) ([4344a22](https://github.com/uuidjs/uuid/commit/4344a22e7aed33be8627eeaaf05360f256a21753)), closes [#378](https://github.com/uuidjs/uuid/issues/378)
## [7.0.0](https://github.com/uuidjs/uuid/compare/v3.4.0...v7.0.0) (2020-02-24)
### ⚠ BREAKING CHANGES
- The default export, which used to be the v4() method but which was already discouraged in v3.x of this library, has been removed.
- Explicitly note that deep imports of the different uuid version functions are deprecated and no longer encouraged and that ECMAScript module named imports should be used instead. Emit a deprecation warning for people who deep-require the different algorithm variants.
- Remove builtin support for insecure random number generators in the browser. Users who want that will have to supply their own random number generator function.
- Remove support for generating v3 and v5 UUIDs in Node.js<4.x
- Convert code base to ECMAScript Modules (ESM) and release CommonJS build for node and ESM build for browser bundlers.
### Features
- add UMD build to npm package ([#357](https://github.com/uuidjs/uuid/issues/357)) ([4e75adf](https://github.com/uuidjs/uuid/commit/4e75adf435196f28e3fbbe0185d654b5ded7ca2c)), closes [#345](https://github.com/uuidjs/uuid/issues/345)
- add various es module and CommonJS examples ([b238510](https://github.com/uuidjs/uuid/commit/b238510bf352463521f74bab175a3af9b7a42555))
- ensure that docs are up-to-date in CI ([ee5e77d](https://github.com/uuidjs/uuid/commit/ee5e77db547474f5a8f23d6c857a6d399209986b))
- hybrid CommonJS & ECMAScript modules build ([a3f078f](https://github.com/uuidjs/uuid/commit/a3f078faa0baff69ab41aed08e041f8f9c8993d0))
- remove insecure fallback random number generator ([3a5842b](https://github.com/uuidjs/uuid/commit/3a5842b141a6e5de0ae338f391661e6b84b167c9)), closes [#173](https://github.com/uuidjs/uuid/issues/173)
- remove support for pre Node.js v4 Buffer API ([#356](https://github.com/uuidjs/uuid/issues/356)) ([b59b5c5](https://github.com/uuidjs/uuid/commit/b59b5c5ecad271c5453f1a156f011671f6d35627))
- rename repository to github:uuidjs/uuid ([#351](https://github.com/uuidjs/uuid/issues/351)) ([c37a518](https://github.com/uuidjs/uuid/commit/c37a518e367ac4b6d0aa62dba1bc6ce9e85020f7)), closes [#338](https://github.com/uuidjs/uuid/issues/338)
### Bug Fixes
- add deep-require proxies for local testing and adjust tests ([#365](https://github.com/uuidjs/uuid/issues/365)) ([7fedc79](https://github.com/uuidjs/uuid/commit/7fedc79ac8fda4bfd1c566c7f05ef4ac13b2db48))
- add note about removal of default export ([#372](https://github.com/uuidjs/uuid/issues/372)) ([12749b7](https://github.com/uuidjs/uuid/commit/12749b700eb49db8a9759fd306d8be05dbfbd58c)), closes [#370](https://github.com/uuidjs/uuid/issues/370)
- deprecated deep requiring of the different algorithm versions ([#361](https://github.com/uuidjs/uuid/issues/361)) ([c0bdf15](https://github.com/uuidjs/uuid/commit/c0bdf15e417639b1aeb0b247b2fb11f7a0a26b23))
## [3.4.0](https://github.com/uuidjs/uuid/compare/v3.3.3...v3.4.0) (2020-01-16)
### Features
- rename repository to github:uuidjs/uuid ([#351](https://github.com/uuidjs/uuid/issues/351)) ([e2d7314](https://github.com/uuidjs/uuid/commit/e2d7314)), closes [#338](https://github.com/uuidjs/uuid/issues/338)
## [3.3.3](https://github.com/uuidjs/uuid/compare/v3.3.2...v3.3.3) (2019-08-19)
### Bug Fixes
- no longer run ci tests on node v4
- upgrade dependencies
## [3.3.2](https://github.com/uuidjs/uuid/compare/v3.3.1...v3.3.2) (2018-06-28)
### Bug Fixes
- typo ([305d877](https://github.com/uuidjs/uuid/commit/305d877))
## [3.3.1](https://github.com/uuidjs/uuid/compare/v3.3.0...v3.3.1) (2018-06-28)
### Bug Fixes
- fix [#284](https://github.com/uuidjs/uuid/issues/284) by setting function name in try-catch ([f2a60f2](https://github.com/uuidjs/uuid/commit/f2a60f2))
# [3.3.0](https://github.com/uuidjs/uuid/compare/v3.2.1...v3.3.0) (2018-06-22)
### Bug Fixes
- assignment to readonly property to allow running in strict mode ([#270](https://github.com/uuidjs/uuid/issues/270)) ([d062fdc](https://github.com/uuidjs/uuid/commit/d062fdc))
- fix [#229](https://github.com/uuidjs/uuid/issues/229) ([c9684d4](https://github.com/uuidjs/uuid/commit/c9684d4))
- Get correct version of IE11 crypto ([#274](https://github.com/uuidjs/uuid/issues/274)) ([153d331](https://github.com/uuidjs/uuid/commit/153d331))
- mem issue when generating uuid ([#267](https://github.com/uuidjs/uuid/issues/267)) ([c47702c](https://github.com/uuidjs/uuid/commit/c47702c))
### Features
- enforce Conventional Commit style commit messages ([#282](https://github.com/uuidjs/uuid/issues/282)) ([cc9a182](https://github.com/uuidjs/uuid/commit/cc9a182))
## [3.2.1](https://github.com/uuidjs/uuid/compare/v3.2.0...v3.2.1) (2018-01-16)
### Bug Fixes
- use msCrypto if available. Fixes [#241](https://github.com/uuidjs/uuid/issues/241) ([#247](https://github.com/uuidjs/uuid/issues/247)) ([1fef18b](https://github.com/uuidjs/uuid/commit/1fef18b))
# [3.2.0](https://github.com/uuidjs/uuid/compare/v3.1.0...v3.2.0) (2018-01-16)
### Bug Fixes
- remove mistakenly added typescript dependency, rollback version (standard-version will auto-increment) ([09fa824](https://github.com/uuidjs/uuid/commit/09fa824))
- use msCrypto if available. Fixes [#241](https://github.com/uuidjs/uuid/issues/241) ([#247](https://github.com/uuidjs/uuid/issues/247)) ([1fef18b](https://github.com/uuidjs/uuid/commit/1fef18b))
### Features
- Add v3 Support ([#217](https://github.com/uuidjs/uuid/issues/217)) ([d94f726](https://github.com/uuidjs/uuid/commit/d94f726))
# [3.1.0](https://github.com/uuidjs/uuid/compare/v3.1.0...v3.0.1) (2017-06-17)
### Bug Fixes
- (fix) Add .npmignore file to exclude test/ and other non-essential files from packing. (#183)
- Fix typo (#178)
- Simple typo fix (#165)
### Features
- v5 support in CLI (#197)
- V5 support (#188)
# 3.0.1 (2016-11-28)
- split uuid versions into separate files
# 3.0.0 (2016-11-17)
- remove .parse and .unparse
# 2.0.0
- Removed uuid.BufferClass
# 1.4.0
- Improved module context detection
- Removed public RNG functions
# 1.3.2
- Improve tests and handling of v1() options (Issue #24)
- Expose RNG option to allow for perf testing with different generators
# 1.3.0
- Support for version 1 ids, thanks to [@ctavan](https://github.com/ctavan)!
- Support for node.js crypto API
- De-emphasizing performance in favor of a) cryptographic quality PRNGs where available and b) more manageable code
+18
View File
@@ -0,0 +1,18 @@
# Contributing
Please feel free to file GitHub Issues or propose Pull Requests. We're always happy to discuss improvements to this library!
## Testing
```shell
npm test
```
## Releasing
Releases are supposed to be done from master, version bumping is automated through [`standard-version`](https://github.com/conventional-changelog/standard-version):
```shell
npm run release -- --dry-run # verify output manually
npm run release # follow the instructions from the output of this command
```
+9
View File
@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+505
View File
@@ -0,0 +1,505 @@
<!--
-- This file is auto-generated from README_js.md. Changes should be made there.
-->
# uuid [![CI](https://github.com/uuidjs/uuid/workflows/CI/badge.svg)](https://github.com/uuidjs/uuid/actions?query=workflow%3ACI) [![Browser](https://github.com/uuidjs/uuid/workflows/Browser/badge.svg)](https://github.com/uuidjs/uuid/actions?query=workflow%3ABrowser)
For the creation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDs
- **Complete** - Support for RFC4122 version 1, 3, 4, and 5 UUIDs
- **Cross-platform** - Support for ...
- CommonJS, [ECMAScript Modules](#ecmascript-modules) and [CDN builds](#cdn-builds)
- Node 8, 10, 12, 14
- Chrome, Safari, Firefox, Edge, IE 11 browsers
- Webpack and rollup.js module bundlers
- [React Native / Expo](#react-native--expo)
- **Secure** - Cryptographically-strong random values
- **Small** - Zero-dependency, small footprint, plays nice with "tree shaking" packagers
- **CLI** - Includes the [`uuid` command line](#command-line) utility
**Upgrading from `uuid@3.x`?** Your code is probably okay, but check out [Upgrading From `uuid@3.x`](#upgrading-from-uuid3x) for details.
## Quickstart
To create a random UUID...
**1. Install**
```shell
npm install uuid
```
**2. Create a UUID** (ES6 module syntax)
```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
```
... or using CommonJS syntax:
```javascript
const { v4: uuidv4 } = require('uuid');
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
```
For timestamp UUIDs, namespace UUIDs, and other options read on ...
## API Summary
| | | |
| --- | --- | --- |
| [`uuid.NIL`](#uuidnil) | The nil UUID string (all zeros) | New in `uuid@8.3` |
| [`uuid.parse()`](#uuidparsestr) | Convert UUID string to array of bytes | New in `uuid@8.3` |
| [`uuid.stringify()`](#uuidstringifyarr-offset) | Convert array of bytes to UUID string | New in `uuid@8.3` |
| [`uuid.v1()`](#uuidv1options-buffer-offset) | Create a version 1 (timestamp) UUID | |
| [`uuid.v3()`](#uuidv3name-namespace-buffer-offset) | Create a version 3 (namespace w/ MD5) UUID | |
| [`uuid.v4()`](#uuidv4options-buffer-offset) | Create a version 4 (random) UUID | |
| [`uuid.v5()`](#uuidv5name-namespace-buffer-offset) | Create a version 5 (namespace w/ SHA-1) UUID | |
| [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` |
| [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` |
## API
### uuid.NIL
The nil UUID string (all zeros).
Example:
```javascript
import { NIL as NIL_UUID } from 'uuid';
NIL_UUID; // ⇨ '00000000-0000-0000-0000-000000000000'
```
### uuid.parse(str)
Convert UUID string to array of bytes
| | |
| --------- | ---------------------------------------- |
| `str` | A valid UUID `String` |
| _returns_ | `Uint8Array[16]` |
| _throws_ | `TypeError` if `str` is not a valid UUID |
Note: Ordering of values in the byte arrays used by `parse()` and `stringify()` follows the left &Rarr; right order of hex-pairs in UUID strings. As shown in the example below.
Example:
```javascript
import { parse as uuidParse } from 'uuid';
// Parse a UUID
const bytes = uuidParse('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b');
// Convert to hex strings to show byte order (for documentation purposes)
[...bytes].map((v) => v.toString(16).padStart(2, '0')); // ⇨
// [
// '6e', 'c0', 'bd', '7f',
// '11', 'c0', '43', 'da',
// '97', '5e', '2a', '8a',
// 'd9', 'eb', 'ae', '0b'
// ]
```
### uuid.stringify(arr[, offset])
Convert array of bytes to UUID string
| | |
| -------------- | ---------------------------------------------------------------------------- |
| `arr` | `Array`-like collection of 16 values (starting from `offset`) between 0-255. |
| [`offset` = 0] | `Number` Starting index in the Array |
| _returns_ | `String` |
| _throws_ | `TypeError` if a valid UUID string cannot be generated |
Note: Ordering of values in the byte arrays used by `parse()` and `stringify()` follows the left &Rarr; right order of hex-pairs in UUID strings. As shown in the example below.
Example:
```javascript
import { stringify as uuidStringify } from 'uuid';
const uuidBytes = [
0x6e,
0xc0,
0xbd,
0x7f,
0x11,
0xc0,
0x43,
0xda,
0x97,
0x5e,
0x2a,
0x8a,
0xd9,
0xeb,
0xae,
0x0b,
];
uuidStringify(uuidBytes); // ⇨ '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'
```
### uuid.v1([options[, buffer[, offset]]])
Create an RFC version 1 (timestamp) UUID
| | |
| --- | --- |
| [`options`] | `Object` with one or more of the following properties: |
| [`options.node` ] | RFC "node" field as an `Array[6]` of byte values (per 4.1.6) |
| [`options.clockseq`] | RFC "clock sequence" as a `Number` between 0 - 0x3fff |
| [`options.msecs`] | RFC "timestamp" field (`Number` of milliseconds, unix epoch) |
| [`options.nsecs`] | RFC "timestamp" field (`Number` of nanseconds to add to `msecs`, should be 0-10,000) |
| [`options.random`] | `Array` of 16 random bytes (0-255) |
| [`options.rng`] | Alternative to `options.random`, a `Function` that returns an `Array` of 16 random bytes (0-255) |
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
| _throws_ | `Error` if more than 10M UUIDs/sec are requested |
Note: The default [node id](https://tools.ietf.org/html/rfc4122#section-4.1.6) (the last 12 digits in the UUID) is generated once, randomly, on process startup, and then remains unchanged for the duration of the process.
Note: `options.random` and `options.rng` are only meaningful on the very first call to `v1()`, where they may be passed to initialize the internal `node` and `clockseq` fields.
Example:
```javascript
import { v1 as uuidv1 } from 'uuid';
uuidv1(); // ⇨ '2c5ea4c0-4067-11e9-8bad-9b1deb4d3b7d'
```
Example using `options`:
```javascript
import { v1 as uuidv1 } from 'uuid';
const v1options = {
node: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab],
clockseq: 0x1234,
msecs: new Date('2011-11-01').getTime(),
nsecs: 5678,
};
uuidv1(v1options); // ⇨ '710b962e-041c-11e1-9234-0123456789ab'
```
### uuid.v3(name, namespace[, buffer[, offset]])
Create an RFC version 3 (namespace w/ MD5) UUID
API is identical to `v5()`, but uses "v3" instead.
&#x26a0;&#xfe0f; Note: Per the RFC, "_If backward compatibility is not an issue, SHA-1 [Version 5] is preferred_."
### uuid.v4([options[, buffer[, offset]]])
Create an RFC version 4 (random) UUID
| | |
| --- | --- |
| [`options`] | `Object` with one or more of the following properties: |
| [`options.random`] | `Array` of 16 random bytes (0-255) |
| [`options.rng`] | Alternative to `options.random`, a `Function` that returns an `Array` of 16 random bytes (0-255) |
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
Example:
```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
```
Example using predefined `random` values:
```javascript
import { v4 as uuidv4 } from 'uuid';
const v4options = {
random: [
0x10,
0x91,
0x56,
0xbe,
0xc4,
0xfb,
0xc1,
0xea,
0x71,
0xb4,
0xef,
0xe1,
0x67,
0x1c,
0x58,
0x36,
],
};
uuidv4(v4options); // ⇨ '109156be-c4fb-41ea-b1b4-efe1671c5836'
```
### uuid.v5(name, namespace[, buffer[, offset]])
Create an RFC version 5 (namespace w/ SHA-1) UUID
| | |
| --- | --- |
| `name` | `String \| Array` |
| `namespace` | `String \| Array[16]` Namespace UUID |
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
Note: The RFC `DNS` and `URL` namespaces are available as `v5.DNS` and `v5.URL`.
Example with custom namespace:
```javascript
import { v5 as uuidv5 } from 'uuid';
// Define a custom namespace. Readers, create your own using something like
// https://www.uuidgenerator.net/
const MY_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341';
uuidv5('Hello, World!', MY_NAMESPACE); // ⇨ '630eb68f-e0fa-5ecc-887a-7c7a62614681'
```
Example with RFC `URL` namespace:
```javascript
import { v5 as uuidv5 } from 'uuid';
uuidv5('https://www.w3.org/', uuidv5.URL); // ⇨ 'c106a26a-21bb-5538-8bf2-57095d1976c1'
```
### uuid.validate(str)
Test a string to see if it is a valid UUID
| | |
| --------- | --------------------------------------------------- |
| `str` | `String` to validate |
| _returns_ | `true` if string is a valid UUID, `false` otherwise |
Example:
```javascript
import { validate as uuidValidate } from 'uuid';
uuidValidate('not a UUID'); // ⇨ false
uuidValidate('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'); // ⇨ true
```
Using `validate` and `version` together it is possible to do per-version validation, e.g. validate for only v4 UUIds.
```javascript
import { version as uuidVersion } from 'uuid';
import { validate as uuidValidate } from 'uuid';
function uuidValidateV4(uuid) {
return uuidValidate(uuid) && uuidVersion(uuid) === 4;
}
const v1Uuid = 'd9428888-122b-11e1-b85c-61cd3cbb3210';
const v4Uuid = '109156be-c4fb-41ea-b1b4-efe1671c5836';
uuidValidateV4(v4Uuid); // ⇨ true
uuidValidateV4(v1Uuid); // ⇨ false
```
### uuid.version(str)
Detect RFC version of a UUID
| | |
| --------- | ---------------------------------------- |
| `str` | A valid UUID `String` |
| _returns_ | `Number` The RFC version of the UUID |
| _throws_ | `TypeError` if `str` is not a valid UUID |
Example:
```javascript
import { version as uuidVersion } from 'uuid';
uuidVersion('45637ec4-c85f-11ea-87d0-0242ac130003'); // ⇨ 1
uuidVersion('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'); // ⇨ 4
```
## Command Line
UUIDs can be generated from the command line using `uuid`.
```shell
$ uuid
ddeb27fb-d9a0-4624-be4d-4615062daed4
```
The default is to generate version 4 UUIDS, however the other versions are supported. Type `uuid --help` for details:
```shell
$ uuid --help
Usage:
uuid
uuid v1
uuid v3 <name> <namespace uuid>
uuid v4
uuid v5 <name> <namespace uuid>
uuid --help
Note: <namespace uuid> may be "URL" or "DNS" to use the corresponding UUIDs
defined by RFC4122
```
## ECMAScript Modules
This library comes with [ECMAScript Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) support for Node.js versions that support it ([example](./examples/node-esmodules/)) as well as bundlers like [rollup.js](https://rollupjs.org/guide/en/#tree-shaking) ([example](./examples/browser-rollup/)) and [webpack](https://webpack.js.org/guides/tree-shaking/) ([example](./examples/browser-webpack/)) (targeting both, Node.js and browser environments).
```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
```
To run the examples you must first create a dist build of this library in the module root:
```shell
npm run build
```
## CDN Builds
### ECMAScript Modules
To load this module directly into modern browsers that [support loading ECMAScript Modules](https://caniuse.com/#feat=es6-module) you can make use of [jspm](https://jspm.org/):
```html
<script type="module">
import { v4 as uuidv4 } from 'https://jspm.dev/uuid';
console.log(uuidv4()); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
</script>
```
### UMD
To load this module directly into older browsers you can use the [UMD (Universal Module Definition)](https://github.com/umdjs/umd) builds from any of the following CDNs:
**Using [UNPKG](https://unpkg.com/uuid@latest/dist/umd/)**:
```html
<script src="https://unpkg.com/uuid@latest/dist/umd/uuidv4.min.js"></script>
```
**Using [jsDelivr](https://cdn.jsdelivr.net/npm/uuid@latest/dist/umd/)**:
```html
<script src="https://cdn.jsdelivr.net/npm/uuid@latest/dist/umd/uuidv4.min.js"></script>
```
**Using [cdnjs](https://cdnjs.com/libraries/uuid)**:
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.1.0/uuidv4.min.js"></script>
```
These CDNs all provide the same [`uuidv4()`](#uuidv4options-buffer-offset) method:
```html
<script>
uuidv4(); // ⇨ '55af1e37-0734-46d8-b070-a1e42e4fc392'
</script>
```
Methods for the other algorithms ([`uuidv1()`](#uuidv1options-buffer-offset), [`uuidv3()`](#uuidv3name-namespace-buffer-offset) and [`uuidv5()`](#uuidv5name-namespace-buffer-offset)) are available from the files `uuidv1.min.js`, `uuidv3.min.js` and `uuidv5.min.js` respectively.
## "getRandomValues() not supported"
This error occurs in environments where the standard [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) API is not supported. This issue can be resolved by adding an appropriate polyfill:
### React Native / Expo
1. Install [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values#readme)
1. Import it _before_ `uuid`. Since `uuid` might also appear as a transitive dependency of some other imports it's safest to just import `react-native-get-random-values` as the very first thing in your entry point:
```javascript
import 'react-native-get-random-values';
import { v4 as uuidv4 } from 'uuid';
```
Note: If you are using Expo, you must be using at least `react-native-get-random-values@1.5.0` and `expo@39.0.0`.
### Web Workers / Service Workers (Edge <= 18)
[In Edge <= 18, Web Crypto is not supported in Web Workers or Service Workers](https://caniuse.com/#feat=cryptography) and we are not aware of a polyfill (let us know if you find one, please).
## Upgrading From `uuid@7.x`
### Only Named Exports Supported When Using with Node.js ESM
`uuid@7.x` did not come with native ECMAScript Module (ESM) support for Node.js. Importing it in Node.js ESM consequently imported the CommonJS source with a default export. This library now comes with true Node.js ESM support and only provides named exports.
Instead of doing:
```javascript
import uuid from 'uuid';
uuid.v4();
```
you will now have to use the named exports:
```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4();
```
### Deep Requires No Longer Supported
Deep requires like `require('uuid/v4')` [which have been deprecated in `uuid@7.x`](#deep-requires-now-deprecated) are no longer supported.
## Upgrading From `uuid@3.x`
"_Wait... what happened to `uuid@4.x` - `uuid@6.x`?!?_"
In order to avoid confusion with RFC [version 4](#uuidv4options-buffer-offset) and [version 5](#uuidv5name-namespace-buffer-offset) UUIDs, and a possible [version 6](http://gh.peabody.io/uuidv6/), releases 4 thru 6 of this module have been skipped.
### Deep Requires Now Deprecated
`uuid@3.x` encouraged the use of deep requires to minimize the bundle size of browser builds:
```javascript
const uuidv4 = require('uuid/v4'); // <== NOW DEPRECATED!
uuidv4();
```
As of `uuid@7.x` this library now provides ECMAScript modules builds, which allow packagers like Webpack and Rollup to do "tree-shaking" to remove dead code. Instead, use the `import` syntax:
```javascript
import { v4 as uuidv4 } from 'uuid';
uuidv4();
```
... or for CommonJS:
```javascript
const { v4: uuidv4 } = require('uuid');
uuidv4();
```
### Default Export Removed
`uuid@3.x` was exporting the Version 4 UUID method as a default export:
```javascript
const uuid = require('uuid'); // <== REMOVED!
```
This usage pattern was already discouraged in `uuid@3.x` and has been removed in `uuid@7.x`.
----
Markdown generated from [README_js.md](README_js.md) by [![RunMD Logo](http://i.imgur.com/h0FVyzU.png)](https://github.com/broofa/runmd)
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../uuid-bin');
@@ -0,0 +1,9 @@
export { default as v1 } from './v1.js';
export { default as v3 } from './v3.js';
export { default as v4 } from './v4.js';
export { default as v5 } from './v5.js';
export { default as NIL } from './nil.js';
export { default as version } from './version.js';
export { default as validate } from './validate.js';
export { default as stringify } from './stringify.js';
export { default as parse } from './parse.js';
+215
View File
@@ -0,0 +1,215 @@
/*
* Browser-compatible JavaScript MD5
*
* Modification of JavaScript MD5
* https://github.com/blueimp/JavaScript-MD5
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*
* Based on
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
function md5(bytes) {
if (typeof bytes === 'string') {
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = new Uint8Array(msg.length);
for (var i = 0; i < msg.length; ++i) {
bytes[i] = msg.charCodeAt(i);
}
}
return md5ToHexEncodedArray(wordsToMd5(bytesToWords(bytes), bytes.length * 8));
}
/*
* Convert an array of little-endian words to an array of bytes
*/
function md5ToHexEncodedArray(input) {
var output = [];
var length32 = input.length * 32;
var hexTab = '0123456789abcdef';
for (var i = 0; i < length32; i += 8) {
var x = input[i >> 5] >>> i % 32 & 0xff;
var hex = parseInt(hexTab.charAt(x >>> 4 & 0x0f) + hexTab.charAt(x & 0x0f), 16);
output.push(hex);
}
return output;
}
/**
* Calculate output length with padding and bit length
*/
function getOutputLength(inputLength8) {
return (inputLength8 + 64 >>> 9 << 4) + 14 + 1;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function wordsToMd5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << len % 32;
x[getOutputLength(len) - 1] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936);
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5hh(d, a, b, c, x[i], 11, -358537222);
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844);
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, olda);
b = safeAdd(b, oldb);
c = safeAdd(c, oldc);
d = safeAdd(d, oldd);
}
return [a, b, c, d];
}
/*
* Convert an array bytes to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function bytesToWords(input) {
if (input.length === 0) {
return [];
}
var length8 = input.length * 8;
var output = new Uint32Array(getOutputLength(length8));
for (var i = 0; i < length8; i += 8) {
output[i >> 5] |= (input[i / 8] & 0xff) << i % 32;
}
return output;
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safeAdd(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return msw << 16 | lsw & 0xffff;
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bitRotateLeft(num, cnt) {
return num << cnt | num >>> 32 - cnt;
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5cmn(q, a, b, x, s, t) {
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
}
function md5ff(a, b, c, d, x, s, t) {
return md5cmn(b & c | ~b & d, a, b, x, s, t);
}
function md5gg(a, b, c, d, x, s, t) {
return md5cmn(b & d | c & ~d, a, b, x, s, t);
}
function md5hh(a, b, c, d, x, s, t) {
return md5cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5ii(a, b, c, d, x, s, t) {
return md5cmn(c ^ (b | ~d), a, b, x, s, t);
}
export default md5;
+1
View File
@@ -0,0 +1 @@
export default '00000000-0000-0000-0000-000000000000';
+35
View File
@@ -0,0 +1,35 @@
import validate from './validate.js';
function parse(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
var v;
var arr = new Uint8Array(16); // Parse ########-....-....-....-............
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
arr[1] = v >>> 16 & 0xff;
arr[2] = v >>> 8 & 0xff;
arr[3] = v & 0xff; // Parse ........-####-....-....-............
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
arr[5] = v & 0xff; // Parse ........-....-####-....-............
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
arr[7] = v & 0xff; // Parse ........-....-....-####-............
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
arr[9] = v & 0xff; // Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff;
arr[11] = v / 0x100000000 & 0xff;
arr[12] = v >>> 24 & 0xff;
arr[13] = v >>> 16 & 0xff;
arr[14] = v >>> 8 & 0xff;
arr[15] = v & 0xff;
return arr;
}
export default parse;
@@ -0,0 +1 @@
export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
+19
View File
@@ -0,0 +1,19 @@
// Unique ID creation requires a high quality random # generator. In the browser we therefore
// require the crypto API and do not support built-in fallback to lower quality random number
// generators (like Math.random()).
var getRandomValues;
var rnds8 = new Uint8Array(16);
export default function rng() {
// lazy load so that environments that need to polyfill have a chance to do so
if (!getRandomValues) {
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also,
// find the complete implementation of crypto (msCrypto) on IE11.
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto);
if (!getRandomValues) {
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
}
}
return getRandomValues(rnds8);
}
+96
View File
@@ -0,0 +1,96 @@
// Adapted from Chris Veness' SHA1 code at
// http://www.movable-type.co.uk/scripts/sha1.html
function f(s, x, y, z) {
switch (s) {
case 0:
return x & y ^ ~x & z;
case 1:
return x ^ y ^ z;
case 2:
return x & y ^ x & z ^ y & z;
case 3:
return x ^ y ^ z;
}
}
function ROTL(x, n) {
return x << n | x >>> 32 - n;
}
function sha1(bytes) {
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
var H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
if (typeof bytes === 'string') {
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = [];
for (var i = 0; i < msg.length; ++i) {
bytes.push(msg.charCodeAt(i));
}
} else if (!Array.isArray(bytes)) {
// Convert Array-like to Array
bytes = Array.prototype.slice.call(bytes);
}
bytes.push(0x80);
var l = bytes.length / 4 + 2;
var N = Math.ceil(l / 16);
var M = new Array(N);
for (var _i = 0; _i < N; ++_i) {
var arr = new Uint32Array(16);
for (var j = 0; j < 16; ++j) {
arr[j] = bytes[_i * 64 + j * 4] << 24 | bytes[_i * 64 + j * 4 + 1] << 16 | bytes[_i * 64 + j * 4 + 2] << 8 | bytes[_i * 64 + j * 4 + 3];
}
M[_i] = arr;
}
M[N - 1][14] = (bytes.length - 1) * 8 / Math.pow(2, 32);
M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = (bytes.length - 1) * 8 & 0xffffffff;
for (var _i2 = 0; _i2 < N; ++_i2) {
var W = new Uint32Array(80);
for (var t = 0; t < 16; ++t) {
W[t] = M[_i2][t];
}
for (var _t = 16; _t < 80; ++_t) {
W[_t] = ROTL(W[_t - 3] ^ W[_t - 8] ^ W[_t - 14] ^ W[_t - 16], 1);
}
var a = H[0];
var b = H[1];
var c = H[2];
var d = H[3];
var e = H[4];
for (var _t2 = 0; _t2 < 80; ++_t2) {
var s = Math.floor(_t2 / 20);
var T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[_t2] >>> 0;
e = d;
d = c;
c = ROTL(b, 30) >>> 0;
b = a;
a = T;
}
H[0] = H[0] + a >>> 0;
H[1] = H[1] + b >>> 0;
H[2] = H[2] + c >>> 0;
H[3] = H[3] + d >>> 0;
H[4] = H[4] + e >>> 0;
}
return [H[0] >> 24 & 0xff, H[0] >> 16 & 0xff, H[0] >> 8 & 0xff, H[0] & 0xff, H[1] >> 24 & 0xff, H[1] >> 16 & 0xff, H[1] >> 8 & 0xff, H[1] & 0xff, H[2] >> 24 & 0xff, H[2] >> 16 & 0xff, H[2] >> 8 & 0xff, H[2] & 0xff, H[3] >> 24 & 0xff, H[3] >> 16 & 0xff, H[3] >> 8 & 0xff, H[3] & 0xff, H[4] >> 24 & 0xff, H[4] >> 16 & 0xff, H[4] >> 8 & 0xff, H[4] & 0xff];
}
export default sha1;
@@ -0,0 +1,30 @@
import validate from './validate.js';
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
var byteToHex = [];
for (var i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).substr(1));
}
function stringify(arr) {
var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one
// of the following:
// - One or more input array values don't map to a hex octet (leading to
// "undefined" in the uuid)
// - Invalid input values for the RFC `version` or `variant` fields
if (!validate(uuid)) {
throw TypeError('Stringified UUID is invalid');
}
return uuid;
}
export default stringify;
+95
View File
@@ -0,0 +1,95 @@
import rng from './rng.js';
import stringify from './stringify.js'; // **`v1()` - Generate time-based UUID**
//
// Inspired by https://github.com/LiosK/UUID.js
// and http://docs.python.org/library/uuid.html
var _nodeId;
var _clockseq; // Previous uuid creation time
var _lastMSecs = 0;
var _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details
function v1(options, buf, offset) {
var i = buf && offset || 0;
var b = buf || new Array(16);
options = options || {};
var node = options.node || _nodeId;
var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not
// specified. We do this lazily to minimize issues related to insufficient
// system entropy. See #189
if (node == null || clockseq == null) {
var seedBytes = options.random || (options.rng || rng)();
if (node == null) {
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]];
}
if (clockseq == null) {
// Per 4.2.2, randomize (14 bit) clockseq
clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
}
} // UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
var msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs)
var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options.clockseq === undefined) {
clockseq = clockseq + 1 & 0x3fff;
} // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
nsecs = 0;
} // Per 4.2.1.2 Throw error if too many uuids are requested
if (nsecs >= 10000) {
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
}
_lastMSecs = msecs;
_lastNSecs = nsecs;
_clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000; // `time_low`
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
b[i++] = tl >>> 24 & 0xff;
b[i++] = tl >>> 16 & 0xff;
b[i++] = tl >>> 8 & 0xff;
b[i++] = tl & 0xff; // `time_mid`
var tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
b[i++] = tmh >>> 8 & 0xff;
b[i++] = tmh & 0xff; // `time_high_and_version`
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low`
b[i++] = clockseq & 0xff; // `node`
for (var n = 0; n < 6; ++n) {
b[i + n] = node[n];
}
return buf || stringify(b);
}
export default v1;
+4
View File
@@ -0,0 +1,4 @@
import v35 from './v35.js';
import md5 from './md5.js';
var v3 = v35('v3', 0x30, md5);
export default v3;
+64
View File
@@ -0,0 +1,64 @@
import stringify from './stringify.js';
import parse from './parse.js';
function stringToBytes(str) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
var bytes = [];
for (var i = 0; i < str.length; ++i) {
bytes.push(str.charCodeAt(i));
}
return bytes;
}
export var DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
export var URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
export default function (name, version, hashfunc) {
function generateUUID(value, namespace, buf, offset) {
if (typeof value === 'string') {
value = stringToBytes(value);
}
if (typeof namespace === 'string') {
namespace = parse(namespace);
}
if (namespace.length !== 16) {
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
} // Compute hash of namespace and value, Per 4.3
// Future: Use spread syntax when supported on all platforms, e.g. `bytes =
// hashfunc([...namespace, ... value])`
var bytes = new Uint8Array(16 + value.length);
bytes.set(namespace);
bytes.set(value, namespace.length);
bytes = hashfunc(bytes);
bytes[6] = bytes[6] & 0x0f | version;
bytes[8] = bytes[8] & 0x3f | 0x80;
if (buf) {
offset = offset || 0;
for (var i = 0; i < 16; ++i) {
buf[offset + i] = bytes[i];
}
return buf;
}
return stringify(bytes);
} // Function#name is not settable on some platforms (#270)
try {
generateUUID.name = name; // eslint-disable-next-line no-empty
} catch (err) {} // For CommonJS default export support
generateUUID.DNS = DNS;
generateUUID.URL = URL;
return generateUUID;
}
+24
View File
@@ -0,0 +1,24 @@
import rng from './rng.js';
import stringify from './stringify.js';
function v4(options, buf, offset) {
options = options || {};
var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = rnds[6] & 0x0f | 0x40;
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
if (buf) {
offset = offset || 0;
for (var i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return stringify(rnds);
}
export default v4;
+4
View File
@@ -0,0 +1,4 @@
import v35 from './v35.js';
import sha1 from './sha1.js';
var v5 = v35('v5', 0x50, sha1);
export default v5;
@@ -0,0 +1,7 @@
import REGEX from './regex.js';
function validate(uuid) {
return typeof uuid === 'string' && REGEX.test(uuid);
}
export default validate;
@@ -0,0 +1,11 @@
import validate from './validate.js';
function version(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
return parseInt(uuid.substr(14, 1), 16);
}
export default version;
+9
View File
@@ -0,0 +1,9 @@
export { default as v1 } from './v1.js';
export { default as v3 } from './v3.js';
export { default as v4 } from './v4.js';
export { default as v5 } from './v5.js';
export { default as NIL } from './nil.js';
export { default as version } from './version.js';
export { default as validate } from './validate.js';
export { default as stringify } from './stringify.js';
export { default as parse } from './parse.js';
+13
View File
@@ -0,0 +1,13 @@
import crypto from 'crypto';
function md5(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === 'string') {
bytes = Buffer.from(bytes, 'utf8');
}
return crypto.createHash('md5').update(bytes).digest();
}
export default md5;
+1
View File
@@ -0,0 +1 @@
export default '00000000-0000-0000-0000-000000000000';
+35
View File
@@ -0,0 +1,35 @@
import validate from './validate.js';
function parse(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
let v;
const arr = new Uint8Array(16); // Parse ########-....-....-....-............
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
arr[1] = v >>> 16 & 0xff;
arr[2] = v >>> 8 & 0xff;
arr[3] = v & 0xff; // Parse ........-####-....-....-............
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
arr[5] = v & 0xff; // Parse ........-....-####-....-............
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
arr[7] = v & 0xff; // Parse ........-....-....-####-............
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
arr[9] = v & 0xff; // Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff;
arr[11] = v / 0x100000000 & 0xff;
arr[12] = v >>> 24 & 0xff;
arr[13] = v >>> 16 & 0xff;
arr[14] = v >>> 8 & 0xff;
arr[15] = v & 0xff;
return arr;
}
export default parse;
+1
View File
@@ -0,0 +1 @@
export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
+12
View File
@@ -0,0 +1,12 @@
import crypto from 'crypto';
const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate
let poolPtr = rnds8Pool.length;
export default function rng() {
if (poolPtr > rnds8Pool.length - 16) {
crypto.randomFillSync(rnds8Pool);
poolPtr = 0;
}
return rnds8Pool.slice(poolPtr, poolPtr += 16);
}
+13
View File
@@ -0,0 +1,13 @@
import crypto from 'crypto';
function sha1(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === 'string') {
bytes = Buffer.from(bytes, 'utf8');
}
return crypto.createHash('sha1').update(bytes).digest();
}
export default sha1;
+29
View File
@@ -0,0 +1,29 @@
import validate from './validate.js';
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).substr(1));
}
function stringify(arr, offset = 0) {
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one
// of the following:
// - One or more input array values don't map to a hex octet (leading to
// "undefined" in the uuid)
// - Invalid input values for the RFC `version` or `variant` fields
if (!validate(uuid)) {
throw TypeError('Stringified UUID is invalid');
}
return uuid;
}
export default stringify;
+95
View File
@@ -0,0 +1,95 @@
import rng from './rng.js';
import stringify from './stringify.js'; // **`v1()` - Generate time-based UUID**
//
// Inspired by https://github.com/LiosK/UUID.js
// and http://docs.python.org/library/uuid.html
let _nodeId;
let _clockseq; // Previous uuid creation time
let _lastMSecs = 0;
let _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details
function v1(options, buf, offset) {
let i = buf && offset || 0;
const b = buf || new Array(16);
options = options || {};
let node = options.node || _nodeId;
let clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not
// specified. We do this lazily to minimize issues related to insufficient
// system entropy. See #189
if (node == null || clockseq == null) {
const seedBytes = options.random || (options.rng || rng)();
if (node == null) {
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]];
}
if (clockseq == null) {
// Per 4.2.2, randomize (14 bit) clockseq
clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
}
} // UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
let msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
let nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs)
const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options.clockseq === undefined) {
clockseq = clockseq + 1 & 0x3fff;
} // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
nsecs = 0;
} // Per 4.2.1.2 Throw error if too many uuids are requested
if (nsecs >= 10000) {
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
}
_lastMSecs = msecs;
_lastNSecs = nsecs;
_clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000; // `time_low`
const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
b[i++] = tl >>> 24 & 0xff;
b[i++] = tl >>> 16 & 0xff;
b[i++] = tl >>> 8 & 0xff;
b[i++] = tl & 0xff; // `time_mid`
const tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
b[i++] = tmh >>> 8 & 0xff;
b[i++] = tmh & 0xff; // `time_high_and_version`
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low`
b[i++] = clockseq & 0xff; // `node`
for (let n = 0; n < 6; ++n) {
b[i + n] = node[n];
}
return buf || stringify(b);
}
export default v1;
+4
View File
@@ -0,0 +1,4 @@
import v35 from './v35.js';
import md5 from './md5.js';
const v3 = v35('v3', 0x30, md5);
export default v3;
+64
View File
@@ -0,0 +1,64 @@
import stringify from './stringify.js';
import parse from './parse.js';
function stringToBytes(str) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
const bytes = [];
for (let i = 0; i < str.length; ++i) {
bytes.push(str.charCodeAt(i));
}
return bytes;
}
export const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
export const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
export default function (name, version, hashfunc) {
function generateUUID(value, namespace, buf, offset) {
if (typeof value === 'string') {
value = stringToBytes(value);
}
if (typeof namespace === 'string') {
namespace = parse(namespace);
}
if (namespace.length !== 16) {
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
} // Compute hash of namespace and value, Per 4.3
// Future: Use spread syntax when supported on all platforms, e.g. `bytes =
// hashfunc([...namespace, ... value])`
let bytes = new Uint8Array(16 + value.length);
bytes.set(namespace);
bytes.set(value, namespace.length);
bytes = hashfunc(bytes);
bytes[6] = bytes[6] & 0x0f | version;
bytes[8] = bytes[8] & 0x3f | 0x80;
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = bytes[i];
}
return buf;
}
return stringify(bytes);
} // Function#name is not settable on some platforms (#270)
try {
generateUUID.name = name; // eslint-disable-next-line no-empty
} catch (err) {} // For CommonJS default export support
generateUUID.DNS = DNS;
generateUUID.URL = URL;
return generateUUID;
}
+24
View File
@@ -0,0 +1,24 @@
import rng from './rng.js';
import stringify from './stringify.js';
function v4(options, buf, offset) {
options = options || {};
const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = rnds[6] & 0x0f | 0x40;
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return stringify(rnds);
}
export default v4;
+4
View File
@@ -0,0 +1,4 @@
import v35 from './v35.js';
import sha1 from './sha1.js';
const v5 = v35('v5', 0x50, sha1);
export default v5;
@@ -0,0 +1,7 @@
import REGEX from './regex.js';
function validate(uuid) {
return typeof uuid === 'string' && REGEX.test(uuid);
}
export default validate;
+11
View File
@@ -0,0 +1,11 @@
import validate from './validate.js';
function version(uuid) {
if (!validate(uuid)) {
throw TypeError('Invalid UUID');
}
return parseInt(uuid.substr(14, 1), 16);
}
export default version;
+79
View File
@@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "v1", {
enumerable: true,
get: function () {
return _v.default;
}
});
Object.defineProperty(exports, "v3", {
enumerable: true,
get: function () {
return _v2.default;
}
});
Object.defineProperty(exports, "v4", {
enumerable: true,
get: function () {
return _v3.default;
}
});
Object.defineProperty(exports, "v5", {
enumerable: true,
get: function () {
return _v4.default;
}
});
Object.defineProperty(exports, "NIL", {
enumerable: true,
get: function () {
return _nil.default;
}
});
Object.defineProperty(exports, "version", {
enumerable: true,
get: function () {
return _version.default;
}
});
Object.defineProperty(exports, "validate", {
enumerable: true,
get: function () {
return _validate.default;
}
});
Object.defineProperty(exports, "stringify", {
enumerable: true,
get: function () {
return _stringify.default;
}
});
Object.defineProperty(exports, "parse", {
enumerable: true,
get: function () {
return _parse.default;
}
});
var _v = _interopRequireDefault(require("./v1.js"));
var _v2 = _interopRequireDefault(require("./v3.js"));
var _v3 = _interopRequireDefault(require("./v4.js"));
var _v4 = _interopRequireDefault(require("./v5.js"));
var _nil = _interopRequireDefault(require("./nil.js"));
var _version = _interopRequireDefault(require("./version.js"));
var _validate = _interopRequireDefault(require("./validate.js"));
var _stringify = _interopRequireDefault(require("./stringify.js"));
var _parse = _interopRequireDefault(require("./parse.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+223
View File
@@ -0,0 +1,223 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
/*
* Browser-compatible JavaScript MD5
*
* Modification of JavaScript MD5
* https://github.com/blueimp/JavaScript-MD5
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*
* Based on
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
function md5(bytes) {
if (typeof bytes === 'string') {
const msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = new Uint8Array(msg.length);
for (let i = 0; i < msg.length; ++i) {
bytes[i] = msg.charCodeAt(i);
}
}
return md5ToHexEncodedArray(wordsToMd5(bytesToWords(bytes), bytes.length * 8));
}
/*
* Convert an array of little-endian words to an array of bytes
*/
function md5ToHexEncodedArray(input) {
const output = [];
const length32 = input.length * 32;
const hexTab = '0123456789abcdef';
for (let i = 0; i < length32; i += 8) {
const x = input[i >> 5] >>> i % 32 & 0xff;
const hex = parseInt(hexTab.charAt(x >>> 4 & 0x0f) + hexTab.charAt(x & 0x0f), 16);
output.push(hex);
}
return output;
}
/**
* Calculate output length with padding and bit length
*/
function getOutputLength(inputLength8) {
return (inputLength8 + 64 >>> 9 << 4) + 14 + 1;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function wordsToMd5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << len % 32;
x[getOutputLength(len) - 1] = len;
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (let i = 0; i < x.length; i += 16) {
const olda = a;
const oldb = b;
const oldc = c;
const oldd = d;
a = md5ff(a, b, c, d, x[i], 7, -680876936);
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5gg(b, c, d, a, x[i], 20, -373897302);
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5hh(d, a, b, c, x[i], 11, -358537222);
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5ii(a, b, c, d, x[i], 6, -198630844);
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safeAdd(a, olda);
b = safeAdd(b, oldb);
c = safeAdd(c, oldc);
d = safeAdd(d, oldd);
}
return [a, b, c, d];
}
/*
* Convert an array bytes to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function bytesToWords(input) {
if (input.length === 0) {
return [];
}
const length8 = input.length * 8;
const output = new Uint32Array(getOutputLength(length8));
for (let i = 0; i < length8; i += 8) {
output[i >> 5] |= (input[i / 8] & 0xff) << i % 32;
}
return output;
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safeAdd(x, y) {
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return msw << 16 | lsw & 0xffff;
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bitRotateLeft(num, cnt) {
return num << cnt | num >>> 32 - cnt;
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5cmn(q, a, b, x, s, t) {
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
}
function md5ff(a, b, c, d, x, s, t) {
return md5cmn(b & c | ~b & d, a, b, x, s, t);
}
function md5gg(a, b, c, d, x, s, t) {
return md5cmn(b & d | c & ~d, a, b, x, s, t);
}
function md5hh(a, b, c, d, x, s, t) {
return md5cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5ii(a, b, c, d, x, s, t) {
return md5cmn(c ^ (b | ~d), a, b, x, s, t);
}
var _default = md5;
exports.default = _default;
+23
View File
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _crypto = _interopRequireDefault(require("crypto"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function md5(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === 'string') {
bytes = Buffer.from(bytes, 'utf8');
}
return _crypto.default.createHash('md5').update(bytes).digest();
}
var _default = md5;
exports.default = _default;
+8
View File
@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = '00000000-0000-0000-0000-000000000000';
exports.default = _default;
+45
View File
@@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _validate = _interopRequireDefault(require("./validate.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function parse(uuid) {
if (!(0, _validate.default)(uuid)) {
throw TypeError('Invalid UUID');
}
let v;
const arr = new Uint8Array(16); // Parse ########-....-....-....-............
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
arr[1] = v >>> 16 & 0xff;
arr[2] = v >>> 8 & 0xff;
arr[3] = v & 0xff; // Parse ........-####-....-....-............
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
arr[5] = v & 0xff; // Parse ........-....-####-....-............
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
arr[7] = v & 0xff; // Parse ........-....-....-####-............
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
arr[9] = v & 0xff; // Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff;
arr[11] = v / 0x100000000 & 0xff;
arr[12] = v >>> 24 & 0xff;
arr[13] = v >>> 16 & 0xff;
arr[14] = v >>> 8 & 0xff;
arr[15] = v & 0xff;
return arr;
}
var _default = parse;
exports.default = _default;
+8
View File
@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
exports.default = _default;
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = rng;
// Unique ID creation requires a high quality random # generator. In the browser we therefore
// require the crypto API and do not support built-in fallback to lower quality random number
// generators (like Math.random()).
let getRandomValues;
const rnds8 = new Uint8Array(16);
function rng() {
// lazy load so that environments that need to polyfill have a chance to do so
if (!getRandomValues) {
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also,
// find the complete implementation of crypto (msCrypto) on IE11.
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto);
if (!getRandomValues) {
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
}
}
return getRandomValues(rnds8);
}
+24
View File
@@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = rng;
var _crypto = _interopRequireDefault(require("crypto"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate
let poolPtr = rnds8Pool.length;
function rng() {
if (poolPtr > rnds8Pool.length - 16) {
_crypto.default.randomFillSync(rnds8Pool);
poolPtr = 0;
}
return rnds8Pool.slice(poolPtr, poolPtr += 16);
}
+104
View File
@@ -0,0 +1,104 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
// Adapted from Chris Veness' SHA1 code at
// http://www.movable-type.co.uk/scripts/sha1.html
function f(s, x, y, z) {
switch (s) {
case 0:
return x & y ^ ~x & z;
case 1:
return x ^ y ^ z;
case 2:
return x & y ^ x & z ^ y & z;
case 3:
return x ^ y ^ z;
}
}
function ROTL(x, n) {
return x << n | x >>> 32 - n;
}
function sha1(bytes) {
const K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
const H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
if (typeof bytes === 'string') {
const msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
bytes = [];
for (let i = 0; i < msg.length; ++i) {
bytes.push(msg.charCodeAt(i));
}
} else if (!Array.isArray(bytes)) {
// Convert Array-like to Array
bytes = Array.prototype.slice.call(bytes);
}
bytes.push(0x80);
const l = bytes.length / 4 + 2;
const N = Math.ceil(l / 16);
const M = new Array(N);
for (let i = 0; i < N; ++i) {
const arr = new Uint32Array(16);
for (let j = 0; j < 16; ++j) {
arr[j] = bytes[i * 64 + j * 4] << 24 | bytes[i * 64 + j * 4 + 1] << 16 | bytes[i * 64 + j * 4 + 2] << 8 | bytes[i * 64 + j * 4 + 3];
}
M[i] = arr;
}
M[N - 1][14] = (bytes.length - 1) * 8 / Math.pow(2, 32);
M[N - 1][14] = Math.floor(M[N - 1][14]);
M[N - 1][15] = (bytes.length - 1) * 8 & 0xffffffff;
for (let i = 0; i < N; ++i) {
const W = new Uint32Array(80);
for (let t = 0; t < 16; ++t) {
W[t] = M[i][t];
}
for (let t = 16; t < 80; ++t) {
W[t] = ROTL(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1);
}
let a = H[0];
let b = H[1];
let c = H[2];
let d = H[3];
let e = H[4];
for (let t = 0; t < 80; ++t) {
const s = Math.floor(t / 20);
const T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t] >>> 0;
e = d;
d = c;
c = ROTL(b, 30) >>> 0;
b = a;
a = T;
}
H[0] = H[0] + a >>> 0;
H[1] = H[1] + b >>> 0;
H[2] = H[2] + c >>> 0;
H[3] = H[3] + d >>> 0;
H[4] = H[4] + e >>> 0;
}
return [H[0] >> 24 & 0xff, H[0] >> 16 & 0xff, H[0] >> 8 & 0xff, H[0] & 0xff, H[1] >> 24 & 0xff, H[1] >> 16 & 0xff, H[1] >> 8 & 0xff, H[1] & 0xff, H[2] >> 24 & 0xff, H[2] >> 16 & 0xff, H[2] >> 8 & 0xff, H[2] & 0xff, H[3] >> 24 & 0xff, H[3] >> 16 & 0xff, H[3] >> 8 & 0xff, H[3] & 0xff, H[4] >> 24 & 0xff, H[4] >> 16 & 0xff, H[4] >> 8 & 0xff, H[4] & 0xff];
}
var _default = sha1;
exports.default = _default;
+23
View File
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _crypto = _interopRequireDefault(require("crypto"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function sha1(bytes) {
if (Array.isArray(bytes)) {
bytes = Buffer.from(bytes);
} else if (typeof bytes === 'string') {
bytes = Buffer.from(bytes, 'utf8');
}
return _crypto.default.createHash('sha1').update(bytes).digest();
}
var _default = sha1;
exports.default = _default;
+39
View File
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _validate = _interopRequireDefault(require("./validate.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Convert array of 16 byte values to UUID string format of the form:
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
*/
const byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).substr(1));
}
function stringify(arr, offset = 0) {
// Note: Be careful editing this code! It's been tuned for performance
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one
// of the following:
// - One or more input array values don't map to a hex octet (leading to
// "undefined" in the uuid)
// - Invalid input values for the RFC `version` or `variant` fields
if (!(0, _validate.default)(uuid)) {
throw TypeError('Stringified UUID is invalid');
}
return uuid;
}
var _default = stringify;
exports.default = _default;
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidNIL=n()}(this,(function(){"use strict";return"00000000-0000-0000-0000-000000000000"}));
@@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidParse=n()}(this,(function(){"use strict";var e=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;return function(n){if(!function(n){return"string"==typeof n&&e.test(n)}(n))throw TypeError("Invalid UUID");var t,i=new Uint8Array(16);return i[0]=(t=parseInt(n.slice(0,8),16))>>>24,i[1]=t>>>16&255,i[2]=t>>>8&255,i[3]=255&t,i[4]=(t=parseInt(n.slice(9,13),16))>>>8,i[5]=255&t,i[6]=(t=parseInt(n.slice(14,18),16))>>>8,i[7]=255&t,i[8]=(t=parseInt(n.slice(19,23),16))>>>8,i[9]=255&t,i[10]=(t=parseInt(n.slice(24,36),16))/1099511627776&255,i[11]=t/4294967296&255,i[12]=t>>>24&255,i[13]=t>>>16&255,i[14]=t>>>8&255,i[15]=255&t,i}}));
@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidStringify=t()}(this,(function(){"use strict";var e=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function t(t){return"string"==typeof t&&e.test(t)}for(var i=[],n=0;n<256;++n)i.push((n+256).toString(16).substr(1));return function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,f=(i[e[n+0]]+i[e[n+1]]+i[e[n+2]]+i[e[n+3]]+"-"+i[e[n+4]]+i[e[n+5]]+"-"+i[e[n+6]]+i[e[n+7]]+"-"+i[e[n+8]]+i[e[n+9]]+"-"+i[e[n+10]]+i[e[n+11]]+i[e[n+12]]+i[e[n+13]]+i[e[n+14]]+i[e[n+15]]).toLowerCase();if(!t(f))throw TypeError("Stringified UUID is invalid");return f}}));
@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidValidate=t()}(this,(function(){"use strict";var e=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;return function(t){return"string"==typeof t&&e.test(t)}}));
@@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidVersion=t()}(this,(function(){"use strict";var e=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;return function(t){if(!function(t){return"string"==typeof t&&e.test(t)}(t))throw TypeError("Invalid UUID");return parseInt(t.substr(14,1),16)}}));
+1
View File
@@ -0,0 +1 @@
!function(e,o){"object"==typeof exports&&"undefined"!=typeof module?module.exports=o():"function"==typeof define&&define.amd?define(o):(e="undefined"!=typeof globalThis?globalThis:e||self).uuidv1=o()}(this,(function(){"use strict";var e,o=new Uint8Array(16);function t(){if(!e&&!(e="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return e(o)}var n=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function r(e){return"string"==typeof e&&n.test(e)}for(var i,u,s=[],a=0;a<256;++a)s.push((a+256).toString(16).substr(1));var d=0,f=0;return function(e,o,n){var a=o&&n||0,c=o||new Array(16),l=(e=e||{}).node||i,p=void 0!==e.clockseq?e.clockseq:u;if(null==l||null==p){var v=e.random||(e.rng||t)();null==l&&(l=i=[1|v[0],v[1],v[2],v[3],v[4],v[5]]),null==p&&(p=u=16383&(v[6]<<8|v[7]))}var y=void 0!==e.msecs?e.msecs:Date.now(),m=void 0!==e.nsecs?e.nsecs:f+1,g=y-d+(m-f)/1e4;if(g<0&&void 0===e.clockseq&&(p=p+1&16383),(g<0||y>d)&&void 0===e.nsecs&&(m=0),m>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");d=y,f=m,u=p;var h=(1e4*(268435455&(y+=122192928e5))+m)%4294967296;c[a++]=h>>>24&255,c[a++]=h>>>16&255,c[a++]=h>>>8&255,c[a++]=255&h;var w=y/4294967296*1e4&268435455;c[a++]=w>>>8&255,c[a++]=255&w,c[a++]=w>>>24&15|16,c[a++]=w>>>16&255,c[a++]=p>>>8|128,c[a++]=255&p;for(var b=0;b<6;++b)c[a+b]=l[b];return o||function(e){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,t=(s[e[o+0]]+s[e[o+1]]+s[e[o+2]]+s[e[o+3]]+"-"+s[e[o+4]]+s[e[o+5]]+"-"+s[e[o+6]]+s[e[o+7]]+"-"+s[e[o+8]]+s[e[o+9]]+"-"+s[e[o+10]]+s[e[o+11]]+s[e[o+12]]+s[e[o+13]]+s[e[o+14]]+s[e[o+15]]).toLowerCase();if(!r(t))throw TypeError("Stringified UUID is invalid");return t}(c)}}));
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).uuidv4=e()}(this,(function(){"use strict";var t,e=new Uint8Array(16);function o(){if(!t&&!(t="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto)))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return t(e)}var n=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function r(t){return"string"==typeof t&&n.test(t)}for(var i=[],u=0;u<256;++u)i.push((u+256).toString(16).substr(1));return function(t,e,n){var u=(t=t||{}).random||(t.rng||o)();if(u[6]=15&u[6]|64,u[8]=63&u[8]|128,e){n=n||0;for(var f=0;f<16;++f)e[n+f]=u[f];return e}return function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,o=(i[t[e+0]]+i[t[e+1]]+i[t[e+2]]+i[t[e+3]]+"-"+i[t[e+4]]+i[t[e+5]]+"-"+i[t[e+6]]+i[t[e+7]]+"-"+i[t[e+8]]+i[t[e+9]]+"-"+i[t[e+10]]+i[t[e+11]]+i[t[e+12]]+i[t[e+13]]+i[t[e+14]]+i[t[e+15]]).toLowerCase();if(!r(o))throw TypeError("Stringified UUID is invalid");return o}(u)}}));
+1
View File
@@ -0,0 +1 @@
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).uuidv5=e()}(this,(function(){"use strict";var r=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;function e(e){return"string"==typeof e&&r.test(e)}for(var t=[],n=0;n<256;++n)t.push((n+256).toString(16).substr(1));function a(r,e,t,n){switch(r){case 0:return e&t^~e&n;case 1:return e^t^n;case 2:return e&t^e&n^t&n;case 3:return e^t^n}}function o(r,e){return r<<e|r>>>32-e}return function(r,n,a){function o(r,o,i,f){if("string"==typeof r&&(r=function(r){r=unescape(encodeURIComponent(r));for(var e=[],t=0;t<r.length;++t)e.push(r.charCodeAt(t));return e}(r)),"string"==typeof o&&(o=function(r){if(!e(r))throw TypeError("Invalid UUID");var t,n=new Uint8Array(16);return n[0]=(t=parseInt(r.slice(0,8),16))>>>24,n[1]=t>>>16&255,n[2]=t>>>8&255,n[3]=255&t,n[4]=(t=parseInt(r.slice(9,13),16))>>>8,n[5]=255&t,n[6]=(t=parseInt(r.slice(14,18),16))>>>8,n[7]=255&t,n[8]=(t=parseInt(r.slice(19,23),16))>>>8,n[9]=255&t,n[10]=(t=parseInt(r.slice(24,36),16))/1099511627776&255,n[11]=t/4294967296&255,n[12]=t>>>24&255,n[13]=t>>>16&255,n[14]=t>>>8&255,n[15]=255&t,n}(o)),16!==o.length)throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");var s=new Uint8Array(16+r.length);if(s.set(o),s.set(r,o.length),(s=a(s))[6]=15&s[6]|n,s[8]=63&s[8]|128,i){f=f||0;for(var u=0;u<16;++u)i[f+u]=s[u];return i}return function(r){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,a=(t[r[n+0]]+t[r[n+1]]+t[r[n+2]]+t[r[n+3]]+"-"+t[r[n+4]]+t[r[n+5]]+"-"+t[r[n+6]]+t[r[n+7]]+"-"+t[r[n+8]]+t[r[n+9]]+"-"+t[r[n+10]]+t[r[n+11]]+t[r[n+12]]+t[r[n+13]]+t[r[n+14]]+t[r[n+15]]).toLowerCase();if(!e(a))throw TypeError("Stringified UUID is invalid");return a}(s)}try{o.name=r}catch(r){}return o.DNS="6ba7b810-9dad-11d1-80b4-00c04fd430c8",o.URL="6ba7b811-9dad-11d1-80b4-00c04fd430c8",o}("v5",80,(function(r){var e=[1518500249,1859775393,2400959708,3395469782],t=[1732584193,4023233417,2562383102,271733878,3285377520];if("string"==typeof r){var n=unescape(encodeURIComponent(r));r=[];for(var i=0;i<n.length;++i)r.push(n.charCodeAt(i))}else Array.isArray(r)||(r=Array.prototype.slice.call(r));r.push(128);for(var f=r.length/4+2,s=Math.ceil(f/16),u=new Array(s),c=0;c<s;++c){for(var l=new Uint32Array(16),p=0;p<16;++p)l[p]=r[64*c+4*p]<<24|r[64*c+4*p+1]<<16|r[64*c+4*p+2]<<8|r[64*c+4*p+3];u[c]=l}u[s-1][14]=8*(r.length-1)/Math.pow(2,32),u[s-1][14]=Math.floor(u[s-1][14]),u[s-1][15]=8*(r.length-1)&4294967295;for(var d=0;d<s;++d){for(var h=new Uint32Array(80),v=0;v<16;++v)h[v]=u[d][v];for(var y=16;y<80;++y)h[y]=o(h[y-3]^h[y-8]^h[y-14]^h[y-16],1);for(var g=t[0],b=t[1],w=t[2],U=t[3],A=t[4],I=0;I<80;++I){var m=Math.floor(I/20),C=o(g,5)+a(m,b,w,U)+A+e[m]+h[I]>>>0;A=U,U=w,w=o(b,30)>>>0,b=g,g=C}t[0]=t[0]+g>>>0,t[1]=t[1]+b>>>0,t[2]=t[2]+w>>>0,t[3]=t[3]+U>>>0,t[4]=t[4]+A>>>0}return[t[0]>>24&255,t[0]>>16&255,t[0]>>8&255,255&t[0],t[1]>>24&255,t[1]>>16&255,t[1]>>8&255,255&t[1],t[2]>>24&255,t[2]>>16&255,t[2]>>8&255,255&t[2],t[3]>>24&255,t[3]>>16&255,t[3]>>8&255,255&t[3],t[4]>>24&255,t[4]>>16&255,t[4]>>8&255,255&t[4]]}))}));
+85
View File
@@ -0,0 +1,85 @@
"use strict";
var _assert = _interopRequireDefault(require("assert"));
var _v = _interopRequireDefault(require("./v1.js"));
var _v2 = _interopRequireDefault(require("./v3.js"));
var _v3 = _interopRequireDefault(require("./v4.js"));
var _v4 = _interopRequireDefault(require("./v5.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function usage() {
console.log('Usage:');
console.log(' uuid');
console.log(' uuid v1');
console.log(' uuid v3 <name> <namespace uuid>');
console.log(' uuid v4');
console.log(' uuid v5 <name> <namespace uuid>');
console.log(' uuid --help');
console.log('\nNote: <namespace uuid> may be "URL" or "DNS" to use the corresponding UUIDs defined by RFC4122');
}
const args = process.argv.slice(2);
if (args.indexOf('--help') >= 0) {
usage();
process.exit(0);
}
const version = args.shift() || 'v4';
switch (version) {
case 'v1':
console.log((0, _v.default)());
break;
case 'v3':
{
const name = args.shift();
let namespace = args.shift();
(0, _assert.default)(name != null, 'v3 name not specified');
(0, _assert.default)(namespace != null, 'v3 namespace not specified');
if (namespace === 'URL') {
namespace = _v2.default.URL;
}
if (namespace === 'DNS') {
namespace = _v2.default.DNS;
}
console.log((0, _v2.default)(name, namespace));
break;
}
case 'v4':
console.log((0, _v3.default)());
break;
case 'v5':
{
const name = args.shift();
let namespace = args.shift();
(0, _assert.default)(name != null, 'v5 name not specified');
(0, _assert.default)(namespace != null, 'v5 namespace not specified');
if (namespace === 'URL') {
namespace = _v4.default.URL;
}
if (namespace === 'DNS') {
namespace = _v4.default.DNS;
}
console.log((0, _v4.default)(name, namespace));
break;
}
default:
usage();
process.exit(1);
}
+107
View File
@@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _rng = _interopRequireDefault(require("./rng.js"));
var _stringify = _interopRequireDefault(require("./stringify.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// **`v1()` - Generate time-based UUID**
//
// Inspired by https://github.com/LiosK/UUID.js
// and http://docs.python.org/library/uuid.html
let _nodeId;
let _clockseq; // Previous uuid creation time
let _lastMSecs = 0;
let _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details
function v1(options, buf, offset) {
let i = buf && offset || 0;
const b = buf || new Array(16);
options = options || {};
let node = options.node || _nodeId;
let clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not
// specified. We do this lazily to minimize issues related to insufficient
// system entropy. See #189
if (node == null || clockseq == null) {
const seedBytes = options.random || (options.rng || _rng.default)();
if (node == null) {
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]];
}
if (clockseq == null) {
// Per 4.2.2, randomize (14 bit) clockseq
clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
}
} // UUID timestamps are 100 nano-second units since the Gregorian epoch,
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
let msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock
// cycle to simulate higher resolution clock
let nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs)
const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression
if (dt < 0 && options.clockseq === undefined) {
clockseq = clockseq + 1 & 0x3fff;
} // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
// time interval
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
nsecs = 0;
} // Per 4.2.1.2 Throw error if too many uuids are requested
if (nsecs >= 10000) {
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
}
_lastMSecs = msecs;
_lastNSecs = nsecs;
_clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
msecs += 12219292800000; // `time_low`
const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
b[i++] = tl >>> 24 & 0xff;
b[i++] = tl >>> 16 & 0xff;
b[i++] = tl >>> 8 & 0xff;
b[i++] = tl & 0xff; // `time_mid`
const tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
b[i++] = tmh >>> 8 & 0xff;
b[i++] = tmh & 0xff; // `time_high_and_version`
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low`
b[i++] = clockseq & 0xff; // `node`
for (let n = 0; n < 6; ++n) {
b[i + n] = node[n];
}
return buf || (0, _stringify.default)(b);
}
var _default = v1;
exports.default = _default;
+16
View File
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _v = _interopRequireDefault(require("./v35.js"));
var _md = _interopRequireDefault(require("./md5.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const v3 = (0, _v.default)('v3', 0x30, _md.default);
var _default = v3;
exports.default = _default;
+78
View File
@@ -0,0 +1,78 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = _default;
exports.URL = exports.DNS = void 0;
var _stringify = _interopRequireDefault(require("./stringify.js"));
var _parse = _interopRequireDefault(require("./parse.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function stringToBytes(str) {
str = unescape(encodeURIComponent(str)); // UTF8 escape
const bytes = [];
for (let i = 0; i < str.length; ++i) {
bytes.push(str.charCodeAt(i));
}
return bytes;
}
const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
exports.DNS = DNS;
const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
exports.URL = URL;
function _default(name, version, hashfunc) {
function generateUUID(value, namespace, buf, offset) {
if (typeof value === 'string') {
value = stringToBytes(value);
}
if (typeof namespace === 'string') {
namespace = (0, _parse.default)(namespace);
}
if (namespace.length !== 16) {
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
} // Compute hash of namespace and value, Per 4.3
// Future: Use spread syntax when supported on all platforms, e.g. `bytes =
// hashfunc([...namespace, ... value])`
let bytes = new Uint8Array(16 + value.length);
bytes.set(namespace);
bytes.set(value, namespace.length);
bytes = hashfunc(bytes);
bytes[6] = bytes[6] & 0x0f | version;
bytes[8] = bytes[8] & 0x3f | 0x80;
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = bytes[i];
}
return buf;
}
return (0, _stringify.default)(bytes);
} // Function#name is not settable on some platforms (#270)
try {
generateUUID.name = name; // eslint-disable-next-line no-empty
} catch (err) {} // For CommonJS default export support
generateUUID.DNS = DNS;
generateUUID.URL = URL;
return generateUUID;
}
+37
View File
@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _rng = _interopRequireDefault(require("./rng.js"));
var _stringify = _interopRequireDefault(require("./stringify.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function v4(options, buf, offset) {
options = options || {};
const rnds = options.random || (options.rng || _rng.default)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
rnds[6] = rnds[6] & 0x0f | 0x40;
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
if (buf) {
offset = offset || 0;
for (let i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return (0, _stringify.default)(rnds);
}
var _default = v4;
exports.default = _default;
+16
View File
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _v = _interopRequireDefault(require("./v35.js"));
var _sha = _interopRequireDefault(require("./sha1.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const v5 = (0, _v.default)('v5', 0x50, _sha.default);
var _default = v5;
exports.default = _default;
+17
View File
@@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _regex = _interopRequireDefault(require("./regex.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function validate(uuid) {
return typeof uuid === 'string' && _regex.default.test(uuid);
}
var _default = validate;
exports.default = _default;
+21
View File
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _validate = _interopRequireDefault(require("./validate.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function version(uuid) {
if (!(0, _validate.default)(uuid)) {
throw TypeError('Invalid UUID');
}
return parseInt(uuid.substr(14, 1), 16);
}
var _default = version;
exports.default = _default;
+135
View File
@@ -0,0 +1,135 @@
{
"name": "uuid",
"version": "8.3.2",
"description": "RFC4122 (v1, v4, and v5) UUIDs",
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"keywords": [
"uuid",
"guid",
"rfc4122"
],
"license": "MIT",
"bin": {
"uuid": "./dist/bin/uuid"
},
"sideEffects": false,
"main": "./dist/index.js",
"exports": {
".": {
"node": {
"module": "./dist/esm-node/index.js",
"require": "./dist/index.js",
"import": "./wrapper.mjs"
},
"default": "./dist/esm-browser/index.js"
},
"./package.json": "./package.json"
},
"module": "./dist/esm-node/index.js",
"browser": {
"./dist/md5.js": "./dist/md5-browser.js",
"./dist/rng.js": "./dist/rng-browser.js",
"./dist/sha1.js": "./dist/sha1-browser.js",
"./dist/esm-node/index.js": "./dist/esm-browser/index.js"
},
"files": [
"CHANGELOG.md",
"CONTRIBUTING.md",
"LICENSE.md",
"README.md",
"dist",
"wrapper.mjs"
],
"devDependencies": {
"@babel/cli": "7.11.6",
"@babel/core": "7.11.6",
"@babel/preset-env": "7.11.5",
"@commitlint/cli": "11.0.0",
"@commitlint/config-conventional": "11.0.0",
"@rollup/plugin-node-resolve": "9.0.0",
"babel-eslint": "10.1.0",
"bundlewatch": "0.3.1",
"eslint": "7.10.0",
"eslint-config-prettier": "6.12.0",
"eslint-config-standard": "14.1.1",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.1",
"husky": "4.3.0",
"jest": "25.5.4",
"lint-staged": "10.4.0",
"npm-run-all": "4.1.5",
"optional-dev-dependency": "2.0.1",
"prettier": "2.1.2",
"random-seed": "0.3.0",
"rollup": "2.28.2",
"rollup-plugin-terser": "7.0.2",
"runmd": "1.3.2",
"standard-version": "9.0.0"
},
"optionalDevDependencies": {
"@wdio/browserstack-service": "6.4.0",
"@wdio/cli": "6.4.0",
"@wdio/jasmine-framework": "6.4.0",
"@wdio/local-runner": "6.4.0",
"@wdio/spec-reporter": "6.4.0",
"@wdio/static-server-service": "6.4.0",
"@wdio/sync": "6.4.0"
},
"scripts": {
"examples:browser:webpack:build": "cd examples/browser-webpack && npm install && npm run build",
"examples:browser:rollup:build": "cd examples/browser-rollup && npm install && npm run build",
"examples:node:commonjs:test": "cd examples/node-commonjs && npm install && npm test",
"examples:node:esmodules:test": "cd examples/node-esmodules && npm install && npm test",
"lint": "npm run eslint:check && npm run prettier:check",
"eslint:check": "eslint src/ test/ examples/ *.js",
"eslint:fix": "eslint --fix src/ test/ examples/ *.js",
"pretest": "[ -n $CI ] || npm run build",
"test": "BABEL_ENV=commonjs node --throw-deprecation node_modules/.bin/jest test/unit/",
"pretest:browser": "optional-dev-dependency && npm run build && npm-run-all --parallel examples:browser:**",
"test:browser": "wdio run ./wdio.conf.js",
"pretest:node": "npm run build",
"test:node": "npm-run-all --parallel examples:node:**",
"test:pack": "./scripts/testpack.sh",
"pretest:benchmark": "npm run build",
"test:benchmark": "cd examples/benchmark && npm install && npm test",
"prettier:check": "prettier --ignore-path .prettierignore --check '**/*.{js,jsx,json,md}'",
"prettier:fix": "prettier --ignore-path .prettierignore --write '**/*.{js,jsx,json,md}'",
"bundlewatch": "npm run pretest:browser && bundlewatch --config bundlewatch.config.json",
"md": "runmd --watch --output=README.md README_js.md",
"docs": "( node --version | grep -q 'v12' ) && ( npm run build && runmd --output=README.md README_js.md )",
"docs:diff": "npm run docs && git diff --quiet README.md",
"build": "./scripts/build.sh",
"prepack": "npm run build",
"release": "standard-version --no-verify"
},
"repository": {
"type": "git",
"url": "https://github.com/uuidjs/uuid.git"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,json,md}": [
"prettier --write"
],
"*.{js,jsx}": [
"eslint --fix"
]
},
"standard-version": {
"scripts": {
"postchangelog": "prettier --write CHANGELOG.md"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
import uuid from './dist/index.js';
export const v1 = uuid.v1;
export const v3 = uuid.v3;
export const v4 = uuid.v4;
export const v5 = uuid.v5;
export const NIL = uuid.NIL;
export const version = uuid.version;
export const validate = uuid.validate;
export const stringify = uuid.stringify;
export const parse = uuid.parse;
+128
View File
@@ -0,0 +1,128 @@
{
"name": "bull",
"version": "4.16.5",
"description": "Job manager",
"engines": {
"node": ">=12"
},
"main": "./index.js",
"types": "./index.d.ts",
"repository": {
"type": "git",
"url": "git://github.com/OptimalBits/bull.git"
},
"keywords": [
"job",
"queue",
"task",
"parallel"
],
"author": "OptimalBits",
"license": "MIT",
"readmeFilename": "README.md",
"dependencies": {
"cron-parser": "^4.9.0",
"get-port": "^5.1.1",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"msgpackr": "^1.11.2",
"semver": "^7.5.2",
"uuid": "^8.3.0"
},
"devDependencies": {
"@commitlint/cli": "^7.6.1",
"@commitlint/config-conventional": "^7.6.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/git": "^9.0.0",
"@semantic-release/github": "^7.2.1",
"@semantic-release/npm": "^7.1.1",
"@semantic-release/release-notes-generator": "^9.0.2",
"chai": "^4.2.0",
"coveralls": "^3.1.0",
"delay": "^4.3.0",
"eslint": "^7.4.0",
"eslint-plugin-mocha": "^7.0.1",
"eslint-plugin-node": "^8.0.1",
"expect.js": "^0.3.1",
"fast-glob": "^3.3.2",
"husky": "^4.2.5",
"istanbul": "^0.4.5",
"lint-staged": "^8.2.1",
"minimatch": "^7.4.4",
"mocha": "^8.1.1",
"mocha-lcov-reporter": "^1.3.0",
"moment": "^2.24.0",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"p-reflect": "^1.0.0",
"prettier": "^1.19.1",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.2",
"sinon": "^7.5.0"
},
"scripts": {
"clean:scripts": "rimraf rawScripts lib/scripts",
"dc:up": "docker-compose -f docker-compose.yml up -d",
"dc:down": "docker-compose -f docker-compose.yml down",
"dry-run": "npm publish --dry-run",
"generate:raw:scripts": "node generateRawScripts.js",
"pretest": "npm-run-all clean:scripts generate:raw:scripts transform:commands lint",
"lint": "eslint lib test *.js",
"test": "NODE_ENV=test nyc mocha -- 'test/test_*' --recursive --exit",
"test:nolint": "NODE_ENV=test mocha 'test/test_*' --recursive --exit",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"postpublish": "git push && git push --tags",
"prettier": "prettier --config package.json --write '**/*.js'",
"precommit": "lint-staged",
"build": "tsc",
"transform:commands": "node ./commandTransform.js ./rawScripts ./lib/scripts"
},
"lint-staged": {
"*.{js,json}": [
"prettier --write",
"git add"
]
},
"prettier": {
"singleQuote": true
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"release": {
"branches": [
"develop"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": true
}
],
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": [
"package.json",
"yarn.lock",
"CHANGELOG.md"
],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
]
]
}
}