* Copyright (C) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Utils, MessageParam } from './Util';
import { Constants, TypeConstants } from './Constants';
enum GetStatuses {
UNCONNECTED = 'unconnected',
CONNECTED = 'connected',
LOGINED = 'logined',
LOGINFAILEDBYLACKSESSION = 'loginFailedByLackSession',
UPFRADING = 'upgrading',
UPGRADESUCCESS = 'upgradeSuccess',
UPGRADEFAILED = 'upgradeFailed',
READY = 'ready'
}
const INTERMEDIATE_STATE = 'Intermediate state';
const FAILED_STATE = 'Failed state';
export class WebSocketManager {
static instance: WebSocketManager | null | undefined = null;
url: string = `ws://localhost:${Constants.NODE_PORT}`;
private websocket: WebSocket | null | undefined = null;
private distributeMap: Map<number, { 'messageCallbacks': Function[], 'eventCallBack': Function }> = new Map<number, { 'messageCallbacks': Function[], 'eventCallBack': Function }>();
private sessionId: number | null | undefined;
private session: bigint | null | undefined;
private heartbeatInterval: number | null | undefined;
public status: string = GetStatuses.UNCONNECTED;
private cacheInfo: Map<number, unknown> = new Map<number, unknown>();
private reconnect: number = -1;
private connectStatus: HTMLElement | null | undefined;
constructor() {
if (WebSocketManager.instance) {
return WebSocketManager.instance;
}
WebSocketManager.instance = this;
this.connectWebSocket();
}
connectWebSocket(): void {
this.connectStatus = document.querySelector("body > sp-application").shadowRoot.querySelector("#main-menu").shadowRoot.querySelector("div.bottom > div.extend_connect");
this.websocket = new WebSocket(this.url);
this.websocket.binaryType = 'arraybuffer';
this.websocket.onopen = (): void => {
this.status = GetStatuses.CONNECTED;
this.sendHeartbeat();
this.login();
};
this.websocket.onmessage = (event): void => {
let decode: MessageParam = Utils.decode(event.data);
if (decode.type === TypeConstants.HEARTBEAT_TYPE) {
return;
}
this.onmessage(decode!);
};
this.websocket.onerror = (error): void => {
console.error('error:', error);
this.extendTips(false);
};
this.websocket.onclose = (event): void => {
this.status = GetStatuses.UNCONNECTED;
this.extendTips(false);
this.finalStatus();
this.initLoginInfo();
this.clearHeartbeat();
};
}
* 接收webSocket返回的buffer数据
* 分别处理登录、其他业务的数据
* 其他业务数据分发
*/
onmessage(decode: MessageParam): void {
if (decode.type === TypeConstants.LOGIN_TYPE) {
this.loginMessage(decode);
} else if (decode.type === TypeConstants.UPDATE_TYPE) {
this.updateMessage(decode);
} else {
this.businessMessage(decode);
}
}
extendTips(flag: boolean): void {
if(flag) {
this.connectStatus?.style.backgroundColor = 'green';
this.connectStatus?.title = 'The extended service is connected.';
}else{
this.connectStatus?.style.backgroundColor = 'red';
this.connectStatus?.title = 'The extended service is not connected.';
}
}
loginMessage(decode: MessageParam): void {
if (decode.cmd === Constants.LOGIN_CMD) {
this.status = GetStatuses.LOGINED;
this.sessionId = decode.session_id;
this.session = decode.session;
this.getVersion();
} else if (decode.cmd === Constants.SESSION_EXCEED) {
this.status = GetStatuses.LOGINFAILEDBYLACKSESSION;
this.finalStatus();
}
}
updateMessage(decode: MessageParam): void {
if (decode.cmd === Constants.GET_VERSION_CMD) {
let targetVersion = '1.1.4';
let currentVersion = new TextDecoder().decode(decode.data);
let result = this.compareVersion(currentVersion, targetVersion);
if (result === -1) {
this.status = GetStatuses.UPFRADING;
this.updateVersion();
return;
}
this.status = GetStatuses.READY;
this.extendTips(true);
this.finalStatus();
} else if (decode.cmd === Constants.UPDATE_SUCCESS_CMD) {
this.status = GetStatuses.UPGRADESUCCESS;
this.finalStatus();
} else if (decode.cmd === Constants.UPDATE_FAIL_CMD) {
this.status = GetStatuses.UPGRADEFAILED;
this.finalStatus();
}
}
businessMessage(decode: MessageParam): void {
if (this.distributeMap.has(decode.type!)) {
const callbackObj = this.distributeMap.get(decode.type!)!;
callbackObj.messageCallbacks.forEach(callback => {
callback(decode.cmd, decode.data);
});
}
}
getVersion(): void {
this.send(TypeConstants.UPDATE_TYPE, Constants.GET_VERSION_CMD);
}
compareVersion(currentVersion: string, targetVersion: string): number {
let parts1 = currentVersion.split('.');
let parts2 = targetVersion.split('.');
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
let currentNum = i < parts1.length ? parseInt(parts1[i], 10) : 0;
let targetNum = i < parts2.length ? parseInt(parts2[i], 10) : 0;
if (currentNum > targetNum) {
return 1;
} else if (currentNum < targetNum) {
return -1;
}
}
return 0;
}
updateVersion(): void {
let url = `https://${window.location.host.split(':')[0]}:${window.location.port
}${window.location.pathname}extend/hi-smart-perf-host-extend-update.zip`;
fetch(url).then(response => {
if (!response.ok) {
throw new Error('No corresponding upgrade compression package found');
}
return response.arrayBuffer();
}).then((arrayBuffer) => {
this.send(TypeConstants.UPDATE_TYPE, Constants.UPDATE_CMD, new Uint8Array(arrayBuffer));
}).catch((error) => {
this.status = GetStatuses.UPGRADEFAILED;
this.finalStatus();
console.error(error);
});
}
login(): void {
this.websocket!.send(Utils.encode(Constants.LOGIN_PARAM));
}
static getInstance(): WebSocketManager | null | undefined {
if (!WebSocketManager.instance) {
new WebSocketManager();
}
return WebSocketManager.instance;
}
* 消息监听器
* listener是不同模块传来接收数据的函数
* 模块调用
*/
registerMessageListener(type: number, callback: Function, eventCallBack: Function, allowMultipleCallback: boolean = false): void {
let callbackObj = this.distributeMap.get(type);
if (!callbackObj) {
callbackObj = {
messageCallbacks: [callback],
eventCallBack: eventCallBack
};
this.distributeMap.set(type, callbackObj);
} else {
if (allowMultipleCallback) {
callbackObj.messageCallbacks.push(callback);
}
callbackObj.eventCallBack = eventCallBack;
}
}
unregisterCallback(type: number, callback: Function): void {
if (!this.distributeMap.has(type)) {
return;
}
const callbackObj = this.distributeMap.get(type)!;
callbackObj.messageCallbacks = callbackObj.messageCallbacks.filter((cb) => cb !== callback);
if (callbackObj.messageCallbacks.length === 0 && !callbackObj.eventCallBack) {
this.distributeMap.delete(type);
}
}
* 传递数据信息至webSocket
* 模块调
*/
sendMessage(type: number, cmd?: number, data?: Uint8Array): void {
this.reconnect = -1;
if (this.status !== GetStatuses.READY) {
this.cache(type, cmd, data);
this.checkStatus(type);
} else {
this.send(type, cmd, data);
}
}
send(type: number, cmd?: number, data?: Uint8Array): void {
let message: MessageParam = {
type: type,
cmd: cmd,
session_id: this.sessionId!,
session: this.session!,
data_lenght: data ? data.byteLength : undefined,
data: data
};
let encode = Utils.encode(message);
this.websocket!.send(encode!);
}
sendHeartbeat(): void {
this.heartbeatInterval = window.setInterval(() => {
if (this.status === GetStatuses.READY) {
this.send(TypeConstants.HEARTBEAT_TYPE, undefined, undefined);
}
}, Constants.INTERVAL_TIME);
}
* 重连时初始化登录信息
* 在异常关闭时调用
*/
initLoginInfo(): void {
this.sessionId = null;
this.session = null;
}
clearHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
cache(type: number, cmd?: number, data?: Uint8Array): void {
if (!this.cacheInfo.has(type)) {
this.cacheInfo.set(type, { type, cmd, data });
} else {
let obj = this.cacheInfo.get(type);
obj.cmd = cmd;
obj.data = data;
}
}
checkStatus(type: number): void {
let statuses = this.getStatusesPrompt()[this.status];
const distributeEntry = this.distributeMap.get(type);
if (distributeEntry && typeof distributeEntry.eventCallBack === 'function') {
if (statuses.type === INTERMEDIATE_STATE) {
distributeEntry.eventCallBack(this.status);
} else if (statuses.type === FAILED_STATE) {
this.reconnect = type;
this.connectWebSocket();
}
}
}
finalStatus(): void {
if (this.reconnect !== -1) {
if (this.status === GetStatuses.READY) {
this.sendMessage(this.reconnect, this.cacheInfo.get(this.reconnect)!.cmd, this.cacheInfo.get(this.reconnect)!.data);
return;
}
this.distributeMap.get(this.reconnect)!.eventCallBack(this.status);
}
this.reconnect = -1;
}
getStatusesPrompt(): unknown {
return {
unconnected: {
type: FAILED_STATE
},
connected: {
type: INTERMEDIATE_STATE
},
logined: {
type: INTERMEDIATE_STATE
},
loginFailedByLackSession: {
type: FAILED_STATE
},
upgrading: {
type: INTERMEDIATE_STATE
},
upgradeSuccess: {
type: FAILED_STATE,
},
upgradeFailed: {
type: FAILED_STATE,
},
};
}
}