ts-server-lib 0.0.17

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/LICENSE ADDED
@@ -0,0 +1 @@
1
+ This is not free
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # ts-server-lib
2
+ Server-side collection
3
+
4
+
5
+ ## Clean solutions
6
+ ```bash
7
+ rm -rf {db,ussd,ussd/providers,utils,ws}/*.{d.ts,js}
8
+ ```
@@ -0,0 +1,103 @@
1
+ /**
2
+ * TSMongo – MongoDB connection (pooling, health, index registration).
3
+ * Static API like TSRedis. Use TSMongo.connect(), TSMongo.getDatabase(), etc.
4
+ */
5
+ import { MongoClient, Db } from 'mongodb';
6
+ export { MongoClient, Db, ObjectId, ReadPreference, type Collection, type Filter, type Document, type FindOptions, type ClientSession, type TransactionOptions, type CommandStartedEvent, type CommandSucceededEvent, type CommandFailedEvent, type ReadPreferenceLike, type UpdateFilter, type Sort, type IndexDescription, } from 'mongodb';
7
+ export type MongoEventLevel = 'error' | 'warn' | 'info';
8
+ /**
9
+ * Structured event emitted by TSMongo for significant pool lifecycle moments.
10
+ *
11
+ * Wire a handler once at service startup via `TSMongo.setEventHandler()` to route
12
+ * these events into your structured logger with correlation context.
13
+ *
14
+ * When no handler is registered, events fall back to `console.error` / `console.warn`
15
+ * using the `TSMongo:ERROR` / `TSMongo:WARN` prefix convention.
16
+ */
17
+ export interface MongoEvent {
18
+ level: MongoEventLevel;
19
+ /** Low-cardinality dot-namespaced event identifier. */
20
+ event: string;
21
+ message: string;
22
+ timestamp: string;
23
+ data: Record<string, unknown>;
24
+ }
25
+ export type MongoEventHandler = (event: MongoEvent) => void;
26
+ /**
27
+ * Register a structured event handler for TSMongo lifecycle events.
28
+ *
29
+ * Call once at service startup before `TSMongo.connect()`. Pass `null` to
30
+ * remove a previously registered handler and revert to console fallback.
31
+ *
32
+ * @example
33
+ * TSMongo.setEventHandler((evt) => {
34
+ * logger[evt.level]('TSMongo event', { operation: 'core.database', event: evt.event, ...evt.data });
35
+ * });
36
+ */
37
+ declare function setEventHandlerImpl(handler: MongoEventHandler | null): void;
38
+ export interface ConnectionPoolStats {
39
+ totalConnections: number;
40
+ checkedOut: number;
41
+ availableConnections: number;
42
+ waitQueueSize: number;
43
+ maxPoolSize: number;
44
+ minPoolSize: number;
45
+ totalCheckouts: number;
46
+ totalCheckins: number;
47
+ connectionCreated: number;
48
+ connectionClosed: number;
49
+ waitQueueTimeouts: number;
50
+ lastWaitQueueTimeout: Date | null;
51
+ }
52
+ export interface MongoConfig {
53
+ uri: string;
54
+ dbName?: string;
55
+ maxPoolSize?: number;
56
+ minPoolSize?: number;
57
+ maxIdleTimeMS?: number;
58
+ waitQueueTimeoutMS?: number;
59
+ maxWaitingRequests?: number;
60
+ connectTimeoutMS?: number;
61
+ socketTimeoutMS?: number;
62
+ serverSelectionTimeoutMS?: number;
63
+ readPreference?: 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest';
64
+ writeConcern?: 'majority' | number;
65
+ retryWrites?: boolean;
66
+ retryReads?: boolean;
67
+ compressors?: ('snappy' | 'zlib' | 'zstd')[];
68
+ monitorCommands?: boolean;
69
+ }
70
+ export declare const DEFAULT_MONGO_CONFIG: Omit<Required<MongoConfig>, 'uri' | 'dbName' | 'compressors' | 'monitorCommands'>;
71
+ declare function registerIndexesImpl(collection: string, indexes: Array<{
72
+ key: Record<string, 1 | -1>;
73
+ unique?: boolean;
74
+ }>): void;
75
+ export declare class TSMongo {
76
+ static connect: typeof connectImpl;
77
+ static getDatabase: typeof getDatabaseImpl;
78
+ static getClient: typeof getClientImpl;
79
+ static close: typeof closeImpl;
80
+ static checkHealth: typeof checkHealthImpl;
81
+ static registerIndexes: typeof registerIndexesImpl;
82
+ static getConnectionPoolStats: typeof getConnectionPoolStatsImpl;
83
+ static getPoolHealthStatus: typeof getPoolHealthStatusImpl;
84
+ static getDatabaseStats: typeof getDatabaseStatsImpl;
85
+ static setEventHandler: typeof setEventHandlerImpl;
86
+ }
87
+ declare function connectImpl(uri: string, config?: Partial<MongoConfig>): Promise<Db>;
88
+ declare function getDatabaseImpl(): Db;
89
+ declare function getClientImpl(): MongoClient;
90
+ declare function closeImpl(): Promise<void>;
91
+ declare function getConnectionPoolStatsImpl(): ConnectionPoolStats;
92
+ declare function getPoolHealthStatusImpl(): {
93
+ status: 'healthy' | 'warning' | 'critical';
94
+ utilizationPercent: number;
95
+ message: string;
96
+ };
97
+ declare function checkHealthImpl(): Promise<{
98
+ healthy: boolean;
99
+ latencyMs: number;
100
+ connections: number;
101
+ checkedOut: number;
102
+ }>;
103
+ declare function getDatabaseStatsImpl(): Promise<Record<string, unknown>>;
package/db/TSMongo.js ADDED
@@ -0,0 +1,483 @@
1
+ "use strict";
2
+ /**
3
+ * TSMongo – MongoDB connection (pooling, health, index registration).
4
+ * Static API like TSRedis. Use TSMongo.connect(), TSMongo.getDatabase(), etc.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TSMongo = exports.DEFAULT_MONGO_CONFIG = exports.ReadPreference = exports.ObjectId = exports.Db = exports.MongoClient = void 0;
8
+ const mongodb_1 = require("mongodb");
9
+ // Re-export MongoDB types so consumers (core-service, microservices) import from
10
+ // 'ts-server-lib/db/TSMongo.js' instead of 'mongodb' directly — single resolution
11
+ // path, no duplicate mongodb installs across the monorepo.
12
+ var mongodb_2 = require("mongodb");
13
+ Object.defineProperty(exports, "MongoClient", { enumerable: true, get: function () { return mongodb_2.MongoClient; } });
14
+ Object.defineProperty(exports, "Db", { enumerable: true, get: function () { return mongodb_2.Db; } });
15
+ Object.defineProperty(exports, "ObjectId", { enumerable: true, get: function () { return mongodb_2.ObjectId; } });
16
+ Object.defineProperty(exports, "ReadPreference", { enumerable: true, get: function () { return mongodb_2.ReadPreference; } });
17
+ const ERR_PREFIX = 'TSMongo:ERROR';
18
+ const WARN_PREFIX = 'TSMongo:WARN';
19
+ let client = null;
20
+ let db = null;
21
+ let _eventHandler = null;
22
+ /**
23
+ * Register a structured event handler for TSMongo lifecycle events.
24
+ *
25
+ * Call once at service startup before `TSMongo.connect()`. Pass `null` to
26
+ * remove a previously registered handler and revert to console fallback.
27
+ *
28
+ * @example
29
+ * TSMongo.setEventHandler((evt) => {
30
+ * logger[evt.level]('TSMongo event', { operation: 'core.database', event: evt.event, ...evt.data });
31
+ * });
32
+ */
33
+ function setEventHandlerImpl(handler) {
34
+ _eventHandler = handler;
35
+ }
36
+ function emitEvent(evt) {
37
+ if (_eventHandler) {
38
+ _eventHandler(evt);
39
+ return;
40
+ }
41
+ const prefix = evt.level === 'error' ? ERR_PREFIX : WARN_PREFIX;
42
+ const logFn = evt.level === 'error' ? console.error : console.warn;
43
+ logFn(prefix, `[${evt.event}]`, evt.message, evt.data);
44
+ }
45
+ /** Tracks the highest utilization threshold currently crossed; prevents repeated emissions. */
46
+ let _lastUtilizationThreshold = 0;
47
+ // Queue-depth threshold tracking — fires EARLIER than utilization-based warnings under
48
+ // the real failure mode (queue overflow with pool at moderate utilization). Hysteresis:
49
+ // "50" = waitQueueSize >= maxPoolSize * 0.5 (warning); "100" = >= maxPoolSize (critical).
50
+ let _lastQueueThreshold = 0;
51
+ let connectionPoolStats = {
52
+ totalConnections: 0,
53
+ checkedOut: 0,
54
+ availableConnections: 0,
55
+ waitQueueSize: 0,
56
+ maxPoolSize: 200,
57
+ minPoolSize: 10,
58
+ totalCheckouts: 0,
59
+ totalCheckins: 0,
60
+ connectionCreated: 0,
61
+ connectionClosed: 0,
62
+ waitQueueTimeouts: 0,
63
+ lastWaitQueueTimeout: null,
64
+ };
65
+ exports.DEFAULT_MONGO_CONFIG = {
66
+ maxPoolSize: 200,
67
+ minPoolSize: 10,
68
+ maxIdleTimeMS: 30000,
69
+ waitQueueTimeoutMS: 10000,
70
+ maxWaitingRequests: 500,
71
+ connectTimeoutMS: 10000,
72
+ socketTimeoutMS: 45000,
73
+ serverSelectionTimeoutMS: 30000,
74
+ readPreference: 'nearest',
75
+ writeConcern: 'majority',
76
+ retryWrites: true,
77
+ retryReads: true,
78
+ };
79
+ let baseUri = null;
80
+ function getServerKey(uriObj) {
81
+ return `${uriObj.protocol}//${uriObj.host}`;
82
+ }
83
+ function resetPoolStats() {
84
+ connectionPoolStats = {
85
+ ...connectionPoolStats,
86
+ totalConnections: 0,
87
+ checkedOut: 0,
88
+ availableConnections: 0,
89
+ waitQueueSize: 0,
90
+ totalCheckouts: 0,
91
+ totalCheckins: 0,
92
+ connectionCreated: 0,
93
+ connectionClosed: 0,
94
+ waitQueueTimeouts: 0,
95
+ lastWaitQueueTimeout: null,
96
+ };
97
+ _lastUtilizationThreshold = 0;
98
+ _lastQueueThreshold = 0;
99
+ }
100
+ // eslint-disable-next-line max-lines-per-function
101
+ function attachClientEventHandlers(client, _opts) {
102
+ client.on('connectionPoolClosed', () => resetPoolStats());
103
+ client.on('connectionCreated', () => {
104
+ connectionPoolStats.totalConnections++;
105
+ connectionPoolStats.connectionCreated++;
106
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
107
+ });
108
+ client.on('connectionClosed', () => {
109
+ connectionPoolStats.totalConnections = Math.max(0, connectionPoolStats.totalConnections - 1);
110
+ connectionPoolStats.connectionClosed++;
111
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
112
+ });
113
+ client.on('connectionCheckedOut', () => {
114
+ connectionPoolStats.checkedOut++;
115
+ connectionPoolStats.totalCheckouts++;
116
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
117
+ connectionPoolStats.waitQueueSize = Math.max(0, connectionPoolStats.waitQueueSize - 1);
118
+ // Hysteresis: reset queue threshold tracking when the queue drains.
119
+ // queueRatio < 0.25 → fully reset (re-warn on next ≥0.5 crossing)
120
+ // queueRatio < 0.75 with critical previously → downgrade critical→warning
121
+ if (_lastQueueThreshold > 0) {
122
+ const maxPool = connectionPoolStats.maxPoolSize;
123
+ const queueRatio = maxPool > 0 ? connectionPoolStats.waitQueueSize / maxPool : 0;
124
+ if (queueRatio < 0.25) {
125
+ _lastQueueThreshold = 0;
126
+ }
127
+ else if (_lastQueueThreshold === 100 && queueRatio < 0.75) {
128
+ _lastQueueThreshold = 50;
129
+ }
130
+ }
131
+ // Emit utilization threshold events once per crossing (hysteresis resets in checkedIn).
132
+ //
133
+ // F6a: `effectiveDemandPercent = (checkedOut + waitQueueSize) / maxPool * 100` — the real
134
+ // demand signal (can exceed 100 under bursty load). `utilizationPercent` is checkedOut-only.
135
+ const maxPool = connectionPoolStats.maxPoolSize;
136
+ const util = maxPool > 0 ? (connectionPoolStats.checkedOut / maxPool) * 100 : 0;
137
+ const effectiveDemand = maxPool > 0
138
+ ? ((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / maxPool) * 100
139
+ : 0;
140
+ const utilizationData = {
141
+ utilizationPercent: Math.round(util),
142
+ effectiveDemandPercent: Math.round(effectiveDemand),
143
+ checkedOut: connectionPoolStats.checkedOut,
144
+ maxPoolSize: maxPool,
145
+ availableConnections: connectionPoolStats.availableConnections,
146
+ waitQueueSize: connectionPoolStats.waitQueueSize,
147
+ };
148
+ if (effectiveDemand >= 95 && _lastUtilizationThreshold < 95) {
149
+ _lastUtilizationThreshold = 95;
150
+ emitEvent({
151
+ level: 'error',
152
+ event: 'pool.utilization_critical',
153
+ message: `Pool utilization critical: ${Math.round(effectiveDemand)}% effective (${Math.round(util)}% checkedOut + ${connectionPoolStats.waitQueueSize} queued)`,
154
+ timestamp: new Date().toISOString(),
155
+ data: utilizationData,
156
+ });
157
+ }
158
+ else if (effectiveDemand >= 80 && _lastUtilizationThreshold < 80) {
159
+ _lastUtilizationThreshold = 80;
160
+ emitEvent({
161
+ level: 'warn',
162
+ event: 'pool.utilization_warning',
163
+ message: `Pool utilization warning: ${Math.round(effectiveDemand)}% effective (${Math.round(util)}% checkedOut + ${connectionPoolStats.waitQueueSize} queued)`,
164
+ timestamp: new Date().toISOString(),
165
+ data: utilizationData,
166
+ });
167
+ }
168
+ });
169
+ client.on('connectionCheckedIn', () => {
170
+ connectionPoolStats.checkedOut = Math.max(0, connectionPoolStats.checkedOut - 1);
171
+ connectionPoolStats.totalCheckins++;
172
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
173
+ // Hysteresis: reset threshold tracking when effective demand cools below 70%.
174
+ // Downgrade critical→warning when effective demand falls back below 85%.
175
+ // Uses `effectiveDemand` (checkedOut + waitQueueSize) for symmetry with the alert side —
176
+ // otherwise a long-draining waitQueue with low checkedOut would prematurely reset and
177
+ // re-fire on the next spike.
178
+ if (_lastUtilizationThreshold > 0) {
179
+ const maxPool = connectionPoolStats.maxPoolSize;
180
+ const effectiveDemand = maxPool > 0
181
+ ? ((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / maxPool) * 100
182
+ : 0;
183
+ if (effectiveDemand < 70) {
184
+ _lastUtilizationThreshold = 0;
185
+ }
186
+ else if (_lastUtilizationThreshold === 95 && effectiveDemand < 85) {
187
+ _lastUtilizationThreshold = 80;
188
+ }
189
+ }
190
+ });
191
+ client.on('connectionCheckOutStarted', () => {
192
+ connectionPoolStats.waitQueueSize++;
193
+ // Queue-depth early-warning — fires BEFORE the utilization-based warnings above
194
+ // because queue can overflow while pool sits at moderate utilization (the actual
195
+ // failure mode we observe under bursty load: pool at 70% checkedOut but waitQueue
196
+ // grows past maxPoolSize and triggers waitQueueTimeoutMS). Hysteresis resets when
197
+ // the queue drains (handled in connectionCheckedOut / connectionCheckOutFailed).
198
+ const maxPool = connectionPoolStats.maxPoolSize;
199
+ if (maxPool > 0) {
200
+ const queue = connectionPoolStats.waitQueueSize;
201
+ const queueRatio = queue / maxPool;
202
+ const queueData = {
203
+ waitQueueSize: queue,
204
+ maxPoolSize: maxPool,
205
+ checkedOut: connectionPoolStats.checkedOut,
206
+ utilizationPercent: Math.round((connectionPoolStats.checkedOut / maxPool) * 100),
207
+ // F6a: effective demand includes the wait queue. Dashboards should chart this — the
208
+ // checkedOut-only `utilizationPercent` can sit at 50% while real demand is 158%.
209
+ effectiveDemandPercent: Math.round(((connectionPoolStats.checkedOut + queue) / maxPool) * 100),
210
+ };
211
+ if (queueRatio >= 1.0 && _lastQueueThreshold < 100) {
212
+ _lastQueueThreshold = 100;
213
+ emitEvent({
214
+ level: 'error',
215
+ event: 'pool.queue_critical',
216
+ message: `Pool waitQueue critical: ${queue} queued (${Math.round(queueRatio * 100)}% of pool)`,
217
+ timestamp: new Date().toISOString(),
218
+ data: queueData,
219
+ });
220
+ }
221
+ else if (queueRatio >= 0.5 && _lastQueueThreshold < 50) {
222
+ _lastQueueThreshold = 50;
223
+ emitEvent({
224
+ level: 'warn',
225
+ event: 'pool.queue_warning',
226
+ message: `Pool waitQueue warning: ${queue} queued (${Math.round(queueRatio * 100)}% of pool)`,
227
+ timestamp: new Date().toISOString(),
228
+ data: queueData,
229
+ });
230
+ }
231
+ }
232
+ });
233
+ client.on('connectionCheckOutFailed', (event) => {
234
+ connectionPoolStats.waitQueueSize = Math.max(0, connectionPoolStats.waitQueueSize - 1);
235
+ // Same queue-threshold hysteresis as connectionCheckedOut — keeps the reset path
236
+ // symmetric across success and timeout decrements.
237
+ if (_lastQueueThreshold > 0) {
238
+ const maxPool = connectionPoolStats.maxPoolSize;
239
+ const queueRatio = maxPool > 0 ? connectionPoolStats.waitQueueSize / maxPool : 0;
240
+ if (queueRatio < 0.25) {
241
+ _lastQueueThreshold = 0;
242
+ }
243
+ else if (_lastQueueThreshold === 100 && queueRatio < 0.75) {
244
+ _lastQueueThreshold = 50;
245
+ }
246
+ }
247
+ if (event.reason === 'timeout') {
248
+ connectionPoolStats.waitQueueTimeouts++;
249
+ connectionPoolStats.lastWaitQueueTimeout = new Date();
250
+ emitEvent({
251
+ level: 'error',
252
+ event: 'pool.checkout_timeout',
253
+ message: 'pool exhausted - checkout timeout',
254
+ timestamp: new Date().toISOString(),
255
+ data: {
256
+ waitQueueTimeouts: connectionPoolStats.waitQueueTimeouts,
257
+ checkedOut: connectionPoolStats.checkedOut,
258
+ maxPoolSize: connectionPoolStats.maxPoolSize,
259
+ waitQueueSize: connectionPoolStats.waitQueueSize,
260
+ utilizationPercent: connectionPoolStats.maxPoolSize > 0
261
+ ? Math.round((connectionPoolStats.checkedOut / connectionPoolStats.maxPoolSize) * 100)
262
+ : 0,
263
+ // F6a: see the connectionCheckedOut handler — effective demand is the only metric
264
+ // that surfaces "queue blew past the pool" at a glance.
265
+ effectiveDemandPercent: connectionPoolStats.maxPoolSize > 0
266
+ ? Math.round(((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / connectionPoolStats.maxPoolSize) * 100)
267
+ : 0,
268
+ },
269
+ });
270
+ }
271
+ });
272
+ }
273
+ const customIndexes = new Map();
274
+ function registerIndexesImpl(collection, indexes) {
275
+ customIndexes.set(collection, indexes);
276
+ }
277
+ // ═══════════════════════════════════════════════════════════════════
278
+ // TSMongo – static API
279
+ // ═══════════════════════════════════════════════════════════════════
280
+ class TSMongo {
281
+ static connect = connectImpl;
282
+ static getDatabase = getDatabaseImpl;
283
+ static getClient = getClientImpl;
284
+ static close = closeImpl;
285
+ static checkHealth = checkHealthImpl;
286
+ static registerIndexes = registerIndexesImpl;
287
+ static getConnectionPoolStats = getConnectionPoolStatsImpl;
288
+ static getPoolHealthStatus = getPoolHealthStatusImpl;
289
+ static getDatabaseStats = getDatabaseStatsImpl;
290
+ static setEventHandler = setEventHandlerImpl;
291
+ }
292
+ exports.TSMongo = TSMongo;
293
+ async function ensureIndexes(database) {
294
+ if (customIndexes.size === 0)
295
+ return;
296
+ try {
297
+ const collections = await database.listCollections().toArray();
298
+ const collNames = collections.map((c) => c.name);
299
+ for (const [collName, indexes] of customIndexes) {
300
+ if (collNames.includes(collName)) {
301
+ await database.collection(collName).createIndexes(indexes);
302
+ }
303
+ }
304
+ }
305
+ catch (error) {
306
+ const message = error instanceof Error ? error.message : String(error);
307
+ emitEvent({
308
+ level: 'error',
309
+ event: 'index.ensure_failed',
310
+ message: `ensure indexes failed: ${message}`,
311
+ timestamp: new Date().toISOString(),
312
+ data: { error: message },
313
+ });
314
+ }
315
+ }
316
+ const READ_PREF_MAP = {
317
+ primary: mongodb_1.ReadPreference.PRIMARY,
318
+ secondary: mongodb_1.ReadPreference.SECONDARY_PREFERRED,
319
+ secondaryPreferred: mongodb_1.ReadPreference.SECONDARY_PREFERRED,
320
+ nearest: mongodb_1.ReadPreference.NEAREST,
321
+ };
322
+ function applyEnvOverrides(cfg) {
323
+ if (process.env.MONGO_MAX_POOL_SIZE) {
324
+ cfg.maxPoolSize = parseInt(process.env.MONGO_MAX_POOL_SIZE, 10) || cfg.maxPoolSize;
325
+ }
326
+ if (process.env.MONGO_MIN_POOL_SIZE) {
327
+ cfg.minPoolSize = parseInt(process.env.MONGO_MIN_POOL_SIZE, 10) || cfg.minPoolSize;
328
+ }
329
+ if (process.env.MONGO_READ_PREFERENCE && process.env.MONGO_READ_PREFERENCE in READ_PREF_MAP) {
330
+ cfg.readPreference = process.env.MONGO_READ_PREFERENCE;
331
+ }
332
+ }
333
+ function resolveDbName(config, pathOrUri) {
334
+ let dbName = config.dbName || pathOrUri || 'default';
335
+ if (dbName.includes('?'))
336
+ dbName = dbName.split('?')[0];
337
+ return dbName.trim();
338
+ }
339
+ async function tryReuseConnection(uriObj, config) {
340
+ if (!client || !baseUri || getServerKey(new URL(baseUri)) !== getServerKey(uriObj)) {
341
+ return null;
342
+ }
343
+ try {
344
+ await client.db('admin').command({ ping: 1 });
345
+ const explicitPath = uriObj.pathname.slice(1).replace(/\/$/, '');
346
+ if (explicitPath || config.dbName) {
347
+ const dbName = resolveDbName(config, explicitPath);
348
+ db = client.db(dbName);
349
+ }
350
+ return db;
351
+ }
352
+ catch {
353
+ client = null;
354
+ db = null;
355
+ baseUri = null;
356
+ return null;
357
+ }
358
+ }
359
+ async function connectImpl(uri, config = {}) {
360
+ const uriObj = new URL(uri);
361
+ const currentBaseUri = `${uriObj.protocol}//${uriObj.host}${uriObj.search || ''}`;
362
+ const reused = await tryReuseConnection(uriObj, config);
363
+ if (reused)
364
+ return reused;
365
+ const cfg = { ...exports.DEFAULT_MONGO_CONFIG, ...config };
366
+ applyEnvOverrides(cfg);
367
+ const isLocalhost = uriObj.hostname === 'localhost' || uriObj.hostname === '127.0.0.1';
368
+ let finalUri = uri;
369
+ if (isLocalhost) {
370
+ if (!uri.includes('directConnection=')) {
371
+ finalUri = `${uri}${uri.includes('?') ? '&' : '?'}directConnection=true`;
372
+ }
373
+ if (finalUri.includes('replicaSet=')) {
374
+ finalUri = finalUri.replace(/[?&]replicaSet=[^&]*/, '');
375
+ }
376
+ }
377
+ const clientOptions = {
378
+ maxPoolSize: cfg.maxPoolSize,
379
+ minPoolSize: cfg.minPoolSize,
380
+ maxIdleTimeMS: cfg.maxIdleTimeMS,
381
+ waitQueueTimeoutMS: cfg.waitQueueTimeoutMS,
382
+ connectTimeoutMS: cfg.connectTimeoutMS,
383
+ socketTimeoutMS: cfg.socketTimeoutMS,
384
+ serverSelectionTimeoutMS: cfg.serverSelectionTimeoutMS,
385
+ readPreference: READ_PREF_MAP[cfg.readPreference || 'nearest'],
386
+ writeConcern: new mongodb_1.WriteConcern((cfg.writeConcern ?? 'majority')),
387
+ retryWrites: cfg.retryWrites,
388
+ retryReads: cfg.retryReads,
389
+ monitorCommands: config.monitorCommands ?? true,
390
+ ...(isLocalhost && { directConnection: true }),
391
+ };
392
+ if (config.compressors?.length) {
393
+ clientOptions.compressors = config.compressors;
394
+ }
395
+ baseUri = currentBaseUri;
396
+ client = new mongodb_1.MongoClient(finalUri, clientOptions);
397
+ connectionPoolStats.maxPoolSize = cfg.maxPoolSize;
398
+ connectionPoolStats.minPoolSize = cfg.minPoolSize;
399
+ attachClientEventHandlers(client, { monitorCommands: config.monitorCommands ?? true });
400
+ await client.connect();
401
+ const dbName = resolveDbName(config, new URL(finalUri).pathname.slice(1));
402
+ db = client.db(dbName);
403
+ await ensureIndexes(db);
404
+ return db;
405
+ }
406
+ function getDatabaseImpl() {
407
+ if (!db)
408
+ throw new Error('Database not connected');
409
+ return db;
410
+ }
411
+ function getClientImpl() {
412
+ if (!client)
413
+ throw new Error('Database not connected');
414
+ return client;
415
+ }
416
+ async function closeImpl() {
417
+ if (client) {
418
+ await client.close();
419
+ client = null;
420
+ db = null;
421
+ baseUri = null;
422
+ resetPoolStats();
423
+ }
424
+ }
425
+ function getConnectionPoolStatsImpl() {
426
+ return {
427
+ ...connectionPoolStats,
428
+ availableConnections: connectionPoolStats.totalConnections - connectionPoolStats.checkedOut,
429
+ };
430
+ }
431
+ function getPoolHealthStatusImpl() {
432
+ const utilization = connectionPoolStats.maxPoolSize > 0 ? (connectionPoolStats.checkedOut / connectionPoolStats.maxPoolSize) * 100 : 0;
433
+ const recentTimeout = connectionPoolStats.lastWaitQueueTimeout &&
434
+ Date.now() - connectionPoolStats.lastWaitQueueTimeout.getTime() < 60000;
435
+ if (recentTimeout || utilization >= 95) {
436
+ return {
437
+ status: 'critical',
438
+ utilizationPercent: Math.round(utilization),
439
+ message: recentTimeout
440
+ ? `Pool exhausted - ${connectionPoolStats.waitQueueTimeouts} timeout(s)`
441
+ : 'Pool nearly exhausted (>95%)',
442
+ };
443
+ }
444
+ if (utilization >= 80) {
445
+ return { status: 'warning', utilizationPercent: Math.round(utilization), message: 'High pool utilization (>80%)' };
446
+ }
447
+ return { status: 'healthy', utilizationPercent: Math.round(utilization), message: 'Pool healthy' };
448
+ }
449
+ async function checkHealthImpl() {
450
+ if (!db || !client)
451
+ return { healthy: false, latencyMs: -1, connections: 0, checkedOut: 0 };
452
+ const start = Date.now();
453
+ try {
454
+ await db.command({ ping: 1 });
455
+ return {
456
+ healthy: true,
457
+ latencyMs: Date.now() - start,
458
+ connections: connectionPoolStats.totalConnections,
459
+ checkedOut: connectionPoolStats.checkedOut,
460
+ };
461
+ }
462
+ catch {
463
+ return { healthy: false, latencyMs: -1, connections: 0, checkedOut: 0 };
464
+ }
465
+ }
466
+ async function getDatabaseStatsImpl() {
467
+ if (!db)
468
+ return {};
469
+ try {
470
+ const stats = await db.stats();
471
+ return {
472
+ collections: stats.collections,
473
+ objects: stats.objects,
474
+ dataSize: stats.dataSize,
475
+ storageSize: stats.storageSize,
476
+ indexes: stats.indexes,
477
+ indexSize: stats.indexSize,
478
+ };
479
+ }
480
+ catch {
481
+ return {};
482
+ }
483
+ }