Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

An Angular PWA From Front-End to Backend: Send/Receive Messages

DZone 's Guide to

An Angular PWA From Front-End to Backend: Send/Receive Messages

Learn how to build a PWS with Angular on the front-end, Spring Boot on the backend, and MongoDB to persist data.

· Web Dev Zone ·
Free Resource

This is the third part of the series about the AngularPwaMessenger project. It is a chat system with offline capability. The second part showed how the login process for online and offline works.

This part will be about how to send and receive messages. A message can be sent while offline. It is then stored in the indexed DB and sent when the device is online again.

The encryption of the messages will not be discusses because it adds optional complexity.

Architecture

The AngularPwaMessenger uses polling to send and receive messages. The use of push messages uses a lot of threads on the server what limits the scalability of the server or needs an external service like Firebase. The goal of the project is to have a standalone messaging service that is easy to use and to deploy. That are the reasons for a polling based solution.

The server works only as a message relay. It stores the messages that are sent until the receiver gets them. The chat history is not stored on the server to limit the database requirement of the service. That shifts the burden of keeping the chat history to the client and distributes the storage demand.

The Message Record

The message record that is used and stored on the client side is the message.ts file:

export interface Message {
  id?: number;
  fromId: string;
  toId: string;
  timestamp?: Date,
  text: string,
  send: boolean,
  received: boolean
}

In line 2, the id is the id of the local indexed DB record.

In line 3, the fromId is the MongoDB ID of the current user.

In line 4, the toId is the MongoDB ID of the receiver of the message.

In line 5, the timestamp is the time the server received the message.

In line 6, the text is the message text or image.

In line 7, we have the send flag that shows that the message was sent to the server.

In line 8, is the received flag shows that the message was pulled from the server by the receiver.

The message record on the server side is the Message.java file:

@Document
public class Message {
@Id
private ObjectId _id;
@Indexed
@JsonProperty
private String fromId;
@Indexed
@JsonProperty
private String toId;
@JsonProperty
private Long id;
@JsonProperty
private Date timestamp;
@JsonProperty
private String text;
@JsonProperty
private boolean send;
@JsonProperty
private boolean received;

Lines 1-2 define the class as a MongoDB document dto.

Lines 3-4 define the unique MongoDB object ID that is created by the database.

Lines 5-7 define the fromId that has the object ID of the contact record of the sender. The property is indexed by MongoDB.

Lines 8-10 define the toId that has the object id of the contact record of the receiver. The property is indexed by MongoDB.

Lines 11-12 define the id that stores the ID of the local record of the sender.

Lines 13-14 define the timestamp when the server received the message.

Lines 15-16 define the text content of the message.

Lines 17-18 define the send flag that shows that the server has received the message.

Lines 19-20 defines the received flag that shows that the client has polled the message.

A message can be a text or an image. The image gets base64 encoded and is also send in the text field.

Sending a Message

The Front-End

To send a message the sendMessage method in the main.component.ts is called:

