diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index 48f6acc85e..082fe57db5 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -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} 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 c3fe051f12..7cc07f05cc 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() {