@pikku/queue-bullmq 0.12.6 → 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,135 @@
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
+
29
+ ## 0.12.7
30
+
31
+ ### Patch Changes
32
+
33
+ - 266e3bc: One door per name: `@pikku/core/ecosystem/*` and the package root are gone
34
+
35
+ `@pikku/core` published every module twice. `ecosystem/http` re-exported
36
+ `./http`, `ecosystem/services` re-exported `./services`, and a name was
37
+ reachable through either — so every addition had to be made in two places, and a
38
+ consumer's import said nothing about what it actually used. The package root was
39
+ the same problem at a larger scale: a single barrel of 206 names that no bundler
40
+ could take apart, and the one specifier that revealed nothing at all.
41
+
42
+ Both are deleted. Every name now lives on the subpath that owns it, and every
43
+ import carries that subpath — `@pikku/core/http`, `@pikku/core/services`,
44
+ `@pikku/core/errors`, `@pikku/core/types`.
45
+
46
+ Deleting the facades meant the raw subpaths had to become a superset of them,
47
+ which they were not: the facade tree had accumulated 25 names with no raw home
48
+ and about 26 more filed under a different area than the module they came from.
49
+ Those names moved to the area that owns them, and three areas were published as
50
+ new entry points rather than left on a root that is going away — `./types` (the
51
+ shared type surface, the largest single destination), `./state` and
52
+ `./classification`.
53
+
54
+ `./classification` is one door onto one subject: what a value is and how it must
55
+ be handled. Its three halves would each have been an entry point — the brands
56
+ and manifest types, the stored-form helpers (`hashToken`, `unsafeAsSealed` and
57
+ friends), and `SecretValue` — split by whether a name happens to be a type or a
58
+ value, which is the same defect as the facades. The duration and versioned-id
59
+ helpers went to `./utils`, which already published, and `PikkuRequest` went to
60
+ `./function`: it is the transport-agnostic request base, not an HTTP one — HTTP
61
+ has `PikkuHTTPAbstractRequest`, and the only thing outside core that extends
62
+ `PikkuRequest` is Azure's timer request.
63
+
64
+ `./types` inherited the root barrel's habit before it inherited its contents, so
65
+ the names with an owner elsewhere were moved off it. The middleware types and the
66
+ five middleware factories — `pikkuMiddleware`, `pikkuMiddlewareFactory`,
67
+ `pikkuChannelMiddleware`, `pikkuChannelMiddlewareFactory` and
68
+ `pikkuAgentMiddleware`, runtime values on a types entry point — are now
69
+ `@pikku/core/middleware`; the function meta types are `@pikku/core/function`;
70
+ `SerializedError` is `@pikku/core/errors`; and the generic TypeScript helpers
71
+ (`MakeRequired`, `PickRequired`, `PickOptional`, `RequireAtLeastOne`,
72
+ `JSONPrimitive`, `JSONValue`) are `@pikku/core/utils`. What is left on `./types`
73
+ is the vocabulary the wirings share, which no single module owns.
74
+
75
+ `pikku` was itself a root barrel — `export * from '@pikku/core'` — and
76
+ now exports only the services it bundles.
77
+
78
+ One module survives at the old specifier, and only for the bootstrap:
79
+ `packages/cli` is generated by the _published_ CLI pinned in its `build.sh`, and
80
+ that CLI still writes a bare `@pikku/core` into the files it generates for the
81
+ CLI itself. `bootstrap-compat/root.ts` carries the eight types it names, a test
82
+ in core fails if that list grows, and it goes when the pin moves to a CLI
83
+ released from this branch. The adapter names the pinned CLI reaches for —
84
+ `pikkuState` and `CreateWireServices` — are rewritten to `@pikku/core/state` and
85
+ `@pikku/core/types` by the same `build.sh` patch pass, so no second shim is
86
+ needed for them.
87
+
88
+ A guard test keeps the root shut: it parses imports and rejects a bare
89
+ `@pikku/core` rather than grepping for it, because several tests hold a user's
90
+ file as a template literal, where `import … from '@pikku/core'` is fixture text
91
+ rather than an import this repo makes.
92
+
93
+ An agent scaffold a project generated under an older CLI is refreshed rather
94
+ than left to fail: `pikku all` already deleted one importing an entry point
95
+ `@pikku/core` no longer publishes, and the `#pikku` hub joins that list.
96
+ Without it a project that scaffolds the agent endpoint but
97
+ declares no agents keeps the old file forever — the generator that would rewrite
98
+ it only runs when agents exist, and the file being present is what stops it
99
+ being regenerated as missing.
100
+
101
+ `pikku new addon` also wrote a tsconfig `paths` map naming only the deleted hub.
102
+ An addon's `imports` map points into `dist`, so `paths` is what resolves
103
+ `#pikku/<leaf>` for the addon's own source build — it now names the two leaf
104
+ patterns, in both the addon and its test harness.
105
+
106
+ - 786dae5: Bump every dependency whose latest release is a major across the monorepo, and
107
+ port the code the majors broke: `cookie` 2's `parseCookie`/`stringifySetCookie`
108
+ API in `@pikku/core` and the three runtime HTTP adapters, and assistant-ui 0.15's
109
+ store client in `@pikku/assistant-ui`.
110
+ - 6eef0a0: Bump every dependency to its latest compatible minor/patch across the monorepo.
111
+ - Updated dependencies [7722ceb]
112
+ - Updated dependencies [375c1ff]
113
+ - Updated dependencies [02a70cd]
114
+ - Updated dependencies [aeef159]
115
+ - Updated dependencies [a281de6]
116
+ - Updated dependencies [266e3bc]
117
+ - Updated dependencies [02a70cd]
118
+ - Updated dependencies [786dae5]
119
+ - Updated dependencies [6eef0a0]
120
+ - Updated dependencies [3561d67]
121
+ - Updated dependencies [a91c433]
122
+ - Updated dependencies [02a70cd]
123
+ - Updated dependencies [9537f74]
124
+ - Updated dependencies [2b57ca8]
125
+ - Updated dependencies [266e3bc]
126
+ - Updated dependencies [9fce0f1]
127
+ - Updated dependencies [83683a0]
128
+ - Updated dependencies [456c88b]
129
+ - Updated dependencies [456c88b]
130
+ - Updated dependencies [c127273]
131
+ - @pikku/core@0.12.85
132
+
1
133
  ## 0.12.6
