Skip to content

feat: Add HTTP connection pooling for Lighthouse API calls#75

Merged
Patrick-Ehimen merged 2 commits intomainfrom
feat/connection-pooling
Feb 13, 2026
Merged

feat: Add HTTP connection pooling for Lighthouse API calls#75
Patrick-Ehimen merged 2 commits intomainfrom
feat/connection-pooling

Conversation

@Patrick-Ehimen
Copy link
Owner

@Patrick-Ehimen Patrick-Ehimen commented Feb 11, 2026

Summary

  • Integrate the existing (but unused) ConnectionPool into LighthouseAISDK so direct HTTP calls reuse pooled keep-alive connections instead of creating new ones per request
  • Add configurable pool settings via LighthouseConfig.pool and environment variables (LIGHTHOUSE_POOL_MAX_CONNECTIONS, LIGHTHOUSE_POOL_IDLE_TIMEOUT, LIGHTHOUSE_POOL_REQUEST_TIMEOUT, LIGHTHOUSE_POOL_KEEP_ALIVE)
  • Expose connection reuse metrics through getConnectionPoolStats() and the MCP server's getSDKMetrics()

Changes

File What changed
packages/sdk-wrapper/src/types.ts Added optional pool config to LighthouseConfig
packages/sdk-wrapper/src/LighthouseAISDK.ts Pool instantiation in constructor, executeHttpRequest helper, refactored uploadViDirectAPI to use pool, getConnectionPoolStats(), pool cleanup in destroy()
apps/mcp-server/src/services/LighthouseService.ts Accept pool config in constructor, expose pool stats in getSDKMetrics()
apps/mcp-server/src/config/server-config.ts ConnectionPoolServerConfig interface, DEFAULT_CONNECTION_POOL_CONFIG with env var support
packages/sdk-wrapper/src/__tests__/ConnectionPool.test.ts 22 new unit tests for pool lifecycle, queuing, events, stats
packages/sdk-wrapper/src/__tests__/LighthouseAISDK.test.ts 5 new integration tests for pool config and SDK integration

Test plan

  • pnpm run build — all 7 packages compile successfully
  • pnpm --filter @lighthouse-tooling/sdk-wrapper test — 32 new/updated tests pass
  • Verify pool defaults work without any config changes (backward compatible)
  • Verify pool: false disables pooling without breaking functionality
  • Verify env vars override defaults in MCP server context

Closes #54

Summary by Sourcery

Integrate HTTP connection pooling into the Lighthouse SDK and MCP server to reuse connections, expose pool metrics, and make pooling configurable.

New Features:

  • Add configurable connection pool support to LighthouseAISDK via the LighthouseConfig.pool option.
  • Expose connection pool statistics through LighthouseAISDK.getConnectionPoolStats() and surface them in the MCP server SDK metrics response.
  • Allow the MCP server to configure connection pooling via new connectionPool server config with environment-variable-based defaults.

Enhancements:

  • Refactor LighthouseAISDK HTTP upload logic to route direct API calls through a shared connection pool when enabled.
  • Ensure SDK destroy lifecycle also cleans up connection pool resources to avoid leaks.

Tests:

  • Add unit tests for the ConnectionPool covering lifecycle, configuration, queuing behavior, events, and statistics reporting.
  • Extend LighthouseAISDK tests to cover pool creation, disabling, custom configuration, timeout defaults, and cleanup behavior.

Wire the existing ConnectionPool into LighthouseAISDK so that direct
HTTP calls (e.g. the uploadViDirectAPI fallback) reuse pooled
connections with keep-alive instead of creating new ones per request.

- Add optional `pool` config to LighthouseConfig (default enabled,
  set false to disable)
- Create ConnectionPool in SDK constructor with configurable max
  connections, timeouts, and keep-alive
- Add executeHttpRequest helper that routes through pool or falls back
  to direct axios
- Expose pool metrics via getConnectionPoolStats()
- Add env var support (LIGHTHOUSE_POOL_MAX_CONNECTIONS, etc.) in
  MCP server config
- Add 32 new tests covering pool lifecycle, queuing, and SDK integration

Closes #54
@sourcery-ai
Copy link

sourcery-ai bot commented Feb 11, 2026

Reviewer's Guide

