pg-boss 9.0.3 → 10.0.0-beta10

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
@@ -10,24 +9,19 @@ async function readme() {
10
9
  const PgBoss = require('pg-boss');
11
10
  const boss = new PgBoss('postgres://user:pass@host/database');
12
11
 
13
- boss.on('error', error => console.error(error));
12
+ boss.on('error', console.error)
14
13
 
15
- await boss.start();
14
+ await boss.start()
16
15
 
17
- const queue = 'some-queue';
16
+ const queue = 'readme-queue'
18
17
 
19
- let jobId = await boss.send(queue, { param1: 'foo' })
18
+ const id = await boss.send(queue, { arg1: 'read me' })
20
19
 
21
- console.log(`created job in queue ${queue}: ${jobId}`);
20
+ console.log(`created job ${id} in queue ${queue}`)
22
21
 
23
- await boss.work(queue, someAsyncJobHandler);
24
- }
25
-
26
- async function someAsyncJobHandler(job) {
27
- console.log(`job ${job.id} received with data:`);
28
- console.log(JSON.stringify(job.data));
29
-
30
- await doSomethingAsyncWithThis(job.data);
22
+ await boss.work(queue, async job => {
23
+ console.log(`received job ${job.id} with data ${JSON.stringify(job.data)}`)
24
+ })
31
25
  }
32
26
  ```
33
27
 
@@ -42,16 +36,14 @@ This will likely cater the most to teams already familiar with the simplicity of
42
36
  * Backpressure-compatible polling workers
43
37
  * Cron scheduling
44
38
  * 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
39
+ * Priority queues, deferral, retries (with exponential backoff), rate limiting, debouncing
40
+ * Table operations via SQL for bulk loads via COPY or INSERT
48
41
  * 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
42
+ * Dead letter queues
51
43
 
52
44
  ## Requirements
53
- * Node 16 or higher
54
- * PostgreSQL 11 or higher
45
+ * Node 20 or higher
46
+ * PostgreSQL 13 or higher
55
47
 
56
48
  ## Installation
57
49
 
@@ -67,29 +59,16 @@ yarn add pg-boss
67
59
  * [Docs](docs/readme.md)
68
60
 
69
61
  ## Contributing
70
-
71
62
  To setup a development environment for this library:
72
63
 
73
64
  ```bash
74
65
  git clone https://github.com/timgit/pg-boss.git
75
66
  npm install
76
-
77
67
  ```
78
68
 
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
- ```
88
-
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
-
69
+ To run the test suite, linter and code coverage:
93
70
  ```bash
94
- npm test
71
+ npm run cover
95
72
  ```
