ts-server-lib 0.0.48

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.
Files changed (46) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +8 -0
  3. package/db/TSJournal.d.ts +108 -0
  4. package/db/TSJournal.js +229 -0
  5. package/db/TSMongo.d.ts +103 -0
  6. package/db/TSMongo.js +516 -0
  7. package/db/TSRQW.d.ts +625 -0
  8. package/db/TSRQW.js +1204 -0
  9. package/db/TSRedis.d.ts +530 -0
  10. package/db/TSRedis.js +1368 -0
  11. package/db/TSRedisTB.d.ts +80 -0
  12. package/db/TSRedisTB.js +178 -0
  13. package/package.json +85 -0
  14. package/ussd/TSUssdMenu.d.ts +139 -0
  15. package/ussd/TSUssdMenu.js +368 -0
  16. package/ussd/TSUssdScreen.d.ts +58 -0
  17. package/ussd/TSUssdScreen.js +218 -0
  18. package/ussd/index.d.ts +3 -0
  19. package/ussd/index.js +19 -0
  20. package/ussd/providers/AfricasTalking.d.ts +3 -0
  21. package/ussd/providers/AfricasTalking.js +17 -0
  22. package/ussd/providers/AirtelDRC.d.ts +9 -0
  23. package/ussd/providers/AirtelDRC.js +31 -0
  24. package/ussd/providers/OrangeDRC.d.ts +5 -0
  25. package/ussd/providers/OrangeDRC.js +213 -0
  26. package/ussd/providers/VodacomDRC.d.ts +9 -0
  27. package/ussd/providers/VodacomDRC.js +48 -0
  28. package/ussd/providers/_.d.ts +55 -0
  29. package/ussd/providers/_.js +83 -0
  30. package/ussd/providers/index.d.ts +13 -0
  31. package/ussd/providers/index.js +56 -0
  32. package/utils/TSFifo.d.ts +109 -0
  33. package/utils/TSFifo.js +145 -0
  34. package/utils/TSFile.d.ts +36 -0
  35. package/utils/TSFile.js +244 -0
  36. package/utils/TSHash.d.ts +19 -0
  37. package/utils/TSHash.js +71 -0
  38. package/utils/TSRequest.d.ts +248 -0
  39. package/utils/TSRequest.js +689 -0
  40. package/utils/TSStub.d.ts +159 -0
  41. package/utils/TSStub.js +296 -0
  42. package/utils/abort.d.ts +18 -0
  43. package/utils/abort.js +97 -0
  44. package/utils/mime.json +11358 -0
  45. package/utils/object-keys.d.ts +39 -0
  46. package/utils/object-keys.js +52 -0
