pg-boss 9.0.3 → 10.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/README.md CHANGED
@@ -1,8 +1,7 @@
1
- Queueing jobs in Node.js using PostgreSQL like a boss.
1
+ Queueing jobs in Postgres from Node.js 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,14 @@ 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
47
- * Direct table access for bulk loads via COPY or INSERT
44
+ * Priority queues, deferral, retries (with exponential backoff), rate limiting, debouncing
45
+ * Table operations via SQL for bulk loads via COPY or INSERT
48
46
  * Multi-master compatible (for example, in a Kubernetes ReplicaSet)
49
- * Automatic creation and migration of storage tables
50
- * Automatic maintenance operations to manage table growth
47
+ * Dead letter queues
51
48
 
52
49
  ## Requirements
53
- * Node 16 or higher
54
- * PostgreSQL 11 or higher
50
+ * Node 20 or higher
51
+ * PostgreSQL 12 or higher
55
52
 
56
53
  ## Installation
57
54
 
@@ -67,29 +64,16 @@ yarn add pg-boss
67
64
  * [Docs](docs/readme.md)
68
65
 
69
66
  ## Contributing
70
-
71
67
  To setup a development environment for this library:
72
68
 
73
69
  ```bash
74
70
  git clone https://github.com/timgit/pg-boss.git
75
71
  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
72
  ```
88
73
 
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
-
74
+ To run the test suite, linter and code coverage:
93
75
  ```bash
94
- npm test
76
+ npm run cover
95
77
  ```
78
+
79
+ 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,35 +1,31 @@
1
1
  {
2
2
  "name": "pg-boss",
3
- "version": "9.0.3",
4
- "description": "Queueing jobs in Node.js using PostgreSQL like a boss",
3
+ "version": "10.0.0-beta2",
4
+ "description": "Queueing jobs in Postgres from Node.js like a boss",
5
5
  "main": "./src/index.js",
6
6
  "engines": {
7
- "node": ">=16"
7
+ "node": ">=20"
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",
20
18
  "luxon": "^3.0.1",
21
19
  "mocha": "^10.0.0",
22
- "nyc": "^15.1.0",
20
+ "nyc": "^17.0.0",
23
21
  "standard": "^17.0.0"
24
22
  },
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
- "readme": "node ./test/readme.js"
27
+ "readme": "node ./test/readme.js",
28
+ "migrate": "node -e 'console.log(require(\"./src\").getMigrationPlans())'"
33
29
  },
34
30
  "mocha": {
35
31
  "timeout": 10000,
package/src/attorney.js CHANGED
@@ -1,20 +1,19 @@
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
+ assertPostgresObjectName
11
12
  }
12
13
 
14
+ const MAX_INTERVAL_HOURS = 24
15
+
13
16
  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
