monsqlize 2.0.2 → 2.0.4

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.
@@ -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 \u5FC5\u987B\u662F Redis URL \u5B57\u7B26\u4E32\u6216 ioredis \u5B9E\u4F8B";
132
- var LEGACY_IOREDIS_MISSING_ERROR = "ioredis \u672A\u5B89\u88C5\u3002\u8BF7\u8FD0\u884C: npm install ioredis\n\u6216\u4F20\u5165\u5DF2\u521B\u5EFA\u7684 ioredis \u5B9E\u4F8B";
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 new Error("DistributedCacheInvalidator requires a cache instance");
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 new Error("DistributedCacheInvalidator requires either redis or redisUrl");
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(path2, options) {
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(path2)) {
374
- return new _PopulatePromise(this.executor, [...this.paths, ...path2.map(toConfig)]);
373
+ if (Array.isArray(path3)) {
374
+ return new _PopulatePromise(this.executor, [...this.paths, ...path3.map(toConfig)]);
375
375
  }
376
- const config = toConfig(path2);
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(path2) {
491
- return typeof path2 === "string" ? { path: path2 } : path2;
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, path2) {
643
- return path2.split(".").reduce((current, key) => {
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, path2) {
699
- const config = normalizePopulateConfig(path2);
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: (path2) => {
817
- const paths = Array.isArray(path2) ? path2 : [path2];
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
@@ -980,6 +980,75 @@ function stableIndexStringify(value) {
980
980
  }
981
981
  return JSON.stringify(value) ?? "undefined";
982
982
  }
983
+ function getIndexOptionName(options) {
984
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
985
+ }
986
+ function summarizeIndexError(error) {
987
+ if (error instanceof Error) {
988
+ const record = error;
989
+ return {
990
+ name: error.name,
991
+ message: error.message,
992
+ code: record.code ?? record.codeName
993
+ };
994
+ }
995
+ return {
996
+ message: String(error)
997
+ };
998
+ }
999
+ function isRecord(value) {
1000
+ return !!value && typeof value === "object" && !Array.isArray(value);
1001
+ }
1002
+ function getExistingIndexKey(index) {
1003
+ return index.key;
1004
+ }
1005
+ function declaredOptionEntries(options) {
1006
+ return Object.entries(options).filter(([name, value]) => {
1007
+ if (value === void 0) return false;
1008
+ if (name === "background") return false;
1009
+ return true;
1010
+ });
1011
+ }
1012
+ function indexOptionsMatch(existing, declared) {
1013
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
1014
+ return false;
1015
+ }
1016
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
1017
+ const existingValue = existing[name];
1018
+ if (value === false && existingValue === void 0) {
1019
+ continue;
1020
+ }
1021
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
1022
+ return false;
1023
+ }
1024
+ }
1025
+ return true;
1026
+ }
1027
+ function findExistingIndexByName(existingIndexes, name) {
1028
+ if (!name) return void 0;
1029
+ return existingIndexes.find((index) => index.name === name);
1030
+ }
1031
+ function findExistingIndexByKey(existingIndexes, key) {
1032
+ const fingerprint = stableIndexStringify(key);
1033
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
1034
+ }
1035
+ function createIndexEnsureError(message, result, cause) {
1036
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
1037
+ }
1038
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
1039
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
1040
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1041
+ if (value === false) {
1042
+ return { enabled: false, emitEvents: true };
1043
+ }
1044
+ if (value && typeof value === "object") {
1045
+ return {
1046
+ enabled: value.enabled !== false,
1047
+ emitEvents: value.emitEvents !== false
1048
+ };
1049
+ }
1050
+ return { enabled: true, emitEvents: true };
1051
+ }
983
1052
  function getIndexTaskRegistry(runtime) {
984
1053
  if (!runtime) {
985
1054
  return fallbackModelIndexTasks;
@@ -1007,6 +1076,20 @@ function resolveIndexTaskScope(collection, options) {
1007
1076
  };
1008
1077
  }
1009
1078
  }
1079
+ function toIndexNamespace(scope) {
1080
+ return {
1081
+ db: scope.dbName,
1082
+ collection: scope.collectionName,
1083
+ poolName: scope.poolName
1084
+ };
1085
+ }
1086
+ function emitIndexFailure(runtime, payload, emitEvents) {
1087
+ if (!emitEvents) {
1088
+ return;
1089
+ }
1090
+ const emitter = runtime;
1091
+ emitter?.emit?.("model-index-error", payload);
1092
+ }
1010
1093
  function warnIndexFailure(runtime, taskKey, error) {
1011
1094
  const logger = runtime;
1012
1095
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1014,9 +1097,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1014
1097
  error: error instanceof Error ? error.message : String(error)
1015
1098
  });
1016
1099
  }
1017
- function scheduleIndexTask(collection, key, indexOptions, options) {
1100
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1018
1101
  const scope = resolveIndexTaskScope(collection, options);
1019
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1102
+ const { key, options: indexOptions } = declaredIndex;
1103
+ const indexFingerprint = declaredIndex.fingerprint;
1020
1104
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1021
1105
  const registry = getIndexTaskRegistry(options?.runtime);
1022
1106
  const existing = registry.get(taskKey);
@@ -1034,6 +1118,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1034
1118
  task.status = "failed";
1035
1119
  task.error = error;
1036
1120
  warnIndexFailure(options?.runtime, taskKey, error);
1121
+ emitIndexFailure(options?.runtime, {
1122
+ namespace: scope,
1123
+ taskKey,
1124
+ source: declaredIndex.source,
1125
+ key,
1126
+ options: indexOptions,
1127
+ error: summarizeIndexError(error)
1128
+ }, emitEvents);
1037
1129
  resolve();
1038
1130
  });
1039
1131
  });
@@ -1150,6 +1242,134 @@ function resolveModelHooksFactory(definition) {
1150
1242
  const hooks = toCompatDefinition(definition).hooks;
1151
1243
  return typeof hooks === "function" ? hooks : null;
1152
1244
  }
1245
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1246
+ const declared = [];
1247
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1248
+ const key = { [softDeleteConfig.field]: 1 };
1249
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1250
+ declared.push({
1251
+ source: "softDelete",
1252
+ key,
1253
+ options,
1254
+ name: getIndexOptionName(options),
1255
+ fingerprint: stableIndexStringify({ key, options })
1256
+ });
1257
+ }
1258
+ const indexes = toCompatDefinition(definition).indexes;
1259
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1260
+ return declared;
1261
+ }
1262
+ for (const indexSpec of indexes) {
1263
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1264
+ continue;
1265
+ }
1266
+ const { key, ...indexOptions } = indexSpec;
1267
+ declared.push({
1268
+ source: "definition",
1269
+ key,
1270
+ options: indexOptions,
1271
+ name: getIndexOptionName(indexOptions),
1272
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1273
+ });
1274
+ }
1275
+ return declared;
1276
+ }
1277
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1278
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1279
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1280
+ const existingIndexes = await collection.listIndexes();
1281
+ const existing = [];
1282
+ const missing = [];
1283
+ const conflicts = [];
1284
+ for (const declaredIndex of declared) {
1285
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1286
+ if (existingByName) {
1287
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1288
+ existing.push({ declared: declaredIndex, existing: existingByName });
1289
+ } else {
1290
+ conflicts.push({
1291
+ declared: declaredIndex,
1292
+ existing: existingByName,
1293
+ reason: "name-conflict"
1294
+ });
1295
+ }
1296
+ continue;
1297
+ }
1298
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1299
+ if (existingByKey) {
1300
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1301
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1302
+ } else {
1303
+ conflicts.push({
1304
+ declared: declaredIndex,
1305
+ existing: existingByKey,
1306
+ reason: "options-conflict"
1307
+ });
1308
+ }
1309
+ continue;
1310
+ }
1311
+ missing.push(declaredIndex);
1312
+ }
1313
+ const result = {
1314
+ dryRun: options.dryRun === true,
1315
+ namespace,
1316
+ declared,
1317
+ existing,
1318
+ missing,
1319
+ created: [],
1320
+ conflicts,
1321
+ failed: [],
1322
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1323
+ };
1324
+ if (conflicts.length > 0 && options.throwOnError) {
1325
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1326
+ }
1327
+ if (options.dryRun === true) {
1328
+ return result;
1329
+ }
1330
+ for (const declaredIndex of missing) {
1331
+ try {
1332
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1333
+ result.created.push({
1334
+ declared: declaredIndex,
1335
+ name: typeof createdName === "string" ? createdName : void 0,
1336
+ result: createdName
1337
+ });
1338
+ } catch (error) {
1339
+ result.failed.push({
1340
+ declared: declaredIndex,
1341
+ error: summarizeIndexError(error)
1342
+ });
1343
+ if (options.throwOnError) {
1344
+ throw createIndexEnsureError(
1345
+ "Model index creation failed.",
1346
+ result,
1347
+ error instanceof Error ? error : void 0
1348
+ );
1349
+ }
1350
+ }
1351
+ }
1352
+ return result;
1353
+ }
1354
+ function summarizeModelIndexEnsureResults(results) {
1355
+ return results.reduce((totals, result) => ({
1356
+ declared: totals.declared + result.declared.length,
1357
+ existing: totals.existing + result.existing.length,
1358
+ missing: totals.missing + result.missing.length,
1359
+ created: totals.created + result.created.length,
1360
+ conflicts: totals.conflicts + result.conflicts.length,
1361
+ failed: totals.failed + result.failed.length,
1362
+ skipped: totals.skipped + result.skipped.length
1363
+ }), {
1364
+ declared: 0,
1365
+ existing: 0,
1366
+ missing: 0,
1367
+ created: 0,
1368
+ conflicts: 0,
1369
+ failed: 0,
1370
+ skipped: 0
1371
+ });
1372
+ }
1153
1373
  function initializeModelV1Methods(target, definition) {
1154
1374
  const methods = toCompatDefinition(definition).methods;
1155
1375
  if (typeof methods !== "function") {
@@ -1174,25 +1394,12 @@ function initializeModelV1Methods(target, definition) {
1174
1394
  }
1175
1395
  }
1176
1396
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1177
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1178
- const softDeleteIndex = softDeleteConfig;
1179
- scheduleIndexTask(
1180
- collection,
1181
- { [softDeleteIndex.field]: 1 },
1182
- { expireAfterSeconds: softDeleteIndex.ttl },
1183
- options
1184
- );
1185
- }
1186
- const indexes = toCompatDefinition(definition).indexes;
1187
- if (!Array.isArray(indexes) || indexes.length === 0) {
1397
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1398
+ if (!autoIndex.enabled) {
1188
1399
  return;
1189
1400
  }
1190
- for (const indexSpec of indexes) {
1191
- if (!indexSpec?.key) {
1192
- continue;
1193
- }
1194
- const { key, ...indexOptions } = indexSpec;
1195
- scheduleIndexTask(collection, key, indexOptions, options);
1401
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1402
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1196
1403
  }
1197
1404
  }
1198
1405
 
@@ -1760,7 +1967,8 @@ var ModelInstance = class {
1760
1967
  runtime: this.runtime,
1761
1968
  dbName: options.dbName,
1762
1969
  poolName: options.poolName,
1763
- collectionName: options.collectionName
1970
+ collectionName: options.collectionName,
1971
+ autoIndex: this.runtime.options?.autoIndex
1764
1972
  });
1765
1973
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1766
1974
  }
