monsqlize 2.0.1 → 2.0.3

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