diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index 4d88700bf5..8500345df4 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -2,7 +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'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; @@ -13,7 +13,7 @@ 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 { onDestroy, onMount, untrack } from 'svelte'; let { asset: viewingAsset, gridScrollTarget } = assetViewingStore; @@ -196,6 +196,9 @@ await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); } }; + onDestroy(() => { + assetCacheManager.invalidate(); + }); const onAssetUpdate = ({ asset }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => { if (asset.id === assetCursor.current.id) { void loadCloseAssets(asset); @@ -222,7 +225,10 @@ {album} {person} preAction={handlePreAction} - onAction={handleAction} + onAction={(action) => { + handleAction(action); + assetCacheManager.invalidate(); + }} onUndoDelete={handleUndoDelete} onPrevious={() => handleNavigateToAsset(assetCursor.previousAsset)} onNext={() => handleNavigateToAsset(assetCursor.nextAsset)} diff --git a/web/src/lib/managers/AssetCacheManager.svelte.ts b/web/src/lib/managers/AssetCacheManager.svelte.ts new file mode 100644 index 0000000000..0b5e697683 --- /dev/null +++ b/web/src/lib/managers/AssetCacheManager.svelte.ts @@ -0,0 +1,60 @@ +import { getAssetInfo, getAssetOcr, type AssetOcrResponseDto, type AssetResponseDto } from '@immich/sdk'; + +const defaultSerializer = (params: K) => JSON.stringify(params); + +class AsyncCache { + #cache = new Map(); + + async getOrFetch( + params: K, + fetcher: (params: K) => Promise, + keySerializer: (params: K) => string = defaultSerializer, + updateCache: boolean, + ): Promise { + 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(); + #ocrCache = new AsyncCache(); + + 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(); diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index daf1d04ed5..b6c0cad616 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -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 | 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() {