pg-boss 11.1.2 → 12.0.0-beta2

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/src/attorney.js DELETED
@@ -1,258 +0,0 @@
1
- const assert = require('node:assert')
2
- const { DEFAULT_SCHEMA } = require('./plans')
3
-
4
- const POLICY = {
5
- MAX_EXPIRATION_HOURS: 24,
6
- MIN_POLLING_INTERVAL_MS: 500,
7
- MAX_RETENTION_DAYS: 365
8
- }
9
-
10
- module.exports = {
11
- POLICY,
12
- getConfig,
13
- checkSendArgs,
14
- validateQueueArgs,
15
- checkWorkArgs,
16
- checkFetchArgs,
17
- assertPostgresObjectName,
18
- assertQueueName,
19
- assertKey
20
- }
21
-
22
- function validateQueueArgs (config = {}) {
23
- assert(!('deadLetter' in config) || config.deadLetter === null || (typeof config.deadLetter === 'string'), 'deadLetter must be a string')
24
- assert(!('deadLetter' in config) || config.deadLetter === null || /[\w-]/.test(config.deadLetter), 'deadLetter can only contain alphanumeric characters, underscores, or hyphens')
25
-
26
- validateRetryConfig(config)
27
- validateExpirationConfig(config)
28
- validateRetentionConfig(config)
29
- validateDeletionConfig(config)
30
- }
31
-
32
- function checkSendArgs (args) {
33
- let name, data, options
34
-
35
- if (typeof args[0] === 'string') {
36
- name = args[0]
37
- data = args[1]
38
-
39
- assert(typeof data !== 'function', 'send() cannot accept a function as the payload. Did you intend to use work()?')
40
-
41
- options = args[2]
42
- } else if (typeof args[0] === 'object') {
43
- assert(args.length === 1, 'send object API only accepts 1 argument')
44
-
45
- const job = args[0]
46
-
47
- assert(job, 'boss requires all jobs to have a name')
48
-
49
- name = job.name
50
- data = job.data
51
- options = job.options
52
- }
53
-
54
- options = options || {}
55
-
56
- assert(name, 'boss requires all jobs to have a queue name')
57
- assert(typeof options === 'object', 'options should be an object')
58
-
59
- options = { ...options }
60
-
61
- assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer')
62
- options.priority = options.priority || 0
63
-
64
- options.startAfter = (options.startAfter instanceof Date && typeof options.startAfter.toISOString === 'function')
65
- ? options.startAfter.toISOString()
66
- : (options.startAfter > 0)
67
- ? '' + options.startAfter
68
- : (typeof options.startAfter === 'string')
69
- ? options.startAfter
70
- : null
71
-
72
- validateRetryConfig(options)
73
- validateExpirationConfig(options)
74
- validateRetentionConfig(options)
75
- validateDeletionConfig(options)
76
-
77
- return { name, data, options }
78
- }
79
-
80
- function checkWorkArgs (name, args) {
81
- let options, callback
82
-
83
- assert(name, 'missing job name')
84
-
85
- if (args.length === 1) {
86
- callback = args[0]
87
- options = {}
88
- } else if (args.length > 1) {
89
- options = args[0] || {}
90
- callback = args[1]
91
- }
92
-
93
- assert(typeof callback === 'function', 'expected callback to be a function')
94
- assert(typeof options === 'object', 'expected config to be an object')
95
-
96
- options = { ...options }
97
-
98
- applyPollingInterval(options)
99
-
100
- assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
101
- assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
102
- assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean')
103
-
104
- options.batchSize = options.batchSize || 1
105
-
106
- return { options, callback }
107
- }
108
-
109
- function checkFetchArgs (name, options) {
110
- assert(name, 'missing queue name')
111
-
112
- assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
113
- assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
114
- assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean')
115
- assert(!('ignoreStartAfter' in options) || typeof options.ignoreStartAfter === 'boolean', 'ignoreStartAfter must be a boolean')
116
-
117
- options.batchSize = options.batchSize || 1
118
- }
119
-
120
- function getConfig (value) {
121
- assert(value && (typeof value === 'object' || typeof value === 'string'),
122
- 'configuration assert: string or config object is required to connect to postgres')
123
-
124
- const config = (typeof value === 'string')
125
- ? { connectionString: value }
126
- : { ...value }
127
-
128
- config.schedule = ('schedule' in config) ? config.schedule : true
129
- config.supervise = ('supervise' in config) ? config.supervise : true
130
- config.migrate = ('migrate' in config) ? config.migrate : true
131
-
132
- applySchemaConfig(config)
133
- applyOpsConfig(config)
134
- applyScheduleConfig(config)
135
- validateWarningConfig(config)
136
-
137
- return config
138
- }
139
-
140
- function applySchemaConfig (config) {
141
- if (config.schema) {
142
- assertPostgresObjectName(config.schema)
143
- }
144
-
145
- config.schema = config.schema || DEFAULT_SCHEMA
146
- }
147
-
148
- function validateWarningConfig (config) {
149
- assert(!('warningQueueSize' in config) || config.warningQueueSize >= 1,
150
- 'configuration assert: warningQueueSize must be at least 1')
151
-
152
- assert(!('warningSlowQuerySeconds' in config) || config.warningSlowQuerySeconds >= 1,
153
- 'configuration assert: warningSlowQuerySeconds must be at least 1')
154
- }
155
-
156
- function assertPostgresObjectName (name) {
157
- assert(typeof name === 'string', 'Name must be a string')
158
- assert(name.length <= 50, 'Name cannot exceed 50 characters')
159
- assert(!/\W/.test(name), 'Name can only contain alphanumeric characters or underscores')
160
- assert(!/^\d/.test(name), 'Name cannot start with a number')
161
- }
162
-
163
- function assertQueueName (name) {
164
- assert(name, 'Name is required')
165
- assert(typeof name === 'string', 'Name must be a string')
166
- assert(/[\w-]/.test(name), 'Name can only contain alphanumeric characters, underscores, or hyphens')
167
- }
168
-
169
- function assertKey (key) {
170
- if (!key) return
171
- assert(typeof key === 'string', 'Key must be a string')
172
- assert(/[\w-]/.test(key), 'Key can only contain alphanumeric characters, underscores, or hyphens')
173
- }
174
-
175
- function validateRetentionConfig (config) {
176
- assert(!('retentionSeconds' in config) || config.retentionSeconds >= 1,
177
- 'configuration assert: retentionSeconds must be at least every second')
178
- }
179
-
180
- function validateExpirationConfig (config) {
181
- assert(!('expireInSeconds' in config) || config.expireInSeconds >= 1,
182
- 'configuration assert: expireInSeconds must be at least every second')
183
-
184
- assert(!config.expireInSeconds || config.expireInSeconds / 60 / 60 < POLICY.MAX_EXPIRATION_HOURS, `configuration assert: expiration cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`)
185
- }
186
-
187
- function validateRetryConfig (config) {
188
- assert(!('retryDelay' in config) || (Number.isInteger(config.retryDelay) && config.retryDelay >= 0), 'retryDelay must be an integer >= 0')
189
- assert(!('retryLimit' in config) || (Number.isInteger(config.retryLimit) && config.retryLimit >= 0), 'retryLimit must be an integer >= 0')
190
- assert(!('retryBackoff' in config) || (config.retryBackoff === true || config.retryBackoff === false), 'retryBackoff must be either true or false')
191
- assert(!('retryDelayMax' in config) || config.retryDelayMax === null || config.retryBackoff === true, 'retryDelayMax can only be set if retryBackoff is true')
192
- assert(!('retryDelayMax' in config) || config.retryDelayMax === null || (Number.isInteger(config.retryDelayMax) && config.retryDelayMax >= 0), 'retryDelayMax must be an integer >= 0')
193
- }
194
-
195
- function applyPollingInterval (config) {
196
- assert(!('pollingIntervalSeconds' in config) || config.pollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1000,
197
- `configuration assert: pollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`)
198
-
199
- config.pollingInterval = ('pollingIntervalSeconds' in config)
200
- ? config.pollingIntervalSeconds * 1000
201
- : 2000
202
- }
203
-
204
- function applyOpsConfig (config) {
205
- assert(!('superviseIntervalSeconds' in config) || config.superviseIntervalSeconds >= 1,
206
- 'configuration assert: superviseIntervalSeconds must be at least every second')
207
-
208
- config.superviseIntervalSeconds = config.superviseIntervalSeconds || 60
209
-
210
- assert(config.superviseIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS,
211
- `configuration assert: superviseIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`)
212
-
213
- assert(!('maintenanceIntervalSeconds' in config) || config.maintenanceIntervalSeconds >= 1,
214
- 'configuration assert: maintenanceIntervalSeconds must be at least every second')
215
-
216
- config.maintenanceIntervalSeconds = config.maintenanceIntervalSeconds || POLICY.MAX_EXPIRATION_HOURS * 60 * 60
217
-
218
- assert(config.maintenanceIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS,
219
- `configuration assert: maintenanceIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`)
220
-
221
- assert(!('monitorIntervalSeconds' in config) || config.monitorIntervalSeconds >= 1,
222
- 'configuration assert: monitorIntervalSeconds must be at least every second')
223
-
224
- config.monitorIntervalSeconds = config.monitorIntervalSeconds || 60
225
-
226
- assert(config.monitorIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS,
227
- `configuration assert: monitorIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`)
228
-
229
- assert(!('queueCacheIntervalSeconds' in config) || config.queueCacheIntervalSeconds >= 1,
230
- 'configuration assert: queueCacheIntervalSeconds must be at least every second')
231
-
232
- config.queueCacheIntervalSeconds = config.queueCacheIntervalSeconds || 60
233
-
234
- assert(config.queueCacheIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS,
235
- `configuration assert: queueCacheIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`)
236
- }
237
-
238
- function validateDeletionConfig (config) {
239
- assert(!('deleteAfterSeconds' in config) || config.deleteAfterSeconds >= 1,
240
- 'configuration assert: deleteAfterSeconds must be at least every second')
241
- }
242
-
243
- function applyScheduleConfig (config) {
244
- assert(!('clockMonitorIntervalSeconds' in config) || (config.clockMonitorIntervalSeconds >= 1 && config.clockMonitorIntervalSeconds <= 600),
245
- 'configuration assert: clockMonitorIntervalSeconds must be between 1 second and 10 minutes')
246
-
247
- config.clockMonitorIntervalSeconds = config.clockMonitorIntervalSeconds || 600
248
-
249
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
250
- 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
251
-
252
- config.cronMonitorIntervalSeconds = config.cronMonitorIntervalSeconds || 30
253
-
254
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
255
- 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
256
-
257
- config.cronWorkerIntervalSeconds = config.cronWorkerIntervalSeconds || 5
258
- }
package/src/boss.js DELETED
@@ -1,158 +0,0 @@
1
- const EventEmitter = require('node:events')
2
- const plans = require('./plans')
3
- const { unwrapSQLResult } = require('./tools')
4
-
5
- const events = {
6
- error: 'error',
7
- warning: 'warning'
8
- }
9
-
10
- const WARNINGS = {
11
- SLOW_QUERY: { seconds: 30, message: 'Warning: slow query. Your queues and/or database server should be reviewed' },
12
- LARGE_QUEUE: { size: 10_000, mesasge: 'Warning: large queue backlog. Your queue should be reviewed' }
13
- }
14
-
15
- class Boss extends EventEmitter {
16
- #stopped
17
- #maintaining
18
- #superviseInterval
19
- #db
20
- #config
21
- #manager
22
-
23
- constructor (db, config) {
24
- super()
25
-
26
- this.#db = db
27
- this.#config = config
28
- this.#manager = config.manager
29
- this.#stopped = true
30
-
31
- this.events = events
32
- this.functions = [
33
- this.supervise
34
- ]
35
-
36
- if (config.warningSlowQuerySeconds) {
37
- WARNINGS.SLOW_QUERY.seconds = config.warningSlowQuerySeconds
38
- }
39
-
40
- if (config.warningQueueSize) {
41
- WARNINGS.LARGE_QUEUE.size = config.warningQueueSize
42
- }
43
- }
44
-
45
- async start () {
46
- if (this.#stopped) {
47
- this.#superviseInterval = setInterval(() => this.#onSupervise(), this.#config.superviseIntervalSeconds * 1000)
48
- this.#stopped = false
49
- }
50
- }
51
-
52
- async stop () {
53
- if (!this.#stopped) {
54
- if (this.#superviseInterval) clearInterval(this.#superviseInterval)
55
- this.#stopped = true
56
- }
57
- }
58
-
59
- async #executeSql (sql, values) {
60
- const started = Date.now()
61
-
62
- const result = unwrapSQLResult(await this.#db.executeSql(sql, values))
63
-
64
- const ended = Date.now()
65
-
66
- const elapsed = (ended - started) / 1000
67
-
68
- if (elapsed > WARNINGS.SLOW_QUERY.seconds || this.#config.__test__warn_slow_query) {
69
- this.emit(events.warning, { message: WARNINGS.SLOW_QUERY.message, data: { elapsed, sql, values } })
70
- }
71
-
72
- return result
73
- }
74
-
75
- async #onSupervise () {
76
- try {
77
- if (this.#stopped) return
78
- if (this.#maintaining) return
79
- if (this.#config.__test__throw_maint) throw new Error(this.#config.__test__throw_maint)
80
-
81
- this.#maintaining = true
82
-
83
- const queues = await this.#manager.getQueues()
84
-
85
- for (const queue of queues) {
86
- !this.#stopped && await this.supervise(queue)
87
- }
88
- } catch (err) {
89
- this.emit(events.error, err)
90
- } finally {
91
- this.#maintaining = false
92
- }
93
- }
94
-
95
- async supervise (value) {
96
- let queues
97
-
98
- if (!value) {
99
- queues = await this.#manager.getQueues()
100
- } if (typeof value === 'string') {
101
- queues = await this.#manager.getQueues(value)
102
- } else if (typeof value === 'object') {
103
- queues = [value]
104
- }
105
-
106
- const queueGroups = queues.reduce((acc, q) => {
107
- const { table } = q
108
- acc[table] = acc[table] || { table, queues: [] }
109
- acc[table].queues.push(q)
110
- return acc
111
- }, {})
112
-
113
- for (const queueGroup of Object.values(queueGroups)) {
114
- const { table, queues } = queueGroup
115
- const names = queues.map(i => i.name)
116
-
117
- while (names.length) {
118
- const chunk = names.splice(0, 100)
119
-
120
- await this.#monitor(table, chunk)
121
- await this.#maintain(table, chunk)
122
- }
123
- }
124
- }
125
-
126
- async #monitor (table, names) {
127
- const command = plans.trySetQueueMonitorTime(this.#config.schema, names, this.#config.monitorIntervalSeconds)
128
- const { rows } = await this.#executeSql(command)
129
-
130
- if (rows.length) {
131
- const queues = rows.map(q => q.name)
132
-
133
- const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues)
134
- const { rows: rowsCacheStats } = await this.#executeSql(cacheStatsSql)
135
- const warnings = rowsCacheStats.filter(i => i.queuedCount > (i.warningQueueSize || WARNINGS.LARGE_QUEUE.size))
136
-
137
- for (const warning of warnings) {
138
- this.emit(events.warning, { message: WARNINGS.LARGE_QUEUE.mesasge, data: warning })
139
- }
140
-
141
- const sql = plans.failJobsByTimeout(this.#config.schema, table, queues)
142
- await this.#executeSql(sql)
143
- }
144
- }
145
-
146
- async #maintain (table, names) {
147
- const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds)
148
- const { rows } = await this.#executeSql(command)
149
-
150
- if (rows.length) {
151
- const queues = rows.map(q => q.name)
152
- const sql = plans.deletion(this.#config.schema, table, queues)
153
- await this.#executeSql(sql)
154
- }
155
- }
156
- }
157
-
158
- module.exports = Boss
package/src/contractor.js DELETED
@@ -1,99 +0,0 @@
1
- const assert = require('node:assert')
2
- const plans = require('./plans')
3
- const { DEFAULT_SCHEMA } = plans
4
- const migrationStore = require('./migrationStore')
5
- const schemaVersion = require('../version.json').schema
6
-
7
- class Contractor {
8
- static constructionPlans (schema = DEFAULT_SCHEMA) {
9
- return plans.create(schema, schemaVersion)
10
- }
11
-
12
- static migrationPlans (schema = DEFAULT_SCHEMA, version = schemaVersion - 1) {
13
- return migrationStore.migrate(schema, version)
14
- }
15
-
16
- static rollbackPlans (schema = DEFAULT_SCHEMA, version = schemaVersion) {
17
- return migrationStore.rollback(schema, version)
18
- }
19
-
20
- constructor (db, config) {
21
- this.config = config
22
- this.db = db
23
- this.migrations = this.config.migrations || migrationStore.getAll(this.config.schema)
24
-
25
- // exported api to index
26
- this.functions = [
27
- this.schemaVersion,
28
- this.isInstalled
29
- ]
30
- }
31
-
32
- async schemaVersion () {
33
- const result = await this.db.executeSql(plans.getVersion(this.config.schema))
34
- return result.rows.length ? parseInt(result.rows[0].version) : null
35
- }
36
-
37
- async isInstalled () {
38
- const result = await this.db.executeSql(plans.versionTableExists(this.config.schema))
39
- return !!result.rows[0].name
40
- }
41
-
42
- async start () {
43
- const installed = await this.isInstalled()
44
-
45
- if (installed) {
46
- const version = await this.schemaVersion()
47
-
48
- if (schemaVersion > version) {
49
- await this.migrate(version)
50
- }
51
- } else {
52
- await this.create()
53
- }
54
- }
55
-
56
- async check () {
57
- const installed = await this.isInstalled()
58
-
59
- if (!installed) {
60
- throw new Error('pg-boss is not installed')
61
- }
62
-
63
- const version = await this.schemaVersion()
64
-
65
- if (schemaVersion !== version) {
66
- throw new Error('pg-boss database requires migrations')
67
- }
68
- }
69
-
70
- async create () {
71
- try {
72
- const commands = plans.create(this.config.schema, schemaVersion)
73
- await this.db.executeSql(commands)
74
- } catch (err) {
75
- assert(err.message.includes(plans.CREATE_RACE_MESSAGE), err)
76
- }
77
- }
78
-
79
- async migrate (version) {
80
- try {
81
- const commands = migrationStore.migrate(this.config, version, this.migrations)
82
- await this.db.executeSql(commands)
83
- } catch (err) {
84
- assert(err.message.includes(plans.MIGRATE_RACE_MESSAGE), err)
85
- }
86
- }
87
-
88
- async next (version) {
89
- const commands = migrationStore.next(this.config.schema, version, this.migrations)
90
- await this.db.executeSql(commands)
91
- }
92
-
93
- async rollback (version) {
94
- const commands = migrationStore.rollback(this.config.schema, version, this.migrations)
95
- await this.db.executeSql(commands)
96
- }
97
- }
98
-
99
- module.exports = Contractor
package/src/db.js DELETED
@@ -1,50 +0,0 @@
1
- const EventEmitter = require('node:events')
2
- const pg = require('pg')
3
-
4
- class Db extends EventEmitter {
5
- constructor (config) {
6
- super()
7
-
8
- config.application_name = config.application_name || 'pgboss'
9
- // config.maxUses = config.maxUses || 1000
10
-
11
- this.config = config
12
- this._pgbdb = true
13
- this.opened = false
14
- }
15
-
16
- events = {
17
- error: 'error'
18
- }
19
-
20
- async open () {
21
- this.pool = new pg.Pool(this.config)
22
- this.pool.on('error', error => this.emit('error', error))
23
- this.opened = true
24
- }
25
-
26
- async close () {
27
- if (!this.pool.ending) {
28
- this.opened = false
29
- await this.pool.end()
30
- }
31
- }
32
-
33
- async executeSql (text, values) {
34
- if (this.opened) {
35
- // if (this.config.debug === true) {
36
- // console.log(`${new Date().toISOString()}: DEBUG SQL`)
37
- // console.log(text)
38
-
39
- // if (values) {
40
- // console.log(`${new Date().toISOString()}: DEBUG VALUES`)
41
- // console.log(values)
42
- // }
43
- // }
44
-
45
- return await this.pool.query(text, values)
46
- }
47
- }
48
- }
49
-
50
- module.exports = Db