How to share data between microfrontends with pub/sub and single-spa framework

207 views Asked by At

I'm implementing a microfrontends architecture, with the single-spa framework, in the scenario I'm implementing I need to share some data, this data will be posted in the topic by an angular application, and then redirected to another application in react, which should receive this data and execute the necessary logic for it. for example: let's imagine a flow where I have a list of products in an angular application, and the details of each product in a react application, and when clicking on the angular application details button, it redirects me to the react page with the product id and the react application makes the necessary call to the back end.

What I've already tried

I implemented the Pub/Sub class:

// @ts-ignore
import { v4 as uuid, validate as validateUUID } from "uuid";

type Topic = string;
type Message = Record<string, unknown>;
type ID = string;
type OnMessageFn = (message: Message) => void;

export type Subscribe = (topic: Topic, onMessage: OnMessageFn) => ID;
export type Publish = (topic: Topic, message: Record<string, unknown>) => void;
export type UnSusbscribe = (id: ID) => void;

export interface Events {
  subscribe: Subscribe;
  publish: Publish;
  unsubscribe: UnSusbscribe;
}

export class PubSub {
  constructor({ persistedTopics }: { persistedTopics?: Topic[] } = {}) {
    if (persistedTopics && !Array.isArray(persistedTopics)) {
      throw new Error("Persisted topics must be an array of topics.");
    }
    if (persistedTopics) {
      this.persistedMessages = persistedTopics.reduce(
        (acc: Record<Topic, Message>, cur: Topic) => {
          acc[cur] = {};
          return acc;
        },
        {}
      );
    }
    this.subscribe.bind(this);
    this.publish.bind(this);
  }

  private subscriberOnMsg: Record<ID, OnMessageFn> = {};
  private subscriberTopics: Record<ID, Topic> = {};
  private topics: Record<Topic, ID[]> = {};
  private topicMessages: Record<Topic, Message[]> = {};
  private persistedMessages: Record<Topic, Message> = {};

  /**
   * Assine as mensagens publicadas no tópico determinado.
   * @param topic Nome do canal/tópico onde as mensagens são publicadas.
   * @param onMessage Função chamada sempre que novas mensagens sobre o tema são publicadas.
   * @returns ID desta assinatura.
   */

  public subscribe(topic: Topic, onMessage: OnMessageFn): ID {
    debugger;
    console.log("Topic:", topic, "Message:", onMessage);
    if (typeof topic !== "string") throw new Error("Topic must be a string.");
    if (typeof onMessage !== "function")
      throw new Error("onMessage must be a function.");
    const subID = {} as any;
    if (!(topic in this.topics)) {
      this.topics[topic] = [subID];
    } else {
      this.topics[topic].push(subID);
    }
    this.subscriberOnMsg[subID] = onMessage;
    this.subscriberTopics[subID] = topic;
    if (topic in this.persistedMessages) {
      onMessage({ id: subID, message: this.persistedMessages[topic] });
    }
    return subID;
  }

  /**
   * Cancele a assinatura de um determinado ID de assinatura.
   * @param id ID da assinatura
   */
  public unsubscribe(id: ID): void {
    if (typeof id !== "string" || !validateUUID(id)) {
      throw new Error("ID must be a valid UUID.");
    }
    if (id in this.subscriberOnMsg && id in this.subscriberTopics) {
      delete this.subscriberOnMsg[id];
      const topic = this.subscriberTopics[id];
      if (topic && topic in this.topics) {
        const idx = this.topics[topic].findIndex((tID) => tID === id);
        if (idx > -1) {
          this.topics[topic].splice(idx, 1);
        }
        if (this.topics[topic].length === 0) {
          delete this.topics[topic];
        }
      }
      delete this.subscriberTopics[id];
    }
  }

  /**
   * Publique mensagens sobre um tópico para todos os assinantes receberem.
   * @param topic O tópico para onde a mensagem é enviada.
   * @param mensagem A mensagem a ser enviada. Apenas o formato de objeto é suportado.
   */
  public publish(topic: Topic, message: Message) {
    console.log("Topic:", topic, "Message:", message);
    debugger;
    if (typeof topic !== "string") throw new Error("Topic must be a string.");
    if (typeof message !== "object") {
      throw new Error("Message must be an object.");
    }
    // If topic exists, add the message to the messages array
    if (topic in this.topics) {
      if (!(topic in this.topicMessages)) {
        this.topicMessages[topic] = [];
      }
      this.topicMessages[topic].push(message);
    }
    // Notify subscribers
    if (topic in this.topics) {
      const subIDs = this.topics[topic];
      subIDs.forEach((id) => {
        if (id in this.subscriberOnMsg) {
          this.subscriberOnMsg[id](message);
        }
      });
    }
  }

  public getMessagesForSubscription(subscriptionId: ID): Message[] {
    debugger;
    console.log("Middleware SubscriptionId", subscriptionId);
    const topic = this.subscriberTopics[subscriptionId];

    console.log("Topico", topic);
    console.log("Mensagem do Topico", topic);

    if (topic && topic in this.topicMessages) {
      return this.topicMessages[topic];
    } else {
      return [];
    }
  }
}

export default PubSub ;

In the root-config application, I configured the pub/sub instance as follows

import { registerApplication, start } from "single-spa";
import PubSub from "../../middleware-data/src/middleware-data";
import {
  constructApplications,
  constructRoutes,
  constructLayoutEngine,
} from "single-spa-layout";
import microfrontendLayout from "./microfrontend-layout.html";

const routes = constructRoutes(microfrontendLayout);
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name);
  },
});
window.PubSub = new PubSub();

const layoutEngine = constructLayoutEngine({ routes, applications });

applications.forEach(registerApplication);
layoutEngine.activate();
start({
  urlRerouteOnly: true,
}); 

in the angular application I call the publish method

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
})
export class LoginComponent {
  username: string = '';
  password: string = '';
  loginError: boolean = false;

  constructor(private authService: AuthService, public router: Router) {
    window.PubSub;
  }
  onSubmit() {
    this.authService
      .login(this.username, this.password)
      .subscribe((success) => {
        if (success) {
          window.PubSub.publish('LoginSucesso', {
            'Teste Login': 'Login Efetuado',
          });
          this.router.navigate(['react/certificacao']);
        } else {
          this.loginError = true;
        }
      });
  }
}

in the react application I try to read the topic:

import { useEffect, useState } from "react";
import FormularioPesquisa from "./components/formpesquisa/formpesquisa";
import "./global.css";
export default function Root(props) {
  const pubSub = window.PubSub;
  const channel = new BroadcastChannel("dados-login");
  channel.onmessage = (event) => {
    console.log("Mensagem recebida:", event.data);
  };

  useEffect(() => {
    const subscriptionId = window.PubSub.subscribe("LoginSucesso", (data) => {
      console.log(data);
    });
    const ConteudoMensagem =
      window.PubSub.getMessagesForSubscription(subscriptionId);
    console.log("Mensagem recebida:", ConteudoMensagem);
    channel.onmessage = (event) => {
      console.log("Mensagem recebida:", event.data);
    };
  }, []);

  return (
    <div className="form-container">
      <FormularioPesquisa />
    </div>
  );
}

I expected that even after the redirection I would be able to read the message posted in the topic, since the pub/sub instance is being created in the root-config, but the subscriptionId always returns an empty object and the ContentMessage as an empty array.How to solve this problem?

0

There are 0 answers