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()