rhoam-shared-utils 1.0.0 → 1.0.2
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/package.json +1 -1
- package/src/constants/constant.js +3 -3
- package/src/kafka/client.js +20 -2
- package/src/kafka/consumer.js +42 -10
- package/src/kafka/eventlistener.js +26 -5
- package/src/kafka/producer.js +40 -5
- package/src/kafka/topics.js +17 -1
package/package.json
CHANGED
|
@@ -24,9 +24,9 @@ export const BASE_URLS = {
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
export const SERVICE_URLS = {
|
|
27
|
-
user_services: "http://
|
|
28
|
-
auth_services: "http://
|
|
29
|
-
admin_services: "http://
|
|
27
|
+
user_services: "http://user-service:4001/api/v1/users",
|
|
28
|
+
auth_services: "http://auth-service:4000/api/v1/auth",
|
|
29
|
+
admin_services: "http://admin-service:4003/api/v1/admin",
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
|
package/src/kafka/client.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { Kafka, logLevel } from "kafkajs";
|
|
2
2
|
|
|
3
3
|
let kafka;
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BROKERS = (process.env.KAFKA_BROKERS || "localhost:9092").split(",");
|
|
6
|
+
const DEFAULT_LOG_LEVEL = "INFO";
|
|
7
|
+
|
|
8
|
+
function parseLogLevel(level) {
|
|
9
|
+
const normalized = String(level || DEFAULT_LOG_LEVEL).trim().toUpperCase();
|
|
10
|
+
const map = {
|
|
11
|
+
NOTHING: logLevel.NOTHING,
|
|
12
|
+
ERROR: logLevel.ERROR,
|
|
13
|
+
WARN: logLevel.WARN,
|
|
14
|
+
INFO: logLevel.INFO,
|
|
15
|
+
DEBUG: logLevel.DEBUG,
|
|
16
|
+
};
|
|
17
|
+
return map[normalized] ?? logLevel.INFO;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getKafka(brokers = DEFAULT_BROKERS) {
|
|
5
21
|
if (!kafka) {
|
|
6
22
|
kafka = new Kafka({
|
|
7
23
|
clientId: process.env.KAFKA_CLIENT_ID || "generic-service",
|
|
8
24
|
brokers,
|
|
9
|
-
logLevel:
|
|
25
|
+
logLevel: parseLogLevel(process.env.KAFKA_LOG_LEVEL),
|
|
26
|
+
connectionTimeout: Number(process.env.KAFKA_CONNECTION_TIMEOUT_MS || 3000),
|
|
27
|
+
requestTimeout: Number(process.env.KAFKA_REQUEST_TIMEOUT_MS || 30000),
|
|
10
28
|
});
|
|
11
29
|
}
|
|
12
30
|
return kafka;
|
package/src/kafka/consumer.js
CHANGED
|
@@ -1,23 +1,55 @@
|
|
|
1
1
|
import { getKafka } from "./client.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const DEFAULT_SESSION_TIMEOUT = Number(process.env.KAFKA_CONSUMER_SESSION_TIMEOUT_MS || 30000);
|
|
4
|
+
const DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = Number(process.env.KAFKA_CONSUMER_PARTITIONS_CONCURRENTLY || 1);
|
|
5
|
+
|
|
6
|
+
export async function createConsumer(groupId, options = {}) {
|
|
7
|
+
const consumer = getKafka().consumer({
|
|
8
|
+
groupId,
|
|
9
|
+
sessionTimeout: options.sessionTimeout || DEFAULT_SESSION_TIMEOUT,
|
|
10
|
+
...options.consumerConfig,
|
|
11
|
+
});
|
|
5
12
|
await consumer.connect();
|
|
6
13
|
console.log(`Kafka consumer connected (group: ${groupId})`);
|
|
7
14
|
return consumer;
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
export async function subscribeRun(consumer, topic, onMessage, fromBeginning = false) {
|
|
11
|
-
await consumer.subscribe({ topic, fromBeginning });
|
|
17
|
+
export async function subscribeRun(consumer, topic, onMessage, fromBeginning = false, options = {}) {
|
|
18
|
+
await consumer.subscribe({ topic, fromBeginning, ...options.subscribeConfig });
|
|
19
|
+
|
|
12
20
|
await consumer.run({
|
|
21
|
+
partitionsConsumedConcurrently: options.partitionsConsumedConcurrently ?? DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY,
|
|
13
22
|
eachMessage: async ({ topic, partition, message }) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const maxHandlerRetries = Number(options.maxHandlerRetries ?? 3);
|
|
24
|
+
let attempt = 0;
|
|
25
|
+
let lastError;
|
|
26
|
+
|
|
27
|
+
while (attempt <= maxHandlerRetries) {
|
|
28
|
+
try {
|
|
29
|
+
await onMessage({ topic, partition, message });
|
|
30
|
+
return;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
lastError = err;
|
|
33
|
+
attempt += 1;
|
|
34
|
+
console.error(`Consumer handler error, attempt ${attempt}/${maxHandlerRetries}:`, err?.message || err);
|
|
35
|
+
if (attempt > maxHandlerRetries) {
|
|
36
|
+
if (typeof options.onError === "function") {
|
|
37
|
+
await options.onError({ topic, partition, message, error: err });
|
|
38
|
+
}
|
|
39
|
+
if (options.throwOnError) {
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const delayMs = Math.min(1000 * 2 ** (attempt - 1), 30000);
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (lastError) {
|
|
50
|
+
console.error(`Dropping message after ${maxHandlerRetries} retries for topic:${topic}, partition:${partition}`);
|
|
19
51
|
}
|
|
20
|
-
}
|
|
52
|
+
},
|
|
21
53
|
});
|
|
22
54
|
}
|
|
23
55
|
|
|
@@ -5,25 +5,46 @@ import { createConsumer, subscribeRun } from "./consumer.js";
|
|
|
5
5
|
* @param {string} groupId - Kafka consumer group ID (unique per service).
|
|
6
6
|
* @param {string} topic - Kafka topic name to subscribe to.
|
|
7
7
|
* @param {function} handler - Function called for each event message.
|
|
8
|
+
* @param {object} [options] - Optional listener settings.
|
|
8
9
|
*/
|
|
10
|
+
export async function listenToEvent(groupId, topic, handler, options = {}) {
|
|
11
|
+
if (!groupId || !topic || typeof handler !== "function") {
|
|
12
|
+
throw new Error("listenToEvent requires groupId, topic, and handler");
|
|
13
|
+
}
|
|
9
14
|
|
|
10
|
-
export async function listenToEvent(groupId, topic, handler) {
|
|
11
15
|
try {
|
|
12
|
-
const consumer = await createConsumer(groupId);
|
|
16
|
+
const consumer = await createConsumer(groupId, options);
|
|
13
17
|
|
|
14
18
|
await subscribeRun(consumer, topic, async ({ message }) => {
|
|
15
19
|
const key = message.key?.toString();
|
|
16
20
|
const value = message.value?.toString();
|
|
17
21
|
|
|
18
|
-
if (!key || !value)
|
|
22
|
+
if (!key || !value) {
|
|
23
|
+
console.warn("Skipped empty Kafka message", { topic, partition: message.partition });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
data = JSON.parse(value);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error("Failed to parse message", err.message);
|
|
32
|
+
if (typeof options.onError === "function") {
|
|
33
|
+
await options.onError({ topic, message, error: err });
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
19
37
|
|
|
20
|
-
const data = JSON.parse(value);
|
|
21
38
|
await handler(key, data);
|
|
22
|
-
});
|
|
39
|
+
}, false, options);
|
|
23
40
|
|
|
24
41
|
console.log(`Listening to Kafka topic: ${topic} (group: ${groupId})`);
|
|
25
42
|
return consumer;
|
|
26
43
|
} catch (err) {
|
|
27
44
|
console.error(`Error initializing Kafka consumer for ${topic}:`, err.message);
|
|
45
|
+
if (typeof options.onError === "function") {
|
|
46
|
+
await options.onError({ topic, error: err });
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
28
49
|
}
|
|
29
50
|
}
|
package/src/kafka/producer.js
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
1
|
import { getKafka } from "./client.js";
|
|
2
2
|
let producer;
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
export async function getProducer(options = {}) {
|
|
4
5
|
if (!producer) {
|
|
5
|
-
producer = getKafka().producer({
|
|
6
|
+
producer = getKafka().producer({
|
|
7
|
+
allowAutoTopicCreation: options.allowAutoTopicCreation !== undefined ? options.allowAutoTopicCreation : true,
|
|
8
|
+
idempotent: options.idempotent !== undefined ? options.idempotent : true,
|
|
9
|
+
...options.producerConfig,
|
|
10
|
+
});
|
|
6
11
|
await producer.connect();
|
|
7
12
|
console.log("Kafka producer connected");
|
|
8
13
|
}
|
|
9
14
|
return producer;
|
|
10
15
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
export async function publish(topic, messages, headers = {}, options = {}) {
|
|
20
|
+
if (!topic) {
|
|
21
|
+
throw new Error("Topic is required for publish");
|
|
22
|
+
}
|
|
23
|
+
if (!messages || !messages.length) {
|
|
24
|
+
throw new Error("Messages array is required for publish");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const p = await getProducer(options);
|
|
28
|
+
const maxRetries = Number(options.retries ?? 5);
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
|
|
31
|
+
while (true) {
|
|
32
|
+
try {
|
|
33
|
+
await p.send({ topic, messages, headers });
|
|
34
|
+
return;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
attempt += 1;
|
|
37
|
+
const errMsg = err?.message || String(err);
|
|
38
|
+
console.error(`Kafka publish failed (attempt ${attempt}/${maxRetries}):`, errMsg);
|
|
39
|
+
|
|
40
|
+
if (attempt >= maxRetries) {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const backoffMs = Number(options.retryBackoffMs ?? Math.min(1000 * 2 ** (attempt - 1), 30000));
|
|
45
|
+
await delay(backoffMs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
14
48
|
}
|
|
49
|
+
|
|
15
50
|
export async function shutdownProducer() {
|
|
16
51
|
if (producer) {
|
|
17
52
|
await producer.disconnect();
|
package/src/kafka/topics.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export const Topics = {
|
|
2
2
|
USER_EVENTS : 'user-events',
|
|
3
3
|
EKYC_EVENTS: 'ekyc-events',
|
|
4
|
+
LOG_EVENTS: 'log-events',
|
|
5
|
+
PAYMENT_EVENTS: 'payment-events',
|
|
6
|
+
CHAT_EVENTS: 'chat-events',
|
|
7
|
+
SECURITY_EVENTS: 'security-events',
|
|
4
8
|
};
|
|
5
9
|
|
|
6
10
|
export const Keys = {
|
|
@@ -8,12 +12,24 @@ export const Keys = {
|
|
|
8
12
|
USER_EMAIL_OTP: 'user-email-otp',
|
|
9
13
|
USER_MOBILE_OTP: 'user-mobile-otp',
|
|
10
14
|
USER_PASSWORD_CHANGED: 'user-password-changed',
|
|
15
|
+
USER_DELETED: 'user-deleted',
|
|
11
16
|
USER_FCM_REGISTER: 'user-fcm-store',
|
|
12
17
|
USER_DETAILS_UPDATE: 'user-details-update',
|
|
13
18
|
EKYC_DOCUMENT_UPLOAD: 'ekyc-user-document-upload',
|
|
14
19
|
EKYC_DOCUMENT_DELETED: 'ekyc-user-document-delete',
|
|
20
|
+
EKYC_VERIFICATION_COMPLETED : 'ekyc-verification-completed',
|
|
15
21
|
ERRORS : 'errors',
|
|
16
22
|
AUDIT_EVENTS : 'audit-event',
|
|
17
|
-
|
|
23
|
+
USER_TRACKING : 'user-tracking',
|
|
24
|
+
PAYMENT_RECEIVED: 'payment-received',
|
|
25
|
+
PAYMENT_REQUESTED: 'payment-requested',
|
|
26
|
+
PAYMENT_SENT: 'payment-sent',
|
|
27
|
+
PAYMENT_SPLIT_INITIATE: 'payment-split-initated',
|
|
28
|
+
PAYMENT_SPLIT_PAID: 'payment-split-paid',
|
|
29
|
+
MESSAGE_RECEIVED: 'message-received',
|
|
30
|
+
MESSAGE_SENT: 'message-sent',
|
|
31
|
+
NEW_DEVICE_LOGIN : 'new-device-login',
|
|
32
|
+
DEVICE_SIGNED_OUT: 'device-signed-out',
|
|
33
|
+
PASSWORD_CHANGED : 'password-changed',
|
|
18
34
|
|
|
19
35
|
};
|