From ffb8e4994e47095d2f242661b397a51840991634 Mon Sep 17 00:00:00 2001 From: Adrian Zuercher Date: Sat, 12 Jul 2025 23:29:44 +0200 Subject: [PATCH] optimize subscription model bug fixes --- .gitignore | 4 + package.json | 2 +- src/components/dbm/AddDatapoint.vue | 0 src/components/dbm/DBMTree.vue | 243 ++++++-------------- src/components/dbm/SubMenu.vue | 63 +++-- src/components/dbm/UpdateValue.vue | 13 -- src/components/dbm/dataTable.vue | 54 +++-- src/components/dialog/AddDatapoint.vue | 101 ++++++++ src/components/dialog/UpdateValueDialog.vue | 143 +++++++----- src/components/lights/LightBarCBL.vue | 25 +- src/components/lights/MovingHead.vue | 13 +- src/composables/dbm/dbmTree.ts | 138 ++++------- src/composables/dbm/useContextMenu.ts | 12 - src/models/Publish.ts | 10 +- src/models/Response.ts | 6 +- src/models/Set.ts | 2 +- src/models/Subscribe.ts | 40 +++- src/models/Subscriptions.ts | 67 ++++++ src/services/websocket.ts | 33 +-- src/vueLib/buttons/DataTypes.vue | 32 +++ src/vueLib/buttons/RadioButton.vue | 31 +++ src/vueLib/dialog/DialogFrame.vue | 65 ++++++ 22 files changed, 640 insertions(+), 457 deletions(-) delete mode 100644 src/components/dbm/AddDatapoint.vue delete mode 100644 src/components/dbm/UpdateValue.vue create mode 100644 src/components/dialog/AddDatapoint.vue delete mode 100644 src/composables/dbm/useContextMenu.ts create mode 100644 src/models/Subscriptions.ts create mode 100644 src/vueLib/buttons/DataTypes.vue create mode 100644 src/vueLib/buttons/RadioButton.vue create mode 100644 src/vueLib/dialog/DialogFrame.vue diff --git a/.gitignore b/.gitignore index c233544..c6324f2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules /src-cordova/www # Capacitor related directories and files +/src-capacitor /src-capacitor/www /src-capacitor/node_modules @@ -38,3 +39,6 @@ yarn-error.log* # local .log files *.log + +# golang quasar websever executable +backend/server-linux-arm64 diff --git a/package.json b/package.json index 1a47ab8..7753f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lightcontrol", - "version": "0.0.17", + "version": "0.0.18", "description": "A Tecamino App", "productName": "Light Control", "author": "A. Zuercher", diff --git a/src/components/dbm/AddDatapoint.vue b/src/components/dbm/AddDatapoint.vue deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dbm/DBMTree.vue b/src/components/dbm/DBMTree.vue index d2adc7d..c2a9214 100644 --- a/src/components/dbm/DBMTree.vue +++ b/src/components/dbm/DBMTree.vue @@ -16,101 +16,56 @@ - + - + diff --git a/src/components/dbm/SubMenu.vue b/src/components/dbm/SubMenu.vue index 3c96254..33fe24f 100644 --- a/src/components/dbm/SubMenu.vue +++ b/src/components/dbm/SubMenu.vue @@ -4,50 +4,49 @@ Add Datapoint - + Delete Datapoint + diff --git a/src/components/dbm/UpdateValue.vue b/src/components/dbm/UpdateValue.vue deleted file mode 100644 index af20b1c..0000000 --- a/src/components/dbm/UpdateValue.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/src/components/dbm/dataTable.vue b/src/components/dbm/dataTable.vue index cda59ad..d61f57e 100644 --- a/src/components/dbm/dataTable.vue +++ b/src/components/dbm/dataTable.vue @@ -14,21 +14,42 @@ > + - + diff --git a/src/components/dialog/AddDatapoint.vue b/src/components/dialog/AddDatapoint.vue new file mode 100644 index 0000000..5ac1e11 --- /dev/null +++ b/src/components/dialog/AddDatapoint.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/components/dialog/UpdateValueDialog.vue b/src/components/dialog/UpdateValueDialog.vue index e4506e3..2f8a5eb 100644 --- a/src/components/dialog/UpdateValueDialog.vue +++ b/src/components/dialog/UpdateValueDialog.vue @@ -1,42 +1,61 @@ + + diff --git a/src/components/lights/LightBarCBL.vue b/src/components/lights/LightBarCBL.vue index 6a70226..029e771 100644 --- a/src/components/lights/LightBarCBL.vue +++ b/src/components/lights/LightBarCBL.vue @@ -69,43 +69,30 @@ import { useQuasar } from 'quasar'; import LightSlider from './LightSlider.vue'; import { ref, onMounted, onUnmounted } from 'vue'; -import { subscribe, unsubscribe } from 'src/services/websocket'; +import { unsubscribe, subscribeToPath } from 'src/services/websocket'; import SettingDialog from 'src/components/lights/SettingDomeLight.vue'; import { NotifyResponse } from 'src/composables/notify'; -import { updateValue, buildTree, dbmData } from 'src/composables/dbm/dbmTree'; +import { updateValue } from 'src/composables/dbm/dbmTree'; +import { removeAllSubscriptions } from 'src/models/Subscriptions'; const $q = useQuasar(); const settings = ref(false); const brightness = updateValue('LightBar:Brightness', $q); const state = updateValue('LightBar:State', $q); onMounted(() => { - subscribe([ - { - path: 'LightBar:.*', - depth: 0, - }, - ]) - .then((response) => { - if (response?.subscribe) { - dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe)); - } else { - NotifyResponse($q, response); - } - }) - .catch((err) => { - NotifyResponse($q, err, 'error'); - }); + subscribeToPath($q, 'LightBar:.*'); }); onUnmounted(() => { unsubscribe([ { - path: '.*', + path: 'LightBar', depth: 0, }, ]).catch((err) => { NotifyResponse($q, err, 'error'); }); + removeAllSubscriptions(); }); function changeState() { diff --git a/src/components/lights/MovingHead.vue b/src/components/lights/MovingHead.vue index e1ed573..3b4b286 100644 --- a/src/components/lights/MovingHead.vue +++ b/src/components/lights/MovingHead.vue @@ -107,10 +107,11 @@ import { NotifyResponse } from 'src/composables/notify'; import { onBeforeUpdate, computed, onMounted, onUnmounted, ref } from 'vue'; import { subscribeToPath, unsubscribe, setValues } from 'src/services/websocket'; import { LocalStorage } from 'quasar'; -import { getSubscriptionsByPath, updateValue } from 'src/composables/dbm/dbmTree'; +import { updateValue } from 'src/composables/dbm/dbmTree'; import DragPad from 'src/components/lights/DragPad.vue'; import SettingDialog from './SettingMovingHead.vue'; import type { Settings } from 'src/models/MovingHead'; +import { findSubscriptionByPath, removeAllSubscriptions } from 'src/models/Subscriptions'; const $q = useQuasar(); const brightness = updateBrightnessValue('MovingHead:Brightness'); @@ -141,31 +142,29 @@ onUnmounted(() => { ]).catch((err) => { NotifyResponse($q, err, 'error'); }); + removeAllSubscriptions(); }); function changeState() { - console.log(55, brightness.value); - console.log(56, state.value); if (brightness.value === 0) { if (state.value === 0) { brightness.value = 255; return; } brightness.value = state.value; - console.log(57, brightness.value); return; } state.value = brightness.value; - console.log(58, state.value); brightness.value = 0; } function updateBrightnessValue(path: string) { return computed({ get() { - const sub = getSubscriptionsByPath(path); + const sub = findSubscriptionByPath(path); + if (!sub) return 0; if (!sub.value) return 0; - return Number(sub.value.value); + return Number(sub.value); }, set(val) { const setPaths = [{ path, value: val }]; diff --git a/src/composables/dbm/dbmTree.ts b/src/composables/dbm/dbmTree.ts index c57785f..948d0d0 100644 --- a/src/composables/dbm/dbmTree.ts +++ b/src/composables/dbm/dbmTree.ts @@ -1,110 +1,76 @@ -import type { Subs, Subscribe } from 'src/models/Subscribe'; -import type { Ref } from 'vue'; -import { nextTick, computed, reactive, ref } from 'vue'; -import { setValues } from 'src/services/websocket'; -import { NotifyResponse } from 'src/composables/notify'; +import { reactive, computed, type Ref } from 'vue'; import type { QVueGlobals } from 'quasar'; - -const Subscriptions = reactive>({}); +import type { Subs } from 'src/models/Subscribe'; +import { NotifyResponse } from '../notify'; +import { setValues } from 'src/services/websocket'; +import { findSubscriptionByPath } from 'src/models/Subscriptions'; export const dbmData = reactive([]); +//export const reactiveValues = new Map>(); -export interface TreeNode { +export type TreeNode = { path: string | undefined; key?: string; // optional: useful for QTree's node-key - value?: string | number | boolean | undefined; lazy: boolean; children?: TreeNode[]; -} +}; + +type TreeMap = { + [key: string]: { + __children: TreeMap; + uuid?: string; + value?: string | number | boolean | null; + lazy: boolean; + }; +}; + +const root: TreeMap = {}; export function buildTree(subs: Subs): TreeNode[] { - type TreeMap = { - [key: string]: { - __children: TreeMap; - uuid?: string; - value?: string | undefined; - lazy: boolean; - }; - }; + for (const { path, uuid, value, hasChild } of subs) { + if (!path) continue; - const root: TreeMap = {}; - - for (const item of subs) { - if (item.path) { - addNewSubscription(item); - } - const pathParts = item.path?.split(':') ?? []; + const parts = path.split(':'); let current = root; - for (let i = 0; i < pathParts.length; i++) { - const part = pathParts[i]; - - if (!part) continue; + parts.forEach((part, idx) => { + if (!part) return; if (!current[part]) { current[part] = { __children: {}, lazy: true }; } - // Optionally attach uuid only at the final part - if (i === pathParts.length - 1 && item.uuid) { - current[part].uuid = item.uuid; - current[part].value = item.value !== undefined ? String(item.value) : ''; - current[part].lazy = item.hasChild ?? false; + if (idx === parts.length - 1 && uuid) { + if (current[part].uuid === uuid) return; + + current[part].uuid = uuid; + current[part].value = value?.value ?? null; + current[part].lazy = hasChild ?? false; } + current = current[part].__children; - } + }); } - function convert(map: TreeMap): TreeNode[] { - return reactive( - Object.entries(map).map(([path, node]) => ({ - path, - key: node.uuid ?? path, // `key` is used by QTree - value: node.value, - lazy: node.lazy, - children: convert(node.__children), - })), - ); + function mapToTree(map: TreeMap): TreeNode[] { + return Object.entries(map).map(([key, node]) => ({ + path: key, + key: node.uuid ?? key, + value: node.value, + lazy: node.lazy, + children: mapToTree(node.__children), + })); } - return [ + const newTree = [ { path: 'DBM', key: '00000000-0000-0000-0000-000000000000', lazy: true, - children: convert(root), + children: mapToTree(root), }, ]; -} - -export function getTreeElementByPath(path: string) { - const sub = dbmData.find((s) => s.path === path); - return ref(sub); -} - -export function getSubscriptionsByUuid(uid: string) { - const sub = Object.values(Subscriptions).find((sub) => sub.uuid === uid); - return ref(sub); -} - -export function addChildrentoTree(subs: Subs) { - const ZERO_UUID = '00000000-0000-0000-0000-000000000000'; - const existingIds = new Set(Object.values(Subscriptions).map((sub) => sub.uuid)); - const newSubs = subs - .filter((sub) => sub.uuid !== ZERO_UUID) // Skip UUIDs with all zeroes - .filter((sub) => !existingIds.has(sub.uuid)); - - for (const sub of newSubs) { - if (sub.path !== undefined) { - Subscriptions[sub.path] = sub; - } else { - console.warn('Skipping sub with undefined path', sub); - } - } - - void nextTick(() => { - dbmData.splice(0, dbmData.length, ...buildTree(Object.values(Subscriptions))); - }); + return dbmData.splice(0, dbmData.length, ...newTree); } export function removeSubtreeByParentKey(parentKey: string) { @@ -122,23 +88,9 @@ export function removeSubtreeByParentKey(parentKey: string) { } return false; } - removeChildrenAndMarkLazy(dbmData, parentKey); } -export function getSubscriptionsByPath(path: string) { - return ref(Subscriptions[path]); -} - -export function addNewSubscription(sub: Subscribe) { - if (!sub.path) return; - Subscriptions[sub.path] = sub; -} - -export function getAllSubscriptions() { - return Object.values(Subscriptions); -} - export function updateValue( path1: string, $q: QVueGlobals, @@ -149,8 +101,8 @@ export function updateValue( ) { return computed({ get() { - const sub = getSubscriptionsByPath(toggle?.value && path2 ? path2 : path1); - return sub?.value ? Number(sub.value.value ?? 0) : 0; + const sub = findSubscriptionByPath(toggle?.value && path2 ? path2 : path1); + return sub?.value ? Number(sub.value ?? 0) : 0; }, set(val) { const baseValue = val; diff --git a/src/composables/dbm/useContextMenu.ts b/src/composables/dbm/useContextMenu.ts deleted file mode 100644 index 8ed739f..0000000 --- a/src/composables/dbm/useContextMenu.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ref } from 'vue'; -import type { TreeNode } from './dbmTree'; - -export const contextMenuRef = ref(); - -export const contextMenuState = ref(); - -export function openContextMenu(event: MouseEvent, node: undefined) { - event.preventDefault(); - contextMenuState.value = node; - contextMenuRef.value?.show(event); -} diff --git a/src/models/Publish.ts b/src/models/Publish.ts index 009c07d..13b0c81 100644 --- a/src/models/Publish.ts +++ b/src/models/Publish.ts @@ -3,6 +3,14 @@ export type Publish = { uuid: string; path: string; type: string; - value: undefined; + value: string | number | boolean | null; }; export type Pubs = Publish[]; + +import { updateSubscriptionValue } from './Subscriptions'; + +export function publishToSubscriptions(pubs: Pubs) { + pubs.forEach((pub) => { + updateSubscriptionValue(pub.uuid, pub.value); + }); +} diff --git a/src/models/Response.ts b/src/models/Response.ts index eecd8f8..5301ba2 100644 --- a/src/models/Response.ts +++ b/src/models/Response.ts @@ -1,13 +1,13 @@ import type { Gets } from './Get'; import type { Sets } from './Set'; -import type { Subs } from './Subscribe'; +import type { RawSubs } from './Subscribe'; import type { Pubs } from './Publish'; export type Response = { get?: Gets; set?: Sets; - subscribe?: Subs; - unsubscribe?: Subs; + subscribe?: RawSubs; + unsubscribe?: RawSubs; publish?: Pubs; error?: boolean; message?: string; diff --git a/src/models/Set.ts b/src/models/Set.ts index 94a937d..a7f2054 100644 --- a/src/models/Set.ts +++ b/src/models/Set.ts @@ -1,6 +1,6 @@ export type Set = { uuid?: string | undefined; - path: string; + path?: string; type?: string; value: string | number | boolean | undefined; create?: boolean; diff --git a/src/models/Subscribe.ts b/src/models/Subscribe.ts index d3dc684..142cd7d 100644 --- a/src/models/Subscribe.ts +++ b/src/models/Subscribe.ts @@ -1,10 +1,46 @@ -// API type (from backend) +import { ref } from 'vue'; +import type { Ref } from 'vue'; + export type Subscribe = { uuid?: string; path?: string; depth?: number; - value?: string | number | boolean; + type?: string; + drivers?: object | undefined; + value?: Ref; hasChild?: boolean; }; export type Subs = Subscribe[]; + +export type RawSubscribe = { + uuid?: string; + path?: string; + depth?: number; + value?: string | number | boolean | null; + hasChild?: boolean; +}; + +export type RawSubs = RawSubscribe[]; + +export function convertToSubscribe(raw: RawSubscribe): Subscribe { + return { + ...raw, + value: ref(raw.value ?? null), + }; +} + +export function convertToSubscribes(rawList: RawSubs): Subs { + const subs = rawList.map(convertToSubscribe); + return subs as Subs; +} + +export function convertToRaw(sub: Subscribe): RawSubscribe { + return { + ...(sub.uuid !== undefined ? { uuid: sub.uuid } : {}), + ...(sub.path !== undefined ? { path: sub.path } : {}), + ...(sub.depth !== undefined ? { depth: sub.depth } : {}), + ...(sub.value?.value !== undefined ? { value: sub.value.value } : {}), + ...(sub.hasChild !== undefined ? { hasChild: sub.hasChild } : {}), + }; +} diff --git a/src/models/Subscriptions.ts b/src/models/Subscriptions.ts new file mode 100644 index 0000000..629da25 --- /dev/null +++ b/src/models/Subscriptions.ts @@ -0,0 +1,67 @@ +import type { Ref } from 'vue'; +import { reactive, ref } from 'vue'; +import { convertToSubscribe } from 'src/models/Subscribe'; +import type { Subs, Subscribe, RawSubs } from 'src/models/Subscribe'; + +const EMPTYUUID = '00000000-0000-0000-0000-000000000000'; +export const Subscriptions = reactive>({}); + +export type TableSubscription = { + path: string; + value: Ref; +}; + +export function getRows(): Subs { + return Object.values(Subscriptions).map((sub) => { + sub.path = sub.path?.split(':').pop() ?? ''; + if (!sub.type) sub.type = 'None'; + else sub.type = sub.type.toLowerCase(); + return sub; + }); +} + +export function addSubscriptions(subs: RawSubs) { + subs.forEach((sub) => addSubscription(convertToSubscribe(sub))); +} + +function addSubscription(sub: Subscribe) { + if (EMPTYUUID === sub.uuid) { + sub.path = 'DBM'; + } + if (!sub.uuid) return; + Subscriptions[sub.uuid] = sub; +} + +export function updateSubscription(sub: Subscribe) { + if (!sub.uuid) return; + Subscriptions[sub.uuid] = sub; +} + +export function updateSubscriptionValue( + uuid: string, + value: string | number | boolean | null | undefined, +) { + if (!uuid) return; + if (!Subscriptions[uuid]) return; + Subscriptions[uuid].value = ref(value); +} + +export function removeSubscriptions(subs: RawSubs) { + subs.forEach((sub) => { + removeSubscription(sub.path); + }); +} + +export function removeAllSubscriptions() { + Object.keys(Subscriptions).forEach((key) => delete Subscriptions[key]); +} + +function removeSubscription(uuid: string | undefined) { + if (uuid === undefined) return; + if (!Subscriptions || Subscriptions[uuid] === undefined) return; + delete Subscriptions[uuid]; +} + +export function findSubscriptionByPath(path: string): Subscribe | undefined { + return Object.values(Subscriptions).find((sub) => sub.path === path); +} diff --git a/src/services/websocket.ts b/src/services/websocket.ts index 6f4af78..0ac28fb 100644 --- a/src/services/websocket.ts +++ b/src/services/websocket.ts @@ -1,19 +1,13 @@ import type { Response } from 'src/models/Response'; -import type { Publish } from 'src/models/Publish'; +import { publishToSubscriptions } from 'src/models/Publish'; import type { Request } from 'src/models/Request'; import type { QVueGlobals } from 'quasar'; -import { - buildTree, - dbmData, - getSubscriptionsByPath, - getAllSubscriptions, -} from 'src/composables/dbm/dbmTree'; import { ref } from 'vue'; -import type { Subs } from 'src/models/Subscribe'; +import { NotifyResponse } from 'src/composables/notify'; +import { type Subs } from 'src/models/Subscribe'; import type { Sets } from 'src/models/Set'; import type { PongMessage } from 'src/models/Pong'; -import { NotifyResponse } from 'src/composables/notify'; - +import { addSubscriptions } from 'src/models/Subscriptions'; const pendingResponses = new Map void>(); //const lastKnownValues: Record = reactive({}); @@ -91,13 +85,7 @@ export function initWebSocket(url: string, $q?: QVueGlobals) { } if (message.publish) { - (message.publish as Publish[]).forEach((pub) => { - const sub = getSubscriptionsByPath(pub.path); - if (sub.value && pub.value) { - sub.value.value = pub.value; - } - dbmData.splice(0, dbmData.length, ...buildTree(getAllSubscriptions())); // rebuild reactive tree - }); + publishToSubscriptions(message.publish); } } }; @@ -139,10 +127,6 @@ function waitForSocketConnection(): Promise { }); } -export function subscribe(data: Subs): Promise { - return send({ subscribe: data }); -} - export function subscribeToPath(q: QVueGlobals, path: string) { subscribe([ { @@ -151,9 +135,8 @@ export function subscribeToPath(q: QVueGlobals, path: string) { }, ]) .then((response) => { - console.log(response); if (response?.subscribe) { - dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe)); + addSubscriptions(response.subscribe); } else { NotifyResponse(q, response); } @@ -163,6 +146,10 @@ export function subscribeToPath(q: QVueGlobals, path: string) { }); } +export function subscribe(data: Subs): Promise { + return send({ subscribe: data }); +} + export function unsubscribe(data: Subs): Promise { return send({ unsubscribe: data }); } diff --git a/src/vueLib/buttons/DataTypes.vue b/src/vueLib/buttons/DataTypes.vue new file mode 100644 index 0000000..8acdec9 --- /dev/null +++ b/src/vueLib/buttons/DataTypes.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/vueLib/buttons/RadioButton.vue b/src/vueLib/buttons/RadioButton.vue new file mode 100644 index 0000000..24c75ec --- /dev/null +++ b/src/vueLib/buttons/RadioButton.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/vueLib/dialog/DialogFrame.vue b/src/vueLib/dialog/DialogFrame.vue new file mode 100644 index 0000000..e9402d9 --- /dev/null +++ b/src/vueLib/dialog/DialogFrame.vue @@ -0,0 +1,65 @@ + + + + +