lecoffre-front/src/sdk/MessageBus.ts
Sosthene e4d8064abc MessageBus refactoring
* better error management
* Keep tracks of when messages sent don't have answers
* New convenient methods
2025-09-10 16:00:20 +02:00

1199 lines
39 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import IframeReference from './IframeReference';
import EventBus from './EventBus';
import User from './User';
import MapUtils from './MapUtils';
import { FileBlob } from '../front/Api/Entities/types';
export default class MessageBus {
private static instance: MessageBus;
private isListening: boolean = false;
private messagesSent: Set<string> = new Set();
public static getInstance(): MessageBus {
if (!MessageBus.instance) {
MessageBus.instance = new MessageBus();
}
return MessageBus.instance;
}
public initMessageListener(): void {
window.addEventListener('message', this.handleMessage.bind(this));
}
public destroyMessageListener(): void {
window.removeEventListener('message', this.handleMessage.bind(this));
this.isListening = false; // Reset the flag when destroying listener
}
public resetListeningState(): void {
this.isListening = false; // Allow external components to reset listening state
}
public isReady(): Promise<void> {
if (this.isListening) {
return Promise.resolve();
}
return new Promise<void>((resolve: () => void) => {
const unsubscribe = EventBus.getInstance().on('IS_READY', () => {
unsubscribe();
resolve();
});
});
}
public isWaitingForMessage(): boolean {
return this.messagesSent.size > 0;
}
public requestLink(): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
const messageId = `REQUEST_LINK_${uuidv4()}`;
console.log('[MessageBus] requestLink - waiting for messageId:', messageId);
const unsubscribe = EventBus.getInstance().on('LINK_ACCEPTED', (responseId: string, message: { accessToken: string, refreshToken: string }) => {
console.log('[MessageBus] LINK_ACCEPTED received with responseId:', responseId, 'expected:', messageId);
if (responseId !== messageId) {
return;
}
console.log('[MessageBus] LINK_ACCEPTED matched - resolving');
unsubscribe();
unsubscribeError();
User.getInstance().setTokens(message.accessToken, message.refreshToken);
resolve();
});
const unsubscribeError = EventBus.getInstance().on('ERROR_LINK_ACCEPTED', (responseId: string, error: string) => {
console.log('[MessageBus] ERROR_LINK_ACCEPTED received with responseId:', responseId, 'expected:', messageId);
if (responseId !== messageId) {
return;
}
console.log('[MessageBus] ERROR_LINK_ACCEPTED matched - rejecting with error:', error);
unsubscribe();
unsubscribeError();
reject(error);
});
console.log('[MessageBus] requestLink - sending REQUEST_LINK message');
this.sendMessage({
type: 'REQUEST_LINK',
messageId
});
});
}
public createPairing(): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `CREATE_PAIRING_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('PAIRING_CREATED', (responseId: string, pairingId: string) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
User.getInstance().setPairingId(pairingId);
resolve();
});
const unsubscribeError = EventBus.getInstance().on('ERROR_CREATE_PAIRING', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'CREATE_PAIRING',
accessToken,
messageId
});
}).catch(reject);
});
}
public getPairingId(): Promise<string> {
return new Promise<string>((resolve: (pairingId: string) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_PAIRING_ID_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('GET_PAIRING_ID', (responseId: string, pairingId: string) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(pairingId);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_GET_PAIRING_ID', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_PAIRING_ID',
accessToken,
messageId
});
}).catch(reject);
});
}
public getFileByUid(uid: string): Promise<any> {
return new Promise<any>((resolve: (files: any[]) => void, reject: (error: string) => void) => {
this.getProcesses().then(async (processes: any) => {
const files: any[] = [];
for (const processId of Object.keys(processes)) {
const process = processes[processId];
if (!process.states) {
continue;
}
const publicDataDecoded: { [key: string]: any } = {};
// We only care about the public data as they are in the last commited state
const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast((state: any) => state.commited_in !== processTip);
if (!lastCommitedState) {
continue;
}
const publicDataEncoded = lastCommitedState.public_data;
if (!publicDataEncoded) {
continue;
}
for (const key of Object.keys(publicDataEncoded)) {
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
}
if (!(publicDataDecoded['uid'] && publicDataDecoded['uid'] === uid && publicDataDecoded['utype'] && publicDataDecoded['utype'] === 'file' && publicDataDecoded['isDeleted'] === 'false')) {
continue;
}
try {
const lastArchivedAtFileState = process.states.findLast((state: any) => state.pcd_commitment['archived_at']);
if (lastArchivedAtFileState) {
const archivedAt = (await this.getPublicData(lastArchivedAtFileState.public_data['archived_at']));
if (archivedAt) {
continue;
}
}
} catch (error) { }
// We take the file in it's latest commited state
let file: any;
// Which is the last state that updated file_blob?
const lastUpdatedFileNameState = process.states.findLast((state: any) => state.pcd_commitment['file_name']);
const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']);
if (!lastUpdatedFileNameState || !lastUpdatedFileState) {
continue;
}
try {
const fileName: string = (await this.getData(processId, lastUpdatedFileNameState.state_id)).file_name;
const processData = await this.getData(processId, lastUpdatedFileState.state_id);
const isEmpty = Object.keys(processData).length === 0;
if (isEmpty) {
continue;
}
const publicDataEncoded = lastUpdatedFileState.public_data;
if (!publicDataEncoded) {
continue;
}
if (!file) {
file = {
processId,
lastUpdatedFileState,
processData,
publicDataDecoded,
};
} else {
for (const key of Object.keys(processData)) {
file.processData[key] = processData[key];
}
file.lastUpdatedFileState = lastUpdatedFileState;
for (const key of Object.keys(publicDataDecoded)) {
file.publicDataDecoded[key] = publicDataDecoded[key];
}
}
file.processData.file_name = fileName;
} catch (error) {
console.error(error);
}
files.push(file);
break;
}
resolve(files[0]);
}).catch(reject);
});
}
public getFileByUtype(utype: string): Promise<any> {
return new Promise<any>((resolve: (files: any[]) => void, reject: (error: string) => void) => {
this.getProcesses().then(async (processes: any) => {
const files: any[] = [];
for (const processId of Object.keys(processes)) {
const process = processes[processId];
if (!process.states) {
continue;
}
const publicDataDecoded: { [key: string]: any } = {};
// We only care about the public data as they are in the last commited state
const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast((state: any) => state.commited_in !== processTip);
if (!lastCommitedState) {
continue;
}
const publicDataEncoded = lastCommitedState.public_data;
if (!publicDataEncoded) {
continue;
}
for (const key of Object.keys(publicDataEncoded)) {
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
}
if (!(publicDataDecoded['uid'] && publicDataDecoded['utype'] && publicDataDecoded['utype'] === utype && publicDataDecoded['isDeleted'] === 'false')) {
continue;
}
try {
const lastArchivedAtFileState = process.states.findLast((state: any) => state.pcd_commitment['archived_at']);
if (lastArchivedAtFileState) {
const archivedAt = (await this.getPublicData(lastArchivedAtFileState.public_data['archived_at']));
if (archivedAt) {
continue;
}
}
} catch (error) { }
// We take the file in it's latest commited state
let file: any;
// Which is the last state that updated file_blob?
const lastUpdatedFileNameState = process.states.findLast((state: any) => state.pcd_commitment['file_name']);
const lastUpdatedFileState = process.states.findLast((state: any) => state.pcd_commitment['file_blob']);
if (!lastUpdatedFileNameState || !lastUpdatedFileState) {
continue;
}
try {
const fileName: string = (await this.getData(processId, lastUpdatedFileNameState.state_id)).file_name;
const processData = await this.getData(processId, lastUpdatedFileState.state_id);
const isEmpty = Object.keys(processData).length === 0;
if (isEmpty) {
continue;
}
const publicDataEncoded = lastUpdatedFileState.public_data;
if (!publicDataEncoded) {
continue;
}
if (!file) {
file = {
processId,
lastUpdatedFileState,
processData,
publicDataDecoded,
};
} else {
for (const key of Object.keys(processData)) {
file.processData[key] = processData[key];
}
file.lastUpdatedFileState = lastUpdatedFileState;
for (const key of Object.keys(publicDataDecoded)) {
file.publicDataDecoded[key] = publicDataDecoded[key];
}
}
file.processData.file_name = fileName;
} catch (error) {
console.error(error);
}
files.push(file);
break;
}
resolve(files[0]);
}).catch(reject);
});
}
public getRolesForProcess(processId: string): Promise<any> {
return new Promise<any>((resolve: (roles: any[]) => void, reject: (error: string) => void) => {
this.getAllProcesses().then((processes: any) => {
const process = processes[processId];
if (!process.states || process.states.length < 2) {
reject('No states found for process');
}
const roles = process.states[process.states.length - 2].roles;
if (!roles) {
reject('No roles found for process');
}
resolve(roles);
});
});
}
// Returns all processes details, including processes we only have public data for
public getAllProcessesDecoded(filterPublicValues: (publicValues: { [key: string]: any }) => boolean): Promise<any[]> {
return new Promise<any[]>((resolve: (processesDecoded: any[]) => void, reject: (error: string) => void) => {
this.getAllProcesses().then(async (processes: any) => {
const processesDecoded: any[] = [];
for (const processId of Object.keys(processes)) {
const process = processes[processId];
if (!process.states) {
continue;
}
const publicDataDecoded: { [key: string]: any } = {};
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
const state = process.states[stateId];
if (!state) {
continue;
}
const publicDataEncoded = state.public_data;
if (!publicDataEncoded) {
continue;
}
for (const key of Object.keys(publicDataEncoded)) {
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
}
}
if (!filterPublicValues(publicDataDecoded)) {
continue;
}
let processDecoded: any;
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
const lastState = process.states[stateId];
if (!lastState) {
continue;
}
const lastStateId = lastState.state_id;
if (!lastStateId) {
continue;
}
try {
let processData = await this.getData(processId, lastStateId);
if (!processData) {
continue;
}
const isEmpty = Object.keys(processData).length === 0;
if (isEmpty) {
continue;
}
for (const key of Object.keys(publicDataDecoded)) {
processData[key] = publicDataDecoded[key];
}
processData = MapUtils.toJson(processData);
if (!processDecoded) {
processDecoded = {
processId,
lastStateId,
processData,
};
} else {
for (const key of Object.keys(processData)) {
processDecoded.processData[key] = processData[key];
}
processDecoded.lastStateId = lastStateId;
}
} catch (error) {
console.error(error);
}
}
processesDecoded.push(processDecoded);
}
resolve(processesDecoded);
}).catch(reject);
});
}
// Returns details about processes that we are involved in
public getProcessesDecoded(filterPublicValues: (publicValues: { [key: string]: any }) => boolean): Promise<any[]> {
return new Promise<any[]>((resolve: (processesDecoded: any[]) => void, reject: (error: string) => void) => {
this.getProcesses().then(async (processes: any) => {
const processesDecoded: any[] = [];
for (const processId of Object.keys(processes)) {
const process = processes[processId];
if (!process.states) {
continue;
}
const publicDataDecoded: { [key: string]: any } = {};
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
const state = process.states[stateId];
if (!state) {
continue;
}
const publicDataEncoded = state.public_data;
if (!publicDataEncoded) {
continue;
}
for (const key of Object.keys(publicDataEncoded)) {
publicDataDecoded[key] = await this.getPublicData(publicDataEncoded[key]);
}
}
if (!filterPublicValues(publicDataDecoded)) {
continue;
}
let processDecoded: any;
for (let stateId = 0; stateId < process.states.length - 1; stateId++) {
const lastState = process.states[stateId];
if (!lastState) {
continue;
}
const lastStateId = lastState.state_id;
if (!lastStateId) {
continue;
}
try {
let processData = await this.getData(processId, lastStateId);
if (!processData) {
continue;
}
const isEmpty = Object.keys(processData).length === 0;
if (isEmpty) {
continue;
}
for (const key of Object.keys(publicDataDecoded)) {
processData[key] = publicDataDecoded[key];
}
processData = MapUtils.toJson(processData);
if (!processDecoded) {
processDecoded = {
processId,
lastStateId,
processData,
};
} else {
for (const key of Object.keys(processData)) {
processDecoded.processData[key] = processData[key];
}
processDecoded.lastStateId = lastStateId;
}
} catch (error) {
console.error(error);
}
}
processesDecoded.push(processDecoded);
}
resolve(processesDecoded);
}).catch(reject);
});
}
public createProcess(processData: any, privateFields: string[], roles: {}): Promise<any> {
return new Promise<any>((resolve: (processCreated: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `CREATE_PROCESS_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(processCreated);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'CREATE_PROCESS',
processData: processData,
privateFields: privateFields,
roles: roles,
accessToken,
messageId
});
}).catch(reject);
});
}
public updateProcess(processId: string, newData: any, privateFields: string[], roles: {} | null): Promise<any> {
return new Promise<any>((resolve: (processUpdated: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = uuidv4();
const unsubscribe = EventBus.getInstance().on('PROCESS_UPDATED', (responseId: string, processUpdated: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(processUpdated);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_UPDATED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'UPDATE_PROCESS',
processId,
newData,
privateFields,
roles,
accessToken,
messageId
});
}).catch(reject);
});
}
// Returns all processes, including processes that have nothing to do with us
public getAllProcesses(): Promise<{ [processId: string]: any }> {
return new Promise<{ [processId: string]: any }>((resolve: (processes: { [processId: string]: any }) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_PROCESSES_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(processes);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESSES_RETRIEVED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_PROCESSES',
accessToken,
messageId
});
}).catch(reject);
});
}
// Returns processes that we are involved in
public getProcesses(): Promise<{ [processId: string]: any }> {
return new Promise<{ [processId: string]: any }>((resolve: (processes: { [processId: string]: any }) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_PROCESSES_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('PROCESSES_RETRIEVED', (responseId: string, processes: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
// Filter processes by my processes
setTimeout(() => {
this.getMyProcesses().then((myProcesses: string[]) => {
const processesFiltered: { [processId: string]: any } = {};
for (const processId of myProcesses) {
const process = processes[processId];
if (process) {
processesFiltered[processId] = process;
}
}
resolve(processesFiltered);
}).catch(reject);
}, 500);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESSES_RETRIEVED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_PROCESSES',
accessToken,
messageId
});
}).catch(reject);
});
}
// Returns the processes id of processes we are involved in
// It's meant to be used to filter processes in the getProcesses() method
public getMyProcesses(): Promise<string[]> {
return new Promise<string[]>((resolve: (processes: string[]) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_MY_PROCESSES_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('GET_MY_PROCESSES', (responseId: string, processes: string[]) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(processes);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_GET_MY_PROCESSES', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_MY_PROCESSES',
accessToken,
messageId
});
}).catch(reject);
});
}
public notifyUpdate(processId: string, stateId: string): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = uuidv4();
const unsubscribe = EventBus.getInstance().on('UPDATE_NOTIFIED', (responseId: string) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve();
});
const unsubscribeError = EventBus.getInstance().on('ERROR_UPDATE_NOTIFIED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'NOTIFY_UPDATE',
processId,
stateId,
accessToken,
messageId
});
}).catch(reject);
});
}
public validateState(processId: string, stateId: string): Promise<any> {
return new Promise<any>((resolve: (stateValidated: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `VALIDATE_STATE_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, stateValidated: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(stateValidated);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_STATE_VALIDATED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'VALIDATE_STATE',
processId,
stateId,
accessToken,
messageId
});
}).catch(reject);
});
}
public getData(processId: string, stateId: string): Promise<any> {
return new Promise<any>((resolve: (data: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `DATA_RETRIEVED_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('DATA_RETRIEVED', (responseId: string, data: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(data);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_DATA_RETRIEVED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'RETRIEVE_DATA',
processId,
stateId,
accessToken,
messageId
});
}).catch(reject);
});
}
public getPublicData(encodedData: number[]): Promise<any> {
return new Promise<any>((resolve: (data: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `PUBLIC_DATA_DECODED_${uuidv4()}`
const unsubscribe = EventBus.getInstance().on('PUBLIC_DATA_DECODED', (responseId: string, data: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(data);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_PUBLIC_DATA_DECODED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'DECODE_PUBLIC_DATA',
encodedData,
accessToken,
messageId
});
}).catch(reject);
});
}
/**
* Hash a document file using SHA-256
* @param fileBlob - The file blob to hash
* @returns Promise<string> - The SHA-256 hash of the file
*/
public hashDocument(fileBlob: FileBlob, commitedIn: string): Promise<string> {
return new Promise<string>((resolve: (hash: string) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `HASH_VALUE_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('VALUE_HASHED', (responseId: string, hash: string) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(hash);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_VALUE_HASHED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
const label = 'file_blob';
this.sendMessage({
type: 'HASH_VALUE',
accessToken,
commitedIn,
label,
fileBlob,
messageId
});
}).catch(reject);
});
}
public generateMerkleProof(processState: any, attributeName: string): Promise<any> {
return new Promise<any>((resolve: (proof: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `GET_MERKLE_PROOF_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_RETRIEVED', (responseId: string, proof: any) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(proof);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_RETRIEVED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'GET_MERKLE_PROOF',
accessToken,
processState,
attributeName,
messageId
});
}).catch(reject);
});
}
public verifyMerkleProof(merkleProof: string, documentHash: string): Promise<boolean> {
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const messageId = `VALIDATE_MERKLE_PROOF_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('MERKLE_PROOF_VALIDATED', (responseId: string, isValid: boolean) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(isValid);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_MERKLE_PROOF_VALIDATED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
this.sendMessage({
type: 'VALIDATE_MERKLE_PROOF',
accessToken,
merkleProof,
documentHash,
messageId
});
}).catch(reject);
});
}
private validateToken(): Promise<boolean> {
return new Promise<boolean>((resolve: (isValid: boolean) => void, reject: (error: string) => void) => {
const messageId = `VALIDATE_TOKEN_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('TOKEN_VALIDATED', (responseId: string, isValid: boolean) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
resolve(isValid);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_VALIDATED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const accessToken = user.getAccessToken()!;
const refreshToken = user.getRefreshToken()!;
this.sendMessage({
type: 'VALIDATE_TOKEN',
accessToken,
refreshToken,
messageId
});
});
}
private renewToken(): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
const messageId = `RENEW_TOKEN_${uuidv4()}`;
const unsubscribe = EventBus.getInstance().on('TOKEN_RENEWED', (responseId: string, message: { accessToken: string, refreshToken: string }) => {
if (responseId !== messageId) {
return;
}
unsubscribe();
User.getInstance().setTokens(message.accessToken, message.refreshToken);
resolve();
});
const unsubscribeError = EventBus.getInstance().on('ERROR_TOKEN_RENEWED', (responseId: string, error: string) => {
if (responseId !== messageId) {
return;
}
unsubscribeError();
reject(error);
});
const user = User.getInstance();
const refreshToken = user.getRefreshToken()!;
this.sendMessage({
type: 'RENEW_TOKEN',
refreshToken,
messageId
});
});
}
private checkToken(): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
this.isReady().then(() => {
this.validateToken().then((isValid: boolean) => {
if (!isValid) {
this.renewToken().then(resolve).catch(reject);
} else {
resolve();
}
}).catch(reject);
}).catch(reject);
});
}
private sendMessage(message: any): void {
this.isReady().then(() => {
try {
const targetOrigin = IframeReference.getTargetOrigin();
const iframe = IframeReference.getIframe();
iframe.contentWindow?.postMessage(message, targetOrigin);
this.messagesSent.add(message.messageId);
} catch (error) {
console.error('[MessageBus] sendMessage: error', error);
}
}).catch(console.error);
}
private handleMessage(event: MessageEvent): void {
if (!event.data || ['PassClientScriptReady', 'PassIFrameReady', 'Pass::MainWorld::Message', 'Pass::MainWorld::Response'].includes(event.data.type)) {
return;
}
try {
const targetOrigin = IframeReference.getTargetOrigin();
if (event.origin !== targetOrigin) {
throw new Error(`origin don't match: expected ${targetOrigin}, got ${event.origin} with type ${event.data.type}`);
}
if (!event.data || typeof event.data !== 'object') {
throw new Error('data not found');
}
} catch (error) {
console.error('[MessageBus] handleMessage:', error);
return;
}
const message = event.data;
switch (message.type) {
case 'LISTENING':
this.isListening = true;
EventBus.getInstance().emit('IS_READY');
break;
case 'LINK_ACCEPTED':
this.doHandleMessage(message.messageId, 'LINK_ACCEPTED', message, (message: any) => ({
accessToken: message.accessToken,
refreshToken: message.refreshToken
}));
break;
case 'VALIDATE_TOKEN':
this.doHandleMessage(message.messageId, 'TOKEN_VALIDATED', message, (message: any) => message.isValid);
break;
case 'RENEW_TOKEN':
this.doHandleMessage(message.messageId, 'TOKEN_RENEWED', message, (message: any) => ({
accessToken: message.accessToken,
refreshToken: message.refreshToken
}));
break;
case 'GET_PAIRING_ID':
this.doHandleMessage(message.messageId, 'GET_PAIRING_ID', message, (message: any) => message.userPairingId);
break;
case 'PROCESS_CREATED': // CREATE_PROCESS
this.doHandleMessage(message.messageId, 'PROCESS_CREATED', message, (message: any) => message.processCreated);
break;
case 'PROCESS_UPDATED': // UPDATE_PROCESS
this.doHandleMessage(message.messageId, 'PROCESS_UPDATED', message, (message: any) => message.updatedProcess);
break;
case 'PROCESSES_RETRIEVED': // GET_PROCESSES
this.doHandleMessage(message.messageId, 'PROCESSES_RETRIEVED', message, (message: any) => message.processes);
break;
case 'GET_MY_PROCESSES': // GET_MY_PROCESSES
this.doHandleMessage(message.messageId, 'GET_MY_PROCESSES', message, (message: any) => message.myProcesses);
break;
case 'DATA_RETRIEVED': // RETRIEVE_DATA
this.doHandleMessage(message.messageId, 'DATA_RETRIEVED', message, (message: any) => message.data);
break;
case 'PUBLIC_DATA_DECODED': // DECODE_PUBLIC_DATA
this.doHandleMessage(message.messageId, 'PUBLIC_DATA_DECODED', message, (message: any) => message.decodedData);
break;
case 'UPDATE_NOTIFIED': // NOTIFY_UPDATE
this.doHandleMessage(message.messageId, 'UPDATE_NOTIFIED', message, () => { });
break;
case 'STATE_VALIDATED': // VALIDATE_STATE
this.doHandleMessage(message.messageId, 'STATE_VALIDATED', message, (message: any) => message.validatedProcess);
break;
case 'VALUE_HASHED': // HASH_VALUE
this.doHandleMessage(message.messageId, 'VALUE_HASHED', message, (message: any) => message.hash);
break;
case 'MERKLE_PROOF_RETRIEVED': // GET_MERKLE_PROOF
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_RETRIEVED', message, (message: any) => message.proof);
break;
case 'MERKLE_PROOF_VALIDATED': // VALIDATE_MERKLE_PROOF
this.doHandleMessage(message.messageId, 'MERKLE_PROOF_VALIDATED', message, (message: any) => message.isValid);
break;
case 'ERROR':
console.error('Error:', message);
// Extract operation type from messageId by splitting on last underscore
const operationType = this.extractOperationTypeFromMessageId(message.messageId);
EventBus.getInstance().emit(`ERROR_${operationType}`, message.messageId, message.error);
break;
}
}
private extractOperationTypeFromMessageId(messageId: string): string {
// Split on last underscore to extract operation type
// e.g., "GET_PAIRING_ID_abc123" -> "GET_PAIRING_ID"
const lastUnderscoreIndex = messageId.lastIndexOf('_');
if (lastUnderscoreIndex === -1) {
return messageId; // No underscore found, return as-is
}
return messageId.substring(0, lastUnderscoreIndex);
}
private doHandleMessage(messageId: string, messageType: string, message: any, callback: (message: any) => any) {
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit(messageType, messageId, callback(message));
this.messagesSent.delete(messageId);
}
}