UNPKG

@satorijs/core

Version:

Core components of Satorijs

142 lines (115 loc) 4.14 kB
import { Awaitable, remove, Time } from 'cosmokit' import { Status, WebSocket } from '@satorijs/protocol' import { Context, z } from 'cordis' import { Bot } from './bot' export abstract class Adapter<C extends Context = Context, B extends Bot<C> = Bot<C>> { static schema = false as const public bots: B[] = [] constructor(protected ctx: C) {} async connect(bot: B) {} async disconnect(bot: B) {} fork(ctx: Context, bot: B) { bot.adapter = this this.bots.push(bot) ctx.on('dispose', () => { remove(this.bots, bot) }) } } export namespace Adapter { export interface WsClientConfig { retryLazy: number retryTimes: number retryInterval: number } export const WsClientConfig: z<WsClientConfig> = z.object({ retryTimes: z.natural().description('初次连接时的最大重试次数。').default(6), retryInterval: z.natural().role('ms').description('初次连接时的重试时间间隔。').default(5 * Time.second), retryLazy: z.natural().role('ms').description('连接关闭后的重试时间间隔。').default(Time.minute), }).description('连接设置') export abstract class WsClientBase<C extends Context, B extends Bot<C>> extends Adapter<C, B> { protected socket?: WebSocket protected connectionId = 0 protected abstract prepare(): Awaitable<WebSocket> protected abstract accept(socket: WebSocket): void protected abstract getActive(): boolean protected abstract setStatus(status: Status, error?: Error): void constructor(ctx: C, public config: WsClientConfig) { super(ctx) } async start() { let retryCount = 0 const connectionId = ++this.connectionId const logger = this.ctx.logger('adapter') const { retryTimes, retryInterval, retryLazy } = this.config const reconnect = (initial: boolean, message: string) => { if (!this.getActive() || connectionId !== this.connectionId) return let timeout = retryInterval if (retryCount >= retryTimes) { if (initial) { return this.setStatus(Status.OFFLINE, new Error(message)) } else { timeout = retryLazy } } retryCount++ this.setStatus(Status.RECONNECT) logger.warn(`${message}, will retry in ${Time.format(timeout)}...`) setTimeout(() => { if (!this.getActive() || connectionId !== this.connectionId) return connect() }, timeout) } const connect = async (initial = false) => { logger.debug('websocket client opening') let socket: WebSocket try { socket = await this.prepare() } catch (error: any) { reconnect(initial, error.toString() || `failed to prepare websocket`) return } // remove query args to protect privacy const url = socket.url.replace(/\?.+/, '') socket.addEventListener('error', (event) => { if (event.message) logger.warn(event.message) }) socket.addEventListener('close', ({ code, reason }) => { if (this.socket === socket) this.socket = undefined logger.debug(`websocket closed with ${code}`) reconnect(initial, reason.toString() || `failed to connect to ${url}, code: ${code}`) }) socket.addEventListener('open', () => { retryCount = 0 this.socket = socket logger.info('connect to server: %c', url) this.accept(socket) }) } connect(true) } async stop() { this.socket?.close() } } export abstract class WsClient<C extends Context, B extends Bot<C, WsClientConfig>> extends WsClientBase<C, B> { static reusable = true constructor(ctx: C, public bot: B) { super(ctx, bot.config) bot.adapter = this } getActive() { return this.bot.isActive } setStatus(status: Status, error?: Error) { this.bot.status = status this.bot.error = error } async connect(bot: B) { this.start() } async disconnect(bot: B) { this.stop() } } }