Skip to content

Commit

Permalink
feat(console): support WebSocket heartbeat, fix koishijs#114
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 7, 2023
1 parent 1fea029 commit 5b75b5e
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 13 deletions.
33 changes: 23 additions & 10 deletions packages/client/client/data.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AbstractWebSocket, ClientConfig, Console, DataService, Events } from '@koishijs/plugin-console'
import { Promisify } from 'koishi'
import { markRaw, reactive, ref } from 'vue'
import { useLocalStorage } from '@vueuse/core'
import { RemovableRef, useLocalStorage } from '@vueuse/core'

interface StorageData<T> {
version: number
Expand All @@ -19,7 +19,7 @@ export function createStorage<T extends object>(key: string, version: number, fa
return reactive<T>(storage.value['data'])
}

export let useStorage = <T extends object>(key: string, version: number, fallback?: () => T) => {
export let useStorage = <T extends object>(key: string, version: number, fallback?: () => T): RemovableRef<T> => {
const initial = fallback ? fallback() : {} as T
initial['__version__'] = version
const storage = useLocalStorage('koishi.console.' + key, initial)
Expand Down Expand Up @@ -91,15 +91,17 @@ receive('response', ({ id, value, error }) => {
export function connect(callback: () => AbstractWebSocket) {
const value = callback()

value.addEventListener('message', (ev) => {
const data = JSON.parse(ev.data)
console.debug('%c', 'color:purple', data.type, data.body)
if (data.type in listeners) {
listeners[data.type](data.body)
}
})
let sendTimer: number
let closeTimer: number
const refresh = () => {
if (!config.heartbeat) return
clearTimeout(sendTimer)
clearTimeout(closeTimer)
sendTimer = +setTimeout(() => send('ping'), config.heartbeat.interval)
closeTimer = +setTimeout(() => value?.close(), config.heartbeat.timeout)
}

value.addEventListener('close', () => {
const reconnect = () => {
socket.value = null
for (const key in store) {
store[key] = undefined
Expand All @@ -110,8 +112,19 @@ export function connect(callback: () => AbstractWebSocket) {
console.log('[koishi] websocket disconnected, will retry in 1s...')
})
}, 1000)
}

value.addEventListener('message', (ev) => {
refresh()
const data = JSON.parse(ev.data)
console.debug('%c', 'color:purple', data.type, data.body)
if (data.type in listeners) {
listeners[data.type](data.body)
}
})

value.addEventListener('close', reconnect)

return new Promise<AbstractWebSocket.Event>((resolve, reject) => {
value.addEventListener('open', (event) => {
socket.value = markRaw(value)
Expand Down
16 changes: 14 additions & 2 deletions packages/console/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, noop, Schema, WebSocketLayer } from 'koishi'
import { Context, noop, Schema, Time, WebSocketLayer } from 'koishi'
import { Console, Entry } from '../shared'
import { ViteDevServer } from 'vite'
import { extname, resolve } from 'path'
Expand All @@ -18,6 +18,12 @@ interface ClientConfig {
uiPath: string
endpoint: string
static?: boolean
heartbeat?: HeartbeatConfig
}

interface HeartbeatConfig {
interval?: number
timeout?: number
}

class NodeConsole extends Console {
Expand All @@ -29,9 +35,10 @@ class NodeConsole extends Console {
constructor(public ctx: Context, public config: NodeConsole.Config) {
super(ctx)

const { devMode, uiPath, apiPath, selfUrl } = config
const { devMode, uiPath, apiPath, selfUrl, heartbeat } = config
this.global.devMode = devMode
this.global.uiPath = uiPath
this.global.heartbeat = heartbeat
this.global.endpoint = selfUrl + apiPath

this.layer = ctx.router.ws(config.apiPath, (socket) => {
Expand Down Expand Up @@ -193,6 +200,7 @@ namespace NodeConsole {
open?: boolean
selfUrl?: string
apiPath?: string
heartbeat?: HeartbeatConfig
}

export const Config: Schema<Config> = Schema.object({
Expand All @@ -201,6 +209,10 @@ namespace NodeConsole {
apiPath: Schema.string().description('后端 API 服务的路径。').default('/status'),
selfUrl: Schema.string().description('Koishi 服务暴露在公网的地址。').role('link').default(''),
open: Schema.boolean().description('在应用启动后自动在浏览器中打开控制台。'),
heartbeat: Schema.object({
interval: Schema.number().description('心跳发送间隔 (单位毫秒)。').default(Time.second * 30),
timeout: Schema.number().description('心跳超时时间 (单位毫秒)。').default(Time.minute),
}),
devMode: Schema.boolean().description('启用调试模式 (仅供开发者使用)。').default(process.env.NODE_ENV === 'development').hidden(),
cacheDir: Schema.string().description('调试服务器缓存目录。').default('.vite').hidden(),
})
Expand Down
5 changes: 4 additions & 1 deletion packages/console/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export abstract class Console extends Service {
super(ctx, 'console', true)
ctx.plugin(EntryProvider)
ctx.plugin(SchemaProvider)
this.addListener('ping', () => 'pong')
}

protected accept(socket: AbstractWebSocket) {
Expand Down Expand Up @@ -100,7 +101,9 @@ export abstract class Console extends Service {
}
}

export interface Events {}
export interface Events {
'ping'(): string
}

export namespace Console {
export interface Services {
Expand Down
1 change: 1 addition & 0 deletions packages/console/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export namespace AbstractWebSocket {
}

export interface AbstractWebSocket {
close(code?: number, reason?: string): void
send(data: string): void
dispatchEvent(event: any): boolean
addEventListener<K extends keyof AbstractWebSocket.EventMap>(type: K, listener: (event: AbstractWebSocket.EventMap[K]) => void): void
Expand Down

0 comments on commit 5b75b5e

Please sign in to comment.