Files
lightController/src/vueLib/dbm/dbmTree.ts

265 lines
6.7 KiB
TypeScript

import { ref, computed, type Ref } from 'vue';
import { convertToSubscribes, type Subs } from '../models/Subscribe';
import { setValues, subscribe, unsubscribe } from '../services/websocket';
import {
findSubscriptionByPath,
addRawSubscriptions,
removeAllSubscriptions,
} from '../models/Subscriptions';
import { UpdateTable } from '..//dbm/updateTable';
import type { Response } from '..//models/Response';
import type { RawSubs } from '..//models/Subscribe';
import { getRequest } from '../models/Request';
import type { Pubs } from '../models/Publish';
const ZERO_UUID = '00000000-0000-0000-0000-000000000000';
export const dbmData = ref<TreeNode[]>([]);
export const expanded = ref<string[]>([]);
let lastExpanded: string[] = [];
export type TreeNode = {
path?: string;
key?: string;
lazy: boolean;
children?: TreeNode[];
};
type TreeMap = {
[key: string]: {
__children: TreeMap;
uuid?: string;
value?: string | number | boolean | null;
lazy: boolean;
};
};
const root: TreeMap = {};
export function buildTreeWithRawSubs(subs: RawSubs): TreeNode[] {
return buildTree(convertToSubscribes(subs));
}
export function buildTree(subs: Subs | null): TreeNode[] {
if (subs) {
for (const { path, uuid, value, hasChild } of subs) {
if (!path) continue;
const parts = path.split(':');
let current = root;
parts.forEach((part, idx) => {
if (!part) return;
if (!current[part]) current[part] = { __children: {}, lazy: true };
if (idx === parts.length - 1 && uuid) {
current[part].uuid = uuid;
current[part].value = value?.value ?? null;
current[part].lazy = !!hasChild;
}
current = current[part].__children;
});
}
}
const toTreeNodes = (map: TreeMap): TreeNode[] =>
Object.entries(map)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, node]) => ({
path: key,
key: node.uuid ?? key,
value: node.value,
lazy: node.lazy,
children: toTreeNodes(node.__children),
}));
const newTree = [
{
path: 'DBM',
key: ZERO_UUID,
lazy: true,
children: toTreeNodes(root),
},
];
dbmData.value.splice(0, dbmData.value.length, ...newTree);
return newTree;
}
export function removeNodes(pubs: Pubs) {
pubs.forEach((pub) => {
removeNode(pub.uuid);
});
}
export function removeNode(uuid: string) {
removeFromTreeMap(root, uuid);
removeFromTree(dbmData.value, uuid);
collapseNode(uuid);
}
function removeFromTreeMap(tree: TreeMap, uuid: string): boolean {
for (const [key, node] of Object.entries(tree)) {
if (node.uuid === uuid) {
delete tree[key];
return true;
}
if (removeFromTreeMap(node.__children, uuid)) return true;
}
return false;
}
function removeFromTree(nodes: TreeNode[], uuid: string): boolean {
const index = nodes.findIndex((n) => n.key === uuid);
if (index !== -1) {
nodes.splice(index, 1);
return true;
}
for (const node of nodes) {
if (node.children && removeFromTree(node.children, uuid)) {
if (node.children.length === 0) {
delete node.children;
node.lazy = true;
}
return true;
}
}
return false;
}
export function removeSubtreeByParentKey(parentKey: string) {
const recurse = (nodes: TreeNode[]): boolean => {
for (const node of nodes) {
if (node.key === parentKey) {
delete node.children;
node.lazy = true;
return true;
}
if (node.children && recurse(node.children)) return true;
}
return false;
};
recurse(dbmData.value);
}
function collapseNode(uuid: string) {
const idx = expanded.value.indexOf(uuid);
if (idx !== -1) {
expanded.value.splice(idx, 1);
const lastIdx = lastExpanded.indexOf(uuid);
if (lastIdx !== -1) lastExpanded.splice(lastIdx, 1);
onExpandedChange([...expanded.value]).catch(console.error);
}
}
export function updateValue(
NotifyResponse: (
response: Response | string | undefined,
type?: 'warning' | 'error',
timeout?: 5000,
) => void,
path1: string,
toggle?: Ref<boolean>,
path2?: string,
path3?: string,
value3?: number,
) {
return computed({
get() {
const path = toggle?.value && path2 ? path2 : path1;
return Number(findSubscriptionByPath(path)?.value ?? 0);
},
set(val) {
const baseValue = val;
const updates = [
{ path: toggle?.value && path2 ? path2 : path1, value: baseValue },
...(path3 ? [{ path: path3, value: value3 ?? baseValue }] : []),
];
setValues(updates)
.then((response) => NotifyResponse(response))
.catch((err) =>
NotifyResponse(`Failed to update [${[path1, path2, path3].join(' ')}]: ${err}`, 'error'),
);
},
});
}
export async function onExpandedChange(newExpanded: readonly string[]) {
const collapsed = lastExpanded.filter((k) => !newExpanded.includes(k));
const newlyExpanded = newExpanded.filter((k) => !lastExpanded.includes(k));
try {
await unsubscribe([{ path: '.*', depth: 0 }]).then(removeAllSubscriptions);
for (const key of collapsed) {
removeSubtreeByParentKey(key);
fetchAndUpdateNode(key);
}
for (const key of newlyExpanded) {
fetchAndUpdateNode(key);
}
lastExpanded = [...newExpanded];
} catch (err) {
console.error('error in expand function', err);
}
}
function fetchAndUpdateNode(key: string) {
getRequest(key, '', 2)
.then((resp) => {
if (resp) {
buildTreeWithRawSubs(resp);
subscribe([{ uuid: key, path: '', depth: 2 }]).catch((err) => console.error(err));
addRawSubscriptions(resp);
}
UpdateTable(key);
})
.catch((err) => console.error(err));
}
export function findParentKey(
childKey: string,
parentKey: string | null = null,
nodes?: TreeNode[],
): string | null {
if (!nodes) nodes = dbmData.value;
for (const node of nodes) {
if (node.key === childKey) return parentKey;
if (node.children) {
const found = findParentKey(childKey, node.key ?? null, node.children);
if (found) return found;
}
}
return null;
}
function getNodeUuidByPath(path: string, nodes?: TreeNode[]): string {
if (!nodes) nodes = dbmData.value;
for (const node of nodes) {
if (node.path === path) return node.key ?? '';
if (node.children) {
const found = getNodeUuidByPath(path, node.children);
if (found !== '') return found;
}
}
return '';
}
export function pathIsExpanded(path: string): boolean {
if (!path.includes(':')) {
return true;
}
let p = path.replace(/:.+$/, '');
if (expanded.value.includes(getNodeUuidByPath(p))) {
return true;
}
p = path.replace(/:.+$/, '');
return expanded.value.includes(getNodeUuidByPath(p));
}