2
134
 
3
135
  ### Patch Changes
@@ -1,7 +1,8 @@
1
1
  import type { ConnectionOptions, JobsOptions } from 'bullmq';
2
2
  import type Bull from 'bullmq';
3
- import type { QueueService, QueueJob } from '@pikku/core/queue';
4
- import type { JobOptions } from '@pikku/core/ecosystem/queue';
3
+ import { Queue, QueueEvents } from 'bullmq';
4
+ import type { QueueJob, QueueService } from '@pikku/core/queue';
5
+ import type { JobOptions } from '@pikku/core/queue';
5
6
  export declare const mapPikkuJobToBull: (options?: JobOptions) => JobsOptions;
6
7
  /**
7
8
  * Bull/Redis queue service implementation
@@ -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>>;
15
- protected queueEvents: Map<string, Bull.QueueEvents>;
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,4 +1,4 @@
1
- import { Queue, QueueEvents, RedisConnection } from 'bullmq';
1
+ import { Queue, QueueEvents } from 'bullmq';
2
2
  import { mapBullJobToQueueJob } from './utils.js';
3
3
  export const mapPikkuJobToBull = (options) => {
4
4
  const bullOptions = {};
@@ -30,14 +30,6 @@ export const mapPikkuJobToBull = (options) => {
30
30
  }
31
31
  return bullOptions;
32
32
  };
33
- class PikkuRedisConnection extends RedisConnection {
34
- constructor(options) {
35
- super(options);
36
- this.on('error', (error) => {
37
- console.error('Redis connection error:', error);
38
- });
39
- }
40
- }
41
33
  /**
42
34
  * Bull/Redis queue service implementation
43
35
  * Supports job results, retries, and most queue features
@@ -58,7 +50,10 @@ export class BullQueueService {
58
50
  if (this.queues.has(queueName)) {
59
51
  return this.queues.get(queueName);
60
52
  }
61
- const queue = new Queue(queueName, config, PikkuRedisConnection);
53
+ const queue = new Queue(queueName, config);
54
+ queue.on('error', (error) => {
55
+ console.error('Redis connection error:', error);
56
+ });
62
57
  await queue.waitUntilReady();
63
58
  this.queues.set(queueName, queue);
64
59
  if (!this.queueEvents.has(queueName)) {
@@ -1,6 +1,6 @@
1
1
  import type { ConnectionOptions, WorkerOptions } from 'bullmq';
2
- import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/ecosystem/queue';
3
- export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions, "connection">;
2
+ import type { QueueWorkers, PikkuWorkerConfig, QueueConfigMapping, ConfigValidationResult } from '@pikku/core/queue';
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
@@ -1,7 +1,7 @@
1
1
  import { Worker } from 'bullmq';
2
2
  import { runQueueJob } from '@pikku/core/queue';
3
- import { registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/ecosystem/queue';
4
- import { pikkuState } from '@pikku/core/ecosystem';
3
+ import { registerQueueWorkers, QueueJobFailedError, QueueJobDiscardedError, } from '@pikku/core/queue';
4
+ import { pikkuState } from '@pikku/core/state';
5
5
  import { mapBullJobToQueueJob } from './utils.js';
6
6
  export const mapPikkuWorkerToBull = (workerConfig) => {
7
7
  const workerOptions = {};
@@ -1,13 +1,13 @@
1
1
  import type { ConnectionOptions } from 'bullmq';
2
- import type { CoreUserSession } from '@pikku/core';
3
- import type { ScheduledTaskInfo, ScheduledTaskSummary } from '@pikku/core/ecosystem/services';
4
- import { SchedulerService } from '@pikku/core';
2
+ import { SchedulerService } from '@pikku/core/services';
3
+ import type { CoreUserSession } from '@pikku/core/types';
4
+ import type { ScheduledTaskInfo, ScheduledTaskSummary } from '@pikku/core/services';
5
5
  export declare class BullSchedulerService extends SchedulerService {
6
6
  private redisConnectionOptions;
7
7
  private queue;
8
8
  private recurringQueue;
9
9
  private recurringWorker?;
10
- private repeatJobKeys;
10
+ private jobSchedulerIds;
11
11
  constructor(redisConnectionOptions: ConnectionOptions);
12
12
  /**
13
13
  * Initialize - wait for queue to be ready
@@ -40,7 +40,7 @@ export declare class BullSchedulerService extends SchedulerService {
40
40
  */
