monsqlize 2.0.1 → 2.0.3
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/CHANGELOG.md +11 -5
- package/README.md +35 -19
- package/changelogs/README.md +8 -4
- package/changelogs/v2.0.0.md +1 -1
- package/changelogs/v2.0.2.md +22 -0
- package/changelogs/v2.0.3.md +58 -0
- package/dist/cjs/index.cjs +648 -192
- package/dist/cjs/transaction/Transaction.cjs +88 -3
- package/dist/cjs/transaction/TransactionManager.cjs +135 -11
- package/dist/esm/index.mjs +643 -187
- package/dist/types/collection.d.ts +5 -3
- package/dist/types/model.d.ts +175 -175
- package/dist/types/mongodb.d.ts +8 -1
- package/dist/types/monsqlize.d.ts +28 -3
- package/dist/types/pool.d.ts +1 -1
- package/dist/types/runtime.d.ts +31 -7
- package/dist/types/transaction.d.ts +12 -0
- package/package.json +36 -35
package/dist/cjs/index.cjs
CHANGED
|
@@ -145,8 +145,8 @@ function createQueryTimeoutError(timeoutMs) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// src/capabilities/cache/redis-cache-adapter.ts
|
|
148
|
-
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance
|
|
149
|
-
var LEGACY_IOREDIS_MISSING_ERROR = "ioredis
|
|
148
|
+
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance must be a Redis URL string or an ioredis instance";
|
|
149
|
+
var LEGACY_IOREDIS_MISSING_ERROR = "Unable to load ioredis. monsqlize installs ioredis by default; check package installation completeness or pass an existing ioredis instance";
|
|
150
150
|
function isMissingIoredisError(error) {
|
|
151
151
|
if (!(error instanceof Error)) {
|
|
152
152
|
return false;
|
|
@@ -156,14 +156,14 @@ function isMissingIoredisError(error) {
|
|
|
156
156
|
function createLegacyRedisError(message, code = ErrorCodes.INVALID_ARGUMENT) {
|
|
157
157
|
return createError(code, message);
|
|
158
158
|
}
|
|
159
|
-
function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
159
|
+
function createRedisCacheAdapter(redisUrlOrInstance, options) {
|
|
160
160
|
if (typeof redisUrlOrInstance === "string") {
|
|
161
161
|
const redisUrl = redisUrlOrInstance.trim();
|
|
162
162
|
if (!redisUrl) {
|
|
163
163
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
164
164
|
}
|
|
165
165
|
try {
|
|
166
|
-
return (0, import_redis.createRedisCacheAdapter)(redisUrl);
|
|
166
|
+
return (0, import_redis.createRedisCacheAdapter)(redisUrl, options);
|
|
167
167
|
} catch (error) {
|
|
168
168
|
if (isMissingIoredisError(error)) {
|
|
169
169
|
throw createLegacyRedisError(LEGACY_IOREDIS_MISSING_ERROR, ErrorCodes.CACHE_UNAVAILABLE);
|
|
@@ -172,7 +172,7 @@ function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
if (redisUrlOrInstance && typeof redisUrlOrInstance === "object") {
|
|
175
|
-
return (0, import_redis.createRedisCacheAdapter)(redisUrlOrInstance);
|
|
175
|
+
return (0, import_redis.createRedisCacheAdapter)(redisUrlOrInstance, options);
|
|
176
176
|
}
|
|
177
177
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
178
178
|
}
|
|
@@ -182,7 +182,7 @@ var import_crypto = require("crypto");
|
|
|
182
182
|
var DistributedCacheInvalidator = class {
|
|
183
183
|
constructor(options) {
|
|
184
184
|
if (!options.cache) {
|
|
185
|
-
throw
|
|
185
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
|
|
186
186
|
}
|
|
187
187
|
this._cache = options.cache;
|
|
188
188
|
this._logger = options.logger ?? null;
|
|
@@ -200,7 +200,7 @@ var DistributedCacheInvalidator = class {
|
|
|
200
200
|
this.pub = new Redis(options.redisUrl);
|
|
201
201
|
this.sub = new Redis(options.redisUrl);
|
|
202
202
|
} else {
|
|
203
|
-
throw
|
|
203
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires either redis or redisUrl");
|
|
204
204
|
}
|
|
205
205
|
this._setupSubscription();
|
|
206
206
|
}
|
|
@@ -380,17 +380,17 @@ var _PopulatePromise = class _PopulatePromise {
|
|
|
380
380
|
/**
|
|
381
381
|
* Append a populate path and return a new PopulatePromise (chainable).
|
|
382
382
|
*/
|
|
383
|
-
populate(
|
|
383
|
+
populate(path3, options) {
|
|
384
384
|
const toConfig = (item) => {
|
|
385
385
|
if (typeof item !== "string" && (typeof item !== "object" || item === null || Array.isArray(item))) {
|
|
386
386
|
throw createError(ErrorCodes.INVALID_ARGUMENT, "populate param must be a string, array, or object");
|
|
387
387
|
}
|
|
388
388
|
return typeof item === "string" ? { path: item, ...options } : { ...item, ...options };
|
|
389
389
|
};
|
|
390
|
-
if (Array.isArray(
|
|
391
|
-
return new _PopulatePromise(this.executor, [...this.paths, ...
|
|
390
|
+
if (Array.isArray(path3)) {
|
|
391
|
+
return new _PopulatePromise(this.executor, [...this.paths, ...path3.map(toConfig)]);
|
|
392
392
|
}
|
|
393
|
-
const config = toConfig(
|
|
393
|
+
const config = toConfig(path3);
|
|
394
394
|
return new _PopulatePromise(this.executor, [...this.paths, config]);
|
|
395
395
|
}
|
|
396
396
|
/**
|
|
@@ -504,8 +504,8 @@ function validateRelationConfig(name, config) {
|
|
|
504
504
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
|
-
function normalizePopulateConfig(
|
|
508
|
-
return typeof
|
|
507
|
+
function normalizePopulateConfig(path3) {
|
|
508
|
+
return typeof path3 === "string" ? { path: path3 } : path3;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
// src/capabilities/model/model-registry.ts
|
|
@@ -656,8 +656,8 @@ function groupBy(values, keySelector) {
|
|
|
656
656
|
}
|
|
657
657
|
return map;
|
|
658
658
|
}
|
|
659
|
-
function getByPath(source,
|
|
660
|
-
return
|
|
659
|
+
function getByPath(source, path3) {
|
|
660
|
+
return path3.split(".").reduce((current, key) => {
|
|
661
661
|
if (!current || typeof current !== "object") {
|
|
662
662
|
return void 0;
|
|
663
663
|
}
|
|
@@ -712,8 +712,8 @@ function resolveRegisteredCollectionName(registered, fallback) {
|
|
|
712
712
|
const definition = registered.definition;
|
|
713
713
|
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
714
714
|
}
|
|
715
|
-
async function populateModelPath(context, docs,
|
|
716
|
-
const config = normalizePopulateConfig(
|
|
715
|
+
async function populateModelPath(context, docs, path3) {
|
|
716
|
+
const config = normalizePopulateConfig(path3);
|
|
717
717
|
if (docs.length === 0) {
|
|
718
718
|
return docs;
|
|
719
719
|
}
|
|
@@ -830,8 +830,8 @@ function hydrateModelDocument(context, doc) {
|
|
|
830
830
|
populate: {
|
|
831
831
|
configurable: true,
|
|
832
832
|
enumerable: false,
|
|
833
|
-
value: (
|
|
834
|
-
const paths = Array.isArray(
|
|
833
|
+
value: (path3) => {
|
|
834
|
+
const paths = Array.isArray(path3) ? path3 : [path3];
|
|
835
835
|
return new PopulatePromise(
|
|
836
836
|
(resolvedPaths) => context.populateDocument(hydrated, resolvedPaths),
|
|
837
837
|
paths
|
|
@@ -2066,18 +2066,18 @@ var ModelInstance = class {
|
|
|
2066
2066
|
}
|
|
2067
2067
|
async populateDocuments(docs, paths) {
|
|
2068
2068
|
let current = docs;
|
|
2069
|
-
for (const
|
|
2070
|
-
current = await this.populatePath(current,
|
|
2069
|
+
for (const path3 of paths) {
|
|
2070
|
+
current = await this.populatePath(current, path3);
|
|
2071
2071
|
}
|
|
2072
2072
|
return current;
|
|
2073
2073
|
}
|
|
2074
|
-
async populatePath(docs,
|
|
2074
|
+
async populatePath(docs, path3) {
|
|
2075
2075
|
return populateModelPath({
|
|
2076
2076
|
relations: this.relations,
|
|
2077
2077
|
runtime: this.runtime,
|
|
2078
2078
|
dbName: this.dbName,
|
|
2079
2079
|
poolName: this.poolName
|
|
2080
|
-
}, docs,
|
|
2080
|
+
}, docs, path3);
|
|
2081
2081
|
}
|
|
2082
2082
|
hydrateDocuments(docs) {
|
|
2083
2083
|
return docs.filter(Boolean).map((doc) => this.hydrateDocument(doc));
|
|
@@ -2250,9 +2250,9 @@ var Transaction = class {
|
|
|
2250
2250
|
*/
|
|
2251
2251
|
async start() {
|
|
2252
2252
|
if (this.state !== "pending") {
|
|
2253
|
-
throw
|
|
2253
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
|
|
2254
2254
|
}
|
|
2255
|
-
this.session.startTransaction();
|
|
2255
|
+
this.session.startTransaction(this.options.transactionOptions);
|
|
2256
2256
|
this.state = "active";
|
|
2257
2257
|
this.startedAt = Date.now();
|
|
2258
2258
|
const timeout = this.options.timeout ?? 3e4;
|
|
@@ -2272,7 +2272,7 @@ var Transaction = class {
|
|
|
2272
2272
|
*/
|
|
2273
2273
|
async commit() {
|
|
2274
2274
|
if (this.state !== "active") {
|
|
2275
|
-
throw
|
|
2275
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
2276
2276
|
}
|
|
2277
2277
|
if (typeof this.session.commitTransaction === "function") {
|
|
2278
2278
|
await this.session.commitTransaction();
|
|
@@ -2370,7 +2370,9 @@ var TransactionManager = class {
|
|
|
2370
2370
|
this.stats = {
|
|
2371
2371
|
totalTransactions: 0,
|
|
2372
2372
|
successfulTransactions: 0,
|
|
2373
|
-
failedTransactions: 0
|
|
2373
|
+
failedTransactions: 0,
|
|
2374
|
+
readOnlyTransactions: 0,
|
|
2375
|
+
writeTransactions: 0
|
|
2374
2376
|
};
|
|
2375
2377
|
const options = "client" in input ? input : {
|
|
2376
2378
|
client: input,
|
|
@@ -2381,6 +2383,10 @@ var TransactionManager = class {
|
|
|
2381
2383
|
this.cache = options.cache ?? null;
|
|
2382
2384
|
this.logger = options.logger ?? null;
|
|
2383
2385
|
this.lockManager = options.lockManager ?? null;
|
|
2386
|
+
this.defaultReadConcern = options.defaultReadConcern;
|
|
2387
|
+
this.defaultWriteConcern = options.defaultWriteConcern;
|
|
2388
|
+
this.defaultReadPreference = options.defaultReadPreference;
|
|
2389
|
+
this.maxStatsSamples = options.maxStatsSamples ?? 1e3;
|
|
2384
2390
|
this.defaultOptions = {
|
|
2385
2391
|
maxDuration: options.maxDuration ?? 3e4,
|
|
2386
2392
|
enableRetry: options.enableRetry ?? true,
|
|
@@ -2397,11 +2403,17 @@ var TransactionManager = class {
|
|
|
2397
2403
|
const session = this.client.startSession({
|
|
2398
2404
|
causalConsistency: options.causalConsistency !== false
|
|
2399
2405
|
});
|
|
2406
|
+
const transactionOptions = {
|
|
2407
|
+
readConcern: options.readConcern ?? this.defaultReadConcern,
|
|
2408
|
+
writeConcern: options.writeConcern ?? this.defaultWriteConcern,
|
|
2409
|
+
readPreference: options.readPreference ?? this.defaultReadPreference
|
|
2410
|
+
};
|
|
2400
2411
|
const transaction = new Transaction(session, {
|
|
2401
2412
|
cache: this.cache,
|
|
2402
2413
|
logger: this.logger,
|
|
2403
2414
|
lockManager: options.enableCacheLock === false ? null : this.lockManager,
|
|
2404
|
-
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration
|
|
2415
|
+
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration,
|
|
2416
|
+
transactionOptions: compactUndefined(transactionOptions)
|
|
2405
2417
|
});
|
|
2406
2418
|
const originalEnd = transaction.end.bind(transaction);
|
|
2407
2419
|
transaction.end = async () => {
|
|
@@ -2428,12 +2440,12 @@ var TransactionManager = class {
|
|
|
2428
2440
|
await transaction.start();
|
|
2429
2441
|
const result = await callback(transaction);
|
|
2430
2442
|
await transaction.commit();
|
|
2431
|
-
this.recordStats(Date.now() - startedAt, true);
|
|
2443
|
+
this.recordStats(transaction, Date.now() - startedAt, true);
|
|
2432
2444
|
return result;
|
|
2433
2445
|
} catch (error) {
|
|
2434
2446
|
lastError = error;
|
|
2435
2447
|
await transaction.abort();
|
|
2436
|
-
this.recordStats(Date.now() - startedAt, false);
|
|
2448
|
+
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
2437
2449
|
if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
|
|
2438
2450
|
throw error;
|
|
2439
2451
|
}
|
|
@@ -2469,27 +2481,54 @@ var TransactionManager = class {
|
|
|
2469
2481
|
*/
|
|
2470
2482
|
getStats() {
|
|
2471
2483
|
const averageDuration = this.durations.length === 0 ? 0 : this.durations.reduce((sum, item) => sum + item, 0) / this.durations.length;
|
|
2484
|
+
const sortedDurations = [...this.durations].sort((a, b) => a - b);
|
|
2485
|
+
const p95Duration = percentile(sortedDurations, 0.95);
|
|
2486
|
+
const p99Duration = percentile(sortedDurations, 0.99);
|
|
2487
|
+
const totalTransactions = this.stats.totalTransactions;
|
|
2472
2488
|
return {
|
|
2473
|
-
totalTransactions
|
|
2489
|
+
totalTransactions,
|
|
2474
2490
|
successfulTransactions: this.stats.successfulTransactions,
|
|
2475
2491
|
failedTransactions: this.stats.failedTransactions,
|
|
2492
|
+
readOnlyTransactions: this.stats.readOnlyTransactions,
|
|
2493
|
+
writeTransactions: this.stats.writeTransactions,
|
|
2476
2494
|
activeTransactions: this.activeTransactions.size,
|
|
2477
|
-
averageDuration
|
|
2495
|
+
averageDuration,
|
|
2496
|
+
p95Duration,
|
|
2497
|
+
p99Duration,
|
|
2498
|
+
successRate: totalTransactions > 0 ? `${(this.stats.successfulTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2499
|
+
readOnlyRatio: totalTransactions > 0 ? `${(this.stats.readOnlyTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2500
|
+
sampleCount: this.durations.length
|
|
2478
2501
|
};
|
|
2479
2502
|
}
|
|
2480
|
-
recordStats(duration, success) {
|
|
2503
|
+
recordStats(transaction, duration, success) {
|
|
2481
2504
|
this.stats.totalTransactions += 1;
|
|
2482
2505
|
if (success) {
|
|
2483
2506
|
this.stats.successfulTransactions += 1;
|
|
2484
2507
|
} else {
|
|
2485
2508
|
this.stats.failedTransactions += 1;
|
|
2486
2509
|
}
|
|
2510
|
+
if (transaction.pendingInvalidations.size > 0) {
|
|
2511
|
+
this.stats.writeTransactions += 1;
|
|
2512
|
+
} else {
|
|
2513
|
+
this.stats.readOnlyTransactions += 1;
|
|
2514
|
+
}
|
|
2487
2515
|
this.durations.push(duration);
|
|
2488
|
-
if (this.durations.length >
|
|
2516
|
+
if (this.durations.length > this.maxStatsSamples) {
|
|
2489
2517
|
this.durations.shift();
|
|
2490
2518
|
}
|
|
2491
2519
|
}
|
|
2492
2520
|
};
|
|
2521
|
+
function percentile(sortedValues, ratio) {
|
|
2522
|
+
if (sortedValues.length === 0) {
|
|
2523
|
+
return 0;
|
|
2524
|
+
}
|
|
2525
|
+
const index = Math.floor(sortedValues.length * ratio);
|
|
2526
|
+
return sortedValues[Math.min(index, sortedValues.length - 1)] ?? 0;
|
|
2527
|
+
}
|
|
2528
|
+
function compactUndefined(value) {
|
|
2529
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0);
|
|
2530
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
2531
|
+
}
|
|
2493
2532
|
function stringifySessionId(id) {
|
|
2494
2533
|
if (typeof id === "string") {
|
|
2495
2534
|
return id;
|
|
@@ -2526,18 +2565,166 @@ async function sleep(ms) {
|
|
|
2526
2565
|
}
|
|
2527
2566
|
|
|
2528
2567
|
// src/adapters/mongodb/common/connect.ts
|
|
2568
|
+
var import_node_fs = require("node:fs");
|
|
2569
|
+
var import_node_path = __toESM(require("node:path"));
|
|
2529
2570
|
var import_mongodb = require("mongodb");
|
|
2571
|
+
var DEFAULT_MEMORY_SERVER_VERSION = "7.0.14";
|
|
2572
|
+
var MANAGED_DB_PATH_PREFIXES = ["single-", "replset-", "examples-single-", "examples-replset-", "probe-single-", "probe-replset-"];
|
|
2530
2573
|
var _memoryServerInstance = null;
|
|
2574
|
+
var _memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2575
|
+
var _memoryServerDbPath = null;
|
|
2576
|
+
var _memoryServerClients = /* @__PURE__ */ new Set();
|
|
2577
|
+
function setDefaultEnv(name, value) {
|
|
2578
|
+
if (!process.env[name]) {
|
|
2579
|
+
process.env[name] = value;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function sanitizePathSegment(input) {
|
|
2583
|
+
return input.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
|
|
2584
|
+
}
|
|
2585
|
+
function parseManagedPathPid(name) {
|
|
2586
|
+
if (!MANAGED_DB_PATH_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
2587
|
+
return null;
|
|
2588
|
+
}
|
|
2589
|
+
const match = /-(\d+)-[^-]+$/.exec(name);
|
|
2590
|
+
if (!match) {
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
const pid = Number.parseInt(match[1], 10);
|
|
2594
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
2595
|
+
}
|
|
2596
|
+
function isProcessAlive(pid) {
|
|
2597
|
+
if (pid === process.pid) {
|
|
2598
|
+
return true;
|
|
2599
|
+
}
|
|
2600
|
+
try {
|
|
2601
|
+
process.kill(pid, 0);
|
|
2602
|
+
return true;
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
return error.code === "EPERM";
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
function pruneManagedDbRoot(dbRoot) {
|
|
2608
|
+
let entries;
|
|
2609
|
+
try {
|
|
2610
|
+
entries = (0, import_node_fs.readdirSync)(dbRoot, { withFileTypes: true });
|
|
2611
|
+
} catch {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
for (const entry of entries) {
|
|
2615
|
+
if (!entry.isDirectory()) {
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
const pid = parseManagedPathPid(entry.name);
|
|
2619
|
+
if (!pid || isProcessAlive(pid)) {
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
try {
|
|
2623
|
+
(0, import_node_fs.rmSync)(import_node_path.default.join(dbRoot, entry.name), { recursive: true, force: true });
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function resolveMemoryServerBinaryVersion(memoryServerOptions = {}) {
|
|
2629
|
+
return memoryServerOptions?.binary?.version || process.env.MONSQLIZE_REPLSET_BINARY_VERSION || process.env.MONSQLIZE_MEMORY_MONGO_BINARY_VERSION || process.env.MONGOMS_VERSION || DEFAULT_MEMORY_SERVER_VERSION;
|
|
2630
|
+
}
|
|
2631
|
+
function resolveMemoryServerPolicy(binaryVersion) {
|
|
2632
|
+
const cacheRoot = import_node_path.default.resolve(process.env.MONSQLIZE_MEMORY_SERVER_CACHE_DIR || import_node_path.default.join(process.cwd(), ".cache", "mongodb-memory-server"));
|
|
2633
|
+
const downloadDir = import_node_path.default.resolve(process.env.MONGOMS_DOWNLOAD_DIR || import_node_path.default.join(cacheRoot, "binaries"));
|
|
2634
|
+
const dbRoot = import_node_path.default.resolve(process.env.MONSQLIZE_MEMORY_SERVER_DB_DIR || import_node_path.default.join(cacheRoot, "db"));
|
|
2635
|
+
(0, import_node_fs.mkdirSync)(downloadDir, { recursive: true });
|
|
2636
|
+
(0, import_node_fs.mkdirSync)(dbRoot, { recursive: true });
|
|
2637
|
+
pruneManagedDbRoot(dbRoot);
|
|
2638
|
+
setDefaultEnv("MONGOMS_DOWNLOAD_DIR", downloadDir);
|
|
2639
|
+
setDefaultEnv("MONGOMS_PREFER_GLOBAL_PATH", "false");
|
|
2640
|
+
setDefaultEnv("MONGOMS_RUNTIME_DOWNLOAD", "true");
|
|
2641
|
+
setDefaultEnv("MONGOMS_VERSION", binaryVersion);
|
|
2642
|
+
return { downloadDir, dbRoot };
|
|
2643
|
+
}
|
|
2644
|
+
function createManagedDbPath(dbRoot, dbName) {
|
|
2645
|
+
return (0, import_node_fs.mkdtempSync)(import_node_path.default.join(dbRoot, `replset-${sanitizePathSegment(dbName)}-${process.pid}-`));
|
|
2646
|
+
}
|
|
2647
|
+
function isManagedCleanupError(error, dbPath) {
|
|
2648
|
+
if (!error || typeof error !== "object") {
|
|
2649
|
+
return false;
|
|
2650
|
+
}
|
|
2651
|
+
const candidate = error;
|
|
2652
|
+
if (!candidate.code || !["ENOTEMPTY", "EBUSY", "EPERM", "ENOENT"].includes(candidate.code)) {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
return !candidate.path || import_node_path.default.resolve(candidate.path).startsWith(import_node_path.default.resolve(dbPath));
|
|
2656
|
+
}
|
|
2657
|
+
function delay(ms) {
|
|
2658
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2659
|
+
}
|
|
2660
|
+
async function cleanupManagedDbPath(dbPath) {
|
|
2661
|
+
if (!dbPath) {
|
|
2662
|
+
return true;
|
|
2663
|
+
}
|
|
2664
|
+
for (const waitMs of [0, 50, 100, 200, 400, 800]) {
|
|
2665
|
+
if (waitMs > 0) {
|
|
2666
|
+
await delay(waitMs);
|
|
2667
|
+
}
|
|
2668
|
+
try {
|
|
2669
|
+
(0, import_node_fs.rmSync)(dbPath, { recursive: true, force: true });
|
|
2670
|
+
if (!(0, import_node_fs.existsSync)(dbPath)) {
|
|
2671
|
+
return true;
|
|
2672
|
+
}
|
|
2673
|
+
} catch {
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return !(0, import_node_fs.existsSync)(dbPath);
|
|
2677
|
+
}
|
|
2678
|
+
function resolveLaunchTimeout() {
|
|
2679
|
+
const raw = process.env.MONSQLIZE_MEMORY_MONGO_LAUNCH_TIMEOUT_MS;
|
|
2680
|
+
if (!raw) {
|
|
2681
|
+
return void 0;
|
|
2682
|
+
}
|
|
2683
|
+
const value = Number.parseInt(raw, 10);
|
|
2684
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2685
|
+
}
|
|
2686
|
+
async function seedMemoryServerBinaryCache(binaryVersion, downloadDir) {
|
|
2687
|
+
try {
|
|
2688
|
+
const { DryMongoBinary } = require("mongodb-memory-server-core/lib/util/DryMongoBinary");
|
|
2689
|
+
const options = await DryMongoBinary.generateOptions({ version: binaryVersion, downloadDir });
|
|
2690
|
+
options.downloadDir = downloadDir;
|
|
2691
|
+
const paths = await DryMongoBinary.generatePaths(options);
|
|
2692
|
+
if (paths.resolveConfig && paths.homeCache && import_node_path.default.resolve(paths.resolveConfig) !== import_node_path.default.resolve(paths.homeCache) && (0, import_node_fs.existsSync)(paths.homeCache) && !(0, import_node_fs.existsSync)(paths.resolveConfig)) {
|
|
2693
|
+
(0, import_node_fs.mkdirSync)(import_node_path.default.dirname(paths.resolveConfig), { recursive: true });
|
|
2694
|
+
(0, import_node_fs.copyFileSync)(paths.homeCache, paths.resolveConfig);
|
|
2695
|
+
}
|
|
2696
|
+
} catch {
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2531
2699
|
async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
2532
2700
|
if (_memoryServerInstance) {
|
|
2533
2701
|
return _memoryServerInstance.getUri();
|
|
2534
2702
|
}
|
|
2703
|
+
const binaryVersion = resolveMemoryServerBinaryVersion(memoryServerOptions);
|
|
2704
|
+
const { dbRoot, downloadDir } = resolveMemoryServerPolicy(binaryVersion);
|
|
2705
|
+
await seedMemoryServerBinaryCache(binaryVersion, downloadDir);
|
|
2535
2706
|
const { MongoMemoryReplSet } = require("mongodb-memory-server");
|
|
2536
|
-
logger?.info?.("
|
|
2707
|
+
logger?.info?.("Starting MongoDB Memory ReplSet", { binaryVersion });
|
|
2708
|
+
const dbName = memoryServerOptions?.instance?.dbName || "monsqlize_memory";
|
|
2709
|
+
const instanceConfig = { ...memoryServerOptions?.instance ?? {} };
|
|
2710
|
+
const hasUserDbPath = typeof instanceConfig.dbPath === "string" && instanceConfig.dbPath.length > 0;
|
|
2711
|
+
if (!hasUserDbPath) {
|
|
2712
|
+
_memoryServerDbPath = createManagedDbPath(dbRoot, dbName);
|
|
2713
|
+
instanceConfig.dbPath = _memoryServerDbPath;
|
|
2714
|
+
} else {
|
|
2715
|
+
_memoryServerDbPath = null;
|
|
2716
|
+
}
|
|
2717
|
+
if (instanceConfig.launchTimeout === void 0) {
|
|
2718
|
+
const launchTimeout = resolveLaunchTimeout();
|
|
2719
|
+
if (launchTimeout) {
|
|
2720
|
+
instanceConfig.launchTimeout = launchTimeout;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: !hasUserDbPath };
|
|
2537
2724
|
const defaultConfig = {
|
|
2538
2725
|
replSet: { count: 1, storageEngine: "wiredTiger" },
|
|
2539
|
-
binary: { version:
|
|
2540
|
-
instanceOpts: [
|
|
2726
|
+
binary: { version: binaryVersion },
|
|
2727
|
+
instanceOpts: [instanceConfig]
|
|
2541
2728
|
};
|
|
2542
2729
|
const resolvedConfig = {
|
|
2543
2730
|
...defaultConfig,
|
|
@@ -2546,11 +2733,43 @@ async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
|
2546
2733
|
try {
|
|
2547
2734
|
_memoryServerInstance = await MongoMemoryReplSet.create(resolvedConfig);
|
|
2548
2735
|
const uri = _memoryServerInstance.getUri();
|
|
2549
|
-
logger?.info?.("
|
|
2736
|
+
logger?.info?.("MongoDB Memory ReplSet started", { uri });
|
|
2550
2737
|
return uri;
|
|
2551
2738
|
} catch (err) {
|
|
2552
|
-
|
|
2553
|
-
|
|
2739
|
+
if (!hasUserDbPath) {
|
|
2740
|
+
await cleanupManagedDbPath(_memoryServerDbPath);
|
|
2741
|
+
_memoryServerDbPath = null;
|
|
2742
|
+
}
|
|
2743
|
+
logger?.error?.("Failed to start MongoDB Memory ReplSet", err);
|
|
2744
|
+
throw createConnectionError(
|
|
2745
|
+
`Failed to start MongoDB Memory ReplSet: ${err.message}`,
|
|
2746
|
+
err instanceof Error ? err : void 0
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
async function stopMemoryServer(logger) {
|
|
2751
|
+
if (!_memoryServerInstance) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const instance = _memoryServerInstance;
|
|
2755
|
+
const dbPath = _memoryServerDbPath;
|
|
2756
|
+
_memoryServerInstance = null;
|
|
2757
|
+
_memoryServerDbPath = null;
|
|
2758
|
+
let stopError = null;
|
|
2759
|
+
try {
|
|
2760
|
+
await instance.stop(_memoryServerCleanupOptions);
|
|
2761
|
+
logger?.info?.("MongoDB Memory ReplSet stopped");
|
|
2762
|
+
} catch (cause) {
|
|
2763
|
+
stopError = cause;
|
|
2764
|
+
logger?.warn?.("Failed to stop MongoDB Memory ReplSet cleanly.", cause);
|
|
2765
|
+
} finally {
|
|
2766
|
+
if (_memoryServerCleanupOptions.force) {
|
|
2767
|
+
const cleaned = await cleanupManagedDbPath(dbPath);
|
|
2768
|
+
if (!cleaned && (!stopError || isManagedCleanupError(stopError, dbPath ?? ""))) {
|
|
2769
|
+
logger?.warn?.("Failed to remove MongoDB Memory ReplSet dbPath.", { dbPath });
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2554
2773
|
}
|
|
2555
2774
|
}
|
|
2556
2775
|
async function connectMongo(params) {
|
|
@@ -2559,13 +2778,15 @@ async function connectMongo(params) {
|
|
|
2559
2778
|
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name must be a non-empty string.");
|
|
2560
2779
|
}
|
|
2561
2780
|
let effectiveUri = params.config?.uri?.trim();
|
|
2781
|
+
let usesManagedMemoryServer = false;
|
|
2562
2782
|
if (!effectiveUri && params.config?.useMemoryServer === true) {
|
|
2563
2783
|
if (process.env["MONSQLIZE_USE_SYSTEM_MONGO"] === "true") {
|
|
2564
2784
|
const systemUri = process.env["MONSQLIZE_SYSTEM_MONGO_URI"] ?? "mongodb://127.0.0.1:27017";
|
|
2565
|
-
params.logger?.info?.("
|
|
2785
|
+
params.logger?.info?.("Using system MongoDB instead of memory server", { uri: systemUri });
|
|
2566
2786
|
effectiveUri = systemUri;
|
|
2567
2787
|
} else {
|
|
2568
2788
|
effectiveUri = await startMemoryServer(params.logger, params.config.memoryServerOptions);
|
|
2789
|
+
usesManagedMemoryServer = true;
|
|
2569
2790
|
}
|
|
2570
2791
|
}
|
|
2571
2792
|
if (!effectiveUri) {
|
|
@@ -2579,6 +2800,9 @@ async function connectMongo(params) {
|
|
|
2579
2800
|
try {
|
|
2580
2801
|
await client.connect();
|
|
2581
2802
|
const db = client.db(databaseName);
|
|
2803
|
+
if (usesManagedMemoryServer) {
|
|
2804
|
+
_memoryServerClients.add(client);
|
|
2805
|
+
}
|
|
2582
2806
|
params.logger?.info?.("MongoDB connected", { databaseName });
|
|
2583
2807
|
return { client, db };
|
|
2584
2808
|
} catch (cause) {
|
|
@@ -2586,6 +2810,9 @@ async function connectMongo(params) {
|
|
|
2586
2810
|
await client.close();
|
|
2587
2811
|
} catch {
|
|
2588
2812
|
}
|
|
2813
|
+
if (usesManagedMemoryServer && _memoryServerClients.size === 0) {
|
|
2814
|
+
await stopMemoryServer(params.logger);
|
|
2815
|
+
}
|
|
2589
2816
|
throw createConnectionError(
|
|
2590
2817
|
`Failed to connect to MongoDB database: ${databaseName}`,
|
|
2591
2818
|
cause instanceof Error ? cause : void 0
|
|
@@ -2596,17 +2823,26 @@ async function closeMongo(client, logger) {
|
|
|
2596
2823
|
if (!client) {
|
|
2597
2824
|
return;
|
|
2598
2825
|
}
|
|
2826
|
+
const shouldReleaseMemoryServer = _memoryServerClients.delete(client);
|
|
2827
|
+
let closeError = null;
|
|
2599
2828
|
try {
|
|
2600
2829
|
await client.close();
|
|
2601
2830
|
logger?.info?.("MongoDB connection closed");
|
|
2602
2831
|
} catch (cause) {
|
|
2603
|
-
|
|
2832
|
+
closeError = createError(
|
|
2604
2833
|
ErrorCodes.CONNECTION_CLOSED,
|
|
2605
2834
|
"Failed to close MongoDB connection cleanly.",
|
|
2606
2835
|
void 0,
|
|
2607
2836
|
cause instanceof Error ? cause : void 0
|
|
2608
2837
|
);
|
|
2609
|
-
logger?.warn?.(
|
|
2838
|
+
logger?.warn?.(closeError.message, closeError.cause);
|
|
2839
|
+
} finally {
|
|
2840
|
+
if (shouldReleaseMemoryServer && _memoryServerClients.size === 0) {
|
|
2841
|
+
await stopMemoryServer(logger);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
if (closeError) {
|
|
2845
|
+
return;
|
|
2610
2846
|
}
|
|
2611
2847
|
}
|
|
2612
2848
|
|
|
@@ -2618,8 +2854,9 @@ function loadSsh2Client() {
|
|
|
2618
2854
|
try {
|
|
2619
2855
|
return require("ssh2").Client;
|
|
2620
2856
|
} catch {
|
|
2621
|
-
throw
|
|
2622
|
-
|
|
2857
|
+
throw createError(
|
|
2858
|
+
ErrorCodes.INVALID_CONFIG,
|
|
2859
|
+
"Unable to load ssh2. monsqlize installs ssh2 by default; check that the package installation is complete and that your runtime can resolve bundled dependencies."
|
|
2623
2860
|
);
|
|
2624
2861
|
}
|
|
2625
2862
|
}
|
|
@@ -2647,10 +2884,10 @@ var SSHTunnelSSH2 = class {
|
|
|
2647
2884
|
keepaliveInterval = 3e4
|
|
2648
2885
|
} = this._sshConfig;
|
|
2649
2886
|
if (!host || !username) {
|
|
2650
|
-
throw
|
|
2887
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
|
|
2651
2888
|
}
|
|
2652
2889
|
if (!password && !privateKey && !privateKeyPath) {
|
|
2653
|
-
throw
|
|
2890
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
|
|
2654
2891
|
}
|
|
2655
2892
|
const config = { host, port, username, readyTimeout, keepaliveInterval };
|
|
2656
2893
|
if (password) {
|
|
@@ -2725,7 +2962,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2725
2962
|
}
|
|
2726
2963
|
getTunnelUri(_protocol, originalUri) {
|
|
2727
2964
|
if (!this.isConnected || this.localPort === null) {
|
|
2728
|
-
throw
|
|
2965
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2729
2966
|
}
|
|
2730
2967
|
return originalUri.replace(
|
|
2731
2968
|
`${this.remoteHost}:${this.remotePort}`,
|
|
@@ -2734,7 +2971,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2734
2971
|
}
|
|
2735
2972
|
getLocalAddress() {
|
|
2736
2973
|
if (!this.isConnected || this.localPort === null) {
|
|
2737
|
-
throw
|
|
2974
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2738
2975
|
}
|
|
2739
2976
|
return `localhost:${this.localPort}`;
|
|
2740
2977
|
}
|
|
@@ -3183,7 +3420,7 @@ var HealthChecker = class {
|
|
|
3183
3420
|
async _pingPool(poolName, timeout) {
|
|
3184
3421
|
const stored = this._clients.get(poolName);
|
|
3185
3422
|
const client = stored ?? this._poolManager?._getPool(poolName);
|
|
3186
|
-
if (!client) throw
|
|
3423
|
+
if (!client) throw createError(ErrorCodes.POOL_NOT_FOUND, `No client for pool: ${poolName}`);
|
|
3187
3424
|
const db = client.db("admin");
|
|
3188
3425
|
const pingFn = db.command ? () => db.command({ ping: 1 }) : () => db.admin().ping();
|
|
3189
3426
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Ping timeout")), timeout));
|
|
@@ -3215,7 +3452,7 @@ var PoolSelector = class {
|
|
|
3215
3452
|
}
|
|
3216
3453
|
select(pools, context) {
|
|
3217
3454
|
if (!pools || pools.length === 0) {
|
|
3218
|
-
throw
|
|
3455
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available pools");
|
|
3219
3456
|
}
|
|
3220
3457
|
switch (this._strategy) {
|
|
3221
3458
|
case "auto":
|
|
@@ -3236,7 +3473,10 @@ var PoolSelector = class {
|
|
|
3236
3473
|
selectByAuto(pools, context) {
|
|
3237
3474
|
const { operation, poolPreference } = context;
|
|
3238
3475
|
let candidates = pools;
|
|
3239
|
-
|
|
3476
|
+
const preferred = this.filterByPreference(pools, poolPreference);
|
|
3477
|
+
if (preferred.length > 0) {
|
|
3478
|
+
candidates = preferred;
|
|
3479
|
+
} else if (operation === "read") {
|
|
3240
3480
|
const secondaries = pools.filter((pool) => pool.role === "secondary");
|
|
3241
3481
|
if (secondaries.length > 0) {
|
|
3242
3482
|
candidates = secondaries;
|
|
@@ -3247,10 +3487,19 @@ var PoolSelector = class {
|
|
|
3247
3487
|
candidates = primaries;
|
|
3248
3488
|
}
|
|
3249
3489
|
}
|
|
3490
|
+
if (candidates.length === 1) {
|
|
3491
|
+
return candidates[0].name;
|
|
3492
|
+
}
|
|
3493
|
+
return this.selectByWeighted(candidates);
|
|
3494
|
+
}
|
|
3495
|
+
filterByPreference(pools, poolPreference) {
|
|
3496
|
+
let candidates = pools;
|
|
3497
|
+
let applied = false;
|
|
3250
3498
|
if (poolPreference?.role) {
|
|
3251
3499
|
const filteredByRole = candidates.filter((pool) => pool.role === poolPreference.role);
|
|
3252
3500
|
if (filteredByRole.length > 0) {
|
|
3253
3501
|
candidates = filteredByRole;
|
|
3502
|
+
applied = true;
|
|
3254
3503
|
}
|
|
3255
3504
|
}
|
|
3256
3505
|
if (poolPreference?.tags?.length) {
|
|
@@ -3263,12 +3512,10 @@ var PoolSelector = class {
|
|
|
3263
3512
|
});
|
|
3264
3513
|
if (filteredByTags.length > 0) {
|
|
3265
3514
|
candidates = filteredByTags;
|
|
3515
|
+
applied = true;
|
|
3266
3516
|
}
|
|
3267
3517
|
}
|
|
3268
|
-
|
|
3269
|
-
return candidates[0].name;
|
|
3270
|
-
}
|
|
3271
|
-
return this.selectByWeighted(candidates);
|
|
3518
|
+
return applied ? candidates : [];
|
|
3272
3519
|
}
|
|
3273
3520
|
selectByRoundRobin(pools, context) {
|
|
3274
3521
|
let candidates = pools;
|
|
@@ -3428,43 +3675,43 @@ var DEFAULT_POOL_CONNECT_OPTIONS = {
|
|
|
3428
3675
|
serverSelectionTimeoutMS: 5e3
|
|
3429
3676
|
};
|
|
3430
3677
|
function validatePoolConfig(config) {
|
|
3431
|
-
if (!config || typeof config !== "object") throw
|
|
3432
|
-
if (!config.name || typeof config.name !== "string") throw
|
|
3433
|
-
if (!config.uri || typeof config.uri !== "string") throw
|
|
3678
|
+
if (!config || typeof config !== "object") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config must be an object");
|
|
3679
|
+
if (!config.name || typeof config.name !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.name is required and must be a string");
|
|
3680
|
+
if (!config.uri || typeof config.uri !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri is required and must be a string");
|
|
3434
3681
|
if (!config.uri.startsWith("mongodb://") && !config.uri.startsWith("mongodb+srv://")) {
|
|
3435
|
-
throw
|
|
3682
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri must start with mongodb:// or mongodb+srv://");
|
|
3436
3683
|
}
|
|
3437
3684
|
if (config.role) {
|
|
3438
3685
|
const validRoles = ["primary", "secondary", "analytics", "custom"];
|
|
3439
|
-
if (!validRoles.includes(config.role)) throw
|
|
3686
|
+
if (!validRoles.includes(config.role)) throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.role must be one of: ${validRoles.join(", ")}`);
|
|
3440
3687
|
}
|
|
3441
3688
|
if (config.weight !== void 0) {
|
|
3442
|
-
if (typeof config.weight !== "number") throw
|
|
3443
|
-
if (config.weight < 0) throw
|
|
3689
|
+
if (typeof config.weight !== "number") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a number");
|
|
3690
|
+
if (config.weight < 0) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a non-negative number");
|
|
3444
3691
|
}
|
|
3445
3692
|
if (config.options !== void 0) {
|
|
3446
|
-
if (typeof config.options !== "object" || Array.isArray(config.options)) throw
|
|
3693
|
+
if (typeof config.options !== "object" || Array.isArray(config.options)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.options must be an object");
|
|
3447
3694
|
const opts = config.options;
|
|
3448
3695
|
for (const key of ["maxPoolSize", "minPoolSize", "maxIdleTimeMS", "waitQueueTimeoutMS", "connectTimeoutMS", "serverSelectionTimeoutMS"]) {
|
|
3449
3696
|
if (opts[key] !== void 0 && (typeof opts[key] !== "number" || opts[key] < 0)) {
|
|
3450
|
-
throw
|
|
3697
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.options.${key} must be a non-negative number`);
|
|
3451
3698
|
}
|
|
3452
3699
|
}
|
|
3453
3700
|
}
|
|
3454
3701
|
if (config.healthCheck !== void 0) {
|
|
3455
|
-
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw
|
|
3702
|
+
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck must be an object");
|
|
3456
3703
|
const hc = config.healthCheck;
|
|
3457
|
-
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw
|
|
3704
|
+
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck.enabled must be a boolean");
|
|
3458
3705
|
for (const key of ["interval", "timeout", "retries"]) {
|
|
3459
3706
|
if (hc[key] !== void 0 && (typeof hc[key] !== "number" || hc[key] < 0)) {
|
|
3460
|
-
throw
|
|
3707
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.healthCheck.${key} must be a non-negative number`);
|
|
3461
3708
|
}
|
|
3462
3709
|
}
|
|
3463
3710
|
}
|
|
3464
3711
|
if (config.tags !== void 0) {
|
|
3465
|
-
if (!Array.isArray(config.tags)) throw
|
|
3712
|
+
if (!Array.isArray(config.tags)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array");
|
|
3466
3713
|
for (const tag of config.tags) {
|
|
3467
|
-
if (typeof tag !== "string") throw
|
|
3714
|
+
if (typeof tag !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array of strings");
|
|
3468
3715
|
}
|
|
3469
3716
|
}
|
|
3470
3717
|
}
|
|
@@ -3523,10 +3770,10 @@ function validatePoolConfigSafe(config) {
|
|
|
3523
3770
|
}
|
|
3524
3771
|
function validatePoolConfigInternal(config) {
|
|
3525
3772
|
if (!config.name?.trim()) {
|
|
3526
|
-
throw
|
|
3773
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty name");
|
|
3527
3774
|
}
|
|
3528
3775
|
if (!config.uri?.trim()) {
|
|
3529
|
-
throw
|
|
3776
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty uri");
|
|
3530
3777
|
}
|
|
3531
3778
|
}
|
|
3532
3779
|
function createEmptyPoolStats(name) {
|
|
@@ -3603,10 +3850,10 @@ var ConnectionPoolManager = class {
|
|
|
3603
3850
|
async addPool(config) {
|
|
3604
3851
|
validatePoolConfigInternal(config);
|
|
3605
3852
|
if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
|
|
3606
|
-
throw
|
|
3853
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3607
3854
|
}
|
|
3608
3855
|
if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
|
|
3609
|
-
throw
|
|
3856
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
|
|
3610
3857
|
}
|
|
3611
3858
|
this._pendingAdds.add(config.name);
|
|
3612
3859
|
try {
|
|
@@ -3614,7 +3861,7 @@ var ConnectionPoolManager = class {
|
|
|
3614
3861
|
if (this.pools.has(config.name)) {
|
|
3615
3862
|
await client.close().catch(() => {
|
|
3616
3863
|
});
|
|
3617
|
-
throw
|
|
3864
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3618
3865
|
}
|
|
3619
3866
|
this.pools.set(config.name, {
|
|
3620
3867
|
client,
|
|
@@ -3653,7 +3900,7 @@ var ConnectionPoolManager = class {
|
|
|
3653
3900
|
async removePool(name) {
|
|
3654
3901
|
const pool = this.pools.get(name);
|
|
3655
3902
|
if (!pool) {
|
|
3656
|
-
throw
|
|
3903
|
+
throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${name}' not found`);
|
|
3657
3904
|
}
|
|
3658
3905
|
this.stopHealthCheck(name);
|
|
3659
3906
|
await pool.client.close();
|
|
@@ -3677,27 +3924,28 @@ var ConnectionPoolManager = class {
|
|
|
3677
3924
|
selectPool(operation, options = {}) {
|
|
3678
3925
|
if (options.pool) {
|
|
3679
3926
|
const poolData2 = this.pools.get(options.pool);
|
|
3680
|
-
if (!poolData2) throw
|
|
3927
|
+
if (!poolData2) throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${options.pool}' not found`);
|
|
3681
3928
|
return this._createPoolResult(options.pool, poolData2.client);
|
|
3682
3929
|
}
|
|
3683
3930
|
let candidates = this._getHealthyPools();
|
|
3684
3931
|
if (candidates.length === 0) {
|
|
3685
3932
|
if (!this.fallback.enabled) {
|
|
3686
|
-
throw
|
|
3933
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3687
3934
|
}
|
|
3688
3935
|
candidates = this._handleAllPoolsDown(operation);
|
|
3689
3936
|
if (candidates.length === 0) {
|
|
3690
|
-
throw
|
|
3937
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3691
3938
|
}
|
|
3692
3939
|
}
|
|
3940
|
+
const poolPreference = options.poolPreference ?? (options.tags?.length ? { tags: options.tags } : void 0);
|
|
3693
3941
|
const poolName = this._selector.select(candidates, {
|
|
3694
3942
|
operation,
|
|
3695
3943
|
stats: this._stats.getAllStats(),
|
|
3696
|
-
poolPreference
|
|
3944
|
+
poolPreference
|
|
3697
3945
|
});
|
|
3698
3946
|
const poolData = this.pools.get(poolName);
|
|
3699
3947
|
if (!poolData) {
|
|
3700
|
-
throw
|
|
3948
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Selected pool '${poolName}' not available`);
|
|
3701
3949
|
}
|
|
3702
3950
|
this._stats.recordSelection(poolName, operation);
|
|
3703
3951
|
this.recordSelection(poolName, true);
|
|
@@ -3820,10 +4068,12 @@ var ConnectionPoolManager = class {
|
|
|
3820
4068
|
_getHealthyPools() {
|
|
3821
4069
|
const result = [];
|
|
3822
4070
|
for (const [name, config] of this._configs.entries()) {
|
|
3823
|
-
const
|
|
3824
|
-
|
|
3825
|
-
|
|
4071
|
+
const compatStatus = this._healthChecker.getStatus(name);
|
|
4072
|
+
const publicStatus = this.healthStatus.get(name);
|
|
4073
|
+
if (compatStatus?.status === "down" || publicStatus?.status === "down") {
|
|
4074
|
+
continue;
|
|
3826
4075
|
}
|
|
4076
|
+
result.push(config);
|
|
3827
4077
|
}
|
|
3828
4078
|
return result;
|
|
3829
4079
|
}
|
|
@@ -3976,7 +4226,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
|
|
|
3976
4226
|
logger
|
|
3977
4227
|
});
|
|
3978
4228
|
} catch (err) {
|
|
3979
|
-
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014
|
|
4229
|
+
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
|
|
3980
4230
|
return null;
|
|
3981
4231
|
}
|
|
3982
4232
|
}
|
|
@@ -3984,7 +4234,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
3984
4234
|
const modelsConfig = options.models;
|
|
3985
4235
|
if (!modelsConfig) return;
|
|
3986
4236
|
if (typeof modelsConfig !== "string" && typeof modelsConfig !== "object") return;
|
|
3987
|
-
const { readdirSync } = await import("node:fs");
|
|
4237
|
+
const { readdirSync: readdirSync2 } = await import("node:fs");
|
|
3988
4238
|
const { resolve, join, isAbsolute } = await import("node:path");
|
|
3989
4239
|
const { createRequire } = await import("node:module");
|
|
3990
4240
|
let targetPath;
|
|
@@ -4008,7 +4258,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
4008
4258
|
const collectFiles = (dir) => {
|
|
4009
4259
|
let entries;
|
|
4010
4260
|
try {
|
|
4011
|
-
entries =
|
|
4261
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
4012
4262
|
} catch {
|
|
4013
4263
|
logger.warn?.(`[Models] cannot read directory: ${dir}`);
|
|
4014
4264
|
return [];
|
|
@@ -4487,7 +4737,7 @@ async function indexStatsForAccessor(collectionRef) {
|
|
|
4487
4737
|
}
|
|
4488
4738
|
async function setValidatorForAccessor(collectionRef, collectionName, dbRef, validator, options = {}) {
|
|
4489
4739
|
if (validator === null || typeof validator !== "object") {
|
|
4490
|
-
throw
|
|
4740
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Validator must be a non-null object");
|
|
4491
4741
|
}
|
|
4492
4742
|
const isEmptyValidator = Object.keys(validator).length === 0;
|
|
4493
4743
|
const command = {
|
|
@@ -4508,14 +4758,14 @@ async function setValidatorForAccessor(collectionRef, collectionName, dbRef, val
|
|
|
4508
4758
|
}
|
|
4509
4759
|
async function setValidationLevelForAccessor(collectionRef, collectionName, dbRef, level) {
|
|
4510
4760
|
if (typeof level !== "string" || !["off", "strict", "moderate"].includes(level)) {
|
|
4511
|
-
throw
|
|
4761
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation level: must be "off", "strict", or "moderate"');
|
|
4512
4762
|
}
|
|
4513
4763
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationLevel: level });
|
|
4514
4764
|
return { ok: result["ok"], validationLevel: level };
|
|
4515
4765
|
}
|
|
4516
4766
|
async function setValidationActionForAccessor(collectionRef, collectionName, dbRef, action) {
|
|
4517
4767
|
if (typeof action !== "string" || !["error", "warn"].includes(action)) {
|
|
4518
|
-
throw
|
|
4768
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation action: must be "error" or "warn"');
|
|
4519
4769
|
}
|
|
4520
4770
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationAction: action });
|
|
4521
4771
|
return { ok: result["ok"], validationAction: action };
|
|
@@ -4547,14 +4797,14 @@ async function statsForAccessor(collectionRef, dbName, collectionName, options =
|
|
|
4547
4797
|
}
|
|
4548
4798
|
async function renameCollectionForAccessor(collectionRef, collectionName, newName, options = {}) {
|
|
4549
4799
|
if (!newName || typeof newName !== "string") {
|
|
4550
|
-
throw
|
|
4800
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "New collection name is required and must be a non-empty string");
|
|
4551
4801
|
}
|
|
4552
4802
|
await collectionRef.rename(newName, { dropTarget: options.dropTarget ?? false });
|
|
4553
4803
|
return { renamed: true, from: collectionName, to: newName };
|
|
4554
4804
|
}
|
|
4555
4805
|
async function collModForAccessor(collectionRef, collectionName, dbRef, modifications) {
|
|
4556
4806
|
if (modifications === null || typeof modifications !== "object") {
|
|
4557
|
-
throw
|
|
4807
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Modifications must be a non-null object");
|
|
4558
4808
|
}
|
|
4559
4809
|
return resolveDb(collectionRef, dbRef).command({
|
|
4560
4810
|
collMod: collectionName,
|
|
@@ -4563,10 +4813,10 @@ async function collModForAccessor(collectionRef, collectionName, dbRef, modifica
|
|
|
4563
4813
|
}
|
|
4564
4814
|
async function convertToCappedForAccessor(collectionRef, collectionName, dbRef, size, options = {}) {
|
|
4565
4815
|
if (typeof size !== "number") {
|
|
4566
|
-
throw
|
|
4816
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a number");
|
|
4567
4817
|
}
|
|
4568
4818
|
if (size <= 0) {
|
|
4569
|
-
throw
|
|
4819
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a positive number");
|
|
4570
4820
|
}
|
|
4571
4821
|
const command = { convertToCapped: collectionName, size };
|
|
4572
4822
|
if (options.max !== void 0) {
|
|
@@ -4747,7 +4997,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4747
4997
|
}
|
|
4748
4998
|
case "REDUCE": {
|
|
4749
4999
|
const lambdaMatch = /\((\w+),\s*(\w+)\)\s*=>\s*(.+)/.exec(args[2]);
|
|
4750
|
-
if (!lambdaMatch) throw
|
|
5000
|
+
if (!lambdaMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "REDUCE requires a lambda: (acc, item) => expr");
|
|
4751
5001
|
const [, accVar, itemVar, lambdaExpr] = lambdaMatch;
|
|
4752
5002
|
const compiledExpr = lambdaExpr.replace(new RegExp(`\\b${accVar}\\b`, "g"), "$$value").replace(new RegExp(`\\b${itemVar}\\b`, "g"), "$$this");
|
|
4753
5003
|
return { $reduce: { input: parseValue(args[0]), initialValue: parseValue(args[1]), in: compileInnerExpression(compiledExpr) } };
|
|
@@ -4892,7 +5142,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4892
5142
|
return { $setUnion: unionArgs };
|
|
4893
5143
|
}
|
|
4894
5144
|
case "SWITCH": {
|
|
4895
|
-
if (args.length < 2) throw
|
|
5145
|
+
if (args.length < 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "SWITCH requires at least 2 arguments");
|
|
4896
5146
|
const branches = [];
|
|
4897
5147
|
let defaultValue = null;
|
|
4898
5148
|
for (let index = 0; index < args.length - 1; index += 2) {
|
|
@@ -4910,15 +5160,15 @@ function dispatchFunction(name, argsStr) {
|
|
|
4910
5160
|
case "ANY_ELEMENT_TRUE":
|
|
4911
5161
|
return { $anyElementTrue: [parseValue(args[0])] };
|
|
4912
5162
|
case "COND": {
|
|
4913
|
-
if (args.length !== 3) throw
|
|
5163
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "COND requires 3 arguments");
|
|
4914
5164
|
return { $cond: { if: compileInnerExpression(args[0]), then: parseValue(args[1]), else: parseValue(args[2]) } };
|
|
4915
5165
|
}
|
|
4916
5166
|
case "IF_NULL": {
|
|
4917
|
-
if (args.length !== 2) throw
|
|
5167
|
+
if (args.length !== 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "IF_NULL requires 2 arguments");
|
|
4918
5168
|
return { $ifNull: [parseValue(args[0]), parseValue(args[1])] };
|
|
4919
5169
|
}
|
|
4920
5170
|
case "SET_FIELD": {
|
|
4921
|
-
if (args.length !== 3) throw
|
|
5171
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "SET_FIELD requires 3 arguments: (field, value, input)");
|
|
4922
5172
|
return { $setField: { field: parseValue(args[0]), input: parseValue(args[2]), value: parseValue(args[1]) } };
|
|
4923
5173
|
}
|
|
4924
5174
|
case "UNSET_FIELD":
|
|
@@ -4935,7 +5185,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4935
5185
|
return { $setIsSubset: [parseValue(args[0]), parseValue(args[1])] };
|
|
4936
5186
|
case "LET": {
|
|
4937
5187
|
const varsMatch = /\{(.+)\}/.exec(args[0]);
|
|
4938
|
-
if (!varsMatch) throw
|
|
5188
|
+
if (!varsMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "LET requires an object literal for variables");
|
|
4939
5189
|
const varPairs = varsMatch[1].split(",").map((pair) => {
|
|
4940
5190
|
const [key, ...rest] = pair.split(":");
|
|
4941
5191
|
return [key.trim(), rest.join(":").trim()];
|
|
@@ -4953,7 +5203,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4953
5203
|
case "SAMPLE_RATE":
|
|
4954
5204
|
return { $sampleRate: parseValue(args[0]) };
|
|
4955
5205
|
default:
|
|
4956
|
-
throw
|
|
5206
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, `Unsupported function: ${name}`);
|
|
4957
5207
|
}
|
|
4958
5208
|
}
|
|
4959
5209
|
function compileFilterCondition(condition, varName) {
|
|
@@ -5280,7 +5530,7 @@ function decodeCursor(cursor, secret) {
|
|
|
5280
5530
|
}
|
|
5281
5531
|
const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
5282
5532
|
if (payload?.v !== 1 || !Array.isArray(payload.values)) {
|
|
5283
|
-
throw
|
|
5533
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Invalid cursor payload.");
|
|
5284
5534
|
}
|
|
5285
5535
|
return payload.values;
|
|
5286
5536
|
} catch (cause) {
|
|
@@ -5379,6 +5629,7 @@ function buildEffectiveProjection(projection, sort) {
|
|
|
5379
5629
|
}
|
|
5380
5630
|
|
|
5381
5631
|
// src/adapters/mongodb/queries/find-page.ts
|
|
5632
|
+
var import_node_crypto4 = require("node:crypto");
|
|
5382
5633
|
function normalizePositiveInteger(value, fallback, field) {
|
|
5383
5634
|
if (value === void 0 || value === null) {
|
|
5384
5635
|
return fallback;
|
|
@@ -5397,54 +5648,192 @@ function mergeFilters(base, extra) {
|
|
|
5397
5648
|
}
|
|
5398
5649
|
return { $and: [base, extra] };
|
|
5399
5650
|
}
|
|
5651
|
+
function stableStringify2(value) {
|
|
5652
|
+
if (value === void 0) {
|
|
5653
|
+
return '"__undefined__"';
|
|
5654
|
+
}
|
|
5655
|
+
if (value === null) {
|
|
5656
|
+
return "null";
|
|
5657
|
+
}
|
|
5658
|
+
if (Array.isArray(value)) {
|
|
5659
|
+
return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
5660
|
+
}
|
|
5661
|
+
if (value instanceof Date) {
|
|
5662
|
+
return JSON.stringify(value.toISOString());
|
|
5663
|
+
}
|
|
5664
|
+
if (typeof value === "object") {
|
|
5665
|
+
const customJson = value.toJSON;
|
|
5666
|
+
if (typeof customJson === "function" && value.constructor?.name !== "Object") {
|
|
5667
|
+
return stableStringify2(customJson.call(value));
|
|
5668
|
+
}
|
|
5669
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`);
|
|
5670
|
+
return `{${entries.join(",")}}`;
|
|
5671
|
+
}
|
|
5672
|
+
return JSON.stringify(value);
|
|
5673
|
+
}
|
|
5674
|
+
function hashPayload(payload) {
|
|
5675
|
+
return (0, import_node_crypto4.createHash)("sha256").update(stableStringify2(payload)).digest("hex");
|
|
5676
|
+
}
|
|
5677
|
+
function buildFindPageCacheKey(collection, options, normalized) {
|
|
5678
|
+
const payload = {
|
|
5679
|
+
query: normalized.query,
|
|
5680
|
+
sort: normalized.sort,
|
|
5681
|
+
limit: normalized.limit,
|
|
5682
|
+
page: normalized.page,
|
|
5683
|
+
after: options.after,
|
|
5684
|
+
before: options.before,
|
|
5685
|
+
projection: options.projection,
|
|
5686
|
+
pipeline: options.pipeline ?? [],
|
|
5687
|
+
totals: options.totals,
|
|
5688
|
+
jump: options.jump,
|
|
5689
|
+
offsetJump: options.offsetJump,
|
|
5690
|
+
maxTimeMS: normalized.maxTimeMS,
|
|
5691
|
+
hint: options.hint,
|
|
5692
|
+
collation: options.collation,
|
|
5693
|
+
batchSize: options.batchSize,
|
|
5694
|
+
options: options.options
|
|
5695
|
+
};
|
|
5696
|
+
const keyHash = hashPayload(payload);
|
|
5697
|
+
return { key: `findPage:${collection.namespace}:${keyHash}`, keyHash };
|
|
5698
|
+
}
|
|
5699
|
+
function buildTotalsCacheKey(collection, query, limit, totals) {
|
|
5700
|
+
const payload = {
|
|
5701
|
+
query,
|
|
5702
|
+
limit,
|
|
5703
|
+
mode: totals.mode ?? "sync",
|
|
5704
|
+
hint: totals.hint,
|
|
5705
|
+
collation: totals.collation,
|
|
5706
|
+
maxTimeMS: totals.maxTimeMS
|
|
5707
|
+
};
|
|
5708
|
+
const token = hashPayload(payload);
|
|
5709
|
+
return { key: `findPageTotals:${collection.namespace}:${token}`, token };
|
|
5710
|
+
}
|
|
5711
|
+
function cloneFindPageResult(result) {
|
|
5712
|
+
return {
|
|
5713
|
+
...result,
|
|
5714
|
+
items: Array.isArray(result.items) ? [...result.items] : result.items,
|
|
5715
|
+
pageInfo: result.pageInfo && typeof result.pageInfo === "object" ? { ...result.pageInfo } : result.pageInfo,
|
|
5716
|
+
totals: result.totals && typeof result.totals === "object" ? { ...result.totals } : result.totals,
|
|
5717
|
+
meta: result.meta && typeof result.meta === "object" ? {
|
|
5718
|
+
...result.meta,
|
|
5719
|
+
ns: { ...result.meta.ns },
|
|
5720
|
+
steps: result.meta.steps ? [...result.meta.steps] : void 0
|
|
5721
|
+
} : result.meta
|
|
5722
|
+
};
|
|
5723
|
+
}
|
|
5724
|
+
function getPositiveTtl(value, fallback) {
|
|
5725
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
5726
|
+
}
|
|
5400
5727
|
var _asyncTotalsCache = new MemoryCache({
|
|
5401
5728
|
maxEntries: 1e4,
|
|
5402
5729
|
enableStats: false
|
|
5403
5730
|
});
|
|
5404
|
-
|
|
5731
|
+
var _totalsInflight = /* @__PURE__ */ new Map();
|
|
5732
|
+
function runTotalsOnce(key, task) {
|
|
5733
|
+
if (_totalsInflight.has(key)) {
|
|
5734
|
+
return;
|
|
5735
|
+
}
|
|
5736
|
+
const promise = task().catch(() => {
|
|
5737
|
+
}).finally(() => {
|
|
5738
|
+
_totalsInflight.delete(key);
|
|
5739
|
+
});
|
|
5740
|
+
_totalsInflight.set(key, promise);
|
|
5741
|
+
}
|
|
5742
|
+
async function computeTotals(coll, query, limit, totals, defaults = {}, queryCache) {
|
|
5405
5743
|
const mode = totals.mode ?? "sync";
|
|
5406
|
-
|
|
5744
|
+
const cache = queryCache ?? _asyncTotalsCache;
|
|
5745
|
+
const ttlMs = getPositiveTtl(totals.ttlMs, 10 * 6e4);
|
|
5746
|
+
const { key: cacheKey, token } = buildTotalsCacheKey(coll, query, limit, totals);
|
|
5747
|
+
const buildCountOptions = (fallbackMaxTimeMS) => {
|
|
5407
5748
|
const countOpts = {};
|
|
5408
|
-
|
|
5409
|
-
|
|
5749
|
+
const maxTimeMS = totals.maxTimeMS ?? fallbackMaxTimeMS;
|
|
5750
|
+
if (maxTimeMS !== void 0) {
|
|
5751
|
+
countOpts.maxTimeMS = maxTimeMS;
|
|
5752
|
+
}
|
|
5753
|
+
if (totals.hint !== void 0) {
|
|
5754
|
+
countOpts.hint = totals.hint;
|
|
5755
|
+
}
|
|
5756
|
+
if (totals.collation !== void 0) {
|
|
5757
|
+
countOpts.collation = totals.collation;
|
|
5410
5758
|
}
|
|
5411
|
-
|
|
5412
|
-
|
|
5759
|
+
return countOpts;
|
|
5760
|
+
};
|
|
5761
|
+
const countWithOptions = async () => {
|
|
5762
|
+
const countOpts = buildCountOptions(2e3);
|
|
5763
|
+
const countQuery = query;
|
|
5764
|
+
const runner = () => coll.countDocuments(
|
|
5765
|
+
countQuery,
|
|
5413
5766
|
countOpts
|
|
5414
5767
|
);
|
|
5415
|
-
|
|
5416
|
-
|
|
5768
|
+
return defaults.countQueue ? defaults.countQueue.execute(runner) : runner();
|
|
5769
|
+
};
|
|
5770
|
+
const buildPayload = (total, approx = false) => ({
|
|
5771
|
+
mode,
|
|
5772
|
+
total,
|
|
5773
|
+
totalPages: total > 0 ? Math.ceil(total / limit) : 0,
|
|
5774
|
+
ts: Date.now(),
|
|
5775
|
+
...approx ? { approx: true } : {}
|
|
5776
|
+
});
|
|
5777
|
+
const buildFailurePayload = (error) => ({
|
|
5778
|
+
mode,
|
|
5779
|
+
total: null,
|
|
5780
|
+
totalPages: null,
|
|
5781
|
+
ts: Date.now(),
|
|
5782
|
+
error
|
|
5783
|
+
});
|
|
5784
|
+
if (mode === "sync") {
|
|
5785
|
+
const cached = cache.get(cacheKey);
|
|
5786
|
+
if (cached !== void 0) {
|
|
5787
|
+
return { ...cached, mode: "sync" };
|
|
5788
|
+
}
|
|
5789
|
+
try {
|
|
5790
|
+
const payload = buildPayload(await countWithOptions());
|
|
5791
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5792
|
+
return { ...payload, mode: "sync" };
|
|
5793
|
+
} catch {
|
|
5794
|
+
const payload = buildFailurePayload("count_failed");
|
|
5795
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5796
|
+
return { ...payload, mode: "sync" };
|
|
5797
|
+
}
|
|
5417
5798
|
}
|
|
5418
5799
|
if (mode === "async") {
|
|
5419
|
-
const
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
if (cachedTotal !== void 0) {
|
|
5423
|
-
return { mode: "async", total: cachedTotal, token };
|
|
5800
|
+
const cached = cache.get(cacheKey);
|
|
5801
|
+
if (cached !== void 0) {
|
|
5802
|
+
return { ...cached, mode: "async", token };
|
|
5424
5803
|
}
|
|
5425
|
-
setImmediate(
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5804
|
+
setImmediate(() => {
|
|
5805
|
+
runTotalsOnce(cacheKey, async () => {
|
|
5806
|
+
try {
|
|
5807
|
+
const payload = buildPayload(await countWithOptions());
|
|
5808
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5809
|
+
} catch {
|
|
5810
|
+
await Promise.resolve(cache.set(cacheKey, buildFailurePayload("count_failed"), ttlMs));
|
|
5811
|
+
}
|
|
5812
|
+
});
|
|
5433
5813
|
});
|
|
5434
5814
|
return { mode: "async", total: null, token };
|
|
5435
5815
|
}
|
|
5436
5816
|
if (mode === "approx") {
|
|
5437
|
-
const
|
|
5438
|
-
if (
|
|
5439
|
-
|
|
5817
|
+
const cached = cache.get(cacheKey);
|
|
5818
|
+
if (cached !== void 0) {
|
|
5819
|
+
return { ...cached, mode: "approx" };
|
|
5820
|
+
}
|
|
5821
|
+
try {
|
|
5822
|
+
const total = Object.keys(query ?? {}).length > 0 ? await countWithOptions() : await coll.estimatedDocumentCount({
|
|
5823
|
+
maxTimeMS: totals.maxTimeMS ?? 1e3
|
|
5824
|
+
});
|
|
5825
|
+
const payload = buildPayload(total, true);
|
|
5826
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5827
|
+
return { ...payload, mode: "approx" };
|
|
5828
|
+
} catch {
|
|
5829
|
+
const payload = buildFailurePayload("approx_failed");
|
|
5830
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5831
|
+
return { ...payload, mode: "approx" };
|
|
5440
5832
|
}
|
|
5441
|
-
const total = await coll.estimatedDocumentCount(countOpts);
|
|
5442
|
-
const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
|
|
5443
|
-
return { mode: "approx", total, totalPages, ts: Date.now() };
|
|
5444
5833
|
}
|
|
5445
5834
|
return { mode: mode ?? "sync" };
|
|
5446
5835
|
}
|
|
5447
|
-
async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
5836
|
+
async function executeFindPage(collection, options = {}, defaults = {}, queryCache) {
|
|
5448
5837
|
const metaEnabled = options.meta === true || typeof options.meta === "object" && options.meta !== null;
|
|
5449
5838
|
const metaOptions = options.meta && typeof options.meta === "object" ? options.meta : {};
|
|
5450
5839
|
const metaLevel = options.meta === true ? "op" : metaOptions.level ?? "op";
|
|
@@ -5475,6 +5864,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5475
5864
|
driverOpts.projection = buildEffectiveProjection(options.projection, sort);
|
|
5476
5865
|
}
|
|
5477
5866
|
const jumpOpts = ext.jump;
|
|
5867
|
+
const cacheTTL = typeof ext.cache === "number" && ext.cache > 0 ? ext.cache : 0;
|
|
5868
|
+
const pageResultCache = cacheTTL > 0 && queryCache && options.stream !== true && (options.explain === void 0 || options.explain === false) ? buildFindPageCacheKey(collection, options, { query: baseQuery, sort, limit, page, maxTimeMS: effectiveMaxTimeMS }) : null;
|
|
5869
|
+
const shouldRefreshAsyncTotals = options.totals?.mode === "async";
|
|
5870
|
+
let findPageCacheHit = false;
|
|
5478
5871
|
const finishResult = (result2) => {
|
|
5479
5872
|
if (!metaEnabled) {
|
|
5480
5873
|
return result2;
|
|
@@ -5505,6 +5898,9 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5505
5898
|
endTs: metaEndTs,
|
|
5506
5899
|
durationMs: metaEndTs - metaStartTs,
|
|
5507
5900
|
...typeof effectiveMaxTimeMS === "number" ? { maxTimeMS: effectiveMaxTimeMS } : {},
|
|
5901
|
+
cacheHit: findPageCacheHit,
|
|
5902
|
+
...findPageCacheHit ? { fromCache: true } : {},
|
|
5903
|
+
...pageResultCache ? { cacheTtl: cacheTTL, keyHash: pageResultCache.keyHash } : {},
|
|
5508
5904
|
page,
|
|
5509
5905
|
after: Boolean(options.after),
|
|
5510
5906
|
before: Boolean(options.before),
|
|
@@ -5573,7 +5969,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5573
5969
|
};
|
|
5574
5970
|
const timedComputeTotals = async () => {
|
|
5575
5971
|
const stepStartTs = Date.now();
|
|
5576
|
-
const result2 = await computeTotals(collection, baseQuery, limit, options.totals);
|
|
5972
|
+
const result2 = await computeTotals(collection, baseQuery, limit, options.totals, defaults, queryCache);
|
|
5577
5973
|
pushMetaStep("computeTotals", Date.now() - stepStartTs, "totals");
|
|
5578
5974
|
return result2;
|
|
5579
5975
|
};
|
|
@@ -5592,6 +5988,32 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5592
5988
|
...extra.currentPage !== void 0 ? { currentPage: extra.currentPage } : {}
|
|
5593
5989
|
};
|
|
5594
5990
|
};
|
|
5991
|
+
const writePageResultCache = (result2) => {
|
|
5992
|
+
if (!pageResultCache || !queryCache) {
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
const cacheValue = cloneFindPageResult(result2);
|
|
5996
|
+
delete cacheValue.meta;
|
|
5997
|
+
if (shouldRefreshAsyncTotals) {
|
|
5998
|
+
delete cacheValue.totals;
|
|
5999
|
+
}
|
|
6000
|
+
void queryCache.set(pageResultCache.key, cacheValue, cacheTTL);
|
|
6001
|
+
};
|
|
6002
|
+
const finishAndCache = (result2) => {
|
|
6003
|
+
writePageResultCache(result2);
|
|
6004
|
+
return finishResult(result2);
|
|
6005
|
+
};
|
|
6006
|
+
if (pageResultCache && queryCache) {
|
|
6007
|
+
const cached = queryCache.get(pageResultCache.key);
|
|
6008
|
+
if (cached !== void 0) {
|
|
6009
|
+
findPageCacheHit = true;
|
|
6010
|
+
const result2 = cloneFindPageResult(cached);
|
|
6011
|
+
if (options.totals && options.totals.mode !== "none" && (shouldRefreshAsyncTotals || result2.totals === void 0)) {
|
|
6012
|
+
result2.totals = await timedComputeTotals();
|
|
6013
|
+
}
|
|
6014
|
+
return finishResult(result2);
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
5595
6017
|
if (options.stream === true) {
|
|
5596
6018
|
const direction = options.before ? "before" : "after";
|
|
5597
6019
|
const { queryFilter, effectiveSort } = buildPageQuery(options.after ?? options.before, direction);
|
|
@@ -5634,7 +6056,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5634
6056
|
if (options.totals && options.totals.mode !== "none") {
|
|
5635
6057
|
result2.totals = await timedComputeTotals();
|
|
5636
6058
|
}
|
|
5637
|
-
return
|
|
6059
|
+
return finishAndCache(result2);
|
|
5638
6060
|
}
|
|
5639
6061
|
if (options.after || options.before) {
|
|
5640
6062
|
const direction = options.after ? "after" : "before";
|
|
@@ -5644,7 +6066,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5644
6066
|
const first = items2[0] ?? null;
|
|
5645
6067
|
const last = items2[items2.length - 1] ?? null;
|
|
5646
6068
|
const enc = (item) => item ? encodeCursor(Object.keys(sort).map((f) => item[f]), cursorSecret) : null;
|
|
5647
|
-
|
|
6069
|
+
const result2 = {
|
|
5648
6070
|
items: items2,
|
|
5649
6071
|
pageInfo: {
|
|
5650
6072
|
hasNext: direction === "before" ? Boolean(options.before) : hasMore2,
|
|
@@ -5652,7 +6074,11 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5652
6074
|
startCursor: enc(first),
|
|
5653
6075
|
endCursor: enc(last)
|
|
5654
6076
|
}
|
|
5655
|
-
}
|
|
6077
|
+
};
|
|
6078
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6079
|
+
result2.totals = await timedComputeTotals();
|
|
6080
|
+
}
|
|
6081
|
+
return finishAndCache(result2);
|
|
5656
6082
|
}
|
|
5657
6083
|
const { queryFilter: q0, effectiveSort: es0 } = buildPageQuery();
|
|
5658
6084
|
let { items, hasMore } = await timedFetchItems("initialFetch", page > 1 ? "hop" : "fetch", q0, es0, {}, 1);
|
|
@@ -5664,15 +6090,19 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5664
6090
|
if (options.totals && options.totals.mode !== "none") {
|
|
5665
6091
|
result2.totals = await timedComputeTotals();
|
|
5666
6092
|
}
|
|
5667
|
-
return
|
|
6093
|
+
return finishAndCache(result2);
|
|
5668
6094
|
}
|
|
5669
6095
|
for (let cp = 2; cp <= page; cp++) {
|
|
5670
6096
|
const lastItem = items[items.length - 1];
|
|
5671
6097
|
if (!lastItem) {
|
|
5672
|
-
|
|
6098
|
+
const result2 = {
|
|
5673
6099
|
items,
|
|
5674
6100
|
pageInfo: buildPageInfo(items, false, { hasPrev: cp > 2, currentPage: cp - 1 })
|
|
5675
|
-
}
|
|
6101
|
+
};
|
|
6102
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6103
|
+
result2.totals = await timedComputeTotals();
|
|
6104
|
+
}
|
|
6105
|
+
return finishAndCache(result2);
|
|
5676
6106
|
}
|
|
5677
6107
|
const endCursor = encodeCursor(
|
|
5678
6108
|
Object.keys(sort).map((f) => lastItem[f]),
|
|
@@ -5690,10 +6120,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5690
6120
|
if (options.totals && options.totals.mode !== "none") {
|
|
5691
6121
|
result.totals = await timedComputeTotals();
|
|
5692
6122
|
}
|
|
5693
|
-
return
|
|
6123
|
+
return finishAndCache(result);
|
|
5694
6124
|
}
|
|
5695
|
-
async function findPageDocuments(collection, options = {}, defaults) {
|
|
5696
|
-
return executeFindPage(collection, options, defaults ?? {});
|
|
6125
|
+
async function findPageDocuments(collection, options = {}, defaults, queryCache) {
|
|
6126
|
+
return executeFindPage(collection, options, defaults ?? {}, queryCache);
|
|
5697
6127
|
}
|
|
5698
6128
|
|
|
5699
6129
|
// src/adapters/mongodb/queries/find-by-id.ts
|
|
@@ -5915,21 +6345,21 @@ var FindChain = class {
|
|
|
5915
6345
|
}
|
|
5916
6346
|
limit(value) {
|
|
5917
6347
|
if (typeof value !== "number" || value < 0) {
|
|
5918
|
-
throw
|
|
6348
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `limit() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5919
6349
|
}
|
|
5920
6350
|
this.options.limit = value;
|
|
5921
6351
|
return this;
|
|
5922
6352
|
}
|
|
5923
6353
|
skip(value) {
|
|
5924
6354
|
if (typeof value !== "number" || value < 0) {
|
|
5925
|
-
throw
|
|
6355
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `skip() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5926
6356
|
}
|
|
5927
6357
|
this.options.skip = value;
|
|
5928
6358
|
return this;
|
|
5929
6359
|
}
|
|
5930
6360
|
sort(value) {
|
|
5931
6361
|
if (!value || typeof value !== "object") {
|
|
5932
|
-
throw
|
|
6362
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `sort() requires an object or array, got: ${typeof value}`);
|
|
5933
6363
|
}
|
|
5934
6364
|
this.options.sort = value;
|
|
5935
6365
|
return this;
|
|
@@ -5973,7 +6403,7 @@ var FindChain = class {
|
|
|
5973
6403
|
}
|
|
5974
6404
|
toArray() {
|
|
5975
6405
|
if (this.executed) {
|
|
5976
|
-
throw
|
|
6406
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
5977
6407
|
}
|
|
5978
6408
|
this.executed = true;
|
|
5979
6409
|
return this.collection.find(this.normalizedQuery, buildFindDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6061,7 +6491,7 @@ var AggregateChain = class {
|
|
|
6061
6491
|
}
|
|
6062
6492
|
toArray() {
|
|
6063
6493
|
if (this.executed) {
|
|
6064
|
-
throw
|
|
6494
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
6065
6495
|
}
|
|
6066
6496
|
this.executed = true;
|
|
6067
6497
|
return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6856,7 +7286,7 @@ async function insertOneForAccessor(context, doc, options) {
|
|
|
6856
7286
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6857
7287
|
if (elapsed > threshold && context.logger) {
|
|
6858
7288
|
try {
|
|
6859
|
-
context.logger.warn("[insertOne]
|
|
7289
|
+
context.logger.warn("[insertOne] slow operation warning", {
|
|
6860
7290
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6861
7291
|
threshold,
|
|
6862
7292
|
duration: elapsed,
|
|
@@ -6901,7 +7331,7 @@ async function insertManyForAccessor(context, documents, options) {
|
|
|
6901
7331
|
const elapsed = Date.now() - startedAt;
|
|
6902
7332
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6903
7333
|
if (elapsed >= threshold && context.logger) {
|
|
6904
|
-
context.logger.warn("[insertMany]
|
|
7334
|
+
context.logger.warn("[insertMany] slow operation warning", {
|
|
6905
7335
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6906
7336
|
threshold,
|
|
6907
7337
|
duration: elapsed,
|
|
@@ -7032,11 +7462,17 @@ var MongoCollectionAccessor = class {
|
|
|
7032
7462
|
const legacyNamespacePatterns = [
|
|
7033
7463
|
`${String(this.management.defaults?.namespace?.instanceId)}:mongodb:${this.dbName}:${this.collectionName}:*`
|
|
7034
7464
|
];
|
|
7035
|
-
const
|
|
7465
|
+
const findPagePatterns = [
|
|
7466
|
+
`findPage:${namespace}:*`,
|
|
7467
|
+
`findPageTotals:${namespace}:*`,
|
|
7468
|
+
`bookmark:${bookmarkNamespace}:*`,
|
|
7469
|
+
`${bookmarkNamespace}:bm:*`
|
|
7470
|
+
];
|
|
7471
|
+
const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? findPagePatterns : [
|
|
7036
7472
|
`find:${namespace}:*`,
|
|
7037
7473
|
`findOne:${namespace}:*`,
|
|
7038
7474
|
`count:${namespace}:*`,
|
|
7039
|
-
|
|
7475
|
+
...findPagePatterns
|
|
7040
7476
|
];
|
|
7041
7477
|
patterns.push(...legacyNamespacePatterns);
|
|
7042
7478
|
let deleted = 0;
|
|
@@ -7166,7 +7602,7 @@ var MongoCollectionAccessor = class {
|
|
|
7166
7602
|
}
|
|
7167
7603
|
async findPage(options = {}) {
|
|
7168
7604
|
const resolvedOptions = options.query ? { ...options, query: this._cvFilter(options.query) } : options;
|
|
7169
|
-
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults);
|
|
7605
|
+
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults, this.management.queryCache);
|
|
7170
7606
|
}
|
|
7171
7607
|
/** Opens a change stream on the collection with an optional aggregation pipeline. */
|
|
7172
7608
|
watch(pipeline = [], options) {
|
|
@@ -7858,7 +8294,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7858
8294
|
dropDatabase: async (name, adminOptions) => {
|
|
7859
8295
|
host.ensureConnected();
|
|
7860
8296
|
if (!name || typeof name !== "string") {
|
|
7861
|
-
throw
|
|
8297
|
+
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name is required and must be a non-empty string");
|
|
7862
8298
|
}
|
|
7863
8299
|
if (!adminOptions?.confirm) {
|
|
7864
8300
|
const error = new Error(
|
|
@@ -7894,7 +8330,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7894
8330
|
runCommand: async (command, adminOptions) => {
|
|
7895
8331
|
host.ensureConnected();
|
|
7896
8332
|
if (command === null || typeof command !== "object") {
|
|
7897
|
-
throw
|
|
8333
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Command must be a non-null object");
|
|
7898
8334
|
}
|
|
7899
8335
|
return host.db().runCommand(command, adminOptions ?? {});
|
|
7900
8336
|
},
|
|
@@ -7910,7 +8346,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7910
8346
|
}
|
|
7911
8347
|
|
|
7912
8348
|
// src/capabilities/lock/index.ts
|
|
7913
|
-
var
|
|
8349
|
+
var import_node_crypto5 = require("node:crypto");
|
|
7914
8350
|
var LockAcquireError = class extends Error {
|
|
7915
8351
|
constructor(message) {
|
|
7916
8352
|
super(message);
|
|
@@ -8020,13 +8456,13 @@ var LockManager = class {
|
|
|
8020
8456
|
if (attempt === retryTimes) {
|
|
8021
8457
|
break;
|
|
8022
8458
|
}
|
|
8023
|
-
const
|
|
8024
|
-
await sleep3(
|
|
8459
|
+
const delay2 = retryDelay * Math.pow(retryBackoff, attempt);
|
|
8460
|
+
await sleep3(delay2);
|
|
8025
8461
|
}
|
|
8026
8462
|
this.stats.errors += 1;
|
|
8027
8463
|
if (options.fallbackToNoLock) {
|
|
8028
8464
|
this.logger?.warn?.(`[LockManager] fallback to no-lock execution for ${key}`);
|
|
8029
|
-
return new Lock(this.normalizeKey(key), `noop:${(0,
|
|
8465
|
+
return new Lock(this.normalizeKey(key), `noop:${(0, import_node_crypto5.randomUUID)()}`, new NoopLockManager(), options.ttl ?? 1e4);
|
|
8030
8466
|
}
|
|
8031
8467
|
throw new LockTimeoutError(`Failed to acquire lock for key '${key}' within retry budget.`);
|
|
8032
8468
|
}
|
|
@@ -8042,7 +8478,7 @@ var LockManager = class {
|
|
|
8042
8478
|
if (globalStore.has(normalizedKey)) {
|
|
8043
8479
|
return null;
|
|
8044
8480
|
}
|
|
8045
|
-
const lockId = (0,
|
|
8481
|
+
const lockId = (0, import_node_crypto5.randomUUID)();
|
|
8046
8482
|
globalStore.set(normalizedKey, {
|
|
8047
8483
|
lockId,
|
|
8048
8484
|
expiresAt: Date.now() + ttl
|
|
@@ -8145,7 +8581,7 @@ var DistributedCacheLockManager = class {
|
|
|
8145
8581
|
errors: 0
|
|
8146
8582
|
};
|
|
8147
8583
|
if (!options.redis) {
|
|
8148
|
-
throw
|
|
8584
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheLockManager requires a Redis instance");
|
|
8149
8585
|
}
|
|
8150
8586
|
this.redis = options.redis;
|
|
8151
8587
|
this.lockKeyPrefix = options.lockKeyPrefix ?? "monsqlize:cache:lock:";
|
|
@@ -8334,7 +8770,7 @@ var DistributedCacheLockManager = class {
|
|
|
8334
8770
|
};
|
|
8335
8771
|
|
|
8336
8772
|
// src/capabilities/saga/index.ts
|
|
8337
|
-
var
|
|
8773
|
+
var import_node_crypto6 = require("node:crypto");
|
|
8338
8774
|
var SagaExecutionContext = class {
|
|
8339
8775
|
constructor(executionId, data) {
|
|
8340
8776
|
this.executionId = executionId;
|
|
@@ -8430,7 +8866,7 @@ var SagaOrchestrator = class {
|
|
|
8430
8866
|
if (!definition) {
|
|
8431
8867
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `Saga '${name}' is not defined`);
|
|
8432
8868
|
}
|
|
8433
|
-
const sagaId = `saga_${(0,
|
|
8869
|
+
const sagaId = `saga_${(0, import_node_crypto6.randomBytes)(8).toString("hex")}`;
|
|
8434
8870
|
const startedAt = Date.now();
|
|
8435
8871
|
const context = new SagaExecutionContext(sagaId, data);
|
|
8436
8872
|
const completedSteps = [];
|
|
@@ -8803,17 +9239,17 @@ var BatchQueue = class {
|
|
|
8803
9239
|
var import_mongodb6 = require("mongodb");
|
|
8804
9240
|
|
|
8805
9241
|
// src/capabilities/slow-query-log/slow-query-log-records.ts
|
|
8806
|
-
var
|
|
8807
|
-
function
|
|
9242
|
+
var import_node_crypto7 = require("node:crypto");
|
|
9243
|
+
function stableStringify3(value) {
|
|
8808
9244
|
if (Array.isArray(value)) {
|
|
8809
|
-
return `[${value.map((item) =>
|
|
9245
|
+
return `[${value.map((item) => stableStringify3(item)).join(",")}]`;
|
|
8810
9246
|
}
|
|
8811
9247
|
if (value instanceof Date) {
|
|
8812
9248
|
return JSON.stringify(value.toISOString());
|
|
8813
9249
|
}
|
|
8814
9250
|
if (value && typeof value === "object") {
|
|
8815
9251
|
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
8816
|
-
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${
|
|
9252
|
+
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify3(current)}`).join(",")}}`;
|
|
8817
9253
|
}
|
|
8818
9254
|
return JSON.stringify(value);
|
|
8819
9255
|
}
|
|
@@ -8830,7 +9266,7 @@ function normalizeHashInput(input) {
|
|
|
8830
9266
|
};
|
|
8831
9267
|
}
|
|
8832
9268
|
function generateQueryHash(input) {
|
|
8833
|
-
return (0,
|
|
9269
|
+
return (0, import_node_crypto7.createHash)("sha256").update(stableStringify3(normalizeHashInput(input))).digest("hex").slice(0, 16);
|
|
8834
9270
|
}
|
|
8835
9271
|
function handleSlowQueryLogError(logger, policy, error) {
|
|
8836
9272
|
if (policy === "throw") {
|
|
@@ -9280,7 +9716,7 @@ var SlowQueryLogManager = class {
|
|
|
9280
9716
|
|
|
9281
9717
|
// src/capabilities/sync/index.ts
|
|
9282
9718
|
var import_promises = require("node:fs/promises");
|
|
9283
|
-
var
|
|
9719
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
9284
9720
|
var import_mongodb7 = require("mongodb");
|
|
9285
9721
|
function validateTargetConfig(target, index) {
|
|
9286
9722
|
if (!target || typeof target !== "object") {
|
|
@@ -9387,7 +9823,7 @@ var ResumeTokenStore = class {
|
|
|
9387
9823
|
await Promise.resolve(this.redis.set(this.redisKey, payload));
|
|
9388
9824
|
return;
|
|
9389
9825
|
}
|
|
9390
|
-
await (0, import_promises.mkdir)(
|
|
9826
|
+
await (0, import_promises.mkdir)(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
9391
9827
|
await (0, import_promises.writeFile)(this.path, payload, "utf8");
|
|
9392
9828
|
} catch (error) {
|
|
9393
9829
|
this.logger?.error?.("[Sync] failed to save resume token", error);
|
|
@@ -9659,7 +10095,16 @@ function getOrCreateTransactionManager(config) {
|
|
|
9659
10095
|
client: config.client,
|
|
9660
10096
|
cache: config.cache,
|
|
9661
10097
|
logger: config.logger,
|
|
9662
|
-
lockManager: config.lockManager
|
|
10098
|
+
lockManager: config.lockManager,
|
|
10099
|
+
maxDuration: config.transaction?.maxDuration ?? config.transaction?.defaultTimeout,
|
|
10100
|
+
enableRetry: config.transaction?.enableRetry,
|
|
10101
|
+
maxRetries: config.transaction?.maxRetries,
|
|
10102
|
+
retryDelay: config.transaction?.retryDelay,
|
|
10103
|
+
retryBackoff: config.transaction?.retryBackoff,
|
|
10104
|
+
defaultReadConcern: config.transaction?.defaultReadConcern,
|
|
10105
|
+
defaultWriteConcern: config.transaction?.defaultWriteConcern,
|
|
10106
|
+
defaultReadPreference: config.transaction?.defaultReadPreference,
|
|
10107
|
+
maxStatsSamples: config.transaction?.maxStatsSamples
|
|
9663
10108
|
});
|
|
9664
10109
|
}
|
|
9665
10110
|
function getOrCreateLockManager(current, logger) {
|
|
@@ -9946,7 +10391,7 @@ function resolveCacheSource(cacheOrDb) {
|
|
|
9946
10391
|
return cache;
|
|
9947
10392
|
}
|
|
9948
10393
|
}
|
|
9949
|
-
throw
|
|
10394
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance from MonSQLize");
|
|
9950
10395
|
}
|
|
9951
10396
|
function toWithCacheStats(stats, totalTime = 0) {
|
|
9952
10397
|
const calls = stats.hits + stats.misses;
|
|
@@ -9975,46 +10420,46 @@ function normalizeFunctionCacheStats(stats, timings, name) {
|
|
|
9975
10420
|
}
|
|
9976
10421
|
function validateFunctionCacheOptions(options) {
|
|
9977
10422
|
if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
|
|
9978
|
-
throw
|
|
10423
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
|
|
9979
10424
|
}
|
|
9980
10425
|
const opts = options ?? {};
|
|
9981
10426
|
if (opts.namespace !== void 0 && typeof opts.namespace !== "string") {
|
|
9982
|
-
throw
|
|
10427
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "namespace must be a string");
|
|
9983
10428
|
}
|
|
9984
10429
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
9985
10430
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
9986
|
-
throw
|
|
10431
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
9987
10432
|
}
|
|
9988
10433
|
return opts;
|
|
9989
10434
|
}
|
|
9990
10435
|
function validateFunctionCachePerFnOptions(options) {
|
|
9991
10436
|
if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
|
|
9992
|
-
throw
|
|
10437
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
|
|
9993
10438
|
}
|
|
9994
10439
|
const opts = options ?? {};
|
|
9995
10440
|
if (opts.keyBuilder !== void 0 && typeof opts.keyBuilder !== "function") {
|
|
9996
|
-
throw
|
|
10441
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
9997
10442
|
}
|
|
9998
10443
|
if (opts.condition !== void 0 && typeof opts.condition !== "function") {
|
|
9999
|
-
throw
|
|
10444
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
10000
10445
|
}
|
|
10001
10446
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
10002
10447
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
10003
|
-
throw
|
|
10448
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
10004
10449
|
}
|
|
10005
10450
|
return opts;
|
|
10006
10451
|
}
|
|
10007
10452
|
function withCache(fn, options = {}) {
|
|
10008
|
-
if (typeof fn !== "function") throw
|
|
10453
|
+
if (typeof fn !== "function") throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
10009
10454
|
const { ttl = 6e4, namespace, keyBuilder, condition, cache: externalCache, enableStats = true } = options;
|
|
10010
10455
|
if (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)
|
|
10011
|
-
throw
|
|
10456
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "ttl must be a non-negative number");
|
|
10012
10457
|
if (keyBuilder !== void 0 && typeof keyBuilder !== "function")
|
|
10013
|
-
throw
|
|
10458
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
10014
10459
|
if (condition !== void 0 && typeof condition !== "function")
|
|
10015
|
-
throw
|
|
10460
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
10016
10461
|
if (externalCache !== void 0 && !isValidCache(externalCache))
|
|
10017
|
-
throw
|
|
10462
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance");
|
|
10018
10463
|
const wrapped = (0, import_function_cache.withCache)(fn, {
|
|
10019
10464
|
ttl,
|
|
10020
10465
|
namespace: namespace ?? "fn",
|
|
@@ -10056,9 +10501,9 @@ var FunctionCache = class {
|
|
|
10056
10501
|
}
|
|
10057
10502
|
register(name, fn, options) {
|
|
10058
10503
|
if (!name || typeof name !== "string")
|
|
10059
|
-
throw
|
|
10504
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10060
10505
|
if (typeof fn !== "function")
|
|
10061
|
-
throw
|
|
10506
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
10062
10507
|
const opts = validateFunctionCachePerFnOptions(options);
|
|
10063
10508
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
10064
10509
|
this._inner.register(name, fn, {
|
|
@@ -10084,13 +10529,13 @@ var FunctionCache = class {
|
|
|
10084
10529
|
}
|
|
10085
10530
|
async invalidate(name, ...args) {
|
|
10086
10531
|
if (!name || typeof name !== "string") {
|
|
10087
|
-
throw
|
|
10532
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10088
10533
|
}
|
|
10089
10534
|
await this._inner.invalidate(name, ...args);
|
|
10090
10535
|
}
|
|
10091
10536
|
async invalidatePattern(pattern) {
|
|
10092
10537
|
if (!pattern || typeof pattern !== "string")
|
|
10093
|
-
throw
|
|
10538
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Pattern must be a non-empty string");
|
|
10094
10539
|
return this._inner.invalidatePattern(pattern);
|
|
10095
10540
|
}
|
|
10096
10541
|
list() {
|
|
@@ -10125,15 +10570,15 @@ function b64urlDecodeStr(s) {
|
|
|
10125
10570
|
const b64 = pad(String(s || "")).replace(/-/g, "+").replace(/_/g, "/");
|
|
10126
10571
|
return Buffer.from(b64, "base64").toString();
|
|
10127
10572
|
}
|
|
10128
|
-
function makeInvalidCursorError(cause) {
|
|
10129
|
-
const err = new Error(
|
|
10573
|
+
function makeInvalidCursorError(message = "Invalid cursor", cause) {
|
|
10574
|
+
const err = new Error(message);
|
|
10130
10575
|
err.code = "INVALID_CURSOR";
|
|
10131
10576
|
if (cause !== void 0) err.cause = cause;
|
|
10132
10577
|
return err;
|
|
10133
10578
|
}
|
|
10134
10579
|
function encodeCursor2(payload) {
|
|
10135
10580
|
if (!payload.s || !payload.a) {
|
|
10136
|
-
throw
|
|
10581
|
+
throw makeInvalidCursorError("encodeCursor requires sort (s) and anchor (a)");
|
|
10137
10582
|
}
|
|
10138
10583
|
const json = JSON.stringify({ v: payload.v ?? 1, s: payload.s, a: payload.a, d: payload.d });
|
|
10139
10584
|
return b64urlEncodeStr(json);
|
|
@@ -10142,11 +10587,11 @@ function decodeCursor2(str) {
|
|
|
10142
10587
|
try {
|
|
10143
10588
|
const obj = JSON.parse(b64urlDecodeStr(str));
|
|
10144
10589
|
if (!obj || obj["v"] !== 1 || !obj["s"] || !obj["a"]) {
|
|
10145
|
-
throw
|
|
10590
|
+
throw makeInvalidCursorError("bad-structure");
|
|
10146
10591
|
}
|
|
10147
10592
|
return obj;
|
|
10148
10593
|
} catch (e) {
|
|
10149
|
-
throw makeInvalidCursorError(e);
|
|
10594
|
+
throw makeInvalidCursorError("Invalid cursor", e);
|
|
10150
10595
|
}
|
|
10151
10596
|
}
|
|
10152
10597
|
|
|
@@ -10218,7 +10663,11 @@ var MonSQLizeRuntime = class {
|
|
|
10218
10663
|
} : rawCacheInput;
|
|
10219
10664
|
this._cache = normalizeRuntimeCache(cacheInput);
|
|
10220
10665
|
this._logger = Logger.create(options.logger ?? null);
|
|
10221
|
-
this._cacheLockManager = new CacheLockManager({
|
|
10666
|
+
this._cacheLockManager = new CacheLockManager({
|
|
10667
|
+
logger: options.logger ?? null,
|
|
10668
|
+
maxDuration: options.transaction?.lockMaxDuration,
|
|
10669
|
+
cleanupInterval: options.transaction?.lockCleanupInterval
|
|
10670
|
+
});
|
|
10222
10671
|
this._cache.setLockManager?.(this._cacheLockManager);
|
|
10223
10672
|
this._runtimeDefaults = buildRuntimeDefaults(options);
|
|
10224
10673
|
this._adapterCacheOverride = void 0;
|
|
@@ -10589,9 +11038,15 @@ var MonSQLizeRuntime = class {
|
|
|
10589
11038
|
listSagas() {
|
|
10590
11039
|
return this.initializeSagaOrchestrator().listSagas();
|
|
10591
11040
|
}
|
|
11041
|
+
getTransactionStats() {
|
|
11042
|
+
return this._transactionManager?.getStats() ?? null;
|
|
11043
|
+
}
|
|
10592
11044
|
getSagaStats() {
|
|
10593
11045
|
return this.initializeSagaOrchestrator().getStats();
|
|
10594
11046
|
}
|
|
11047
|
+
getDistributedCacheInvalidatorStats() {
|
|
11048
|
+
return this._distributedInvalidator?.getStats() ?? null;
|
|
11049
|
+
}
|
|
10595
11050
|
async startSync() {
|
|
10596
11051
|
this.ensureConnected();
|
|
10597
11052
|
const manager = await this.initializeSyncManager();
|
|
@@ -10670,7 +11125,8 @@ var MonSQLizeRuntime = class {
|
|
|
10670
11125
|
client: this._client,
|
|
10671
11126
|
cache: this._cache,
|
|
10672
11127
|
logger: this.options.logger ?? null,
|
|
10673
|
-
lockManager: this._cacheLockManager
|
|
11128
|
+
lockManager: this._cacheLockManager,
|
|
11129
|
+
transaction: this.options.transaction
|
|
10674
11130
|
});
|
|
10675
11131
|
return this._transactionManager;
|
|
10676
11132
|
}
|