drizzle-multitenant 1.0.10 → 1.2.0

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.
@@ -9407,8 +9407,161 @@ var DebugLogger = class {
9407
9407
  function createDebugLogger(config) {
9408
9408
  return new DebugLogger(config);
9409
9409
  }
9410
+ var PoolCache = class {
9411
+ cache;
9412
+ poolTtlMs;
9413
+ onDispose;
9414
+ constructor(options) {
9415
+ this.poolTtlMs = options.poolTtlMs;
9416
+ this.onDispose = options.onDispose;
9417
+ this.cache = new LRUCache({
9418
+ max: options.maxPools,
9419
+ dispose: (entry, key) => {
9420
+ void this.handleDispose(key, entry);
9421
+ },
9422
+ noDisposeOnSet: true
9423
+ });
9424
+ }
9425
+ /**
9426
+ * Get a pool entry from cache
9427
+ *
9428
+ * This does NOT update the last access time automatically.
9429
+ * Use `touch()` to update access time when needed.
9430
+ */
9431
+ get(schemaName) {
9432
+ return this.cache.get(schemaName);
9433
+ }
9434
+ /**
9435
+ * Set a pool entry in cache
9436
+ *
9437
+ * If the cache is full, the least recently used entry will be evicted.
9438
+ */
9439
+ set(schemaName, entry) {
9440
+ this.cache.set(schemaName, entry);
9441
+ }
9442
+ /**
9443
+ * Check if a pool exists in cache
9444
+ */
9445
+ has(schemaName) {
9446
+ return this.cache.has(schemaName);
9447
+ }
9448
+ /**
9449
+ * Delete a pool from cache
9450
+ *
9451
+ * Note: This triggers the dispose callback if configured.
9452
+ */
9453
+ delete(schemaName) {
9454
+ return this.cache.delete(schemaName);
9455
+ }
9456
+ /**
9457
+ * Get the number of pools in cache
9458
+ */
9459
+ size() {
9460
+ return this.cache.size;
9461
+ }
9462
+ /**
9463
+ * Get all schema names in cache
9464
+ */
9465
+ keys() {
9466
+ return Array.from(this.cache.keys());
9467
+ }
9468
+ /**
9469
+ * Iterate over all entries in cache
9470
+ *
9471
+ * @yields [schemaName, entry] pairs
9472
+ */
9473
+ *entries() {
9474
+ for (const [key, value] of this.cache.entries()) {
9475
+ yield [key, value];
9476
+ }
9477
+ }
9478
+ /**
9479
+ * Clear all pools from cache
9480
+ *
9481
+ * Each pool's dispose callback will be triggered by the LRU cache.
9482
+ */
9483
+ async clear() {
9484
+ this.cache.clear();
9485
+ await Promise.resolve();
9486
+ }
9487
+ /**
9488
+ * Evict the least recently used pool
9489
+ *
9490
+ * @returns The schema name of the evicted pool, or undefined if cache is empty
9491
+ */
9492
+ evictLRU() {
9493
+ const keys = Array.from(this.cache.keys());
9494
+ if (keys.length === 0) {
9495
+ return void 0;
9496
+ }
9497
+ const lruKey = keys[keys.length - 1];
9498
+ this.cache.delete(lruKey);
9499
+ return lruKey;
9500
+ }
9501
+ /**
9502
+ * Evict pools that have exceeded TTL
9503
+ *
9504
+ * @returns Array of schema names that were evicted
9505
+ */
9506
+ async evictExpired() {
9507
+ if (!this.poolTtlMs) {
9508
+ return [];
9509
+ }
9510
+ const now = Date.now();
9511
+ const toEvict = [];
9512
+ for (const [schemaName, entry] of this.cache.entries()) {
9513
+ if (now - entry.lastAccess > this.poolTtlMs) {
9514
+ toEvict.push(schemaName);
9515
+ }
9516
+ }
9517
+ for (const schemaName of toEvict) {
9518
+ this.cache.delete(schemaName);
9519
+ }
9520
+ return toEvict;
9521
+ }
9522
+ /**
9523
+ * Update last access time for a pool
9524
+ *
9525
+ * This moves the pool to the front of the LRU list.
9526
+ */
9527
+ touch(schemaName) {
9528
+ const entry = this.cache.get(schemaName);
9529
+ if (entry) {
9530
+ entry.lastAccess = Date.now();
9531
+ }
9532
+ }
9533
+ /**
9534
+ * Get the maximum number of pools allowed in cache
9535
+ */
9536
+ getMaxPools() {
9537
+ return this.cache.max;
9538
+ }
9539
+ /**
9540
+ * Get the configured TTL in milliseconds
9541
+ */
9542
+ getTtlMs() {
9543
+ return this.poolTtlMs;
9544
+ }
9545
+ /**
9546
+ * Check if an entry has expired based on TTL
9547
+ */
9548
+ isExpired(entry) {
9549
+ if (!this.poolTtlMs) {
9550
+ return false;
9551
+ }
9552
+ return Date.now() - entry.lastAccess > this.poolTtlMs;
9553
+ }
9554
+ /**
9555
+ * Handle disposal of a cache entry
9556
+ */
9557
+ async handleDispose(schemaName, entry) {
9558
+ if (this.onDispose) {
9559
+ await this.onDispose(schemaName, entry);
9560
+ }
9561
+ }
9562
+ };
9410
9563
 
9411
- // src/retry.ts
9564
+ // src/pool/retry/retry-handler.ts
9412
9565
  function isRetryableError(error) {
9413
9566
  const message = error.message.toLowerCase();
9414
9567
  if (message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("enotfound") || message.includes("connection refused") || message.includes("connection reset") || message.includes("connection terminated") || message.includes("connection timed out") || message.includes("timeout expired") || message.includes("socket hang up")) {
@@ -9422,77 +9575,315 @@ function isRetryableError(error) {
9422
9575
  }
9423
9576
  return false;
9424
9577
  }
9425
- function calculateDelay(attempt, config) {
9426
- const exponentialDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
9427
- const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
9428
- if (config.jitter) {
9429
- const jitterFactor = 1 + Math.random() * 0.25;
9430
- return Math.floor(cappedDelay * jitterFactor);
9431
- }
9432
- return Math.floor(cappedDelay);
9433
- }
9434
9578
  function sleep(ms) {
9435
9579
  return new Promise((resolve) => setTimeout(resolve, ms));
9436
9580
  }
9437
- async function withRetry(operation, config) {
9438
- const retryConfig = {
9439
- maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
9440
- initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
9441
- maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
9442
- backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
9443
- jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
9444
- isRetryable: config?.isRetryable ?? isRetryableError,
9445
- onRetry: config?.onRetry
9446
- };
9447
- const startTime = Date.now();
9448
- let lastError = null;
9449
- for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
9581
+ var RetryHandler = class {
9582
+ config;
9583
+ constructor(config) {
9584
+ this.config = {
9585
+ maxAttempts: config?.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
9586
+ initialDelayMs: config?.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
9587
+ maxDelayMs: config?.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
9588
+ backoffMultiplier: config?.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
9589
+ jitter: config?.jitter ?? DEFAULT_CONFIG.retry.jitter,
9590
+ isRetryable: config?.isRetryable ?? isRetryableError,
9591
+ onRetry: config?.onRetry
9592
+ };
9593
+ }
9594
+ /**
9595
+ * Execute an operation with retry logic
9596
+ *
9597
+ * @param operation - The async operation to execute
9598
+ * @param overrideConfig - Optional config to override defaults for this call
9599
+ * @returns Result with metadata about attempts and timing
9600
+ */
9601
+ async withRetry(operation, overrideConfig) {
9602
+ const config = overrideConfig ? { ...this.config, ...overrideConfig } : this.config;
9603
+ const startTime = Date.now();
9604
+ let lastError = null;
9605
+ for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
9606
+ try {
9607
+ const result = await operation();
9608
+ return {
9609
+ result,
9610
+ attempts: attempt + 1,
9611
+ totalTimeMs: Date.now() - startTime
9612
+ };
9613
+ } catch (error) {
9614
+ lastError = error;
9615
+ const isLastAttempt = attempt >= config.maxAttempts - 1;
9616
+ const checkRetryable = config.isRetryable ?? this.isRetryable;
9617
+ if (isLastAttempt || !checkRetryable(lastError)) {
9618
+ throw lastError;
9619
+ }
9620
+ const delay = this.calculateDelay(attempt, config);
9621
+ config.onRetry?.(attempt + 1, lastError, delay);
9622
+ await sleep(delay);
9623
+ }
9624
+ }
9625
+ throw lastError ?? new Error("Retry failed with no error");
9626
+ }
9627
+ /**
9628
+ * Calculate delay with exponential backoff and optional jitter
9629
+ *
9630
+ * @param attempt - Current attempt number (0-indexed)
9631
+ * @param config - Retry configuration
9632
+ * @returns Delay in milliseconds
9633
+ */
9634
+ calculateDelay(attempt, config) {
9635
+ const cfg = config ? { ...this.config, ...config } : this.config;
9636
+ const exponentialDelay = cfg.initialDelayMs * Math.pow(cfg.backoffMultiplier, attempt);
9637
+ const cappedDelay = Math.min(exponentialDelay, cfg.maxDelayMs);
9638
+ if (cfg.jitter) {
9639
+ const jitterFactor = 1 + Math.random() * 0.25;
9640
+ return Math.floor(cappedDelay * jitterFactor);
9641
+ }
9642
+ return Math.floor(cappedDelay);
9643
+ }
9644
+ /**
9645
+ * Check if an error is retryable
9646
+ *
9647
+ * Uses the configured isRetryable function or the default implementation.
9648
+ */
9649
+ isRetryable(error) {
9650
+ return (this.config.isRetryable ?? isRetryableError)(error);
9651
+ }
9652
+ /**
9653
+ * Get the current configuration
9654
+ */
9655
+ getConfig() {
9656
+ return { ...this.config };
9657
+ }
9658
+ /**
9659
+ * Get the maximum number of attempts
9660
+ */
9661
+ getMaxAttempts() {
9662
+ return this.config.maxAttempts;
9663
+ }
9664
+ };
9665
+
9666
+ // src/pool/health/health-checker.ts
9667
+ var HealthChecker = class {
9668
+ constructor(deps) {
9669
+ this.deps = deps;
9670
+ }
9671
+ /**
9672
+ * Check health of all pools and connections
9673
+ *
9674
+ * Verifies the health of tenant pools and optionally the shared database.
9675
+ * Returns detailed status information for monitoring and load balancer integration.
9676
+ *
9677
+ * @example
9678
+ * ```typescript
9679
+ * // Basic health check
9680
+ * const health = await healthChecker.checkHealth();
9681
+ * console.log(health.healthy); // true/false
9682
+ *
9683
+ * // Check specific tenants only
9684
+ * const health = await healthChecker.checkHealth({
9685
+ * tenantIds: ['tenant-1', 'tenant-2'],
9686
+ * ping: true,
9687
+ * pingTimeoutMs: 3000,
9688
+ * });
9689
+ * ```
9690
+ */
9691
+ async checkHealth(options = {}) {
9692
+ const startTime = Date.now();
9693
+ const {
9694
+ ping = true,
9695
+ pingTimeoutMs = 5e3,
9696
+ includeShared = true,
9697
+ tenantIds
9698
+ } = options;
9699
+ const poolHealthResults = [];
9700
+ let sharedDbStatus = "ok";
9701
+ let sharedDbResponseTimeMs;
9702
+ let sharedDbError;
9703
+ const poolsToCheck = this.getPoolsToCheck(tenantIds);
9704
+ const poolChecks = poolsToCheck.map(async ({ schemaName, tenantId, entry }) => {
9705
+ return this.checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs);
9706
+ });
9707
+ poolHealthResults.push(...await Promise.all(poolChecks));
9708
+ const sharedPool = this.deps.getSharedPool();
9709
+ if (includeShared && sharedPool) {
9710
+ const sharedResult = await this.checkSharedDbHealth(sharedPool, ping, pingTimeoutMs);
9711
+ sharedDbStatus = sharedResult.status;
9712
+ sharedDbResponseTimeMs = sharedResult.responseTimeMs;
9713
+ sharedDbError = sharedResult.error;
9714
+ }
9715
+ const degradedPools = poolHealthResults.filter((p) => p.status === "degraded").length;
9716
+ const unhealthyPools = poolHealthResults.filter((p) => p.status === "unhealthy").length;
9717
+ const healthy = unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
9718
+ const result = {
9719
+ healthy,
9720
+ pools: poolHealthResults,
9721
+ sharedDb: sharedDbStatus,
9722
+ totalPools: poolHealthResults.length,
9723
+ degradedPools,
9724
+ unhealthyPools,
9725
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9726
+ durationMs: Date.now() - startTime
9727
+ };
9728
+ if (sharedDbResponseTimeMs !== void 0) {
9729
+ result.sharedDbResponseTimeMs = sharedDbResponseTimeMs;
9730
+ }
9731
+ if (sharedDbError !== void 0) {
9732
+ result.sharedDbError = sharedDbError;
9733
+ }
9734
+ return result;
9735
+ }
9736
+ /**
9737
+ * Get pools to check based on options
9738
+ */
9739
+ getPoolsToCheck(tenantIds) {
9740
+ const poolsToCheck = [];
9741
+ if (tenantIds && tenantIds.length > 0) {
9742
+ for (const tenantId of tenantIds) {
9743
+ const schemaName = this.deps.getSchemaName(tenantId);
9744
+ const entry = this.deps.getPoolEntry(schemaName);
9745
+ if (entry) {
9746
+ poolsToCheck.push({ schemaName, tenantId, entry });
9747
+ }
9748
+ }
9749
+ } else {
9750
+ for (const [schemaName, entry] of this.deps.getPoolEntries()) {
9751
+ const tenantId = this.deps.getTenantIdBySchema(schemaName) ?? schemaName;
9752
+ poolsToCheck.push({ schemaName, tenantId, entry });
9753
+ }
9754
+ }
9755
+ return poolsToCheck;
9756
+ }
9757
+ /**
9758
+ * Check health of a single tenant pool
9759
+ */
9760
+ async checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs) {
9761
+ const pool = entry.pool;
9762
+ const totalConnections = pool.totalCount;
9763
+ const idleConnections = pool.idleCount;
9764
+ const waitingRequests = pool.waitingCount;
9765
+ let status = "ok";
9766
+ let responseTimeMs;
9767
+ let error;
9768
+ if (waitingRequests > 0) {
9769
+ status = "degraded";
9770
+ }
9771
+ if (ping) {
9772
+ const pingResult = await this.executePingQuery(pool, pingTimeoutMs);
9773
+ responseTimeMs = pingResult.responseTimeMs;
9774
+ if (!pingResult.success) {
9775
+ status = "unhealthy";
9776
+ error = pingResult.error;
9777
+ } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
9778
+ if (status === "ok") {
9779
+ status = "degraded";
9780
+ }
9781
+ }
9782
+ }
9783
+ const result = {
9784
+ tenantId,
9785
+ schemaName,
9786
+ status,
9787
+ totalConnections,
9788
+ idleConnections,
9789
+ waitingRequests
9790
+ };
9791
+ if (responseTimeMs !== void 0) {
9792
+ result.responseTimeMs = responseTimeMs;
9793
+ }
9794
+ if (error !== void 0) {
9795
+ result.error = error;
9796
+ }
9797
+ return result;
9798
+ }
9799
+ /**
9800
+ * Check health of shared database
9801
+ */
9802
+ async checkSharedDbHealth(sharedPool, ping, pingTimeoutMs) {
9803
+ let status = "ok";
9804
+ let responseTimeMs;
9805
+ let error;
9806
+ const waitingRequests = sharedPool.waitingCount;
9807
+ if (waitingRequests > 0) {
9808
+ status = "degraded";
9809
+ }
9810
+ if (ping) {
9811
+ const pingResult = await this.executePingQuery(sharedPool, pingTimeoutMs);
9812
+ responseTimeMs = pingResult.responseTimeMs;
9813
+ if (!pingResult.success) {
9814
+ status = "unhealthy";
9815
+ error = pingResult.error;
9816
+ } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
9817
+ if (status === "ok") {
9818
+ status = "degraded";
9819
+ }
9820
+ }
9821
+ }
9822
+ const result = { status };
9823
+ if (responseTimeMs !== void 0) {
9824
+ result.responseTimeMs = responseTimeMs;
9825
+ }
9826
+ if (error !== void 0) {
9827
+ result.error = error;
9828
+ }
9829
+ return result;
9830
+ }
9831
+ /**
9832
+ * Execute a ping query with timeout
9833
+ */
9834
+ async executePingQuery(pool, timeoutMs) {
9835
+ const startTime = Date.now();
9450
9836
  try {
9451
- const result = await operation();
9837
+ const timeoutPromise = new Promise((_, reject) => {
9838
+ setTimeout(() => reject(new Error("Health check ping timeout")), timeoutMs);
9839
+ });
9840
+ const queryPromise = pool.query("SELECT 1");
9841
+ await Promise.race([queryPromise, timeoutPromise]);
9452
9842
  return {
9453
- result,
9454
- attempts: attempt + 1,
9455
- totalTimeMs: Date.now() - startTime
9843
+ success: true,
9844
+ responseTimeMs: Date.now() - startTime
9845
+ };
9846
+ } catch (err) {
9847
+ return {
9848
+ success: false,
9849
+ responseTimeMs: Date.now() - startTime,
9850
+ error: err.message
9456
9851
  };
9457
- } catch (error) {
9458
- lastError = error;
9459
- const isLastAttempt = attempt >= retryConfig.maxAttempts - 1;
9460
- if (isLastAttempt || !retryConfig.isRetryable(lastError)) {
9461
- throw lastError;
9462
- }
9463
- const delay = calculateDelay(attempt, retryConfig);
9464
- retryConfig.onRetry?.(attempt + 1, lastError, delay);
9465
- await sleep(delay);
9466
9852
  }
9467
9853
  }
