mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-11-03 18:53:17 -05:00 
			
		
		
		
	fix: Revert "fix: Offline Shopping List Fixes" (#3835)
This commit is contained in:
		@@ -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;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user