fix: 连接断开后 IPC handler 不再报 Client null / Connection closed

- connection.ts: 新增 requireClient(id),状态非 ready 时抛明确错误
- connection.ts: Redis client 监听 close 事件自动从 Map 清理
- 8 个 redis 模块: getClient + null check 统一替换为 requireClient
- sentinel/cluster/普通连接三种模式均添加 close 自动清理
This commit is contained in:
2026-07-11 22:31:55 +08:00
parent 0ffe3ae83f
commit b7e9c80022
10 changed files with 82 additions and 105 deletions
+24
View File
@@ -153,6 +153,14 @@ export function getClient(id: string): RedisClient | undefined {
return clients.get(id)?.client
}
export function requireClient(id: string): RedisClient {
const entry = clients.get(id)
if (!entry || entry.client.status !== 'ready') {
throw new Error(`Connection ${id} is not connected`)
}
return entry.client
}
export function isClientConnected(id: string): boolean {
const entry = clients.get(id)
return entry?.client?.status === 'ready'
@@ -195,6 +203,11 @@ export async function connect(id: string, opts: ConnectionOptions): Promise<void
redis.once('ready', () => {
clients.set(id, { client: redis })
redis.once('close', () => {
if (clients.get(id)?.client === redis) {
clients.delete(id)
}
})
resolve()
})
redis.once('error', (err: Error) => {
@@ -231,6 +244,11 @@ export async function connect(id: string, opts: ConnectionOptions): Promise<void
redis.once('ready', () => {
clients.set(id, { client: redis as any })
redis.once('close', () => {
if (clients.get(id)?.client === (redis as any)) {
clients.delete(id)
}
})
resolve()
})
redis.once('error', (err: Error) => {
@@ -259,6 +277,12 @@ export async function connect(id: string, opts: ConnectionOptions): Promise<void
redis.once('ready', () => {
clients.set(id, { client: redis, sshClient, tunnelServer })
// Auto-cleanup when connection is lost
redis.once('close', () => {
if (clients.get(id)?.client === redis) {
clients.delete(id)
}
})
resolve()
})
+4 -7
View File
@@ -1,11 +1,10 @@
// Hash 操作
import { getClient } from './connection'
import { requireClient } 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 client = requireClient(id)
const [nextCursor, raw] = await client.hscan(key, cursor, 'COUNT', count)
const fields: [string, string][] = []
for (let i = 0; i < raw.length; i += 2) {
@@ -17,13 +16,11 @@ export async function getHashFields(
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
await client.hdel(key, field)
}
+10 -19
View File
@@ -1,27 +1,23 @@
// Key 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
if (ttl < 0) {
await client.persist(key)
} else {
@@ -30,20 +26,17 @@ export async function setKeyTTL(id: string, key: string, ttl: number): Promise<v
}
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
if (keys.length === 0) return 0
const pipeline = client.pipeline()
for (const key of keys) {
@@ -54,8 +47,7 @@ export async function batchDelete(id: string, keys: string[]): Promise<number> {
}
export async function memoryUsage(id: string, key: string): Promise<number | null> {
const client = getClient(id)
if (!client) throw new Error(`Client ${id} not found`)
const client = requireClient(id)
return client.memory('USAGE', key) as Promise<number | null>
}
@@ -65,8 +57,7 @@ export async function scanKeys(
pattern: string,
count: number
): Promise<{ cursor: string; keys: string[] }> {
const client = getClient(id)
if (!client) throw new Error(`Client ${id} not found`)
const client = requireClient(id)
const [nextCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count)
return { cursor: nextCursor, keys }
}
+6 -11
View File
@@ -1,32 +1,27 @@
// List 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return client.lrem(key, count, value)
}
+3 -3
View File
@@ -1,6 +1,6 @@
// 发布订阅 + MONITOR 流式接口
import Redis from 'ioredis'
import { getClient } from './connection'
import { requireClient } from './connection'
import type { WebContents } from 'electron'
const subClients = new Map<string, Redis>()
@@ -13,7 +13,7 @@ export function startSubscribe(
sender: WebContents
): void {
stopSubscribe(id)
const existing = getClient(id)
const existing = requireClient(id)
if (!existing) throw new Error('No active connection')
const sub = new Redis(existing.options)
@@ -48,7 +48,7 @@ export function stopSubscribe(id: string): void {
export function startMonitor(id: string, sender: WebContents): void {
stopMonitor(id)
const existing = getClient(id)
const existing = requireClient(id)
if (!existing) throw new Error('No active connection')
const monitor = new Redis(existing.options)
+8 -15
View File
@@ -1,44 +1,37 @@
// 服务器操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return client.slowlog('LEN') as Promise<number>
}
+5 -9
View File
@@ -1,26 +1,22 @@
// Set 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return client.srem(key, ...members)
}
+11 -21
View File
@@ -1,71 +1,61 @@
// Stream 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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 client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return (client as any).xack(key, group, ...ids)
}
+6 -11
View File
@@ -1,33 +1,28 @@
// String + ReJson 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return (client as any).call('JSON.DEL', key, path)
}
+5 -9
View File
@@ -1,27 +1,23 @@
// Sorted Set 操作
import { getClient } from './connection'
import { requireClient } 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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
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`)
const client = requireClient(id)
return client.zrem(key, ...members)
}