@alacard-project/config-sdk 1.1.0 → 1.1.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.
@@ -1,8 +1,6 @@
1
1
  import { ConfigOptions } from '../types/config.types';
2
2
  export declare class ConfigClient {
3
- private readonly logger;
4
3
  private configMap;
5
- private consumer;
6
4
  private options;
7
5
  private vaultClient;
8
6
  private breaker;
@@ -16,7 +14,6 @@ export declare class ConfigClient {
16
14
  private loadVaultSecrets;
17
15
  private initCircuitBreaker;
18
16
  private fetchRemoteConfig;
19
- private startWatching;
20
17
  get<T = string>(key: string, defaultValue?: T): T;
21
18
  getInt(key: string, defaultValue?: number): number;
22
19
  getBool(key: string, defaultValue?: boolean): boolean;
@@ -38,17 +38,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.ConfigClient = void 0;
40
40
  const grpc = __importStar(require("@grpc/grpc-js"));
41
- const kafkajs_1 = require("kafkajs");
42
41
  const path = __importStar(require("path"));
43
42
  const dotenv = __importStar(require("dotenv"));
44
43
  const opossum_1 = __importDefault(require("opossum"));
45
- const common_1 = require("@nestjs/common");
46
44
  const config_1 = require("../generated/config");
47
45
  const vault_client_1 = require("./vault.client");
48
46
  class ConfigClient {
49
- logger = new common_1.Logger('ConfigSDK');
50
47
  configMap = {};
51
- consumer = null;
52
48
  options;
53
49
  vaultClient = null;
54
50
  breaker = null;
@@ -86,15 +82,11 @@ class ConfigClient {
86
82
  }
87
83
  // 4. Fetch Remote Config via gRPC (with Circuit Breaker)
88
84
  await this.fetchRemoteConfig(serviceName, environment, grpcUrl, version);
89
- // 5. Start Kafka Watcher (if configured)
90
- if (this.options.kafkaBrokers && this.options.kafkaBrokers.length > 0) {
91
- await this.startWatching();
92
- }
93
- this.logger.log(`Configuration initialized with ${Object.keys(this.configMap).length} keys`);
85
+ console.log(`[ConfigSDK] Configuration initialized with ${Object.keys(this.configMap).length} keys`);
94
86
  }
95
87
  catch (error) {
96
88
  const errorMsg = error instanceof Error ? error.message : String(error);
97
- this.logger.error(`Critical initialization failure: ${errorMsg}. Service starting with loaded config.`);
89
+ console.error(`[ConfigSDK] Critical initialization failure: ${errorMsg}. Service starting with loaded config.`);
98
90
  }
99
91
  }
100
92
  loadProcessEnv() {
@@ -106,19 +98,17 @@ class ConfigClient {
106
98
  }
107
99
  }
108
100
  loadDotEnv(environment) {
109
- // Only support standard .env and environment-specific .env
110
101
  const envFiles = [`.env.${environment}`, '.env'];
111
102
  for (const file of envFiles) {
112
103
  const filePath = path.resolve(process.cwd(), file);
113
104
  const result = dotenv.config({ path: filePath });
114
105
  if (result.parsed) {
115
- // Do not overwrite existing keys (process.env wins)
116
106
  for (const [key, value] of Object.entries(result.parsed)) {
117
107
  if (!this.configMap[key]) {
118
108
  this.configMap[key] = value;
119
109
  }
120
110
  }
121
- this.logger.log(`Loaded variables from ${file}`);
111
+ console.log(`[ConfigSDK] Loaded variables from ${file}`);
122
112
  }
123
113
  }
124
114
  }
@@ -128,19 +118,16 @@ class ConfigClient {
128
118
  this.vaultClient = new vault_client_1.VaultClient(this.options.vault);
129
119
  try {
130
120
  const vaultSecrets = await this.vaultClient.getKVSecrets(`config-service/${serviceName}`);
131
- // Vault secrets should overwrite env vars if needed, or fill gaps.
132
- // Usually Vault > Env, but here we treat Env > Vault for local overrides.
133
- // Let's adopt a safe merge: Existing keys (from Env) take precedence.
134
121
  for (const [key, value] of Object.entries(vaultSecrets)) {
135
122
  if (!this.configMap[key]) {
136
123
  this.configMap[key] = value;
137
124
  }
138
125
  }
139
- this.logger.log(`Loaded secrets from Vault for ${serviceName}`);
126
+ console.log(`[ConfigSDK] Loaded secrets from Vault for ${serviceName}`);
140
127
  }
141
128
  catch (error) {
142
129
  const errorMsg = error instanceof Error ? error.message : String(error);
143
- this.logger.warn(`Vault secrets unavailable: ${errorMsg}. Continuing with local config.`);
130
+ console.warn(`[ConfigSDK] Vault secrets unavailable: ${errorMsg}. Continuing with local config.`);
144
131
  }
145
132
  }
146
133
  initCircuitBreaker(client) {
@@ -154,13 +141,10 @@ class ConfigClient {
154
141
  });
155
142
  };
