From 7a6806835cf264050caed24828e52cc58f6ecef2 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 3 Sep 2025 11:23:44 -0400 Subject: [PATCH] add docs --- .../resolvers/docker/container-status.job.ts | 3 + .../docker/docker-manifest.service.ts | 11 +++ .../resolvers/docker/docker-php.service.ts | 5 ++ packages/unraid-shared/src/util/processing.ts | 76 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/api/src/unraid-api/graph/resolvers/docker/container-status.job.ts b/api/src/unraid-api/graph/resolvers/docker/container-status.job.ts index dca009310..16fa04e49 100644 --- a/api/src/unraid-api/graph/resolvers/docker/container-status.job.ts +++ b/api/src/unraid-api/graph/resolvers/docker/container-status.job.ts @@ -15,6 +15,9 @@ export class ContainerStatusJob implements OnApplicationBootstrap { private readonly dockerConfigService: DockerConfigService ) {} + /** + * Initialize cron job for refreshing the update status for all containers on a user-configurable schedule. + */ onApplicationBootstrap() { const cronExpression = this.dockerConfigService.getConfig().updateCheckCronSchedule; const cronJob = CronJob.from({ diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts index 03a40430c..b14fe8606 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker-manifest.service.ts @@ -28,6 +28,12 @@ export class DockerManifestService { }); } + /** + * Checks if an update is available for a given container image. + * @param imageRef - The image reference to check, e.g. "unraid/baseimage:latest". If no tag is provided, "latest" is assumed, following the webgui's implementation. + * @param cacheData read from /var/lib/docker/unraid-update-status.json by default + * @returns True if an update is available, false if not, or null if the status is unknown + */ async isUpdateAvailableCached(imageRef: string, cacheData?: Record) { let taggedRef = imageRef; if (!taggedRef.includes(':')) taggedRef += ':latest'; @@ -38,6 +44,11 @@ export class DockerManifestService { return containerData.status?.toLowerCase() === 'true'; } + /** + * Checks if a container is rebuild ready. + * @param networkMode - The network mode of the container, e.g. "container:unraid/baseimage:latest". + * @returns True if the container is rebuild ready, false if not + */ async isRebuildReady(networkMode?: string) { if (!networkMode || !networkMode.startsWith('container:')) return false; const target = networkMode.slice('container:'.length); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts index 721074113..8bbcb317c 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts @@ -113,6 +113,11 @@ export class DockerPhpService { } } + /** + * Gets the update statuses for all containers by triggering `DockerTemplates->getAllInfo(true)` via DockerContainers.php + * @param dockerContainersPath - Path to the DockerContainers.php file + * @returns The update statuses for all containers + */ async getContainerUpdateStatuses( dockerContainersPath = '/usr/local/emhttp/plugins/dynamix.docker.manager/include/DockerContainers.php' ): Promise { diff --git a/packages/unraid-shared/src/util/processing.ts b/packages/unraid-shared/src/util/processing.ts index 1dbd796f4..5764bfc37 100644 --- a/packages/unraid-shared/src/util/processing.ts +++ b/packages/unraid-shared/src/util/processing.ts @@ -34,15 +34,91 @@ export function makeSafeRunner(onError: (error: unknown) => void) { type AsyncOperation = () => Promise; +/** + * A mutex for asynchronous operations that ensures only one operation runs at a time. + * + * When multiple callers attempt to execute operations simultaneously, they will all + * receive the same promise from the currently running operation, effectively deduplicating + * concurrent calls. This is useful for expensive operations like API calls, file operations, + * or database queries that should not be executed multiple times concurrently. + * + * @template T - The default return type for operations when using a default operation + * + * @example + * // Basic usage with explicit operations + * const mutex = new AsyncMutex(); + * + * // Multiple concurrent calls will deduplicate + * const [result1, result2, result3] = await Promise.all([ + * mutex.do(() => fetch('/api/data')), + * mutex.do(() => fetch('/api/data')), // Same request, will get same promise + * mutex.do(() => fetch('/api/data')) // Same request, will get same promise + * ]); + * // Only one fetch actually happens + * + * @example + * // Usage with a default operation + * const dataLoader = new AsyncMutex(() => + * fetch('/api/expensive-data').then(res => res.json()) + * ); + * + * // Multiple components can call this without duplication + * const data1 = await dataLoader.do(); // Executes the fetch + * const data2 = await dataLoader.do(); // Gets the same promise result + */ export class AsyncMutex { private currentOperation: Promise | null = null; private defaultOperation?: AsyncOperation; + /** + * Creates a new AsyncMutex instance. + * + * @param operation - Optional default operation to execute when calling `do()` without arguments. + * This is useful when you have a specific operation that should be deduplicated. + * + * @example + * // Without default operation + * const mutex = new AsyncMutex(); + * await mutex.do(() => someAsyncWork()); + * + * @example + * // With default operation + * const dataMutex = new AsyncMutex(() => loadExpensiveData()); + * await dataMutex.do(); // Executes loadExpensiveData() + */ constructor(operation?: AsyncOperation) { this.defaultOperation = operation; } + /** + * Executes the default operation if one was provided in the constructor. + * @returns Promise that resolves with the result of the default operation + * @throws Error if no default operation was set in the constructor + */ do(): Promise; + /** + * Executes the provided operation, ensuring only one runs at a time. + * + * If an operation is already running, all subsequent calls will receive + * the same promise from the currently running operation. This effectively + * deduplicates concurrent calls to the same expensive operation. + * + * @param operation - Optional operation to execute. If not provided, uses the default operation. + * @returns Promise that resolves with the result of the operation + * @throws Error if no operation is provided and no default operation was set + * + * @example + * const mutex = new AsyncMutex(); + * + * // These will all return the same promise + * const promise1 = mutex.do(() => fetch('/api/data')); + * const promise2 = mutex.do(() => fetch('/api/other')); // Still gets first promise! + * const promise3 = mutex.do(() => fetch('/api/another')); // Still gets first promise! + * + * // After the first operation completes, new operations can run + * await promise1; + * const newPromise = mutex.do(() => fetch('/api/new')); // This will execute + */ do(operation: AsyncOperation): Promise; do(operation?: AsyncOperation): Promise { if (!operation && !this.defaultOperation) {