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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhoam-shared-utils",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Shared infrastructure utilities for Rhoam microservices",
5
5
  "license": "MIT",
6
6
  "author": "Jabez Nehemiah",
@@ -24,9 +24,9 @@ export const BASE_URLS = {
24
24
 
25
25
 
26
26
  export const SERVICE_URLS = {
27
- user_services: "http://localhost:4001/api/v1/users",
28
- auth_services: "http://localhost:4000/api/v1/auth",
29
- admin_services: "http://locahost:4003/api/v1/admin",
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
 
@@ -1,12 +1,30 @@
1
1
  import { Kafka, logLevel } from "kafkajs";
2
2
 
3
3
  let kafka;
4
- export function getKafka(brokers = (process.env.KAFKA_BROKERS || "localhost:9092").split(",")) {
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: logLevel.NOTHING,
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;
@@ -1,23 +1,55 @@
1
1
  import { getKafka } from "./client.js";
2
2
 
3
- export async function createConsumer(groupId) {
4
- const consumer = getKafka().consumer({ groupId });
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
- try {
15
- await onMessage({ topic, partition, message });
16
- } catch (err) {
17
- console.error("Consumer handler error:", err.message);
18
- // TODO: push to DLQ if needed
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) return console.warn("Skipped empty Kafka message");
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
  }
@@ -1,17 +1,52 @@
1
1
  import { getKafka } from "./client.js";
2
2
  let producer;
3
- export async function getProducer() {
3
+
4
+ export async function getProducer(options = {}) {
4
5
  if (!producer) {
5
- producer = getKafka().producer({ allowAutoTopicCreation: true });
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
- export async function publish(topic, messages, headers = {}) {
12
- const p = await getProducer();
13
- await p.send({ topic, messages, headers });
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();
@@ -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
- EKYC_VERIFICATION_COMPLETED : 'ekyc-verification-completed',
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
  };