refactor: add node map to store all tree node by tree key, improve speed add and delete node

refactor: improve speed of load all tree node
This commit is contained in:
tiny-craft 2023-07-07 02:23:33 +08:00
parent 9cd0d34c5d
commit f85b381992
4 changed files with 141 additions and 111 deletions

View File

@ -266,6 +266,10 @@ const onLoadTree = async (node) => {
loading.value = false loading.value = false
} }
break break
case ConnectionType.RedisKey:
// load all children
// node.children = []
break
} }
} }

View File

@ -323,7 +323,6 @@ const handleDrop = ({ node, dragNode, dropPosition }) => {
:cancelable="false" :cancelable="false"
:draggable="true" :draggable="true"
:data="connectionStore.connections" :data="connectionStore.connections"
:expand-on-click="true"
:expanded-keys="expandedKeys" :expanded-keys="expandedKeys"
@update:selected-keys="onUpdateSelectedKeys" @update:selected-keys="onUpdateSelectedKeys"
:node-props="nodeProps" :node-props="nodeProps"

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { endsWith, findIndex, get, isEmpty, last, remove, size, sortedIndexBy, split, uniq } from 'lodash' import { endsWith, findIndex, get, isEmpty, last, size, split, uniq } from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -29,7 +29,6 @@ import {
} from '../../wailsjs/go/services/connectionService.js' } from '../../wailsjs/go/services/connectionService.js'
import { ConnectionType } from '../consts/connection_type.js' import { ConnectionType } from '../consts/connection_type.js'
import useTabStore from './tab.js' import useTabStore from './tab.js'
import { nextTick } from 'vue'
const separator = ':' const separator = ':'
@ -51,18 +50,28 @@ const useConnectionStore = defineStore('connections', {
* @property {number} type * @property {number} type
* @property {number} [db] - database index, type == ConnectionType.RedisDB only * @property {number} [db] - database index, type == ConnectionType.RedisDB only
* @property {number} keys * @property {number} keys
* @property {boolean} [isLeaf]
* @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only
* @property {boolean} [expanded] - current node is expanded * @property {boolean} [expanded] - current node is expanded
*/ */
/**
* @typedef {Object} ConnectionState
* @property {string[]} groups
* @property {Object.<string, DatabaseItem[]>} databases
* @property {ConnectionItem[]} connections
* @property {Object.<string, Map<string, DatabaseItem>>} nodeMap key format likes 'server#db', children key format likes 'key#type'
*/
/** /**
* *
* @returns {{groups: string[], databases: Object<string, DatabaseItem[]>, connections: ConnectionItem[]}} * @returns {ConnectionState}
*/ */
state: () => ({ state: () => ({
groups: [], // all group name set groups: [], // all group name set
connections: [], // all connections connections: [], // all connections
databases: {}, // all databases in opened connections group by server name databases: {}, // all databases in opened connections group by server name
nodeMap: {}, // all node in opened connections group by server+db and key+type
}), }),
getters: { getters: {
anyConnectionOpened() { anyConnectionOpened() {
@ -384,15 +393,16 @@ const useConnectionStore = defineStore('connections', {
throw new Error(msg) throw new Error(msg)
} }
const { keys = [] } = data const { keys = [] } = data
const dbs = this.databases[connName]
dbs[db].opened = true
if (isEmpty(keys)) { if (isEmpty(keys)) {
const dbs = this.databases[connName]
dbs[db].children = [] dbs[db].children = []
dbs[db].opened = true
return return
} }
// append db node to current connection's children // append db node to current connection's children
this._updateNodeChildren(connName, db, keys) this._addKeyNodes(connName, db, keys)
this._tidyNodeChildren(dbs[db])
}, },
/** /**
@ -405,6 +415,8 @@ const useConnectionStore = defineStore('connections', {
const dbs = this.databases[connName] const dbs = this.databases[connName]
dbs[db].children = undefined dbs[db].children = undefined
dbs[db].isLeaf = false dbs[db].isLeaf = false
delete this.nodeMap[`${connName}#${db}`]
}, },
/** /**
@ -473,7 +485,8 @@ const useConnectionStore = defineStore('connections', {
// remove current keys below prefix // remove current keys below prefix
this._deleteKeyNodes(connName, db, prefix) this._deleteKeyNodes(connName, db, prefix)
this._updateNodeChildren(connName, db, keys) this._addKeyNodes(connName, db, keys)
this._tidyNodeChildren(this.databases[connName][db])
}, },
/** /**
@ -513,85 +526,99 @@ const useConnectionStore = defineStore('connections', {
* @param {string[]} keys * @param {string[]} keys
* @private * @private
*/ */
_updateNodeChildren(connName, db, keys) { _addKeyNodes(connName, db, keys) {
// find match key node in node list
const findNodeByKey = (nodes, key) => {
const idx = findIndex(nodes, { key })
return idx !== -1 ? nodes[idx] : null
}
// insert child to children list by order
const sortedInsertChild = (childrenList, item) => {
const insertIdx = sortedIndexBy(childrenList, item, 'key')
childrenList.splice(insertIdx, 0, item)
// childrenList.push(item)
}
// update all node item's children num
const updateChildrenNum = (node) => {
let count = 0
const totalChildren = size(node.children)
if (totalChildren > 0) {
for (const elem of node.children) {
updateChildrenNum(elem)
count += elem.keys
}
} else {
count += 1
}
node.keys = count
// node.children = sortBy(node.children, 'label')
}
const dbs = this.databases[connName] const dbs = this.databases[connName]
if (dbs[db].children == null) { if (dbs[db].children == null) {
dbs[db].children = [] dbs[db].children = []
} }
const keyStruct = dbs[db].children if (this.nodeMap[`${connName}#${db}`] == null) {
this.nodeMap[`${connName}#${db}`] = new Map()
}
// construct tree node list, the format of item key likes 'server/db#type/key'
const nodeMap = this.nodeMap[`${connName}#${db}`]
const rootChildren = dbs[db].children
let count = 0
for (const key of keys) { for (const key of keys) {
const keyPart = split(key, separator) const keyPart = split(key, separator)
// const prefixLen = size(keyPart) - 1 // const prefixLen = size(keyPart) - 1
const len = size(keyPart) const len = size(keyPart)
const lastIdx = len - 1
let handlePath = '' let handlePath = ''
let ks = keyStruct let children = rootChildren
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
handlePath += keyPart[i] handlePath += keyPart[i]
if (i !== len - 1) { if (i !== lastIdx) {
// layer // layer
const curKey = `${connName}/db${db}/${handlePath}@${ConnectionType.RedisKey}` const nodeKey = `#${ConnectionType.RedisKey}/${handlePath}`
let selectedNode = findNodeByKey(ks, curKey) let selectedNode = nodeMap.get(nodeKey)
if (selectedNode == null) { if (selectedNode == null) {
selectedNode = { selectedNode = {
key: curKey, key: `${connName}/db${db}${nodeKey}`,
label: keyPart[i], label: keyPart[i],
name: connName, name: connName,
db, db,
keys: 0, keys: 0,
redisKey: handlePath, redisKey: handlePath,
type: ConnectionType.RedisKey, type: ConnectionType.RedisKey,
isLeaf: false,
children: [], children: [],
} }
sortedInsertChild(ks, selectedNode) nodeMap.set(nodeKey, selectedNode)
children.push(selectedNode)
} }
ks = selectedNode.children children = selectedNode.children
handlePath += separator handlePath += separator
} else { } else {
// key // key
const curKey = `${connName}/db${db}/${handlePath}@${ConnectionType.RedisValue}` const nodeKey = `#${ConnectionType.RedisValue}/${handlePath}`
const selectedNode = { const selectedNode = {
key: curKey, key: `${connName}/db${db}${nodeKey}`,
label: keyPart[i], label: keyPart[i],
name: connName, name: connName,
db, db,
keys: 0, keys: 0,
redisKey: handlePath, redisKey: handlePath,
type: ConnectionType.RedisValue, type: ConnectionType.RedisValue,
isLeaf: true,
} }
sortedInsertChild(ks, selectedNode) nodeMap.set(nodeKey, selectedNode)
children.push(selectedNode)
} }
} }
console.log('count:', ++count)
} }
},
dbs[db].opened = true /**
updateChildrenNum(dbs[db]) *
* @param {DatabaseItem[]} nodeList
* @private
*/
_sortNodes(nodeList) {
nodeList.sort((a, b) => {
return a.key > b.key ? 1 : -1
})
},
/**
* sort all node item's children and calculate keys count
* @param node
* @private
*/
_tidyNodeChildren(node) {
let count = 0
const totalChildren = size(node.children)
if (totalChildren > 0) {
this._sortNodes(node.children)
for (const elem of node.children) {
this._tidyNodeChildren(elem)
count += elem.keys
}
} else {
count += 1
}
node.keys = count
}, },
/** /**
@ -626,13 +653,13 @@ const useConnectionStore = defineStore('connections', {
const treeKey = get(nodeList[j], 'key') const treeKey = get(nodeList[j], 'key')
const isLast = j >= len - 1 const isLast = j >= len - 1
const keyType = isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey const keyType = isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
const currentKey = `${connName}/db${db}/${redisKey}@${keyType}` const curKey = `${connName}/db${db}#${keyType}/${redisKey}`
if (treeKey > currentKey || isLast) { if (treeKey > curKey || isLast) {
// out of search range, add new item // out of search range, add new item
if (isLastKeyPart) { if (isLastKeyPart) {
// key not exists, add new one // key not exists, add new one
const item = { const item = {
key: currentKey, key: curKey,
label: keyPart[i], label: keyPart[i],
name: connName, name: connName,
db, db,
@ -649,7 +676,7 @@ const useConnectionStore = defineStore('connections', {
} else { } else {
// layer not exists, add new one // layer not exists, add new one
const item = { const item = {
key: currentKey, key: curKey,
label: keyPart[i], label: keyPart[i],
name: connName, name: connName,
db, db,
@ -669,7 +696,7 @@ const useConnectionStore = defineStore('connections', {
added = true added = true
} }
break break
} else if (treeKey === currentKey) { } else if (treeKey === curKey) {
if (isLastKeyPart) { if (isLastKeyPart) {
// same key exists, do nothing // same key exists, do nothing
console.log('TODO: same key exist, do nothing now, should replace value later') console.log('TODO: same key exist, do nothing now, should replace value later')
@ -716,7 +743,9 @@ const useConnectionStore = defineStore('connections', {
const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl) const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl)
if (success) { if (success) {
// update tree view data // update tree view data
this._addKey(connName, db, key) // this._addKey(connName, db, key)
this._addKeyNodes(connName, db, [key])
this._tidyNodeChildren(this.databases[connName][db])
return { success } return { success }
} else { } else {
return { success, msg } return { success, msg }
@ -1069,61 +1098,49 @@ const useConnectionStore = defineStore('connections', {
return return
} }
const descendantChain = [dbDetail] const nodeMap = this.nodeMap[`${connName}#${db}`]
const keyPart = split(key, separator) if (nodeMap == null) {
let redisKey = '' return
const keyLen = size(keyPart) }
let deleted = false const idx = key.lastIndexOf(separator)
let forceBreak = false let parentNode = null
for (let i = 0; i < keyLen && !forceBreak; i++) { let parentKey = ''
redisKey += keyPart[i] if (idx === -1) {
// root
const node = last(descendantChain) parentNode = dbDetail
const nodeList = get(node, 'children', []) } else {
const len = size(nodeList) parentKey = key.substring(0, idx)
const isLastKeyPart = i === keyLen - 1 parentNode = nodeMap.get(`#${ConnectionType.RedisKey}/${parentKey}`)
for (let j = 0; j < len; j++) {
const treeKey = get(nodeList[j], 'key')
const keyType = isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey
const currentKey = `${connName}/db${db}/${redisKey}@${keyType}`
if (treeKey > currentKey) {
// out of search range, target not exists
forceBreak = true
break
} else if (treeKey === currentKey) {
if (isLastKeyPart) {
// find target
nodeList.splice(j, 1)
node.keys -= 1
deleted = true
forceBreak = true
} else {
// find into it's children
descendantChain.push(nodeList[j])
redisKey += separator
}
break
}
}
if (forceBreak) {
break
}
} }
// console.log(JSON.stringify(descendantChain))
// update ancestor node's info if (parentNode == null || parentNode.children == null) {
if (deleted) { return
const desLen = size(descendantChain) }
for (let i = desLen - 1; i > 0; i--) {
const children = get(descendantChain[i], 'children', []) // remove children
const parent = descendantChain[i - 1] const delIdx = findIndex(parentNode.children, { redisKey: key })
if (isEmpty(children)) { if (delIdx !== -1) {
const parentChildren = get(parent, 'children', []) const childKeys = parentNode.children[delIdx].keys || 1
const k = get(descendantChain[i], 'key') parentNode.children.splice(delIdx, 1)
remove(parentChildren, (item) => item.key === k) parentNode.keys = Math.max(parentNode.keys - childKeys, 0)
}
// also remove parent node if no more children
while (isEmpty(parentNode.children)) {
const idx = parentKey.lastIndexOf(separator)
if (idx !== -1) {
parentKey = parentKey.substring(0, idx)
parentNode = nodeMap.get(`#${ConnectionType.RedisKey}/${parentKey}`)
if (parentNode != null) {
parentNode.keys = (parentNode.keys || 1) - 1
parentNode.children = []
} }
parent.keys -= 1 } else {
// reach root, remove from db
const delIdx = findIndex(dbDetail.children, { redisKey: parentKey })
dbDetail.keys = (dbDetail.keys || 1) - 1
dbDetail.children.splice(delIdx, 1)
break
} }
} }
}, },
@ -1169,11 +1186,17 @@ const useConnectionStore = defineStore('connections', {
} }
const { data, success, msg } = await DeleteKey(connName, db, prefix) const { data, success, msg } = await DeleteKey(connName, db, prefix)
if (success) { if (success) {
const { deleted: keys = [] } = data // const { deleted: keys = [] } = data
for (const key of keys) { // for (const key of keys) {
await this._deleteKeyNode(connName, db, key) // await this._deleteKeyNode(connName, db, key)
await nextTick() // }
if (endsWith(prefix, '*')) {
prefix = prefix.substring(0, prefix.length - 1)
} }
if (endsWith(prefix, separator)) {
prefix = prefix.substring(0, prefix.length - 1)
}
await this._deleteKeyNode(connName, db, prefix)
return true return true
} }
} finally { } finally {

View File

@ -107,6 +107,10 @@ body {
gap: 10px; gap: 10px;
} }
.n-tree-node-content__text {
@extend .ellipsis;
}
.context-menu-item { .context-menu-item {
min-width: 100px; min-width: 100px;
padding-right: 10px; padding-right: 10px;