2023-12-26 01:13:21 +08:00
|
|
|
import { initial, isEmpty, join, last, mapValues, size, slice, sortBy, split, toUpper } from 'lodash'
|
2023-12-25 16:22:29 +08:00
|
|
|
import useConnectionStore from 'stores/connections.js'
|
|
|
|
import { ConnectionType } from '@/consts/connection_type.js'
|
|
|
|
import { RedisDatabaseItem } from '@/objects/redisDatabaseItem.js'
|
|
|
|
import { KeyViewType } from '@/consts/key_view_type.js'
|
|
|
|
import { RedisNodeItem } from '@/objects/redisNodeItem.js'
|
|
|
|
import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* server connection state
|
|
|
|
*/
|
|
|
|
export class RedisServerState {
|
|
|
|
/**
|
|
|
|
* @typedef {Object} LoadingState
|
|
|
|
* @property {boolean} loading indicated that is loading children now
|
|
|
|
* @property {boolean} fullLoaded indicated that all children already loaded
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} name server name
|
|
|
|
* @param {number} db current opened database
|
2024-01-03 18:44:51 +08:00
|
|
|
* @param {number} reloadKey try to reload when changed
|
2023-12-25 16:22:29 +08:00
|
|
|
* @param {{}} stats current server status info
|
|
|
|
* @param {Object.<number, RedisDatabaseItem>} databases database list
|
|
|
|
* @param {string|null} patternFilter pattern filter
|
|
|
|
* @param {string|null} typeFilter redis type filter
|
|
|
|
* @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState
|
|
|
|
* @param {KeyViewType} viewType view type selection for all opened connections group by 'server'
|
2023-12-26 12:04:46 +08:00
|
|
|
* @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key"
|
2023-12-25 16:22:29 +08:00
|
|
|
*/
|
|
|
|
constructor({
|
|
|
|
name,
|
|
|
|
db = 0,
|
|
|
|
stats = {},
|
|
|
|
databases = {},
|
|
|
|
patternFilter = null,
|
|
|
|
typeFilter = null,
|
|
|
|
loadingState = {},
|
|
|
|
viewType = KeyViewType.Tree,
|
|
|
|
nodeMap = new Map(),
|
|
|
|
}) {
|
|
|
|
this.name = name
|
|
|
|
this.db = db
|
2024-01-03 18:44:51 +08:00
|
|
|
this.reloadKey = Date.now()
|
2023-12-25 16:22:29 +08:00
|
|
|
this.stats = stats
|
|
|
|
this.databases = databases
|
|
|
|
this.patternFilter = patternFilter
|
|
|
|
this.typeFilter = typeFilter
|
|
|
|
this.loadingState = loadingState
|
|
|
|
this.viewType = viewType
|
|
|
|
this.nodeMap = nodeMap
|
|
|
|
this.getRoot()
|
|
|
|
|
|
|
|
const connStore = useConnectionStore()
|
|
|
|
const { keySeparator } = connStore.getDefaultSeparator(name)
|
|
|
|
this.separator = isEmpty(keySeparator) ? ':' : keySeparator
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
this.stats = {}
|
|
|
|
this.patternFilter = null
|
|
|
|
this.typeFilter = null
|
|
|
|
this.nodeMap.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
closeDatabase() {
|
|
|
|
this.patternFilter = null
|
|
|
|
this.typeFilter = null
|
|
|
|
this.nodeMap.clear()
|
|
|
|
}
|
|
|
|
|
|
|
|
setDatabaseKeyCount(db, maxKeys) {
|
|
|
|
const dbInst = this.databases[db]
|
|
|
|
if (dbInst == null) {
|
|
|
|
this.databases[db] = new RedisDatabaseItem({ db, maxKeys })
|
|
|
|
} else {
|
|
|
|
dbInst.maxKeys = maxKeys
|
|
|
|
}
|
|
|
|
return dbInst
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* update max key by increase/decrease value
|
2023-12-26 01:13:21 +08:00
|
|
|
* @param {number} db
|
2023-12-25 16:22:29 +08:00
|
|
|
* @param {number} updateVal
|
|
|
|
*/
|
|
|
|
updateDBKeyCount(db, updateVal) {
|
2024-01-03 17:58:21 +08:00
|
|
|
const dbInst = this.databases[db]
|
2023-12-25 16:22:29 +08:00
|
|
|
if (dbInst != null) {
|
|
|
|
dbInst.maxKeys = Math.max(0, dbInst.maxKeys + updateVal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* set db max keys value
|
|
|
|
* @param {number} db
|
|
|
|
* @param {number} count
|
|
|
|
*/
|
|
|
|
setDBKeyCount(db, count) {
|
|
|
|
const dbInst = this.databases[db]
|
|
|
|
if (dbInst != null) {
|
|
|
|
dbInst.maxKeys = Math.max(0, count)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* get tree root item
|
|
|
|
* @returns {RedisNodeItem}
|
|
|
|
*/
|
|
|
|
getRoot() {
|
|
|
|
const rootKey = `${ConnectionType.RedisDB}`
|
|
|
|
let root = this.nodeMap.get(rootKey)
|
|
|
|
if (root == null) {
|
|
|
|
// create root node
|
|
|
|
root = new RedisNodeItem({
|
|
|
|
key: rootKey,
|
2023-12-27 18:23:40 +08:00
|
|
|
label: `db${this.db}`,
|
2023-12-25 16:22:29 +08:00
|
|
|
type: ConnectionType.RedisDB,
|
|
|
|
children: [],
|
|
|
|
})
|
|
|
|
this.nodeMap.set(rootKey, root)
|
|
|
|
}
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* get database list sort by db asc
|
|
|
|
* @return {RedisDatabaseItem[]}
|
|
|
|
*/
|
|
|
|
getDatabase() {
|
|
|
|
return sortBy(mapValues(this.databases), 'db')
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {ConnectionType} type
|
|
|
|
* @param {string} keyPath
|
|
|
|
* @param {RedisNodeItem} node
|
|
|
|
*/
|
|
|
|
addNode(type, keyPath, node) {
|
|
|
|
this.nodeMap.set(`${type}/${keyPath}`, node)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* add keys to current opened database
|
|
|
|
* @param {Array<string|number[]>|Set<string|number[]>} keys
|
|
|
|
* @param {boolean} [sortInsert]
|
|
|
|
* @return {{newKey: number, newLayer: number, success: boolean, replaceKey: number}}
|
|
|
|
*/
|
|
|
|
addKeyNodes(keys, sortInsert) {
|
|
|
|
const result = {
|
|
|
|
success: false,
|
|
|
|
newLayer: 0,
|
|
|
|
newKey: 0,
|
|
|
|
replaceKey: 0,
|
|
|
|
}
|
|
|
|
const root = this.getRoot()
|
|
|
|
|
|
|
|
if (this.viewType === KeyViewType.List) {
|
|
|
|
// construct list view data
|
|
|
|
for (const key of keys) {
|
|
|
|
const k = decodeRedisKey(key)
|
|
|
|
const isBinaryKey = k !== key
|
|
|
|
const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}`
|
|
|
|
const replaceKey = this.nodeMap.has(nodeKey)
|
|
|
|
const selectedNode = new RedisNodeItem({
|
|
|
|
key: `${this.name}/db${this.db}#${nodeKey}`,
|
|
|
|
label: k,
|
|
|
|
db: this.db,
|
|
|
|
keyCount: 0,
|
|
|
|
redisKey: k,
|
|
|
|
redisKeyCode: isBinaryKey ? key : undefined,
|
|
|
|
redisKeyType: undefined,
|
|
|
|
type: ConnectionType.RedisValue,
|
|
|
|
isLeaf: true,
|
|
|
|
})
|
|
|
|
this.nodeMap.set(nodeKey, selectedNode)
|
|
|
|
if (!replaceKey) {
|
|
|
|
root.addChild(selectedNode, sortInsert)
|
|
|
|
result.newKey += 1
|
|
|
|
} else {
|
|
|
|
result.replaceKey += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// construct tree view data
|
|
|
|
for (const key of keys) {
|
|
|
|
const k = decodeRedisKey(key)
|
|
|
|
const isBinaryKey = k !== key
|
|
|
|
const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, this.separator)
|
|
|
|
const len = size(keyParts)
|
|
|
|
const lastIdx = len - 1
|
|
|
|
let handlePath = ''
|
|
|
|
let node = root
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
handlePath += keyParts[i]
|
|
|
|
if (i !== lastIdx) {
|
|
|
|
// layer
|
|
|
|
const nodeKey = `${ConnectionType.RedisKey}/${handlePath}`
|
|
|
|
let selectedNode = this.nodeMap.get(nodeKey)
|
|
|
|
if (selectedNode == null) {
|
|
|
|
selectedNode = new RedisNodeItem({
|
|
|
|
key: `${this.name}/db${this.db}#${nodeKey}`,
|
|
|
|
label: keyParts[i],
|
|
|
|
db: this.db,
|
|
|
|
keyCount: 0,
|
|
|
|
redisKey: handlePath,
|
|
|
|
type: ConnectionType.RedisKey,
|
|
|
|
isLeaf: false,
|
|
|
|
children: [],
|
|
|
|
})
|
|
|
|
this.nodeMap.set(nodeKey, selectedNode)
|
|
|
|
node.addChild(selectedNode, sortInsert)
|
|
|
|
result.newLayer += 1
|
|
|
|
}
|
|
|
|
node = selectedNode
|
|
|
|
handlePath += this.separator
|
|
|
|
} else {
|
|
|
|
// key
|
|
|
|
const nodeKey = `${ConnectionType.RedisValue}/${handlePath}`
|
|
|
|
const replaceKey = this.nodeMap.has(nodeKey)
|
|
|
|
const selectedNode = new RedisNodeItem({
|
|
|
|
key: `${this.name}/db${this.db}#${nodeKey}`,
|
|
|
|
label: isBinaryKey ? k : keyParts[i],
|
|
|
|
db: this.db,
|
|
|
|
keyCount: 0,
|
|
|
|
redisKey: handlePath,
|
|
|
|
redisKeyCode: isBinaryKey ? key : undefined,
|
|
|
|
redisKeyType: undefined,
|
|
|
|
type: ConnectionType.RedisValue,
|
|
|
|
isLeaf: true,
|
|
|
|
})
|
|
|
|
this.nodeMap.set(nodeKey, selectedNode)
|
|
|
|
if (!replaceKey) {
|
|
|
|
node.addChild(selectedNode, sortInsert)
|
|
|
|
result.newKey += 1
|
|
|
|
} else {
|
|
|
|
result.replaceKey += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* rename key to a new name
|
|
|
|
* @param key
|
|
|
|
* @param newKey
|
|
|
|
*/
|
|
|
|
renameKey(key, newKey) {
|
|
|
|
const oldLayer = initial(key.split(this.separator)).join(this.separator)
|
|
|
|
const newLayer = initial(newKey.split(this.separator)).join(this.separator)
|
|
|
|
if (oldLayer !== newLayer) {
|
|
|
|
// also change layer
|
|
|
|
this.removeKeyNode(key, false)
|
|
|
|
const { success } = this.addKeyNodes([newKey], true)
|
|
|
|
if (success) {
|
|
|
|
this.tidyNode(newLayer)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// change key name only
|
|
|
|
const oldNodeKeyName = `${ConnectionType.RedisValue}/${key}`
|
|
|
|
const newNodeKeyName = `${ConnectionType.RedisValue}/${newKey}`
|
|
|
|
const keyNode = this.nodeMap.get(oldNodeKeyName)
|
|
|
|
keyNode.key = `${this.name}/db${this.db}#${newNodeKeyName}`
|
|
|
|
keyNode.label = last(split(newKey, this.separator))
|
|
|
|
keyNode.redisKey = newKey
|
|
|
|
// not support rename binary key name yet
|
|
|
|
// keyNode.redisKeyCode = []
|
|
|
|
this.nodeMap.set(newNodeKeyName, keyNode)
|
|
|
|
this.nodeMap.delete(oldNodeKeyName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* remove key node by key name
|
|
|
|
* @param {string} [key]
|
|
|
|
* @param {boolean} [isLayer]
|
|
|
|
* @return {boolean}
|
|
|
|
*/
|
|
|
|
removeKeyNode(key, isLayer) {
|
|
|
|
if (isLayer === true) {
|
|
|
|
this.deleteChildrenKeyNodes(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
const dbRoot = this.getRoot()
|
|
|
|
if (isEmpty(key)) {
|
|
|
|
// clear all key nodes
|
|
|
|
this.nodeMap.clear()
|
|
|
|
this.getRoot()
|
2024-01-03 17:58:21 +08:00
|
|
|
const dbInst = this.databases[this.db]
|
|
|
|
if (dbInst != null) {
|
|
|
|
dbInst.maxKeys = 0
|
|
|
|
dbInst.keyCount = 0
|
|
|
|
}
|
2023-12-25 16:22:29 +08:00
|
|
|
} else {
|
|
|
|
const keyParts = split(key, this.separator)
|
|
|
|
const totalParts = size(keyParts)
|
|
|
|
// remove from parent in tree node
|
|
|
|
const parentKey = slice(keyParts, 0, totalParts - 1)
|
|
|
|
let parentNode
|
|
|
|
if (isEmpty(parentKey)) {
|
|
|
|
parentNode = dbRoot
|
|
|
|
} else {
|
|
|
|
parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, this.separator)}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// not found parent node
|
|
|
|
if (parentNode == null) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
parentNode.removeChild({
|
|
|
|
type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue,
|
|
|
|
redisKey: key,
|
|
|
|
})
|
|
|
|
|
2023-12-26 01:13:21 +08:00
|
|
|
// // check and remove empty layer node
|
|
|
|
// let i = totalParts - 1
|
|
|
|
// for (; i >= 0; i--) {
|
|
|
|
// const anceKey = join(slice(keyParts, 0, i), this.separator)
|
|
|
|
// if (i > 0) {
|
|
|
|
// const anceNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
|
|
|
|
// const redisKey = join(slice(keyParts, 0, i + 1), this.separator)
|
|
|
|
// anceNode.removeChild({ type: ConnectionType.RedisKey, redisKey })
|
|
|
|
//
|
|
|
|
// if (isEmpty(anceNode.children)) {
|
|
|
|
// this.nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`)
|
|
|
|
// } else {
|
|
|
|
// break
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// // last one, remove from db node
|
|
|
|
// dbRoot.removeChild({ type: ConnectionType.RedisKey, redisKey: keyParts[0] })
|
|
|
|
// this.nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`)
|
|
|
|
// }
|
|
|
|
// }
|
2023-12-25 16:22:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* tidy node by key
|
|
|
|
* @param {string} [key]
|
|
|
|
* @param {boolean} [skipResort]
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
tidyNode(key, skipResort) {
|
2023-12-26 12:04:46 +08:00
|
|
|
const rootNode = this.getRoot()
|
2023-12-25 16:22:29 +08:00
|
|
|
const keyParts = split(key, this.separator)
|
|
|
|
const totalParts = size(keyParts)
|
|
|
|
let node
|
|
|
|
// find last exists ancestor key
|
|
|
|
let i = totalParts - 1
|
|
|
|
for (; i > 0; i--) {
|
|
|
|
const parentKey = join(slice(keyParts, 0, i), this.separator)
|
|
|
|
node = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
|
|
|
|
if (node != null) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (node == null) {
|
2023-12-26 12:04:46 +08:00
|
|
|
node = rootNode
|
2023-12-25 16:22:29 +08:00
|
|
|
}
|
|
|
|
const keyCountUpdated = node.tidy(skipResort)
|
|
|
|
if (keyCountUpdated) {
|
|
|
|
// update key count of parent and above
|
|
|
|
for (; i > 0; i--) {
|
|
|
|
const parentKey = join(slice(keyParts, 0, i), this.separator)
|
|
|
|
const parentNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`)
|
|
|
|
if (parentNode == null) {
|
|
|
|
break
|
|
|
|
}
|
2023-12-26 12:04:46 +08:00
|
|
|
const count = parentNode.reCalcKeyCount()
|
|
|
|
if (count <= 0) {
|
|
|
|
let anceKeyNode = rootNode
|
|
|
|
// remove from ancestor node
|
|
|
|
if (i > 1) {
|
|
|
|
const anceKey = join(slice(keyParts, 0, i - 1), this.separator)
|
|
|
|
anceKeyNode = this.nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`)
|
|
|
|
}
|
|
|
|
if (anceKeyNode != null) {
|
|
|
|
anceKeyNode.removeChild({ type: ConnectionType.RedisKey, redisKey: parentKey })
|
|
|
|
}
|
|
|
|
}
|
2023-12-25 16:22:29 +08:00
|
|
|
}
|
|
|
|
// update key count of db
|
|
|
|
const dbInst = this.databases[this.db]
|
|
|
|
if (dbInst != null) {
|
2023-12-26 12:04:46 +08:00
|
|
|
dbInst.keyCount = rootNode.reCalcKeyCount()
|
2023-12-25 16:22:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* add keys to current opened database
|
|
|
|
* @param {ConnectionType} type
|
|
|
|
* @param {string} keyPath
|
|
|
|
* @return {RedisNodeItem|null}
|
|
|
|
*/
|
|
|
|
getNode(type, keyPath) {
|
|
|
|
return this.nodeMap.get(`${type}/${keyPath}`) || null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* delete node and all it's children from nodeMap
|
|
|
|
* @param {string} [key] clean nodeMap if key is empty
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
deleteChildrenKeyNodes(key) {
|
|
|
|
if (isEmpty(key)) {
|
|
|
|
this.nodeMap.clear()
|
|
|
|
this.getRoot()
|
|
|
|
} else {
|
|
|
|
const nodeKey = `${ConnectionType.RedisKey}/${key}`
|
|
|
|
const node = this.nodeMap.get(nodeKey)
|
|
|
|
const children = node.children || []
|
|
|
|
for (const child of children) {
|
|
|
|
if (child.type === ConnectionType.RedisValue) {
|
|
|
|
if (!this.nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) {
|
|
|
|
console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`)
|
|
|
|
}
|
|
|
|
} else if (child.type === ConnectionType.RedisKey) {
|
|
|
|
this.deleteChildrenKeyNodes(child.redisKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!this.nodeMap.delete(nodeKey)) {
|
|
|
|
console.warn('delete map key', nodeKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getFilter() {
|
|
|
|
let pattern = this.patternFilter
|
|
|
|
if (isEmpty(pattern)) {
|
|
|
|
const conn = useConnectionStore()
|
|
|
|
pattern = conn.getDefaultKeyFilter(this.name)
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
match: pattern,
|
|
|
|
type: toUpper(this.typeFilter),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* set key filter
|
|
|
|
* @param {string} [pattern]
|
|
|
|
* @param {string} [type]
|
|
|
|
*/
|
|
|
|
setFilter({ pattern, type }) {
|
|
|
|
this.patternFilter = pattern === null ? this.patternFilter : pattern
|
|
|
|
this.typeFilter = type === null ? this.typeFilter : type
|
|
|
|
}
|
|
|
|
}
|