9468
- throw lastError ?? new Error("Retry failed with no error");
9469
- }
9854
+ /**
9855
+ * Determine overall health status from pool health results
9856
+ */
9857
+ determineOverallHealth(pools, sharedDbStatus = "ok") {
9858
+ const unhealthyPools = pools.filter((p) => p.status === "unhealthy").length;
9859
+ return unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
9860
+ }
9861
+ };
9470
9862
 
9471
9863
  // src/pool.ts
9472
9864
  var PoolManager = class {
9473
9865
  constructor(config) {
9474
9866
  this.config = config;
9475
9867
  const maxPools = config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
9868
+ const poolTtlMs = config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
9476
9869
  this.debugLogger = createDebugLogger(config.debug);
9477
- const userRetry = config.connection.retry ?? {};
9478
- this.retryConfig = {
9479
- maxAttempts: userRetry.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
9480
- initialDelayMs: userRetry.initialDelayMs ?? DEFAULT_CONFIG.retry.initialDelayMs,
9481
- maxDelayMs: userRetry.maxDelayMs ?? DEFAULT_CONFIG.retry.maxDelayMs,
9482
- backoffMultiplier: userRetry.backoffMultiplier ?? DEFAULT_CONFIG.retry.backoffMultiplier,
9483
- jitter: userRetry.jitter ?? DEFAULT_CONFIG.retry.jitter,
9484
- isRetryable: userRetry.isRetryable ?? isRetryableError,
9485
- onRetry: userRetry.onRetry
9486
- };
9487
- this.pools = new LRUCache({
9488
- max: maxPools,
9489
- dispose: (entry, key) => {
9490
- this.disposePoolEntry(entry, key);
9491
- },
9492
- noDisposeOnSet: true
9870
+ this.retryHandler = new RetryHandler(config.connection.retry);
9871
+ this.poolCache = new PoolCache({
9872
+ maxPools,
9873
+ poolTtlMs,
9874
+ onDispose: (schemaName, entry) => {
9875
+ this.disposePoolEntry(entry, schemaName);
9876
+ }
9877
+ });
9878
+ this.healthChecker = new HealthChecker({
9879
+ getPoolEntries: () => this.poolCache.entries(),
9880
+ getTenantIdBySchema: (schemaName) => this.tenantIdBySchema.get(schemaName),
9881
+ getPoolEntry: (schemaName) => this.poolCache.get(schemaName),
9882
+ getSchemaName: (tenantId) => this.config.isolation.schemaNameTemplate(tenantId),
9883
+ getSharedPool: () => this.sharedPool
9493
9884
  });
9494
9885
  }
9495
- pools;
9886
+ poolCache;
9496
9887
  tenantIdBySchema = /* @__PURE__ */ new Map();
9497
9888
  pendingConnections = /* @__PURE__ */ new Map();
9498
9889
  sharedPool = null;
@@ -9501,22 +9892,23 @@ var PoolManager = class {
9501
9892
  cleanupInterval = null;
9502
9893
  disposed = false;
9503
9894
  debugLogger;
9504
- retryConfig;
9895
+ retryHandler;
9896
+ healthChecker;
9505
9897
  /**
9506
9898
  * Get or create a database connection for a tenant
9507
9899
  */
9508
9900
  getDb(tenantId) {
9509
9901
  this.ensureNotDisposed();
9510
9902
  const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
9511
- let entry = this.pools.get(schemaName);
9903
+ let entry = this.poolCache.get(schemaName);
9512
9904
  if (!entry) {
9513
9905
  entry = this.createPoolEntry(tenantId, schemaName);
9514
- this.pools.set(schemaName, entry);
9906
+ this.poolCache.set(schemaName, entry);
9515
9907
  this.tenantIdBySchema.set(schemaName, tenantId);
9516
9908
  this.debugLogger.logPoolCreated(tenantId, schemaName);
9517
9909
  void this.config.hooks?.onPoolCreated?.(tenantId);
9518
9910
  }
9519
- entry.lastAccess = Date.now();
9911
+ this.poolCache.touch(schemaName);
9520
9912
  return entry.db;
9521
9913
  }
9522
9914
  /**
@@ -9537,26 +9929,26 @@ var PoolManager = class {
9537
9929
  async getDbAsync(tenantId) {
9538
9930
  this.ensureNotDisposed();
9539
9931
  const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
9540
- let entry = this.pools.get(schemaName);
9932
+ let entry = this.poolCache.get(schemaName);
9541
9933
  if (entry) {
9542
- entry.lastAccess = Date.now();
9934
+ this.poolCache.touch(schemaName);
9543
9935
  return entry.db;
9544
9936
  }
9545
9937
  const pending = this.pendingConnections.get(schemaName);
9546
9938
  if (pending) {
9547
9939
  entry = await pending;
9548
- entry.lastAccess = Date.now();
9940
+ this.poolCache.touch(schemaName);
9549
9941
  return entry.db;
9550
9942
  }
9551
9943
  const connectionPromise = this.connectWithRetry(tenantId, schemaName);
9552
9944
  this.pendingConnections.set(schemaName, connectionPromise);
9553
9945
  try {
9554
9946
  entry = await connectionPromise;
9555
- this.pools.set(schemaName, entry);
9947
+ this.poolCache.set(schemaName, entry);
9556
9948
  this.tenantIdBySchema.set(schemaName, tenantId);
9557
9949
  this.debugLogger.logPoolCreated(tenantId, schemaName);
9558
9950
  void this.config.hooks?.onPoolCreated?.(tenantId);
9559
- entry.lastAccess = Date.now();
9951
+ this.poolCache.touch(schemaName);
9560
9952
  return entry.db;
9561
9953
  } finally {
9562
9954
  this.pendingConnections.delete(schemaName);
@@ -9566,8 +9958,9 @@ var PoolManager = class {
9566
9958
  * Connect to a tenant database with retry logic
9567
9959
  */
9568
9960
  async connectWithRetry(tenantId, schemaName) {
9569
- const maxAttempts = this.retryConfig.maxAttempts;
9570
- const result = await withRetry(
9961
+ const retryConfig = this.retryHandler.getConfig();
9962
+ const maxAttempts = retryConfig.maxAttempts;
9963
+ const result = await this.retryHandler.withRetry(
9571
9964
  async () => {
9572
9965
  const entry = this.createPoolEntry(tenantId, schemaName);
9573
9966
  try {
@@ -9582,10 +9975,9 @@ var PoolManager = class {
9582
9975
  }
9583
9976
  },
9584
9977
  {
9585
- ...this.retryConfig,
9586
9978
  onRetry: (attempt, error, delayMs) => {
9587
9979
  this.debugLogger.logConnectionRetry(tenantId, attempt, maxAttempts, error, delayMs);
9588
- this.retryConfig.onRetry?.(attempt, error, delayMs);
9980
+ retryConfig.onRetry?.(attempt, error, delayMs);
9589
9981
  }
9590
9982
  }
9591
9983
  );
@@ -9647,8 +10039,9 @@ var PoolManager = class {
9647
10039
  * Connect to shared database with retry logic
9648
10040
  */
9649
10041
  async connectSharedWithRetry() {
9650
- const maxAttempts = this.retryConfig.maxAttempts;
9651
- const result = await withRetry(
10042
+ const retryConfig = this.retryHandler.getConfig();
10043
+ const maxAttempts = retryConfig.maxAttempts;
10044
+ const result = await this.retryHandler.withRetry(
9652
10045
  async () => {
9653
10046
  const pool = new Pool({
9654
10047
  connectionString: this.config.connection.url,
@@ -9674,10 +10067,9 @@ var PoolManager = class {
9674
10067
  }
9675
10068
  },
9676
10069
  {
9677
- ...this.retryConfig,
9678
10070
  onRetry: (attempt, error, delayMs) => {
9679
10071
  this.debugLogger.logConnectionRetry("shared", attempt, maxAttempts, error, delayMs);
9680
- this.retryConfig.onRetry?.(attempt, error, delayMs);
10072
+ retryConfig.onRetry?.(attempt, error, delayMs);
9681
10073
  }
9682
10074
  }
9683
10075
  );
@@ -9695,13 +10087,13 @@ var PoolManager = class {
9695
10087
  */
9696
10088
  hasPool(tenantId) {
9697
10089
  const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
9698
- return this.pools.has(schemaName);
10090
+ return this.poolCache.has(schemaName);
9699
10091
  }
9700
10092
  /**
9701
10093
  * Get count of active pools
9702
10094
  */
9703
10095
  getPoolCount() {
9704
- return this.pools.size;
10096
+ return this.poolCache.size();
9705
10097
  }
9706
10098
  /**
9707
10099
  * Get all active tenant IDs
@@ -9713,7 +10105,7 @@ var PoolManager = class {
9713
10105
  * Get the retry configuration
9714
10106
  */
9715
10107
  getRetryConfig() {
9716
- return { ...this.retryConfig };
10108
+ return this.retryHandler.getConfig();
9717
10109
  }
9718
10110
  /**
9719
10111
  * Pre-warm pools for specified tenants to reduce cold start latency
@@ -9772,15 +10164,97 @@ var PoolManager = class {
9772
10164
  details: results
9773
10165
  };
9774
10166
  }
10167
+ /**
10168
+ * Get current metrics for all pools
10169
+ *
10170
+ * Collects metrics on demand with zero overhead when not called.
10171
+ * Returns raw data that can be formatted for any monitoring system.
10172
+ *
10173
+ * @example
10174
+ * ```typescript
10175
+ * const metrics = manager.getMetrics();
10176
+ * console.log(metrics.pools.total); // 15
10177
+ *
10178
+ * // Format for Prometheus
10179
+ * for (const pool of metrics.pools.tenants) {
10180
+ * gauge.labels(pool.tenantId).set(pool.connections.idle);
10181
+ * }
10182
+ * ```
10183
+ */
10184
+ getMetrics() {
10185
+ this.ensureNotDisposed();
10186
+ const maxPools = this.config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
10187
+ const tenantMetrics = [];
10188
+ for (const [schemaName, entry] of this.poolCache.entries()) {
10189
+ const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
10190
+ const pool = entry.pool;
10191
+ tenantMetrics.push({
10192
+ tenantId,
10193
+ schemaName,
10194
+ connections: {
10195
+ total: pool.totalCount,
10196
+ idle: pool.idleCount,
10197
+ waiting: pool.waitingCount
10198
+ },
10199
+ lastAccessedAt: new Date(entry.lastAccess).toISOString()
10200
+ });
10201
+ }
10202
+ return {
10203
+ pools: {
10204
+ total: tenantMetrics.length,
10205
+ maxPools,
10206
+ tenants: tenantMetrics
10207
+ },
10208
+ shared: {
10209
+ initialized: this.sharedPool !== null,
10210
+ connections: this.sharedPool ? {
10211
+ total: this.sharedPool.totalCount,
10212
+ idle: this.sharedPool.idleCount,
10213
+ waiting: this.sharedPool.waitingCount
10214
+ } : null
10215
+ },
10216
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10217
+ };
10218
+ }
10219
+ /**
10220
+ * Check health of all pools and connections
10221
+ *
10222
+ * Verifies the health of tenant pools and optionally the shared database.
10223
+ * Returns detailed status information for monitoring and load balancer integration.
10224
+ *
10225
+ * @example
10226
+ * ```typescript
10227
+ * // Basic health check
10228
+ * const health = await manager.healthCheck();
10229
+ * console.log(health.healthy); // true/false
10230
+ *
10231
+ * // Use with Express endpoint
10232
+ * app.get('/health', async (req, res) => {
10233
+ * const health = await manager.healthCheck();
10234
+ * res.status(health.healthy ? 200 : 503).json(health);
10235
+ * });
10236
+ *
10237
+ * // Check specific tenants only
10238
+ * const health = await manager.healthCheck({
10239
+ * tenantIds: ['tenant-1', 'tenant-2'],
10240
+ * ping: true,
10241
+ * pingTimeoutMs: 3000,
10242
+ * });
10243
+ * ```
10244
+ */
10245
+ async healthCheck(options = {}) {
10246
+ this.ensureNotDisposed();
10247
+ return this.healthChecker.checkHealth(options);
10248
+ }
9775
10249
  /**
9776
10250
  * Manually evict a tenant pool
9777
10251
  */
9778
10252
  async evictPool(tenantId, reason = "manual") {
9779
10253
  const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
9780
- const entry = this.pools.get(schemaName);
10254
+ const entry = this.poolCache.get(schemaName);
9781
10255
  if (entry) {
9782
10256
  this.debugLogger.logPoolEvicted(tenantId, schemaName, reason);
9783
- this.pools.delete(schemaName);
10257
+ this.poolCache.delete(schemaName);
9784
10258
  this.tenantIdBySchema.delete(schemaName);
9785
10259
  await this.closePool(entry.pool, tenantId);
9786
10260
  }
@@ -9790,10 +10264,9 @@ var PoolManager = class {
9790
10264
  */
9791
10265
  startCleanup() {
9792
10266
  if (this.cleanupInterval) return;
9793
- const poolTtlMs = this.config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
9794
10267
  const cleanupIntervalMs = DEFAULT_CONFIG.cleanupIntervalMs;
9795
10268
  this.cleanupInterval = setInterval(() => {
9796
- void this.cleanupIdlePools(poolTtlMs);
10269
+ void this.cleanupIdlePools();
9797
10270
  }, cleanupIntervalMs);
9798
10271
  this.cleanupInterval.unref();
9799
10272
  }
@@ -9814,11 +10287,11 @@ var PoolManager = class {
9814
10287
  this.disposed = true;
9815
10288
  this.stopCleanup();
9816
10289
  const closePromises = [];
9817
- for (const [schemaName, entry] of this.pools.entries()) {
10290
+ for (const [schemaName, entry] of this.poolCache.entries()) {
9818
10291
  const tenantId = this.tenantIdBySchema.get(schemaName);
9819
10292
  closePromises.push(this.closePool(entry.pool, tenantId ?? schemaName));
9820
10293
  }
9821
- this.pools.clear();
10294
+ await this.poolCache.clear();
9822
10295
  this.tenantIdBySchema.clear();
9823
10296
  if (this.sharedPool) {
9824
10297
  closePromises.push(this.closePool(this.sharedPool, "shared"));
@@ -9880,18 +10353,13 @@ var PoolManager = class {
9880
10353
  /**
9881
10354
  * Cleanup pools that have been idle for too long
9882
10355
  */
9883
- async cleanupIdlePools(poolTtlMs) {
9884
- const now = Date.now();
9885
- const toEvict = [];
9886
- for (const [schemaName, entry] of this.pools.entries()) {
9887
- if (now - entry.lastAccess > poolTtlMs) {
9888
- toEvict.push(schemaName);
9889
- }
9890
- }
9891
- for (const schemaName of toEvict) {
10356
+ async cleanupIdlePools() {
10357
+ const evictedSchemas = await this.poolCache.evictExpired();
10358
+ for (const schemaName of evictedSchemas) {
9892
10359
  const tenantId = this.tenantIdBySchema.get(schemaName);
9893
10360
  if (tenantId) {
9894
- await this.evictPool(tenantId, "ttl_expired");
10361
+ this.debugLogger.logPoolEvicted(tenantId, schemaName, "ttl_expired");
10362
+ this.tenantIdBySchema.delete(schemaName);
9895
10363
  }
9896
10364
  }
9897
10365
  }
@@ -9943,6 +10411,12 @@ function createTenantManager(config) {
9943
10411
  async warmup(tenantIds, options) {
9944
10412
  return poolManager.warmup(tenantIds, options);
9945
10413
  },
10414
+ async healthCheck(options) {
10415
+ return poolManager.healthCheck(options);
10416
+ },
10417
+ getMetrics() {
10418
+ return poolManager.getMetrics();
10419
+ },
9946
10420
  async dispose() {
9947
10421
  await poolManager.dispose();
9948
10422
  }