Archive

Posts Tagged ‘Web Sockets’

Aurelia and Pusher Collection

September 3, 2019 Leave a comment

In this post, we are going to look at using Pusher and building a real-time collection. Just like our post on Firebase Collection, we are going to cover all the database CRUD (create, retrieve, update, delete) capabilities.

Pusher is a powerful solution when it comes to building real-time feature in your applications. We are going to be looking at the Channels product. Let’s get started by looking at what it takes to build a real-time collection using the Pusher API in Aurelia.

In the following example, we are going to be making the assumption that you are storing your data in a database like MongoDB. This implies that you will have a unique key, _id. As you will see, we will use this in our PusherCollection implementation.

PusherService

The first code example we will look at will deal with configuring the Pusher API:

import 'pusher-js';
import environment from '../environment.js';
const {baseUrl, apiKey, cluster, forceTLS} = environment.pusherConfig;

export class PusherService {
  constructor() {
    // Enable pusher logging - don't include this in production
    // Pusher.logToConsole = true;

    this.pusher = new Pusher(apiKey, {
      cluster: cluster,
      forceTLS: forceTLS,
      authEndpoint: `${baseUrl}pusher/auth`
    });
  }
  subscribe(channelName, eventName, dataHandler) {
    const channel = this.pusher.subscribe(channelName);
    channel.bind(eventName, dataHandler);
  }
  unsubscribe(channelName) {
    this.pusher.unsubscribe(channelName);
  }
}

With this class, we are wrapping the Pusher object passing in configuration settings from our environment.js file. Basically, we are creating a new instance of the Pusher object. Notice, that if you want to use private or presence concepts from Pusher, that an authEndpoint is included.

The final pieces are the two functions, subscribe and unscubscribe. These functions simply allow us to subscribe to a given channel and event as well as passing along a handler that will react when the given channel and event fire. The unsubscribe simply unsubscribes from the Pusher object.

PusherCollection

Let’s move on to our PusherCollection object:

import {Container} from 'aurelia-dependency-injection';
import {PusherService} from '../services/pusher-service.js';

export class PusherCollection {
  constructor(config = {dependencies: [], channel: '', initItems: [], callbacks: {}}) {
    this.pusherSvc = Container.instance.get(PusherService);
    const {channel, initItems = [], callbacks = {}} = config;
    this.channel = channel;
    this.items = initItems;
    this.callbacks = callbacks;

    this.listen();
    if (this.callbacks['init']) {
      this.callbacks['init']();
    }
  }
  listen() {
    this.pusherSvc.subscribe(this.channel, 'item-added', this.onItemAdded.bind(this));
    this.pusherSvc.subscribe(this.channel, 'items-added', this.onItemsAdded.bind(this));
    this.pusherSvc.subscribe(this.channel, 'item-removed', this.onItemRemoved.bind(this));
    this.pusherSvc.subscribe(this.channel, 'items-removed', this.onItemsRemoved.bind(this));
    this.pusherSvc.subscribe(this.channel, 'item-updated', this.onItemUpdated.bind(this));
    this.pusherSvc.subscribe(this.channel, 'items-updated', this.onItemsUpdated.bind(this));
  }
  stopListening() {
    this.pusherSvc.unsubscribe(this.channel);
    this.pusherSvc = null;
  }
  async onItemAdded(data) {
    this.items.push(data);
    if (this.callbacks['item-added']) {
      this.callbacks['item-added']();
    }
  }
  async onItemsAdded(data = []) {
    this.items.push(...data);
    if (this.callbacks['items-added']) {
      this.callbacks['items-added']();
    }
  }
  async onItemRemoved(data) {
    const {_id} = data;
    const index = this.items.findIndex(c => c._id === _id);
    this.items.splice(index, 1);
    if (this.callbacks['item-removed']) {
      this.callbacks['item-removed']();
    }
  }
  async onItemsRemoved(data = []) {
    for (let d of data) {
      const {_id} = d;
      const index = this.items.findIndex(c => c._id === _id);
      this.items.splice(index, 1);
    }
    if (this.callbacks['items-removed']) {
      this.callbacks['items-removed']();
    }
  }
  async onItemUpdated(data) {
    const {_id} = data;
    const index = this.items.findIndex(c => c._id === _id);
    if (index > -1) {
      if (Array.isArray(this.items[index])) {
        this.items[index].push(data);
      } else {
        this.items[index] = Object.assign(this.items[index], data);
      }
      if (this.callbacks['item-updated']) {
        this.callbacks['item-updated']();
      }
    }
  }
  async onItemsUpdated(data = []) {
    for (let d of data) {
      const {_id} = d;
      const index = this.items.findIndex(c => c._id === _id);
      this.items[index] = Object.assign(this.items[index], data);
    }
    if (this.callbacks['items-updated']) {
      this.callbacks['items-updated']();
    }
  }
}

