253 lines
5.4 KiB
JavaScript
253 lines
5.4 KiB
JavaScript
const ImType = {
|
|
login: 0,
|
|
keepAlive: 1,
|
|
commonData: 2,
|
|
logout: 3,
|
|
recived: 4,
|
|
responseLogin: 50,
|
|
responseKeepAlive: 51,
|
|
kickout: 54
|
|
}
|
|
|
|
export const ImTypeu = {
|
|
text: 1,
|
|
image: 2,
|
|
file: 3,
|
|
card: 4,
|
|
recall: 5,
|
|
read: 6,
|
|
call: 8,
|
|
group: 9
|
|
}
|
|
|
|
function frame(partial) {
|
|
return {
|
|
bridge: false,
|
|
type: 0,
|
|
dataContent: null,
|
|
from: '-1',
|
|
to: '-1',
|
|
fp: null,
|
|
QoS: false,
|
|
typeu: -1,
|
|
sm: Date.now(),
|
|
...partial
|
|
}
|
|
}
|
|
|
|
function uuid() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
const r = (Math.random() * 16) | 0
|
|
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
|
return v.toString(16)
|
|
})
|
|
}
|
|
|
|
class ImClient {
|
|
constructor() {
|
|
this.userId = ''
|
|
this.token = ''
|
|
this.url = ''
|
|
this._closed = true
|
|
this._tries = 0
|
|
this._alive = 0
|
|
this._reconnect = 0
|
|
this._handlers = {}
|
|
this._bound = false
|
|
this._opened = false
|
|
this._ready = false
|
|
this._queue = []
|
|
}
|
|
|
|
on(event, fn) {
|
|
this._handlers[event] = fn
|
|
}
|
|
|
|
emit(event, payload) {
|
|
const fn = this._handlers[event]
|
|
if (fn) fn(payload)
|
|
}
|
|
|
|
connect({ userId, token, url }) {
|
|
this._closed = false
|
|
this.userId = userId
|
|
this.token = token
|
|
this.url = url
|
|
this._open()
|
|
}
|
|
|
|
_bindSocket() {
|
|
if (this._bound) return
|
|
this._bound = true
|
|
uni.onSocketOpen(() => {
|
|
this._tries = 0
|
|
this._opened = true
|
|
this._send(frame({
|
|
type: ImType.login,
|
|
from: String(this.userId || ''),
|
|
to: '0',
|
|
dataContent: JSON.stringify({
|
|
loginUserId: String(this.userId || ''),
|
|
loginToken: this.token,
|
|
extra: 'hbuilder-app',
|
|
firstLoginTime: 0
|
|
})
|
|
}))
|
|
})
|
|
uni.onSocketMessage((res) => this._onMessage(res.data))
|
|
uni.onSocketClose(() => {
|
|
this._opened = false
|
|
this._ready = false
|
|
this._clearAlive()
|
|
if (!this._closed) this._schedule()
|
|
})
|
|
uni.onSocketError(() => {
|
|
if (!this._closed) this._schedule()
|
|
})
|
|
}
|
|
|
|
_open() {
|
|
this._clear()
|
|
this.emit('status', 'reconnecting')
|
|
this._bindSocket()
|
|
uni.connectSocket({ url: this.url })
|
|
}
|
|
|
|
_onMessage(raw) {
|
|
let p
|
|
try {
|
|
p = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
} catch (_) {
|
|
return
|
|
}
|
|
if (!p || typeof p !== 'object') return
|
|
const type = p.type
|
|
if (type === ImType.responseLogin) {
|
|
let info = {}
|
|
try {
|
|
info = JSON.parse(p.dataContent || '{}')
|
|
} catch (_) {}
|
|
if (info.code === 0) {
|
|
this._ready = true
|
|
this.emit('status', 'connected')
|
|
this._keep()
|
|
this._flush()
|
|
} else {
|
|
this.emit('status', 'offline')
|
|
this.emit('loginFailed', info)
|
|
}
|
|
return
|
|
}
|
|
if (type === ImType.responseKeepAlive) return
|
|
if (type === ImType.kickout) {
|
|
this._closed = true
|
|
this.emit('kick')
|
|
try { uni.closeSocket() } catch (_) {}
|
|
return
|
|
}
|
|
if (type === ImType.recived) {
|
|
this.emit('ack', p.fp || p.dataContent)
|
|
return
|
|
}
|
|
if (type === ImType.commonData) {
|
|
if (p.QoS && p.fp) {
|
|
this._send(frame({
|
|
type: ImType.recived,
|
|
from: this.userId,
|
|
to: '0',
|
|
dataContent: p.fp,
|
|
fp: p.fp
|
|
}))
|
|
}
|
|
let data = {}
|
|
try {
|
|
const dc = p.dataContent
|
|
data = typeof dc === 'string' ? JSON.parse(dc) : (dc || {})
|
|
} catch (_) {
|
|
data = { text: p.dataContent, kind: 'text' }
|
|
}
|
|
this.emit('data', { ...p, data, typeu: p.typeu || ImTypeu.text })
|
|
}
|
|
}
|
|
|
|
sendData({ to, typeu, payload, qos = true }) {
|
|
const fp = uuid()
|
|
this._send(frame({
|
|
type: ImType.commonData,
|
|
from: String(this.userId || ''),
|
|
to: String(to || ''),
|
|
typeu,
|
|
QoS: qos,
|
|
fp,
|
|
dataContent: JSON.stringify(payload)
|
|
}))
|
|
return fp
|
|
}
|
|
|
|
logout() {
|
|
this._closed = true
|
|
try {
|
|
this._send(frame({ type: ImType.logout, from: this.userId, to: '0' }))
|
|
} catch (_) {}
|
|
try { uni.closeSocket() } catch (_) {}
|
|
this._clear()
|
|
}
|
|
|
|
_send(p) {
|
|
const raw = JSON.stringify(p)
|
|
const ctrl = p.type === ImType.login || p.type === ImType.keepAlive || p.type === ImType.logout || p.type === ImType.recived
|
|
if (!this._opened || (!this._ready && !ctrl)) {
|
|
if (!ctrl) this._queue.push(raw)
|
|
return
|
|
}
|
|
try {
|
|
uni.sendSocketMessage({
|
|
data: raw,
|
|
fail: () => {
|
|
if (!ctrl) this._queue.push(raw)
|
|
}
|
|
})
|
|
} catch (_) {
|
|
if (!ctrl) this._queue.push(raw)
|
|
}
|
|
}
|
|
|
|
_flush() {
|
|
const pending = this._queue.splice(0, this._queue.length)
|
|
pending.forEach((raw) => {
|
|
try { uni.sendSocketMessage({ data: raw }) } catch (_) {}
|
|
})
|
|
}
|
|
|
|
_keep() {
|
|
this._clearAlive()
|
|
this._alive = setInterval(() => {
|
|
this._send(frame({ type: ImType.keepAlive, from: this.userId, to: '0', dataContent: '{}' }))
|
|
}, 5000)
|
|
}
|
|
|
|
_schedule() {
|
|
this.emit('status', 'reconnecting')
|
|
const wait = Math.min(15000, 800 * Math.pow(2, this._tries))
|
|
this._tries += 1
|
|
this._reconnect = setTimeout(() => this._open(), wait)
|
|
}
|
|
|
|
_clearAlive() {
|
|
if (this._alive) {
|
|
clearInterval(this._alive)
|
|
this._alive = 0
|
|
}
|
|
}
|
|
|
|
_clear() {
|
|
this._clearAlive()
|
|
if (this._reconnect) {
|
|
clearTimeout(this._reconnect)
|
|
this._reconnect = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
export const imClient = new ImClient()
|