/** * Fan-out helper used wherever PhotoPrism lacks a true batch endpoint. * Bounded concurrency keeps the indexer happy on slow hosts; each item's * result is collected and the aggregate `{updated, errors[]}` mirrors the * shape the legacy mule-image bulk endpoint returned, so existing toast * + undo plumbing slots in without changes. */ export interface BatchResult { updated: T[]; errors: { id: string; message: string }[]; } export interface BatchOptions { concurrency?: number; onProgress?: (done: number, total: number, completedId: string) => void; } export async function batchEdit( ids: string[], fn: (id: string) => Promise, opts: BatchOptions = {} ): Promise> { const concurrency = Math.max(1, opts.concurrency ?? 8); const updated: T[] = []; const errors: { id: string; message: string }[] = []; let i = 0; let done = 0; async function worker() { while (true) { const idx = i++; if (idx >= ids.length) return; const id = ids[idx]; try { updated.push(await fn(id)); } catch (err) { errors.push({ id, message: err instanceof Error ? err.message : String(err) }); } finally { done++; opts.onProgress?.(done, ids.length, id); } } } await Promise.all( Array.from({ length: Math.min(concurrency, ids.length) }, () => worker()) ); return { updated, errors }; }