pg-boss 12.25.1 → 12.26.1
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 +3 -2
- package/dist/adapters/knex.d.ts.map +1 -1
- package/dist/adapters/knex.js +6 -1
- package/dist/attorney.d.ts.map +1 -1
- package/dist/attorney.js +31 -9
- package/dist/bam.d.ts.map +1 -1
- package/dist/bam.js +23 -1
- package/dist/boss.d.ts.map +1 -1
- package/dist/boss.js +8 -12
- package/dist/cli.js +147 -3
- package/dist/contractor.d.ts +1 -0
- package/dist/contractor.d.ts.map +1 -1
- package/dist/contractor.js +97 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +9 -1
- package/dist/drifter.d.ts +84 -0
- package/dist/drifter.d.ts.map +1 -0
- package/dist/drifter.js +423 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -55
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +68 -22
- package/dist/migrationStore.d.ts +2 -1
- package/dist/migrationStore.d.ts.map +1 -1
- package/dist/migrationStore.js +77 -4
- package/dist/plans.d.ts +24 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +381 -72
- package/dist/schema.json +2069 -0
- package/dist/timekeeper.d.ts.map +1 -1
- package/dist/timekeeper.js +10 -4
- package/dist/types.d.ts +139 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Queueing jobs in Postgres from Node.js like a boss.
|
|
2
2
|
|
|
3
|
-
[](https://nodei.co/npm/pg-boss/)
|
|
4
3
|
[](https://github.com/timgit/pg-boss/actions/workflows/ci.yml)
|
|
5
|
-
[](https://coveralls.io/github/timgit/pg-boss?branch=master)
|
|
5
|
+
[](https://www.npmjs.com/package/pg-boss)
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
```js
|
|
8
9
|
async function readme() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knex.d.ts","sourceRoot":"","sources":["../../src/adapters/knex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAI5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,EAAE,CAAC,CAAA;CACrG;AAED,wBAAgB,QAAQ,CAAE,GAAG,EAAE,mBAAmB,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"knex.d.ts","sourceRoot":"","sources":["../../src/adapters/knex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAI5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,EAAE,CAAC,CAAA;CACrG;AAED,wBAAgB,QAAQ,CAAE,GAAG,EAAE,mBAAmB,GAAG,SAAS,CAgB7D"}
|
package/dist/adapters/knex.js
CHANGED
|
@@ -7,7 +7,12 @@ export function fromKnex(trx) {
|
|
|
7
7
|
// so each textual occurrence (including reuse of the same $N) must be
|
|
8
8
|
// mapped to its own ? with the value duplicated in textual order.
|
|
9
9
|
const { parts, reordered } = parsePlaceholders(text, values);
|
|
10
|
-
|
|
10
|
+
// Several pg-boss queries (e.g. updateJob) also contain literal jsonb `?` key-exists
|
|
11
|
+
// operators. knex.raw() scans the whole string for `?` to fill bindings, so those
|
|
12
|
+
// literal occurrences must be escaped as `\?` (knex's own literal-? syntax) before
|
|
13
|
+
// joining in the real placeholders — otherwise knex miscounts bindings and throws
|
|
14
|
+
// "Undefined binding(s) detected" on any query that mixes both.
|
|
15
|
+
const knexSql = parts.map(part => part.replace(/\?/g, '\\?')).join('?');
|
|
11
16
|
return unwrapSQLResult(await trx.raw(knexSql, reordered));
|
|
12
17
|
}
|
|
13
18
|
};
|
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;AAqED,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAc3C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CAgDhD;AAOD,iBAAS,eAAe,CAAE,IAAI,EAAE,GAAG,EAAE,EAAE,MAAc,EAAE;;CAAK,GAAG,KAAK,CAAC,OAAO,CA4D3E;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,CAwB9F;AA4DD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAK9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AAoJD,OAAO,EACL,SAAS,EACT,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,MAAM,EACN,gBAAgB,EAChB,iBAAiB,EAClB,CAAA"}
|
package/dist/attorney.js
CHANGED
|
@@ -15,7 +15,8 @@ const COMPATIBILITY_FLAGS = [
|
|
|
15
15
|
'noDeferrableConstraints',
|
|
16
16
|
'noAdvisoryLocks',
|
|
17
17
|
'noCoveringIndexes',
|
|
18
|
-
'noListenNotify'
|
|
18
|
+
'noListenNotify',
|
|
19
|
+
'noIndexProgressView'
|
|
19
20
|
];
|
|
20
21
|
// The single source of truth for backend presets, mirrored by test/testHelper.ts.
|
|
21
22
|
const BACKEND_PROFILES = {
|
|
@@ -29,16 +30,27 @@ const BACKEND_PROFILES = {
|
|
|
29
30
|
noDeferrableConstraints: true,
|
|
30
31
|
noAdvisoryLocks: true,
|
|
31
32
|
noCoveringIndexes: true,
|
|
32
|
-
noListenNotify: true
|
|
33
|
+
noListenNotify: true,
|
|
34
|
+
// Online DDL runs as a schema-change job, not the PG CONCURRENTLY path, and
|
|
35
|
+
// pg_stat_progress_create_index isn't available — so BAM can't use liveness-based reclaim.
|
|
36
|
+
noIndexProgressView: true
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
yugabytedb: {
|
|
36
40
|
kind: 'distributed',
|
|
37
41
|
flags: {
|
|
38
42
|
noAdvisoryLocks: true,
|
|
39
|
-
noTablePartitioning: true
|
|
43
|
+
noTablePartitioning: true,
|
|
44
|
+
// Index builds are a distributed backfill that pg_stat_progress_create_index doesn't reflect,
|
|
45
|
+
// so liveness would misread an in-flight build as dead. BAM falls back to the timeout instead.
|
|
46
|
+
noIndexProgressView: true
|
|
40
47
|
}
|
|
41
48
|
},
|
|
49
|
+
// No noIndexProgressView: pg-boss keeps its tables coordinator-local (it never calls
|
|
50
|
+
// create_distributed_table), so CREATE INDEX CONCURRENTLY runs against ordinary local Postgres tables
|
|
51
|
+
// on the coordinator, where pg_stat_progress_create_index is accurate and liveness-based reclaim is
|
|
52
|
+
// valid. This holds ONLY while the tables stay coordinator-local — if they are ever distributed, the
|
|
53
|
+
// coordinator's progress view would misread in-flight worker builds as dead and BAM could double-build.
|
|
42
54
|
citus: { kind: 'distributed', flags: {} },
|
|
43
55
|
pglite: { kind: 'embedded', flags: {} }
|
|
44
56
|
};
|
|
@@ -127,12 +139,17 @@ function checkUpdateArgs(args, { upsert = false } = {}) {
|
|
|
127
139
|
assert(match === undefined || JOB_MATCH_STRATEGIES.includes(match), `match must be one of: ${JOB_MATCH_STRATEGIES.join(', ')}`);
|
|
128
140
|
assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer');
|
|
129
141
|
if ('startAfter' in options) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
// Unlike send(), update() must honor a numeric startAfter of 0 (or negative) — the caller is
|
|
143
|
+
// explicitly pulling a deferred job forward to now. The send-style `+startAfter > 0` guard
|
|
144
|
+
// coerced 0 to undefined, which JSON.stringify then dropped, silently making the edit a no-op.
|
|
145
|
+
// Any finite number is passed through as a seconds interval ('0' -> now()); a string is kept.
|
|
146
|
+
const startAfter = options.startAfter;
|
|
147
|
+
options.startAfter = (startAfter instanceof Date && typeof startAfter.toISOString === 'function')
|
|
148
|
+
? startAfter.toISOString()
|
|
149
|
+
: (typeof startAfter === 'number' && Number.isFinite(startAfter))
|
|
150
|
+
? '' + startAfter
|
|
151
|
+
: (typeof startAfter === 'string')
|
|
152
|
+
? startAfter
|
|
136
153
|
: undefined;
|
|
137
154
|
}
|
|
138
155
|
validateRetryConfig(options);
|
|
@@ -377,6 +394,11 @@ function resolveBackend(config) {
|
|
|
377
394
|
if (config.__test__noAdvisoryLocks) {
|
|
378
395
|
config.noAdvisoryLocks = true;
|
|
379
396
|
}
|
|
397
|
+
// Test hook: exercise the no-liveness BAM reclaim path (timeout-only, no CONCURRENTLY healing)
|
|
398
|
+
// used by CockroachDB/YugabyteDB, on a plain Postgres instance.
|
|
399
|
+
if (config.__test__noIndexProgressView) {
|
|
400
|
+
config.noIndexProgressView = true;
|
|
401
|
+
}
|
|
380
402
|
}
|
|
381
403
|
function assertPostgresObjectName(name) {
|
|
382
404
|
assert(typeof name === 'string', 'Name must be a string');
|
package/dist/bam.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAGtC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAOnC,cAAM,GAAI,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAOzD,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAU1C,IAAI,OAAO,IAAK,OAAO,CAEtB;IAEK,KAAK;IAWL,IAAI;
|
|
1
|
+
{"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAGtC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAOnC,cAAM,GAAI,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAOzD,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAU1C,IAAI,OAAO,IAAK,OAAO,CAEtB;IAEK,KAAK;IAWL,IAAI;CA8HX;AAED,eAAe,GAAG,CAAA"}
|
package/dist/bam.js
CHANGED
|
@@ -80,6 +80,28 @@ class Bam extends EventEmitter {
|
|
|
80
80
|
table: entry.table
|
|
81
81
|
});
|
|
82
82
|
try {
|
|
83
|
+
// A re-attempted command (a stale in_progress reclaim, or a retry of a prior 'failed' — including
|
|
84
|
+
// failed rows left by older releases) may have an INVALID index behind it from an interrupted or
|
|
85
|
+
// failed CREATE INDEX CONCURRENTLY. Drop it first (best-effort, IF EXISTS) so the re-run rebuilds
|
|
86
|
+
// cleanly instead of the command's own IF NOT EXISTS skipping over a broken index forever. Only on
|
|
87
|
+
// the liveness path — CockroachDB/YugabyteDB roll interrupted builds back, so there's nothing to
|
|
88
|
+
// heal and DROP ... CONCURRENTLY isn't their model.
|
|
89
|
+
if (entry.reattempt && !this.#config.noIndexProgressView) {
|
|
90
|
+
const dropSql = plans.bamHealDrop(this.#config.schema, entry.command);
|
|
91
|
+
if (dropSql) {
|
|
92
|
+
// Only heal an index the previous attempt left INVALID. A re-attempt can also fire for a
|
|
93
|
+
// build that actually SUCCEEDED but whose row was never marked completed (a graceful stop
|
|
94
|
+
// landed between the CREATE and markCompleted) — that index is VALID and in use, so dropping
|
|
95
|
+
// it would tear down a live production index for the whole rebuild window. Probe indisvalid
|
|
96
|
+
// first; skip the drop for a valid (or absent) index and let the command's IF NOT EXISTS re-run
|
|
97
|
+
// no-op it and mark the row done.
|
|
98
|
+
const probeSql = plans.bamHealProbe(this.#config.schema, entry.command);
|
|
99
|
+
const { rows } = await this.#db.executeSql(probeSql);
|
|
100
|
+
if (rows[0]?.invalid) {
|
|
101
|
+
await this.#db.executeSql(dropSql);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
83
105
|
await this.#db.executeSql(entry.command);
|
|
84
106
|
if (this.#stopped)
|
|
85
107
|
return;
|
|
@@ -108,7 +130,7 @@ class Bam extends EventEmitter {
|
|
|
108
130
|
}
|
|
109
131
|
}
|
|
110
132
|
async #getNextCommand() {
|
|
111
|
-
const sql = plans.getNextBamCommand(this.#config.schema);
|
|
133
|
+
const sql = plans.getNextBamCommand(this.#config.schema, { useLiveness: !this.#config.noIndexProgressView });
|
|
112
134
|
const { rows } = await this.#db.executeSql(sql);
|
|
113
135
|
return rows[0] || null;
|
|
114
136
|
}
|
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;AAmBnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAW1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAWL,IAAI;IA8FJ,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;AAmBnC,cAAM,IAAK,SAAQ,YAAa,YAAW,KAAK,CAAC,WAAW;;IAW1D,MAAM;;;MAAS;gBAGb,EAAE,EAAE,KAAK,CAAC,SAAS,EACnB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,KAAK,CAAC,0BAA0B;IAa1C,IAAI,WAAW,IAAK,OAAO,CAE1B;IAEK,KAAK;IAWL,IAAI;IA8FJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE;CAgItD;AAED,eAAe,IAAI,CAAA"}
|
package/dist/boss.js
CHANGED
|
@@ -146,7 +146,6 @@ class Boss extends EventEmitter {
|
|
|
146
146
|
acc[table].queues.push(q);
|
|
147
147
|
return acc;
|
|
148
148
|
}, {});
|
|
149
|
-
const heartbeatQueueNames = new Set(queues.filter(q => q.heartbeatSeconds != null).map(q => q.name));
|
|
150
149
|
for (const queueGroup of Object.values(queueGroups)) {
|
|
151
150
|
if (this.#stopping)
|
|
152
151
|
return;
|
|
@@ -156,7 +155,7 @@ class Boss extends EventEmitter {
|
|
|
156
155
|
if (this.#stopping)
|
|
157
156
|
return;
|
|
158
157
|
const chunk = names.splice(0, 100);
|
|
159
|
-
await this.#monitor(table, chunk
|
|
158
|
+
await this.#monitor(table, chunk);
|
|
160
159
|
await this.#maintain(table, chunk);
|
|
161
160
|
}
|
|
162
161
|
}
|
|
@@ -165,7 +164,7 @@ class Boss extends EventEmitter {
|
|
|
165
164
|
await this.#maintainWarnings();
|
|
166
165
|
await this.#maintainQueueStats();
|
|
167
166
|
}
|
|
168
|
-
async #monitor(table, names
|
|
167
|
+
async #monitor(table, names) {
|
|
169
168
|
if (this.#stopping)
|
|
170
169
|
return;
|
|
171
170
|
const command = plans.trySetQueueMonitorTime(this.#config.schema, names, this.#config.monitorIntervalSeconds);
|
|
@@ -200,15 +199,12 @@ class Boss extends EventEmitter {
|
|
|
200
199
|
}
|
|
201
200
|
if (this.#stopping)
|
|
202
201
|
return;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const heartbeatSql = plans.failJobsByHeartbeat(this.#config.schema, table, heartbeatQueues, this.#config.noAdvisoryLocks);
|
|
210
|
-
await this.#executeQuery(heartbeatSql);
|
|
211
|
-
}
|
|
202
|
+
if (this.#config.noMultiMutationCte) {
|
|
203
|
+
await this.#manager.failJobsByHeartbeatDistributed(table, queues);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const heartbeatSql = plans.failJobsByHeartbeat(this.#config.schema, table, queues, this.#config.noAdvisoryLocks);
|
|
207
|
+
await this.#executeQuery(heartbeatSql);
|
|
212
208
|
}
|
|
213
209
|
}
|
|
214
210
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import Db from "./db.js";
|
|
6
6
|
import * as plans from "./plans.js";
|
|
7
|
+
import Contractor from "./contractor.js";
|
|
7
8
|
import * as migrationStore from "./migrationStore.js";
|
|
8
9
|
import packageJson from '../package.json' with { type: 'json' };
|
|
9
10
|
const schemaVersion = packageJson.pgboss.schema;
|
|
@@ -17,6 +18,7 @@ Commands:
|
|
|
17
18
|
migrate Run pending migrations (creates schema if not exists)
|
|
18
19
|
create Create the pg-boss schema (initial installation)
|
|
19
20
|
version Show current schema version
|
|
21
|
+
doctor Check for schema/index drift against the expected shape
|
|
20
22
|
plans Output SQL plans without executing
|
|
21
23
|
rollback Rollback the last migration
|
|
22
24
|
|
|
@@ -234,12 +236,15 @@ async function cmdMigrate(args) {
|
|
|
234
236
|
const schema = config.schema || plans.DEFAULT_SCHEMA;
|
|
235
237
|
if (args.dryRun) {
|
|
236
238
|
// 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
|
|
239
|
+
// (best effort) to read the DB's actual version and enumerate partitioned tables; fall back to
|
|
240
|
+
// job_common only offline.
|
|
238
241
|
let partitionTables = [];
|
|
242
|
+
let version = null;
|
|
239
243
|
try {
|
|
240
244
|
const db = await createDb(config);
|
|
241
245
|
try {
|
|
242
246
|
partitionTables = await getPartitionTables(db, schema);
|
|
247
|
+
version = await getSchemaVersion(db, schema);
|
|
243
248
|
}
|
|
244
249
|
finally {
|
|
245
250
|
await db.close();
|
|
@@ -248,8 +253,16 @@ async function cmdMigrate(args) {
|
|
|
248
253
|
catch {
|
|
249
254
|
// no reachable database: emit a job_common-only static script
|
|
250
255
|
}
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
if (version !== null && version >= schemaVersion) {
|
|
257
|
+
console.log(`-- pg-boss schema "${schema}" is already at version ${version} (latest: ${schemaVersion}); nothing to migrate.`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Render from the DB's actual version so the printed SQL is exactly what `migrate` would run.
|
|
261
|
+
// Offline (or not yet installed) we can't know it, so fall back to the oldest supported starting
|
|
262
|
+
// version — the full chain — instead of a bogus "from 0" that fails on non-idempotent steps.
|
|
263
|
+
const fromVersion = version ?? migrationStore.getMinVersion(schema);
|
|
264
|
+
const sql = migrationStore.migrate(schema, fromVersion, undefined, undefined, { inlineAsync: true, partitionTables });
|
|
265
|
+
console.log(`-- SQL to migrate pg-boss from version ${fromVersion} to ${schemaVersion}:`);
|
|
253
266
|
console.log(sql);
|
|
254
267
|
return;
|
|
255
268
|
}
|
|
@@ -312,6 +325,134 @@ async function cmdRollback(args) {
|
|
|
312
325
|
await db.close();
|
|
313
326
|
}
|
|
314
327
|
}
|
|
328
|
+
async function cmdDoctor(args) {
|
|
329
|
+
const config = getConnectionConfig(args);
|
|
330
|
+
const schema = config.schema || plans.DEFAULT_SCHEMA;
|
|
331
|
+
const db = await createDb(config);
|
|
332
|
+
try {
|
|
333
|
+
const version = await getSchemaVersion(db, schema);
|
|
334
|
+
if (version === null) {
|
|
335
|
+
console.log(`pg-boss is not installed in schema "${schema}"`);
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
console.log(`Schema "${schema}" version ${version} (latest: ${schemaVersion})`);
|
|
340
|
+
if (version < schemaVersion) {
|
|
341
|
+
console.log(`⚠ Migrations pending: ${schemaVersion - version} — run "pg-boss migrate" before trusting drift results`);
|
|
342
|
+
}
|
|
343
|
+
// Reuse the same drift scan the boss.detectSchemaDrift() API runs, rather than duplicating the
|
|
344
|
+
// catalog queries + computeSchemaDrift wiring here (the two copies had already drifted apart and
|
|
345
|
+
// carried divergent best-effort/backend-gating bugs). Contractor.detectDrift handles the
|
|
346
|
+
// partitioned probe, best-effort catalog fallbacks, and backend-specific gating in one place.
|
|
347
|
+
const contractor = new Contractor(db, { ...config, schema });
|
|
348
|
+
const report = await contractor.detectDrift();
|
|
349
|
+
if (report.building.length) {
|
|
350
|
+
console.log(`\nBuilding (async index build in progress — not yet drift) (${report.building.length}):`);
|
|
351
|
+
for (const i of report.building)
|
|
352
|
+
console.log(` ${i.table}.${i.name}`);
|
|
353
|
+
}
|
|
354
|
+
// Extra indexes are informational (a stale pg-boss index or a user-added one) — a warning, not
|
|
355
|
+
// drift. Printed regardless of overall status; it never changes the exit code.
|
|
356
|
+
if (report.extraIndexes.length) {
|
|
357
|
+
console.log(`\n⚠ EXTRA INDEXES (present on a managed table but not expected — harmless) (${report.extraIndexes.length}):`);
|
|
358
|
+
for (const i of report.extraIndexes)
|
|
359
|
+
console.log(` ${i.table}.${i.name}`);
|
|
360
|
+
}
|
|
361
|
+
if (report.ok) {
|
|
362
|
+
console.log('\n✓ No drift detected');
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (report.missingTables.length) {
|
|
366
|
+
console.log(`\nMISSING TABLES (expected but absent) (${report.missingTables.length}):`);
|
|
367
|
+
for (const t of report.missingTables)
|
|
368
|
+
console.log(` ${t}`);
|
|
369
|
+
}
|
|
370
|
+
// Each drifted index is printed with the expected (correct) definition and, where one exists, the
|
|
371
|
+
// actual definition beneath it, for a direct side-by-side comparison and copy-paste remediation.
|
|
372
|
+
if (report.missing.length) {
|
|
373
|
+
console.log(`\nMISSING (expected but absent) (${report.missing.length}):`);
|
|
374
|
+
for (const i of report.missing) {
|
|
375
|
+
console.log(` ${i.table}.${i.name}`);
|
|
376
|
+
if (i.definition)
|
|
377
|
+
console.log(` expected: ${i.definition}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (report.invalid.length) {
|
|
381
|
+
// The definition is correct — the index is just invalid (interrupted build) — so show the DDL to
|
|
382
|
+
// drop and rebuild it, not an expected-vs-actual comparison (they would be identical).
|
|
383
|
+
console.log(`\nINVALID (present but marked invalid — drop and rebuild) (${report.invalid.length}):`);
|
|
384
|
+
for (const i of report.invalid) {
|
|
385
|
+
console.log(` ${i.table}.${i.name}`);
|
|
386
|
+
if (i.definition)
|
|
387
|
+
console.log(` rebuild: ${i.definition}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (report.mismatched.length) {
|
|
391
|
+
console.log(`\nMISMATCHED (definition differs) (${report.mismatched.length}):`);
|
|
392
|
+
for (const m of report.mismatched) {
|
|
393
|
+
console.log(` ${m.table}.${m.name} [${m.differs.join(', ')}]`);
|
|
394
|
+
if (m.definition)
|
|
395
|
+
console.log(` expected: ${m.definition}`);
|
|
396
|
+
console.log(` actual: ${m.actualDefinition}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (report.missingFunctions.length) {
|
|
400
|
+
console.log(`\nMISSING FUNCTIONS (expected but absent) (${report.missingFunctions.length}):`);
|
|
401
|
+
for (const f of report.missingFunctions) {
|
|
402
|
+
console.log(` ${f.name}`);
|
|
403
|
+
console.log(` expected: ${f.definition}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (report.mismatchedFunctions.length) {
|
|
407
|
+
console.log(`\nMISMATCHED FUNCTIONS (body differs) (${report.mismatchedFunctions.length}):`);
|
|
408
|
+
for (const f of report.mismatchedFunctions) {
|
|
409
|
+
console.log(` ${f.name}`);
|
|
410
|
+
console.log(` expected: ${f.definition}`);
|
|
411
|
+
console.log(` actual: ${f.actualDefinition}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (report.columnDrift.length) {
|
|
415
|
+
console.log(`\nCOLUMN DRIFT (missing/unexpected columns, or default/type/nullability drift) (${report.columnDrift.length}):`);
|
|
416
|
+
for (const c of report.columnDrift) {
|
|
417
|
+
console.log(` ${c.table}`);
|
|
418
|
+
if (c.missingColumns.length)
|
|
419
|
+
console.log(` missing: ${c.missingColumns.join(', ')}`);
|
|
420
|
+
if (c.unexpectedColumns.length)
|
|
421
|
+
console.log(` unexpected: ${c.unexpectedColumns.join(', ')}`);
|
|
422
|
+
for (const d of c.defaultMismatches)
|
|
423
|
+
console.log(` default ${d.column}: expected ${d.expected}, actual ${d.actual}`);
|
|
424
|
+
for (const d of c.typeMismatches)
|
|
425
|
+
console.log(` type ${d.column}: expected ${d.expected}, actual ${d.actual}`);
|
|
426
|
+
for (const d of c.nullabilityMismatches)
|
|
427
|
+
console.log(` nullability ${d.column}: expected ${d.expected ? 'NOT NULL' : 'nullable'}, actual ${d.actual ? 'NOT NULL' : 'nullable'}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (report.constraintDrift.length) {
|
|
431
|
+
console.log(`\nCONSTRAINT DRIFT (missing or unexpected constraints) (${report.constraintDrift.length}):`);
|
|
432
|
+
for (const c of report.constraintDrift) {
|
|
433
|
+
console.log(` ${c.table}`);
|
|
434
|
+
if (c.missingConstraints.length)
|
|
435
|
+
for (const d of c.missingConstraints)
|
|
436
|
+
console.log(` missing: ${d}`);
|
|
437
|
+
if (c.unexpectedConstraints.length)
|
|
438
|
+
for (const d of c.unexpectedConstraints)
|
|
439
|
+
console.log(` unexpected: ${d}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (report.enumDrift) {
|
|
443
|
+
const e = report.enumDrift;
|
|
444
|
+
console.log('\nENUM DRIFT (value set/order differs):');
|
|
445
|
+
console.log(` ${e.name}`);
|
|
446
|
+
console.log(` expected: ${e.expectedValues.join(', ')}`);
|
|
447
|
+
console.log(` actual: ${e.actualValues.join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
console.log('\n✗ Schema drift detected');
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
await db.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
315
456
|
async function cmdPlans(args) {
|
|
316
457
|
const fileConfig = loadConfigFile(args.config);
|
|
317
458
|
const schema = args.schema || process.env.PGBOSS_SCHEMA || fileConfig.schema || plans.DEFAULT_SCHEMA;
|
|
@@ -371,6 +512,9 @@ async function main() {
|
|
|
371
512
|
case 'version':
|
|
372
513
|
await cmdVersion(args);
|
|
373
514
|
break;
|
|
515
|
+
case 'doctor':
|
|
516
|
+
await cmdDoctor(args);
|
|
517
|
+
break;
|
|
374
518
|
case 'create':
|
|
375
519
|
await cmdCreate(args);
|
|
376
520
|
break;
|
package/dist/contractor.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ declare class Contractor {
|
|
|
14
14
|
schemaVersion(): Promise<number | null>;
|
|
15
15
|
isInstalled(): Promise<boolean>;
|
|
16
16
|
start(): Promise<void>;
|
|
17
|
+
detectDrift(): Promise<types.SchemaDriftReport>;
|
|
17
18
|
check(): Promise<void>;
|
|
18
19
|
create(): Promise<void>;
|
|
19
20
|
migrate(version: number): Promise<void>;
|
package/dist/contractor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"contractor.d.ts","sourceRoot":"","sources":["../src/contractor.ts"],"names":[],"mappings":"AAKA,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;IAiBL,WAAW,IAAK,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC;IAqGhD,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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import * as plans from "./plans.js";
|
|
3
|
+
import * as drifter from "./drifter.js";
|
|
3
4
|
import * as migrationStore from "./migrationStore.js";
|
|
4
5
|
import packageJson from '../package.json' with { type: 'json' };
|
|
5
6
|
const schemaVersion = packageJson.pgboss.schema;
|
|
@@ -44,6 +45,102 @@ class Contractor {
|
|
|
44
45
|
await this.create();
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
// Presence-level schema drift scan: compares the managed indexes the code expects against the live
|
|
49
|
+
// catalog. Partitioned vs. non-partitioned is read from the database (job_common presence), and the
|
|
50
|
+
// per-queue policy indexes are computed from the queue table, so conditional indexes are handled.
|
|
51
|
+
async detectDrift() {
|
|
52
|
+
const schema = this.config.schema;
|
|
53
|
+
const probe = await this.db.executeSql(plans.jobCommonExists(schema));
|
|
54
|
+
const partitioned = !!probe.rows[0].name;
|
|
55
|
+
const partitions = partitioned
|
|
56
|
+
? (await this.db.executeSql(plans.getManagedQueuePartitions(schema))).rows
|
|
57
|
+
: [];
|
|
58
|
+
const liveResult = await this.db.executeSql(drifter.getSchemaIndexes(schema));
|
|
59
|
+
const live = liveResult.rows.map((r) => ({
|
|
60
|
+
name: r.name,
|
|
61
|
+
table: r.table,
|
|
62
|
+
valid: r.valid,
|
|
63
|
+
def: r.def,
|
|
64
|
+
constraintBacked: r.constraintBacked
|
|
65
|
+
}));
|
|
66
|
+
// The bam table only exists from schema v27; ignore its absence on very old schemas.
|
|
67
|
+
let bamCommands = [];
|
|
68
|
+
try {
|
|
69
|
+
const bamResult = await this.db.executeSql(plans.getIncompleteBamCommands(schema));
|
|
70
|
+
bamCommands = bamResult.rows.map((r) => r.command);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
bamCommands = [];
|
|
74
|
+
}
|
|
75
|
+
// Function-body and enum drift are best-effort: pg_get_functiondef is unsupported on some backends
|
|
76
|
+
// (CockroachDB), so a failure here SKIPS the function check rather than aborting the whole scan.
|
|
77
|
+
// `functionsSupported` must be tracked separately from an empty result: an empty `liveFunctions`
|
|
78
|
+
// means "query failed / unsupported", which is NOT the same as "no functions found" — feeding
|
|
79
|
+
// `live: []` to the drift check would report every expected function as missing and flip `ok` to
|
|
80
|
+
// false on every CockroachDB scan. So the check is gated (passed `undefined`) when the query throws.
|
|
81
|
+
let liveFunctions = [];
|
|
82
|
+
let functionsSupported = true;
|
|
83
|
+
try {
|
|
84
|
+
const fnResult = await this.db.executeSql(drifter.getSchemaFunctions(schema));
|
|
85
|
+
liveFunctions = fnResult.rows.map((r) => ({ name: r.name, def: r.def }));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
functionsSupported = false;
|
|
89
|
+
}
|
|
90
|
+
let enumLabels = [];
|
|
91
|
+
try {
|
|
92
|
+
const enumResult = await this.db.executeSql(drifter.getEnumDefinition(schema));
|
|
93
|
+
enumLabels = enumResult.rows.map((r) => r.label);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
enumLabels = [];
|
|
97
|
+
}
|
|
98
|
+
// Table presence is read from a catalog-only query independent of the column diff below. The column
|
|
99
|
+
// query uses pg_get_expr (unsupported on some backends) and is best-effort; if it throws, the column
|
|
100
|
+
// check is skipped — but table presence must NOT collapse to "everything missing", so it comes from
|
|
101
|
+
// its own pg_class probe. pg_class is available everywhere, so this rarely throws; if it somehow
|
|
102
|
+
// does, fall back to the columns-derived set rather than aborting the scan.
|
|
103
|
+
let liveTables = null;
|
|
104
|
+
try {
|
|
105
|
+
const tableResult = await this.db.executeSql(drifter.getSchemaTables(schema));
|
|
106
|
+
liveTables = tableResult.rows.map((r) => r.table);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
liveTables = null;
|
|
110
|
+
}
|
|
111
|
+
let liveColumns = [];
|
|
112
|
+
try {
|
|
113
|
+
const colResult = await this.db.executeSql(drifter.getSchemaColumns(schema));
|
|
114
|
+
liveColumns = colResult.rows.map((r) => ({ table: r.table, column: r.column, default: r.default, type: r.type, notNull: r.notNull }));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
liveColumns = [];
|
|
118
|
+
}
|
|
119
|
+
let liveConstraints = [];
|
|
120
|
+
try {
|
|
121
|
+
const conResult = await this.db.executeSql(drifter.getSchemaConstraints(schema));
|
|
122
|
+
liveConstraints = conResult.rows.map((r) => ({ table: r.table, def: r.def }));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
liveConstraints = [];
|
|
126
|
+
}
|
|
127
|
+
const building = new Set(bamCommands.map(plans.bamCommandIndexName).filter((n) => n !== null));
|
|
128
|
+
// CockroachDB renders column types (INT8 vs integer), default expressions, and constraint
|
|
129
|
+
// definitions differently from standard Postgres, so the canonical-form checks would false-positive
|
|
130
|
+
// there. Restrict type/default/constraint drift to Postgres-typed backends; the presence checks
|
|
131
|
+
// (tables, indexes, column names, functions, enum) still run everywhere.
|
|
132
|
+
const canonicalPg = this.config.backend !== 'cockroachdb';
|
|
133
|
+
const expectedColumns = plans.expectedManagedColumns(schema, partitioned, partitions)
|
|
134
|
+
.map(c => canonicalPg ? c : { table: c.table, columns: c.columns });
|
|
135
|
+
return drifter.computeSchemaDrift({
|
|
136
|
+
indexes: { expected: plans.expectedManagedIndexes(schema, partitioned, partitions), live, building },
|
|
137
|
+
tables: { expected: plans.expectedManagedTables(schema, partitioned, partitions), live: liveTables ?? [...new Set(liveColumns.map(c => c.table))] },
|
|
138
|
+
functions: functionsSupported ? { expected: plans.expectedManagedFunctions(schema, partitioned), live: liveFunctions } : undefined,
|
|
139
|
+
columns: { expected: expectedColumns, live: liveColumns },
|
|
140
|
+
constraints: canonicalPg ? { expected: plans.expectedManagedConstraints(schema, partitioned), live: liveConstraints } : undefined,
|
|
141
|
+
enum: { name: 'job_state', expected: plans.EXPECTED_JOB_STATES, actual: enumLabels }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
47
144
|
async check() {
|
|
48
145
|
const installed = await this.isInstalled();
|
|
49
146
|
if (!installed) {
|
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;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;
|
|
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;IAmFxB,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
|
@@ -52,6 +52,12 @@ class Db extends EventEmitter {
|
|
|
52
52
|
let client = null;
|
|
53
53
|
let reconnectTimer = null;
|
|
54
54
|
let attempt = 0;
|
|
55
|
+
// Only self-heal once the listener has been established at least once. If the INITIAL connect
|
|
56
|
+
// fails, the rejection propagates to the caller (Notifier.start), which falls back to
|
|
57
|
+
// polling-only and discards this subscription's close handle — so a reconnect scheduled from
|
|
58
|
+
// the client 'error' handler would be an untracked connection nothing can close, keeping the
|
|
59
|
+
// event loop alive and delivering notifications into a stopped manager.
|
|
60
|
+
let established = false;
|
|
55
61
|
const scheduleReconnect = () => {
|
|
56
62
|
if (closed || reconnectTimer)
|
|
57
63
|
return;
|
|
@@ -73,7 +79,8 @@ class Db extends EventEmitter {
|
|
|
73
79
|
next.end().catch(() => { });
|
|
74
80
|
if (client === next)
|
|
75
81
|
client = null;
|
|
76
|
-
|
|
82
|
+
if (established)
|
|
83
|
+
scheduleReconnect();
|
|
77
84
|
}
|
|
78
85
|
});
|
|
79
86
|
next.on('notification', msg => {
|
|
@@ -98,6 +105,7 @@ class Db extends EventEmitter {
|
|
|
98
105
|
throw err;
|
|
99
106
|
}
|
|
100
107
|
attempt = 0;
|
|
108
|
+
established = true;
|
|
101
109
|
onReconnect();
|
|
102
110
|
};
|
|
103
111
|
await connect();
|