wsp-ms-core 1.1.23 → 1.1.26

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/dist/index.d.mts CHANGED
@@ -640,12 +640,29 @@ declare class HttpNotFoundController implements HttpController {
640
640
  }
641
641
 
642
642
  declare class KafkaManager extends EventManager {
643
- private readonly kafka;
644
- private readonly consumer;
645
- private readonly producer;
643
+ private readonly runtimeProcess;
644
+ private kafka;
645
+ private consumer;
646
+ private producer;
647
+ private brokers;
648
+ private readonly seedBrokers;
649
+ private readonly groupId;
650
+ private leaderSelectionPromise;
646
651
  private producerConnected;
652
+ private isRecoveringLeader;
647
653
  constructor(connection: EventManagerConnection);
648
654
  private ensureProducerConnected;
655
+ private initializeKafkaClients;
656
+ private pinToLeaderBroker;
657
+ private buildKafkaClient;
658
+ private fetchLeaderBroker;
659
+ private recoverFromLeaderChange;
660
+ private waitForLeaderSelection;
661
+ private resolveBrokerList;
662
+ private normalizeBroker;
663
+ private registerConsumerEventHandlers;
664
+ private logConsumerEvent;
665
+ private handleKafkaError;
649
666
  private commitMessage;
650
667
  private run;
651
668
  send(e: EventMessage): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -640,12 +640,29 @@ declare class HttpNotFoundController implements HttpController {
640
640
  }
641
641
 
642
642
  declare class KafkaManager extends EventManager {
643
- private readonly kafka;
644
- private readonly consumer;
645
- private readonly producer;
643
+ private readonly runtimeProcess;
644
+ private kafka;
645
+ private consumer;
646
+ private producer;
647
+ private brokers;
648
+ private readonly seedBrokers;
649
+ private readonly groupId;
650
+ private leaderSelectionPromise;
646
651
  private producerConnected;
652
+ private isRecoveringLeader;
647
653
  constructor(connection: EventManagerConnection);
648
654
  private ensureProducerConnected;
655
+ private initializeKafkaClients;
656
+ private pinToLeaderBroker;
657
+ private buildKafkaClient;
658
+ private fetchLeaderBroker;
659
+ private recoverFromLeaderChange;
660
+ private waitForLeaderSelection;
661
+ private resolveBrokerList;
662
+ private normalizeBroker;
663
+ private registerConsumerEventHandlers;
664
+ private logConsumerEvent;
665
+ private handleKafkaError;
649
666
  private commitMessage;
650
667
  private run;
651
668
  send(e: EventMessage): Promise<void>;
package/dist/index.js CHANGED
@@ -1698,13 +1698,33 @@ var HttpNotFoundController = class {
1698
1698
  };
1699
1699
 
1700
1700
  // src/infrastructure/kafka/KafkaManager.ts
1701
- import { Kafka, logLevel } from "kafkajs";
1701
+ import {
1702
+ Kafka,
1703
+ KafkaJSRequestTimeoutError,
1704
+ logLevel
1705
+ } from "kafkajs";
1702
1706
  var KafkaManager = class extends EventManager {
1703
1707
  constructor(connection) {
1704
1708
  super(connection);
1709
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1710
+ this.runtimeProcess = globalThis?.process;
1705
1711
  this.producerConnected = false;
1712
+ this.isRecoveringLeader = false;
1713
+ this.seedBrokers = this.resolveBrokerList(this._connection.brokers ?? []);
1714
+ this.groupId = this.runtimeProcess?.env?.KAFKA_GROUP || "default";
1715
+ this.initializeKafkaClients(this.seedBrokers);
1716
+ this.leaderSelectionPromise = this.pinToLeaderBroker();
1717
+ }
1718
+ async ensureProducerConnected() {
1719
+ await this.waitForLeaderSelection();
1720
+ if (this.producerConnected)
1721
+ return;
1722
+ await this.producer.connect();
1723
+ this.producerConnected = true;
1724
+ }
1725
+ initializeKafkaClients(brokers) {
1706
1726
  this.kafka = new Kafka({
1707
- brokers: this._connection.brokers,
1727
+ brokers,
1708
1728
  ssl: true,
1709
1729
  sasl: {
1710
1730
  mechanism: "scram-sha-256",
@@ -1713,14 +1733,195 @@ var KafkaManager = class extends EventManager {
1713
1733
  },
1714
1734
  logLevel: logLevel.ERROR
1715
1735
  });
1716
- this.consumer = this.kafka.consumer({ groupId: process.env.KAFKA_GROUP || "default" });
1736
+ this.consumer = this.kafka.consumer({ groupId: this.groupId });
1717
1737
  this.producer = this.kafka.producer();
1738
+ this.brokers = brokers;
1739
+ this.producerConnected = false;
1740
+ this.registerConsumerEventHandlers();
1718
1741
  }
1719
- async ensureProducerConnected() {
1720
- if (this.producerConnected)
1742
+ async pinToLeaderBroker() {
1743
+ try {
1744
+ const leaderBroker = await this.fetchLeaderBroker(this.kafka);
1745
+ if (!leaderBroker) {
1746
+ console.warn("[KafkaManager] Unable to determine leader broker. Using configured list.", { brokers: this.brokers });
1747
+ return;
1748
+ }
1749
+ if (this.brokers.length === 1 && this.brokers[0] === leaderBroker) {
1750
+ return;
1751
+ }
1752
+ console.info("[KafkaManager] Leader broker resolved. Reinitializing clients", { leaderBroker });
1753
+ this.initializeKafkaClients([leaderBroker]);
1754
+ } catch (error) {
1755
+ console.warn("[KafkaManager] Error resolving leader broker. Falling back to configured brokers.", error);
1756
+ }
1757
+ }
1758
+ buildKafkaClient(brokers) {
1759
+ return new Kafka({
1760
+ brokers,
1761
+ ssl: true,
1762
+ sasl: {
1763
+ mechanism: "scram-sha-256",
1764
+ username: this._connection.userName,
1765
+ password: this._connection.password
1766
+ },
1767
+ logLevel: logLevel.ERROR
1768
+ });
1769
+ }
1770
+ async fetchLeaderBroker(kafkaClient) {
1771
+ const admin = kafkaClient.admin();
1772
+ try {
1773
+ await admin.connect();
1774
+ const cluster = await admin.describeCluster();
1775
+ const controllerId = cluster?.controller;
1776
+ const controller = controllerId !== void 0 ? cluster?.brokers?.find((node) => node.nodeId === controllerId) : void 0;
1777
+ if (!controller?.host) {
1778
+ return null;
1779
+ }
1780
+ const host = controller.host;
1781
+ const port = controller.port ?? 9092;
1782
+ return this.normalizeBroker(`${host}:${port}`);
1783
+ } finally {
1784
+ await admin.disconnect().catch(() => void 0);
1785
+ }
1786
+ }
1787
+ async recoverFromLeaderChange(origin) {
1788
+ if (this.isRecoveringLeader)
1721
1789
  return;
1722
- await this.producer.connect();
1723
- this.producerConnected = true;
1790
+ this.isRecoveringLeader = true;
1791
+ console.warn("[KafkaManager] Recovering from leader change", { origin, brokers: this.brokers });
1792
+ await this.execCallback(this._onReconnect, {
1793
+ message: "[KafkaManager] Leader change detected. Reconnecting to new leader.",
1794
+ context: { origin }
1795
+ });
1796
+ try {
1797
+ await this.stop().catch((error) => {
1798
+ console.warn("[KafkaManager] Error stopping previous connection during recovery", error);
1799
+ });
1800
+ const seedClient = this.buildKafkaClient(this.seedBrokers);
1801
+ const leaderBroker = await this.fetchLeaderBroker(seedClient).catch((error) => {
1802
+ console.warn("[KafkaManager] Error resolving new leader broker during recovery", error);
1803
+ return null;
1804
+ });
1805
+ const brokersToUse = leaderBroker ? [leaderBroker] : this.seedBrokers;
1806
+ console.info("[KafkaManager] Reconnecting using brokers", { brokersToUse });
1807
+ this.initializeKafkaClients(brokersToUse);
1808
+ this.leaderSelectionPromise = Promise.resolve();
1809
+ await this.run();
1810
+ } catch (error) {
1811
+ console.error("[KafkaManager] Failed to recover from leader change", { origin, error });
1812
+ await this.execCallback(this._onError, error);
1813
+ } finally {
1814
+ this.isRecoveringLeader = false;
1815
+ }
1816
+ }
1817
+ async waitForLeaderSelection() {
1818
+ try {
1819
+ await this.leaderSelectionPromise;
1820
+ } catch {
1821
+ }
1822
+ }
1823
+ resolveBrokerList(brokers) {
1824
+ const envBrokers = (this.runtimeProcess?.env?.KAFKA_BROKERS ?? "").split(",").map((value) => value.trim()).filter(Boolean);
1825
+ const normalized = [...brokers, ...envBrokers].map((value) => this.normalizeBroker(value)).filter((value) => Boolean(value));
1826
+ const uniqueBrokers = Array.from(new Set(normalized));
1827
+ if (!uniqueBrokers.length) {
1828
+ throw new InternalError(ErrorManager.APP_ERRORS.PROCESS, "Kafka brokers configuration is empty.");
1829
+ }
1830
+ console.info("[KafkaManager] Using brokers", uniqueBrokers);
1831
+ return uniqueBrokers;
1832
+ }
1833
+ normalizeBroker(broker) {
1834
+ if (!broker)
1835
+ return null;
1836
+ const trimmed = broker.trim();
1837
+ if (!trimmed)
1838
+ return null;
1839
+ if (trimmed.startsWith("PLAINTEXT://") || trimmed.startsWith("SSL://")) {
1840
+ return trimmed;
1841
+ }
1842
+ return trimmed.includes(":") ? trimmed : `${trimmed}:9092`;
1843
+ }
1844
+ registerConsumerEventHandlers() {
1845
+ const events = this.consumer.events;
1846
+ this.consumer.on(events.REBALANCING, async (event) => {
1847
+ await this.logConsumerEvent("REBALANCING", event);
1848
+ });
1849
+ const rebalancedEvent = this.consumer.events.REBALANCED;
1850
+ if (rebalancedEvent) {
1851
+ this.consumer.on(rebalancedEvent, async (event) => {
1852
+ await this.logConsumerEvent("REBALANCED", event);
1853
+ });
1854
+ }
1855
+ this.consumer.on(events.REQUEST_TIMEOUT, async (event) => {
1856
+ await this.logConsumerEvent("REQUEST_TIMEOUT", event, "error");
1857
+ this.handleKafkaError(event?.payload?.error, "consumer.REQUEST_TIMEOUT", { broker: event?.payload?.broker });
1858
+ });
1859
+ this.consumer.on(events.CRASH, async (event) => {
1860
+ await this.logConsumerEvent("CRASH", event, "error");
1861
+ this.handleKafkaError(event?.payload?.error, "consumer.CRASH", { broker: event?.payload?.broker });
1862
+ if (event?.payload?.restart === false) {
1863
+ void this.recoverFromLeaderChange("consumer.CRASH.no-restart");
1864
+ }
1865
+ });
1866
+ this.consumer.on(events.DISCONNECT, async (event) => {
1867
+ await this.logConsumerEvent("DISCONNECT", event, "error");
1868
+ });
1869
+ this.consumer.on(events.CONNECT, async (event) => {
1870
+ await this.logConsumerEvent("CONNECT", event);
1871
+ });
1872
+ }
1873
+ async logConsumerEvent(eventName, kafkaEvent, level = "info") {
1874
+ const payload = kafkaEvent?.payload ?? {};
1875
+ const broker = payload?.broker ?? payload?.coordinator ?? payload?.leaderId ?? null;
1876
+ const error = payload?.error ? {
1877
+ type: payload.error.type ?? payload.error.name,
1878
+ message: payload.error.message
1879
+ } : void 0;
1880
+ const context = {
1881
+ event: eventName,
1882
+ timestamp: kafkaEvent?.timestamp ?? DateTime.now().value,
1883
+ broker,
1884
+ brokers: this.brokers,
1885
+ groupId: payload?.groupId,
1886
+ memberId: payload?.memberId,
1887
+ leaderId: payload?.leaderId,
1888
+ clientId: payload?.clientId,
1889
+ topicPartitions: payload?.topicPartitions,
1890
+ error
1891
+ };
1892
+ const message = `[KafkaManager][${eventName}] ${error?.message ?? ""}`.trim();
1893
+ if (level === "error") {
1894
+ console.error(message, context);
1895
+ await this.execCallback(this._onError, { message, context });
1896
+ } else {
1897
+ console.info(message, context);
1898
+ await this.execCallback(this._onReconnect, { message, context });
1899
+ }
1900
+ }
1901
+ handleKafkaError(error, origin, metadata = {}) {
1902
+ if (!error)
1903
+ return;
1904
+ const errorType = error.type ?? error.name;
1905
+ const isNotLeaderError = errorType === "NOT_LEADER_FOR_PARTITION" || error?.name === "KafkaJSNotLeaderForPartition";
1906
+ if (isNotLeaderError) {
1907
+ const payload = {
1908
+ origin,
1909
+ broker: error?.broker ?? metadata?.broker,
1910
+ topic: error?.topic,
1911
+ partition: error?.partition,
1912
+ metadata
1913
+ };
1914
+ console.error("[KafkaManager][NOT_LEADER_FOR_PARTITION] Reconnecting to new leader", payload);
1915
+ void this.execCallback(this._onCrash, {
1916
+ message: "[KafkaManager] NOT_LEADER_FOR_PARTITION detected. Reconnecting to new leader.",
1917
+ context: payload
1918
+ });
1919
+ void this.recoverFromLeaderChange(origin);
1920
+ return;
1921
+ }
1922
+ if (error instanceof KafkaJSRequestTimeoutError || errorType === "REQUEST_TIMEOUT") {
1923
+ console.warn("[KafkaManager][REQUEST_TIMEOUT]", { origin, metadata });
1924
+ }
1724
1925
  }
1725
1926
  async commitMessage(topic, partition, message) {
1726
1927
  const next = (BigInt(message.offset) + 1n).toString();
@@ -1728,9 +1929,10 @@ var KafkaManager = class extends EventManager {
1728
1929
  }
1729
1930
  async run(autocommit = false) {
1730
1931
  const __run = async () => {
1932
+ await this.waitForLeaderSelection();
1731
1933
  await this.execCallback(this._onStart, { message: `--- ${this.constructor.name} started ---` });
1732
1934
  await this.consumer.connect();
1733
- await this.execCallback(this._onConnected, { message: `--- ${this.constructor.name} connected to ${this._connection.brokers} ---` });
1935
+ await this.execCallback(this._onConnected, { message: `--- ${this.constructor.name} connected to ${this.brokers.join(", ")} ---` });
1734
1936
  for (let topic of this._topics) {
1735
1937
  try {
1736
1938
  await this.consumer.subscribe({ topic, fromBeginning: false });
@@ -1767,6 +1969,7 @@ var KafkaManager = class extends EventManager {
1767
1969
  await heartbeat();
1768
1970
  } catch (error) {
1769
1971
  await this.execCallback(this._onError, error);
1972
+ this.handleKafkaError(error, "consumer.eachMessage", { topic, partition });
1770
1973
  throw error;
1771
1974
  }
1772
1975
  }
@@ -1779,6 +1982,7 @@ var KafkaManager = class extends EventManager {
1779
1982
  }
1780
1983
  }
1781
1984
  async send(e) {
1985
+ await this.waitForLeaderSelection();
1782
1986
  const evt = {
1783
1987
  date: DateTime.now().value,
1784
1988
  tenant: e.tenant.value,
@@ -1807,9 +2011,6 @@ var KafkaManager = class extends EventManager {
1807
2011
  }
1808
2012
  }
1809
2013
  async start(autocommit = false) {
1810
- this.consumer.on(this.consumer.events.CRASH, async (error) => {
1811
- await this.execCallback(this._onError, new InternalError(ErrorManager.APP_ERRORS.PROCESS, error.payload.error.stack));
1812
- });
1813
2014
  await this.run();
1814
2015
  }
1815
2016
  async pause() {