  sendMessage( msg: Message ) {
    msg.fromId = this.ownContact.userId;
    this.cryptoService.encryptTextAes( this.myUser.password, this.myUser.salt, msg.text )
      .then( value => {
      msg.text = value;
      return msg;
    } ).then( myMsg => this.localdbService.storeMessage( myMsg ) )
      .then( () => this.addMessages().then( () => this.syncMsgs() ) );
  }

In lines 1-2, the method is defined and the fromId of the message is set.

In line 7, the message gets stored in the indexed DB.

In line 8, the addMessages method shows the new message in the component and the syncMsgs method starts a new send/receive call to the server.

To send or receive the messages stored in indexed DB the syncMsgs method is called:

private syncMsgs() {
    if ( this.ownContact && this.netConnectionService.connetionStatus 
        && !this.jwttokenService.localLogin ) {
      const contactIds = this.contacts.map( con => con.userId );
      const syncMsgs1: SyncMsgs = {
        ownId: this.ownContact.userId,
        contactIds: contactIds,
        lastUpdate: this.getLastSyncDate()
      };
      this.receiveRemoteMsgs( syncMsgs1 );
      this.sendRemoteMsgs( syncMsgs1 );
      this.storeReceivedMessages();
    }
}

In lines 2-3, we check that the ownContact property exists and the device has a network connection and the client has a JWT token from the server.

In line 4, the contactIds of all the contacts are mapped in an array.

In lines 6-8, the ownId is set to receive messages sent to the user. The contacts are set to pull the messages from my contacts only. The lastUpdate date is set to the newest message in the indexed DB to set a time window.

In line 10, the receive method is called.

In line 11, the send method is called.

In line 12, the storeReceivedMessages method is called to update the records of messages that have been polled by the receiver.

To send the message, the sendRemoteMsgs method is called:

private sendRemoteMsgs( syncMsgs1: SyncMsgs ) {
    this.localdbService.toSyncMessages( this.ownContact ).then( msgs => {
      const oriMsgs: Message[] = JSON.parse( JSON.stringify( msgs ) );
      this.decryptLocalMsgs( msgs ).then( value => {
        const promises: PromiseLike<Message>[] = [];
        value.forEach( msg => {
          const fromCon = !this.contacts.filter( con => con.userId = msg.toId ) ? null : this.contacts.filter( con => con.userId = msg.toId )[0];
          if ( !fromCon ) {
            console.log( fromCon );
          } else {
            promises.push( (this.cryptoService.encryptLargeText(msg.text, fromCon.publicKey)).then( result => {
              msg.text = result;
              return msg;
            } ) );
          }
        } );
        Promise.all( promises ).then( myMsgs => {
          const syncMsgs2: SyncMsgs = {
            ownId: this.ownContact.userId,
            msgs: myMsgs
          };
          this.messageService.sendMessages( syncMsgs2 ).subscribe( myMsgs => {
            const promises2: PromiseLike<number>[] = [];
            msgs.forEach( msg => {
              const newMsg = oriMsgs.filter( oriMsg => oriMsg.id === msg.id )[0];
              const myMsg = myMsgs.filter( myMsg2 => myMsg2.id === msg.id )[0];
              newMsg.send = true;
              newMsg.timestamp = myMsg.timestamp;
              promises2.push( this.localdbService.updateMessage( newMsg ) );//.then(result => console.log(msg), reject => console.log(reject));
            } );
            Promise.all( promises2 ).then( () => this.addMessages() );
          }, error => console.log( 'sendRemoteMsgs failed.' ) );
        } )
      } );
    } );
  }

In line 2, the messages to sync are pulled from the indexed DB.

In line 3, the messages are deep cloned to store them again later.

In lines 4-17, the messages are decrypted and encrypted for transmission.

In lines 17-21, the SyncMsgs interface is created with the messages to send and the sender object ID.

In line 22, the messages are sent and the result with the updated messages from the server is received.

In lines 24-30, the original messages that were pulled from the indexed DB are updated with the send flag and the server timestamp from the messages returned by the server and then stored again with the updateMessage method.

In line 31, Promise.all() waits until all updates are done and then calles the addMessages() method to update the currently displayed messages.

The Backend

The backend has the putStoreMessage() method in the MessageController:

@PostMapping("/storeMsgs")
public Flux<Message> putStoreMessages(@RequestBody SyncMsgs syncMsgs) {
    List<Message> msgs = syncMsgs.getMsgs().stream().map(msg -> {
        msg.setSend(true);
        msg.setTimestamp(new Date());
        return msg;
    }).collect(Collectors.toList());
    return this.operations.insertAll(msgs);
}

In line 1, the POST REST endpoint is defined.

In line 2, the reactive method is defined and the requestbody is mapped in the syncMsgs.

In lines 3-7, the flag is sent and the timestamps are set.

In line 8, the messages are inserted in MongoDB and the new messages are returned with the response.

Receiving a Message

The Front-End

The polling of messages works in a 15 second interval that calls the SyncMsgs() method that has been shown in the message sending. The SyncMsgs() method calls the receiveRemoteMsgs() method in the main.component.ts file:

