skeleton/src/sdk/MessageBus.ts
2025-06-15 22:27:52 +02:00

703 lines
24 KiB
TypeScript

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<void> | 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<void> {
if (this.isReadyFlag) {
return Promise.resolve();
}
if (this.readyPromise) {
return this.readyPromise;
}
this.readyPromise = new Promise<void>((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<void> {
return new Promise<void>((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<string> {
return new Promise<string>((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<boolean> {
return new Promise<boolean>((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<void> {
return new Promise<void>((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<any> {
return new Promise<any>((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<string[]> {
return new Promise<string[]>((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<Record<string, any>> {
return new Promise<Record<string, any>>((resolve: (data: Record<string, 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: Record<string, 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,
accessToken
});
}).catch(console.error);
});
}
public createProfile(profileData: ProfileData, profilePrivateData: string[], roles: Record<string, RoleDefinition>): Promise<ProfileCreated> {
return new Promise<ProfileCreated>((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<string, RoleDefinition>): Promise<FolderCreated> {
return new Promise<FolderCreated>((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 updateProcess(processId: string, lastStateId: string, newData: Record<string, any>, privateFields: string[], roles: Record<string, RoleDefinition> | null): Promise<any> {
return new Promise<any>((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('PROCESS_UPDATED', (responseId: string, updatedProcess: any) => {
console.log('PROCESS_UPDATED', updatedProcess);
if (responseId !== correlationId) {
return;
}
unsubscribe();
this.destroyMessageListener();
resolve(updatedProcess);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_UPDATED', (responseId: string, error: string) => {
if (responseId !== correlationId) {
return;
}
unsubscribeError();
this.destroyMessageListener();
reject(error);
});
this.sendMessage({
type: 'UPDATE_PROCESS',
processId,
lastStateId,
newData,
privateFields,
roles,
accessToken
});
}).catch(console.error);
});
}
public notifyProcessUpdate(processId: string, stateId: string): Promise<void> {
return new Promise<void>((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<any> {
return new Promise<any>((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<void> {
return new Promise<void>((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 '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 'PROCESS_UPDATED':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_PROCESS_UPDATED', correlationId, error);
return;
}
console.log('PROCESS_UPDATED', message);
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('PROCESS_UPDATED', correlationId, message.updatedProcess);
break;
case 'ERROR':
console.error('Error:', message);
this.errors[correlationId] = message.error;
break;
}
}
}