drizzle-multitenant 1.1.0 → 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
@@ -9793,7 +10185,7 @@ var PoolManager = class {
9793
10185
  this.ensureNotDisposed();
9794
10186
  const maxPools = this.config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
9795
10187
  const tenantMetrics = [];
9796
- for (const [schemaName, entry] of this.pools.entries()) {
10188
+ for (const [schemaName, entry] of this.poolCache.entries()) {
9797
10189
  const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
9798
10190
  const pool = entry.pool;
9799
10191
  tenantMetrics.push({
@@ -9852,156 +10244,17 @@ var PoolManager = class {
9852
10244
  */
9853
10245
  async healthCheck(options = {}) {
9854
10246
  this.ensureNotDisposed();
9855
- const startTime = Date.now();
9856
- const {
9857
- ping = true,
9858
- pingTimeoutMs = 5e3,
9859
- includeShared = true,
9860
- tenantIds
9861
- } = options;
9862
- const poolHealthResults = [];
9863
- let sharedDbStatus = "ok";
9864
- let sharedDbResponseTimeMs;
9865
- let sharedDbError;
9866
- const poolsToCheck = [];
9867
- if (tenantIds && tenantIds.length > 0) {
9868
- for (const tenantId of tenantIds) {
9869
- const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
9870
- const entry = this.pools.get(schemaName);
9871
- if (entry) {
9872
- poolsToCheck.push({ schemaName, tenantId, entry });
9873
- }
9874
- }
9875
- } else {
9876
- for (const [schemaName, entry] of this.pools.entries()) {
9877
- const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
9878
- poolsToCheck.push({ schemaName, tenantId, entry });
9879
- }
9880
- }
9881
- const poolChecks = poolsToCheck.map(async ({ schemaName, tenantId, entry }) => {
9882
- const poolHealth = await this.checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs);
9883
- return poolHealth;
9884
- });
9885
- poolHealthResults.push(...await Promise.all(poolChecks));
9886
- if (includeShared && this.sharedPool) {
9887
- const sharedResult = await this.checkSharedDbHealth(ping, pingTimeoutMs);
9888
- sharedDbStatus = sharedResult.status;
9889
- sharedDbResponseTimeMs = sharedResult.responseTimeMs;
9890
- sharedDbError = sharedResult.error;
9891
- }
9892
- const degradedPools = poolHealthResults.filter((p) => p.status === "degraded").length;
9893
- const unhealthyPools = poolHealthResults.filter((p) => p.status === "unhealthy").length;
9894
- const healthy = unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
9895
- return {
9896
- healthy,
9897
- pools: poolHealthResults,
9898
- sharedDb: sharedDbStatus,
9899
- sharedDbResponseTimeMs,
9900
- sharedDbError,
9901
- totalPools: poolHealthResults.length,
9902
- degradedPools,
9903
- unhealthyPools,
9904
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9905
- durationMs: Date.now() - startTime
9906
- };
9907
- }
9908
- /**
9909
- * Check health of a single tenant pool
9910
- */
9911
- async checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs) {
9912
- const pool = entry.pool;
9913
- const totalConnections = pool.totalCount;
9914
- const idleConnections = pool.idleCount;
9915
- const waitingRequests = pool.waitingCount;
9916
- let status = "ok";
9917
- let responseTimeMs;
9918
- let error;
9919
- if (waitingRequests > 0) {
9920
- status = "degraded";
9921
- }
9922
- if (ping) {
9923
- const pingResult = await this.executePingQuery(pool, pingTimeoutMs);
9924
- responseTimeMs = pingResult.responseTimeMs;
9925
- if (!pingResult.success) {
9926
- status = "unhealthy";
9927
- error = pingResult.error;
9928
- } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
9929
- if (status === "ok") {
9930
- status = "degraded";
9931
- }
9932
- }
9933
- }
9934
- return {
9935
- tenantId,
9936
- schemaName,
9937
- status,
9938
- totalConnections,
9939
- idleConnections,
9940
- waitingRequests,
9941
- responseTimeMs,
9942
- error
9943
- };
9944
- }
9945
- /**
9946
- * Check health of shared database
9947
- */
9948
- async checkSharedDbHealth(ping, pingTimeoutMs) {
9949
- if (!this.sharedPool) {
9950
- return { status: "ok" };
9951
- }
9952
- let status = "ok";
9953
- let responseTimeMs;
9954
- let error;
9955
- const waitingRequests = this.sharedPool.waitingCount;
9956
- if (waitingRequests > 0) {
9957
- status = "degraded";
9958
- }
9959
- if (ping) {
9960
- const pingResult = await this.executePingQuery(this.sharedPool, pingTimeoutMs);
9961
- responseTimeMs = pingResult.responseTimeMs;
9962
- if (!pingResult.success) {
9963
- status = "unhealthy";
9964
- error = pingResult.error;
9965
- } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
9966
- if (status === "ok") {
9967
- status = "degraded";
9968
- }
9969
- }
9970
- }
9971
- return { status, responseTimeMs, error };
9972
- }
9973
- /**
9974
- * Execute a ping query with timeout
9975
- */
9976
- async executePingQuery(pool, timeoutMs) {
9977
- const startTime = Date.now();
9978
- try {
9979
- const timeoutPromise = new Promise((_, reject) => {
9980
- setTimeout(() => reject(new Error("Health check ping timeout")), timeoutMs);
9981
- });
9982
- const queryPromise = pool.query("SELECT 1");
9983
- await Promise.race([queryPromise, timeoutPromise]);
9984
- return {
9985
- success: true,
9986
- responseTimeMs: Date.now() - startTime
9987
- };
9988
- } catch (err) {
9989
- return {
9990
- success: false,
9991
- responseTimeMs: Date.now() - startTime,
9992
- error: err.message
9993
- };
9994
- }
10247
+ return this.healthChecker.checkHealth(options);
9995
10248
  }
9996
10249
  /**
9997
10250
  * Manually evict a tenant pool
9998
10251
  */
9999
10252
  async evictPool(tenantId, reason = "manual") {
10000
10253
  const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
10001
- const entry = this.pools.get(schemaName);
10254
+ const entry = this.poolCache.get(schemaName);
10002
10255
  if (entry) {
10003
10256
  this.debugLogger.logPoolEvicted(tenantId, schemaName, reason);
10004
- this.pools.delete(schemaName);
10257
+ this.poolCache.delete(schemaName);
10005
10258
  this.tenantIdBySchema.delete(schemaName);
10006
10259
  await this.closePool(entry.pool, tenantId);
10007
10260
  }
@@ -10011,10 +10264,9 @@ var PoolManager = class {
10011
10264
  */
10012
10265
  startCleanup() {
10013
10266
  if (this.cleanupInterval) return;
10014
- const poolTtlMs = this.config.isolation.poolTtlMs ?? DEFAULT_CONFIG.poolTtlMs;
10015
10267
  const cleanupIntervalMs = DEFAULT_CONFIG.cleanupIntervalMs;
10016
10268
  this.cleanupInterval = setInterval(() => {
10017
- void this.cleanupIdlePools(poolTtlMs);
10269
+ void this.cleanupIdlePools();
10018
10270
  }, cleanupIntervalMs);
10019
10271
  this.cleanupInterval.unref();
10020
10272
  }
@@ -10035,11 +10287,11 @@ var PoolManager = class {
10035
10287
  this.disposed = true;
10036
10288
  this.stopCleanup();
10037
10289
  const closePromises = [];
10038
- for (const [schemaName, entry] of this.pools.entries()) {
10290
+ for (const [schemaName, entry] of this.poolCache.entries()) {
10039
10291
  const tenantId = this.tenantIdBySchema.get(schemaName);
10040
10292
  closePromises.push(this.closePool(entry.pool, tenantId ?? schemaName));
10041
10293
  }
10042
- this.pools.clear();
10294
+ await this.poolCache.clear();
10043
10295
  this.tenantIdBySchema.clear();
10044
10296
  if (this.sharedPool) {
10045
10297
  closePromises.push(this.closePool(this.sharedPool, "shared"));
@@ -10101,18 +10353,13 @@ var PoolManager = class {
10101
10353
  /**
10102
10354
  * Cleanup pools that have been idle for too long
10103
10355
  */
10104
- async cleanupIdlePools(poolTtlMs) {
10105
- const now = Date.now();
10106
- const toEvict = [];
10107
- for (const [schemaName, entry] of this.pools.entries()) {
10108
- if (now - entry.lastAccess > poolTtlMs) {
10109
- toEvict.push(schemaName);
10110
- }
10111
- }
10112
- for (const schemaName of toEvict) {
10356
+ async cleanupIdlePools() {
10357
+ const evictedSchemas = await this.poolCache.evictExpired();
10358
+ for (const schemaName of evictedSchemas) {
10113
10359
  const tenantId = this.tenantIdBySchema.get(schemaName);
10114
10360
  if (tenantId) {
10115
- await this.evictPool(tenantId, "ttl_expired");
10361
+ this.debugLogger.logPoolEvicted(tenantId, schemaName, "ttl_expired");
10362
+ this.tenantIdBySchema.delete(schemaName);
10116
10363
  }
10117
10364
  }
10118
10365
  }