@@ -1964,6 +2172,15 @@ var ModelInstance = class {
1964
2172
  listIndexes() {
1965
2173
  return this.collection.listIndexes();
1966
2174
  }
2175
+ ensureIndexes(options = {}) {
2176
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2177
+ ...options,
2178
+ runtime: this.runtime,
2179
+ dbName: this.dbName,
2180
+ poolName: this.poolName,
2181
+ collectionName: this.collectionName
2182
+ });
2183
+ }
1967
2184
  dropIndex(name) {
1968
2185
  return this.collection.dropIndex(name);
1969
2186
  }
@@ -2049,18 +2266,18 @@ var ModelInstance = class {
2049
2266
  }
2050
2267
  async populateDocuments(docs, paths) {
2051
2268
  let current = docs;
2052
- for (const path2 of paths) {
2053
- current = await this.populatePath(current, path2);
2269
+ for (const path3 of paths) {
2270
+ current = await this.populatePath(current, path3);
2054
2271
  }
2055
2272
  return current;
2056
2273
  }
2057
- async populatePath(docs, path2) {
2274
+ async populatePath(docs, path3) {
2058
2275
  return populateModelPath({
2059
2276
  relations: this.relations,
2060
2277
  runtime: this.runtime,
2061
2278
  dbName: this.dbName,
2062
2279
  poolName: this.poolName
2063
- }, docs, path2);
2280
+ }, docs, path3);
2064
2281
  }