156
143
  this.breaker = new opossum_1.default(fetchFunction, {
157
- timeout: 5000, // 5s timeout
158
- errorThresholdPercentage: 50, // 50% errors opens the breaker
159
- resetTimeout: 10000, // wait 10s before trying again
144
+ timeout: 5000,
145
+ errorThresholdPercentage: 50,
146
+ resetTimeout: 10000,
160
147
  });
161
- this.breaker.on('open', () => this.logger.warn('Circuit Breaker (Config gRPC) is OPEN'));
162
- this.breaker.on('halfOpen', () => this.logger.log('Circuit Breaker (Config gRPC) is HALF_OPEN'));
163
- this.breaker.on('close', () => this.logger.log('Circuit Breaker (Config gRPC) is CLOSED'));
164
148
  }
165
149
  async fetchRemoteConfig(serviceName, environment, grpcUrl, version) {
166
150
  let credentials = grpc.credentials.createInsecure();
@@ -179,59 +163,17 @@ class ConfigClient {
179
163
  try {
180
164
  const values = await this.breaker.fire(request, metadata);
181
165
  if (values) {
182
- // Remote config fills in the gaps
183
166
  for (const [key, value] of Object.entries(values)) {
184
167
  if (!this.configMap[key]) {
185
168
  this.configMap[key] = value;
186
169
  }
187
170
  }
188
- this.logger.log(`Remote config loaded from config-service`);
171
+ console.log(`[ConfigSDK] Remote config loaded via gRPC`);
189
172
  }
190
173
  }
191
174
  catch (error) {
192
175
  const errorMsg = error instanceof Error ? error.message : String(error);
193
- this.logger.warn(`Remote config (gRPC) failure: ${errorMsg}. Using local/vault fallback.`);
194
- }
195
- }
196
- async startWatching() {
197
- if (this.consumer)
198
- return;
199
- try {
200
- const kafka = new kafkajs_1.Kafka({
201
- clientId: `config-sdk-${this.options.serviceName}`,
202
- brokers: this.options.kafkaBrokers,
203
- retry: {
204
- retries: 5, // Retry up to 5 times
205
- maxRetryTime: 30000 // Max time spent retrying
206
- }
207
- });
208
- const groupId = `config-watch-${this.options.serviceName}-${this.options.environment}`;
209
- this.consumer = kafka.consumer({ groupId });
210
- await this.consumer.connect();
211
- await this.consumer.subscribe({ topic: 'config.updated', fromBeginning: false });
212
- await this.consumer.run({
213
- eachMessage: async ({ message }) => {
214
- if (!message.value)
215
- return;
216
- try {
217
- const event = JSON.parse(message.value.toString());
218
- if (event.service === 'global' || event.service === this.options.serviceName) {
219
- if (event.environment === this.options.environment) {
220
- this.logger.log(`Hot-reload: ${event.key} updated`);
221
- this.configMap[event.key] = event.value;
222
- }
223
- }
224
- }
225
- catch (err) {
226
- const errorMsg = err instanceof Error ? err.message : String(err);
227
- this.logger.error(`Failed to parse config update: ${errorMsg}`);
228
- }
229
- },
230
- });
231
- }
232
- catch (error) {
233
- const errorMsg = error instanceof Error ? error.message : String(error);
234
- this.logger.warn(`Config watch mode (Kafka) failed: ${errorMsg}`);
176
+ console.warn(`[ConfigSDK] Remote config (gRPC) failure: ${errorMsg}. Using local/vault fallback.`);
235
177
  }
236
178
  }
237
179
  get(key, defaultValue) {
@@ -240,8 +182,7 @@ class ConfigClient {
240
182
  if (defaultValue !== undefined) {
241
183
  return defaultValue;
242
184
  }
243
- // Strict mode: warn if key is missing and no default value
244
- this.logger.warn(`Configuration key '${key}' not found and no default value provided.`);
185
+ console.warn(`[ConfigSDK] Configuration key '${key}' not found and no default value provided.`);
245
186
  return undefined;
246
187
  }
247
188
  return val;
@@ -1,5 +1,5 @@
1
1
  import { DynamicModule } from '@nestjs/common';
2
- import { ConfigOptions } from '../types/config.types';
2
+ import { ConfigOptions } from './types/config.types';
3
3
  export declare class ConfigModule {
4
4
  static forRoot(options: ConfigOptions): DynamicModule;
5
5
  }
@@ -36,8 +36,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.ConfigModule = void 0;
38
38
  const common_1 = require("@nestjs/common");
39
- const config_client_1 = require("../clients/config.client");
40
- const constants_1 = require("../constants");
39
+ const config_client_1 = require("./clients/config.client");
41
40
  let ConfigModule = (() => {
42
41
  let _classDecorators = [(0, common_1.Global)(), (0, common_1.Module)({})];
43
42
  let _classDescriptor;
@@ -53,20 +52,19 @@ let ConfigModule = (() => {
53
52
  __runInitializers(_classThis, _classExtraInitializers);
54
53
  }
55
54
  static forRoot(options) {
56
- const optionsProvider = {
57
- provide: constants_1.CONFIG_OPTIONS,
58
- useValue: options,
59
- };
60
- const configClientProvider = {
61
- provide: config_client_1.ConfigClient,
62
- useFactory: async () => {
63
- return await config_client_1.ConfigClient.initialize(options);
64
- },
65
- };
66
55
  return {
67
56
  module: ConfigModule,
68
- providers: [optionsProvider, configClientProvider],
69
- exports: [configClientProvider, optionsProvider],
57
+ providers: [
58
+ {
59
+ provide: 'CONFIG_OPTIONS',
60
+ useValue: options,
61
+ },
62
+ {
63
+ provide: config_client_1.ConfigClient,
64
+ useFactory: async () => await config_client_1.ConfigClient.initialize(options),
65
+ },
66
+ ],
67
+ exports: [config_client_1.ConfigClient],
70
68
  };
71
69
  }
72
70
  };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export * from './clients/config.client';
2
2
  export * from './clients/vault.client';
3
- export * from './modules/config.module';
4
- export * from './utils/nest-helpers';
3
+ export * from './utils/config.helpers';
5
4
  export * from './constants';
6
5
  export * from './enums/env.enum';
7
6
  export * from './types/config.types';
8
7
  export * from './types/grpc.types';
8
+ export { ConfigServiceClient } from './generated/config';
9
+ export * from './config.module';
package/dist/index.js CHANGED
@@ -14,11 +14,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.ConfigServiceClient = void 0;
17
18
  __exportStar(require("./clients/config.client"), exports);
18
19
  __exportStar(require("./clients/vault.client"), exports);
19
- __exportStar(require("./modules/config.module"), exports);
20
- __exportStar(require("./utils/nest-helpers"), exports);
20
+ __exportStar(require("./utils/config.helpers"), exports);
21
21
  __exportStar(require("./constants"), exports);
22
22
  __exportStar(require("./enums/env.enum"), exports);
23
23
  __exportStar(require("./types/config.types"), exports);
24
24
  __exportStar(require("./types/grpc.types"), exports);
25
+ var config_1 = require("./generated/config");
26
+ Object.defineProperty(exports, "ConfigServiceClient", { enumerable: true, get: function () { return config_1.ConfigServiceClient; } });
27
+ __exportStar(require("./config.module"), exports);
@@ -1,7 +1,7 @@
1
1
  import { Environment } from '../enums/env.enum';
2
2
  export declare abstract class BaseConfigService {
3
3
  constructor();
4
- protected get<T = string>(key: string, defaultValue?: T): T;
4
+ get<T = string>(key: string, defaultValue?: T): T;
5
5
  getRequiredString(key: string): string;
6
6
  getRequiredNumber(key: string): number;
7
7
  getOptionalString(key: string, defaultValue: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alacard-project/config-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "engines": {
5
5
  "node": ">=24.0.0"
6
6
  },
@@ -11,6 +11,10 @@
11
11
  "exports": {
12
12
  ".": "./dist/index.js"
13
13
  },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
14
18
  "scripts": {
15
19
  "build": "tsc",
16
20
  "lint": "eslint \"src/**/*.ts\" --fix",
@@ -20,27 +24,27 @@
20
24
  "prepublishOnly": "npm run build"
21
25
  },
22
26
  "dependencies": {
23
- "@bufbuild/protobuf": "^2.2.3",
24
- "@grpc/grpc-js": "^1.12.5",
25
- "@grpc/proto-loader": "^0.7.13",
26
- "axios": "^1.7.9",
27
- "dotenv": "^16.4.7",
28
- "kafkajs": "^2.2.4",
29
- "opossum": "^9.0.0"
27
+ "@bufbuild/protobuf": "2.2.3"
28
+ },
29
+ "peerDependencies": {
30
+ "@grpc/grpc-js": "^1.11.0",
31
+ "@grpc/proto-loader": "^0.7.0",
32
+ "axios": "^1.7.0",
33
+ "dotenv": "^16.0.0 || ^17.0.0",
34
+ "opossum": "^8.0.0 || ^9.0.0"
30
35
  },
31
36
  "devDependencies": {
32
- "@eslint/js": "^9.39.2",
33
- "@nestjs/common": "^11.1.12",
34
- "@types/jest": "^29.5.14",
35
- "@types/node": "^22.10.7",
36
- "@types/opossum": "^8.1.9",
37
- "eslint": "^9.39.2",
38
- "globals": "^15.15.0",
39
- "jest": "^29.7.0",
40
- "ts-jest": "^29.2.5",
41
- "ts-proto": "^2.6.1",
42
- "typescript": "^5.7.3",
43
- "typescript-eslint": "^8.54.0"
37
+ "@eslint/js": "9.18.0",
38
+ "@types/jest": "29.5.14",
39
+ "@types/node": "22.10.10",
40
+ "@types/opossum": "8.1.9",
41
+ "eslint": "9.18.0",
42
+ "globals": "17.3.0",
43
+ "jest": "29.7.0",
44
+ "ts-jest": "29.2.6",
45
+ "ts-proto": "2.6.1",
46
+ "typescript": "5.7.3",
47
+ "typescript-eslint": "8.20.0"
44
48
  },
45
49
  "jest": {
46
50
  "moduleFileExtensions": [
@@ -59,4 +63,4 @@
59
63
  "coverageDirectory": "./coverage",
60
64
  "testEnvironment": "node"
61
65
  }
62
- }
66
+ }
package/eslint.config.mjs DELETED
@@ -1,29 +0,0 @@
1
- import js from "@eslint/js";
2
- import globals from "globals";
3
- import tsParser from "@typescript-eslint/parser";
4
- import tsPlugin from "@typescript-eslint/eslint-plugin";
5
-
6
- export default [
7
- js.configs.recommended,
8
- {
9
- ignores: ["src/generated/**/*"],
10
- },
11
- {
12
- files: ["src/**/*.ts"],
13
- languageOptions: {
14
- parser: tsParser,
15
- globals: {
16
- ...globals.node,
17
- ...globals.jest,
18
- },
19
- },
20
- plugins: {
21
- "@typescript-eslint": tsPlugin,
22
- },
23
- rules: {
24
- "no-console": "error",
25
- "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
26
- "@typescript-eslint/no-explicit-any": "warn",
27
- },
28
- },
29
- ];
@@ -1,39 +0,0 @@
1
- syntax = "proto3";
2
-
3
- package config;
4
-
5
- service ConfigService {
6
- rpc GetConfig (GetConfigRequest) returns (ConfigResponse);
7
- rpc SetConfig (SetConfigRequest) returns (ConfigResponse);
8
- rpc ListConfigs (ListConfigsRequest) returns (ListConfigsResponse);
9
- }
10
-
11
- message GetConfigRequest {
12
- string serviceName = 1;
13
- string environment = 2;
14
- string version = 3;
15
- }
16
-
17
- message ConfigResponse {
18
- map<string, string> values = 1;
19
- }
20
-
21
- message SetConfigRequest {
22
- string serviceName = 1;
23
- string environment = 2;
24
- string key = 3;
25
- string value = 4;
26
- }
27
-
28
- message ListConfigsRequest {
29
- string environment = 1;
30
- }
31
-
32
- message ServiceConfig {
33
- string serviceName = 1;
34
- map<string, string> values = 2;
35
- }
36
-
37
- message ListConfigsResponse {
38
- repeated ServiceConfig configs = 1;
39
- }
@@ -1,252 +0,0 @@
1
- import * as grpc from '@grpc/grpc-js';
2
- import { Kafka, Consumer, EachMessagePayload } from 'kafkajs';
3
- import * as path from 'path';
4
- import * as dotenv from 'dotenv';
5
- import CircuitBreaker from 'opossum';
6
- import { Logger } from '@nestjs/common';
7
- import { ConfigServiceClient } from '../generated/config';
8
- import { GetConfigRequest } from '../types/grpc.types';
9
- import { VaultClient } from './vault.client';
10
- import { ConfigOptions, ConfigUpdatedEvent } from '../types/config.types';
11
-
12
- export class ConfigClient {
13
- private readonly logger: Logger = new Logger('ConfigSDK');
14
- private configMap: Record<string, string> = {};
15
- private consumer: Consumer | null = null;
16
- private options: ConfigOptions;
17
- private vaultClient: VaultClient | null = null;
18
- private breaker: CircuitBreaker | null = null;
19
- private static instance: ConfigClient | null = null;
20
-
21
- private constructor(options: ConfigOptions) {
22
- this.options = options;
23
- }
24
-
25
- public static async initialize(options: ConfigOptions): Promise<ConfigClient> {
26
- if (this.instance) {
27
- return this.instance;
28
- }
29
- const client = new ConfigClient(options);
30
- await client.init();
31
- this.instance = client;
32
- return client;
33
- }
34
-
35
- public static getInstance(): ConfigClient {
36
- if (!this.instance) {
37
- throw new Error('[ConfigSDK] ConfigClient not initialized. Call initialize() first.');
38
- }
39
- return this.instance;
40
- }
41
-
42
- private async init(): Promise<void> {
43
- const { serviceName, environment, grpcUrl, version = 'v1', useDotenvFallback = true } = this.options;
44
-
45
- try {
46
- // 1. Load from Process Env (Highest Priority for overrides)
47
- this.loadProcessEnv();
48
-
49
- // 2. Load from DotEnv (if enabled)
50
- if (useDotenvFallback) {
51
- this.loadDotEnv(environment);
52
- }
53
-
54
- // 3. Load from Vault (if configured)
55
- if (this.options.vault) {
56
- await this.loadVaultSecrets(serviceName);
57
- }
58
-
59
- // 4. Fetch Remote Config via gRPC (with Circuit Breaker)
60
- await this.fetchRemoteConfig(serviceName, environment, grpcUrl, version);
61
-
62
- // 5. Start Kafka Watcher (if configured)
63
- if (this.options.kafkaBrokers && this.options.kafkaBrokers.length > 0) {
64
- await this.startWatching();
65
- }
66
-
67
- this.logger.log(`Configuration initialized with ${Object.keys(this.configMap).length} keys`);
68
- } catch (error: unknown) {
69
- const errorMsg = error instanceof Error ? error.message : String(error);
70
- this.logger.error(`Critical initialization failure: ${errorMsg}. Service starting with loaded config.`);
71
- }
72
- }
73
-
74
- private loadProcessEnv(): void {
75
- const ALLOWED_ENV_PREFIXES = ['APP_', 'KAFKA_', 'DB_', 'REDIS_', 'GRPC_', 'PORT', 'NODE_ENV', 'DATABASE_URL'];
76
- for (const [key, value] of Object.entries(process.env)) {
77
- if (value && ALLOWED_ENV_PREFIXES.some(prefix => key.startsWith(prefix))) {
78
- this.configMap[key] = value;
79
- }
80
- }
81
- }
82
-
83
- private loadDotEnv(environment: string): void {
84
- // Only support standard .env and environment-specific .env
85
- const envFiles = [`.env.${environment}`, '.env'];
86
- for (const file of envFiles) {
87
- const filePath = path.resolve(process.cwd(), file);
88
- const result = dotenv.config({ path: filePath });
89
- if (result.parsed) {
90
- // Do not overwrite existing keys (process.env wins)
91
- for (const [key, value] of Object.entries(result.parsed)) {
92
- if (!this.configMap[key]) {
93
- this.configMap[key] = value;
94
- }
95
- }
96
- this.logger.log(`Loaded variables from ${file}`);
97
- }
98
- }
99
- }
100
-
101
- private async loadVaultSecrets(serviceName: string): Promise<void> {
102
- if (!this.options.vault) return;
103
-
104
- this.vaultClient = new VaultClient(this.options.vault);
105
- try {
106
- const vaultSecrets = await this.vaultClient.getKVSecrets(`config-service/${serviceName}`);
107
- // Vault secrets should overwrite env vars if needed, or fill gaps.
108
- // Usually Vault > Env, but here we treat Env > Vault for local overrides.
109
- // Let's adopt a safe merge: Existing keys (from Env) take precedence.
110
- for (const [key, value] of Object.entries(vaultSecrets)) {
111
- if (!this.configMap[key]) {
112
- this.configMap[key] = value as string;
113
- }
114
- }
115
- this.logger.log(`Loaded secrets from Vault for ${serviceName}`);
116
- } catch (error: unknown) {
117
- const errorMsg = error instanceof Error ? error.message : String(error);
118
- this.logger.warn(`Vault secrets unavailable: ${errorMsg}. Continuing with local config.`);
119
- }
120
- }
121
-
122
- private initCircuitBreaker(client: ConfigServiceClient): void {
123
- const fetchFunction = (request: GetConfigRequest, metadata: grpc.Metadata) => {
124
- return new Promise<Record<string, string>>((resolve, reject) => {
125
- client.getConfig(request, metadata, (error, response) => {
126
- if (error) return reject(error);
127
- resolve(response.values || {});
128
- });
129
- });
130
- };
131
-
132
- this.breaker = new CircuitBreaker(fetchFunction, {
133
- timeout: 5000, // 5s timeout
134
- errorThresholdPercentage: 50, // 50% errors opens the breaker
135
- resetTimeout: 10000, // wait 10s before trying again
136
- });
137
-
138
- this.breaker.on('open', () => this.logger.warn('Circuit Breaker (Config gRPC) is OPEN'));
139
- this.breaker.on('halfOpen', () => this.logger.log('Circuit Breaker (Config gRPC) is HALF_OPEN'));
140
- this.breaker.on('close', () => this.logger.log('Circuit Breaker (Config gRPC) is CLOSED'));
141
- }
142
-
143
- private async fetchRemoteConfig(serviceName: string, environment: string, grpcUrl: string, version: string): Promise<void> {
144
- let credentials = grpc.credentials.createInsecure();
145
- if (this.options.tls) {
146
- credentials = grpc.credentials.createSsl(
147
- this.options.tls.rootCert,
148
- this.options.tls.clientKey,
149
- this.options.tls.clientCert,
150
- );
151
- }
152
-
153
- const client = new ConfigServiceClient(grpcUrl, credentials);
154
-
155
- if (!this.breaker) {
156
- this.initCircuitBreaker(client);
157
- }
158
-
159
- const request: GetConfigRequest = { serviceName, environment, version };
160
- const metadata = new grpc.Metadata();
161
- if (this.options.internalKey) {
162
- metadata.set('x-internal-key', this.options.internalKey);
163
- }
164
-
165
- try {
166
- const values = await this.breaker!.fire(request, metadata);
167
- if (values) {
168
- // Remote config fills in the gaps
169
- for (const [key, value] of Object.entries(values)) {
170
- if (!this.configMap[key]) {
171
- this.configMap[key] = value as string;
172
- }
173
- }
174
- this.logger.log(`Remote config loaded from config-service`);
175
- }
176
- } catch (error: unknown) {
177
- const errorMsg = error instanceof Error ? error.message : String(error);
178
- this.logger.warn(`Remote config (gRPC) failure: ${errorMsg}. Using local/vault fallback.`);
179
- }
180
- }
181
-
182
- private async startWatching(): Promise<void> {
183
- if (this.consumer) return;
184
- try {
185
- const kafka = new Kafka({
186
- clientId: `config-sdk-${this.options.serviceName}`,
187
- brokers: this.options.kafkaBrokers!,
188
- retry: {
189
- retries: 5, // Retry up to 5 times
190
- maxRetryTime: 30000 // Max time spent retrying
191
- }
192
- });
193
- const groupId = `config-watch-${this.options.serviceName}-${this.options.environment}`;
194
- this.consumer = kafka.consumer({ groupId });
195
- await this.consumer.connect();
196
- await this.consumer.subscribe({ topic: 'config.updated', fromBeginning: false });
197
-
198
- await this.consumer.run({
199
- eachMessage: async ({ message }: EachMessagePayload) => {
200
- if (!message.value) return;
201
- try {
202
- const event: ConfigUpdatedEvent = JSON.parse(message.value.toString());
203
- if (event.service === 'global' || event.service === this.options.serviceName) {
204
- if (event.environment === this.options.environment) {
205
- this.logger.log(`Hot-reload: ${event.key} updated`);
206
- this.configMap[event.key] = event.value;
207
- }
208
- }
209
- } catch (err: unknown) {
210
- const errorMsg = err instanceof Error ? err.message : String(err);
211
- this.logger.error(`Failed to parse config update: ${errorMsg}`);
212
- }
213
- },
214
- });
215
- } catch (error: unknown) {
216
- const errorMsg = error instanceof Error ? error.message : String(error);
217
- this.logger.warn(`Config watch mode (Kafka) failed: ${errorMsg}`);
218
- }
219
- }
220
-
221
- public get<T = string>(key: string, defaultValue?: T): T {
222
- const val = this.configMap[key];
223
-
224
- if (val === undefined || val === null) {
225
- if (defaultValue !== undefined) {
226
- return defaultValue;
227
- }
228
- // Strict mode: warn if key is missing and no default value
229
- this.logger.warn(`Configuration key '${key}' not found and no default value provided.`);
230
- return undefined as unknown as T;
231
- }
232
-
233
- return val as unknown as T;
234
- }
235
-
236
- public getInt(key: string, defaultValue: number = 0): number {
237
- const val = this.get<string>(key);
238
- if (!val) return defaultValue;
239
- const parsed = parseInt(val, 10);
240
- return isNaN(parsed) ? defaultValue : parsed;
241
- }
242
-
243
- public getBool(key: string, defaultValue: boolean = false): boolean {
244
- const val = this.get<string>(key);
245
- if (!val) return defaultValue;
246
- return val.toLowerCase() === 'true' || val === '1';
247
- }
248
-
249
- public getAll(): Record<string, string> {
250
- return { ...this.configMap };
251
- }
252
- }