@pikku/queue-pg-boss 0.11.2 → 0.12.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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.12.0
2
+
3
+ ### New Features
4
+
5
+ - `setServices()`, `start()`, `stop()` lifecycle methods on scheduler and queue workers
6
+
1
7
  ## 0.11.0
2
8
 
3
9
  ## 0.11.2
@@ -8,8 +14,6 @@
8
14
  - Updated dependencies [db9c7bf]
9
15
  - @pikku/core@0.11.2
10
16
 
11
- ## 0.11.2
12
-
13
17
  ### Fixes
14
18
 
15
19
  - ddd87eaf: Simplify CreateWireServices type signature for custom Config compatibility
@@ -1,14 +1,12 @@
1
1
  import { PgBoss, type WorkOptions } from 'pg-boss';
2
- import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
3
- import { CoreSingletonServices, CreateWireServices } from '@pikku/core';
2
+ import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult, QueueJob } from '@pikku/core/queue';
3
+ import type { Logger } from '@pikku/core/services';
4
4
  export declare const mapPikkuWorkerToPgBoss: (workerConfig?: PikkuWorkerConfig) => WorkOptions;
5
5
  /**
6
6
  * pg-boss/PostgreSQL queue worker implementation
7
7
  * Supports job results, retries, and PostgreSQL-based queuing
8
8
  */
