清理: - 删除 34 份过时文档(gap reports/QA临时报告/验收报告/阶段性文档) - 删除 docs/.hermes/skills 第三方 skills 副本(16 文件) - 删除 skills-lock.json 目录归集: - 根目录仅保留 README.md 索引 - product/ — 产品与架构设计(PRD + ARCHITECTURE + P2设计文档 + AI/企业路线图) - tracking/ — Chatwoot parity 开发跟踪 - requirements/ — M01-M12 模块需求 - plans/ — 历史实现计划 - parity/ — 路由 parity 与前端契约 - qa/ — QA 报告与测试计划 - ops/ — 运维部署 命名规范: - 全小写 kebab-case,禁止全大写文件名 - product/tracking/ops 用 NN- 序号前缀 - requirements 用 MNN- 两位零填充模块号 - plans/qa 用 YYYY-MM-DD- 日期前缀 - requirements M1-M9 零填充为 M01-M09(修复字典序) 同步更新: - backend/cmd/route_parity/main.go 路径默认值 - backend/scripts/parity_frontend_smoke.sh 报告路径 - 所有 docs 内部交叉引用 - .gitignore 排除编译产物 (backend/gochat, backend/route_parity) - 新增迁移 000052/000053 - 前端 WS 相关修改
115 lines
3.0 KiB
JavaScript
115 lines
3.0 KiB
JavaScript
import { createConsumer } from '@rails/actioncable';
|
|
import Cookies from 'js-cookie';
|
|
|
|
const PRESENCE_INTERVAL = 20000;
|
|
const RECONNECT_INTERVAL = 1000;
|
|
|
|
class BaseActionCableConnector {
|
|
static isDisconnected = false;
|
|
|
|
constructor(
|
|
app,
|
|
pubsubToken,
|
|
websocketHost = '',
|
|
presenceInterval = PRESENCE_INTERVAL
|
|
) {
|
|
// Read access-token for WebSocket auth from cookie
|
|
let accessToken = '';
|
|
try {
|
|
const raw = Cookies.get('cw_d_session_info');
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
accessToken = parsed['access-token'] || '';
|
|
}
|
|
} catch (e) {
|
|
// Ignore cookie parse errors
|
|
}
|
|
|
|
// Default to the page origin so the URL is never undefined.
|
|
const wsOrigin = websocketHost || window.location.origin;
|
|
let websocketURL = `${wsOrigin}/cable`;
|
|
if (accessToken) {
|
|
websocketURL += `?access-token=${encodeURIComponent(accessToken)}`;
|
|
}
|
|
|
|
this.consumer = createConsumer(websocketURL);
|
|
this.subscription = this.consumer.subscriptions.create(
|
|
{
|
|
channel: 'RoomChannel',
|
|
pubsub_token: pubsubToken,
|
|
account_id: app.$store.getters.getCurrentAccountId,
|
|
user_id: app.$store.getters.getCurrentUserID,
|
|
},
|
|
{
|
|
updatePresence() {
|
|
this.perform('update_presence');
|
|
},
|
|
received: this.onReceived,
|
|
disconnected: () => {
|
|
BaseActionCableConnector.isDisconnected = true;
|
|
this.onDisconnected();
|
|
this.initReconnectTimer();
|
|
},
|
|
}
|
|
);
|
|
this.app = app;
|
|
this.events = {};
|
|
this.reconnectTimer = null;
|
|
this.isAValidEvent = () => true;
|
|
this.triggerPresenceInterval = () => {
|
|
setTimeout(() => {
|
|
this.subscription.updatePresence();
|
|
this.triggerPresenceInterval();
|
|
}, presenceInterval);
|
|
};
|
|
this.triggerPresenceInterval();
|
|
}
|
|
|
|
checkConnection() {
|
|
const isConnectionActive = this.consumer.connection.isOpen();
|
|
const isReconnected =
|
|
BaseActionCableConnector.isDisconnected && isConnectionActive;
|
|
if (isReconnected) {
|
|
this.clearReconnectTimer();
|
|
this.onReconnect();
|
|
BaseActionCableConnector.isDisconnected = false;
|
|
} else {
|
|
this.initReconnectTimer();
|
|
}
|
|
}
|
|
|
|
clearReconnectTimer = () => {
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
this.reconnectTimer = null;
|
|
}
|
|
};
|
|
|
|
initReconnectTimer = () => {
|
|
this.clearReconnectTimer();
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.checkConnection();
|
|
}, RECONNECT_INTERVAL);
|
|
};
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
onReconnect = () => {};
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
onDisconnected = () => {};
|
|
|
|
disconnect() {
|
|
this.consumer.disconnect();
|
|
}
|
|
|
|
onReceived = ({ event, data } = {}) => {
|
|
if (this.isAValidEvent(data)) {
|
|
if (this.events[event] && typeof this.events[event] === 'function') {
|
|
this.events[event](data);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
export default BaseActionCableConnector;
|