diff --git a/js/src/features/particles/stream-view.tsx b/js/src/features/particles/stream-view.tsx index 7a7e08b..32a4950 100644 --- a/js/src/features/particles/stream-view.tsx +++ b/js/src/features/particles/stream-view.tsx @@ -18,6 +18,7 @@ import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbP import { WindowControls } from "@/components/window-controls"; import { formatDistanceToNow } from "@/lib/time-utils"; import { useStreamPlayback } from "@/hooks/use-stream-playback"; +import { usePrefetchAdjacentMedia } from "@/hooks/use-prefetch-adjacent-media"; function getParticleDisplayName(particle: Particle): string { switch (particle.type) { @@ -113,6 +114,8 @@ export function StreamView({ path, streamParticle }: StreamViewProps) { resume, } = useStreamPlayback(streamParticle, path); + usePrefetchAdjacentMedia(children, currentIndex); + const [composeActive, setComposeActive] = useState(false); const [progress, setProgress] = useState(0); diff --git a/js/src/hooks/use-download-url.ts b/js/src/hooks/use-download-url.ts index ab6c226..80709c3 100644 --- a/js/src/hooks/use-download-url.ts +++ b/js/src/hooks/use-download-url.ts @@ -6,5 +6,6 @@ export function useDownloadUrl(objectId?: string) { queryKey: ["download-url", objectId], queryFn: () => apiClient.getParticleDownloadUrl(objectId!), enabled: !!objectId, + staleTime: 1000 * 60 * 60, // 1 hour — signed URLs valid for 24h }); } diff --git a/js/src/hooks/use-prefetch-adjacent-media.ts b/js/src/hooks/use-prefetch-adjacent-media.ts new file mode 100644 index 0000000..71c2001 --- /dev/null +++ b/js/src/hooks/use-prefetch-adjacent-media.ts @@ -0,0 +1,47 @@ +import { useEffect } from "react"; +import { preload } from "react-dom"; +import { useQueryClient } from "@tanstack/react-query"; +import type { Particle } from "@/api/types"; +import { apiClient } from "@/api/client"; + +// TODO: verify that caching is actually working by slowing down our network to simulate +/** + * Prefetches download URLs and warms the browser cache for adjacent media particles. + */ +export function usePrefetchAdjacentMedia( + children: Particle[], + currentIndex: number, +) { + const queryClient = useQueryClient(); + + useEffect(() => { + const adjacentIndices = [currentIndex - 1, currentIndex + 1]; + const mediaParticles = adjacentIndices + .filter((i) => i >= 0 && i < children.length) + .map((i) => children[i]) + .filter( + (p): p is Extract => p.type === "media", + ); + + for (const particle of mediaParticles) { + const objectId = particle.properties.object_id; + const isAudio = particle.properties.mime_type?.startsWith("audio/"); + + queryClient + .prefetchQuery({ + queryKey: ["download-url", objectId], + queryFn: () => apiClient.getParticleDownloadUrl(objectId), + staleTime: 1000 * 60 * 60, + }) + .then(() => { + const url = queryClient.getQueryData([ + "download-url", + objectId, + ]); + if (url) { + preload(url, { as: isAudio ? "audio" : "video" }); + } + }); + } + }, [children, currentIndex, queryClient]); +}