We start off by importing Container and PusherService. We use the Container to allow us to get an instance of the PusherService. The constructor takes a single config object. It is comprised of the following:

  • dependencies – a collection of dependencies that could possibly be necessary
  • channel – the channel name
  • initItems – the initial array of items
  • callbacks – callback handlers for individual events

We wire up all the handlers and events for a given channel by calling the listen() function. If there is an init callback, we also call it.

listen()

This function simply subscribes to a set of events and wires up each with a handler.

stopListening()

This function unsubscribes all events for a given channel and releases the instance of the PusherService.

on ItemAdded(data)

This functions handles when an item is added. It pushes the data on to the collection. If a callback, item-added, is provided, then it is called.

onItemsAdded(data = [])

This function handles when multiple items are added. It does this by pushing all the items onto the collection. If a callback, items-added, is provided, then it is called.

onItemRemoved(data)

This function handles when an item is removed. It grabs the _id from the data object and finds the index in the items collection. If a valid index is found, then it is spliced from the items collection. If a callback, item-removed, is provided, then it is called.

onItemsRemoved(data = [])

This function handles when multiple items are removed. It does this by looping over the data collection. It grabs the _id from each individual item and finds the index in the items collection. If a valid index is found, then it is spliced from the items collection. If a callback, items-removed, is provided, then it is called.

onItemUpdated(data)

This function handles when an item is updated. It grabs the _id from the data object and finds the index in the items collection. If a valid index is found, then the location is updated with the data object. If a callback, item-updated, is provided, then it is called.

onItemsUpdated(data = [])

This function handles when multiple items are updated. It does this by looping over the data collection. It grabs the _id from each individual item and finds the index in the items collection. If a valid index is found, then it is updated with the data object. If a callback, items-updated, is provided, then it is called.

Events and Callbacks

As you have already noticed, we are using a convention of events:

  • item-added
  • items-added
  • item-removed
  • items-removed
  • item-updated
  • items-updated

This allows us to have a generic data API so that we can handle any CRUD operation for a given channel. Think of a channel as a table or collection in your database.

We also have a set of callbacks:

  • init
  • item-added
  • items-added
  • item-removed
  • items-removed
  • item-updated
  • items-updated

These callbacks provide a means for doing any custom UI operations like animations or moving to the corresponding change.

UI Sample

This is where Aurelia really shines, we now have a real-time object in the form of a PusherCollection. It exposes an items property and we can simply use it in our HTML markup normally.

View

<table>
  ...
  <tbody>
    <tr repeat.for="item in quoteItems.items">
      ...
    </tr>
  </tbody>
</table>

This simplified markup focuses on the binding of the items collection.

View Model

The following snippet is how you would use the PusherCollection in a given view model:

// Load initial data
// data = [...] or []
// Wire up Pusher
const channelName = 'quotes';
const config = {
  dependencies: [],
  channel: channelName,
  initItems: data,
  callbacks: {
    'init': async () => {
      await wait(750);
      this.scrollToBottom();
    },
    'item-added': async () => {
      await wait(250);
      this.scrollToBottom();
    }
  }
}
this.quoteItems = new PusherCollection(config);

In our view model, we either load up initial data or have an empty array. We set the channelName, initItems, and callbacks. In this example, we are wiring up the init callback to scroll to the bottom of the array of data. In the item-added callback we perform the same operation. We can do pretty much anything we want to help notify the user of changes coming in.

UI

Here is a screen shot of a table wired to a PusherCollection in action:

PusherCollection in action

Hope this helps!

Aurelia and Firebase Collection

July 25, 2019 1 comment

Building a vertical line of business application can get complicated quickly. It is easy to simply provide a forms over data solution that allows user to perform the various database CRUD (create, retrieve, update, delete) capabilities. However, you will find most clients are now demanding more mature and advanced user experiences. One such experience is providing real-time or near real-time updates. With the broad browser support of Web Sockets this is not only feasible but most Cloud providers offer their own flavor of allowing you to integrate this feature in your application.

We will be looking at one such offering, Firebase Realtime Database. Although the example code that we will evaluate is Firebase Realtime Database specific, it would be easy to modify and work with Firestore or even your own REST API using Socket.io or SignalR.

Before we dive into looking at the code, it is important to understand what Web Sockets bring to the table and how it is different from HTTP. When serving static assets from a server, HTTP is the perfect protocol. However, you will find that it is limited with regard to long-lasting connections and bi-directional communication. Web Sockets give us a protocol that allows to establish a connection with a server and then subscribe to events that we are interested in by listening for changes. If we were to build a chat application, we might want to listen to a message event in order to update our user interface.

