@sprqvntrs/workflows 0.2.4
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/LICENSE +21 -0
- package/README.md +649 -0
- package/index.ts +234 -0
- package/package.json +53 -0
- package/src/coordination/lock-manager.ts +431 -0
- package/src/engine/execution-engine.ts +715 -0
- package/src/infrastructure/db-state.ts +703 -0
- package/src/infrastructure/pg-boss.ts +585 -0
- package/src/infrastructure/pgboss-queries.ts +320 -0
- package/src/infrastructure/schema.ts +342 -0
- package/src/operations/registry.ts +409 -0
- package/src/orchestrator.ts +1056 -0
- package/src/templates/registry.ts +591 -0
- package/src/testing/index.ts +476 -0
- package/src/types.ts +946 -0
- package/src/worker/index.ts +325 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pg-boss job queue infrastructure.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the pg-boss setup and configuration for the workflow
|
|
5
|
+
* orchestrator. It handles initialization, queue management, and graceful shutdown.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createBoss, getDefaultQueueConfig } from '@sprqvntrs/workflows';
|
|
10
|
+
*
|
|
11
|
+
* const boss = await createBoss({
|
|
12
|
+
* connectionString: process.env.DATABASE_URL,
|
|
13
|
+
* schema: 'pgboss',
|
|
14
|
+
* application: 'my-app',
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* await boss.start();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import PgBoss from 'pg-boss';
|
|
22
|
+
import type { QueueConfig, QueueDefinition } from '../types';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for pg-boss initialization.
|
|
30
|
+
*/
|
|
31
|
+
export interface PgBossConfig {
|
|
32
|
+
/**
|
|
33
|
+
* PostgreSQL connection string.
|
|
34
|
+
*/
|
|
35
|
+
connectionString: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Schema name for pg-boss tables.
|
|
39
|
+
* @default 'pgboss'
|
|
40
|
+
*/
|
|
41
|
+
schema?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Application name for PostgreSQL connection.
|
|
45
|
+
*/
|
|
46
|
+
application?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether to automatically create/migrate pg-boss schema.
|
|
50
|
+
* @default true
|
|
51
|
+
*/
|
|
52
|
+
migrate?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether to enable debug logging.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
debug?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How long to retain completed jobs (in seconds).
|
|
62
|
+
* @default 604800 (7 days)
|
|
63
|
+
*/
|
|
64
|
+
retentionSeconds?: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How long to retain archived jobs (in seconds).
|
|
68
|
+
* @default 604800 (7 days)
|
|
69
|
+
*/
|
|
70
|
+
archiveSeconds?: number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Interval for job maintenance (in seconds).
|
|
74
|
+
* @default 120 (2 minutes)
|
|
75
|
+
*/
|
|
76
|
+
maintenanceIntervalSeconds?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Job data structure for workflow jobs.
|
|
81
|
+
*/
|
|
82
|
+
export interface WorkflowJobData {
|
|
83
|
+
/**
|
|
84
|
+
* The workflow ID to process.
|
|
85
|
+
*/
|
|
86
|
+
workflowId: string;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Workflow type for logging/debugging.
|
|
90
|
+
*/
|
|
91
|
+
workflowType: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* When the job was created.
|
|
95
|
+
*/
|
|
96
|
+
createdAt: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Job handler function signature.
|
|
101
|
+
*/
|
|
102
|
+
export type JobHandler<T = WorkflowJobData> = (job: PgBoss.Job<T>) => Promise<void>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Batch job handler function signature.
|
|
106
|
+
*/
|
|
107
|
+
export type BatchJobHandler<T = WorkflowJobData> = (jobs: PgBoss.Job<T>[]) => Promise<void>;
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Default Configuration
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Default queue configuration values.
|
|
115
|
+
*
|
|
116
|
+
* These defaults are used when no specific configuration is provided.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const config = {
|
|
121
|
+
* ...DEFAULT_QUEUE_CONFIG,
|
|
122
|
+
* retryLimit: 5, // Override retry limit
|
|
123
|
+
* };
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export const DEFAULT_QUEUE_CONFIG: Required<QueueConfig> = {
|
|
127
|
+
retryLimit: 3,
|
|
128
|
+
retryDelay: 5,
|
|
129
|
+
retryBackoff: true,
|
|
130
|
+
expireInSeconds: 3600,
|
|
131
|
+
retentionSeconds: 86400,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default pg-boss configuration values.
|
|
136
|
+
*/
|
|
137
|
+
export const DEFAULT_PGBOSS_CONFIG: Required<Omit<PgBossConfig, 'connectionString'>> = {
|
|
138
|
+
schema: 'pgboss',
|
|
139
|
+
application: 'workflow-orchestrator',
|
|
140
|
+
migrate: true,
|
|
141
|
+
debug: false,
|
|
142
|
+
retentionSeconds: 604800,
|
|
143
|
+
archiveSeconds: 604800,
|
|
144
|
+
maintenanceIntervalSeconds: 120,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// =============================================================================
|
|
148
|
+
// Factory Functions
|
|
149
|
+
// =============================================================================
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates and configures a pg-boss instance.
|
|
153
|
+
*
|
|
154
|
+
* This function creates a new pg-boss instance with the provided configuration.
|
|
155
|
+
* The instance must be started with `boss.start()` before use.
|
|
156
|
+
*
|
|
157
|
+
* @param config - pg-boss configuration
|
|
158
|
+
* @returns Configured pg-boss instance (not yet started)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* const boss = createBoss({
|
|
163
|
+
* connectionString: 'postgresql://localhost/mydb',
|
|
164
|
+
* schema: 'pgboss',
|
|
165
|
+
* application: 'my-worker',
|
|
166
|
+
* });
|
|
167
|
+
*
|
|
168
|
+
* // Start the boss
|
|
169
|
+
* await boss.start();
|
|
170
|
+
*
|
|
171
|
+
* // Register workers...
|
|
172
|
+
*
|
|
173
|
+
* // Graceful shutdown
|
|
174
|
+
* process.on('SIGTERM', async () => {
|
|
175
|
+
* await boss.stop();
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function createBoss(config: PgBossConfig): PgBoss {
|
|
180
|
+
const fullConfig = {
|
|
181
|
+
...DEFAULT_PGBOSS_CONFIG,
|
|
182
|
+
...config,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return new PgBoss({
|
|
186
|
+
connectionString: fullConfig.connectionString,
|
|
187
|
+
schema: fullConfig.schema,
|
|
188
|
+
application_name: fullConfig.application,
|
|
189
|
+
migrate: fullConfig.migrate,
|
|
190
|
+
// Retention settings
|
|
191
|
+
retentionMinutes: Math.floor(fullConfig.retentionSeconds / 60),
|
|
192
|
+
archiveCompletedAfterSeconds: fullConfig.archiveSeconds,
|
|
193
|
+
// Maintenance
|
|
194
|
+
maintenanceIntervalSeconds: fullConfig.maintenanceIntervalSeconds,
|
|
195
|
+
// Monitoring
|
|
196
|
+
...(fullConfig.debug && { monitorStateIntervalSeconds: 30 }),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Gets the queue configuration for a specific queue.
|
|
202
|
+
*
|
|
203
|
+
* Merges default configuration with queue-specific overrides.
|
|
204
|
+
*
|
|
205
|
+
* @param queueConfig - Optional queue-specific configuration
|
|
206
|
+
* @returns Complete queue configuration
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const config = getQueueConfig({ retryLimit: 5 });
|
|
211
|
+
* // Returns: { retryLimit: 5, retryDelay: 5, retryBackoff: true, ... }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export function getQueueConfig(queueConfig?: QueueConfig): Required<QueueConfig> {
|
|
215
|
+
return {
|
|
216
|
+
...DEFAULT_QUEUE_CONFIG,
|
|
217
|
+
...queueConfig,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Creates pg-boss send options from queue configuration.
|
|
223
|
+
*
|
|
224
|
+
* Converts our QueueConfig to pg-boss SendOptions format.
|
|
225
|
+
*
|
|
226
|
+
* @param config - Queue configuration
|
|
227
|
+
* @param overrides - Additional send options
|
|
228
|
+
* @returns pg-boss SendOptions
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const sendOptions = createSendOptions(
|
|
233
|
+
* { retryLimit: 3, retryDelay: 10 },
|
|
234
|
+
* { priority: 5 }
|
|
235
|
+
* );
|
|
236
|
+
* await boss.send('my-queue', data, sendOptions);
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function createSendOptions(
|
|
240
|
+
config: QueueConfig,
|
|
241
|
+
overrides?: Partial<PgBoss.SendOptions>,
|
|
242
|
+
): PgBoss.SendOptions {
|
|
243
|
+
const fullConfig = getQueueConfig(config);
|
|
244
|
+
|
|
245
|
+
const baseOptions: PgBoss.SendOptions = {
|
|
246
|
+
retryLimit: fullConfig.retryLimit,
|
|
247
|
+
retryDelay: fullConfig.retryDelay,
|
|
248
|
+
retryBackoff: fullConfig.retryBackoff,
|
|
249
|
+
expireInSeconds: fullConfig.expireInSeconds,
|
|
250
|
+
retentionSeconds: fullConfig.retentionSeconds,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Filter out undefined values from overrides to prevent pg-boss validation errors
|
|
254
|
+
// (e.g., "priority must be an integer" when priority is undefined)
|
|
255
|
+
if (overrides) {
|
|
256
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
257
|
+
if (value !== undefined) {
|
|
258
|
+
(baseOptions as Record<string, unknown>)[key] = value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return baseOptions;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Creates pg-boss work options from queue definition.
|
|
268
|
+
*
|
|
269
|
+
* Converts our QueueDefinition to pg-boss WorkOptions format.
|
|
270
|
+
*
|
|
271
|
+
* @param queue - Queue definition
|
|
272
|
+
* @returns pg-boss WorkOptions
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```typescript
|
|
276
|
+
* const workOptions = createWorkOptions({ name: 'my-queue', workers: 5 });
|
|
277
|
+
* await boss.work('my-queue', workOptions, handler);
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export function createWorkOptions(queue: QueueDefinition): PgBoss.WorkOptions {
|
|
281
|
+
return {
|
|
282
|
+
batchSize: queue.batchSize ?? 1,
|
|
283
|
+
pollingIntervalSeconds: Math.floor((queue.pollingIntervalMs ?? 2000) / 1000),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// =============================================================================
|
|
288
|
+
// Utility Functions
|
|
289
|
+
// =============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Registers a worker for a specific queue.
|
|
293
|
+
*
|
|
294
|
+
* Convenience function that combines work options creation with registration.
|
|
295
|
+
*
|
|
296
|
+
* @param boss - pg-boss instance
|
|
297
|
+
* @param queue - Queue definition
|
|
298
|
+
* @param handler - Job handler function
|
|
299
|
+
* @returns Worker ID for unsubscribing
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* const workerId = await registerWorker(boss, { name: 'default', workers: 5 }, async (job) => {
|
|
304
|
+
* console.log('Processing job:', job.data);
|
|
305
|
+
* });
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export async function registerWorker<T = WorkflowJobData>(
|
|
309
|
+
boss: PgBoss,
|
|
310
|
+
queue: QueueDefinition,
|
|
311
|
+
handler: JobHandler<T>,
|
|
312
|
+
): Promise<string> {
|
|
313
|
+
const options = createWorkOptions(queue);
|
|
314
|
+
return boss.work<T>(queue.name, options, async ([job]) => {
|
|
315
|
+
if (job) {
|
|
316
|
+
await handler(job);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Registers a batch worker for a specific queue.
|
|
323
|
+
*
|
|
324
|
+
* For queues that benefit from processing multiple jobs at once.
|
|
325
|
+
*
|
|
326
|
+
* @param boss - pg-boss instance
|
|
327
|
+
* @param queue - Queue definition
|
|
328
|
+
* @param handler - Batch job handler function
|
|
329
|
+
* @returns Worker ID for unsubscribing
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```typescript
|
|
333
|
+
* const workerId = await registerBatchWorker(boss, { name: 'bulk', batchSize: 10 }, async (jobs) => {
|
|
334
|
+
* console.log(`Processing ${jobs.length} jobs`);
|
|
335
|
+
* });
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export async function registerBatchWorker<T = WorkflowJobData>(
|
|
339
|
+
boss: PgBoss,
|
|
340
|
+
queue: QueueDefinition,
|
|
341
|
+
handler: BatchJobHandler<T>,
|
|
342
|
+
): Promise<string> {
|
|
343
|
+
const options = createWorkOptions(queue);
|
|
344
|
+
return boss.work<T>(queue.name, options, handler);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Schedules a recurring job using cron expression.
|
|
349
|
+
*
|
|
350
|
+
* Wrapper around pg-boss schedule that handles unscheduling and rescheduling.
|
|
351
|
+
*
|
|
352
|
+
* @param boss - pg-boss instance
|
|
353
|
+
* @param name - Unique schedule name
|
|
354
|
+
* @param cron - Cron expression
|
|
355
|
+
* @param queueName - Queue to send jobs to
|
|
356
|
+
* @param data - Data to include in each job
|
|
357
|
+
* @param options - Additional schedule options
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* // Schedule daily cleanup at 4 AM UTC
|
|
362
|
+
* await scheduleRecurring(boss, 'daily-cleanup', '0 4 * * *', 'cleanup', {
|
|
363
|
+
* scope: 'all',
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
export async function scheduleRecurring<T extends object>(
|
|
368
|
+
boss: PgBoss,
|
|
369
|
+
name: string,
|
|
370
|
+
cron: string,
|
|
371
|
+
_queueName: string,
|
|
372
|
+
data?: T,
|
|
373
|
+
options?: PgBoss.ScheduleOptions,
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
// Unschedule first to ensure latest config is used
|
|
376
|
+
await boss.unschedule(name);
|
|
377
|
+
await boss.schedule(name, cron, data ?? {}, {
|
|
378
|
+
...options,
|
|
379
|
+
tz: options?.tz ?? 'UTC',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Unschedules a recurring job.
|
|
385
|
+
*
|
|
386
|
+
* @param boss - pg-boss instance
|
|
387
|
+
* @param name - Schedule name to remove
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* await unschedule(boss, 'daily-cleanup');
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
export async function unschedule(boss: PgBoss, name: string): Promise<void> {
|
|
395
|
+
await boss.unschedule(name);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Gets information about a job by ID.
|
|
400
|
+
*
|
|
401
|
+
* @param boss - pg-boss instance
|
|
402
|
+
* @param jobId - Job ID to look up
|
|
403
|
+
* @returns Job information or null if not found
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```typescript
|
|
407
|
+
* const job = await getJob(boss, 'some-job-id');
|
|
408
|
+
* if (job) {
|
|
409
|
+
* console.log('Job state:', job.state);
|
|
410
|
+
* }
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
export async function getJob<T = WorkflowJobData>(
|
|
414
|
+
boss: PgBoss,
|
|
415
|
+
queueName: string,
|
|
416
|
+
jobId: string,
|
|
417
|
+
): Promise<PgBoss.JobWithMetadata<T> | null> {
|
|
418
|
+
return boss.getJobById<T>(queueName, jobId);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Cancels a job by ID.
|
|
423
|
+
*
|
|
424
|
+
* Only works for jobs that haven't started yet.
|
|
425
|
+
*
|
|
426
|
+
* @param boss - pg-boss instance
|
|
427
|
+
* @param jobId - Job ID to cancel
|
|
428
|
+
* @returns True if job was cancelled
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```typescript
|
|
432
|
+
* const cancelled = await cancelJob(boss, 'some-job-id');
|
|
433
|
+
* if (cancelled) {
|
|
434
|
+
* console.log('Job cancelled successfully');
|
|
435
|
+
* }
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
export async function cancelJob(boss: PgBoss, queueName: string, jobId: string): Promise<void> {
|
|
439
|
+
await boss.cancel(queueName, jobId);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Resumes a job by ID.
|
|
444
|
+
*
|
|
445
|
+
* Only works for jobs that are in a cancelled or failed state.
|
|
446
|
+
*
|
|
447
|
+
* @param boss - pg-boss instance
|
|
448
|
+
* @param jobId - Job ID to resume
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* await resumeJob(boss, 'some-job-id');
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
export async function resumeJob(boss: PgBoss, queueName: string, jobId: string): Promise<void> {
|
|
456
|
+
await boss.resume(queueName, jobId);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// =============================================================================
|
|
460
|
+
// Lifecycle Helpers
|
|
461
|
+
// =============================================================================
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Sets up graceful shutdown handlers.
|
|
465
|
+
*
|
|
466
|
+
* Registers SIGTERM and SIGINT handlers to stop pg-boss gracefully.
|
|
467
|
+
*
|
|
468
|
+
* @param boss - pg-boss instance
|
|
469
|
+
* @param options - Shutdown options
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* const boss = createBoss({ connectionString: '...' });
|
|
474
|
+
* await boss.start();
|
|
475
|
+
*
|
|
476
|
+
* setupGracefulShutdown(boss, {
|
|
477
|
+
* timeout: 30000,
|
|
478
|
+
* onShutdown: () => console.log('Shutting down...'),
|
|
479
|
+
* });
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
export function setupGracefulShutdown(
|
|
483
|
+
boss: PgBoss,
|
|
484
|
+
options?: {
|
|
485
|
+
/**
|
|
486
|
+
* Maximum time to wait for jobs to complete (ms).
|
|
487
|
+
* @default 30000
|
|
488
|
+
*/
|
|
489
|
+
timeout?: number;
|
|
490
|
+
/**
|
|
491
|
+
* Callback when shutdown starts.
|
|
492
|
+
*/
|
|
493
|
+
onShutdown?: () => void;
|
|
494
|
+
/**
|
|
495
|
+
* Callback when shutdown completes.
|
|
496
|
+
*/
|
|
497
|
+
onComplete?: () => void;
|
|
498
|
+
},
|
|
499
|
+
): void {
|
|
500
|
+
const { timeout = 30000, onShutdown, onComplete } = options ?? {};
|
|
501
|
+
|
|
502
|
+
let isShuttingDown = false;
|
|
503
|
+
|
|
504
|
+
const shutdown = async (_signal: string): Promise<void> => {
|
|
505
|
+
if (isShuttingDown) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
isShuttingDown = true;
|
|
509
|
+
|
|
510
|
+
onShutdown?.();
|
|
511
|
+
|
|
512
|
+
// Set up timeout for forceful exit
|
|
513
|
+
const forceExitTimeout = setTimeout(() => {
|
|
514
|
+
console.error(`Shutdown timeout (${timeout}ms) exceeded, forcing exit`);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}, timeout);
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
await boss.stop({ graceful: true, timeout });
|
|
520
|
+
clearTimeout(forceExitTimeout);
|
|
521
|
+
onComplete?.();
|
|
522
|
+
process.exit(0);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
clearTimeout(forceExitTimeout);
|
|
525
|
+
console.error('Error during shutdown:', error);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
531
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// =============================================================================
|
|
535
|
+
// Event Helpers
|
|
536
|
+
// =============================================================================
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Event types emitted by pg-boss.
|
|
540
|
+
*/
|
|
541
|
+
export type PgBossEvent = 'error' | 'monitor-states' | 'wip' | 'stopped';
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Sets up event listeners for monitoring.
|
|
545
|
+
*
|
|
546
|
+
* @param boss - pg-boss instance
|
|
547
|
+
* @param handlers - Event handlers
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```typescript
|
|
551
|
+
* setupEventListeners(boss, {
|
|
552
|
+
* onError: (error) => console.error('pg-boss error:', error),
|
|
553
|
+
* onMonitorStates: (states) => metrics.gauge('pgboss.active', states.active),
|
|
554
|
+
* });
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
export function setupEventListeners(
|
|
558
|
+
boss: PgBoss,
|
|
559
|
+
handlers: {
|
|
560
|
+
/**
|
|
561
|
+
* Called when an error occurs.
|
|
562
|
+
*/
|
|
563
|
+
onError?: (error: Error) => void;
|
|
564
|
+
/**
|
|
565
|
+
* Called periodically with job state counts.
|
|
566
|
+
*/
|
|
567
|
+
onMonitorStates?: (states: PgBoss.MonitorStates) => void;
|
|
568
|
+
/**
|
|
569
|
+
* Called when boss stops.
|
|
570
|
+
*/
|
|
571
|
+
onStopped?: () => void;
|
|
572
|
+
},
|
|
573
|
+
): void {
|
|
574
|
+
if (handlers.onError) {
|
|
575
|
+
boss.on('error', handlers.onError);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (handlers.onMonitorStates) {
|
|
579
|
+
boss.on('monitor-states', handlers.onMonitorStates);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (handlers.onStopped) {
|
|
583
|
+
boss.on('stopped', handlers.onStopped);
|
|
584
|
+
}
|
|
585
|
+
}
|