Integrates a configurable HTTP ConnectionPool into LighthouseAISDK and the MCP server to reuse keep-alive connections for direct Lighthouse API calls, exposes pool metrics, and wires configuration through SDK and server config with tests covering lifecycle, configuration, and metrics.

Sequence diagram for HTTP request execution with ConnectionPool in LighthouseAISDK

sequenceDiagram
  actor Client
  participant LighthouseService
  participant LighthouseAISDK
  participant ConnectionPool
  participant AxiosInstance
  participant AxiosStatic
  participant LighthouseAPI

  Client->>LighthouseService: call uploadViDirectAPI
  LighthouseService->>LighthouseAISDK: uploadViDirectAPI(fileBuffer, fileName, apiKey)
  activate LighthouseAISDK
  LighthouseAISDK->>LighthouseAISDK: executeHttpRequest(config)

  alt connectionPool enabled
    LighthouseAISDK->>ConnectionPool: acquire()
    ConnectionPool-->>LighthouseAISDK: AxiosInstance
    LighthouseAISDK->>AxiosInstance: request(config)
    AxiosInstance-->>LighthouseAISDK: AxiosResponse
    LighthouseAISDK->>ConnectionPool: release(AxiosInstance)
  else connectionPool disabled
    LighthouseAISDK->>AxiosStatic: request(config)
    AxiosStatic-->>LighthouseAISDK: AxiosResponse
  end

  LighthouseAISDK-->>LighthouseService: AxiosResponse
  deactivate LighthouseAISDK
  LighthouseService-->>Client: upload result
Loading

Class diagram for LighthouseAISDK, LighthouseService, and ConnectionPool configuration

classDiagram
  class LighthouseConfig {
    string apiKey
    string? baseUrl
    number? timeout
    number? maxRetries
    boolean? debug
    ConnectionPoolConfig or false pool
  }

  class ConnectionPoolConfig {
    number maxConnections
    number acquireTimeout
    number idleTimeout
    number requestTimeout
    boolean keepAlive
    number maxSockets
  }

  class ConnectionPool {
    +ConnectionPool(config ConnectionPoolConfig)
    +acquire() Promise~AxiosInstance~
    +release(instance AxiosInstance) void
    +getStats() ConnectionPoolStats
    +destroy() void
    +on(event string, handler Function) void
  }

  class ConnectionPoolStats {
    number totalConnections
    number activeConnections
    number idleConnections
    number queuedRequests
    number totalRequests
    number averageWaitTime
  }

  class LighthouseAISDK {
    -LighthouseAutoAuth auth
    -EncryptionManager encryption
    -RateLimiter rateLimiter
    -CircuitBreaker circuitBreaker
    -ConnectionPool or null connectionPool
    -LighthouseConfig config
    +LighthouseAISDK(config LighthouseConfig)
    -executeHttpRequest(config AxiosRequestConfig) Promise~AxiosResponse~
    +uploadViDirectAPI(fileBuffer Buffer, fileName string, apiKey string) Promise~AxiosResponse~
    +getConnectionPoolStats() ConnectionPoolStats or null
    +getCircuitBreakerStatus() any
    +getErrorMetrics() any
    +getActiveOperations() any
    +destroy() void
  }

  class ConnectionPoolServerConfig {
    number maxConnections
    number idleTimeoutMs
    number requestTimeoutMs
    boolean keepAlive
  }

  class ServerConfig {
    string name
    string version
    AuthConfig authentication
    PerformanceConfig performance
    MultiTenancyConfig multiTenancy
    ConnectionPoolServerConfig connectionPool
  }

  class LighthouseService {
    -LighthouseAISDK sdk
    -Logger logger
    -string or undefined dbPath
    -Map~string, StoredFile~ fileCache
    -Map~string, Dataset~ datasetCache
    +LighthouseService(apiKey string, logger Logger, dbPath string, poolConfig ConnectionPoolConfig)
    +getSDKMetrics() any
  }

  class getSDKMetrics

  LighthouseConfig --> ConnectionPoolConfig : uses
  LighthouseAISDK --> LighthouseConfig : has
  LighthouseAISDK --> ConnectionPool : optional_has
  ConnectionPool --> ConnectionPoolConfig : configured_by
  ConnectionPool --> ConnectionPoolStats : returns
  ServerConfig --> ConnectionPoolServerConfig : has
  LighthouseService --> LighthouseAISDK : composes
  LighthouseService --> ConnectionPoolConfig : passes_to_pool
  LighthouseService --> ServerConfig : configured_by
  LighthouseService --> LighthouseConfig : builds
  LighthouseService --> getSDKMetrics : returns_connectionPoolStats_via_SDK
  LighthouseAISDK --> ConnectionPoolStats : exposes_via_getConnectionPoolStats
  ConnectionPoolServerConfig --> ConnectionPoolConfig : maps_to_at_SDK_creation
