feat: cache asset info for prev/next navigation

This commit is contained in:
midzelis
2025-12-08 11:36:17 +00:00
parent 4836963196
commit fe29d74a13
3 changed files with 78 additions and 10 deletions

View File

@@ -2,6 +2,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte';
import { AssetAction } from '$lib/constants';
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
@@ -11,8 +12,8 @@
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
import { navigate } from '$lib/utils/navigation';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk';
import { onMount, untrack } from 'svelte';
import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto } from '@immich/sdk';
import { onDestroy, onMount, untrack } from 'svelte';
let { asset: viewingAsset, gridScrollTarget } = assetViewingStore;
@@ -46,7 +47,7 @@
const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset);
if (earlierTimelineAsset) {
const asset = await getAssetInfo({ ...authManager.params, id: earlierTimelineAsset.id });
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id });
if (preload) {
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
void getNextAsset(asset, false);
@@ -59,7 +60,7 @@
const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset);
if (laterTimelineAsset) {
const asset = await getAssetInfo({ ...authManager.params, id: laterTimelineAsset.id });
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id });
if (preload) {
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
void getPreviousAsset(asset, false);
@@ -194,6 +195,9 @@
}
}
};
onDestroy(() => {
assetCacheManager.invalidate();
});
const onAssetUpdate = ({ asset }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => {
if (asset.id === assetCursor.current.id) {
void loadCloseAssets(asset);
@@ -220,7 +224,10 @@
{album}
{person}
preAction={handlePreAction}
onAction={handleAction}
onAction={(action) => {
handleAction(action);
assetCacheManager.invalidate();
}}
onPrevious={() => handleNavigateToAsset(assetCursor.previousAsset)}
onNext={() => handleNavigateToAsset(assetCursor.nextAsset)}
onRandom={handleRandom}

View File

@@ -0,0 +1,60 @@
import { getAssetInfo, getAssetOcr, type AssetOcrResponseDto, type AssetResponseDto } from '@immich/sdk';
const defaultSerializer = <K>(params: K) => JSON.stringify(params);
class AsyncCache<V> {
#cache = new Map<string, V>();
async getOrFetch<K>(
params: K,
fetcher: (params: K) => Promise<V>,
keySerializer: (params: K) => string = defaultSerializer,
updateCache: boolean,
): Promise<V> {
const cacheKey = keySerializer(params);
const cached = this.#cache.get(cacheKey);
if (cached) {
return cached;
}
const value = await fetcher(params);
if (value && updateCache) {
this.#cache.set(cacheKey, value);
}
return value;
}
clear() {
this.#cache.clear();
}
}
class AssetCacheManager {
#assetCache = new AsyncCache<AssetResponseDto>();
#ocrCache = new AsyncCache<AssetOcrResponseDto[]>();
async getAsset(assetIdentifier: { key?: string; slug?: string; id: string }, updateCache = true) {
return this.#assetCache.getOrFetch(assetIdentifier, getAssetInfo, defaultSerializer, updateCache);
}
async getAssetOcr(id: string) {
return this.#ocrCache.getOrFetch({ id }, getAssetOcr, (params) => params.id, true);
}
clearAssetCache() {
this.#assetCache.clear();
}
clearOcrCache() {
this.#ocrCache.clear();
}
invalidate() {
this.clearAssetCache();
this.clearOcrCache();
}
}
export const assetCacheManager = new AssetCacheManager();

View File

@@ -1,8 +1,8 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { RouteId } from '$app/types';
import { AppRoute } from '$lib/constants';
import { getAssetInfo } from '@immich/sdk';
import type { NavigationTarget } from '@sveltejs/kit';
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
import { get } from 'svelte/store';
export type AssetGridRouteSearchParams = {
@@ -20,11 +20,12 @@ export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(u
export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]');
export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWith('/(user)/locked');
export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
!!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {}));
export const isAssetViewerRoute = (
target?: { route?: { id?: RouteId | null }; params?: Record<string, string> | null } | null,
) => !!(target?.route?.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {}));
export function getAssetInfoFromParam({ assetId, slug, key }: { assetId?: string; key?: string; slug?: string }) {
return assetId ? getAssetInfo({ id: assetId, slug, key }) : undefined;
return assetId ? assetCacheManager.getAsset({ id: assetId, slug, key }, false) : undefined;
}
function currentUrlWithoutAsset() {