@pikku/queue-pg-boss 0.11.2 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 0.12.0
2
+
3
+ ## 0.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 62a8725: Fix pg-boss queue initialisation: create the queue before registering a worker to avoid a race condition on first startup. Also sanitise scheduler names to meet pg-boss naming constraints.
8
+ - Updated dependencies [62a8725]
9
+ - Updated dependencies [a3bdb0d]
10
+ - Updated dependencies [e0349ff]
11
+ - Updated dependencies [62a8725]
12
+ - Updated dependencies [e04531f]
13
+ - Updated dependencies [62a8725]
14
+ - Updated dependencies [a83efb8]
15
+ - Updated dependencies [8eed717]
16
+ - Updated dependencies [62a8725]
17
+ - Updated dependencies [62a8725]
18
+ - Updated dependencies [62a8725]
19
+ - @pikku/core@0.12.1
20
+
21
+ ### New Features
22
+
23
+ - `setServices()`, `start()`, `stop()` lifecycle methods on scheduler and queue workers
24
+
1
25
  ## 0.11.0
2
26
 
3
27
  ## 0.11.2
@@ -8,8 +32,6 @@
8
32
  - Updated dependencies [db9c7bf]
9
33
  - @pikku/core@0.11.2
10
34
 
11
- ## 0.11.2
12
-
13
35
  ### Fixes
14
36
 
15
37
  - ddd87eaf: Simplify CreateWireServices type signature for custom Config compatibility
@@ -1,4 +1,4 @@
1
- import { PgBoss, Request as PgBossRequest } from 'pg-boss';
1
+ import type { PgBoss, Request as PgBossRequest } from 'pg-boss';
2
2
  import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue';
3
3
  export declare const mapPikkuJobToPgBoss: (options?: JobOptions) => PgBossRequest["options"];
4
4
  /**
@@ -1,14 +1,11 @@
1
- import { PgBoss, type WorkOptions } from 'pg-boss';
1
+ import type { PgBoss, WorkOptions } from 'pg-boss';
2
2
  import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
3
- import { CoreSingletonServices, CreateWireServices } from '@pikku/core';
4
3
  export declare const mapPikkuWorkerToPgBoss: (workerConfig?: PikkuWorkerConfig) => WorkOptions;
5
4
  /**
6
5
  * pg-boss/PostgreSQL queue worker implementation
7
6
  * Supports job results, retries, and PostgreSQL-based queuing
8
7
  */
