chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
/**
* MqttClient —— 浏览器/小程序 MQTT over WebSocket 重连封装
*
* 稳定性策略(对应路线 B2「即时通信稳定性」):
* 1. 指数退避重连:基数 1s,上限 30s,成功一次后重置。
* 2. 会话恢复:clean=false 时重连自动补投离线消息(由 Broker 保证)。
* 3. 遗嘱(LWT):连接异常断开时 Broker 代发 will,上层可据此标记「离线」。
* 4. 心跳兜底:onclose 即触发重连,不依赖 broker 的 keepalive 超时。
*
* 用法:
* const c = new MqttClient('wss://your.host/mqtt', {
* clientId: 'web-' + uid, username, password, clean: false,
* will: { topic: 'presence/' + uid, payload: 'offline', qos: 1, retain: true }
* });
* c.on('connect', () => c.subscribe('topic/a'));
* c.on('message', (t, p) => console.log(t, p));
*/
(function (global) {
'use strict';
const BACKOFF_BASE = 1000; // 1s
const BACKOFF_MAX = 30000; // 30s
const BACKOFF_FACTOR = 1.8;
function MqttClient(url, opts) {
this.url = url;
this.opts = opts || {};
this.clientId = this.opts.clientId || ('mqttjs_' + Math.random().toString(16).slice(2, 10));
this._listeners = {};
this._ws = null;
this._connected = false;
this._closedByUser = false;
this._backoff = BACKOFF_BASE;
this._retryTimer = null;
this._subscriptions = {}; // topic -> qos,重连后自动重订
}
MqttClient.prototype.on = function (evt, fn) {
(this._listeners[evt] = this._listeners[evt] || []).push(fn);
return this;
};
MqttClient.prototype._emit = function (evt, a, b) {
(this._listeners[evt] || []).forEach((fn) => fn(a, b));
};
MqttClient.prototype.connect = function () {
this._closedByUser = false;
this._open();
return this;
};
MqttClient.prototype._open = function () {
let ws;
try {
ws = new WebSocket(this.url, ['mqtt']);
} catch (e) {
return this._scheduleReconnect();
}
this._ws = ws;
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
// 发送 MQTT CONNECT(极简 CONNECT 报文,支持 will
const buf = this._buildConnect();
ws.send(buf);
};
ws.onmessage = (ev) => {
const pkt = this._parseConnAck(ev.data);
if (pkt && pkt.sessionPresent) {
this._emit('resume'); // 会话恢复,Broker 会在后台补投离线消息
}
this._connected = true;
this._backoff = BACKOFF_BASE;
this._emit('connect');
Object.keys(this._subscriptions).forEach((t) => this.subscribe(t, this._subscriptions[t]));
};
ws.onclose = () => {
if (this._connected) this._emit('disconnect');
this._connected = false;
if (!this._closedByUser) this._scheduleReconnect();
};
ws.onerror = () => { try { ws.close(); } catch (e) {} };
};
MqttClient.prototype._scheduleReconnect = function () {
if (this._closedByUser) return;
this._emit('reconnecting', this._backoff);
clearTimeout(this._retryTimer);
this._retryTimer = setTimeout(() => {
this._backoff = Math.min(BACKOFF_MAX, Math.floor(this._backoff * BACKOFF_FACTOR));
this._open();
}, this._backoff);
};
MqttClient.prototype.subscribe = function (topic, qos) {
this._subscriptions[topic] = qos || 0;
if (!this._connected || !this._ws) return;
this._ws.send(this._buildSubscribe(topic, qos || 0));
};
MqttClient.prototype.publish = function (topic, payload, qos, retain) {
if (!this._connected || !this._ws) return false;
this._ws.send(this._buildPublish(topic, payload, qos || 0, retain ? 1 : 0));
return true;
};
MqttClient.prototype.end = function () {
this._closedByUser = true;
clearTimeout(this._retryTimer);
if (this._ws) { try { this._ws.close(); } catch (e) {} }
};
/* ---------- 以下为最小 MQTT 报文构造(仅覆盖 CONNECT/SUBSCRIBE/PUBLISH ---------- */
MqttClient.prototype._encStr = function (s) {
const b = new TextEncoder().encode(s);
return new Uint8Array([b.length >> 8, b.length & 0xff, ...b]);
};
MqttClient.prototype._buildConnect = function () {
const o = this.opts;
const cid = this._encStr(this.clientId);
let payload = cid;
if (o.username) payload = this._join(payload, this._encStr(o.username), this._encStr(o.password || ''));
if (o.will) {
payload = this._join(payload, this._encStr(o.will.topic), this._encStr(o.will.payload || ''));
}
let varHdr = new Uint8Array([4, 0x02 /*clean*/, 0x3c /*keepalive 60*/]);
if (o.clean === false) varHdr[1] = 0;
let flags = 0;
if (o.username) flags |= 0x80;
if (o.password) flags |= 0x40;
if (o.will) flags |= 0x04 | ((o.will.qos || 0) << 3) | ((o.will.retain ? 1 : 0) << 5);
varHdr[1] |= flags;
const body = this._join(varHdr, payload);
return this._wrap(1, body);
};
MqttClient.prototype._buildSubscribe = function (topic, qos) {
const pid = new Uint8Array([0x00, 0x01]);
const t = this._join(this._encStr(topic), new Uint8Array([qos]));
return this._wrap(8, this._join(pid, t));
};
MqttClient.prototype._buildPublish = function (topic, payload, qos, retain) {
const t = this._encStr(topic);
const p = typeof payload === 'string' ? new TextEncoder().encode(payload) : new Uint8Array(payload);
return this._wrap(3 | (qos << 1) | (retain << 0), this._join(t, p));
};
MqttClient.prototype._wrap = function (cmd, body) {
const len = body.length;
const rem = [len & 0x7f];
if (len > 127) rem.unshift((len >> 7) & 0x7f | 0x80);
return new Uint8Array([cmd << 4, ...rem, ...body]);
};
MqttClient.prototype._join = function (...arrs) {
let n = 0; arrs.forEach((a) => n += a.length);
const out = new Uint8Array(n);
let p = 0; arrs.forEach((a) => { out.set(a, p); p += a.length; });
return out;
};
MqttClient.prototype._parseConnAck = function (data) {
try {
const buf = new Uint8Array(data);
return { sessionPresent: (buf[2] & 0x01) === 1, code: buf[3] };
} catch (e) { return null; }
};
global.MqttClient = MqttClient;
if (typeof module !== 'undefined') module.exports = MqttClient;
})(typeof window !== 'undefined' ? window : this);