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.
@@ -145,8 +145,8 @@ function createQueryTimeoutError(timeoutMs) {
145
145
  }
146
146
 
147
147
  // src/capabilities/cache/redis-cache-adapter.ts
148
- var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance \u5FC5\u987B\u662F Redis URL \u5B57\u7B26\u4E32\u6216 ioredis \u5B9E\u4F8B";
149
- 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";
148
+ var LEGACY_INVALID_REDIS_ARG_ERROR = "redisUrlOrInstance must be a Redis URL string or an ioredis instance";
149
+ var LEGACY_IOREDIS_MISSING_ERROR = "Unable to load ioredis. monsqlize installs ioredis by default; check package installation completeness or pass an existing ioredis instance";
150
150
  function isMissingIoredisError(error) {
151
151
  if (!(error instanceof Error)) {
152
152
  return false;
@@ -156,14 +156,14 @@ function isMissingIoredisError(error) {
156
156
  function createLegacyRedisError(message, code = ErrorCodes.INVALID_ARGUMENT) {
157
157
  return createError(code, message);
158
158
  }
159
- function createRedisCacheAdapter(redisUrlOrInstance) {
159
+ function createRedisCacheAdapter(redisUrlOrInstance, options) {
160
160
  if (typeof redisUrlOrInstance === "string") {
161
161
  const redisUrl = redisUrlOrInstance.trim();
162
162
  if (!redisUrl) {
163
163
  throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
164
164
  }
165
165
  try {
166
- return (0, import_redis.createRedisCacheAdapter)(redisUrl);
166
+ return (0, import_redis.createRedisCacheAdapter)(redisUrl, options);
167
167
  } catch (error) {
168
168
  if (isMissingIoredisError(error)) {
169
169
  throw createLegacyRedisError(LEGACY_IOREDIS_MISSING_ERROR, ErrorCodes.CACHE_UNAVAILABLE);
@@ -172,7 +172,7 @@ function createRedisCacheAdapter(redisUrlOrInstance) {
172
172
  }
173
173
  }
174
174
  if (redisUrlOrInstance && typeof redisUrlOrInstance === "object") {
175
- return (0, import_redis.createRedisCacheAdapter)(redisUrlOrInstance);
175
+ return (0, import_redis.createRedisCacheAdapter)(redisUrlOrInstance, options);
176
176
  }
177
177
  throw createLegacyRedisError(LEGACY_INVALID_REDIS_ARG_ERROR);
178
178
  }
@@ -182,7 +182,7 @@ var import_crypto = require("crypto");
182
182
  var DistributedCacheInvalidator = class {
183
183
  constructor(options) {
184
184
  if (!options.cache) {
185
- throw new Error("DistributedCacheInvalidator requires a cache instance");
185
+ throw createError(ErrorCodes.CACHE_UNAVAILABLE, "DistributedCacheInvalidator requires a cache instance");
186
186
  }
187
187
  this._cache = options.cache;
188
188
  this._logger = options.logger ?? null;
@@ -200,7 +200,7 @@ var DistributedCacheInvalidator = class {
200
200
  this.pub = new Redis(options.redisUrl);
201
201
  this.sub = new Redis(options.redisUrl);
202
202
  } else {
203
- throw new Error("DistributedCacheInvalidator requires either redis or redisUrl");
203
+ throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheInvalidator requires either redis or redisUrl");
204
204
  }
205
205
  this._setupSubscription();
206
206
  }
@@ -380,17 +380,17 @@ var _PopulatePromise = class _PopulatePromise {
380
380
  /**
381
381
  * Append a populate path and return a new PopulatePromise (chainable).
382
382
  */
383
- populate(path2, options) {
383
+ populate(path3, options) {
384
384
  const toConfig = (item) => {
385
385
  if (typeof item !== "string" && (typeof item !== "object" || item === null || Array.isArray(item))) {
386
386
  throw createError(ErrorCodes.INVALID_ARGUMENT, "populate param must be a string, array, or object");
387
387
  }
388
388
  return typeof item === "string" ? { path: item, ...options } : { ...item, ...options };
389
389
  };
390
- if (Array.isArray(path2)) {
391
- return new _PopulatePromise(this.executor, [...this.paths, ...path2.map(toConfig)]);
390
+ if (Array.isArray(path3)) {
391
+ return new _PopulatePromise(this.executor, [...this.paths, ...path3.map(toConfig)]);
392
392
  }
393
- const config = toConfig(path2);
393
+ const config = toConfig(path3);
394
394
  return new _PopulatePromise(this.executor, [...this.paths, config]);
395
395
  }
396
396
  /**
@@ -504,8 +504,8 @@ function validateRelationConfig(name, config) {
504
504
  throw createError(ErrorCodes.INVALID_ARGUMENT, `relations.single must be a boolean`);
505
505
  }
506
506
  }
507
- function normalizePopulateConfig(path2) {
508
- return typeof path2 === "string" ? { path: path2 } : path2;
507
+ function normalizePopulateConfig(path3) {
508
+ return typeof path3 === "string" ? { path: path3 } : path3;
509
509
  }
510
510
 
511
511
  // src/capabilities/model/model-registry.ts
@@ -656,8 +656,8 @@ function groupBy(values, keySelector) {
656
656
  }
657
657
  return map;
658
658
  }
659
- function getByPath(source, path2) {
660
- return path2.split(".").reduce((current, key) => {
659
+ function getByPath(source, path3) {
660
+ return path3.split(".").reduce((current, key) => {
661
661
  if (!current || typeof current !== "object") {
662
662
  return void 0;
663
663
  }
@@ -712,8 +712,8 @@ function resolveRegisteredCollectionName(registered, fallback) {
712
712
  const definition = registered.definition;
713
713
  return definition.collection ?? definition.name ?? registered.collectionName;
714
714
  }
715
- async function populateModelPath(context, docs, path2) {
716
- const config = normalizePopulateConfig(path2);
715
+ async function populateModelPath(context, docs, path3) {
716
+ const config = normalizePopulateConfig(path3);
717
717
  if (docs.length === 0) {
718
718
  return docs;
719
719
  }
@@ -830,8 +830,8 @@ function hydrateModelDocument(context, doc) {
830
830
  populate: {
831
831
  configurable: true,
832
832
  enumerable: false,
833
- value: (path2) => {
834
- const paths = Array.isArray(path2) ? path2 : [path2];
833
+ value: (path3) => {
834
+ const paths = Array.isArray(path3) ? path3 : [path3];
835
835
  return new PopulatePromise(
836
836
  (resolvedPaths) => context.populateDocument(hydrated, resolvedPaths),
837
837
  paths
@@ -997,6 +997,75 @@ function stableIndexStringify(value) {
997
997
  }
998
998
  return JSON.stringify(value) ?? "undefined";
999
999
  }
1000
+ function getIndexOptionName(options) {
1001
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
1002
+ }
1003
+ function summarizeIndexError(error) {
1004
+ if (error instanceof Error) {
1005
+ const record = error;
1006
+ return {
1007
+ name: error.name,
1008
+ message: error.message,
1009
+ code: record.code ?? record.codeName
1010
+ };
1011
+ }
1012
+ return {
1013
+ message: String(error)
1014
+ };
1015
+ }
1016
+ function isRecord(value) {
1017
+ return !!value && typeof value === "object" && !Array.isArray(value);
1018
+ }
1019
+ function getExistingIndexKey(index) {
1020
+ return index.key;
1021
+ }
1022
+ function declaredOptionEntries(options) {
1023
+ return Object.entries(options).filter(([name, value]) => {
1024
+ if (value === void 0) return false;
1025
+ if (name === "background") return false;
1026
+ return true;
1027
+ });
1028
+ }
1029
+ function indexOptionsMatch(existing, declared) {
1030
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
1031
+ return false;
1032
+ }
1033
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
1034
+ const existingValue = existing[name];
1035
+ if (value === false && existingValue === void 0) {
1036
+ continue;
1037
+ }
1038
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ return true;
1043
+ }
1044
+ function findExistingIndexByName(existingIndexes, name) {
1045
+ if (!name) return void 0;
1046
+ return existingIndexes.find((index) => index.name === name);
1047
+ }
1048
+ function findExistingIndexByKey(existingIndexes, key) {
1049
+ const fingerprint = stableIndexStringify(key);
1050
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
1051
+ }
1052
+ function createIndexEnsureError(message, result, cause) {
1053
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
1054
+ }
1055
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
1056
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
1057
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1058
+ if (value === false) {
1059
+ return { enabled: false, emitEvents: true };
1060
+ }
1061
+ if (value && typeof value === "object") {
1062
+ return {
1063
+ enabled: value.enabled !== false,
1064
+ emitEvents: value.emitEvents !== false
1065
+ };
1066
+ }
1067
+ return { enabled: true, emitEvents: true };
1068
+ }
1000
1069
  function getIndexTaskRegistry(runtime) {
1001
1070
  if (!runtime) {
1002
1071
  return fallbackModelIndexTasks;
@@ -1024,6 +1093,20 @@ function resolveIndexTaskScope(collection, options) {
1024
1093
  };
1025
1094
  }
1026
1095
  }
1096
+ function toIndexNamespace(scope) {
1097
+ return {
1098
+ db: scope.dbName,
1099
+ collection: scope.collectionName,
1100
+ poolName: scope.poolName
1101
+ };
1102
+ }
1103
+ function emitIndexFailure(runtime, payload, emitEvents) {
1104
+ if (!emitEvents) {
1105
+ return;
1106
+ }
1107
+ const emitter = runtime;
1108
+ emitter?.emit?.("model-index-error", payload);
1109
+ }
1027
1110
  function warnIndexFailure(runtime, taskKey, error) {
1028
1111
  const logger = runtime;
1029
1112
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1031,9 +1114,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1031
1114
  error: error instanceof Error ? error.message : String(error)
1032
1115
  });
1033
1116
  }
1034
- function scheduleIndexTask(collection, key, indexOptions, options) {
1117
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1035
1118
  const scope = resolveIndexTaskScope(collection, options);
1036
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1119
+ const { key, options: indexOptions } = declaredIndex;
1120
+ const indexFingerprint = declaredIndex.fingerprint;
1037
1121
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1038
1122
  const registry = getIndexTaskRegistry(options?.runtime);
1039
1123
  const existing = registry.get(taskKey);
@@ -1051,6 +1135,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1051
1135
  task.status = "failed";
1052
1136
  task.error = error;
1053
1137
  warnIndexFailure(options?.runtime, taskKey, error);
1138
+ emitIndexFailure(options?.runtime, {
1139
+ namespace: scope,
1140
+ taskKey,
1141
+ source: declaredIndex.source,
1142
+ key,
1143
+ options: indexOptions,
1144
+ error: summarizeIndexError(error)
1145
+ }, emitEvents);
1054
1146
  resolve();
1055
1147
  });
1056
1148
  });
@@ -1167,6 +1259,134 @@ function resolveModelHooksFactory(definition) {
1167
1259
  const hooks = toCompatDefinition(definition).hooks;
1168
1260
  return typeof hooks === "function" ? hooks : null;
1169
1261
  }
1262
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1263
+ const declared = [];
1264
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1265
+ const key = { [softDeleteConfig.field]: 1 };
1266
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1267
+ declared.push({
1268
+ source: "softDelete",
1269
+ key,
1270
+ options,
1271
+ name: getIndexOptionName(options),
1272
+ fingerprint: stableIndexStringify({ key, options })
1273
+ });
1274
+ }
1275
+ const indexes = toCompatDefinition(definition).indexes;
1276
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1277
+ return declared;
1278
+ }
1279
+ for (const indexSpec of indexes) {
1280
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1281
+ continue;
1282
+ }
1283
+ const { key, ...indexOptions } = indexSpec;
1284
+ declared.push({
1285
+ source: "definition",
1286
+ key,
1287
+ options: indexOptions,
1288
+ name: getIndexOptionName(indexOptions),
1289
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1290
+ });
1291
+ }
1292
+ return declared;
1293
+ }
1294
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1295
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1296
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1297
+ const existingIndexes = await collection.listIndexes();
1298
+ const existing = [];
1299
+ const missing = [];
1300
+ const conflicts = [];
1301
+ for (const declaredIndex of declared) {
1302
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1303
+ if (existingByName) {
1304
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1305
+ existing.push({ declared: declaredIndex, existing: existingByName });
1306
+ } else {
1307
+ conflicts.push({
1308
+ declared: declaredIndex,
1309
+ existing: existingByName,
1310
+ reason: "name-conflict"
1311
+ });
1312
+ }
1313
+ continue;
1314
+ }
1315
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1316
+ if (existingByKey) {
1317
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1318
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1319
+ } else {
1320
+ conflicts.push({
1321
+ declared: declaredIndex,
1322
+ existing: existingByKey,
1323
+ reason: "options-conflict"
1324
+ });
1325
+ }
1326
+ continue;
1327
+ }
1328
+ missing.push(declaredIndex);
1329
+ }
1330
+ const result = {
1331
+ dryRun: options.dryRun === true,
1332
+ namespace,
1333
+ declared,
1334
+ existing,
1335
+ missing,
1336
+ created: [],
1337
+ conflicts,
1338
+ failed: [],
1339
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1340
+ };
1341
+ if (conflicts.length > 0 && options.throwOnError) {
1342
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1343
+ }
1344
+ if (options.dryRun === true) {
1345
+ return result;
1346
+ }
1347
+ for (const declaredIndex of missing) {
1348
+ try {
1349
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1350
+ result.created.push({
1351
+ declared: declaredIndex,
1352
+ name: typeof createdName === "string" ? createdName : void 0,
1353
+ result: createdName
1354
+ });
1355
+ } catch (error) {
1356
+ result.failed.push({
1357
+ declared: declaredIndex,
1358
+ error: summarizeIndexError(error)
1359
+ });
1360
+ if (options.throwOnError) {
1361
+ throw createIndexEnsureError(
1362
+ "Model index creation failed.",
1363
+ result,
1364
+ error instanceof Error ? error : void 0
1365
+ );
1366
+ }
1367
+ }
1368
+ }
1369
+ return result;
1370
+ }
1371
+ function summarizeModelIndexEnsureResults(results) {
1372
+ return results.reduce((totals, result) => ({
1373
+ declared: totals.declared + result.declared.length,
1374
+ existing: totals.existing + result.existing.length,
1375
+ missing: totals.missing + result.missing.length,
1376
+ created: totals.created + result.created.length,
1377
+ conflicts: totals.conflicts + result.conflicts.length,
1378
+ failed: totals.failed + result.failed.length,
1379
+ skipped: totals.skipped + result.skipped.length
1380
+ }), {
1381
+ declared: 0,
1382
+ existing: 0,
1383
+ missing: 0,
1384
+ created: 0,
1385
+ conflicts: 0,
1386
+ failed: 0,
1387
+ skipped: 0
1388
+ });
1389
+ }
1170
1390
  function initializeModelV1Methods(target, definition) {
1171
1391
  const methods = toCompatDefinition(definition).methods;
1172
1392
  if (typeof methods !== "function") {
@@ -1191,25 +1411,12 @@ function initializeModelV1Methods(target, definition) {
1191
1411
  }
1192
1412
  }
1193
1413
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1194
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1195
- const softDeleteIndex = softDeleteConfig;
1196
- scheduleIndexTask(
1197
- collection,
1198
- { [softDeleteIndex.field]: 1 },
1199
- { expireAfterSeconds: softDeleteIndex.ttl },
1200
- options
1201
- );
1202
- }
1203
- const indexes = toCompatDefinition(definition).indexes;
1204
- if (!Array.isArray(indexes) || indexes.length === 0) {
1414
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1415
+ if (!autoIndex.enabled) {
1205
1416
  return;
1206
1417
  }
1207
- for (const indexSpec of indexes) {
1208
- if (!indexSpec?.key) {
1209
- continue;
1210
- }
1211
- const { key, ...indexOptions } = indexSpec;
1212
- scheduleIndexTask(collection, key, indexOptions, options);
1418
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1419
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1213
1420
  }
1214
1421
  }
1215
1422
 
@@ -1777,7 +1984,8 @@ var ModelInstance = class {
1777
1984
  runtime: this.runtime,
1778
1985
  dbName: options.dbName,
1779
1986
  poolName: options.poolName,
1780
- collectionName: options.collectionName
1987
+ collectionName: options.collectionName,
1988
+ autoIndex: this.runtime.options?.autoIndex
1781
1989
  });
1782
1990
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1783
1991
  }
@@ -1981,6 +2189,15 @@ var ModelInstance = class {
1981
2189
  listIndexes() {
1982
2190
  return this.collection.listIndexes();
1983
2191
  }
2192
+ ensureIndexes(options = {}) {
2193
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2194
+ ...options,
2195
+ runtime: this.runtime,
2196
+ dbName: this.dbName,
2197
+ poolName: this.poolName,
2198
+ collectionName: this.collectionName
2199
+ });
2200
+ }
1984
2201
  dropIndex(name) {
1985
2202
  return this.collection.dropIndex(name);
1986
2203
  }
@@ -2066,18 +2283,18 @@ var ModelInstance = class {
2066
2283
  }
2067
2284
  async populateDocuments(docs, paths) {
2068
2285
  let current = docs;
2069
- for (const path2 of paths) {
2070
- current = await this.populatePath(current, path2);
2286
+ for (const path3 of paths) {
2287
+ current = await this.populatePath(current, path3);
2071
2288
  }
2072
2289
  return current;
2073
2290
  }
2074
- async populatePath(docs, path2) {
2291
+ async populatePath(docs, path3) {
2075
2292
  return populateModelPath({
2076
2293
  relations: this.relations,
2077
2294
  runtime: this.runtime,
2078
2295
  dbName: this.dbName,
2079
2296
  poolName: this.poolName
2080
- }, docs, path2);
2297
+ }, docs, path3);
2081
2298
  }
2082
2299
  hydrateDocuments(docs) {
2083
2300
  return docs.filter(Boolean).map((doc) => this.hydrateDocument(doc));
@@ -2250,9 +2467,9 @@ var Transaction = class {
2250
2467
  */
2251
2468
  async start() {
2252
2469
  if (this.state !== "pending") {
2253
- throw new Error(`Cannot start transaction in state: ${this.state}`);
2470
+ throw createError(ErrorCodes.INVALID_OPERATION, `Cannot start transaction in state: ${this.state}`);
2254
2471
  }
2255
- this.session.startTransaction();
2472
+ this.session.startTransaction(this.options.transactionOptions);
2256
2473
  this.state = "active";
2257
2474
  this.startedAt = Date.now();
2258
2475
  const timeout = this.options.timeout ?? 3e4;
@@ -2272,7 +2489,7 @@ var Transaction = class {
2272
2489
  */
2273
2490
  async commit() {
2274
2491
  if (this.state !== "active") {
2275
- throw new Error(`Cannot commit transaction in state: ${this.state}`);
2492
+ throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
2276
2493
  }
2277
2494
  if (typeof this.session.commitTransaction === "function") {
2278
2495
  await this.session.commitTransaction();
@@ -2370,7 +2587,9 @@ var TransactionManager = class {
2370
2587
  this.stats = {
2371
2588
  totalTransactions: 0,
2372
2589
  successfulTransactions: 0,
2373
- failedTransactions: 0
2590
+ failedTransactions: 0,
2591
+ readOnlyTransactions: 0,
2592
+ writeTransactions: 0
2374
2593
  };
2375
2594
  const options = "client" in input ? input : {
2376
2595
  client: input,
@@ -2381,6 +2600,10 @@ var TransactionManager = class {
2381
2600
  this.cache = options.cache ?? null;
2382
2601
  this.logger = options.logger ?? null;
2383
2602
  this.lockManager = options.lockManager ?? null;
2603
+ this.defaultReadConcern = options.defaultReadConcern;
2604
+ this.defaultWriteConcern = options.defaultWriteConcern;
2605
+ this.defaultReadPreference = options.defaultReadPreference;
2606
+ this.maxStatsSamples = options.maxStatsSamples ?? 1e3;
2384
2607
  this.defaultOptions = {
2385
2608
  maxDuration: options.maxDuration ?? 3e4,
2386
2609
  enableRetry: options.enableRetry ?? true,
@@ -2397,11 +2620,17 @@ var TransactionManager = class {
2397
2620
  const session = this.client.startSession({
2398
2621
  causalConsistency: options.causalConsistency !== false
2399
2622
  });
2623
+ const transactionOptions = {
2624
+ readConcern: options.readConcern ?? this.defaultReadConcern,
2625
+ writeConcern: options.writeConcern ?? this.defaultWriteConcern,
2626
+ readPreference: options.readPreference ?? this.defaultReadPreference
2627
+ };
2400
2628
  const transaction = new Transaction(session, {
2401
2629
  cache: this.cache,
2402
2630
  logger: this.logger,
2403
2631
  lockManager: options.enableCacheLock === false ? null : this.lockManager,
2404
- timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration
2632
+ timeout: options.timeout ?? options.maxDuration ?? this.defaultOptions.maxDuration,
2633
+ transactionOptions: compactUndefined(transactionOptions)
2405
2634
  });
2406
2635
  const originalEnd = transaction.end.bind(transaction);
2407
2636
  transaction.end = async () => {
@@ -2428,12 +2657,12 @@ var TransactionManager = class {
2428
2657
  await transaction.start();
2429
2658
  const result = await callback(transaction);
2430
2659
  await transaction.commit();
2431
- this.recordStats(Date.now() - startedAt, true);
2660
+ this.recordStats(transaction, Date.now() - startedAt, true);
2432
2661
  return result;
2433
2662
  } catch (error) {
2434
2663
  lastError = error;
2435
2664
  await transaction.abort();
2436
- this.recordStats(Date.now() - startedAt, false);
2665
+ this.recordStats(transaction, Date.now() - startedAt, false);
2437
2666
  if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
2438
2667
  throw error;
2439
2668
  }
@@ -2469,27 +2698,54 @@ var TransactionManager = class {
2469
2698
  */
2470
2699
  getStats() {
2471
2700
  const averageDuration = this.durations.length === 0 ? 0 : this.durations.reduce((sum, item) => sum + item, 0) / this.durations.length;
2701
+ const sortedDurations = [...this.durations].sort((a, b) => a - b);
2702
+ const p95Duration = percentile(sortedDurations, 0.95);
2703
+ const p99Duration = percentile(sortedDurations, 0.99);
2704
+ const totalTransactions = this.stats.totalTransactions;
2472
2705
  return {
2473
- totalTransactions: this.stats.totalTransactions,
2706
+ totalTransactions,
2474
2707
  successfulTransactions: this.stats.successfulTransactions,
2475
2708
  failedTransactions: this.stats.failedTransactions,
2709
+ readOnlyTransactions: this.stats.readOnlyTransactions,
2710
+ writeTransactions: this.stats.writeTransactions,
2476
2711
  activeTransactions: this.activeTransactions.size,
2477
- averageDuration
2712
+ averageDuration,
2713
+ p95Duration,
2714
+ p99Duration,
2715
+ successRate: totalTransactions > 0 ? `${(this.stats.successfulTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
2716
+ readOnlyRatio: totalTransactions > 0 ? `${(this.stats.readOnlyTransactions / totalTransactions * 100).toFixed(2)}%` : "0%",
2717
+ sampleCount: this.durations.length
2478
2718
  };
2479
2719
  }
2480
- recordStats(duration, success) {
2720
+ recordStats(transaction, duration, success) {
2481
2721
  this.stats.totalTransactions += 1;
2482
2722
  if (success) {
2483
2723
  this.stats.successfulTransactions += 1;
2484
2724
  } else {
2485
2725
  this.stats.failedTransactions += 1;
2486
2726
  }
2727
+ if (transaction.pendingInvalidations.size > 0) {
2728
+ this.stats.writeTransactions += 1;
2729
+ } else {
2730
+ this.stats.readOnlyTransactions += 1;
2731
+ }
2487
2732
  this.durations.push(duration);
2488
- if (this.durations.length > 100) {
2733
+ if (this.durations.length > this.maxStatsSamples) {
2489
2734
  this.durations.shift();
2490
2735
  }
2491
2736
  }
2492
2737
  };
2738
+ function percentile(sortedValues, ratio) {
2739
+ if (sortedValues.length === 0) {
2740
+ return 0;
2741
+ }
2742
+ const index = Math.floor(sortedValues.length * ratio);
2743
+ return sortedValues[Math.min(index, sortedValues.length - 1)] ?? 0;
2744
+ }
2745
+ function compactUndefined(value) {
2746
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0);
2747
+ return entries.length === 0 ? void 0 : Object.fromEntries(entries);
2748
+ }
2493
2749
  function stringifySessionId(id) {
2494
2750
  if (typeof id === "string") {
2495
2751
  return id;
@@ -2526,18 +2782,166 @@ async function sleep(ms) {
2526
2782
  }
2527
2783
 
2528
2784
  // src/adapters/mongodb/common/connect.ts
2785
+ var import_node_fs = require("node:fs");
2786
+ var import_node_path = __toESM(require("node:path"));
2529
2787
  var import_mongodb = require("mongodb");
2788
+ var DEFAULT_MEMORY_SERVER_VERSION = "7.0.14";
2789
+ var MANAGED_DB_PATH_PREFIXES = ["single-", "replset-", "examples-single-", "examples-replset-", "probe-single-", "probe-replset-"];
2530
2790
  var _memoryServerInstance = null;
2791
+ var _memoryServerCleanupOptions = { doCleanup: true, force: true };
2792
+ var _memoryServerDbPath = null;
2793
+ var _memoryServerClients = /* @__PURE__ */ new Set();
2794
+ function setDefaultEnv(name, value) {
2795
+ if (!process.env[name]) {
2796
+ process.env[name] = value;
2797
+ }
2798
+ }
2799
+ function sanitizePathSegment(input) {
2800
+ return input.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
2801
+ }
2802
+ function parseManagedPathPid(name) {
2803
+ if (!MANAGED_DB_PATH_PREFIXES.some((prefix) => name.startsWith(prefix))) {
2804
+ return null;
2805
+ }
2806
+ const match = /-(\d+)-[^-]+$/.exec(name);
2807
+ if (!match) {
2808
+ return null;
2809
+ }
2810
+ const pid = Number.parseInt(match[1], 10);
2811
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
2812
+ }
2813
+ function isProcessAlive(pid) {
2814
+ if (pid === process.pid) {
2815
+ return true;
2816
+ }
2817
+ try {
2818
+ process.kill(pid, 0);
2819
+ return true;
2820
+ } catch (error) {
2821
+ return error.code === "EPERM";
2822
+ }
2823
+ }
2824
+ function pruneManagedDbRoot(dbRoot) {
2825
+ let entries;
2826
+ try {
2827
+ entries = (0, import_node_fs.readdirSync)(dbRoot, { withFileTypes: true });
2828
+ } catch {
2829
+ return;
2830
+ }
2831
+ for (const entry of entries) {
2832
+ if (!entry.isDirectory()) {
2833
+ continue;
2834
+ }
2835
+ const pid = parseManagedPathPid(entry.name);
2836
+ if (!pid || isProcessAlive(pid)) {
2837
+ continue;
2838
+ }
2839
+ try {
2840
+ (0, import_node_fs.rmSync)(import_node_path.default.join(dbRoot, entry.name), { recursive: true, force: true });
2841
+ } catch {
2842
+ }
2843
+ }
2844
+ }
2845
+ function resolveMemoryServerBinaryVersion(memoryServerOptions = {}) {
2846
+ 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;
2847
+ }
2848
+ function resolveMemoryServerPolicy(binaryVersion) {
2849
+ const cacheRoot = import_node_path.default.resolve(process.env.MONSQLIZE_MEMORY_SERVER_CACHE_DIR || import_node_path.default.join(process.cwd(), ".cache", "mongodb-memory-server"));
2850
+ const downloadDir = import_node_path.default.resolve(process.env.MONGOMS_DOWNLOAD_DIR || import_node_path.default.join(cacheRoot, "binaries"));
2851
+ const dbRoot = import_node_path.default.resolve(process.env.MONSQLIZE_MEMORY_SERVER_DB_DIR || import_node_path.default.join(cacheRoot, "db"));
2852
+ (0, import_node_fs.mkdirSync)(downloadDir, { recursive: true });
2853
+ (0, import_node_fs.mkdirSync)(dbRoot, { recursive: true });
2854
+ pruneManagedDbRoot(dbRoot);
2855
+ setDefaultEnv("MONGOMS_DOWNLOAD_DIR", downloadDir);
2856
+ setDefaultEnv("MONGOMS_PREFER_GLOBAL_PATH", "false");
2857
+ setDefaultEnv("MONGOMS_RUNTIME_DOWNLOAD", "true");
2858
+ setDefaultEnv("MONGOMS_VERSION", binaryVersion);
2859
+ return { downloadDir, dbRoot };
2860
+ }
2861
+ function createManagedDbPath(dbRoot, dbName) {
2862
+ return (0, import_node_fs.mkdtempSync)(import_node_path.default.join(dbRoot, `replset-${sanitizePathSegment(dbName)}-${process.pid}-`));
2863
+ }
2864
+ function isManagedCleanupError(error, dbPath) {
2865
+ if (!error || typeof error !== "object") {
2866
+ return false;
2867
+ }
2868
+ const candidate = error;
2869
+ if (!candidate.code || !["ENOTEMPTY", "EBUSY", "EPERM", "ENOENT"].includes(candidate.code)) {
2870
+ return false;
2871
+ }
2872
+ return !candidate.path || import_node_path.default.resolve(candidate.path).startsWith(import_node_path.default.resolve(dbPath));
2873
+ }
2874
+ function delay(ms) {
2875
+ return new Promise((resolve) => setTimeout(resolve, ms));
2876
+ }
2877
+ async function cleanupManagedDbPath(dbPath) {
2878
+ if (!dbPath) {
2879
+ return true;
2880
+ }
2881
+ for (const waitMs of [0, 50, 100, 200, 400, 800]) {
2882
+ if (waitMs > 0) {
2883
+ await delay(waitMs);
2884
+ }
2885
+ try {
2886
+ (0, import_node_fs.rmSync)(dbPath, { recursive: true, force: true });
2887
+ if (!(0, import_node_fs.existsSync)(dbPath)) {
2888
+ return true;
2889
+ }
2890
+ } catch {
2891
+ }
2892
+ }
2893
+ return !(0, import_node_fs.existsSync)(dbPath);
2894
+ }
2895
+ function resolveLaunchTimeout() {
2896
+ const raw = process.env.MONSQLIZE_MEMORY_MONGO_LAUNCH_TIMEOUT_MS;
2897
+ if (!raw) {
2898
+ return void 0;
2899
+ }
2900
+ const value = Number.parseInt(raw, 10);
2901
+ return Number.isFinite(value) && value > 0 ? value : void 0;
2902
+ }
2903
+ async function seedMemoryServerBinaryCache(binaryVersion, downloadDir) {
2904
+ try {
2905
+ const { DryMongoBinary } = require("mongodb-memory-server-core/lib/util/DryMongoBinary");
2906
+ const options = await DryMongoBinary.generateOptions({ version: binaryVersion, downloadDir });
2907
+ options.downloadDir = downloadDir;
2908
+ const paths = await DryMongoBinary.generatePaths(options);
2909
+ if (paths.resolveConfig && paths.homeCache && import_node_path.default.resolve(paths.resolveConfig) !== import_node_path.default.resolve(paths.homeCache) && (0, import_node_fs.existsSync)(paths.homeCache) && !(0, import_node_fs.existsSync)(paths.resolveConfig)) {
2910
+ (0, import_node_fs.mkdirSync)(import_node_path.default.dirname(paths.resolveConfig), { recursive: true });
2911
+ (0, import_node_fs.copyFileSync)(paths.homeCache, paths.resolveConfig);
2912
+ }
2913
+ } catch {
2914
+ }
2915
+ }
2531
2916
  async function startMemoryServer(logger, memoryServerOptions = {}) {
2532
2917
  if (_memoryServerInstance) {
2533
2918
  return _memoryServerInstance.getUri();
2534
2919
  }
2920
+ const binaryVersion = resolveMemoryServerBinaryVersion(memoryServerOptions);
2921
+ const { dbRoot, downloadDir } = resolveMemoryServerPolicy(binaryVersion);
2922
+ await seedMemoryServerBinaryCache(binaryVersion, downloadDir);
2535
2923
  const { MongoMemoryReplSet } = require("mongodb-memory-server");
2536
- logger?.info?.("\u{1F680} Starting MongoDB Memory ReplSet (transactions supported)...");
2924
+ logger?.info?.("Starting MongoDB Memory ReplSet", { binaryVersion });
2925
+ const dbName = memoryServerOptions?.instance?.dbName || "monsqlize_memory";
2926
+ const instanceConfig = { ...memoryServerOptions?.instance ?? {} };
2927
+ const hasUserDbPath = typeof instanceConfig.dbPath === "string" && instanceConfig.dbPath.length > 0;
2928
+ if (!hasUserDbPath) {
2929
+ _memoryServerDbPath = createManagedDbPath(dbRoot, dbName);
2930
+ instanceConfig.dbPath = _memoryServerDbPath;
2931
+ } else {
2932
+ _memoryServerDbPath = null;
2933
+ }
2934
+ if (instanceConfig.launchTimeout === void 0) {
2935
+ const launchTimeout = resolveLaunchTimeout();
2936
+ if (launchTimeout) {
2937
+ instanceConfig.launchTimeout = launchTimeout;
2938
+ }
2939
+ }
2940
+ _memoryServerCleanupOptions = { doCleanup: true, force: !hasUserDbPath };
2537
2941
  const defaultConfig = {
2538
2942
  replSet: { count: 1, storageEngine: "wiredTiger" },
2539
- binary: { version: "6.0.12" },
2540
- instanceOpts: [{ ...memoryServerOptions?.instance ?? {} }]
2943
+ binary: { version: binaryVersion },
2944
+ instanceOpts: [instanceConfig]
2541
2945
  };
2542
2946
  const resolvedConfig = {
2543
2947
  ...defaultConfig,
@@ -2546,11 +2950,43 @@ async function startMemoryServer(logger, memoryServerOptions = {}) {
2546
2950
  try {
2547
2951
  _memoryServerInstance = await MongoMemoryReplSet.create(resolvedConfig);
2548
2952
  const uri = _memoryServerInstance.getUri();
2549
- logger?.info?.("\u2705 MongoDB Memory ReplSet started", { uri });
2953
+ logger?.info?.("MongoDB Memory ReplSet started", { uri });
2550
2954
  return uri;
2551
2955
  } catch (err) {
2552
- logger?.error?.("\u274C Failed to start MongoDB Memory ReplSet", err);
2553
- throw new Error(`Failed to start MongoDB Memory ReplSet: ${err.message}`);
2956
+ if (!hasUserDbPath) {
2957
+ await cleanupManagedDbPath(_memoryServerDbPath);
2958
+ _memoryServerDbPath = null;
2959
+ }
2960
+ logger?.error?.("Failed to start MongoDB Memory ReplSet", err);
2961
+ throw createConnectionError(
2962
+ `Failed to start MongoDB Memory ReplSet: ${err.message}`,
2963
+ err instanceof Error ? err : void 0
2964
+ );
2965
+ }
2966
+ }
2967
+ async function stopMemoryServer(logger) {
2968
+ if (!_memoryServerInstance) {
2969
+ return;
2970
+ }
2971
+ const instance = _memoryServerInstance;
2972
+ const dbPath = _memoryServerDbPath;
2973
+ _memoryServerInstance = null;
2974
+ _memoryServerDbPath = null;
2975
+ let stopError = null;
2976
+ try {
2977
+ await instance.stop(_memoryServerCleanupOptions);
2978
+ logger?.info?.("MongoDB Memory ReplSet stopped");
2979
+ } catch (cause) {
2980
+ stopError = cause;
2981
+ logger?.warn?.("Failed to stop MongoDB Memory ReplSet cleanly.", cause);
2982
+ } finally {
2983
+ if (_memoryServerCleanupOptions.force) {
2984
+ const cleaned = await cleanupManagedDbPath(dbPath);
2985
+ if (!cleaned && (!stopError || isManagedCleanupError(stopError, dbPath ?? ""))) {
2986
+ logger?.warn?.("Failed to remove MongoDB Memory ReplSet dbPath.", { dbPath });
2987
+ }
2988
+ }
2989
+ _memoryServerCleanupOptions = { doCleanup: true, force: true };
2554
2990
  }
2555
2991
  }
2556
2992
  async function connectMongo(params) {
@@ -2559,13 +2995,15 @@ async function connectMongo(params) {
2559
2995
  throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name must be a non-empty string.");
2560
2996
  }
2561
2997
  let effectiveUri = params.config?.uri?.trim();
2998
+ let usesManagedMemoryServer = false;
2562
2999
  if (!effectiveUri && params.config?.useMemoryServer === true) {
2563
3000
  if (process.env["MONSQLIZE_USE_SYSTEM_MONGO"] === "true") {
2564
3001
  const systemUri = process.env["MONSQLIZE_SYSTEM_MONGO_URI"] ?? "mongodb://127.0.0.1:27017";
2565
- params.logger?.info?.("\u{1F527} Using system MongoDB instead of memory server", { uri: systemUri });
3002
+ params.logger?.info?.("Using system MongoDB instead of memory server", { uri: systemUri });
2566
3003
  effectiveUri = systemUri;
2567
3004
  } else {
2568
3005
  effectiveUri = await startMemoryServer(params.logger, params.config.memoryServerOptions);
3006
+ usesManagedMemoryServer = true;
2569
3007
  }
2570
3008
  }
2571
3009
  if (!effectiveUri) {
@@ -2579,6 +3017,9 @@ async function connectMongo(params) {
2579
3017
  try {
2580
3018
  await client.connect();
2581
3019
  const db = client.db(databaseName);
3020
+ if (usesManagedMemoryServer) {
3021
+ _memoryServerClients.add(client);
3022
+ }
2582
3023
  params.logger?.info?.("MongoDB connected", { databaseName });
2583
3024
  return { client, db };
2584
3025
  } catch (cause) {
@@ -2586,6 +3027,9 @@ async function connectMongo(params) {
2586
3027
  await client.close();
2587
3028
  } catch {
2588
3029
  }
3030
+ if (usesManagedMemoryServer && _memoryServerClients.size === 0) {
3031
+ await stopMemoryServer(params.logger);
3032
+ }
2589
3033
  throw createConnectionError(
2590
3034
  `Failed to connect to MongoDB database: ${databaseName}`,
2591
3035
  cause instanceof Error ? cause : void 0
@@ -2596,17 +3040,26 @@ async function closeMongo(client, logger) {
2596
3040
  if (!client) {
2597
3041
  return;
2598
3042
  }
3043
+ const shouldReleaseMemoryServer = _memoryServerClients.delete(client);
3044
+ let closeError = null;
2599
3045
  try {
2600
3046
  await client.close();
2601
3047
  logger?.info?.("MongoDB connection closed");
2602
3048
  } catch (cause) {
2603
- const error = createError(
3049
+ closeError = createError(
2604
3050
  ErrorCodes.CONNECTION_CLOSED,
2605
3051
  "Failed to close MongoDB connection cleanly.",
2606
3052
  void 0,
2607
3053
  cause instanceof Error ? cause : void 0
2608
3054
  );
2609
- logger?.warn?.(error.message, error.cause);
3055
+ logger?.warn?.(closeError.message, closeError.cause);
3056
+ } finally {
3057
+ if (shouldReleaseMemoryServer && _memoryServerClients.size === 0) {
3058
+ await stopMemoryServer(logger);
3059
+ }
3060
+ }
3061
+ if (closeError) {
3062
+ return;
2610
3063
  }
2611
3064
  }
2612
3065
 
@@ -2618,8 +3071,9 @@ function loadSsh2Client() {
2618
3071
  try {
2619
3072
  return require("ssh2").Client;
2620
3073
  } catch {
2621
- throw new Error(
2622
- 'ssh2 is not installed. SSH tunnel support requires the optional "ssh2" package.\nRun: npm install ssh2'
3074
+ throw createError(
3075
+ ErrorCodes.INVALID_CONFIG,
3076
+ "Unable to load ssh2. monsqlize installs ssh2 by default; check that the package installation is complete and that your runtime can resolve bundled dependencies."
2623
3077
  );
2624
3078
  }
2625
3079
  }
@@ -2647,10 +3101,10 @@ var SSHTunnelSSH2 = class {
2647
3101
  keepaliveInterval = 3e4
2648
3102
  } = this._sshConfig;
2649
3103
  if (!host || !username) {
2650
- throw new Error("SSH config requires: host, username");
3104
+ throw createError(ErrorCodes.INVALID_CONFIG, "SSH config requires: host, username");
2651
3105
  }
2652
3106
  if (!password && !privateKey && !privateKeyPath) {
2653
- throw new Error("SSH authentication required: provide password, privateKey, or privateKeyPath");
3107
+ throw createError(ErrorCodes.INVALID_CONFIG, "SSH authentication required: provide password, privateKey, or privateKeyPath");
2654
3108
  }
2655
3109
  const config = { host, port, username, readyTimeout, keepaliveInterval };
2656
3110
  if (password) {
@@ -2725,7 +3179,7 @@ var SSHTunnelSSH2 = class {
2725
3179
  }
2726
3180
  getTunnelUri(_protocol, originalUri) {
2727
3181
  if (!this.isConnected || this.localPort === null) {
2728
- throw new Error(`SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
3182
+ throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
2729
3183
  }
2730
3184
  return originalUri.replace(
2731
3185
  `${this.remoteHost}:${this.remotePort}`,
@@ -2734,7 +3188,7 @@ var SSHTunnelSSH2 = class {
2734
3188
  }
2735
3189
  getLocalAddress() {
2736
3190
  if (!this.isConnected || this.localPort === null) {
2737
- throw new Error(`SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
3191
+ throw createError(ErrorCodes.NOT_CONNECTED, `SSH tunnel [${this.remoteHost}:${this.remotePort}] not connected`);
2738
3192
  }
2739
3193
  return `localhost:${this.localPort}`;
2740
3194
  }
@@ -3183,7 +3637,7 @@ var HealthChecker = class {
3183
3637
  async _pingPool(poolName, timeout) {
3184
3638
  const stored = this._clients.get(poolName);
3185
3639
  const client = stored ?? this._poolManager?._getPool(poolName);
3186
- if (!client) throw new Error(`No client for pool: ${poolName}`);
3640
+ if (!client) throw createError(ErrorCodes.POOL_NOT_FOUND, `No client for pool: ${poolName}`);
3187
3641
  const db = client.db("admin");
3188
3642
  const pingFn = db.command ? () => db.command({ ping: 1 }) : () => db.admin().ping();
3189
3643
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Ping timeout")), timeout));
@@ -3215,7 +3669,7 @@ var PoolSelector = class {
3215
3669
  }
3216
3670
  select(pools, context) {
3217
3671
  if (!pools || pools.length === 0) {
3218
- throw new Error("No available pools");
3672
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available pools");
3219
3673
  }
3220
3674
  switch (this._strategy) {
3221
3675
  case "auto":
@@ -3236,7 +3690,10 @@ var PoolSelector = class {
3236
3690
  selectByAuto(pools, context) {
3237
3691
  const { operation, poolPreference } = context;
3238
3692
  let candidates = pools;
3239
- if (operation === "read") {
3693
+ const preferred = this.filterByPreference(pools, poolPreference);
3694
+ if (preferred.length > 0) {
3695
+ candidates = preferred;
3696
+ } else if (operation === "read") {
3240
3697
  const secondaries = pools.filter((pool) => pool.role === "secondary");
3241
3698
  if (secondaries.length > 0) {
3242
3699
  candidates = secondaries;
@@ -3247,10 +3704,19 @@ var PoolSelector = class {
3247
3704
  candidates = primaries;
3248
3705
  }
3249
3706
  }
3707
+ if (candidates.length === 1) {
3708
+ return candidates[0].name;
3709
+ }
3710
+ return this.selectByWeighted(candidates);
3711
+ }
3712
+ filterByPreference(pools, poolPreference) {
3713
+ let candidates = pools;
3714
+ let applied = false;
3250
3715
  if (poolPreference?.role) {
3251
3716
  const filteredByRole = candidates.filter((pool) => pool.role === poolPreference.role);
3252
3717
  if (filteredByRole.length > 0) {
3253
3718
  candidates = filteredByRole;
3719
+ applied = true;
3254
3720
  }
3255
3721
  }
3256
3722
  if (poolPreference?.tags?.length) {
@@ -3263,12 +3729,10 @@ var PoolSelector = class {
3263
3729
  });
3264
3730
  if (filteredByTags.length > 0) {
3265
3731
  candidates = filteredByTags;
3732
+ applied = true;
3266
3733
  }
3267
3734
  }
3268
- if (candidates.length === 1) {
3269
- return candidates[0].name;
3270
- }
3271
- return this.selectByWeighted(candidates);
3735
+ return applied ? candidates : [];
3272
3736
  }
3273
3737
  selectByRoundRobin(pools, context) {
3274
3738
  let candidates = pools;
@@ -3428,43 +3892,43 @@ var DEFAULT_POOL_CONNECT_OPTIONS = {
3428
3892
  serverSelectionTimeoutMS: 5e3
3429
3893
  };
3430
3894
  function validatePoolConfig(config) {
3431
- if (!config || typeof config !== "object") throw new Error("Pool config must be an object");
3432
- if (!config.name || typeof config.name !== "string") throw new Error("Pool config.name is required and must be a string");
3433
- if (!config.uri || typeof config.uri !== "string") throw new Error("Pool config.uri is required and must be a string");
3895
+ if (!config || typeof config !== "object") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config must be an object");
3896
+ if (!config.name || typeof config.name !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.name is required and must be a string");
3897
+ if (!config.uri || typeof config.uri !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri is required and must be a string");
3434
3898
  if (!config.uri.startsWith("mongodb://") && !config.uri.startsWith("mongodb+srv://")) {
3435
- throw new Error("Pool config.uri must start with mongodb:// or mongodb+srv://");
3899
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.uri must start with mongodb:// or mongodb+srv://");
3436
3900
  }
3437
3901
  if (config.role) {
3438
3902
  const validRoles = ["primary", "secondary", "analytics", "custom"];
3439
- if (!validRoles.includes(config.role)) throw new Error(`Pool config.role must be one of: ${validRoles.join(", ")}`);
3903
+ if (!validRoles.includes(config.role)) throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.role must be one of: ${validRoles.join(", ")}`);
3440
3904
  }
3441
3905
  if (config.weight !== void 0) {
3442
- if (typeof config.weight !== "number") throw new Error("Pool config.weight must be a number");
3443
- if (config.weight < 0) throw new Error("Pool config.weight must be a non-negative number");
3906
+ if (typeof config.weight !== "number") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a number");
3907
+ if (config.weight < 0) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.weight must be a non-negative number");
3444
3908
  }
3445
3909
  if (config.options !== void 0) {
3446
- if (typeof config.options !== "object" || Array.isArray(config.options)) throw new Error("Pool config.options must be an object");
3910
+ if (typeof config.options !== "object" || Array.isArray(config.options)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.options must be an object");
3447
3911
  const opts = config.options;
3448
3912
  for (const key of ["maxPoolSize", "minPoolSize", "maxIdleTimeMS", "waitQueueTimeoutMS", "connectTimeoutMS", "serverSelectionTimeoutMS"]) {
3449
3913
  if (opts[key] !== void 0 && (typeof opts[key] !== "number" || opts[key] < 0)) {
3450
- throw new Error(`Pool config.options.${key} must be a non-negative number`);
3914
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.options.${key} must be a non-negative number`);
3451
3915
  }
3452
3916
  }
3453
3917
  }
3454
3918
  if (config.healthCheck !== void 0) {
3455
- if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw new Error("Pool config.healthCheck must be an object");
3919
+ if (typeof config.healthCheck !== "object" || Array.isArray(config.healthCheck)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck must be an object");
3456
3920
  const hc = config.healthCheck;
3457
- if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw new Error("Pool config.healthCheck.enabled must be a boolean");
3921
+ if (hc.enabled !== void 0 && typeof hc.enabled !== "boolean") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.healthCheck.enabled must be a boolean");
3458
3922
  for (const key of ["interval", "timeout", "retries"]) {
3459
3923
  if (hc[key] !== void 0 && (typeof hc[key] !== "number" || hc[key] < 0)) {
3460
- throw new Error(`Pool config.healthCheck.${key} must be a non-negative number`);
3924
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool config.healthCheck.${key} must be a non-negative number`);
3461
3925
  }
3462
3926
  }
3463
3927
  }
3464
3928
  if (config.tags !== void 0) {
3465
- if (!Array.isArray(config.tags)) throw new Error("Pool config.tags must be an array");
3929
+ if (!Array.isArray(config.tags)) throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array");
3466
3930
  for (const tag of config.tags) {
3467
- if (typeof tag !== "string") throw new Error("Pool config.tags must be an array of strings");
3931
+ if (typeof tag !== "string") throw createError(ErrorCodes.INVALID_CONFIG, "Pool config.tags must be an array of strings");
3468
3932
  }
3469
3933
  }
3470
3934
  }
@@ -3523,10 +3987,10 @@ function validatePoolConfigSafe(config) {
3523
3987
  }
3524
3988
  function validatePoolConfigInternal(config) {
3525
3989
  if (!config.name?.trim()) {
3526
- throw new Error("Pool config requires a non-empty name");
3990
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty name");
3527
3991
  }
3528
3992
  if (!config.uri?.trim()) {
3529
- throw new Error("Pool config requires a non-empty uri");
3993
+ throw createError(ErrorCodes.INVALID_CONFIG, "Pool config requires a non-empty uri");
3530
3994
  }
3531
3995
  }
3532
3996
  function createEmptyPoolStats(name) {
@@ -3603,10 +4067,10 @@ var ConnectionPoolManager = class {
3603
4067
  async addPool(config) {
3604
4068
  validatePoolConfigInternal(config);
3605
4069
  if (this.pools.has(config.name) || this._pendingAdds.has(config.name)) {
3606
- throw new Error(`Pool '${config.name}' already exists`);
4070
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
3607
4071
  }
3608
4072
  if (this.maxPoolsCount > 0 && this.pools.size >= this.maxPoolsCount) {
3609
- throw new Error(`Maximum pool count (${this.maxPoolsCount}) reached`);
4073
+ throw createError(ErrorCodes.INVALID_CONFIG, `Maximum pool count (${this.maxPoolsCount}) reached`);
3610
4074
  }
3611
4075
  this._pendingAdds.add(config.name);
3612
4076
  try {
@@ -3614,7 +4078,7 @@ var ConnectionPoolManager = class {
3614
4078
  if (this.pools.has(config.name)) {
3615
4079
  await client.close().catch(() => {
3616
4080
  });
3617
- throw new Error(`Pool '${config.name}' already exists`);
4081
+ throw createError(ErrorCodes.INVALID_CONFIG, `Pool '${config.name}' already exists`);
3618
4082
  }
3619
4083
  this.pools.set(config.name, {
3620
4084
  client,
@@ -3653,7 +4117,7 @@ var ConnectionPoolManager = class {
3653
4117
  async removePool(name) {
3654
4118
  const pool = this.pools.get(name);
3655
4119
  if (!pool) {
3656
- throw new Error(`Pool '${name}' not found`);
4120
+ throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${name}' not found`);
3657
4121
  }
3658
4122
  this.stopHealthCheck(name);
3659
4123
  await pool.client.close();
@@ -3677,27 +4141,28 @@ var ConnectionPoolManager = class {
3677
4141
  selectPool(operation, options = {}) {
3678
4142
  if (options.pool) {
3679
4143
  const poolData2 = this.pools.get(options.pool);
3680
- if (!poolData2) throw new Error(`Pool '${options.pool}' not found`);
4144
+ if (!poolData2) throw createError(ErrorCodes.POOL_NOT_FOUND, `Pool '${options.pool}' not found`);
3681
4145
  return this._createPoolResult(options.pool, poolData2.client);
3682
4146
  }
3683
4147
  let candidates = this._getHealthyPools();
3684
4148
  if (candidates.length === 0) {
3685
4149
  if (!this.fallback.enabled) {
3686
- throw new Error("No available connection pool");
4150
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
3687
4151
  }
3688
4152
  candidates = this._handleAllPoolsDown(operation);
3689
4153
  if (candidates.length === 0) {
3690
- throw new Error("No available connection pool");
4154
+ throw createError(ErrorCodes.INVALID_OPERATION, "No available connection pool");
3691
4155
  }
3692
4156
  }
4157
+ const poolPreference = options.poolPreference ?? (options.tags?.length ? { tags: options.tags } : void 0);
3693
4158
  const poolName = this._selector.select(candidates, {
3694
4159
  operation,
3695
4160
  stats: this._stats.getAllStats(),
3696
- poolPreference: options.poolPreference
4161
+ poolPreference
3697
4162
  });
3698
4163
  const poolData = this.pools.get(poolName);
3699
4164
  if (!poolData) {
3700
- throw new Error(`Selected pool '${poolName}' not available`);
4165
+ throw createError(ErrorCodes.INVALID_OPERATION, `Selected pool '${poolName}' not available`);
3701
4166
  }
3702
4167
  this._stats.recordSelection(poolName, operation);
3703
4168
  this.recordSelection(poolName, true);
@@ -3820,10 +4285,12 @@ var ConnectionPoolManager = class {
3820
4285
  _getHealthyPools() {
3821
4286
  const result = [];
3822
4287
  for (const [name, config] of this._configs.entries()) {
3823
- const status = this._healthChecker.getStatus(name);
3824
- if (!status || status.status !== "down") {
3825
- result.push(config);
4288
+ const compatStatus = this._healthChecker.getStatus(name);
4289
+ const publicStatus = this.healthStatus.get(name);
4290
+ if (compatStatus?.status === "down" || publicStatus?.status === "down") {
4291
+ continue;
3826
4292
  }
4293
+ result.push(config);
3827
4294
  }
3828
4295
  return result;
3829
4296
  }
@@ -3976,7 +4443,7 @@ async function initializeDistributedCacheInvalidator(options, cache, logger) {
3976
4443
  logger
3977
4444
  });
3978
4445
  } catch (err) {
3979
- logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 is ioredis installed?", err);
4446
+ logger.warn?.("[Cache] Failed to initialize distributed cache invalidator \u2014 check Redis config or package installation completeness.", err);
3980
4447
  return null;
3981
4448
  }
3982
4449
  }
@@ -3984,7 +4451,7 @@ async function loadModelFiles(options, logger, opts = {}) {
3984
4451
  const modelsConfig = options.models;
3985
4452
  if (!modelsConfig) return;
3986
4453
  if (typeof modelsConfig !== "string" && typeof modelsConfig !== "object") return;
3987
- const { readdirSync } = await import("node:fs");
4454
+ const { readdirSync: readdirSync2 } = await import("node:fs");
3988
4455
  const { resolve, join, isAbsolute } = await import("node:path");
3989
4456
  const { createRequire } = await import("node:module");
3990
4457
  let targetPath;
@@ -4008,7 +4475,7 @@ async function loadModelFiles(options, logger, opts = {}) {
4008
4475
  const collectFiles = (dir) => {
4009
4476
  let entries;
4010
4477
  try {
4011
- entries = readdirSync(dir, { withFileTypes: true });
4478
+ entries = readdirSync2(dir, { withFileTypes: true });
4012
4479
  } catch {
4013
4480
  logger.warn?.(`[Models] cannot read directory: ${dir}`);
4014
4481
  return [];
@@ -4487,7 +4954,7 @@ async function indexStatsForAccessor(collectionRef) {
4487
4954
  }
4488
4955
  async function setValidatorForAccessor(collectionRef, collectionName, dbRef, validator, options = {}) {
4489
4956
  if (validator === null || typeof validator !== "object") {
4490
- throw new Error("Validator must be a non-null object");
4957
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Validator must be a non-null object");
4491
4958
  }
4492
4959
  const isEmptyValidator = Object.keys(validator).length === 0;
4493
4960
  const command = {
@@ -4508,14 +4975,14 @@ async function setValidatorForAccessor(collectionRef, collectionName, dbRef, val
4508
4975
  }
4509
4976
  async function setValidationLevelForAccessor(collectionRef, collectionName, dbRef, level) {
4510
4977
  if (typeof level !== "string" || !["off", "strict", "moderate"].includes(level)) {
4511
- throw new Error('Invalid validation level: must be "off", "strict", or "moderate"');
4978
+ throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation level: must be "off", "strict", or "moderate"');
4512
4979
  }
4513
4980
  const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationLevel: level });
4514
4981
  return { ok: result["ok"], validationLevel: level };
4515
4982
  }
4516
4983
  async function setValidationActionForAccessor(collectionRef, collectionName, dbRef, action) {
4517
4984
  if (typeof action !== "string" || !["error", "warn"].includes(action)) {
4518
- throw new Error('Invalid validation action: must be "error" or "warn"');
4985
+ throw createError(ErrorCodes.INVALID_ARGUMENT, 'Invalid validation action: must be "error" or "warn"');
4519
4986
  }
4520
4987
  const result = await resolveDb(collectionRef, dbRef).command({ collMod: collectionName, validationAction: action });
4521
4988
  return { ok: result["ok"], validationAction: action };
@@ -4547,14 +5014,14 @@ async function statsForAccessor(collectionRef, dbName, collectionName, options =
4547
5014
  }
4548
5015
  async function renameCollectionForAccessor(collectionRef, collectionName, newName, options = {}) {
4549
5016
  if (!newName || typeof newName !== "string") {
4550
- throw new Error("New collection name is required and must be a non-empty string");
5017
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "New collection name is required and must be a non-empty string");
4551
5018
  }
4552
5019
  await collectionRef.rename(newName, { dropTarget: options.dropTarget ?? false });
4553
5020
  return { renamed: true, from: collectionName, to: newName };
4554
5021
  }
4555
5022
  async function collModForAccessor(collectionRef, collectionName, dbRef, modifications) {
4556
5023
  if (modifications === null || typeof modifications !== "object") {
4557
- throw new Error("Modifications must be a non-null object");
5024
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Modifications must be a non-null object");
4558
5025
  }
4559
5026
  return resolveDb(collectionRef, dbRef).command({
4560
5027
  collMod: collectionName,
@@ -4563,10 +5030,10 @@ async function collModForAccessor(collectionRef, collectionName, dbRef, modifica
4563
5030
  }
4564
5031
  async function convertToCappedForAccessor(collectionRef, collectionName, dbRef, size, options = {}) {
4565
5032
  if (typeof size !== "number") {
4566
- throw new Error("Size must be a number");
5033
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a number");
4567
5034
  }
4568
5035
  if (size <= 0) {
4569
- throw new Error("Size must be a positive number");
5036
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Size must be a positive number");
4570
5037
  }
4571
5038
  const command = { convertToCapped: collectionName, size };
4572
5039
  if (options.max !== void 0) {
@@ -4747,7 +5214,7 @@ function dispatchFunction(name, argsStr) {
4747
5214
  }
4748
5215
  case "REDUCE": {
4749
5216
  const lambdaMatch = /\((\w+),\s*(\w+)\)\s*=>\s*(.+)/.exec(args[2]);
4750
- if (!lambdaMatch) throw new Error("REDUCE requires a lambda: (acc, item) => expr");
5217
+ if (!lambdaMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "REDUCE requires a lambda: (acc, item) => expr");
4751
5218
  const [, accVar, itemVar, lambdaExpr] = lambdaMatch;
4752
5219
  const compiledExpr = lambdaExpr.replace(new RegExp(`\\b${accVar}\\b`, "g"), "$$value").replace(new RegExp(`\\b${itemVar}\\b`, "g"), "$$this");
4753
5220
  return { $reduce: { input: parseValue(args[0]), initialValue: parseValue(args[1]), in: compileInnerExpression(compiledExpr) } };
@@ -4892,7 +5359,7 @@ function dispatchFunction(name, argsStr) {
4892
5359
  return { $setUnion: unionArgs };
4893
5360
  }
4894
5361
  case "SWITCH": {
4895
- if (args.length < 2) throw new Error("SWITCH requires at least 2 arguments");
5362
+ if (args.length < 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "SWITCH requires at least 2 arguments");
4896
5363
  const branches = [];
4897
5364
  let defaultValue = null;
4898
5365
  for (let index = 0; index < args.length - 1; index += 2) {
@@ -4910,15 +5377,15 @@ function dispatchFunction(name, argsStr) {
4910
5377
  case "ANY_ELEMENT_TRUE":
4911
5378
  return { $anyElementTrue: [parseValue(args[0])] };
4912
5379
  case "COND": {
4913
- if (args.length !== 3) throw new Error("COND requires 3 arguments");
5380
+ if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "COND requires 3 arguments");
4914
5381
  return { $cond: { if: compileInnerExpression(args[0]), then: parseValue(args[1]), else: parseValue(args[2]) } };
4915
5382
  }
4916
5383
  case "IF_NULL": {
4917
- if (args.length !== 2) throw new Error("IF_NULL requires 2 arguments");
5384
+ if (args.length !== 2) throw createError(ErrorCodes.INVALID_EXPRESSION, "IF_NULL requires 2 arguments");
4918
5385
  return { $ifNull: [parseValue(args[0]), parseValue(args[1])] };
4919
5386
  }
4920
5387
  case "SET_FIELD": {
4921
- if (args.length !== 3) throw new Error("SET_FIELD requires 3 arguments: (field, value, input)");
5388
+ if (args.length !== 3) throw createError(ErrorCodes.INVALID_EXPRESSION, "SET_FIELD requires 3 arguments: (field, value, input)");
4922
5389
  return { $setField: { field: parseValue(args[0]), input: parseValue(args[2]), value: parseValue(args[1]) } };
4923
5390
  }
4924
5391
  case "UNSET_FIELD":
@@ -4935,7 +5402,7 @@ function dispatchFunction(name, argsStr) {
4935
5402
  return { $setIsSubset: [parseValue(args[0]), parseValue(args[1])] };
4936
5403
  case "LET": {
4937
5404
  const varsMatch = /\{(.+)\}/.exec(args[0]);
4938
- if (!varsMatch) throw new Error("LET requires an object literal for variables");
5405
+ if (!varsMatch) throw createError(ErrorCodes.INVALID_EXPRESSION, "LET requires an object literal for variables");
4939
5406
  const varPairs = varsMatch[1].split(",").map((pair) => {
4940
5407
  const [key, ...rest] = pair.split(":");
4941
5408
  return [key.trim(), rest.join(":").trim()];
@@ -4953,7 +5420,7 @@ function dispatchFunction(name, argsStr) {
4953
5420
  case "SAMPLE_RATE":
4954
5421
  return { $sampleRate: parseValue(args[0]) };
4955
5422
  default:
4956
- throw new Error(`Unsupported function: ${name}`);
5423
+ throw createError(ErrorCodes.INVALID_EXPRESSION, `Unsupported function: ${name}`);
4957
5424
  }
4958
5425
  }
4959
5426
  function compileFilterCondition(condition, varName) {
@@ -5280,7 +5747,7 @@ function decodeCursor(cursor, secret) {
5280
5747
  }
5281
5748
  const payload = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
5282
5749
  if (payload?.v !== 1 || !Array.isArray(payload.values)) {
5283
- throw new Error("Invalid cursor payload.");
5750
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Invalid cursor payload.");
5284
5751
  }
5285
5752
  return payload.values;
5286
5753
  } catch (cause) {
@@ -5379,6 +5846,7 @@ function buildEffectiveProjection(projection, sort) {
5379
5846
  }
5380
5847
 
5381
5848
  // src/adapters/mongodb/queries/find-page.ts
5849
+ var import_node_crypto4 = require("node:crypto");
5382
5850
  function normalizePositiveInteger(value, fallback, field) {
5383
5851
  if (value === void 0 || value === null) {
5384
5852
  return fallback;
@@ -5397,54 +5865,192 @@ function mergeFilters(base, extra) {
5397
5865
  }
5398
5866
  return { $and: [base, extra] };
5399
5867
  }
5868
+ function stableStringify2(value) {
5869
+ if (value === void 0) {
5870
+ return '"__undefined__"';
5871
+ }
5872
+ if (value === null) {
5873
+ return "null";
5874
+ }
5875
+ if (Array.isArray(value)) {
5876
+ return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
5877
+ }
5878
+ if (value instanceof Date) {
5879
+ return JSON.stringify(value.toISOString());
5880
+ }
5881
+ if (typeof value === "object") {
5882
+ const customJson = value.toJSON;
5883
+ if (typeof customJson === "function" && value.constructor?.name !== "Object") {
5884
+ return stableStringify2(customJson.call(value));
5885
+ }
5886
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify2(item)}`);
5887
+ return `{${entries.join(",")}}`;
5888
+ }
5889
+ return JSON.stringify(value);
5890
+ }
5891
+ function hashPayload(payload) {
5892
+ return (0, import_node_crypto4.createHash)("sha256").update(stableStringify2(payload)).digest("hex");
5893
+ }
5894
+ function buildFindPageCacheKey(collection, options, normalized) {
5895
+ const payload = {
5896
+ query: normalized.query,
5897
+ sort: normalized.sort,
5898
+ limit: normalized.limit,
5899
+ page: normalized.page,
5900
+ after: options.after,
5901
+ before: options.before,
5902
+ projection: options.projection,
5903
+ pipeline: options.pipeline ?? [],
5904
+ totals: options.totals,
5905
+ jump: options.jump,
5906
+ offsetJump: options.offsetJump,
5907
+ maxTimeMS: normalized.maxTimeMS,
5908
+ hint: options.hint,
5909
+ collation: options.collation,
5910
+ batchSize: options.batchSize,
5911
+ options: options.options
5912
+ };
5913
+ const keyHash = hashPayload(payload);
5914
+ return { key: `findPage:${collection.namespace}:${keyHash}`, keyHash };
5915
+ }
5916
+ function buildTotalsCacheKey(collection, query, limit, totals) {
5917
+ const payload = {
5918
+ query,
5919
+ limit,
5920
+ mode: totals.mode ?? "sync",
5921
+ hint: totals.hint,
5922
+ collation: totals.collation,
5923
+ maxTimeMS: totals.maxTimeMS
5924
+ };
5925
+ const token = hashPayload(payload);
5926
+ return { key: `findPageTotals:${collection.namespace}:${token}`, token };
5927
+ }
5928
+ function cloneFindPageResult(result) {
5929
+ return {
5930
+ ...result,
5931
+ items: Array.isArray(result.items) ? [...result.items] : result.items,
5932
+ pageInfo: result.pageInfo && typeof result.pageInfo === "object" ? { ...result.pageInfo } : result.pageInfo,
5933
+ totals: result.totals && typeof result.totals === "object" ? { ...result.totals } : result.totals,
5934
+ meta: result.meta && typeof result.meta === "object" ? {
5935
+ ...result.meta,
5936
+ ns: { ...result.meta.ns },
5937
+ steps: result.meta.steps ? [...result.meta.steps] : void 0
5938
+ } : result.meta
5939
+ };
5940
+ }
5941
+ function getPositiveTtl(value, fallback) {
5942
+ return typeof value === "number" && value > 0 ? value : fallback;
5943
+ }
5400
5944
  var _asyncTotalsCache = new MemoryCache({
5401
5945
  maxEntries: 1e4,
5402
5946
  enableStats: false
5403
5947
  });
5404
- async function computeTotals(coll, query, limit, totals) {
5948
+ var _totalsInflight = /* @__PURE__ */ new Map();
5949
+ function runTotalsOnce(key, task) {
5950
+ if (_totalsInflight.has(key)) {
5951
+ return;
5952
+ }
5953
+ const promise = task().catch(() => {
5954
+ }).finally(() => {
5955
+ _totalsInflight.delete(key);
5956
+ });
5957
+ _totalsInflight.set(key, promise);
5958
+ }
5959
+ async function computeTotals(coll, query, limit, totals, defaults = {}, queryCache) {
5405
5960
  const mode = totals.mode ?? "sync";
5406
- if (mode === "sync") {
5961
+ const cache = queryCache ?? _asyncTotalsCache;
5962
+ const ttlMs = getPositiveTtl(totals.ttlMs, 10 * 6e4);
5963
+ const { key: cacheKey, token } = buildTotalsCacheKey(coll, query, limit, totals);
5964
+ const buildCountOptions = (fallbackMaxTimeMS) => {
5407
5965
  const countOpts = {};
5408
- if (totals.maxTimeMS !== void 0) {
5409
- countOpts.maxTimeMS = totals.maxTimeMS;
5966
+ const maxTimeMS = totals.maxTimeMS ?? fallbackMaxTimeMS;
5967
+ if (maxTimeMS !== void 0) {
5968
+ countOpts.maxTimeMS = maxTimeMS;
5969
+ }
5970
+ if (totals.hint !== void 0) {
5971
+ countOpts.hint = totals.hint;
5410
5972
  }
5411
- const total = await coll.countDocuments(
5412
- query,
5973
+ if (totals.collation !== void 0) {
5974
+ countOpts.collation = totals.collation;
5975
+ }
5976
+ return countOpts;
5977
+ };
5978
+ const countWithOptions = async () => {
5979
+ const countOpts = buildCountOptions(2e3);
5980
+ const countQuery = query;
5981
+ const runner = () => coll.countDocuments(
5982
+ countQuery,
5413
5983
  countOpts
5414
5984
  );
5415
- const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
5416
- return { mode: "sync", total, totalPages, ts: Date.now() };
5985
+ return defaults.countQueue ? defaults.countQueue.execute(runner) : runner();
5986
+ };
5987
+ const buildPayload = (total, approx = false) => ({
5988
+ mode,
5989
+ total,
5990
+ totalPages: total > 0 ? Math.ceil(total / limit) : 0,
5991
+ ts: Date.now(),
5992
+ ...approx ? { approx: true } : {}
5993
+ });
5994
+ const buildFailurePayload = (error) => ({
5995
+ mode,
5996
+ total: null,
5997
+ totalPages: null,
5998
+ ts: Date.now(),
5999
+ error
6000
+ });
6001
+ if (mode === "sync") {
6002
+ const cached = cache.get(cacheKey);
6003
+ if (cached !== void 0) {
6004
+ return { ...cached, mode: "sync" };
6005
+ }
6006
+ try {
6007
+ const payload = buildPayload(await countWithOptions());
6008
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6009
+ return { ...payload, mode: "sync" };
6010
+ } catch {
6011
+ const payload = buildFailurePayload("count_failed");
6012
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6013
+ return { ...payload, mode: "sync" };
6014
+ }
5417
6015
  }
5418
6016
  if (mode === "async") {
5419
- const cacheKey = JSON.stringify({ ns: coll.namespace, q: query });
5420
- const token = Buffer.from(cacheKey).toString("base64url");
5421
- const cachedTotal = _asyncTotalsCache.get(cacheKey);
5422
- if (cachedTotal !== void 0) {
5423
- return { mode: "async", total: cachedTotal, token };
6017
+ const cached = cache.get(cacheKey);
6018
+ if (cached !== void 0) {
6019
+ return { ...cached, mode: "async", token };
5424
6020
  }
5425
- setImmediate(async () => {
5426
- try {
5427
- const n = await coll.countDocuments(
5428
- query
5429
- );
5430
- _asyncTotalsCache.set(cacheKey, n);
5431
- } catch {
5432
- }
6021
+ setImmediate(() => {
6022
+ runTotalsOnce(cacheKey, async () => {
6023
+ try {
6024
+ const payload = buildPayload(await countWithOptions());
6025
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6026
+ } catch {
6027
+ await Promise.resolve(cache.set(cacheKey, buildFailurePayload("count_failed"), ttlMs));
6028
+ }
6029
+ });
5433
6030
  });
5434
6031
  return { mode: "async", total: null, token };
5435
6032
  }
5436
6033
  if (mode === "approx") {
5437
- const countOpts = {};
5438
- if (totals.maxTimeMS !== void 0) {
5439
- countOpts.maxTimeMS = totals.maxTimeMS;
6034
+ const cached = cache.get(cacheKey);
6035
+ if (cached !== void 0) {
6036
+ return { ...cached, mode: "approx" };
6037
+ }
6038
+ try {
6039
+ const total = Object.keys(query ?? {}).length > 0 ? await countWithOptions() : await coll.estimatedDocumentCount({
6040
+ maxTimeMS: totals.maxTimeMS ?? 1e3
6041
+ });
6042
+ const payload = buildPayload(total, true);
6043
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6044
+ return { ...payload, mode: "approx" };
6045
+ } catch {
6046
+ const payload = buildFailurePayload("approx_failed");
6047
+ await Promise.resolve(cache.set(cacheKey, payload, ttlMs));
6048
+ return { ...payload, mode: "approx" };
5440
6049
  }
5441
- const total = await coll.estimatedDocumentCount(countOpts);
5442
- const totalPages = total > 0 ? Math.ceil(total / limit) : 0;
5443
- return { mode: "approx", total, totalPages, ts: Date.now() };
5444
6050
  }
5445
6051
  return { mode: mode ?? "sync" };
5446
6052
  }
5447
- async function executeFindPage(collection, options = {}, defaults = {}) {
6053
+ async function executeFindPage(collection, options = {}, defaults = {}, queryCache) {
5448
6054
  const metaEnabled = options.meta === true || typeof options.meta === "object" && options.meta !== null;
5449
6055
  const metaOptions = options.meta && typeof options.meta === "object" ? options.meta : {};
5450
6056
  const metaLevel = options.meta === true ? "op" : metaOptions.level ?? "op";
@@ -5475,6 +6081,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5475
6081
  driverOpts.projection = buildEffectiveProjection(options.projection, sort);
5476
6082
  }
5477
6083
  const jumpOpts = ext.jump;
6084
+ const cacheTTL = typeof ext.cache === "number" && ext.cache > 0 ? ext.cache : 0;
6085
+ 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;
6086
+ const shouldRefreshAsyncTotals = options.totals?.mode === "async";
6087
+ let findPageCacheHit = false;
5478
6088
  const finishResult = (result2) => {
5479
6089
  if (!metaEnabled) {
5480
6090
  return result2;
@@ -5505,6 +6115,9 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5505
6115
  endTs: metaEndTs,
5506
6116
  durationMs: metaEndTs - metaStartTs,
5507
6117
  ...typeof effectiveMaxTimeMS === "number" ? { maxTimeMS: effectiveMaxTimeMS } : {},
6118
+ cacheHit: findPageCacheHit,
6119
+ ...findPageCacheHit ? { fromCache: true } : {},
6120
+ ...pageResultCache ? { cacheTtl: cacheTTL, keyHash: pageResultCache.keyHash } : {},
5508
6121
  page,
5509
6122
  after: Boolean(options.after),
5510
6123
  before: Boolean(options.before),
@@ -5573,7 +6186,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5573
6186
  };
5574
6187
  const timedComputeTotals = async () => {
5575
6188
  const stepStartTs = Date.now();
5576
- const result2 = await computeTotals(collection, baseQuery, limit, options.totals);
6189
+ const result2 = await computeTotals(collection, baseQuery, limit, options.totals, defaults, queryCache);
5577
6190
  pushMetaStep("computeTotals", Date.now() - stepStartTs, "totals");
5578
6191
  return result2;
5579
6192
  };
@@ -5592,6 +6205,32 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5592
6205
  ...extra.currentPage !== void 0 ? { currentPage: extra.currentPage } : {}
5593
6206
  };
5594
6207
  };
6208
+ const writePageResultCache = (result2) => {
6209
+ if (!pageResultCache || !queryCache) {
6210
+ return;
6211
+ }
6212
+ const cacheValue = cloneFindPageResult(result2);
6213
+ delete cacheValue.meta;
6214
+ if (shouldRefreshAsyncTotals) {
6215
+ delete cacheValue.totals;
6216
+ }
6217
+ void queryCache.set(pageResultCache.key, cacheValue, cacheTTL);
6218
+ };
6219
+ const finishAndCache = (result2) => {
6220
+ writePageResultCache(result2);
6221
+ return finishResult(result2);
6222
+ };
6223
+ if (pageResultCache && queryCache) {
6224
+ const cached = queryCache.get(pageResultCache.key);
6225
+ if (cached !== void 0) {
6226
+ findPageCacheHit = true;
6227
+ const result2 = cloneFindPageResult(cached);
6228
+ if (options.totals && options.totals.mode !== "none" && (shouldRefreshAsyncTotals || result2.totals === void 0)) {
6229
+ result2.totals = await timedComputeTotals();
6230
+ }
6231
+ return finishResult(result2);
6232
+ }
6233
+ }
5595
6234
  if (options.stream === true) {
5596
6235
  const direction = options.before ? "before" : "after";
5597
6236
  const { queryFilter, effectiveSort } = buildPageQuery(options.after ?? options.before, direction);
@@ -5634,7 +6273,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5634
6273
  if (options.totals && options.totals.mode !== "none") {
5635
6274
  result2.totals = await timedComputeTotals();
5636
6275
  }
5637
- return finishResult(result2);
6276
+ return finishAndCache(result2);
5638
6277
  }
5639
6278
  if (options.after || options.before) {
5640
6279
  const direction = options.after ? "after" : "before";
@@ -5644,7 +6283,7 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5644
6283
  const first = items2[0] ?? null;
5645
6284
  const last = items2[items2.length - 1] ?? null;
5646
6285
  const enc = (item) => item ? encodeCursor(Object.keys(sort).map((f) => item[f]), cursorSecret) : null;
5647
- return finishResult({
6286
+ const result2 = {
5648
6287
  items: items2,
5649
6288
  pageInfo: {
5650
6289
  hasNext: direction === "before" ? Boolean(options.before) : hasMore2,
@@ -5652,7 +6291,11 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5652
6291
  startCursor: enc(first),
5653
6292
  endCursor: enc(last)
5654
6293
  }
5655
- });
6294
+ };
6295
+ if (options.totals && options.totals.mode !== "none") {
6296
+ result2.totals = await timedComputeTotals();
6297
+ }
6298
+ return finishAndCache(result2);
5656
6299
  }
5657
6300
  const { queryFilter: q0, effectiveSort: es0 } = buildPageQuery();
5658
6301
  let { items, hasMore } = await timedFetchItems("initialFetch", page > 1 ? "hop" : "fetch", q0, es0, {}, 1);
@@ -5664,15 +6307,19 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5664
6307
  if (options.totals && options.totals.mode !== "none") {
5665
6308
  result2.totals = await timedComputeTotals();
5666
6309
  }
5667
- return finishResult(result2);
6310
+ return finishAndCache(result2);
5668
6311
  }
5669
6312
  for (let cp = 2; cp <= page; cp++) {
5670
6313
  const lastItem = items[items.length - 1];
5671
6314
  if (!lastItem) {
5672
- return finishResult({
6315
+ const result2 = {
5673
6316
  items,
5674
6317
  pageInfo: buildPageInfo(items, false, { hasPrev: cp > 2, currentPage: cp - 1 })
5675
- });
6318
+ };
6319
+ if (options.totals && options.totals.mode !== "none") {
6320
+ result2.totals = await timedComputeTotals();
6321
+ }
6322
+ return finishAndCache(result2);
5676
6323
  }
5677
6324
  const endCursor = encodeCursor(
5678
6325
  Object.keys(sort).map((f) => lastItem[f]),
@@ -5690,10 +6337,10 @@ async function executeFindPage(collection, options = {}, defaults = {}) {
5690
6337
  if (options.totals && options.totals.mode !== "none") {
5691
6338
  result.totals = await timedComputeTotals();
5692
6339
  }
5693
- return finishResult(result);
6340
+ return finishAndCache(result);
5694
6341
  }
5695
- async function findPageDocuments(collection, options = {}, defaults) {
5696
- return executeFindPage(collection, options, defaults ?? {});
6342
+ async function findPageDocuments(collection, options = {}, defaults, queryCache) {
6343
+ return executeFindPage(collection, options, defaults ?? {}, queryCache);
5697
6344
  }
5698
6345
 
5699
6346
  // src/adapters/mongodb/queries/find-by-id.ts
@@ -5915,21 +6562,21 @@ var FindChain = class {
5915
6562
  }
5916
6563
  limit(value) {
5917
6564
  if (typeof value !== "number" || value < 0) {
5918
- throw new Error(`limit() requires a non-negative number, got: ${typeof value} (${value})`);
6565
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `limit() requires a non-negative number, got: ${typeof value} (${value})`);
5919
6566
  }
5920
6567
  this.options.limit = value;
5921
6568
  return this;
5922
6569
  }
5923
6570
  skip(value) {
5924
6571
  if (typeof value !== "number" || value < 0) {
5925
- throw new Error(`skip() requires a non-negative number, got: ${typeof value} (${value})`);
6572
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `skip() requires a non-negative number, got: ${typeof value} (${value})`);
5926
6573
  }
5927
6574
  this.options.skip = value;
5928
6575
  return this;
5929
6576
  }
5930
6577
  sort(value) {
5931
6578
  if (!value || typeof value !== "object") {
5932
- throw new Error(`sort() requires an object or array, got: ${typeof value}`);
6579
+ throw createError(ErrorCodes.INVALID_ARGUMENT, `sort() requires an object or array, got: ${typeof value}`);
5933
6580
  }
5934
6581
  this.options.sort = value;
5935
6582
  return this;
@@ -5973,7 +6620,7 @@ var FindChain = class {
5973
6620
  }
5974
6621
  toArray() {
5975
6622
  if (this.executed) {
5976
- throw new Error("Query already executed.");
6623
+ throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
5977
6624
  }
5978
6625
  this.executed = true;
5979
6626
  return this.collection.find(this.normalizedQuery, buildFindDriverOptions(this.buildExecuteOptions())).toArray();
@@ -6061,7 +6708,7 @@ var AggregateChain = class {
6061
6708
  }
6062
6709
  toArray() {
6063
6710
  if (this.executed) {
6064
- throw new Error("Query already executed.");
6711
+ throw createError(ErrorCodes.INVALID_OPERATION, "Query already executed.");
6065
6712
  }
6066
6713
  this.executed = true;
6067
6714
  return this.collection.aggregate(this.pipeline, buildAggregateDriverOptions(this.buildExecuteOptions())).toArray();
@@ -6856,7 +7503,7 @@ async function insertOneForAccessor(context, doc, options) {
6856
7503
  const threshold = context.defaults?.slowQueryMs ?? 500;
6857
7504
  if (elapsed > threshold && context.logger) {
6858
7505
  try {
6859
- context.logger.warn("[insertOne] \u6162\u64CD\u4F5C\u8B66\u544A", {
7506
+ context.logger.warn("[insertOne] slow operation warning", {
6860
7507
  ns: `${context.dbName}.${context.collectionName}`,
6861
7508
  threshold,
6862
7509
  duration: elapsed,
@@ -6901,7 +7548,7 @@ async function insertManyForAccessor(context, documents, options) {
6901
7548
  const elapsed = Date.now() - startedAt;
6902
7549
  const threshold = context.defaults?.slowQueryMs ?? 500;
6903
7550
  if (elapsed >= threshold && context.logger) {
6904
- context.logger.warn("[insertMany] \u6162\u64CD\u4F5C\u8B66\u544A", {
7551
+ context.logger.warn("[insertMany] slow operation warning", {
6905
7552
  ns: `${context.dbName}.${context.collectionName}`,
6906
7553
  threshold,
6907
7554
  duration: elapsed,
@@ -7032,11 +7679,17 @@ var MongoCollectionAccessor = class {
7032
7679
  const legacyNamespacePatterns = [
7033
7680
  `${String(this.management.defaults?.namespace?.instanceId)}:mongodb:${this.dbName}:${this.collectionName}:*`
7034
7681
  ];
7035
- const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? [`bookmark:${bookmarkNamespace}:*`] : [
7682
+ const findPagePatterns = [
7683
+ `findPage:${namespace}:*`,
7684
+ `findPageTotals:${namespace}:*`,
7685
+ `bookmark:${bookmarkNamespace}:*`,
7686
+ `${bookmarkNamespace}:bm:*`
7687
+ ];
7688
+ const patterns = operation === "find" ? [`find:${namespace}:*`] : operation === "findOne" ? [`findOne:${namespace}:*`] : operation === "count" ? [`count:${namespace}:*`] : operation === "findPage" ? findPagePatterns : [
7036
7689
  `find:${namespace}:*`,
7037
7690
  `findOne:${namespace}:*`,
7038
7691
  `count:${namespace}:*`,
7039
- `bookmark:${bookmarkNamespace}:*`
7692
+ ...findPagePatterns
7040
7693
  ];
7041
7694
  patterns.push(...legacyNamespacePatterns);
7042
7695
  let deleted = 0;
@@ -7166,7 +7819,7 @@ var MongoCollectionAccessor = class {
7166
7819
  }
7167
7820
  async findPage(options = {}) {
7168
7821
  const resolvedOptions = options.query ? { ...options, query: this._cvFilter(options.query) } : options;
7169
- return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults);
7822
+ return findPageDocuments(this.collectionRef, resolvedOptions, this.management.defaults, this.management.queryCache);
7170
7823
  }
7171
7824
  /** Opens a change stream on the collection with an optional aggregation pipeline. */
7172
7825
  watch(pipeline = [], options) {
@@ -7643,6 +8296,26 @@ function createRuntimeModelInstance(host, name, scope) {
7643
8296
  });
7644
8297
  return instance;
7645
8298
  }
8299
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8300
+ const modelNames = options.models ?? Model.list();
8301
+ const models = [];
8302
+ for (const name of modelNames) {
8303
+ const model = host.scopedModel(name, {
8304
+ database: options.database,
8305
+ pool: options.pool
8306
+ });
8307
+ const result = await model.ensureIndexes({
8308
+ dryRun: options.dryRun,
8309
+ throwOnError: options.throwOnError
8310
+ });
8311
+ models.push({ name, result });
8312
+ }
8313
+ return {
8314
+ dryRun: options.dryRun === true,
8315
+ models,
8316
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8317
+ };
8318
+ }
7646
8319
 
7647
8320
  // src/entry/runtime-core-hosts.ts
7648
8321
  function resolveAdapterCache(state) {
@@ -7858,7 +8531,7 @@ function createRuntimeAdapterBridge(host) {
7858
8531
  dropDatabase: async (name, adminOptions) => {
7859
8532
  host.ensureConnected();
7860
8533
  if (!name || typeof name !== "string") {
7861
- throw new Error("Database name is required and must be a non-empty string");
8534
+ throw createError(ErrorCodes.INVALID_DATABASE_NAME, "Database name is required and must be a non-empty string");
7862
8535
  }
7863
8536
  if (!adminOptions?.confirm) {
7864
8537
  const error = new Error(
@@ -7894,7 +8567,7 @@ function createRuntimeAdapterBridge(host) {
7894
8567
  runCommand: async (command, adminOptions) => {
7895
8568
  host.ensureConnected();
7896
8569
  if (command === null || typeof command !== "object") {
7897
- throw new Error("Command must be a non-null object");
8570
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Command must be a non-null object");
7898
8571
  }
7899
8572
  return host.db().runCommand(command, adminOptions ?? {});
7900
8573
  },
@@ -7910,7 +8583,7 @@ function createRuntimeAdapterBridge(host) {
7910
8583
  }
7911
8584
 
7912
8585
  // src/capabilities/lock/index.ts
7913
- var import_node_crypto4 = require("node:crypto");
8586
+ var import_node_crypto5 = require("node:crypto");
7914
8587
  var LockAcquireError = class extends Error {
7915
8588
  constructor(message) {
7916
8589
  super(message);
@@ -8020,13 +8693,13 @@ var LockManager = class {
8020
8693
  if (attempt === retryTimes) {
8021
8694
  break;
8022
8695
  }
8023
- const delay = retryDelay * Math.pow(retryBackoff, attempt);
8024
- await sleep3(delay);
8696
+ const delay2 = retryDelay * Math.pow(retryBackoff, attempt);
8697
+ await sleep3(delay2);
8025
8698
  }
8026
8699
  this.stats.errors += 1;
8027
8700
  if (options.fallbackToNoLock) {
8028
8701
  this.logger?.warn?.(`[LockManager] fallback to no-lock execution for ${key}`);
8029
- return new Lock(this.normalizeKey(key), `noop:${(0, import_node_crypto4.randomUUID)()}`, new NoopLockManager(), options.ttl ?? 1e4);
8702
+ return new Lock(this.normalizeKey(key), `noop:${(0, import_node_crypto5.randomUUID)()}`, new NoopLockManager(), options.ttl ?? 1e4);
8030
8703
  }
8031
8704
  throw new LockTimeoutError(`Failed to acquire lock for key '${key}' within retry budget.`);
8032
8705
  }
@@ -8042,7 +8715,7 @@ var LockManager = class {
8042
8715
  if (globalStore.has(normalizedKey)) {
8043
8716
  return null;
8044
8717
  }
8045
- const lockId = (0, import_node_crypto4.randomUUID)();
8718
+ const lockId = (0, import_node_crypto5.randomUUID)();
8046
8719
  globalStore.set(normalizedKey, {
8047
8720
  lockId,
8048
8721
  expiresAt: Date.now() + ttl
@@ -8145,7 +8818,7 @@ var DistributedCacheLockManager = class {
8145
8818
  errors: 0
8146
8819
  };
8147
8820
  if (!options.redis) {
8148
- throw new Error("DistributedCacheLockManager requires a Redis instance");
8821
+ throw createError(ErrorCodes.INVALID_CONFIG, "DistributedCacheLockManager requires a Redis instance");
8149
8822
  }
8150
8823
  this.redis = options.redis;
8151
8824
  this.lockKeyPrefix = options.lockKeyPrefix ?? "monsqlize:cache:lock:";
@@ -8334,7 +9007,7 @@ var DistributedCacheLockManager = class {
8334
9007
  };
8335
9008
 
8336
9009
  // src/capabilities/saga/index.ts
8337
- var import_node_crypto5 = require("node:crypto");
9010
+ var import_node_crypto6 = require("node:crypto");
8338
9011
  var SagaExecutionContext = class {
8339
9012
  constructor(executionId, data) {
8340
9013
  this.executionId = executionId;
@@ -8430,7 +9103,7 @@ var SagaOrchestrator = class {
8430
9103
  if (!definition) {
8431
9104
  throw createError(ErrorCodes.INVALID_ARGUMENT, `Saga '${name}' is not defined`);
8432
9105
  }
8433
- const sagaId = `saga_${(0, import_node_crypto5.randomBytes)(8).toString("hex")}`;
9106
+ const sagaId = `saga_${(0, import_node_crypto6.randomBytes)(8).toString("hex")}`;
8434
9107
  const startedAt = Date.now();
8435
9108
  const context = new SagaExecutionContext(sagaId, data);
8436
9109
  const completedSteps = [];
@@ -8803,17 +9476,17 @@ var BatchQueue = class {
8803
9476
  var import_mongodb6 = require("mongodb");
8804
9477
 
8805
9478
  // src/capabilities/slow-query-log/slow-query-log-records.ts
8806
- var import_node_crypto6 = require("node:crypto");
8807
- function stableStringify2(value) {
9479
+ var import_node_crypto7 = require("node:crypto");
9480
+ function stableStringify3(value) {
8808
9481
  if (Array.isArray(value)) {
8809
- return `[${value.map((item) => stableStringify2(item)).join(",")}]`;
9482
+ return `[${value.map((item) => stableStringify3(item)).join(",")}]`;
8810
9483
  }
8811
9484
  if (value instanceof Date) {
8812
9485
  return JSON.stringify(value.toISOString());
8813
9486
  }
8814
9487
  if (value && typeof value === "object") {
8815
9488
  const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
8816
- return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify2(current)}`).join(",")}}`;
9489
+ return `{${entries.map(([key, current]) => `${JSON.stringify(key)}:${stableStringify3(current)}`).join(",")}}`;
8817
9490
  }
8818
9491
  return JSON.stringify(value);
8819
9492
  }
@@ -8830,7 +9503,7 @@ function normalizeHashInput(input) {
8830
9503
  };
8831
9504
  }
8832
9505
  function generateQueryHash(input) {
8833
- return (0, import_node_crypto6.createHash)("sha256").update(stableStringify2(normalizeHashInput(input))).digest("hex").slice(0, 16);
9506
+ return (0, import_node_crypto7.createHash)("sha256").update(stableStringify3(normalizeHashInput(input))).digest("hex").slice(0, 16);
8834
9507
  }
8835
9508
  function handleSlowQueryLogError(logger, policy, error) {
8836
9509
  if (policy === "throw") {
@@ -9280,7 +9953,7 @@ var SlowQueryLogManager = class {
9280
9953
 
9281
9954
  // src/capabilities/sync/index.ts
9282
9955
  var import_promises = require("node:fs/promises");
9283
- var import_node_path = __toESM(require("node:path"));
9956
+ var import_node_path2 = __toESM(require("node:path"));
9284
9957
  var import_mongodb7 = require("mongodb");
9285
9958
  function validateTargetConfig(target, index) {
9286
9959
  if (!target || typeof target !== "object") {
@@ -9387,7 +10060,7 @@ var ResumeTokenStore = class {
9387
10060
  await Promise.resolve(this.redis.set(this.redisKey, payload));
9388
10061
  return;
9389
10062
  }
9390
- await (0, import_promises.mkdir)(import_node_path.default.dirname(this.path), { recursive: true });
10063
+ await (0, import_promises.mkdir)(import_node_path2.default.dirname(this.path), { recursive: true });
9391
10064
  await (0, import_promises.writeFile)(this.path, payload, "utf8");
9392
10065
  } catch (error) {
9393
10066
  this.logger?.error?.("[Sync] failed to save resume token", error);
@@ -9659,7 +10332,16 @@ function getOrCreateTransactionManager(config) {
9659
10332
  client: config.client,
9660
10333
  cache: config.cache,
9661
10334
  logger: config.logger,
9662
- lockManager: config.lockManager
10335
+ lockManager: config.lockManager,
10336
+ maxDuration: config.transaction?.maxDuration ?? config.transaction?.defaultTimeout,
10337
+ enableRetry: config.transaction?.enableRetry,
10338
+ maxRetries: config.transaction?.maxRetries,
10339
+ retryDelay: config.transaction?.retryDelay,
10340
+ retryBackoff: config.transaction?.retryBackoff,
10341
+ defaultReadConcern: config.transaction?.defaultReadConcern,
10342
+ defaultWriteConcern: config.transaction?.defaultWriteConcern,
10343
+ defaultReadPreference: config.transaction?.defaultReadPreference,
10344
+ maxStatsSamples: config.transaction?.maxStatsSamples
9663
10345
  });
9664
10346
  }
9665
10347
  function getOrCreateLockManager(current, logger) {
@@ -9946,7 +10628,7 @@ function resolveCacheSource(cacheOrDb) {
9946
10628
  return cache;
9947
10629
  }
9948
10630
  }
9949
- throw new Error("Invalid cache instance from MonSQLize");
10631
+ throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance from MonSQLize");
9950
10632
  }
9951
10633
  function toWithCacheStats(stats, totalTime = 0) {
9952
10634
  const calls = stats.hits + stats.misses;
@@ -9975,46 +10657,46 @@ function normalizeFunctionCacheStats(stats, timings, name) {
9975
10657
  }
9976
10658
  function validateFunctionCacheOptions(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.namespace !== void 0 && typeof opts.namespace !== "string") {
9982
- throw new Error("namespace must be a string");
10664
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "namespace must be a string");
9983
10665
  }
9984
10666
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
9985
10667
  if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
9986
- throw new Error("defaultTTL must be a non-negative number");
10668
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
9987
10669
  }
9988
10670
  return opts;
9989
10671
  }
9990
10672
  function validateFunctionCachePerFnOptions(options) {
9991
10673
  if (options !== void 0 && (typeof options !== "object" || options === null || Array.isArray(options))) {
9992
- throw new Error("options must be an object");
10674
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "options must be an object");
9993
10675
  }
9994
10676
  const opts = options ?? {};
9995
10677
  if (opts.keyBuilder !== void 0 && typeof opts.keyBuilder !== "function") {
9996
- throw new Error("keyBuilder must be a function");
10678
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
9997
10679
  }
9998
10680
  if (opts.condition !== void 0 && typeof opts.condition !== "function") {
9999
- throw new Error("condition must be a function");
10681
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
10000
10682
  }
10001
10683
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
10002
10684
  if (ttl !== void 0 && (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)) {
10003
- throw new Error("defaultTTL must be a non-negative number");
10685
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "defaultTTL must be a non-negative number");
10004
10686
  }
10005
10687
  return opts;
10006
10688
  }
10007
10689
  function withCache(fn, options = {}) {
10008
- if (typeof fn !== "function") throw new Error("fn must be a function");
10690
+ if (typeof fn !== "function") throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
10009
10691
  const { ttl = 6e4, namespace, keyBuilder, condition, cache: externalCache, enableStats = true } = options;
10010
10692
  if (typeof ttl !== "number" || Number.isNaN(ttl) || ttl < 0)
10011
- throw new Error("ttl must be a non-negative number");
10693
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "ttl must be a non-negative number");
10012
10694
  if (keyBuilder !== void 0 && typeof keyBuilder !== "function")
10013
- throw new Error("keyBuilder must be a function");
10695
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "keyBuilder must be a function");
10014
10696
  if (condition !== void 0 && typeof condition !== "function")
10015
- throw new Error("condition must be a function");
10697
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "condition must be a function");
10016
10698
  if (externalCache !== void 0 && !isValidCache(externalCache))
10017
- throw new Error("Invalid cache instance");
10699
+ throw createError(ErrorCodes.CACHE_UNAVAILABLE, "Invalid cache instance");
10018
10700
  const wrapped = (0, import_function_cache.withCache)(fn, {
10019
10701
  ttl,
10020
10702
  namespace: namespace ?? "fn",
@@ -10056,9 +10738,9 @@ var FunctionCache = class {
10056
10738
  }
10057
10739
  register(name, fn, options) {
10058
10740
  if (!name || typeof name !== "string")
10059
- throw new Error("Function name must be a non-empty string");
10741
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
10060
10742
  if (typeof fn !== "function")
10061
- throw new Error("fn must be a function");
10743
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "fn must be a function");
10062
10744
  const opts = validateFunctionCachePerFnOptions(options);
10063
10745
  const ttl = opts.ttl !== void 0 ? opts.ttl : opts.defaultTTL;
10064
10746
  this._inner.register(name, fn, {
@@ -10084,13 +10766,13 @@ var FunctionCache = class {
10084
10766
  }
10085
10767
  async invalidate(name, ...args) {
10086
10768
  if (!name || typeof name !== "string") {
10087
- throw new Error("Function name must be a non-empty string");
10769
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Function name must be a non-empty string");
10088
10770
  }
10089
10771
  await this._inner.invalidate(name, ...args);
10090
10772
  }
10091
10773
  async invalidatePattern(pattern) {
10092
10774
  if (!pattern || typeof pattern !== "string")
10093
- throw new Error("Pattern must be a non-empty string");
10775
+ throw createError(ErrorCodes.INVALID_ARGUMENT, "Pattern must be a non-empty string");
10094
10776
  return this._inner.invalidatePattern(pattern);
10095
10777
  }
10096
10778
  list() {
@@ -10125,15 +10807,15 @@ function b64urlDecodeStr(s) {
10125
10807
  const b64 = pad(String(s || "")).replace(/-/g, "+").replace(/_/g, "/");
10126
10808
  return Buffer.from(b64, "base64").toString();
10127
10809
  }
10128
- function makeInvalidCursorError(cause) {
10129
- const err = new Error("Invalid cursor");
10810
+ function makeInvalidCursorError(message = "Invalid cursor", cause) {
10811
+ const err = new Error(message);
10130
10812
  err.code = "INVALID_CURSOR";
10131
10813
  if (cause !== void 0) err.cause = cause;
10132
10814
  return err;
10133
10815
  }
10134
10816
  function encodeCursor2(payload) {
10135
10817
  if (!payload.s || !payload.a) {
10136
- throw new Error("encodeCursor requires sort (s) and anchor (a)");
10818
+ throw makeInvalidCursorError("encodeCursor requires sort (s) and anchor (a)");
10137
10819
  }
10138
10820
  const json = JSON.stringify({ v: payload.v ?? 1, s: payload.s, a: payload.a, d: payload.d });
10139
10821
  return b64urlEncodeStr(json);
@@ -10142,11 +10824,11 @@ function decodeCursor2(str) {
10142
10824
  try {
10143
10825
  const obj = JSON.parse(b64urlDecodeStr(str));
10144
10826
  if (!obj || obj["v"] !== 1 || !obj["s"] || !obj["a"]) {
10145
- throw new Error("bad-structure");
10827
+ throw makeInvalidCursorError("bad-structure");
10146
10828
  }
10147
10829
  return obj;
10148
10830
  } catch (e) {
10149
- throw makeInvalidCursorError(e);
10831
+ throw makeInvalidCursorError("Invalid cursor", e);
10150
10832
  }
10151
10833
  }
10152
10834
 
@@ -10218,7 +10900,11 @@ var MonSQLizeRuntime = class {
10218
10900
  } : rawCacheInput;
10219
10901
  this._cache = normalizeRuntimeCache(cacheInput);
10220
10902
  this._logger = Logger.create(options.logger ?? null);
10221
- this._cacheLockManager = new CacheLockManager({ logger: options.logger ?? null });
10903
+ this._cacheLockManager = new CacheLockManager({
10904
+ logger: options.logger ?? null,
10905
+ maxDuration: options.transaction?.lockMaxDuration,
10906
+ cleanupInterval: options.transaction?.lockCleanupInterval
10907
+ });
10222
10908
  this._cache.setLockManager?.(this._cacheLockManager);
10223
10909
  this._runtimeDefaults = buildRuntimeDefaults(options);
10224
10910
  this._adapterCacheOverride = void 0;
@@ -10338,7 +11024,8 @@ var MonSQLizeRuntime = class {
10338
11024
  namespace: d.namespace,
10339
11025
  log: d.log,
10340
11026
  countQueue: this.options.countQueue,
10341
- models: this.options.models
11027
+ models: this.options.models,
11028
+ autoIndex: this.options.autoIndex
10342
11029
  };
10343
11030
  }
10344
11031
  async close() {
@@ -10535,6 +11222,10 @@ var MonSQLizeRuntime = class {
10535
11222
  cache.set(name, instance);
10536
11223
  return instance;
10537
11224
  }
11225
+ async ensureModelIndexes(options = {}) {
11226
+ this.ensureConnected();
11227
+ return ensureRuntimeModelIndexes(this, options);
11228
+ }
10538
11229
  // Capability delegation ----------------------------------------------------
10539
11230
  async startSession(options = {}) {
10540
11231
  this.ensureConnected();
@@ -10589,9 +11280,15 @@ var MonSQLizeRuntime = class {
10589
11280
  listSagas() {
10590
11281
  return this.initializeSagaOrchestrator().listSagas();
10591
11282
  }
11283
+ getTransactionStats() {
11284
+ return this._transactionManager?.getStats() ?? null;
11285
+ }
10592
11286
  getSagaStats() {
10593
11287
  return this.initializeSagaOrchestrator().getStats();
10594
11288
  }
11289
+ getDistributedCacheInvalidatorStats() {
11290
+ return this._distributedInvalidator?.getStats() ?? null;
11291
+ }
10595
11292
  async startSync() {
10596
11293
  this.ensureConnected();
10597
11294
  const manager = await this.initializeSyncManager();
@@ -10670,7 +11367,8 @@ var MonSQLizeRuntime = class {
10670
11367
  client: this._client,
10671
11368
  cache: this._cache,
10672
11369
  logger: this.options.logger ?? null,
10673
- lockManager: this._cacheLockManager
11370
+ lockManager: this._cacheLockManager,
11371
+ transaction: this.options.transaction
10674
11372
  });
10675
11373
  return this._transactionManager;
10676
11374
  }