* -------------------------------------------------------------------------
* 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 { customConsole as console } from '../utils';
type ReservedEventHandler = 'request';
type EventHanlder = string;
interface SendParams<T extends EventHanlder> {
[x: string]: unknown;
to?: string;
module?: string;
event: T extends ReservedEventHandler ? never : T;
}
type ListenerCallback = (res: MessageEvent) => void;
export interface ListenerHandler { event: EventHanlder; sequence: number }
type TargetWindow = Window;
type GetTragetWindows = () => TargetWindow[];
type SendTargetKey = number | string;
abstract class BaseConnector {
protected readonly _errMsgType = 'postMessage';
protected invalidFunc: null | (() => void) = null;
protected _listeners: Map<EventHanlder, Array<ListenerCallback | null>> = new Map();
protected constructor() {
if (typeof window !== 'object') {
const errMsg = 'cannot find global Window object, please check your runtime environment';
console.error(this.printErrMsg(errMsg));
this.invalidFunc = (): void => { throw new Error(this.printErrMsg(errMsg)); };
}
window.onmessage = (event: MessageEvent): void => {
const res = { ...event };
try {
if (typeof event.data === 'string') {
res.data = JSON.parse(event.data);
} else {
res.data = event.data;
}
if (res.data?.source === 'react-devtools-content-script') {
return;
}
const listener = this._listeners.get(res.data.event);
if (res.data.event === 'request') {
this.awaitFetch(res);
} else if (listener) {
listener.forEach(cb => cb?.(res));
} else if (['mouseover'].includes(res.data.event)) {
} else {
console.warn(this.printErrMsg('missed [event] in your message, please check your params, or maybe have an invalid send'));
}
} catch (e) {
console.error(e);
}
};
Object.defineProperty(window, 'onmessage', {
configurable: false,
set: () => {
console.warn(this.printErrMsg('the property \'window.onmessage\' in used, reassign it is invalid'));
},
});
}
send<T extends EventHanlder>(body: SendParams<T>, reject?: (err: Error) => void): void {
if (this.invalidFunc) {
this.invalidFunc();
return;
}
const targetWindows = body.to !== undefined ? this.getClientWindows(body.to) : this.getTargetWindows();
const isPostMessageInvalid = targetWindows.length === 0 || (body.to !== undefined && targetWindows[0]?.postMessage === undefined);
if (isPostMessageInvalid) {
const errMsg = 'missed postMessage function, please check your iframe element';
console.warn(this.printErrMsg(errMsg));
reject?.(new Error(errMsg));
return;
}
body.from = window.name;
const postBody = JSON.stringify(body);
targetWindows.forEach(targetWindow => {
targetWindow.postMessage(postBody, this.getTargetOrigin());
});
}
addListener<T extends EventHanlder>(event: T extends ReservedEventHandler ? never : T, callback: ListenerCallback): ListenerHandler {
if (this.invalidFunc) {
this.invalidFunc();
}
if (this._listeners.has(event)) {
(this._listeners.get(event) as ListenerCallback[]).push(callback);
} else {
this._listeners.set(event, [callback]);
}
return { event, sequence: (this._listeners.get(event) as ListenerCallback[]).length - 1 };
}
removeListener({ event, sequence }: ListenerHandler): void {
if (this.invalidFunc) {
this.invalidFunc();
}
const listeners = this._listeners.get(event);
if (listeners) {
listeners[sequence] = null;
if (listeners.filter(item => item).length === 0) {
this._listeners.delete(event);
}
}
}
getTargetOrigin(): string {
return process.env.NODE_ENV === 'development' || window.location.origin.startsWith('file') ? '*' : window.location.origin;
}
protected printErrMsg(errMsg: string): string {
return `[${this._errMsgType}]: ${errMsg}`;
}
protected getClientWindows(windowName?: string): TargetWindow[] {
const frames = this.getFrameWindow().frames;
if (windowName) {
return [(frames as any)[windowName]];
} else {
const res: TargetWindow[] = [];
for (let i = 0; i < frames.length; i++) {
res.push(frames[i]);
}
return res;
}
}
protected abstract getFrameWindow(): TargetWindow;
protected abstract getTargetWindows(): TargetWindow[];
protected abstract awaitFetch(e: MessageEvent): void;
}
type FetchSequenceID = number;
interface FetchParams {
[x: string]: unknown;
event?: never;
}
interface FetchRequest {
[x: string]: any;
event: 'request';
from: SendTargetKey;
id: FetchSequenceID;
}
export class ClientConnector extends BaseConnector {
protected readonly _level: string = 'client';
private readonly _msgSequence: Map<FetchSequenceID, { resolve: (value: unknown) => void; reject: (value: unknown) => void }> = new Map();
private _curFetchSequenceID: FetchSequenceID = 0;
private readonly _module: string;
constructor({
module,
}: {
getTargetWindow?: GetTragetWindows;
module: string;
}) {
super();
this._module = module;
this._level = 'client';
}
send<T extends EventHanlder>(body: SendParams<T>, reject?: (err: Error) => void): void {
body.module = body.module ?? this._module;
super.send(body, reject);
}
async fetch(params: FetchParams): Promise<unknown> {
return new Promise((resolve, reject) => {
params.id = this._curFetchSequenceID;
(params as Record<string, unknown>).event = 'request';
const body = params as unknown as SendParams<string>;
this.send(body, reject);
this._msgSequence.set(this._curFetchSequenceID++, { resolve, reject });
});
};
protected awaitFetch(res: MessageEvent<FetchRequest>): void {
const result = this._msgSequence.get(res.data.id);
if (result !== undefined) {
const { resolve, reject } = result;
if (res.data.body?.error !== null && res.data.body?.error !== undefined) {
reject(res.data.body.error);
} else {
resolve(res.data);
}
this._msgSequence.delete(res.data.id);
}
}
protected getFrameWindow(): TargetWindow {
return window.parent;
}
protected getTargetWindows(): TargetWindow[] {
return [this.getFrameWindow()];
}
}
type Response = (res: MessageEvent) => Promise<Record<string, unknown>>;
export class ServerConnector extends BaseConnector {
protected readonly _getInterceptorHandlers: (command: string) => <T>(event: MessageEvent, responseInterceptor: T) => void;
protected readonly _sendBefore?: <T extends EventHanlder>(body: SendParams<T>) => SendParams<T>;
protected readonly _level: string = 'server';
private _responseForFetch: Response | null = null;
constructor({ getInterceptorHandlers, sendBefore }: {
getTargetWindow?: GetTragetWindows;
getInterceptorHandlers: (command: string) => <T>(event: MessageEvent, responseInterceptor: T) => void;
sendBefore?: <T extends EventHanlder>(body: SendParams<T>) => SendParams<T>;
}) {
super();
this._getInterceptorHandlers = getInterceptorHandlers;
this._sendBefore = sendBefore;
}
registerAwaitFetch(callback: Response): void {
this._responseForFetch = callback;
}
send<T extends EventHanlder>(body: SendParams<T>, reject?: (err: Error) => void): void {
let sendbody = body;
if (this._sendBefore) {
sendbody = this._sendBefore(sendbody);
}
super.send(sendbody, reject);
}
protected async awaitFetch(event: MessageEvent): Promise<void> {
if (typeof event.data.id !== 'number' || !this._responseForFetch) {
const errMsg = 'something wrong with request response for fetch listener or data.id, please check your config';
console.error(this.printErrMsg(errMsg));
return;
}
const res = await this._responseForFetch(event);
const callback = this._getInterceptorHandlers(event.data.args.command);
if (typeof callback === 'function') {
callback(event, res);
}
this.send({
event: 'request',
to: event.data.from,
id: event.data.id,
...res,
} as SendParams<EventHanlder>);
}
protected getFrameWindow(): TargetWindow {
return window;
}
protected getTargetWindows(): TargetWindow[] {
return this.getClientWindows();
}
}