41
41
  start(): Promise<void>;
42
42
  /**
43
- * Stop recurring scheduled tasks by removing repeat jobs.
43
+ * Stop recurring scheduled tasks by removing their job schedulers.
44
44
  */
45
45
  stop(): Promise<void>;
46
46
  }
@@ -1,16 +1,16 @@
1
1
  import { Queue, Worker } from 'bullmq';
2
- import { SchedulerService } from '@pikku/core';
3
- import { parseDurationString } from '@pikku/core/ecosystem/types';
4
- import { pikkuState } from '@pikku/core/ecosystem';
5
2
  import { runScheduledTask } from '@pikku/core/scheduler';
6
- import { getScheduledTasks } from '@pikku/core/ecosystem/scheduler';
3
+ import { SchedulerService } from '@pikku/core/services';
4
+ import { parseDurationString } from '@pikku/core/utils';
5
+ import { pikkuState } from '@pikku/core/state';
6
+ import { getScheduledTasks } from '@pikku/core/scheduler';
7
7
  const RECURRING_QUEUE_NAME = 'pikku-recurring-scheduled-task';
8
8
  export class BullSchedulerService extends SchedulerService {
9
9
  redisConnectionOptions;
10
10
  queue;
11
11
  recurringQueue;
12
12
  recurringWorker;
13
- repeatJobKeys = [];
13
+ jobSchedulerIds = [];
14
14
  constructor(redisConnectionOptions) {
15
15
  super();
16
16
  this.redisConnectionOptions = redisConnectionOptions;
@@ -127,23 +127,19 @@ export class BullSchedulerService extends SchedulerService {
127
127
  logger.error(`Recurring task worker error: ${err}`);
128
128
  });
129
129
  for (const [name, task] of scheduledTasks) {
130
- const job = await this.recurringQueue.add(name, { rpcName: name }, {
131
- repeat: { pattern: task.schedule },
132
- jobId: `recurring:${name}`,
133
- });
134
- if (job.repeatJobKey) {
135
- this.repeatJobKeys.push(job.repeatJobKey);
136
- }
130
+ const jobSchedulerId = `recurring:${name}`;
131
+ await this.recurringQueue.upsertJobScheduler(jobSchedulerId, { pattern: task.schedule }, { name, data: { rpcName: name } });
132
+ this.jobSchedulerIds.push(jobSchedulerId);
137
133
  }
138
134
  }