Loading

File-Level Changes

Change Details Files
Integrate ConnectionPool into LighthouseAISDK and reuse it for direct HTTP uploads.
  • Add a nullable connectionPool field and initialize it in the constructor with sensible defaults that can be overridden via LighthouseConfig.pool, including a hard disable with pool: false.
  • Forward connection pool lifecycle events from ConnectionPool to LighthouseAISDK EventEmitter.
  • Introduce an executeHttpRequest helper that uses the pool when available and falls back to direct axios requests when disabled.
  • Refactor uploadViDirectAPI to use executeHttpRequest instead of constructing its own axios instance.
  • Add getConnectionPoolStats to surface pool metrics and ensure destroy() tears down the pool.
packages/sdk-wrapper/src/LighthouseAISDK.ts
Extend SDK configuration and MCP server wiring to support connection pool configuration and metrics exposure.
  • Extend LighthouseConfig with an optional pool field that accepts ConnectionPoolConfig or false.
  • Add ConnectionPoolServerConfig and DEFAULT_CONNECTION_POOL_CONFIG, sourcing values from LIGHTHOUSE_POOL_* environment variables, and include this in DEFAULT_SERVER_CONFIG and getDefaultServerConfig.
  • Update LighthouseService to accept an optional poolConfig parameter, pass it through to LighthouseAISDK, and expose connection pool stats via getSDKMetrics().
packages/sdk-wrapper/src/types.ts
apps/mcp-server/src/config/server-config.ts
apps/mcp-server/src/services/LighthouseService.ts
Add tests for connection pool behavior and SDK integration.
  • Create ConnectionPool.test.ts to cover pool lifecycle, queuing, events, and stats behavior.
  • Extend LighthouseAISDK.test.ts with tests for default pool creation, disabling the pool, custom pool configuration, using SDK timeout as the default requestTimeout, and pool cleanup on destroy.
packages/sdk-wrapper/src/__tests__/ConnectionPool.test.ts
packages/sdk-wrapper/src/__tests__/LighthouseAISDK.test.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#54 Implement HTTP connection pooling with keep-alive and configurable timeouts for Lighthouse API HTTP calls, and integrate it into the SDK wrapper and MCP server (including env-var-based configuration).
#54 Expose connection reuse / connection pool metrics through the metrics/SDK reporting path (e.g., MetricsCollector or equivalent).

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `apps/mcp-server/src/config/server-config.ts:10-14` </location>
<code_context>
 import * as path from "path";
 import * as os from "os";

+export interface ConnectionPoolServerConfig {
+  maxConnections: number;
+  idleTimeoutMs: number;
+  requestTimeoutMs: number;
+  keepAlive: boolean;
+}
+
</code_context>

<issue_to_address>
**issue (bug_risk):** ConnectionPoolServerConfig shape doesn’t align with ConnectionPoolConfig used by the SDK

This type uses `idleTimeoutMs` / `requestTimeoutMs`, while `LighthouseConfig.pool` is `ConnectionPoolConfig`, which (per `LighthouseAISDK` usage) expects `idleTimeout`, `requestTimeout`, `acquireTimeout`, and `maxSockets`. If `ServerConfig.connectionPool` is passed through to `LighthouseService`/SDK, your timeout values will be ignored and required fields may be missing. Please either align this interface with `ConnectionPoolConfig` or explicitly map it to a `ConnectionPoolConfig` before calling `LighthouseAISDK`.
</issue_to_address>

