@mayurbhusare/resilient-queue-ui 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +205 -3
- package/package.json +55 -4
- package/src/adapters/express.js +39 -0
- package/src/adapters/fastify.js +192 -0
- package/src/dispatcher.js +936 -0
- package/src/handlers/dlq.js +51 -0
- package/src/handlers/job.js +87 -0
- package/src/handlers/metrics.js +60 -0
- package/src/handlers/queues.js +58 -0
- package/src/index.js +44 -0
- package/src/registry.js +102 -0
- package/src/ui/api-client.js +228 -0
- package/src/ui/app.js +1303 -0
- package/src/ui/index.html +141 -0
- package/src/ui/styles.css +1197 -0
- package/src/ui/views/dlq.js +501 -0
- package/src/ui/views/job.js +549 -0
- package/src/ui/views/overview.js +670 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure handler for querying Dead-Letter Queue (DLQ) jobs.
|
|
5
|
+
*
|
|
6
|
+
* @param {import("@mayurbhusare/resilient-queue").ResilientQueue} queue
|
|
7
|
+
* @param {object} [options]
|
|
8
|
+
* @param {number} [options.start=0]
|
|
9
|
+
* @param {number} [options.limit=50]
|
|
10
|
+
* @param {string} [options.order="asc"]
|
|
11
|
+
* @returns {Promise<{ total: number, jobs: Array<object> }>}
|
|
12
|
+
*/
|
|
13
|
+
async function handleGetDLQ(queue, options = {}) {
|
|
14
|
+
if (!queue || (typeof queue !== "object" && typeof queue !== "function")) {
|
|
15
|
+
throw new TypeError("Valid ResilientQueue instance is required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Pre-dispatch closed check: ResilientQueue sets `isClosed = true` on queue.close()
|
|
19
|
+
if (Boolean(queue.isClosed)) {
|
|
20
|
+
const error = new Error("Queue is closed");
|
|
21
|
+
error.code = "QUEUE_CLOSED";
|
|
22
|
+
error.statusCode = 503;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const dlqResult = await queue.getDLQ(options);
|
|
28
|
+
return {
|
|
29
|
+
total: Number(dlqResult?.total) || 0,
|
|
30
|
+
jobs: Array.isArray(dlqResult?.jobs) ? dlqResult.jobs : []
|
|
31
|
+
};
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Defense-in-depth: Catch "Queue is closed" or closed errors
|
|
34
|
+
if (
|
|
35
|
+
err &&
|
|
36
|
+
(err.code === "QUEUE_CLOSED" ||
|
|
37
|
+
(typeof err.message === "string" && /queue is closed/i.test(err.message)))
|
|
38
|
+
) {
|
|
39
|
+
const closedError = new Error("Queue is closed");
|
|
40
|
+
closedError.code = "QUEUE_CLOSED";
|
|
41
|
+
closedError.statusCode = 503;
|
|
42
|
+
throw closedError;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
handleGetDLQ
|
|
51
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely serializes a Job descriptor, omitting non-serializable properties (such as updateProgress).
|
|
5
|
+
*
|
|
6
|
+
* @param {object} job Raw Job instance or object
|
|
7
|
+
* @returns {object|null}
|
|
8
|
+
*/
|
|
9
|
+
function serializeJob(job) {
|
|
10
|
+
if (!job || typeof job !== "object") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const serialized = {};
|
|
15
|
+
for (const [key, value] of Object.entries(job)) {
|
|
16
|
+
// Explicitly omit function-valued properties (e.g. updateProgress)
|
|
17
|
+
if (typeof value === "function") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
// Omit undefined values
|
|
21
|
+
if (value !== undefined) {
|
|
22
|
+
serialized[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return serialized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pure handler for retrieving a persisted job by ID.
|
|
31
|
+
*
|
|
32
|
+
* @param {import("@mayurbhusare/resilient-queue").ResilientQueue} queue
|
|
33
|
+
* @param {string} jobId
|
|
34
|
+
* @returns {Promise<object|null>}
|
|
35
|
+
*/
|
|
36
|
+
async function handleGetJob(queue, jobId) {
|
|
37
|
+
if (!queue || (typeof queue !== "object" && typeof queue !== "function")) {
|
|
38
|
+
throw new TypeError("Valid ResilientQueue instance is required");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (jobId === undefined || jobId === null || typeof jobId !== "string" || jobId.trim().length === 0) {
|
|
42
|
+
throw new TypeError("jobId must be a non-empty string");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Pre-dispatch closed check: ResilientQueue sets `isClosed = true` on queue.close()
|
|
46
|
+
if (Boolean(queue.isClosed)) {
|
|
47
|
+
const error = new Error("Queue is closed");
|
|
48
|
+
error.code = "QUEUE_CLOSED";
|
|
49
|
+
error.statusCode = 503;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof queue.getJob !== "function") {
|
|
54
|
+
const notSupportedError = new Error("Queue instance does not implement getJob");
|
|
55
|
+
notSupportedError.code = "NOT_SUPPORTED";
|
|
56
|
+
notSupportedError.statusCode = 501;
|
|
57
|
+
throw notSupportedError;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const job = await queue.getJob(jobId.trim());
|
|
62
|
+
if (!job) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return serializeJob(job);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Defense-in-depth: Catch "Queue is closed" or closed errors
|
|
69
|
+
if (
|
|
70
|
+
err &&
|
|
71
|
+
(err.code === "QUEUE_CLOSED" ||
|
|
72
|
+
(typeof err.message === "string" && /queue is closed/i.test(err.message)))
|
|
73
|
+
) {
|
|
74
|
+
const closedError = new Error("Queue is closed");
|
|
75
|
+
closedError.code = "QUEUE_CLOSED";
|
|
76
|
+
closedError.statusCode = 503;
|
|
77
|
+
throw closedError;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
handleGetJob,
|
|
86
|
+
serializeJob
|
|
87
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure handler for retrieving queue metrics and pause state.
|
|
5
|
+
*
|
|
6
|
+
* @param {import("@mayurbhusare/resilient-queue").ResilientQueue} queue
|
|
7
|
+
* @param {object} [options]
|
|
8
|
+
* @param {boolean} [options.localOnlyPaused=false]
|
|
9
|
+
* @returns {Promise<{ isPaused: boolean, metrics: { waiting: number, active: number, delayed: number, dlq: number } }>}
|
|
10
|
+
*/
|
|
11
|
+
async function handleGetMetrics(queue, options = {}) {
|
|
12
|
+
if (!queue || (typeof queue !== "object" && typeof queue !== "function")) {
|
|
13
|
+
throw new TypeError("Valid ResilientQueue instance is required");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Pre-dispatch closed check: ResilientQueue sets `isClosed = true` on queue.close()
|
|
17
|
+
if (Boolean(queue.isClosed)) {
|
|
18
|
+
const error = new Error("Queue is closed");
|
|
19
|
+
error.code = "QUEUE_CLOSED";
|
|
20
|
+
error.statusCode = 503;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const localOnly = Boolean(options && options.localOnlyPaused);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const [isPaused, metrics] = await Promise.all([
|
|
28
|
+
queue.isPaused({ localOnly }),
|
|
29
|
+
queue.getMetrics()
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
isPaused: Boolean(isPaused),
|
|
34
|
+
metrics: {
|
|
35
|
+
waiting: Number(metrics?.waiting) || 0,
|
|
36
|
+
active: Number(metrics?.active) || 0,
|
|
37
|
+
delayed: Number(metrics?.delayed) || 0,
|
|
38
|
+
dlq: Number(metrics?.dlq) || 0
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Defense-in-depth: Catch "Queue is closed" thrown by queue.getMetrics()
|
|
43
|
+
if (
|
|
44
|
+
err &&
|
|
45
|
+
(err.code === "QUEUE_CLOSED" ||
|
|
46
|
+
(typeof err.message === "string" && /queue is closed/i.test(err.message)))
|
|
47
|
+
) {
|
|
48
|
+
const closedError = new Error("Queue is closed");
|
|
49
|
+
closedError.code = "QUEUE_CLOSED";
|
|
50
|
+
closedError.statusCode = 503;
|
|
51
|
+
throw closedError;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
handleGetMetrics
|
|
60
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { handleGetMetrics } = require("./metrics");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure handler for retrieving a summary list of all explicitly registered queues,
|
|
7
|
+
* their current pause state, and instantaneous metrics.
|
|
8
|
+
*
|
|
9
|
+
* @param {import("../registry").QueueRegistry|Map<string, object>|Record<string, object>|Array<{ name: string, queue: object }>} registry
|
|
10
|
+
* @param {object} [options]
|
|
11
|
+
* @param {boolean} [options.localOnlyPaused=false]
|
|
12
|
+
* @returns {Promise<{ queues: Array<{ name: string, isPaused: boolean, metrics: { waiting: number, active: number, delayed: number, dlq: number } }> }>}
|
|
13
|
+
*/
|
|
14
|
+
async function handleGetQueues(registry, options = {}) {
|
|
15
|
+
if (!registry || typeof registry !== "object") {
|
|
16
|
+
throw new TypeError("Valid QueueRegistry instance or queue map is required");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let entries;
|
|
20
|
+
if (typeof registry.getEntries === "function") {
|
|
21
|
+
entries = registry.getEntries();
|
|
22
|
+
} else if (registry instanceof Map) {
|
|
23
|
+
entries = Array.from(registry.entries()).map(([name, queue]) => ({
|
|
24
|
+
name,
|
|
25
|
+
queue
|
|
26
|
+
}));
|
|
27
|
+
} else if (Array.isArray(registry)) {
|
|
28
|
+
entries = registry;
|
|
29
|
+
} else {
|
|
30
|
+
entries = Object.entries(registry).map(([name, queue]) => ({
|
|
31
|
+
name,
|
|
32
|
+
queue
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return { queues: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const localOnlyPaused = Boolean(options && options.localOnlyPaused);
|
|
41
|
+
|
|
42
|
+
const queues = await Promise.all(
|
|
43
|
+
entries.map(async ({ name, queue }) => {
|
|
44
|
+
const result = await handleGetMetrics(queue, { localOnlyPaused });
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
isPaused: result.isPaused,
|
|
48
|
+
metrics: result.metrics
|
|
49
|
+
};
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return { queues };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
handleGetQueues
|
|
58
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @mayurbhusare/resilient-queue-ui
|
|
5
|
+
* Standalone administration dashboard and API adapter for @mayurbhusare/resilient-queue.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { QueueRegistry, createQueueRegistry } = require("./registry");
|
|
9
|
+
const { handleGetMetrics } = require("./handlers/metrics");
|
|
10
|
+
const { handleGetDLQ } = require("./handlers/dlq");
|
|
11
|
+
const { handleGetJob, serializeJob } = require("./handlers/job");
|
|
12
|
+
const { handleGetQueues } = require("./handlers/queues");
|
|
13
|
+
const {
|
|
14
|
+
DashboardDispatcher,
|
|
15
|
+
createDashboardDispatcher,
|
|
16
|
+
createErrorResponse,
|
|
17
|
+
parseDLQQueryParams,
|
|
18
|
+
sanitizeErrorMessage
|
|
19
|
+
} = require("./dispatcher");
|
|
20
|
+
const { createExpressAdapter } = require("./adapters/express");
|
|
21
|
+
const { fastifyDashboardPlugin } = require("./adapters/fastify");
|
|
22
|
+
const { ApiClient, ApiError } = require("./ui/api-client");
|
|
23
|
+
|
|
24
|
+
const VERSION = "1.0.0";
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
VERSION,
|
|
28
|
+
QueueRegistry,
|
|
29
|
+
createQueueRegistry,
|
|
30
|
+
handleGetMetrics,
|
|
31
|
+
handleGetDLQ,
|
|
32
|
+
handleGetJob,
|
|
33
|
+
handleGetQueues,
|
|
34
|
+
serializeJob,
|
|
35
|
+
DashboardDispatcher,
|
|
36
|
+
createDashboardDispatcher,
|
|
37
|
+
createExpressAdapter,
|
|
38
|
+
fastifyDashboardPlugin,
|
|
39
|
+
createErrorResponse,
|
|
40
|
+
parseDLQQueryParams,
|
|
41
|
+
sanitizeErrorMessage,
|
|
42
|
+
ApiClient,
|
|
43
|
+
ApiError
|
|
44
|
+
};
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Queue Registry for @mayurbhusare/resilient-queue-ui.
|
|
5
|
+
* Manages explicit registration and lookup of ResilientQueue instances.
|
|
6
|
+
*/
|
|
7
|
+
class QueueRegistry {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
if (!options || typeof options !== "object") {
|
|
10
|
+
throw new TypeError("Dashboard adapter options must be an object");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!options.queues) {
|
|
14
|
+
throw new TypeError(
|
|
15
|
+
"options.queues is required and must be an object, Map, or array of queue entries"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this._queues = new Map();
|
|
20
|
+
this._normalize(options.queues);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_register(name, queue) {
|
|
24
|
+
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
25
|
+
throw new TypeError("Queue name must be a non-empty string");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!queue || (typeof queue !== "object" && typeof queue !== "function")) {
|
|
29
|
+
throw new TypeError(`Queue instance is required for queue '${name}'`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizedName = name.trim();
|
|
33
|
+
if (this._queues.has(normalizedName)) {
|
|
34
|
+
throw new Error(`Duplicate queue name registered: '${normalizedName}'`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._queues.set(normalizedName, queue);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_normalize(queues) {
|
|
41
|
+
if (Array.isArray(queues)) {
|
|
42
|
+
for (const entry of queues) {
|
|
43
|
+
if (!entry || typeof entry !== "object") {
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
"Each queue entry in the array must be an object with name and queue properties"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
this._register(entry.name, entry.queue);
|
|
49
|
+
}
|
|
50
|
+
} else if (queues instanceof Map) {
|
|
51
|
+
for (const [name, queue] of queues.entries()) {
|
|
52
|
+
this._register(name, queue);
|
|
53
|
+
}
|
|
54
|
+
} else if (typeof queues === "object" && queues !== null) {
|
|
55
|
+
for (const [name, queue] of Object.entries(queues)) {
|
|
56
|
+
this._register(name, queue);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
"options.queues must be an object, Map, or array of queue entries"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get(name) {
|
|
66
|
+
if (!name || typeof name !== "string") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return this._queues.get(name.trim());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
has(name) {
|
|
73
|
+
if (!name || typeof name !== "string") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return this._queues.has(name.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getNames() {
|
|
80
|
+
return Array.from(this._queues.keys());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getEntries() {
|
|
84
|
+
return Array.from(this._queues.entries()).map(([name, queue]) => ({
|
|
85
|
+
name,
|
|
86
|
+
queue
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get size() {
|
|
91
|
+
return this._queues.size;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createQueueRegistry(options) {
|
|
96
|
+
return new QueueRegistry(options);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
QueueRegistry,
|
|
101
|
+
createQueueRegistry
|
|
102
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom error class for API errors returned by resilient-queue-ui endpoints or network issues.
|
|
5
|
+
*/
|
|
6
|
+
class ApiError extends Error {
|
|
7
|
+
constructor(message, options = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
this.status = typeof options.status === "number" ? options.status : 0;
|
|
11
|
+
this.code = options.code || "UNKNOWN_ERROR";
|
|
12
|
+
this.details = options.details !== undefined ? options.details : null;
|
|
13
|
+
this.rawResponse = options.rawResponse !== undefined ? options.rawResponse : null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Frontend API client for interacting with @mayurbhusare/resilient-queue-ui REST endpoints.
|
|
19
|
+
* Conforms strictly to docs/api-contract.md.
|
|
20
|
+
*/
|
|
21
|
+
class ApiClient {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object|string} [options={}] Configuration options or baseUrl string
|
|
24
|
+
* @param {string} [options.baseUrl=""] Base URL for API calls (e.g. "/admin/queues" or "http://localhost:3000")
|
|
25
|
+
* @param {typeof fetch} [options.fetch] Custom fetch implementation (defaults to globalThis.fetch)
|
|
26
|
+
*/
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
const config = typeof options === "string" ? { baseUrl: options } : options || {};
|
|
29
|
+
this.baseUrl = (config.baseUrl || "").replace(/\/+$/, "");
|
|
30
|
+
this.fetch = config.fetch || (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Constructs an absolute or relative URL with encoded path segments and query parameters.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} path API path starting with "/"
|
|
37
|
+
* @param {Record<string, any>} [query={}] Optional query parameter dictionary
|
|
38
|
+
* @returns {string} Fully resolved URL string
|
|
39
|
+
*/
|
|
40
|
+
buildUrl(path, query = {}) {
|
|
41
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
42
|
+
const urlStr = `${this.baseUrl}${normalizedPath}`;
|
|
43
|
+
|
|
44
|
+
const params = new URLSearchParams();
|
|
45
|
+
for (const [key, value] of Object.entries(query)) {
|
|
46
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
47
|
+
params.append(key, String(value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const queryString = params.toString();
|
|
51
|
+
return queryString ? `${urlStr}?${queryString}` : urlStr;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Executes an HTTP GET request and normalizes responses or errors.
|
|
56
|
+
*
|
|
57
|
+
* @private
|
|
58
|
+
* @param {string} url
|
|
59
|
+
* @returns {Promise<any>}
|
|
60
|
+
*/
|
|
61
|
+
async _request(url) {
|
|
62
|
+
if (typeof this.fetch !== "function") {
|
|
63
|
+
throw new ApiError("No fetch implementation available in the current environment", {
|
|
64
|
+
code: "ENVIRONMENT_ERROR",
|
|
65
|
+
status: 0
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await this.fetch(url, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: {
|
|
74
|
+
Accept: "application/json"
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (networkErr) {
|
|
78
|
+
throw new ApiError(networkErr?.message || "Network request failed", {
|
|
79
|
+
code: "NETWORK_ERROR",
|
|
80
|
+
status: 0,
|
|
81
|
+
details: networkErr
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = text ? JSON.parse(text) : null;
|
|
89
|
+
} catch (parseErr) {
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new ApiError(`HTTP error ${response.status}`, {
|
|
92
|
+
code: "HTTP_ERROR",
|
|
93
|
+
status: response.status,
|
|
94
|
+
rawResponse: text
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw new ApiError("Invalid JSON response from server", {
|
|
98
|
+
code: "PARSE_ERROR",
|
|
99
|
+
status: response.status,
|
|
100
|
+
rawResponse: text,
|
|
101
|
+
details: parseErr
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorCode = data?.error?.code || "HTTP_ERROR";
|
|
107
|
+
const errorMessage = data?.error?.message || `HTTP error ${response.status}`;
|
|
108
|
+
const errorDetails = data?.error?.details !== undefined ? data.error.details : null;
|
|
109
|
+
|
|
110
|
+
throw new ApiError(errorMessage, {
|
|
111
|
+
code: errorCode,
|
|
112
|
+
status: response.status,
|
|
113
|
+
details: errorDetails,
|
|
114
|
+
rawResponse: data
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* GET /api/queues
|
|
123
|
+
* Retrieves summary and metrics for all explicitly registered queues.
|
|
124
|
+
*
|
|
125
|
+
* @returns {Promise<{ queues: Array<{ name: string, isPaused: boolean, metrics: { waiting: number, active: number, delayed: number, dlq: number } }> }>}
|
|
126
|
+
*/
|
|
127
|
+
async getQueues() {
|
|
128
|
+
const url = this.buildUrl("/api/queues");
|
|
129
|
+
return this._request(url);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* GET /api/queues/:queueName/metrics
|
|
134
|
+
* Retrieves instantaneous metrics and pause status for a single registered queue.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} queueName
|
|
137
|
+
* @returns {Promise<{ queue: string, isPaused: boolean, metrics: { waiting: number, active: number, delayed: number, dlq: number } }>}
|
|
138
|
+
*/
|
|
139
|
+
async getQueueMetrics(queueName) {
|
|
140
|
+
const normalizedQueueName = queueName !== undefined && queueName !== null ? String(queueName).trim() : "";
|
|
141
|
+
if (normalizedQueueName.length === 0) {
|
|
142
|
+
throw new ApiError("queueName must be a non-empty string", {
|
|
143
|
+
code: "VALIDATION_ERROR",
|
|
144
|
+
status: 400
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const encodedName = encodeURIComponent(normalizedQueueName);
|
|
149
|
+
const url = this.buildUrl(`/api/queues/${encodedName}/metrics`);
|
|
150
|
+
return this._request(url);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* GET /api/queues/:queueName/dlq
|
|
155
|
+
* Retrieves paginated DLQ jobs for a registered queue.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} queueName
|
|
158
|
+
* @param {object} [query={}]
|
|
159
|
+
* @param {number} [query.start]
|
|
160
|
+
* @param {number} [query.limit]
|
|
161
|
+
* @param {"asc"|"desc"} [query.order]
|
|
162
|
+
* @returns {Promise<{ queue: string, total: number, start: number, limit: number, order: string, jobs: Array<object> }>}
|
|
163
|
+
*/
|
|
164
|
+
async getQueueDLQ(queueName, query = {}) {
|
|
165
|
+
const normalizedQueueName = queueName !== undefined && queueName !== null ? String(queueName).trim() : "";
|
|
166
|
+
if (normalizedQueueName.length === 0) {
|
|
167
|
+
throw new ApiError("queueName must be a non-empty string", {
|
|
168
|
+
code: "VALIDATION_ERROR",
|
|
169
|
+
status: 400
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const encodedName = encodeURIComponent(normalizedQueueName);
|
|
174
|
+
const queryParams = {};
|
|
175
|
+
if (query.start !== undefined && query.start !== null) queryParams.start = query.start;
|
|
176
|
+
if (query.limit !== undefined && query.limit !== null) queryParams.limit = query.limit;
|
|
177
|
+
if (query.order !== undefined && query.order !== null) queryParams.order = query.order;
|
|
178
|
+
|
|
179
|
+
const url = this.buildUrl(`/api/queues/${encodedName}/dlq`, queryParams);
|
|
180
|
+
return this._request(url);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* GET /api/queues/:queueName/jobs/:jobId
|
|
185
|
+
* Retrieves single job descriptor by unique job ID.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} queueName
|
|
188
|
+
* @param {string|number} jobId
|
|
189
|
+
* @returns {Promise<{ queue: string, job: object }>}
|
|
190
|
+
*/
|
|
191
|
+
async getJob(queueName, jobId) {
|
|
192
|
+
const normalizedQueueName = queueName !== undefined && queueName !== null ? String(queueName).trim() : "";
|
|
193
|
+
if (normalizedQueueName.length === 0) {
|
|
194
|
+
throw new ApiError("queueName must be a non-empty string", {
|
|
195
|
+
code: "VALIDATION_ERROR",
|
|
196
|
+
status: 400
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const normalizedJobId = jobId !== undefined && jobId !== null ? String(jobId).trim() : "";
|
|
201
|
+
if (normalizedJobId.length === 0) {
|
|
202
|
+
throw new ApiError("jobId must be a non-empty string", {
|
|
203
|
+
code: "VALIDATION_ERROR",
|
|
204
|
+
status: 400
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const encodedName = encodeURIComponent(normalizedQueueName);
|
|
209
|
+
const encodedJobId = encodeURIComponent(normalizedJobId);
|
|
210
|
+
const url = this.buildUrl(`/api/queues/${encodedName}/jobs/${encodedJobId}`);
|
|
211
|
+
return this._request(url);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Universal module definition supporting Node.js (CommonJS) and browser globals
|
|
216
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
217
|
+
module.exports = {
|
|
218
|
+
ApiClient,
|
|
219
|
+
ApiError
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof globalThis !== "undefined") {
|
|
224
|
+
globalThis.ResilientQueueApiClient = {
|
|
225
|
+
ApiClient,
|
|
226
|
+
ApiError
|
|
227
|
+
};
|
|
228
|
+
}
|