  private receiveRemoteMsgs( syncMsgs1: SyncMsgs ) {
    this.messageService.findMessages( syncMsgs1 ).subscribe( msgs => {
      let promises: PromiseLike<Message>[] = [];
      msgs = msgs.filter( msg => syncMsgs1.lastUpdate.getTime() < new Date( msg.timestamp ).getTime() );
      msgs.forEach( msg => {
        promises.push( (this.cryptoService.decryptLargeText(msg.text, this.myUser.privateKey, this.myUser.password)).then( value => {
          msg.text = value;
          return msg;
        } ) );
      } );
      Promise.all( promises ).then( myMsgs => {
        let promises2: PromiseLike<Promise<number>>[] = [];
        myMsgs.forEach( msg => {
          promises2.push(
            this.cryptoService.encryptTextAes( this.myUser.password, this.myUser.salt, msg.text )
              .then( value => {
                msg.text = value;
                return msg;
              } ).then( myValue => this.localdbService.storeMessage( msg ) ) );
        } );

        Promise.all( promises2 ).then( values => Promise.all( values ).then( myValue => {
          if ( promises.length > 0 ) {
            this.addMessages();
          }
        } ) );
      } )
    }, error => console.log( 'findMessages failed.' ) );
}

In line 2, messageService is called to get the messages from the server. The SyncMsgs parameter has the MongoDB object ID, the contact IDs to get, and the lastUpdate to get only pending messages.

In line 4, the received messages are filtered to get only the messages that are newer than the lastUpdate.

In lines 5-10, the decrytion happens.

In lines 11-18, the local encryption happens.

In line 19, the new message is stored in the indexed DB with the storeMessage(...) call.

In line 22, the Promise.all(...) calls wait until all messages are stored in the indexed DB.

In line 23, it is checked if a new message was received.

In line 24, addMessages is called to update to displayed messages out of the indexed DB.

In line 28, an error is logged if the findMessges call to the server fails.

The Backend

The backend has the findMessages(...) method in the MessageController.java file:

@PostMapping("/findMsgs")
public Flux<Message> getFindMessages(@RequestBody SyncMsgs syncMsgs) {
     List<Message> msgToUpdate = new LinkedList<>();
     return operations
        .find(new Query().addCriteria(Criteria.where("fromId").in(syncMsgs.getContactIds())
        .orOperator(Criteria.where("toId").is(syncMsgs.getOwnId())
        .andOperator(Criteria.where("timestamp").gt(syncMsgs.getLastUpdate())))),
        Message.class)
       .doOnEach(msg -> {
          if (msg.hasValue()) {
          msg.get().setReceived(true);
          msgToUpdate.add(msg.get());
       }}).doAfterTerminate(() -> msgToUpdate.forEach(msg -> 
            operations.save(msg).block()));
}

In line 1, the request mapping is defined.

In line 2, the reactive getFindMessages method is defined and the SyncMsgs parameter is mapped from the body.

In lines 4-8, a MongoDB query is build with the fromId criteria that checks for messages from the users contacts and the toId criteria that checks that the message is send to the user and the timestamp that checks that the message is newer than the newest message on the client.

In lines 9-13, the the received flag is set on the messages that are send to the client and the messages are added to the msgToUpdate list.

In lines 14-15, the the messages with the new set received flag are stored in the MongoDB. It is done after the response is send to the client. Then the client can request the state of the now received message.

Conclusion

That amount of code is sufficiant to send and receive messages. The technology stack of Angular with Material/Dexie in the front-end and and Spring Boot and MongoDB worked very well on the project. It took less code than expected to implement the standalone chat system with a PWA frontend. Implementing a PWA has become a simplE, flexible alternative to implementing an app for mobile devices and should considered if the HTML5 APIs are sufficiant.

The next part of the series will be about a Kubernetes deployment of the chat system with Minikube, Helm, and Ingress (for SSL support).

Topics:
typescript ,mongodb ,web dev ,angular tutorial ,spring boot tutorial ,java web development

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}