feat(FN-4133): lazy-load dockerode in DockerClientService

Lazy-loads the `dockerode` module in `DockerClientService` so the heavy dependency is only imported when Docker functionality is actually needed, improving startup time for non-Docker use cases. Tests were added to verify the lazy-loading behavior.

Fusion-Task-Id: FN-4133
This commit is contained in:
Fusion
2026-05-12 18:29:48 -07:00
committed by gsxdsm
parent 8b037e4c7e
commit 0f5a5b5d09
3 changed files with 113 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import Docker from "dockerode";
import { exec } from "node:child_process";
import type Docker from "dockerode";
import { readFile } from "node:fs/promises";
import { promisify } from "node:util";
import type {
@@ -15,6 +15,11 @@ const EXEC_OPTIONS = {
maxBuffer: 5 * 1024 * 1024,
} as const;
type DockerodeModule = typeof import("dockerode");
type DockerConstructor = DockerodeModule extends { default: infer T } ? T : DockerodeModule;
let dockerodeConstructorPromise: Promise<DockerConstructor> | null = null;
function isLocalDaemonHost(host?: string): boolean {
return !host || host.trim() === "" || host === "unix:///var/run/docker.sock";
}
@@ -24,6 +29,42 @@ function toErrorMessage(error: unknown): string {
return String(error);
}
function isDockerodeMissingModuleError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const code = "code" in error ? (error as Error & { code?: string }).code : undefined;
return (
code === "ERR_MODULE_NOT_FOUND" ||
code === "MODULE_NOT_FOUND" ||
/cannot find (package|module) ['"]dockerode['"]/i.test(error.message)
);
}
async function loadDockerode(): Promise<DockerConstructor> {
if (!dockerodeConstructorPromise) {
dockerodeConstructorPromise = import("dockerode")
.then((mod) => ("default" in mod && mod.default ? mod.default : mod) as DockerConstructor)
.catch((error: unknown) => {
// Clear the cache on any failure so a later retry can succeed after environment changes.
dockerodeConstructorPromise = null;
if (isDockerodeMissingModuleError(error)) {
throw new Error("Docker support requires the optional 'dockerode' package. Install it with: npm install dockerode", {
cause: error,
});
}
throw new Error(`Failed to load 'dockerode': ${toErrorMessage(error)}`, {
cause: error,
});
});
}
return dockerodeConstructorPromise;
}
interface DockerContextCliEntry {
Name?: string;
Description?: string;
@@ -53,6 +94,7 @@ export class DockerClientService {
const parsed = JSON.parse(stdout) as Array<{ Endpoints?: { docker?: { Host?: string } } }>;
const dockerHost = parsed[0]?.Endpoints?.docker?.Host;
if (!dockerHost) throw new Error(`Docker context "${contextName}" does not define a Docker endpoint host`);
const Docker = await loadDockerode();
return new Docker({ host: dockerHost });
}
@@ -72,9 +114,11 @@ export class DockerClientService {
if (hostConfig.tlsKeyPath) options.key = await readFile(hostConfig.tlsKeyPath);
if (hostConfig.tlsVerify === false) options.rejectUnauthorized = false;
const Docker = await loadDockerode();
return new Docker(options);
}
const Docker = await loadDockerode();
return new Docker();
}