├── .gitignore
├── Procfile
├── README.md
├── app.json
├── client.js
├── index.html
├── package-lock.json
├── package.json
├── server.js
└── worker.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node server.js
2 | worker: node worker.js
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Background Jobs in Node.js with Redis
2 |
3 | Redis-backed background worker example using [OptimalBits/bull](https://github.com/OptimalBits/bull) and [throng](https://github.com/hunterloftis/throng).
4 |
5 | 
6 |
7 | [](https://heroku.com/deploy?template=https://github.com/heroku-examples/node-workers-example)
8 |
9 | ## Installing Local Dependencies
10 |
11 | - [Redis](https://redis.io/)
12 |
13 | ```
14 | $ brew install redis
15 | $ brew services start redis
16 | ```
17 |
18 | ## Getting Started
19 |
20 | 1. `npm install`
21 | 2. `npm start`
22 | 3. [http://localhost:5000](http://localhost:5000)
23 |
24 | ## Deploying
25 |
26 | ```
27 | $ git clone git@github.com:heroku-examples/node-workers-example.git
28 | $ cd node-workers-example
29 |
30 | $ heroku create
31 | $ heroku addons:create heroku-redis
32 | $ git push heroku main
33 | $ heroku ps:scale worker=1
34 | $ heroku open
35 | ```
36 |
37 | ## Application Overview
38 |
39 | The application is comprised of two process:
40 |
41 | - **`web`** - An [Express](https://expressjs.com/) server that serves the frontend assets, accepts new background jobs, and reports on the status us existing jobs
42 | - **`worker`** - A small node process that listens for and executes incoming jobs
43 |
44 | Because these are separate processes, they can be scaled independently based on specific application needs. Read the [Process Model](https://devcenter.heroku.com/articles/process-model) article for a more in-depth understanding of Heroku’s process model.
45 |
46 | The `web` process serves the `index.html` and `client.js` files which implement a simplified example of a frontend interface that kicks off new jobs and checks in on them.
47 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "addons": [
3 | "heroku-redis"
4 | ]
5 | }
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 |
2 | // Store for all of the jobs in progress
3 | let jobs = {};
4 |
5 | // Kick off a new job by POST-ing to the server
6 | async function addJob() {
7 | let res = await fetch('job/', {method: 'POST'});
8 | let job = await res.json();
9 | jobs[job.id] = {id: job.id, state: "queued"};
10 | render();
11 | }
12 |
13 | // Fetch updates for each job
14 | async function updateJobs() {
15 | for (let id of Object.keys(jobs)) {
16 | let res = await fetch(`/job/${id}`);
17 | let result = await res.json();
18 | if (!!jobs[id]) {
19 | jobs[id] = result;
20 | }
21 | render();
22 | }
23 | }
24 |
25 | // Delete all stored jobs
26 | function clear() {
27 | jobs = {};
28 | render();
29 | }
30 |
31 | // Update the UI
32 | function render() {
33 | let s = "";
34 | for (let id of Object.keys(jobs)) {
35 | s += renderJob(jobs[id]);
36 | }
37 |
38 | // For demo simplicity this blows away all of the existing HTML and replaces it,
39 | // which is very inefficient. In a production app a library like React or Vue should
40 | // handle this work
41 | document.querySelector("#job-summary").innerHTML = s;
42 | }
43 |
44 | // Renders the HTML for each job object
45 | function renderJob(job) {
46 | let progress = job.progress || 0;
47 | let color = "bg-light-purple";
48 |
49 | if (job.state === "completed") {
50 | color = "bg-purple";
51 | progress = 100;
52 | } else if (job.state === "failed") {
53 | color = "bg-dark-red";
54 | progress = 100;
55 | }
56 |
57 | return document.querySelector('#job-template')
58 | .innerHTML
59 | .replace('{{id}}', job.id)
60 | .replace('{{state}}', job.state)
61 | .replace('{{color}}', color)
62 | .replace('{{progress}}', progress);
63 | }
64 |
65 | // Attach click handlers and kick off background processes
66 | window.onload = function() {
67 | document.querySelector("#add-job").addEventListener("click", addJob);
68 | document.querySelector("#clear").addEventListener("click", clear);
69 |
70 | setInterval(updateJobs, 200);
71 | };
72 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
25 |
Create long-running jobs
26 |
30 |
31 |
32 |
33 |
34 | Tip: Jobs not running? Be sure to run heroku ps:scale worker=1
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-workers-example",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.5",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
11 | "requires": {
12 | "mime-types": "~2.1.18",
13 | "negotiator": "0.6.1"
14 | }
15 | },
16 | "array-filter": {
17 | "version": "0.0.1",
18 | "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
19 | "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw="
20 | },
21 | "array-flatten": {
22 | "version": "1.1.1",
23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
25 | },
26 | "array-map": {
27 | "version": "0.0.0",
28 | "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
29 | "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI="
30 | },
31 | "array-reduce": {
32 | "version": "0.0.0",
33 | "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
34 | "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys="
35 | },
36 | "body-parser": {
37 | "version": "1.18.3",
38 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
39 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
40 | "requires": {
41 | "bytes": "3.0.0",
42 | "content-type": "~1.0.4",
43 | "debug": "2.6.9",
44 | "depd": "~1.1.2",
45 | "http-errors": "~1.6.3",
46 | "iconv-lite": "0.4.23",
47 | "on-finished": "~2.3.0",
48 | "qs": "6.5.2",
49 | "raw-body": "2.3.3",
50 | "type-is": "~1.6.16"
51 | },
52 | "dependencies": {
53 | "debug": {
54 | "version": "2.6.9",
55 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
56 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
57 | "requires": {
58 | "ms": "2.0.0"
59 | }
60 | },
61 | "ms": {
62 | "version": "2.0.0",
63 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
64 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
65 | }
66 | }
67 | },
68 | "bull": {
69 | "version": "3.7.0",
70 | "resolved": "https://registry.npmjs.org/bull/-/bull-3.7.0.tgz",
71 | "integrity": "sha512-DHCALp+OOahK+q2hB3sZQew0CJn4W3zYIQsdMlnBCy7JYCnJ/bdj0MFHjo5k0ZhNZxzwhLErXt1yd3llV494UQ==",
72 | "requires": {
73 | "cron-parser": "^2.7.3",
74 | "debuglog": "^1.0.0",
75 | "get-port": "^4.2.0",
76 | "ioredis": "^4.5.1",
77 | "lodash": "^4.17.11",
78 | "p-timeout": "^2.0.1",
79 | "promise.prototype.finally": "^3.1.0",
80 | "semver": "^5.6.0",
81 | "util.promisify": "^1.0.0",
82 | "uuid": "^3.2.1"
83 | }
84 | },
85 | "bytes": {
86 | "version": "3.0.0",
87 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
88 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
89 | },
90 | "cluster-key-slot": {
91 | "version": "1.0.12",
92 | "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz",
93 | "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg=="
94 | },
95 | "commander": {
96 | "version": "2.20.0",
97 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
98 | "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
99 | },
100 | "content-disposition": {
101 | "version": "0.5.2",
102 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
103 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
104 | },
105 | "content-type": {
106 | "version": "1.0.4",
107 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
108 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
109 | },
110 | "cookie": {
111 | "version": "0.3.1",
112 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
113 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
114 | },
115 | "cookie-signature": {
116 | "version": "1.0.6",
117 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
118 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
119 | },
120 | "cron-parser": {
121 | "version": "2.10.0",
122 | "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.10.0.tgz",
123 | "integrity": "sha512-E181Gbg+wYT0hSikwBOokL7VHgJDYUlFsRFHIlnTP8GGefhcIyf8PSc2IXztmghj5mhAZupU0n3jKfEpZVEmVg==",
124 | "requires": {
125 | "is-nan": "^1.2.1",
126 | "moment-timezone": "^0.5.23"
127 | }
128 | },
129 | "debug": {
130 | "version": "3.2.6",
131 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
132 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
133 | "requires": {
134 | "ms": "^2.1.1"
135 | }
136 | },
137 | "debuglog": {
138 | "version": "1.0.1",
139 | "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
140 | "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI="
141 | },
142 | "define-properties": {
143 | "version": "1.1.3",
144 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
145 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
146 | "requires": {
147 | "object-keys": "^1.0.12"
148 | }
149 | },
150 | "denque": {
151 | "version": "1.4.0",
152 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.0.tgz",
153 | "integrity": "sha512-gh513ac7aiKrAgjiIBWZG0EASyDF9p4JMWwKA8YU5s9figrL5SRNEMT6FDynsegakuhWd1wVqTvqvqAoDxw7wQ=="
154 | },
155 | "depd": {
156 | "version": "1.1.2",
157 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
158 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
159 | },
160 | "destroy": {
161 | "version": "1.0.4",
162 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
163 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
164 | },
165 | "ee-first": {
166 | "version": "1.1.1",
167 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
168 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
169 | },
170 | "encodeurl": {
171 | "version": "1.0.2",
172 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
173 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
174 | },
175 | "es-abstract": {
176 | "version": "1.13.0",
177 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
178 | "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
179 | "requires": {
180 | "es-to-primitive": "^1.2.0",
181 | "function-bind": "^1.1.1",
182 | "has": "^1.0.3",
183 | "is-callable": "^1.1.4",
184 | "is-regex": "^1.0.4",
185 | "object-keys": "^1.0.12"
186 | }
187 | },
188 | "es-to-primitive": {
189 | "version": "1.2.0",
190 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
191 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
192 | "requires": {
193 | "is-callable": "^1.1.4",
194 | "is-date-object": "^1.0.1",
195 | "is-symbol": "^1.0.2"
196 | }
197 | },
198 | "escape-html": {
199 | "version": "1.0.3",
200 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
201 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
202 | },
203 | "etag": {
204 | "version": "1.8.1",
205 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
206 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
207 | },
208 | "eventemitter3": {
209 | "version": "3.1.0",
210 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
211 | "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA=="
212 | },
213 | "express": {
214 | "version": "4.16.4",
215 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
216 | "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
217 | "requires": {
218 | "accepts": "~1.3.5",
219 | "array-flatten": "1.1.1",
220 | "body-parser": "1.18.3",
221 | "content-disposition": "0.5.2",
222 | "content-type": "~1.0.4",
223 | "cookie": "0.3.1",
224 | "cookie-signature": "1.0.6",
225 | "debug": "2.6.9",
226 | "depd": "~1.1.2",
227 | "encodeurl": "~1.0.2",
228 | "escape-html": "~1.0.3",
229 | "etag": "~1.8.1",
230 | "finalhandler": "1.1.1",
231 | "fresh": "0.5.2",
232 | "merge-descriptors": "1.0.1",
233 | "methods": "~1.1.2",
234 | "on-finished": "~2.3.0",
235 | "parseurl": "~1.3.2",
236 | "path-to-regexp": "0.1.7",
237 | "proxy-addr": "~2.0.4",
238 | "qs": "6.5.2",
239 | "range-parser": "~1.2.0",
240 | "safe-buffer": "5.1.2",
241 | "send": "0.16.2",
242 | "serve-static": "1.13.2",
243 | "setprototypeof": "1.1.0",
244 | "statuses": "~1.4.0",
245 | "type-is": "~1.6.16",
246 | "utils-merge": "1.0.1",
247 | "vary": "~1.1.2"
248 | },
249 | "dependencies": {
250 | "debug": {
251 | "version": "2.6.9",
252 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
253 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
254 | "requires": {
255 | "ms": "2.0.0"
256 | }
257 | },
258 | "ms": {
259 | "version": "2.0.0",
260 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
261 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
262 | }
263 | }
264 | },
265 | "finalhandler": {
266 | "version": "1.1.1",
267 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
268 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
269 | "requires": {
270 | "debug": "2.6.9",
271 | "encodeurl": "~1.0.2",
272 | "escape-html": "~1.0.3",
273 | "on-finished": "~2.3.0",
274 | "parseurl": "~1.3.2",
275 | "statuses": "~1.4.0",
276 | "unpipe": "~1.0.0"
277 | },
278 | "dependencies": {
279 | "debug": {
280 | "version": "2.6.9",
281 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
282 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
283 | "requires": {
284 | "ms": "2.0.0"
285 | }
286 | },
287 | "ms": {
288 | "version": "2.0.0",
289 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
290 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
291 | }
292 | }
293 | },
294 | "flexbuffer": {
295 | "version": "0.0.6",
296 | "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz",
297 | "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA="
298 | },
299 | "follow-redirects": {
300 | "version": "1.7.0",
301 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
302 | "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
303 | "requires": {
304 | "debug": "^3.2.6"
305 | }
306 | },
307 | "foreman": {
308 | "version": "3.0.1",
309 | "resolved": "https://registry.npmjs.org/foreman/-/foreman-3.0.1.tgz",
310 | "integrity": "sha512-ek/qoM0vVKpxzkBUQN9k4Fs7l0XsHv4bqxuEW6oqIS4s0ouYKsQ19YjBzUJKTFRumFiSpUv7jySkrI6lfbhjlw==",
311 | "requires": {
312 | "commander": "^2.15.1",
313 | "http-proxy": "^1.17.0",
314 | "mustache": "^2.2.1",
315 | "shell-quote": "^1.6.1"
316 | }
317 | },
318 | "forwarded": {
319 | "version": "0.1.2",
320 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
321 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
322 | },
323 | "fresh": {
324 | "version": "0.5.2",
325 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
326 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
327 | },
328 | "function-bind": {
329 | "version": "1.1.1",
330 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
331 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
332 | },
333 | "get-port": {
334 | "version": "4.2.0",
335 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz",
336 | "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw=="
337 | },
338 | "has": {
339 | "version": "1.0.3",
340 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
341 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
342 | "requires": {
343 | "function-bind": "^1.1.1"
344 | }
345 | },
346 | "has-symbols": {
347 | "version": "1.0.0",
348 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
349 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
350 | },
351 | "http-errors": {
352 | "version": "1.6.3",
353 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
354 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
355 | "requires": {
356 | "depd": "~1.1.2",
357 | "inherits": "2.0.3",
358 | "setprototypeof": "1.1.0",
359 | "statuses": ">= 1.4.0 < 2"
360 | }
361 | },
362 | "http-proxy": {
363 | "version": "1.17.0",
364 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz",
365 | "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==",
366 | "requires": {
367 | "eventemitter3": "^3.0.0",
368 | "follow-redirects": "^1.0.0",
369 | "requires-port": "^1.0.0"
370 | }
371 | },
372 | "iconv-lite": {
373 | "version": "0.4.23",
374 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
375 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
376 | "requires": {
377 | "safer-buffer": ">= 2.1.2 < 3"
378 | }
379 | },
380 | "inherits": {
381 | "version": "2.0.3",
382 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
383 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
384 | },
385 | "ioredis": {
386 | "version": "4.9.0",
387 | "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.9.0.tgz",
388 | "integrity": "sha512-YzfCLsN++Ct43QqGK9CWxaEK6OUvJ7rnENieAPNw3DVp/oF2uBrP2NJChbhO74Ng3LWA+i5zdIEUsZYr6dKDIQ==",
389 | "requires": {
390 | "cluster-key-slot": "^1.0.6",
391 | "debug": "^3.1.0",
392 | "denque": "^1.1.0",
393 | "flexbuffer": "0.0.6",
394 | "lodash.defaults": "^4.2.0",
395 | "lodash.flatten": "^4.4.0",
396 | "redis-commands": "1.4.0",
397 | "redis-errors": "^1.2.0",
398 | "redis-parser": "^3.0.0",
399 | "standard-as-callback": "^2.0.1"
400 | }
401 | },
402 | "ipaddr.js": {
403 | "version": "1.8.0",
404 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
405 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
406 | },
407 | "is-callable": {
408 | "version": "1.1.4",
409 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
410 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
411 | },
412 | "is-date-object": {
413 | "version": "1.0.1",
414 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
415 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
416 | },
417 | "is-nan": {
418 | "version": "1.2.1",
419 | "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz",
420 | "integrity": "sha1-n69ltvttskt/XAYoR16nH5iEAeI=",
421 | "requires": {
422 | "define-properties": "^1.1.1"
423 | }
424 | },
425 | "is-regex": {
426 | "version": "1.0.4",
427 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
428 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
429 | "requires": {
430 | "has": "^1.0.1"
431 | }
432 | },
433 | "is-symbol": {
434 | "version": "1.0.2",
435 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
436 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
437 | "requires": {
438 | "has-symbols": "^1.0.0"
439 | }
440 | },
441 | "jsonify": {
442 | "version": "0.0.0",
443 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
444 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
445 | },
446 | "lodash": {
447 | "version": "4.17.19",
448 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
449 | "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
450 | },
451 | "lodash.defaults": {
452 | "version": "4.2.0",
453 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
454 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
455 | },
456 | "lodash.flatten": {
457 | "version": "4.4.0",
458 | "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
459 | "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
460 | },
461 | "media-typer": {
462 | "version": "0.3.0",
463 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
464 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
465 | },
466 | "merge-descriptors": {
467 | "version": "1.0.1",
468 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
469 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
470 | },
471 | "methods": {
472 | "version": "1.1.2",
473 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
474 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
475 | },
476 | "mime": {
477 | "version": "1.4.1",
478 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
479 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
480 | },
481 | "mime-db": {
482 | "version": "1.38.0",
483 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
484 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
485 | },
486 | "mime-types": {
487 | "version": "2.1.22",
488 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
489 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
490 | "requires": {
491 | "mime-db": "~1.38.0"
492 | }
493 | },
494 | "moment": {
495 | "version": "2.24.0",
496 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
497 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
498 | },
499 | "moment-timezone": {
500 | "version": "0.5.23",
501 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
502 | "integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
503 | "requires": {
504 | "moment": ">= 2.9.0"
505 | }
506 | },
507 | "ms": {
508 | "version": "2.1.1",
509 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
510 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
511 | },
512 | "mustache": {
513 | "version": "2.3.2",
514 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz",
515 | "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ=="
516 | },
517 | "negotiator": {
518 | "version": "0.6.1",
519 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
520 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
521 | },
522 | "object-keys": {
523 | "version": "1.1.0",
524 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz",
525 | "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg=="
526 | },
527 | "object.getownpropertydescriptors": {
528 | "version": "2.0.3",
529 | "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
530 | "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
531 | "requires": {
532 | "define-properties": "^1.1.2",
533 | "es-abstract": "^1.5.1"
534 | }
535 | },
536 | "on-finished": {
537 | "version": "2.3.0",
538 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
539 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
540 | "requires": {
541 | "ee-first": "1.1.1"
542 | }
543 | },
544 | "p-finally": {
545 | "version": "1.0.0",
546 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
547 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
548 | },
549 | "p-timeout": {
550 | "version": "2.0.1",
551 | "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz",
552 | "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==",
553 | "requires": {
554 | "p-finally": "^1.0.0"
555 | }
556 | },
557 | "parseurl": {
558 | "version": "1.3.2",
559 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
560 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
561 | },
562 | "path-to-regexp": {
563 | "version": "0.1.7",
564 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
565 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
566 | },
567 | "promise.prototype.finally": {
568 | "version": "3.1.0",
569 | "resolved": "https://registry.npmjs.org/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz",
570 | "integrity": "sha512-7p/K2f6dI+dM8yjRQEGrTQs5hTQixUAdOGpMEA3+pVxpX5oHKRSKAXyLw9Q9HUWDTdwtoo39dSHGQtN90HcEwQ==",
571 | "requires": {
572 | "define-properties": "^1.1.2",
573 | "es-abstract": "^1.9.0",
574 | "function-bind": "^1.1.1"
575 | }
576 | },
577 | "proxy-addr": {
578 | "version": "2.0.4",
579 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
580 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
581 | "requires": {
582 | "forwarded": "~0.1.2",
583 | "ipaddr.js": "1.8.0"
584 | }
585 | },
586 | "qs": {
587 | "version": "6.5.2",
588 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
589 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
590 | },
591 | "range-parser": {
592 | "version": "1.2.0",
593 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
594 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
595 | },
596 | "raw-body": {
597 | "version": "2.3.3",
598 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
599 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
600 | "requires": {
601 | "bytes": "3.0.0",
602 | "http-errors": "1.6.3",
603 | "iconv-lite": "0.4.23",
604 | "unpipe": "1.0.0"
605 | }
606 | },
607 | "redis-commands": {
608 | "version": "1.4.0",
609 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
610 | "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw=="
611 | },
612 | "redis-errors": {
613 | "version": "1.2.0",
614 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
615 | "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
616 | },
617 | "redis-parser": {
618 | "version": "3.0.0",
619 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
620 | "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
621 | "requires": {
622 | "redis-errors": "^1.0.0"
623 | }
624 | },
625 | "requires-port": {
626 | "version": "1.0.0",
627 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
628 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
629 | },
630 | "safe-buffer": {
631 | "version": "5.1.2",
632 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
633 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
634 | },
635 | "safer-buffer": {
636 | "version": "2.1.2",
637 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
638 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
639 | },
640 | "semver": {
641 | "version": "5.7.0",
642 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
643 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
644 | },
645 | "send": {
646 | "version": "0.16.2",
647 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
648 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
649 | "requires": {
650 | "debug": "2.6.9",
651 | "depd": "~1.1.2",
652 | "destroy": "~1.0.4",
653 | "encodeurl": "~1.0.2",
654 | "escape-html": "~1.0.3",
655 | "etag": "~1.8.1",
656 | "fresh": "0.5.2",
657 | "http-errors": "~1.6.2",
658 | "mime": "1.4.1",
659 | "ms": "2.0.0",
660 | "on-finished": "~2.3.0",
661 | "range-parser": "~1.2.0",
662 | "statuses": "~1.4.0"
663 | },
664 | "dependencies": {
665 | "debug": {
666 | "version": "2.6.9",
667 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
668 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
669 | "requires": {
670 | "ms": "2.0.0"
671 | }
672 | },
673 | "ms": {
674 | "version": "2.0.0",
675 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
676 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
677 | }
678 | }
679 | },
680 | "serve-static": {
681 | "version": "1.13.2",
682 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
683 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
684 | "requires": {
685 | "encodeurl": "~1.0.2",
686 | "escape-html": "~1.0.3",
687 | "parseurl": "~1.3.2",
688 | "send": "0.16.2"
689 | }
690 | },
691 | "setprototypeof": {
692 | "version": "1.1.0",
693 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
694 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
695 | },
696 | "shell-quote": {
697 | "version": "1.6.1",
698 | "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
699 | "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
700 | "requires": {
701 | "array-filter": "~0.0.0",
702 | "array-map": "~0.0.0",
703 | "array-reduce": "~0.0.0",
704 | "jsonify": "~0.0.0"
705 | }
706 | },
707 | "standard-as-callback": {
708 | "version": "2.0.1",
709 | "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz",
710 | "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg=="
711 | },
712 | "statuses": {
713 | "version": "1.4.0",
714 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
715 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
716 | },
717 | "throng": {
718 | "version": "4.0.0",
719 | "resolved": "https://registry.npmjs.org/throng/-/throng-4.0.0.tgz",
720 | "integrity": "sha1-mDxroZk7WOroWZmKpof/6I34TBc=",
721 | "requires": {
722 | "lodash.defaults": "^4.0.1"
723 | }
724 | },
725 | "type-is": {
726 | "version": "1.6.16",
727 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
728 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
729 | "requires": {
730 | "media-typer": "0.3.0",
731 | "mime-types": "~2.1.18"
732 | }
733 | },
734 | "unpipe": {
735 | "version": "1.0.0",
736 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
737 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
738 | },
739 | "util.promisify": {
740 | "version": "1.0.0",
741 | "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
742 | "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
743 | "requires": {
744 | "define-properties": "^1.1.2",
745 | "object.getownpropertydescriptors": "^2.0.3"
746 | }
747 | },
748 | "utils-merge": {
749 | "version": "1.0.1",
750 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
751 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
752 | },
753 | "uuid": {
754 | "version": "3.3.2",
755 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
756 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
757 | },
758 | "vary": {
759 | "version": "1.1.2",
760 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
761 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
762 | }
763 | }
764 | }
765 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-workers-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "license": "MIT",
7 | "dependencies": {
8 | "bull": "^3.7.0",
9 | "express": "^4.16.4",
10 | "foreman": "^3.0.1",
11 | "throng": "^4.0.0"
12 | },
13 | "scripts": {
14 | "start": "nf start"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | let express = require('express');
2 | let Queue = require('bull');
3 |
4 | // Serve on PORT on Heroku and on localhost:5000 locally
5 | let PORT = process.env.PORT || '5000';
6 | // Connect to a local redis intance locally, and the Heroku-provided URL in production
7 | let REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
8 |
9 | let app = express();
10 |
11 | // Create / Connect to a named work queue
12 | let workQueue = new Queue('work', REDIS_URL);
13 |
14 | // Serve the two static assets
15 | app.get('/', (req, res) => res.sendFile('index.html', { root: __dirname }));
16 | app.get('/client.js', (req, res) => res.sendFile('client.js', { root: __dirname }));
17 |
18 | // Kick off a new job by adding it to the work queue
19 | app.post('/job', async (req, res) => {
20 | // This would be where you could pass arguments to the job
21 | // Ex: workQueue.add({ url: 'https://www.heroku.com' })
22 | // Docs: https://github.com/OptimalBits/bull/blob/develop/REFERENCE.md#queueadd
23 | let job = await workQueue.add();
24 | res.json({ id: job.id });
25 | });
26 |
27 | // Allows the client to query the state of a background job
28 | app.get('/job/:id', async (req, res) => {
29 | let id = req.params.id;
30 | let job = await workQueue.getJob(id);
31 |
32 | if (job === null) {
33 | res.status(404).end();
34 | } else {
35 | let state = await job.getState();
36 | let progress = job._progress;
37 | let reason = job.failedReason;
38 | res.json({ id, state, progress, reason });
39 | }
40 | });
41 |
42 | // You can listen to global events to get notified when jobs are processed
43 | workQueue.on('global:completed', (jobId, result) => {
44 | console.log(`Job completed with result ${result}`);
45 | });
46 |
47 | app.listen(PORT, () => console.log("Server started!"));
48 |
--------------------------------------------------------------------------------
/worker.js:
--------------------------------------------------------------------------------
1 | let throng = require('throng');
2 | let Queue = require("bull");
3 |
4 | // Connect to a local redis instance locally, and the Heroku-provided URL in production
5 | let REDIS_URL = process.env.REDIS_URL || "redis://127.0.0.1:6379";
6 |
7 | // Spin up multiple processes to handle jobs to take advantage of more CPU cores
8 | // See: https://devcenter.heroku.com/articles/node-concurrency for more info
9 | let workers = process.env.WEB_CONCURRENCY || 2;
10 |
11 | // The maximum number of jobs each worker should process at once. This will need
12 | // to be tuned for your application. If each job is mostly waiting on network
13 | // responses it can be much higher. If each job is CPU-intensive, it might need
14 | // to be much lower.
15 | let maxJobsPerWorker = 50;
16 |
17 | function sleep(ms) {
18 | return new Promise(resolve => setTimeout(resolve, ms));
19 | }
20 |
21 | function start() {
22 | // Connect to the named work queue
23 | let workQueue = new Queue('work', REDIS_URL);
24 |
25 | workQueue.process(maxJobsPerWorker, async (job) => {
26 | // This is an example job that just slowly reports on progress
27 | // while doing no work. Replace this with your own job logic.
28 | let progress = 0;
29 |
30 | // throw an error 5% of the time
31 | if (Math.random() < 0.05) {
32 | throw new Error("This job failed!")
33 | }
34 |
35 | while (progress < 100) {
36 | await sleep(50);
37 | progress += 1;
38 | job.progress(progress)
39 | }
40 |
41 | // A job can return values that will be stored in Redis as JSON
42 | // This return value is unused in this demo application.
43 | return { value: "This will be stored" };
44 | });
45 | }
46 |
47 | // Initialize the clustered worker process
48 | // See: https://devcenter.heroku.com/articles/node-concurrency for more info
49 | throng({ workers, start });
50 |
--------------------------------------------------------------------------------