Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 40 additions & 108 deletions frontend/composables/use-shopping-list-item-actions.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}

Expand All @@ -88,21 +50,18 @@ 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) {
const removedFromCreate = removeFromQueue(queue.create, item);
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) {
Expand All @@ -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) {
Expand All @@ -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 };
Expand All @@ -143,81 +98,58 @@ export function useShoppingListItemActions(shoppingListId: string) {
/**
* Handles the response from the backend and sets the isOffline flag if necessary.
*/
function handleResponse(response: RequestResponse<any>) {
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<RequestResponse<any>>,
action: (items: ShoppingListItemOut[]) => Promise<any>,
itemQueueType: ItemQueueType,
): Promise<boolean> {
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;
}

const data = await getList();
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;
}
}

Expand Down
Loading