9
9
  export declare class PgBossQueueWorkers implements QueueWorkers {
10
- private singletonServices;
11
- private createWireServices?;
12
10
  readonly name = "pg-boss";
13
11
  readonly supportsResults = true;
14
12
  /**
@@ -18,7 +16,13 @@ export declare class PgBossQueueWorkers implements QueueWorkers {
18
16
  readonly configMappings: QueueConfigMapping;
19
17
  private pgBoss;
20
18
  private activeWorkers;
21
- constructor(pgBoss: PgBoss, singletonServices: CoreSingletonServices, createWireServices?: CreateWireServices | undefined);
19
+ private runJob?;
20
+ private logger?;
21
+ constructor(pgBoss: PgBoss);
22
+ setJobRunner(runJob: (params: {
23
+ job: QueueJob;
24
+ updateProgress?: (progress: number | string | object) => Promise<void>;
25
+ }) => Promise<void>, logger: Logger): void;
22
26
  /**
23
27
  * Scan state and register all compatible processors
24
28
  */
@@ -1,4 +1,4 @@
1
- import { runQueueJob, registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
1
+ import { registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
2
2
  import { mapPgBossJobToQueueJob } from './utils.js';
3
3
  export const mapPikkuWorkerToPgBoss = (workerConfig) => {
4
4
  const workerOptions = {
@@ -17,8 +17,6 @@ export const mapPikkuWorkerToPgBoss = (workerConfig) => {
17
17
  * Supports job results, retries, and PostgreSQL-based queuing
18
18
  */
19
19
  export class PgBossQueueWorkers {
20
- singletonServices;
21
- createWireServices;
22
20
  name = 'pg-boss';
23
21
  supportsResults = true;
24
22
  /**
@@ -85,34 +83,36 @@ export class PgBossQueueWorkers {
85
83
  };
86
84
  pgBoss;
87
85
  activeWorkers = new Map();
88
- constructor(pgBoss, singletonServices, createWireServices) {
89
- this.singletonServices = singletonServices;
90
- this.createWireServices = createWireServices;
86
+ runJob;
87
+ logger;
88
+ constructor(pgBoss) {
91
89
  this.pgBoss = pgBoss;
92
90
  }
91
+ setJobRunner(runJob, logger) {
92
+ this.runJob = runJob;
93
+ this.logger = logger;
94
+ }
93
95
  /**
94
96
  * Scan state and register all compatible processors
95
97
  */
96
98
  async registerQueues() {
97
- return await registerQueueWorkers(this.configMappings, this.singletonServices.logger, async (queueName, processor) => {
99
+ if (!this.runJob || !this.logger) {
100
+ throw new Error('PgBossQueueWorkers requires setJobRunner() before registerQueues()');
101
+ }
102
+ return await registerQueueWorkers(this.configMappings, this.logger, async (queueName, processor) => {
98
103
  await this.pgBoss.createQueue(queueName);
99
- const workerId = await this.pgBoss.work(processor.queueName, {
104
+ const workerId = await this.pgBoss.work(processor.name, {
100
105
  ...mapPikkuWorkerToPgBoss(processor.config),
101
106
  includeMetadata: true,
102
107
  }, async (jobs) => {
103
- if (!this.singletonServices) {
104
- throw new Error('Singleton services not available');
105
- }
106
108
  if (!jobs || jobs.length === 0) {
107
- this.singletonServices.logger.warn(`No jobs received for queue ${queueName}`);
109
+ this.logger.warn(`No jobs received for queue ${queueName}`);
108
110
  return;
109
111
  }
110
112
  // Process all jobs in parallel
111
113
  await Promise.all(jobs.map(async (job) => {
112
114
  try {
113
- await runQueueJob({
114
- singletonServices: this.singletonServices,
115
- createWireServices: this.createWireServices,
115
+ await this.runJob({
116
116
  job: mapPgBossJobToQueueJob(job, this.pgBoss),
117
117
  });
118
118
  }
@@ -125,7 +125,7 @@ export class PgBossQueueWorkers {
125
125
  }
126
126
  else if (error instanceof QueueJobDiscardedError) {
127
127
  // For pg-boss, complete the job successfully to discard it
128
- this.singletonServices.logger.info(`PgBoss job ${job.id} discarded: ${error.message}`);
128
+ this.logger.info(`PgBoss job ${job.id} discarded: ${error.message}`);
129
129
  await this.pgBoss.complete(queueName, job.id);
130
130
  }
131
131
  else {
@@ -1,12 +1,21 @@
1
1
  import { PgBoss } from 'pg-boss';
2
- import { SchedulerService, ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
2
+ import { SchedulerService, SchedulerRuntimeHandlers, ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
3
3
  /**
4
4
  * pg-boss scheduler service implementation
5
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
5
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
6
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
6
7
  */
7
8
  export declare class PgBossSchedulerService extends SchedulerService {
8
9
  private pgBoss;
10
+ private scheduledCronNames;
11
+ private handlers?;
9
12
  constructor(pgBoss: PgBoss);
13
+ /**
14
+ * Set services needed for processing recurring tasks.
15
+ * Must be called before start() since the scheduler is typically
16
+ * created before singletonServices are available.
17
+ */
18
+ setServices(handlers: SchedulerRuntimeHandlers): void;
10
19
  /**
11
20
  * Initialize - no-op as pg-boss is already initialized by factory
12
21
  */
@@ -32,4 +41,13 @@ export declare class PgBossSchedulerService extends SchedulerService {
32
41
  * Close - no-op as pg-boss lifecycle is managed by factory
33
42
  */
34
43
  close(): Promise<void>;
44
+ /**
45
+ * Start recurring scheduled tasks.
46
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
47
+ */
48
+ start(): Promise<void>;
49
+ /**
50
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
51
+ */
52
+ stop(): Promise<void>;
35
53
  }
@@ -1,14 +1,26 @@
1
1
  import { SchedulerService, parseDurationString, } from '@pikku/core';
2
+ import { getScheduledTasks } from '@pikku/core/scheduler';
2
3
  /**
3
4
  * pg-boss scheduler service implementation
4
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
5
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
6
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
5
7
  */
6
8
  export class PgBossSchedulerService extends SchedulerService {
7
9
  pgBoss;
10
+ scheduledCronNames = [];
11
+ handlers;
8
12
  constructor(pgBoss) {
9
13
  super();
10
14
  this.pgBoss = pgBoss;
11
15
  }
16
+ /**
17
+ * Set services needed for processing recurring tasks.
18
+ * Must be called before start() since the scheduler is typically
19
+ * created before singletonServices are available.
20
+ */
21
+ setServices(handlers) {
22
+ this.handlers = handlers;
23
+ }
12
24
  /**
13
25
  * Initialize - no-op as pg-boss is already initialized by factory
14
26
  */
@@ -77,4 +89,38 @@ export class PgBossSchedulerService extends SchedulerService {
77
89
  async close() {
78
90
  // No-op - pg-boss lifecycle is managed by factory
79
91
  }
92
+ /**
93
+ * Start recurring scheduled tasks.
94
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
95
+ */
96
+ async start() {
97
+ if (!this.handlers) {
98
+ throw new Error('PgBossSchedulerService requires setServices() before start()');
99
+ }
100
+ const scheduledTasks = getScheduledTasks();
101
+ for (const [name, task] of scheduledTasks) {
102
+ const cronName = `pikku-recurring-scheduled-task:${name}`;
103
+ await this.pgBoss.schedule(cronName, task.schedule, {
104
+ rpcName: name,
105
+ });
106
+ this.scheduledCronNames.push(cronName);
107
+ // Register a worker to process the scheduled jobs
108
+ await this.pgBoss.work(cronName, async (jobs) => {
109
+ for (const job of jobs) {
110
+ const { rpcName } = job.data;
111
+ this.handlers.logger.info(`Running scheduled task: ${rpcName}`);
112
+ await this.handlers.runScheduledTask(rpcName);
113
+ }
114
+ });
115
+ }
116
+ }
117
+ /**
118
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
119
+ */
120
+ async stop() {
121
+ for (const cronName of this.scheduledCronNames) {
122
+ await this.pgBoss.unschedule(cronName);
123
+ }
124
+ this.scheduledCronNames = [];
125
+ }
80
126
  }
@@ -30,6 +30,7 @@ export declare class PgBossServiceFactory {
30
30
  * Get the scheduler service for managing recurring tasks
31
31
  */
32
32
  getSchedulerService(): PgBossSchedulerService;
33
+ setSchedulerRuntime(singletonServices: CoreSingletonServices, createWireServices?: CreateWireServices): void;
33
34
  /**
34
35
  * Close all services and the pg-boss connection
35
36
  */
@@ -2,6 +2,8 @@ import { PgBoss } from 'pg-boss';
2
2
  import { PgBossQueueService } from './pg-boss-queue-service.js';
3
3
  import { PgBossQueueWorkers } from './pg-boss-queue-worker.js';
4
4
  import { PgBossSchedulerService } from './pg-boss-scheduler-service.js';
5
+ import { createQueueJobRunner } from '@pikku/core/queue';
6
+ import { createSchedulerRuntimeHandlers } from '@pikku/core/scheduler';
5
7
  /**
6
8
  * Factory class for pg-boss services
7
9
  * Manages a single PgBoss instance shared across queue, worker, and scheduler services
@@ -46,8 +48,9 @@ export class PgBossServiceFactory {
46
48
  */
47
49
  getQueueWorkers(singletonServices, createWireServices) {
48
50
  if (!this.queueWorkers) {
49
- this.queueWorkers = new PgBossQueueWorkers(this.pgBoss, singletonServices, createWireServices);
51
+ this.queueWorkers = new PgBossQueueWorkers(this.pgBoss);
50
52
  }
53
+ this.queueWorkers.setJobRunner(createQueueJobRunner({ singletonServices, createWireServices }), singletonServices.logger);
51
54
  return this.queueWorkers;
52
55
  }
53
56
  /**
@@ -59,6 +62,12 @@ export class PgBossServiceFactory {
59
62
  }
60
63
  return this.schedulerService;
61
64
  }
65
+ setSchedulerRuntime(singletonServices, createWireServices) {
66
+ this.getSchedulerService().setServices(createSchedulerRuntimeHandlers({
67
+ singletonServices,
68
+ createWireServices,
69
+ }));
70
+ }
62
71
  /**
63
72
  * Close all services and the pg-boss connection
64
73
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/queue-pg-boss",
3
- "version": "0.11.2",
3
+ "version": "0.12.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.11.2"
16
+ "@pikku/core": "^0.12.0"
17
17
  },
18
18
  "dependencies": {
19
- "pg-boss": "^12.2.0"
19
+ "pg-boss": "^12.9.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "^5.9"
@@ -0,0 +1,15 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
5
+
6
+ describe('PgBossQueueWorkers', () => {
7
+ test('requires setJobRunner before registerQueues', async () => {
8
+ const workers = new PgBossQueueWorkers({} as any)
9
+
10
+ await assert.rejects(() => workers.registerQueues(), {
11
+ message:
12
+ 'PgBossQueueWorkers requires setJobRunner() before registerQueues()',
13
+ })
14
+ })
15
+ })
@@ -4,14 +4,14 @@ import type {
4
4
  PikkuWorkerConfig,
5
5
  QueueConfigMapping,
6
6
  ConfigValidationResult,
7
+ QueueJob,
7
8
  } from '@pikku/core/queue'
8
9
  import {
9
- runQueueJob,
10
10
  registerQueueWorkers,
11
11
  QueueJobFailedError,
12
12
  QueueJobDiscardedError,
13
13
  } from '@pikku/core/queue'
14
- import { CoreSingletonServices, CreateWireServices } from '@pikku/core'
14
+ import type { Logger } from '@pikku/core/services'
15
15
  import { mapPgBossJobToQueueJob } from './utils.js'
16
16
 
17
17
  export const mapPikkuWorkerToPgBoss = (
@@ -120,47 +120,60 @@ export class PgBossQueueWorkers implements QueueWorkers {
120
120
 
121
121
  private pgBoss: PgBoss
122
122
  private activeWorkers = new Map<string, string>()
123
+ private runJob?:
124
+ | ((params: {
125
+ job: QueueJob
126
+ updateProgress?: (progress: number | string | object) => Promise<void>
127
+ }) => Promise<void>)
128
+ | undefined
129
+ private logger?: Logger
123
130
 
124
- constructor(
125
- pgBoss: PgBoss,
126
- private singletonServices: CoreSingletonServices,
127
- private createWireServices?: CreateWireServices
128
- ) {
131
+ constructor(pgBoss: PgBoss) {
129
132
  this.pgBoss = pgBoss
130
133
  }
131
134
 
135
+ setJobRunner(
136
+ runJob: (params: {
137
+ job: QueueJob
138
+ updateProgress?: (progress: number | string | object) => Promise<void>
139
+ }) => Promise<void>,
140
+ logger: Logger
141
+ ): void {
142
+ this.runJob = runJob
143
+ this.logger = logger
144
+ }
145
+
132
146
  /**
133
147
  * Scan state and register all compatible processors
134
148
  */
135
149
  async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
150
+ if (!this.runJob || !this.logger) {
151
+ throw new Error(
152
+ 'PgBossQueueWorkers requires setJobRunner() before registerQueues()'
153
+ )
154
+ }
155
+
136
156
  return await registerQueueWorkers(
137
157
  this.configMappings,
138
- this.singletonServices.logger,
158
+ this.logger,
139
159
  async (queueName, processor) => {
140
160
  await this.pgBoss.createQueue(queueName)
141
161
  const workerId = await this.pgBoss.work<any>(
142
- processor.queueName,
162
+ processor.name,
143
163
  {
144
164
  ...mapPikkuWorkerToPgBoss(processor.config),
145
165
  includeMetadata: true,
146
166
  },
147
167
  async (jobs) => {
148
- if (!this.singletonServices) {
149
- throw new Error('Singleton services not available')
150
- }
151
168
  if (!jobs || jobs.length === 0) {
152
- this.singletonServices.logger.warn(
153
- `No jobs received for queue ${queueName}`
154
- )
169
+ this.logger!.warn(`No jobs received for queue ${queueName}`)
155
170
  return
156
171
  }
157
172
  // Process all jobs in parallel
158
173
  await Promise.all(
159
174
  jobs.map(async (job) => {
160
175
  try {
161
- await runQueueJob({
162
- singletonServices: this.singletonServices,
163
- createWireServices: this.createWireServices,
176
+ await this.runJob!({
164
177
  job: mapPgBossJobToQueueJob(job, this.pgBoss),
165
178
  })
166
179
  } catch (error: unknown) {
@@ -171,7 +184,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
171
184
  })
172
185
  } else if (error instanceof QueueJobDiscardedError) {
173
186
  // For pg-boss, complete the job successfully to discard it
174
- this.singletonServices.logger.info(
187
+ this.logger!.info(
175
188
  `PgBoss job ${job.id} discarded: ${error.message}`
176
189
  )
177
190
  await this.pgBoss.complete(queueName, job.id)
@@ -1,11 +1,13 @@
1
1
  import { PgBoss } from 'pg-boss'
2
2
  import {
3
3
  SchedulerService,
4
+ SchedulerRuntimeHandlers,
4
5
  ScheduledTaskInfo,
5
6
  ScheduledTaskSummary,
6
7
  CoreUserSession,
7
8
  parseDurationString,
8
9
  } from '@pikku/core'
10
+ import { getScheduledTasks } from '@pikku/core/scheduler'
9
11
 
10
12
  /**
11
13
  * Data stored in scheduled job
@@ -18,13 +20,26 @@ interface ScheduledJobData {
18
20
 
19
21
  /**
20
22
  * pg-boss scheduler service implementation
21
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
23
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
24
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
22
25
  */
23
26
  export class PgBossSchedulerService extends SchedulerService {
27
+ private scheduledCronNames: string[] = []
28
+ private handlers?: SchedulerRuntimeHandlers
29
+
24
30
  constructor(private pgBoss: PgBoss) {
25
31
  super()
26
32
  }
27
33
 
34
+ /**
35
+ * Set services needed for processing recurring tasks.
36
+ * Must be called before start() since the scheduler is typically
37
+ * created before singletonServices are available.
38
+ */
39
+ setServices(handlers: SchedulerRuntimeHandlers): void {
40
+ this.handlers = handlers
41
+ }
42
+
28
43
  /**
29
44
  * Initialize - no-op as pg-boss is already initialized by factory
30
45
  */
@@ -118,4 +133,45 @@ export class PgBossSchedulerService extends SchedulerService {
118
133
  async close(): Promise<void> {
119
134
  // No-op - pg-boss lifecycle is managed by factory
120
135
  }
136
+
137
+ /**
138
+ * Start recurring scheduled tasks.
139
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
140
+ */
141
+ async start(): Promise<void> {
142
+ if (!this.handlers) {
143
+ throw new Error(
144
+ 'PgBossSchedulerService requires setServices() before start()'
145
+ )
146
+ }
147
+
148
+ const scheduledTasks = getScheduledTasks()
149
+
150
+ for (const [name, task] of scheduledTasks) {
151
+ const cronName = `pikku-recurring-scheduled-task:${name}`
152
+ await this.pgBoss.schedule(cronName, task.schedule, {
153
+ rpcName: name,
154
+ } as ScheduledJobData)
155
+ this.scheduledCronNames.push(cronName)
156
+
157
+ // Register a worker to process the scheduled jobs
158
+ await this.pgBoss.work<ScheduledJobData>(cronName, async (jobs) => {
159
+ for (const job of jobs) {
160
+ const { rpcName } = job.data
161
+ this.handlers!.logger.info(`Running scheduled task: ${rpcName}`)
162
+ await this.handlers!.runScheduledTask(rpcName)
163
+ }
164
+ })
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
170
+ */
171
+ async stop(): Promise<void> {
172
+ for (const cronName of this.scheduledCronNames) {
173
+ await this.pgBoss.unschedule(cronName)
174
+ }
175
+ this.scheduledCronNames = []
176
+ }
121
177
  }
@@ -3,6 +3,8 @@ import { PgBossQueueService } from './pg-boss-queue-service.js'
3
3
  import { PgBossQueueWorkers } from './pg-boss-queue-worker.js'
4
4
  import { PgBossSchedulerService } from './pg-boss-scheduler-service.js'
5
5
  import type { CoreSingletonServices, CreateWireServices } from '@pikku/core'
6
+ import { createQueueJobRunner } from '@pikku/core/queue'
7
+ import { createSchedulerRuntimeHandlers } from '@pikku/core/scheduler'
6
8
 
7
9
  /**
8
10
  * Factory class for pg-boss services
@@ -55,12 +57,12 @@ export class PgBossServiceFactory {
55
57
  createWireServices?: CreateWireServices
56
58
  ): PgBossQueueWorkers {
57
59
  if (!this.queueWorkers) {
58
- this.queueWorkers = new PgBossQueueWorkers(
59
- this.pgBoss,
60
- singletonServices,
61
- createWireServices
62
- )
60
+ this.queueWorkers = new PgBossQueueWorkers(this.pgBoss)
63
61
  }
62
+ this.queueWorkers.setJobRunner(
63
+ createQueueJobRunner({ singletonServices, createWireServices }),
64
+ singletonServices.logger
65
+ )
64
66
  return this.queueWorkers
65
67
  }
66
68
 
@@ -74,6 +76,18 @@ export class PgBossServiceFactory {
74
76
  return this.schedulerService
75
77
  }
76
78
 
79
+ setSchedulerRuntime(
80
+ singletonServices: CoreSingletonServices,
81
+ createWireServices?: CreateWireServices
82
+ ): void {
83
+ this.getSchedulerService().setServices(
84
+ createSchedulerRuntimeHandlers({
85
+ singletonServices,
86
+ createWireServices,
87
+ })
88
+ )
89
+ }
90
+
77
91
  /**
78
92
  * Close all services and the pg-boss connection
79
93
  */