Goeleven.com

Projections - Code Example - Client

P

Today I'd like to continue with the code example that shows how to build offline progressive web apps using client side event sourcing.

The previous part covered the command handling side of the cqrs architecture, which I typically use.

This time I'll cover how to project events into a view model.

Roster Management

The scenario remains the same as in last post, where we created a new team roster.

This time we'll work on the second step in the roster management process and show the roster through a web component.

Client side aggregate root

The view model for our roster can be recreated based on events previously emitted by the aggregate root.

So far there is only one event type in the stream, RosterCreated, but the coded pattern is repeatable for any future event type that may be added to the lifecycle of a roster.

Projection

Converting a stream of events into state is the responsibility of a projection.

I typically created two classes to perform this job.

The first class will mediate between the event store and the projection logic itself to produce projected models.

It has the responsibility to:

  • Populate the underlying event store
  • Execute the projection based on the set of events loaded from the event store.

The projection logic itself is extracted into its own class.

This class has methods that are called by convention.

Invoking the correct method is done by the Projection base class. Based on the value in an event's context.what property, which contains the event type name, the correct project{EventName} method is resolved an invoked.

This approach is very similar to how the apply methods are called on the aggregate as explained in the command handling article.

import { Projection } from "../messagehandler.eventsourcing.projection.js"
import app from "../clubmanagement.roster.admin.app.js";

class RostersProjection{
    
    constructor(eventStore){
        this.eventStore = eventStore;
    }
    
    async populate(rosterId){
        await this.eventStore.populate(rosterId);
        app.topics.publish("roster.refreshed", rosterId);
    }

    async projectRoster(rosterId){     
        var projection = new RosterProjection();
        var events = await this.eventStore.get(rosterId);				
        var rosters = projection.project(events);
        return rosters[0];
    }

    async projectRosters(groupId){     
        var projection = new RosterProjection();
        var events = await this.eventStore.all();				
        var rosters = projection.project(events);
        return rosters.filter(r => r.groupId == groupId);
    }

}

class RosterProjection extends Projection{
    projectRosterCreated(roster, evt){
        roster.rosterId = evt.data.rosterId;
        roster.name = evt.data.name;
        roster.participations = [];
    }
}

export { RostersProjection };

Web component

To visualize a view model, I'll build multiple web components that show aspects of said viewmodel.

In this example I'll show the roster name, without any templating.

In the cache aside article, you can find another such component that shows the roster participations.

The behavior of the RosterName component is very similar to the CurrentRosterParticipants component discussed in the cache aside article.

It will first render itself based on previously cached events, by calling projectRoster(this.rosterId) on the projection instance, which is exposed by the roster admin app.

Only after rendering it will load newer events from the server by calling populate(this.rosterId) on the projection.

When the events are refreshed, which is detected by subcribing to the topic "roster.refreshed", the component will re-render itself.

import authorization from "../../clubmanagement.authorization.app.js";
import app from "../../clubmanagement.roster.admin.app.js";
import queryString from "../../dish.shell.querystring.js";

class RosterName extends HTMLElement {

  constructor() {
    super();

  }

  static get observedAttributes() {
    return ['data-roster-id'];
  }

  get rosterId() {
    return this.getAttribute('data-roster-id');
  }

  set rosterId(val) {
    if (val) {
      this.setAttribute('data-roster-id', val);
    } else {
      this.removeAttribute('data-roster-id');
    }
  }

  async connectedCallback() {  
    
    if(!this.rosterId){
        this.rosterId = queryString.get("r");
    }
    
    if(this.rosterId){
        await this.render();
        
        app.topics.subscribe("roster.refreshed", async () => await this.render());      
        app.topics.subscribe("roster.persisted", async () => await this.render());

        await this.load();

        authorization.topics.subscribe("authorized", async () => {
          await this.render();      
          await this.load();
        });   
    }    
  }

  async load(){
    if(authorization.organization){
      await app.projection.populate(this.rosterId);
    }
  }

  async render(){
 
     if(authorization.organization){

      var roster = await app.projection.projectRoster(this.rosterId);
      if(!roster) return;

      this.innerHTML = roster.name;
      
    }
  }
}

export { RosterName }

Infrastructure

The projection instance, used to generate the roster view model state, must also be initialized on startup.

So I've updated the RosterAdminApp, which is the entry point for this capability.

It now also creates an instance of RostersProjection by passing in the same RosterEventStore instance that is used by the RosterRepository.

The RosterRepository was used on the command handling side for mediating between the aggregate, used to emit RosterCreated, and this event store.

import dbFactory from "./messagehandler.eventsourcing.indexeddb.factory.js";
import shell from "./dish.shell.js";
import config from "./roster.admin/config.js";
import { Topics } from "./dish.shell.pubsub.js";
import { RosterEventStore } from "./roster.admin/eventstore.js";
import { RosterRepository } from "./roster.admin/repository.js";
import { RostersProjection } from "./roster.admin/projection.js";
import { RosterAdminFeature } from "./clubmanagement.roster.admin.feature.js";

class RosterAdminApp{
    
    constructor(){
        this.name = "Roster Admin";
        this.topics = new Topics();
    }

    async initialize(){             

        var _db = await dbFactory.open(config.dbname, config.dbversion, (db, tx) => {
            new RosterEventStore(db).upgrade(tx);
        });
    
        this.eventStore = new RosterEventStore(_db);
        this.repository = new RosterRepository(this.eventStore);
        this.projection = new RostersProjection(this.eventStore);

        shell.features.push(new RosterAdminFeature());  

    }

}

var app = new RosterAdminApp();

shell.apps.push(app);

export default app;

Wrapup

In this code walkthrough I've covered what is required to turn a stream of events into a read model.

For simple use cases, where you only want to show an aspect of a piece of state this approach is fine.

For more advanced cases, where true querying capabilities are required, you'll need a registry on top of the projected model.

Back to guide

This article is part of the building offline progressive web apps guide. Return to this guide to explore more aspects of building offline progressive web apps.

About the author

YVES GOELEVEN

I love to build web and cloud apps and have been doing so for the past 20 years.

My main areas of expertise are progressive web applications, event driven architecture, domain driven design, event sourcing, messaging, and Microsoft Azure.

Through this blog I hope to share some of this experience with you.

Get in touch

Sign up to my newsletter to get notified about new content

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.