139
135
  /**
140
- * Stop recurring scheduled tasks by removing repeat jobs.
136
+ * Stop recurring scheduled tasks by removing their job schedulers.
141
137
  */
142
138
  async stop() {
143
- for (const key of this.repeatJobKeys) {
144
- await this.recurringQueue.removeRepeatableByKey(key);
139
+ for (const id of this.jobSchedulerIds) {
140
+ await this.recurringQueue.removeJobScheduler(id);
145
141
  }
146
- this.repeatJobKeys = [];
142
+ this.jobSchedulerIds = [];
147
143
  if (this.recurringWorker) {
148
144
  await this.recurringWorker.close();
149
145
  this.recurringWorker = undefined;
package/dist/index.d.ts CHANGED
@@ -8,5 +8,5 @@ export { BullServiceFactory } from './bull-service-factory.js';
8
8
  export { BullQueueService } from './bull-queue-service.js';
9
9
  export { BullQueueWorkers } from './bull-queue-worker.js';
10
10
  export { BullSchedulerService } from './bull-scheduler-service.js';
11
- export type { QueueService, QueueJob } from '@pikku/core/queue';
12
- export type { PikkuWorkerConfig, PikkuJobConfig, JobOptions, } from '@pikku/core/ecosystem/queue';
11
+ export type { QueueJob, QueueService } from '@pikku/core/queue';
12
+ export type { PikkuWorkerConfig, PikkuJobConfig, JobOptions, } from '@pikku/core/queue';
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@pikku/queue-bullmq",
3
- "version": "0.12.6",
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,17 +18,14 @@
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.84"
24
+ "@pikku/core": "^0.12.108"
20
25
  },
21
26
  "dependencies": {
22
- "bullmq": "^5.70.1",
23
- "ioredis": "^5.10.0"
24
- },
25
- "devDependencies": {
26
- "typescript": "^6.0.3"
27
+ "bullmq": "^6.1.1",
28
+ "ioredis": "^6.0.0"
27
29
  },
28
30
  "exports": {
29
31
  ".": "./dist/index.js"
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,36 +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(mapPikkuJobToBull({ attempts: 6, backoff: 'exponential' }).backoff, {
17
- type: 'exponential',
18
- })
19
- })
20
-
21
- test('object backoff → { type, delay } (delay stays in ms)', () => {
22
- assert.deepEqual(
23
- mapPikkuJobToBull({ attempts: 4, backoff: { type: 'fixed', delay: 250 } })
24
- .backoff,
25
- { type: 'fixed', delay: 250 }
26
- )
27
- })
28
-
29
- test('no backoff → backoff left unset', () => {
30
- assert.equal(mapPikkuJobToBull({ attempts: 6 }).backoff, undefined)
31
- })
32
-
33
- test('no options → empty job options', () => {
34
- assert.deepEqual(mapPikkuJobToBull(undefined), {})
35
- })
36
- })
@@ -1,137 +0,0 @@
1
- import type { ConnectionOptions, JobsOptions } from 'bullmq'
2
- import type Bull from 'bullmq'
3
- import { Queue, QueueEvents, RedisConnection } from 'bullmq'
4
- import type { QueueService, QueueJob } from '@pikku/core/queue'
5
- import type { JobOptions } from '@pikku/core/ecosystem/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
- class PikkuRedisConnection extends RedisConnection {
48
- constructor(options: ConnectionOptions) {
49
- super(options)
50
- this.on('error', (error) => {
51
- console.error('Redis connection error:', error)
52
- })
53
- }
54
- }
55
-
56
- /**
57
- * Bull/Redis queue service implementation
58
- * Supports job results, retries, and most queue features
59
- */
60
- export class BullQueueService implements QueueService {
61
- readonly name = 'bull'
62
- readonly supportsResults = true
63
- protected queues = new Map<string, Bull.Queue>()
64
- protected queueEvents = new Map<string, Bull.QueueEvents>()
65
-
66
- constructor(private redisConnectionOptions: ConnectionOptions = {}) {}
67
-
68
- /**
69
- * Create a Bull queue with translated configuration
70
- */
71
- protected async createQueue(
72
- queueName: string,
73
- config?: Bull.QueueOptions
74
- ): Promise<Bull.Queue> {
75
- if (this.queues.has(queueName)) {
76
- return this.queues.get(queueName)!
77
- }
78
-
79
- const queue = new Queue(queueName, config, PikkuRedisConnection)
80
- await queue.waitUntilReady()
81
-
82
- this.queues.set(queueName, queue)
83
-
84
- if (!this.queueEvents.has(queueName)) {
85
- const queueEvents = new QueueEvents(
86
- queueName,
87
- this.redisConnectionOptions
88
- ? { connection: this.redisConnectionOptions }
89
- : undefined
90
- )
91
- await queueEvents.waitUntilReady()
92
- this.queueEvents.set(queueName, queueEvents)
93
- }
94
-
95
- return queue
96
- }
97
-
98
- /**
99
- * Add a job to the queue
100
- */
101
- public async add<In>(
102
- queueName: string,
103
- data: In,
104
- options?: JobOptions
105
- ): Promise<string> {
106
- const queue = await this.createQueue(queueName)
107
- const bullJob = await queue.add(queueName, data, mapPikkuJobToBull(options))
108
- if (!bullJob.id) {
109
- throw new Error(`Failed to add job to queue '${queueName}'.`)
110
- }
111
- return bullJob.id
112
- }
113
-
114
- /**
115
- * Get job by ID
116
- */
117
- async getJob<T, R>(
118
- queueName: string,
119
- jobId: string
120
- ): Promise<QueueJob<T, R> | null> {
121
- const queue = this.queues.get(queueName)
122
- if (!queue) {
123
- throw new Error(`Queue '${queueName}' not found.`)
124
- }
125
-
126
- const bullJob = await queue.getJob(jobId)
127
- if (!bullJob) {
128
- return null
129
- }
130
-
131
- return mapBullJobToQueueJob<T, R>(
132
- bullJob,
133
- this.redisConnectionOptions,
134
- this.queueEvents
135
- )
136
- }
137
- }
@@ -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/ecosystem/queue'
10
- import { runQueueJob } from '@pikku/core/queue'
11
- import {
12
- registerQueueWorkers,
13
- QueueJobFailedError,
14
- QueueJobDiscardedError,
15
- } from '@pikku/core/ecosystem/queue'
16
- import { pikkuState } from '@pikku/core/ecosystem'
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,202 +0,0 @@
1
- import type { ConnectionOptions } from 'bullmq'
2
- import { Queue, Worker } from 'bullmq'
3
- import type { CoreUserSession } from '@pikku/core'
4
- import type {
5
- ScheduledTaskInfo,
6
- ScheduledTaskSummary,
7
- } from '@pikku/core/ecosystem/services'
8
- import { SchedulerService } from '@pikku/core'
9
- import { parseDurationString } from '@pikku/core/ecosystem/types'
10
- import { pikkuState } from '@pikku/core/ecosystem'
11
- import { runScheduledTask } from '@pikku/core/scheduler'
12
- import { getScheduledTasks } from '@pikku/core/ecosystem/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 repeatJobKeys: 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 job = await this.recurringQueue.add(
176
- name,
177
- { rpcName: name } as ScheduledJobData,
178
- {
179
- repeat: { pattern: task.schedule },
180
- jobId: `recurring:${name}`,
181
- }
182
- )
183
- if (job.repeatJobKey) {
184
- this.repeatJobKeys.push(job.repeatJobKey)
185
- }
186
- }
187
- }
188
-
189
- /**
190
- * Stop recurring scheduled tasks by removing repeat jobs.
191
- */
192
- async stop(): Promise<void> {
193
- for (const key of this.repeatJobKeys) {
194
- await this.recurringQueue.removeRepeatableByKey(key)
195
- }
196
- this.repeatJobKeys = []
197
- if (this.recurringWorker) {
198
- await this.recurringWorker.close()
199
- this.recurringWorker = undefined
200
- }
201
- }
202
- }
@@ -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 { QueueService, QueueJob } from '@pikku/core/queue'
15
- export type {
16
- PikkuWorkerConfig,
17
- PikkuJobConfig,
18
- JobOptions,
19
- } from '@pikku/core/ecosystem/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"}