| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import { useLocalStorage, useOnline } from "@vueuse/core"; | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  | import { useUserApi } from "~/composables/api"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | import type { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/household"; | 
					
						
							|  |  |  | import type { RequestResponse } from "~/lib/api/types/non-generated"; | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | const localStorageKey = "shopping-list-queue"; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | const queueTimeout = 5 * 60 * 1000; // 5 minutes
 | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | type ItemQueueType = "create" | "update" | "delete"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface ShoppingListQueue { | 
					
						
							|  |  |  |   create: ShoppingListItemOut[]; | 
					
						
							|  |  |  |   update: ShoppingListItemOut[]; | 
					
						
							|  |  |  |   delete: ShoppingListItemOut[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   lastUpdate: number; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface Storage { | 
					
						
							|  |  |  |   [key: string]: ShoppingListQueue; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function useShoppingListItemActions(shoppingListId: string) { | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |   const isOnline = useOnline(); | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   const api = useUserApi(); | 
					
						
							|  |  |  |   const storage = useLocalStorage(localStorageKey, {} as Storage, { deep: true }); | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |   const queue = reactive(getQueue()); | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   const queueEmpty = computed(() => !queue.create.length && !queue.update.length && !queue.delete.length); | 
					
						
							|  |  |  |   if (queueEmpty.value) { | 
					
						
							|  |  |  |     queue.lastUpdate = Date.now(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |   storage.value[shoppingListId] = { ...queue }; | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |   watch( | 
					
						
							|  |  |  |     () => queue, | 
					
						
							|  |  |  |     (value) => { | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       storage.value[shoppingListId] = { ...value }; | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     }, | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       deep: true, | 
					
						
							|  |  |  |       immediate: true, | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |   ); | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |   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); | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     const lastUpdateValid = typeof obj.lastUpdate === "number" && !isNaN(new Date(obj.lastUpdate).getTime()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return arraysValid && lastUpdateValid; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function createEmptyQueue(): ShoppingListQueue { | 
					
						
							|  |  |  |     const newQueue = { create: [], update: [], delete: [], lastUpdate: Date.now() }; | 
					
						
							|  |  |  |     return newQueue; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |   function getQueue(): ShoppingListQueue { | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const fetchedQueue = storage.value[shoppingListId]; | 
					
						
							|  |  |  |       if (!isValidQueueObject(fetchedQueue)) { | 
					
						
							|  |  |  |         console.log("Invalid queue object in local storage; resetting queue."); | 
					
						
							|  |  |  |         return createEmptyQueue(); | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       } | 
					
						
							|  |  |  |       else { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |         return fetchedQueue; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     } | 
					
						
							|  |  |  |     catch (error) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |       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); | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |     if (index === -1) { | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     itemQueue.splice(index, 1); | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |     return true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |   function mergeListItemsByLatest( | 
					
						
							|  |  |  |     list1: ShoppingListItemOut[], | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     list2: ShoppingListItemOut[], | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |   ) { | 
					
						
							|  |  |  |     const mergedList = [...list1]; | 
					
						
							|  |  |  |     list2.forEach((list2Item) => { | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       const conflictingItem = mergedList.find(item => item.id === list2Item.id); | 
					
						
							|  |  |  |       if (conflictingItem | 
					
						
							|  |  |  |         && list2Item.updatedAt && conflictingItem.updatedAt | 
					
						
							|  |  |  |         && list2Item.updatedAt > conflictingItem.updatedAt) { | 
					
						
							|  |  |  |         mergedList.splice(mergedList.indexOf(conflictingItem), 1, list2Item); | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |       else if (!conflictingItem) { | 
					
						
							|  |  |  |         mergedList.push(list2Item); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return mergedList; | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   async function getList() { | 
					
						
							|  |  |  |     const response = await api.shopping.lists.getOne(shoppingListId); | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     if (!isOnline.value && response.data) { | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |       const createAndUpdateQueues = mergeListItemsByLatest(queue.update, queue.create); | 
					
						
							|  |  |  |       response.data.listItems = mergeListItemsByLatest(response.data.listItems ?? [], createAndUpdateQueues); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     return response.data; | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function createItem(item: ShoppingListItemOut) { | 
					
						
							|  |  |  |     removeFromQueue(queue.create, item); | 
					
						
							|  |  |  |     queue.create.push(item); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     removeFromQueue(queue.update, item); | 
					
						
							|  |  |  |     queue.update.push(item); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function deleteItem(item: ShoppingListItemOut) { | 
					
						
							|  |  |  |     const removedFromCreate = removeFromQueue(queue.create, item); | 
					
						
							|  |  |  |     if (removedFromCreate) { | 
					
						
							|  |  |  |       // this item hasn't been created yet, so we don't need to delete it
 | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     removeFromQueue(queue.update, item); | 
					
						
							|  |  |  |     removeFromQueue(queue.delete, item); | 
					
						
							|  |  |  |     queue.delete.push(item); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function getQueueItems(itemQueueType: ItemQueueType) { | 
					
						
							|  |  |  |     return queue[itemQueueType]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function clearQueueItems(itemQueueType: ItemQueueType | "all", itemIds: string[] | null = null) { | 
					
						
							|  |  |  |     if (itemQueueType === "create" || itemQueueType === "all") { | 
					
						
							|  |  |  |       queue.create = itemIds ? queue.create.filter(item => !itemIds.includes(item.id)) : []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (itemQueueType === "update" || itemQueueType === "all") { | 
					
						
							|  |  |  |       queue.update = itemIds ? queue.update.filter(item => !itemIds.includes(item.id)) : []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (itemQueueType === "delete" || itemQueueType === "all") { | 
					
						
							|  |  |  |       queue.delete = itemIds ? queue.delete.filter(item => !itemIds.includes(item.id)) : []; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     if (queueEmpty.value) { | 
					
						
							|  |  |  |       queue.lastUpdate = Date.now(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |   function checkUpdateState(list: ShoppingListOut) { | 
					
						
							|  |  |  |     const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString(); | 
					
						
							| 
									
										
										
										
											2024-08-22 10:14:32 -05:00
										 |  |  |     if (list.updatedAt && list.updatedAt > cutoffDate) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |       // 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"); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2025-09-27 13:57:53 -05:00
										 |  |  |    * Processes the queue items and returns whether the processing was successful. | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   async function processQueueItems( | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>, | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |     itemQueueType: ItemQueueType, | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |   ): Promise<boolean> { | 
					
						
							|  |  |  |     let queueItems: ShoppingListItemOut[]; | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       queueItems = getQueueItems(itemQueueType); | 
					
						
							|  |  |  |       if (!queueItems.length) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     } | 
					
						
							|  |  |  |     catch (error) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |       console.log(`Error fetching queue items of type ${itemQueueType}:`, error); | 
					
						
							|  |  |  |       clearQueueItems(itemQueueType); | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       const itemsToProcess = [...queueItems]; | 
					
						
							|  |  |  |       await action(itemsToProcess) | 
					
						
							|  |  |  |         .then(() => { | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |           if (isOnline.value) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |             clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id)); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     } | 
					
						
							|  |  |  |     catch (error) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |       console.log(`Error processing queue items of type ${itemQueueType}:`, error); | 
					
						
							|  |  |  |       clearQueueItems(itemQueueType); | 
					
						
							|  |  |  |       return false; | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     return true; | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async function process() { | 
					
						
							| 
									
										
										
										
											2025-06-17 19:41:35 +01:00
										 |  |  |     if (queueEmpty.value) { | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |       queue.lastUpdate = Date.now(); | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const data = await getList(); | 
					
						
							|  |  |  |     if (!data) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  |     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; | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     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++; | 
					
						
							| 
									
										
										
										
											2024-07-27 21:25:58 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // 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
 | 
					
						
							| 
									
										
										
										
											2025-06-20 00:09:12 +07:00
										 |  |  |     if (isOnline.value || queueEmpty.value || failures === 3) { | 
					
						
							| 
									
										
										
										
											2024-06-29 04:58:58 -05:00
										 |  |  |       queue.lastUpdate = Date.now(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     getList, | 
					
						
							|  |  |  |     createItem, | 
					
						
							|  |  |  |     updateItem, | 
					
						
							|  |  |  |     deleteItem, | 
					
						
							|  |  |  |     process, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } |