@pikku/queue-pg-boss 0.9.5-next.0 → 0.11.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/CHANGELOG.md +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pg-boss-queue-service.d.ts +1 -9
- package/dist/pg-boss-queue-service.js +27 -19
- package/dist/pg-boss-queue-worker.d.ts +1 -9
- package/dist/pg-boss-queue-worker.js +2 -19
- package/dist/pg-boss-scheduler-service.d.ts +35 -0
- package/dist/pg-boss-scheduler-service.js +80 -0
- package/dist/pg-boss-service-factory.d.ts +37 -0
- package/dist/pg-boss-service-factory.js +71 -0
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/pg-boss-queue-service.ts +32 -22
- package/src/pg-boss-queue-worker.ts +2 -20
- package/src/pg-boss-scheduler-service.ts +121 -0
- package/src/pg-boss-service-factory.ts +94 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
## 0.11.0
|
|
2
|
+
|
|
3
|
+
### Minor Changes
|
|
4
|
+
|
|
5
|
+
- Add scheduler service implementation
|
|
6
|
+
- Add service factory for queue and scheduler creation
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
# @pikku/queue-pg-boss
|
|
2
10
|
|
|
11
|
+
## 0.10.0
|
|
12
|
+
|
|
13
|
+
This release includes significant improvements across the framework including tree-shaking support, middleware/permission factories, enhanced CLI functionality, improved TypeScript type safety, and comprehensive test strategies.
|
|
14
|
+
|
|
15
|
+
For complete details, see https://pikku.dev/changelogs/0_10_0.md
|
|
16
|
+
|
|
3
17
|
## 0.9.5-next.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* PostgreSQL queue service for Pikku using pg-boss
|
|
5
5
|
* Provides job results, retries, priorities, and PostgreSQL-based queuing
|
|
6
6
|
*/
|
|
7
|
+
export { PgBossServiceFactory } from './pg-boss-service-factory.js';
|
|
7
8
|
export { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
8
9
|
export { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
10
|
+
export { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
|
|
9
11
|
export type { QueueService, PikkuWorkerConfig, PikkuJobConfig, QueueJob, JobOptions, } from '@pikku/core/queue';
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,7 @@
|
|
|
4
4
|
* PostgreSQL queue service for Pikku using pg-boss
|
|
5
5
|
* Provides job results, retries, priorities, and PostgreSQL-based queuing
|
|
6
6
|
*/
|
|
7
|
+
export { PgBossServiceFactory } from './pg-boss-service-factory.js';
|
|
7
8
|
export { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
8
9
|
export { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
10
|
+
export { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
|
|
@@ -9,11 +9,7 @@ export declare class PgBossQueueService implements QueueService {
|
|
|
9
9
|
readonly name = "pg-boss";
|
|
10
10
|
readonly supportsResults = true;
|
|
11
11
|
protected pgBoss: PgBoss;
|
|
12
|
-
constructor(
|
|
13
|
-
/**
|
|
14
|
-
* Initialize the pg-boss instance
|
|
15
|
-
*/
|
|
16
|
-
init(): Promise<void>;
|
|
12
|
+
constructor(pgBoss: PgBoss);
|
|
17
13
|
/**
|
|
18
14
|
* Add a job to the queue
|
|
19
15
|
*/
|
|
@@ -22,8 +18,4 @@ export declare class PgBossQueueService implements QueueService {
|
|
|
22
18
|
* Get job by ID
|
|
23
19
|
*/
|
|
24
20
|
getJob<T, R>(queueName: string, jobId: string): Promise<QueueJob<T, R> | null>;
|
|
25
|
-
/**
|
|
26
|
-
* Close the pg-boss connection
|
|
27
|
-
*/
|
|
28
|
-
close(): Promise<void>;
|
|
29
21
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import PgBoss from 'pg-boss';
|
|
2
1
|
import { mapPgBossJobToQueueJob } from './utils.js';
|
|
3
2
|
export const mapPikkuJobToPgBoss = (options) => {
|
|
4
3
|
const pgBossOptions = {};
|
|
@@ -11,6 +10,30 @@ export const mapPikkuJobToPgBoss = (options) => {
|
|
|
11
10
|
if (options?.jobId !== undefined) {
|
|
12
11
|
pgBossOptions.singletonKey = options.jobId;
|
|
13
12
|
}
|
|
13
|
+
// Map retry options
|
|
14
|
+
if (options?.attempts !== undefined) {
|
|
15
|
+
pgBossOptions.retryLimit = options.attempts - 1;
|
|
16
|
+
}
|
|
17
|
+
if (options?.backoff !== undefined) {
|
|
18
|
+
if (typeof options.backoff === 'string') {
|
|
19
|
+
// If backoff is a string, assume it's 'exponential' or 'fixed'
|
|
20
|
+
pgBossOptions.retryBackoff = options.backoff === 'exponential';
|
|
21
|
+
}
|
|
22
|
+
else if (typeof options.backoff === 'object') {
|
|
23
|
+
pgBossOptions.retryBackoff = options.backoff.type === 'exponential';
|
|
24
|
+
if (options.backoff.delay !== undefined) {
|
|
25
|
+
// pg-boss uses seconds, we use milliseconds
|
|
26
|
+
pgBossOptions.retryDelay = Math.floor(options.backoff.delay / 1000);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (options?.removeOnComplete !== undefined) {
|
|
31
|
+
pgBossOptions.onComplete = options.removeOnComplete === 0;
|
|
32
|
+
}
|
|
33
|
+
if (options?.removeOnFail !== undefined) {
|
|
34
|
+
// pg-boss doesn't have a direct equivalent, but we can track this for consistency
|
|
35
|
+
// Note: pg-boss keeps failed jobs by default
|
|
36
|
+
}
|
|
14
37
|
return pgBossOptions;
|
|
15
38
|
};
|
|
16
39
|
/**
|
|
@@ -21,23 +44,14 @@ export class PgBossQueueService {
|
|
|
21
44
|
name = 'pg-boss';
|
|
22
45
|
supportsResults = true;
|
|
23
46
|
pgBoss;
|
|
24
|
-
constructor(
|
|
25
|
-
|
|
26
|
-
// If a string is provided, treat it as the connection URL
|
|
27
|
-
optionsOrUrl = { connectionString: optionsOrUrl };
|
|
28
|
-
}
|
|
29
|
-
this.pgBoss = new PgBoss(optionsOrUrl);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Initialize the pg-boss instance
|
|
33
|
-
*/
|
|
34
|
-
async init() {
|
|
35
|
-
await this.pgBoss.start();
|
|
47
|
+
constructor(pgBoss) {
|
|
48
|
+
this.pgBoss = pgBoss;
|
|
36
49
|
}
|
|
37
50
|
/**
|
|
38
51
|
* Add a job to the queue
|
|
39
52
|
*/
|
|
40
53
|
async add(queueName, data, options) {
|
|
54
|
+
await this.pgBoss.createQueue(queueName);
|
|
41
55
|
const jobId = await this.pgBoss.send(queueName, data, mapPikkuJobToPgBoss(options));
|
|
42
56
|
if (!jobId) {
|
|
43
57
|
throw new Error('Failed to create job');
|
|
@@ -54,10 +68,4 @@ export class PgBossQueueService {
|
|
|
54
68
|
}
|
|
55
69
|
return mapPgBossJobToQueueJob(job, this.pgBoss);
|
|
56
70
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Close the pg-boss connection
|
|
59
|
-
*/
|
|
60
|
-
async close() {
|
|
61
|
-
await this.pgBoss.stop();
|
|
62
|
-
}
|
|
63
71
|
}
|
|
@@ -18,17 +18,9 @@ export declare class PgBossQueueWorkers implements QueueWorkers {
|
|
|
18
18
|
readonly configMappings: QueueConfigMapping;
|
|
19
19
|
private pgBoss;
|
|
20
20
|
private activeWorkers;
|
|
21
|
-
constructor(
|
|
22
|
-
/**
|
|
23
|
-
* Initialize pg-boss
|
|
24
|
-
*/
|
|
25
|
-
init(): Promise<void>;
|
|
21
|
+
constructor(pgBoss: PgBoss, singletonServices: CoreSingletonServices, createSessionServices?: CreateSessionServices<CoreSingletonServices, CoreServices, any> | undefined);
|
|
26
22
|
/**
|
|
27
23
|
* Scan state and register all compatible processors
|
|
28
24
|
*/
|
|
29
25
|
registerQueues(): Promise<Record<string, ConfigValidationResult[]>>;
|
|
30
|
-
/**
|
|
31
|
-
* Close all workers and connections
|
|
32
|
-
*/
|
|
33
|
-
close(): Promise<void>;
|
|
34
26
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import PgBoss from 'pg-boss';
|
|
2
1
|
import { runQueueJob, registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
|
|
3
2
|
import { mapPgBossJobToQueueJob } from './utils.js';
|
|
4
3
|
export const mapPikkuWorkerToPgBoss = (workerConfig) => {
|
|
@@ -84,20 +83,10 @@ export class PgBossQueueWorkers {
|
|
|
84
83
|
};
|
|
85
84
|
pgBoss;
|
|
86
85
|
activeWorkers = new Map();
|
|
87
|
-
constructor(
|
|
86
|
+
constructor(pgBoss, singletonServices, createSessionServices) {
|
|
88
87
|
this.singletonServices = singletonServices;
|
|
89
88
|
this.createSessionServices = createSessionServices;
|
|
90
|
-
|
|
91
|
-
// If a string is provided, treat it as the connection string
|
|
92
|
-
options = { connectionString: options };
|
|
93
|
-
}
|
|
94
|
-
this.pgBoss = new PgBoss(options);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Initialize pg-boss
|
|
98
|
-
*/
|
|
99
|
-
async init() {
|
|
100
|
-
await this.pgBoss.start();
|
|
89
|
+
this.pgBoss = pgBoss;
|
|
101
90
|
}
|
|
102
91
|
/**
|
|
103
92
|
* Scan state and register all compatible processors
|
|
@@ -139,10 +128,4 @@ export class PgBossQueueWorkers {
|
|
|
139
128
|
this.activeWorkers.set(queueName, workerId);
|
|
140
129
|
});
|
|
141
130
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Close all workers and connections
|
|
144
|
-
*/
|
|
145
|
-
async close() {
|
|
146
|
-
await this.pgBoss.stop();
|
|
147
|
-
}
|
|
148
131
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { SchedulerService, ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
|
|
3
|
+
/**
|
|
4
|
+
* pg-boss scheduler service implementation
|
|
5
|
+
* Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
|
|
6
|
+
*/
|
|
7
|
+
export declare class PgBossSchedulerService extends SchedulerService {
|
|
8
|
+
private pgBoss;
|
|
9
|
+
constructor(pgBoss: PgBoss);
|
|
10
|
+
/**
|
|
11
|
+
* Initialize - no-op as pg-boss is already initialized by factory
|
|
12
|
+
*/
|
|
13
|
+
init(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Schedule a one-off delayed RPC call
|
|
16
|
+
* Uses pg-boss send() with startAfter for one-time execution
|
|
17
|
+
*/
|
|
18
|
+
scheduleRPC(delay: number | string, rpcName: string, data?: any, session?: CoreUserSession): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Unschedule (cancel) a task by ID
|
|
21
|
+
*/
|
|
22
|
+
unschedule(taskId: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Get a scheduled task by ID with full details
|
|
25
|
+
*/
|
|
26
|
+
getTask(taskId: string): Promise<ScheduledTaskInfo | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Get all scheduled tasks with minimal info
|
|
29
|
+
*/
|
|
30
|
+
getAllTasks(): Promise<ScheduledTaskSummary[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Close - no-op as pg-boss lifecycle is managed by factory
|
|
33
|
+
*/
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { SchedulerService, parseDurationString, } from '@pikku/core';
|
|
2
|
+
/**
|
|
3
|
+
* pg-boss scheduler service implementation
|
|
4
|
+
* Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
|
|
5
|
+
*/
|
|
6
|
+
export class PgBossSchedulerService extends SchedulerService {
|
|
7
|
+
pgBoss;
|
|
8
|
+
constructor(pgBoss) {
|
|
9
|
+
super();
|
|
10
|
+
this.pgBoss = pgBoss;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize - no-op as pg-boss is already initialized by factory
|
|
14
|
+
*/
|
|
15
|
+
async init() {
|
|
16
|
+
// No-op - pg-boss is already initialized by factory
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Schedule a one-off delayed RPC call
|
|
20
|
+
* Uses pg-boss send() with startAfter for one-time execution
|
|
21
|
+
*/
|
|
22
|
+
async scheduleRPC(delay, rpcName, data, session) {
|
|
23
|
+
// Parse delay if it's a string
|
|
24
|
+
const delayMs = typeof delay === 'string' ? parseDurationString(delay) : delay;
|
|
25
|
+
const jobData = {
|
|
26
|
+
rpcName,
|
|
27
|
+
data,
|
|
28
|
+
session,
|
|
29
|
+
};
|
|
30
|
+
// Use send() with startAfter for one-off delayed execution
|
|
31
|
+
const taskId = await this.pgBoss.send('pikku-remote-internal-rpc', jobData, {
|
|
32
|
+
startAfter: new Date(Date.now() + delayMs),
|
|
33
|
+
singletonKey: `${rpcName}-${Date.now()}`, // Ensure uniqueness
|
|
34
|
+
});
|
|
35
|
+
if (!taskId) {
|
|
36
|
+
throw new Error('Failed to schedule RPC');
|
|
37
|
+
}
|
|
38
|
+
return taskId;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Unschedule (cancel) a task by ID
|
|
42
|
+
*/
|
|
43
|
+
async unschedule(taskId) {
|
|
44
|
+
await this.pgBoss.cancel('pikku-remote-internal-rpc', taskId);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get a scheduled task by ID with full details
|
|
49
|
+
*/
|
|
50
|
+
async getTask(taskId) {
|
|
51
|
+
const job = await this.pgBoss.getJobById('pikku-remote-internal-rpc', taskId);
|
|
52
|
+
if (!job) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const jobData = job.data;
|
|
56
|
+
return {
|
|
57
|
+
taskId: job.id,
|
|
58
|
+
rpcName: jobData.rpcName,
|
|
59
|
+
scheduledFor: job.startAfter || job.createdOn,
|
|
60
|
+
data: jobData.data,
|
|
61
|
+
session: jobData.session,
|
|
62
|
+
status: job.state,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get all scheduled tasks with minimal info
|
|
67
|
+
*/
|
|
68
|
+
async getAllTasks() {
|
|
69
|
+
// pg-boss doesn't have a simple way to list all pending jobs
|
|
70
|
+
// This would require querying the database directly or maintaining a separate index
|
|
71
|
+
// For now, return empty array - can be implemented if needed
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Close - no-op as pg-boss lifecycle is managed by factory
|
|
76
|
+
*/
|
|
77
|
+
async close() {
|
|
78
|
+
// No-op - pg-boss lifecycle is managed by factory
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
3
|
+
import { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
4
|
+
import { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
|
|
5
|
+
import type { CoreServices, CoreSingletonServices, CreateSessionServices } from '@pikku/core';
|
|
6
|
+
/**
|
|
7
|
+
* Factory class for pg-boss services
|
|
8
|
+
* Manages a single PgBoss instance shared across queue, worker, and scheduler services
|
|
9
|
+
*/
|
|
10
|
+
export declare class PgBossServiceFactory {
|
|
11
|
+
private pgBoss;
|
|
12
|
+
private queueService?;
|
|
13
|
+
private queueWorkers?;
|
|
14
|
+
private schedulerService?;
|
|
15
|
+
private initialized;
|
|
16
|
+
constructor(optionsOrInstance: PgBoss.ConstructorOptions | string | PgBoss);
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the pg-boss instance
|
|
19
|
+
*/
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Get the queue service for publishing jobs
|
|
23
|
+
*/
|
|
24
|
+
getQueueService(): PgBossQueueService;
|
|
25
|
+
/**
|
|
26
|
+
* Get the queue workers for processing jobs
|
|
27
|
+
*/
|
|
28
|
+
getQueueWorkers(singletonServices: CoreSingletonServices, createSessionServices?: CreateSessionServices<CoreSingletonServices, CoreServices, any>): PgBossQueueWorkers;
|
|
29
|
+
/**
|
|
30
|
+
* Get the scheduler service for managing recurring tasks
|
|
31
|
+
*/
|
|
32
|
+
getSchedulerService(): PgBossSchedulerService;
|
|
33
|
+
/**
|
|
34
|
+
* Close all services and the pg-boss connection
|
|
35
|
+
*/
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss';
|
|
2
|
+
import { PgBossQueueService } from './pg-boss-queue-service.js';
|
|
3
|
+
import { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
|
|
4
|
+
import { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
|
|
5
|
+
/**
|
|
6
|
+
* Factory class for pg-boss services
|
|
7
|
+
* Manages a single PgBoss instance shared across queue, worker, and scheduler services
|
|
8
|
+
*/
|
|
9
|
+
export class PgBossServiceFactory {
|
|
10
|
+
pgBoss;
|
|
11
|
+
queueService;
|
|
12
|
+
queueWorkers;
|
|
13
|
+
schedulerService;
|
|
14
|
+
initialized = false;
|
|
15
|
+
constructor(optionsOrInstance) {
|
|
16
|
+
if (optionsOrInstance instanceof PgBoss) {
|
|
17
|
+
this.pgBoss = optionsOrInstance;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const options = typeof optionsOrInstance === 'string'
|
|
21
|
+
? { connectionString: optionsOrInstance }
|
|
22
|
+
: optionsOrInstance;
|
|
23
|
+
this.pgBoss = new PgBoss(options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the pg-boss instance
|
|
28
|
+
*/
|
|
29
|
+
async init() {
|
|
30
|
+
if (!this.initialized) {
|
|
31
|
+
await this.pgBoss.start();
|
|
32
|
+
this.initialized = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the queue service for publishing jobs
|
|
37
|
+
*/
|
|
38
|
+
getQueueService() {
|
|
39
|
+
if (!this.queueService) {
|
|
40
|
+
this.queueService = new PgBossQueueService(this.pgBoss);
|
|
41
|
+
}
|
|
42
|
+
return this.queueService;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the queue workers for processing jobs
|
|
46
|
+
*/
|
|
47
|
+
getQueueWorkers(singletonServices, createSessionServices) {
|
|
48
|
+
if (!this.queueWorkers) {
|
|
49
|
+
this.queueWorkers = new PgBossQueueWorkers(this.pgBoss, singletonServices, createSessionServices);
|
|
50
|
+
}
|
|
51
|
+
return this.queueWorkers;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the scheduler service for managing recurring tasks
|
|
55
|
+
*/
|
|
56
|
+
getSchedulerService() {
|
|
57
|
+
if (!this.schedulerService) {
|
|
58
|
+
this.schedulerService = new PgBossSchedulerService(this.pgBoss);
|
|
59
|
+
}
|
|
60
|
+
return this.schedulerService;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Close all services and the pg-boss connection
|
|
64
|
+
*/
|
|
65
|
+
async close() {
|
|
66
|
+
if (this.initialized) {
|
|
67
|
+
await this.pgBoss.stop();
|
|
68
|
+
this.initialized = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/queue-pg-boss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"release": "npm run build && npm test"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@pikku/core": "^0.
|
|
16
|
+
"@pikku/core": "^0.11.0"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"pg-boss": "^
|
|
19
|
+
"pg-boss": "^11.1.2"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.9"
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* Provides job results, retries, priorities, and PostgreSQL-based queuing
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export { PgBossServiceFactory } from './pg-boss-service-factory.js'
|
|
8
9
|
export { PgBossQueueService } from './pg-boss-queue-service.js'
|
|
9
10
|
export { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
|
|
11
|
+
export { PgBossSchedulerService } from './pg-boss-scheduler-service.js'
|
|
10
12
|
|
|
11
13
|
// Re-export core queue types for convenience
|
|
12
14
|
export type {
|
|
@@ -5,7 +5,7 @@ import { mapPgBossJobToQueueJob } from './utils.js'
|
|
|
5
5
|
export const mapPikkuJobToPgBoss = (
|
|
6
6
|
options?: JobOptions
|
|
7
7
|
): PgBoss.JobOptions => {
|
|
8
|
-
const pgBossOptions:
|
|
8
|
+
const pgBossOptions: any = {}
|
|
9
9
|
|
|
10
10
|
if (options?.priority !== undefined) {
|
|
11
11
|
pgBossOptions.priority = options.priority
|
|
@@ -19,7 +19,34 @@ export const mapPikkuJobToPgBoss = (
|
|
|
19
19
|
pgBossOptions.singletonKey = options.jobId
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Map retry options
|
|
23
|
+
if (options?.attempts !== undefined) {
|
|
24
|
+
pgBossOptions.retryLimit = options.attempts - 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options?.backoff !== undefined) {
|
|
28
|
+
if (typeof options.backoff === 'string') {
|
|
29
|
+
// If backoff is a string, assume it's 'exponential' or 'fixed'
|
|
30
|
+
pgBossOptions.retryBackoff = options.backoff === 'exponential'
|
|
31
|
+
} else if (typeof options.backoff === 'object') {
|
|
32
|
+
pgBossOptions.retryBackoff = options.backoff.type === 'exponential'
|
|
33
|
+
if (options.backoff.delay !== undefined) {
|
|
34
|
+
// pg-boss uses seconds, we use milliseconds
|
|
35
|
+
pgBossOptions.retryDelay = Math.floor(options.backoff.delay / 1000)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options?.removeOnComplete !== undefined) {
|
|
41
|
+
pgBossOptions.onComplete = options.removeOnComplete === 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options?.removeOnFail !== undefined) {
|
|
45
|
+
// pg-boss doesn't have a direct equivalent, but we can track this for consistency
|
|
46
|
+
// Note: pg-boss keeps failed jobs by default
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return pgBossOptions as PgBoss.JobOptions
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
/**
|
|
@@ -31,19 +58,8 @@ export class PgBossQueueService implements QueueService {
|
|
|
31
58
|
readonly supportsResults = true
|
|
32
59
|
protected pgBoss: PgBoss
|
|
33
60
|
|
|
34
|
-
constructor(
|
|
35
|
-
|
|
36
|
-
// If a string is provided, treat it as the connection URL
|
|
37
|
-
optionsOrUrl = { connectionString: optionsOrUrl }
|
|
38
|
-
}
|
|
39
|
-
this.pgBoss = new PgBoss(optionsOrUrl)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Initialize the pg-boss instance
|
|
44
|
-
*/
|
|
45
|
-
async init(): Promise<void> {
|
|
46
|
-
await this.pgBoss.start()
|
|
61
|
+
constructor(pgBoss: PgBoss) {
|
|
62
|
+
this.pgBoss = pgBoss
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
/**
|
|
@@ -54,6 +70,7 @@ export class PgBossQueueService implements QueueService {
|
|
|
54
70
|
data: In,
|
|
55
71
|
options?: JobOptions
|
|
56
72
|
): Promise<string> {
|
|
73
|
+
await this.pgBoss.createQueue(queueName)
|
|
57
74
|
const jobId = await this.pgBoss.send(
|
|
58
75
|
queueName,
|
|
59
76
|
data as any,
|
|
@@ -78,11 +95,4 @@ export class PgBossQueueService implements QueueService {
|
|
|
78
95
|
}
|
|
79
96
|
return mapPgBossJobToQueueJob<T, R>(job, this.pgBoss)
|
|
80
97
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Close the pg-boss connection
|
|
84
|
-
*/
|
|
85
|
-
async close(): Promise<void> {
|
|
86
|
-
await this.pgBoss.stop()
|
|
87
|
-
}
|
|
88
98
|
}
|
|
@@ -123,7 +123,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
|
|
|
123
123
|
private activeWorkers = new Map<string, string>()
|
|
124
124
|
|
|
125
125
|
constructor(
|
|
126
|
-
|
|
126
|
+
pgBoss: PgBoss,
|
|
127
127
|
private singletonServices: CoreSingletonServices,
|
|
128
128
|
private createSessionServices?: CreateSessionServices<
|
|
129
129
|
CoreSingletonServices,
|
|
@@ -131,18 +131,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
|
|
|
131
131
|
any
|
|
132
132
|
>
|
|
133
133
|
) {
|
|
134
|
-
|
|
135
|
-
// If a string is provided, treat it as the connection string
|
|
136
|
-
options = { connectionString: options }
|
|
137
|
-
}
|
|
138
|
-
this.pgBoss = new PgBoss(options)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Initialize pg-boss
|
|
143
|
-
*/
|
|
144
|
-
async init(): Promise<void> {
|
|
145
|
-
await this.pgBoss.start()
|
|
134
|
+
this.pgBoss = pgBoss
|
|
146
135
|
}
|
|
147
136
|
|
|
148
137
|
/**
|
|
@@ -195,11 +184,4 @@ export class PgBossQueueWorkers implements QueueWorkers {
|
|
|
195
184
|
}
|
|
196
185
|
)
|
|
197
186
|
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Close all workers and connections
|
|
201
|
-
*/
|
|
202
|
-
async close(): Promise<void> {
|
|
203
|
-
await this.pgBoss.stop()
|
|
204
|
-
}
|
|
205
187
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss'
|
|
2
|
+
import {
|
|
3
|
+
SchedulerService,
|
|
4
|
+
ScheduledTaskInfo,
|
|
5
|
+
ScheduledTaskSummary,
|
|
6
|
+
CoreUserSession,
|
|
7
|
+
parseDurationString,
|
|
8
|
+
} from '@pikku/core'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Data stored in scheduled job
|
|
12
|
+
*/
|
|
13
|
+
interface ScheduledJobData {
|
|
14
|
+
rpcName: string
|
|
15
|
+
data?: any
|
|
16
|
+
session?: CoreUserSession
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* pg-boss scheduler service implementation
|
|
21
|
+
* Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
|
|
22
|
+
*/
|
|
23
|
+
export class PgBossSchedulerService extends SchedulerService {
|
|
24
|
+
constructor(private pgBoss: PgBoss) {
|
|
25
|
+
super()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize - no-op as pg-boss is already initialized by factory
|
|
30
|
+
*/
|
|
31
|
+
async init(): Promise<void> {
|
|
32
|
+
// No-op - pg-boss is already initialized by factory
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Schedule a one-off delayed RPC call
|
|
37
|
+
* Uses pg-boss send() with startAfter for one-time execution
|
|
38
|
+
*/
|
|
39
|
+
async scheduleRPC(
|
|
40
|
+
delay: number | string,
|
|
41
|
+
rpcName: string,
|
|
42
|
+
data?: any,
|
|
43
|
+
session?: CoreUserSession
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
// Parse delay if it's a string
|
|
46
|
+
const delayMs =
|
|
47
|
+
typeof delay === 'string' ? parseDurationString(delay) : delay
|
|
48
|
+
|
|
49
|
+
const jobData: ScheduledJobData = {
|
|
50
|
+
rpcName,
|
|
51
|
+
data,
|
|
52
|
+
session,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use send() with startAfter for one-off delayed execution
|
|
56
|
+
const taskId = await this.pgBoss.send(
|
|
57
|
+
'pikku-remote-internal-rpc',
|
|
58
|
+
jobData,
|
|
59
|
+
{
|
|
60
|
+
startAfter: new Date(Date.now() + delayMs),
|
|
61
|
+
singletonKey: `${rpcName}-${Date.now()}`, // Ensure uniqueness
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (!taskId) {
|
|
66
|
+
throw new Error('Failed to schedule RPC')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return taskId
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Unschedule (cancel) a task by ID
|
|
74
|
+
*/
|
|
75
|
+
async unschedule(taskId: string): Promise<boolean> {
|
|
76
|
+
await this.pgBoss.cancel('pikku-remote-internal-rpc', taskId)
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get a scheduled task by ID with full details
|
|
82
|
+
*/
|
|
83
|
+
async getTask(taskId: string): Promise<ScheduledTaskInfo | null> {
|
|
84
|
+
const job = await this.pgBoss.getJobById(
|
|
85
|
+
'pikku-remote-internal-rpc',
|
|
86
|
+
taskId
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (!job) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const jobData = job.data as ScheduledJobData
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
taskId: job.id,
|
|
97
|
+
rpcName: jobData.rpcName,
|
|
98
|
+
scheduledFor: job.startAfter || job.createdOn,
|
|
99
|
+
data: jobData.data,
|
|
100
|
+
session: jobData.session,
|
|
101
|
+
status: job.state as any,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all scheduled tasks with minimal info
|
|
107
|
+
*/
|
|
108
|
+
async getAllTasks(): Promise<ScheduledTaskSummary[]> {
|
|
109
|
+
// pg-boss doesn't have a simple way to list all pending jobs
|
|
110
|
+
// This would require querying the database directly or maintaining a separate index
|
|
111
|
+
// For now, return empty array - can be implemented if needed
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Close - no-op as pg-boss lifecycle is managed by factory
|
|
117
|
+
*/
|
|
118
|
+
async close(): Promise<void> {
|
|
119
|
+
// No-op - pg-boss lifecycle is managed by factory
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import PgBoss from 'pg-boss'
|
|
2
|
+
import { PgBossQueueService } from './pg-boss-queue-service.js'
|
|
3
|
+
import { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
|
|
4
|
+
import { PgBossSchedulerService } from './pg-boss-scheduler-service.js'
|
|
5
|
+
import type {
|
|
6
|
+
CoreServices,
|
|
7
|
+
CoreSingletonServices,
|
|
8
|
+
CreateSessionServices,
|
|
9
|
+
} from '@pikku/core'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factory class for pg-boss services
|
|
13
|
+
* Manages a single PgBoss instance shared across queue, worker, and scheduler services
|
|
14
|
+
*/
|
|
15
|
+
export class PgBossServiceFactory {
|
|
16
|
+
private pgBoss: PgBoss
|
|
17
|
+
private queueService?: PgBossQueueService
|
|
18
|
+
private queueWorkers?: PgBossQueueWorkers
|
|
19
|
+
private schedulerService?: PgBossSchedulerService
|
|
20
|
+
private initialized = false
|
|
21
|
+
|
|
22
|
+
constructor(optionsOrInstance: PgBoss.ConstructorOptions | string | PgBoss) {
|
|
23
|
+
if (optionsOrInstance instanceof PgBoss) {
|
|
24
|
+
this.pgBoss = optionsOrInstance
|
|
25
|
+
} else {
|
|
26
|
+
const options =
|
|
27
|
+
typeof optionsOrInstance === 'string'
|
|
28
|
+
? { connectionString: optionsOrInstance }
|
|
29
|
+
: optionsOrInstance
|
|
30
|
+
this.pgBoss = new PgBoss(options)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the pg-boss instance
|
|
36
|
+
*/
|
|
37
|
+
async init(): Promise<void> {
|
|
38
|
+
if (!this.initialized) {
|
|
39
|
+
await this.pgBoss.start()
|
|
40
|
+
this.initialized = true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the queue service for publishing jobs
|
|
46
|
+
*/
|
|
47
|
+
getQueueService(): PgBossQueueService {
|
|
48
|
+
if (!this.queueService) {
|
|
49
|
+
this.queueService = new PgBossQueueService(this.pgBoss)
|
|
50
|
+
}
|
|
51
|
+
return this.queueService
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the queue workers for processing jobs
|
|
56
|
+
*/
|
|
57
|
+
getQueueWorkers(
|
|
58
|
+
singletonServices: CoreSingletonServices,
|
|
59
|
+
createSessionServices?: CreateSessionServices<
|
|
60
|
+
CoreSingletonServices,
|
|
61
|
+
CoreServices,
|
|
62
|
+
any
|
|
63
|
+
>
|
|
64
|
+
): PgBossQueueWorkers {
|
|
65
|
+
if (!this.queueWorkers) {
|
|
66
|
+
this.queueWorkers = new PgBossQueueWorkers(
|
|
67
|
+
this.pgBoss,
|
|
68
|
+
singletonServices,
|
|
69
|
+
createSessionServices
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return this.queueWorkers
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the scheduler service for managing recurring tasks
|
|
77
|
+
*/
|
|
78
|
+
getSchedulerService(): PgBossSchedulerService {
|
|
79
|
+
if (!this.schedulerService) {
|
|
80
|
+
this.schedulerService = new PgBossSchedulerService(this.pgBoss)
|
|
81
|
+
}
|
|
82
|
+
return this.schedulerService
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Close all services and the pg-boss connection
|
|
87
|
+
*/
|
|
88
|
+
async close(): Promise<void> {
|
|
89
|
+
if (this.initialized) {
|
|
90
|
+
await this.pgBoss.stop()
|
|
91
|
+
this.initialized = false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.ts","./src/pg-boss-queue-service.ts","./src/pg-boss-queue-worker.ts","./src/utils.ts"],"version":"5.9.
|
|
1
|
+
{"root":["./src/index.ts","./src/pg-boss-queue-service.ts","./src/pg-boss-queue-worker.ts","./src/pg-boss-scheduler-service.ts","./src/pg-boss-service-factory.ts","./src/utils.ts"],"version":"5.9.3"}
|