diff options
| author | Fuwn <[email protected]> | 2026-02-09 23:15:58 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-09 23:15:58 -0800 |
| commit | bd35899a42e84a4a20c649e80adaebff057de43d (patch) | |
| tree | 0fe34e2892df306a69f62ba2946d5cc83be6f506 /apps/web | |
| parent | feat: offline support tier 1 — IndexedDB query persistence and offline banner (diff) | |
| download | asa.news-bd35899a42e84a4a20c649e80adaebff057de43d.tar.xz asa.news-bd35899a42e84a4a20c649e80adaebff057de43d.zip | |
feat: offline support tier 2 — prefetch entry content and SW runtime caching
Prefetch content_html for the first 10 timeline entries in the
background so they are available offline without needing to click
each one. Add NetworkFirst runtime caching in service worker for
Supabase REST GET requests (24h expiry, 200 entry limit).
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/app/reader/_components/entry-list.tsx | 15 | ||||
| -rw-r--r-- | apps/web/app/sw.ts | 22 | ||||
| -rw-r--r-- | apps/web/lib/hooks/use-prefetch-entry-details.ts | 45 |
3 files changed, 75 insertions, 7 deletions
diff --git a/apps/web/app/reader/_components/entry-list.tsx b/apps/web/app/reader/_components/entry-list.tsx index acc0990..65e35f7 100644 --- a/apps/web/app/reader/_components/entry-list.tsx +++ b/apps/web/app/reader/_components/entry-list.tsx @@ -1,12 +1,13 @@ "use client" -import { useRef, useEffect } from "react" +import { useRef, useEffect, useMemo } from "react" import { useVirtualizer } from "@tanstack/react-virtual" import { useTimeline } from "@/lib/queries/use-timeline" import { useSavedEntries } from "@/lib/queries/use-saved-entries" import { useCustomFeedTimeline } from "@/lib/queries/use-custom-feed-timeline" import { useUserInterfaceStore } from "@/lib/stores/user-interface-store" import { EntryListItem } from "./entry-list-item" +import { usePrefetchEntryDetails } from "@/lib/hooks/use-prefetch-entry-details" interface EntryListProperties { feedFilter: "all" | "saved" @@ -77,11 +78,15 @@ export function EntryList({ const firstEntryIdentifier = allEntries[0]?.entryIdentifier const lastEntryIdentifier = allEntries[allEntries.length - 1]?.entryIdentifier + const prefetchIdentifiers = useMemo( + () => allEntries.map((entry) => entry.entryIdentifier), + [firstEntryIdentifier, lastEntryIdentifier, allEntries.length] + ) + usePrefetchEntryDetails(prefetchIdentifiers) + useEffect(() => { - setNavigableEntryIdentifiers( - allEntries.map((entry) => entry.entryIdentifier) - ) - }, [firstEntryIdentifier, lastEntryIdentifier, allEntries.length, setNavigableEntryIdentifiers]) + setNavigableEntryIdentifiers(prefetchIdentifiers) + }, [prefetchIdentifiers, setNavigableEntryIdentifiers]) function getEstimatedItemSize() { switch (entryListViewMode) { diff --git a/apps/web/app/sw.ts b/apps/web/app/sw.ts index 0469377..e18cc5a 100644 --- a/apps/web/app/sw.ts +++ b/apps/web/app/sw.ts @@ -1,7 +1,7 @@ /// <reference lib="webworker" /> import { defaultCache } from "@serwist/next/worker" import type { PrecacheEntry, SerwistGlobalConfig } from "serwist" -import { Serwist } from "serwist" +import { Serwist, NetworkFirst, ExpirationPlugin } from "serwist" declare global { interface WorkerGlobalScope extends SerwistGlobalConfig { @@ -27,7 +27,25 @@ const serwist = new Serwist({ skipWaiting: true, clientsClaim: true, navigationPreload: true, - runtimeCaching: sameOriginCache, + runtimeCaching: [ + { + matcher: ({ url, request }) => + url.hostname.endsWith(".supabase.co") && + url.pathname.startsWith("/rest/v1/") && + request.method === "GET", + handler: new NetworkFirst({ + cacheName: "supabase-rest-get", + networkTimeoutSeconds: 10, + plugins: [ + new ExpirationPlugin({ + maxEntries: 200, + maxAgeSeconds: 60 * 60 * 24, + }), + ], + }), + }, + ...sameOriginCache, + ], }) serwist.addEventListeners() diff --git a/apps/web/lib/hooks/use-prefetch-entry-details.ts b/apps/web/lib/hooks/use-prefetch-entry-details.ts new file mode 100644 index 0000000..8b88bb8 --- /dev/null +++ b/apps/web/lib/hooks/use-prefetch-entry-details.ts @@ -0,0 +1,45 @@ +"use client" + +import { useEffect } from "react" +import { useQueryClient } from "@tanstack/react-query" +import { createSupabaseBrowserClient } from "@/lib/supabase/client" +import { queryKeys } from "@/lib/queries/query-keys" + +const PREFETCH_BATCH_SIZE = 10 + +export function usePrefetchEntryDetails(entryIdentifiers: string[]) { + const queryClient = useQueryClient() + + useEffect(() => { + if (entryIdentifiers.length === 0) return + + const supabaseClient = createSupabaseBrowserClient() + const identifiersToPrefetch = entryIdentifiers.slice(0, PREFETCH_BATCH_SIZE) + + for (const entryIdentifier of identifiersToPrefetch) { + const existingData = queryClient.getQueryData( + queryKeys.entryDetail.single(entryIdentifier) + ) + + if (existingData) continue + + queryClient.prefetchQuery({ + queryKey: queryKeys.entryDetail.single(entryIdentifier), + queryFn: async () => { + const { data, error } = await supabaseClient + .from("entries") + .select( + "id, title, url, author, content_html, summary, published_at, enclosure_url, feeds!inner(title)" + ) + .eq("id", entryIdentifier) + .single() + + if (error) throw error + + return data + }, + staleTime: 5 * 60 * 1000, + }) + } + }, [entryIdentifiers, queryClient]) +} |