pg-boss 9.0.3 → 10.0.0-beta1

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/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  Queueing jobs in Node.js using PostgreSQL like a boss.
2
2
 
3
- [![PostgreSql Version](https://img.shields.io/badge/PostgreSQL-11+-blue.svg?maxAge=2592000)](http://www.postgresql.org)
4
3
  [![npm version](https://badge.fury.io/js/pg-boss.svg)](https://badge.fury.io/js/pg-boss)
5
- ![Build](https://github.com/timgit/pg-boss/actions/workflows/ci.yml/badge.svg?branch=master)
4
+ [![Build](https://github.com/timgit/pg-boss/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/timgit/pg-boss/actions/workflows/ci.yml)
6
5
  [![Coverage Status](https://coveralls.io/repos/github/timgit/pg-boss/badge.svg?branch=master)](https://coveralls.io/github/timgit/pg-boss?branch=master)
7
6
 
8
7
  ```js
@@ -42,16 +41,16 @@ This will likely cater the most to teams already familiar with the simplicity of
42
41
  * Backpressure-compatible polling workers
43
42
  * Cron scheduling
44
43
  * Pub/sub API for fan-out queue relationships
45
- * Deferral, retries (with exponential backoff), rate limiting, debouncing
46
- * Completion jobs for orchestrations/sagas
44
+ * Priority, deferral, retries (with exponential backoff), rate limiting, debouncing
47
45
  * Direct table access for bulk loads via COPY or INSERT
48
46
  * Multi-master compatible (for example, in a Kubernetes ReplicaSet)
47
+ * Dead letter queues
49
48
  * Automatic creation and migration of storage tables
50
49
  * Automatic maintenance operations to manage table growth
51
50
 
52
51
  ## Requirements
53
- * Node 16 or higher
54
- * PostgreSQL 11 or higher
52
+ * Node 18 or higher
53
+ * PostgreSQL 12 or higher
55
54
 
56
55
  ## Installation
57
56
 
@@ -73,23 +72,11 @@ To setup a development environment for this library:
73
72
  ```bash
74
73
  git clone https://github.com/timgit/pg-boss.git
75
74
  npm install
76
-
77
- ```
78
-
79
- To run the test suite you will need to pgboss access to an empty postgres database. You can set one up using the following commands on a local postgres instance:
80
-
81
- ```sql
82
- CREATE DATABASE pgboss;
83
- CREATE user postgres WITH PASSWORD 'postgres';
84
- GRANT ALL PRIVILEGES ON DATABASE pgboss to postgres;
85
- -- run the following command in the context of the pgboss database
86
- CREATE EXTENSION pgcrypto;
87
75
  ```
88
76
 
89
- If you use a different database name, username or password, or want to run the test suite against a database that is running on a remote machine then you will need to edit the `test/config.json` file with the appropriate connection values.
90
-
91
- You can then run the linter and test suite using
92
-
77
+ To run the test suite, linter and code coverage:
93
78
  ```bash
94
- npm test
79
+ npm run cover
95
80
  ```
81
+
82
+ The test suite will try and create a new database named pgboss. The [config.json](test/config.json) file has the default credentials to connect to postgres.
package/package.json CHANGED
@@ -1,19 +1,17 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "9.0.3",
3
+ "version": "10.0.0-beta1",
4
4
  "description": "Queueing jobs in Node.js using PostgreSQL like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
7
- "node": ">=16"
7
+ "node": ">=18"
8
8
  },
9
9
  "dependencies": {
10
10
  "cron-parser": "^4.0.0",
11
- "delay": "^5.0.0",
12
11
  "lodash.debounce": "^4.0.8",
13
12
  "p-map": "^4.0.0",
14
13
  "pg": "^8.5.1",
15
- "serialize-error": "^8.1.0",
16
- "uuid": "^9.0.0"
14
+ "serialize-error": "^8.1.0"
17
15
  },
18
16
  "devDependencies": {
19
17
  "@types/node": "^20.3.3",
@@ -25,9 +23,6 @@
25
23
  "scripts": {
26
24
  "test": "standard && mocha",
27
25
  "cover": "nyc npm test",
28
- "export-schema": "node ./scripts/construct.js",
29
- "export-migration": "node ./scripts/migrate.js",
30
- "export-rollback": "node ./scripts/rollback.js",
31
26
  "tsc": "tsc --noEmit types.d.ts",
32
27
  "readme": "node ./test/readme.js"
33
28
  },
package/src/attorney.js CHANGED
@@ -1,20 +1,20 @@
1
1
  const assert = require('assert')
2
- const { DEFAULT_SCHEMA, SINGLETON_QUEUE_KEY } = require('./plans')
2
+ const { DEFAULT_SCHEMA } = require('./plans')
3
3
 
4
4
  module.exports = {
5
5
  getConfig,
6
6
  checkSendArgs,
7
- checkInsertArgs,
7
+ checkQueueArgs,
8
8
  checkWorkArgs,
9
9
  checkFetchArgs,
10
- warnClockSkew
10
+ warnClockSkew,
11
+ queueNameHasPatternMatch,
12
+ assertPostgresObjectName
11
13
  }
12
14
 
15
+ const MAX_INTERVAL_HOURS = 24
16
+
13
17
  const WARNINGS = {
14
- EXPIRE_IN_REMOVED: {
15
- message: '\'expireIn\' option detected. This option has been removed. Use expireInSeconds, expireInMinutes or expireInHours.',
16
- code: 'pg-boss-w01'
17
- },
18
18
  CLOCK_SKEW: {
19
19
  message: 'Timekeeper detected clock skew between this instance and the database server. This will not affect scheduling operations, but this warning is shown any time the skew exceeds 60 seconds.',
20
20
  code: 'pg-boss-w02'
@@ -22,9 +22,25 @@ const WARNINGS = {
22
22
  CRON_DISABLED: {
23
23
  message: 'Archive interval is set less than 60s. Cron processing is disabled.',
24
24
  code: 'pg-boss-w03'
25
+ },
26
+ ON_COMPLETE_REMOVED: {
27
+ message: '\'onComplete\' option detected. This option has been removed. Consider deadLetter if needed.',
28
+ code: 'pg-boss-w04'
25
29
  }
26
30
  }
27
31
 
32
+ function checkQueueArgs (name, options = {}) {
33
+ assertPostgresObjectName(name)
34
+
35
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
36
+
37
+ applyRetryConfig(options)
38
+ applyExpirationConfig(options)
39
+ applyRetentionConfig(options)
40
+
41
+ return options
42
+ }
43
+
28
44
  function checkSendArgs (args, defaults) {
29
45
  let name, data, options
30
46
 
@@ -57,11 +73,11 @@ function checkSendArgs (args, defaults) {
57
73
  assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer')
58
74
  options.priority = options.priority || 0
59
75
 
76
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
77
+
60
78
  applyRetryConfig(options, defaults)
61
79
  applyExpirationConfig(options, defaults)
62
80
  applyRetentionConfig(options, defaults)
63
- applyCompletionConfig(options, defaults)
64
- applySingletonKeyConfig(options)
65
81
 
66
82
  const { startAfter, singletonSeconds, singletonMinutes, singletonHours } = options
67
83
 
@@ -83,23 +99,11 @@ function checkSendArgs (args, defaults) {
83
99
 
84
100
  assert(!singletonSeconds || singletonSeconds <= defaults.archiveSeconds, `throttling interval ${singletonSeconds}s cannot exceed archive interval ${defaults.archiveSeconds}s`)
85
101
 
86
- return { name, data, options }
87
- }
88
-
89
- function checkInsertArgs (jobs) {
90
- assert(Array.isArray(jobs), `jobs argument should be an array. Received '${typeof jobs}'`)
91
- return jobs.map(job => {
92
- job = { ...job }
93
- applySingletonKeyConfig(job)
94
- return job
95
- })
96
- }
97
-
98
- function applySingletonKeyConfig (options) {
99
- if (options.singletonKey && options.useSingletonQueue && options.singletonKey !== SINGLETON_QUEUE_KEY) {
100
- options.singletonKey = SINGLETON_QUEUE_KEY + options.singletonKey
102
+ if (options.onComplete) {
103
+ emitWarning(WARNINGS.ON_COMPLETE_REMOVED)
101
104
  }
102
- delete options.useSingletonQueue
105
+
106
+ return { name, data, options }
103
107
  }
104
108
 
105
109
  function checkWorkArgs (name, args, defaults) {
@@ -129,7 +133,6 @@ function checkWorkArgs (name, args, defaults) {
129
133
  assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
130
134
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
131
135
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
132
- assert(!('enforceSingletonQueueActiveLimit' in options) || typeof options.enforceSingletonQueueActiveLimit === 'boolean', 'enforceSingletonQueueActiveLimit must be a boolean')
133
136
 
134
137
  return { options, callback }
135
138
  }
@@ -137,11 +140,12 @@ function checkWorkArgs (name, args, defaults) {
137
140
  function checkFetchArgs (name, batchSize, options) {
138
141
  assert(name, 'missing queue name')
139
142
 
140
- name = sanitizeQueueNameForFetch(name)
143
+ if (queueNameHasPatternMatch(name)) {
144
+ name = sanitizeQueueNameForFetch(name)
145
+ }
141
146
 
142
147
  assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
143
148
  assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean')
144
- assert(!('enforceSingletonQueueActiveLimit' in options) || typeof options.enforceSingletonQueueActiveLimit === 'boolean', 'enforceSingletonQueueActiveLimit must be a boolean')
145
149
 
146
150
  return { name }
147
151
  }
@@ -150,6 +154,10 @@ function sanitizeQueueNameForFetch (name) {
150
154
  return name.replace(/[%_*]/g, match => match === '*' ? '%' : '\\' + match)
151
155
  }
152
156
 
157
+ function queueNameHasPatternMatch (name) {
158
+ return name.includes('*')
159
+ }
160
+
153
161
  function getConfig (value) {
154
162
  assert(value && (typeof value === 'object' || typeof value === 'string'),
155
163
  'configuration assert: string or config object is required to connect to postgres')
@@ -158,32 +166,35 @@ function getConfig (value) {
158
166
  ? { connectionString: value }
159
167
  : { ...value }
160
168
 
161
- applyDatabaseConfig(config)
169
+ applySchemaConfig(config)
162
170
  applyMaintenanceConfig(config)
163
171
  applyArchiveConfig(config)
164
172
  applyArchiveFailedConfig(config)
165
173
  applyDeleteConfig(config)
166
174
  applyMonitoringConfig(config)
167
- applyUuidConfig(config)
168
175
 
169
176
  applyNewJobCheckInterval(config)
170
177
  applyExpirationConfig(config)
171
178
  applyRetentionConfig(config)
172
- applyCompletionConfig(config)
173
179
 
174
180
  return config
175
181
  }
176
182
 
177
- function applyDatabaseConfig (config) {
183
+ function applySchemaConfig (config) {
178
184
  if (config.schema) {
179
- assert(typeof config.schema === 'string', 'configuration assert: schema must be a string')
180
- assert(config.schema.length <= 50, 'configuration assert: schema name cannot exceed 50 characters')
181
- assert(!/\W/.test(config.schema), `configuration assert: ${config.schema} cannot be used as a schema. Only alphanumeric characters and underscores are allowed`)
185
+ assertPostgresObjectName(config.schema)
182
186
  }
183
187
 
184
188
  config.schema = config.schema || DEFAULT_SCHEMA
185
189
  }
186
190
 
191
+ function assertPostgresObjectName (name) {
192
+ assert(typeof name === 'string', 'Name must be a string')
193
+ assert(name.length <= 50, 'Name cannot exceed 50 characters')
194
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters and underscores')
195
+ assert(!/^d/.test(name), 'Name cannot start with a number')
196
+ }
197
+
187
198
  function applyArchiveConfig (config) {
188
199
  const ARCHIVE_DEFAULT = 60 * 60 * 12
189
200
 
@@ -211,18 +222,7 @@ function applyArchiveFailedConfig (config) {
211
222
  }
212
223
  }
213
224
 
214
- function applyCompletionConfig (config, defaults) {
215
- assert(!('onComplete' in config) || config.onComplete === true || config.onComplete === false,
216
- 'configuration assert: onComplete must be either true or false')
217
-
218
- if (!('onComplete' in config)) {
219
- config.onComplete = defaults
220
- ? defaults.onComplete
221
- : false
222
- }
223
- }
224
-
225
- function applyRetentionConfig (config, defaults) {
225
+ function applyRetentionConfig (config, defaults = {}) {
226
226
  assert(!('retentionSeconds' in config) || config.retentionSeconds >= 1,
227
227
  'configuration assert: retentionSeconds must be at least every second')
228
228
 
@@ -243,18 +243,13 @@ function applyRetentionConfig (config, defaults) {
243
243
  ? `${config.retentionMinutes} minutes`
244
244
  : ('retentionSeconds' in config)
245
245
  ? `${config.retentionSeconds} seconds`
246
- : defaults
247
- ? defaults.keepUntil
248
- : '14 days'
246
+ : null
249
247
 
250
248
  config.keepUntil = keepUntil
249
+ config.keepUntilDefault = defaults?.keepUntil
251
250
  }
252
251
 
253
- function applyExpirationConfig (config, defaults) {
254
- if ('expireIn' in config) {
255
- emitWarning(WARNINGS.EXPIRE_IN_REMOVED)
256
- }
257
-
252
+ function applyExpirationConfig (config, defaults = {}) {
258
253
  assert(!('expireInSeconds' in config) || config.expireInSeconds >= 1,
259
254
  'configuration assert: expireInSeconds must be at least every second')
260
255
 
@@ -265,16 +260,17 @@ function applyExpirationConfig (config, defaults) {
265
260
  'configuration assert: expireInHours must be at least every hour')
266
261
 
267
262
  const expireIn = ('expireInHours' in config)
268
- ? `${config.expireInHours} hours`
263
+ ? config.expireInHours * 60 * 60
269
264
  : ('expireInMinutes' in config)
270
- ? `${config.expireInMinutes} minutes`
265
+ ? config.expireInMinutes * 60
271
266
  : ('expireInSeconds' in config)
272
- ? `${config.expireInSeconds} seconds`
273
- : defaults
274
- ? defaults.expireIn
275
- : '15 minutes'
267
+ ? config.expireInSeconds
268
+ : null
269
+
270
+ assert(!expireIn || expireIn / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: expiration cannot exceed ${MAX_INTERVAL_HOURS} hours`)
276
271
 
277
272
  config.expireIn = expireIn
273
+ config.expireInDefault = defaults?.expireIn
278
274
  }
279
275
 
280
276
  function applyRetryConfig (config, defaults) {
@@ -282,35 +278,23 @@ function applyRetryConfig (config, defaults) {
282
278
  assert(!('retryLimit' in config) || (Number.isInteger(config.retryLimit) && config.retryLimit >= 0), 'retryLimit must be an integer >= 0')
283
279
  assert(!('retryBackoff' in config) || (config.retryBackoff === true || config.retryBackoff === false), 'retryBackoff must be either true or false')
284
280
 
285
- if (defaults) {
286
- config.retryDelay = config.retryDelay || defaults.retryDelay
287
- config.retryLimit = config.retryLimit || defaults.retryLimit
288
- config.retryBackoff = config.retryBackoff || defaults.retryBackoff
289
- }
290
-
291
- config.retryDelay = config.retryDelay || 0
292
- config.retryLimit = config.retryLimit || 0
293
- config.retryBackoff = !!config.retryBackoff
294
- config.retryDelay = (config.retryBackoff && !config.retryDelay) ? 1 : config.retryDelay
295
- config.retryLimit = (config.retryDelay && !config.retryLimit) ? 1 : config.retryLimit
281
+ config.retryDelayDefault = defaults?.retryDelay
282
+ config.retryLimitDefault = defaults?.retryLimit
283
+ config.retryBackoffDefault = defaults?.retryBackoff
296
284
  }
297
285
 
298
286
  function applyNewJobCheckInterval (config, defaults) {
299
- const second = 1000
300
-
301
- assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 100,
302
- 'configuration assert: newJobCheckInterval must be at least every 100ms')
287
+ assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 500,
288
+ 'configuration assert: newJobCheckInterval must be at least every 500ms')
303
289
 
304
290
  assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
305
291
  'configuration assert: newJobCheckIntervalSeconds must be at least every second')
306
292
 
307
293
  config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
308
- ? config.newJobCheckIntervalSeconds * second
294
+ ? config.newJobCheckIntervalSeconds * 1000
309
295
  : ('newJobCheckInterval' in config)
310
296
  ? config.newJobCheckInterval
311
- : defaults
312
- ? defaults.newJobCheckInterval
313
- : second * 2
297
+ : defaults?.newJobCheckInterval || 2000
314
298
  }
315
299
 
316
300
  function applyMaintenanceConfig (config) {
@@ -325,6 +309,12 @@ function applyMaintenanceConfig (config) {
325
309
  : ('maintenanceIntervalSeconds' in config)
326
310
  ? config.maintenanceIntervalSeconds
327
311
  : 120
312
+
313
+ assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
314
+
315
+ config.schedule = ('schedule' in config) ? config.schedule : true
316
+ config.supervise = ('supervise' in config) ? config.supervise : true
317
+ config.migrate = ('migrate' in config) ? config.migrate : true
328
318
  }
329
319
 
330
320
  function applyDeleteConfig (config) {
@@ -367,6 +357,10 @@ function applyMonitoringConfig (config) {
367
357
  ? config.monitorStateIntervalSeconds
368
358
  : null
369
359
 
360
+ if (config.monitorStateIntervalSeconds) {
361
+ assert(config.monitorStateIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: state monitoring interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
362
+ }
363
+
370
364
  const TEN_MINUTES_IN_SECONDS = 600
371
365
 
372
366
  assert(!('clockMonitorIntervalSeconds' in config) || (config.clockMonitorIntervalSeconds >= 1 && config.clockMonitorIntervalSeconds <= TEN_MINUTES_IN_SECONDS),
@@ -399,11 +393,6 @@ function applyMonitoringConfig (config) {
399
393
  : 4
400
394
  }
401
395
 
402
- function applyUuidConfig (config) {
403
- assert(!('uuid' in config) || config.uuid === 'v1' || config.uuid === 'v4', 'configuration assert: uuid option only supports v1 or v4')
404
- config.uuid = config.uuid || 'v4'
405
- }
406
-
407
396
  function warnClockSkew (message) {
408
397
  emitWarning(WARNINGS.CLOCK_SKEW, message, { force: true })
409
398
  }