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.
- package/LICENSE +1 -0
- package/README.md +8 -0
- package/db/TSJournal.d.ts +108 -0
- package/db/TSJournal.js +229 -0
- package/db/TSMongo.d.ts +103 -0
- package/db/TSMongo.js +516 -0
- package/db/TSRQW.d.ts +625 -0
- package/db/TSRQW.js +1204 -0
- package/db/TSRedis.d.ts +530 -0
- package/db/TSRedis.js +1368 -0
- package/db/TSRedisTB.d.ts +80 -0
- package/db/TSRedisTB.js +178 -0
- package/package.json +85 -0
- package/ussd/TSUssdMenu.d.ts +139 -0
- package/ussd/TSUssdMenu.js +368 -0
- package/ussd/TSUssdScreen.d.ts +58 -0
- package/ussd/TSUssdScreen.js +218 -0
- package/ussd/index.d.ts +3 -0
- package/ussd/index.js +19 -0
- package/ussd/providers/AfricasTalking.d.ts +3 -0
- package/ussd/providers/AfricasTalking.js +17 -0
- package/ussd/providers/AirtelDRC.d.ts +9 -0
- package/ussd/providers/AirtelDRC.js +31 -0
- package/ussd/providers/OrangeDRC.d.ts +5 -0
- package/ussd/providers/OrangeDRC.js +213 -0
- package/ussd/providers/VodacomDRC.d.ts +9 -0
- package/ussd/providers/VodacomDRC.js +48 -0
- package/ussd/providers/_.d.ts +55 -0
- package/ussd/providers/_.js +83 -0
- package/ussd/providers/index.d.ts +13 -0
- package/ussd/providers/index.js +56 -0
- package/utils/TSFifo.d.ts +109 -0
- package/utils/TSFifo.js +145 -0
- package/utils/TSFile.d.ts +36 -0
- package/utils/TSFile.js +244 -0
- package/utils/TSHash.d.ts +19 -0
- package/utils/TSHash.js +71 -0
- package/utils/TSRequest.d.ts +248 -0
- package/utils/TSRequest.js +689 -0
- package/utils/TSStub.d.ts +159 -0
- package/utils/TSStub.js +296 -0
- package/utils/abort.d.ts +18 -0
- package/utils/abort.js +97 -0
- package/utils/mime.json +11358 -0
- package/utils/object-keys.d.ts +39 -0
- 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
|
+
}
|