### Comment 2
<location> `packages/sdk-wrapper/src/LighthouseAISDK.ts:370-379` </location>
<code_context>
+   * Execute an HTTP request using the connection pool if available,
+   * otherwise fall back to a direct axios call.
+   */
+  private async executeHttpRequest<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
+    if (this.connectionPool) {
+      const instance = await this.connectionPool.acquire();
+      try {
+        return await instance.request<T>(config);
+      } finally {
+        this.connectionPool.release(instance);
+      }
+    } else {
+      const axiosLib: { request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>> } =
+        eval("require")("axios");
+      return axiosLib.request(config);
</code_context>

<issue_to_address>
**suggestion (performance):** Dynamic axios require in executeHttpRequest is repeated on every non-pooled call

In the non-pooled branch, `axios` is dynamically loaded via `eval("require")` on every call. Since `executeHttpRequest` is now on a hot path, this adds unnecessary overhead and can confuse bundlers. Consider caching the loaded `axios` instance (e.g., in a private field or module-level variable) so the dynamic require runs only once while preserving the indirection.

Suggested implementation:

```typescript
let cachedAxiosLib:
  | { request: (config: AxiosRequestConfig) => Promise<AxiosResponse<any>> }
  | null = null;

function getAxiosLib<T = any>(): {
  request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
} {
  if (!cachedAxiosLib) {
    // Use dynamic require to avoid bundler issues while caching the result
    cachedAxiosLib = eval("require")("axios");
  }
  return cachedAxiosLib as {
    request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
  };
}

/**
   * Execute an HTTP request using the connection pool if available,
   * otherwise fall back to a direct axios call.
   */
  private async executeHttpRequest<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {

```

```typescript
    } else {
      const axiosLib = getAxiosLib<T>();
      return axiosLib.request(config);
    }

```

None required, assuming `AxiosRequestConfig` and `AxiosResponse` are already imported in this file (they are referenced in the existing code). The helper and cache are module-level so they will be shared across all uses in this module without further changes.
</issue_to_address>

### Comment 3
<location> `packages/sdk-wrapper/src/__tests__/LighthouseAISDK.test.ts:236-245` </location>
<code_context>
+      customSdk.destroy();
+    });
+
+    it("should use SDK timeout as pool requestTimeout default", () => {
+      const customTimeoutSdk = new LighthouseAISDK({
+        apiKey: "test-key",
+        timeout: 60000,
+      });
+
+      // Pool should be created with requestTimeout matching SDK timeout
+      const stats = customTimeoutSdk.getConnectionPoolStats();
+      expect(stats).not.toBeNull();
+
+      customTimeoutSdk.destroy();
+    });
+  });
</code_context>

<issue_to_address>
**issue (testing):** This test does not actually verify that the pool requestTimeout matches the SDK timeout and may be misleading.

The assertions only check that `getConnectionPoolStats()` is non-null, which doesn’t verify that `requestTimeout` actually matches the SDK `timeout` as the test name implies. Either make the timeout observable (e.g., expose pool config for tests and assert the values match) or add a behavioral check that would fail if `requestTimeout` differed (e.g., fake timers and asserting when a request times out). If neither is feasible, consider renaming the test to reflect what it truly validates to avoid a false sense of coverage.
</issue_to_address>

### Comment 4
<location> `packages/sdk-wrapper/src/__tests__/LighthouseAISDK.test.ts:191` </location>
<code_context>
     });
   });

