From ccd827de1099a9180c8581be63979fd4263df13d Mon Sep 17 00:00:00 2001 From: amansinghbais Date: Fri, 10 Jan 2025 19:34:58 +0530 Subject: [PATCH] Implemented: support for websocket for realtime counting (#598) --- src/composables/useWebSocketComposables.d.ts | 9 ++ src/composables/useWebSocketComposables.ts | 88 ++++++++++++++++++++ src/store/modules/user/getters.ts | 7 ++ src/views/CountDetail.vue | 27 ++++++ src/views/HardCountDetail.vue | 36 +++++++- 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/composables/useWebSocketComposables.d.ts create mode 100644 src/composables/useWebSocketComposables.ts diff --git a/src/composables/useWebSocketComposables.d.ts b/src/composables/useWebSocketComposables.d.ts new file mode 100644 index 00000000..24dd33aa --- /dev/null +++ b/src/composables/useWebSocketComposables.d.ts @@ -0,0 +1,9 @@ +// This module provides composable functions for managing NetSuite integrations, allowing for asynchronous operations such as +//adding, editing, updating, and removing NetSuite IDs based on specified integration mapping data. +declare module "@/composables/useWebSocketComposables" { + export function useWebSocketComposables(webSocketUrl: string): { + initWebSocket: () => Promise; + tryReopen: () => Promise; + registerListener: (topic: string, callback: any) => Promise; + }; +} \ No newline at end of file diff --git a/src/composables/useWebSocketComposables.ts b/src/composables/useWebSocketComposables.ts new file mode 100644 index 00000000..b9e7523e --- /dev/null +++ b/src/composables/useWebSocketComposables.ts @@ -0,0 +1,88 @@ +import { onIonViewDidLeave } from '@ionic/vue'; +import { reactive } from 'vue'; + +export function useWebSocketComposables(webSocketUrl: string) { + const state = reactive({ + webSocket: null, + currentTopic: null, + topicListener: null, + tryReopenCount: 0, + }) as any; + + const initWebSocket = () => { + state.webSocket = new WebSocket(webSocketUrl); + + state.webSocket.onopen = () => { + state.tryReopenCount = 0; + + // Subscribe to all topics + if(state.currentTopic) { + state.webSocket.send(`subscribe:${state.currentTopic}`); + } + }; + + state.webSocket.onmessage = (event: any) => { + const jsonObj = JSON.parse(event.data); + if (jsonObj.topic === state.currentTopic && state.topicListener) { + state.topicListener(jsonObj); + } + }; + + state.webSocket.onclose = () => { + console.error('WebSocket closed'); + setTimeout(() => tryReopen(), 30 * 1000); + }; + + state.webSocket.onerror = (event: any) => { + console.error('WebSocket error', event); + }; + }; + + const tryReopen = () => { + if ( + (!state.webSocket || + state.webSocket.readyState === WebSocket.CLOSED || + state.webSocket.readyState === WebSocket.CLOSING) && + state.tryReopenCount < 6 + ) { + state.tryReopenCount += 1; + initWebSocket(); + } + }; + + const registerListener = (topic: string, callback: any) => { + if (!state.webSocket) { + initWebSocket(); + } + + if (state.currentTopic !== topic) { + // Unsubscribe from the previous topic + if (state.currentTopic && state.webSocket.readyState === WebSocket.OPEN) { + state.webSocket.send(`unsubscribe:${state.currentTopic}`); + } + + // Update the current topic and listener + state.currentTopic = topic; + state.topicListener = callback; + + // Subscribe to the new topic + if (state.webSocket.readyState === WebSocket.OPEN) { + state.webSocket.send(`subscribe:${topic}`); + } + } else if (state.topicListener !== callback) { + // Update the callback if it has changed + state.topicListener = callback; + } + }; + + onIonViewDidLeave(() => { + if (state.webSocket) { + state.webSocket.onclose = null; + state.webSocket.close(); + } + }); + + return { + registerListener + }; +} diff --git a/src/store/modules/user/getters.ts b/src/store/modules/user/getters.ts index c1fcf271..3c222b47 100644 --- a/src/store/modules/user/getters.ts +++ b/src/store/modules/user/getters.ts @@ -52,6 +52,13 @@ const getters: GetterTree = { }, getGoodIdentificationTypes(state) { return state.goodIdentificationTypes; + }, + getWebSocketUrl(state) { + let baseURL = state.instanceUrl + if(baseURL.startsWith("http")) { + baseURL = baseURL.replace(/https?:\/\/|\/api|\/+/g, ""); + } + return `ws://${baseURL}/notws?api_key=${state.token}`; } } export default getters; \ No newline at end of file diff --git a/src/views/CountDetail.vue b/src/views/CountDetail.vue index 19bcd391..3469cee8 100644 --- a/src/views/CountDetail.vue +++ b/src/views/CountDetail.vue @@ -253,6 +253,7 @@ import { paperPlaneOutline } from "ionicons/icons" import Image from "@/components/Image.vue"; import router from "@/router" import { onBeforeRouteLeave } from 'vue-router'; +import { useWebSocketComposables } from '@/composables/useWebSocketComposables'; const store = useStore(); @@ -263,6 +264,8 @@ const cycleCountItems = computed(() => store.getters["count/getCycleCountItems"] const userProfile = computed(() => store.getters["user/getUserProfile"]) const productStoreSettings = computed(() => store.getters["user/getProductStoreSettings"]) const currentItemIndex = computed(() => !product.value ? 0 : itemsList?.value.findIndex((item) => item.productId === product?.value.productId && item.importItemSeqId === product?.value.importItemSeqId)); +const currentFacility = computed(() => store.getters["user/getCurrentFacility"]) +const webSocketUrl = computed(() => store.getters["user/getWebSocketUrl"]) const itemsList = computed(() => { if (selectedSegment.value === 'all') { @@ -283,6 +286,8 @@ const itemsList = computed(() => { } }); +const { registerListener } = useWebSocketComposables(webSocketUrl.value); + const props = defineProps(["id"]); let selectedSegment = ref('all'); let cycleCount = ref([]); @@ -303,6 +308,7 @@ onIonViewDidEnter(async() => { previousItem = itemsList.value[0] await store.dispatch("product/currentProduct", itemsList.value[0]) barcodeInput.value?.$el?.setFocus(); + registerListener(currentFacility.value.facilityId, handleNewMessage); emitter.emit("dismissLoader") }) @@ -638,6 +644,27 @@ async function readyForReview() { }); await alert.present(); } + +function handleNewMessage(jsonObj) { + const updatedItem = jsonObj.message + if(updatedItem.inventoryCountImportId !== cycleCount.value.inventoryCountImportId) return; + + const items = JSON.parse(JSON.stringify(cycleCountItems.value.itemList)) + const currentItemIndex = items.findIndex((item) => item.productId === updatedItem.productId && item.importItemSeqId === updatedItem.importItemSeqId); + + if(currentItemIndex !== -1) { + items[currentItemIndex] = updatedItem + } else { + store.dispatch("product/fetchProducts", { productIds: [updatedItem.productId] }) + items.push(updatedItem) + } + + store.dispatch('count/updateCycleCountItems', items); + if(product.value.productId === updatedItem.productId) { + store.dispatch('product/currentProduct', updatedItem); + } +} +