import type { EntryKey, UseInfiniteQueryData } from "@pinia/colada" import isDeepEqual from "fast-deep-equal" type NotificationPages = UseInfiniteQueryData const NOTIFICATION_QUERY_KEYS = { list: (limit: number) => ["notifications", "list", limit] as const, count: (read: boolean) => ["notifications", "count", read] as const, listRoot: ["notifications", "list"] as const, countRoot: ["notifications", "count"] as const, } export default function () { const { $coreAPIClient } = useNuxtApp() const queryCache = useQueryCache() function useFetchManyNotifications( paramsRef: MaybeRefOrGetter, ) { return useInfiniteQuery({ key: () => NOTIFICATION_QUERY_KEYS.list(toValue(paramsRef).limit), query: async ({ pageParam }) => { const searchParams = new URLSearchParams({ limit: String(toValue(paramsRef).limit), page: String(pageParam), }) return await $coreAPIClient( `/api/notifications?${searchParams.toString()}`, { method: "GET" }, ) }, initialPageParam: 1, getNextPageParam: (lastPage, _pages, lastPageParam) => lastPageParam < lastPage.pageCount ? lastPageParam + 1 : null, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: 60 * 1000, // 1 min autoRefetch: true, }) } function useFetchNotificationCount( paramsRef: MaybeRefOrGetter, ) { return useQuery({ key: () => { const params = toValue(paramsRef) return NOTIFICATION_QUERY_KEYS.count(params.read) }, query: async () => { const params = toValue(paramsRef) const searchParams = new URLSearchParams() searchParams.set("read", String(params.read)) const query = searchParams.toString() const url = query ? `/api/notifications/count?${query}` : `/api/notifications/count` return await $coreAPIClient<{ count: number }>(url, { method: "GET" }) }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: 60 * 1000, // 1 min autoRefetch: true, }) } const markNotificationsRead = useMutation({ // count is hard to optimistically update, so we skip it onMutate: (req) => { const entries = queryCache.getEntries({ key: NOTIFICATION_QUERY_KEYS.listRoot, }) // the key travels next to the data rather than inside it, so the // bookkeeping never ends up in the cached data const oldNotifs: { key: EntryKey; data: NotificationPages }[] = [] entries.forEach((entry) => { const oldNotifData = clone( queryCache.getQueryData(entry.key), ) // entries can exist without data (a query that has not // resolved yet); including them would make the update loop // below throw on the missing pages array and abort the whole // mutation if (!oldNotifData) { return } oldNotifs.push({ key: clone(entry.key), data: oldNotifData }) }) const newNotifs = clone(oldNotifs) newNotifs.forEach(({ key, data }) => { data.pages.forEach((page) => { page.notifications.forEach((notif) => { if (req.ids.length === 0 || req.ids.includes(notif.id)) { notif.read = true } }) }) queryCache.setQueryData(key, data) queryCache.cancelQueries({ key }) }) return { newNotifs, oldNotifs } }, mutation: async (req: MarkNotificationsReadRequest) => { await $coreAPIClient(`/api/notifications/read-status`, { method: "PUT", body: req, }) }, async onSuccess() { await queryCache.invalidateQueries({ key: NOTIFICATION_QUERY_KEYS.listRoot, }) await queryCache.invalidateQueries({ key: NOTIFICATION_QUERY_KEYS.countRoot, }) }, onError(_err, _data, { oldNotifs, newNotifs }) { const entries = queryCache.getEntries({ key: NOTIFICATION_QUERY_KEYS.listRoot, }) const cachedNotifs: { key: EntryKey; data: NotificationPages }[] = [] entries.forEach((entry) => { const cachedNotifData = clone( queryCache.getQueryData(entry.key), ) // mirror the onMutate filter so the rollback comparison sees // the same set of entries the optimistic update touched if (!cachedNotifData) { return } cachedNotifs.push({ key: clone(entry.key), data: cachedNotifData }) }) if (!isDeepEqual(newNotifs, cachedNotifs)) { return } // rollback oldNotifs?.forEach(({ key, data }) => { queryCache.setQueryData(key, data) }) }, }) return { useFetchManyNotifications, useFetchNotificationCount, markNotificationsRead, } }