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/esm/index.mjs
CHANGED
|
@@ -128,8 +128,8 @@ function createQueryTimeoutError(timeoutMs) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// src/capabilities/cache/redis-cache-adapter.ts
|
|
131
|
-
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance
|
|
132
|
-
var LEGACY_IOREDIS_MISSING_ERROR = "ioredis
|
|
131
|
+
var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance must be a Redis URL string or an ioredis instance";
|
|
132
|
+
var LEGACY_IOREDIS_MISSING_ERROR = "Unable to load ioredis. monsqlize installs ioredis by default; check package installation completeness or pass an existing ioredis instance";
|
|
133
133
|
function isMissingIoredisError(error) {
|
|
134
134
|
if (!(error instanceof Error)) {
|
|
135
135
|
return false;
|
|
@@ -139,14 +139,14 @@ function isMissingIoredisError(error) {
|
|
|
139
139
|
function createLegacyRedisError(message, code = ErrorCodes.INVALID_ARGUMENT) {
|
|
140
140
|
return createError(code, message);
|
|
141
141
|
}
|
|
142
|
-
function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
142
|
+
function createRedisCacheAdapter(redisUrlOrInstance, options) {
|
|
143
143
|
if (typeof redisUrlOrInstance === "string") {
|
|
144
144
|
const redisUrl = redisUrlOrInstance.trim();
|
|
145
145
|
if (!redisUrl) {
|
|
146
146
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
147
147
|
}
|
|
148
148
|
try {
|
|
149
|
-
return createHubRedisCacheAdapter(redisUrl);
|
|
149
|
+
return createHubRedisCacheAdapter(redisUrl, options);
|
|
150
150
|
} catch (error) {
|
|
151
151
|
if (isMissingIoredisError(error)) {
|
|
152
152
|
throw createLegacyRedisError(LEGACY_IOREDIS_MISSING_ERROR, ErrorCodes.CACHE_UNAVAILABLE);
|
|
@@ -155,7 +155,7 @@ function createRedisCacheAdapter(redisUrlOrInstance) {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
if (redisUrlOrInstance && typeof redisUrlOrInstance === "object") {
|
|
158
|
-
return createHubRedisCacheAdapter(redisUrlOrInstance);
|
|
158
|
+
return createHubRedisCacheAdapter(redisUrlOrInstance, options);
|
|
159
159
|
}
|
|
160
160
|
throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
|
|
161
161
|
}
|
|
@@ -165,7 +165,7 @@ import { randomBytes } from "crypto";
|
|
|
165
165
|
var DistributedCacheInvalidator = class {
|
|
166
166
|
constructor(options) {
|
|
167
167
|
if (!options.cache) {
|
|
168
|
-
throw
|
|
168
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
|
|
169
169
|
}
|
|
170
170
|
this._cache = options.cache;
|
|
171
171
|
this._logger = options.logger ?? null;
|
|
@@ -183,7 +183,7 @@ var DistributedCacheInvalidator = class {
|
|
|
183
183
|
this.pub = new Redis(options.redisUrl);
|
|
184
184
|
this.sub = new Redis(options.redisUrl);
|
|
185
185
|
} else {
|
|
186
|
-
throw
|
|
186
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires either redis or redisUrl");
|
|
187
187
|
}
|
|
188
188
|
this._setupSubscription();
|
|
189
189
|
}
|
|
@@ -363,17 +363,17 @@ var _PopulatePromise = class _PopulatePromise {
|
|
|
363
363
|
/**
|
|
364
364
|
* Append a populate path and return a new PopulatePromise (chainable).
|
|
365
365
|
*/
|
|
366
|
-
populate(
|
|
366
|
+
populate(path3, options) {
|
|
367
367
|
const toConfig = (item) => {
|
|
368
368
|
if (typeof item !== "string" && (typeof item !== "object" || item === null || Array.isArray(item))) {
|
|
369
369
|
throw createError(ErrorCodes.INVALID_ARGUMENT, "populate param must be a string, array, or object");
|
|
370
370
|
}
|
|
371
371
|
return typeof item === "string" ? { path: item, ...options } : { ...item, ...options };
|
|
372
372
|
};
|
|
373
|
-
if (Array.isArray(
|
|
374
|
-
return new _PopulatePromise(this.executor, [...this.paths, ...
|
|
373
|
+
if (Array.isArray(path3)) {
|
|
374
|
+
return new _PopulatePromise(this.executor, [...this.paths, ...path3.map(toConfig)]);
|
|
375
375
|
}
|
|
376
|
-
const config = toConfig(
|
|
376
|
+
const config = toConfig(path3);
|
|
377
377
|
return new _PopulatePromise(this.executor, [...this.paths, config]);
|
|
378
378
|
}
|
|
379
379
|
/**
|
|
@@ -487,8 +487,8 @@ function validateRelationConfig(name, config) {
|
|
|
487
487
|
throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
|
-
function normalizePopulateConfig(
|
|
491
|
-
return typeof
|
|
490
|
+
function normalizePopulateConfig(path3) {
|
|
491
|
+
return typeof path3 === "string" ? { path: path3 } : path3;
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// src/capabilities/model/model-registry.ts
|
|
@@ -639,8 +639,8 @@ function groupBy(values, keySelector) {
|
|
|
639
639
|
}
|
|
640
640
|
return map;
|
|
641
641
|
}
|
|
642
|
-
function getByPath(source,
|
|
643
|
-
return
|
|
642
|
+
function getByPath(source, path3) {
|
|
643
|
+
return path3.split(".").reduce((current, key) => {
|
|
644
644
|
if (!current || typeof current !== "object") {
|
|
645
645
|
return void 0;
|
|
646
646
|
}
|
|
@@ -695,8 +695,8 @@ function resolveRegisteredCollectionName(registered, fallback) {
|
|
|
695
695
|
const definition = registered.definition;
|
|
696
696
|
return definition.collection ?? definition.name ?? registered.collectionName;
|
|
697
697
|
}
|
|
698
|
-
async function populateModelPath(context, docs,
|
|
699
|
-
const config = normalizePopulateConfig(
|
|
698
|
+
async function populateModelPath(context, docs, path3) {
|
|
699
|
+
const config = normalizePopulateConfig(path3);
|
|
700
700
|
if (docs.length === 0) {
|
|
701
701
|
return docs;
|
|
702
702
|
}
|
|
@@ -813,8 +813,8 @@ function hydrateModelDocument(context, doc) {
|
|
|
813
813
|
populate: {
|
|
814
814
|
configurable: true,
|
|
815
815
|
enumerable: false,
|
|
816
|
-
value: (
|
|
817
|
-
const paths = Array.isArray(
|
|
816
|
+
value: (path3) => {
|
|
817
|
+
const paths = Array.isArray(path3) ? path3 : [path3];
|
|
818
818
|
return new PopulatePromise(
|
|
819
819
|
(resolvedPaths) => context.populateDocument(hydrated, resolvedPaths),
|
|
820
820
|
paths
|
|
@@ -2049,18 +2049,18 @@ var ModelInstance = class {
|
|
|
2049
2049
|
}
|
|
2050
2050
|
async populateDocuments(docs, paths) {
|
|
2051
2051
|
let current = docs;
|
|
2052
|
-
for (const
|
|
2053
|
-
current = await this.populatePath(current,
|
|
2052
|
+
for (const path3 of paths) {
|
|
2053
|
+
current = await this.populatePath(current, path3);
|
|
2054
2054
|
}
|
|
2055
2055
|
return current;
|
|
2056
2056
|
}
|
|
2057
|
-
async populatePath(docs,
|
|
2057
|
+
async populatePath(docs, path3) {
|
|
2058
2058
|
return populateModelPath({
|
|
2059
2059
|
relations: this.relations,
|
|
2060
2060
|
runtime: this.runtime,
|
|
2061
2061
|
dbName: this.dbName,
|
|
2062
2062
|
poolName: this.poolName
|
|
2063
|
-
}, docs,
|
|
2063
|
+
}, docs, path3);
|
|
2064
2064
|
}
|
|
2065
2065
|
hydrateDocuments(docs) {
|
|
2066
2066
|
return docs.filter(Boolean).map((doc) => this.hydrateDocument(doc));
|
|
@@ -2233,9 +2233,9 @@ var Transaction = class {
|
|
|
2233
2233
|
*/
|
|
2234
2234
|
async start() {
|
|
2235
2235
|
if (this.state !== "pending") {
|
|
2236
|
-
throw
|
|
2236
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
|
|
2237
2237
|
}
|
|
2238
|
-
this.session.startTransaction();
|
|
2238
|
+
this.session.startTransaction(this.options.transactionOptions);
|
|
2239
2239
|
this.state = "active";
|
|
2240
2240
|
this.startedAt = Date.now();
|
|
2241
2241
|
const timeout = this.options.timeout ?? 3e4;
|
|
@@ -2255,7 +2255,7 @@ var Transaction = class {
|
|
|
2255
2255
|
*/
|
|
2256
2256
|
async commit() {
|
|
2257
2257
|
if (this.state !== "active") {
|
|
2258
|
-
throw
|
|
2258
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
2259
2259
|
}
|
|
2260
2260
|
if (typeof this.session.commitTransaction === "function") {
|
|
2261
2261
|
await this.session.commitTransaction();
|
|
@@ -2353,7 +2353,9 @@ var TransactionManager = class {
|
|
|
2353
2353
|
this.stats = {
|
|
2354
2354
|
totalTransactions: 0,
|
|
2355
2355
|
successfulTransactions: 0,
|
|
2356
|
-
failedTransactions: 0
|
|
2356
|
+
failedTransactions: 0,
|
|
2357
|
+
readOnlyTransactions: 0,
|
|
2358
|
+
writeTransactions: 0
|
|
2357
2359
|
};
|
|
2358
2360
|
const options = "client" in input ? input : {
|
|
2359
2361
|
client: input,
|
|
@@ -2364,6 +2366,10 @@ var TransactionManager = class {
|
|
|
2364
2366
|
this.cache = options.cache ?? null;
|
|
2365
2367
|
this.logger = options.logger ?? null;
|
|
2366
2368
|
this.lockManager = options.lockManager ?? null;
|
|
2369
|
+
this.defaultReadConcern = options.defaultReadConcern;
|
|
2370
|
+
this.defaultWriteConcern = options.defaultWriteConcern;
|
|
2371
|
+
this.defaultReadPreference = options.defaultReadPreference;
|
|
2372
|
+
this.maxStatsSamples = options.maxStatsSamples ?? 1e3;
|
|
2367
2373
|
this.defaultOptions = {
|
|
2368
2374
|
maxDuration: options.maxDuration ?? 3e4,
|
|
2369
2375
|
enableRetry: options.enableRetry ?? true,
|
|
@@ -2380,11 +2386,17 @@ var TransactionManager = class {
|
|
|
2380
2386
|
const session = this.client.startSession({
|
|
2381
2387
|
causalConsistency: options.causalConsistency !== false
|
|
2382
2388
|
});
|
|
2389
|
+
const transactionOptions = {
|
|
2390
|
+
readConcern: options.readConcern ?? this.defaultReadConcern,
|
|
2391
|
+
writeConcern: options.writeConcern ?? this.defaultWriteConcern,
|
|
2392
|
+
readPreference: options.readPreference ?? this.defaultReadPreference
|
|
2393
|
+
};
|
|
2383
2394
|
const transaction = new Transaction(session, {
|
|
2384
2395
|
cache: this.cache,
|
|
2385
2396
|
logger: this.logger,
|
|
2386
2397
|
lockManager: options.enableCacheLock === false ? null : this.lockManager,
|
|
2387
|
-
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration
|
|
2398
|
+
timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration,
|
|
2399
|
+
transactionOptions: compactUndefined(transactionOptions)
|
|
2388
2400
|
});
|
|
2389
2401
|
const originalEnd = transaction.end.bind(transaction);
|
|
2390
2402
|
transaction.end = async () => {
|
|
@@ -2411,12 +2423,12 @@ var TransactionManager = class {
|
|
|
2411
2423
|
await transaction.start();
|
|
2412
2424
|
const result = await callback(transaction);
|
|
2413
2425
|
await transaction.commit();
|
|
2414
|
-
this.recordStats(Date.now() - startedAt, true);
|
|
2426
|
+
this.recordStats(transaction, Date.now() - startedAt, true);
|
|
2415
2427
|
return result;
|
|
2416
2428
|
} catch (error) {
|
|
2417
2429
|
lastError = error;
|
|
2418
2430
|
await transaction.abort();
|
|
2419
|
-
this.recordStats(Date.now() - startedAt, false);
|
|
2431
|
+
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
2420
2432
|
if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
|
|
2421
2433
|
throw error;
|
|
2422
2434
|
}
|
|
@@ -2452,27 +2464,54 @@ var TransactionManager = class {
|
|
|
2452
2464
|
*/
|
|
2453
2465
|
getStats() {
|
|
2454
2466
|
const averageDuration = this.durations.length === 0 ? 0 : this.durations.reduce((sum, item) => sum + item, 0) / this.durations.length;
|
|
2467
|
+
const sortedDurations = [...this.durations].sort((a, b) => a - b);
|
|
2468
|
+
const p95Duration = percentile(sortedDurations, 0.95);
|
|
2469
|
+
const p99Duration = percentile(sortedDurations, 0.99);
|
|
2470
|
+
const totalTransactions = this.stats.totalTransactions;
|
|
2455
2471
|
return {
|
|
2456
|
-
totalTransactions
|
|
2472
|
+
totalTransactions,
|
|
2457
2473
|
successfulTransactions: this.stats.successfulTransactions,
|
|
2458
2474
|
failedTransactions: this.stats.failedTransactions,
|
|
2475
|
+
readOnlyTransactions: this.stats.readOnlyTransactions,
|
|
2476
|
+
writeTransactions: this.stats.writeTransactions,
|
|
2459
2477
|
activeTransactions: this.activeTransactions.size,
|
|
2460
|
-
averageDuration
|
|
2478
|
+
averageDuration,
|
|
2479
|
+
p95Duration,
|
|
2480
|
+
p99Duration,
|
|
2481
|
+
successRate: totalTransactions > 0 ? `${(this.stats.successfulTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2482
|
+
readOnlyRatio: totalTransactions > 0 ? `${(this.stats.readOnlyTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
|
|
2483
|
+
sampleCount: this.durations.length
|
|
2461
2484
|
};
|
|
2462
2485
|
}
|
|
2463
|
-
recordStats(duration, success) {
|
|
2486
|
+
recordStats(transaction, duration, success) {
|
|
2464
2487
|
this.stats.totalTransactions += 1;
|
|
2465
2488
|
if (success) {
|
|
2466
2489
|
this.stats.successfulTransactions += 1;
|
|
2467
2490
|
} else {
|
|
2468
2491
|
this.stats.failedTransactions += 1;
|
|
2469
2492
|
}
|
|
2493
|
+
if (transaction.pendingInvalidations.size > 0) {
|
|
2494
|
+
this.stats.writeTransactions += 1;
|
|
2495
|
+
} else {
|
|
2496
|
+
this.stats.readOnlyTransactions += 1;
|
|
2497
|
+
}
|
|
2470
2498
|
this.durations.push(duration);
|
|
2471
|
-
if (this.durations.length >
|
|
2499
|
+
if (this.durations.length > this.maxStatsSamples) {
|
|
2472
2500
|
this.durations.shift();
|
|
2473
2501
|
}
|
|
2474
2502
|
}
|
|
2475
2503
|
};
|
|
2504
|
+
function percentile(sortedValues, ratio) {
|
|
2505
|
+
if (sortedValues.length === 0) {
|
|
2506
|
+
return 0;
|
|
2507
|
+
}
|
|
2508
|
+
const index = Math.floor(sortedValues.length * ratio);
|
|
2509
|
+
return sortedValues[Math.min(index, sortedValues.length - 1)] ?? 0;
|
|
2510
|
+
}
|
|
2511
|
+
function compactUndefined(value) {
|
|
2512
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0);
|
|
2513
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
2514
|
+
}
|
|
2476
2515
|
function stringifySessionId(id) {
|
|
2477
2516
|
if (typeof id === "string") {
|
|
2478
2517
|
return id;
|
|
@@ -2509,18 +2548,166 @@ async function sleep(ms) {
|
|
|
2509
2548
|
}
|
|
2510
2549
|
|
|
2511
2550
|
// src/adapters/mongodb/common/connect.ts
|
|
2551
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
2552
|
+
import path from "node:path";
|
|
2512
2553
|
import { MongoClient } from "mongodb";
|
|
2554
|
+
var DEFAULT_MEMORY_SERVER_VERSION = "7.0.14";
|
|
2555
|
+
var MANAGED_DB_PATH_PREFIXES = ["single-", "replset-", "examples-single-", "examples-replset-", "probe-single-", "probe-replset-"];
|
|
2513
2556
|
var _memoryServerInstance = null;
|
|
2557
|
+
var _memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2558
|
+
var _memoryServerDbPath = null;
|
|
2559
|
+
var _memoryServerClients = /* @__PURE__ */ new Set();
|
|
2560
|
+
function setDefaultEnv(name, value) {
|
|
2561
|
+
if (!process.env[name]) {
|
|
2562
|
+
process.env[name] = value;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function sanitizePathSegment(input) {
|
|
2566
|
+
return input.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
|
|
2567
|
+
}
|
|
2568
|
+
function parseManagedPathPid(name) {
|
|
2569
|
+
if (!MANAGED_DB_PATH_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
2570
|
+
return null;
|
|
2571
|
+
}
|
|
2572
|
+
const match = /-(\d+)-[^-]+$/.exec(name);
|
|
2573
|
+
if (!match) {
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
const pid = Number.parseInt(match[1], 10);
|
|
2577
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
2578
|
+
}
|
|
2579
|
+
function isProcessAlive(pid) {
|
|
2580
|
+
if (pid === process.pid) {
|
|
2581
|
+
return true;
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
process.kill(pid, 0);
|
|
2585
|
+
return true;
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
return error.code === "EPERM";
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
function pruneManagedDbRoot(dbRoot) {
|
|
2591
|
+
let entries;
|
|
2592
|
+
try {
|
|
2593
|
+
entries = readdirSync(dbRoot, { withFileTypes: true });
|
|
2594
|
+
} catch {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
for (const entry of entries) {
|
|
2598
|
+
if (!entry.isDirectory()) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
const pid = parseManagedPathPid(entry.name);
|
|
2602
|
+
if (!pid || isProcessAlive(pid)) {
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
rmSync(path.join(dbRoot, entry.name), { recursive: true, force: true });
|
|
2607
|
+
} catch {
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
function resolveMemoryServerBinaryVersion(memoryServerOptions = {}) {
|
|
2612
|
+
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;
|
|
2613
|
+
}
|
|
2614
|
+
function resolveMemoryServerPolicy(binaryVersion) {
|
|
2615
|
+
const cacheRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_CACHE_DIR || path.join(process.cwd(), ".cache", "mongodb-memory-server"));
|
|
2616
|
+
const downloadDir = path.resolve(process.env.MONGOMS_DOWNLOAD_DIR || path.join(cacheRoot, "binaries"));
|
|
2617
|
+
const dbRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_DB_DIR || path.join(cacheRoot, "db"));
|
|
2618
|
+
mkdirSync(downloadDir, { recursive: true });
|
|
2619
|
+
mkdirSync(dbRoot, { recursive: true });
|
|
2620
|
+
pruneManagedDbRoot(dbRoot);
|
|
2621
|
+
setDefaultEnv("MONGOMS_DOWNLOAD_DIR", downloadDir);
|
|
2622
|
+
setDefaultEnv("MONGOMS_PREFER_GLOBAL_PATH", "false");
|
|
2623
|
+
setDefaultEnv("MONGOMS_RUNTIME_DOWNLOAD", "true");
|
|
2624
|
+
setDefaultEnv("MONGOMS_VERSION", binaryVersion);
|
|
2625
|
+
return { downloadDir, dbRoot };
|
|
2626
|
+
}
|
|
2627
|
+
function createManagedDbPath(dbRoot, dbName) {
|
|
2628
|
+
return mkdtempSync(path.join(dbRoot, `replset-${sanitizePathSegment(dbName)}-${process.pid}-`));
|
|
2629
|
+
}
|
|
2630
|
+
function isManagedCleanupError(error, dbPath) {
|
|
2631
|
+
if (!error || typeof error !== "object") {
|
|
2632
|
+
return false;
|
|
2633
|
+
}
|
|
2634
|
+
const candidate = error;
|
|
2635
|
+
if (!candidate.code || !["ENOTEMPTY", "EBUSY", "EPERM", "ENOENT"].includes(candidate.code)) {
|
|
2636
|
+
return false;
|
|
2637
|
+
}
|
|
2638
|
+
return !candidate.path || path.resolve(candidate.path).startsWith(path.resolve(dbPath));
|
|
2639
|
+
}
|
|
2640
|
+
function delay(ms) {
|
|
2641
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2642
|
+
}
|
|
2643
|
+
async function cleanupManagedDbPath(dbPath) {
|
|
2644
|
+
if (!dbPath) {
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
for (const waitMs of [0, 50, 100, 200, 400, 800]) {
|
|
2648
|
+
if (waitMs > 0) {
|
|
2649
|
+
await delay(waitMs);
|
|
2650
|
+
}
|
|
2651
|
+
try {
|
|
2652
|
+
rmSync(dbPath, { recursive: true, force: true });
|
|
2653
|
+
if (!existsSync(dbPath)) {
|
|
2654
|
+
return true;
|
|
2655
|
+
}
|
|
2656
|
+
} catch {
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
return !existsSync(dbPath);
|
|
2660
|
+
}
|
|
2661
|
+
function resolveLaunchTimeout() {
|
|
2662
|
+
const raw = process.env.MONSQLIZE_MEMORY_MONGO_LAUNCH_TIMEOUT_MS;
|
|
2663
|
+
if (!raw) {
|
|
2664
|
+
return void 0;
|
|
2665
|
+
}
|
|
2666
|
+
const value = Number.parseInt(raw, 10);
|
|
2667
|
+
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
2668
|
+
}
|
|
2669
|
+
async function seedMemoryServerBinaryCache(binaryVersion, downloadDir) {
|
|
2670
|
+
try {
|
|
2671
|
+
const { DryMongoBinary } = __require("mongodb-memory-server-core/lib/util/DryMongoBinary");
|
|
2672
|
+
const options = await DryMongoBinary.generateOptions({ version: binaryVersion, downloadDir });
|
|
2673
|
+
options.downloadDir = downloadDir;
|
|
2674
|
+
const paths = await DryMongoBinary.generatePaths(options);
|
|
2675
|
+
if (paths.resolveConfig && paths.homeCache && path.resolve(paths.resolveConfig) !== path.resolve(paths.homeCache) && existsSync(paths.homeCache) && !existsSync(paths.resolveConfig)) {
|
|
2676
|
+
mkdirSync(path.dirname(paths.resolveConfig), { recursive: true });
|
|
2677
|
+
copyFileSync(paths.homeCache, paths.resolveConfig);
|
|
2678
|
+
}
|
|
2679
|
+
} catch {
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2514
2682
|
async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
2515
2683
|
if (_memoryServerInstance) {
|
|
2516
2684
|
return _memoryServerInstance.getUri();
|
|
2517
2685
|
}
|
|
2686
|
+
const binaryVersion = resolveMemoryServerBinaryVersion(memoryServerOptions);
|
|
2687
|
+
const { dbRoot, downloadDir } = resolveMemoryServerPolicy(binaryVersion);
|
|
2688
|
+
await seedMemoryServerBinaryCache(binaryVersion, downloadDir);
|
|
2518
2689
|
const { MongoMemoryReplSet } = __require("mongodb-memory-server");
|
|
2519
|
-
logger?.info?.("
|
|
2690
|
+
logger?.info?.("Starting MongoDB Memory ReplSet", { binaryVersion });
|
|
2691
|
+
const dbName = memoryServerOptions?.instance?.dbName || "monsqlize_memory";
|
|
2692
|
+
const instanceConfig = { ...memoryServerOptions?.instance ?? {} };
|
|
2693
|
+
const hasUserDbPath = typeof instanceConfig.dbPath === "string" && instanceConfig.dbPath.length > 0;
|
|
2694
|
+
if (!hasUserDbPath) {
|
|
2695
|
+
_memoryServerDbPath = createManagedDbPath(dbRoot, dbName);
|
|
2696
|
+
instanceConfig.dbPath = _memoryServerDbPath;
|
|
2697
|
+
} else {
|
|
2698
|
+
_memoryServerDbPath = null;
|
|
2699
|
+
}
|
|
2700
|
+
if (instanceConfig.launchTimeout === void 0) {
|
|
2701
|
+
const launchTimeout = resolveLaunchTimeout();
|
|
2702
|
+
if (launchTimeout) {
|
|
2703
|
+
instanceConfig.launchTimeout = launchTimeout;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: !hasUserDbPath };
|
|
2520
2707
|
const defaultConfig = {
|
|
2521
2708
|
replSet: { count: 1, storageEngine: "wiredTiger" },
|
|
2522
|
-
binary: { version:
|
|
2523
|
-
instanceOpts: [
|
|
2709
|
+
binary: { version: binaryVersion },
|
|
2710
|
+
instanceOpts: [instanceConfig]
|
|
2524
2711
|
};
|
|
2525
2712
|
const resolvedConfig = {
|
|
2526
2713
|
...defaultConfig,
|
|
@@ -2529,11 +2716,43 @@ async function startMemoryServer(logger, memoryServerOptions = {}) {
|
|
|
2529
2716
|
try {
|
|
2530
2717
|
_memoryServerInstance = await MongoMemoryReplSet.create(resolvedConfig);
|
|
2531
2718
|
const uri = _memoryServerInstance.getUri();
|
|
2532
|
-
logger?.info?.("
|
|
2719
|
+
logger?.info?.("MongoDB Memory ReplSet started", { uri });
|
|
2533
2720
|
return uri;
|
|
2534
2721
|
} catch (err) {
|
|
2535
|
-
|
|
2536
|
-
|
|
2722
|
+
if (!hasUserDbPath) {
|
|
2723
|
+
await cleanupManagedDbPath(_memoryServerDbPath);
|
|
2724
|
+
_memoryServerDbPath = null;
|
|
2725
|
+
}
|
|
2726
|
+
logger?.error?.("Failed to start MongoDB Memory ReplSet", err);
|
|
2727
|
+
throw createConnectionError(
|
|
2728
|
+
`Failed to start MongoDB Memory ReplSet: ${err.message}`,
|
|
2729
|
+
err instanceof Error ? err : void 0
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
async function stopMemoryServer(logger) {
|
|
2734
|
+
if (!_memoryServerInstance) {
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const instance = _memoryServerInstance;
|
|
2738
|
+
const dbPath = _memoryServerDbPath;
|
|
2739
|
+
_memoryServerInstance = null;
|
|
2740
|
+
_memoryServerDbPath = null;
|
|
2741
|
+
let stopError = null;
|
|
2742
|
+
try {
|
|
2743
|
+
await instance.stop(_memoryServerCleanupOptions);
|
|
2744
|
+
logger?.info?.("MongoDB Memory ReplSet stopped");
|
|
2745
|
+
} catch (cause) {
|
|
2746
|
+
stopError = cause;
|
|
2747
|
+
logger?.warn?.("Failed to stop MongoDB Memory ReplSet cleanly.", cause);
|
|
2748
|
+
} finally {
|
|
2749
|
+
if (_memoryServerCleanupOptions.force) {
|
|
2750
|
+
const cleaned = await cleanupManagedDbPath(dbPath);
|
|
2751
|
+
if (!cleaned && (!stopError || isManagedCleanupError(stopError, dbPath ?? ""))) {
|
|
2752
|
+
logger?.warn?.("Failed to remove MongoDB Memory ReplSet dbPath.", { dbPath });
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
_memoryServerCleanupOptions = { doCleanup: true, force: true };
|
|
2537
2756
|
}
|
|
2538
2757
|
}
|
|
2539
2758
|
async function connectMongo(params) {
|
|
@@ -2542,13 +2761,15 @@ async function connectMongo(params) {
|
|
|
2542
2761
|
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name must be a non-empty string.");
|
|
2543
2762
|
}
|
|
2544
2763
|
let effectiveUri = params.config?.uri?.trim();
|
|
2764
|
+
let usesManagedMemoryServer = false;
|
|
2545
2765
|
if (!effectiveUri && params.config?.useMemoryServer === true) {
|
|
2546
2766
|
if (process.env["MONSQLIZE_USE_SYSTEM_MONGO"] === "true") {
|
|
2547
2767
|
const systemUri = process.env["MONSQLIZE_SYSTEM_MONGO_URI"] ?? "mongodb://127.0.0.1:27017";
|
|
2548
|
-
params.logger?.info?.("
|
|
2768
|
+
params.logger?.info?.("Using system MongoDB instead of memory server", { uri: systemUri });
|
|
2549
2769
|
effectiveUri = systemUri;
|
|
2550
2770
|
} else {
|
|
2551
2771
|
effectiveUri = await startMemoryServer(params.logger, params.config.memoryServerOptions);
|
|
2772
|
+
usesManagedMemoryServer = true;
|
|
2552
2773
|
}
|
|
2553
2774
|
}
|
|
2554
2775
|
if (!effectiveUri) {
|
|
@@ -2562,6 +2783,9 @@ async function connectMongo(params) {
|
|
|
2562
2783
|
try {
|
|
2563
2784
|
await client.connect();
|
|
2564
2785
|
const db = client.db(databaseName);
|
|
2786
|
+
if (usesManagedMemoryServer) {
|
|
2787
|
+
_memoryServerClients.add(client);
|
|
2788
|
+
}
|
|
2565
2789
|
params.logger?.info?.("MongoDB connected", { databaseName });
|
|
2566
2790
|
return { client, db };
|
|
2567
2791
|
} catch (cause) {
|
|
@@ -2569,6 +2793,9 @@ async function connectMongo(params) {
|
|
|
2569
2793
|
await client.close();
|
|
2570
2794
|
} catch {
|
|
2571
2795
|
}
|
|
2796
|
+
if (usesManagedMemoryServer && _memoryServerClients.size === 0) {
|
|
2797
|
+
await stopMemoryServer(params.logger);
|
|
2798
|
+
}
|
|
2572
2799
|
throw createConnectionError(
|
|
2573
2800
|
`Failed to connect to MongoDB database: ${databaseName}`,
|
|
2574
2801
|
cause instanceof Error ? cause : void 0
|
|
@@ -2579,17 +2806,26 @@ async function closeMongo(client, logger) {
|
|
|
2579
2806
|
if (!client) {
|
|
2580
2807
|
return;
|
|
2581
2808
|
}
|
|
2809
|
+
const shouldReleaseMemoryServer = _memoryServerClients.delete(client);
|
|
2810
|
+
let closeError = null;
|
|
2582
2811
|
try {
|
|
2583
2812
|
await client.close();
|
|
2584
2813
|
logger?.info?.("MongoDB connection closed");
|
|
2585
2814
|
} catch (cause) {
|
|
2586
|
-
|
|
2815
|
+
closeError = createError(
|
|
2587
2816
|
ErrorCodes.CONNECTION_CLOSED,
|
|
2588
2817
|
"Failed to close MongoDB connection cleanly.",
|
|
2589
2818
|
void 0,
|
|
2590
2819
|
cause instanceof Error ? cause : void 0
|
|
2591
2820
|
);
|
|
2592
|
-
logger?.warn?.(
|
|
2821
|
+
logger?.warn?.(closeError.message, closeError.cause);
|
|
2822
|
+
} finally {
|
|
2823
|
+
if (shouldReleaseMemoryServer && _memoryServerClients.size === 0) {
|
|
2824
|
+
await stopMemoryServer(logger);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
if (closeError) {
|
|
2828
|
+
return;
|
|
2593
2829
|
}
|
|
2594
2830
|
}
|
|
2595
2831
|
|
|
@@ -2601,8 +2837,9 @@ function loadSsh2Client() {
|
|
|
2601
2837
|
try {
|
|
2602
2838
|
return __require("ssh2").Client;
|
|
2603
2839
|
} catch {
|
|
2604
|
-
throw
|
|
2605
|
-
|
|
2840
|
+
throw createError(
|
|
2841
|
+
ErrorCodes.INVALID_CONFIG,
|
|
2842
|
+
"Unable to load ssh2. monsqlize installs ssh2 by default; check that the package installation is complete and that your runtime can resolve bundled dependencies."
|
|
2606
2843
|
);
|
|
2607
2844
|
}
|
|
2608
2845
|
}
|
|
@@ -2630,10 +2867,10 @@ var SSHTunnelSSH2 = class {
|
|
|
2630
2867
|
keepaliveInterval = 3e4
|
|
2631
2868
|
} = this._sshConfig;
|
|
2632
2869
|
if (!host || !username) {
|
|
2633
|
-
throw
|
|
2870
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
|
|
2634
2871
|
}
|
|
2635
2872
|
if (!password && !privateKey && !privateKeyPath) {
|
|
2636
|
-
throw
|
|
2873
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
|
|
2637
2874
|
}
|
|
2638
2875
|
const config = { host, port, username, readyTimeout, keepaliveInterval };
|
|
2639
2876
|
if (password) {
|
|
@@ -2708,7 +2945,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2708
2945
|
}
|
|
2709
2946
|
getTunnelUri(_protocol, originalUri) {
|
|
2710
2947
|
if (!this.isConnected || this.localPort === null) {
|
|
2711
|
-
throw
|
|
2948
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2712
2949
|
}
|
|
2713
2950
|
return originalUri.replace(
|
|
2714
2951
|
`${this.remoteHost}:${this.remotePort}`,
|
|
@@ -2717,7 +2954,7 @@ var SSHTunnelSSH2 = class {
|
|
|
2717
2954
|
}
|
|
2718
2955
|
getLocalAddress() {
|
|
2719
2956
|
if (!this.isConnected || this.localPort === null) {
|
|
2720
|
-
throw
|
|
2957
|
+
throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
|
|
2721
2958
|
}
|
|
2722
2959
|
return `localhost:${this.localPort}`;
|
|
2723
2960
|
}
|
|
@@ -3166,7 +3403,7 @@ var HealthChecker = class {
|
|
|
3166
3403
|
async _pingPool(poolName, timeout) {
|
|
3167
3404
|
const stored = this._clients.get(poolName);
|
|
3168
3405
|
const client = stored ?? this._poolManager?._getPool(poolName);
|
|
3169
|
-
if (!client) throw
|
|
3406
|
+
if (!client) throw createError(ErrorCodes.POOL_NOT_FOUND, `No client for pool: ${poolName}`);
|
|
3170
3407
|
const db = client.db("admin");
|
|
3171
3408
|
const pingFn = db.command ? () => db.command({ ping: 1 }) : () => db.admin().ping();
|
|
3172
3409
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Ping timeout")), timeout));
|
|
@@ -3198,7 +3435,7 @@ var PoolSelector = class {
|
|
|
3198
3435
|
}
|
|
3199
3436
|
select(pools, context) {
|
|
3200
3437
|
if (!pools || pools.length === 0) {
|
|
3201
|
-
throw
|
|
3438
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available pools");
|
|
3202
3439
|
}
|
|
3203
3440
|
switch (this._strategy) {
|
|
3204
3441
|
case "auto":
|
|
@@ -3219,7 +3456,10 @@ var PoolSelector = class {
|
|
|
3219
3456
|
selectByAuto(pools, context) {
|
|
3220
3457
|
const { operation, poolPreference } = context;
|
|
3221
3458
|
let candidates = pools;
|
|
3222
|
-
|
|
3459
|
+
const preferred = this.filterByPreference(pools, poolPreference);
|
|
3460
|
+
if (preferred.length > 0) {
|
|
3461
|
+
candidates = preferred;
|
|
3462
|
+
} else if (operation === "read") {
|
|
3223
3463
|
const secondaries = pools.filter((pool) => pool.role === "secondary");
|
|
3224
3464
|
if (secondaries.length > 0) {
|
|
3225
3465
|
candidates = secondaries;
|
|
@@ -3230,10 +3470,19 @@ var PoolSelector = class {
|
|
|
3230
3470
|
candidates = primaries;
|
|
3231
3471
|
}
|
|
3232
3472
|
}
|
|
3473
|
+
if (candidates.length === 1) {
|
|
3474
|
+
return candidates[0].name;
|
|
3475
|
+
}
|
|
3476
|
+
return this.selectByWeighted(candidates);
|
|
3477
|
+
}
|
|
3478
|
+
filterByPreference(pools, poolPreference) {
|
|
3479
|
+
let candidates = pools;
|
|
3480
|
+
let applied = false;
|
|
3233
3481
|
if (poolPreference?.role) {
|
|
3234
3482
|
const filteredByRole = candidates.filter((pool) => pool.role === poolPreference.role);
|
|
3235
3483
|
if (filteredByRole.length > 0) {
|
|
3236
3484
|
candidates = filteredByRole;
|
|
3485
|
+
applied = true;
|
|
3237
3486
|
}
|
|
3238
3487
|
}
|
|
3239
3488
|
if (poolPreference?.tags?.length) {
|
|
@@ -3246,12 +3495,10 @@ var PoolSelector = class {
|
|
|
3246
3495
|
});
|
|
3247
3496
|
if (filteredByTags.length > 0) {
|
|
3248
3497
|
candidates = filteredByTags;
|
|
3498
|
+
applied = true;
|
|
3249
3499
|
}
|
|
3250
3500
|
}
|
|
3251
|
-
|
|
3252
|
-
return candidates[0].name;
|
|
3253
|
-
}
|
|
3254
|
-
return this.selectByWeighted(candidates);
|
|
3501
|
+
return applied ? candidates : [];
|
|
3255
3502
|
}
|
|
3256
3503
|
selectByRoundRobin(pools, context) {
|
|
3257
3504
|
let candidates = pools;
|
|
@@ -3411,43 +3658,43 @@ var DEFAULT_POOL_CONNECT_OPTIONS = {
|
|
|
3411
3658
|
serverSelectionTimeoutMS: 5e3
|
|
3412
3659
|
};
|
|
3413
3660
|
function validatePoolConfig(config) {
|
|
3414
|
-
if (!config || typeof config !== "object") throw
|
|
3415
|
-
if (!config.name || typeof config.name !== "string") throw
|
|
3416
|
-
if (!config.uri || typeof config.uri !== "string") throw
|
|
3661
|
+
if (!config || typeof config !== "object") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config must be an object");
|
|
3662
|
+
if (!config.name || typeof config.name !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.name is required and must be a string");
|
|
3663
|
+
if (!config.uri || typeof config.uri !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri is required and must be a string");
|
|
3417
3664
|
if (!config.uri.startsWith("mongodb://") && !config.uri.startsWith("mongodb+srv://")) {
|
|
3418
|
-
throw
|
|
3665
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri must start with mongodb:// or mongodb+srv://");
|
|
3419
3666
|
}
|
|
3420
3667
|
if (config.role) {
|
|
3421
3668
|
const validRoles = ["primary", "secondary", "analytics", "custom"];
|
|
3422
|
-
if (!validRoles.includes(config.role)) throw
|
|
3669
|
+
if (!validRoles.includes(config.role)) throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.role must be one of: ${validRoles.join(", ")}`);
|
|
3423
3670
|
}
|
|
3424
3671
|
if (config.weight !== void 0) {
|
|
3425
|
-
if (typeof config.weight !== "number") throw
|
|
3426
|
-
if (config.weight < 0) throw
|
|
3672
|
+
if (typeof config.weight !== "number") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a number");
|
|
3673
|
+
if (config.weight < 0) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a non-negative number");
|
|
3427
3674
|
}
|
|
3428
3675
|
if (config.options !== void 0) {
|
|
3429
|
-
if (typeof config.options !== "object" || Array.isArray(config.options)) throw
|
|
3676
|
+
if (typeof config.options !== "object" || Array.isArray(config.options)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.options must be an object");
|
|
3430
3677
|
const opts = config.options;
|
|
3431
3678
|
for (const key of ["maxPoolSize", "minPoolSize", "maxIdleTimeMS", "waitQueueTimeoutMS", "connectTimeoutMS", "serverSelectionTimeoutMS"]) {
|
|
3432
3679
|
if (opts[key] !== void 0 && (typeof opts[key] !== "number" || opts[key] < 0)) {
|
|
3433
|
-
throw
|
|
3680
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.options.${key} must be a non-negative number`);
|
|
3434
3681
|
}
|
|
3435
3682
|
}
|
|
3436
3683
|
}
|
|
3437
3684
|
if (config.healthCheck !== void 0) {
|
|
3438
|
-
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw
|
|
3685
|
+
if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck must be an object");
|
|
3439
3686
|
const hc = config.healthCheck;
|
|
3440
|
-
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw
|
|
3687
|
+
if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck.enabled must be a boolean");
|
|
3441
3688
|
for (const key of ["interval", "timeout", "retries"]) {
|
|
3442
3689
|
if (hc[key] !== void 0 && (typeof hc[key] !== "number" || hc[key] < 0)) {
|
|
3443
|
-
throw
|
|
3690
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.healthCheck.${key} must be a non-negative number`);
|
|
3444
3691
|
}
|
|
3445
3692
|
}
|
|
3446
3693
|
}
|
|
3447
3694
|
if (config.tags !== void 0) {
|
|
3448
|
-
if (!Array.isArray(config.tags)) throw
|
|
3695
|
+
if (!Array.isArray(config.tags)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array");
|
|
3449
3696
|
for (const tag of config.tags) {
|
|
3450
|
-
if (typeof tag !== "string") throw
|
|
3697
|
+
if (typeof tag !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array of strings");
|
|
3451
3698
|
}
|
|
3452
3699
|
}
|
|
3453
3700
|
}
|
|
@@ -3506,10 +3753,10 @@ function validatePoolConfigSafe(config) {
|
|
|
3506
3753
|
}
|
|
3507
3754
|
function validatePoolConfigInternal(config) {
|
|
3508
3755
|
if (!config.name?.trim()) {
|
|
3509
|
-
throw
|
|
3756
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty name");
|
|
3510
3757
|
}
|
|
3511
3758
|
if (!config.uri?.trim()) {
|
|
3512
|
-
throw
|
|
3759
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty uri");
|
|
3513
3760
|
}
|
|
3514
3761
|
}
|
|
3515
3762
|
function createEmptyPoolStats(name) {
|
|
@@ -3586,10 +3833,10 @@ var ConnectionPoolManager = class {
|
|
|
3586
3833
|
async addPool(config) {
|
|
3587
3834
|
validatePoolConfigInternal(config);
|
|
3588
3835
|
if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
|
|
3589
|
-
throw
|
|
3836
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3590
3837
|
}
|
|
3591
3838
|
if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
|
|
3592
|
-
throw
|
|
3839
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
|
|
3593
3840
|
}
|
|
3594
3841
|
this._pendingAdds.add(config.name);
|
|
3595
3842
|
try {
|
|
@@ -3597,7 +3844,7 @@ var ConnectionPoolManager = class {
|
|
|
3597
3844
|
if (this.pools.has(config.name)) {
|
|
3598
3845
|
await client.close().catch(() => {
|
|
3599
3846
|
});
|
|
3600
|
-
throw
|
|
3847
|
+
throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
|
|
3601
3848
|
}
|
|
3602
3849
|
this.pools.set(config.name, {
|
|
3603
3850
|
client,
|
|
@@ -3636,7 +3883,7 @@ var ConnectionPoolManager = class {
|
|
|
3636
3883
|
async removePool(name) {
|
|
3637
3884
|
const pool = this.pools.get(name);
|
|
3638
3885
|
if (!pool) {
|
|
3639
|
-
throw
|
|
3886
|
+
throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${name}' not found`);
|
|
3640
3887
|
}
|
|
3641
3888
|
this.stopHealthCheck(name);
|
|
3642
3889
|
await pool.client.close();
|
|
@@ -3660,27 +3907,28 @@ var ConnectionPoolManager = class {
|
|
|
3660
3907
|
selectPool(operation, options = {}) {
|
|
3661
3908
|
if (options.pool) {
|
|
3662
3909
|
const poolData2 = this.pools.get(options.pool);
|
|
3663
|
-
if (!poolData2) throw
|
|
3910
|
+
if (!poolData2) throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${options.pool}' not found`);
|
|
3664
3911
|
return this._createPoolResult(options.pool, poolData2.client);
|
|
3665
3912
|
}
|
|
3666
3913
|
let candidates = this._getHealthyPools();
|
|
3667
3914
|
if (candidates.length === 0) {
|
|
3668
3915
|
if (!this.fallback.enabled) {
|
|
3669
|
-
throw
|
|
3916
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3670
3917
|
}
|
|
3671
3918
|
candidates = this._handleAllPoolsDown(operation);
|
|
3672
3919
|
if (candidates.length === 0) {
|
|
3673
|
-
throw
|
|
3920
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
|
|
3674
3921
|
}
|
|
3675
3922
|
}
|
|
3923
|
+
const poolPreference = options.poolPreference ?? (options.tags?.length ? { tags: options.tags } : void 0);
|
|
3676
3924
|
const poolName = this._selector.select(candidates, {
|
|
3677
3925
|
operation,
|
|
3678
3926
|
stats: this._stats.getAllStats(),
|
|
3679
|
-
poolPreference
|
|
3927
|
+
poolPreference
|
|
3680
3928
|
});
|
|
3681
3929
|
const poolData = this.pools.get(poolName);
|
|
3682
3930
|
if (!poolData) {
|
|
3683
|
-
throw
|
|
3931
|
+
throw createError(ErrorCodes.INVALID_OPERATION, `Selected pool '${poolName}' not available`);
|
|
3684
3932
|
}
|
|
3685
3933
|
this._stats.recordSelection(poolName, operation);
|
|
3686
3934
|
this.recordSelection(poolName, true);
|
|
@@ -3803,10 +4051,12 @@ var ConnectionPoolManager = class {
|
|
|
3803
4051
|
_getHealthyPools() {
|
|
3804
4052
|
const result = [];
|
|
3805
4053
|
for (const [name, config] of this._configs.entries()) {
|
|
3806
|
-
const
|
|
3807
|
-
|
|
3808
|
-
|
|
4054
|
+
const compatStatus = this._healthChecker.getStatus(name);
|
|
4055
|
+
const publicStatus = this.healthStatus.get(name);
|
|
4056
|
+
if (compatStatus?.status === "down" || publicStatus?.status === "down") {
|
|
4057
|
+
continue;
|
|
3809
4058
|
}
|
|
4059
|
+
result.push(config);
|
|
3810
4060
|
}
|
|
3811
4061
|
return result;
|
|
3812
4062
|
}
|
|
@@ -3959,7 +4209,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
|
|
|
3959
4209
|
logger
|
|
3960
4210
|
});
|
|
3961
4211
|
} catch (err) {
|
|
3962
|
-
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014
|
|
4212
|
+
logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
|
|
3963
4213
|
return null;
|
|
3964
4214
|
}
|
|
3965
4215
|
}
|
|
@@ -3967,7 +4217,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
3967
4217
|
const modelsConfig = options.models;
|
|
3968
4218
|
if (!modelsConfig) return;
|
|
3969
4219
|
if (typeof modelsConfig !== "string" && typeof modelsConfig !== "object") return;
|
|
3970
|
-
const { readdirSync } = await import("node:fs");
|
|
4220
|
+
const { readdirSync: readdirSync2 } = await import("node:fs");
|
|
3971
4221
|
const { resolve, join, isAbsolute } = await import("node:path");
|
|
3972
4222
|
const { createRequire } = await import("node:module");
|
|
3973
4223
|
let targetPath;
|
|
@@ -3991,7 +4241,7 @@ async function loadModelFiles(options, logger, opts = {}) {
|
|
|
3991
4241
|
const collectFiles = (dir) => {
|
|
3992
4242
|
let entries;
|
|
3993
4243
|
try {
|
|
3994
|
-
entries =
|
|
4244
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
3995
4245
|
} catch {
|
|
3996
4246
|
logger.warn?.(`[Models] cannot read directory: ${dir}`);
|
|
3997
4247
|
return [];
|
|
@@ -4470,7 +4720,7 @@ async function indexStatsForAccessor(collectionRef) {
|
|
|
4470
4720
|
}
|
|
4471
4721
|
async function setValidatorForAccessor(collectionRef, collectionName, dbRef, validator, options = {}) {
|
|
4472
4722
|
if (validator === null || typeof validator !== "object") {
|
|
4473
|
-
throw
|
|
4723
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Validator must be a non-null object");
|
|
4474
4724
|
}
|
|
4475
4725
|
const isEmptyValidator = Object.keys(validator).length === 0;
|
|
4476
4726
|
const command = {
|
|
@@ -4491,14 +4741,14 @@ async function setValidatorForAccessor(collectionRef, collectionName, dbRef, val
|
|
|
4491
4741
|
}
|
|
4492
4742
|
async function setValidationLevelForAccessor(collectionRef, collectionName, dbRef, level) {
|
|
4493
4743
|
if (typeof level !== "string" || !["off", "strict", "moderate"].includes(level)) {
|
|
4494
|
-
throw
|
|
4744
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation level: must be "off", "strict", or "moderate"');
|
|
4495
4745
|
}
|
|
4496
4746
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationLevel: level });
|
|
4497
4747
|
return { ok: result["ok"], validationLevel: level };
|
|
4498
4748
|
}
|
|
4499
4749
|
async function setValidationActionForAccessor(collectionRef, collectionName, dbRef, action) {
|
|
4500
4750
|
if (typeof action !== "string" || !["error", "warn"].includes(action)) {
|
|
4501
|
-
throw
|
|
4751
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation action: must be "error" or "warn"');
|
|
4502
4752
|
}
|
|
4503
4753
|
const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationAction: action });
|
|
4504
4754
|
return { ok: result["ok"], validationAction: action };
|
|
@@ -4530,14 +4780,14 @@ async function statsForAccessor(collectionRef, dbName, collectionName, options =
|
|
|
4530
4780
|
}
|
|
4531
4781
|
async function renameCollectionForAccessor(collectionRef, collectionName, newName, options = {}) {
|
|
4532
4782
|
if (!newName || typeof newName !== "string") {
|
|
4533
|
-
throw
|
|
4783
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "New collection name is required and must be a non-empty string");
|
|
4534
4784
|
}
|
|
4535
4785
|
await collectionRef.rename(newName, { dropTarget: options.dropTarget ?? false });
|
|
4536
4786
|
return { renamed: true, from: collectionName, to: newName };
|
|
4537
4787
|
}
|
|
4538
4788
|
async function collModForAccessor(collectionRef, collectionName, dbRef, modifications) {
|
|
4539
4789
|
if (modifications === null || typeof modifications !== "object") {
|
|
4540
|
-
throw
|
|
4790
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Modifications must be a non-null object");
|
|
4541
4791
|
}
|
|
4542
4792
|
return resolveDb(collectionRef, dbRef).command({
|
|
4543
4793
|
collMod: collectionName,
|
|
@@ -4546,10 +4796,10 @@ async function collModForAccessor(collectionRef, collectionName, dbRef, modifica
|
|
|
4546
4796
|
}
|
|
4547
4797
|
async function convertToCappedForAccessor(collectionRef, collectionName, dbRef, size, options = {}) {
|
|
4548
4798
|
if (typeof size !== "number") {
|
|
4549
|
-
throw
|
|
4799
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a number");
|
|
4550
4800
|
}
|
|
4551
4801
|
if (size <= 0) {
|
|
4552
|
-
throw
|
|
4802
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a positive number");
|
|
4553
4803
|
}
|
|
4554
4804
|
const command = { convertToCapped: collectionName, size };
|
|
4555
4805
|
if (options.max !== void 0) {
|
|
@@ -4730,7 +4980,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4730
4980
|
}
|
|
4731
4981
|
case "REDUCE": {
|
|
4732
4982
|
const lambdaMatch = /\((\w+),\s*(\w+)\)\s*=>\s*(.+)/.exec(args[2]);
|
|
4733
|
-
if (!lambdaMatch) throw
|
|
4983
|
+
if (!lambdaMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "REDUCE requires a lambda: (acc, item) => expr");
|
|
4734
4984
|
const [, accVar, itemVar, lambdaExpr] = lambdaMatch;
|
|
4735
4985
|
const compiledExpr = lambdaExpr.replace(new RegExp(`\\b${accVar}\\b`, "g"), "$$value").replace(new RegExp(`\\b${itemVar}\\b`, "g"), "$$this");
|
|
4736
4986
|
return { $reduce: { input: parseValue(args[0]), initialValue: parseValue(args[1]), in: compileInnerExpression(compiledExpr) } };
|
|
@@ -4875,7 +5125,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4875
5125
|
return { $setUnion: unionArgs };
|
|
4876
5126
|
}
|
|
4877
5127
|
case "SWITCH": {
|
|
4878
|
-
if (args.length < 2) throw
|
|
5128
|
+
if (args.length < 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "SWITCH requires at least 2 arguments");
|
|
4879
5129
|
const branches = [];
|
|
4880
5130
|
let defaultValue = null;
|
|
4881
5131
|
for (let index = 0; index < args.length - 1; index += 2) {
|
|
@@ -4893,15 +5143,15 @@ function dispatchFunction(name, argsStr) {
|
|
|
4893
5143
|
case "ANY_ELEMENT_TRUE":
|
|
4894
5144
|
return { $anyElementTrue: [parseValue(args[0])] };
|
|
4895
5145
|
case "COND": {
|
|
4896
|
-
if (args.length !== 3) throw
|
|
5146
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "COND requires 3 arguments");
|
|
4897
5147
|
return { $cond: { if: compileInnerExpression(args[0]), then: parseValue(args[1]), else: parseValue(args[2]) } };
|
|
4898
5148
|
}
|
|
4899
5149
|
case "IF_NULL": {
|
|
4900
|
-
if (args.length !== 2) throw
|
|
5150
|
+
if (args.length !== 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "IF_NULL requires 2 arguments");
|
|
4901
5151
|
return { $ifNull: [parseValue(args[0]), parseValue(args[1])] };
|
|
4902
5152
|
}
|
|
4903
5153
|
case "SET_FIELD": {
|
|
4904
|
-
if (args.length !== 3) throw
|
|
5154
|
+
if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "SET_FIELD requires 3 arguments: (field, value, input)");
|
|
4905
5155
|
return { $setField: { field: parseValue(args[0]), input: parseValue(args[2]), value: parseValue(args[1]) } };
|
|
4906
5156
|
}
|
|
4907
5157
|
case "UNSET_FIELD":
|
|
@@ -4918,7 +5168,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4918
5168
|
return { $setIsSubset: [parseValue(args[0]), parseValue(args[1])] };
|
|
4919
5169
|
case "LET": {
|
|
4920
5170
|
const varsMatch = /\{(.+)\}/.exec(args[0]);
|
|
4921
|
-
if (!varsMatch) throw
|
|
5171
|
+
if (!varsMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "LET requires an object literal for variables");
|
|
4922
5172
|
const varPairs = varsMatch[1].split(",").map((pair) => {
|
|
4923
5173
|
const [key, ...rest] = pair.split(":");
|
|
4924
5174
|
return [key.trim(), rest.join(":").trim()];
|
|
@@ -4936,7 +5186,7 @@ function dispatchFunction(name, argsStr) {
|
|
|
4936
5186
|
case "SAMPLE_RATE":
|
|
4937
5187
|
return { $sampleRate: parseValue(args[0]) };
|
|
4938
5188
|
default:
|
|
4939
|
-
throw
|
|
5189
|
+
throw createError(ErrorCodes.INVALID_EXPRESSION, `Unsupported function: ${name}`);
|
|
4940
5190
|
}
|
|
4941
5191
|
}
|
|
4942
5192
|
function compileFilterCondition(condition, varName) {
|
|
@@ -5263,7 +5513,7 @@ function decodeCursor(cursor, secret) {
|
|
|
5263
5513
|
}
|
|
5264
5514
|
const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
5265
5515
|
if (payload?.v !== 1 || !Array.isArray(payload.values)) {
|
|
5266
|
-
throw
|
|
5516
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Invalid cursor payload.");
|
|
5267
5517
|
}
|
|
5268
5518
|
return payload.values;
|
|
5269
5519
|
} catch (cause) {
|
|
@@ -5362,6 +5612,7 @@ function buildEffectiveProjection(projection, sort) {
|
|
|
5362
5612
|
}
|
|
5363
5613
|
|
|
5364
5614
|
// src/adapters/mongodb/queries/find-page.ts
|
|
5615
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5365
5616
|
function normalizePositiveInteger(value, fallback, field) {
|
|
5366
5617
|
if (value === void 0 || value === null) {
|
|
5367
5618
|
return fallback;
|
|
@@ -5380,54 +5631,192 @@ function mergeFilters(base, extra) {
|
|
|
5380
5631
|
}
|
|
5381
5632
|
return { $and: [base, extra] };
|
|
5382
5633
|
}
|
|
5634
|
+
function stableStringify2(value) {
|
|
5635
|
+
if (value === void 0) {
|
|
5636
|
+
return '"__undefined__"';
|
|
5637
|
+
}
|
|
5638
|
+
if (value === null) {
|
|
5639
|
+
return "null";
|
|
5640
|
+
}
|
|
5641
|
+
if (Array.isArray(value)) {
|
|
5642
|
+
return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
|
|
5643
|
+
}
|
|
5644
|
+
if (value instanceof Date) {
|
|
5645
|
+
return JSON.stringify(value.toISOString());
|
|
5646
|
+
}
|
|
5647
|
+
if (typeof value === "object") {
|
|
5648
|
+
const customJson = value.toJSON;
|
|
5649
|
+
if (typeof customJson === "function" && value.constructor?.name !== "Object") {
|
|
5650
|
+
return stableStringify2(customJson.call(value));
|
|
5651
|
+
}
|
|
5652
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`);
|
|
5653
|
+
return `{${entries.join(",")}}`;
|
|
5654
|
+
}
|
|
5655
|
+
return JSON.stringify(value);
|
|
5656
|
+
}
|
|
5657
|
+
function hashPayload(payload) {
|
|
5658
|
+
return createHash2("sha256").update(stableStringify2(payload)).digest("hex");
|
|
5659
|
+
}
|
|
5660
|
+
function buildFindPageCacheKey(collection, options, normalized) {
|
|
5661
|
+
const payload = {
|
|
5662
|
+
query: normalized.query,
|
|
5663
|
+
sort: normalized.sort,
|
|
5664
|
+
limit: normalized.limit,
|
|
5665
|
+
page: normalized.page,
|
|
5666
|
+
after: options.after,
|
|
5667
|
+
before: options.before,
|
|
5668
|
+
projection: options.projection,
|
|
5669
|
+
pipeline: options.pipeline ?? [],
|
|
5670
|
+
totals: options.totals,
|
|
5671
|
+
jump: options.jump,
|
|
5672
|
+
offsetJump: options.offsetJump,
|
|
5673
|
+
maxTimeMS: normalized.maxTimeMS,
|
|
5674
|
+
hint: options.hint,
|
|
5675
|
+
collation: options.collation,
|
|
5676
|
+
batchSize: options.batchSize,
|
|
5677
|
+
options: options.options
|
|
5678
|
+
};
|
|
5679
|
+
const keyHash = hashPayload(payload);
|
|
5680
|
+
return { key: `findPage:${collection.namespace}:${keyHash}`, keyHash };
|
|
5681
|
+
}
|
|
5682
|
+
function buildTotalsCacheKey(collection, query, limit, totals) {
|
|
5683
|
+
const payload = {
|
|
5684
|
+
query,
|
|
5685
|
+
limit,
|
|
5686
|
+
mode: totals.mode ?? "sync",
|
|
5687
|
+
hint: totals.hint,
|
|
5688
|
+
collation: totals.collation,
|
|
5689
|
+
maxTimeMS: totals.maxTimeMS
|
|
5690
|
+
};
|
|
5691
|
+
const token = hashPayload(payload);
|
|
5692
|
+
return { key: `findPageTotals:${collection.namespace}:${token}`, token };
|
|
5693
|
+
}
|
|
5694
|
+
function cloneFindPageResult(result) {
|
|
5695
|
+
return {
|
|
5696
|
+
...result,
|
|
5697
|
+
items: Array.isArray(result.items) ? [...result.items] : result.items,
|
|
5698
|
+
pageInfo: result.pageInfo && typeof result.pageInfo === "object" ? { ...result.pageInfo } : result.pageInfo,
|
|
5699
|
+
totals: result.totals && typeof result.totals === "object" ? { ...result.totals } : result.totals,
|
|
5700
|
+
meta: result.meta && typeof result.meta === "object" ? {
|
|
5701
|
+
...result.meta,
|
|
5702
|
+
ns: { ...result.meta.ns },
|
|
5703
|
+
steps: result.meta.steps ? [...result.meta.steps] : void 0
|
|
5704
|
+
} : result.meta
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
function getPositiveTtl(value, fallback) {
|
|
5708
|
+
return typeof value === "number" && value > 0 ? value : fallback;
|
|
5709
|
+
}
|
|
5383
5710
|
var _asyncTotalsCache = new MemoryCache({
|
|
5384
5711
|
maxEntries: 1e4,
|
|
5385
5712
|
enableStats: false
|
|
5386
5713
|
});
|
|
5387
|
-
|
|
5714
|
+
var _totalsInflight = /* @__PURE__ */ new Map();
|
|
5715
|
+
function runTotalsOnce(key, task) {
|
|
5716
|
+
if (_totalsInflight.has(key)) {
|
|
5717
|
+
return;
|
|
5718
|
+
}
|
|
5719
|
+
const promise = task().catch(() => {
|
|
5720
|
+
}).finally(() => {
|
|
5721
|
+
_totalsInflight.delete(key);
|
|
5722
|
+
});
|
|
5723
|
+
_totalsInflight.set(key, promise);
|
|
5724
|
+
}
|
|
5725
|
+
async function computeTotals(coll, query, limit, totals, defaults = {}, queryCache) {
|
|
5388
5726
|
const mode = totals.mode ?? "sync";
|
|
5389
|
-
|
|
5727
|
+
const cache = queryCache ?? _asyncTotalsCache;
|
|
5728
|
+
const ttlMs = getPositiveTtl(totals.ttlMs, 10 * 6e4);
|
|
5729
|
+
const { key: cacheKey, token } = buildTotalsCacheKey(coll, query, limit, totals);
|
|
5730
|
+
const buildCountOptions = (fallbackMaxTimeMS) => {
|
|
5390
5731
|
const countOpts = {};
|
|
5391
|
-
|
|
5392
|
-
|
|
5732
|
+
const maxTimeMS = totals.maxTimeMS ?? fallbackMaxTimeMS;
|
|
5733
|
+
if (maxTimeMS !== void 0) {
|
|
5734
|
+
countOpts.maxTimeMS = maxTimeMS;
|
|
5393
5735
|
}
|
|
5394
|
-
|
|
5395
|
-
|
|
5736
|
+
if (totals.hint !== void 0) {
|
|
5737
|
+
countOpts.hint = totals.hint;
|
|
5738
|
+
}
|
|
5739
|
+
if (totals.collation !== void 0) {
|
|
5740
|
+
countOpts.collation = totals.collation;
|
|
5741
|
+
}
|
|
5742
|
+
return countOpts;
|
|
5743
|
+
};
|
|
5744
|
+
const countWithOptions = async () => {
|
|
5745
|
+
const countOpts = buildCountOptions(2e3);
|
|
5746
|
+
const countQuery = query;
|
|
5747
|
+
const runner = () => coll.countDocuments(
|
|
5748
|
+
countQuery,
|
|
5396
5749
|
countOpts
|
|
5397
5750
|
);
|
|
5398
|
-
|
|
5399
|
-
|
|
5751
|
+
return defaults.countQueue ? defaults.countQueue.execute(runner) : runner();
|
|
5752
|
+
};
|
|
5753
|
+
const buildPayload = (total, approx = false) => ({
|
|
5754
|
+
mode,
|
|
5755
|
+
total,
|
|
5756
|
+
totalPages: total > 0 ? Math.ceil(total / limit) : 0,
|
|
5757
|
+
ts: Date.now(),
|
|
5758
|
+
...approx ? { approx: true } : {}
|
|
5759
|
+
});
|
|
5760
|
+
const buildFailurePayload = (error) => ({
|
|
5761
|
+
mode,
|
|
5762
|
+
total: null,
|
|
5763
|
+
totalPages: null,
|
|
5764
|
+
ts: Date.now(),
|
|
5765
|
+
error
|
|
5766
|
+
});
|
|
5767
|
+
if (mode === "sync") {
|
|
5768
|
+
const cached = cache.get(cacheKey);
|
|
5769
|
+
if (cached !== void 0) {
|
|
5770
|
+
return { ...cached, mode: "sync" };
|
|
5771
|
+
}
|
|
5772
|
+
try {
|
|
5773
|
+
const payload = buildPayload(await countWithOptions());
|
|
5774
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5775
|
+
return { ...payload, mode: "sync" };
|
|
5776
|
+
} catch {
|
|
5777
|
+
const payload = buildFailurePayload("count_failed");
|
|
5778
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5779
|
+
return { ...payload, mode: "sync" };
|
|
5780
|
+
}
|
|
5400
5781
|
}
|
|
5401
5782
|
if (mode === "async") {
|
|
5402
|
-
const
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
if (cachedTotal !== void 0) {
|
|
5406
|
-
return { mode: "async", total: cachedTotal, token };
|
|
5783
|
+
const cached = cache.get(cacheKey);
|
|
5784
|
+
if (cached !== void 0) {
|
|
5785
|
+
return { ...cached, mode: "async", token };
|
|
5407
5786
|
}
|
|
5408
|
-
setImmediate(
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5787
|
+
setImmediate(() => {
|
|
5788
|
+
runTotalsOnce(cacheKey, async () => {
|
|
5789
|
+
try {
|
|
5790
|
+
const payload = buildPayload(await countWithOptions());
|
|
5791
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5792
|
+
} catch {
|
|
5793
|
+
await Promise.resolve(cache.set(cacheKey, buildFailurePayload("count_failed"), ttlMs));
|
|
5794
|
+
}
|
|
5795
|
+
});
|
|
5416
5796
|
});
|
|
5417
5797
|
return { mode: "async", total: null, token };
|
|
5418
5798
|
}
|
|
5419
5799
|
if (mode === "approx") {
|
|
5420
|
-
const
|
|
5421
|
-
if (
|
|
5422
|
-
|
|
5800
|
+
const cached = cache.get(cacheKey);
|
|
5801
|
+
if (cached !== void 0) {
|
|
5802
|
+
return { ...cached, mode: "approx" };
|
|
5803
|
+
}
|
|
5804
|
+
try {
|
|
5805
|
+
const total = Object.keys(query ?? {}).length > 0 ? await countWithOptions() : await coll.estimatedDocumentCount({
|
|
5806
|
+
maxTimeMS: totals.maxTimeMS ?? 1e3
|
|
5807
|
+
});
|
|
5808
|
+
const payload = buildPayload(total, true);
|
|
5809
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5810
|
+
return { ...payload, mode: "approx" };
|
|
5811
|
+
} catch {
|
|
5812
|
+
const payload = buildFailurePayload("approx_failed");
|
|
5813
|
+
await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
|
|
5814
|
+
return { ...payload, mode: "approx" };
|
|
5423
5815
|
}
|
|
5424
|
-
const total = await coll.estimatedDocumentCount(countOpts);
|
|
5425
|
-
const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
|
|
5426
|
-
return { mode: "approx", total, totalPages, ts: Date.now() };
|
|
5427
5816
|
}
|
|
5428
5817
|
return { mode: mode ?? "sync" };
|
|
5429
5818
|
}
|
|
5430
|
-
async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
5819
|
+
async function executeFindPage(collection, options = {}, defaults = {}, queryCache) {
|
|
5431
5820
|
const metaEnabled = options.meta === true || typeof options.meta === "object" && options.meta !== null;
|
|
5432
5821
|
const metaOptions = options.meta && typeof options.meta === "object" ? options.meta : {};
|
|
5433
5822
|
const metaLevel = options.meta === true ? "op" : metaOptions.level ?? "op";
|
|
@@ -5458,6 +5847,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5458
5847
|
driverOpts.projection = buildEffectiveProjection(options.projection, sort);
|
|
5459
5848
|
}
|
|
5460
5849
|
const jumpOpts = ext.jump;
|
|
5850
|
+
const cacheTTL = typeof ext.cache === "number" && ext.cache > 0 ? ext.cache : 0;
|
|
5851
|
+
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;
|
|
5852
|
+
const shouldRefreshAsyncTotals = options.totals?.mode === "async";
|
|
5853
|
+
let findPageCacheHit = false;
|
|
5461
5854
|
const finishResult = (result2) => {
|
|
5462
5855
|
if (!metaEnabled) {
|
|
5463
5856
|
return result2;
|
|
@@ -5488,6 +5881,9 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5488
5881
|
endTs: metaEndTs,
|
|
5489
5882
|
durationMs: metaEndTs - metaStartTs,
|
|
5490
5883
|
...typeof effectiveMaxTimeMS === "number" ? { maxTimeMS: effectiveMaxTimeMS } : {},
|
|
5884
|
+
cacheHit: findPageCacheHit,
|
|
5885
|
+
...findPageCacheHit ? { fromCache: true } : {},
|
|
5886
|
+
...pageResultCache ? { cacheTtl: cacheTTL, keyHash: pageResultCache.keyHash } : {},
|
|
5491
5887
|
page,
|
|
5492
5888
|
after: Boolean(options.after),
|
|
5493
5889
|
before: Boolean(options.before),
|
|
@@ -5556,7 +5952,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5556
5952
|
};
|
|
5557
5953
|
const timedComputeTotals = async () => {
|
|
5558
5954
|
const stepStartTs = Date.now();
|
|
5559
|
-
const result2 = await computeTotals(collection, baseQuery, limit, options.totals);
|
|
5955
|
+
const result2 = await computeTotals(collection, baseQuery, limit, options.totals, defaults, queryCache);
|
|
5560
5956
|
pushMetaStep("computeTotals", Date.now() - stepStartTs, "totals");
|
|
5561
5957
|
return result2;
|
|
5562
5958
|
};
|
|
@@ -5575,6 +5971,32 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5575
5971
|
...extra.currentPage !== void 0 ? { currentPage: extra.currentPage } : {}
|
|
5576
5972
|
};
|
|
5577
5973
|
};
|
|
5974
|
+
const writePageResultCache = (result2) => {
|
|
5975
|
+
if (!pageResultCache || !queryCache) {
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
const cacheValue = cloneFindPageResult(result2);
|
|
5979
|
+
delete cacheValue.meta;
|
|
5980
|
+
if (shouldRefreshAsyncTotals) {
|
|
5981
|
+
delete cacheValue.totals;
|
|
5982
|
+
}
|
|
5983
|
+
void queryCache.set(pageResultCache.key, cacheValue, cacheTTL);
|
|
5984
|
+
};
|
|
5985
|
+
const finishAndCache = (result2) => {
|
|
5986
|
+
writePageResultCache(result2);
|
|
5987
|
+
return finishResult(result2);
|
|
5988
|
+
};
|
|
5989
|
+
if (pageResultCache && queryCache) {
|
|
5990
|
+
const cached = queryCache.get(pageResultCache.key);
|
|
5991
|
+
if (cached !== void 0) {
|
|
5992
|
+
findPageCacheHit = true;
|
|
5993
|
+
const result2 = cloneFindPageResult(cached);
|
|
5994
|
+
if (options.totals && options.totals.mode !== "none" && (shouldRefreshAsyncTotals || result2.totals === void 0)) {
|
|
5995
|
+
result2.totals = await timedComputeTotals();
|
|
5996
|
+
}
|
|
5997
|
+
return finishResult(result2);
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
5578
6000
|
if (options.stream === true) {
|
|
5579
6001
|
const direction = options.before ? "before" : "after";
|
|
5580
6002
|
const { queryFilter, effectiveSort } = buildPageQuery(options.after ?? options.before, direction);
|
|
@@ -5617,7 +6039,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5617
6039
|
if (options.totals && options.totals.mode !== "none") {
|
|
5618
6040
|
result2.totals = await timedComputeTotals();
|
|
5619
6041
|
}
|
|
5620
|
-
return
|
|
6042
|
+
return finishAndCache(result2);
|
|
5621
6043
|
}
|
|
5622
6044
|
if (options.after || options.before) {
|
|
5623
6045
|
const direction = options.after ? "after" : "before";
|
|
@@ -5627,7 +6049,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5627
6049
|
const first = items2[0] ?? null;
|
|
5628
6050
|
const last = items2[items2.length - 1] ?? null;
|
|
5629
6051
|
const enc = (item) => item ? encodeCursor(Object.keys(sort).map((f) => item[f]), cursorSecret) : null;
|
|
5630
|
-
|
|
6052
|
+
const result2 = {
|
|
5631
6053
|
items: items2,
|
|
5632
6054
|
pageInfo: {
|
|
5633
6055
|
hasNext: direction === "before" ? Boolean(options.before) : hasMore2,
|
|
@@ -5635,7 +6057,11 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5635
6057
|
startCursor: enc(first),
|
|
5636
6058
|
endCursor: enc(last)
|
|
5637
6059
|
}
|
|
5638
|
-
}
|
|
6060
|
+
};
|
|
6061
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6062
|
+
result2.totals = await timedComputeTotals();
|
|
6063
|
+
}
|
|
6064
|
+
return finishAndCache(result2);
|
|
5639
6065
|
}
|
|
5640
6066
|
const { queryFilter: q0, effectiveSort: es0 } = buildPageQuery();
|
|
5641
6067
|
let { items, hasMore } = await timedFetchItems("initialFetch", page > 1 ? "hop" : "fetch", q0, es0, {}, 1);
|
|
@@ -5647,15 +6073,19 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5647
6073
|
if (options.totals && options.totals.mode !== "none") {
|
|
5648
6074
|
result2.totals = await timedComputeTotals();
|
|
5649
6075
|
}
|
|
5650
|
-
return
|
|
6076
|
+
return finishAndCache(result2);
|
|
5651
6077
|
}
|
|
5652
6078
|
for (let cp = 2; cp <= page; cp++) {
|
|
5653
6079
|
const lastItem = items[items.length - 1];
|
|
5654
6080
|
if (!lastItem) {
|
|
5655
|
-
|
|
6081
|
+
const result2 = {
|
|
5656
6082
|
items,
|
|
5657
6083
|
pageInfo: buildPageInfo(items, false, { hasPrev: cp > 2, currentPage: cp - 1 })
|
|
5658
|
-
}
|
|
6084
|
+
};
|
|
6085
|
+
if (options.totals && options.totals.mode !== "none") {
|
|
6086
|
+
result2.totals = await timedComputeTotals();
|
|
6087
|
+
}
|
|
6088
|
+
return finishAndCache(result2);
|
|
5659
6089
|
}
|
|
5660
6090
|
const endCursor = encodeCursor(
|
|
5661
6091
|
Object.keys(sort).map((f) => lastItem[f]),
|
|
@@ -5673,10 +6103,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
|
|
|
5673
6103
|
if (options.totals && options.totals.mode !== "none") {
|
|
5674
6104
|
result.totals = await timedComputeTotals();
|
|
5675
6105
|
}
|
|
5676
|
-
return
|
|
6106
|
+
return finishAndCache(result);
|
|
5677
6107
|
}
|
|
5678
|
-
async function findPageDocuments(collection, options = {}, defaults) {
|
|
5679
|
-
return executeFindPage(collection, options, defaults ?? {});
|
|
6108
|
+
async function findPageDocuments(collection, options = {}, defaults, queryCache) {
|
|
6109
|
+
return executeFindPage(collection, options, defaults ?? {}, queryCache);
|
|
5680
6110
|
}
|
|
5681
6111
|
|
|
5682
6112
|
// src/adapters/mongodb/queries/find-by-id.ts
|
|
@@ -5898,21 +6328,21 @@ var FindChain = class {
|
|
|
5898
6328
|
}
|
|
5899
6329
|
limit(value) {
|
|
5900
6330
|
if (typeof value !== "number" || value < 0) {
|
|
5901
|
-
throw
|
|
6331
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `limit() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5902
6332
|
}
|
|
5903
6333
|
this.options.limit = value;
|
|
5904
6334
|
return this;
|
|
5905
6335
|
}
|
|
5906
6336
|
skip(value) {
|
|
5907
6337
|
if (typeof value !== "number" || value < 0) {
|
|
5908
|
-
throw
|
|
6338
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `skip() requires a non-negative number, got: ${typeof value} (${value})`);
|
|
5909
6339
|
}
|
|
5910
6340
|
this.options.skip = value;
|
|
5911
6341
|
return this;
|
|
5912
6342
|
}
|
|
5913
6343
|
sort(value) {
|
|
5914
6344
|
if (!value || typeof value !== "object") {
|
|
5915
|
-
throw
|
|
6345
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `sort() requires an object or array, got: ${typeof value}`);
|
|
5916
6346
|
}
|
|
5917
6347
|
this.options.sort = value;
|
|
5918
6348
|
return this;
|
|
@@ -5956,7 +6386,7 @@ var FindChain = class {
|
|
|
5956
6386
|
}
|
|
5957
6387
|
toArray() {
|
|
5958
6388
|
if (this.executed) {
|
|
5959
|
-
throw
|
|
6389
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
5960
6390
|
}
|
|
5961
6391
|
this.executed = true;
|
|
5962
6392
|
return this.collection.find(this.normalizedQuery, buildFindDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6044,7 +6474,7 @@ var AggregateChain = class {
|
|
|
6044
6474
|
}
|
|
6045
6475
|
toArray() {
|
|
6046
6476
|
if (this.executed) {
|
|
6047
|
-
throw
|
|
6477
|
+
throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
|
|
6048
6478
|
}
|
|
6049
6479
|
this.executed = true;
|
|
6050
6480
|
return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray();
|
|
@@ -6839,7 +7269,7 @@ async function insertOneForAccessor(context, doc, options) {
|
|
|
6839
7269
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6840
7270
|
if (elapsed > threshold && context.logger) {
|
|
6841
7271
|
try {
|
|
6842
|
-
context.logger.warn("[insertOne]
|
|
7272
|
+
context.logger.warn("[insertOne] slow operation warning", {
|
|
6843
7273
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6844
7274
|
threshold,
|
|
6845
7275
|
duration: elapsed,
|
|
@@ -6884,7 +7314,7 @@ async function insertManyForAccessor(context, documents, options) {
|
|
|
6884
7314
|
const elapsed = Date.now() - startedAt;
|
|
6885
7315
|
const threshold = context.defaults?.slowQueryMs ?? 500;
|
|
6886
7316
|
if (elapsed >= threshold && context.logger) {
|
|
6887
|
-
context.logger.warn("[insertMany]
|
|
7317
|
+
context.logger.warn("[insertMany] slow operation warning", {
|
|
6888
7318
|
ns: `${context.dbName}.${context.collectionName}`,
|
|
6889
7319
|
threshold,
|
|
6890
7320
|
duration: elapsed,
|
|
@@ -7015,11 +7445,17 @@ var MongoCollectionAccessor = class {
|
|
|
7015
7445
|
const legacyNamespacePatterns = [
|
|
7016
7446
|
`${String(this.management.defaults?.namespace?.instanceId)}:mongodb:${this.dbName}:${this.collectionName}:*`
|
|
7017
7447
|
];
|
|
7018
|
-
const
|
|
7448
|
+
const findPagePatterns = [
|
|
7449
|
+
`findPage:${namespace}:*`,
|
|
7450
|
+
`findPageTotals:${namespace}:*`,
|
|
7451
|
+
`bookmark:${bookmarkNamespace}:*`,
|
|
7452
|
+
`${bookmarkNamespace}:bm:*`
|
|
7453
|
+
];
|
|
7454
|
+
const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? findPagePatterns : [
|
|
7019
7455
|
`find:${namespace}:*`,
|
|
7020
7456
|
`findOne:${namespace}:*`,
|
|
7021
7457
|
`count:${namespace}:*`,
|
|
7022
|
-
|
|
7458
|
+
...findPagePatterns
|
|
7023
7459
|
];
|
|
7024
7460
|
patterns.push(...legacyNamespacePatterns);
|
|
7025
7461
|
let deleted = 0;
|
|
@@ -7149,7 +7585,7 @@ var MongoCollectionAccessor = class {
|
|
|
7149
7585
|
}
|
|
7150
7586
|
async findPage(options = {}) {
|
|
7151
7587
|
const resolvedOptions = options.query ? { ...options, query: this._cvFilter(options.query) } : options;
|
|
7152
|
-
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults);
|
|
7588
|
+
return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults, this.management.queryCache);
|
|
7153
7589
|
}
|
|
7154
7590
|
/** Opens a change stream on the collection with an optional aggregation pipeline. */
|
|
7155
7591
|
watch(pipeline = [], options) {
|
|
@@ -7841,7 +8277,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7841
8277
|
dropDatabase: async (name, adminOptions) => {
|
|
7842
8278
|
host.ensureConnected();
|
|
7843
8279
|
if (!name || typeof name !== "string") {
|
|
7844
|
-
throw
|
|
8280
|
+
throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name is required and must be a non-empty string");
|
|
7845
8281
|
}
|
|
7846
8282
|
if (!adminOptions?.confirm) {
|
|
7847
8283
|
const error = new Error(
|
|
@@ -7877,7 +8313,7 @@ function createRuntimeAdapterBridge(host) {
|
|
|
7877
8313
|
runCommand: async (command, adminOptions) => {
|
|
7878
8314
|
host.ensureConnected();
|
|
7879
8315
|
if (command === null || typeof command !== "object") {
|
|
7880
|
-
throw
|
|
8316
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Command must be a non-null object");
|
|
7881
8317
|
}
|
|
7882
8318
|
return host.db().runCommand(command, adminOptions ?? {});
|
|
7883
8319
|
},
|
|
@@ -8003,8 +8439,8 @@ var LockManager = class {
|
|
|
8003
8439
|
if (attempt === retryTimes) {
|
|
8004
8440
|
break;
|
|
8005
8441
|
}
|
|
8006
|
-
const
|
|
8007
|
-
await sleep3(
|
|
8442
|
+
const delay2 = retryDelay * Math.pow(retryBackoff, attempt);
|
|
8443
|
+
await sleep3(delay2);
|
|
8008
8444
|
}
|
|
8009
8445
|
this.stats.errors += 1;
|
|
8010
8446
|
if (options.fallbackToNoLock) {
|
|
@@ -8128,7 +8564,7 @@ var DistributedCacheLockManager = class {
|
|
|
8128
8564
|
errors: 0
|
|
8129
8565
|
};
|
|
8130
8566
|
if (!options.redis) {
|
|
8131
|
-
throw
|
|
8567
|
+
throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheLockManager requires a Redis instance");
|
|
8132
8568
|
}
|
|
8133
8569
|
this.redis = options.redis;
|
|
8134
8570
|
this.lockKeyPrefix = options.lockKeyPrefix ?? "monsqlize:cache:lock:";
|
|
@@ -8786,17 +9222,17 @@ var BatchQueue = class {
|
|
|
8786
9222
|
import { MongoClient as MongoDriverClient2 } from "mongodb";
|
|
8787
9223
|
|
|
8788
9224
|
// src/capabilities/slow-query-log/slow-query-log-records.ts
|
|
8789
|
-
import { createHash as
|
|
8790
|
-
function
|
|
9225
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
9226
|
+
function stableStringify3(value) {
|
|
8791
9227
|
if (Array.isArray(value)) {
|
|
8792
|
-
return `[${value.map((item) =>
|
|
9228
|
+
return `[${value.map((item) => stableStringify3(item)).join(",")}]`;
|
|
8793
9229
|
}
|
|
8794
9230
|
if (value instanceof Date) {
|
|
8795
9231
|
return JSON.stringify(value.toISOString());
|
|
8796
9232
|
}
|
|
8797
9233
|
if (value && typeof value === "object") {
|
|
8798
9234
|
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
8799
|
-
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${
|
|
9235
|
+
return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify3(current)}`).join(",")}}`;
|
|
8800
9236
|
}
|
|
8801
9237
|
return JSON.stringify(value);
|
|
8802
9238
|
}
|
|
@@ -8813,7 +9249,7 @@ function normalizeHashInput(input) {
|
|
|
8813
9249
|
};
|
|
8814
9250
|
}
|
|
8815
9251
|
function generateQueryHash(input) {
|
|
8816
|
-
return
|
|
9252
|
+
return createHash3("sha256").update(stableStringify3(normalizeHashInput(input))).digest("hex").slice(0, 16);
|
|
8817
9253
|
}
|
|
8818
9254
|
function handleSlowQueryLogError(logger, policy, error) {
|
|
8819
9255
|
if (policy === "throw") {
|
|
@@ -9263,7 +9699,7 @@ var SlowQueryLogManager = class {
|
|
|
9263
9699
|
|
|
9264
9700
|
// src/capabilities/sync/index.ts
|
|
9265
9701
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
9266
|
-
import
|
|
9702
|
+
import path2 from "node:path";
|
|
9267
9703
|
import { MongoClient as MongoDriverClient3 } from "mongodb";
|
|
9268
9704
|
function validateTargetConfig(target, index) {
|
|
9269
9705
|
if (!target || typeof target !== "object") {
|
|
@@ -9370,7 +9806,7 @@ var ResumeTokenStore = class {
|
|
|
9370
9806
|
await Promise.resolve(this.redis.set(this.redisKey, payload));
|
|
9371
9807
|
return;
|
|
9372
9808
|
}
|
|
9373
|
-
await mkdir(
|
|
9809
|
+
await mkdir(path2.dirname(this.path), { recursive: true });
|
|
9374
9810
|
await writeFile(this.path, payload, "utf8");
|
|
9375
9811
|
} catch (error) {
|
|
9376
9812
|
this.logger?.error?.("[Sync] failed to save resume token", error);
|
|
@@ -9642,7 +10078,16 @@ function getOrCreateTransactionManager(config) {
|
|
|
9642
10078
|
client: config.client,
|
|
9643
10079
|
cache: config.cache,
|
|
9644
10080
|
logger: config.logger,
|
|
9645
|
-
lockManager: config.lockManager
|
|
10081
|
+
lockManager: config.lockManager,
|
|
10082
|
+
maxDuration: config.transaction?.maxDuration ?? config.transaction?.defaultTimeout,
|
|
10083
|
+
enableRetry: config.transaction?.enableRetry,
|
|
10084
|
+
maxRetries: config.transaction?.maxRetries,
|
|
10085
|
+
retryDelay: config.transaction?.retryDelay,
|
|
10086
|
+
retryBackoff: config.transaction?.retryBackoff,
|
|
10087
|
+
defaultReadConcern: config.transaction?.defaultReadConcern,
|
|
10088
|
+
defaultWriteConcern: config.transaction?.defaultWriteConcern,
|
|
10089
|
+
defaultReadPreference: config.transaction?.defaultReadPreference,
|
|
10090
|
+
maxStatsSamples: config.transaction?.maxStatsSamples
|
|
9646
10091
|
});
|
|
9647
10092
|
}
|
|
9648
10093
|
function getOrCreateLockManager(current, logger) {
|
|
@@ -9932,7 +10377,7 @@ function resolveCacheSource(cacheOrDb) {
|
|
|
9932
10377
|
return cache;
|
|
9933
10378
|
}
|
|
9934
10379
|
}
|
|
9935
|
-
throw
|
|
10380
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance from MonSQLize");
|
|
9936
10381
|
}
|
|
9937
10382
|
function toWithCacheStats(stats, totalTime = 0) {
|
|
9938
10383
|
const calls = stats.hits + stats.misses;
|
|
@@ -9961,46 +10406,46 @@ function normalizeFunctionCacheStats(stats, timings, name) {
|
|
|
9961
10406
|
}
|
|
9962
10407
|
function validateFunctionCacheOptions(options) {
|
|
9963
10408
|
if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
|
|
9964
|
-
throw
|
|
10409
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
|
|
9965
10410
|
}
|
|
9966
10411
|
const opts = options ?? {};
|
|
9967
10412
|
if (opts.namespace !== void 0 && typeof opts.namespace !== "string") {
|
|
9968
|
-
throw
|
|
10413
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "namespace must be a string");
|
|
9969
10414
|
}
|
|
9970
10415
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
9971
10416
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
9972
|
-
throw
|
|
10417
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
9973
10418
|
}
|
|
9974
10419
|
return opts;
|
|
9975
10420
|
}
|
|
9976
10421
|
function validateFunctionCachePerFnOptions(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.keyBuilder !== void 0 && typeof opts.keyBuilder !== "function") {
|
|
9982
|
-
throw
|
|
10427
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
9983
10428
|
}
|
|
9984
10429
|
if (opts.condition !== void 0 && typeof opts.condition !== "function") {
|
|
9985
|
-
throw
|
|
10430
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
9986
10431
|
}
|
|
9987
10432
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
9988
10433
|
if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
|
|
9989
|
-
throw
|
|
10434
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
|
|
9990
10435
|
}
|
|
9991
10436
|
return opts;
|
|
9992
10437
|
}
|
|
9993
10438
|
function withCache(fn, options = {}) {
|
|
9994
|
-
if (typeof fn !== "function") throw
|
|
10439
|
+
if (typeof fn !== "function") throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
9995
10440
|
const { ttl = 6e4, namespace, keyBuilder, condition, cache: externalCache, enableStats = true } = options;
|
|
9996
10441
|
if (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)
|
|
9997
|
-
throw
|
|
10442
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "ttl must be a non-negative number");
|
|
9998
10443
|
if (keyBuilder !== void 0 && typeof keyBuilder !== "function")
|
|
9999
|
-
throw
|
|
10444
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
|
|
10000
10445
|
if (condition !== void 0 && typeof condition !== "function")
|
|
10001
|
-
throw
|
|
10446
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
|
|
10002
10447
|
if (externalCache !== void 0 && !isValidCache(externalCache))
|
|
10003
|
-
throw
|
|
10448
|
+
throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance");
|
|
10004
10449
|
const wrapped = hubWithCache(fn, {
|
|
10005
10450
|
ttl,
|
|
10006
10451
|
namespace: namespace ?? "fn",
|
|
@@ -10042,9 +10487,9 @@ var FunctionCache = class {
|
|
|
10042
10487
|
}
|
|
10043
10488
|
register(name, fn, options) {
|
|
10044
10489
|
if (!name || typeof name !== "string")
|
|
10045
|
-
throw
|
|
10490
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10046
10491
|
if (typeof fn !== "function")
|
|
10047
|
-
throw
|
|
10492
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
|
|
10048
10493
|
const opts = validateFunctionCachePerFnOptions(options);
|
|
10049
10494
|
const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
|
|
10050
10495
|
this._inner.register(name, fn, {
|
|
@@ -10070,13 +10515,13 @@ var FunctionCache = class {
|
|
|
10070
10515
|
}
|
|
10071
10516
|
async invalidate(name, ...args) {
|
|
10072
10517
|
if (!name || typeof name !== "string") {
|
|
10073
|
-
throw
|
|
10518
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
|
|
10074
10519
|
}
|
|
10075
10520
|
await this._inner.invalidate(name, ...args);
|
|
10076
10521
|
}
|
|
10077
10522
|
async invalidatePattern(pattern) {
|
|
10078
10523
|
if (!pattern || typeof pattern !== "string")
|
|
10079
|
-
throw
|
|
10524
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Pattern must be a non-empty string");
|
|
10080
10525
|
return this._inner.invalidatePattern(pattern);
|
|
10081
10526
|
}
|
|
10082
10527
|
list() {
|
|
@@ -10111,15 +10556,15 @@ function b64urlDecodeStr(s) {
|
|
|
10111
10556
|
const b64 = pad(String(s || "")).replace(/-/g, "+").replace(/_/g, "/");
|
|
10112
10557
|
return Buffer.from(b64, "base64").toString();
|
|
10113
10558
|
}
|
|
10114
|
-
function makeInvalidCursorError(cause) {
|
|
10115
|
-
const err = new Error(
|
|
10559
|
+
function makeInvalidCursorError(message = "Invalid cursor", cause) {
|
|
10560
|
+
const err = new Error(message);
|
|
10116
10561
|
err.code = "INVALID_CURSOR";
|
|
10117
10562
|
if (cause !== void 0) err.cause = cause;
|
|
10118
10563
|
return err;
|
|
10119
10564
|
}
|
|
10120
10565
|
function encodeCursor2(payload) {
|
|
10121
10566
|
if (!payload.s || !payload.a) {
|
|
10122
|
-
throw
|
|
10567
|
+
throw makeInvalidCursorError("encodeCursor requires sort (s) and anchor (a)");
|
|
10123
10568
|
}
|
|
10124
10569
|
const json = JSON.stringify({ v: payload.v ?? 1, s: payload.s, a: payload.a, d: payload.d });
|
|
10125
10570
|
return b64urlEncodeStr(json);
|
|
@@ -10128,11 +10573,11 @@ function decodeCursor2(str) {
|
|
|
10128
10573
|
try {
|
|
10129
10574
|
const obj = JSON.parse(b64urlDecodeStr(str));
|
|
10130
10575
|
if (!obj || obj["v"] !== 1 || !obj["s"] || !obj["a"]) {
|
|
10131
|
-
throw
|
|
10576
|
+
throw makeInvalidCursorError("bad-structure");
|
|
10132
10577
|
}
|
|
10133
10578
|
return obj;
|
|
10134
10579
|
} catch (e) {
|
|
10135
|
-
throw makeInvalidCursorError(e);
|
|
10580
|
+
throw makeInvalidCursorError("Invalid cursor", e);
|
|
10136
10581
|
}
|
|
10137
10582
|
}
|
|
10138
10583
|
|
|
@@ -10204,7 +10649,11 @@ var MonSQLizeRuntime = class {
|
|
|
10204
10649
|
} : rawCacheInput;
|
|
10205
10650
|
this._cache = normalizeRuntimeCache(cacheInput);
|
|
10206
10651
|
this._logger = Logger.create(options.logger ?? null);
|
|
10207
|
-
this._cacheLockManager = new CacheLockManager({
|
|
10652
|
+
this._cacheLockManager = new CacheLockManager({
|
|
10653
|
+
logger: options.logger ?? null,
|
|
10654
|
+
maxDuration: options.transaction?.lockMaxDuration,
|
|
10655
|
+
cleanupInterval: options.transaction?.lockCleanupInterval
|
|
10656
|
+
});
|
|
10208
10657
|
this._cache.setLockManager?.(this._cacheLockManager);
|
|
10209
10658
|
this._runtimeDefaults = buildRuntimeDefaults(options);
|
|
10210
10659
|
this._adapterCacheOverride = void 0;
|
|
@@ -10575,9 +11024,15 @@ var MonSQLizeRuntime = class {
|
|
|
10575
11024
|
listSagas() {
|
|
10576
11025
|
return this.initializeSagaOrchestrator().listSagas();
|
|
10577
11026
|
}
|
|
11027
|
+
getTransactionStats() {
|
|
11028
|
+
return this._transactionManager?.getStats() ?? null;
|
|
11029
|
+
}
|
|
10578
11030
|
getSagaStats() {
|
|
10579
11031
|
return this.initializeSagaOrchestrator().getStats();
|
|
10580
11032
|
}
|
|
11033
|
+
getDistributedCacheInvalidatorStats() {
|
|
11034
|
+
return this._distributedInvalidator?.getStats() ?? null;
|
|
11035
|
+
}
|
|
10581
11036
|
async startSync() {
|
|
10582
11037
|
this.ensureConnected();
|
|
10583
11038
|
const manager = await this.initializeSyncManager();
|
|
@@ -10656,7 +11111,8 @@ var MonSQLizeRuntime = class {
|
|
|
10656
11111
|
client: this._client,
|
|
10657
11112
|
cache: this._cache,
|
|
10658
11113
|
logger: this.options.logger ?? null,
|
|
10659
|
-
lockManager: this._cacheLockManager
|
|
11114
|
+
lockManager: this._cacheLockManager,
|
|
11115
|
+
transaction: this.options.transaction
|
|
10660
11116
|
});
|
|
10661
11117
|
return this._transactionManager;
|
|
10662
11118
|
}
|