pg-boss 10.4.0 → 10.4.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.
Files changed (48) hide show
  1. package/.claude/settings.local.json +11 -1
  2. package/dist/attorney.d.ts +19 -0
  3. package/dist/attorney.d.ts.map +1 -0
  4. package/dist/attorney.js +227 -0
  5. package/dist/bam.d.ts +14 -0
  6. package/dist/bam.d.ts.map +1 -0
  7. package/dist/bam.js +114 -0
  8. package/dist/boss.d.ts +16 -0
  9. package/dist/boss.d.ts.map +1 -0
  10. package/dist/boss.js +163 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +333 -0
  14. package/dist/contractor.d.ts +22 -0
  15. package/dist/contractor.d.ts.map +1 -0
  16. package/dist/contractor.js +81 -0
  17. package/dist/db.d.ts +16 -0
  18. package/dist/db.d.ts.map +1 -0
  19. package/dist/db.js +46 -0
  20. package/dist/index.d.ts +72 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +266 -0
  23. package/dist/manager.d.ts +93 -0
  24. package/dist/manager.d.ts.map +1 -0
  25. package/dist/manager.js +746 -0
  26. package/dist/migrationStore.d.ts +7 -0
  27. package/dist/migrationStore.d.ts.map +1 -0
  28. package/dist/migrationStore.js +425 -0
  29. package/dist/plans.d.ts +102 -0
  30. package/dist/plans.d.ts.map +1 -0
  31. package/dist/plans.js +1220 -0
  32. package/dist/spy.d.ts +23 -0
  33. package/dist/spy.d.ts.map +1 -0
  34. package/dist/spy.js +73 -0
  35. package/dist/timekeeper.d.ts +34 -0
  36. package/dist/timekeeper.d.ts.map +1 -0
  37. package/dist/timekeeper.js +158 -0
  38. package/dist/tools.d.ts +18 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +45 -0
  41. package/dist/types.d.ts +301 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +1 -0
  44. package/dist/worker.d.ts +43 -0
  45. package/dist/worker.d.ts.map +1 -0
  46. package/dist/worker.js +113 -0
  47. package/package.json +1 -1
  48. package/src/plans.js +4 -4
@@ -7,7 +7,17 @@
7
7
  "Bash(done)",
8
8
  "Bash(find:*)",
9
9
  "Bash(npm test:*)",
10
- "mcp__ide__getDiagnostics"
10
+ "mcp__ide__getDiagnostics",
11
+ "WebFetch(domain:github.com)",
12
+ "Bash(npm run build:*)",
13
+ "Bash(npm run tsc:*)",
14
+ "Bash(cut:*)",
15
+ "Bash(grep:*)",
16
+ "WebSearch",
17
+ "WebFetch(domain:docs.npmjs.com)",
18
+ "WebFetch(domain:ankushkun.medium.com)",
19
+ "WebFetch(domain:github.blog)",
20
+ "Bash(ls:*)"
11
21
  ],
12
22
  "deny": [],
13
23
  "ask": []
