fix: Revert "fix: Offline Shopping List Fixes" (#3835)

This commit is contained in:
boc-the-git 2024-07-03 22:08:13 +10:00 committed by GitHub
parent 7931e383b2
commit d639d168fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

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