diff --git a/frontend/composables/use-shopping-list-item-actions.ts b/frontend/composables/use-shopping-list-item-actions.ts index a66a4a60278..f1ff15339c3 100644 --- a/frontend/composables/use-shopping-list-item-actions.ts +++ b/frontend/composables/use-shopping-list-item-actions.ts @@ -1,8 +1,7 @@ import { computed, ref } from "@nuxtjs/composition-api"; import { useLocalStorage } from "@vueuse/core"; import { useUserApi } from "~/composables/api"; -import { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/group"; -import { RequestResponse } from "~/lib/api/types/non-generated"; +import { ShoppingListItemOut } from "~/lib/api/types/group"; const localStorageKey = "shopping-list-queue"; const queueTimeout = 5 * 60 * 1000; // 5 minutes @@ -24,58 +23,21 @@ interface Storage { export function useShoppingListItemActions(shoppingListId: string) { const api = useUserApi(); const storage = useLocalStorage(localStorageKey, {} as Storage, { deep: true }); - const queue = getQueue(); + const queue = storage.value[shoppingListId] ||= { create: [], update: [], delete: [], lastUpdate: Date.now()}; const queueEmpty = computed(() => !queue.create.length && !queue.update.length && !queue.delete.length); if (queueEmpty.value) { queue.lastUpdate = Date.now(); - storage.value[shoppingListId].lastUpdate = queue.lastUpdate; } const isOffline = ref(false); - function isValidQueueObject(obj: any): obj is ShoppingListQueue { - if (typeof obj !== "object" || obj === null) { - return false; - } - - const hasRequiredProps = "create" in obj && "update" in obj && "delete" in obj && "lastUpdate" in obj; - if (!hasRequiredProps) { - return false; - } - - const arraysValid = Array.isArray(obj.create) && Array.isArray(obj.update) && Array.isArray(obj.delete); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const lastUpdateValid = typeof obj.lastUpdate === "number" && !isNaN(new Date(obj.lastUpdate).getTime()); - - return arraysValid && lastUpdateValid; - } - - function createEmptyQueue(): ShoppingListQueue { - return { create: [], update: [], delete: [], lastUpdate: Date.now() }; - } - - function getQueue(): ShoppingListQueue { - try { - const queue = storage.value[shoppingListId]; - if (!isValidQueueObject(queue)) { - console.log("Invalid queue object in local storage; resetting queue."); - return createEmptyQueue(); - } else { - return queue; - } - } catch (error) { - console.log("Error validating queue object in local storage; resetting queue.", error); - return createEmptyQueue(); - } - } - - function removeFromQueue(itemQueue: ShoppingListItemOut[], item: ShoppingListItemOut): boolean { - const index = itemQueue.findIndex(i => i.id === item.id); + function removeFromQueue(queue: ShoppingListItemOut[], item: ShoppingListItemOut): boolean { + const index = queue.findIndex(i => i.id === item.id); if (index === -1) { return false; } - itemQueue.splice(index, 1); + queue.splice(index, 1); return true; } @@ -88,7 +50,6 @@ export function useShoppingListItemActions(shoppingListId: string) { function createItem(item: ShoppingListItemOut) { removeFromQueue(queue.create, item); queue.create.push(item); - storage.value[shoppingListId] = { ...queue }; } function updateItem(item: ShoppingListItemOut) { @@ -96,13 +57,11 @@ export function useShoppingListItemActions(shoppingListId: string) { if (removedFromCreate) { // this item hasn't been created yet, so we don't need to update it queue.create.push(item); - storage.value[shoppingListId] = { ...queue }; return; } removeFromQueue(queue.update, item); queue.update.push(item); - storage.value[shoppingListId] = { ...queue }; } function deleteItem(item: ShoppingListItemOut) { @@ -115,7 +74,6 @@ export function useShoppingListItemActions(shoppingListId: string) { removeFromQueue(queue.update, item); removeFromQueue(queue.delete, item); queue.delete.push(item); - storage.value[shoppingListId] = { ...queue }; } function getQueueItems(itemQueueType: ItemQueueType) { @@ -132,9 +90,6 @@ export function useShoppingListItemActions(shoppingListId: string) { if (itemQueueType === "delete" || itemQueueType === "all") { queue.delete = itemIds ? queue.delete.filter(item => !itemIds.includes(item.id)) : []; } - if (queueEmpty.value) { - queue.lastUpdate = Date.now(); - } // Set the storage value explicitly so changes are saved in the browser. storage.value[shoppingListId] = { ...queue }; @@ -143,60 +98,36 @@ export function useShoppingListItemActions(shoppingListId: string) { /** * Handles the response from the backend and sets the isOffline flag if necessary. */ - function handleResponse(response: RequestResponse) { - isOffline.value = response?.response?.status === undefined; - } - - function checkUpdateState(list: ShoppingListOut) { - const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString(); - if (list.updateAt && list.updateAt > cutoffDate) { - // If the queue is too far behind the shopping list to reliably do updates, we clear the queue - console.log("Out of sync with server; clearing queue"); - clearQueueItems("all"); - } + function handleResponse(response: any) { + // TODO: is there a better way of checking for network errors? + isOffline.value = response.error?.message?.includes("Network Error") || false; } - /** - * Processes the queue items and returns whether the processing was successful. - */ async function processQueueItems( - action: (items: ShoppingListItemOut[]) => Promise>, + action: (items: ShoppingListItemOut[]) => Promise, itemQueueType: ItemQueueType, - ): Promise { - let queueItems: ShoppingListItemOut[]; - try { - queueItems = getQueueItems(itemQueueType); - if (!queueItems.length) { - return true; - } - } catch (error) { - console.log(`Error fetching queue items of type ${itemQueueType}:`, error); - clearQueueItems(itemQueueType); - return false; - } - - try { - const itemsToProcess = [...queueItems]; - await action(itemsToProcess) - .then((response) => { - handleResponse(response); - if (!isOffline.value) { - clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id)); - } - }); - } catch (error) { - console.log(`Error processing queue items of type ${itemQueueType}:`, error); - clearQueueItems(itemQueueType); - return false; + ) { + const queueItems = getQueueItems(itemQueueType); + if (!queueItems.length) { + return; } - return true; + const itemsToProcess = [...queueItems]; + await action(itemsToProcess) + .then((response) => { + handleResponse(response); + if (!isOffline.value) { + clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id)); + } + }); } async function process() { - if(queueEmpty.value) { - queue.lastUpdate = Date.now(); - storage.value[shoppingListId].lastUpdate = queue.lastUpdate; + if( + !queue.create.length && + !queue.update.length && + !queue.delete.length + ) { return; } @@ -204,20 +135,21 @@ export function useShoppingListItemActions(shoppingListId: string) { if (!data) { return; } - checkUpdateState(data); - - // We send each bulk request one at a time, since the backend may merge items - // "failures" here refers to an actual error, rather than failing to reach the backend - let failures = 0; - if (!(await processQueueItems((items) => api.shopping.items.deleteMany(items), "delete"))) failures++; - if (!(await processQueueItems((items) => api.shopping.items.updateMany(items), "update"))) failures++; - if (!(await processQueueItems((items) => api.shopping.items.createMany(items), "create"))) failures++; - - // If we're online, or the queue is empty, the queue is fully processed, so we're up to date - // Otherwise, if all three queue processes failed, we've already reset the queue, so we need to reset the date - if (!isOffline.value || queueEmpty.value || failures === 3) { + + const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString(); + if (data.updateAt && data.updateAt > cutoffDate) { + // If the queue is too far behind the shopping list to reliably do updates, we clear the queue + clearQueueItems("all"); + } else { + // We send each bulk request one at a time, since the backend may merge items + await processQueueItems((items) => api.shopping.items.deleteMany(items), "delete"); + await processQueueItems((items) => api.shopping.items.updateMany(items), "update"); + await processQueueItems((items) => api.shopping.items.createMany(items), "create"); + } + + // If we're online, the queue is fully processed, so we're up to date + if (!isOffline.value) { queue.lastUpdate = Date.now(); - storage.value[shoppingListId].lastUpdate = queue.lastUpdate; } }