* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import type { ConnectHost, DataRequest, ModuleName, Notification, Request, Response, ResponseHandler } from './defs';
import { CONTENT_LENGTH_PREFIX, isResponse, LOCAL_HOST, PORT } from './defs';
import connector from '@/connection';
import { Modal } from 'antd';
import { errorCenter, WsError, ErrorCode } from '@insight/lib/utils';
import { connectRemote } from '../server';
import { store } from '../../store';
import { runInAction } from 'mobx';
import i18n from '@insight/lib/i18n';
import { WebviewSocket } from '@/vscode-adapter/WebviewSocket';
import { isVscodeEnv } from '@/vscode-adapter';
const createRequestHead = function (
id: number,
module: string,
command: string,
args: Request['params'],
): Request {
const params = {};
Object.assign(params, args);
let fileId = (params as any).dbPath ?? '';
if (module === 'leaks') {
fileId = store.sessionStore.activeSession?.activeDataSource?.selectedFilePath ?? '';
}
return {
id,
moduleName: module,
type: 'request',
command,
fileId,
projectName: store.sessionStore.activeSession?.activeDataSource?.projectName ?? '',
params,
};
};
const MAX_RESPONSE_HANDLERS = 1000;
export interface ErrorMsg {
error: {
code: number;
message: string;
};
};
export class Connection {
private readonly _ws: WebSocket | WebviewSocket | undefined;
private readonly _host: ConnectHost;
private _msgId: number = 0;
private readonly _responseHandlers: Map<number, ResponseHandler> = new Map();
private readonly _requestMsg: Map<number, Request> = new Map();
private _fetchFlag: boolean = true;
constructor(initHost: ConnectHost) {
console.info('[connector]', 'init');
if (this._ws !== undefined) {
}
this._msgId = 0;
const session = store.sessionStore.activeSession;
if (isVscodeEnv()) {
const { ws, toIframeUrl } = WebviewSocket.createForHost(initHost);
this._ws = ws;
if (toIframeUrl) {
runInAction(() => {
session.toIframeUrl = toIframeUrl;
});
}
} else {
const protocol = `${window.location.protocol === 'https:' && window.location.host !== 'wry.localhost' ? 'wss:' : 'ws:'}//`;
if (initHost.jupyterlabProxy) {
const { host, search } = window.location;
const path = `${window.location.pathname.replace(/\/resources\/profiler\/frontend/, `/proxy/${initHost.port}/resources/profiler/frontend`)}`;
const uri = protocol + host + path + search;
this._ws = new WebSocket(uri);
runInAction(() => {
session.toIframeUrl = `${protocol}${host}${window.location.pathname.replace(/\/resources\/profiler\/frontend\/.*/, `/proxy/${initHost.port}`)}`;
});
} else if (!window.location.pathname.includes('/proxy/')) {
const hostname = window.location.hostname || LOCAL_HOST;
let pathname = window.location.pathname && window.location.pathname !== '/' ? window.location.pathname.replace(/\/resources\/profiler\/frontend\/index.html/, '') : '';
pathname = pathname.replace(/\/index.html/, '');
this._ws = new WebSocket(`${protocol}${hostname}${pathname}:${initHost.port}${window.location.search}`);
runInAction(() => {
session.toIframeUrl = `${protocol}${hostname}${pathname}:${initHost.port}`;
});
} else {
const { location } = window;
const { host } = location;
const path = `${window.location.pathname}`.replace(/proxy\/\d{4}/, `proxy/${initHost.port}`);
const { search } = location;
const uri = protocol + host + path + search;
this._ws = new WebSocket(uri);
runInAction(() => {
session.toIframeUrl = `${protocol}${host}${path.replace(/\/index.html/, '')}`;
});
}
}
this._host = initHost;
}
get isConnected(): boolean {
return this._ws?.readyState === WebSocket.OPEN;
}
async reset(): Promise<void> {
console.info('[connector]', 'reset');
}
disconnect(): void {
if (!this._ws) {
throw Error('connection is not initialized');
}
this._ws.close();
}
async connect(): Promise<void> {
this._fetchFlag = true;
return new Promise((resolve, reject) => {
if (!this._ws) {
reject(new Error('connection is not initialized'));
return;
}
this._ws.onopen = (ev: Event): void => {
console.info('[connector]', 'onopen');
this.initHeartCheck();
};
this._ws.onmessage = (ev: MessageEvent<string>): void => {
resolve();
};
this._ws.onerror = (ev: Event): void => {
errorCenter.handleError(new WsError(ErrorCode.WS_ERROR, i18n.t('framework:error.ConnectionErrorMessage')));
console.error('[connector]', ev);
reject(new Error('connect failed.'));
};
this._ws.onclose = (ev: Event): void => {
Modal.error({
title: i18n.t('framework:error.ConnectionClosed'),
content: i18n.t('framework:error.ConnectionClosedMessage'),
okText: i18n.t('framework:error.Reconnect'),
onOk: () => connectRemote(this._host),
closable: true,
});
};
}) as Promise<void>;
}
async fetch<T>(module: ModuleName, dataRequest: DataRequest): Promise<T | ErrorMsg> {
if (!this.isConnected) {
return Promise.reject(new Error(i18n.t('framework:error.ConnectionNotAvailable')));
}
if (this._ws === undefined) {
return Promise.reject(new Error('connection is not initialized'));
}
if (this._ws.onmessage !== null && this._fetchFlag) {
this._fetchFlag = false;
this._ws.onmessage = this.fetchDataOnMessage;
}
return new Promise((resolve: (v: T | ErrorMsg) => void, reject) => {
const id = this._msgId++;
const msg: Request = createRequestHead(
id,
module,
dataRequest.command,
dataRequest.params,
);
this.request(msg);
const reqCallback = this.getCallback(resolve);
if (this._responseHandlers.size > MAX_RESPONSE_HANDLERS) {
const firstKey = this._responseHandlers.keys().next().value;
this._responseHandlers.delete(firstKey);
this._requestMsg.delete(firstKey);
}
this._responseHandlers.set(id, reqCallback);
this._requestMsg.set(id, msg);
});
}
getCallback<T>(resolve: (p: T | ErrorMsg) => void): (res: Response) => void {
return (res: Response): void => {
if (res.command === 'import/action') {
resolve(res as any);
} else if (res.result && res.body !== undefined) {
resolve(res.body as T);
} else {
const { code, message } = res.error ?? { code: -1, message: 'Unknown error' };
resolve({ error: { code, message } } as ErrorMsg);
}
};
}
fetchDataOnMessage = (ev: MessageEvent<string>): void => {
if (ev.data.startsWith(CONTENT_LENGTH_PREFIX)) {
return;
}
let msg: Response | Notification;
try {
msg = JSON.parse(ev.data);
} catch {
console.warn('cannot parse json data:', ev);
return;
}
if (!isResponse(msg)) {
if (msg.event === 'parse/leaksMemoryCompleted') {
const { toBeActivedProject } = store.sessionStore.activeSession;
if (toBeActivedProject !== undefined && toBeActivedProject.selectedFilePath !== msg.body.dbPath) {
console.warn(`event #${msg.event} has been abandoned`);
return;
}
}
msg.body.dataSource = this._host;
connector.send({ ...msg });
return;
}
const reqId = msg.requestId;
const callback = this._responseHandlers.get(reqId);
const requestMsg = this._requestMsg.get(reqId);
const currentProjectName = store.sessionStore.activeSession?.activeDataSource?.projectName ?? '';
const isImport = requestMsg?.command === 'import/action';
const isOldRequest = requestMsg?.projectName !== undefined && requestMsg.projectName !== currentProjectName;
if (!isImport && isOldRequest) {
console.warn(`request #${reqId} has been abandoned`);
this._requestMsg.delete(reqId);
this._responseHandlers.delete(reqId);
return;
}
if (callback === undefined) {
console.warn(`handler for msg #${reqId} not found`);
return;
}
this._requestMsg.delete(reqId);
this._responseHandlers.delete(reqId);
callback(msg);
};
async findServerPort(): Promise<number> {
return Promise.resolve(PORT);
}
private async request(msg: Request): Promise<void> {
const msgStr = JSON.stringify(msg);
if (this._ws === undefined) {
throw new Error('');
}
this._ws.send(msgStr);
}
private initHeartCheck(): void {
this.sendHeartCheck();
setTimeout(() => {
this.initHeartCheck();
}, 30 * 1000);
}
private sendHeartCheck(): void {
const msg: Request = createRequestHead(this._msgId++, 'global', 'heartCheck', {});
this.request(msg);
}
}