package/db/TSMongo.js ADDED
@@ -0,0 +1,516 @@
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
+ /**
22
+ * In-flight connect() attempt, if any. `connectImpl` has no other synchronization around the
23
+ * module-level `client`/`db`/`baseUri` singleton, so concurrent callers (e.g. several services'
24
+ * e2e suites calling `setConfig` around the same time) would otherwise each construct their own
25
+ * `MongoClient` and race to overwrite these variables mid-`await`, leaving later reads (`db =
26
+ * client.db(...)`) pointing at whichever concurrent call's client happened to still be mutating
27
+ * things last. Serializing behind this promise makes concurrent connect() calls await the same
28
+ * attempt instead of racing.
29
+ */
30
+ let connectingPromise = null;
31
+ let _eventHandler = null;
32
+ /**
33
+ * Register a structured event handler for TSMongo lifecycle events.
34
+ *
35
+ * Call once at service startup before `TSMongo.connect()`. Pass `null` to
36
+ * remove a previously registered handler and revert to console fallback.
37
+ *
38
+ * @example
39
+ * TSMongo.setEventHandler((evt) => {
40
+ * logger[evt.level]('TSMongo event', { operation: 'core.database', event: evt.event, ...evt.data });
41
+ * });
42
+ */
43
+ function setEventHandlerImpl(handler) {
44
+ _eventHandler = handler;
45
+ }
46
+ function emitEvent(evt) {
47
+ if (_eventHandler) {
48
+ _eventHandler(evt);
49
+ return;
50
+ }
51
+ const prefix = evt.level === 'error' ? ERR_PREFIX : WARN_PREFIX;
52
+ const logFn = evt.level === 'error' ? console.error : console.warn;
53
+ logFn(prefix, `[${evt.event}]`, evt.message, evt.data);
54
+ }
55
+ /** Tracks the highest utilization threshold currently crossed; prevents repeated emissions. */
56
+ let _lastUtilizationThreshold = 0;
57
+ // Queue-depth threshold tracking — fires EARLIER than utilization-based warnings under
58
+ // the real failure mode (queue overflow with pool at moderate utilization). Hysteresis:
59
+ // "50" = waitQueueSize >= maxPoolSize * 0.5 (warning); "100" = >= maxPoolSize (critical).
60
+ let _lastQueueThreshold = 0;
61
+ let connectionPoolStats = {
62
+ totalConnections: 0,
63
+ checkedOut: 0,
64
+ availableConnections: 0,
65
+ waitQueueSize: 0,
66
+ maxPoolSize: 200,
67
+ minPoolSize: 10,
68
+ totalCheckouts: 0,
69
+ totalCheckins: 0,
70
+ connectionCreated: 0,
71
+ connectionClosed: 0,
72
+ waitQueueTimeouts: 0,
73
+ lastWaitQueueTimeout: null
74
+ };
75
+ exports.DEFAULT_MONGO_CONFIG = {
76
+ maxPoolSize: 200,
77
+ minPoolSize: 10,
78
+ maxIdleTimeMS: 30000,
79
+ waitQueueTimeoutMS: 10000,
80
+ maxWaitingRequests: 500,
81
+ connectTimeoutMS: 10000,
82
+ socketTimeoutMS: 45000,
83
+ serverSelectionTimeoutMS: 30000,
84
+ readPreference: 'nearest',
85
+ writeConcern: 'majority',
86
+ retryWrites: true,
87
+ retryReads: true
88
+ };
89
+ let baseUri = null;
90
+ function getServerKey(uriObj) {
91
+ return `${uriObj.protocol}//${uriObj.host}`;
92
+ }
93
+ function resetPoolStats() {
94
+ connectionPoolStats = {
95
+ ...connectionPoolStats,
96
+ totalConnections: 0,
97
+ checkedOut: 0,
98
+ availableConnections: 0,
99
+ waitQueueSize: 0,
100
+ totalCheckouts: 0,
101
+ totalCheckins: 0,
102
+ connectionCreated: 0,
103
+ connectionClosed: 0,
104
+ waitQueueTimeouts: 0,
105
+ lastWaitQueueTimeout: null
106
+ };
107
+ _lastUtilizationThreshold = 0;
108
+ _lastQueueThreshold = 0;
109
+ }
110
+ // eslint-disable-next-line max-lines-per-function
111
+ function attachClientEventHandlers(client, _opts) {
112
+ client.on('connectionPoolClosed', () => resetPoolStats());
113
+ client.on('connectionCreated', () => {
114
+ connectionPoolStats.totalConnections++;
115
+ connectionPoolStats.connectionCreated++;
116
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
117
+ });
118
+ client.on('connectionClosed', () => {
119
+ connectionPoolStats.totalConnections = Math.max(0, connectionPoolStats.totalConnections - 1);
120
+ connectionPoolStats.connectionClosed++;
121
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
122
+ });
123
+ client.on('connectionCheckedOut', () => {
124
+ connectionPoolStats.checkedOut++;
125
+ connectionPoolStats.totalCheckouts++;
126
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
127
+ connectionPoolStats.waitQueueSize = Math.max(0, connectionPoolStats.waitQueueSize - 1);
128
+ // Hysteresis: reset queue threshold tracking when the queue drains.
129
+ // queueRatio < 0.25 → fully reset (re-warn on next ≥0.5 crossing)
130
+ // queueRatio < 0.75 with critical previously → downgrade critical→warning
131
+ if (_lastQueueThreshold > 0) {
132
+ const maxPool = connectionPoolStats.maxPoolSize;
133
+ const queueRatio = maxPool > 0 ? connectionPoolStats.waitQueueSize / maxPool : 0;
134
+ if (queueRatio < 0.25) {
135
+ _lastQueueThreshold = 0;
136
+ }
137
+ else if (_lastQueueThreshold === 100 && queueRatio < 0.75) {
138
+ _lastQueueThreshold = 50;
139
+ }
140
+ }
141
+ // Emit utilization threshold events once per crossing (hysteresis resets in checkedIn).
142
+ //
143
+ // F6a: `effectiveDemandPercent = (checkedOut + waitQueueSize) / maxPool * 100` — the real
144
+ // demand signal (can exceed 100 under bursty load). `utilizationPercent` is checkedOut-only.
145
+ const maxPool = connectionPoolStats.maxPoolSize;
146
+ const util = maxPool > 0 ? (connectionPoolStats.checkedOut / maxPool) * 100 : 0;
147
+ const effectiveDemand = maxPool > 0
148
+ ? ((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / maxPool) * 100
149
+ : 0;
150
+ const utilizationData = {
151
+ utilizationPercent: Math.round(util),
152
+ effectiveDemandPercent: Math.round(effectiveDemand),
153
+ checkedOut: connectionPoolStats.checkedOut,
154
+ maxPoolSize: maxPool,
155
+ availableConnections: connectionPoolStats.availableConnections,
156
+ waitQueueSize: connectionPoolStats.waitQueueSize
157
+ };
158
+ if (effectiveDemand >= 95 && _lastUtilizationThreshold < 95) {
159
+ _lastUtilizationThreshold = 95;
160
+ emitEvent({
161
+ level: 'error',
162
+ event: 'pool.utilization_critical',
163
+ message: `Pool utilization critical: ${Math.round(effectiveDemand)}% effective (${Math.round(util)}% checkedOut + ${connectionPoolStats.waitQueueSize} queued)`,
164
+ timestamp: new Date().toISOString(),
165
+ data: utilizationData
166
+ });
167
+ }
168
+ else if (effectiveDemand >= 80 && _lastUtilizationThreshold < 80) {
169
+ _lastUtilizationThreshold = 80;
170
+ emitEvent({
171
+ level: 'warn',
172
+ event: 'pool.utilization_warning',
173
+ message: `Pool utilization warning: ${Math.round(effectiveDemand)}% effective (${Math.round(util)}% checkedOut + ${connectionPoolStats.waitQueueSize} queued)`,
174
+ timestamp: new Date().toISOString(),
175
+ data: utilizationData
176
+ });
177
+ }
178
+ });
179
+ client.on('connectionCheckedIn', () => {
180
+ connectionPoolStats.checkedOut = Math.max(0, connectionPoolStats.checkedOut - 1);
181
+ connectionPoolStats.totalCheckins++;
182
+ connectionPoolStats.availableConnections = connectionPoolStats.totalConnections - connectionPoolStats.checkedOut;
183
+ // Hysteresis: reset threshold tracking when effective demand cools below 70%.
184
+ // Downgrade critical→warning when effective demand falls back below 85%.
185
+ // Uses `effectiveDemand` (checkedOut + waitQueueSize) for symmetry with the alert side —
186
+ // otherwise a long-draining waitQueue with low checkedOut would prematurely reset and
187
+ // re-fire on the next spike.
188
+ if (_lastUtilizationThreshold > 0) {
189
+ const maxPool = connectionPoolStats.maxPoolSize;
190
+ const effectiveDemand = maxPool > 0
191
+ ? ((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / maxPool) * 100
192
+ : 0;
193
+ if (effectiveDemand < 70) {
194
+ _lastUtilizationThreshold = 0;
195
+ }
196
+ else if (_lastUtilizationThreshold === 95 && effectiveDemand < 85) {
197
+ _lastUtilizationThreshold = 80;
198
+ }
199
+ }
200
+ });
201
+ client.on('connectionCheckOutStarted', () => {
202
+ connectionPoolStats.waitQueueSize++;
203
+ // Queue-depth early-warning — fires BEFORE the utilization-based warnings above
204
+ // because queue can overflow while pool sits at moderate utilization (the actual
205
+ // failure mode we observe under bursty load: pool at 70% checkedOut but waitQueue
206
+ // grows past maxPoolSize and triggers waitQueueTimeoutMS). Hysteresis resets when
207
+ // the queue drains (handled in connectionCheckedOut / connectionCheckOutFailed).
208
+ const maxPool = connectionPoolStats.maxPoolSize;
209
+ if (maxPool > 0) {
210
+ const queue = connectionPoolStats.waitQueueSize;
211
+ const queueRatio = queue / maxPool;
212
+ const queueData = {
213
+ waitQueueSize: queue,
214
+ maxPoolSize: maxPool,
215
+ checkedOut: connectionPoolStats.checkedOut,
216
+ utilizationPercent: Math.round((connectionPoolStats.checkedOut / maxPool) * 100),
217
+ // F6a: effective demand includes the wait queue. Dashboards should chart this — the
218
+ // checkedOut-only `utilizationPercent` can sit at 50% while real demand is 158%.
219
+ effectiveDemandPercent: Math.round(((connectionPoolStats.checkedOut + queue) / maxPool) * 100)
220
+ };
221
+ if (queueRatio >= 1.0 && _lastQueueThreshold < 100) {
222
+ _lastQueueThreshold = 100;
223
+ emitEvent({
224
+ level: 'error',
225
+ event: 'pool.queue_critical',
226
+ message: `Pool waitQueue critical: ${queue} queued (${Math.round(queueRatio * 100)}% of pool)`,
227
+ timestamp: new Date().toISOString(),
228
+ data: queueData
229
+ });
230
+ }
231
+ else if (queueRatio >= 0.5 && _lastQueueThreshold < 50) {
232
+ _lastQueueThreshold = 50;
233
+ emitEvent({
234
+ level: 'warn',
235
+ event: 'pool.queue_warning',
236
+ message: `Pool waitQueue warning: ${queue} queued (${Math.round(queueRatio * 100)}% of pool)`,
237
+ timestamp: new Date().toISOString(),
238
+ data: queueData
239
+ });
240
+ }
241
+ }
242
+ });
243
+ client.on('connectionCheckOutFailed', (event) => {
244
+ connectionPoolStats.waitQueueSize = Math.max(0, connectionPoolStats.waitQueueSize - 1);
245
+ // Same queue-threshold hysteresis as connectionCheckedOut — keeps the reset path
246
+ // symmetric across success and timeout decrements.
247
+ if (_lastQueueThreshold > 0) {
248
+ const maxPool = connectionPoolStats.maxPoolSize;
249
+ const queueRatio = maxPool > 0 ? connectionPoolStats.waitQueueSize / maxPool : 0;
250
+ if (queueRatio < 0.25) {
251
+ _lastQueueThreshold = 0;
252
+ }
253
+ else if (_lastQueueThreshold === 100 && queueRatio < 0.75) {
254
+ _lastQueueThreshold = 50;
255
+ }
256
+ }
257
+ if (event.reason === 'timeout') {
258
+ connectionPoolStats.waitQueueTimeouts++;
259
+ connectionPoolStats.lastWaitQueueTimeout = new Date();
260
+ emitEvent({
261
+ level: 'error',
262
+ event: 'pool.checkout_timeout',
263
+ message: 'pool exhausted - checkout timeout',
264
+ timestamp: new Date().toISOString(),
265
+ data: {
266
+ waitQueueTimeouts: connectionPoolStats.waitQueueTimeouts,
267
+ checkedOut: connectionPoolStats.checkedOut,
268
+ maxPoolSize: connectionPoolStats.maxPoolSize,
269
+ waitQueueSize: connectionPoolStats.waitQueueSize,
270
+ utilizationPercent: connectionPoolStats.maxPoolSize > 0
271
+ ? Math.round((connectionPoolStats.checkedOut / connectionPoolStats.maxPoolSize) * 100)
272
+ : 0,
273
+ // F6a: see the connectionCheckedOut handler — effective demand is the only metric
274
+ // that surfaces "queue blew past the pool" at a glance.
275
+ effectiveDemandPercent: connectionPoolStats.maxPoolSize > 0
276
+ ? Math.round(((connectionPoolStats.checkedOut + connectionPoolStats.waitQueueSize) / connectionPoolStats.maxPoolSize) * 100)
277
+ : 0
278
+ }
279
+ });
280
+ }
281
+ });
282
+ }
283
+ const customIndexes = new Map();
284
+ function registerIndexesImpl(collection, indexes) {
285
+ customIndexes.set(collection, indexes);
286
+ }
287
+ // ═══════════════════════════════════════════════════════════════════
288
+ // TSMongo – static API
289
+ // ═══════════════════════════════════════════════════════════════════
290
+ class TSMongo {
291
+ static connect = connectImpl;
292
+ static getDatabase = getDatabaseImpl;
293
+ static getClient = getClientImpl;
294
+ static close = closeImpl;
295
+ static checkHealth = checkHealthImpl;
296
+ static registerIndexes = registerIndexesImpl;
297
+ static getConnectionPoolStats = getConnectionPoolStatsImpl;
298
+ static getPoolHealthStatus = getPoolHealthStatusImpl;
299
+ static getDatabaseStats = getDatabaseStatsImpl;
300
+ static setEventHandler = setEventHandlerImpl;
301
+ }
302
+ exports.TSMongo = TSMongo;
303
+ async function ensureIndexes(database) {
304
+ if (customIndexes.size === 0)
305
+ return;
306
+ try {
307
+ const collections = await database.listCollections().toArray();
308
+ const collNames = collections.map((c) => c.name);
309
+ for (const [collName, indexes] of customIndexes) {
310
+ if (collNames.includes(collName)) {
311
+ await database.collection(collName).createIndexes(indexes);
312
+ }
313
+ }
314
+ }
315
+ catch (error) {
316
+ const message = error instanceof Error ? error.message : String(error);
317
+ emitEvent({
318
+ level: 'error',
319
+ event: 'index.ensure_failed',
320
+ message: `ensure indexes failed: ${message}`,
321
+ timestamp: new Date().toISOString(),
322
+ data: { error: message }
323
+ });
324
+ }
325
+ }
326
+ const READ_PREF_MAP = {
327
+ primary: mongodb_1.ReadPreference.PRIMARY,
328
+ secondary: mongodb_1.ReadPreference.SECONDARY_PREFERRED,
329
+ secondaryPreferred: mongodb_1.ReadPreference.SECONDARY_PREFERRED,
330
+ nearest: mongodb_1.ReadPreference.NEAREST
331
+ };
332
+ function applyEnvOverrides(cfg) {
333
+ if (process.env.MONGO_MAX_POOL_SIZE) {
334
+ cfg.maxPoolSize = parseInt(process.env.MONGO_MAX_POOL_SIZE, 10) || cfg.maxPoolSize;
335
+ }
336
+ if (process.env.MONGO_MIN_POOL_SIZE) {
337
+ cfg.minPoolSize = parseInt(process.env.MONGO_MIN_POOL_SIZE, 10) || cfg.minPoolSize;
338
+ }
339
+ if (process.env.MONGO_READ_PREFERENCE && process.env.MONGO_READ_PREFERENCE in READ_PREF_MAP) {
340
+ cfg.readPreference = process.env.MONGO_READ_PREFERENCE;
341
+ }
342
+ }
343
+ function resolveDbName(config, pathOrUri) {
344
+ let dbName = config.dbName || pathOrUri || 'default';
345
+ if (dbName.includes('?'))
346
+ dbName = dbName.split('?')[0];
347
+ return dbName.trim();
348
+ }
349
+ async function tryReuseConnection(uriObj, config) {
350
+ if (!client || !baseUri || getServerKey(new URL(baseUri)) !== getServerKey(uriObj)) {
351
+ return null;
352
+ }
353
+ try {
354
+ await client.db('admin').command({ ping: 1 });
355
+ const explicitPath = uriObj.pathname.slice(1).replace(/\/$/, '');
356
+ if (explicitPath || config.dbName) {
357
+ const dbName = resolveDbName(config, explicitPath);
358
+ db = client.db(dbName);
359
+ }
360
+ return db;
361
+ }
362
+ catch {
363
+ client = null;
364
+ db = null;
365
+ baseUri = null;
366
+ return null;
367
+ }
368
+ }
369
+ async function connectImpl(uri, config = {}) {
370
+ const uriObj = new URL(uri);
371
+ const reused = await tryReuseConnection(uriObj, config);
372
+ if (reused)
373
+ return reused;
374
+ // Another connect() is already establishing a client (possibly for a different server —
375
+ // tryReuseConnection above already ruled out reusing it). Wait for it to settle, then
376
+ // re-run from the top: either it connected to the same server we want (reuse succeeds) or
377
+ // it didn't and we fall through to start our own attempt, but never two at once.
378
+ if (connectingPromise) {
379
+ await connectingPromise.catch(() => undefined);
380
+ return connectImpl(uri, config);
381
+ }
382
+ const doConnect = async () => {
383
+ const currentBaseUri = `${uriObj.protocol}//${uriObj.host}${uriObj.search || ''}`;
384
+ const cfg = { ...exports.DEFAULT_MONGO_CONFIG, ...config };
385
+ applyEnvOverrides(cfg);
386
+ const isLocalhost = uriObj.hostname === 'localhost' || uriObj.hostname === '127.0.0.1';
387
+ let finalUri = uri;
388
+ if (isLocalhost) {
389
+ if (!uri.includes('directConnection=')) {
390
+ finalUri = `${uri}${uri.includes('?') ? '&' : '?'}directConnection=true`;
391
+ }
392
+ if (finalUri.includes('replicaSet=')) {
393
+ finalUri = finalUri.replace(/[?&]replicaSet=[^&]*/, '');
394
+ }
395
+ }
396
+ const clientOptions = {
397
+ maxPoolSize: cfg.maxPoolSize,
398
+ minPoolSize: cfg.minPoolSize,
399
+ maxIdleTimeMS: cfg.maxIdleTimeMS,
400
+ waitQueueTimeoutMS: cfg.waitQueueTimeoutMS,
401
+ connectTimeoutMS: cfg.connectTimeoutMS,
402
+ socketTimeoutMS: cfg.socketTimeoutMS,
403
+ serverSelectionTimeoutMS: cfg.serverSelectionTimeoutMS,
404
+ readPreference: READ_PREF_MAP[cfg.readPreference || 'nearest'],
405
+ writeConcern: new mongodb_1.WriteConcern((cfg.writeConcern ?? 'majority')),
406
+ retryWrites: cfg.retryWrites,
407
+ retryReads: cfg.retryReads,
408
+ monitorCommands: config.monitorCommands ?? true,
409
+ ...(isLocalhost && { directConnection: true })
410
+ };
411
+ if (config.compressors?.length) {
412
+ clientOptions.compressors = config.compressors;
413
+ }
414
+ // Build and connect using a LOCAL client/db reference throughout — only publish to the
415
+ // shared module-level `client`/`db`/`baseUri` once the whole sequence has succeeded, so a
416
+ // concurrent reader can never observe a client whose `.connect()`/`ensureIndexes()` is still
417
+ // in flight (see the comment on `connectingPromise` above).
418
+ const newClient = new mongodb_1.MongoClient(finalUri, clientOptions);
419
+ attachClientEventHandlers(newClient, { monitorCommands: config.monitorCommands ?? true });
420
+ await newClient.connect();
421
+ const dbName = resolveDbName(config, new URL(finalUri).pathname.slice(1));
422
+ const newDb = newClient.db(dbName);
423
+ await ensureIndexes(newDb);
424
+ baseUri = currentBaseUri;
425
+ client = newClient;
426
+ db = newDb;
427
+ connectionPoolStats.maxPoolSize = cfg.maxPoolSize;
428
+ connectionPoolStats.minPoolSize = cfg.minPoolSize;
429
+ return newDb;
430
+ };
431
+ connectingPromise = doConnect();
432
+ try {
433
+ return await connectingPromise;
434
+ }
435
+ finally {
436
+ connectingPromise = null;
437
+ }
438
+ }
439
+ function getDatabaseImpl() {
440
+ if (!db)
441
+ throw new Error('Database not connected');
442
+ return db;
443
+ }
444
+ function getClientImpl() {
445
+ if (!client)
446
+ throw new Error('Database not connected');
447
+ return client;
448
+ }
449
+ async function closeImpl() {
450
+ if (client) {
451
+ await client.close();
452
+ client = null;
453
+ db = null;
454
+ baseUri = null;
455
+ resetPoolStats();
456
+ }
457
+ }
458
+ function getConnectionPoolStatsImpl() {
459
+ return {
460
+ ...connectionPoolStats,
461
+ availableConnections: connectionPoolStats.totalConnections - connectionPoolStats.checkedOut
462
+ };
463
+ }
464
+ function getPoolHealthStatusImpl() {
465
+ const utilization = connectionPoolStats.maxPoolSize > 0 ? (connectionPoolStats.checkedOut / connectionPoolStats.maxPoolSize) * 100 : 0;
466
+ const recentTimeout = connectionPoolStats.lastWaitQueueTimeout &&
467
+ Date.now() - connectionPoolStats.lastWaitQueueTimeout.getTime() < 60000;
468
+ if (recentTimeout || utilization >= 95) {
469
+ return {
470
+ status: 'critical',
471
+ utilizationPercent: Math.round(utilization),
472
+ message: recentTimeout
473
+ ? `Pool exhausted - ${connectionPoolStats.waitQueueTimeouts} timeout(s)`
474
+ : 'Pool nearly exhausted (>95%)'
475
+ };
476
+ }
477
+ if (utilization >= 80) {
478
+ return { status: 'warning', utilizationPercent: Math.round(utilization), message: 'High pool utilization (>80%)' };
479
+ }
480
+ return { status: 'healthy', utilizationPercent: Math.round(utilization), message: 'Pool healthy' };
481
+ }
482
+ async function checkHealthImpl() {
483
+ if (!db || !client)
484
+ return { healthy: false, latencyMs: -1, connections: 0, checkedOut: 0 };
485
+ const start = Date.now();
486
+ try {
487
+ await db.command({ ping: 1 });
488
+ return {
489
+ healthy: true,
490
+ latencyMs: Date.now() - start,
491
+ connections: connectionPoolStats.totalConnections,
492
+ checkedOut: connectionPoolStats.checkedOut
493
+ };
494
+ }
495
+ catch {
496
+ return { healthy: false, latencyMs: -1, connections: 0, checkedOut: 0 };
497
+ }
498
+ }
499
+ async function getDatabaseStatsImpl() {
500
+ if (!db)
501
+ return {};
502
+ try {
503
+ const stats = await db.stats();
504
+ return {
505
+ collections: stats.collections,
506
+ objects: stats.objects,
507
+ dataSize: stats.dataSize,
508
+ storageSize: stats.storageSize,
509
+ indexes: stats.indexes,
510
+ indexSize: stats.indexSize
511
+ };
512
+ }
513
+ catch {
514
+ return {};
515
+ }
516
+ }