/
adapter.ts
136 lines (111 loc) · 3.78 KB
/
adapter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { Awaitable, remove, Time } from 'cosmokit'
import { Status, WebSocket } from '@satorijs/protocol'
import { z } from 'cordis'
import { Context } from '.'
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 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 logger = this.ctx.logger('adapter')
const { retryTimes, retryInterval, retryLazy } = this.config
const reconnect = async (initial = false) => {
logger.debug('websocket client opening')
let socket: WebSocket
try {
socket = await this.prepare()
} catch (error) {
logger.warn(error)
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 }) => {
this.socket = null
logger.debug(`websocket closed with ${code}`)
if (!this.getActive()) return
const message = reason.toString() || `failed to connect to ${url}, code: ${code}`
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()) reconnect()
}, timeout)
})
socket.addEventListener('open', () => {
_retryCount = 0
this.socket = socket
logger.info('connect to server: %c', url)
this.accept(socket)
})
}
reconnect(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 = null) {
this.bot.status = status
this.bot.error = error
}
async connect(bot: B) {
this.start()
}
async disconnect(bot: B) {
this.stop()
}
}
}