@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 +132 -0
- package/dist/bull-queue-service.d.ts +5 -4
- package/dist/bull-queue-service.js +5 -10
- package/dist/bull-queue-worker.d.ts +2 -2
- package/dist/bull-queue-worker.js +2 -2
- package/dist/bull-scheduler-service.d.ts +5 -5
- package/dist/bull-scheduler-service.js +12 -16
- package/dist/index.d.ts +2 -2
- package/package.json +10 -8
- package/run-tests.sh +21 -9
- package/src/bull-queue-service.test.ts +0 -36
- package/src/bull-queue-service.ts +0 -137
- package/src/bull-queue-worker.test.ts +0 -15
- package/src/bull-queue-worker.ts +0 -209
- package/src/bull-scheduler-service.ts +0 -202
- package/src/bull-service-factory.ts +0 -83
- package/src/index.ts +0 -19
- package/src/utils.ts +0 -66
- package/tsconfig.json +0 -17
- package/tsconfig.tsbuildinfo +0 -1
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
|
|
4
|
-
import type {
|
|
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,
|
|
15
|
-
protected queueEvents: Map<string,
|
|
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
|
|
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
|
|
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/
|
|
3
|
-
export declare const mapPikkuWorkerToBull: (workerConfig?: PikkuWorkerConfig) => Omit<WorkerOptions,
|
|
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/
|
|
4
|
-
import { pikkuState } from '@pikku/core/
|
|
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
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
136
|
+
* Stop recurring scheduled tasks by removing their job schedulers.
|
|
141
137
|
*/
|
|
142
138
|
async stop() {
|
|
143
|
-
for (const
|
|
144
|
-
await this.recurringQueue.
|
|
139
|
+
for (const id of this.jobSchedulerIds) {
|
|
140
|
+
await this.recurringQueue.removeJobScheduler(id);
|
|
145
141
|
}
|
|
146
|
-
this.
|
|
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 {
|
|
12
|
-
export type { PikkuWorkerConfig, PikkuJobConfig, JobOptions, } from '@pikku/core/
|
|
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.
|
|
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": "
|
|
21
|
+
"prepublishOnly": "bun run build"
|
|
17
22
|
},
|
|
18
23
|
"peerDependencies": {
|
|
19
|
-
"@pikku/core": "^0.12.
|
|
24
|
+
"@pikku/core": "^0.12.108"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
|
-
"bullmq": "^
|
|
23
|
-
"ioredis": "^
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
"${
|
|
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
|
-
})
|
package/src/bull-queue-worker.ts
DELETED
|
@@ -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
|
-
}
|
package/tsconfig.tsbuildinfo
DELETED
|
@@ -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"}
|