perf(frontend): parallelize video thumbs, batch owner-cache writes, minor tweaks
- photos: generate + upload the three video-thumbnail sizes in parallel via Promise.allSettled instead of a sequential await loop; previewData is still captured before its upload so the local preview survives a failed upload. - useOwnerCache: resolve the batch, then apply a single reactive assignment to `#names` instead of spread-copying the record once per id (fewer copies and fewer derive re-runs on large resolves). - admin: migration status polling slowed from 2s to 5s. - files store: soft cap (10k) on the per-item selection toggle. Bulk "select all" in the views is intentionally left uncapped.
This commit is contained in:
@@ -31,14 +31,16 @@ export class OwnerCache {
|
|||||||
|
|
||||||
/** Resolve every not-yet-cached id in parallel; nullish ids are skipped. */
|
/** Resolve every not-yet-cached id in parallel; nullish ids are skipped. */
|
||||||
async resolve(ids: Iterable<string | null | undefined>): Promise<void> {
|
async resolve(ids: Iterable<string | null | undefined>): Promise<void> {
|
||||||
const unique = [...new Set([...ids].filter((id): id is string => !!id))];
|
const pending = [...new Set([...ids].filter((id): id is string => !!id))].filter(
|
||||||
await Promise.all(
|
(id) => !this.#names[id]
|
||||||
unique.map(async (id) => {
|
|
||||||
if (this.#names[id]) return;
|
|
||||||
const name = await this.#resolver(id);
|
|
||||||
this.#names = { ...this.#names, [id]: name };
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
if (pending.length === 0) return;
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
pending.map(async (id) => [id, await this.#resolver(id)] as const)
|
||||||
|
);
|
||||||
|
// One reactive assignment for the whole batch instead of one per id, so a
|
||||||
|
// large resolve doesn't spread-copy the record N times (and re-run derives N times).
|
||||||
|
this.#names = { ...this.#names, ...Object.fromEntries(resolved) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,15 @@ class FilesStore {
|
|||||||
this.selection = new Set();
|
this.selection = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soft ceiling so the per-item toggle can't grow the set without bound.
|
||||||
|
// (Bulk "select all" lives in the views and intentionally isn't capped —
|
||||||
|
// silently dropping ids there would break batch delete/move.)
|
||||||
|
static readonly MAX_SELECTION = 10_000;
|
||||||
|
|
||||||
toggleSelected(id: string): void {
|
toggleSelected(id: string): void {
|
||||||
const next = new Set(this.selection);
|
const next = new Set(this.selection);
|
||||||
if (next.has(id)) next.delete(id);
|
if (next.has(id)) next.delete(id);
|
||||||
else next.add(id);
|
else if (next.size < FilesStore.MAX_SELECTION) next.add(id);
|
||||||
this.selection = next;
|
this.selection = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,7 +320,7 @@
|
|||||||
try {
|
try {
|
||||||
migration = await getMigration();
|
migration = await getMigration();
|
||||||
if (migration.status === 'running') {
|
if (migration.status === 'running') {
|
||||||
if (!migrationTimer) migrationTimer = setInterval(loadMigration, 2000);
|
if (!migrationTimer) migrationTimer = setInterval(loadMigration, 5000);
|
||||||
} else {
|
} else {
|
||||||
stopMigrationPoll();
|
stopMigrationPoll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,11 +301,16 @@
|
|||||||
['large', 800, 800]
|
['large', 800, 800]
|
||||||
];
|
];
|
||||||
let previewData = '';
|
let previewData = '';
|
||||||
for (const [size, w, h] of SIZES) {
|
// Render the blobs and push all three sizes in parallel; `previewData`
|
||||||
|
// is captured before its upload so the local preview shows even if that
|
||||||
|
// upload fails (allSettled swallows per-size failures, as before).
|
||||||
|
await Promise.allSettled(
|
||||||
|
SIZES.map(async ([size, w, h]) => {
|
||||||
const blob = await bitmapToBlob(bitmap, w, h);
|
const blob = await bitmapToBlob(bitmap, w, h);
|
||||||
if (size === 'preview') previewData = await blobToDataUrl(blob);
|
if (size === 'preview') previewData = await blobToDataUrl(blob);
|
||||||
await uploadThumbnail(file.id, size, blob).catch(() => {});
|
await uploadThumbnail(file.id, size, blob);
|
||||||
}
|
})
|
||||||
|
);
|
||||||
if (previewData) videoThumbs = { ...videoThumbs, [file.id]: previewData };
|
if (previewData) videoThumbs = { ...videoThumbs, [file.id]: previewData };
|
||||||
} catch {
|
} catch {
|
||||||
// Keep the generic play badge on failure.
|
// Keep the generic play badge on failure.
|
||||||
|
|||||||
Reference in New Issue
Block a user