9
8
  export declare class PgBossQueueWorkers implements QueueWorkers {
10
- private singletonServices;
11
- private createWireServices?;
12
9
  readonly name = "pg-boss";
13
10
  readonly supportsResults = true;
14
11
  /**
@@ -18,7 +15,7 @@ export declare class PgBossQueueWorkers implements QueueWorkers {
18
15
  readonly configMappings: QueueConfigMapping;
19
16
  private pgBoss;
20
17
  private activeWorkers;
21
- constructor(pgBoss: PgBoss, singletonServices: CoreSingletonServices, createWireServices?: CreateWireServices | undefined);
18
+ constructor(pgBoss: PgBoss);
22
19
  /**
23
20
  * Scan state and register all compatible processors
24
21
  */
@@ -1,4 +1,5 @@
1
- import { runQueueJob, registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
1
+ import { registerQueueWorkers, runQueueJob, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
2
+ import { pikkuState } from '@pikku/core/internal';
2
3
  import { mapPgBossJobToQueueJob } from './utils.js';
3
4
  export const mapPikkuWorkerToPgBoss = (workerConfig) => {
4
5
  const workerOptions = {
@@ -17,8 +18,6 @@ export const mapPikkuWorkerToPgBoss = (workerConfig) => {
17
18
  * Supports job results, retries, and PostgreSQL-based queuing
18
19
  */
19
20
  export class PgBossQueueWorkers {
20
- singletonServices;
21
- createWireServices;
22
21
  name = 'pg-boss';
23
22
  supportsResults = true;
24
23
  /**
@@ -85,34 +84,28 @@ export class PgBossQueueWorkers {
85
84
  };
86
85
  pgBoss;
87
86
  activeWorkers = new Map();
88
- constructor(pgBoss, singletonServices, createWireServices) {
89
- this.singletonServices = singletonServices;
90
- this.createWireServices = createWireServices;
87
+ constructor(pgBoss) {
91
88
  this.pgBoss = pgBoss;
92
89
  }
93
90
  /**
94
91
  * Scan state and register all compatible processors
95
92
  */
96
93
  async registerQueues() {
97
- return await registerQueueWorkers(this.configMappings, this.singletonServices.logger, async (queueName, processor) => {
94
+ const logger = pikkuState(null, 'package', 'singletonServices').logger;
95
+ return await registerQueueWorkers(this.configMappings, logger, async (queueName, processor) => {
98
96
  await this.pgBoss.createQueue(queueName);
99
- const workerId = await this.pgBoss.work(processor.queueName, {
97
+ const workerId = await this.pgBoss.work(processor.name, {
100
98
  ...mapPikkuWorkerToPgBoss(processor.config),
101
99
  includeMetadata: true,
102
100
  }, async (jobs) => {
103
- if (!this.singletonServices) {
104
- throw new Error('Singleton services not available');
105
- }
106
101
  if (!jobs || jobs.length === 0) {
107
- this.singletonServices.logger.warn(`No jobs received for queue ${queueName}`);
102
+ logger.warn(`No jobs received for queue ${queueName}`);
108
103
  return;
109
104
  }
110
105
  // Process all jobs in parallel
111
106
  await Promise.all(jobs.map(async (job) => {
112
107
  try {
113
108
  await runQueueJob({
114
- singletonServices: this.singletonServices,
115
- createWireServices: this.createWireServices,
116
109
  job: mapPgBossJobToQueueJob(job, this.pgBoss),
117
110
  });
118
111
  }
@@ -125,7 +118,7 @@ export class PgBossQueueWorkers {
125
118
  }
126
119
  else if (error instanceof QueueJobDiscardedError) {
127
120
  // For pg-boss, complete the job successfully to discard it
128
- this.singletonServices.logger.info(`PgBoss job ${job.id} discarded: ${error.message}`);
121
+ logger.info(`PgBoss job ${job.id} discarded: ${error.message}`);
129
122
  await this.pgBoss.complete(queueName, job.id);
130
123
  }
131
124
  else {
@@ -1,11 +1,14 @@
1
- import { PgBoss } from 'pg-boss';
2
- import { SchedulerService, ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
1
+ import type { PgBoss } from 'pg-boss';
2
+ import type { ScheduledTaskInfo, ScheduledTaskSummary, CoreUserSession } from '@pikku/core';
3
+ import { SchedulerService } from '@pikku/core';
3
4
  /**
4
5
  * pg-boss scheduler service implementation
5
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
6
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
7
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
6
8
  */
7
9
  export declare class PgBossSchedulerService extends SchedulerService {
8
10
  private pgBoss;
11
+ private scheduledCronNames;
9
12
  constructor(pgBoss: PgBoss);
10
13
  /**
11
14
  * Initialize - no-op as pg-boss is already initialized by factory
@@ -32,4 +35,13 @@ export declare class PgBossSchedulerService extends SchedulerService {
32
35
  * Close - no-op as pg-boss lifecycle is managed by factory
33
36
  */
34
37
  close(): Promise<void>;
38
+ /**
39
+ * Start recurring scheduled tasks.
40
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
41
+ */
42
+ start(): Promise<void>;
43
+ /**
44
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
45
+ */
46
+ stop(): Promise<void>;
35
47
  }
@@ -1,10 +1,14 @@
1
- import { SchedulerService, parseDurationString, } from '@pikku/core';
1
+ import { SchedulerService, parseDurationString } from '@pikku/core';
2
+ import { pikkuState } from '@pikku/core/internal';
3
+ import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler';
2
4
  /**
3
5
  * pg-boss scheduler service implementation
4
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
6
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
7
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
5
8
  */
6
9
  export class PgBossSchedulerService extends SchedulerService {
7
10
  pgBoss;
11
+ scheduledCronNames = [];
8
12
  constructor(pgBoss) {
9
13
  super();
10
14
  this.pgBoss = pgBoss;
@@ -77,4 +81,37 @@ export class PgBossSchedulerService extends SchedulerService {
77
81
  async close() {
78
82
  // No-op - pg-boss lifecycle is managed by factory
79
83
  }
84
+ /**
85
+ * Start recurring scheduled tasks.
86
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
87
+ */
88
+ async start() {
89
+ const logger = pikkuState(null, 'package', 'singletonServices').logger;
90
+ const scheduledTasks = getScheduledTasks();
91
+ for (const [name, task] of scheduledTasks) {
92
+ const cronName = `pikku-recurring-scheduled-task_${name.replace(/[^a-zA-Z0-9_\-.]/g, '_')}`;
93
+ await this.pgBoss.createQueue(cronName);
94
+ await this.pgBoss.schedule(cronName, task.schedule, {
95
+ rpcName: name,
96
+ });
97
+ this.scheduledCronNames.push(cronName);
98
+ // Register a worker to process the scheduled jobs
99
+ await this.pgBoss.work(cronName, async (jobs) => {
100
+ for (const job of jobs) {
101
+ const { rpcName } = job.data;
102
+ logger.info(`Running scheduled task: ${rpcName}`);
103
+ await runScheduledTask({ name: rpcName });
104
+ }
105
+ });
106
+ }
107
+ }
108
+ /**
109
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
110
+ */
111
+ async stop() {
112
+ for (const cronName of this.scheduledCronNames) {
113
+ await this.pgBoss.unschedule(cronName);
114
+ }
115
+ this.scheduledCronNames = [];
116
+ }
80
117
  }
@@ -2,7 +2,6 @@ import { PgBoss, type ConstructorOptions } 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 type { CoreSingletonServices, CreateWireServices } from '@pikku/core';
6
5
  /**
7
6
  * Factory class for pg-boss services
8
7
  * Manages a single PgBoss instance shared across queue, worker, and scheduler services
@@ -25,7 +24,7 @@ export declare class PgBossServiceFactory {
25
24
  /**
26
25
  * Get the queue workers for processing jobs
27
26
  */
28
- getQueueWorkers(singletonServices: CoreSingletonServices, createWireServices?: CreateWireServices): PgBossQueueWorkers;
27
+ getQueueWorkers(): PgBossQueueWorkers;
29
28
  /**
30
29
  * Get the scheduler service for managing recurring tasks
31
30
  */
@@ -44,9 +44,9 @@ export class PgBossServiceFactory {
44
44
  /**
45
45
  * Get the queue workers for processing jobs
46
46
  */
47
- getQueueWorkers(singletonServices, createWireServices) {
47
+ getQueueWorkers() {
48
48
  if (!this.queueWorkers) {
49
- this.queueWorkers = new PgBossQueueWorkers(this.pgBoss, singletonServices, createWireServices);
49
+ this.queueWorkers = new PgBossQueueWorkers(this.pgBoss);
50
50
  }
51
51
  return this.queueWorkers;
52
52
  }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { QueueJob } from '@pikku/core/queue';
2
- import { PgBoss, type JobWithMetadata } from 'pg-boss';
1
+ import type { QueueJob } from '@pikku/core/queue';
2
+ import type { PgBoss, JobWithMetadata } from 'pg-boss';
3
3
  /**
4
4
  * Map pg-boss job to QueueJob interface
5
5
  */
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.1",
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.1"
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"
@@ -1,4 +1,4 @@
1
- import { PgBoss, Request as PgBossRequest } from 'pg-boss'
1
+ import type { PgBoss, Request as PgBossRequest } from 'pg-boss'
2
2
  import type { QueueService, QueueJob, JobOptions } from '@pikku/core/queue'
3
3
  import { mapPgBossJobToQueueJob } from './utils.js'
4
4
 
@@ -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
+ })
@@ -1,4 +1,4 @@
1
- import { PgBoss, type WorkOptions } from 'pg-boss'
1
+ import type { PgBoss, WorkOptions } from 'pg-boss'
2
2
  import type {
3
3
  QueueWorkers,
4
4
  PikkuWorkerConfig,
@@ -6,12 +6,12 @@ import type {
6
6
  ConfigValidationResult,
7
7
  } from '@pikku/core/queue'
8
8
  import {
9
- runQueueJob,
10
9
  registerQueueWorkers,
10
+ runQueueJob,
11
11
  QueueJobFailedError,
12
12
  QueueJobDiscardedError,
13
13
  } from '@pikku/core/queue'
14
- import { CoreSingletonServices, CreateWireServices } from '@pikku/core'
14
+ import { pikkuState } from '@pikku/core/internal'
15
15
  import { mapPgBossJobToQueueJob } from './utils.js'
16
16
 
17
17
  export const mapPikkuWorkerToPgBoss = (
@@ -121,11 +121,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
121
121
  private pgBoss: PgBoss
122
122
  private activeWorkers = new Map<string, string>()
123
123
 
124
- constructor(
125
- pgBoss: PgBoss,
126
- private singletonServices: CoreSingletonServices,
127
- private createWireServices?: CreateWireServices
128
- ) {
124
+ constructor(pgBoss: PgBoss) {
129
125
  this.pgBoss = pgBoss
130
126
  }
131
127
 
@@ -133,25 +129,22 @@ export class PgBossQueueWorkers implements QueueWorkers {
133
129
  * Scan state and register all compatible processors
134
130
  */
135
131
  async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
132
+ const logger = pikkuState(null, 'package', 'singletonServices')!.logger
133
+
136
134
  return await registerQueueWorkers(
137
135
  this.configMappings,
138
- this.singletonServices.logger,
136
+ logger,
139
137
  async (queueName, processor) => {
140
138
  await this.pgBoss.createQueue(queueName)
141
139
  const workerId = await this.pgBoss.work<any>(
142
- processor.queueName,
140
+ processor.name,
143
141
  {
144
142
  ...mapPikkuWorkerToPgBoss(processor.config),
145
143
  includeMetadata: true,
146
144
  },
147
145
  async (jobs) => {
148
- if (!this.singletonServices) {
149
- throw new Error('Singleton services not available')
150
- }
151
146
  if (!jobs || jobs.length === 0) {
152
- this.singletonServices.logger.warn(
153
- `No jobs received for queue ${queueName}`
154
- )
147
+ logger.warn(`No jobs received for queue ${queueName}`)
155
148
  return
156
149
  }
157
150
  // Process all jobs in parallel
@@ -159,8 +152,6 @@ export class PgBossQueueWorkers implements QueueWorkers {
159
152
  jobs.map(async (job) => {
160
153
  try {
161
154
  await runQueueJob({
162
- singletonServices: this.singletonServices,
163
- createWireServices: this.createWireServices,
164
155
  job: mapPgBossJobToQueueJob(job, this.pgBoss),
165
156
  })
166
157
  } catch (error: unknown) {
@@ -171,7 +162,7 @@ export class PgBossQueueWorkers implements QueueWorkers {
171
162
  })
172
163
  } else if (error instanceof QueueJobDiscardedError) {
173
164
  // For pg-boss, complete the job successfully to discard it
174
- this.singletonServices.logger.info(
165
+ logger.info(
175
166
  `PgBoss job ${job.id} discarded: ${error.message}`
176
167
  )
177
168
  await this.pgBoss.complete(queueName, job.id)
@@ -1,11 +1,12 @@
1
- import { PgBoss } from 'pg-boss'
2
- import {
3
- SchedulerService,
1
+ import type { PgBoss } from 'pg-boss'
2
+ import type {
4
3
  ScheduledTaskInfo,
5
4
  ScheduledTaskSummary,
6
5
  CoreUserSession,
7
- parseDurationString,
8
6
  } from '@pikku/core'
7
+ import { SchedulerService, parseDurationString } from '@pikku/core'
8
+ import { pikkuState } from '@pikku/core/internal'
9
+ import { runScheduledTask, getScheduledTasks } from '@pikku/core/scheduler'
9
10
 
10
11
  /**
11
12
  * Data stored in scheduled job
@@ -18,9 +19,12 @@ interface ScheduledJobData {
18
19
 
19
20
  /**
20
21
  * pg-boss scheduler service implementation
21
- * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL
22
+ * Uses pg-boss's schedule() API to store recurring jobs in PostgreSQL.
23
+ * Handles both one-off delayed RPCs and recurring cron-scheduled tasks.
22
24
  */
23
25
  export class PgBossSchedulerService extends SchedulerService {
26
+ private scheduledCronNames: string[] = []
27
+
24
28
  constructor(private pgBoss: PgBoss) {
25
29
  super()
26
30
  }
@@ -118,4 +122,41 @@ export class PgBossSchedulerService extends SchedulerService {
118
122
  async close(): Promise<void> {
119
123
  // No-op - pg-boss lifecycle is managed by factory
120
124
  }
125
+
126
+ /**
127
+ * Start recurring scheduled tasks.
128
+ * Registers a pg-boss worker to process recurring jobs via runScheduledTask.
129
+ */
130
+ async start(): Promise<void> {
131
+ const logger = pikkuState(null, 'package', 'singletonServices')!.logger
132
+ const scheduledTasks = getScheduledTasks()
133
+
134
+ for (const [name, task] of scheduledTasks) {
135
+ const cronName = `pikku-recurring-scheduled-task_${name.replace(/[^a-zA-Z0-9_\-.]/g, '_')}`
136
+ await this.pgBoss.createQueue(cronName)
137
+ await this.pgBoss.schedule(cronName, task.schedule, {
138
+ rpcName: name,
139
+ } as ScheduledJobData)
140
+ this.scheduledCronNames.push(cronName)
141
+
142
+ // Register a worker to process the scheduled jobs
143
+ await this.pgBoss.work<ScheduledJobData>(cronName, async (jobs) => {
144
+ for (const job of jobs) {
145
+ const { rpcName } = job.data
146
+ logger.info(`Running scheduled task: ${rpcName}`)
147
+ await runScheduledTask({ name: rpcName })
148
+ }
149
+ })
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Stop recurring scheduled tasks by unscheduling them from pg-boss.
155
+ */
156
+ async stop(): Promise<void> {
157
+ for (const cronName of this.scheduledCronNames) {
158
+ await this.pgBoss.unschedule(cronName)
159
+ }
160
+ this.scheduledCronNames = []
161
+ }
121
162
  }
@@ -2,7 +2,6 @@ import { PgBoss, type ConstructorOptions } 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 type { CoreSingletonServices, CreateWireServices } from '@pikku/core'
6
5
 
7
6
  /**
8
7
  * Factory class for pg-boss services
@@ -50,16 +49,9 @@ export class PgBossServiceFactory {
50
49
  /**
51
50
  * Get the queue workers for processing jobs
52
51
  */
53
- getQueueWorkers(
54
- singletonServices: CoreSingletonServices,
55
- createWireServices?: CreateWireServices
56
- ): PgBossQueueWorkers {
52
+ getQueueWorkers(): PgBossQueueWorkers {
57
53
  if (!this.queueWorkers) {
58
- this.queueWorkers = new PgBossQueueWorkers(
59
- this.pgBoss,
60
- singletonServices,
61
- createWireServices
62
- )
54
+ this.queueWorkers = new PgBossQueueWorkers(this.pgBoss)
63
55
  }
64
56
  return this.queueWorkers
65
57
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { QueueJob, QueueJobStatus } from '@pikku/core/queue'
2
- import { PgBoss, type JobWithMetadata } from 'pg-boss'
1
+ import type { QueueJob, QueueJobStatus } from '@pikku/core/queue'
2
+ import type { PgBoss, JobWithMetadata } from 'pg-boss'
3
3
 
4
4
  /**
5
5
  * Map pg-boss job state to our queue job status