Skip to main content

Walkthrough

The nodes and apps built out in this repository are intended to demonstrate the real-time, cross-node data synchronization patterns enabled by the underlying libraries that they are built upon. The sections that follow will walk through the scenarios that demonstrate these patterns and highlight some of the infrastructure that enables the associated functionality.

info

This guide assumes that you have already followed the steps in getting-started.

tip

This guide will provide a high-level overview of how the underlying functionality is enabled. The Tutorial will provide a thorough step-by-step guide for how to build a node and an app with the underlying libraries.

Also, a lot of the infrastructure demonstrated in this walkthrough is from the underlying libraries. There is a lot of complexity that is exposed here to illustrate how this functionality is enabled. Reference the actual nodes (Proposals and Workflows) and apps (Proposals and Workflows) to see how the libraries simplify the implementation details.

Setup

If you haven't already, make sure that all nodes and apps are running. Be sure the apps are started with the approprate command (npm run start:dev if running local or npm run start:code if running in a codespace). The sections that follow will demonstrate how the API facilitates real time data synchronization and cross-node interactivity. With this in mind, you will want to configure two browser windows side by side with each app running in tabs in both windows:

browser-setup

Internal Node Synchronization

Mutations to data within a node are executed through Command methods. This is typically exposed as a Save method on a specific Entity type internally owned by the node. When a Command is executed, an Event is broadcast with a message containing the affected data. Any node or application with access to a Listener associated with the affected data can then react to the change in state.

If additional data mutations need to occur as a result of the command, these interactions can be defined through the use of Hook methods. These hooks represent points in the Command method where functionality can be injected. For instance, OnAdd is executed prior to the mutations implemented in the internal Add method. On the other hand, AfterUpdate is executed after the mutations implemented in the internal Update method. OnSave is executed prior to the mutations of either Add or Update via the public Save method.

You'll notice that the Save and Remove methods are transactional through the use of db.Database.BeginTransactionAsync. If any aspect of the method fails, to include the result of any hook calls, the entire transaction is rolled back.

To see this in action, execute the following steps with both browser windows showing the Proposals app:

  1. Click the Create Proposal button indicated by the plus icon to the left of the Home heading.

  2. Provide a Title and Value in the Add Proposal dialog, then click Save Proposal:

There are two things to note about this transaction:

  1. The Proposal record that was created synchronized across all connected instances of the Proposals app.

  2. In addition to creating a Proposal, an associated Status was created (illustrated by the Created status to the top right of the Proposal card in the application).

The EntityCommand class defines a SyncAdd hook that is called after the internal Add transaction is successful. Every EntityCommand must be initialized with an EventHub, which is a strongly-typed SignalR hub that is defined with an IEventHub interface. When SyncAdd is called, it broadcasts the added entity through the IEventHub.OnAdd method.

A TypeScript EventListener class is provided that allows you to define interfaces to EventHub instances. In this case, a ProposalListener is defined and injected into the HomeRoute. In the ngOnInit lifecycle hook, the listener is initialized and events are registered.

The ProposalCommand provides an AfterAdd hook that generates a created status and saves it to the proposal.

Cross-Node Synchronization

There are three primary interfaces that facilitate cross-node interactivity:

  • Contract - an object that is sent to the remote node to initiate interactivity and track progress across the service lifetime.
    • Package is a Workflows node Contract.
  • Gateway - the API interface for sending to and retrieving data from a remote node.
  • Listener - the event listener provided by the remote node for tracking contract events.

The following scenarios show patterns for working with these interfaces.

Submitting a Contract

The following interactions occur to facilitate submitting a contract to a remote node:

  1. The data for the contract is filled out by the initiating node
  2. The contract is sent through the gateway interface for the remote node
  3. The remote node receives and processes the contract
  4. The remote node broadcasts an event when the contract has been successfully processed

To see this in action, set the left browser tab to the Proposals app and the right browser tab to the Workflows app. Execute the following steps:

  1. In the Proposals app on the Documentation Proposal card, click the middle Submit Package icon button represented by a blue right-facing arrow.

  2. A PackageDialog will open. Provide entries for Title and Value, then click Save Package.

