pg-boss 12.19.1 → 12.21.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/README.md +8 -48
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/pglite.d.ts +12 -0
- package/dist/adapters/pglite.d.ts.map +1 -0
- package/dist/adapters/pglite.js +51 -0
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +76 -0
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +23 -8
- package/dist/cli.js +71 -4
- package/dist/contractor.d.ts +3 -1
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +8 -5
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +74 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/manager.d.ts +13 -5
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +483 -42
- package/dist/migrationStore.d.ts +13 -4
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +572 -735
- package/dist/notifier.d.ts +16 -0
- package/dist/notifier.d.ts.map +1 -0
- package/dist/notifier.js +81 -0
- package/dist/plans.d.ts +55 -13
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +482 -89
- package/dist/types.d.ts +167 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +3 -3
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +14 -5
- package/package.json +20 -11
package/README.md
CHANGED
|
@@ -42,15 +42,17 @@ This will likely cater the most to teams already familiar with the simplicity of
|
|
|
42
42
|
|
|
43
43
|
## Summary
|
|
44
44
|
* Exactly-once job delivery
|
|
45
|
-
* Create jobs
|
|
46
|
-
* Backpressure-compatible polling workers
|
|
47
|
-
*
|
|
45
|
+
* Create jobs in an existing db transaction, including adapters for popular ORMs such as Drizzle, Knex, Kysely, Prisma
|
|
46
|
+
* Backpressure-compatible polling workers, including support for LISTEN/NOTIFY low latency delivery
|
|
47
|
+
* Job dependency workflow orchestration
|
|
48
|
+
* Cron scheduling, job deferral
|
|
48
49
|
* Queue storage policies to support a variety of rate limiting, debouncing, and concurrency use cases
|
|
49
|
-
* Priority queues, dead letter queues,
|
|
50
|
+
* Priority queues, dead letter queues, automatic retries with exponential backoff
|
|
50
51
|
* Pub/sub API for fan-out queue relationships
|
|
51
52
|
* SQL support for non-Node.js runtimes for most operations
|
|
52
53
|
* Serverless function compatible
|
|
53
54
|
* Multi-master compatible (for example, in a Kubernetes ReplicaSet)
|
|
55
|
+
* [Additional database backends](https://timgit.github.io/pg-boss/database-backends) for Postgres-based databases such as CockroachDB, YugabyteDB and Citus. Or, use embedded PGlite for running entirely in-process.
|
|
54
56
|
|
|
55
57
|
## CLI
|
|
56
58
|
|
|
@@ -62,55 +64,13 @@ See the [CLI documentation](https://timgit.github.io/pg-boss/cli) for details.
|
|
|
62
64
|
|
|
63
65
|
A web-based dashboard is available in the [`@pg-boss/dashboard`](https://www.npmjs.com/package/@pg-boss/dashboard) package for monitoring and managing jobs, queues and schedules.
|
|
64
66
|
|
|
65
|
-
See the [dashboard documentation](https://github.
|
|
67
|
+
See the [dashboard documentation](https://timgit.github.io/pg-boss/dashboard) for details.
|
|
66
68
|
|
|
67
69
|
## Proxy
|
|
68
70
|
|
|
69
71
|
A HTTP proxy is available in the [`@pg-boss/proxy`](https://www.npmjs.com/package/@pg-boss/proxy) package if needed to support use cases such as platform compatibility and connection pooling or scalability.
|
|
70
72
|
|
|
71
|
-
See the [proxy documentation](https://github.
|
|
72
|
-
|
|
73
|
-
## ORM Transaction Adapters
|
|
74
|
-
|
|
75
|
-
pg-boss ships adapters for running operations inside ORM-managed transactions. Each adapter wraps the ORM's transaction object as an `IDatabase` you can pass via the `db` option on `send()`, `insert()`, `fetch()`, `complete()`, and other methods.
|
|
76
|
-
|
|
77
|
-
### Knex / Kysely / Prisma
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { fromKnex, fromKysely, fromPrisma } from 'pg-boss'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
// Knex
|
|
85
|
-
await knex.transaction(async (trx) => {
|
|
86
|
-
await boss.send('my-queue', data, { db: fromKnex(trx) })
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// Kysely
|
|
90
|
-
await db.transaction().execute(async (trx) => {
|
|
91
|
-
await boss.send('my-queue', data, { db: fromKysely(trx) })
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// Prisma (v7+ with @prisma/adapter-pg)
|
|
95
|
-
await prisma.$transaction(async (tx) => {
|
|
96
|
-
await boss.send('my-queue', data, { db: fromPrisma(tx) })
|
|
97
|
-
})
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Drizzle
|
|
101
|
-
|
|
102
|
-
The Drizzle adapter accepts the `sql` tagged-template function from `drizzle-orm` as a second argument so it can construct parameterised queries without a runtime dependency on `drizzle-orm`.
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
import { fromDrizzle } from 'pg-boss'
|
|
106
|
-
import { sql } from 'drizzle-orm'
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
await db.transaction(async (tx) => {
|
|
111
|
-
await boss.send('my-queue', data, { db: fromDrizzle(tx, sql) })
|
|
112
|
-
})
|
|
113
|
-
```
|
|
73
|
+
See the [proxy documentation](https://timgit.github.io/pg-boss/proxy) for details.
|
|
114
74
|
|
|
115
75
|
## Requirements
|
|
116
76
|
* Node 22.12 or higher for CommonJS's require(esm)
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export { fromKnex } from './knex.ts';
|
|
|
2
2
|
export { fromKysely } from './kysely.ts';
|
|
3
3
|
export { fromDrizzle } from './drizzle.ts';
|
|
4
4
|
export { fromPrisma } from './prisma.ts';
|
|
5
|
+
export { fromPglite } from './pglite.ts';
|
|
5
6
|
export type { KnexTransactionLike } from './knex.ts';
|
|
6
7
|
export type { KyselyTransactionLike } from './kysely.ts';
|
|
7
8
|
export type { DrizzleTransactionLike, DrizzleSqlTagLike } from './drizzle.ts';
|
|
8
9
|
export type { PrismaTransactionLike } from './prisma.ts';
|
|
10
|
+
export type { PGliteLike } from './pglite.ts';
|
|
9
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpD,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACxD,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/adapters/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IDatabase } from '../types.ts';
|
|
2
|
+
export interface PGliteLike {
|
|
3
|
+
query<T = any>(query: string, params?: unknown[]): Promise<{
|
|
4
|
+
rows: T[];
|
|
5
|
+
}>;
|
|
6
|
+
exec(query: string): Promise<Array<{
|
|
7
|
+
rows: any[];
|
|
8
|
+
}>>;
|
|
9
|
+
listen?(channel: string, callback: (payload: string) => void): Promise<() => Promise<void>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function fromPglite(pglite: PGliteLike): IDatabase;
|
|
12
|
+
//# sourceMappingURL=pglite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pglite.d.ts","sourceRoot":"","sources":["../../src/adapters/pglite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAM5C,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,CAAC,CAAA;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC,CAAA;IACpD,MAAM,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;CAC5F;AAUD,wBAAgB,UAAU,CAAE,MAAM,EAAE,UAAU,GAAG,SAAS,CA6CzD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Adapts a PGlite instance (embedded single-connection WASM PostgreSQL) to pg-boss's IDatabase.
|
|
2
|
+
// PGlite is full PostgreSQL, so it needs none of the distributed compatibility flags — pair it
|
|
3
|
+
// with `backend: 'pglite'`. The user owns the PGlite instance lifecycle (construction and close).
|
|
4
|
+
//
|
|
5
|
+
// PGlite uses native `$1` placeholders, so no placeholder translation is needed. The one wrinkle is
|
|
6
|
+
// that `query()` runs a single statement only, while pg-boss issues concatenated multi-statement DDL
|
|
7
|
+
// (migrations/schema creation) with no parameters — those must go through `exec()`, which mirrors the
|
|
8
|
+
// simple-vs-extended protocol split that the default `pg.Pool`-backed driver relies on.
|
|
9
|
+
export function fromPglite(pglite) {
|
|
10
|
+
// pg-boss issues each statement expecting connection-pool semantics: an error on one statement
|
|
11
|
+
// must not affect the next. PGlite has a single connection, so a failed statement inside a
|
|
12
|
+
// BEGIN...COMMIT block (e.g. a migration that rolls back) leaves the connection in an aborted
|
|
13
|
+
// transaction that poisons every later query. A pooled driver sidesteps this by handing out a
|
|
14
|
+
// fresh connection; we emulate it by rolling back any aborted transaction before rethrowing.
|
|
15
|
+
const run = async (text, values) => {
|
|
16
|
+
if (values?.length) {
|
|
17
|
+
return await pglite.query(text, values);
|
|
18
|
+
}
|
|
19
|
+
// No parameters: may be a multi-statement block (e.g. a `locked()` BEGIN ... RETURNING ...
|
|
20
|
+
// COMMIT). exec() returns one result per statement; flatten their rows so a RETURNING in the
|
|
21
|
+
// middle isn't lost behind a trailing COMMIT. This mirrors how pg-boss unwraps the array that
|
|
22
|
+
// node-postgres returns for multi-statement queries (see unwrapSQLResult).
|
|
23
|
+
const results = await pglite.exec(text);
|
|
24
|
+
return { rows: results.flatMap(r => r.rows ?? []) };
|
|
25
|
+
};
|
|
26
|
+
const db = {
|
|
27
|
+
async executeSql(text, values) {
|
|
28
|
+
try {
|
|
29
|
+
return await run(text, values);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
await pglite.query('ROLLBACK').catch(() => { });
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// PGlite is embedded single-connection PostgreSQL, so LISTEN/NOTIFY works entirely in-process:
|
|
38
|
+
// the same instance both NOTIFYs (via pg-boss's inlined pg_notify) and delivers to listeners.
|
|
39
|
+
// Only expose `listen` when the instance actually supports it (older builds/mocks may not), so
|
|
40
|
+
// the notifier cleanly falls back to polling otherwise. There is no network connection to drop,
|
|
41
|
+
// hence no reconnect loop — onReconnect is invoked once after the initial subscribe to mirror
|
|
42
|
+
// the pooled driver and force a gap-recovery fetch.
|
|
43
|
+
if (typeof pglite.listen === 'function') {
|
|
44
|
+
db.listen = async (channel, onNotification, onReconnect) => {
|
|
45
|
+
const unsubscribe = await pglite.listen(channel, onNotification);
|
|
46
|
+
onReconnect();
|
|
47
|
+
return { close: async () => { await unsubscribe(); } };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return db;
|
|
51
|
+
}
|
package/dist/attorney.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;
|
|
1
|
+
{"version":3,"file":"attorney.d.ts","sourceRoot":"","sources":["../src/attorney.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,QAAA,MAAM,MAAM;;;;CAIX,CAAA;AAyDD,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAWD,iBAAS,gBAAgB,CAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,QA2D/C;AA2GD,iBAAS,aAAa,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG;IAClD,OAAO,EAAE,KAAK,CAAC,mBAAmB,CAAA;IAClC,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;CACjC,CA8BA;AAED,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAQlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CAuB9F;AAgDD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAK9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AAmID,OAAO,EACL,SAAS,EACT,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,MAAM,EACN,gBAAgB,EAChB,iBAAiB,EAClB,CAAA"}
|
package/dist/attorney.js
CHANGED
|
@@ -5,11 +5,49 @@ const POLICY = {
|
|
|
5
5
|
MIN_POLLING_INTERVAL_MS: 500,
|
|
6
6
|
MAX_RETENTION_DAYS: 365
|
|
7
7
|
};
|
|
8
|
+
// The internal compatibility flags a backend can toggle. A backend sets only the flags that differ
|
|
9
|
+
// from stock PostgreSQL; everything else defaults to false. These are derived from the backend
|
|
10
|
+
// profile and are not user-configurable (see resolveBackend).
|
|
11
|
+
const COMPATIBILITY_FLAGS = [
|
|
12
|
+
'noSkipLocked',
|
|
13
|
+
'noMultiMutationCte',
|
|
14
|
+
'noTablePartitioning',
|
|
15
|
+
'noDeferrableConstraints',
|
|
16
|
+
'noAdvisoryLocks',
|
|
17
|
+
'noCoveringIndexes',
|
|
18
|
+
'noListenNotify'
|
|
19
|
+
];
|
|
20
|
+
// The single source of truth for backend presets, mirrored by test/testHelper.ts.
|
|
21
|
+
const BACKEND_PROFILES = {
|
|
22
|
+
postgres: { kind: 'standard', flags: {} },
|
|
23
|
+
cockroachdb: {
|
|
24
|
+
kind: 'distributed',
|
|
25
|
+
flags: {
|
|
26
|
+
noSkipLocked: true,
|
|
27
|
+
noMultiMutationCte: true,
|
|
28
|
+
noTablePartitioning: true,
|
|
29
|
+
noDeferrableConstraints: true,
|
|
30
|
+
noAdvisoryLocks: true,
|
|
31
|
+
noCoveringIndexes: true,
|
|
32
|
+
noListenNotify: true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
yugabytedb: {
|
|
36
|
+
kind: 'distributed',
|
|
37
|
+
flags: {
|
|
38
|
+
noAdvisoryLocks: true,
|
|
39
|
+
noTablePartitioning: true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
citus: { kind: 'distributed', flags: {} },
|
|
43
|
+
pglite: { kind: 'embedded', flags: {} }
|
|
44
|
+
};
|
|
8
45
|
function assertObjectName(value, name = 'Name') {
|
|
9
46
|
assert(/^[\w.\-/]+$/.test(value), `${name} can only contain alphanumeric characters, underscores, hyphens, periods, or forward slashes`);
|
|
10
47
|
}
|
|
11
48
|
function validateQueueArgs(config = {}) {
|
|
12
49
|
assert(!('deadLetter' in config) || config.deadLetter === null || (typeof config.deadLetter === 'string'), 'deadLetter must be a string');
|
|
50
|
+
assert(!('notify' in config) || typeof config.notify === 'boolean', 'notify must be a boolean');
|
|
13
51
|
if (config.deadLetter) {
|
|
14
52
|
assertObjectName(config.deadLetter, 'deadLetter');
|
|
15
53
|
}
|
|
@@ -224,6 +262,7 @@ function checkWorkArgs(name, args) {
|
|
|
224
262
|
assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean');
|
|
225
263
|
assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean');
|
|
226
264
|
assert(!('localConcurrency' in options) || (Number.isInteger(options.localConcurrency) && options.localConcurrency >= 1), 'localConcurrency must be an integer >= 1');
|
|
265
|
+
assert(!('perJobResults' in options) || typeof options.perJobResults === 'boolean', 'perJobResults must be a boolean');
|
|
227
266
|
validatePriorityRangeConfig(options);
|
|
228
267
|
validateGroupConcurrencyConfig(options);
|
|
229
268
|
validateHeartbeatRefreshConfig(options);
|
|
@@ -246,6 +285,8 @@ function getConfig(value) {
|
|
|
246
285
|
config.supervise = ('supervise' in config) ? config.supervise : true;
|
|
247
286
|
config.migrate = ('migrate' in config) ? config.migrate : true;
|
|
248
287
|
config.createSchema = ('createSchema' in config) ? config.createSchema : true;
|
|
288
|
+
config.useListenNotify = ('useListenNotify' in config) ? config.useListenNotify : false;
|
|
289
|
+
resolveBackend(config);
|
|
249
290
|
applySchemaConfig(config);
|
|
250
291
|
applyOpsConfig(config);
|
|
251
292
|
applyScheduleConfig(config);
|
|
@@ -265,6 +306,24 @@ function validateWarningConfig(config) {
|
|
|
265
306
|
assert(!('warningRetentionDays' in config) || (Number.isInteger(config.warningRetentionDays) && config.warningRetentionDays >= 1), 'configuration assert: warningRetentionDays must be an integer >= 1');
|
|
266
307
|
assert(!('warningRetentionDays' in config) || config.warningRetentionDays <= POLICY.MAX_RETENTION_DAYS, `configuration assert: warningRetentionDays cannot exceed ${POLICY.MAX_RETENTION_DAYS} days`);
|
|
267
308
|
}
|
|
309
|
+
// Expands config.backend into the internal compatibility flags. The flags are derived
|
|
310
|
+
// solely from the backend profile — they are not part of the public input, so a
|
|
311
|
+
// deployment can't end up with an inconsistent combination.
|
|
312
|
+
function resolveBackend(config) {
|
|
313
|
+
const backend = ('backend' in config) ? config.backend : 'postgres';
|
|
314
|
+
assert(backend in BACKEND_PROFILES, `configuration assert: backend must be one of ${Object.keys(BACKEND_PROFILES).join(', ')}`);
|
|
315
|
+
config.backend = backend;
|
|
316
|
+
const { flags } = BACKEND_PROFILES[backend];
|
|
317
|
+
for (const flag of COMPATIBILITY_FLAGS) {
|
|
318
|
+
config[flag] = flags[flag] ?? false;
|
|
319
|
+
}
|
|
320
|
+
// Test hook: exercise the distributed runtime paths (atomic fetch + split mutations)
|
|
321
|
+
// on top of the current backend's schema, without standing up a distributed database.
|
|
322
|
+
if (config.__test__distributed) {
|
|
323
|
+
config.noSkipLocked = true;
|
|
324
|
+
config.noMultiMutationCte = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
268
327
|
function assertPostgresObjectName(name) {
|
|
269
328
|
assert(typeof name === 'string', 'Name must be a string');
|
|
270
329
|
assert(name.length <= 50, 'Name cannot exceed 50 characters');
|
|
@@ -309,6 +368,23 @@ function applyPollingInterval(config) {
|
|
|
309
368
|
config.pollingInterval = ('pollingIntervalSeconds' in config)
|
|
310
369
|
? config.pollingIntervalSeconds * 1000
|
|
311
370
|
: 2000;
|
|
371
|
+
assert(!('notifyPollingIntervalSeconds' in config) || config.notifyPollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1000, `configuration assert: notifyPollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
|
|
372
|
+
// Relaxed backstop poll used only while NOTIFY is active for the queue; falls back to
|
|
373
|
+
// pollingInterval when notify is unavailable. It must never be smaller than the base poll —
|
|
374
|
+
// that would make a notify-active queue poll more aggressively than an idle one, the opposite
|
|
375
|
+
// of the intent. When explicit, reject a value below the base; when defaulted, floor it at the
|
|
376
|
+
// base so bumping pollingIntervalSeconds past 30s can't silently leave notify smaller.
|
|
377
|
+
if ('notifyPollingIntervalSeconds' in config) {
|
|
378
|
+
config.notifyPollingInterval = config.notifyPollingIntervalSeconds * 1000;
|
|
379
|
+
assert(config.notifyPollingInterval >= config.pollingInterval, 'configuration assert: notifyPollingIntervalSeconds must be at least pollingIntervalSeconds');
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
config.notifyPollingInterval = Math.max(30000, config.pollingInterval);
|
|
383
|
+
}
|
|
384
|
+
// Burst triggers: no transform, just validation. Both put the worker into continuous-fetch
|
|
385
|
+
// (no delay) mode; see JobPollingOptions for precedence.
|
|
386
|
+
assert(!('burstWhenReadyExceeds' in config) || (Number.isInteger(config.burstWhenReadyExceeds) && config.burstWhenReadyExceeds >= 1), 'configuration assert: burstWhenReadyExceeds must be an integer >= 1');
|
|
387
|
+
assert(!('burstWhenBatchFull' in config) || typeof config.burstWhenBatchFull === 'boolean', 'configuration assert: burstWhenBatchFull must be a boolean');
|
|
312
388
|
}
|
|
313
389
|
function applyOpsConfig(config) {
|
|
314
390
|
assert(!('superviseIntervalSeconds' in config) || config.superviseIntervalSeconds >= 1, 'configuration assert: superviseIntervalSeconds must be at least every second');
|
package/dist/boss.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAkBnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAS1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAmB1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAWL,IAAI;IA+EJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE;
|
|
1
|
+
{"version":3,"file":"boss.d.ts","sourceRoot":"","sources":["../src/boss.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAkBnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAS1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAmB1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAWL,IAAI;IA+EJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE;CAqHtD;AAED,eAAe,IAAI,CAAA"}
|
package/dist/boss.js
CHANGED
|
@@ -151,22 +151,37 @@ class Boss extends EventEmitter {
|
|
|
151
151
|
return;
|
|
152
152
|
if (rows.length) {
|
|
153
153
|
const queues = rows.map((q) => q.name);
|
|
154
|
-
const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues);
|
|
154
|
+
const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
155
155
|
const { rows: rowsCacheStats } = await this.#executeQuery(cacheStatsSql);
|
|
156
156
|
if (this.#stopping)
|
|
157
157
|
return;
|
|
158
|
-
|
|
158
|
+
// Coerce with Number(): CockroachDB returns these integer columns as strings, so a bare `>`
|
|
159
|
+
// would compare lexicographically ("100" > "9" === false) and silently miss the backlog. On
|
|
160
|
+
// standard Postgres these are already numbers, so Number() is a no-op.
|
|
161
|
+
const warnings = rowsCacheStats.filter(i => Number(i.queuedCount) > (Number(i.warningQueueSize) || WARNINGS.LARGE_QUEUE.size));
|
|
159
162
|
for (const warning of warnings) {
|
|
160
163
|
await emitAndPersistWarning(this.#warningContext, WARNING_TYPES.QUEUE_BACKLOG, WARNINGS.LARGE_QUEUE.message, warning);
|
|
161
164
|
}
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
// CockroachDB rejects the multi-mutation failJobs() CTE these use, so under noMultiMutationCte
|
|
166
|
+
// route expiry through the manager's split select/delete/re-insert variants instead.
|
|
167
|
+
if (this.#config.noMultiMutationCte) {
|
|
168
|
+
await this.#manager.failJobsByTimeoutDistributed(table, queues);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const sql = plans.failJobsByTimeout(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
172
|
+
await this.#executeQuery(sql);
|
|
173
|
+
}
|
|
164
174
|
if (this.#stopping)
|
|
165
175
|
return;
|
|
166
176
|
const heartbeatQueues = queues.filter(q => heartbeatQueueNames.has(q));
|
|
167
177
|
if (heartbeatQueues.length) {
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
if (this.#config.noMultiMutationCte) {
|
|
179
|
+
await this.#manager.failJobsByHeartbeatDistributed(table, heartbeatQueues);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const heartbeatSql = plans.failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues, this.#config.noAdvisoryLocks);
|
|
183
|
+
await this.#executeQuery(heartbeatSql);
|
|
184
|
+
}
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
}
|
|
@@ -179,9 +194,9 @@ class Boss extends EventEmitter {
|
|
|
179
194
|
return;
|
|
180
195
|
if (rows.length) {
|
|
181
196
|
const queues = rows.map((q) => q.name);
|
|
182
|
-
const sql = plans.deletion(this.#config.schema, table, queues);
|
|
197
|
+
const sql = plans.deletion(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
183
198
|
await this.#executeQuery(sql);
|
|
184
|
-
const depSql = plans.cleanupDependencies(this.#config.schema, table, queues);
|
|
199
|
+
const depSql = plans.cleanupDependencies(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
185
200
|
await this.#executeQuery(depSql);
|
|
186
201
|
}
|
|
187
202
|
}
|
package/dist/cli.js
CHANGED
|
@@ -146,6 +146,27 @@ async function createDb(config) {
|
|
|
146
146
|
await db.open();
|
|
147
147
|
return db;
|
|
148
148
|
}
|
|
149
|
+
// Like getConnectionConfig, but returns null instead of exiting when no connection is
|
|
150
|
+
// configured — used by commands (e.g. `plans`) where a connection is optional.
|
|
151
|
+
function tryGetConnectionConfig(args) {
|
|
152
|
+
const fileConfig = loadConfigFile(args.config);
|
|
153
|
+
const hasConnection = args.connectionString || process.env.PGBOSS_DATABASE_URL || fileConfig.connectionString ||
|
|
154
|
+
args.host || process.env.PGBOSS_HOST || fileConfig.host ||
|
|
155
|
+
args.database || process.env.PGBOSS_DATABASE || fileConfig.database;
|
|
156
|
+
return hasConnection ? getConnectionConfig(args) : null;
|
|
157
|
+
}
|
|
158
|
+
// Enumerates partitioned queue table names so inlined index builds can fan out across
|
|
159
|
+
// them. Returns [] on any failure (e.g. unreachable DB or pre-partition schema), leaving
|
|
160
|
+
// the export to target job_common only.
|
|
161
|
+
async function getPartitionTables(db, schema) {
|
|
162
|
+
try {
|
|
163
|
+
const result = await db.executeSql(plans.getPartitionedQueueTables(schema));
|
|
164
|
+
return result.rows.map((row) => row.table_name);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
149
170
|
async function getSchemaVersion(db, schema) {
|
|
150
171
|
try {
|
|
151
172
|
const result = await db.executeSql(plans.versionTableExists(schema));
|
|
@@ -212,7 +233,22 @@ async function cmdMigrate(args) {
|
|
|
212
233
|
const config = getConnectionConfig(args);
|
|
213
234
|
const schema = config.schema || plans.DEFAULT_SCHEMA;
|
|
214
235
|
if (args.dryRun) {
|
|
215
|
-
|
|
236
|
+
// The CLI has no BAM worker, so inline the async index builds as direct DDL. Connect
|
|
237
|
+
// (best effort) to enumerate partitioned tables; fall back to job_common only offline.
|
|
238
|
+
let partitionTables = [];
|
|
239
|
+
try {
|
|
240
|
+
const db = await createDb(config);
|
|
241
|
+
try {
|
|
242
|
+
partitionTables = await getPartitionTables(db, schema);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await db.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// no reachable database: emit a job_common-only static script
|
|
250
|
+
}
|
|
251
|
+
const sql = migrationStore.migrate(schema, 0, undefined, undefined, { inlineAsync: true, partitionTables });
|
|
216
252
|
console.log('-- SQL to migrate pg-boss from version 0 to latest:');
|
|
217
253
|
console.log(sql);
|
|
218
254
|
return;
|
|
@@ -232,8 +268,15 @@ async function cmdMigrate(args) {
|
|
|
232
268
|
return;
|
|
233
269
|
}
|
|
234
270
|
console.log(`Migrating pg-boss schema "${schema}" from version ${version} to ${schemaVersion}...`);
|
|
235
|
-
|
|
271
|
+
// Inline the async index builds rather than enqueuing BAM rows that nothing will run
|
|
272
|
+
// (the CLI exits without a worker); enumerate partitions so they are covered too.
|
|
273
|
+
const partitionTables = await getPartitionTables(db, schema);
|
|
274
|
+
const { sql, concurrent } = migrationStore.migrateCommands(schema, version, undefined, undefined, { inlineAsync: true, partitionTables });
|
|
236
275
|
await db.executeSql(sql);
|
|
276
|
+
// CONCURRENTLY index builds must run outside the migration transaction, one at a time.
|
|
277
|
+
for (const statement of concurrent) {
|
|
278
|
+
await db.executeSql(statement);
|
|
279
|
+
}
|
|
237
280
|
console.log(`Successfully migrated pg-boss schema "${schema}" to version ${schemaVersion}`);
|
|
238
281
|
}
|
|
239
282
|
finally {
|
|
@@ -279,10 +322,34 @@ async function cmdPlans(args) {
|
|
|
279
322
|
console.log('-- SQL to create pg-boss schema:');
|
|
280
323
|
console.log(plans.create(schema, schemaVersion, { createSchema: true }));
|
|
281
324
|
break;
|
|
282
|
-
case 'migrate':
|
|
325
|
+
case 'migrate': {
|
|
326
|
+
// Inline the async index builds (no BAM worker runs an exported script). A connection
|
|
327
|
+
// is optional: with one, fan the builds out across partitioned tables; without one,
|
|
328
|
+
// emit a job_common-only script and note the limitation.
|
|
329
|
+
const connectionConfig = tryGetConnectionConfig(args);
|
|
330
|
+
let partitionTables = [];
|
|
331
|
+
if (connectionConfig) {
|
|
332
|
+
try {
|
|
333
|
+
const db = await createDb(connectionConfig);
|
|
334
|
+
try {
|
|
335
|
+
partitionTables = await getPartitionTables(db, schema);
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
await db.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// unreachable database: fall back to a job_common-only script
|
|
343
|
+
}
|
|
344
|
+
}
|
|
283
345
|
console.log('-- SQL to migrate pg-boss (from version 0 to latest):');
|
|
284
|
-
|
|
346
|
+
if (!connectionConfig) {
|
|
347
|
+
console.log('-- note: no database connection provided; partitioned queue tables were not enumerated.');
|
|
348
|
+
console.log('-- Run with a connection (e.g. --connection-string) to include per-partition index builds.');
|
|
349
|
+
}
|
|
350
|
+
console.log(migrationStore.migrate(schema, 0, undefined, undefined, { inlineAsync: true, partitionTables }));
|
|
285
351
|
break;
|
|
352
|
+
}
|
|
286
353
|
case 'rollback':
|
|
287
354
|
console.log(`-- SQL to rollback pg-boss from version ${schemaVersion} to ${schemaVersion - 1}:`);
|
|
288
355
|
console.log(migrationStore.rollback(schema, schemaVersion));
|
package/dist/contractor.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ declare class Contractor {
|
|
|
3
3
|
static constructionPlans(schema?: string, options?: {
|
|
4
4
|
createSchema: boolean;
|
|
5
5
|
}): string;
|
|
6
|
-
static migrationPlans(schema?: string, version?: number
|
|
6
|
+
static migrationPlans(schema?: string, version?: number, options?: {
|
|
7
|
+
partitionTables?: string[];
|
|
8
|
+
}): string;
|
|
7
9
|
static rollbackPlans(schema?: string, version?: number): string;
|
|
8
10
|
private config;
|
|
9
11
|
private db;
|
package/dist/contractor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAIxC,cAAM,UAAU;IACd,MAAM,CAAC,iBAAiB,CAAE,MAAM,SAAuB,EAAE,OAAO;;KAAyB;IAIzF,MAAM,CAAC,cAAc,CAAE,MAAM,SAAuB,EAAE,OAAO,SAAoB;
|
|
1
|
+
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAIxC,cAAM,UAAU;IACd,MAAM,CAAC,iBAAiB,CAAE,MAAM,SAAuB,EAAE,OAAO;;KAAyB;IAIzF,MAAM,CAAC,cAAc,CAAE,MAAM,SAAuB,EAAE,OAAO,SAAoB,EAAE,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAAO;IAO/H,MAAM,CAAC,aAAa,CAAE,MAAM,SAAuB,EAAE,OAAO,SAAgB;IAI5E,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,EAAE,CAAiB;IAC3B,OAAO,CAAC,UAAU,CAAmB;gBAExB,EAAE,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAMpE,aAAa;IAKb,WAAW;IAKX,KAAK;IAcL,KAAK;IAcL,MAAM;IASN,OAAO,CAAE,OAAO,EAAE,MAAM;IASxB,IAAI,CAAE,OAAO,EAAE,MAAM;IAKrB,QAAQ,CAAE,OAAO,EAAE,MAAM;CAIhC;AAED,eAAe,UAAU,CAAA"}
|
package/dist/contractor.js
CHANGED
|
@@ -7,8 +7,11 @@ class Contractor {
|
|
|
7
7
|
static constructionPlans(schema = plans.DEFAULT_SCHEMA, options = { createSchema: true }) {
|
|
8
8
|
return plans.create(schema, schemaVersion, options);
|
|
9
9
|
}
|
|
10
|
-
static migrationPlans(schema = plans.DEFAULT_SCHEMA, version = schemaVersion - 1) {
|
|
11
|
-
|
|
10
|
+
static migrationPlans(schema = plans.DEFAULT_SCHEMA, version = schemaVersion - 1, options = {}) {
|
|
11
|
+
// Exported plans run without a BAM worker, so inline the async index builds as direct
|
|
12
|
+
// DDL rather than job_table_run_async() enqueues (see issue #766). Callers that hold a
|
|
13
|
+
// live connection can pass partitionTables to fan the builds out across partitions.
|
|
14
|
+
return migrationStore.migrate(schema, version, undefined, undefined, { inlineAsync: true, partitionTables: options.partitionTables });
|
|
12
15
|
}
|
|
13
16
|
static rollbackPlans(schema = plans.DEFAULT_SCHEMA, version = schemaVersion) {
|
|
14
17
|
return migrationStore.rollback(schema, version);
|
|
@@ -62,7 +65,7 @@ class Contractor {
|
|
|
62
65
|
}
|
|
63
66
|
async migrate(version) {
|
|
64
67
|
try {
|
|
65
|
-
const commands = migrationStore.migrate(this.config.schema, version, this.migrations);
|
|
68
|
+
const commands = migrationStore.migrate(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
|
|
66
69
|
await this.db.executeSql(commands);
|
|
67
70
|
}
|
|
68
71
|
catch (err) {
|
|
@@ -70,11 +73,11 @@ class Contractor {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
async next(version) {
|
|
73
|
-
const commands = migrationStore.next(this.config.schema, version, this.migrations);
|
|
76
|
+
const commands = migrationStore.next(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
|
|
74
77
|
await this.db.executeSql(commands);
|
|
75
78
|
}
|
|
76
79
|
async rollback(version) {
|
|
77
|
-
const commands = migrationStore.rollback(this.config.schema, version, this.migrations);
|
|
80
|
+
const commands = migrationStore.rollback(this.config.schema, version, this.migrations, this.config.noAdvisoryLocks);
|
|
78
81
|
await this.db.executeSql(commands);
|
|
79
82
|
}
|
|
80
83
|
}
|
package/dist/db.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ declare class Db extends EventEmitter implements types.IDatabase, types.EventsMi
|
|
|
11
11
|
open(): Promise<void>;
|
|
12
12
|
close(): Promise<void>;
|
|
13
13
|
executeSql(text: string, values?: unknown[]): Promise<import("pg").QueryResult<any>>;
|
|
14
|
+
listen(channel: string, onNotification: (payload: string) => void, onReconnect: () => void): Promise<types.ListenHandle>;
|
|
14
15
|
withTransaction<T>(fn: (db: types.IDatabase) => Promise<T>): Promise<T>;
|
|
15
16
|
}
|
|
16
17
|
export default Db;
|
package/dist/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAGtC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,cAAM,EAAG,SAAQ,YAAa,YAAW,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW;IACzE,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAuB;IAGrC,MAAM,EAAE,OAAO,CAAA;gBAEF,MAAM,EAAE,KAAK,CAAC,eAAe;IAY1C,MAAM;;MAEL;IAEK,IAAI;IAMJ,KAAK;IAOL,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAGtC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC,cAAM,EAAG,SAAQ,YAAa,YAAW,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW;IACzE,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAuB;IAGrC,MAAM,EAAE,OAAO,CAAA;gBAEF,MAAM,EAAE,KAAK,CAAC,eAAe;IAY1C,MAAM;;MAEL;IAEK,IAAI;IAMJ,KAAK;IAOL,UAAU,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE;IAoB5C,MAAM,CACV,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACzC,WAAW,EAAE,MAAM,IAAI,GACtB,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC;IA4ExB,eAAe,CAAC,CAAC,EAAG,EAAE,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAmB/E;AAED,eAAe,EAAE,CAAA"}
|
package/dist/db.js
CHANGED
|
@@ -42,6 +42,80 @@ class Db extends EventEmitter {
|
|
|
42
42
|
// }
|
|
43
43
|
return await this.pool.query(text, values);
|
|
44
44
|
}
|
|
45
|
+
// Opens a dedicated, session-pinned connection for LISTEN/NOTIFY. A separate pg.Client
|
|
46
|
+
// (not a pooled connection) is used so the listener never depletes the query pool and so
|
|
47
|
+
// reconnection is self-contained. On any drop the client reconnects with capped backoff
|
|
48
|
+
// and re-runs LISTEN, then calls onReconnect so the caller can recover missed messages.
|
|
49
|
+
async listen(channel, onNotification, onReconnect) {
|
|
50
|
+
assert(this.opened, 'Database not opened. Call open() before listening.');
|
|
51
|
+
let closed = false;
|
|
52
|
+
let client = null;
|
|
53
|
+
let reconnectTimer = null;
|
|
54
|
+
let attempt = 0;
|
|
55
|
+
const scheduleReconnect = () => {
|
|
56
|
+
if (closed || reconnectTimer)
|
|
57
|
+
return;
|
|
58
|
+
const backoff = Math.min(30000, 1000 * 2 ** Math.min(attempt, 5));
|
|
59
|
+
attempt++;
|
|
60
|
+
reconnectTimer = setTimeout(() => {
|
|
61
|
+
reconnectTimer = null;
|
|
62
|
+
connect().catch(() => scheduleReconnect());
|
|
63
|
+
}, backoff);
|
|
64
|
+
};
|
|
65
|
+
const connect = async () => {
|
|
66
|
+
if (closed)
|
|
67
|
+
return;
|
|
68
|
+
const next = new pg.Client(this.config);
|
|
69
|
+
next.on('error', error => {
|
|
70
|
+
this.emit('error', error);
|
|
71
|
+
if (!closed) {
|
|
72
|
+
next.removeAllListeners();
|
|
73
|
+
next.end().catch(() => { });
|
|
74
|
+
if (client === next)
|
|
75
|
+
client = null;
|
|
76
|
+
scheduleReconnect();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
next.on('notification', msg => {
|
|
80
|
+
if (msg.payload !== undefined)
|
|
81
|
+
onNotification(msg.payload);
|
|
82
|
+
});
|
|
83
|
+
// Track the client before connecting so close() can tear down a connect still in flight
|
|
84
|
+
// (e.g. shutdown during a reconnect). If connect or LISTEN then rejects, the catch ends
|
|
85
|
+
// it and rethrows — without that, a LISTEN that fails after connect() succeeded would
|
|
86
|
+
// leak an open connection. The reconnect .catch below reschedules on failure; an initial
|
|
87
|
+
// failure propagates to the caller.
|
|
88
|
+
client = next;
|
|
89
|
+
try {
|
|
90
|
+
await next.connect();
|
|
91
|
+
await next.query(`LISTEN "${channel}"`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
next.removeAllListeners();
|
|
95
|
+
await next.end().catch(() => { });
|
|
96
|
+
if (client === next)
|
|
97
|
+
client = null;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
attempt = 0;
|
|
101
|
+
onReconnect();
|
|
102
|
+
};
|
|
103
|
+
await connect();
|
|
104
|
+
return {
|
|
105
|
+
close: async () => {
|
|
106
|
+
closed = true;
|
|
107
|
+
if (reconnectTimer) {
|
|
108
|
+
clearTimeout(reconnectTimer);
|
|
109
|
+
reconnectTimer = null;
|
|
110
|
+
}
|
|
111
|
+
if (client) {
|
|
112
|
+
client.removeAllListeners();
|
|
113
|
+
await client.end().catch(() => { });
|
|
114
|
+
client = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
45
119
|
async withTransaction(fn) {
|
|
46
120
|
assert(this.opened, 'Database not opened. Call open() before executing SQL.');
|
|
47
121
|
const client = await this.pool.connect();
|