@@ -0,0 +1,19 @@
1
+ import type * as types from './types.ts';
2
+ declare const POLICY: {
3
+ MAX_EXPIRATION_HOURS: number;
4
+ MIN_POLLING_INTERVAL_MS: number;
5
+ MAX_RETENTION_DAYS: number;
6
+ };
7
+ declare function validateQueueArgs(config?: any): void;
8
+ declare function checkSendArgs(args: any): types.Request;
9
+ declare function checkWorkArgs(name: string, args: any[]): {
10
+ options: types.ResolvedWorkOptions;
11
+ callback: types.WorkHandler<any>;
12
+ };
13
+ declare function checkFetchArgs(name: string, options: any): void;
14
+ declare function getConfig(value: string | types.ConstructorOptions): types.ResolvedConstructorOptions;
15
+ declare function assertPostgresObjectName(name: string): void;
16
+ declare function assertQueueName(name: string): void;
17
+ declare function assertKey(key: string): void;
18
+ export { assertKey, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkWorkArgs, getConfig, POLICY, validateQueueArgs };
19
+ //# sourceMappingURL=attorney.d.ts.map
@@ -0,0 +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;AAMD,iBAAS,iBAAiB,CAAE,MAAM,GAAE,GAAQ,QAW3C;AAED,iBAAS,aAAa,CAAE,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,OAAO,CA+ChD;AAmED,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,CA2BA;AAED,iBAAS,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,QAOlD;AAED,iBAAS,SAAS,CAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC,0BAA0B,CAoB9F;AAkBD,iBAAS,wBAAwB,CAAE,IAAI,EAAE,MAAM,QAK9C;AAED,iBAAS,eAAe,CAAE,IAAI,EAAE,MAAM,QAIrC;AAED,iBAAS,SAAS,CAAE,GAAG,EAAE,MAAM,QAI9B;AA+FD,OAAO,EACL,SAAS,EACT,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,MAAM,EACN,iBAAiB,EAClB,CAAA"}
@@ -0,0 +1,227 @@
1
+ import assert from 'node:assert';
2
+ import { DEFAULT_SCHEMA } from "./plans.js";
3
+ const POLICY = {
4
+ MAX_EXPIRATION_HOURS: 24,
5
+ MIN_POLLING_INTERVAL_MS: 500,
6
+ MAX_RETENTION_DAYS: 365
7
+ };
8
+ function assertObjectName(value, name = 'Name') {
9
+ assert(/^[\w.-]+$/.test(value), `${name} can only contain alphanumeric characters, underscores, hyphens, or periods`);
10
+ }
11
+ function validateQueueArgs(config = {}) {
12
+ assert(!('deadLetter' in config) || config.deadLetter === null || (typeof config.deadLetter === 'string'), 'deadLetter must be a string');
13
+ if (config.deadLetter) {
14
+ assertObjectName(config.deadLetter, 'deadLetter');
15
+ }
16
+ validateRetryConfig(config);
17
+ validateExpirationConfig(config);
18
+ validateRetentionConfig(config);
19
+ validateDeletionConfig(config);
20
+ }
21
+ function checkSendArgs(args) {
22
+ let name, data, options;
23
+ if (typeof args[0] === 'string') {
24
+ name = args[0];
25
+ data = args[1];
26
+ assert(typeof data !== 'function', 'send() cannot accept a function as the payload. Did you intend to use work()?');
27
+ options = args[2];
28
+ }
29
+ else if (typeof args[0] === 'object') {
30
+ assert(args.length === 1, 'send object API only accepts 1 argument');
31
+ const job = args[0];
32
+ assert(job, 'boss requires all jobs to have a name');
33
+ name = job.name;
34
+ data = job.data;
35
+ options = job.options;
36
+ }
37
+ options = options || {};
38
+ assert(name, 'boss requires all jobs to have a queue name');
39
+ assert(typeof options === 'object', 'options should be an object');
40
+ options = { ...options };
41
+ assert(!('priority' in options) || (Number.isInteger(options.priority)), 'priority must be an integer');
42
+ options.priority = options.priority || 0;
43
+ options.startAfter = (options.startAfter instanceof Date && typeof options.startAfter.toISOString === 'function')
44
+ ? options.startAfter.toISOString()
45
+ : (+options.startAfter > 0)
46
+ ? '' + options.startAfter
47
+ : (typeof options.startAfter === 'string')
48
+ ? options.startAfter
49
+ : undefined;
50
+ validateRetryConfig(options);
51
+ validateExpirationConfig(options);
52
+ validateRetentionConfig(options);
53
+ validateDeletionConfig(options);
54
+ validateGroupConfig(options);
55
+ return { name, data, options };
56
+ }
57
+ function validateGroupConfig(config) {
58
+ if (!('group' in config) || config.group === undefined || config.group === null) {
59
+ return;
60
+ }
61
+ assert(typeof config.group === 'object', 'group must be an object');
62
+ assert(typeof config.group.id === 'string' && config.group.id.length > 0, 'group.id must be a non-empty string');
63
+ assert(!('tier' in config.group) || (typeof config.group.tier === 'string' && config.group.tier.length > 0), 'group.tier must be a non-empty string if provided');
64
+ }
65
+ function validateGroupConcurrencyValue(value, optionName) {
66
+ if (typeof value === 'number') {
67
+ assert(Number.isInteger(value) && value >= 1, `${optionName} must be an integer >= 1`);
68
+ return;
69
+ }
70
+ assert(typeof value === 'object', `${optionName} must be a number or an object with { default, tiers? }`);
71
+ assert(Number.isInteger(value.default) && value.default >= 1, `${optionName}.default must be an integer >= 1`);
72
+ if ('tiers' in value && value.tiers) {
73
+ assert(typeof value.tiers === 'object', `${optionName}.tiers must be an object`);
74
+ for (const [tier, limit] of Object.entries(value.tiers)) {
75
+ assert(typeof tier === 'string' && tier.length > 0, `${optionName} tier keys must be non-empty strings`);
76
+ assert(Number.isInteger(limit) && limit >= 1, `${optionName}.tiers["${tier}"] must be an integer >= 1`);
77
+ }
78
+ }
79
+ }
80
+ function validateGroupConcurrencyConfig(config) {
81
+ const hasGlobal = config.groupConcurrency != null;
82
+ const hasLocal = config.localGroupConcurrency != null;
83
+ assert(!(hasGlobal && hasLocal), 'cannot specify both groupConcurrency and localGroupConcurrency - choose one');
84
+ if (hasGlobal)
85
+ validateGroupConcurrencyValue(config.groupConcurrency, 'groupConcurrency');
86
+ if (hasLocal) {
87
+ validateGroupConcurrencyValue(config.localGroupConcurrency, 'localGroupConcurrency');
88
+ validateLocalGroupConcurrencyLimit(config.localGroupConcurrency, config.localConcurrency);
89
+ }
90
+ }
91
+ function validateLocalGroupConcurrencyLimit(localGroupConcurrency, localConcurrency) {
92
+ const effectiveLocalConcurrency = localConcurrency ?? 1;
93
+ if (typeof localGroupConcurrency === 'number') {
94
+ assert(localGroupConcurrency <= effectiveLocalConcurrency, `localGroupConcurrency (${localGroupConcurrency}) cannot exceed localConcurrency (${effectiveLocalConcurrency})`);
95
+ }
96
+ else if (typeof localGroupConcurrency === 'object') {
97
+ assert(localGroupConcurrency.default <= effectiveLocalConcurrency, `localGroupConcurrency.default (${localGroupConcurrency.default}) cannot exceed localConcurrency (${effectiveLocalConcurrency})`);
98
+ if (localGroupConcurrency.tiers) {
99
+ for (const [tier, limit] of Object.entries(localGroupConcurrency.tiers)) {
100
+ assert(limit <= effectiveLocalConcurrency, `localGroupConcurrency.tiers["${tier}"] (${limit}) cannot exceed localConcurrency (${effectiveLocalConcurrency})`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ function checkWorkArgs(name, args) {
106
+ let options, callback;
107
+ assert(name, 'queue name is required');
108
+ if (args.length === 1) {
109
+ callback = args[0];
110
+ options = {};
111
+ }
112
+ else if (args.length > 1) {
113
+ options = args[0] || {};
114
+ callback = args[1];
115
+ }
116
+ assert(typeof callback === 'function', 'expected callback to be a function');
117
+ assert(typeof options === 'object', 'expected config to be an object');
118
+ options = { ...options };
119
+ applyPollingInterval(options);
120
+ assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0');
121
+ assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean');
122
+ assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean');
123
+ assert(!('localConcurrency' in options) || (Number.isInteger(options.localConcurrency) && options.localConcurrency >= 1), 'localConcurrency must be an integer >= 1');
124
+ validateGroupConcurrencyConfig(options);
125
+ return { options, callback };
126
+ }
127
+ function checkFetchArgs(name, options) {
128
+ assert(name, 'missing queue name');
129
+ assert(!('batchSize' in options) || (Number.isInteger(options.batchSize) && options.batchSize >= 1), 'batchSize must be an integer > 0');
130
+ assert(!('includeMetadata' in options) || typeof options.includeMetadata === 'boolean', 'includeMetadata must be a boolean');
131
+ assert(!('priority' in options) || typeof options.priority === 'boolean', 'priority must be a boolean');
132
+ assert(!('ignoreStartAfter' in options) || typeof options.ignoreStartAfter === 'boolean', 'ignoreStartAfter must be a boolean');
133
+ }
134
+ function getConfig(value) {
135
+ assert(value && (typeof value === 'object' || typeof value === 'string'), 'configuration assert: string or config object is required to connect to postgres');
136
+ const config = (typeof value === 'string')
137
+ ? { connectionString: value }
138
+ : { ...value };
139
+ config.schedule = ('schedule' in config) ? config.schedule : true;
140
+ config.supervise = ('supervise' in config) ? config.supervise : true;
141
+ config.migrate = ('migrate' in config) ? config.migrate : true;
142
+ config.createSchema = ('createSchema' in config) ? config.createSchema : true;
143
+ applySchemaConfig(config);
144
+ applyOpsConfig(config);
145
+ applyScheduleConfig(config);
146
+ applyBamConfig(config);
147
+ validateWarningConfig(config);
148
+ return config;
149
+ }
150
+ function applySchemaConfig(config) {
151
+ if (config.schema) {
152
+ assertPostgresObjectName(config.schema);
153
+ }
154
+ config.schema = config.schema || DEFAULT_SCHEMA;
155
+ }
156
+ function validateWarningConfig(config) {
157
+ assert(!('warningQueueSize' in config) || config.warningQueueSize >= 1, 'configuration assert: warningQueueSize must be at least 1');
158
+ assert(!('warningSlowQuerySeconds' in config) || config.warningSlowQuerySeconds >= 1, 'configuration assert: warningSlowQuerySeconds must be at least 1');
159
+ }
160
+ function assertPostgresObjectName(name) {
161
+ assert(typeof name === 'string', 'Name must be a string');
162
+ assert(name.length <= 50, 'Name cannot exceed 50 characters');
163
+ assert(!/\W/.test(name), 'Name can only contain alphanumeric characters or underscores');
164
+ assert(!/^\d/.test(name), 'Name cannot start with a number');
165
+ }
166
+ function assertQueueName(name) {
167
+ assert(name, 'Name is required');
168
+ assert(typeof name === 'string', 'Name must be a string');
169
+ assertObjectName(name);
170
+ }
171
+ function assertKey(key) {
172
+ if (!key)
173
+ return;
174
+ assert(typeof key === 'string', 'Key must be a string');
175
+ assertObjectName(key, 'Key');
176
+ }
177
+ function validateRetentionConfig(config) {
178
+ assert(!('retentionSeconds' in config) || config.retentionSeconds >= 1, 'configuration assert: retentionSeconds must be at least every second');
179
+ }
180
+ function validateExpirationConfig(config) {
181
+ assert(!('expireInSeconds' in config) || config.expireInSeconds >= 1, 'configuration assert: expireInSeconds must be at least every second');
182
+ assert(!config.expireInSeconds || config.expireInSeconds / 60 / 60 < POLICY.MAX_EXPIRATION_HOURS, `configuration assert: expiration cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
183
+ }
184
+ function validateRetryConfig(config) {
185
+ assert(!('retryDelay' in config) || (Number.isInteger(config.retryDelay) && config.retryDelay >= 0), 'retryDelay must be an integer >= 0');
186
+ assert(!('retryLimit' in config) || (Number.isInteger(config.retryLimit) && config.retryLimit >= 0), 'retryLimit must be an integer >= 0');
187
+ assert(!('retryBackoff' in config) || (config.retryBackoff === true || config.retryBackoff === false), 'retryBackoff must be either true or false');
188
+ assert(!('retryDelayMax' in config) || config.retryDelayMax === null || config.retryBackoff === true, 'retryDelayMax can only be set if retryBackoff is true');
189
+ assert(!('retryDelayMax' in config) || config.retryDelayMax === null || (Number.isInteger(config.retryDelayMax) && config.retryDelayMax >= 0), 'retryDelayMax must be an integer >= 0');
190
+ }
191
+ function applyPollingInterval(config) {
192
+ assert(!('pollingIntervalSeconds' in config) || config.pollingIntervalSeconds >= POLICY.MIN_POLLING_INTERVAL_MS / 1000, `configuration assert: pollingIntervalSeconds must be at least every ${POLICY.MIN_POLLING_INTERVAL_MS}ms`);
193
+ config.pollingInterval = ('pollingIntervalSeconds' in config)
194
+ ? config.pollingIntervalSeconds * 1000
195
+ : 2000;
196
+ }
197
+ function applyOpsConfig(config) {
198
+ assert(!('superviseIntervalSeconds' in config) || config.superviseIntervalSeconds >= 1, 'configuration assert: superviseIntervalSeconds must be at least every second');
199
+ config.superviseIntervalSeconds = config.superviseIntervalSeconds || 60;
200
+ assert(config.superviseIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS, `configuration assert: superviseIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
201
+ assert(!('maintenanceIntervalSeconds' in config) || config.maintenanceIntervalSeconds >= 1, 'configuration assert: maintenanceIntervalSeconds must be at least every second');
202
+ config.maintenanceIntervalSeconds = config.maintenanceIntervalSeconds || POLICY.MAX_EXPIRATION_HOURS * 60 * 60;
203
+ assert(config.maintenanceIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS, `configuration assert: maintenanceIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
204
+ assert(!('monitorIntervalSeconds' in config) || config.monitorIntervalSeconds >= 1, 'configuration assert: monitorIntervalSeconds must be at least every second');
205
+ config.monitorIntervalSeconds = config.monitorIntervalSeconds || 60;
206
+ assert(config.monitorIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS, `configuration assert: monitorIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
207
+ assert(!('queueCacheIntervalSeconds' in config) || config.queueCacheIntervalSeconds >= 1, 'configuration assert: queueCacheIntervalSeconds must be at least every second');
208
+ config.queueCacheIntervalSeconds = config.queueCacheIntervalSeconds || 60;
209
+ assert(config.queueCacheIntervalSeconds / 60 / 60 <= POLICY.MAX_EXPIRATION_HOURS, `configuration assert: queueCacheIntervalSeconds cannot exceed ${POLICY.MAX_EXPIRATION_HOURS} hours`);
210
+ }
211
+ function validateDeletionConfig(config) {
212
+ assert(!('deleteAfterSeconds' in config) || config.deleteAfterSeconds >= 0, 'configuration assert: deleteAfterSeconds must be at least 0 (0 disables deletion)');
213
+ }
214
+ function applyScheduleConfig(config) {
215
+ assert(!('clockMonitorIntervalSeconds' in config) || (config.clockMonitorIntervalSeconds >= 1 && config.clockMonitorIntervalSeconds <= 600), 'configuration assert: clockMonitorIntervalSeconds must be between 1 second and 10 minutes');
216
+ config.clockMonitorIntervalSeconds = config.clockMonitorIntervalSeconds || 600;
217
+ assert(!('cronMonitorIntervalSeconds' in config) || (config.cronMonitorIntervalSeconds >= 1 && config.cronMonitorIntervalSeconds <= 45), 'configuration assert: cronMonitorIntervalSeconds must be between 1 and 45 seconds');
218
+ config.cronMonitorIntervalSeconds = config.cronMonitorIntervalSeconds || 30;
219
+ assert(!('cronWorkerIntervalSeconds' in config) || (config.cronWorkerIntervalSeconds >= 1 && config.cronWorkerIntervalSeconds <= 45), 'configuration assert: cronWorkerIntervalSeconds must be between 1 and 45 seconds');
220
+ config.cronWorkerIntervalSeconds = config.cronWorkerIntervalSeconds || 5;
221
+ }
222
+ function applyBamConfig(config) {
223
+ const minInterval = config.__test__bypass_bam_interval_check ? 1 : 10;
224
+ assert(!('bamIntervalSeconds' in config) || config.bamIntervalSeconds >= minInterval, `configuration assert: bamIntervalSeconds must be at least ${minInterval} seconds`);
225
+ config.bamIntervalSeconds = config.bamIntervalSeconds || 60;
226
+ }
227
+ export { assertKey, assertPostgresObjectName, assertQueueName, checkFetchArgs, checkSendArgs, checkWorkArgs, getConfig, POLICY, validateQueueArgs };
package/dist/bam.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import EventEmitter from 'node:events';
2
+ import * as types from './types.js';
3
+ declare class Bam extends EventEmitter implements types.EventsMixin {
4
+ #private;
5
+ events: {
6
+ error: string;
7
+ bam: string;
8
+ };
9
+ constructor(db: types.IDatabase, config: types.ResolvedConstructorOptions);
10
+ start(): Promise<void>;
11
+ stop(): Promise<void>;
12
+ }
13
+ export default Bam;
14
+ //# sourceMappingURL=bam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bam.d.ts","sourceRoot":"","sources":["../src/bam.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,aAAa,CAAA;AAEtC,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;IAUpC,KAAK;IAWL,IAAI;CAgGX;AAED,eAAe,GAAG,CAAA"}
package/dist/bam.js ADDED
@@ -0,0 +1,114 @@
1
+ import EventEmitter from 'node:events';
2
+ import * as plans from './plans.js';
3
+ import * as types from './types.js';
4
+ const events = {
5
+ error: 'error',
6
+ bam: 'bam'
7
+ };
8
+ class Bam extends EventEmitter {
9
+ #stopped;
10
+ #working;
11
+ #pollInterval;
12
+ #db;
13
+ #config;
14
+ events = events;
15
+ constructor(db, config) {
16
+ super();
17
+ this.#db = db;
18
+ this.#config = config;
19
+ this.#stopped = true;
20
+ this.#working = false;
21
+ }
22
+ async start() {
23
+ if (!this.#stopped)
24
+ return;
25
+ this.#stopped = false;
26
+ setImmediate(() => this.#onPoll());
27
+ this.#pollInterval = setInterval(() => this.#onPoll(), this.#config.bamIntervalSeconds * 1000);
28
+ }
29
+ async stop() {
30
+ if (this.#stopped)
31
+ return;
32
+ this.#stopped = true;
33
+ if (this.#pollInterval) {
34
+ clearInterval(this.#pollInterval);
35
+ this.#pollInterval = undefined;
36
+ }
37
+ }
38
+ async #onPoll() {
39
+ if (this.#stopped || this.#working || !this.#config.migrate)
40
+ return;
41
+ this.#working = true;
42
+ try {
43
+ if (this.#config.__test__throw_bam) {
44
+ throw new Error(this.#config.__test__throw_bam);
45
+ }
46
+ const sql = plans.trySetBamTime(this.#config.schema, this.#config.bamIntervalSeconds);
47
+ const { rows } = await this.#db.executeSql(sql);
48
+ if (rows.length === 1) {
49
+ await this.#processCommands();
50
+ }
51
+ }
52
+ catch (err) {
53
+ this.emit(events.error, err);
54
+ }
55
+ finally {
56
+ this.#working = false;
57
+ }
58
+ }
59
+ async #processCommands() {
60
+ if (this.#stopped)
61
+ return;
62
+ const entry = await this.#getNextCommand();
63
+ if (!entry || this.#stopped)
64
+ return;
65
+ this.emit(events.bam, {
66
+ id: entry.id,
67
+ name: entry.name,
68
+ status: 'in_progress',
69
+ queue: entry.queue,
70
+ table: entry.table
71
+ });
72
+ try {
73
+ await this.#db.executeSql(entry.command);
74
+ if (this.#stopped)
75
+ return;
76
+ await this.#markCompleted(entry.id);
77
+ this.emit(events.bam, {
78
+ id: entry.id,
79
+ name: entry.name,
80
+ status: 'completed',
81
+ queue: entry.queue,
82
+ table: entry.table
83
+ });
84
+ }
85
+ catch (err) {
86
+ if (this.#stopped)
87
+ return;
88
+ await this.#markFailed(entry.id, err);
89
+ this.emit(events.error, err);
90
+ this.emit(events.bam, {
91
+ id: entry.id,
92
+ name: entry.name,
93
+ status: 'failed',
94
+ queue: entry.queue,
95
+ table: entry.table,
96
+ error: String(err)
97
+ });
98
+ }
99
+ }
100
+ async #getNextCommand() {
101
+ const sql = plans.getNextBamCommand(this.#config.schema);
102
+ const { rows } = await this.#db.executeSql(sql);
103
+ return rows[0] || null;
104
+ }
105
+ async #markCompleted(id) {
106
+ const sql = plans.setBamCompleted(this.#config.schema, id);
107
+ await this.#db.executeSql(sql);
108
+ }
109
+ async #markFailed(id, error) {
110
+ const sql = plans.setBamFailed(this.#config.schema, id, String(error));
111
+ await this.#db.executeSql(sql);
112
+ }
113
+ }
114
+ export default Bam;
package/dist/boss.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import EventEmitter from 'node:events';
2
+ import type Manager from './manager.js';
3
+ import * as types from './types.js';
4
+ declare class Boss extends EventEmitter implements types.EventsMixin {
5
+ #private;
6
+ events: {
7
+ error: string;
8
+ warning: string;
9
+ };
10
+ constructor(db: types.IDatabase, manager: Manager, config: types.ResolvedConstructorOptions);
11
+ start(): Promise<void>;
12
+ stop(): Promise<void>;
13
+ supervise(value?: string | types.QueueResult[]): Promise<void>;
14
+ }
15
+ export default Boss;
16
+ //# sourceMappingURL=boss.d.ts.map
@@ -0,0 +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;AAYnC,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;IAmBpC,KAAK;IAWL,IAAI;IAkEJ,SAAS,CAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE;CAuFtD;AAED,eAAe,IAAI,CAAA"}
package/dist/boss.js ADDED
@@ -0,0 +1,163 @@
1
+ import EventEmitter from 'node:events';
2
+ import * as plans from './plans.js';
3
+ import { unwrapSQLResult } from './tools.js';
4
+ import * as types from './types.js';
5
+ const events = {
6
+ error: 'error',
7
+ warning: 'warning'
8
+ };
9
+ const WARNINGS = {
10
+ SLOW_QUERY: { seconds: 30, message: 'Warning: slow query. Your queues and/or database server should be reviewed' },
11
+ LARGE_QUEUE: { size: 10_000, message: 'Warning: large queue backlog. Your queue should be reviewed' }
12
+ };
13
+ class Boss extends EventEmitter {
14
+ #stopped;
15
+ #stopping;
16
+ #maintaining;
17
+ #superviseInterval;
18
+ #db;
19
+ #config;
20
+ #manager;
21
+ events = events;
22
+ constructor(db, manager, config) {
23
+ super();
24
+ this.#db = db;
25
+ this.#config = config;
26
+ this.#manager = manager;
27
+ this.#stopped = true;
28
+ this.#stopping = false;
29
+ if (config.warningSlowQuerySeconds) {
30
+ WARNINGS.SLOW_QUERY.seconds = config.warningSlowQuerySeconds;
31
+ }
32
+ if (config.warningQueueSize) {
33
+ WARNINGS.LARGE_QUEUE.size = config.warningQueueSize;
34
+ }
35
+ }
36
+ async start() {
37
+ if (this.#stopped) {
38
+ this.#stopping = false;
39
+ this.#superviseInterval = setInterval(() => this.#onSupervise(), this.#config.superviseIntervalSeconds * 1000);
40
+ this.#stopped = false;
41
+ }
42
+ }
43
+ async stop() {
44
+ if (!this.#stopped) {
45
+ this.#stopping = true;
46
+ if (this.#superviseInterval)
47
+ clearInterval(this.#superviseInterval);
48
+ this.#stopped = true;
49
+ }
50
+ }
51
+ async #executeSql(sql) {
52
+ const started = Date.now();
53
+ const result = unwrapSQLResult(await this.#db.executeSql(sql));
54
+ const elapsed = (Date.now() - started) / 1000;
55
+ if (elapsed > WARNINGS.SLOW_QUERY.seconds ||
56
+ this.#config.__test__warn_slow_query) {
57
+ this.emit(events.warning, {
58
+ message: WARNINGS.SLOW_QUERY.message,
59
+ data: { elapsed, sql },
60
+ });
61
+ }
62
+ return result;
63
+ }
64
+ async #executeQuery(query) {
65
+ const started = Date.now();
66
+ const result = unwrapSQLResult(await this.#db.executeSql(query.text, query.values));
67
+ const elapsed = (Date.now() - started) / 1000;
68
+ if (elapsed > WARNINGS.SLOW_QUERY.seconds ||
69
+ this.#config.__test__warn_slow_query) {
70
+ this.emit(events.warning, {
71
+ message: WARNINGS.SLOW_QUERY.message,
72
+ data: { elapsed, sql: query.text, values: query.values },
73
+ });
74
+ }
75
+ return result;
76
+ }
77
+ async #onSupervise() {
78
+ try {
79
+ if (this.#stopped)
80
+ return;
81
+ if (this.#maintaining)
82
+ return;
83
+ if (this.#config.__test__throw_maint) {
84
+ throw new Error(this.#config.__test__throw_maint);
85
+ }
86
+ this.#maintaining = true;
87
+ const queues = await this.#manager.getQueues();
88
+ !this.#stopped && (await this.supervise(queues));
89
+ }
90
+ catch (err) {
91
+ this.emit(events.error, err);
92
+ }
93
+ finally {
94
+ this.#maintaining = false;
95
+ }
96
+ }
97
+ async supervise(value) {
98
+ let queues;
99
+ if (Array.isArray(value)) {
100
+ queues = value;
101
+ }
102
+ else {
103
+ queues = await this.#manager.getQueues(value);
104
+ }
105
+ const queueGroups = queues.reduce((acc, q) => {
106
+ const { table } = q;
107
+ acc[table] = acc[table] || { table, queues: [] };
108
+ acc[table].queues.push(q);
109
+ return acc;
110
+ }, {});
111
+ for (const queueGroup of Object.values(queueGroups)) {
112
+ if (this.#stopping)
113
+ return;
114
+ const { table, queues } = queueGroup;
115
+ const names = queues.map((i) => i.name);
116
+ while (names.length) {
117
+ if (this.#stopping)
118
+ return;
119
+ const chunk = names.splice(0, 100);
120
+ await this.#monitor(table, chunk);
121
+ await this.#maintain(table, chunk);
122
+ }
123
+ }
124
+ }
125
+ async #monitor(table, names) {
126
+ if (this.#stopping)
127
+ return;
128
+ const command = plans.trySetQueueMonitorTime(this.#config.schema, names, this.#config.monitorIntervalSeconds);
129
+ const { rows } = await this.#executeQuery(command);
130
+ if (this.#stopping)
131
+ return;
132
+ if (rows.length) {
133
+ const queues = rows.map((q) => q.name);
134
+ const cacheStatsSql = plans.cacheQueueStats(this.#config.schema, table, queues);
135
+ const { rows: rowsCacheStats } = await this.#executeSql(cacheStatsSql);
136
+ if (this.#stopping)
137
+ return;
138
+ const warnings = rowsCacheStats.filter(i => i.queuedCount > (i.warningQueueSize || WARNINGS.LARGE_QUEUE.size));
139
+ for (const warning of warnings) {
140
+ this.emit(events.warning, {
141
+ message: WARNINGS.LARGE_QUEUE.message,
142
+ data: warning,
143
+ });
144
+ }
145
+ const sql = plans.failJobsByTimeout(this.#config.schema, table, queues);
146
+ await this.#executeSql(sql);
147
+ }
148
+ }
149
+ async #maintain(table, names) {
150
+ if (this.#stopping)
151
+ return;
152
+ const command = plans.trySetQueueDeletionTime(this.#config.schema, names, this.#config.maintenanceIntervalSeconds);
153
+ const { rows } = await this.#executeQuery(command);
154
+ if (this.#stopping)
155
+ return;
156
+ if (rows.length) {
157
+ const queues = rows.map((q) => q.name);
158
+ const sql = plans.deletion(this.#config.schema, table, queues);
159
+ await this.#executeSql(sql);
160
+ }
161
+ }
162
+ }
163
+ export default Boss;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}