73
+
74
+ 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-beta10",
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,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
+ assertPostgresObjectName,
12
+ assertQueueName
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,23 @@ 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
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
34
+
35
+ applyRetryConfig(options)
36
+ applyExpirationConfig(options)
37
+ applyRetentionConfig(options)
38
+
39
+ return options
40
+ }
41
+
28
42
  function checkSendArgs (args, defaults) {
29
43
  let name, data, options
30
44
 
@@ -57,11 +71,11 @@ function checkSendArgs (args, defaults) {
57
71
  assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer')
58
72
  options.priority = options.priority || 0
59
73
 
74
+ assert(!('deadLetter' in options) || (typeof options.deadLetter === 'string'), 'deadLetter must be a string')
75
+
60
76
  applyRetryConfig(options, defaults)
61
77
  applyExpirationConfig(options, defaults)
62
78
  applyRetentionConfig(options, defaults)
63
- applyCompletionConfig(options, defaults)
64
- applySingletonKeyConfig(options)
65
79
 
66
80
  const { startAfter, singletonSeconds, singletonMinutes, singletonHours } = options
67
81
 
@@ -83,23 +97,11 @@ function checkSendArgs (args, defaults) {
83
97
 
84
98
  assert(!singletonSeconds || singletonSeconds <= defaults.archiveSeconds, `throttling interval ${singletonSeconds}s cannot exceed archive interval ${defaults.archiveSeconds}s`)
85
99
 
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
100
+ if (options.onComplete) {
101
+ emitWarning(WARNINGS.ON_COMPLETE_REMOVED)
101
102
  }
102
- delete options.useSingletonQueue
103
+
104
+ return { name, data, options }
103
105
  }
104
106
 
105
107
  function checkWorkArgs (name, args, defaults) {
@@ -120,7 +122,7 @@ function checkWorkArgs (name, args, defaults) {
120
122
 
121
123
  options = { ...options }
122
124
 
123
- applyNewJobCheckInterval(options, defaults)
125
+ applyPollingInterval(options, defaults)
124
126
 
125
127
  assert(!('teamConcurrency' in options) ||
126
128
  (Number.isInteger(options.teamConcurrency) && options.teamConcurrency >= 1 && options.teamConcurrency <= 1000),
@@ -129,7 +131,6 @@ function checkWorkArgs (name, args, defaults) {
129
131
  assert(!('teamSize' in options) || (Number.isInteger(options.teamSize) && options.teamSize >= 1), 'teamSize must be an integer > 0')
130
132
  assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0')
131
133
  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
134
 
134
135
  return { options, callback }
135
136
  }
@@ -137,19 +138,12 @@ function checkWorkArgs (name, args, defaults) {
137
138
  function checkFetchArgs (name, batchSize, options) {
138
139
  assert(name, 'missing queue name')
139
140
 
140
- name = sanitizeQueueNameForFetch(name)
141
-
142
141
  assert(!batchSize || (Number.isInteger(batchSize) && batchSize >= 1), 'batchSize must be an integer > 0')
143
142
  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
143
 
146
144
  return { name }
147
145
  }
148
146
 
149
- function sanitizeQueueNameForFetch (name) {
150
- return name.replace(/[%_*]/g, match => match === '*' ? '%' : '\\' + match)
151
- }
152
-
153
147
  function getConfig (value) {
154
148
  assert(value && (typeof value === 'object' || typeof value === 'string'),
155
149
  'configuration assert: string or config object is required to connect to postgres')
@@ -158,32 +152,45 @@ function getConfig (value) {
158
152
  ? { connectionString: value }
159
153
  : { ...value }
160
154
 
161
- applyDatabaseConfig(config)
155
+ config.schedule = ('schedule' in config) ? config.schedule : true
156
+ config.supervise = ('supervise' in config) ? config.supervise : true
157
+ config.migrate = ('migrate' in config) ? config.migrate : true
158
+
159
+ applySchemaConfig(config)
162
160
  applyMaintenanceConfig(config)
163
161
  applyArchiveConfig(config)
164
162
  applyArchiveFailedConfig(config)
165
163
  applyDeleteConfig(config)
166
164
  applyMonitoringConfig(config)
167
- applyUuidConfig(config)
168
165
 
169
- applyNewJobCheckInterval(config)
166
+ applyPollingInterval(config)
170
167
  applyExpirationConfig(config)
171
168
  applyRetentionConfig(config)
172
- applyCompletionConfig(config)
173
169
 
174
170
  return config
175
171
  }
176
172
 
177
- function applyDatabaseConfig (config) {
173
+ function applySchemaConfig (config) {
178
174
  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`)
175
+ assertPostgresObjectName(config.schema)
182
176
  }
183
177
 
184
178
  config.schema = config.schema || DEFAULT_SCHEMA
185
179
  }
186
180
 
181
+ function assertPostgresObjectName (name) {
182
+ assert(typeof name === 'string', 'Name must be a string')
183
+ assert(name.length <= 50, 'Name cannot exceed 50 characters')
184
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters or underscores')
185
+ assert(!/^\d/.test(name), 'Name cannot start with a number')
186
+ }
187
+
188
+ function assertQueueName (name) {
189
+ assert(typeof name === 'string', 'Name must be a string')
190
+ assert(name.length <= 50, 'Name cannot exceed 50 characters')
191
+ assert(/[\w-]/.test(name), 'Name can only contain alphanumeric characters, underscores, or hyphens')
192
+ }
193
+
187
194
  function applyArchiveConfig (config) {
188
195
  const ARCHIVE_DEFAULT = 60 * 60 * 12
189
196
 
@@ -211,18 +218,7 @@ function applyArchiveFailedConfig (config) {
211
218
  }
212
219
  }
213
220
 
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) {
221
+ function applyRetentionConfig (config, defaults = {}) {
226
222
  assert(!('retentionSeconds' in config) || config.retentionSeconds >= 1,
227
223
  'configuration assert: retentionSeconds must be at least every second')
228
224
 
@@ -243,18 +239,13 @@ function applyRetentionConfig (config, defaults) {
243
239
  ? `${config.retentionMinutes} minutes`
244
240
  : ('retentionSeconds' in config)
245
241
  ? `${config.retentionSeconds} seconds`
246
- : defaults
247
- ? defaults.keepUntil
248
- : '14 days'
242
+ : null
249
243
 
250
244
  config.keepUntil = keepUntil
245
+ config.keepUntilDefault = defaults?.keepUntil
251
246
  }
252
247
 
253
- function applyExpirationConfig (config, defaults) {
254
- if ('expireIn' in config) {
255
- emitWarning(WARNINGS.EXPIRE_IN_REMOVED)
256
- }
257
-
248
+ function applyExpirationConfig (config, defaults = {}) {
258
249
  assert(!('expireInSeconds' in config) || config.expireInSeconds >= 1,
259
250
  'configuration assert: expireInSeconds must be at least every second')
260
251
 
@@ -265,16 +256,17 @@ function applyExpirationConfig (config, defaults) {
265
256
  'configuration assert: expireInHours must be at least every hour')
266
257
 
267
258
  const expireIn = ('expireInHours' in config)
268
- ? `${config.expireInHours} hours`
259
+ ? config.expireInHours * 60 * 60
269
260
  : ('expireInMinutes' in config)
270
- ? `${config.expireInMinutes} minutes`
261
+ ? config.expireInMinutes * 60
271
262
  : ('expireInSeconds' in config)
272
- ? `${config.expireInSeconds} seconds`
273
- : defaults
274
- ? defaults.expireIn
275
- : '15 minutes'
263
+ ? config.expireInSeconds
264
+ : null
265
+
266
+ assert(!expireIn || expireIn / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: expiration cannot exceed ${MAX_INTERVAL_HOURS} hours`)
276
267
 
277
268
  config.expireIn = expireIn
269
+ config.expireInDefault = defaults?.expireIn
278
270
  }
279
271
 
280
272
  function applyRetryConfig (config, defaults) {
@@ -282,35 +274,18 @@ function applyRetryConfig (config, defaults) {
282
274
  assert(!('retryLimit' in config) || (Number.isInteger(config.retryLimit) && config.retryLimit >= 0), 'retryLimit must be an integer >= 0')
283
275
  assert(!('retryBackoff' in config) || (config.retryBackoff === true || config.retryBackoff === false), 'retryBackoff must be either true or false')
284
276
 
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
277
+ config.retryDelayDefault = defaults?.retryDelay
278
+ config.retryLimitDefault = defaults?.retryLimit
279
+ config.retryBackoffDefault = defaults?.retryBackoff
296
280
  }
297
281
 
298
- 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')
303
-
304
- assert(!('newJobCheckIntervalSeconds' in config) || config.newJobCheckIntervalSeconds >= 1,
305
- 'configuration assert: newJobCheckIntervalSeconds must be at least every second')
282
+ function applyPollingInterval (config, defaults) {
283
+ assert(!('pollingIntervalSeconds' in config) || config.pollingIntervalSeconds >= 0.5,
284
+ 'configuration assert: pollingIntervalSeconds must be at least every 500ms')
306
285
 
307
- config.newJobCheckInterval = ('newJobCheckIntervalSeconds' in config)
308
- ? config.newJobCheckIntervalSeconds * second
309
- : ('newJobCheckInterval' in config)
310
- ? config.newJobCheckInterval
311
- : defaults
312
- ? defaults.newJobCheckInterval
313
- : second * 2
286
+ config.pollingInterval = ('pollingIntervalSeconds' in config)
287
+ ? config.pollingIntervalSeconds * 1000
288
+ : defaults?.pollingInterval || 2000
314
289
  }
315
290
 
316
291
  function applyMaintenanceConfig (config) {
@@ -325,6 +300,8 @@ function applyMaintenanceConfig (config) {
325
300
  : ('maintenanceIntervalSeconds' in config)
326
301
  ? config.maintenanceIntervalSeconds
327
302
  : 120
303
+
304
+ assert(config.maintenanceIntervalSeconds / 60 / 60 < MAX_INTERVAL_HOURS, `configuration assert: maintenance interval cannot exceed ${MAX_INTERVAL_HOURS} hours`)
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),
@@ -382,26 +363,21 @@ function applyMonitoringConfig (config) {
382
363
  ? config.clockMonitorIntervalSeconds
383
364
  : TEN_MINUTES_IN_SECONDS
384
365
 
385
- assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 60),
386
- 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 60 seconds')
366
+ assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45),
367
+ 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds')
387
368
 
388
369
  config.cronMonitorIntervalSeconds =
389
370
  ('cronMonitorIntervalSeconds' in config)
390
371
  ? config.cronMonitorIntervalSeconds
391
- : 60
372
+ : 30
392
373
 
393
- assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 60),
394
- 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 60 seconds')
374
+ assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45),
375
+ 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds')
395
376
 
396
377
  config.cronWorkerIntervalSeconds =
397
378
  ('cronWorkerIntervalSeconds' in config)
398
379
  ? config.cronWorkerIntervalSeconds
399
- : 4
400
- }
401
-
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'
380
+ : 5
405
381
  }
406
382
 
407
383
  function warnClockSkew (message) {