@dudousxd/nestjs-telescope-bullmq 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # @dudousxd/nestjs-telescope-bullmq
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`73b50ad`](https://github.com/DavideCarvalho/nestjs-telescope/commit/73b50ad00193127271fdec36ad080d2858045922) - Add the BullMQ job watcher (`@dudousxd/nestjs-telescope-bullmq`). It discovers
8
+ `@nestjs/bullmq` `WorkerHost` processors and wraps each job in a `'queue'` batch,
9
+ capturing job outcome/duration/attempts and correlating the queries and
10
+ exceptions a job emits to that job. Core now imports `DiscoveryModule` so
11
+ discovery-based watchers can resolve `DiscoveryService`, and the canonical
12
+ `JobContent` gains `id` and `maxAttempts`.
13
+
14
+ - [`a9f517c`](https://github.com/DavideCarvalho/nestjs-telescope/commit/a9f517c076461aef55cfb90d072ec38427ace91b) - Add gated queue **mutation** endpoints on top of the live-queue reads. The core
15
+ controller now exposes `POST /telescope/api/queues/live/:driver/:queue/jobs/:id/:action`
16
+ (`retry` / `remove` / `promote`) and `POST .../actions/:action` (`retry-all`,
17
+ `redrive`), each carrying `:action` so a single `TelescopeActionGuard` authorizes
18
+ them uniformly.
19
+
20
+ Mutations are **default-deny**: they run behind a new `authorizeAction` option
21
+ _in addition to_ the read `authorizer`, and the guard fails closed. Without an
22
+ `authorizeAction` callback every mutation returns `403` — even for callers the
23
+ read authorizer already trusts. `authorizeAction(ctx, { driver, queue, action,
24
+ jobId?, state? })` opts in; returning falsy or throwing denies.
25
+
26
+ `BullMqQueueManager` implements `retry` / `remove` / `promote` / `retryAll`
27
+ against the real BullMQ `Job.retry()` / `Job.remove()` / `Job.promote()` and
28
+ `Queue.retryJobs()` APIs (`retryAll` returns the pre-action count for the state).
29
+ `redrive` remains SQS-only and returns `405` on the bullmq driver.
30
+
31
+ - [`9d8eb65`](https://github.com/DavideCarvalho/nestjs-telescope/commit/9d8eb6562a7584801d5aa8b74491091f0fade5f9) - Let operators **enqueue (send) a new job/message** onto a queue from the dashboard.
32
+
33
+ Core adds an optional `enqueue?(queue, payload, opts, ctx)` method to the
34
+ `QueueManager` SPI and a new `POST /telescope/api/queues/live/:driver/:queue/enqueue`
35
+ route. Unlike the other mutations it carries a JSON body (`{ name?, payload }`),
36
+ so it lives on its own path rather than under `:action` — but it flows through the
37
+ same default-deny `TelescopeActionGuard` as `retry` / `remove` / `redrive`: the
38
+ guard recovers the `enqueue` action from the request path, so without an
39
+ `authorizeAction` callback it returns `403`. The route returns `400` when the
40
+ payload is absent and `404` when the driver is unknown or doesn't implement
41
+ `enqueue`. `enqueue` is added to `QUEUE_ACTIONS` and advertised in the
42
+ `/queues/live` `actionsByDriver` capabilities when a manager implements it.
43
+
44
+ `BullMqQueueManager` implements `enqueue` via the real `Queue.add(name, data)`
45
+ (defaulting the name to `manual`), returning the new job id.
46
+
47
+ The UI gains an "Send message" form on the queue console — shown only when the
48
+ selected driver advertises `enqueue` and mutations are enabled. It parses the
49
+ payload textarea as JSON (invalid JSON surfaces inline without calling the API),
50
+ posts via a new `queueEnqueue` client method, and on success confirms and
51
+ refreshes the queue counts/jobs. A `403` surfaces inline as "Not authorized".
52
+
53
+ - [`418f1f0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/418f1f0421948b40b25f845441a716fa4c6655c2) - Add a driver-agnostic live-queue read layer. Core gains the `QueueManager` SPI
54
+ (`QueueManager`, `QueueManagerContext`, `QueueManagerRegistry`) and its DTO types
55
+ (`QueueState`, `QueueCounts`, `QueueSummary`, `QueueJob`, `QueueJobDetail`,
56
+ `JobPage`), wired through a `queueManagers` option on `TelescopeModule.forRoot`
57
+ and surfaced as read endpoints under the existing authorizer:
58
+ `GET /telescope/api/queues/live`, `…/live/:driver/:queue/counts`,
59
+ `…/live/:driver/:queue/jobs?state=`, and `…/live/:driver/:queue/jobs/:id`.
60
+
61
+ `@dudousxd/nestjs-telescope-bullmq` adds `BullMqQueueManager`, which discovers
62
+ `@nestjs/bullmq` `Queue` instances via `DiscoveryService` (duck-typed, optional
63
+ explicit allow-list) and reads them through the BullMQ `Queue` API to report
64
+ live counts, the jobs in each list, and per-job detail. Job payloads are passed
65
+ through core redaction before leaving the server. Reads only this phase — queue
66
+ actions (retry/remove/promote/redrive) land in Phase 2.
67
+
68
+ - [`de29d2f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/de29d2f519b1f25bc702c9dcc737d99b4751c8c9) - Add Horizon-style queue metrics. A new `GET /telescope/api/queues?window=1h`
69
+ endpoint aggregates captured `job` entries into per-queue throughput, runtime
70
+ and wait-time percentiles, and failure rate (`QueueMetricsService` +
71
+ `aggregateQueueMetrics`). The BullMQ watcher now captures `waitMs`
72
+ (`processedOn − enqueue`) on each job, and the canonical `JobContent` gains a
73
+ `waitMs` field. `durationToMs` is extracted as a shared, exported util.
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [[`73b50ad`](https://github.com/DavideCarvalho/nestjs-telescope/commit/73b50ad00193127271fdec36ad080d2858045922), [`9126bb0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/9126bb04777cdaec6af3b0a1c5fe6f91d055ce82), [`1f00e62`](https://github.com/DavideCarvalho/nestjs-telescope/commit/1f00e62c8e60482b64251813680a5f866ef1619a), [`090cd1f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/090cd1ff871dbe46c1c877a26f90496550b5304c), [`b7326b3`](https://github.com/DavideCarvalho/nestjs-telescope/commit/b7326b33d8d55b5f1ac5de4256f5e1980278699e), [`bfc0e26`](https://github.com/DavideCarvalho/nestjs-telescope/commit/bfc0e268388b5563d05c24e4de6ff99c74d1201a), [`7797a2a`](https://github.com/DavideCarvalho/nestjs-telescope/commit/7797a2a1554aff49bb59f5ca1b204974a7e04a41), [`6817fe6`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6817fe62775b1ff847fdb1038d3298e7709569e0), [`20ceb87`](https://github.com/DavideCarvalho/nestjs-telescope/commit/20ceb878cd4495dfbc7a3c71d882ae216a633757), [`1dd4db0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/1dd4db0f3cd46d04b35ac112343cfb424c2d3190), [`a9f517c`](https://github.com/DavideCarvalho/nestjs-telescope/commit/a9f517c076461aef55cfb90d072ec38427ace91b), [`9d8eb65`](https://github.com/DavideCarvalho/nestjs-telescope/commit/9d8eb6562a7584801d5aa8b74491091f0fade5f9), [`418f1f0`](https://github.com/DavideCarvalho/nestjs-telescope/commit/418f1f0421948b40b25f845441a716fa4c6655c2), [`e76980d`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e76980ddd7ee740f1b337a422a98ca98d97a007e), [`80c8f97`](https://github.com/DavideCarvalho/nestjs-telescope/commit/80c8f9769c8ab9ee724635086740910bc4d44ea3), [`e14ac60`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e14ac603551372fc3767c63a349c509582b5e6ab), [`6fa0946`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6fa0946f5543868704864af2e32793eb448ac827), [`d507547`](https://github.com/DavideCarvalho/nestjs-telescope/commit/d507547df3c13e76e90b8a97c4e3e1d8aef25bd1), [`affd07e`](https://github.com/DavideCarvalho/nestjs-telescope/commit/affd07e4cb9ee85cfabaabed424833e8c638d04a), [`6a4d8d5`](https://github.com/DavideCarvalho/nestjs-telescope/commit/6a4d8d56321d3840fe64e646130ccfdafcfb1bdd), [`c1f1ec9`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c1f1ec903d470d6b884924e2713de305b61b7481), [`cad6dae`](https://github.com/DavideCarvalho/nestjs-telescope/commit/cad6dae0dba4f22e476d78c23ce2f74f7f6848e4), [`c8596c8`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c8596c85712880cb235e8cce059a1d93d339e9bd), [`c4222b1`](https://github.com/DavideCarvalho/nestjs-telescope/commit/c4222b16ef4c0fc9c61694eb67033f03369ff24e), [`de29d2f`](https://github.com/DavideCarvalho/nestjs-telescope/commit/de29d2f519b1f25bc702c9dcc737d99b4751c8c9), [`10a3bc2`](https://github.com/DavideCarvalho/nestjs-telescope/commit/10a3bc224f6e0b1a237e1e7631acad70493b4c12), [`abde392`](https://github.com/DavideCarvalho/nestjs-telescope/commit/abde39264effb31b0524cc4fa89a335276c8dccb), [`8ff32a2`](https://github.com/DavideCarvalho/nestjs-telescope/commit/8ff32a2cc95775224eea3377460d91674dfda47f), [`5f2eddd`](https://github.com/DavideCarvalho/nestjs-telescope/commit/5f2eddd0ed5d72bf1de323b45870d7ddcaf64349), [`b14a201`](https://github.com/DavideCarvalho/nestjs-telescope/commit/b14a20175eae3d3017e8cbc068d367a03f634175), [`a90ef56`](https://github.com/DavideCarvalho/nestjs-telescope/commit/a90ef569ba12484bece07d8de2045e13ff2ff528), [`593bcc8`](https://github.com/DavideCarvalho/nestjs-telescope/commit/593bcc85ad6558040c62ba66bd1e5e0cbe5a6ac7), [`7b6636b`](https://github.com/DavideCarvalho/nestjs-telescope/commit/7b6636b54438427cd53ea0cbedd186b77d807169), [`e64f35a`](https://github.com/DavideCarvalho/nestjs-telescope/commit/e64f35a1bb7cae15b2ef24404888463d04f81eef), [`4892ef6`](https://github.com/DavideCarvalho/nestjs-telescope/commit/4892ef61e45e5486d34c7ec82764e6767fe8233d)]:
78
+ - @dudousxd/nestjs-telescope@1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davi Carvalho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @dudousxd/nestjs-telescope-bullmq
2
+
3
+ BullMQ job watcher for [`@dudousxd/nestjs-telescope`](../../README.md). Captures
4
+ every job (id, name, queue, outcome, duration, attempts) and correlates the
5
+ queries and exceptions a job emits to that job's batch.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @dudousxd/nestjs-telescope-bullmq
11
+ ```
12
+
13
+ Peer deps: `@dudousxd/nestjs-telescope`, `@nestjs/bullmq`, `bullmq`,
14
+ `@nestjs/common`, `@nestjs/core`, `reflect-metadata`.
15
+
16
+ ## Usage
17
+
18
+ Add the watcher to `TelescopeModule`. No host wiring is required — the watcher
19
+ discovers your `@Processor` (`WorkerHost`) classes and instruments them
20
+ automatically.
21
+
22
+ ```ts
23
+ import { TelescopeModule } from '@dudousxd/nestjs-telescope';
24
+ import { BullMqJobWatcher } from '@dudousxd/nestjs-telescope-bullmq';
25
+
26
+ @Module({
27
+ imports: [
28
+ TelescopeModule.forRoot({
29
+ watchers: [new BullMqJobWatcher({ slowMs: 1000 })],
30
+ }),
31
+ // ...your BullModule.registerQueue(...) and @Processor classes
32
+ ],
33
+ })
34
+ export class AppModule {}
35
+ ```
36
+
37
+ Each processed job becomes a `job` entry with `origin: 'queue'`. Queries and
38
+ exceptions emitted while the job runs share the job's `batchId`, so opening a
39
+ job in the dashboard shows everything it caused.
40
+
41
+ With these entries captured, the core endpoint
42
+ `GET /telescope/api/queues?window=1h` reports per-queue throughput, runtime and
43
+ wait-time percentiles, and failure rate (wait time = `processedOn − enqueue`).
44
+
45
+ ## Live queue reads (`BullMqQueueManager`)
46
+
47
+ The watcher above records *what jobs did*. `BullMqQueueManager` is the
48
+ complementary read side: it browses your queues' **current** state directly via
49
+ the BullMQ `Queue` API (counts, paused flag, and the jobs sitting in each list),
50
+ so the dashboard can show live queue depth and inspect individual jobs.
51
+
52
+ It is a `QueueManager`, passed via the `queueManagers` option (not `watchers`).
53
+ At bootstrap it discovers every `@nestjs/bullmq` `Queue` instance in the Nest
54
+ container through `DiscoveryService` (duck-typed — no extra wiring):
55
+
56
+ ```ts
57
+ import { TelescopeModule } from '@dudousxd/nestjs-telescope';
58
+ import { BullMqQueueManager } from '@dudousxd/nestjs-telescope-bullmq';
59
+
60
+ @Module({
61
+ imports: [
62
+ TelescopeModule.forRoot({
63
+ queueManagers: [new BullMqQueueManager()],
64
+ }),
65
+ BullModule.forRoot({ connection }),
66
+ BullModule.registerQueue({ name: 'mail' }),
67
+ ],
68
+ })
69
+ export class AppModule {}
70
+ ```
71
+
72
+ Pass an explicit allow-list to expose only some queues:
73
+ `new BullMqQueueManager(['mail', 'reports'])`.
74
+
75
+ This surfaces these read endpoints on the core controller, under the **existing
76
+ Telescope authorizer** (same gate as the rest of the dashboard):
77
+
78
+ | Method & path | Returns |
79
+ |---------------|---------|
80
+ | `GET /telescope/api/queues/live` | `{ queues: QueueSummary[] }` across all drivers — name, per-state counts, `isPaused`. |
81
+ | `GET /telescope/api/queues/live/bullmq/:queue/counts` | `QueueCounts` for one queue. |
82
+ | `GET /telescope/api/queues/live/bullmq/:queue/jobs?state=&cursor=&limit=` | A `JobPage` of jobs in `state` (`waiting`/`active`/`delayed`/`failed`/`completed`/`paused`); `nextCursor` paginates. |
83
+ | `GET /telescope/api/queues/live/bullmq/:queue/jobs/:id` | A `QueueJobDetail` (adds redacted `data`, `opts`, `stacktrace`, `returnValue`). |
84
+
85
+ Job payloads (`data`) are passed through core redaction before they leave the
86
+ server, so secret-keyed fields (e.g. `password`, `token`) are masked.
87
+
88
+ ## Queue actions (mutations)
89
+
90
+ `BullMqQueueManager` also implements the action side of the `QueueManager` SPI —
91
+ `retry`, `remove`, `promote`, and `retryAll` — backed by the real BullMQ
92
+ `Job.retry()` / `Job.remove()` / `Job.promote()` and `Queue.retryJobs()` APIs.
93
+ These surface as **POST** endpoints on the core controller:
94
+
95
+ | Method & path | Effect |
96
+ |---------------|--------|
97
+ | `POST /telescope/api/queues/live/bullmq/:queue/jobs/:id/retry` | Re-queue a failed (or completed) job. |
98
+ | `POST /telescope/api/queues/live/bullmq/:queue/jobs/:id/remove` | Delete a job. |
99
+ | `POST /telescope/api/queues/live/bullmq/:queue/jobs/:id/promote` | Promote a delayed job to run now. |
100
+ | `POST /telescope/api/queues/live/bullmq/:queue/actions/retry-all?state=failed` | Bulk re-queue every job in `state`; responds `{ ok: true, count }` (the pre-action count). |
101
+
102
+ `redrive` is **SQS-only** and is not implemented by `BullMqQueueManager`; calling
103
+ `.../actions/redrive` against the bullmq driver returns `405 Method Not Allowed`.
104
+
105
+ ### Enabling mutations — `authorizeAction` (default-deny)
106
+
107
+ Mutations are **denied by default**. They run behind a second guard
108
+ (`TelescopeActionGuard`) on top of the read `authorizer`, and that guard fails
109
+ closed: **without an `authorizeAction` callback, every mutation endpoint returns
110
+ `403`** — even for callers the read `authorizer` already trusts. Opt in by
111
+ supplying `authorizeAction` on `TelescopeModule.forRoot`:
112
+
113
+ ```ts
114
+ TelescopeModule.forRoot({
115
+ // Reads (browse queues/jobs) — the existing gate:
116
+ authorizer: (ctx) => isAdmin(ctx.request),
117
+ // Mutations (retry/remove/promote/retry-all) — separate, default-deny:
118
+ authorizeAction: (ctx, action) => {
119
+ // action: { driver, queue, action, jobId?, state? }
120
+ return canMutateQueues(ctx.request);
121
+ },
122
+ queueManagers: [new BullMqQueueManager()],
123
+ });
124
+ ```
125
+
126
+ `authorizeAction` receives the same `AuthorizerContext` plus a typed
127
+ `QueueActionRequest` describing the requested mutation. Returning a falsy value —
128
+ or throwing — denies the request (`403`). Keep it strictly narrower than your
129
+ read gate: browsing a queue should not imply the right to drain it.
130
+
131
+ ## Options
132
+
133
+ | Option | Default | Description |
134
+ |--------|---------|-------------|
135
+ | `slowMs` | `1000` | Jobs taking at least this long get a `slow` tag. |
136
+ | `includeJobData` | `true` | Capture `job.data` as the entry payload (core redaction still applies). |
137
+ | `clock` | wall clock | Injectable time source (for tests). |
138
+
139
+ ## Not included
140
+
141
+ This watcher captures per-job entries; the aggregate queue metrics above are
142
+ computed in core from those entries (`GET /telescope/api/queues`). Broader health
143
+ rollups — N+1 insights, slowest request/query/job, top exceptions, and a visual
144
+ dashboard — are the concern of `@dudousxd/nestjs-telescope-pulse` and
145
+ `@dudousxd/nestjs-telescope-ui`, not this watcher.
@@ -0,0 +1,30 @@
1
+ import type { JobPage, QueueCounts, QueueJobDetail, QueueManager, QueueManagerContext, QueueState, QueueSummary } from '@dudousxd/nestjs-telescope';
2
+ export declare class BullMqQueueManager implements QueueManager {
3
+ private readonly queueNames?;
4
+ readonly driver = "bullmq";
5
+ private queues;
6
+ private redact;
7
+ /** @param queueNames optional explicit allow-list; default = auto-discover all. */
8
+ constructor(queueNames?: string[] | undefined);
9
+ init(ctx: QueueManagerContext): void;
10
+ private requireQueue;
11
+ private countsFor;
12
+ listQueues(): Promise<QueueSummary[]>;
13
+ counts(queue: string): Promise<QueueCounts>;
14
+ listJobs(queue: string, state: QueueState, page: {
15
+ cursor?: string;
16
+ limit?: number;
17
+ }): Promise<JobPage>;
18
+ getJob(queue: string, id: string): Promise<QueueJobDetail | null>;
19
+ retry(queue: string, id: string): Promise<void>;
20
+ remove(queue: string, id: string): Promise<void>;
21
+ promote(queue: string, id: string): Promise<void>;
22
+ enqueue(queue: string, payload: unknown, opts: {
23
+ name?: string;
24
+ }): Promise<{
25
+ id: string | null;
26
+ }>;
27
+ retryAll(queue: string, state: QueueState): Promise<number>;
28
+ private toJob;
29
+ }
30
+ //# sourceMappingURL=bull-mq-queue-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bull-mq-queue-manager.d.ts","sourceRoot":"","sources":["../src/bull-mq-queue-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,WAAW,EAEX,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,YAAY,EACb,MAAM,4BAA4B,CAAC;AAuDpC,qBAAa,kBAAmB,YAAW,YAAY;IAMzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC;IALxC,QAAQ,CAAC,MAAM,YAAY;IAC3B,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,MAAM,CAAiD;IAE/D,mFAAmF;gBACtD,UAAU,CAAC,EAAE,MAAM,EAAE,YAAA;IAElD,IAAI,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI;IAUpC,OAAO,CAAC,YAAY;YAMN,SAAS;IAmBjB,UAAU,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAW3C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAIrC,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACxC,OAAO,CAAC,OAAO,CAAC;IAYb,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAajE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjD,OAAO,CACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACtB,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAS3B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IASjE,OAAO,CAAC,KAAK;CAcd"}
@@ -0,0 +1,155 @@
1
+ import { DiscoveryService } from '@nestjs/core';
2
+ import { discoverQueues, hasJobOps } from './queue-discovery.js';
3
+ // BullMQ list names accepted by Queue.getJobs()/getJobCounts(). 'waiting' maps
4
+ // to the internal 'wait' list (BullMQ also accepts 'waiting' as an alias, but
5
+ // 'wait' is the canonical list name). 'paused' jobs live in their own list.
6
+ // Verified against bullmq@5.78.0 (JobType = JobState | 'paused' | 'repeat' | 'wait').
7
+ const STATE_TO_BULL = {
8
+ waiting: 'wait',
9
+ active: 'active',
10
+ delayed: 'delayed',
11
+ failed: 'failed',
12
+ completed: 'completed',
13
+ paused: 'paused',
14
+ };
15
+ const DEFAULT_LIMIT = 50;
16
+ function isBullJobLike(value) {
17
+ return typeof value === 'object' && value !== null;
18
+ }
19
+ function readMaxAttempts(opts) {
20
+ if (typeof opts === 'object' && opts !== null) {
21
+ const attempts = opts.attempts;
22
+ if (typeof attempts === 'number')
23
+ return attempts;
24
+ }
25
+ return null;
26
+ }
27
+ /** Best-effort state for a single fetched job (detail view, non-critical). */
28
+ function deriveState(job) {
29
+ if (job.finishedOn != null) {
30
+ return job.failedReason != null ? 'failed' : 'completed';
31
+ }
32
+ if (job.processedOn != null)
33
+ return 'active';
34
+ return 'waiting';
35
+ }
36
+ export class BullMqQueueManager {
37
+ queueNames;
38
+ driver = 'bullmq';
39
+ queues = new Map();
40
+ redact = (value) => value;
41
+ /** @param queueNames optional explicit allow-list; default = auto-discover all. */
42
+ constructor(queueNames) {
43
+ this.queueNames = queueNames;
44
+ }
45
+ init(ctx) {
46
+ this.redact = ctx.redact;
47
+ const discovery = ctx.moduleRef.get(DiscoveryService, { strict: false });
48
+ for (const queue of discoverQueues(discovery)) {
49
+ if (!this.queueNames || this.queueNames.includes(queue.name)) {
50
+ this.queues.set(queue.name, queue);
51
+ }
52
+ }
53
+ }
54
+ requireQueue(queue) {
55
+ const found = this.queues.get(queue);
56
+ if (!found)
57
+ throw new Error(`Unknown bullmq queue: ${queue}`);
58
+ return found;
59
+ }
60
+ async countsFor(queue) {
61
+ const counts = await queue.getJobCounts('waiting', 'active', 'delayed', 'failed', 'completed', 'paused');
62
+ return {
63
+ waiting: counts.waiting ?? 0,
64
+ active: counts.active ?? 0,
65
+ delayed: counts.delayed ?? 0,
66
+ failed: counts.failed ?? 0,
67
+ completed: counts.completed ?? 0,
68
+ paused: counts.paused ?? 0,
69
+ };
70
+ }
71
+ async listQueues() {
72
+ return Promise.all([...this.queues.values()].map(async (queue) => ({
73
+ driver: this.driver,
74
+ queue: queue.name,
75
+ counts: await this.countsFor(queue),
76
+ isPaused: await queue.isPaused(),
77
+ })));
78
+ }
79
+ counts(queue) {
80
+ return this.countsFor(this.requireQueue(queue));
81
+ }
82
+ async listJobs(queue, state, page) {
83
+ const target = this.requireQueue(queue);
84
+ const limit = page.limit && page.limit > 0 ? page.limit : DEFAULT_LIMIT;
85
+ const start = page.cursor ? Math.max(0, Number.parseInt(page.cursor, 10) || 0) : 0;
86
+ // Fetch one extra (end inclusive: start..start+limit) to detect a next page.
87
+ const raw = await target.getJobs(STATE_TO_BULL[state], start, start + limit, false);
88
+ const bullJobs = raw.filter(isBullJobLike);
89
+ const jobs = bullJobs.slice(0, limit).map((job) => this.toJob(job, state));
90
+ const nextCursor = bullJobs.length > limit ? String(start + limit) : null;
91
+ return { jobs, nextCursor, total: null };
92
+ }
93
+ async getJob(queue, id) {
94
+ const raw = await this.requireQueue(queue).getJob(id);
95
+ if (!isBullJobLike(raw))
96
+ return null;
97
+ const base = this.toJob(raw, deriveState(raw));
98
+ return {
99
+ ...base,
100
+ data: this.redact(raw.data),
101
+ opts: raw.opts ?? null,
102
+ stacktrace: Array.isArray(raw.stacktrace) ? raw.stacktrace : null,
103
+ returnValue: raw.returnvalue ?? null,
104
+ };
105
+ }
106
+ async retry(queue, id) {
107
+ const job = await this.requireQueue(queue).getJob(id);
108
+ if (!hasJobOps(job))
109
+ throw new Error(`Job ${id} not found in ${queue}`);
110
+ await job.retry();
111
+ }
112
+ async remove(queue, id) {
113
+ const job = await this.requireQueue(queue).getJob(id);
114
+ if (!hasJobOps(job))
115
+ throw new Error(`Job ${id} not found in ${queue}`);
116
+ await job.remove();
117
+ }
118
+ async promote(queue, id) {
119
+ const job = await this.requireQueue(queue).getJob(id);
120
+ if (!hasJobOps(job))
121
+ throw new Error(`Job ${id} not found in ${queue}`);
122
+ await job.promote();
123
+ }
124
+ async enqueue(queue, payload, opts) {
125
+ const target = this.requireQueue(queue);
126
+ if (typeof target.add !== 'function') {
127
+ throw new Error(`bullmq queue ${queue} cannot enqueue`);
128
+ }
129
+ const job = await target.add(opts.name ?? 'manual', payload);
130
+ return { id: job.id ?? null };
131
+ }
132
+ async retryAll(queue, state) {
133
+ const target = this.requireQueue(queue);
134
+ const before = (await this.countsFor(target))[state] ?? 0;
135
+ if (typeof target.retryJobs === 'function') {
136
+ await target.retryJobs({ state: STATE_TO_BULL[state] });
137
+ }
138
+ return before;
139
+ }
140
+ toJob(job, state) {
141
+ return {
142
+ id: String(job.id ?? ''),
143
+ name: job.name ?? '',
144
+ state,
145
+ attemptsMade: job.attemptsMade ?? 0,
146
+ maxAttempts: readMaxAttempts(job.opts),
147
+ timestamp: job.timestamp ?? null,
148
+ processedOn: job.processedOn ?? null,
149
+ finishedOn: job.finishedOn ?? null,
150
+ failedReason: job.failedReason ?? null,
151
+ progress: typeof job.progress === 'number' ? job.progress : null,
152
+ };
153
+ }
154
+ }
155
+ //# sourceMappingURL=bull-mq-queue-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bull-mq-queue-manager.js","sourceRoot":"","sources":["../src/bull-mq-queue-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { type Watcher, type WatcherContext } from '@dudousxd/nestjs-telescope';
2
+ export interface BullMqJobWatcherOptions {
3
+ /** Jobs whose processing time is >= this (ms) get a 'slow' tag. Default 1000. */
4
+ slowMs?: number;
5
+ /** Capture `job.data` as the entry payload. Default true (core redaction applies). */
6
+ includeJobData?: boolean;
7
+ /** Time source; injectable for tests. Default wall clock. */
8
+ clock?: {
9
+ now(): number;
10
+ };
11
+ }
12
+ /**
13
+ * Captures BullMQ jobs and correlates each job's queries/exceptions to its batch.
14
+ *
15
+ * ## How it works
16
+ * At registration the watcher uses NestJS `DiscoveryService` to find every
17
+ * `WorkerHost` provider and replaces its subclass-prototype `process` method
18
+ * with a wrapper that runs the original inside `ctx.runInBatch('queue', ...)`.
19
+ *
20
+ * `@nestjs/bullmq` resolves `instance.process(job, token)` at call-time per job
21
+ * (to support request-scoped processors), and jobs only run after the app has
22
+ * bootstrapped -- so a prototype patch applied during `register()` (which the
23
+ * registrar invokes at `onApplicationBootstrap`) always precedes the first job.
24
+ *
25
+ * The wrapper never swallows the host's error: on failure it records a `failed`
26
+ * job entry and re-throws so BullMQ's own retry/lifecycle is unaffected.
27
+ *
28
+ * @remarks
29
+ * Processor detection uses `instanceof WorkerHost`. If the host application
30
+ * resolves two distinct copies of `@nestjs/bullmq` in its module tree,
31
+ * processors from the other copy won't match and will be left un-instrumented
32
+ * (surfaced only by the "no processors found" warning). A single, deduped
33
+ * `@nestjs/bullmq` is assumed.
34
+ */
35
+ export declare class BullMqJobWatcher implements Watcher {
36
+ readonly type: "job";
37
+ private readonly logger;
38
+ private readonly slowMs;
39
+ private readonly includeJobData;
40
+ private readonly clock;
41
+ /** Prototypes already patched, so shared prototypes wrap exactly once. */
42
+ private readonly patched;
43
+ constructor(options?: BullMqJobWatcherOptions);
44
+ register(ctx: WatcherContext): Promise<void>;
45
+ private patchProcess;
46
+ /** Build + hand a job entry to the Recorder, swallowing any failure. Core's
47
+ * record() is already non-throwing; this double-guard keeps the watcher safe
48
+ * even against a custom or regressed WatcherContext, so recording can never
49
+ * alter the host job's outcome. */
50
+ private safeRecord;
51
+ }
52
+ //# sourceMappingURL=bullmq-job.watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-job.watcher.d.ts","sourceRoot":"","sources":["../src/bullmq-job.watcher.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,4BAA4B,CAAC;AAMpC,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,6DAA6D;IAC7D,KAAK,CAAC,EAAE;QAAE,GAAG,IAAI,MAAM,CAAA;KAAE,CAAC;CAC3B;AAWD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,gBAAiB,YAAW,OAAO;IAC9C,QAAQ,CAAC,IAAI,QAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqC;IAC5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,OAAO,GAAE,uBAA4B;IAM3C,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBlD,OAAO,CAAC,YAAY;IA0BpB;;;wCAGoC;IACpC,OAAO,CAAC,UAAU;CA8BnB"}
@@ -0,0 +1,125 @@
1
+ // packages/bullmq/src/bullmq-job.watcher.ts
2
+ import { EntryType, } from '@dudousxd/nestjs-telescope';
3
+ import { WorkerHost } from '@nestjs/bullmq';
4
+ import { Logger } from '@nestjs/common';
5
+ import { DiscoveryService } from '@nestjs/core';
6
+ import { buildJobContent } from './job-content.js';
7
+ /** Narrow an unknown BullMQ job to the structural `JobLike` we read. Every field
8
+ * is accessed defensively in `buildJobContent`, so a non-object degrades to {}. */
9
+ function toJobLike(job) {
10
+ return typeof job === 'object' && job !== null ? job : {};
11
+ }
12
+ /**
13
+ * Captures BullMQ jobs and correlates each job's queries/exceptions to its batch.
14
+ *
15
+ * ## How it works
16
+ * At registration the watcher uses NestJS `DiscoveryService` to find every
17
+ * `WorkerHost` provider and replaces its subclass-prototype `process` method
18
+ * with a wrapper that runs the original inside `ctx.runInBatch('queue', ...)`.
19
+ *
20
+ * `@nestjs/bullmq` resolves `instance.process(job, token)` at call-time per job
21
+ * (to support request-scoped processors), and jobs only run after the app has
22
+ * bootstrapped -- so a prototype patch applied during `register()` (which the
23
+ * registrar invokes at `onApplicationBootstrap`) always precedes the first job.
24
+ *
25
+ * The wrapper never swallows the host's error: on failure it records a `failed`
26
+ * job entry and re-throws so BullMQ's own retry/lifecycle is unaffected.
27
+ *
28
+ * @remarks
29
+ * Processor detection uses `instanceof WorkerHost`. If the host application
30
+ * resolves two distinct copies of `@nestjs/bullmq` in its module tree,
31
+ * processors from the other copy won't match and will be left un-instrumented
32
+ * (surfaced only by the "no processors found" warning). A single, deduped
33
+ * `@nestjs/bullmq` is assumed.
34
+ */
35
+ export class BullMqJobWatcher {
36
+ type = EntryType.Job;
37
+ logger = new Logger(BullMqJobWatcher.name);
38
+ slowMs;
39
+ includeJobData;
40
+ clock;
41
+ /** Prototypes already patched, so shared prototypes wrap exactly once. */
42
+ patched = new WeakSet();
43
+ constructor(options = {}) {
44
+ this.slowMs = options.slowMs ?? 1000;
45
+ this.includeJobData = options.includeJobData ?? true;
46
+ this.clock = options.clock ?? { now: () => Date.now() };
47
+ }
48
+ async register(ctx) {
49
+ const discovery = ctx.moduleRef.get(DiscoveryService, { strict: false });
50
+ let count = 0;
51
+ for (const wrapper of discovery.getProviders()) {
52
+ const instance = wrapper.instance;
53
+ if (!(instance instanceof WorkerHost))
54
+ continue;
55
+ const proto = Object.getPrototypeOf(instance);
56
+ if (this.patched.has(proto))
57
+ continue;
58
+ this.patched.add(proto);
59
+ this.patchProcess(proto, ctx);
60
+ count++;
61
+ }
62
+ if (count === 0) {
63
+ this.logger.warn('BullMqJobWatcher: no @Processor (WorkerHost) providers found. ' +
64
+ 'Jobs will not be captured. Ensure your processors are registered before Telescope bootstraps.');
65
+ }
66
+ else {
67
+ this.logger.log(`BullMqJobWatcher: instrumented ${count} processor class(es).`);
68
+ }
69
+ }
70
+ patchProcess(proto, ctx) {
71
+ const original = proto.process;
72
+ if (typeof original !== 'function')
73
+ return;
74
+ const watcher = this;
75
+ proto.process = function patchedProcess(job, token) {
76
+ return ctx.runInBatch('queue', async () => {
77
+ const startedAt = watcher.clock.now();
78
+ try {
79
+ const result = await original.call(this, job, token);
80
+ // safeRecord never throws, so a telescope failure cannot turn a
81
+ // successful job into a failed one.
82
+ watcher.safeRecord(ctx, job, 'completed', watcher.clock.now() - startedAt, undefined);
83
+ return result;
84
+ }
85
+ catch (error) {
86
+ watcher.safeRecord(ctx, job, 'failed', watcher.clock.now() - startedAt, error);
87
+ throw error; // never swallow the host's error
88
+ }
89
+ });
90
+ };
91
+ }
92
+ /** Build + hand a job entry to the Recorder, swallowing any failure. Core's
93
+ * record() is already non-throwing; this double-guard keeps the watcher safe
94
+ * even against a custom or regressed WatcherContext, so recording can never
95
+ * alter the host job's outcome. */
96
+ safeRecord(ctx, job, status, durationMs, error) {
97
+ try {
98
+ const content = buildJobContent(toJobLike(job), status, error, this.includeJobData);
99
+ const familyHash = [content.queue, content.name].filter(Boolean).join(':') || null;
100
+ const tags = [];
101
+ if (content.queue)
102
+ tags.push(`queue:${content.queue}`);
103
+ if (content.name)
104
+ tags.push(`job:${content.name}`);
105
+ if (status === 'failed')
106
+ tags.push('failed');
107
+ if (durationMs >= this.slowMs)
108
+ tags.push('slow');
109
+ const input = {
110
+ type: EntryType.Job,
111
+ content,
112
+ familyHash,
113
+ durationMs,
114
+ };
115
+ if (tags.length > 0)
116
+ input.tags = tags;
117
+ ctx.record(input);
118
+ }
119
+ catch (recordError) {
120
+ const message = recordError instanceof Error ? recordError.message : String(recordError);
121
+ this.logger.error(`BullMqJobWatcher: failed to record job entry: ${message}`);
122
+ }
123
+ }
124
+ }
125
+ //# sourceMappingURL=bullmq-job.watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullmq-job.watcher.js","sourceRoot":"","sources":["../src/bullmq-job.watcher.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,OAAO,EACL,SAAS,GAIV,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAgC,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAcjF;oFACoF;AACpF,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAE,GAAe,CAAC,CAAC,CAAC,EAAE,CAAC;AACzE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,gBAAgB;IAClB,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC;IACb,MAAM,GAAG,IAAI,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,CAAS;IACf,cAAc,CAAU;IACxB,KAAK,CAAoB;IAC1C,0EAA0E;IACzD,OAAO,GAAG,IAAI,OAAO,EAAU,CAAC;IAEjD,YAAY,UAAmC,EAAE;QAC/C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QACrC,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC;QACrD,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAmB;QAChC,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACzE,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,YAAY,EAAE,EAAE,CAAC;YAC/C,MAAM,QAAQ,GAAY,OAAO,CAAC,QAAQ,CAAC;YAC3C,IAAI,CAAC,CAAC,QAAQ,YAAY,UAAU,CAAC;gBAAE,SAAS;YAChD,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAW,CAAC;YACxD,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxB,IAAI,CAAC,YAAY,CAAC,KAAoB,EAAE,GAAG,CAAC,CAAC;YAC7C,KAAK,EAAE,CAAC;QACV,CAAC;QACD,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gEAAgE;gBAC9D,+FAA+F,CAClG,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,kCAAkC,KAAK,uBAAuB,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAEO,YAAY,CAAC,KAAkB,EAAE,GAAmB;QAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC;QAC/B,IAAI,OAAO,QAAQ,KAAK,UAAU;YAAE,OAAO;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC;QAErB,KAAK,CAAC,OAAO,GAAG,SAAS,cAAc,CAErC,GAAY,EACZ,KAAc;YAEd,OAAO,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;gBACxC,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;gBACtC,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;oBACrD,gEAAgE;oBAChE,oCAAoC;oBACpC,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,SAAS,CAAC,CAAC;oBACtF,OAAO,MAAM,CAAC;gBAChB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,KAAK,CAAC,CAAC;oBAC/E,MAAM,KAAK,CAAC,CAAC,iCAAiC;gBAChD,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;IAED;;;wCAGoC;IAC5B,UAAU,CAChB,GAAmB,EACnB,GAAY,EACZ,MAAiB,EACjB,UAAkB,EAClB,KAAc;QAEd,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;YACpF,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;YAEnF,MAAM,IAAI,GAAa,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,KAAK;gBAAE,IAAI,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YACvD,IAAI,OAAO,CAAC,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,OAAO,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;YACnD,IAAI,MAAM,KAAK,QAAQ;gBAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7C,IAAI,UAAU,IAAI,IAAI,CAAC,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAEjD,MAAM,KAAK,GAAgB;gBACzB,IAAI,EAAE,SAAS,CAAC,GAAG;gBACnB,OAAO;gBACP,UAAU;gBACV,UAAU;aACX,CAAC;YACF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;gBAAE,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC;YACvC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,WAAW,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,WAAW,YAAY,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YACzF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iDAAiD,OAAO,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export { BullMqJobWatcher } from './bullmq-job.watcher.js';
2
+ export type { BullMqJobWatcherOptions } from './bullmq-job.watcher.js';
3
+ export { buildJobContent } from './job-content.js';
4
+ export type { JobLike, JobStatus } from './job-content.js';
5
+ export { BullMqQueueManager } from './bull-mq-queue-manager.js';
6
+ export { discoverQueues, isQueueLike } from './queue-discovery.js';
7
+ export type { QueueLike } from './queue-discovery.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // packages/bullmq/src/index.ts
2
+ export { BullMqJobWatcher } from './bullmq-job.watcher.js';
3
+ export { buildJobContent } from './job-content.js';
4
+ export { BullMqQueueManager } from './bull-mq-queue-manager.js';
5
+ export { discoverQueues, isQueueLike } from './queue-discovery.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { JobContent } from '@dudousxd/nestjs-telescope';
2
+ /** The subset of a BullMQ `Job` this watcher reads. Kept structural so the
3
+ * content builder needs no bullmq runtime import and is trivially testable. */
4
+ export interface JobLike {
5
+ id?: string | number;
6
+ name?: string;
7
+ queueName?: string;
8
+ attemptsMade?: number;
9
+ opts?: {
10
+ attempts?: number;
11
+ };
12
+ data?: unknown;
13
+ /** Epoch ms when the job was enqueued (BullMQ `Job.timestamp`). */
14
+ timestamp?: number;
15
+ /** Epoch ms when the worker began processing (BullMQ `Job.processedOn`). */
16
+ processedOn?: number;
17
+ }
18
+ /** The outcomes this watcher records (a subset of core JobContent['status']). */
19
+ export type JobStatus = 'completed' | 'failed';
20
+ /** Normalize a BullMQ job + outcome into the canonical core `JobContent`.
21
+ * Redaction of `payload` is applied centrally by the core Recorder, not here.
22
+ * When `includeData` is false the payload is nulled (the field stays present
23
+ * so the persisted shape is stable). */
24
+ export declare function buildJobContent(job: JobLike, status: JobStatus, error: unknown, includeData: boolean): JobContent;
25
+ //# sourceMappingURL=job-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-content.d.ts","sourceRoot":"","sources":["../src/job-content.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAE7D;gFACgF;AAChF,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAa/C;;;yCAGyC;AACzC,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,OAAO,GACnB,UAAU,CAYZ"}
@@ -0,0 +1,28 @@
1
+ function failureMessage(error) {
2
+ if (error instanceof Error)
3
+ return error.message;
4
+ return String(error);
5
+ }
6
+ function waitMsOf(job) {
7
+ return typeof job.processedOn === 'number' && typeof job.timestamp === 'number'
8
+ ? job.processedOn - job.timestamp
9
+ : null;
10
+ }
11
+ /** Normalize a BullMQ job + outcome into the canonical core `JobContent`.
12
+ * Redaction of `payload` is applied centrally by the core Recorder, not here.
13
+ * When `includeData` is false the payload is nulled (the field stays present
14
+ * so the persisted shape is stable). */
15
+ export function buildJobContent(job, status, error, includeData) {
16
+ return {
17
+ id: job.id != null ? String(job.id) : null,
18
+ name: job.name ?? '',
19
+ queue: job.queueName ?? '',
20
+ payload: includeData ? (job.data ?? null) : null,
21
+ status,
22
+ attempts: typeof job.attemptsMade === 'number' ? job.attemptsMade : 0,
23
+ maxAttempts: typeof job.opts?.attempts === 'number' ? job.opts.attempts : null,
24
+ waitMs: waitMsOf(job),
25
+ failureReason: status === 'failed' ? failureMessage(error) : null,
26
+ };
27
+ }
28
+ //# sourceMappingURL=job-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-content.js","sourceRoot":"","sources":["../src/job-content.ts"],"names":[],"mappings":"AAqBA,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IACjD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,OAAO,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ;QAC7E,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,SAAS;QACjC,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;yCAGyC;AACzC,MAAM,UAAU,eAAe,CAC7B,GAAY,EACZ,MAAiB,EACjB,KAAc,EACd,WAAoB;IAEpB,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;QAC1C,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE;QACpB,KAAK,EAAE,GAAG,CAAC,SAAS,IAAI,EAAE;QAC1B,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;QAChD,MAAM;QACN,QAAQ,EAAE,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACrE,WAAW,EAAE,OAAO,GAAG,CAAC,IAAI,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;QAC9E,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC;QACrB,aAAa,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;KAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { DiscoveryService } from '@nestjs/core';
2
+ /**
3
+ * A duck-typed BullMQ `Queue`. We avoid importing `bullmq`'s `Queue` at runtime
4
+ * (and even type-only) so this helper works whether or not the consumer's
5
+ * esbuild/tsc setup emits the dependency — structural matching is enough to
6
+ * read queues through the public getter API.
7
+ */
8
+ export interface QueueLike {
9
+ name: string;
10
+ getJobCounts(...types: string[]): Promise<Record<string, number>>;
11
+ getJobs(types: string | string[], start?: number, end?: number, asc?: boolean): Promise<unknown[]>;
12
+ getJob(id: string): Promise<unknown>;
13
+ isPaused(): Promise<boolean>;
14
+ retryJobs?(opts: {
15
+ state?: string;
16
+ count?: number;
17
+ }): Promise<void>;
18
+ add?(name: string, data: unknown, opts?: unknown): Promise<{
19
+ id?: string | null;
20
+ }>;
21
+ }
22
+ /**
23
+ * Structural shape of the bullmq `Job` mutation API we drive. We never import
24
+ * `Job` from bullmq; a single structural guard keeps the manager honest about
25
+ * what a fetched job must expose before we mutate it.
26
+ */
27
+ export interface JobOps {
28
+ retry(state?: string): Promise<void>;
29
+ remove(): Promise<void>;
30
+ promote(): Promise<void>;
31
+ }
32
+ export declare function hasJobOps(value: unknown): value is JobOps;
33
+ export declare function isQueueLike(value: unknown): value is QueueLike;
34
+ /** Find all BullMQ Queue instances registered in the Nest container. */
35
+ export declare function discoverQueues(discovery: DiscoveryService): QueueLike[];
36
+ //# sourceMappingURL=queue-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-discovery.d.ts","sourceRoot":"","sources":["../src/queue-discovery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,OAAO,CACL,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,KAAK,CAAC,EAAE,MAAM,EACd,GAAG,CAAC,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,OAAO,GACZ,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACtB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAG7B,SAAS,CAAC,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CACpF;AAED;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAQzD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAU9D;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,EAAE,CASvE"}
@@ -0,0 +1,30 @@
1
+ export function hasJobOps(value) {
2
+ if (typeof value !== 'object' || value === null)
3
+ return false;
4
+ const candidate = value;
5
+ return (typeof candidate.retry === 'function' &&
6
+ typeof candidate.remove === 'function' &&
7
+ typeof candidate.promote === 'function');
8
+ }
9
+ export function isQueueLike(value) {
10
+ if (typeof value !== 'object' || value === null)
11
+ return false;
12
+ const candidate = value;
13
+ return (typeof candidate.name === 'string' &&
14
+ typeof candidate.getJobCounts === 'function' &&
15
+ typeof candidate.getJobs === 'function' &&
16
+ typeof candidate.getJob === 'function' &&
17
+ typeof candidate.isPaused === 'function');
18
+ }
19
+ /** Find all BullMQ Queue instances registered in the Nest container. */
20
+ export function discoverQueues(discovery) {
21
+ const found = new Map();
22
+ for (const wrapper of discovery.getProviders()) {
23
+ const instance = wrapper.instance;
24
+ if (isQueueLike(instance) && !found.has(instance.name)) {
25
+ found.set(instance.name, instance);
26
+ }
27
+ }
28
+ return [...found.values()];
29
+ }
30
+ //# sourceMappingURL=queue-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-discovery.js","sourceRoot":"","sources":["../src/queue-discovery.ts"],"names":[],"mappings":"AAqCA,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAgC,CAAC;IACnD,OAAO,CACL,OAAO,SAAS,CAAC,KAAK,KAAK,UAAU;QACrC,OAAO,SAAS,CAAC,MAAM,KAAK,UAAU;QACtC,OAAO,SAAS,CAAC,OAAO,KAAK,UAAU,CACxC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAgC,CAAC;IACnD,OAAO,CACL,OAAO,SAAS,CAAC,IAAI,KAAK,QAAQ;QAClC,OAAO,SAAS,CAAC,YAAY,KAAK,UAAU;QAC5C,OAAO,SAAS,CAAC,OAAO,KAAK,UAAU;QACvC,OAAO,SAAS,CAAC,MAAM,KAAK,UAAU;QACtC,OAAO,SAAS,CAAC,QAAQ,KAAK,UAAU,CACzC,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,SAA2B;IACxD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC3C,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,YAAY,EAAE,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAClC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACvD,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@dudousxd/nestjs-telescope-bullmq",
3
+ "version": "1.0.0",
4
+ "description": "BullMQ job watcher for @dudousxd/nestjs-telescope.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/DavideCarvalho/nestjs-telescope.git",
9
+ "directory": "packages/bullmq"
10
+ },
11
+ "author": "Davi Carvalho <davi@goflip.ai>",
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/",
24
+ "README.md",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "peerDependencies": {
28
+ "@nestjs/bullmq": ">=10.0.0",
29
+ "@nestjs/common": ">=10.0.0",
30
+ "@nestjs/core": ">=10.0.0",
31
+ "bullmq": ">=5.0.0",
32
+ "reflect-metadata": ">=0.1.13",
33
+ "@dudousxd/nestjs-telescope": "1.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@nestjs/bullmq": "^11.0.0",
37
+ "@nestjs/common": "^11.0.0",
38
+ "@nestjs/core": "^11.0.0",
39
+ "@nestjs/testing": "^11.0.0",
40
+ "@types/node": "^20.0.0",
41
+ "@types/supertest": "^6.0.2",
42
+ "bullmq": "^5.0.0",
43
+ "reflect-metadata": "^0.2.2",
44
+ "rxjs": "^7.8.1",
45
+ "supertest": "^7.0.0",
46
+ "typescript": "^5.4.0",
47
+ "vitest": "^3.0.0",
48
+ "@dudousxd/nestjs-telescope": "1.0.0",
49
+ "@dudousxd/nestjs-telescope-testing": "1.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "keywords": [
55
+ "nestjs",
56
+ "telescope",
57
+ "bullmq",
58
+ "queue",
59
+ "jobs",
60
+ "observability"
61
+ ],
62
+ "scripts": {
63
+ "build": "tsc -p tsconfig.json",
64
+ "test": "vitest run --passWithNoTests",
65
+ "test:watch": "vitest",
66
+ "typecheck": "tsc -p tsconfig.json --noEmit"
67
+ }
68
+ }