@pikku/queue-bullmq 0.12.7 → 0.12.8

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,31 @@
1
+ ## 0.12.8
2
+
3
+ ### Patch Changes
4
+
5
+ - 4c7a1b5: Run the monorepo's own scripts through bun instead of yarn. What moves is the
6
+ package manager each package's `prepublishOnly` and build scripts invoke, plus
7
+ the two manifest fixes bun needs to resolve the tree: `uWebSockets.js` is
8
+ declared with an explicit `github:` specifier, and `@pikku/uws-handler` marks
9
+ its `uWebSockets.js` peer optional so a bun install of a consumer that brings
10
+ its own uWS app does not try to fetch it from the registry.
11
+
12
+ Three published behaviours change, all of them cases where an isolated
13
+ `node_modules` or bun as the runtime had been papered over by yarn's hoisting:
14
+
15
+ - `@pikku/migrator-sql` turns foreign keys on when it opens a sqlite database
16
+ through bun. `node:sqlite` enforces them by default and `bun:sqlite` does not,
17
+ which silently turned every `ON DELETE CASCADE` into a no-op under
18
+ `bunx --bun pikku`.
19
+ - `@pikku/cli` resolves a deploy provider against the project being deployed
20
+ rather than against wherever the CLI itself is installed, which is what its
21
+ own "is not installed" error asks the user to arrange.
22
+ - `@pikku/cli` treats a specifier a runtime hands straight back — bun does this
23
+ for the modules it implements itself — as not resolved from the project, so
24
+ it falls back rather than loading the runtime's own copy.
25
+
26
+ - Updated dependencies [4c7a1b5]
27
+ - @pikku/core@0.12.108
28
+
1
29
  ## 0.12.7
2
30
 
3
31
  ### Patch Changes
@@ -1,5 +1,6 @@
1
1
  import type { ConnectionOptions, JobsOptions } from 'bullmq';
2
2
  import type Bull from 'bullmq';
3
+ import { Queue, QueueEvents } from 'bullmq';
3
4
  import type { QueueJob, QueueService } from '@pikku/core/queue';
4
5
  import type { JobOptions } from '@pikku/core/queue';
5
6
  export declare const mapPikkuJobToBull: (options?: JobOptions) => JobsOptions;