2065
2282
  hydrateDocuments(docs) {
2066
2283
  return docs.filter(Boolean).map((doc) => this.hydrateDocument(doc));
@@ -2233,9 +2450,9 @@ var Transaction = class {
2233
2450
  */
2234
2451
  async start() {
2235
2452
  if (this.state !== "pending") {
2236
- throw new Error(`Cannot start transaction in state: ${this.state}`);
2453
+ throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
2237
2454
  }
2238
- this.session.startTransaction();
2455
+ this.session.startTransaction(this.options.transactionOptions);
2239
2456
  this.state = "active";
2240
2457
  this.startedAt = Date.now();
2241
2458
  const timeout = this.options.timeout ?? 3e4;
@@ -2255,7 +2472,7 @@ var Transaction = class {
2255
2472
  */
2256
2473
  async commit() {
2257
2474
  if (this.state !== "active") {
2258
- throw new Error(`Cannot commit transaction in state: ${this.state}`);
2475
+ throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
2259
2476
  }
2260
2477
  if (typeof this.session.commitTransaction === "function") {
2261
2478
  await this.session.commitTransaction();
@@ -2353,7 +2570,9 @@ var TransactionManager = class {
2353
2570
  this.stats = {
2354
2571
  totalTransactions: 0,
2355
2572
  successfulTransactions: 0,
2356
- failedTransactions: 0
2573
+ failedTransactions: 0,
2574
+ readOnlyTransactions: 0,
2575
+ writeTransactions: 0
2357
2576
  };
2358
2577
  const options = "client" in input ? input : {
2359
2578
  client: input,
@@ -2364,6 +2583,10 @@ var TransactionManager = class {
2364
2583
  this.cache = options.cache ?? null;
2365
2584
  this.logger = options.logger ?? null;
2366
2585
  this.lockManager = options.lockManager ?? null;
2586
+ this.defaultReadConcern = options.defaultReadConcern;
2587
+ this.defaultWriteConcern = options.defaultWriteConcern;
2588
+ this.defaultReadPreference = options.defaultReadPreference;
2589
+ this.maxStatsSamples = options.maxStatsSamples ?? 1e3;
2367
2590
  this.defaultOptions = {
2368
2591
  maxDuration: options.maxDuration ?? 3e4,
2369
2592
  enableRetry: options.enableRetry ?? true,
@@ -2380,11 +2603,17 @@ var TransactionManager = class {
2380
2603
  const session = this.client.startSession({
2381
2604
  causalConsistency: options.causalConsistency !== false
2382
2605
  });
2606
+ const transactionOptions = {
2607
+ readConcern: options.readConcern ?? this.defaultReadConcern,
2608
+ writeConcern: options.writeConcern ?? this.defaultWriteConcern,
2609
+ readPreference: options.readPreference ?? this.defaultReadPreference
2610
+ };
2383
2611
  const transaction = new Transaction(session, {
2384
2612
  cache: this.cache,
2385
2613
  logger: this.logger,
2386
2614
  lockManager: options.enableCacheLock === false ? null : this.lockManager,
2387
- timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration
2615
+ timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration,
2616
+ transactionOptions: compactUndefined(transactionOptions)
2388
2617
  });
2389
2618
  const originalEnd = transaction.end.bind(transaction);
2390
2619
  transaction.end = async () => {
@@ -2411,12 +2640,12 @@ var TransactionManager = class {
2411
2640
  await transaction.start();
2412
2641
  const result = await callback(transaction);
2413
2642
  await transaction.commit();
2414
- this.recordStats(Date.now() - startedAt, true);
2643
+ this.recordStats(transaction, Date.now() - startedAt, true);
2415
2644
  return result;
2416
2645
  } catch (error) {
2417
2646
  lastError = error;
2418
2647
  await transaction.abort();
2419
- this.recordStats(Date.now() - startedAt, false);
2648
+ this.recordStats(transaction, Date.now() - startedAt, false);
2420
2649
  if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
2421
2650
  throw error;
2422
2651
  }
@@ -2452,27 +2681,54 @@ var TransactionManager = class {
2452
2681
  */
2453
2682
  getStats() {
2454
2683
  const averageDuration = this.durations.length === 0 ? 0 : this.durations.reduce((sum, item) => sum + item, 0) / this.durations.length;
2684
+ const sortedDurations = [...this.durations].sort((a, b) => a - b);
2685
+ const p95Duration = percentile(sortedDurations, 0.95);
2686
+ const p99Duration = percentile(sortedDurations, 0.99);
2687
+ const totalTransactions = this.stats.totalTransactions;
2455
2688
  return {
2456
- totalTransactions: this.stats.totalTransactions,
2689
+ totalTransactions,
2457
2690
  successfulTransactions: this.stats.successfulTransactions,
2458
2691
  failedTransactions: this.stats.failedTransactions,
2692
+ readOnlyTransactions: this.stats.readOnlyTransactions,
2693
+ writeTransactions: this.stats.writeTransactions,
2459
2694
  activeTransactions: this.activeTransactions.size,
2460
- averageDuration
2695
+ averageDuration,
2696
+ p95Duration,
2697
+ p99Duration,
2698
+ successRate: totalTransactions > 0 ? `${(this.stats.successfulTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
2699
+ readOnlyRatio: totalTransactions > 0 ? `${(this.stats.readOnlyTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
2700
+ sampleCount: this.durations.length
2461
2701
  };
2462
2702
  }
2463
- recordStats(duration, success) {
2703
+ recordStats(transaction, duration, success) {
2464
2704
  this.stats.totalTransactions += 1;
2465
2705
  if (success) {
2466
2706
  this.stats.successfulTransactions += 1;
2467
2707
  } else {
2468
2708
  this.stats.failedTransactions += 1;
2469
2709
  }
2710
+ if (transaction.pendingInvalidations.size > 0) {
2711
+ this.stats.writeTransactions += 1;
2712
+ } else {
2713
+ this.stats.readOnlyTransactions += 1;
2714
+ }
2470
2715
  this.durations.push(duration);
2471
- if (this.durations.length > 100) {
2716
+ if (this.durations.length > this.maxStatsSamples) {
2472
2717
  this.durations.shift();
2473
2718
  }
2474
2719
  }
2475
2720
  };
2721
+ function percentile(sortedValues, ratio) {
2722
+ if (sortedValues.length === 0) {
2723
+ return 0;
2724
+ }
2725
+ const index = Math.floor(sortedValues.length * ratio);
2726
+ return sortedValues[Math.min(index, sortedValues.length - 1)] ?? 0;
2727
+ }
2728
+ function compactUndefined(value) {
2729
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0);
2730
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
2731
+ }
2476
2732
  function stringifySessionId(id) {
2477
2733
  if (typeof id === "string") {
2478
2734
  return id;
@@ -2509,18 +2765,166 @@ async function sleep(ms) {
2509
2765
  }
2510
2766
 
2511
2767
  // src/adapters/mongodb/common/connect.ts
2768
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2769
+ import path from "node:path";
2512
2770
  import { MongoClient } from "mongodb";
2771
+ var DEFAULT_MEMORY_SERVER_VERSION = "7.0.14";
2772
+ var MANAGED_DB_PATH_PREFIXES = ["single-", "replset-", "examples-single-", "examples-replset-", "probe-single-", "probe-replset-"];
2513
2773
  var _memoryServerInstance = null;
2774
+ var _memoryServerCleanupOptions = { doCleanup: true, force: true };
2775
+ var _memoryServerDbPath = null;
2776
+ var _memoryServerClients = /* @__PURE__ */ new Set();
2777
+ function setDefaultEnv(name, value) {
2778
+ if (!process.env[name]) {
2779
+ process.env[name] = value;
2780
+ }
2781
+ }
2782
+ function sanitizePathSegment(input) {
2783
+ return input.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
2784
+ }
2785
+ function parseManagedPathPid(name) {
2786
+ if (!MANAGED_DB_PATH_PREFIXES.some((prefix) => name.startsWith(prefix))) {
2787
+ return null;
2788
+ }
2789
+ const match = /-(\d+)-[^-]+$/.exec(name);
2790
+ if (!match) {
2791
+ return null;
2792
+ }
2793
+ const pid = Number.parseInt(match[1], 10);
2794
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
2795
+ }
2796
+ function isProcessAlive(pid) {
2797
+ if (pid === process.pid) {
2798
+ return true;
2799
+ }
2800
+ try {
2801
+ process.kill(pid, 0);
2802
+ return true;
2803
+ } catch (error) {
2804
+ return error.code === "EPERM";
2805
+ }
2806
+ }
2807
+ function pruneManagedDbRoot(dbRoot) {
2808
+ let entries;
2809
+ try {
2810
+ entries = readdirSync(dbRoot, { withFileTypes: true });
2811
+ } catch {
2812
+ return;
2813
+ }
2814
+ for (const entry of entries) {
2815
+ if (!entry.isDirectory()) {
2816
+ continue;
2817
+ }
2818
+ const pid = parseManagedPathPid(entry.name);
2819
+ if (!pid || isProcessAlive(pid)) {
2820
+ continue;
2821
+ }
2822
+ try {
2823
+ rmSync(path.join(dbRoot, entry.name), { recursive: true, force: true });
2824
+ } catch {
2825
+ }
2826
+ }
2827
+ }
2828
+ function resolveMemoryServerBinaryVersion(memoryServerOptions = {}) {
2829
+ 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;
2830
+ }
2831
+ function resolveMemoryServerPolicy(binaryVersion) {
2832
+ const cacheRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_CACHE_DIR || path.join(process.cwd(), ".cache", "mongodb-memory-server"));
2833
+ const downloadDir = path.resolve(process.env.MONGOMS_DOWNLOAD_DIR || path.join(cacheRoot, "binaries"));
2834
+ const dbRoot = path.resolve(process.env.MONSQLIZE_MEMORY_SERVER_DB_DIR || path.join(cacheRoot, "db"));
2835
+ mkdirSync(downloadDir, { recursive: true });
2836
+ mkdirSync(dbRoot, { recursive: true });
2837
+ pruneManagedDbRoot(dbRoot);
2838
+ setDefaultEnv("MONGOMS_DOWNLOAD_DIR", downloadDir);
2839
+ setDefaultEnv("MONGOMS_PREFER_GLOBAL_PATH", "false");
2840
+ setDefaultEnv("MONGOMS_RUNTIME_DOWNLOAD", "true");
2841
+ setDefaultEnv("MONGOMS_VERSION", binaryVersion);
2842
+ return { downloadDir, dbRoot };
2843
+ }
2844
+ function createManagedDbPath(dbRoot, dbName) {
2845
+ return mkdtempSync(path.join(dbRoot, `replset-${sanitizePathSegment(dbName)}-${process.pid}-`));
2846
+ }
2847
+ function isManagedCleanupError(error, dbPath) {
2848
+ if (!error || typeof error !== "object") {
2849
+ return false;
2850
+ }
2851
+ const candidate = error;
2852
+ if (!candidate.code || !["ENOTEMPTY", "EBUSY", "EPERM", "ENOENT"].includes(candidate.code)) {
2853
+ return false;
2854
+ }
2855
+ return !candidate.path || path.resolve(candidate.path).startsWith(path.resolve(dbPath));
2856
+ }
2857
+ function delay(ms) {
2858
+ return new Promise((resolve) => setTimeout(resolve, ms));
2859
+ }
2860
+ async function cleanupManagedDbPath(dbPath) {
2861
+ if (!dbPath) {
2862
+ return true;
2863
+ }
2864
+ for (const waitMs of [0, 50, 100, 200, 400, 800]) {
2865
+ if (waitMs > 0) {
2866
+ await delay(waitMs);
2867
+ }
2868
+ try {
2869
+ rmSync(dbPath, { recursive: true, force: true });
2870
+ if (!existsSync(dbPath)) {
2871
+ return true;
2872
+ }
2873
+ } catch {
2874
+ }
2875
+ }
2876
+ return !existsSync(dbPath);
2877
+ }
2878
+ function resolveLaunchTimeout() {
2879
+ const raw = process.env.MONSQLIZE_MEMORY_MONGO_LAUNCH_TIMEOUT_MS;
2880
+ if (!raw) {
2881
+ return void 0;
2882
+ }
2883
+ const value = Number.parseInt(raw, 10);
2884
+ return Number.isFinite(value) && value > 0 ? value : void 0;
2885
+ }
2886
+ async function seedMemoryServerBinaryCache(binaryVersion, downloadDir) {
2887
+ try {
2888
+ const { DryMongoBinary } = __require("mongodb-memory-server-core/lib/util/DryMongoBinary");
2889
+ const options = await DryMongoBinary.generateOptions({ version: binaryVersion, downloadDir });
2890
+ options.downloadDir = downloadDir;
2891
+ const paths = await DryMongoBinary.generatePaths(options);
2892
+ if (paths.resolveConfig && paths.homeCache && path.resolve(paths.resolveConfig) !== path.resolve(paths.homeCache) && existsSync(paths.homeCache) && !existsSync(paths.resolveConfig)) {
2893
+ mkdirSync(path.dirname(paths.resolveConfig), { recursive: true });
2894
+ copyFileSync(paths.homeCache, paths.resolveConfig);
2895
+ }
2896
+ } catch {
2897
+ }
2898
+ }
2514
2899
  async function startMemoryServer(logger, memoryServerOptions = {}) {
2515
2900
  if (_memoryServerInstance) {
2516
2901
  return _memoryServerInstance.getUri();
2517
2902
  }
2903
+ const binaryVersion = resolveMemoryServerBinaryVersion(memoryServerOptions);
2904
+ const { dbRoot, downloadDir } = resolveMemoryServerPolicy(binaryVersion);
2905
+ await seedMemoryServerBinaryCache(binaryVersion, downloadDir);
2518
2906
  const { MongoMemoryReplSet } = __require("mongodb-memory-server");
2519
- logger?.info?.("\u{1F680} Starting MongoDB Memory ReplSet (transactions supported)...");
2907
+ logger?.info?.("Starting MongoDB Memory ReplSet", { binaryVersion });
2908
+ const dbName = memoryServerOptions?.instance?.dbName || "monsqlize_memory";
2909
+ const instanceConfig = { ...memoryServerOptions?.instance ?? {} };
2910
+ const hasUserDbPath = typeof instanceConfig.dbPath === "string" && instanceConfig.dbPath.length > 0;
2911
+ if (!hasUserDbPath) {
2912
+ _memoryServerDbPath = createManagedDbPath(dbRoot, dbName);
2913
+ instanceConfig.dbPath = _memoryServerDbPath;
2914
+ } else {
2915
+ _memoryServerDbPath = null;
2916
+ }
2917
+ if (instanceConfig.launchTimeout === void 0) {
2918
+ const launchTimeout = resolveLaunchTimeout();
2919
+ if (launchTimeout) {
2920
+ instanceConfig.launchTimeout = launchTimeout;
2921
+ }
2922
+ }
2923
+ _memoryServerCleanupOptions = { doCleanup: true, force: !hasUserDbPath };
2520
2924
  const defaultConfig = {
2521
2925
  replSet: { count: 1, storageEngine: "wiredTiger" },
2522
- binary: { version: "6.0.12" },
2523
- instanceOpts: [{ ...memoryServerOptions?.instance ?? {} }]
2926
+ binary: { version: binaryVersion },
2927
+ instanceOpts: [instanceConfig]
2524
2928
  };
2525
2929
  const resolvedConfig = {
2526
2930
  ...defaultConfig,
@@ -2529,11 +2933,43 @@ async function startMemoryServer(logger, memoryServerOptions = {}) {
2529
2933
  try {
2530
2934
  _memoryServerInstance = await MongoMemoryReplSet.create(resolvedConfig);
2531
2935
  const uri = _memoryServerInstance.getUri();
2532
- logger?.info?.("\u2705 MongoDB Memory ReplSet started", { uri });
2936
+ logger?.info?.("MongoDB Memory ReplSet started", { uri });
2533
2937
  return uri;
2534
2938
  } catch (err) {
2535
- logger?.error?.("\u274C Failed to start MongoDB Memory ReplSet", err);
2536
- throw new Error(`Failed to start MongoDB Memory ReplSet: ${err.message}`);
2939
+ if (!hasUserDbPath) {
2940
+ await cleanupManagedDbPath(_memoryServerDbPath);
2941
+ _memoryServerDbPath = null;
2942
+ }
2943
+ logger?.error?.("Failed to start MongoDB Memory ReplSet", err);
2944
+ throw createConnectionError(
2945
+ `Failed to start MongoDB Memory ReplSet: ${err.message}`,
2946
+ err instanceof Error ? err : void 0
2947
+ );
2948
+ }
2949
+ }
2950
+ async function stopMemoryServer(logger) {
2951
+ if (!_memoryServerInstance) {
2952
+ return;
2953
+ }
2954
+ const instance = _memoryServerInstance;
2955
+ const dbPath = _memoryServerDbPath;
2956
+ _memoryServerInstance = null;
2957
+ _memoryServerDbPath = null;
2958
+ let stopError = null;
2959
+ try {
2960
+ await instance.stop(_memoryServerCleanupOptions);
2961
+ logger?.info?.("MongoDB Memory ReplSet stopped");
2962
+ } catch (cause) {
2963
+ stopError = cause;
2964
+ logger?.warn?.("Failed to stop MongoDB Memory ReplSet cleanly.", cause);
2965
+ } finally {
2966
+ if (_memoryServerCleanupOptions.force) {
2967
+ const cleaned = await cleanupManagedDbPath(dbPath);
2968
+ if (!cleaned && (!stopError || isManagedCleanupError(stopError, dbPath ?? ""))) {
2969
+ logger?.warn?.("Failed to remove MongoDB Memory ReplSet dbPath.", { dbPath });
2970
+ }
2971
+ }
2972
+ _memoryServerCleanupOptions = { doCleanup: true, force: true };
2537
2973
  }
2538
2974
  }
2539
2975
  async function connectMongo(params) {
@@ -2542,13 +2978,15 @@ async function connectMongo(params) {
2542
2978
  throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name must be a non-empty string.");
2543
2979
  }
2544
2980
  let effectiveUri = params.config?.uri?.trim();
2981
+ let usesManagedMemoryServer = false;
2545
2982
  if (!effectiveUri && params.config?.useMemoryServer === true) {
2546
2983
  if (process.env["MONSQLIZE_USE_SYSTEM_MONGO"] === "true") {
2547
2984
  const systemUri = process.env["MONSQLIZE_SYSTEM_MONGO_URI"] ?? "mongodb://127.0.0.1:27017";
2548
- params.logger?.info?.("\u{1F527} Using system MongoDB instead of memory server", { uri: systemUri });
2985
+ params.logger?.info?.("Using system MongoDB instead of memory server", { uri: systemUri });
2549
2986
  effectiveUri = systemUri;
2550
2987
  } else {
2551
2988
  effectiveUri = await startMemoryServer(params.logger, params.config.memoryServerOptions);
2989
+ usesManagedMemoryServer = true;
2552
2990
  }
2553
2991
  }
2554
2992
  if (!effectiveUri) {
@@ -2562,6 +3000,9 @@ async function connectMongo(params) {
2562
3000
  try {
2563
3001
  await client.connect();
2564
3002
  const db = client.db(databaseName);
3003
+ if (usesManagedMemoryServer) {
3004
+ _memoryServerClients.add(client);
3005
+ }
2565
3006
  params.logger?.info?.("MongoDB connected", { databaseName });
2566
3007
  return { client, db };
2567
3008
  } catch (cause) {
@@ -2569,6 +3010,9 @@ async function connectMongo(params) {
2569
3010
  await client.close();
2570
3011
  } catch {
2571
3012
  }
3013
+ if (usesManagedMemoryServer && _memoryServerClients.size === 0) {
3014
+ await stopMemoryServer(params.logger);
3015
+ }
2572
3016
  throw createConnectionError(
2573
3017
  `Failed to connect to MongoDB database: ${databaseName}`,
2574
3018
  cause instanceof Error ? cause : void 0
@@ -2579,17 +3023,26 @@ async function closeMongo(client, logger) {
2579
3023
  if (!client) {
2580
3024
  return;
2581
3025
  }
3026
+ const shouldReleaseMemoryServer = _memoryServerClients.delete(client);
3027
+ let closeError = null;
2582
3028
  try {
2583
3029
  await client.close();
2584
3030
  logger?.info?.("MongoDB connection closed");
2585
3031
  } catch (cause) {
2586
- const error = createError(
3032
+ closeError = createError(
2587
3033
  ErrorCodes.CONNECTION_CLOSED,
2588
3034
  "Failed to close MongoDB connection cleanly.",
2589
3035
  void 0,
2590
3036
  cause instanceof Error ? cause : void 0
2591
3037
  );
2592
- logger?.warn?.(error.message, error.cause);
3038
+ logger?.warn?.(closeError.message, closeError.cause);
3039
+ } finally {
3040
+ if (shouldReleaseMemoryServer && _memoryServerClients.size === 0) {
3041
+ await stopMemoryServer(logger);
3042
+ }
3043
+ }
3044
+ if (closeError) {
3045
+ return;
2593
3046
  }
2594
3047
  }
2595
3048
 
@@ -2601,8 +3054,9 @@ function loadSsh2Client() {
2601
3054
  try {
2602
3055
  return __require("ssh2").Client;
2603
3056
  } catch {
2604
- throw new Error(
2605
- 'ssh2 is not installed. SSH tunnel support requires the optional "ssh2" package.\nRun: npm install ssh2'
3057
+ throw createError(
3058
+ ErrorCodes.INVALID_CONFIG,
3059
+ "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
3060
  );
2607
3061
  }
2608
3062
  }
@@ -2630,10 +3084,10 @@ var SSHTunnelSSH2 = class {
2630
3084
  keepaliveInterval = 3e4
2631
3085
  } = this._sshConfig;
2632
3086
  if (!host || !username) {
2633
- throw new Error("SSH config requires: host, username");
3087
+ throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
2634
3088
  }
2635
3089
  if (!password && !privateKey && !privateKeyPath) {
2636
- throw new Error("SSH authentication required: provide password, privateKey, or privateKeyPath");
3090
+ throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
2637
3091
  }
2638
3092
  const config = { host, port, username, readyTimeout, keepaliveInterval };
2639
3093
  if (password) {
@@ -2708,7 +3162,7 @@ var SSHTunnelSSH2 = class {
2708
3162
  }
2709
3163
  getTunnelUri(_protocol, originalUri) {
2710
3164
  if (!this.isConnected || this.localPort === null) {
2711
- throw new Error(`SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
3165
+ throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
2712
3166
  }
2713
3167
  return originalUri.replace(
2714
3168
  `${this.remoteHost}:${this.remotePort}`,
@@ -2717,7 +3171,7 @@ var SSHTunnelSSH2 = class {
2717
3171
  }
2718
3172
  getLocalAddress() {
2719
3173
  if (!this.isConnected || this.localPort === null) {
2720
- throw new Error(`SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
3174
+ throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
2721
3175
  }
2722
3176
  return `localhost:${this.localPort}`;
2723
3177
  }
@@ -3166,7 +3620,7 @@ var HealthChecker = class {
3166
3620
  async _pingPool(poolName, timeout) {
3167
3621
  const stored = this._clients.get(poolName);
3168
3622
  const client = stored ?? this._poolManager?._getPool(poolName);
3169
- if (!client) throw new Error(`No client for pool: ${poolName}`);
3623
+ if (!client) throw createError(ErrorCodes.POOL_NOT_FOUND, `No client for pool: ${poolName}`);
3170
3624
  const db = client.db("admin");
3171
3625
  const pingFn = db.command ? () => db.command({ ping: 1 }) : () => db.admin().ping();
3172
3626
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Ping timeout")), timeout));
@@ -3198,7 +3652,7 @@ var PoolSelector = class {
3198
3652
  }
3199
3653
  select(pools, context) {
3200
3654
  if (!pools || pools.length === 0) {
3201
- throw new Error("No available pools");
3655
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available pools");
3202
3656
  }
3203
3657
  switch (this._strategy) {
3204
3658
  case "auto":
@@ -3219,7 +3673,10 @@ var PoolSelector = class {
3219
3673
  selectByAuto(pools, context) {
3220
3674
  const { operation, poolPreference } = context;
3221
3675
  let candidates = pools;
3222
- if (operation === "read") {
3676
+ const preferred = this.filterByPreference(pools, poolPreference);
3677
+ if (preferred.length > 0) {
3678
+ candidates = preferred;
3679
+ } else if (operation === "read") {
3223
3680
  const secondaries = pools.filter((pool) => pool.role === "secondary");
3224
3681
  if (secondaries.length > 0) {
3225
3682
  candidates = secondaries;
@@ -3230,10 +3687,19 @@ var PoolSelector = class {
3230
3687
  candidates = primaries;
3231
3688
  }
3232
3689
  }
3690
+ if (candidates.length === 1) {
3691
+ return candidates[0].name;
3692
+ }
3693
+ return this.selectByWeighted(candidates);
3694
+ }
3695
+ filterByPreference(pools, poolPreference) {
3696
+ let candidates = pools;
3697
+ let applied = false;
3233
3698
  if (poolPreference?.role) {
3234
3699
  const filteredByRole = candidates.filter((pool) => pool.role === poolPreference.role);
3235
3700
  if (filteredByRole.length > 0) {
3236
3701
  candidates = filteredByRole;
3702
+ applied = true;
3237
3703
  }
3238
3704
  }
3239
3705
  if (poolPreference?.tags?.length) {
@@ -3246,12 +3712,10 @@ var PoolSelector = class {
3246
3712
  });
3247
3713
  if (filteredByTags.length > 0) {
3248
3714
  candidates = filteredByTags;
3715
+ applied = true;
3249
3716
  }
3250
3717
  }
3251
- if (candidates.length === 1) {
3252
- return candidates[0].name;
3253
- }
3254
- return this.selectByWeighted(candidates);
3718
+ return applied ? candidates : [];
3255
3719
  }
3256
3720
  selectByRoundRobin(pools, context) {
3257
3721
  let candidates = pools;
@@ -3411,43 +3875,43 @@ var DEFAULT_POOL_CONNECT_OPTIONS = {
3411
3875
  serverSelectionTimeoutMS: 5e3
3412
3876
  };
3413
3877
  function validatePoolConfig(config) {
3414
- if (!config || typeof config !== "object") throw new Error("Pool config must be an object");
3415
- if (!config.name || typeof config.name !== "string") throw new Error("Pool config.name is required and must be a string");
3416
- if (!config.uri || typeof config.uri !== "string") throw new Error("Pool config.uri is required and must be a string");
3878
+ if (!config || typeof config !== "object") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config must be an object");
3879
+ if (!config.name || typeof config.name !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.name is required and must be a string");
3880
+ if (!config.uri || typeof config.uri !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri is required and must be a string");
3417
3881
  if (!config.uri.startsWith("mongodb://") && !config.uri.startsWith("mongodb+srv://")) {
3418
- throw new Error("Pool config.uri must start with mongodb:// or mongodb+srv://");
3882
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri must start with mongodb:// or mongodb+srv://");
3419
3883
  }
3420
3884
  if (config.role) {
3421
3885
  const validRoles = ["primary", "secondary", "analytics", "custom"];
3422
- if (!validRoles.includes(config.role)) throw new Error(`Pool config.role must be one of: ${validRoles.join(", ")}`);
3886
+ if (!validRoles.includes(config.role)) throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.role must be one of: ${validRoles.join(", ")}`);
3423
3887
  }
3424
3888
  if (config.weight !== void 0) {
3425
- if (typeof config.weight !== "number") throw new Error("Pool config.weight must be a number");
3426
- if (config.weight < 0) throw new Error("Pool config.weight must be a non-negative number");
3889
+ if (typeof config.weight !== "number") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a number");
3890
+ if (config.weight < 0) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a non-negative number");
3427
3891
  }
3428
3892
  if (config.options !== void 0) {
3429
- if (typeof config.options !== "object" || Array.isArray(config.options)) throw new Error("Pool config.options must be an object");
3893
+ if (typeof config.options !== "object" || Array.isArray(config.options)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.options must be an object");
3430
3894
  const opts = config.options;
3431
3895
  for (const key of ["maxPoolSize", "minPoolSize", "maxIdleTimeMS", "waitQueueTimeoutMS", "connectTimeoutMS", "serverSelectionTimeoutMS"]) {
3432
3896
  if (opts[key] !== void 0 && (typeof opts[key] !== "number" || opts[key] < 0)) {
3433
- throw new Error(`Pool config.options.${key} must be a non-negative number`);
3897
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.options.${key} must be a non-negative number`);
3434
3898
  }
3435
3899
  }
3436
3900
  }
3437
3901
  if (config.healthCheck !== void 0) {
3438
- if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw new Error("Pool config.healthCheck must be an object");
3902
+ if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck must be an object");
3439
3903
  const hc = config.healthCheck;
3440
- if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw new Error("Pool config.healthCheck.enabled must be a boolean");
3904
+ if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck.enabled must be a boolean");
3441
3905
  for (const key of ["interval", "timeout", "retries"]) {
3442
3906
  if (hc[key] !== void 0 && (typeof hc[key] !== "number" || hc[key] < 0)) {
3443
- throw new Error(`Pool config.healthCheck.${key} must be a non-negative number`);
3907
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.healthCheck.${key} must be a non-negative number`);
3444
3908
  }
3445
3909
  }
3446
3910
  }
3447
3911
  if (config.tags !== void 0) {
3448
- if (!Array.isArray(config.tags)) throw new Error("Pool config.tags must be an array");
3912
+ if (!Array.isArray(config.tags)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array");
3449
3913
  for (const tag of config.tags) {
3450
- if (typeof tag !== "string") throw new Error("Pool config.tags must be an array of strings");
3914
+ if (typeof tag !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array of strings");
3451
3915
  }
3452
3916
  }
3453
3917
  }
@@ -3506,10 +3970,10 @@ function validatePoolConfigSafe(config) {
3506
3970
  }
3507
3971
  function validatePoolConfigInternal(config) {
3508
3972
  if (!config.name?.trim()) {
3509
- throw new Error("Pool config requires a non-empty name");
3973
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty name");
3510
3974
  }
3511
3975
  if (!config.uri?.trim()) {
3512
- throw new Error("Pool config requires a non-empty uri");
3976
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty uri");
3513
3977
  }
3514
3978
  }
3515
3979
  function createEmptyPoolStats(name) {
@@ -3586,10 +4050,10 @@ var ConnectionPoolManager = class {
3586
4050
  async addPool(config) {
3587
4051
  validatePoolConfigInternal(config);
3588
4052
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
3589
- throw new Error(`Pool '${config.name}' already exists`);
4053
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
3590
4054
  }
3591
4055
  if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
3592
- throw new Error(`Maximum pool count (${this.maxPoolsCount}) reached`);
4056
+ throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
3593
4057
  }
3594
4058
  this._pendingAdds.add(config.name);
3595
4059
  try {
@@ -3597,7 +4061,7 @@ var ConnectionPoolManager = class {
3597
4061
  if (this.pools.has(config.name)) {
3598
4062
  await client.close().catch(() => {
3599
4063
  });
3600
- throw new Error(`Pool '${config.name}' already exists`);
4064
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
3601
4065
  }
3602
4066
  this.pools.set(config.name, {
3603
4067
  client,
@@ -3636,7 +4100,7 @@ var ConnectionPoolManager = class {
3636
4100
  async removePool(name) {
3637
4101
  const pool = this.pools.get(name);
3638
4102
  if (!pool) {
3639
- throw new Error(`Pool '${name}' not found`);
4103
+ throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${name}' not found`);
3640
4104
  }
3641
4105
  this.stopHealthCheck(name);
3642
4106
  await pool.client.close();
@@ -3660,27 +4124,28 @@ var ConnectionPoolManager = class {
3660
4124
  selectPool(operation, options = {}) {
3661
4125
  if (options.pool) {
3662
4126
  const poolData2 = this.pools.get(options.pool);
3663
- if (!poolData2) throw new Error(`Pool '${options.pool}' not found`);
4127
+ if (!poolData2) throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${options.pool}' not found`);
3664
4128
  return this._createPoolResult(options.pool, poolData2.client);
3665
4129
  }
3666
4130
  let candidates = this._getHealthyPools();
3667
4131
  if (candidates.length === 0) {
3668
4132
  if (!this.fallback.enabled) {
3669
- throw new Error("No available connection pool");
4133
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
3670
4134
  }
3671
4135
  candidates = this._handleAllPoolsDown(operation);
3672
4136
  if (candidates.length === 0) {
3673
- throw new Error("No available connection pool");
4137
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
3674
4138
  }
3675
4139
  }
4140
+ const poolPreference = options.poolPreference ?? (options.tags?.length ? { tags: options.tags } : void 0);
3676
4141
  const poolName = this._selector.select(candidates, {
3677
4142
  operation,
3678
4143
  stats: this._stats.getAllStats(),
3679
- poolPreference: options.poolPreference
4144
+ poolPreference
3680
4145
  });
3681
4146
  const poolData = this.pools.get(poolName);
3682
4147
  if (!poolData) {
3683
- throw new Error(`Selected pool '${poolName}' not available`);
4148
+ throw createError(ErrorCodes.INVALID_OPERATION, `Selected pool '${poolName}' not available`);
3684
4149
  }
3685
4150
  this._stats.recordSelection(poolName, operation);
3686
4151
  this.recordSelection(poolName, true);
@@ -3803,10 +4268,12 @@ var ConnectionPoolManager = class {
3803
4268
  _getHealthyPools() {
3804
4269
  const result = [];
3805
4270
  for (const [name, config] of this._configs.entries()) {
3806
- const status = this._healthChecker.getStatus(name);
3807
- if (!status || status.status !== "down") {
3808
- result.push(config);
4271
+ const compatStatus = this._healthChecker.getStatus(name);
4272
+ const publicStatus = this.healthStatus.get(name);
4273
+ if (compatStatus?.status === "down" || publicStatus?.status === "down") {
4274
+ continue;
3809
4275
  }
4276
+ result.push(config);
3810
4277
  }
3811
4278
  return result;
3812
4279
  }
@@ -3959,7 +4426,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
3959
4426
  logger
3960
4427
  });
3961
4428
  } catch (err) {
3962
- logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 is ioredis installed?", err);
4429
+ logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
3963
4430
  return null;
3964
4431
  }
3965
4432
  }
@@ -3967,7 +4434,7 @@ async function loadModelFiles(options, logger, opts = {}) {
3967
4434
  const modelsConfig = options.models;
3968
4435
  if (!modelsConfig) return;
3969
4436
  if (typeof modelsConfig !== "string" && typeof modelsConfig !== "object") return;
3970
- const { readdirSync } = await import("node:fs");
4437
+ const { readdirSync: readdirSync2 } = await import("node:fs");
3971
4438
  const { resolve, join, isAbsolute } = await import("node:path");
3972
4439
  const { createRequire } = await import("node:module");
3973
4440
  let targetPath;
@@ -3991,7 +4458,7 @@ async function loadModelFiles(options, logger, opts = {}) {
3991
4458
  const collectFiles = (dir) => {
3992
4459
  let entries;
3993
4460
  try {
3994
- entries = readdirSync(dir, { withFileTypes: true });
4461
+ entries = readdirSync2(dir, { withFileTypes: true });
3995
4462
  } catch {
3996
4463
  logger.warn?.(`[Models] cannot read directory: ${dir}`);
3997
4464
  return [];
@@ -4470,7 +4937,7 @@ async function indexStatsForAccessor(collectionRef) {
4470
4937
  }
4471
4938
  async function setValidatorForAccessor(collectionRef, collectionName, dbRef, validator, options = {}) {
4472
4939
  if (validator === null || typeof validator !== "object") {
4473
- throw new Error("Validator must be a non-null object");
4940
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Validator must be a non-null object");
4474
4941
  }
4475
4942
  const isEmptyValidator = Object.keys(validator).length === 0;
4476
4943
  const command = {
@@ -4491,14 +4958,14 @@ async function setValidatorForAccessor(collectionRef, collectionName, dbRef, val
4491
4958
  }
4492
4959
  async function setValidationLevelForAccessor(collectionRef, collectionName, dbRef, level) {
4493
4960
  if (typeof level !== "string" || !["off", "strict", "moderate"].includes(level)) {
4494
- throw new Error('Invalid validation level: must be "off", "strict", or "moderate"');
4961
+ throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation level: must be "off", "strict", or "moderate"');
4495
4962
  }
4496
4963
  const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationLevel: level });
4497
4964
  return { ok: result["ok"], validationLevel: level };
4498
4965
  }
4499
4966
  async function setValidationActionForAccessor(collectionRef, collectionName, dbRef, action) {
4500
4967
  if (typeof action !== "string" || !["error", "warn"].includes(action)) {
4501
- throw new Error('Invalid validation action: must be "error" or "warn"');
4968
+ throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation action: must be "error" or "warn"');
4502
4969
  }
4503
4970
  const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationAction: action });
4504
4971
  return { ok: result["ok"], validationAction: action };
@@ -4530,14 +4997,14 @@ async function statsForAccessor(collectionRef, dbName, collectionName, options =
4530
4997
  }
4531
4998
  async function renameCollectionForAccessor(collectionRef, collectionName, newName, options = {}) {
4532
4999
  if (!newName || typeof newName !== "string") {
4533
- throw new Error("New collection name is required and must be a non-empty string");
5000
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "New collection name is required and must be a non-empty string");
4534
5001
  }
4535
5002
  await collectionRef.rename(newName, { dropTarget: options.dropTarget ?? false });
4536
5003
  return { renamed: true, from: collectionName, to: newName };
4537
5004
  }
4538
5005
  async function collModForAccessor(collectionRef, collectionName, dbRef, modifications) {
4539
5006
  if (modifications === null || typeof modifications !== "object") {
4540
- throw new Error("Modifications must be a non-null object");
5007
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Modifications must be a non-null object");
4541
5008
  }
4542
5009
  return resolveDb(collectionRef, dbRef).command({
4543
5010
  collMod: collectionName,
@@ -4546,10 +5013,10 @@ async function collModForAccessor(collectionRef, collectionName, dbRef, modifica
4546
5013
  }
4547
5014
  async function convertToCappedForAccessor(collectionRef, collectionName, dbRef, size, options = {}) {
4548
5015
  if (typeof size !== "number") {
4549
- throw new Error("Size must be a number");
5016
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a number");
4550
5017
  }
4551
5018
  if (size <= 0) {
4552
- throw new Error("Size must be a positive number");
5019
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a positive number");
4553
5020
  }
4554
5021
  const command = { convertToCapped: collectionName, size };
4555
5022
  if (options.max !== void 0) {
@@ -4730,7 +5197,7 @@ function dispatchFunction(name, argsStr) {
4730
5197
  }
4731
5198
  case "REDUCE": {
4732
5199
  const lambdaMatch = /\((\w+),\s*(\w+)\)\s*=>\s*(.+)/.exec(args[2]);
4733
- if (!lambdaMatch) throw new Error("REDUCE requires a lambda: (acc, item) => expr");
5200
+ if (!lambdaMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "REDUCE requires a lambda: (acc, item) => expr");
4734
5201
  const [, accVar, itemVar, lambdaExpr] = lambdaMatch;
4735
5202
  const compiledExpr = lambdaExpr.replace(new RegExp(`\\b${accVar}\\b`, "g"), "$$value").replace(new RegExp(`\\b${itemVar}\\b`, "g"), "$$this");
4736
5203
  return { $reduce: { input: parseValue(args[0]), initialValue: parseValue(args[1]), in: compileInnerExpression(compiledExpr) } };
@@ -4875,7 +5342,7 @@ function dispatchFunction(name, argsStr) {
4875
5342
  return { $setUnion: unionArgs };
4876
5343
  }
4877
5344
  case "SWITCH": {
4878
- if (args.length < 2) throw new Error("SWITCH requires at least 2 arguments");
5345
+ if (args.length < 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "SWITCH requires at least 2 arguments");
4879
5346
  const branches = [];
4880
5347
  let defaultValue = null;
4881
5348
  for (let index = 0; index < args.length - 1; index += 2) {
@@ -4893,15 +5360,15 @@ function dispatchFunction(name, argsStr) {
4893
5360
  case "ANY_ELEMENT_TRUE":
4894
5361
  return { $anyElementTrue: [parseValue(args[0])] };
4895
5362
  case "COND": {
4896
- if (args.length !== 3) throw new Error("COND requires 3 arguments");
5363
+ if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "COND requires 3 arguments");
4897
5364
  return { $cond: { if: compileInnerExpression(args[0]), then: parseValue(args[1]), else: parseValue(args[2]) } };
4898
5365
  }
4899
5366
  case "IF_NULL": {
4900
- if (args.length !== 2) throw new Error("IF_NULL requires 2 arguments");
5367
+ if (args.length !== 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "IF_NULL requires 2 arguments");
4901
5368
  return { $ifNull: [parseValue(args[0]), parseValue(args[1])] };
4902
5369
  }
4903
5370
  case "SET_FIELD": {
4904
- if (args.length !== 3) throw new Error("SET_FIELD requires 3 arguments: (field, value, input)");
5371
+ if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "SET_FIELD requires 3 arguments: (field, value, input)");
4905
5372
  return { $setField: { field: parseValue(args[0]), input: parseValue(args[2]), value: parseValue(args[1]) } };
4906
5373
  }
4907
5374
  case "UNSET_FIELD":
@@ -4918,7 +5385,7 @@ function dispatchFunction(name, argsStr) {
4918
5385
  return { $setIsSubset: [parseValue(args[0]), parseValue(args[1])] };
4919
5386
  case "LET": {
4920
5387
  const varsMatch = /\{(.+)\}/.exec(args[0]);
4921
- if (!varsMatch) throw new Error("LET requires an object literal for variables");
5388
+ if (!varsMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "LET requires an object literal for variables");
4922
5389
  const varPairs = varsMatch[1].split(",").map((pair) => {
4923
5390
  const [key, ...rest] = pair.split(":");
4924
5391
  return [key.trim(), rest.join(":").trim()];
@@ -4936,7 +5403,7 @@ function dispatchFunction(name, argsStr) {
4936
5403
  case "SAMPLE_RATE":
4937
5404
  return { $sampleRate: parseValue(args[0]) };
4938
5405
  default:
4939
- throw new Error(`Unsupported function: ${name}`);
5406
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Unsupported function: ${name}`);
4940
5407
  }
4941
5408
  }
4942
5409
  function compileFilterCondition(condition, varName) {
@@ -5263,7 +5730,7 @@ function decodeCursor(cursor, secret) {
5263
5730
  }
5264
5731
  const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
5265
5732
  if (payload?.v !== 1 || !Array.isArray(payload.values)) {
5266
- throw new Error("Invalid cursor payload.");
5733
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Invalid cursor payload.");
5267
5734
  }
5268
5735
  return payload.values;
5269
5736
  } catch (cause) {
@@ -5362,6 +5829,7 @@ function buildEffectiveProjection(projection, sort) {
5362
5829
  }
5363
5830
 
5364
5831
  // src/adapters/mongodb/queries/find-page.ts
5832
+ import { createHash as createHash2 } from "node:crypto";
5365
5833
  function normalizePositiveInteger(value, fallback, field) {
5366
5834
  if (value === void 0 || value === null) {
5367
5835
  return fallback;
@@ -5380,54 +5848,192 @@ function mergeFilters(base, extra) {
5380
5848
  }
5381
5849
  return { $and: [base, extra] };
5382
5850
  }
5851
+ function stableStringify2(value) {
5852
+ if (value === void 0) {
5853
+ return '"__undefined__"';
5854
+ }
5855
+ if (value === null) {
5856
+ return "null";
5857
+ }
5858
+ if (Array.isArray(value)) {
5859
+ return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
5860
+ }
5861
+ if (value instanceof Date) {
5862
+ return JSON.stringify(value.toISOString());
5863
+ }
5864
+ if (typeof value === "object") {
5865
+ const customJson = value.toJSON;
5866
+ if (typeof customJson === "function" && value.constructor?.name !== "Object") {
5867
+ return stableStringify2(customJson.call(value));
5868
+ }
5869
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`);
5870
+ return `{${entries.join(",")}}`;
5871
+ }
5872
+ return JSON.stringify(value);
5873
+ }
5874
+ function hashPayload(payload) {
5875
+ return createHash2("sha256").update(stableStringify2(payload)).digest("hex");
5876
+ }
5877
+ function buildFindPageCacheKey(collection, options, normalized) {
5878
+ const payload = {
5879
+ query: normalized.query,
5880
+ sort: normalized.sort,
5881
+ limit: normalized.limit,
5882
+ page: normalized.page,
5883
+ after: options.after,
5884
+ before: options.before,
5885
+ projection: options.projection,
5886
+ pipeline: options.pipeline ?? [],
5887
+ totals: options.totals,
5888
+ jump: options.jump,
5889
+ offsetJump: options.offsetJump,
5890
+ maxTimeMS: normalized.maxTimeMS,
5891
+ hint: options.hint,
5892
+ collation: options.collation,
5893
+ batchSize: options.batchSize,
5894
+ options: options.options
5895
+ };
5896
+ const keyHash = hashPayload(payload);
5897
+ return { key: `findPage:${collection.namespace}:${keyHash}`, keyHash };
5898
+ }
5899
+ function buildTotalsCacheKey(collection, query, limit, totals) {
5900
+ const payload = {
5901
+ query,
5902
+ limit,
5903
+ mode: totals.mode ?? "sync",
5904
+ hint: totals.hint,
5905
+ collation: totals.collation,
5906
+ maxTimeMS: totals.maxTimeMS
5907
+ };
5908
+ const token = hashPayload(payload);
5909
+ return { key: `findPageTotals:${collection.namespace}:${token}`, token };
5910
+ }
5911
+ function cloneFindPageResult(result) {
5912
+ return {
5913
+ ...result,
5914
+ items: Array.isArray(result.items) ? [...result.items] : result.items,
5915
+ pageInfo: result.pageInfo && typeof result.pageInfo === "object" ? { ...result.pageInfo } : result.pageInfo,
5916
+ totals: result.totals && typeof result.totals === "object" ? { ...result.totals } : result.totals,
5917
+ meta: result.meta && typeof result.meta === "object" ? {
5918
+ ...result.meta,
5919
+ ns: { ...result.meta.ns },
5920
+ steps: result.meta.steps ? [...result.meta.steps] : void 0
5921
+ } : result.meta
5922
+ };
5923
+ }
5924
+ function getPositiveTtl(value, fallback) {
5925
+ return typeof value === "number" && value > 0 ? value : fallback;
5926
+ }
5383
5927
  var _asyncTotalsCache = new MemoryCache({
5384
5928
  maxEntries: 1e4,
5385
5929
  enableStats: false
5386
5930
  });
5387
- async function computeTotals(coll, query, limit, totals) {
5931
+ var _totalsInflight = /* @__PURE__ */ new Map();
5932
+ function runTotalsOnce(key, task) {
5933
+ if (_totalsInflight.has(key)) {
5934
+ return;
5935
+ }
5936
+ const promise = task().catch(() => {
5937
+ }).finally(() => {
5938
+ _totalsInflight.delete(key);
5939
+ });
5940
+ _totalsInflight.set(key, promise);
5941
+ }
5942
+ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCache) {
5388
5943
  const mode = totals.mode ?? "sync";
5389
- if (mode === "sync") {
5944
+ const cache = queryCache ?? _asyncTotalsCache;
5945
+ const ttlMs = getPositiveTtl(totals.ttlMs, 10 * 6e4);
5946
+ const { key: cacheKey, token } = buildTotalsCacheKey(coll, query, limit, totals);
5947
+ const buildCountOptions = (fallbackMaxTimeMS) => {
5390
5948
  const countOpts = {};
5391
- if (totals.maxTimeMS !== void 0) {
5392
- countOpts.maxTimeMS = totals.maxTimeMS;
5949
+ const maxTimeMS = totals.maxTimeMS ?? fallbackMaxTimeMS;
5950
+ if (maxTimeMS !== void 0) {
5951
+ countOpts.maxTimeMS = maxTimeMS;
5952
+ }
5953
+ if (totals.hint !== void 0) {
5954
+ countOpts.hint = totals.hint;
5955
+ }
5956
+ if (totals.collation !== void 0) {
5957
+ countOpts.collation = totals.collation;
5393
5958
  }
5394
- const total = await coll.countDocuments(
5395
- query,
5959
+ return countOpts;
5960
+ };
5961
+ const countWithOptions = async () => {
5962
+ const countOpts = buildCountOptions(2e3);
5963
+ const countQuery = query;
5964
+ const runner = () => coll.countDocuments(
5965
+ countQuery,
5396
5966
  countOpts
5397
5967
  );
5398
- const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
5399
- return { mode: "sync", total, totalPages, ts: Date.now() };
5968
+ return defaults.countQueue ? defaults.countQueue.execute(runner) : runner();
5969
+ };
5970
+ const buildPayload = (total, approx = false) => ({
5971
+ mode,
5972
+ total,
5973
+ totalPages: total > 0 ? Math.ceil(total / limit) : 0,
5974
+ ts: Date.now(),
5975
+ ...approx ? { approx: true } : {}
5976
+ });
5977
+ const buildFailurePayload = (error) => ({
5978
+ mode,
5979
+ total: null,
5980
+ totalPages: null,
5981
+ ts: Date.now(),
5982
+ error
5983
+ });
5984
+ if (mode === "sync") {
5985
+ const cached = cache.get(cacheKey);
5986
+ if (cached !== void 0) {
5987
+ return { ...cached, mode: "sync" };
5988
+ }
5989
+ try {
5990
+ const payload = buildPayload(await countWithOptions());
5991
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
5992
+ return { ...payload, mode: "sync" };
5993
+ } catch {
5994
+ const payload = buildFailurePayload("count_failed");
5995
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
5996
+ return { ...payload, mode: "sync" };
5997
+ }
5400
5998
  }
5401
5999
  if (mode === "async") {
5402
- const cacheKey = JSON.stringify({ ns: coll.namespace, q: query });
5403
- const token = Buffer.from(cacheKey).toString("base64url");
5404
- const cachedTotal = _asyncTotalsCache.get(cacheKey);
5405
- if (cachedTotal !== void 0) {
5406
- return { mode: "async", total: cachedTotal, token };
6000
+ const cached = cache.get(cacheKey);
6001
+ if (cached !== void 0) {
6002
+ return { ...cached, mode: "async", token };
5407
6003
  }
5408
- setImmediate(async () => {
5409
- try {
5410
- const n = await coll.countDocuments(
5411
- query
5412
- );
5413
- _asyncTotalsCache.set(cacheKey, n);
5414
- } catch {
5415
- }
6004
+ setImmediate(() => {
6005
+ runTotalsOnce(cacheKey, async () => {
6006
+ try {
6007
+ const payload = buildPayload(await countWithOptions());
6008
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6009
+ } catch {
6010
+ await Promise.resolve(cache.set(cacheKey, buildFailurePayload("count_failed"), ttlMs));
6011
+ }
6012
+ });
5416
6013
  });
5417
6014
  return { mode: "async", total: null, token };
5418
6015
  }
5419
6016
  if (mode === "approx") {
5420
- const countOpts = {};
5421
- if (totals.maxTimeMS !== void 0) {
5422
- countOpts.maxTimeMS = totals.maxTimeMS;
6017
+ const cached = cache.get(cacheKey);
6018
+ if (cached !== void 0) {
6019
+ return { ...cached, mode: "approx" };
6020
+ }
6021
+ try {
6022
+ const total = Object.keys(query ?? {}).length > 0 ? await countWithOptions() : await coll.estimatedDocumentCount({
6023
+ maxTimeMS: totals.maxTimeMS ?? 1e3
6024
+ });
6025
+ const payload = buildPayload(total, true);
6026
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6027
+ return { ...payload, mode: "approx" };
6028
+ } catch {
6029
+ const payload = buildFailurePayload("approx_failed");
6030
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6031
+ return { ...payload, mode: "approx" };
5423
6032
  }
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
6033
  }
5428
6034
  return { mode: mode ?? "sync" };
5429
6035
  }
5430
- async function executeFindPage(collection, options = {}, defaults = {}) {
6036
+ async function executeFindPage(collection, options = {}, defaults = {}, queryCache) {
5431
6037
  const metaEnabled = options.meta === true || typeof options.meta === "object" && options.meta !== null;
5432
6038
  const metaOptions = options.meta && typeof options.meta === "object" ? options.meta : {};
5433
6039
  const metaLevel = options.meta === true ? "op" : metaOptions.level ?? "op";
@@ -5458,6 +6064,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5458
6064
  driverOpts.projection = buildEffectiveProjection(options.projection, sort);
5459
6065
  }
5460
6066
  const jumpOpts = ext.jump;
6067
+ const cacheTTL = typeof ext.cache === "number" && ext.cache > 0 ? ext.cache : 0;
6068
+ 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;
6069
+ const shouldRefreshAsyncTotals = options.totals?.mode === "async";
6070
+ let findPageCacheHit = false;
5461
6071
  const finishResult = (result2) => {
5462
6072
  if (!metaEnabled) {
5463
6073
  return result2;
@@ -5488,6 +6098,9 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5488
6098
  endTs: metaEndTs,
5489
6099
  durationMs: metaEndTs - metaStartTs,
5490
6100
  ...typeof effectiveMaxTimeMS === "number" ? { maxTimeMS: effectiveMaxTimeMS } : {},
6101
+ cacheHit: findPageCacheHit,
6102
+ ...findPageCacheHit ? { fromCache: true } : {},
6103
+ ...pageResultCache ? { cacheTtl: cacheTTL, keyHash: pageResultCache.keyHash } : {},
5491
6104
  page,
5492
6105
  after: Boolean(options.after),
5493
6106
  before: Boolean(options.before),
@@ -5556,7 +6169,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5556
6169
  };
5557
6170
  const timedComputeTotals = async () => {
5558
6171
  const stepStartTs = Date.now();
5559
- const result2 = await computeTotals(collection, baseQuery, limit, options.totals);
6172
+ const result2 = await computeTotals(collection, baseQuery, limit, options.totals, defaults, queryCache);
5560
6173
  pushMetaStep("computeTotals", Date.now() - stepStartTs, "totals");
5561
6174
  return result2;
5562
6175
  };
@@ -5575,6 +6188,32 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5575
6188
  ...extra.currentPage !== void 0 ? { currentPage: extra.currentPage } : {}
5576
6189
  };
5577
6190
  };
6191
+ const writePageResultCache = (result2) => {
6192
+ if (!pageResultCache || !queryCache) {
6193
+ return;
6194
+ }
6195
+ const cacheValue = cloneFindPageResult(result2);
6196
+ delete cacheValue.meta;
6197
+ if (shouldRefreshAsyncTotals) {
6198
+ delete cacheValue.totals;
6199
+ }
6200
+ void queryCache.set(pageResultCache.key, cacheValue, cacheTTL);
6201
+ };
6202
+ const finishAndCache = (result2) => {
6203
+ writePageResultCache(result2);
6204
+ return finishResult(result2);
6205
+ };
6206
+ if (pageResultCache && queryCache) {
6207
+ const cached = queryCache.get(pageResultCache.key);
6208
+ if (cached !== void 0) {
6209
+ findPageCacheHit = true;
6210
+ const result2 = cloneFindPageResult(cached);
6211
+ if (options.totals && options.totals.mode !== "none" && (shouldRefreshAsyncTotals || result2.totals === void 0)) {
6212
+ result2.totals = await timedComputeTotals();
6213
+ }
6214
+ return finishResult(result2);
6215
+ }
6216
+ }
5578
6217
  if (options.stream === true) {
5579
6218
  const direction = options.before ? "before" : "after";
5580
6219
  const { queryFilter, effectiveSort } = buildPageQuery(options.after ?? options.before, direction);
@@ -5617,7 +6256,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5617
6256
  if (options.totals && options.totals.mode !== "none") {
5618
6257
  result2.totals = await timedComputeTotals();
5619
6258
  }
5620
- return finishResult(result2);
6259
+ return finishAndCache(result2);
5621
6260
  }
5622
6261
  if (options.after || options.before) {
5623
6262
  const direction = options.after ? "after" : "before";
@@ -5627,7 +6266,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5627
6266
  const first = items2[0] ?? null;
5628
6267
  const last = items2[items2.length - 1] ?? null;
5629
6268
  const enc = (item) => item ? encodeCursor(Object.keys(sort).map((f) => item[f]), cursorSecret) : null;
5630
- return finishResult({
6269
+ const result2 = {
5631
6270
  items: items2,
5632
6271
  pageInfo: {
5633
6272
  hasNext: direction === "before" ? Boolean(options.before) : hasMore2,
@@ -5635,7 +6274,11 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5635
6274
  startCursor: enc(first),
5636
6275
  endCursor: enc(last)
5637
6276
  }
5638
- });
6277
+ };
6278
+ if (options.totals && options.totals.mode !== "none") {
6279
+ result2.totals = await timedComputeTotals();
6280
+ }
6281
+ return finishAndCache(result2);
5639
6282
  }
5640
6283
  const { queryFilter: q0, effectiveSort: es0 } = buildPageQuery();
5641
6284
  let { items, hasMore } = await timedFetchItems("initialFetch", page > 1 ? "hop" : "fetch", q0, es0, {}, 1);
@@ -5647,15 +6290,19 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5647
6290
  if (options.totals && options.totals.mode !== "none") {
5648
6291
  result2.totals = await timedComputeTotals();
5649
6292
  }
5650
- return finishResult(result2);
6293
+ return finishAndCache(result2);
5651
6294
  }
5652
6295
  for (let cp = 2; cp <= page; cp++) {
5653
6296
  const lastItem = items[items.length - 1];
5654
6297
  if (!lastItem) {
5655
- return finishResult({
6298
+ const result2 = {
5656
6299
  items,
5657
6300
  pageInfo: buildPageInfo(items, false, { hasPrev: cp > 2, currentPage: cp - 1 })
5658
- });
6301
+ };
6302
+ if (options.totals && options.totals.mode !== "none") {
6303
+ result2.totals = await timedComputeTotals();
6304
+ }
6305
+ return finishAndCache(result2);
5659
6306
  }
5660
6307
  const endCursor = encodeCursor(
5661
6308
  Object.keys(sort).map((f) => lastItem[f]),
@@ -5673,10 +6320,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5673
6320
  if (options.totals && options.totals.mode !== "none") {
5674
6321
  result.totals = await timedComputeTotals();
5675
6322
  }
5676
- return finishResult(result);
6323
+ return finishAndCache(result);
5677
6324
  }
5678
- async function findPageDocuments(collection, options = {}, defaults) {
5679
- return executeFindPage(collection, options, defaults ?? {});
6325
+ async function findPageDocuments(collection, options = {}, defaults, queryCache) {
6326
+ return executeFindPage(collection, options, defaults ?? {}, queryCache);
5680
6327
  }
5681
6328
 
5682
6329
  // src/adapters/mongodb/queries/find-by-id.ts
@@ -5898,21 +6545,21 @@ var FindChain = class {
5898
6545
  }
5899
6546
  limit(value) {
5900
6547
  if (typeof value !== "number" || value < 0) {
5901
- throw new Error(`limit() requires a non-negative number, got: ${typeof value} (${value})`);
6548
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `limit() requires a non-negative number, got: ${typeof value} (${value})`);
5902
6549
  }
5903
6550
  this.options.limit = value;
5904
6551
  return this;
5905
6552
  }
5906
6553
  skip(value) {
5907
6554
  if (typeof value !== "number" || value < 0) {
5908
- throw new Error(`skip() requires a non-negative number, got: ${typeof value} (${value})`);
6555
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `skip() requires a non-negative number, got: ${typeof value} (${value})`);
5909
6556
  }
5910
6557
  this.options.skip = value;
5911
6558
  return this;
5912
6559
  }
5913
6560
  sort(value) {
5914
6561
  if (!value || typeof value !== "object") {
5915
- throw new Error(`sort() requires an object or array, got: ${typeof value}`);
6562
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `sort() requires an object or array, got: ${typeof value}`);
5916
6563
  }
5917
6564
  this.options.sort = value;
5918
6565
  return this;
@@ -5956,7 +6603,7 @@ var FindChain = class {
5956
6603
  }
5957
6604
  toArray() {
5958
6605
  if (this.executed) {
5959
- throw new Error("Query already executed.");
6606
+ throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
5960
6607
  }
5961
6608
  this.executed = true;
5962
6609
  return this.collection.find(this.normalizedQuery, buildFindDriverOptions(this.buildExecuteOptions())).toArray();
@@ -6044,7 +6691,7 @@ var AggregateChain = class {
6044
6691
  }
6045
6692
  toArray() {
6046
6693
  if (this.executed) {
6047
- throw new Error("Query already executed.");
6694
+ throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
6048
6695
  }
6049
6696
  this.executed = true;
6050
6697
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray();
@@ -6839,7 +7486,7 @@ async function insertOneForAccessor(context, doc, options) {
6839
7486
  const threshold = context.defaults?.slowQueryMs ?? 500;
6840
7487
  if (elapsed > threshold && context.logger) {
6841
7488
  try {
6842
- context.logger.warn("[insertOne] \u6162\u64CD\u4F5C\u8B66\u544A", {
7489
+ context.logger.warn("[insertOne] slow operation warning", {
6843
7490
  ns: `${context.dbName}.${context.collectionName}`,
6844
7491
  threshold,
6845
7492
  duration: elapsed,
@@ -6884,7 +7531,7 @@ async function insertManyForAccessor(context, documents, options) {
6884
7531
  const elapsed = Date.now() - startedAt;
6885
7532
  const threshold = context.defaults?.slowQueryMs ?? 500;
6886
7533
  if (elapsed >= threshold && context.logger) {
6887
- context.logger.warn("[insertMany] \u6162\u64CD\u4F5C\u8B66\u544A", {
7534
+ context.logger.warn("[insertMany] slow operation warning", {
6888
7535
  ns: `${context.dbName}.${context.collectionName}`,
6889
7536
  threshold,
6890
7537
  duration: elapsed,
@@ -7015,11 +7662,17 @@ var MongoCollectionAccessor = class {
7015
7662
  const legacyNamespacePatterns = [
7016
7663
  `${String(this.management.defaults?.namespace?.instanceId)}:mongodb:${this.dbName}:${this.collectionName}:*`
7017
7664
  ];
7018
- const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? [`bookmark:${bookmarkNamespace}:*`] : [
7665
+ const findPagePatterns = [
7666
+ `findPage:${namespace}:*`,
7667
+ `findPageTotals:${namespace}:*`,
7668
+ `bookmark:${bookmarkNamespace}:*`,
7669
+ `${bookmarkNamespace}:bm:*`
7670
+ ];
7671
+ const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? findPagePatterns : [
7019
7672
  `find:${namespace}:*`,
7020
7673
  `findOne:${namespace}:*`,
7021
7674
  `count:${namespace}:*`,
7022
- `bookmark:${bookmarkNamespace}:*`
7675
+ ...findPagePatterns
7023
7676
  ];
7024
7677
  patterns.push(...legacyNamespacePatterns);
7025
7678
  let deleted = 0;
@@ -7149,7 +7802,7 @@ var MongoCollectionAccessor = class {
7149
7802
  }
7150
7803
  async findPage(options = {}) {
7151
7804
  const resolvedOptions = options.query ? { ...options, query: this._cvFilter(options.query) } : options;
7152
- return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults);
7805
+ return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults, this.management.queryCache);
7153
7806
  }
7154
7807
  /** Opens a change stream on the collection with an optional aggregation pipeline. */
7155
7808
  watch(pipeline = [], options) {
@@ -7626,6 +8279,26 @@ function createRuntimeModelInstance(host, name, scope) {
7626
8279
  });
7627
8280
  return instance;
7628
8281
  }
8282
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8283
+ const modelNames = options.models ?? Model.list();
8284
+ const models = [];
8285
+ for (const name of modelNames) {
8286
+ const model = host.scopedModel(name, {
8287
+ database: options.database,
8288
+ pool: options.pool
8289
+ });
8290
+ const result = await model.ensureIndexes({
8291
+ dryRun: options.dryRun,
8292
+ throwOnError: options.throwOnError
8293
+ });
8294
+ models.push({ name, result });
8295
+ }
8296
+ return {
8297
+ dryRun: options.dryRun === true,
8298
+ models,
8299
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8300
+ };
8301
+ }
7629
8302
 
7630
8303
  // src/entry/runtime-core-hosts.ts
7631
8304
  function resolveAdapterCache(state) {
@@ -7841,7 +8514,7 @@ function createRuntimeAdapterBridge(host) {
7841
8514
  dropDatabase: async (name, adminOptions) => {
7842
8515
  host.ensureConnected();
7843
8516
  if (!name || typeof name !== "string") {
7844
- throw new Error("Database name is required and must be a non-empty string");
8517
+ throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name is required and must be a non-empty string");
7845
8518
  }
7846
8519
  if (!adminOptions?.confirm) {
7847
8520
  const error = new Error(
@@ -7877,7 +8550,7 @@ function createRuntimeAdapterBridge(host) {
7877
8550
  runCommand: async (command, adminOptions) => {
7878
8551
  host.ensureConnected();
7879
8552
  if (command === null || typeof command !== "object") {
7880
- throw new Error("Command must be a non-null object");
8553
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Command must be a non-null object");
7881
8554
  }
7882
8555
  return host.db().runCommand(command, adminOptions ?? {});
7883
8556
  },
@@ -8003,8 +8676,8 @@ var LockManager = class {
8003
8676
  if (attempt === retryTimes) {
8004
8677
  break;
8005
8678
  }
8006
- const delay = retryDelay * Math.pow(retryBackoff, attempt);
8007
- await sleep3(delay);
8679
+ const delay2 = retryDelay * Math.pow(retryBackoff, attempt);
8680
+ await sleep3(delay2);
8008
8681
  }
8009
8682
  this.stats.errors += 1;
8010
8683
  if (options.fallbackToNoLock) {
@@ -8128,7 +8801,7 @@ var DistributedCacheLockManager = class {
8128
8801
  errors: 0
8129
8802
  };
8130
8803
  if (!options.redis) {
8131
- throw new Error("DistributedCacheLockManager requires a Redis instance");
8804
+ throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheLockManager requires a Redis instance");
8132
8805
  }
8133
8806
  this.redis = options.redis;
8134
8807
  this.lockKeyPrefix = options.lockKeyPrefix ?? "monsqlize:cache:lock:";
@@ -8786,17 +9459,17 @@ var BatchQueue = class {
8786
9459
  import { MongoClient as MongoDriverClient2 } from "mongodb";
8787
9460
 
8788
9461
  // src/capabilities/slow-query-log/slow-query-log-records.ts
8789
- import { createHash as createHash2 } from "node:crypto";
8790
- function stableStringify2(value) {
9462
+ import { createHash as createHash3 } from "node:crypto";
9463
+ function stableStringify3(value) {
8791
9464
  if (Array.isArray(value)) {
8792
- return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
9465
+ return `[${value.map((item) => stableStringify3(item)).join(",")}]`;
8793
9466
  }
8794
9467
  if (value instanceof Date) {
8795
9468
  return JSON.stringify(value.toISOString());
8796
9469
  }
8797
9470
  if (value && typeof value === "object") {
8798
9471
  const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
8799
- return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify2(current)}`).join(",")}}`;
9472
+ return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify3(current)}`).join(",")}}`;
8800
9473
  }
8801
9474
  return JSON.stringify(value);
8802
9475
  }
@@ -8813,7 +9486,7 @@ function normalizeHashInput(input) {
8813
9486
  };
8814
9487
  }
8815
9488
  function generateQueryHash(input) {
8816
- return createHash2("sha256").update(stableStringify2(normalizeHashInput(input))).digest("hex").slice(0, 16);
9489
+ return createHash3("sha256").update(stableStringify3(normalizeHashInput(input))).digest("hex").slice(0, 16);
8817
9490
  }
8818
9491
  function handleSlowQueryLogError(logger, policy, error) {
8819
9492
  if (policy === "throw") {
@@ -9263,7 +9936,7 @@ var SlowQueryLogManager = class {
9263
9936
 
9264
9937
  // src/capabilities/sync/index.ts
9265
9938
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
9266
- import path from "node:path";
9939
+ import path2 from "node:path";
9267
9940
  import { MongoClient as MongoDriverClient3 } from "mongodb";
9268
9941
  function validateTargetConfig(target, index) {
9269
9942
  if (!target || typeof target !== "object") {
@@ -9370,7 +10043,7 @@ var ResumeTokenStore = class {
9370
10043
  await Promise.resolve(this.redis.set(this.redisKey, payload));
9371
10044
  return;
9372
10045
  }
9373
- await mkdir(path.dirname(this.path), { recursive: true });
10046
+ await mkdir(path2.dirname(this.path), { recursive: true });
9374
10047
  await writeFile(this.path, payload, "utf8");
9375
10048
  } catch (error) {
9376
10049
  this.logger?.error?.("[Sync] failed to save resume token", error);
@@ -9642,7 +10315,16 @@ function getOrCreateTransactionManager(config) {
9642
10315
  client: config.client,
9643
10316
  cache: config.cache,
9644
10317
  logger: config.logger,
9645
- lockManager: config.lockManager
10318
+ lockManager: config.lockManager,
10319
+ maxDuration: config.transaction?.maxDuration ?? config.transaction?.defaultTimeout,
10320
+ enableRetry: config.transaction?.enableRetry,
10321
+ maxRetries: config.transaction?.maxRetries,
10322
+ retryDelay: config.transaction?.retryDelay,
10323
+ retryBackoff: config.transaction?.retryBackoff,
10324
+ defaultReadConcern: config.transaction?.defaultReadConcern,
10325
+ defaultWriteConcern: config.transaction?.defaultWriteConcern,
10326
+ defaultReadPreference: config.transaction?.defaultReadPreference,
10327
+ maxStatsSamples: config.transaction?.maxStatsSamples
9646
10328
  });
9647
10329
  }
9648
10330
  function getOrCreateLockManager(current, logger) {
@@ -9932,7 +10614,7 @@ function resolveCacheSource(cacheOrDb) {
9932
10614
  return cache;
9933
10615
  }
9934
10616
  }
9935
- throw new Error("Invalid cache instance from MonSQLize");
10617
+ throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance from MonSQLize");
9936
10618
  }
9937
10619
  function toWithCacheStats(stats, totalTime = 0) {
9938
10620
  const calls = stats.hits + stats.misses;
@@ -9961,46 +10643,46 @@ function normalizeFunctionCacheStats(stats, timings, name) {
9961
10643
  }
9962
10644
  function validateFunctionCacheOptions(options) {
9963
10645
  if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
9964
- throw new Error("options must be an object");
10646
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
9965
10647
  }
9966
10648
  const opts = options ?? {};
9967
10649
  if (opts.namespace !== void 0 && typeof opts.namespace !== "string") {
9968
- throw new Error("namespace must be a string");
10650
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "namespace must be a string");
9969
10651
  }
9970
10652
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
9971
10653
  if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
9972
- throw new Error("defaultTTL must be a non-negative number");
10654
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
9973
10655
  }
9974
10656
  return opts;
9975
10657
  }
9976
10658
  function validateFunctionCachePerFnOptions(options) {
9977
10659
  if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
9978
- throw new Error("options must be an object");
10660
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
9979
10661
  }
9980
10662
  const opts = options ?? {};
9981
10663
  if (opts.keyBuilder !== void 0 && typeof opts.keyBuilder !== "function") {
9982
- throw new Error("keyBuilder must be a function");
10664
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
9983
10665
  }
9984
10666
  if (opts.condition !== void 0 && typeof opts.condition !== "function") {
9985
- throw new Error("condition must be a function");
10667
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
9986
10668
  }
9987
10669
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
9988
10670
  if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
9989
- throw new Error("defaultTTL must be a non-negative number");
10671
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
9990
10672
  }
9991
10673
  return opts;
9992
10674
  }
9993
10675
  function withCache(fn, options = {}) {
9994
- if (typeof fn !== "function") throw new Error("fn must be a function");
10676
+ if (typeof fn !== "function") throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
9995
10677
  const { ttl = 6e4, namespace, keyBuilder, condition, cache: externalCache, enableStats = true } = options;
9996
10678
  if (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)
9997
- throw new Error("ttl must be a non-negative number");
10679
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "ttl must be a non-negative number");
9998
10680
  if (keyBuilder !== void 0 && typeof keyBuilder !== "function")
9999
- throw new Error("keyBuilder must be a function");
10681
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
10000
10682
  if (condition !== void 0 && typeof condition !== "function")
10001
- throw new Error("condition must be a function");
10683
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
10002
10684
  if (externalCache !== void 0 && !isValidCache(externalCache))
10003
- throw new Error("Invalid cache instance");
10685
+ throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance");
10004
10686
  const wrapped = hubWithCache(fn, {
10005
10687
  ttl,
10006
10688
  namespace: namespace ?? "fn",
@@ -10042,9 +10724,9 @@ var FunctionCache = class {
10042
10724
  }
10043
10725
  register(name, fn, options) {
10044
10726
  if (!name || typeof name !== "string")
10045
- throw new Error("Function name must be a non-empty string");
10727
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
10046
10728
  if (typeof fn !== "function")
10047
- throw new Error("fn must be a function");
10729
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
10048
10730
  const opts = validateFunctionCachePerFnOptions(options);
10049
10731
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
10050
10732
  this._inner.register(name, fn, {
@@ -10070,13 +10752,13 @@ var FunctionCache = class {
10070
10752
  }
10071
10753
  async invalidate(name, ...args) {
10072
10754
  if (!name || typeof name !== "string") {
10073
- throw new Error("Function name must be a non-empty string");
10755
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
10074
10756
  }
10075
10757
  await this._inner.invalidate(name, ...args);
10076
10758
  }
10077
10759
  async invalidatePattern(pattern) {
10078
10760
  if (!pattern || typeof pattern !== "string")
10079
- throw new Error("Pattern must be a non-empty string");
10761
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Pattern must be a non-empty string");
10080
10762
  return this._inner.invalidatePattern(pattern);
10081
10763
  }
10082
10764
  list() {
@@ -10111,15 +10793,15 @@ function b64urlDecodeStr(s) {
10111
10793
  const b64 = pad(String(s || "")).replace(/-/g, "+").replace(/_/g, "/");
10112
10794
  return Buffer.from(b64, "base64").toString();
10113
10795
  }
10114
- function makeInvalidCursorError(cause) {
10115
- const err = new Error("Invalid cursor");
10796
+ function makeInvalidCursorError(message = "Invalid cursor", cause) {
10797
+ const err = new Error(message);
10116
10798
  err.code = "INVALID_CURSOR";
10117
10799
  if (cause !== void 0) err.cause = cause;
10118
10800
  return err;
10119
10801
  }
10120
10802
  function encodeCursor2(payload) {
10121
10803
  if (!payload.s || !payload.a) {
10122
- throw new Error("encodeCursor requires sort (s) and anchor (a)");
10804
+ throw makeInvalidCursorError("encodeCursor requires sort (s) and anchor (a)");
10123
10805
  }
10124
10806
  const json = JSON.stringify({ v: payload.v ?? 1, s: payload.s, a: payload.a, d: payload.d });
10125
10807
  return b64urlEncodeStr(json);
@@ -10128,11 +10810,11 @@ function decodeCursor2(str) {
10128
10810
  try {
10129
10811
  const obj = JSON.parse(b64urlDecodeStr(str));
10130
10812
  if (!obj || obj["v"] !== 1 || !obj["s"] || !obj["a"]) {
10131
- throw new Error("bad-structure");
10813
+ throw makeInvalidCursorError("bad-structure");
10132
10814
  }
10133
10815
  return obj;
10134
10816
  } catch (e) {
10135
- throw makeInvalidCursorError(e);
10817
+ throw makeInvalidCursorError("Invalid cursor", e);
10136
10818
  }
10137
10819
  }
10138
10820
 
@@ -10204,7 +10886,11 @@ var MonSQLizeRuntime = class {
10204
10886
  } : rawCacheInput;
10205
10887
  this._cache = normalizeRuntimeCache(cacheInput);
10206
10888
  this._logger = Logger.create(options.logger ?? null);
10207
- this._cacheLockManager = new CacheLockManager({ logger: options.logger ?? null });
10889
+ this._cacheLockManager = new CacheLockManager({
10890
+ logger: options.logger ?? null,
10891
+ maxDuration: options.transaction?.lockMaxDuration,
10892
+ cleanupInterval: options.transaction?.lockCleanupInterval
10893
+ });
10208
10894
  this._cache.setLockManager?.(this._cacheLockManager);
10209
10895
  this._runtimeDefaults = buildRuntimeDefaults(options);
10210
10896
  this._adapterCacheOverride = void 0;
@@ -10324,7 +11010,8 @@ var MonSQLizeRuntime = class {
10324
11010
  namespace: d.namespace,
10325
11011
  log: d.log,
10326
11012
  countQueue: this.options.countQueue,
10327
- models: this.options.models
11013
+ models: this.options.models,
11014
+ autoIndex: this.options.autoIndex
10328
11015
  };
10329
11016
  }
10330
11017
  async close() {
@@ -10521,6 +11208,10 @@ var MonSQLizeRuntime = class {
10521
11208
  cache.set(name, instance);
10522
11209
  return instance;
10523
11210
  }
11211
+ async ensureModelIndexes(options = {}) {
11212
+ this.ensureConnected();
11213
+ return ensureRuntimeModelIndexes(this, options);
11214
+ }
10524
11215
  // Capability delegation ----------------------------------------------------
10525
11216
  async startSession(options = {}) {
10526
11217
  this.ensureConnected();
@@ -10575,9 +11266,15 @@ var MonSQLizeRuntime = class {
10575
11266
  listSagas() {
10576
11267
  return this.initializeSagaOrchestrator().listSagas();
10577
11268
  }
11269
+ getTransactionStats() {
11270
+ return this._transactionManager?.getStats() ?? null;
11271
+ }
10578
11272
  getSagaStats() {
10579
11273
  return this.initializeSagaOrchestrator().getStats();
10580
11274
  }
11275
+ getDistributedCacheInvalidatorStats() {
11276
+ return this._distributedInvalidator?.getStats() ?? null;
11277
+ }
10581
11278
  async startSync() {
10582
11279
  this.ensureConnected();
10583
11280
  const manager = await this.initializeSyncManager();
@@ -10656,7 +11353,8 @@ var MonSQLizeRuntime = class {
10656
11353
  client: this._client,
10657
11354
  cache: this._cache,
10658
11355
  logger: this.options.logger ?? null,
10659
- lockManager: this._cacheLockManager
11356
+ lockManager: this._cacheLockManager,
11357
+ transaction: this.options.transaction
10660
11358
  });
10661
11359
  return this._transactionManager;
10662
11360
  }