From 24b8bcf1bad5f5b32e5df7f4b534ce94ed636ec5 Mon Sep 17 00:00:00 2001 From: auraofdivinity Date: Sat, 21 Sep 2024 10:41:04 +0530 Subject: [PATCH 1/4] fix: add request queue to handle concurrent zoom & offset requests --- .../components/visual-editor/v2/Diagrams.tsx | 21 ++++++------ frontend/src/utils/requestQueue.ts | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/requestQueue.ts diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 37f29fc11..8ea2e4405 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -51,6 +51,7 @@ import { BlockPorts } from "@/types/visual-editor.types"; import BlockDialog from "../BlockDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; +import { RequestQueue } from "@/utils/requestQueue"; const Diagrams = () => { const { t } = useTranslation(); @@ -108,25 +109,23 @@ const Diagrams = () => { const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, { invalidate: false, }); + + const requestQueue = useRef(new RequestQueue()); + const enqueueUpdate = (id: string, params: any) => { + requestQueue.current.enqueue(() => updateCategory({ id, params })); + }; + const debouncedZoomEvent = debounce((event) => { if (selectedCategoryId) { engine?.repaintCanvas(); - updateCategory({ - id: selectedCategoryId, - params: { - zoom: event.zoom, - }, - }); + enqueueUpdate(selectedCategoryId, { zoom: event.zoom }); } event.stopPropagation(); }, 200); const debouncedOffsetEvent = debounce((event) => { if (selectedCategoryId) { - updateCategory({ - id: selectedCategoryId, - params: { - offset: [event.offsetX, event.offsetY], - }, + enqueueUpdate(selectedCategoryId, { + offset: [event.offsetX, event.offsetY], }); } event.stopPropagation(); diff --git a/frontend/src/utils/requestQueue.ts b/frontend/src/utils/requestQueue.ts new file mode 100644 index 000000000..742e8d42c --- /dev/null +++ b/frontend/src/utils/requestQueue.ts @@ -0,0 +1,33 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + * 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited. + */ + +export class RequestQueue { + private queue: Array<() => Promise> = []; + private isProcessing = false; + + enqueue(request: () => Promise) { + this.queue.push(request); + this.processQueue(); + } + + private async processQueue() { + if (this.isProcessing) return; + + this.isProcessing = true; + + while (this.queue.length > 0) { + const request = this.queue.shift(); + if (request) { + await request(); + } + } + + this.isProcessing = false; + } +} From dbf1fb002f3a05c1ac3f3e701fed4371aa27f4eb Mon Sep 17 00:00:00 2001 From: auraofdivinity Date: Sun, 22 Sep 2024 18:33:01 +0530 Subject: [PATCH 2/4] fix: extracting debounced update to a custom hook --- .../components/visual-editor/v2/Diagrams.tsx | 61 ++++++++++++------- frontend/src/hooks/useDebouncedUpdate.tsx | 47 ++++++++++++++ frontend/src/utils/requestQueue.ts | 33 ---------- 3 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 frontend/src/hooks/useDebouncedUpdate.tsx delete mode 100644 frontend/src/utils/requestQueue.ts diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 8ea2e4405..1b0b51454 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -32,7 +32,13 @@ import { DiagramModel, DiagramModelGenerics, } from "@projectstorm/react-diagrams"; -import { SyntheticEvent, useEffect, useRef, useState } from "react"; +import { + SyntheticEvent, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { useTranslation } from "react-i18next"; import { DeleteDialog } from "@/app-components/dialogs"; @@ -45,13 +51,13 @@ import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useSearch } from "@/hooks/useSearch"; import { EntityType, Format } from "@/services/types"; import { IBlock } from "@/types/block.types"; -import { ICategory } from "@/types/category.types"; +import { ICategory, ICategoryAttributes } from "@/types/category.types"; import { BlockPorts } from "@/types/visual-editor.types"; import BlockDialog from "../BlockDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; -import { RequestQueue } from "@/utils/requestQueue"; +import useDebouncedUpdate from "@/hooks/useDebouncedUpdate"; const Diagrams = () => { const { t } = useTranslation(); @@ -110,26 +116,37 @@ const Diagrams = () => { invalidate: false, }); - const requestQueue = useRef(new RequestQueue()); - const enqueueUpdate = (id: string, params: any) => { - requestQueue.current.enqueue(() => updateCategory({ id, params })); - }; + const debouncedUpdateCategory = useDebouncedUpdate(updateCategory, 300); + const debouncedZoomEvent = useCallback( + (event: any) => { + if (selectedCategoryId) { + engine?.repaintCanvas(); + debouncedUpdateCategory({ + id: selectedCategoryId, + params: { + zoom: event.zoom, + }, + }); + } + event.stopPropagation(); + }, + [selectedCategoryId, debouncedUpdateCategory], + ); + const debouncedOffsetEvent = useCallback( + (event: any) => { + if (selectedCategoryId) { + debouncedUpdateCategory({ + id: selectedCategoryId, + params: { + offset: [event.offsetX, event.offsetY], + }, + }); + } + event.stopPropagation(); + }, + [selectedCategoryId, debouncedUpdateCategory], + ); - const debouncedZoomEvent = debounce((event) => { - if (selectedCategoryId) { - engine?.repaintCanvas(); - enqueueUpdate(selectedCategoryId, { zoom: event.zoom }); - } - event.stopPropagation(); - }, 200); - const debouncedOffsetEvent = debounce((event) => { - if (selectedCategoryId) { - enqueueUpdate(selectedCategoryId, { - offset: [event.offsetX, event.offsetY], - }); - } - event.stopPropagation(); - }, 200); const getBlockFromCache = useGetFromCache(EntityType.BLOCK); const updateCachedBlock = useUpdateCache(EntityType.BLOCK); const deleteCachedBlock = useDeleteFromCache(EntityType.BLOCK); diff --git a/frontend/src/hooks/useDebouncedUpdate.tsx b/frontend/src/hooks/useDebouncedUpdate.tsx new file mode 100644 index 000000000..5df92b46c --- /dev/null +++ b/frontend/src/hooks/useDebouncedUpdate.tsx @@ -0,0 +1,47 @@ +import { debounce } from "@mui/material"; +import { useCallback, useEffect, useRef } from "react"; + +type DebouncedUpdateParams = { + id: string; + params: Record; +}; + +function useDebouncedUpdate( + apiUpdate: (params: DebouncedUpdateParams) => void, + delay: number = 300, +) { + const accumulatedUpdates = useRef(null); + + const processUpdates = useRef( + debounce(() => { + if (accumulatedUpdates.current) { + apiUpdate(accumulatedUpdates.current); + accumulatedUpdates.current = null; + } + }, delay), + ).current; + + const handleUpdate = useCallback( + (params: DebouncedUpdateParams) => { + accumulatedUpdates.current = { + id: params.id, + params: { + ...(accumulatedUpdates.current?.params || {}), + ...params.params, + }, + }; + processUpdates(); + }, + [processUpdates], + ); + + useEffect(() => { + return () => { + processUpdates.clear(); + }; + }, [processUpdates]); + + return handleUpdate; +} + +export default useDebouncedUpdate; diff --git a/frontend/src/utils/requestQueue.ts b/frontend/src/utils/requestQueue.ts deleted file mode 100644 index 742e8d42c..000000000 --- a/frontend/src/utils/requestQueue.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2024 Hexastack. All rights reserved. - * - * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: - * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. - * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). - * 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited. - */ - -export class RequestQueue { - private queue: Array<() => Promise> = []; - private isProcessing = false; - - enqueue(request: () => Promise) { - this.queue.push(request); - this.processQueue(); - } - - private async processQueue() { - if (this.isProcessing) return; - - this.isProcessing = true; - - while (this.queue.length > 0) { - const request = this.queue.shift(); - if (request) { - await request(); - } - } - - this.isProcessing = false; - } -} From 08e5f6853bf6d361b71516198103e14b5030527f Mon Sep 17 00:00:00 2001 From: auraofdivinity Date: Sun, 22 Sep 2024 19:10:30 +0530 Subject: [PATCH 3/4] fix: fix linting errors --- frontend/src/components/visual-editor/v2/Diagrams.tsx | 9 +++------ frontend/src/hooks/useDebouncedUpdate.tsx | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/visual-editor/v2/Diagrams.tsx b/frontend/src/components/visual-editor/v2/Diagrams.tsx index 1b0b51454..431a40854 100644 --- a/frontend/src/components/visual-editor/v2/Diagrams.tsx +++ b/frontend/src/components/visual-editor/v2/Diagrams.tsx @@ -22,7 +22,6 @@ import { Tab, Tabs, Tooltip, - debounce, tabsClasses, } from "@mui/material"; import { @@ -47,17 +46,17 @@ import { useDelete, useDeleteFromCache } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; import { useGetFromCache } from "@/hooks/crud/useGet"; import { useUpdate, useUpdateCache } from "@/hooks/crud/useUpdate"; +import useDebouncedUpdate from "@/hooks/useDebouncedUpdate"; import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useSearch } from "@/hooks/useSearch"; import { EntityType, Format } from "@/services/types"; import { IBlock } from "@/types/block.types"; -import { ICategory, ICategoryAttributes } from "@/types/category.types"; +import { ICategory } from "@/types/category.types"; import { BlockPorts } from "@/types/visual-editor.types"; import BlockDialog from "../BlockDialog"; import { ZOOM_LEVEL } from "../constants"; import { useVisualEditor } from "../hooks/useVisualEditor"; -import useDebouncedUpdate from "@/hooks/useDebouncedUpdate"; const Diagrams = () => { const { t } = useTranslation(); @@ -115,7 +114,6 @@ const Diagrams = () => { const { mutateAsync: updateBlock } = useUpdate(EntityType.BLOCK, { invalidate: false, }); - const debouncedUpdateCategory = useDebouncedUpdate(updateCategory, 300); const debouncedZoomEvent = useCallback( (event: any) => { @@ -130,7 +128,7 @@ const Diagrams = () => { } event.stopPropagation(); }, - [selectedCategoryId, debouncedUpdateCategory], + [selectedCategoryId, engine, debouncedUpdateCategory], ); const debouncedOffsetEvent = useCallback( (event: any) => { @@ -146,7 +144,6 @@ const Diagrams = () => { }, [selectedCategoryId, debouncedUpdateCategory], ); - const getBlockFromCache = useGetFromCache(EntityType.BLOCK); const updateCachedBlock = useUpdateCache(EntityType.BLOCK); const deleteCachedBlock = useDeleteFromCache(EntityType.BLOCK); diff --git a/frontend/src/hooks/useDebouncedUpdate.tsx b/frontend/src/hooks/useDebouncedUpdate.tsx index 5df92b46c..eecb38f0f 100644 --- a/frontend/src/hooks/useDebouncedUpdate.tsx +++ b/frontend/src/hooks/useDebouncedUpdate.tsx @@ -11,7 +11,6 @@ function useDebouncedUpdate( delay: number = 300, ) { const accumulatedUpdates = useRef(null); - const processUpdates = useRef( debounce(() => { if (accumulatedUpdates.current) { @@ -20,7 +19,6 @@ function useDebouncedUpdate( } }, delay), ).current; - const handleUpdate = useCallback( (params: DebouncedUpdateParams) => { accumulatedUpdates.current = { From 95fd2cbe3ac34a7143f17ea0331eec1b93b8b29d Mon Sep 17 00:00:00 2001 From: auraofdivinity Date: Mon, 23 Sep 2024 14:30:52 +0530 Subject: [PATCH 4/4] fix: adding license details --- frontend/src/hooks/useDebouncedUpdate.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/hooks/useDebouncedUpdate.tsx b/frontend/src/hooks/useDebouncedUpdate.tsx index eecb38f0f..cb36ed9d9 100644 --- a/frontend/src/hooks/useDebouncedUpdate.tsx +++ b/frontend/src/hooks/useDebouncedUpdate.tsx @@ -1,3 +1,12 @@ +/* + * Copyright © 2024 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + * 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited. + */ + import { debounce } from "@mui/material"; import { useCallback, useEffect, useRef } from "react";