import type Docker from "dockerode";
import type { HostConfig } from "../../../types.js";
import type { IDockerClientFactory } from "./client-factory.js";
/**
* Manages Docker client instances with caching.
* Maintains a cache of Docker clients keyed by host configuration to avoid
* creating multiple connections to the same Docker daemon.
*/
export class ClientManager {
private clientCache = new Map<string, Docker>();
private pendingClients = new Map<string, Promise<Docker>>();
constructor(private clientFactory: IDockerClientFactory) {}
/**
* Get or create Docker client for a host configuration.
* Clients are cached based on name and host to reuse connections.
* Concurrent calls for the same host are deduplicated via a pending-promise map
* to prevent race conditions where multiple clients would be created.
*
* @param config - Host configuration for the Docker daemon
* @returns Docker client instance (cached or newly created)
*/
async getClient(config: HostConfig): Promise<Docker> {
const cacheKey = `${config.name}-${config.host}`;
const cached = this.clientCache.get(cacheKey);
if (cached) {
return cached;
}
// Check if another caller is already creating a client for this key
const pending = this.pendingClients.get(cacheKey);
if (pending) {
return pending;
}
// Create client with deduplication — store the promise so concurrent
// callers await the same creation instead of spawning duplicates
const clientPromise = this.clientFactory
.createClient(config)
.then((client) => {
this.clientCache.set(cacheKey, client);
this.pendingClients.delete(cacheKey);
return client;
})
.catch((error) => {
this.pendingClients.delete(cacheKey);
throw error;
});
this.pendingClients.set(cacheKey, clientPromise);
return clientPromise;
}
/**
* Clears all cached Docker clients.
*
* Call this during application shutdown or when you need to force new connections
* to all Docker hosts. The cached client instances will be removed, and any
* underlying HTTP/socket connections will be cleaned up by garbage collection
* when the client objects are no longer referenced.
*
* Note: Dockerode clients do not have an explicit close() method. The HTTP agent
* connections are automatically managed and will be released by the Node.js runtime
* when the client objects are garbage collected.
*
* @example
* ```typescript
* // Force fresh connections on next access
* clientManager.clearClients();
*
* // Or during shutdown
* process.on('SIGTERM', () => {
* clientManager.clearClients();
* process.exit(0);
* });
* ```
*/
clearClients(): void {
this.clientCache.clear();
this.pendingClients.clear();
}
/**
* Clears a specific cached Docker client by host name.
*
* Removes all cache entries where the cache key starts with the given host name.
* This allows granular cache invalidation for a specific host without affecting
* other cached connections.
*
* @param hostName - The name of the host whose cached clients should be removed
*
* @example
* ```typescript
* // Clear only the prod-server cache
* clientManager.clearClient('prod-server');
*
* // Next access to prod-server will create new connection
* const client = clientManager.getClient({ name: 'prod-server', host: '10.0.0.1' });
* ```
*/
clearClient(hostName: string): void {
for (const key of this.clientCache.keys()) {
if (key.startsWith(`${hostName}-`)) {
this.clientCache.delete(key);
}
}
}
/**
* Gets statistics about cached Docker clients.
*
* Provides observability into the cache state, showing total number of
* cached clients and the list of all cache keys.
*
* @returns Object with totalClients count and hosts array of cache keys
*
* @example
* ```typescript
* const stats = clientManager.getStats();
* console.log(`Cached clients: ${stats.totalClients}`);
* console.log(`Hosts: ${stats.hosts.join(', ')}`);
* ```
*/
getStats(): { totalClients: number; hosts: string[] } {
return {
totalClients: this.clientCache.size,
hosts: Array.from(this.clientCache.keys()),
};
}
}