The chat example would most likely have a collection of message objects, each with a set of properties that further describe the message and other metadata. It is possible to roll your own implementation and write a listener that is subscribing to message events and updates an items collection. Wouldn’t it be nice if you could simply create an instance of a socket aware object that could listen to inserts, updates, or deletes? This is where Firebase Collection comes in.

Code Example

This example code assumes you have working knowledge of Aurelia as well as have played with Google Firebase Realtime Database. Let’s take a look a the implementation:

import environment from '../environment';

export class FirebaseCollection {

  firebaseConfig = environment.firebaseQuoteConfig;
  query = null;
  valueMap = new Map();
  items = [];

  constructor(path) {
    if (firebase) {
      const app = firebase.apps.find(f => f.name === path);
      if (app) {
        this.query = app.database().ref(path);
        this.listenToQuery(this.query);

      } else {
        const fb = firebase.initializeApp(this.firebaseConfig, path);
        this.query = fb.database().ref(path);
        this.listenToQuery(this.query);
      }
    }
  }
  listenToQuery(query) {
    query.on('child_added', (snapshot, previousKey) => {
      this.onItemAdded(snapshot, previousKey);
    });
    query.on('child_removed', (snapshot) => {
      this.onItemRemoved(snapshot);
    });
    query.on('child_changed', (snapshot, previousKey) => {
      this.onItemChanged(snapshot, previousKey);
    });
    query.on('child_moved', (snapshot, previousKey) => {
      this.onItemMoved(snapshot, previousKey);
    });
  }
  stopListeningToQuery() {
    this.query.off();
  }
  onItemAdded(snapshot, previousKey) {
    let value = this.valueFromSnapshot(snapshot);
    let index = previousKey !== null ?
      this.items.indexOf(this.valueMap.get(previousKey)) + 1 : 0;
    this.valueMap.set(value.__firebaseKey__, value);
    this.items.splice(index, 0, value);
  }
  onItemRemoved(oldSnapshot) {
    let key = oldSnapshot.key;
    let value = this.valueMap.get(key);

    if (!value) {
      return;
    }

    let index = this.items.indexOf(value);
    this._valueMap.delete(key);
    if (index !== -1) {
      this.items.splice(index, 1);
    }
  }
  onItemChanged(snapshot, previousKey) {
    let value = this.valueFromSnapshot(snapshot);
    let oldValue = this._valueMap.get(value.__firebaseKey__);

    if (!oldValue) {
      return;
    }

    this._valueMap.delete(oldValue.__firebaseKey__);
    this._valueMap.set(value.__firebaseKey__, value);
    this.items.splice(this.items.indexOf(oldValue), 1, value);
  }
  onItemMoved(snapshot, previousKey) {
    let key = snapshot.key;
    let value = this._valueMap.get(key);

    if (!value) {
      return;
    }

    let previousValue = this.valueMap.get(previousKey);
    let newIndex = previousValue !== null ? this.items.indexOf(previousValue) + 1 : 0;
    this.items.splice(this.items.indexOf(value), 1);
    this.items.splice(newIndex, 0, value);
  }
  valueFromSnapshot(snapshot) {
    let value = snapshot.val();
    if (!(value instanceof Object)) {
      value = {
        value: value,
        __firebasePrimitive__: true
      };
    }
    value.__firebaseKey__ = snapshot.key;
    return value;
  }
  add(item) {
    return new Promise((resolve, reject) => {
      let query = this.query.ref().push();
      query.set(item, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve(item);
      });
    });
  }
  remove(item) {
    if (item === null || item.__firebaseKey__ === null) {
      return Promise.reject({message: 'Unknown item'});
    }
    return this.removeByKey(item.__firebaseKey__);
  }
  getByKey(key) {
    return this.valueMap.get(key);
  }
  removeByKey(key) {
    return new Promise((resolve, reject) => {
      this.query.ref().child(key).remove((error) =>{
        if (error) {
          reject(error);
          return;
        }
        resolve(key);
      });
    });
  }
  clear() {
    return new Promise((resolve, reject) => {
      let query = this.query.ref();
      query.remove((error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });
  }
}

Don’t be overwhelmed if you feel like this appears to be overly complex. Hopefully, after we review it, you will feel more confident with how it works.

Firebase Events and Handlers

We begin by importing environment that provides the configuration information for Firebase. Next, the constructor handles configuring the Firebase instance as well as creating a query to listen for changes and a function call, listenToQuery, which handles a the following events: child_added, child_removed, child_changed, child_moved. When each of these events fire, a corresponding handler is called: onItemAdded, onItemRemoved, onItemChanged, onItemMoved.

There is also a function stopListeningToQuery. This simply turns off the subscriptions previously defined.

The onItemAdded function takes two parameters, snapshot and previousKey. It first tries to obtain the value from the snapshot calling the helper function, valueFromSnapshot. Next, it determines the index by seeing if the previousKey is not null and finding the index of the previousKey + 1 or by simply using 0. The valueMap object is next updated with the key and value. Finally, the value is spliced into the items array at the given index position.

The onItemRemoved function takes a single parameter, oldSnapshot, and accesses the key property. It then tries to find the corresponding object stored in the valueMap using the key. If no value is found, we simply return out of the function. If the value is not null, then we obtain the index from the items array. We then delete the key from the valueMap and, finally, splice the object from the items array using the index.

The onItemChanged function takes two parameters, snapshot and previousKey. It first tries to obtain the value from the snapshot calling the helper function, valueFromSnapshot. Next, it tries to find the existing object stored in the valueMap using the__firebaseKey__ from the value. If no oldValue is found, we simply return out of the function. Otherwise, we remove the oldValue from the valueMap and also set the new updated value to the valueMap. Finally, we splice in the new value while removing the old value in the items array.

Data Functions

Each of the following functions will utilize the Firebase API to affect the Realtime Database directly. The user interface will react accordingly when a given event is fired and the corresponding handler handles the event, thus updating the items array. With Aurelia this is a simple repeat.for binding.

The add function takes in a single parameter, item. This simply uses the Firebase API to push the item onto the watched query. This function is promise based and either returns the item upon success or rejects the promise passing the error.

The remove function takes in a single parameter, item. It first checks if the item is not null as well as the property, __firebaseKey is not null. It then simply removes the item by calling a helper function, removeByKey.

The getByKey function takes in a single parameter, key. It simply returns a lookup in the valueMap based on the key.

The removeByKey function takes in a single parameter, key. It returns a promise that attempts to remove the key from the underlying query. It will either resolve the key upon success or reject the promise passing the error.

The clear function returns a promise. It attempts to clear the underlying query. It will either resolve upon success or reject the promise passing the error.

Sample HTML Usage

Let’s now shift gears and look at a simple HTML binding that will respond to our simple chat example. Consider the following example:

<template>
  <div class="chat-container">
    <div class="chat-header">
      <div class="flex-column-1">
        <h4>Chatting in ${channel}</h4>
      </div>
      <div class="flex-column-none">
        <span class="chat-header-close" click.delegate="toggleChatSidebar($event)">
          <i class="fas fa-times"></i>
        </span>
      </div>
    </div>
    <div class="chat-content flex-row-1 margin-bottom-10 overflow-y-auto">
      <ul class="chat-messages">
        <li repeat.for="m of collection.items"
          class="flex-column-1">
          <div class="message-data ${username.toLowerCase() == m.username.toLowerCase() ? '' : 'align-right'}">
            <span class="message-data-name">
              <i class="fa fa-circle online"></i>
              ${m.name | properCase}
            </span>
            <span class="message-data-time">${m.created_date | dateFormat:'date-time'}</span>
            <span class="message-data-delete"
              click.delegate="deleteMessage($event, $index, channel, m)">
              <i class="fa fa-times pointer-events-none"></i>
            </span>
          </div>
          <div class="message ${username.toLowerCase() == m.username.toLowerCase() ? 'my-message' : 'other-message align-self-end'}">
            ${m.msg}
          </div>
        </li>
      </ul>
   </div>
    <form id="messageInputForm" class="chat-input">
      <div class="input-group">
        <input id="messageInput" 
          class="form-control" 
          placeholder="Type your message..."
          value.bind="message"
          keydown.delegate="handleKeydown($event)">
        <div class="input-group-append">
          <button class="btn btn-outline-secondary" type="button"
            click.delegate="sendMessage($event)">SEND</button>
        </div>
      </div>
    </form>
  </div>
</template>

Of all the markup we see, we are really only concerned with the following line:

<li repeat.for="m of collection.items"
          class="flex-column-1">

As you can see, we can have multiple channels for chatting. Each channel would be an instance of the Firebase Collection and we access the collection by referencing the items property.

Sample View Model

In the view model constructor you can simply create a new instance of the Firebase Collection and pass in a path.

import environment from '../../../environment';
import {FirebaseCollection} from '../../../models/firebase-collection';

export class Chat {
  static inject() {
    return [Element];
  }

  collection;
  channel = 'lobby';

  constructor(element) {
    this.element = element;
    this.path = `channels/${this.channel}`;
  }
  attached() {
    this.collection = new FirebaseCollection(this.path);
  }
  detached() {
    this.collection.stopListening();
  }
}

As you can see from above, the view model is minimalistic. Now that we have a FirebaseCollection, we can simply instantiate as many as we need throughout our and gain all the benefits of a realtime collection.

Here is a screen shot of the chat panel in action:

Firebase Collection in action