@@ -11,8 +12,8 @@ export declare class BullQueueService implements QueueService {
11
12
  private redisConnectionOptions;
12
13
  readonly name = "bull";
13
14
  readonly supportsResults = true;
14
- protected queues: Map<string, Bull.Queue<any, any, string, any, any, string, Bull.RedisQueueBackend>>;
15
- protected queueEvents: Map<string, Bull.QueueEvents<any>>;
15
+ protected queues: Map<string, Queue<any, any, string, any, any, string, Bull.RedisQueueBackend>>;
16
+ protected queueEvents: Map<string, QueueEvents<any>>;
16
17
  constructor(redisConnectionOptions?: ConnectionOptions);
17
18
  /**
18
19
  * Create a Bull queue with translated configuration
@@ -1,6 +1,6 @@
1
1
  import type { ConnectionOptions, WorkerOptions } from 'bullmq';
2
2
  import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
3
- export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions, "connection">;
3
+ export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions, 'connection'>;
4
4
  /**
5
5
  * Bull/Redis queue service implementation
6
6
  * Supports job results, retries, and most queue features
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@pikku/queue-bullmq",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/pikkujs/pikku.git",
7
+ "directory": "packages/services/queue-bullmq"
8
+ },
4
9
  "description": "BullMQ queue, worker and scheduler services for Pikku",
5
10
  "author": "yasser.fadl@gmail.com",
6
11
  "license": "MIT",
@@ -13,19 +18,16 @@
13
18
  "ncu": "npx npm-check-updates",
14
19
  "build": "tsc -b",
15
20
  "release": "npm run build && npm test",
16
- "prepublishOnly": "yarn build"
21
+ "prepublishOnly": "bun run build"
17
22
  },
18
23
  "peerDependencies": {
19
- "@pikku/core": "^0.12.85"
24
+ "@pikku/core": "^0.12.108"
20
25
  },
21
26
  "dependencies": {
22
27
  "bullmq": "^6.1.1",
23
28
  "ioredis": "^6.0.0"
24
29
  },
25
- "devDependencies": {
26
- "typescript": "^6.0.3"
27
- },
28
30
  "exports": {
29
31
  ".": "./dist/index.js"
30
32
  }
31
- }
33
+ }
package/run-tests.sh CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/bin/bash
2
2
 
3
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4
- cd "$script_dir"
5
-
6
3
  # Enable nullglob to handle cases where no files match the pattern
7
4
  shopt -s nullglob
8
5
 
@@ -37,20 +34,35 @@ files=($(find src -type f -name "*.test.ts"))
37
34
  # Check if any files matched the pattern
38
35
  if [ ${#files[@]} -eq 0 ]; then
39
36
  echo "No test files found matching pattern: $pattern"
37
+ if [ "${STRICT_TEST_DISCOVERY}" = "1" ]; then
38
+ exit 1
39
+ fi
40
40
  exit 0
41
41
  fi
42
42
 
43
- # Construct the node command
44
- node_cmd=(node --import tsx --test)
43
+ # Construct the bun command
44
+ #
45
+ # --parallel gives every test file its own worker process, which is what node's
46
+ # `--test` did. Several suites keep module-level caches (a TypeScript program, a
47
+ # temp fixture directory) that a shared process would leak between files.
48
+ #
49
+ # --timeout because bun caps a test at 5s by default where node had no cap, and
50
+ # the suites that build real TypeScript programs run well past that.
51
+ bun_cmd=(bun test --parallel --timeout 120000)
45
52
 
46
53
  # Append options based on flags
47
54
  if [ "$watch_mode" = true ]; then
48
- node_cmd+=(--watch)
55
+ # --watch reruns in one process; parallel workers have nothing to watch.
56
+ bun_cmd=(bun test --watch --timeout 120000)
49
57
  fi
50
58
 
51
59
  if [ "$coverage_mode" = true ]; then
52
- node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
60
+ # --coverage-dir=. keeps lcov.info at the package root: CI merges every
61
+ # package's report by prefixing its SF paths with the file's own directory,
62
+ # so a nested coverage/ directory would re-root them one level too deep.
63
+ bun_cmd+=(--coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=.)
64
+ export PIKKU_TEST_COVERAGE=1
53
65
  fi
54
66
 
55
- # Execute the node command with the expanded list of files
56
- "${node_cmd[@]}" "${files[@]}"
67
+ # Execute the bun command with the expanded list of files
68
+ "${bun_cmd[@]}" "${files[@]}"
@@ -1,39 +0,0 @@
1
- import { describe, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { mapPikkuJobToBull } from './bull-queue-service.js'
5
-
6
- // Same contract as the pg-boss adapter: the workflow's resolved retry policy
7
- // (attempts + backoff) must reach BullMQ. backoff mapping in particular was
8
- // previously dropped, so a step's backoff silently never applied on Redis.
9
- describe('mapPikkuJobToBull — retry mapping', () => {
10
- test('attempts passes through unchanged (BullMQ uses attempts directly)', () => {
11
- assert.equal(mapPikkuJobToBull({ attempts: 6 }).attempts, 6)
12
- assert.equal(mapPikkuJobToBull({ attempts: 1 }).attempts, 1)
13
- })
14
-
15
- test("string backoff 'exponential' → { type: 'exponential' }", () => {
16
- assert.deepEqual(
17
- mapPikkuJobToBull({ attempts: 6, backoff: 'exponential' }).backoff,
18
- {
19
- type: 'exponential',
20
- }
21
- )
22
- })
23
-
24
- test('object backoff → { type, delay } (delay stays in ms)', () => {
25
- assert.deepEqual(
26
- mapPikkuJobToBull({ attempts: 4, backoff: { type: 'fixed', delay: 250 } })
27
- .backoff,
28
- { type: 'fixed', delay: 250 }
29
- )
30
- })
31
-
32
- test('no backoff → backoff left unset', () => {
33
- assert.equal(mapPikkuJobToBull({ attempts: 6 }).backoff, undefined)
34
- })
35
-
36
- test('no options → empty job options', () => {
37
- assert.deepEqual(mapPikkuJobToBull(undefined), {})
38
- })
39
- })
@@ -1,131 +0,0 @@
1
- import type { ConnectionOptions, JobsOptions } from 'bullmq'
2
- import type Bull from 'bullmq'
3
- import { Queue, QueueEvents } from 'bullmq'
4
- import type { QueueJob, QueueService } from '@pikku/core/queue'
5
- import type { JobOptions } from '@pikku/core/queue'
6
- import { mapBullJobToQueueJob } from './utils.js'
7
-
8
- export const mapPikkuJobToBull = (options?: JobOptions): JobsOptions => {
9
- const bullOptions: JobsOptions = {}
10
-
11
- if (options?.priority !== undefined) {
12
- bullOptions.priority = options.priority
13
- }
14
-
15
- if (options?.delay !== undefined) {
16
- bullOptions.delay = options.delay
17
- }
18
-
19
- if (options?.attempts !== undefined) {
20
- bullOptions.attempts = options.attempts
21
- }
22
-
23
- // Map retry backoff. BullMQ accepts `{ type, delay }`; our `delay` is in ms,
24
- // which is what BullMQ expects too (unlike pg-boss, which uses seconds).
25
- if (options?.backoff !== undefined) {
26
- bullOptions.backoff =
27
- typeof options.backoff === 'string'
28
- ? { type: options.backoff }
29
- : { type: options.backoff.type, delay: options.backoff.delay }
30
- }
31
-
32
- if (options?.removeOnComplete !== undefined) {
33
- bullOptions.removeOnComplete = options.removeOnComplete
34
- }
35
-
36
- if (options?.removeOnFail !== undefined) {
37
- bullOptions.removeOnFail = options.removeOnFail
38
- }
39
-
40
- if (options?.jobId !== undefined) {
41
- bullOptions.jobId = options.jobId
42
- }
43
-
44
- return bullOptions
45
- }
46
-
47
- /**
48
- * Bull/Redis queue service implementation
49
- * Supports job results, retries, and most queue features
50
- */
51
- export class BullQueueService implements QueueService {
52
- readonly name = 'bull'
53
- readonly supportsResults = true
54
- protected queues = new Map<string, Bull.Queue>()
55
- protected queueEvents = new Map<string, Bull.QueueEvents>()
56
-
57
- constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
58
-
59
- /**
60
- * Create a Bull queue with translated configuration
61
- */
62
- protected async createQueue(
63
- queueName: string,
64
- config?: Bull.QueueOptions
65
- ): Promise<Bull.Queue> {
66
- if (this.queues.has(queueName)) {
67
- return this.queues.get(queueName)!
68
- }
69
-
70
- const queue = new Queue(queueName, config)
71
- queue.on('error', (error) => {
72
- console.error('Redis connection error:', error)
73
- })
74
- await queue.waitUntilReady()
75
-
76
- this.queues.set(queueName, queue)
77
-
78
- if (!this.queueEvents.has(queueName)) {
79
- const queueEvents = new QueueEvents(
80
- queueName,
81
- this.redisConnectionOptions
82
- ? { connection: this.redisConnectionOptions }
83
- : undefined
84
- )
85
- await queueEvents.waitUntilReady()
86
- this.queueEvents.set(queueName, queueEvents)
87
- }
88
-
89
- return queue
90
- }
91
-
92
- /**
93
- * Add a job to the queue
94
- */
95
- public async add<In>(
96
- queueName: string,
97
- data: In,
98
- options?: JobOptions
99
- ): Promise<string> {
100
- const queue = await this.createQueue(queueName)
101
- const bullJob = await queue.add(queueName, data, mapPikkuJobToBull(options))
102
- if (!bullJob.id) {
103
- throw new Error(`Failed to add job to queue '${queueName}'.`)
104
- }
105
- return bullJob.id
106
- }
107
-
108
- /**
109
- * Get job by ID
110
- */
111
- async getJob<T, R>(
112
- queueName: string,
113
- jobId: string
114
- ): Promise<QueueJob<T, R> | null> {
115
- const queue = this.queues.get(queueName)
116
- if (!queue) {
117
- throw new Error(`Queue '${queueName}' not found.`)
118
- }
119
-
120
- const bullJob = await queue.getJob(jobId)
121
- if (!bullJob) {
122
- return null
123
- }
124
-
125
- return mapBullJobToQueueJob<T, R>(
126
- bullJob,
127
- this.redisConnectionOptions,
128
- this.queueEvents
129
- )
130
- }
131
- }
@@ -1,15 +0,0 @@
1
- import { describe, test } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { BullQueueWorkers } from './bull-queue-worker.js'
5
-
6
- describe('BullQueueWorkers', () => {
7
- test('requires a logger (explicit or from singleton services)', async () => {
8
- const workers = new BullQueueWorkers({})
9
-
10
- await assert.rejects(() => workers.registerQueues(), {
11
- message:
12
- 'Logger is required for registerQueues — pass it explicitly or ensure singleton services are initialized first',
13
- })
14
- })
15
- })
@@ -1,209 +0,0 @@
1
- import type { ConnectionOptions, WorkerOptions, QueueEvents } from 'bullmq'
2
- import type Bull from 'bullmq'
3
- import { Worker } from 'bullmq'
4
- import type {
5
- QueueWorkers,
6
- PikkuWorkerConfig,
7
- QueueConfigMapping,
8
- ConfigValidationResult,
9
- } from '@pikku/core/queue'
10
- import { runQueueJob } from '@pikku/core/queue'
11
- import {
12
- registerQueueWorkers,
13
- QueueJobFailedError,
14
- QueueJobDiscardedError,
15
- } from '@pikku/core/queue'
16
- import { pikkuState } from '@pikku/core/state'
17
- import { mapBullJobToQueueJob } from './utils.js'
18
-
19
- export const mapPikkuWorkerToBull = (
20
- workerConfig?: PikkuWorkerConfig
21
- ): Omit<WorkerOptions, 'connection'> => {
22
- const workerOptions: Omit<WorkerOptions, 'connection'> = {}
23
-
24
- // Direct mappings
25
- if (workerConfig?.name !== undefined) {
26
- workerOptions.name = workerConfig.name
27
- }
28
-
29
- if (workerConfig?.batchSize !== undefined) {
30
- workerOptions.concurrency = workerConfig.batchSize
31
- }
32
-
33
- if (workerConfig?.autorun !== undefined) {
34
- workerOptions.autorun = workerConfig.autorun
35
- }
36
-
37
- if (workerConfig?.lockDuration !== undefined) {
38
- workerOptions.lockDuration = workerConfig.lockDuration
39
- }
40
-
41
- if (workerConfig?.drainDelay !== undefined) {
42
- workerOptions.drainDelay = workerConfig.drainDelay
43
- }
44
-
45
- if (workerConfig?.maxStalledCount !== undefined) {
46
- workerOptions.maxStalledCount = workerConfig.maxStalledCount
47
- }
48
-
49
- // Job retention
50
- if (workerConfig?.removeOnComplete !== undefined) {
51
- workerOptions.removeOnComplete = { count: workerConfig.removeOnComplete }
52
- }
53
-
54
- if (workerConfig?.removeOnFail !== undefined) {
55
- workerOptions.removeOnFail = { count: workerConfig.removeOnFail }
56
- }
57
-
58
- return workerOptions
59
- }
60
-
61
- /**
62
- * Bull/Redis queue service implementation
63
- * Supports job results, retries, and most queue features
64
- */
65
- export class BullQueueWorkers implements QueueWorkers {
66
- readonly name = 'bull'
67
- readonly supportsResults = true
68
-
69
- /**
70
- * Configuration mapping rules for BullMQ
71
- * This defines how Pikku worker configs map to BullMQ configurations
72
- */
73
- readonly configMappings: QueueConfigMapping = {
74
- // Configurations that are directly supported
75
- supported: {
76
- name: {
77
- queueProperty: 'name',
78
- description: 'Worker name for identification and monitoring',
79
- },
80
- batchSize: {
81
- queueProperty: 'concurrency',
82
- description:
83
- "Number of jobs to process concurrently, this is Bull's concurrency setting",
84
- },
85
- autorun: {
86
- queueProperty: 'autorun',
87
- description: 'Whether to start processing jobs automatically',
88
- },
89
- lockDuration: {
90
- queueProperty: 'lockDuration',
91
- description: 'Duration of job lock in milliseconds',
92
- },
93
- drainDelay: {
94
- queueProperty: 'drainDelay',
95
- description: 'Delay when queue is empty before polling again',
96
- },
97
- maxStalledCount: {
98
- queueProperty: 'maxStalledCount',
99
- description:
100
- 'Maximum number of times a job can be recovered from stalled state',
101
- },
102
- removeOnComplete: {
103
- queueProperty: 'removeOnComplete',
104
- description: 'Number of completed jobs to keep for inspection',
105
- },
106
- removeOnFail: {
107
- queueProperty: 'removeOnFail',
108
- description: 'Number of failed jobs to keep for inspection',
109
- },
110
- },
111
-
112
- // Configurations that are not supported
113
- unsupported: {
114
- groupConcurrency: {
115
- reason: 'Job groups are a BullMQ Pro feature, not available in BullMQ',
116
- explanation:
117
- 'Give each group its own queue instead — BullMQ is push-based, so extra queues cost no polling',
118
- },
119
- visibilityTimeout: {
120
- reason: 'Bull does not use visibility timeout',
121
- explanation:
122
- 'BullMQ locks jobs during processing instead of using visibility timeouts',
123
- },
124
- pollInterval: {
125
- reason: 'Bull is push-based and does not use polling intervals',
126
- explanation:
127
- 'BullMQ uses Redis pub/sub for real-time job notifications',
128
- },
129
- prefetch: {
130
- reason: 'Bull manages job prefetching internally',
131
- explanation:
132
- 'BullMQ optimizes job fetching automatically based on concurrency settings',
133
- },
134
- },
135
-
136
- // No fallback configurations for BullMQ currently
137
- fallbacks: {},
138
- }
139
-
140
- private workers = new Map<string, Bull.Worker>()
141
- private queueEvents = new Map<string, QueueEvents>()
142
-
143
- constructor(private redisConnectionOptions: ConnectionOptions) {}
144
-
145
- /**
146
- * Scan state and register all compatible processors
147
- */
148
- async registerQueues(): Promise<Record<string, ConfigValidationResult[]>> {
149
- const logger = pikkuState(null, 'package', 'singletonServices')?.logger
150
- if (!logger) {
151
- throw new Error(
152
- 'Logger is required for registerQueues — pass it explicitly or ensure singleton services are initialized first'
153
- )
154
- }
155
-
156
- return await registerQueueWorkers(
157
- this.configMappings,
158
- logger,
159
- async (queueName, processor) => {
160
- const worker = new Worker(
161
- processor.name,
162
- async (job: Bull.Job) => {
163
- try {
164
- return await runQueueJob({
165
- job: await mapBullJobToQueueJob(
166
- job,
167
- this.redisConnectionOptions,
168
- this.queueEvents
169
- ),
170
- updateProgress: async (progress) => {
171
- await job.updateProgress(progress)
172
- },
173
- })
174
- } catch (error) {
175
- if (error instanceof QueueJobFailedError) {
176
- // Let BullMQ handle this as a failed job
177
- throw new Error(error.message)
178
- } else if (error instanceof QueueJobDiscardedError) {
179
- return // Don't throw, job is discarded, which I guess is considered
180
- // a successful job removal
181
- }
182
- throw error
183
- }
184
- },
185
- {
186
- connection: this.redisConnectionOptions,
187
- ...mapPikkuWorkerToBull(processor.config),
188
- }
189
- )
190
- worker.on('error', console.error)
191
- this.workers.set(queueName, worker)
192
- }
193
- )
194
- }
195
-
196
- /**
197
- * Close all queues and connections
198
- */
199
- async close(): Promise<void> {
200
- await Promise.all([
201
- ...Array.from(this.workers.values()).map((worker) => worker.close()),
202
- ...Array.from(this.queueEvents.values()).map((queueEvents) =>
203
- queueEvents.close()
204
- ),
205
- ])
206
- this.workers.clear()
207
- this.queueEvents.clear()
208
- }
209
- }
@@ -1,198 +0,0 @@
1
- import type { ConnectionOptions } from 'bullmq'
2
- import { Queue, Worker } from 'bullmq'
3
- import { runScheduledTask } from '@pikku/core/scheduler'
4
- import { SchedulerService } from '@pikku/core/services'
5
- import type { CoreUserSession } from '@pikku/core/types'
6
- import type {
7
- ScheduledTaskInfo,
8
- ScheduledTaskSummary,
9
- } from '@pikku/core/services'
10
- import { parseDurationString } from '@pikku/core/utils'
11
- import { pikkuState } from '@pikku/core/state'
12
- import { getScheduledTasks } from '@pikku/core/scheduler'
13
-
14
- /**
15
- * Data stored in scheduled job
16
- */
17
- interface ScheduledJobData {
18
- rpcName: string
19
- data?: any
20
- session?: CoreUserSession
21
- }
22
-
23
- const RECURRING_QUEUE_NAME = 'pikku-recurring-scheduled-task'
24
-
25
- export class BullSchedulerService extends SchedulerService {
26
- private queue: Queue
27
- private recurringQueue: Queue
28
- private recurringWorker?: Worker
29
- private jobSchedulerIds: string[] = []
30
-
31
- constructor(private redisConnectionOptions: ConnectionOptions) {
32
- super()
33
- this.queue = new Queue('pikku-remote-internal-rpc', {
34
- connection: redisConnectionOptions,
35
- })
36
- this.recurringQueue = new Queue(RECURRING_QUEUE_NAME, {
37
- connection: redisConnectionOptions,
38
- })
39
- }
40
-
41
- /**
42
- * Initialize - wait for queue to be ready
43
- */
44
- async init(): Promise<void> {
45
- await this.queue.waitUntilReady()
46
- }
47
-
48
- /**
49
- * Schedule a one-off delayed RPC call
50
- * Uses BullMQ add() with delay option
51
- */
52
- async scheduleRPC(
53
- delay: number | string,
54
- rpcName: string,
55
- data?: any,
56
- session?: CoreUserSession
57
- ): Promise<string> {
58
- // Parse delay if it's a string
59
- const delayMs =
60
- typeof delay === 'string' ? parseDurationString(delay) : delay
61
-
62
- const jobData: ScheduledJobData = {
63
- rpcName,
64
- data,
65
- session,
66
- }
67
-
68
- // Use add() with delay for one-off delayed execution
69
- const job = await this.queue.add('pikku-remote-internal-rpc', jobData, {
70
- delay: delayMs, // delay in milliseconds
71
- jobId: `${rpcName}-${Date.now()}`, // Ensure uniqueness
72
- })
73
-
74
- if (!job.id) {
75
- throw new Error('Failed to schedule RPC')
76
- }
77
-
78
- return job.id
79
- }
80
-
81
- /**
82
- * Unschedule (remove) a task by ID
83
- */
84
- async unschedule(taskId: string): Promise<boolean> {
85
- const job = await this.queue.getJob(taskId)
86
- if (job) {
87
- await job.remove()
88
- return true
89
- }
90
- return false
91
- }
92
-
93
- /**
94
- * Get a scheduled task by ID with full details
95
- */
96
- async getTask(taskId: string): Promise<ScheduledTaskInfo | null> {
97
- const job = await this.queue.getJob(taskId)
98
-
99
- if (!job) {
100
- return null
101
- }
102
-
103
- const jobData = job.data as ScheduledJobData
104
- const state = await job.getState()
105
-
106
- // Calculate scheduled time from delay or processedOn
107
- const scheduledFor = job.delay
108
- ? new Date(job.timestamp + job.delay)
109
- : new Date(job.timestamp)
110
-
111
- return {
112
- taskId: job.id!,
113
- rpcName: jobData.rpcName,
114
- scheduledFor,
115
- data: jobData.data,
116
- session: jobData.session,
117
- status: state as any,
118
- }
119
- }
120
-
121
- /**
122
- * Get all scheduled tasks with minimal info
123
- */
124
- async getAllTasks(): Promise<ScheduledTaskSummary[]> {
125
- const jobs = await this.queue.getJobs(['delayed', 'waiting'])
126
-
127
- return jobs.map((job) => {
128
- const jobData = job.data as ScheduledJobData
129
- const scheduledFor = job.delay
130
- ? new Date(job.timestamp + job.delay)
131
- : new Date(job.timestamp)
132
-
133
- return {
134
- taskId: job.id!,
135
- rpcName: jobData.rpcName,
136
- scheduledFor,
137
- }
138
- })
139
- }
140
-
141
- /**
142
- * Close the queue connection
143
- */
144
- async close(): Promise<void> {
145
- if (this.recurringWorker) {
146
- await this.recurringWorker.close()
147
- }
148
- await this.recurringQueue.close()
149
- await this.queue.close()
150
- }
151
-
152
- /**
153
- * Start recurring scheduled tasks.
154
- * Creates a BullMQ Worker to process repeat jobs via runScheduledTask.
155
- */
156
- async start(): Promise<void> {
157
- const logger = pikkuState(null, 'package', 'singletonServices')!.logger
158
- const scheduledTasks = getScheduledTasks()
159
-
160
- // Create a worker to process recurring scheduled task jobs
161
- this.recurringWorker = new Worker(
162
- RECURRING_QUEUE_NAME,
163
- async (job) => {
164
- const { rpcName } = job.data as ScheduledJobData
165
- logger.info(`Running scheduled task: ${rpcName}`)
166
- await runScheduledTask({ name: rpcName })
167
- },
168
- { connection: this.redisConnectionOptions }
169
- )
170
- this.recurringWorker.on('error', (err) => {
171
- logger.error(`Recurring task worker error: ${err}`)
172
- })
173
-
174
- for (const [name, task] of scheduledTasks) {
175
- const jobSchedulerId = `recurring:${name}`
176
- await this.recurringQueue.upsertJobScheduler(
177
- jobSchedulerId,
178
- { pattern: task.schedule },
179
- { name, data: { rpcName: name } as ScheduledJobData }
180
- )
181
- this.jobSchedulerIds.push(jobSchedulerId)
182
- }
183
- }
184
-
185
- /**
186
- * Stop recurring scheduled tasks by removing their job schedulers.
187
- */
188
- async stop(): Promise<void> {
189
- for (const id of this.jobSchedulerIds) {
190
- await this.recurringQueue.removeJobScheduler(id)
191
- }
192
- this.jobSchedulerIds = []
193
- if (this.recurringWorker) {
194
- await this.recurringWorker.close()
195
- this.recurringWorker = undefined
196
- }
197
- }
198
- }
@@ -1,83 +0,0 @@
1
- import type { ConnectionOptions } from 'bullmq'
2
- import { BullQueueService } from './bull-queue-service.js'
3
- import { BullQueueWorkers } from './bull-queue-worker.js'
4
- import { BullSchedulerService } from './bull-scheduler-service.js'
5
-
6
- /**
7
- * Factory class for BullMQ services
8
- * Manages Redis connection options shared across queue, worker, and scheduler services
9
- */
10
- export class BullServiceFactory {
11
- private queueService?: BullQueueService
12
- private queueWorkers?: BullQueueWorkers
13
- private schedulerService?: BullSchedulerService
14
-
15
- constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
16
-
17
- /**
18
- * Initialize - no-op for BullMQ as connections are lazy
19
- */
20
- async init(): Promise<void> {
21
- // No-op - BullMQ creates connections lazily
22
- }
23
-
24
- /**
25
- * Get the queue service for publishing jobs
26
- */
27
- getQueueService(): BullQueueService {
28
- if (!this.queueService) {
29
- this.queueService = new BullQueueService(this.redisConnectionOptions)
30
- }
31
- return this.queueService
32
- }
33
-
34
- /**
35
- * Get the queue workers for processing jobs
36
- */
37
- getQueueWorkers(): BullQueueWorkers {
38
- if (!this.queueWorkers) {
39
- this.queueWorkers = new BullQueueWorkers(this.redisConnectionOptions)
40
- }
41
- return this.queueWorkers
42
- }
43
-
44
- /**
45
- * Get the scheduler service for managing delayed tasks
46
- */
47
- getSchedulerService(): BullSchedulerService {
48
- if (!this.schedulerService) {
49
- this.schedulerService = new BullSchedulerService(
50
- this.redisConnectionOptions
51
- )
52
- }
53
- return this.schedulerService
54
- }
55
-
56
- /**
57
- * Close all services
58
- */
59
- async close(): Promise<void> {
60
- const closePromises: Promise<void>[] = []
61
-
62
- if (this.queueService) {
63
- // BullQueueService doesn't have close method currently
64
- }
65
-
66
- if (this.queueWorkers) {
67
- closePromises.push(this.queueWorkers.close())
68
- }
69
-
70
- if (this.schedulerService) {
71
- closePromises.push(this.schedulerService.close())
72
- }
73
-
74
- await Promise.all(closePromises)
75
- }
76
-
77
- /**
78
- * Alias for close() - used by stopSingletonServices
79
- */
80
- async stop(): Promise<void> {
81
- await this.close()
82
- }
83
- }
package/src/index.ts DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * @module @pikku/queue-bullmq
3
- *
4
- * Bull/Redis queue service for Pikku
5
- * Provides job results, retries, priorities, and most queue features
6
- */
7
-
8
- export { BullServiceFactory } from './bull-service-factory.js'
9
- export { BullQueueService } from './bull-queue-service.js'
10
- export { BullQueueWorkers } from './bull-queue-worker.js'
11
- export { BullSchedulerService } from './bull-scheduler-service.js'
12
-
13
- // Re-export core queue types for convenience
14
- export type { QueueJob, QueueService } from '@pikku/core/queue'
15
- export type {
16
- PikkuWorkerConfig,
17
- PikkuJobConfig,
18
- JobOptions,
19
- } from '@pikku/core/queue'
package/src/utils.ts DELETED
@@ -1,66 +0,0 @@
1
- import type { QueueJob, QueueJobStatus } from '@pikku/core/queue'
2
- import type { ConnectionOptions, Job } from 'bullmq'
3
- import { QueueEvents } from 'bullmq'
4
-
5
- /**
6
- * Map Bull job state to our queue job status
7
- */
8
- const mapBullStateToStatus = (state: string): QueueJobStatus => {
9
- switch (state) {
10
- case 'waiting':
11
- return 'waiting'
12
- case 'active':
13
- return 'active'
14
- case 'completed':
15
- return 'completed'
16
- case 'failed':
17
- return 'failed'
18
- case 'delayed':
19
- return 'delayed'
20
- default:
21
- return 'waiting'
22
- }
23
- }
24
-
25
- /**
26
- * Map Bull job to QueueJob interface
27
- */
28
- export const mapBullJobToQueueJob = async <In, Out>(
29
- bullJob: Job,
30
- redisConnection: ConnectionOptions | undefined,
31
- queueEventsMap: Map<string, QueueEvents>
32
- ): Promise<QueueJob<In, Out>> => {
33
- return {
34
- queueName: bullJob.name,
35
- id: bullJob.id || '',
36
- data: bullJob.data,
37
- result: bullJob.returnvalue as Out,
38
- status: async () => mapBullStateToStatus(await bullJob.getState()),
39
- waitForCompletion: async (ttl?: number): Promise<Out> => {
40
- let queueEvents = queueEventsMap.get(bullJob.queueName)
41
- if (!queueEvents) {
42
- queueEvents = new QueueEvents(
43
- bullJob.queueName,
44
- redisConnection ? { connection: redisConnection } : undefined
45
- )
46
- queueEventsMap.set(bullJob.queueName, queueEvents)
47
- await queueEvents.waitUntilReady()
48
- }
49
- return await bullJob.waitUntilFinished(queueEvents, ttl)
50
- },
51
- metadata: () => ({
52
- progress: bullJob.progress,
53
- attemptsMade: bullJob.attemptsMade,
54
- maxAttempts: bullJob.opts.attempts || 1,
55
- createdAt: new Date(bullJob.timestamp),
56
- processedAt: bullJob.processedOn
57
- ? new Date(bullJob.processedOn)
58
- : undefined,
59
- completedAt: bullJob.finishedOn
60
- ? new Date(bullJob.finishedOn)
61
- : undefined,
62
- failedAt: bullJob.finishedOn ? new Date(bullJob.finishedOn) : undefined,
63
- error: bullJob.failedReason,
64
- }),
65
- }
66
- }
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "module": "Node18",
6
- "outDir": "dist",
7
- "target": "esnext",
8
- "declaration": true
9
- },
10
- "include": ["src/**/*.ts"],
11
- "exclude": ["**/*.test.ts", "node_modules"],
12
- "references": [
13
- {
14
- "path": "../../core/tsconfig.json"
15
- }
16
- ]
17
- }
@@ -1 +0,0 @@
1
- {"root":["./src/bull-queue-service.ts","./src/bull-queue-worker.ts","./src/bull-scheduler-service.ts","./src/bull-service-factory.ts","./src/index.ts","./src/utils.ts"],"version":"6.0.3"}