summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-09 23:15:58 -0800
committerFuwn <[email protected]>2026-02-09 23:15:58 -0800
commitbd35899a42e84a4a20c649e80adaebff057de43d (patch)
tree0fe34e2892df306a69f62ba2946d5cc83be6f506 /apps/web
parentfeat: offline support tier 1 — IndexedDB query persistence and offline banner (diff)
downloadasa.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.tsx15
-rw-r--r--apps/web/app/sw.ts22
-rw-r--r--apps/web/lib/hooks/use-prefetch-entry-details.ts45
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])
+}