feat: CLI SUBSCRIBE/MONITOR streaming mode

- New pubsub.ts: startSubscribe/stopSubscribe/startMonitor/stopMonitor
- IPC: redis:subscribe, redis:unsubscribe, redis:monitor, redis:monitorStop
- Streaming messages pushed via webContents.send
- Preload: onSubscribeMessage/onMonitorMessage callbacks
- CliView: detect SUBSCRIBE/PSUBSCRIBE/MONITOR, switch to streaming mode
- Stop button with pulsing indicator (green=subscribe, yellow=monitor)
- Real-time message display with timestamps
- Auto-cleanup on unmount
- i18n keys (zh-CN + en)
This commit is contained in:
2026-07-07 21:07:57 +08:00
parent 742c62517c
commit c7e47c4d5b
9 changed files with 280 additions and 28 deletions
+26
View File
@@ -239,4 +239,30 @@ export function registerIpcHandlers(): void {
ipcMain.handle('redis:clearCommandLog', () => {
redis.clearLog()
})
// Pub/Sub
ipcMain.handle('redis:subscribe', async (_e, id: string, channels: string[], isPattern: boolean) => {
try {
redis.startSubscribe(id, channels, isPattern, _e.sender)
return { success: true }
} catch (err: any) {
return { success: false, error: err.message }
}
})
ipcMain.handle('redis:unsubscribe', async (_e, id: string) => {
redis.stopSubscribe(id)
})
// Monitor
ipcMain.handle('redis:monitor', async (_e, id: string) => {
try {
redis.startMonitor(id, _e.sender)
return { success: true }
} catch (err: any) {
return { success: false, error: err.message }
}
})
ipcMain.handle('redis:monitorStop', async (_e, id: string) => {
redis.stopMonitor(id)
})
}
+1
View File
@@ -10,3 +10,4 @@ export * from './zset'
export * from './stream'
export * from './server'
export * from './commandLogger'
export * from './pubsub'
+79
View File
@@ -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)
}
+6
View File
@@ -104,6 +104,12 @@ export interface ElectronAPI {
isConnected: (id: string) => Promise<boolean>
getCommandLog: () => Promise<any[]>
clearCommandLog: () => Promise<void>
subscribe: (id: string, channels: string[], isPattern: boolean) => Promise<{ success: boolean; error?: string }>
unsubscribe: (id: string) => Promise<void>
onSubscribeMessage: (callback: (msg: any) => void) => void
monitor: (id: string) => Promise<{ success: boolean; error?: string }>
monitorStop: (id: string) => Promise<void>
onMonitorMessage: (callback: (msg: any) => void) => void
}
updater: {
check: () => Promise<{ available: boolean; version?: string }>
+16
View File
@@ -119,6 +119,22 @@ const electronAPI = {
ipcRenderer.invoke('redis:getCommandLog'),
clearCommandLog: (): Promise<void> =>
ipcRenderer.invoke('redis:clearCommandLog'),
// Pub/Sub
subscribe: (id: string, channels: string[], isPattern: boolean): Promise<any> =>
ipcRenderer.invoke('redis:subscribe', id, channels, isPattern),
unsubscribe: (id: string): Promise<void> =>
ipcRenderer.invoke('redis:unsubscribe', id),
onSubscribeMessage: (callback: (msg: any) => void) => {
ipcRenderer.on('redis:subscribeMessage', (_e, msg) => callback(msg))
},
// Monitor
monitor: (id: string): Promise<any> =>
ipcRenderer.invoke('redis:monitor', id),
monitorStop: (id: string): Promise<void> =>
ipcRenderer.invoke('redis:monitorStop', id),
onMonitorMessage: (callback: (msg: any) => void) => {
ipcRenderer.on('redis:monitorMessage', (_e, msg) => callback(msg))
},
},
updater: {
check: (): Promise<any> => ipcRenderer.invoke('updater:check'),