import IframeReference from './IframeReference'; import EventBus from './EventBus'; import UserStore from './UserStrore'; import { isProfileData, type ProfileCreated, type ProfileData } from './models/ProfileData'; import { isFolderData, type FolderCreated, type FolderData } from './models/FolderData'; import { v4 as uuidv4 } from 'uuid'; import type { RoleDefinition } from './models/Roles'; export default class MessageBus { private static instance: MessageBus; private readonly origin: string; private messageListener: ((event: MessageEvent) => void) | null = null; private errors: { [key: string]: string } = {}; private readyPromise: Promise | null = null; private isReadyFlag = false; private constructor(origin: string) { this.origin = origin; } public static getInstance(origin: string): MessageBus { if (!MessageBus.instance) { MessageBus.instance = new MessageBus(origin); } return MessageBus.instance; } public isReady(): Promise { if (this.isReadyFlag) { return Promise.resolve(); } if (this.readyPromise) { return this.readyPromise; } this.readyPromise = new Promise((resolve) => { const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('IS_READY', (responseId: string) => { if (responseId !== correlationId) return; unsubscribe(); this.destroyMessageListener(); this.isReadyFlag = true; resolve(); }); }); return this.readyPromise; } public requestLink(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('LINK_ACCEPTED', (responseId: string, accessToken: string, refreshToken: string) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); UserStore.getInstance().connect(accessToken, refreshToken); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_LINK_ACCEPTED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'REQUEST_LINK' }); }); } public getUserPairingId(): Promise { return new Promise((resolve: (userPairingId: string) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('PAIRING_ID', (responseId: string, userPairingId: string) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(userPairingId); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PAIRING_ID', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'GET_PAIRING_ID', accessToken, }); }).catch(console.error); }); } public validateToken(): Promise { return new Promise((resolve: (isValid: boolean) => void, reject: (error: string) => void) => { const userStore = UserStore.getInstance(); if (!userStore.isConnected()) { reject('User is not connected'); return; } const accessToken = userStore.getAccessToken()!; const refreshToken = userStore.getRefreshToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('TOKEN_VALIDATED', (responseId: string, isValid: boolean) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(isValid); }); const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_VALIDATED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'VALIDATE_TOKEN', accessToken, refreshToken }); }); } public renewToken(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { const userStore = UserStore.getInstance(); if (!userStore.isConnected()) { reject('User is not connected'); return; } const refreshToken = userStore.getRefreshToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('TOKEN_RENEWED', (responseId: string, accessToken: string, refreshToken: string) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); UserStore.getInstance().connect(accessToken, refreshToken); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_RENEWED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'RENEW_TOKEN', refreshToken }); }); } public getProcesses(): Promise { return new Promise((resolve: (processes: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); console.log(correlationId); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => { console.log(responseId); if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(processes); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESSES_RETRIEVED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'GET_PROCESSES', accessToken, }); }).catch(console.error); }); } public getMyProcesses(): Promise { return new Promise((resolve: (myProcesses: string[]) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('GET_MY_PROCESSES', (responseId: string, myProcesses: string[]) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(myProcesses); }); const unsubscribeError = EventBus.getInstance().on('ERROR_GET_MY_PROCESSES', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'GET_MY_PROCESSES', accessToken, }); }).catch(console.error); }); } public getData(processId: string, stateId: string): Promise { return new Promise((resolve: (data: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('DATA_RETRIEVED', (responseId: string, data: any) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(data); }); const unsubscribeError = EventBus.getInstance().on('ERROR_DATA_RETRIEVED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'RETRIEVE_DATA', processId, stateId, token: accessToken }); }).catch(console.error); }); } public createProfile(profileData: ProfileData, profilePrivateData: string[], roles: Record): Promise { return new Promise((resolve: (profileCreated: ProfileCreated) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); // Return value must contain the data commited in the new process const profileData = processCreated.processData; if (!profileData || !isProfileData(profileData)) { reject('Returned invalid profile data'); } if (!processCreated.processId || typeof processCreated.processId !== 'string') { console.error('Returned invalid process id'); reject('Returned invalid process id'); } // TODO check that process is of type Process const profileCreated: ProfileCreated = { processId: processCreated.processId, process: processCreated.process, profileData }; resolve(profileCreated); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'CREATE_PROCESS', processData: profileData, privateFields: profilePrivateData, roles, accessToken }); }).catch(console.error); }); } public createFolder(folderData: FolderData, folderPrivateData: string[], roles: Record): Promise { return new Promise((resolve: (folderData: FolderCreated) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); // Return value must contain the data commited in the new process const folderData = processCreated.processData; if (!folderData || !isFolderData(folderData)) reject('Returned invalid process data'); if (!processCreated.processId || typeof processCreated.processId !== 'string') reject('Returned invalid process id'); // TODO check that process is of type Process const folderCreated: FolderCreated = { processId: processCreated.processId, process: processCreated.process, folderData }; resolve(folderCreated); }); const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'CREATE_PROCESS', processData: folderData, privateFields: folderPrivateData, roles, accessToken }); }).catch(console.error); }); } public notifyProcessUpdate(processId: string, stateId: string): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('UPDATE_NOTIFIED', (responseId: string) => { if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(); }); const unsubscribeError = EventBus.getInstance().on('ERROR_UPDATE_NOTIFIED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'NOTIFY_UPDATE', processId, stateId, accessToken }); }).catch(console.error); }); } public validateState(processId: string, stateId: string): Promise { return new Promise((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => { this.checkToken().then(() => { const userStore = UserStore.getInstance(); const accessToken = userStore.getAccessToken()!; const correlationId = uuidv4(); this.initMessageListener(correlationId); const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => { console.log(updatedProcess); if (responseId !== correlationId) { return; } unsubscribe(); this.destroyMessageListener(); resolve(updatedProcess); }); const unsubscribeError = EventBus.getInstance().on('ERROR_STATE_VALIDATED', (responseId: string, error: string) => { if (responseId !== correlationId) { return; } unsubscribeError(); this.destroyMessageListener(); reject(error); }); this.sendMessage({ type: 'VALIDATE_STATE', processId, stateId, accessToken }); }).catch(console.error); }); } private checkToken(): Promise { return new Promise((resolve: () => void, reject: (error: string) => void) => { this.validateToken().then((isValid: boolean) => { if (!isValid) { this.renewToken().then(resolve).catch(reject); } else { resolve(); } }).catch(reject); }); } private sendMessage(message: any): void { const iframe = IframeReference.getIframe(); if (!iframe) { console.error('[MessageBus] sendMessage: iframe not found'); return; } console.log('[MessageBus] sendMessage:', message); iframe.contentWindow?.postMessage(message, this.origin); } private initMessageListener(correlationId: string): void { this.destroyMessageListener(); this.messageListener = this.handleMessage.bind(this, correlationId); window.addEventListener('message', this.messageListener); } private destroyMessageListener(): void { if (this.messageListener) { window.removeEventListener('message', this.messageListener); this.messageListener = null; } } private handleMessage(correlationId: string, event: MessageEvent): void { if (event.origin !== this.origin) { return; } if (!event.data || typeof event.data !== 'object') { return; } const message = event.data; switch (message.type) { case 'LISTENING': EventBus.getInstance().emit('IS_READY', correlationId); break; case 'LINK_ACCEPTED': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_LINK_ACCEPTED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('LINK_ACCEPTED', correlationId, message.accessToken, message.refreshToken); break; case 'VALIDATE_TOKEN': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_TOKEN_VALIDATED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('TOKEN_VALIDATED', correlationId, message.isValid); break; case 'RENEW_TOKEN': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_TOKEN_RENEWED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('TOKEN_RENEWED', correlationId, message.accessToken, message.refreshToken); break; case 'GET_PAIRING_ID': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_PAIRING_ID', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('PAIRING_ID', correlationId, message.userPairingId); break; case 'PROCESSES_RETRIEVED': // GET_PROCESSES if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_PROCESSES_RETRIEVED', correlationId, error); return; } EventBus.getInstance().emit('PROCESSES_RETRIEVED', correlationId, message.processes); break; case 'GET_MY_PROCESSES': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_GET_MY_PROCESSES', correlationId, error); return; } EventBus.getInstance().emit('GET_MY_PROCESSES', correlationId, message.myProcesses); break; case 'PROFILE_CREATED': // CREATE_PROFILE if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_PROFILE_CREATED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('PROFILE_CREATED', correlationId, message.profileCreated); break; case 'FOLDER_CREATED': // CREATE_FOLDER if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_FOLDER_CREATED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('FOLDER_CREATED', message.folderCreated); break; case 'DATA_RETRIEVED': // RETRIEVE_DATA if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_DATA_RETRIEVED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('DATA_RETRIEVED', correlationId, message.data); break; case 'PROCESS_CREATED': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_PROCESS_CREATED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('PROCESS_CREATED', correlationId, message.processCreated); break; case 'UPDATE_NOTIFIED': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_UPDATE_NOTIFIED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('UPDATE_NOTIFIED', correlationId); break; case 'STATE_VALIDATED': if (this.errors[correlationId]) { const error = this.errors[correlationId]; delete this.errors[correlationId]; EventBus.getInstance().emit('ERROR_STATE_VALIDATED', correlationId, error); return; } EventBus.getInstance().emit('MESSAGE_RECEIVED', message); EventBus.getInstance().emit('STATE_VALIDATED', correlationId, message.validatedProcess); break; case 'ERROR': console.error('Error:', message); this.errors[correlationId] = message.error; break; } } }