hirefire-resource 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ No changes since release yet.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 HireFire
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ ## HireFire Integration Library for Node.js Applications
2
+
3
+ This library integrates Node.js applications with HireFire's Dyno Managers (Heroku Dyno Autoscalers). Instructions specific to supported web frameworks and worker libraries are provided during the setup process.
4
+
5
+ **Supported web frameworks:**
6
+
7
+ - Express
8
+ - Koa
9
+ - Connect
10
+ - Sails
11
+ - Nest
12
+
13
+ **Supported worker libraries:**
14
+
15
+ - BullMQ
16
+
17
+ ---
18
+
19
+ Since 2011, over 1,000 companies have trusted [HireFire] to autoscale more than 5,000 applications hosted on [Heroku], managing over 10,000 web and worker dynos.
20
+
21
+ HireFire is distinguished by its support for both web and worker dynos, extending autoscaling capabilities to Standard-tier dynos. It provides fine-grained control over scaling behavior and improves scaling accuracy by monitoring more reliable metrics at the application level. These metrics include request queue time (web), job queue latency (worker), and job queue size (worker), which contribute to making more effective scaling decisions.
22
+
23
+ For more information, visit our [home page][HireFire].
24
+
25
+ ---
26
+
27
+ ## Development
28
+
29
+ - Run `bin/setup` to prepare the environment.
30
+ - See `npm run` for common tasks.
31
+
32
+ ## Release
33
+
34
+ 1. Update the version in `package.json` using `npm version <patch|minor|major>`.
35
+ 2. Ensure that `CHANGELOG.md` is up-to-date.
36
+ 3. Commit changes with `git commit`.
37
+ 4. Create a `git tag` matching the new version (e.g., `v1.0.0`).
38
+ 5. Push the new git tag. Continuous Integration will handle the distribution process.
39
+
40
+ ## License
41
+
42
+ This package is licensed under the terms of the MIT license.
43
+
44
+ [HireFire]: https://hirefire.io/
45
+ [Heroku]: https://heroku.com/
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "hirefire-resource",
3
+ "version": "1.0.0",
4
+ "description": "HireFire integration library for Node.js applications",
5
+ "keywords": [
6
+ "hirefire",
7
+ "heroku",
8
+ "autoscale",
9
+ "autoscaling",
10
+ "autoscaler",
11
+ "web",
12
+ "worker",
13
+ "dyno"
14
+ ],
15
+ "homepage": "https://hirefire.io",
16
+ "author": "Michael van Rooijen <support@hirefire.io>",
17
+ "license": "MIT",
18
+ "files": [
19
+ "CHANGELOG.md",
20
+ "src"
21
+ ],
22
+ "exports": {
23
+ ".": "./src/index.js",
24
+ "./middleware/connect": "./src/middleware/connect.js",
25
+ "./middleware/express": "./src/middleware/express.js",
26
+ "./middleware/koa": "./src/middleware/koa.js",
27
+ "./macro/bullmq": "./src/macro/bullmq.js"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/hirefire/hirefire-resource-node.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/hirefire/hirefire-resource-node/issues"
35
+ },
36
+ "scripts": {
37
+ "test": "jest --silent --coverage --coverageReporters=html",
38
+ "check": "prettier --check --no-semi .",
39
+ "format": "prettier --write --no-semi .",
40
+ "doc": "jsdoc"
41
+ },
42
+ "engines": {
43
+ "node": ">=16.0.0 <22.0.0"
44
+ },
45
+ "dependencies": {
46
+ "async-mutex": "<1"
47
+ },
48
+ "devDependencies": {
49
+ "@nestjs/common": "^10.2.8",
50
+ "@nestjs/platform-express": "^10.2.8",
51
+ "@nestjs/testing": "^10.2.8",
52
+ "bullmq": "^4.13.2",
53
+ "connect": "^3.7.0",
54
+ "express": "^4.18.2",
55
+ "jest": "^29.3.1",
56
+ "jsdoc": "^4.0.2",
57
+ "koa": "^2.14.2",
58
+ "nock": "^13.3.0",
59
+ "prettier": "^3.1.0",
60
+ "sails": "^1.5.8",
61
+ "sinon": "^17.0.1",
62
+ "supertest": "^6.3.3"
63
+ }
64
+ }
@@ -0,0 +1,20 @@
1
+ const { Web } = require("./web")
2
+ const { Worker } = require("./worker")
3
+
4
+ class Configuration {
5
+ constructor() {
6
+ this.web = null
7
+ this.workers = []
8
+ this.logger = console
9
+ }
10
+
11
+ dyno(name, fn) {
12
+ if (name === "web") {
13
+ this.web = new Web(this)
14
+ } else {
15
+ this.workers.push(new Worker(name, fn))
16
+ }
17
+ }
18
+ }
19
+
20
+ module.exports = Configuration
package/src/errors.js ADDED
@@ -0,0 +1,23 @@
1
+ class MissingQueueError extends Error {
2
+ constructor() {
3
+ super("No queue was specified. Please specify at least one queue.")
4
+ this.name = "MissingQueueError"
5
+ }
6
+ }
7
+
8
+ class JobQueueLatencyUnsupportedError extends Error {
9
+ constructor(name) {
10
+ super(`${name} currently does not support job queue latency measurements.`)
11
+ this.name = "JobQueueLatencyUnsupportedError"
12
+ }
13
+ }
14
+
15
+ function jobQueueLatencyUnsupported(name) {
16
+ throw new JobQueueLatencyUnsupportedError(name)
17
+ }
18
+
19
+ module.exports = {
20
+ MissingQueueError,
21
+ JobQueueLatencyUnsupportedError,
22
+ jobQueueLatencyUnsupported,
23
+ }
@@ -0,0 +1,13 @@
1
+ const Configuration = require("./configuration")
2
+
3
+ class HireFire {
4
+ constructor() {
5
+ this.configuration = new Configuration()
6
+ }
7
+
8
+ configure(fn) {
9
+ fn(this.configuration)
10
+ }
11
+ }
12
+
13
+ module.exports = HireFire
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const HireFire = require("./hirefire")
2
+
3
+ module.exports = new HireFire()
@@ -0,0 +1,110 @@
1
+ const IORedis = require("ioredis")
2
+ const { unpack } = require("../utility")
3
+ const { jobQueueLatencyUnsupported } = require("../errors")
4
+
5
+ /**
6
+ * Measures job queue latency. Currently, this functionality is not supported for BullMQ.
7
+ *
8
+ * @async
9
+ * @param {...any} args - Any number of arguments (ignored in function).
10
+ * @throws {JobQueueLatencyUnsupportedError} - Indicates that the module does not support job queue latency measurements.
11
+ * @returns {Promise<void>} - The function is asynchronous, but its return value is not used.
12
+ */
13
+ async function jobQueueLatency(...args) {
14
+ jobQueueLatencyUnsupported("BullMQ")
15
+ }
16
+
17
+ /**
18
+ * Calculates the total job queue size across specified queues.
19
+ *
20
+ * @async
21
+ * @param {...string} queues - Names of the queues for size measurement.
22
+ * @param {object} [options] - Optional options object. The options object can include a
23
+ * `connection` property, which is passed to IORedis and is compatible
24
+ * with its connection options. If no connection is provided, the
25
+ * function will use the value of the `REDIS_TLS_URL`, `REDIS_URL`,
26
+ * `REDISTOGO_URL`, `REDISCLOUD_URL`, `OPENREDIS_URL` environment
27
+ * variables, in the order specified. If none of these environment
28
+ * variables are set, it defaults to `redis://localhost:6379/0`.
29
+ * @returns {Promise<number>} Cumulative job queue size across the specified queues.
30
+ * @example
31
+ * // Calculate size across all queues
32
+ * await jobQueueSize()
33
+ * @example
34
+ * // Calculate size for the "default" queue
35
+ * await jobQueueSize("default")
36
+ * @example
37
+ * // Calculate size across "default" and "mailer" queues
38
+ * await jobQueueSize("default", "mailer")
39
+ * @example
40
+ * // Calculate Size using the options.connection property
41
+ * await jobQueueSize("default", { connection: "redis://localhost:6379/0" })
42
+ */
43
+ async function jobQueueSize(...args) {
44
+ let { queues, options } = unpack(args)
45
+
46
+ const redis = new IORedis(
47
+ options.connection ||
48
+ process.env.REDIS_TLS_URL ||
49
+ process.env.REDIS_URL ||
50
+ process.env.REDISTOGO_URL ||
51
+ process.env.REDISCLOUD_URL ||
52
+ process.env.OPENREDIS_URL ||
53
+ "redis://localhost:6379/0",
54
+ )
55
+
56
+ if (queues.length === 0) {
57
+ const pipeline = redis.pipeline()
58
+ pipeline.keys("bull:*:wait")
59
+ pipeline.keys("bull:*:active")
60
+ pipeline.keys("bull:*:delayed")
61
+
62
+ const results = await pipeline.exec()
63
+ const keys = results.flatMap(([err, result]) => result || [])
64
+ const uniqueQueueNames = new Set()
65
+
66
+ keys.forEach((key) => {
67
+ const match = key.match(/^bull:(.*):(wait|active|delayed)$/)
68
+ if (match) {
69
+ uniqueQueueNames.add(match[1])
70
+ }
71
+ })
72
+
73
+ queues = Array.from(uniqueQueueNames)
74
+ }
75
+
76
+ let totalCount = 0
77
+
78
+ try {
79
+ const pipeline = redis.pipeline()
80
+ const now = Date.now() * 0x1000 // Match BullMQ's delayed job timestamp score encoding.
81
+
82
+ for (const queue of queues) {
83
+ pipeline.lindex(`bull:${queue}:wait`, -1)
84
+ pipeline.llen(`bull:${queue}:wait`)
85
+ pipeline.llen(`bull:${queue}:active`)
86
+ pipeline.zcount(`bull:${queue}:delayed`, "-inf", now)
87
+ }
88
+
89
+ const results = await pipeline.exec()
90
+
91
+ for (let i = 0; i < results.length; i += 4) {
92
+ const lastWaitJob = results[i][1]
93
+ const waitCount = results[i + 1][1] || 0
94
+ const activeCount = results[i + 2][1] || 0
95
+ const delayedCount = results[i + 3][1] || 0
96
+
97
+ totalCount += waitCount + activeCount + delayedCount
98
+
99
+ if (lastWaitJob && lastWaitJob.startsWith("0:")) {
100
+ totalCount -= 1
101
+ }
102
+ }
103
+ } finally {
104
+ await redis.quit()
105
+ }
106
+
107
+ return totalCount
108
+ }
109
+
110
+ module.exports = { jobQueueLatency, jobQueueSize }
@@ -0,0 +1,20 @@
1
+ const { RequestInfo, request } = require("../middleware")
2
+
3
+ async function HireFireMiddlewareConnect(req, res, next) {
4
+ const response = await request(
5
+ new RequestInfo(
6
+ req.url,
7
+ req.headers["x-request-start"],
8
+ req.headers["hirefire-token"],
9
+ ),
10
+ )
11
+
12
+ if (response) {
13
+ res.writeHead(response.status, response.headers)
14
+ res.end(JSON.stringify(response.body))
15
+ } else {
16
+ next()
17
+ }
18
+ }
19
+
20
+ module.exports = HireFireMiddlewareConnect
@@ -0,0 +1,19 @@
1
+ const { RequestInfo, request } = require("../middleware")
2
+
3
+ async function HireFireMiddlewareExpress(req, res, next) {
4
+ const response = await request(
5
+ new RequestInfo(
6
+ req.path,
7
+ req.get("X-Request-Start"),
8
+ req.get("HireFire-Token"),
9
+ ),
10
+ )
11
+
12
+ if (response) {
13
+ res.status(response.status).set(response.headers).json(response.body)
14
+ } else {
15
+ next()
16
+ }
17
+ }
18
+
19
+ module.exports = HireFireMiddlewareExpress
@@ -0,0 +1,21 @@
1
+ const { RequestInfo, request } = require("../middleware")
2
+
3
+ async function HireFireMiddlewareKoa(ctx, next) {
4
+ const response = await request(
5
+ new RequestInfo(
6
+ ctx.path,
7
+ ctx.get("X-Request-Start"),
8
+ ctx.get("HireFire-Token"),
9
+ ),
10
+ )
11
+
12
+ if (response) {
13
+ ctx.status = response.status
14
+ ctx.set(response.headers)
15
+ ctx.body = response.body
16
+ } else {
17
+ await next()
18
+ }
19
+ }
20
+
21
+ module.exports = HireFireMiddlewareKoa
@@ -0,0 +1,69 @@
1
+ const HireFire = require(".")
2
+ const VERSION = require("./version")
3
+
4
+ class RequestInfo {
5
+ constructor(path, requestStartTime = null, token = null) {
6
+ this.path = path
7
+ this.requestStartTime = requestStartTime
8
+ ? parseInt(requestStartTime, 10)
9
+ : null
10
+ this.token = token
11
+ }
12
+ }
13
+
14
+ async function request(requestInfo) {
15
+ await processRequestQueueTime(requestInfo)
16
+
17
+ if (matchesHireFirePath(requestInfo) || matchesInfoPath(requestInfo)) {
18
+ return {
19
+ status: 200,
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ "Cache-Control": "must-revalidate, private, max-age=0",
23
+ "HireFire-Resource": `Node-${VERSION}`,
24
+ },
25
+ body: await Promise.all(
26
+ HireFire.configuration.workers.map(async (worker) => ({
27
+ name: worker.name,
28
+ value: await worker.value(),
29
+ })),
30
+ ),
31
+ }
32
+ }
33
+
34
+ return null
35
+ }
36
+
37
+ function matchesHireFirePath(requestInfo) {
38
+ return (
39
+ process.env.HIREFIRE_TOKEN &&
40
+ requestInfo.path === "/hirefire" &&
41
+ requestInfo.token === process.env.HIREFIRE_TOKEN
42
+ )
43
+ }
44
+
45
+ function matchesInfoPath(requestInfo) {
46
+ return (
47
+ process.env.HIREFIRE_TOKEN &&
48
+ requestInfo.path === `/hirefire/${process.env.HIREFIRE_TOKEN}/info`
49
+ )
50
+ }
51
+
52
+ async function processRequestQueueTime(requestInfo) {
53
+ if (
54
+ process.env.HIREFIRE_TOKEN &&
55
+ HireFire.configuration.web &&
56
+ requestInfo.requestStartTime
57
+ ) {
58
+ await HireFire.configuration.web.startDispatcher()
59
+ await HireFire.configuration.web.addToBuffer(
60
+ calculateRequestQueueTime(requestInfo),
61
+ )
62
+ }
63
+ }
64
+
65
+ function calculateRequestQueueTime(requestInfo) {
66
+ return Math.max(Date.now() - requestInfo.requestStartTime, 0)
67
+ }
68
+
69
+ module.exports = { RequestInfo, request }
package/src/utility.js ADDED
@@ -0,0 +1,22 @@
1
+ function unpack(args) {
2
+ const lastArg = args[args.length - 1]
3
+ let queues = []
4
+ let options = {}
5
+
6
+ if (
7
+ typeof lastArg === "object" &&
8
+ lastArg !== null &&
9
+ !Array.isArray(lastArg)
10
+ ) {
11
+ queues = args.slice(0, -1)
12
+ options = lastArg
13
+ } else {
14
+ queues = args
15
+ }
16
+
17
+ queues = queues.flat()
18
+
19
+ return { queues, options }
20
+ }
21
+
22
+ module.exports = { unpack }
package/src/version.js ADDED
@@ -0,0 +1,3 @@
1
+ const pkg = require("../package.json")
2
+
3
+ module.exports = pkg.version
package/src/web.js ADDED
@@ -0,0 +1,216 @@
1
+ const https = require("https")
2
+ const { Mutex } = require("async-mutex")
3
+ const VERSION = require("../src/version")
4
+
5
+ class DispatchError extends Error {
6
+ constructor(message) {
7
+ super(message)
8
+ this.name = "DispatchError"
9
+ }
10
+ }
11
+
12
+ class Web {
13
+ constructor(configuration) {
14
+ this._buffer = {}
15
+ this._mutex = new Mutex()
16
+ this._dispatcherRunning = false
17
+ this._configuration = configuration
18
+ this._dispatchInterval = 1
19
+ this._dispatchTimeout = 5
20
+ this._bufferTTL = 60
21
+ }
22
+
23
+ async startDispatcher() {
24
+ const release = await this._mutex.acquire()
25
+
26
+ try {
27
+ if (this._dispatcherRunning) return false
28
+ this._dispatcherRunning = true
29
+ } finally {
30
+ release()
31
+ }
32
+
33
+ this._logger.info("[HireFire] Starting web metrics dispatcher.")
34
+
35
+ this.dispatcher = setInterval(
36
+ this._dispatchBuffer.bind(this),
37
+ this._dispatchInterval * 1000,
38
+ )
39
+
40
+ return true
41
+ }
42
+
43
+ async stopDispatcher() {
44
+ const release = await this._mutex.acquire()
45
+
46
+ try {
47
+ if (!this._dispatcherRunning) return false
48
+ this._dispatcherRunning = false
49
+ clearInterval(this.dispatcher)
50
+ } finally {
51
+ release()
52
+ }
53
+
54
+ await this._flushBuffer()
55
+
56
+ this._logger.info("[HireFire] Web metrics dispatcher stopped.")
57
+
58
+ return true
59
+ }
60
+
61
+ dispatcherRunning() {
62
+ return this._dispatcherRunning
63
+ }
64
+
65
+ async addToBuffer(requestQueueTime) {
66
+ const release = await this._mutex.acquire()
67
+
68
+ try {
69
+ const timestamp = Math.floor(Date.now() / 1000)
70
+ this._buffer[timestamp] = this._buffer[timestamp] || []
71
+ this._buffer[timestamp].push(requestQueueTime)
72
+ } finally {
73
+ release()
74
+ }
75
+ }
76
+
77
+ async _flushBuffer() {
78
+ const release = await this._mutex.acquire()
79
+
80
+ try {
81
+ const currentBuffer = this._buffer
82
+ this._buffer = {}
83
+ return currentBuffer
84
+ } finally {
85
+ release()
86
+ }
87
+ }
88
+
89
+ async _dispatchBuffer() {
90
+ let buffer
91
+
92
+ try {
93
+ buffer = await this._flushBuffer()
94
+
95
+ if (Object.keys(buffer).length === 0) {
96
+ return
97
+ }
98
+
99
+ if (process.env.HIREFIRE_VERBOSE) {
100
+ this._logger.info(
101
+ `[HireFire] Dispatching web metrics: ${JSON.stringify(buffer)}`,
102
+ )
103
+ }
104
+
105
+ await this._submitBuffer(buffer)
106
+ } catch (error) {
107
+ await this._repopulateBuffer(buffer)
108
+ this._logger.error(
109
+ `[HireFire] Error while dispatching web metrics: ${error.message}`,
110
+ )
111
+ }
112
+ }
113
+
114
+ async _repopulateBuffer(buffer) {
115
+ const release = await this._mutex.acquire()
116
+
117
+ try {
118
+ const now = Math.floor(Date.now() / 1000)
119
+ Object.entries(buffer).forEach(([timestamp, requestQueueTimes]) => {
120
+ if (parseInt(timestamp) >= now - this._bufferTTL) {
121
+ this._buffer[timestamp] = this._buffer[timestamp] || []
122
+ this._buffer[timestamp].push(...requestQueueTimes)
123
+ }
124
+ })
125
+ } finally {
126
+ release()
127
+ }
128
+ }
129
+
130
+ async _submitBuffer(buffer) {
131
+ const hirefireToken = process.env.HIREFIRE_TOKEN
132
+
133
+ if (!hirefireToken) {
134
+ throw new DispatchError(
135
+ "The HIREFIRE_TOKEN environment variable is not set. Unable to submit " +
136
+ "Request Queue Time metric data. The HIREFIRE_TOKEN can be found in " +
137
+ "the HireFire Web UI in the web dyno manager settings.",
138
+ )
139
+ }
140
+
141
+ const data = JSON.stringify(buffer)
142
+ const dispatchUrl = (
143
+ process.env.HIREFIRE_DISPATCH_URL || "logdrain.hirefire.io"
144
+ ).replace(/^(https?:\/\/)/, "")
145
+ const options = {
146
+ hostname: dispatchUrl,
147
+ port: 443,
148
+ path: "/",
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/json",
152
+ "HireFire-Token": hirefireToken,
153
+ "HireFire-Resource": `Node-${VERSION}`,
154
+ "Content-Length": data.length,
155
+ },
156
+ }
157
+
158
+ return new Promise((resolve, reject) => {
159
+ const req = https.request(options, (res) => {
160
+ if (res.statusCode === 200) {
161
+ this._adjustParameters(res)
162
+ resolve()
163
+ } else if (res.statusCode >= 500) {
164
+ reject(
165
+ new DispatchError(
166
+ `Server responded with ${res.statusCode} status.`,
167
+ ),
168
+ )
169
+ } else {
170
+ reject(
171
+ new DispatchError(`Unexpected response code ${res.statusCode}.`),
172
+ )
173
+ }
174
+ })
175
+
176
+ req.on("error", (e) => {
177
+ if (e.code === "ETIMEDOUT" || e.code === "ESOCKETTIMEDOUT") {
178
+ reject(new DispatchError("Request timed out."))
179
+ } else {
180
+ reject(new DispatchError(`Network error occurred (${e.message}).`))
181
+ }
182
+ })
183
+
184
+ req.on("timeout", () => {
185
+ req.destroy()
186
+ reject(new DispatchError("Request timed out."))
187
+ })
188
+
189
+ req.setTimeout(this._dispatchTimeout * 1000)
190
+ req.write(data)
191
+ req.end()
192
+ })
193
+ }
194
+
195
+ _adjustParameters(res) {
196
+ if (res.headers["hirefire-resource-dispatcher-interval"]) {
197
+ this._dispatchInterval = parseInt(
198
+ res.headers["hirefire-resource-dispatcher-interval"],
199
+ )
200
+ }
201
+ if (res.headers["hirefire-resource-dispatcher-timeout"]) {
202
+ this._dispatchTimeout = parseInt(
203
+ res.headers["hirefire-resource-dispatcher-timeout"],
204
+ )
205
+ }
206
+ if (res.headers["hirefire-resource-buffer-ttl"]) {
207
+ this._bufferTTL = parseInt(res.headers["hirefire-resource-buffer-ttl"])
208
+ }
209
+ }
210
+
211
+ get _logger() {
212
+ return this._configuration.logger
213
+ }
214
+ }
215
+
216
+ module.exports = { Web, DispatchError }
package/src/worker.js ADDED
@@ -0,0 +1,45 @@
1
+ class InvalidDynoNameError extends Error {
2
+ constructor(message) {
3
+ super(message)
4
+ this.name = "InvalidDynoNameError"
5
+ }
6
+ }
7
+
8
+ class MissingDynoFnError extends Error {
9
+ constructor(message) {
10
+ super(message)
11
+ this.name = "MissingDynoFnError"
12
+ }
13
+ }
14
+
15
+ class Worker {
16
+ static PROCESS_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]{0,29}$/
17
+
18
+ constructor(name, fn) {
19
+ this._validate(name, fn)
20
+ this.name = name
21
+ this._fn = fn
22
+ }
23
+
24
+ async value() {
25
+ return this._fn()
26
+ }
27
+
28
+ _validate(name, fn) {
29
+ if (!Worker.PROCESS_NAME_PATTERN.test(name || "")) {
30
+ throw new InvalidDynoNameError(
31
+ `Invalid name for new Worker(${name}, fn). ` +
32
+ "Ensure it matches the Procfile process name (i.e. web, worker).",
33
+ )
34
+ }
35
+
36
+ if (!fn || typeof fn !== "function") {
37
+ throw new MissingDynoFnError(
38
+ `Missing function for new Worker(${name}, fn). ` +
39
+ "Ensure that you provide a function that returns the job queue metric.",
40
+ )
41
+ }
42
+ }
43
+ }
44
+
45
+ module.exports = { Worker, InvalidDynoNameError, MissingDynoFnError }