update
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
// electron/main/redis/commandLogger.ts
|
||||
// 命令日志记录器
|
||||
|
||||
export interface CommandEntry {
|
||||
id: number
|
||||
time: string
|
||||
connection: string
|
||||
command: string
|
||||
duration: number
|
||||
isWrite: boolean
|
||||
}
|
||||
|
||||
const MAX_ENTRIES = 5000
|
||||
let nextId = 1
|
||||
const entries: CommandEntry[] = []
|
||||
|
||||
const WRITE_COMMANDS = new Set([
|
||||
'SET', 'DEL', 'HSET', 'HDEL', 'RPUSH', 'LPUSH', 'LSET', 'LREM',
|
||||
'SADD', 'SREM', 'ZADD', 'ZREM', 'XADD', 'XDEL', 'XTRIM',
|
||||
'RENAME', 'EXPIRE', 'PERSIST', 'FLUSHDB', 'SELECT',
|
||||
'SETEX', 'INCR', 'DECR', 'INCRBY', 'DECRBY', 'APPEND',
|
||||
'HMSET', 'HINCRBY', 'HINCRBYFLOAT',
|
||||
'LPOP', 'RPOP', 'BLPOP', 'BRPOP',
|
||||
'ZINCRBY', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE',
|
||||
'UNLINK', 'MOVE', 'RENAMENX',
|
||||
])
|
||||
|
||||
export function log(connection: string, command: string, duration: number): void {
|
||||
const isWrite = WRITE_COMMANDS.has(command.split(' ')[0].toUpperCase())
|
||||
entries.push({
|
||||
id: nextId++,
|
||||
time: new Date().toISOString(),
|
||||
connection,
|
||||
command,
|
||||
duration,
|
||||
isWrite,
|
||||
})
|
||||
if (entries.length > MAX_ENTRIES) {
|
||||
entries.splice(0, entries.length - MAX_ENTRIES)
|
||||
}
|
||||
}
|
||||
|
||||
export function getLog(): CommandEntry[] {
|
||||
return entries
|
||||
}
|
||||
|
||||
export function clearLog(): void {
|
||||
entries.length = 0
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// electron/main/redis/connection.ts
|
||||
// Redis 连接管理(支持 SSH 隧道)
|
||||
import Redis, { RedisOptions } from 'ioredis'
|
||||
import { Client as SSHClient } from 'ssh2'
|
||||
import { createServer, type AddressInfo } from 'net'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve } from 'path'
|
||||
import os from 'os'
|
||||
|
||||
export interface ConnectionOptions {
|
||||
host: string
|
||||
port: number
|
||||
username?: string
|
||||
password?: string
|
||||
tls?: boolean
|
||||
tlsCaPath?: string
|
||||
tlsCertPath?: string
|
||||
tlsKeyPath?: string
|
||||
tlsRejectUnauthorized?: boolean
|
||||
sentinelEnabled?: boolean
|
||||
sentinelHost?: string
|
||||
sentinelPort?: number
|
||||
sentinelMasterName?: string
|
||||
sentinelNodePassword?: string
|
||||
cluster?: boolean
|
||||
sshEnabled?: boolean
|
||||
sshHost?: string
|
||||
sshPort?: number
|
||||
sshUsername?: string
|
||||
sshPassword?: string
|
||||
sshPrivateKeyPath?: string
|
||||
sshPassphrase?: string
|
||||
}
|
||||
|
||||
export type RedisClient = Redis
|
||||
|
||||
interface ClientEntry {
|
||||
client: RedisClient
|
||||
sshClient?: SSHClient
|
||||
tunnelServer?: ReturnType<typeof createServer>
|
||||
}
|
||||
|
||||
const clients = new Map<string, ClientEntry>()
|
||||
|
||||
function expandHomePath(p: string): string {
|
||||
if (p.startsWith('~')) {
|
||||
return resolve(os.homedir(), p.slice(p.startsWith('~/') ? 2 : 1))
|
||||
}
|
||||
return resolve(p)
|
||||
}
|
||||
|
||||
async function createSshTunnel(opts: ConnectionOptions): Promise<{ localPort: number; sshClient: SSHClient; server: ReturnType<typeof createServer> }> {
|
||||
const sshConfig: any = {
|
||||
host: opts.sshHost || opts.host,
|
||||
port: opts.sshPort || 22,
|
||||
username: opts.sshUsername || 'root',
|
||||
readyTimeout: 10000,
|
||||
keepaliveInterval: 10000,
|
||||
}
|
||||
|
||||
if (opts.sshPrivateKeyPath) {
|
||||
try {
|
||||
sshConfig.privateKey = readFileSync(expandHomePath(opts.sshPrivateKeyPath))
|
||||
if (opts.sshPassphrase) {
|
||||
sshConfig.passphrase = opts.sshPassphrase
|
||||
}
|
||||
} catch {
|
||||
// private key file not found, fall through to password auth
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.sshPassword) {
|
||||
sshConfig.password = opts.sshPassword
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const sshClient = new SSHClient()
|
||||
|
||||
sshClient.on('ready', () => {
|
||||
// Create a local TCP server to forward connections
|
||||
const server = createServer((socket) => {
|
||||
sshClient.forwardOut(
|
||||
'127.0.0.1',
|
||||
0,
|
||||
opts.host,
|
||||
opts.port,
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
socket.pipe(stream).pipe(socket)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const localPort = (server.address() as AddressInfo).port
|
||||
resolve({ localPort, sshClient, server })
|
||||
})
|
||||
|
||||
server.on('error', (err) => {
|
||||
sshClient.end()
|
||||
reject(new Error(`SSH tunnel server error: ${err.message}`))
|
||||
})
|
||||
})
|
||||
|
||||
sshClient.on('error', (err: Error) => {
|
||||
reject(new Error(`SSH connection failed: ${err.message}`))
|
||||
})
|
||||
|
||||
sshClient.connect(sshConfig)
|
||||
})
|
||||
}
|
||||
|
||||
function buildRedisOptions(opts: ConnectionOptions): RedisOptions {
|
||||
const options: RedisOptions = {
|
||||
host: opts.host,
|
||||
port: opts.port,
|
||||
username: opts.username || undefined,
|
||||
password: opts.password || undefined,
|
||||
stringNumbers: true,
|
||||
enableReadyCheck: true,
|
||||
family: 0,
|
||||
connectTimeout: 10000,
|
||||
retryStrategy(times: number) {
|
||||
if (times > 3) return null
|
||||
return Math.min(times * 200, 1000)
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
}
|
||||
|
||||
if (opts.tls) {
|
||||
const tlsOpts: any = {
|
||||
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||
checkServerIdentity: () => undefined,
|
||||
}
|
||||
if (opts.tlsCaPath) {
|
||||
try { tlsOpts.ca = readFileSync(expandHomePath(opts.tlsCaPath)) } catch { /* ignore */ }
|
||||
}
|
||||
if (opts.tlsCertPath) {
|
||||
try { tlsOpts.cert = readFileSync(expandHomePath(opts.tlsCertPath)) } catch { /* ignore */ }
|
||||
}
|
||||
if (opts.tlsKeyPath) {
|
||||
try { tlsOpts.key = readFileSync(expandHomePath(opts.tlsKeyPath)) } catch { /* ignore */ }
|
||||
}
|
||||
options.tls = tlsOpts
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export function getClient(id: string): RedisClient | undefined {
|
||||
return clients.get(id)?.client
|
||||
}
|
||||
|
||||
export function isClientConnected(id: string): boolean {
|
||||
const entry = clients.get(id)
|
||||
return entry?.client?.status === 'ready'
|
||||
}
|
||||
|
||||
export async function connect(id: string, opts: ConnectionOptions): Promise<void> {
|
||||
if (clients.has(id)) {
|
||||
await disconnect(id)
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let sshClient: SSHClient | undefined
|
||||
let tunnelServer: ReturnType<typeof createServer> | undefined
|
||||
|
||||
try {
|
||||
if (opts.sentinelEnabled && opts.sentinelMasterName) {
|
||||
// Sentinel mode: connect via sentinel
|
||||
const sentinelOptions: RedisOptions = {
|
||||
sentinels: [{ host: opts.sentinelHost || opts.host, port: opts.sentinelPort || 26379 }],
|
||||
name: opts.sentinelMasterName,
|
||||
sentinelPassword: opts.sentinelNodePassword || undefined,
|
||||
username: opts.username || undefined,
|
||||
password: opts.password || undefined,
|
||||
stringNumbers: true,
|
||||
enableReadyCheck: true,
|
||||
connectTimeout: 10000,
|
||||
retryStrategy(times: number) {
|
||||
if (times > 3) return null
|
||||
return Math.min(times * 200, 1000)
|
||||
},
|
||||
maxRetriesPerRequest: 3,
|
||||
}
|
||||
if (opts.tls) {
|
||||
sentinelOptions.tls = {
|
||||
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||
checkServerIdentity: () => undefined,
|
||||
}
|
||||
}
|
||||
const redis = new Redis(sentinelOptions)
|
||||
|
||||
redis.once('ready', () => {
|
||||
clients.set(id, { client: redis })
|
||||
resolve()
|
||||
})
|
||||
redis.once('error', (err: Error) => {
|
||||
redis.disconnect()
|
||||
reject(err)
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (!clients.has(id)) {
|
||||
redis.disconnect()
|
||||
reject(new Error('连接超时,请检查主机和端口'))
|
||||
}
|
||||
}, 12000)
|
||||
return
|
||||
}
|
||||
|
||||
if (opts.cluster) {
|
||||
// Cluster mode
|
||||
const clusterOptions: any = {
|
||||
redisOptions: {
|
||||
username: opts.username || undefined,
|
||||
password: opts.password || undefined,
|
||||
tls: opts.tls ? {
|
||||
rejectUnauthorized: opts.tlsRejectUnauthorized || false,
|
||||
checkServerIdentity: () => undefined,
|
||||
} : undefined,
|
||||
},
|
||||
clusterRetryStrategy(times: number) {
|
||||
if (times > 3) return null
|
||||
return Math.min(times * 200, 1000)
|
||||
},
|
||||
enableReadyCheck: true,
|
||||
}
|
||||
const redis = new Redis.Cluster([{ host: opts.host, port: opts.port }], clusterOptions)
|
||||
|
||||
redis.once('ready', () => {
|
||||
clients.set(id, { client: redis as any })
|
||||
resolve()
|
||||
})
|
||||
redis.once('error', (err: Error) => {
|
||||
redis.disconnect()
|
||||
reject(err)
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (!clients.has(id)) {
|
||||
redis.disconnect()
|
||||
reject(new Error('连接超时,请检查主机和端口'))
|
||||
}
|
||||
}, 12000)
|
||||
return
|
||||
}
|
||||
|
||||
if (opts.sshEnabled) {
|
||||
const tunnel = await createSshTunnel(opts)
|
||||
sshClient = tunnel.sshClient
|
||||
tunnelServer = tunnel.server
|
||||
// Redirect Redis connection through SSH tunnel
|
||||
opts = { ...opts, host: '127.0.0.1', port: tunnel.localPort }
|
||||
}
|
||||
|
||||
const redisOptions = buildRedisOptions(opts)
|
||||
const redis = new Redis(redisOptions)
|
||||
|
||||
redis.once('ready', () => {
|
||||
clients.set(id, { client: redis, sshClient, tunnelServer })
|
||||
resolve()
|
||||
})
|
||||
|
||||
redis.once('error', (err: Error) => {
|
||||
redis.disconnect()
|
||||
if (sshClient) sshClient.end()
|
||||
if (tunnelServer) tunnelServer.close()
|
||||
reject(err)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (!clients.has(id)) {
|
||||
redis.disconnect()
|
||||
if (sshClient) sshClient.end()
|
||||
if (tunnelServer) tunnelServer.close()
|
||||
reject(new Error('连接超时,请检查主机和端口'))
|
||||
}
|
||||
}, 12000)
|
||||
} catch (err: any) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function disconnect(id: string): Promise<void> {
|
||||
const entry = clients.get(id)
|
||||
if (entry) {
|
||||
entry.client.disconnect()
|
||||
if (entry.sshClient) entry.sshClient.end()
|
||||
if (entry.tunnelServer) entry.tunnelServer.close()
|
||||
clients.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectAll(): void {
|
||||
for (const [id] of clients) {
|
||||
disconnect(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// electron/main/redis/format.ts
|
||||
// 值格式转换(解压/编码/自定义)
|
||||
import { gunzipSync, inflateSync, brotliDecompressSync } from 'zlib'
|
||||
import { execFile } from 'child_process'
|
||||
import { writeFile, unlink, mkdtemp } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
export type FormatType = 'text' | 'hex' | 'json' | 'binary' | 'gzip' | 'deflate' | 'brotli' | 'deflateRaw'
|
||||
|
||||
export function decodeValue(value: string, format: FormatType): string {
|
||||
const buf = Buffer.from(value, 'utf-8')
|
||||
switch (format) {
|
||||
case 'hex':
|
||||
return buf.toString('hex').match(/.{1,2}/g)?.join(' ') ?? ''
|
||||
case 'binary':
|
||||
return buf.toString('binary')
|
||||
case 'json':
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
case 'gzip':
|
||||
try { return gunzipSync(buf).toString('utf-8') } catch { return '[Gzip decode failed]' }
|
||||
case 'deflate':
|
||||
try { return inflateSync(buf).toString('utf-8') } catch { return '[Deflate decode failed]' }
|
||||
case 'brotli':
|
||||
try { return brotliDecompressSync(buf).toString('utf-8') } catch { return '[Brotli decode failed]' }
|
||||
case 'deflateRaw':
|
||||
try { return inflateSync(buf).toString('utf-8') } catch { return '[DeflateRaw decode failed]' }
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export function detectFormat(value: string): FormatType {
|
||||
if (!value) return 'text'
|
||||
// JSON
|
||||
try { JSON.parse(value); return 'json' } catch { /* not json */ }
|
||||
// Gzip magic: 1f 8b
|
||||
if (value.charCodeAt(0) === 0x1f && value.charCodeAt(1) === 0x8b) return 'gzip'
|
||||
// Brotli magic (less reliable, check common patterns)
|
||||
// Deflate (zlib) magic: 78 01/9c/da
|
||||
if (value.charCodeAt(0) === 0x78 && [0x01, 0x9c, 0xda, 0x5e].includes(value.charCodeAt(1))) return 'deflate'
|
||||
// Check for non-printable characters → binary/hex
|
||||
const hasNonPrintable = /[\x00-\x08\x0e-\x1f]/.test(value.substring(0, 100))
|
||||
if (hasNonPrintable) return 'hex'
|
||||
return 'text'
|
||||
}
|
||||
|
||||
export async function customFormat(
|
||||
value: string,
|
||||
command: string,
|
||||
key?: string
|
||||
): Promise<string> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), 'jrdm-fmt-'))
|
||||
const tmpFile = join(tmpDir, 'value.bin')
|
||||
await writeFile(tmpFile, value, 'utf-8')
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const cmd = command
|
||||
.replace(/\{KEY\}/g, key || '')
|
||||
.replace(/\{VALUE\}/g, value)
|
||||
.replace(/\{HEX_FILE\}/g, tmpFile)
|
||||
|
||||
const parts = cmd.split(/\s+/)
|
||||
const exe = parts[0]
|
||||
const args = parts.slice(1)
|
||||
|
||||
execFile(exe, args, { timeout: 10000, maxBuffer: 10 * 1024 * 1024 }, async (err, stdout) => {
|
||||
await unlink(tmpFile).catch(() => {})
|
||||
if (err) {
|
||||
resolve(`[Custom formatter error: ${err.message}]`)
|
||||
} else {
|
||||
resolve(stdout)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// src/main/redis/hash.ts
|
||||
// Hash 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function getHashFields(
|
||||
id: string, key: string, cursor: string, count: number
|
||||
): Promise<{ cursor: string; fields: [string, string][] }> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
const [nextCursor, raw] = await client.hscan(key, cursor, 'COUNT', count)
|
||||
const fields: [string, string][] = []
|
||||
for (let i = 0; i < raw.length; i += 2) {
|
||||
fields.push([raw[i], raw[i + 1]])
|
||||
}
|
||||
return { cursor: nextCursor, fields }
|
||||
}
|
||||
|
||||
export async function setHashField(
|
||||
id: string, key: string, field: string, value: string
|
||||
): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.hset(key, field, value)
|
||||
}
|
||||
|
||||
export async function deleteHashField(id: string, key: string, field: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.hdel(key, field)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// src/main/redis/index.ts
|
||||
// Redis 服务统一导出
|
||||
export * from './connection'
|
||||
export * from './keys'
|
||||
export * from './string'
|
||||
export * from './hash'
|
||||
export * from './list'
|
||||
export * from './set'
|
||||
export * from './zset'
|
||||
export * from './stream'
|
||||
export * from './server'
|
||||
export * from './commandLogger'
|
||||
export * from './pubsub'
|
||||
export * from './format'
|
||||
@@ -0,0 +1,73 @@
|
||||
// src/main/redis/keys.ts
|
||||
// Key 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function getKeyType(id: string, key: string): Promise<string> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.type(key)
|
||||
}
|
||||
|
||||
export async function getKeyTTL(id: string, key: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.ttl(key)
|
||||
}
|
||||
|
||||
export async function deleteKey(id: string, key: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.del(key)
|
||||
}
|
||||
|
||||
export async function setKeyTTL(id: string, key: string, ttl: number): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
if (ttl < 0) {
|
||||
await client.persist(key)
|
||||
} else {
|
||||
await client.expire(key, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
export async function renameKey(id: string, oldKey: string, newKey: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.rename(oldKey, newKey)
|
||||
}
|
||||
|
||||
export async function existsKey(id: string, key: string): Promise<boolean> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return (await client.exists(key)) === 1
|
||||
}
|
||||
|
||||
export async function batchDelete(id: string, keys: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
if (keys.length === 0) return 0
|
||||
const pipeline = client.pipeline()
|
||||
for (const key of keys) {
|
||||
pipeline.del(key)
|
||||
}
|
||||
const results = await pipeline.exec()
|
||||
return results?.filter(([err]) => !err).length ?? 0
|
||||
}
|
||||
|
||||
export async function memoryUsage(id: string, key: string): Promise<number | null> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.memory('USAGE', key) as Promise<number | null>
|
||||
}
|
||||
|
||||
export async function scanKeys(
|
||||
id: string,
|
||||
cursor: string,
|
||||
pattern: string,
|
||||
count: number
|
||||
): Promise<{ cursor: string; keys: string[] }> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
const [nextCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count)
|
||||
return { cursor: nextCursor, keys }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// src/main/redis/list.ts
|
||||
// List 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function listRange(id: string, key: string, start: number, stop: number): Promise<string[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.lrange(key, start, stop)
|
||||
}
|
||||
|
||||
export async function listLength(id: string, key: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.llen(key)
|
||||
}
|
||||
|
||||
export async function listPush(id: string, key: string, ...values: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.rpush(key, ...values)
|
||||
}
|
||||
|
||||
export async function listSet(id: string, key: string, index: number, value: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.lset(key, index, value)
|
||||
}
|
||||
|
||||
export async function listRemove(id: string, key: string, count: number, value: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.lrem(key, count, value)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// electron/main/redis/pubsub.ts
|
||||
// 发布订阅 + MONITOR 流式接口
|
||||
import Redis from 'ioredis'
|
||||
import { getClient } from './connection'
|
||||
import type { WebContents } from 'electron'
|
||||
|
||||
const subClients = new Map<string, Redis>()
|
||||
const monitorClients = new Map<string, Redis>()
|
||||
|
||||
export function startSubscribe(
|
||||
id: string,
|
||||
channels: string[],
|
||||
isPattern: boolean,
|
||||
sender: WebContents
|
||||
): void {
|
||||
stopSubscribe(id)
|
||||
const existing = getClient(id)
|
||||
if (!existing) throw new Error('No active connection')
|
||||
|
||||
const sub = new Redis(existing.options)
|
||||
subClients.set(id, sub)
|
||||
|
||||
if (isPattern) {
|
||||
sub.psubscribe(...channels)
|
||||
sub.on('pmessage', (_pattern, channel, message) => {
|
||||
sender.send('redis:subscribeMessage', { channel, message, pattern: _pattern })
|
||||
})
|
||||
} else {
|
||||
sub.subscribe(...channels)
|
||||
sub.on('message', (channel, message) => {
|
||||
sender.send('redis:subscribeMessage', { channel, message })
|
||||
})
|
||||
}
|
||||
|
||||
sub.on('error', (err) => {
|
||||
sender.send('redis:subscribeMessage', { error: err.message })
|
||||
})
|
||||
}
|
||||
|
||||
export function stopSubscribe(id: string): void {
|
||||
const sub = subClients.get(id)
|
||||
if (sub) {
|
||||
sub.unsubscribe()
|
||||
sub.punsubscribe()
|
||||
sub.disconnect()
|
||||
subClients.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function startMonitor(id: string, sender: WebContents): void {
|
||||
stopMonitor(id)
|
||||
const existing = getClient(id)
|
||||
if (!existing) throw new Error('No active connection')
|
||||
|
||||
const monitor = new Redis(existing.options)
|
||||
monitorClients.set(id, monitor)
|
||||
|
||||
monitor.monitor().then((mon) => {
|
||||
mon.on('monitor', (time: string, args: string[], source: string, database: number) => {
|
||||
sender.send('redis:monitorMessage', { time, args, source, database })
|
||||
})
|
||||
mon.on('error', (err: Error) => {
|
||||
sender.send('redis:monitorMessage', { error: err.message })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function stopMonitor(id: string): void {
|
||||
const mon = monitorClients.get(id)
|
||||
if (mon) {
|
||||
mon.disconnect()
|
||||
monitorClients.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllPubSub(id: string): void {
|
||||
stopSubscribe(id)
|
||||
stopMonitor(id)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// src/main/redis/server.ts
|
||||
// 服务器操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function execute(id: string, command: string, args: string[]): Promise<unknown> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.call(command, ...args)
|
||||
}
|
||||
|
||||
export async function getServerInfo(id: string): Promise<string> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.info() as Promise<string>
|
||||
}
|
||||
|
||||
export async function selectDb(id: string, db: number): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.select(db)
|
||||
}
|
||||
|
||||
export async function dbSize(id: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.dbsize() as Promise<number>
|
||||
}
|
||||
|
||||
export async function ping(id: string): Promise<string> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.ping()
|
||||
}
|
||||
|
||||
export async function slowLogGet(id: string, count: number): Promise<any[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.slowlog('GET', count) as Promise<any[]>
|
||||
}
|
||||
|
||||
export async function slowLogLen(id: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.slowlog('LEN') as Promise<number>
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// src/main/redis/set.ts
|
||||
// Set 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function setMembers(id: string, key: string): Promise<string[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.smembers(key)
|
||||
}
|
||||
|
||||
export async function setSize(id: string, key: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.scard(key)
|
||||
}
|
||||
|
||||
export async function setAdd(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.sadd(key, ...members)
|
||||
}
|
||||
|
||||
export async function setRemove(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.srem(key, ...members)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// src/main/redis/stream.ts
|
||||
// Stream 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function streamInfo(id: string, key: string): Promise<any> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xinfo('STREAM', key)
|
||||
}
|
||||
|
||||
export async function streamRange(
|
||||
id: string, key: string, start: string, end: string, count: number
|
||||
): Promise<any[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xrange(key, start, end, 'COUNT', count)
|
||||
}
|
||||
|
||||
export async function streamAdd(
|
||||
id: string, key: string, streamId: string, fieldValues: string[]
|
||||
): Promise<string | null> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xadd(key, streamId, ...fieldValues)
|
||||
}
|
||||
|
||||
export async function streamTrim(id: string, key: string, maxLen: number): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xtrim(key, 'MAXLEN', maxLen)
|
||||
}
|
||||
|
||||
export async function streamDel(id: string, key: string, ...ids: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xdel(key, ...ids)
|
||||
}
|
||||
|
||||
// Consumer group operations
|
||||
export async function streamGroups(id: string, key: string): Promise<any[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xinfo('GROUPS', key) as Promise<any[]>
|
||||
}
|
||||
|
||||
export async function streamConsumers(id: string, key: string, group: string): Promise<any[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.xinfo('CONSUMERS', key, group) as Promise<any[]>
|
||||
}
|
||||
|
||||
export async function streamGroupCreate(
|
||||
id: string, key: string, group: string, startId: string, mkstream: boolean
|
||||
): Promise<string> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
const args: any[] = [key, group, startId]
|
||||
if (mkstream) args.push('MKSTREAM')
|
||||
return (client as any).xgroup('CREATE', ...args)
|
||||
}
|
||||
|
||||
export async function streamGroupDestroy(id: string, key: string, group: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return (client as any).xgroup('DESTROY', key, group)
|
||||
}
|
||||
|
||||
export async function streamAck(id: string, key: string, group: string, ...ids: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return (client as any).xack(key, group, ...ids)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// src/main/redis/string.ts
|
||||
// String + ReJson 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function getStringValue(id: string, key: string): Promise<string | null> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.get(key)
|
||||
}
|
||||
|
||||
export async function setStringValue(id: string, key: string, value: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await client.set(key, value)
|
||||
}
|
||||
|
||||
// ReJson operations
|
||||
export async function rejsonGet(id: string, key: string, path: string = '.'): Promise<string> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return (client as any).call('JSON.GET', key, path)
|
||||
}
|
||||
|
||||
export async function rejsonSet(id: string, key: string, path: string, value: string): Promise<void> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
await (client as any).call('JSON.SET', key, path, value)
|
||||
}
|
||||
|
||||
export async function rejsonDel(id: string, key: string, path: string = '.'): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return (client as any).call('JSON.DEL', key, path)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// src/main/redis/zset.ts
|
||||
// Sorted Set 操作
|
||||
import { getClient } from './connection'
|
||||
|
||||
export async function zsetRange(id: string, key: string, start: number, stop: number, withScores: boolean): Promise<string[]> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
if (withScores) return client.zrange(key, start, stop, 'WITHSCORES')
|
||||
return client.zrange(key, start, stop)
|
||||
}
|
||||
|
||||
export async function zsetSize(id: string, key: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zcard(key)
|
||||
}
|
||||
|
||||
export async function zsetAdd(id: string, key: string, score: number, member: string): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zadd(key, score, member)
|
||||
}
|
||||
|
||||
export async function zsetRemove(id: string, key: string, ...members: string[]): Promise<number> {
|
||||
const client = getClient(id)
|
||||
if (!client) throw new Error(`Client ${id} not found`)
|
||||
return client.zrem(key, ...members)
|
||||
}
|
||||
Reference in New Issue
Block a user