{"_id":"@avada-falcon/worker-sdk","_rev":"10-3c2329a7ce34a19ffcbad5c8b7f136fa","name":"@avada-falcon/worker-sdk","dist-tags":{"latest":"0.5.8"},"versions":{"0.5.0":{"name":"@avada-falcon/worker-sdk","version":"0.5.0","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.0","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"35e7c8f43a1abb1044b06db27adc7443a3b179ed","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.0.tgz","fileCount":25,"integrity":"sha512-aYRKx0iF/Ukwf4z4N6nalQGIsOe2MBmH87EP+CY4oV/baGcL+HSKi7hH0cU109ZVhCkXnC5swgQL8MkANyn4xw==","signatures":[{"sig":"MEUCIQDnXt3FNs1Iw/k/J4JTHymu5AhqtFYhsejp9+qKzHG8oAIgWM1cnBaZHQeZtS3MqqCCz/xf04wnt0r/0/Q0JoQbZho=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":73169},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"8be5fc0fba6e0ffb39b385f4b3976ffe98714a3e","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.0_1783928585039_0.8952894527332513","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@avada-falcon/worker-sdk","version":"0.5.1","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.1","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"05d885a96a887abb16ab59f599883e66ab689953","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.1.tgz","fileCount":25,"integrity":"sha512-u19Tn+L4URhT8N9MF5Dm2ll+A5cgidSL3+lvgT9qywEzofQRCGF2mPBr4ai3p+cGEe666m1DuAmtMSOgg+tvEw==","signatures":[{"sig":"MEUCIGjmp6CqRMe0jX0a+jbdjGNkTAihu8jDO+s0Nmfj4xRWAiEAm3/c4ZXkgOrCj7yUfs0Oau1i9yzuYVy0s5LuYuUUvCU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74440},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"8be5fc0fba6e0ffb39b385f4b3976ffe98714a3e","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.1_1783928964942_0.8878636220720011","host":"s3://npm-registry-packages-npm-production"}},"0.5.2":{"name":"@avada-falcon/worker-sdk","version":"0.5.2","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.2","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"443712088c6ece0c0d045573283e291996227a2c","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.2.tgz","fileCount":25,"integrity":"sha512-MstKxjsaylHZ9lydYeuqyiLWO3dSlcp0bvjvXis7rvr8esLNM59QbrXCEjJuBiDhmY0W23hWn+wG2ffB3aE5HQ==","signatures":[{"sig":"MEYCIQCEfjljSIsG5A7WwHe8QwDywLZIfBirRCiJmsuxHMAdIAIhAPkM2B5c+F4rcBAo7aPBa9bX4HW0U0nZJ5K1mxtx6Aap","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74875},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"ab75fa0c7677da2abb1adda7a3ade2abe2778a55","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.2_1783936913045_0.715821997244938","host":"s3://npm-registry-packages-npm-production"}},"0.5.3":{"name":"@avada-falcon/worker-sdk","version":"0.5.3","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.3","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"05eb6e71f32eb2bc3bc860e8971b5e7870919eef","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.3.tgz","fileCount":25,"integrity":"sha512-+rJ4OsL565r4iKsy/Ns86j3NvvgkRX0nB/mWYIjTa+THjA/qrywNL44nn5/qJHxV3kLGdrth+1S2A8ZPT9IxQA==","signatures":[{"sig":"MEUCIQD7t8eSAdpD76kyYBW0DoaFU66UQZ48DSwRTeX+44RbagIgGzXhQc/2PWCk1OEcF1I8GIruJoEGkZ2nzRdQttFsnHg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75108},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"356975f25ef98adb84269acd064c4a91559ff5e3","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.3_1783995631021_0.7361949260098752","host":"s3://npm-registry-packages-npm-production"}},"0.5.4":{"name":"@avada-falcon/worker-sdk","version":"0.5.4","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.4","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"86ffdcb09983ba3fbad17c79b510f82046ea1181","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.4.tgz","fileCount":25,"integrity":"sha512-+wT6F2k9eQ7M27QGgHrrEQtq6Zh33KVgJJa7ljwP9fwvH9kYoiqMzuQbAXg0DoqcU6F0TwVy2S6PU2q8lDr4cQ==","signatures":[{"sig":"MEUCIQDA+9fdG/vq2qkds1AIPejW7gU7oA0J+c7jNNnXawskKAIgKP28NcYvSQxwHMK8sa5YEdHXPk9cy8W8VEILy9hVXfA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75108},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"a7c2518d26ddc021309cc0eba282b6c89a914755","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.4_1783996665995_0.13934108481661212","host":"s3://npm-registry-packages-npm-production"}},"0.5.5":{"name":"@avada-falcon/worker-sdk","version":"0.5.5","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.5","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"e62a86bed148b71ac28ab5c6d48487c56ce48e3e","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.5.tgz","fileCount":25,"integrity":"sha512-qOrOQyBQ5rDaRPPKssz/3e23mFIm1YIEIO1ROOOMQ+siPFJeBaSRCVaYyrwW4AFY+s51gb8wrEjFAxqwSdNgIA==","signatures":[{"sig":"MEYCIQCWb3faUEG2J9u3vsIp6sOnYciKpAoSWnPn1ZWa8YNwBwIhAKsV3FwJa/Z+Cu24nx3fRusQ+m6IbDd8EcGP3zCNaQgQ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":75108},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"a7c2518d26ddc021309cc0eba282b6c89a914755","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.5_1785906098381_0.3215828550344435","host":"s3://npm-registry-packages-npm-production"}},"0.5.6":{"name":"@avada-falcon/worker-sdk","version":"0.5.6","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.6","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"},{"name":"minhdevtree","email":"minhht.1.2.vn@gmail.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"3e94feb52abb0d9bddafaa2c6e2b53e3631bef50","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.6.tgz","fileCount":25,"integrity":"sha512-ruglV+UBSzvhxD5n5UYEz126DO0LJGrExfxAtDfV8JXiRq+MOUELxvN7TBBliemg9die5+CeBcJLyLiayFGyFQ==","signatures":[{"sig":"MEUCIQC3hj/UNKAsxM69cF2vEvhEJ5pwMVlHqlbpn8tZeYteXgIgJCJy4tLYx4Dnl14BEio6nhv76/zfLFxKUH/efcCl9QY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":79110},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"c310da532a2927d9444ed0e77ea2adc4d22d2c5f","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"minhdevtree","email":"minhht.1.2.vn@gmail.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.6_1785923198713_0.9394961373481312","host":"s3://npm-registry-packages-npm-production"}},"0.5.7":{"name":"@avada-falcon/worker-sdk","version":"0.5.7","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"license":"MIT","_id":"@avada-falcon/worker-sdk@0.5.7","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"},{"name":"minhdevtree","email":"minhht.1.2.vn@gmail.com"}],"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"dist":{"shasum":"4f8baede0de37ceebd7da9539d20da58c9fed5b5","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.7.tgz","fileCount":25,"integrity":"sha512-TDNp2wGW3mS1gKB2I5vlS5V3hQsCiRc13csH1eQQkKctC84UPEZWGidX7KmOSCcYjNq2SL8hlgfu5SDUS0dXIg==","signatures":[{"sig":"MEQCIEXaCYWgAojqK25MVYNs5l36nobKP44TqSX+uVxIOjhMAiBUsz4rRL5T4yuwAd1OjuZjEyiCBD/8pW2Tw2zJH0bvOA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":79110},"main":"src/index.cjs","type":"module","engines":{"node":">=20"},"exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"gitHead":"8cb82e6256155975777665c30f5bb00a1341bc45","scripts":{"test":"vitest run","test:watch":"vitest"},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"repository":{"url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git","type":"git"},"_npmVersion":"10.9.4","description":"Self-hosted background job runner powered by BullMQ + Redis","directories":{},"_nodeVersion":"22.22.0","dependencies":{"bullmq":"^5.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/worker-sdk_0.5.7_1788410864232_0.1424786560777085","host":"s3://npm-registry-packages-npm-production"}},"0.5.8":{"name":"@avada-falcon/worker-sdk","version":"0.5.8","description":"Self-hosted background job runner powered by BullMQ + Redis","type":"module","main":"src/index.cjs","exports":{".":{"import":"./src/index.js","require":"./src/index.cjs"}},"scripts":{"test":"vitest run","test:watch":"vitest"},"engines":{"node":">=20"},"repository":{"type":"git","url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git"},"license":"MIT","publishConfig":{"access":"public"},"keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"dependencies":{"bullmq":"^5.0.0","@bull-board/api":"^6.0.0","@bull-board/express":"^6.0.0","express":"^5.0.0","ioredis":"^5.0.0","js-yaml":"^4.1.0"},"devDependencies":{"vitest":"^4.0.0"},"_id":"@avada-falcon/worker-sdk@0.5.8","gitHead":"7d006f906bdef25ee102f4b224ff1a3461aa9151","bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-pJOq8qx8FKpcTbuZFWEMIT7tYBw7txJyy+8AurOi9avXVZ3NTXZow78XkiQYR6Ksd1os6NXKL3ktHxuS80LqAA==","shasum":"700619a4cd5e20b6f24b9679b75f01cf18cd0a43","tarball":"https://registry.npmjs.org/@avada-falcon/worker-sdk/-/worker-sdk-0.5.8.tgz","fileCount":25,"unpackedSize":81187,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCASxBpi0tvf/KoEfmV1TtGrMwgO8pLPwLBXmYpADnd4AIgX2VAqt+FofwCtwQKkaJkmqSfZbGRXc0Psmhbwb7Hd3Y="}]},"_npmUser":{"name":"tn22180","email":"tuannv@avadagroup.com"},"directories":{},"maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"},{"name":"minhdevtree","email":"minhht.1.2.vn@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/worker-sdk_0.5.8_1789463319763_0.0863367158745525"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T07:43:04.870Z","modified":"2026-09-15T09:08:40.151Z","0.5.0":"2026-07-13T07:43:05.163Z","0.5.1":"2026-07-13T07:49:25.079Z","0.5.2":"2026-07-13T10:01:53.179Z","0.5.3":"2026-07-14T02:20:31.164Z","0.5.4":"2026-07-14T02:37:46.144Z","0.5.5":"2026-08-05T05:01:38.533Z","0.5.6":"2026-08-05T09:46:38.848Z","0.5.7":"2026-09-03T04:47:44.368Z","0.5.8":"2026-09-15T09:08:39.951Z"},"bugs":{"url":"https://gitlab.com/avada/seoon-team/worker-sdk/issues"},"license":"MIT","homepage":"https://gitlab.com/avada/seoon-team/worker-sdk#readme","keywords":["worker","job-queue","bullmq","redis","background-jobs","self-hosted"],"repository":{"type":"git","url":"git+https://gitlab.com/avada/seoon-team/worker-sdk.git"},"description":"Self-hosted background job runner powered by BullMQ + Redis","maintainers":[{"name":"tn22180","email":"tuannv@avadagroup.com"},{"name":"minhdevtree","email":"minhht.1.2.vn@gmail.com"}],"readme":"# @avada-falcon/worker-sdk\n\nSelf-hosted background job runner powered by BullMQ + Redis. Replace Firebase Cloud Functions with a simple, local job queue.\n\n## Features\n\n- **Tier-based concurrency** — heavy/medium/light queues with configurable limits\n- **Cron jobs** — first-class scheduled job support via BullMQ repeatable jobs\n- **Bull Board dashboard** — built-in web UI for inspecting jobs, retries, errors\n- **TLS Redis support** — connect to remote Redis over TLS (production-ready)\n- **Env-driven config** — single YAML works across local dev and production\n- **Console capture** — `console.log/warn/error` inside handlers automatically appears in Bull Board\n- **Readable job IDs** — `{jobName}-{uuid}` format\n- **Graceful shutdown** — drains in-flight jobs on SIGTERM/SIGINT\n\n## Install\n\n```bash\nnpm install @avada-falcon/worker-sdk\n```\n\nRequires Node.js >= 20 and a Redis instance you can reach.\n\n## Quick Start\n\n### 1. Create config\n\n`worker.config.yml`:\n\n```yaml\nredis:\n  host: ${REDIS_HOST:-127.0.0.1}\n  port: ${REDIS_PORT:-6379}\n  password: ${REDIS_PASSWORD:-}\n  tls: ${REDIS_TLS:-}\n\nlogging:\n  dir: ./logs                        # local file buffer (short retention)\n  retentionDays: 7\n  loki:                              # optional — ship to Loki for long-term search\n    url: ${LOKI_URL:-}\n    batchSize: 100\n    flushInterval: 5000\n    labels:\n      app: my-app\n      env: production\n\ndashboard:\n  port: 3800\n  auth:\n    username: admin\n    password: ${WORKER_DASHBOARD_PASSWORD}\n\nconcurrency:\n  heavy: 2\n  medium: 5\n  light: 10\n\njobs:\n  processOrder:\n    tier: heavy\n    timeout: 60000\n    retry:\n      maxAttempts: 3\n      baseDelay: 2000\n\n  sendNotification:\n    tier: light\n    timeout: 10000\n\n  dailyReport:\n    tier: medium\n    timeout: 300000\n    cron: \"0 0 * * *\"\n```\n\nYAML supports `${VAR}` and `${VAR:-default}` env interpolation. The same file can run locally (defaults to `127.0.0.1`) or in production (override via env vars).\n\n### 2. Define handlers\n\n```js\n// jobs/processOrder.js\nexport async function execute(payload, context) {\n  const {logger, signal, jobId, attempt} = context;\n\n  logger.info('Processing order', {orderId: payload.orderId});\n\n  // console.log/warn/error inside this function (and any code it calls)\n  // is automatically captured to Bull Board logs\n  console.log('Doing some work...');\n\n  return {success: true};\n}\n```\n\n### 3. Start worker\n\n```js\n// worker.mjs\nimport {createWorker} from '@avada-falcon/worker-sdk';\nimport {execute as processOrder} from './jobs/processOrder.js';\nimport {execute as sendNotification} from './jobs/sendNotification.js';\nimport {execute as dailyReport} from './jobs/dailyReport.js';\n\nconst worker = createWorker('./worker.config.yml');\n\nworker.register('processOrder', processOrder);\nworker.register('sendNotification', sendNotification);\nworker.register('dailyReport', dailyReport);\n\nawait worker.start();\n```\n\nRun it: `node worker.mjs`\n\n### 4. Push jobs from your app\n\n```js\nimport {createClient} from '@avada-falcon/worker-sdk';\n\nconst client = createClient('./worker.config.yml');\n\nawait client.add('processOrder', {orderId: 42});\n// → returns {id: \"processOrder-e7a3b5c2-3a4f-...\"}\n```\n\nThe client is lightweight — only creates BullMQ Queue instances on demand. Safe to import anywhere in your app backend.\n\n### 5. Run dashboard as a separate service (recommended)\n\n```js\n// dashboard.mjs\nimport {createDashboard} from '@avada-falcon/worker-sdk';\n\nconst dashboard = createDashboard('./worker.config.yml');\nawait dashboard.start();\n```\n\nRun it: `node dashboard.mjs`\n\nRunning the dashboard as its own process means it stays up even when workers restart, and a single dashboard can serve any number of workers (they all share Redis).\n\n## Tiers\n\nJobs are grouped by resource weight. Each tier maps to a separate BullMQ Worker with its own concurrency limit:\n\n| Tier | Default Concurrency | Use case |\n|------|-------------------|----------|\n| heavy | 2 | CPU/memory intensive work — bulk API calls, image processing |\n| medium | 5 | Moderate processing — page scanning, batch operations |\n| light | 10 | Quick tasks — status updates, notifications |\n\nOverride defaults in `worker.config.yml`:\n\n```yaml\nconcurrency:\n  heavy: 3\n  medium: 10\n  light: 20\n```\n\n## Dashboard\n\nBull Board UI mounted at `http://localhost:3800` (port configurable). Features:\n\n- View all queues with job counts\n- Filter jobs by status (completed/failed/waiting/active/delayed)\n- Inspect payload, result, logs, error/stack trace\n- Retry/delete/promote individual jobs\n- View repeatable cron schedules\n\nBasic auth is required — set `username` and `password` in config.\n\n## Handler Context\n\n```js\nexport async function execute(payload, context) {\n  context.jobId    // unique job ID — format: jobName-uuid\n  context.attempt  // current attempt number (1-based)\n  context.logger   // {info, warn, error} — writes to Bull Board + stdout + log file\n  context.signal   // AbortSignal — fires when timeout expires; 5s later the job is\n                   // failed with JobTimeoutError whether or not the handler listened\n  context.requeue  // async (retryMs?) — decline this job, hand it back to the queue\n}\n```\n\n### Declining a job with `context.requeue()`\n\nUse it when this worker is *temporarily* unable to run the job — an app-level admission\ngate such as a per-worker memory budget. Blocking inside the handler instead would keep\nthe job `active` and pinned to this worker, where no idle peer can pick it up.\n\n```js\nexport async function execute(payload, context) {\n  if (!memoryBudget.tryAcquire(payload.size)) {\n    return context.requeue(500);   // back to the queue, retried in ~500-1000ms\n  }\n  // ...\n}\n```\n\n- The job is moved to `delayed` and re-dispatched to whichever worker is free. **No attempt\n  is burned** and no `failed` event fires — a decline is not a failure.\n- `retryMs` defaults to `200` and is jittered (actual delay is `retryMs`–`2×retryMs`) so\n  a batch of declined jobs doesn't wake in lockstep.\n- **`requeue()` always throws.** Do not wrap it in a `try/catch` that swallows the error —\n  BullMQ would then complete a job that is also sitting in the delayed set, and it would\n  run twice. Call it as `return context.requeue()`, or let the throw propagate.\n- Declines are capped per job at `jobs.<name>.maxRequeues` (default `50`). Past the cap the\n  job fails with a real error instead of ping-ponging forever on a gate that never opens.\n\n```yaml\njobs:\n  heavyImport:\n    tier: heavy\n    timeout: 60000\n    maxRequeues: 100     # optional — default 50\n```\n\n## File Logging\n\nWhen `logging.dir` is configured, all job logs are written to daily JSON line files on disk:\n\n```\nlogs/\n  2026-04-14.log\n  2026-04-13.log\n  ...\n```\n\nEach line is a JSON object:\n\n```json\n{\"ts\":\"2026-04-14T10:30:00.123Z\",\"job\":\"processOrder\",\"id\":\"processOrder-abc123\",\"level\":\"INFO\",\"msg\":\"Processing\",\"data\":{\"orderId\":42}}\n```\n\nBoth `context.logger.info/warn/error` and captured `console.log/warn/error` are written.\n\nLocal files act as a short-term buffer — set `retentionDays` to how many days you want to keep on disk (e.g. 7). Old files are auto-deleted on worker startup. For long-term archive, configure `logging.loki` to ship logs to Grafana Loki (see below).\n\nSearch with grep:\n\n```bash\n# All errors from a specific day\ngrep '\"ERROR\"' logs/2026-04-14.log\n\n# Everything for a specific job ID\ngrep 'processOrder-abc123' logs/2026-04-14.log\n\n# Search across multiple days\ngrep 'shopId.*xyz' logs/2026-04-*.log\n```\n\nFor Docker deployments, mount the logs directory as a volume so files persist on the host:\n\n```yaml\nvolumes:\n  - ./logs:/app/functions/logs\n```\n\n## Cron Jobs\n\nAdd a `cron` field to any job in the config. The SDK registers it as a BullMQ repeatable job — schedule survives restarts.\n\n```yaml\njobs:\n  dailyReport:\n    tier: medium\n    timeout: 300000\n    cron: \"0 0 * * *\"        # every day at midnight\n```\n\nThe handler is registered like any other job — same `execute(payload, context)` signature.\n\n## Env Variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `WORKER_DASHBOARD_PASSWORD` | Yes | Bull Board dashboard password |\n| `REDIS_HOST` | No | Defaults to YAML config |\n| `REDIS_PORT` | No | Defaults to YAML config |\n| `REDIS_PASSWORD` | No | Defaults to YAML config |\n| `REDIS_TLS` | No | Set to `true` to enable TLS |\n\nAny field in `worker.config.yml` can be made env-driven via `${VAR_NAME:-default}` syntax.\n\n## Long-term log search with Loki\n\nThis SDK configures BullMQ to retain only the last ~1000 completed jobs in Redis (`removeOnComplete`). For long-term historical search (weeks or months), configure Loki:\n\n```yaml\nlogging:\n  loki:\n    url: http://loki:3100\n    batchSize: 100\n    flushInterval: 5000\n    labels:\n      app: my-app\n```\n\nThe SDK will push every log entry (both `context.logger.*` and captured `console.*`) to Loki in batches. Use Grafana to search by job name, level, shop ID, date range.\n\nIf `loki.url` is empty or missing, Loki shipping is disabled — the SDK falls back to file-only logging.\n\nLog retention in Loki is controlled by the Loki server's own `retention_period` config, not by the SDK.\n\n**Setup your Loki stack** — see [SETUP.md](./SETUP.md) for a Docker Compose example that runs Loki + Grafana alongside the worker.\n\n## Multi-worker deployments\n\nA single Redis + Loki + Bull Board can serve any number of workers. Scale horizontally on one machine (`docker compose up --scale`) or across hosts.\n\n### Identity and specialization\n\n```yaml\n# worker.config.yml\nworker:\n  id: ${WORKER_ID:-}              # empty → auto-generated ${hostname}-${pid}\n\nconcurrency:\n  heavy: 2\n  medium: 5\n  light: 0                        # 0 = opt out — this worker skips the light tier\n```\n\nEach worker gets its own ID. In Docker Compose `--scale N` mode the auto-generated default (`${hostname}-${pid}`) gives meaningful IDs because Docker assigns unique hostnames per replica. For multi-host deployments set `WORKER_ID=mac-mini` / `WORKER_ID=vps-hanoi` per host so Grafana labels stay readable.\n\nTier opt-out via `concurrency: 0` enables heterogeneous pools: a beefy box can be heavy-only, a small box can be light-only. BullMQ distributes jobs atomically — a worker that doesn't subscribe to a tier never pulls from it.\n\n### Liveness: heartbeats\n\n```yaml\nworker:\n  heartbeat:\n    enabled: true                 # default\n    intervalMs: 10000             # beat every 10s\n    ttlMs: 30000                  # key expires after 30s of silence\n```\n\nEvery worker writes a TTL'd key `worker:heartbeat:<workerId>` to Redis on the interval. If a worker dies or loses Redis connectivity, the key expires automatically. Enumerate live workers:\n\nEach beat also adds the workerId to the `worker:heartbeats` SET, so `listWorkers` resolves the fleet in two round-trips (`SMEMBERS` + `MGET`) instead of scanning the keyspace. That matters when the Redis is shared with BullMQ — thousands of retained job keys turn a SCAN into seconds over a remote link. Set membership has no TTL, so `listWorkers` prunes members whose heartbeat key is gone; when the index is empty (a fleet still on < 0.5.6) it falls back to SCAN.\n\n```js\nimport {listWorkers} from '@avada-falcon/worker-sdk';\nimport Redis from 'ioredis';\n\nconst redis = new Redis({host, port, password});\nconst workers = await listWorkers(redis);\n// → [{workerId, hostname, pid, tiers, startedAt, lastBeat}, ...]\n```\n\n### Health checks\n\nFour admin helpers for monitoring cluster state:\n\n```js\nimport {\n  pingRedis,\n  getQueueDepths,\n  checkDashboard,\n  getClusterHealth\n} from '@avada-falcon/worker-sdk';\n\n// Redis PING + latency\nawait pingRedis(redis);\n// → {ok: true, latencyMs: 3}\n\n// Per-tier queue depth (waiting/active/delayed/completed/failed/paused + total)\n// Each tier reports its own ok flag — a per-tier failure doesn't abort the others.\nawait getQueueDepths(redis, ['heavy', 'medium', 'light']);\n// → {heavy: {ok: true, waiting: 2, active: 1, ..., total: 3}, ...}\n\n// Dashboard /health endpoint probe (no auth)\nawait checkDashboard('http://host:3800');\n// → {ok: true, latencyMs: 12, status: 200, uptime: 1234, timestamp: '...'}\n\n// One-shot aggregate: runs all probes in parallel\nawait getClusterHealth({\n  redis,\n  tiers: ['heavy', 'medium', 'light'],   // optional\n  dashboardUrl: 'http://host:3800',      // optional\n  timeouts: {                            // optional — defaults shown\n    redisMs: 2000,\n    workersMs: 5000,\n    queuesMs: 5000,\n    dashboardMs: 3000\n  }\n});\n// → {ok, status: 'healthy'|'degraded'|'unhealthy', checkedAt, redis,\n//    workers: {count, items}, queues: {byTier}, dashboard}\n```\n\n`getClusterHealth` doesn't short-circuit — a failure in one probe reports `ok: false` for that section but still returns the others. Top-level `ok` is true only if every probed section is ok. The `status` field classifies the rollup: `healthy` (everything ok), `unhealthy` (Redis itself is down — the critical dependency), or `degraded` (Redis ok but at least one other section failed).\n\n`checkDashboard` validates that `baseUrl` parses and uses `http:` or `https:`, sets `redirect: 'manual'`, and rejects responses larger than 64 KiB. These are basic SSRF guards — if you wire `baseUrl` to user input, add your own allow-list on top.\n\n### Scheduled jobs: the cron leader\n\n```yaml\ncron:\n  leader: ${CRON_LEADER:-false}   # EXACTLY ONE worker should set this to true\n```\n\nIn a multi-worker pool exactly one worker should be designated the cron leader. Only that worker registers scheduled jobs — others skip registration. If no worker has `cron.leader: true` and your `jobs` config includes cron entries, the SDK warns on startup and scheduled jobs do not fire.\n\nFor a pool of three, one compose-file pattern:\n\n```yaml\nservices:\n  worker-leader:\n    environment: {CRON_LEADER: \"true\"}\n    # one replica, always on\n  worker:\n    environment: {CRON_LEADER: \"false\"}\n    # scale this service: docker compose up -d --scale worker=N\n```\n\n### Filtering per worker in Grafana\n\nEvery log pushed to Loki carries the `workerId` label automatically:\n\n```logql\n# just worker-2's logs\n{app=\"my-app\", workerId=\"mac-mini-2\"}\n\n# errors from any worker\n{app=\"my-app\", level=\"ERROR\"}\n\n# cross-worker comparison by job name\n{app=\"my-app\", job=\"generateAnchor\"} | json\n```\n\n## Migration from Firebase Pub/Sub\n\n| Before (Firebase) | After (Worker SDK) |\n|---|---|\n| `functions.runWith({memory, timeout})` | `worker.config.yml` job entry |\n| `.pubsub.topic('name').onPublish(fn)` | `worker.register('name', execute)` |\n| `JSON.parse(Buffer.from(message.data))` | `payload` (already parsed) |\n| `console.log()` | Works as-is — captured to Bull Board |\n| `publishTopic('next', data)` | `client.add('next', data)` |\n| Runs on Google Cloud | Runs on your machine |\n\nSee `SETUP.md` for the full integration guide with handler structure, file layout, and migration steps.\n","readmeFilename":"README.md"}