From 4be2a4b43173d5173d1079ac499cf79039d450a0 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 6 Sep 2024 23:51:22 -0400 Subject: [PATCH] Use magic url for sw sync --- nextjs-end/auth-service-worker.js | 37 +++++++++++++++---------- nextjs-end/src/components/Header.jsx | 41 +++++++++------------------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/nextjs-end/auth-service-worker.js b/nextjs-end/auth-service-worker.js index 12db968c..f80c9abf 100644 --- a/nextjs-end/auth-service-worker.js +++ b/nextjs-end/auth-service-worker.js @@ -21,19 +21,6 @@ self.addEventListener("activate", () => { self.clients.claim(); }); -// Notify clients of onAuthStateChanged events, so they can coordinate -// any actions which may normally be prone to race conditions, such as -// router.refresh(); -auth.authStateReady().then(() => { - onAuthStateChanged(auth, async (user) => { - const uid = user?.uid; - const clients = await self.clients.matchAll(); - for (const client of clients) { - client.postMessage({ type: "onAuthStateChanged", uid }); - } - }); -}); - async function getAuthIdToken() { await auth.authStateReady(); if (!auth.currentUser) return; @@ -43,12 +30,34 @@ async function getAuthIdToken() { self.addEventListener("fetch", (event) => { const { origin, pathname } = new URL(event.request.url); if (origin !== self.location.origin) return; + // Use a magic url to ensure that auth state is in sync between + // the client and the sw, this helps with actions such as router.refresh(); + if (pathname.startsWith('/__/auth/wait/')) { + const uid = pathname.split('/').at(-1); + event.respondWith(waitForMatchingUid(uid)); + return; + } if (pathname.startsWith('/_next/')) return; - // Don't add haeders to GET requests with an extension—this skips css, images, fonts, json, etc. + // Don't add headers to non-get requests or those with an extension—this + // helps with css, images, fonts, json, etc. if (event.request.method === "GET" && !pathname.startsWith("/api/") && pathname.includes(".")) return; event.respondWith(fetchWithFirebaseHeaders(event.request)); }); +async function waitForMatchingUid(_uid) { + const uid = _uid === "undefined" ? undefined : _uid; + await auth.authStateReady(); + await new Promise((resolve) => { + const unsubscribe = onAuthStateChanged(auth, (user) => { + if (user?.uid === uid) { + unsubscribe(); + resolve(); + } + }); + }); + return new Response(undefined, { status: 200, headers: { "cache-control": "no-store" } }); +} + async function fetchWithFirebaseHeaders(request) { const authIdToken = await getAuthIdToken(); if (authIdToken) { diff --git a/nextjs-end/src/components/Header.jsx b/nextjs-end/src/components/Header.jsx index 14245ecb..27a4c14e 100644 --- a/nextjs-end/src/components/Header.jsx +++ b/nextjs-end/src/components/Header.jsx @@ -5,17 +5,14 @@ import { signInWithGoogle, signOut, onAuthStateChanged, - onIdTokenChanged, } from "@/src/lib/firebase/auth.js"; import { addFakeRestaurantsAndReviews } from "@/src/lib/firebase/firestore.js"; import { useRouter } from "next/navigation"; import { firebaseConfig } from "@/src/lib/firebase/config"; -import { getIdToken } from "firebase/auth"; function useUserSession(initialUser) { // The initialUser comes from the server via a server component const [user, setUser] = useState(initialUser); - const [serviceWorker, setServiceWorker] = useState(undefined); const router = useRouter(); // Register the service worker that sends auth state back to server @@ -25,36 +22,24 @@ function useUserSession(initialUser) { const serializedFirebaseConfig = encodeURIComponent(JSON.stringify(firebaseConfig)); const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}` - navigator.serviceWorker - .register(serviceWorkerUrl) - .then(async (registration) => { - setServiceWorker(registration.active); - console.log("scope is: ", registration.scope) - }); + navigator + .serviceWorker + .register(serviceWorkerUrl, { scope: "/", updateViaCache: "none" }) + .then((registration) => { + console.log("scope is: ", registration.scope); + registration.update(); + }); } }, []); useEffect(() => { - return onAuthStateChanged((authUser) => { - setUser(authUser) + return onAuthStateChanged(async (authUser) => { + if (user?.uid === authUser?.uid) return; + await fetch(`/__/auth/wait/${authUser?.uid}`, { method: "HEAD" }).catch(() => undefined); + setUser(authUser); + router.refresh(); }); - }, []); - - useEffect(() => { - if (serviceWorker) { - // listen to an onAuthStateChanged event from the service worker when - // refreshing the router, this is preferred over onAuthStateChanged as - // that can introduce race conditions as the client & service worker state - // can be out of sync - return navigator.serviceWorker.addEventListener("message", (event) => { - if (event.source !== serviceWorker) return; - if (event.data.type !== "onAuthStateChanged") return; - event.preventDefault(); - if (user?.uid === event.data?.uid) return; - router.refresh(); - }); - } - }, [user, serviceWorker]); + }, [user]); return user; }