The PackageDialog used to submit the package, along with its associated PackageForm, are exposed through the @distributed/toolkit library as part of the common infrastructure for interfacing with the Workflows node. When the Package contract is saved through the PackageForm, it uses the WorkflowsGateway service to submit the package. The gateway endpoint is configured within each app whenever the ToolkitModule is imported (see Proposals - AppModule).

The GatewayController.SubmitPackage method in the workflows node passes the received package to the PackageCommand.Submit method. Since this is a new package, the Save method will be called, which will trigger the PackageCommand.SyncAdd hook (in the same way as the ProposalCommand service when a new Proposal is created).

The PackageListener defined in @distributed/toolkit is injected in the home route of both the proposals app and the workflows app. Both routes initialize the listener in the respective ngOnInit lifecycle hooks: see proposals and workflows.

The proposals app uses an Observable trigger to feed into each ProposalCard. The card component uses this trigger to handle package events if the received event is associated with its proposal.

If you retrieve the Proposal both before and after submitting the Package, you'll notice that a packageId is assigned when a Package is successfully submitted:

Before

{
"statusId": 1,
"title": "Documentation Proposal",
"id": 1,
"type": "Proposals.Entities.Proposal",
"value": "A demonstrative Proposoal for documentation purposes.",
"dateCreated": "2023-10-13T12:38:52.3668313",
"dateModified": "2023-10-13T13:11:11.73073"
}

After

{
"statusId": 1,
"packageId": 1,
"title": "Documentation Proposal",
"id": 1,
"type": "Proposals.Entities.Proposal",
"value": "A demonstrative Proposoal for documentation purposes.",
"dateCreated": "2023-10-13T12:38:52.3668313",
"dateModified": "2023-10-13T13:15:11.73073"
}

Leveraging an EventListener is not exclusive to client apps. In fact, synchronizing node data that's associated with contracts in remote nodes is exclusively handled through EntitySaga services through events.

In this case, the proposals node defines a PackageListener that reacts to Package events. In this case, whenever a Package is added, the OnAdd event triggers the PackageSaga.OnAdd method. Internal changes spawned by a Saga will still broadcast update events for the affected Entities.

Reacting to Contract Changes

The PackageEventHub defined by the workflows node provides an additional OnStateChanged event. This is used to isolate changes to Package.State from changes to the editable Package entity fields.

In the PackageCommand service, there is validation to ensure state is not improperly modified as well as validation to ensure proper state transitions. Also, a Package can only be modified if it is not in a completed state.

The PackageCommand service also defines methods for changing the state of a Package.

In the Workflows app, pending packages provide three actions for handling that package:

pending-package-card

From left to right, those actions are:

  • Reject - permanently reject the package that was submitted
  • Return - return the package that was submitted for corrections
  • Approve - approve the package and change the status to the associated Entity

The following clips demonstrate returning, resubmitting, and approving:

Returning a Package

Resubmitting a Package

Approving a Package

The Proposal is able to detect when the associated Package is complete because the PackageListener.OnStateChanged event triggers the PackageSaga.OnStateChanged method. The ProposalCardComponent is able to track whether a Package can be submitted via the canSubmit function, which is kept synchronized through the handlePackageEvent trigger.

Cleaning Up Contracts

It's easy to synchronize internal state with changes to contracts through events because the node is aware of the remote node. The inverse, however, is not true. The remote node is never aware of the nodes that consume it. The only link it ever has to its dependent nodes is through the contract data it receives. If a Package contract is submitted based on an associated Proposal and the Proposal is later removed, how does the corresponding Package get cleaned up as well?

By leveraging Command hooks and Gateway services, associated contract data can be cleaned up. The following clip demonstrates submitting a Package, then removing the associated Proposal:

If you look at the ProposalCommand service, you'll see that the WorkflowsGateway service is injected in the constructor. The AfterRemove hook tries to retrieve an active Package associated with the Proposal being removed. If it is found, the WorkflowsGateway.WithdrawPackage method is called. If the package is unable to be withdrawn for any reason, the whole transaction will be aborted and the Proposal will not be removed.