This commit is contained in:
Pujit Mehrotra
2025-09-03 11:23:44 -04:00
parent d1c98495c9
commit 7a6806835c
4 changed files with 95 additions and 0 deletions

View File

@@ -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({

View File

@@ -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<string, CachedStatusEntry>) {
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);

View File

@@ -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<ExplicitStatusItem[]> {

View File

@@ -34,15 +34,91 @@ export function makeSafeRunner(onError: (error: unknown) => void) {
type AsyncOperation<T> = () => Promise<T>;
/**
* 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<T = unknown> {
private currentOperation: Promise<any> | null = null;
private defaultOperation?: AsyncOperation<T>;
/**
* 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<T>) {
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<T>;
/**
* 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<U>(operation: AsyncOperation<U>): Promise<U>;
do<U = T>(operation?: AsyncOperation<U>): Promise<U | T> {
if (!operation && !this.defaultOperation) {