17
  CLOCK_SKEW: {
19
18
  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
19
  code: 'pg-boss-w02'
@@ -22,9 +21,25 @@ const WARNINGS = {
22
21
  CRON_DISABLED: {
23
22
  message: 'Archive interval is set less than 60s. Cron processing is disabled.',
24
23
  code: 'pg-boss-w03'
24
+ },
25
+ ON_COMPLETE_REMOVED: {
26
+ message: '\'onComplete\' option detected. This option has been removed. Consider deadLetter if needed.',
27
+ code: 'pg-boss-w04'
25
28
  }
26
29
  }
27
30
 
31
+ function checkQueueArgs (name, options = {}) {
32
+ assertPostgresObjectName(name)
33
+
34
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
35
+
36
+ applyRetryConfig(options)
37
+ applyExpirationConfig(options)
38
+ applyRetentionConfig(options)
39
+
40
+ return options
41
+ }
42
+
28
43
  function checkSendArgs (args, defaults) {
29
44
  let name, data, options
30
45
 
@@ -57,11 +72,11 @@ function checkSendArgs (args, defaults) {
57
72
  assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer')
58
73
  options.priority = options.priority || 0
59
74
 
75
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
76
+
60
77
  applyRetryConfig(options, defaults)
61
78
  applyExpirationConfig(options, defaults)
62
79
  applyRetentionConfig(options, defaults)
63
- applyCompletionConfig(options, defaults)
64
- applySingletonKeyConfig(options)
65
80
 
66
81
  const { startAfter, singletonSeconds, singletonMinutes, singletonHours } = options
67
82
 
@@ -83,23 +98,11 @@ function checkSendArgs (args, defaults) {
83
98
 
84
99
  assert(!singletonSeconds || singletonSeconds <= defaults.archiveSeconds, `throttling interval ${singletonSeconds}s cannot exceed archive interval ${defaults.archiveSeconds}s`)
85
100
 
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
101
+ if (options.onComplete) {
102
+ emitWarning(WARNINGS.ON_COMPLETE_REMOVED)
101
103
  }
102
- delete options.useSingletonQueue
104
+
105
+ return { name, data, options }
103
106
  }
104
107
 
105
108
  function checkWorkArgs (name, args, defaults) {
@@ -129,7 +132,6 @@ function checkWorkArgs (name, args, defaults) {
129
132
  assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
130
133
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
131
134
  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
135
 
134
136
  return { options, callback }
135
137
  }
@@ -137,19 +139,12 @@ function checkWorkArgs (name, args, defaults) {
137
139
  function checkFetchArgs (name, batchSize, options) {
138
140
  assert(name, 'missing queue name')
139
141
 
140
- name = sanitizeQueueNameForFetch(name)
141
-
142
142
  assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
143
143
  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
144
 
146
145
  return { name }
147
146
  }
148
147
 
149
- function sanitizeQueueNameForFetch (name) {
150
- return name.replace(/[%_*]/g, match => match === '*' ? '%' : '\\' + match)
151
- }
152
-
153
148
  function getConfig (value) {
154
149
  assert(value && (typeof value === 'object' || typeof value === 'string'),
155
150
  'configuration assert: string or config object is required to connect to postgres')
@@ -158,32 +153,35 @@ function getConfig (value) {
158
153
  ? { connectionString: value }
159
154
  : { ...value }
160
155
 
161
- applyDatabaseConfig(config)
156
+ applySchemaConfig(config)
162
157
  applyMaintenanceConfig(config)
163
158
  applyArchiveConfig(config)
164
159
  applyArchiveFailedConfig(config)
165
160
  applyDeleteConfig(config)
166
161
  applyMonitoringConfig(config)
167
- applyUuidConfig(config)
168
162
 
169
163
  applyNewJobCheckInterval(config)
170
164
  applyExpirationConfig(config)
171
165
  applyRetentionConfig(config)
172
- applyCompletionConfig(config)
173
166
 
174
167
  return config
175
168
  }
176
169
 
177
- function applyDatabaseConfig (config) {
170
+ function applySchemaConfig (config) {
178
171
  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`)
172
+ assertPostgresObjectName(config.schema)
182
173
  }
183
174
 
184
175
  config.schema = config.schema || DEFAULT_SCHEMA
185
176
  }
186
177
 
178
+ function assertPostgresObjectName (name) {
179
+ assert(typeof name === 'string', 'Name must be a string')
180
+ assert(name.length <= 50, 'Name cannot exceed 50 characters')
181
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters and underscores')
182
+ assert(!/^d/.test(name), 'Name cannot start with a number')
183
+ }
184
+
187
185
  function applyArchiveConfig (config) {
188
186
  const ARCHIVE_DEFAULT = 60 * 60 * 12
189
187
 
@@ -211,18 +209,7 @@ function applyArchiveFailedConfig (config) {
211
209
  }
212
210
  }
213
211
 
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) {
212
+ function applyRetentionConfig (config, defaults = {}) {
226
213
  assert(!('retentionSeconds' in config) || config.retentionSeconds >= 1,
227
214
  'configuration assert: retentionSeconds must be at least every second')
228
215
 
@@ -243,18 +230,13 @@ function applyRetentionConfig (config, defaults) {
243
230
  ? `${config.retentionMinutes} minutes`
244
231
  : ('retentionSeconds' in config)
245
232
  ? `${config.retentionSeconds} seconds`
246
- : defaults
247
- ? defaults.keepUntil
248
- : '14 days'
233
+ : null
249
234
 
250
235
  config.keepUntil = keepUntil
236
+ config.keepUntilDefault = defaults?.keepUntil
251
237
  }
252
238
 
253
- function applyExpirationConfig (config, defaults) {
254
- if ('expireIn' in config) {
255
- emitWarning(WARNINGS.EXPIRE_IN_REMOVED)
256
- }
257
-
239
+ function applyExpirationConfig (config, defaults = {}) {
258
240
  assert(!('expireInSeconds' in config) || config.expireInSeconds >= 1,
259
241
  'configuration assert: expireInSeconds must be at least every second')
260
242
 
@@ -265,16 +247,17 @@ function applyExpirationConfig (config, defaults) {
265
247
  'configuration assert: expireInHours must be at least every hour')
266
248
 
267
249
  const expireIn = ('expireInHours' in config)
268
- ? `${config.expireInHours} hours`
250
+ ? config.expireInHours * 60 * 60
269
251
  : ('expireInMinutes' in config)
270
- ? `${config.expireInMinutes} minutes`
252
+ ? config.expireInMinutes * 60
271
253
  : ('expireInSeconds' in config)
272
- ? `${config.expireInSeconds} seconds`
273
- : defaults
274
- ? defaults.expireIn
275
- : '15 minutes'
254
+ ? config.expireInSeconds
255
+ : null
256
+
257
+ assert(!expireIn || expireIn / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: expiration cannot exceed ${MAX_INTERVAL_HOURS} hours`)
276
258
 
277
259
  config.expireIn = expireIn
260
+ config.expireInDefault = defaults?.expireIn
278
261
  }
279
262
 
280
263
  function applyRetryConfig (config, defaults) {
@@ -282,35 +265,23 @@ function applyRetryConfig (config, defaults) {
282
265
  assert(!('retryLimit' in config) || (Number.isInteger(config.retryLimit) && config.retryLimit >= 0), 'retryLimit must be an integer >= 0')
283
266
  assert(!('retryBackoff' in config) || (config.retryBackoff === true || config.retryBackoff === false), 'retryBackoff must be either true or false')
284
267
 
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
268
+ config.retryDelayDefault = defaults?.retryDelay
269
+ config.retryLimitDefault = defaults?.retryLimit
270
+ config.retryBackoffDefault = defaults?.retryBackoff
296
271
  }
297
272
 
298
273
  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')
274
+ assert(!('newJobCheckInterval' in config) || config.newJobCheckInterval >= 500,
275
+ 'configuration assert: newJobCheckInterval must be at least every 500ms')
303
276
 
304
277
  assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
305
278
  'configuration assert: newJobCheckIntervalSeconds must be at least every second')
306
279
 
307
280
  config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
308
- ? config.newJobCheckIntervalSeconds * second
281
+ ? config.newJobCheckIntervalSeconds * 1000
309
282
  : ('newJobCheckInterval' in config)
310
283
  ? config.newJobCheckInterval
311
- : defaults
312
- ? defaults.newJobCheckInterval
313
- : second * 2
284
+ : defaults?.newJobCheckInterval || 2000
314
285
  }
315
286
 
316
287
  function applyMaintenanceConfig (config) {
@@ -325,6 +296,12 @@ function applyMaintenanceConfig (config) {
325
296
  : ('maintenanceIntervalSeconds' in config)
326
297
  ? config.maintenanceIntervalSeconds
327
298
  : 120
299
+
300
+ assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
301
+
302
+ config.schedule = ('schedule' in config) ? config.schedule : true
303
+ config.supervise = ('supervise' in config) ? config.supervise : true
304
+ config.migrate = ('migrate' in config) ? config.migrate : true
328
305
  }
329
306
 
330
307
  function applyDeleteConfig (config) {
@@ -367,6 +344,10 @@ function applyMonitoringConfig (config) {
367
344
  ? config.monitorStateIntervalSeconds
368
345
  : null
369
346
 
347
+ if (config.monitorStateIntervalSeconds) {
348
+ assert(config.monitorStateIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: state monitoring interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
349
+ }
350
+
370
351
  const TEN_MINUTES_IN_SECONDS = 600
371
352
 
372
353
  assert(!('clockMonitorIntervalSeconds' in config) || (config.clockMonitorIntervalSeconds >= 1 && config.clockMonitorIntervalSeconds <= TEN_MINUTES_IN_SECONDS),
@@ -399,11 +380,6 @@ function applyMonitoringConfig (config) {
399
380
  : 4
400
381
  }
401
382
 
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
383
  function warnClockSkew (message) {
408
384
  emitWarning(WARNINGS.CLOCK_SKEW, message, { force: true })
409
385
  }