replica-failover-mongodb-ts 3.0.3 → 3.0.8

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.
@@ -20,24 +20,34 @@ const websocket_1 = require("./websocket");
20
20
  const metrics_1 = require("./metrics");
21
21
  class ConnectionManager {
22
22
  constructor(opts) {
23
- var _a;
23
+ var _a, _b, _c;
24
24
  this.primaryClient = null;
25
25
  this.primaryDb = null;
26
+ this.secondaryClients = [];
27
+ this.rrIndex = 0;
26
28
  this.nodes = [];
27
29
  this.replicaUri = opts.replicaUri;
28
30
  this.nodes = (opts.nodes || []).map((u, i) => ({ uri: u, name: `node${i + 1}` }));
29
31
  this.dbName = opts.dbName || 'node-balancer';
30
32
  this.healthCheckIntervalMs = (_a = opts.healthCheckIntervalMs) !== null && _a !== void 0 ? _a : 5000;
31
33
  this.webhookUrl = opts.webhookUrl;
34
+ this.maxPoolSize = (_b = opts.maxPoolSize) !== null && _b !== void 0 ? _b : 20;
35
+ this.minPoolSize = (_c = opts.minPoolSize) !== null && _c !== void 0 ? _c : 1;
32
36
  }
33
37
  init() {
34
38
  return __awaiter(this, void 0, void 0, function* () {
35
39
  logger_1.logger.info('ConnectionManager: init() starting');
36
- // First try replica uri if provided (let driver handle replica set)
40
+ // 1. Try Replica URI
37
41
  if (this.replicaUri) {
38
42
  logger_1.logger.info(`Trying replica URI: ${this.replicaUri}`);
39
43
  try {
40
- const c = new mongodb_1.MongoClient(this.replicaUri, { monitorCommands: true });
44
+ const c = new mongodb_1.MongoClient(this.replicaUri, {
45
+ monitorCommands: true,
46
+ minPoolSize: this.minPoolSize,
47
+ maxPoolSize: this.maxPoolSize
48
+ });
49
+ // Attach events BEFORE connecting to capture initial pool creation (optional but good)
50
+ this.attachPoolMonitor(c, 'primary', 'replica-uri');
41
51
  yield c.connect();
42
52
  const isWritable = yield this.checkWritable(c);
43
53
  if (isWritable) {
@@ -55,38 +65,73 @@ class ConnectionManager {
55
65
  logger_1.logger.warn(`Replica URI connect failed: ${err.message}`);
56
66
  }
57
67
  }
58
- // Fallback: iterate nodes and find a writable node
68
+ // 2. Fallback: Multi-node Manual Connection
69
+ logger_1.logger.info('Initializing multi-node connection...');
70
+ let connectedCount = 0;
59
71
  for (const n of this.nodes) {
60
- logger_1.logger.info(`Trying node ${n.uri}`);
72
+ logger_1.logger.info(`Connecting to node ${n.uri}`);
61
73
  try {
62
- const c = new mongodb_1.MongoClient(n.uri, { directConnection: true, monitorCommands: true });
74
+ const c = new mongodb_1.MongoClient(n.uri, {
75
+ directConnection: true,
76
+ monitorCommands: true,
77
+ minPoolSize: this.minPoolSize,
78
+ maxPoolSize: this.maxPoolSize
79
+ });
80
+ this.attachPoolMonitor(c, 'unknown', n.uri); // Type unknown until verified
63
81
  yield c.connect();
82
+ connectedCount++;
64
83
  const writable = yield this.checkWritable(c);
65
- if (writable) {
66
- logger_1.logger.info(`Found writable node at ${n.uri}`);
84
+ if (writable && !this.primaryClient) {
85
+ logger_1.logger.info(`Found Primary node at ${n.uri}`);
67
86
  this.attachClient(c);
68
- this.startHealthChecks();
69
- return;
87
+ // Re-tag metrics if needed?
88
+ // Metrics are tagged by 'node' URI, so type label 'unknown' might be set once.
89
+ // Ideally we update the label but exposed gauges don't support re-labeling easily without removing.
90
+ // For now, node URI is the main identifier.
70
91
  }
71
92
  else {
72
- yield c.close();
93
+ logger_1.logger.info(`Connected to Secondary node at ${n.uri}`);
94
+ this.secondaryClients.push(c);
73
95
  }
74
96
  }
75
97
  catch (err) {
76
98
  logger_1.logger.warn(`Node connect failed ${n.uri}: ${err.message}`);
77
99
  }
78
100
  }
79
- throw new Error('No writable MongoDB node found. Check your nodes/replica set.');
101
+ if (connectedCount === 0) {
102
+ throw new Error('No available MongoDB nodes found. Check your cluster.');
103
+ }
104
+ if (!this.primaryClient) {
105
+ logger_1.logger.warn('Initialized without a Primary node! System in Read-Only mode until a node becomes writable.');
106
+ }
107
+ this.startHealthChecks();
108
+ });
109
+ }
110
+ attachPoolMonitor(client, type, nodeUri) {
111
+ const label = { type, node: nodeUri };
112
+ client.on('connectionCreated', () => metrics_1.poolSize.inc(label));
113
+ client.on('connectionClosed', () => metrics_1.poolSize.dec(label));
114
+ client.on('connectionCheckedOut', () => {
115
+ metrics_1.poolCheckedOut.inc(label);
116
+ });
117
+ client.on('connectionCheckedIn', () => {
118
+ metrics_1.poolCheckedOut.dec(label);
80
119
  });
120
+ // 'connectionCheckOutStarted' indicates a request entered the queue (or is about to grab one)
121
+ // 'connectionCheckOutFailed' indicates it failed to get one (timeout)
122
+ // We can use these to track queue depth approximately.
123
+ client.on('connectionCheckOutStarted', () => metrics_1.poolWaitQueue.inc(label));
124
+ client.on('connectionCheckOutFailed', () => metrics_1.poolWaitQueue.dec(label));
125
+ // When successfully checked out, it also leaves the queue
126
+ client.on('connectionCheckedOut', () => metrics_1.poolWaitQueue.dec(label));
81
127
  }
82
128
  attachClient(client) {
83
129
  this.primaryClient = client;
84
130
  this.primaryDb = client.db(this.dbName);
85
- // register monitoring events on the client
86
131
  client.on('topologyDescriptionChanged', (td) => {
87
132
  var _a;
88
- logger_1.logger.info(`topologyDescriptionChanged: ${JSON.stringify(this.summarizeTopology(td))}`);
89
133
  const summary = this.summarizeTopology(td);
134
+ // logger.info(`topologyDescriptionChanged: ${JSON.stringify(summary)}`);
90
135
  this.recordEvent('topologyDescriptionChanged', { td: summary }).catch(() => { });
91
136
  (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('topology-change', summary);
92
137
  });
@@ -96,7 +141,7 @@ class ConnectionManager {
96
141
  this.sendAlert('serverHeartbeatFailed', event).catch(() => { });
97
142
  });
98
143
  client.on('serverHeartbeatSucceeded', (event) => {
99
- logger_1.logger.debug(`serverHeartbeatSucceeded: ${JSON.stringify(event)}`);
144
+ // verbose
100
145
  });
101
146
  client.on('close', () => {
102
147
  logger_1.logger.warn('MongoClient close event');
@@ -106,7 +151,6 @@ class ConnectionManager {
106
151
  metrics_1.connectionStatus.set(1);
107
152
  }
108
153
  summarizeTopology(td) {
109
- // gentle summary - driver topologyDescription shape may vary
110
154
  try {
111
155
  return {
112
156
  servers: Object.keys(td.servers || {}).map((k) => ({
@@ -123,15 +167,14 @@ class ConnectionManager {
123
167
  return __awaiter(this, void 0, void 0, function* () {
124
168
  try {
125
169
  const admin = client.db('admin');
126
- // isWritablePrimary command is supported
127
170
  const res = yield admin.command({ isWritablePrimary: 1 }).catch(() => null);
128
171
  if (res && res.isWritablePrimary)
129
172
  return true;
130
- // fallback: isMaster / hello
131
173
  const info = yield admin.command({ hello: 1 }).catch(() => null);
132
- if (info && (info.isWritablePrimary || info.isWritablePrimary === true))
174
+ if (info && (info.isWritablePrimary ||
175
+ info.isWritablePrimary === true ||
176
+ info.ismaster === true))
133
177
  return true;
134
- // if driver can't tell, assume writable if connected and write to a temp collection test (careful)
135
178
  return false;
136
179
  }
137
180
  catch (err) {
@@ -144,19 +187,14 @@ class ConnectionManager {
144
187
  return __awaiter(this, void 0, void 0, function* () {
145
188
  var _a;
146
189
  try {
147
- const event = {
148
- ts: new Date(),
149
- level: 'event',
150
- type,
151
- payload,
152
- };
190
+ const event = { ts: new Date(), level: 'event', type, payload };
153
191
  (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', event);
154
192
  if (!this.primaryDb)
155
193
  return;
156
194
  yield this.primaryDb.collection('logs').insertOne(event);
157
195
  }
158
196
  catch (err) {
159
- logger_1.logger.warn('recordEvent failed: ' + err.message);
197
+ // logger.warn('recordEvent failed: ' + (err as Error).message);
160
198
  }
161
199
  });
162
200
  }
@@ -182,70 +220,108 @@ class ConnectionManager {
182
220
  }
183
221
  healthCheckLoop() {
184
222
  return __awaiter(this, void 0, void 0, function* () {
185
- // If primary still writable -> do nothing
223
+ // 1. Check Primary
186
224
  if (this.primaryClient) {
187
225
  const ok = yield this.checkWritable(this.primaryClient).catch(() => false);
188
- if (ok)
189
- return;
190
- logger_1.logger.warn('Primary client no longer writable. Will search for a writable node.');
191
- try {
192
- yield this.primaryClient.close();
226
+ if (!ok) {
227
+ logger_1.logger.warn('Primary client no longer writable. Demoting to potential secondary.');
228
+ this.secondaryClients.push(this.primaryClient);
229
+ this.primaryClient = null;
230
+ this.primaryDb = null;
231
+ metrics_1.connectionStatus.set(0);
193
232
  }
194
- catch (_a) { }
195
- this.primaryClient = null;
196
- this.primaryDb = null;
197
- metrics_1.connectionStatus.set(0);
198
233
  }
199
- // attempt to find a writable node among nodes (directConnection)
200
- for (const n of this.nodes) {
234
+ // 2. Check Secondaries
235
+ for (let i = this.secondaryClients.length - 1; i >= 0; i--) {
236
+ const sec = this.secondaryClients[i];
201
237
  try {
202
- const c = new mongodb_1.MongoClient(n.uri, { directConnection: true, monitorCommands: true });
203
- yield c.connect();
204
- const writable = yield this.checkWritable(c);
205
- if (writable) {
206
- logger_1.logger.info(`Health-check: promoted ${n.uri} to primary connection`);
207
- this.attachClient(c);
208
- yield this.recordEvent('promote', { node: n.uri });
209
- yield this.sendAlert('promote', { node: n.uri, message: 'Promoted new primary connection' });
210
- metrics_1.failoverCount.inc();
211
- return;
212
- }
213
- else {
214
- yield c.close();
215
- }
238
+ yield sec.db('admin').command({ ping: 1 });
216
239
  }
217
240
  catch (err) {
218
- logger_1.logger.debug(`Health-check connect failed ${n.uri}: ${err.message}`);
241
+ logger_1.logger.warn('Secondary node lost connection. Removing.');
242
+ try {
243
+ yield sec.close();
244
+ }
245
+ catch (_a) { }
246
+ this.secondaryClients.splice(i, 1);
247
+ }
248
+ }
249
+ // 3. Promote if needed
250
+ if (!this.primaryClient) {
251
+ // logger.warn('No Primary! Searching among secondaries...');
252
+ for (let i = 0; i < this.secondaryClients.length; i++) {
253
+ const client = this.secondaryClients[i];
254
+ const isWritable = yield this.checkWritable(client);
255
+ if (isWritable) {
256
+ logger_1.logger.info('Promoting secondary to Primary!');
257
+ this.attachClient(client);
258
+ this.secondaryClients.splice(i, 1);
259
+ yield this.recordEvent('promote', { message: 'Promoted secondary to primary' });
260
+ yield this.sendAlert('promote', { message: 'Promoted new primary connection' });
261
+ metrics_1.failoverCount.inc();
262
+ break;
263
+ }
219
264
  }
220
265
  }
221
- // no writable found
222
- logger_1.logger.error('Health-check: no writable nodes found.');
223
- yield this.recordEvent('no-writable', {});
224
- yield this.sendAlert('no-writable', { message: 'CRITICAL: No writable nodes found in cluster!' });
225
266
  });
226
267
  }
268
+ getSecondary() {
269
+ if (this.secondaryClients.length === 0)
270
+ return null;
271
+ const c = this.secondaryClients[this.rrIndex % this.secondaryClients.length];
272
+ this.rrIndex++;
273
+ return c;
274
+ }
227
275
  getDb() {
228
276
  return this.primaryDb;
229
277
  }
230
- // Generic wrappers that log operations to collection 'logs'
231
278
  read(collectionName_1, op_1) {
232
- return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}) {
233
- const db = this.getDb();
279
+ return __awaiter(this, arguments, void 0, function* (collectionName, op, meta = {}, readPref = 'primary') {
280
+ let clientToUse = this.primaryClient;
281
+ let effectivePref = readPref;
282
+ if (readPref === 'secondary') {
283
+ const sec = this.getSecondary();
284
+ if (sec) {
285
+ clientToUse = sec;
286
+ }
287
+ else {
288
+ throw new Error('No secondary node available for read preference "secondary"');
289
+ }
290
+ }
291
+ else if (readPref === 'secondaryPreferred') {
292
+ const sec = this.getSecondary();
293
+ if (sec) {
294
+ clientToUse = sec;
295
+ effectivePref = 'secondary';
296
+ }
297
+ else {
298
+ clientToUse = this.primaryClient;
299
+ effectivePref = 'primary';
300
+ }
301
+ }
302
+ if (!clientToUse) {
303
+ throw new Error('No active connection available for requested read preference');
304
+ }
305
+ const db = clientToUse.db(this.dbName);
234
306
  const start = Date.now();
235
307
  try {
236
- if (!db)
237
- throw new Error('No DB connection');
238
- const res = yield op(db.collection(collectionName));
308
+ const collection = db.collection(collectionName);
309
+ const res = yield op(collection);
239
310
  const took = Date.now() - start;
240
311
  yield this.safeLog({
241
312
  ts: new Date(),
242
313
  op: 'read',
243
314
  collection: collectionName,
244
315
  success: true,
245
- meta,
316
+ meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
246
317
  durationMs: took,
247
318
  });
248
- metrics_1.operationDuration.observe({ operation: 'read', collection: collectionName, success: 'true' }, took / 1000);
319
+ metrics_1.operationDuration.observe({
320
+ operation: 'read',
321
+ collection: collectionName,
322
+ success: 'true',
323
+ read_preference: effectivePref
324
+ }, took / 1000);
249
325
  return res;
250
326
  }
251
327
  catch (err) {
@@ -256,10 +332,15 @@ class ConnectionManager {
256
332
  collection: collectionName,
257
333
  success: false,
258
334
  error: err.message,
259
- meta,
335
+ meta: Object.assign(Object.assign({}, meta), { readPref: effectivePref }),
260
336
  durationMs: took,
261
337
  });
262
- metrics_1.operationDuration.observe({ operation: 'read', collection: collectionName, success: 'false' }, took / 1000);
338
+ metrics_1.operationDuration.observe({
339
+ operation: 'read',
340
+ collection: collectionName,
341
+ success: 'false',
342
+ read_preference: effectivePref
343
+ }, took / 1000);
263
344
  throw err;
264
345
  }
265
346
  });
@@ -306,14 +387,13 @@ class ConnectionManager {
306
387
  try {
307
388
  (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', doc);
308
389
  if (!this.primaryDb) {
309
- logger_1.logger.warn('safeLog: no primaryDb, skipping db log. Logging to console instead.');
310
- logger_1.logger.info(JSON.stringify(doc));
390
+ // logger.warn('safeLog: no primaryDb, skipping db log.');
311
391
  return;
312
392
  }
313
393
  yield this.primaryDb.collection('logs').insertOne(doc);
314
394
  }
315
395
  catch (err) {
316
- logger_1.logger.warn('safeLog failed: ' + err.message);
396
+ // silent fail for log
317
397
  }
318
398
  });
319
399
  }
@@ -321,11 +401,13 @@ class ConnectionManager {
321
401
  return __awaiter(this, void 0, void 0, function* () {
322
402
  if (this.healthInterval)
323
403
  clearInterval(this.healthInterval);
324
- if (this.primaryClient) {
404
+ if (this.primaryClient)
325
405
  yield this.primaryClient.close().catch(() => { });
326
- this.primaryClient = null;
327
- this.primaryDb = null;
328
- }
406
+ for (const c of this.secondaryClients)
407
+ yield c.close().catch(() => { });
408
+ this.primaryClient = null;
409
+ this.primaryDb = null;
410
+ this.secondaryClients = [];
329
411
  });
330
412
  }
331
413
  }
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
6
+ exports.poolWaitQueue = exports.poolCheckedOut = exports.poolSize = exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
7
7
  const prom_client_1 = __importDefault(require("prom-client"));
8
8
  // Create a Registry
9
9
  exports.register = new prom_client_1.default.Registry();
@@ -23,7 +23,25 @@ exports.failoverCount = new prom_client_1.default.Counter({
23
23
  exports.operationDuration = new prom_client_1.default.Histogram({
24
24
  name: 'node_balancer_operation_duration_seconds',
25
25
  help: 'Duration of database operations in seconds',
26
- labelNames: ['operation', 'collection', 'success'],
26
+ labelNames: ['operation', 'collection', 'success', 'read_preference'],
27
27
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
28
28
  registers: [exports.register]
29
29
  });
30
+ exports.poolSize = new prom_client_1.default.Gauge({
31
+ name: 'node_balancer_pool_size',
32
+ help: 'Current size of the connection pool',
33
+ labelNames: ['type', 'node'],
34
+ registers: [exports.register]
35
+ });
36
+ exports.poolCheckedOut = new prom_client_1.default.Gauge({
37
+ name: 'node_balancer_pool_checked_out',
38
+ help: 'Number of connections currently checked out',
39
+ labelNames: ['type', 'node'],
40
+ registers: [exports.register]
41
+ });
42
+ exports.poolWaitQueue = new prom_client_1.default.Gauge({
43
+ name: 'node_balancer_pool_wait_queue',
44
+ help: 'Number of requests waiting for a connection',
45
+ labelNames: ['type', 'node'],
46
+ registers: [exports.register]
47
+ });
package/dist/index.js CHANGED
@@ -1,5 +1,20 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
17
  exports.ConnectionManager = void 0;
4
18
  var connectionManager_1 = require("./config/connectionManager");
5
19
  Object.defineProperty(exports, "ConnectionManager", { enumerable: true, get: function () { return connectionManager_1.ConnectionManager; } });
20
+ __exportStar(require("./nestjs"), exports);
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./node-balancer.module"), exports);
18
+ __exportStar(require("./node-balancer.decorators"), exports);
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NODE_BALANCER_CONNECTION = void 0;
4
+ exports.InjectConnectionManager = InjectConnectionManager;
5
+ const common_1 = require("@nestjs/common");
6
+ exports.NODE_BALANCER_CONNECTION = 'NODE_BALANCER_CONNECTION';
7
+ function InjectConnectionManager() {
8
+ return (0, common_1.Inject)(exports.NODE_BALANCER_CONNECTION);
9
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var NodeBalancerModule_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.NodeBalancerModule = void 0;
20
+ const common_1 = require("@nestjs/common");
21
+ const connectionManager_1 = require("../config/connectionManager");
22
+ const node_balancer_decorators_1 = require("./node-balancer.decorators");
23
+ let NodeBalancerModule = NodeBalancerModule_1 = class NodeBalancerModule {
24
+ static forRoot(options) {
25
+ const connectionProvider = {
26
+ provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
27
+ useFactory: () => __awaiter(this, void 0, void 0, function* () {
28
+ console.log('DEBUG: NodeBalancerModule useFactory called');
29
+ const manager = new connectionManager_1.ConnectionManager(options);
30
+ console.log('DEBUG: Manager created, initializing...');
31
+ yield manager.init();
32
+ console.log('DEBUG: Manager initialized successfully');
33
+ return manager;
34
+ }),
35
+ };
36
+ return {
37
+ module: NodeBalancerModule_1,
38
+ providers: [connectionProvider],
39
+ exports: [connectionProvider],
40
+ };
41
+ }
42
+ static forRootAsync(options) {
43
+ const connectionProvider = {
44
+ provide: node_balancer_decorators_1.NODE_BALANCER_CONNECTION,
45
+ useFactory: (...args) => __awaiter(this, void 0, void 0, function* () {
46
+ const config = yield options.useFactory(...args);
47
+ const manager = new connectionManager_1.ConnectionManager(config);
48
+ yield manager.init();
49
+ return manager;
50
+ }),
51
+ inject: options.inject || [],
52
+ };
53
+ return {
54
+ module: NodeBalancerModule_1,
55
+ imports: options.imports || [],
56
+ providers: [connectionProvider],
57
+ exports: [connectionProvider],
58
+ };
59
+ }
60
+ };
61
+ exports.NodeBalancerModule = NodeBalancerModule;
62
+ exports.NodeBalancerModule = NodeBalancerModule = NodeBalancerModule_1 = __decorate([
63
+ (0, common_1.Global)(),
64
+ (0, common_1.Module)({})
65
+ ], NodeBalancerModule);
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
15
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
16
+ return new (P || (P = Promise))(function (resolve, reject) {
17
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
18
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
19
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
20
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
21
+ });
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ require("reflect-metadata");
25
+ const common_1 = require("@nestjs/common");
26
+ const core_1 = require("@nestjs/core");
27
+ const node_balancer_module_1 = require("../nestjs/node-balancer.module");
28
+ const node_balancer_decorators_1 = require("../nestjs/node-balancer.decorators");
29
+ const connectionManager_1 = require("../config/connectionManager");
30
+ process.on('unhandledRejection', (reason, p) => {
31
+ console.error('Unhandled Rejection at:', p, 'reason:', reason);
32
+ process.exit(1);
33
+ });
34
+ process.on('uncaughtException', (err) => {
35
+ console.error('Uncaught Exception:', err);
36
+ process.exit(1);
37
+ });
38
+ let AppService = class AppService {
39
+ constructor(connectionManager) {
40
+ this.connectionManager = connectionManager;
41
+ }
42
+ check() {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ if (this.connectionManager) {
45
+ console.log('✅ ConnectionManager injected successfully!');
46
+ const db = this.connectionManager.getDb();
47
+ console.log(`✅ Database context available: ${db ? 'Yes' : 'No (init might be pending or failed)'}`);
48
+ return 'ok';
49
+ }
50
+ else {
51
+ console.error('❌ ConnectionManager failed to inject.');
52
+ return 'fail';
53
+ }
54
+ });
55
+ }
56
+ };
57
+ AppService = __decorate([
58
+ (0, common_1.Injectable)(),
59
+ __param(0, (0, node_balancer_decorators_1.InjectConnectionManager)()),
60
+ __metadata("design:paramtypes", [connectionManager_1.ConnectionManager])
61
+ ], AppService);
62
+ let AppController = class AppController {
63
+ constructor(appService) {
64
+ this.appService = appService;
65
+ }
66
+ getHello() {
67
+ return this.appService.check();
68
+ }
69
+ };
70
+ __decorate([
71
+ (0, common_1.Get)(),
72
+ __metadata("design:type", Function),
73
+ __metadata("design:paramtypes", []),
74
+ __metadata("design:returntype", void 0)
75
+ ], AppController.prototype, "getHello", null);
76
+ AppController = __decorate([
77
+ (0, common_1.Controller)(),
78
+ __metadata("design:paramtypes", [AppService])
79
+ ], AppController);
80
+ let AppModule = class AppModule {
81
+ };
82
+ AppModule = __decorate([
83
+ (0, common_1.Module)({
84
+ imports: [
85
+ node_balancer_module_1.NodeBalancerModule.forRoot({
86
+ nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
87
+ minPoolSize: 1,
88
+ maxPoolSize: 5,
89
+ dbName: 'test_nest'
90
+ })
91
+ ],
92
+ controllers: [AppController],
93
+ providers: [AppService],
94
+ })
95
+ ], AppModule);
96
+ function manualTest() {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ console.log('DEBUG: Starting manual test...');
99
+ try {
100
+ const m = new connectionManager_1.ConnectionManager({
101
+ nodes: ['mongodb://localhost:27017', 'mongodb://localhost:27018', 'mongodb://localhost:27019'],
102
+ minPoolSize: 1,
103
+ maxPoolSize: 5
104
+ });
105
+ yield m.init();
106
+ console.log('DEBUG: Manual test passed');
107
+ // await m.close(); // Keep it open or close? Close.
108
+ }
109
+ catch (e) {
110
+ console.error('DEBUG: Manual test failed', e);
111
+ }
112
+ });
113
+ }
114
+ function bootstrap() {
115
+ return __awaiter(this, void 0, void 0, function* () {
116
+ console.log('🚀 Starting NestJS Context...');
117
+ yield manualTest();
118
+ try {
119
+ const app = yield core_1.NestFactory.createApplicationContext(AppModule, { logger: ['error', 'warn', 'debug', 'verbose'] });
120
+ const service = app.get(AppService);
121
+ yield service.check();
122
+ yield app.close();
123
+ console.log('✅ NestJS Test Finished.');
124
+ }
125
+ catch (error) {
126
+ console.error('❌ NestJS Boot failed:', error);
127
+ process.exit(1);
128
+ }
129
+ });
130
+ }
131
+ bootstrap();
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const connectionManager_1 = require("../config/connectionManager");
16
+ const dotenv_1 = __importDefault(require("dotenv"));
17
+ dotenv_1.default.config({ path: '.env.local' });
18
+ function run() {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ const envNodes = process.env.MONGODB_NODES || '';
21
+ const nodes = envNodes ? envNodes.split(',') : [
22
+ 'mongodb://localhost:27017',
23
+ 'mongodb://localhost:27018',
24
+ 'mongodb://localhost:27019'
25
+ ];
26
+ console.log('--- Testing Read Preference and Multi-Node support ---');
27
+ console.log(`Nodes: ${nodes.join(', ')}`);
28
+ const db = new connectionManager_1.ConnectionManager({
29
+ nodes,
30
+ healthCheckIntervalMs: 2000,
31
+ maxPoolSize: 5,
32
+ minPoolSize: 1
33
+ });
34
+ try {
35
+ yield db.init();
36
+ console.log('✅ Initialization complete.');
37
+ // Test Write (Primary)
38
+ console.log('📝 Testing Write (Primary)...');
39
+ try {
40
+ yield db.write('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
41
+ yield col.insertOne({ test: 'read_pref', date: new Date() });
42
+ }));
43
+ console.log('✅ Write successful.');
44
+ }
45
+ catch (err) {
46
+ console.warn('⚠️ Write failed (No Primary?), skipping to read tests... Error:', err.message);
47
+ }
48
+ // Test Read (Primary default)
49
+ console.log('📖 Testing Read (Primary Default)...');
50
+ try {
51
+ const res1 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
52
+ return col.findOne({ test: 'read_pref' });
53
+ }));
54
+ console.log('✅ Read Primary result:', res1 === null || res1 === void 0 ? void 0 : res1._id);
55
+ }
56
+ catch (err) {
57
+ console.warn('⚠️ Read Primary failed (No Primary?), skipping... Error:', err.message);
58
+ }
59
+ // Test Read (Secondary Strict)
60
+ console.log('📖 Testing Read (Secondary Strict)...');
61
+ try {
62
+ const res2 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
63
+ return col.findOne({ test: 'read_pref' });
64
+ }), {}, 'secondary'); // <--- requesting secondary
65
+ console.log('✅ Read Secondary result:', res2 === null || res2 === void 0 ? void 0 : res2._id);
66
+ }
67
+ catch (err) {
68
+ console.error('❌ Read Secondary failed:', err.message);
69
+ }
70
+ // Test Read (Secondary Preferred)
71
+ console.log('📖 Testing Read (Secondary Preferred)...');
72
+ const res3 = yield db.read('test_reads', (col) => __awaiter(this, void 0, void 0, function* () {
73
+ return col.findOne({ test: 'read_pref' });
74
+ }), {}, 'secondaryPreferred');
75
+ console.log('✅ Read SecondaryPreferred result:', res3 === null || res3 === void 0 ? void 0 : res3._id);
76
+ }
77
+ catch (err) {
78
+ console.error('❌ Test failed:', err);
79
+ }
80
+ finally {
81
+ yield db.close();
82
+ console.log('👋 Connection closed.');
83
+ process.exit(0);
84
+ }
85
+ });
86
+ }
87
+ run();
package/dist/server.js CHANGED
@@ -23,7 +23,8 @@ app_1.default.get('/metrics', (req, res) => __awaiter(void 0, void 0, void 0, fu
23
23
  res.end(yield metrics_1.register.metrics());
24
24
  }
25
25
  catch (ex) {
26
- res.status(500).end(ex);
26
+ logger_1.logger.error('Error while serving /metrics endpoint', ex);
27
+ res.status(500).end('Internal server error');
27
28
  }
28
29
  }));
29
30
  const server = app_1.default.listen(PORT, () => {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "replica-failover-mongodb-ts",
3
- "version": "3.0.3",
3
+ "version": "3.0.8",
4
4
  "description": "O sistema replica um dos conceitos básicos da arquitetura de sistemas distribuído, na qual ele tem uma API em express.js com TypeScript, e usando o mongoDB com replicaSET e failover, consegue automáticamente garantir disponibilidade e redundância de dados das requisições.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
+ "test": "node dist/scripts/test_read_pref.js",
8
9
  "dev": "nodemon --exec ts-node src/server.ts",
9
10
  "build": "tsc",
10
11
  "start": "node dist/server.js",
@@ -29,6 +30,11 @@
29
30
  "keywords": [],
30
31
  "author": "",
31
32
  "license": "ISC",
33
+ "peerDependencies": {
34
+ "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
35
+ "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
36
+ "rxjs": "^7.0.0"
37
+ },
32
38
  "dependencies": {
33
39
  "@types/blessed": "^0.1.26",
34
40
  "@types/inquirer": "^9.0.9",
@@ -46,11 +52,16 @@
46
52
  "winston": "^3.18.3"
47
53
  },
48
54
  "devDependencies": {
55
+ "@nestjs/common": "^11.1.13",
56
+ "@nestjs/core": "^11.1.13",
57
+ "@nestjs/platform-express": "^11.1.13",
49
58
  "@types/express": "^5.0.3",
50
59
  "@types/morgan": "^1.9.10",
51
60
  "@types/node": "^24.8.0",
52
61
  "nodemon": "^3.1.10",
62
+ "reflect-metadata": "^0.2.2",
63
+ "rxjs": "^7.8.2",
53
64
  "ts-node": "^10.9.2",
54
- "typescript": "^5.9.3"
65
+ "typescript": "^5.6.3"
55
66
  }
56
67
  }