From dbe209c7fc70b53eb2bcc4f52de295d0a0041fdf Mon Sep 17 00:00:00 2001
From: m5r <mokht@rmi.al>
Date: Sat, 11 Jun 2022 16:13:00 +0200
Subject: [PATCH] purge phone calls and messages from cache when switching
 phone numbers or twilio account

---
 app/features/keypad/loaders/keypad.ts     | 15 +++++++----
 app/features/messages/loaders/messages.ts | 15 +++++++----
 app/features/phone-calls/loaders/calls.ts | 23 ++++++++++-------
 app/features/settings/actions/phone.ts    |  7 +++--
 app/service-worker/cache-utils.ts         | 31 ++++++++++++++++++++++-
 app/service-worker/fetch.ts               |  6 +++++
 app/service-worker/message.ts             |  4 ---
 7 files changed, 73 insertions(+), 28 deletions(-)

diff --git a/app/features/keypad/loaders/keypad.ts b/app/features/keypad/loaders/keypad.ts
index 549b4e9..c0d2d98 100644
--- a/app/features/keypad/loaders/keypad.ts
+++ b/app/features/keypad/loaders/keypad.ts
@@ -24,11 +24,16 @@ const loader: LoaderFunction = async ({ request }) => {
 			where: { phoneNumberId: phoneNumber.id },
 			orderBy: { createdAt: Prisma.SortOrder.desc },
 		}));
-	return json<KeypadLoaderData>({
-		hasOngoingSubscription,
-		hasPhoneNumber,
-		lastRecipientCalled: lastCall?.recipient,
-	});
+	return json<KeypadLoaderData>(
+		{
+			hasOngoingSubscription,
+			hasPhoneNumber,
+			lastRecipientCalled: lastCall?.recipient,
+		},
+		{
+			headers: { Vary: "Cookie" },
+		},
+	);
 };
 
 export default loader;
diff --git a/app/features/messages/loaders/messages.ts b/app/features/messages/loaders/messages.ts
index af2787c..de56e29 100644
--- a/app/features/messages/loaders/messages.ts
+++ b/app/features/messages/loaders/messages.ts
@@ -23,11 +23,16 @@ const loader: LoaderFunction = async ({ request }) => {
 	const phoneNumber = await db.phoneNumber.findUnique({
 		where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio?.accountSid ?? "", isCurrent: true } },
 	});
-	return json<MessagesLoaderData>({
-		hasPhoneNumber: Boolean(phoneNumber),
-		isFetchingMessages: phoneNumber?.isFetchingMessages ?? null,
-		conversations: await getConversations(phoneNumber),
-	});
+	return json<MessagesLoaderData>(
+		{
+			hasPhoneNumber: Boolean(phoneNumber),
+			isFetchingMessages: phoneNumber?.isFetchingMessages ?? null,
+			conversations: await getConversations(phoneNumber),
+		},
+		{
+			headers: { Vary: "Cookie" },
+		},
+	);
 };
 
 export default loader;
diff --git a/app/features/phone-calls/loaders/calls.ts b/app/features/phone-calls/loaders/calls.ts
index 3fb4bff..630451e 100644
--- a/app/features/phone-calls/loaders/calls.ts
+++ b/app/features/phone-calls/loaders/calls.ts
@@ -44,15 +44,20 @@ const loader: LoaderFunction = async ({ request }) => {
 		where: { phoneNumberId: phoneNumber.id },
 		orderBy: { createdAt: Prisma.SortOrder.desc },
 	});
-	return json<PhoneCallsLoaderData>({
-		hasOngoingSubscription,
-		hasPhoneNumber,
-		phoneCalls: phoneCalls.map((phoneCall) => ({
-			...phoneCall,
-			fromMeta: getPhoneNumberMeta(phoneCall.from),
-			toMeta: getPhoneNumberMeta(phoneCall.to),
-		})),
-	});
+	return json<PhoneCallsLoaderData>(
+		{
+			hasOngoingSubscription,
+			hasPhoneNumber,
+			phoneCalls: phoneCalls.map((phoneCall) => ({
+				...phoneCall,
+				fromMeta: getPhoneNumberMeta(phoneCall.from),
+				toMeta: getPhoneNumberMeta(phoneCall.to),
+			})),
+		},
+		{
+			headers: { Vary: "Cookie" },
+		},
+	);
 };
 
 export default loader;
diff --git a/app/features/settings/actions/phone.ts b/app/features/settings/actions/phone.ts
index cd7f85c..7d027c9 100644
--- a/app/features/settings/actions/phone.ts
+++ b/app/features/settings/actions/phone.ts
@@ -23,7 +23,6 @@ const action: ActionFunction = async ({ request }) => {
 		return badRequest({ errorMessage });
 	}
 
