Background Service Example

This example package includes a background service that runs in Node.js and communicates with the UI via gRPC.

Since this example shows off some more advanced technique, it is advisable to to work through the other examples first and familiarize yourself with the underlying concepts.

Overview

The goal of this example is to show coffee-shops in the vicinity of the current location on the map at all times. This is done by adding a background service that listens to updates to the current location, searches for "coffee" in the area around it and exposes an ever-updating stream of results to its clients. A custom layer is then added on the UI-side to consume these results and display them on the map.

Package Structure

Since this example contains background services for Node.js, there are new moving parts, which are also apparent in some changes in the folder layout:

  • hnod.bundler.json now contains multiple bundles as entry points in the inputs field. The field target for the UI entry point is set to web, while the entry point for the service code is set to node. This determines the environment that the corresponding code is packaged for and executed in.
  • proto is a directory that contains protocol buffer definitions for the gRPC services. These are used to generate TypeScript code during the build, which is required for communication with the service.
  • package.json now contains an invocation of the protogen tool that generates TypeScript code from the proto definitions.
  • src the source folder is now layed out to better separate UI and service code.
    • some shared code is kept at the top level
    • gen contains generated code from the proto definitions. This code is shared between the UI and the background service.
    • node contains code that is only needed in the background service.
    • web contains code that is only needed in the UI.

The Bundles

Since code for background services and UI needs to be separated, we also have two separate bundles that serve as entry points for each environment.

The RunnableBundle for the Node.js environment

The file src/node/NodeBundle.ts contains a default export of type RunnableBundle. RunnableBundles can only expose services (i.e. no UI-related components) because they are executed in the Node.js environment. In this case, we only have one service and the RunnableBundle just exposes the make function that instantiates the implementation for our service, CoffeeLayerServiceImpl.

The WebRunnableBundle for the UI

The file src/web/WebBundle.ts contains a default export of type WebRunnableBundle. WebRunnableBundles can expose UI components, which will be executed in the UI's JavaScript environment.

To make sure that anything shows up on the map, the WebRunnableBundle exposes a custom layer. This is done by returning the CoffeeModel's customLayer as part of the bundle's discoverables.

The Service Definition

Services in HNOD are built on gRPC, and as such require a typed definition of the service interface in the protocol buffer language.

This example contains only one service with a relatively simple interface.

The service is defined in the file proto/CoffeeLayerService.proto. We define a single service interface called CoffeeLayerService with a single method CoffeeLocationsAroundMe. HNOD embraces reactive programming, so the method is marked to return a stream that will receive updates as the current location changes.

Each such result is wrapped in a custom message type CoffeeResult that contains a snapshot of the search result at that point in time. For now, CoffeeResult just contains a single repeated field coffeeLocations that will contains the locations of all coffee-related search results. Each such location is represented by the message type GeoLocation, which is already defined in HNOD. To be able to re-use this message type, we include the relevant proto file at the top.

Hint: Predefined message types

Currently, the proto definitions for the predefined HNOD message types are not included in the SDK and need to be requested separately.

When the service package is built, the protogen tool will translate these service definitions to TypeScript code with corresponding TypeScript-interfaces that the compiler can use for type-checking. Specifically, CoffeeLayerService will become a TypeScript interface that we have to implement.

The Service Implementation

The file src/node/CoffeeLayerServiceImpl.ts contains the class that implements the service. To have everything type-safe, this file imports the typings for our service from the generated code in src/gen/rx-proto.

The proto definition of the service resulted in an interface CoffeeLayerService that the class CoffeeLayerServiceImpl has to implement. The service's single method on the gRPC level also corresponds to a single method in the implementation. Likewise, the argument types and return types in the interface have been replaced with the corresponding types on the TypeScript side. Since gRPC embraces streams of messages it's important to have a convenient representation for streams on the TypeScript side as well. In HNOD services such streams are modeled as RxJS Observables. This is apparent in the required result type of the service.

The class also extends ServiceBase, from which it inherits (among other things) the factory property. This is HNOD's service factory which implements the service locator pattern and allows CoffeeLayerServiceImpl to acquire client stubs for other services.

The implementation of the service's logic in coffeeLocationsAroundMe is now relatively straight-forward:

  • it acquires a client stub for PositioningService via its abstract name
  • it acquires a client stub for PureSearchService via its abstract name
  • the resulting Observable is produced by mapping the stream of matched locations from the positioning service to a stream of corresponding sets of search results from the search service. Since this is an asynchronous transformation, the mergeMap operator is helpful here.
  • to prevent overwhelming the search service, the stream of positions is tidied up before mapping to search results:
    • it is throttled to limit the number of requests in a given time frame
    • redundant request when the position hasn't changed are suppressed with distinctUntilChanged
  • the resulting stream of result sets from the search service is then just tidied up again:
    • redundant result sets are suppressed with distinctUntilChanged
    • result sets are mapped to the required message type

Caution: Mind your Observables

A more realistic example would also optimize the service by using a hot observable that is shared across invocations.

The Model

The file src/web/CoffeeModel.ts contains the UI model for the service package. Since no interaction with the results is required, the only responsibility of the model is to expose a representation of the search results as a CustomLayer.

This is achieved by transforming the stream of results from CoffeeLayerService:

  • a client stub for CoffeeLayerService is acquired via its abstract name
  • the stream of CoffeeResults is mapped to a stream of CustomLayer:
    • map the coffeeLocations to individual Markers
    • wrap the resulting set of markers into a CustomLayer object with a consistent layer id

Build the Example

To build the service package, we can once again use:

yarn build

This processes the proto definitions, compiles the TypeScript code and outputs a single ZIP archive that is ready to run in the HNOD runner.

Run the Example

Once the ZIP is built, we can use the runner to execute it:

yarn run
Markers on the map
Figure 1. Markers for search results on the map

results matching ""

    No results matching ""