+  describe("connection pool", () => {
+    it("should create a connection pool by default", () => {
+      const stats = sdk.getConnectionPoolStats();
</code_context>

<issue_to_address>
**suggestion (testing):** Missing integration tests for event forwarding and pooled vs non-pooled HTTP paths.

The new `LighthouseAISDK` behavior forwards pool events (`pool:acquire`, `pool:create`, `pool:queue`, `pool:release`, `pool:cleanup`) and conditionally routes HTTP calls through `ConnectionPool` vs direct axios. The current tests only cover stats and config wiring, not these behaviors.

To strengthen coverage, add tests that:

1. Event forwarding: stub `ConnectionPool` to emit an `"acquire"` (and/or other) event and assert the SDK emits the corresponding `"pool:acquire"` event with the same payload.
2. Request routing: with `pool` enabled, spy on `ConnectionPool.acquire`/`release` and assert they are called from a method that uses `executeHttpRequest` (e.g., `uploadViDirectAPI`); with `pool: false`, mock `axios.request` and assert it is called while `ConnectionPool.acquire` is not.

This will validate the wiring of the SDK layer to the pool and the correct bypass behavior when pooling is disabled.

Suggested implementation:

```typescript
  describe("connection pool", () => {
    it("should create a connection pool by default", () => {
      const stats = sdk.getConnectionPoolStats();
      expect(stats).not.toBeNull();
      expect(stats).toEqual({
        totalConnections: 0,
        activeConnections: 0,
        idleConnections: 0,
        queuedRequests: 0,
        totalRequests: 0,
        averageWaitTime: 0,
      });
    });

    it("should forward pool events with the correct payload", async () => {
      // Arrange: listen for forwarded pool events on the SDK
      const poolAcquireListener = vi.fn();
      sdk.on("pool:acquire", poolAcquireListener);

      // Act: simulate the underlying pool emitting an event
      // NOTE: this assumes the SDK exposes the underlying pool instance via `getConnectionPool()`
      // or a similar API. Adjust the accessor as needed.
      const pool: any = (sdk as any).getConnectionPool
        ? (sdk as any).getConnectionPool()
        : (sdk as any).connectionPool;

      const payload = { requestId: "req-123", resource: "http" };
      pool.emit("acquire", payload);

      // Assert: SDK should forward the event with the same payload
      expect(poolAcquireListener).toHaveBeenCalledTimes(1);
      expect(poolAcquireListener).toHaveBeenCalledWith(payload);
    });

    it("should route HTTP calls through the connection pool when pooling is enabled", async () => {
      // Arrange: spy on pool acquire/release and a method that uses executeHttpRequest
      const pool: any = (sdk as any).getConnectionPool
        ? (sdk as any).getConnectionPool()
        : (sdk as any).connectionPool;

      const acquireSpy = vi.spyOn(pool, "acquire");
      const releaseSpy = vi.spyOn(pool, "release");

      // Act: call a method that uses executeHttpRequest internally
      // NOTE: adjust the method name/arguments to match the real SDK API.
      await sdk.uploadViDirectAPI({
        file: Buffer.from("test"),
        mimeType: "text/plain",
      });

      // Assert: request should have gone through the pool
      expect(acquireSpy).toHaveBeenCalled();
      expect(releaseSpy).toHaveBeenCalled();
    });

    it("should bypass the connection pool and call axios directly when pooling is disabled", async () => {
      // Arrange: create a non-pooled SDK instance and spy on axios
      // NOTE: adjust constructor/options shape to match the real SDK.
      const nonPooledSdk = new LighthouseAISDK({
        apiKey: "test-api-key",
        pool: false,
      });

      const axiosRequestSpy = vi.spyOn(axios, "request");
      const poolAcquireSpy = vi.spyOn(
        (nonPooledSdk as any).connectionPool || {},
        "acquire",
      ).mockImplementation?.(() => undefined as any);

      // Act: call a method that uses executeHttpRequest internally
      await nonPooledSdk.uploadViDirectAPI({
        file: Buffer.from("test"),
        mimeType: "text/plain",
      });

      // Assert: axios should be called directly and pool should not be used
      expect(axiosRequestSpy).toHaveBeenCalled();
      if (poolAcquireSpy) {
        expect(poolAcquireSpy).not.toHaveBeenCalled();
      }

      axiosRequestSpy.mockRestore();
      if (poolAcquireSpy) {
        poolAcquireSpy.mockRestore();
      }
    });

```

1. Ensure the test file imports `axios` and `LighthouseAISDK` (and `vi`/`describe`/`it` from your test runner) at the top, if not already present:
   - `import axios from "axios";`
   - `import { LighthouseAISSDK as LighthouseAISDK } from "../path/to/LighthouseAISDK";` (adjust path/name as needed)
2. The tests assume:
   - The SDK exposes the underlying pool via `sdk.getConnectionPool()` or a `connectionPool` property; if the actual accessor is different, update the references accordingly.
   - `uploadViDirectAPI` is a method that internally uses `executeHttpRequest`; if another method is more appropriate, replace it in all three new tests.
   - The SDK constructor accepts a `pool: false` option to disable pooling; adjust the configuration object to match the real API.
3. If `connectionPool` is not available on the non-pooled SDK instance, you may want to instead spy on the `ConnectionPool` class (e.g., `vi.spyOn(ConnectionPool.prototype, "acquire")`) to assert it is not called; update the test accordingly.
4. If the SDK uses a different event naming convention (e.g., `'pool:acquire'` vs `'pool.acquire'`) or forwards additional metadata, update the event names and expectations to match the implementation.
</issue_to_address>

### Comment 5
<location> `packages/sdk-wrapper/src/__tests__/LighthouseAISDK.test.ts:259-266` </location>
<code_context>
       expect(removeAllListenersSpy).toHaveBeenCalled();
     });
+
+    it("should cleanup connection pool on destroy", () => {
+      // After destroy, pool stats should still work (pool is destroyed but method handles it)
+      sdk.destroy();
+
+      // Creating a new SDK to verify pool was destroyed cleanly (no lingering timers)
+      const newSdk = new LighthouseAISDK(config);
+      expect(newSdk.getConnectionPoolStats()).not.toBeNull();
+      newSdk.destroy();
+    });
   });
</code_context>

<issue_to_address>
**suggestion (testing):** The destroy test could more directly assert that the connection pool is cleaned up (e.g. timers or destroy call) rather than just creating a new SDK.

Right now this only verifies that creating a new SDK after `sdk.destroy()` still works, which doesn’t directly show that the original pool’s resources were cleaned up.

To better exercise the new cleanup behavior, you could:
- Use Jest fake timers and assert there are no pending timers after `sdk.destroy()` if the pool uses timeouts/intervals, or
- Spy on `ConnectionPool.prototype.destroy` (or the pool instance) and assert it’s called exactly once when `sdk.destroy()` runs.

That would more directly validate the `connectionPool.destroy()` path and resource cleanup semantics.

Suggested implementation:

```typescript
  describe("destroy", () => {
    it("should cleanup resources", () => {
      const removeAllListenersSpy = jest.spyOn(sdk, "removeAllListeners");
      const connectionPoolDestroySpy = jest.spyOn(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (sdk as any).connectionPool,
        "destroy",
      );

      sdk.destroy();

      expect(removeAllListenersSpy).toHaveBeenCalled();
      expect(connectionPoolDestroySpy).toHaveBeenCalledTimes(1);
    });
  });

```

If `connectionPool` is not directly accessible on `sdk` (e.g. it’s truly private or named differently), you will need to:
1. Adjust `(sdk as any).connectionPool` to the actual property path used in `LighthouseAISDK` for the pool instance, or
2. Import and spy on the pool implementation’s prototype (e.g. `jest.spyOn(ConnectionPool.prototype, "destroy")`) if that’s the established pattern elsewhere in the tests.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +10 to +14
export interface ConnectionPoolServerConfig {
maxConnections: number;
idleTimeoutMs: number;
requestTimeoutMs: number;
keepAlive: boolean;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): ConnectionPoolServerConfig shape doesn’t align with ConnectionPoolConfig used by the SDK

This type uses idleTimeoutMs / requestTimeoutMs, while LighthouseConfig.pool is ConnectionPoolConfig, which (per LighthouseAISDK usage) expects idleTimeout, requestTimeout, acquireTimeout, and maxSockets. If ServerConfig.connectionPool is passed through to LighthouseService/SDK, your timeout values will be ignored and required fields may be missing. Please either align this interface with ConnectionPoolConfig or explicitly map it to a ConnectionPoolConfig before calling LighthouseAISDK.

Comment on lines +370 to +379
private async executeHttpRequest<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
if (this.connectionPool) {
const instance = await this.connectionPool.acquire();
try {
return await instance.request<T>(config);
} finally {
this.connectionPool.release(instance);
}
} else {
const axiosLib: { request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>> } =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Dynamic axios require in executeHttpRequest is repeated on every non-pooled call

In the non-pooled branch, axios is dynamically loaded via eval("require") on every call. Since executeHttpRequest is now on a hot path, this adds unnecessary overhead and can confuse bundlers. Consider caching the loaded axios instance (e.g., in a private field or module-level variable) so the dynamic require runs only once while preserving the indirection.

Suggested implementation:

let cachedAxiosLib:
  | { request: (config: AxiosRequestConfig) => Promise<AxiosResponse<any>> }
  | null = null;

function getAxiosLib<T = any>(): {
  request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
} {
  if (!cachedAxiosLib) {
    // Use dynamic require to avoid bundler issues while caching the result
    cachedAxiosLib = eval("require")("axios");
  }
  return cachedAxiosLib as {
    request: (config: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
  };
}

/**
   * Execute an HTTP request using the connection pool if available,
   * otherwise fall back to a direct axios call.
   */
  private async executeHttpRequest<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    } else {
      const axiosLib = getAxiosLib<T>();
      return axiosLib.request(config);
    }

None required, assuming AxiosRequestConfig and AxiosResponse are already imported in this file (they are referenced in the existing code). The helper and cache are module-level so they will be shared across all uses in this module without further changes.

Comment on lines +236 to +245
it("should use SDK timeout as pool requestTimeout default", () => {
const customTimeoutSdk = new LighthouseAISDK({
apiKey: "test-key",
timeout: 60000,
});

// Pool should be created with requestTimeout matching SDK timeout
const stats = customTimeoutSdk.getConnectionPoolStats();
expect(stats).not.toBeNull();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): This test does not actually verify that the pool requestTimeout matches the SDK timeout and may be misleading.

The assertions only check that getConnectionPoolStats() is non-null, which doesn’t verify that requestTimeout actually matches the SDK timeout as the test name implies. Either make the timeout observable (e.g., expose pool config for tests and assert the values match) or add a behavioral check that would fail if requestTimeout differed (e.g., fake timers and asserting when a request times out). If neither is feasible, consider renaming the test to reflect what it truly validates to avoid a false sense of coverage.

});
});

describe("connection pool", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing integration tests for event forwarding and pooled vs non-pooled HTTP paths.

The new LighthouseAISDK behavior forwards pool events (pool:acquire, pool:create, pool:queue, pool:release, pool:cleanup) and conditionally routes HTTP calls through ConnectionPool vs direct axios. The current tests only cover stats and config wiring, not these behaviors.

To strengthen coverage, add tests that:

  1. Event forwarding: stub ConnectionPool to emit an "acquire" (and/or other) event and assert the SDK emits the corresponding "pool:acquire" event with the same payload.
  2. Request routing: with pool enabled, spy on ConnectionPool.acquire/release and assert they are called from a method that uses executeHttpRequest (e.g., uploadViDirectAPI); with pool: false, mock axios.request and assert it is called while ConnectionPool.acquire is not.

This will validate the wiring of the SDK layer to the pool and the correct bypass behavior when pooling is disabled.

Suggested implementation:

  describe("connection pool", () => {
    it("should create a connection pool by default", () => {
      const stats = sdk.getConnectionPoolStats();
      expect(stats).not.toBeNull();
      expect(stats).toEqual({
        totalConnections: 0,
        activeConnections: 0,
        idleConnections: 0,
        queuedRequests: 0,
        totalRequests: 0,
        averageWaitTime: 0,
      });
    });

    it("should forward pool events with the correct payload", async () => {
      // Arrange: listen for forwarded pool events on the SDK
      const poolAcquireListener = vi.fn();
      sdk.on("pool:acquire", poolAcquireListener);

      // Act: simulate the underlying pool emitting an event
      // NOTE: this assumes the SDK exposes the underlying pool instance via `getConnectionPool()`
      // or a similar API. Adjust the accessor as needed.
      const pool: any = (sdk as any).getConnectionPool
        ? (sdk as any).getConnectionPool()
        : (sdk as any).connectionPool;

      const payload = { requestId: "req-123", resource: "http" };
      pool.emit("acquire", payload);

      // Assert: SDK should forward the event with the same payload
      expect(poolAcquireListener).toHaveBeenCalledTimes(1);
      expect(poolAcquireListener).toHaveBeenCalledWith(payload);
    });

    it("should route HTTP calls through the connection pool when pooling is enabled", async () => {
      // Arrange: spy on pool acquire/release and a method that uses executeHttpRequest
      const pool: any = (sdk as any).getConnectionPool
        ? (sdk as any).getConnectionPool()
        : (sdk as any).connectionPool;

      const acquireSpy = vi.spyOn(pool, "acquire");
      const releaseSpy = vi.spyOn(pool, "release");

      // Act: call a method that uses executeHttpRequest internally
      // NOTE: adjust the method name/arguments to match the real SDK API.
      await sdk.uploadViDirectAPI({
        file: Buffer.from("test"),
        mimeType: "text/plain",
      });

      // Assert: request should have gone through the pool
      expect(acquireSpy).toHaveBeenCalled();
      expect(releaseSpy).toHaveBeenCalled();
    });

    it("should bypass the connection pool and call axios directly when pooling is disabled", async () => {
      // Arrange: create a non-pooled SDK instance and spy on axios
      // NOTE: adjust constructor/options shape to match the real SDK.
      const nonPooledSdk = new LighthouseAISDK({
        apiKey: "test-api-key",
        pool: false,
      });

      const axiosRequestSpy = vi.spyOn(axios, "request");
      const poolAcquireSpy = vi.spyOn(
        (nonPooledSdk as any).connectionPool || {},
        "acquire",
      ).mockImplementation?.(() => undefined as any);

      // Act: call a method that uses executeHttpRequest internally
      await nonPooledSdk.uploadViDirectAPI({
        file: Buffer.from("test"),
        mimeType: "text/plain",
      });

      // Assert: axios should be called directly and pool should not be used
      expect(axiosRequestSpy).toHaveBeenCalled();
      if (poolAcquireSpy) {
        expect(poolAcquireSpy).not.toHaveBeenCalled();
      }

      axiosRequestSpy.mockRestore();
      if (poolAcquireSpy) {
        poolAcquireSpy.mockRestore();
      }
    });
  1. Ensure the test file imports axios and LighthouseAISDK (and vi/describe/it from your test runner) at the top, if not already present:
    • import axios from "axios";
    • import { LighthouseAISSDK as LighthouseAISDK } from "../path/to/LighthouseAISDK"; (adjust path/name as needed)
  2. The tests assume:
    • The SDK exposes the underlying pool via sdk.getConnectionPool() or a connectionPool property; if the actual accessor is different, update the references accordingly.
    • uploadViDirectAPI is a method that internally uses executeHttpRequest; if another method is more appropriate, replace it in all three new tests.
    • The SDK constructor accepts a pool: false option to disable pooling; adjust the configuration object to match the real API.
  3. If connectionPool is not available on the non-pooled SDK instance, you may want to instead spy on the ConnectionPool class (e.g., vi.spyOn(ConnectionPool.prototype, "acquire")) to assert it is not called; update the test accordingly.
  4. If the SDK uses a different event naming convention (e.g., 'pool:acquire' vs 'pool.acquire') or forwards additional metadata, update the event names and expectations to match the implementation.

Comment on lines +259 to +266
it("should cleanup connection pool on destroy", () => {
// After destroy, pool stats should still work (pool is destroyed but method handles it)
sdk.destroy();

// Creating a new SDK to verify pool was destroyed cleanly (no lingering timers)
const newSdk = new LighthouseAISDK(config);
expect(newSdk.getConnectionPoolStats()).not.toBeNull();
newSdk.destroy();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): The destroy test could more directly assert that the connection pool is cleaned up (e.g. timers or destroy call) rather than just creating a new SDK.

Right now this only verifies that creating a new SDK after sdk.destroy() still works, which doesn’t directly show that the original pool’s resources were cleaned up.

To better exercise the new cleanup behavior, you could:

  • Use Jest fake timers and assert there are no pending timers after sdk.destroy() if the pool uses timeouts/intervals, or
  • Spy on ConnectionPool.prototype.destroy (or the pool instance) and assert it’s called exactly once when sdk.destroy() runs.

That would more directly validate the connectionPool.destroy() path and resource cleanup semantics.

Suggested implementation:

  describe("destroy", () => {
    it("should cleanup resources", () => {
      const removeAllListenersSpy = jest.spyOn(sdk, "removeAllListeners");
      const connectionPoolDestroySpy = jest.spyOn(
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        (sdk as any).connectionPool,
        "destroy",
      );

      sdk.destroy();

      expect(removeAllListenersSpy).toHaveBeenCalled();
      expect(connectionPoolDestroySpy).toHaveBeenCalledTimes(1);
    });
  });

If connectionPool is not directly accessible on sdk (e.g. it’s truly private or named differently), you will need to:

  1. Adjust (sdk as any).connectionPool to the actual property path used in LighthouseAISDK for the pool instance, or
  2. Import and spy on the pool implementation’s prototype (e.g. jest.spyOn(ConnectionPool.prototype, "destroy")) if that’s the established pattern elsewhere in the tests.

Resolve conflicts to keep both connection pooling (PR #75) and
batch operations + memory management from main.
@Patrick-Ehimen Patrick-Ehimen merged commit e37cee9 into main Feb 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add HTTP connection pooling for Lighthouse API calls

1 participant