-	console.log("formData._action", formData._action);
 	switch (formData._action as Action) {
 		case "setPhoneNumber":
 			return setPhoneNumber(request, formData);
@@ -128,9 +127,6 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
 	};
 	const [phoneNumbers] = await Promise.all([
 		twilioClient.incomingPhoneNumbers.list(),
-		setTwilioApiKeyQueue.add(`set twilio api key for accountSid=${twilioAccountSid}`, {
-			accountSid: twilioAccountSid,
-		}),
 		db.twilioAccount.upsert({
 			where: { organizationId: organization.id },
 			create: {
@@ -143,6 +139,9 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
 		}),
 	]);
 
+	setTwilioApiKeyQueue.add(`set twilio api key for accountSid=${twilioAccountSid}`, {
+		accountSid: twilioAccountSid,
+	});
 	await Promise.all(
 		phoneNumbers.map(async (phoneNumber) => {
 			const phoneNumberId = phoneNumber.sid;
diff --git a/app/service-worker/cache-utils.ts b/app/service-worker/cache-utils.ts
index bcdd9a4..11e0a01 100644
--- a/app/service-worker/cache-utils.ts
+++ b/app/service-worker/cache-utils.ts
@@ -17,6 +17,10 @@ export function isDocumentGetRequest(request: Request) {
 	return request.method.toLowerCase() === "get" && request.mode === "navigate";
 }
 
+export function isMutationRequest(request: Request) {
+	return ["POST", "DELETE"].includes(request.method);
+}
+
 export function fetchAsset(event: FetchEvent): Promise<Response> {
 	// stale-while-revalidate
 	const url = new URL(event.request.url);
@@ -172,5 +176,30 @@ export async function deleteCaches() {
 	const allCaches = await caches.keys();
 	const cachesToDelete = allCaches.filter((cacheName) => cacheName !== ASSET_CACHE);
 	await Promise.all(cachesToDelete.map((cacheName) => caches.delete(cacheName)));
-	console.debug("Caches deleted");
+	console.debug("Old caches deleted");
+}
+
+export async function purgeMutatedLoaders(event: FetchEvent) {
+	const url = new URL(event.request.url);
+	const rootPathname = "/" + url.pathname.split("/")[1];
+	const cache = await caches.open(DATA_CACHE);
+	const cachedLoaders = await cache.keys();
+
+	const loadersToDelete = cachedLoaders.filter((loader) => {
+		const cachedPathname = new URL(loader.url).pathname;
+		const shouldPurge = cachedPathname.startsWith(rootPathname);
+
+		if (url.pathname === "/settings/phone") {
+			// changes phone number or twilio account credentials
+			// so purge messages and phone calls from cache
+			return (
+				shouldPurge ||
+				["/messages", "/calls", "/keypad"].some((pathname) => cachedPathname.startsWith(pathname))
+			);
+		}
+
+		return shouldPurge;
+	});
+	await Promise.all(loadersToDelete.map((loader) => cache.delete(loader)));
+	console.debug("Purged loaders data starting with", rootPathname);
 }
diff --git a/app/service-worker/fetch.ts b/app/service-worker/fetch.ts
index bc81535..c04e0e8 100644
--- a/app/service-worker/fetch.ts
+++ b/app/service-worker/fetch.ts
@@ -5,6 +5,8 @@ import {
 	isAssetRequest,
 	isDocumentGetRequest,
 	isLoaderRequest,
+	isMutationRequest,
+	purgeMutatedLoaders,
 } from "./cache-utils";
 
 declare const self: ServiceWorkerGlobalScope;
@@ -22,5 +24,9 @@ export default async function handleFetch(event: FetchEvent) {
 		return fetchDocument(event);
 	}
 
+	if (isMutationRequest(event.request)) {
+		await purgeMutatedLoaders(event);
+	}
+
 	return fetch(event.request);
 }
diff --git a/app/service-worker/message.ts b/app/service-worker/message.ts
index a734b85..4cbe7dd 100644
--- a/app/service-worker/message.ts
+++ b/app/service-worker/message.ts
@@ -51,9 +51,5 @@ async function purgeStaticAssets(assetsToCache: string[]) {
 	const assetCache = await caches.open(ASSET_CACHE);
 	const cachedAssets = await assetCache.keys();
 	const cachesToDelete = cachedAssets.filter((asset) => !assetsToCache.includes(new URL(asset.url).pathname));
-	console.log(
-		"cachesToDelete",
-		cachesToDelete.map((c) => new URL(c.url).pathname),
-	);
 	await Promise.all(cachesToDelete.map((asset) => assetCache.delete(asset)));
 }