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.
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.
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.jsonnow contains multiple bundles as entry points in the
inputsfield. The field
targetfor 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.
protois 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.jsonnow contains an invocation of the
protogentool that generates TypeScript code from the proto definitions.
srcthe source folder is now layed out to better separate UI and service code.
gencontains generated code from the
protodefinitions. This code is shared between the UI and the background service.
nodecontains code that is only needed in the background service.
webcontains code that is only needed in the UI.
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.
src/node/NodeBundle.ts contains a default export of type
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,
src/web/WebBundle.ts contains a default export of type
To make sure that anything shows up on the map, the
WebRunnableBundle exposes a custom layer. This is done by returning the
customLayer as part of the bundle's
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.
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.
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
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:
PositioningServicevia its abstract name
PureSearchServicevia its abstract name
Observableis 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
mergeMapoperator is helpful here.
A more realistic example would also optimize the service by using a hot observable that is shared across invocations.
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
This is achieved by transforming the stream of results from
CoffeeLayerServiceis acquired via its abstract name
CoffeeResults is mapped to a stream of
CustomLayerobject with a consistent layer id
To build the service package, we can once again use:
This processes the proto definitions, compiles the TypeScript code and outputs a single ZIP archive that is ready to run in the HNOD runner.
Once the ZIP is built, we can use the runner to execute it: