# Service Broker
# Introduction
ExpressWebjs Service broker is an intermediary computer program module that translates a message from the formal messaging protocol of the sender to the formal messaging protocol of the receiver. It handles service calls, actions, emits events and communicates with remote nodes.
# Create a Broker
Broker acts as a layer that exposes the service methods you wish to share with other nodes. You can define actions and subscribe to events. This follows a schema pattern.
import { IBrokerAction } from "Elucidate/Broker";
export class BookBroker {
public name: string;
public version?: number | string;
constructor() {
this.name = "bookService";
this.version = 1;
}
public actions: IBrokerAction = {
getAllBooks: {
handler: async (ctx) => {
return ...;
},
},
};
}
# Base properties
The Service has some base properties in the schema.
this.name = "bookService";
this.version = 1;
The name is a mandatory property so it must be defined. Itβs the first part of action name when you call it.
The version is an optional property. Use it to run multiple version from the same broker. It is a prefix in the action name. It can be a Number or a String.
# Settings
The settings property is a static store, where you can store every settings/options to your broker. You can reach it via this.settings inside the broker.
import { IBrokerAction } from "Elucidate/Broker";
export class BookBroker {
public readonly name = "BookBroker";
public readonly version = 1;
public settings: {
username: "Alex Igbokwe"
},
public actions: IBrokerAction = {
processData: {
handler: async (ctx) => {
if (this.settings.username == "Alex Igbokwe") {
...
}
},
},
};
}
settings is also obtainable on remote nodes. It is transferred during service discovering.
# Internal Settings
There are some internal settings which are used by core modules. These setting names start with $ (dollar sign).
| Name | Type | Default | Description |
|---|---|---|---|
| $noVersionPrefix | Boolean | false | Disable version prefixing in action names. |
| $noServiceNamePrefix | Boolean | false | Disable service name prefixing in action names. |
| $dependencyTimeout | Number | 0 | Timeout for dependency waiting. |
| $shutdownTimeout | Number | 0 | Timeout for waiting for active requests at shutdown. |
| $secureSettings | Array | [] | List of secure settings. |
# Secure service settings
To protect your tokens & API keys, define a $secureSettings: [] property in broker settings and set the protected property keys. The protected settings wonβt be published to other nodes and it wonβt appear in Service Registry. These settings will only be available under this.settings inside the broker functions.
settings: {
$secureSettings: ["app.auth.email", "app.auth.pass"],
app: {
auth: {
user: 'alex@email.com',
pass: 'yourpass'
}
}
}
# Actions
# Connecting broker to service
ts-node maker make-service MyNewService -brokerYou can connect your broker to a service. Let's connect our BookBroker to Book Service.
import { BookService } from "./BookService";
import { IBrokerAction } from "Elucidate/Broker";
export class BookBroker {
public readonly name = "bookService";
public readonly version = 1;
constructor(private bookService: BookService) {}
public actions: IBrokerAction = {
getAllBooks: {
handler: async (ctx) => {
return await this.bookService.fetchAllBooks();
},
},
getUserBooksByUserId: {
handler: async (ctx) => {
return await this.bookService.getUserBooks(ctx.params.user_id);
},
},
};
}
# Registering brokers to Service provider
Before registering your broker to the Service Broker collection, you need to first register it in IOC to enable ExpressWebJs to discover it as a service. Once that is done, we can now register in the service broker.
Let's do this in our AppServiceProvider register method in App/Providers/AppServiceProvider
import ServiceProvider from "Elucidate/Support/ServiceProvider";
import { registerBroker } from "Elucidate/Broker"; π Broker Handler
import { BookService } from "App/Service/BookService";
import { BookBroker } from "App/Service/BookService/BookBroker";
export class AppServiceProvicer extends ServiceProvider {
/**
* Register any application services.
* @return void
*/
public register() {
this.singleton(BookService); π our book service
this.singleton(BookBroker); π our book broker
}
/**
* Register any service broker after application boot stage
* @return void
*/
async booted() {
await registerBroker(this.use(BookBroker)); π register book broker to service broker
}
}
registerBroker method should be used in the booted method.
# Calling our broker
We can call our broker in our project controller / service or in another ExpressWebJs controller / service class so far as it extends their base class.
The controller should extend it's BaseController and the service should extend it's BaseService class.
# Calling in controller
"use strict";
import { BookService } from "App/Service/BookService";
import { Request, Response } from "Config/http";
import { BaseController } from "./BaseController";
export class BooksController extends BaseController {
constructor(private bookService: BookService) {
super();
}
/**
* Display a listing of the resource.
* @method GET
* @endpoint
*/
index = async (req: Request, res: Response) => {
let books = await this.callBrokerService("v1.bookService.getAllBooks");
return this.response.OK(res, books);
};
getUserBooks = async (req: Request, res: Response) => {
const user_id = req.params.user_id;
let books = await this.callBrokerService("v1.bookService.getUserBooksByUserId", { user_id });
return this.response.OK(res, books);
};
}
The v1 is the broker version, bookService is the broker name and getAllBooks is the action
# Calling in service
"use strict";
import { IBookService } from "App/Service/BookService/IBookService";
import { BaseService } from "./BaseService";
export class UserService extends BaseService {
constructor(private bookService: IBookService) {
super();
}
public async getAllBooks() {
return await this.callBrokerService("v1.bookService.getAllBooks");
}
public async getUserBooks(user_id: string) {
return await this.callBrokerService("v1.bookService.getUserBooksByUserId", { user_id });
}
}
The v1 is the broker version, bookService is the broker name and getAllBooks is the action
# Usages
# Call without params
return await this.callBrokerService("v1.user.allUsers");
# Call with params
return await this.callBrokerService("v1.user.getUserById", { id: 19 });
# Call with options
return await this.callBrokerService(
"v1.user.recommendation",
{ limit: 5 },
{
timeout: 500,
retries: 3,
fallbackResponse: defaultRecommendation,
},
);
# Call with promise error handling
return await this.callBrokerService("v1.user.update", { id: 2, email: "alex@email.com" })
.then((res) => console.log("User updated!"))
.catch((err) => console.error("Unable to update User!", err));
# Direct call to a node
return await this.callBrokerService("v1.user.allUsers", null, { nodeID: "node-21" });
# Metadata
Send meta information to services with meta property. Access it via ctx.meta in action handlers. Please note that in nested calls the meta is merged.
return await this.callBrokerService("v1.bookService.getAllBooks", null, {
meta: {
a: "Alex",
b: "Igbokwe",
},
});
import { IBrokerAction } from "Elucidate/Broker";
export class BookBroker {
public readonly name = "bookService";
public readonly version = 1;
public actions: IBrokerAction = {
getAllBooks: {
handler: async (ctx) => {
console.log(ctx.meta); π
// Prints: { a: "Alex", b: "Igbokwe" }
},
},
};
}
# Timeout
Timeout can be set in action definition, as well. It overwrites the global message broker requestTimeout option, but not the timeout in calling options.
import { IBrokerAction } from "Elucidate/Broker";
export class BookBroker {
public readonly name = "bookService";
public readonly version = 1;
public actions: IBrokerAction = {
getAllBooks: {
timeout: 5000, // 5 secs π
handler: async (ctx) => {
...
},
},
};
}
# Multiple calls
Calling multiple actions at the same time is also possible. To do it use mCallBrokerService method.
return await this.mCallBrokerService(
[
{
action: "posts.find",
params: { author: 1 },
options: {
/* Calling options for this call. */
},
},
{ action: "users.find", params: { name: "Alex" } },
],
{
// Common calling options for all calls.
meta: { token: "63f20c2d-8902-4d86-ad87-b58c9e2333c2" },
},
);
# settled option in mCallBrokerService
The mCallBrokerService method has a new settled option to receive all Promise results. If settled: true, the mCallBrokerService returns a resolved Promise in any case and the response contains the statuses and responses of all calls. Note that, without this option you wonβt know how many (and which) calls were rejected.
const res = await this.mCallBrokerService(
[
{ action: "posts.find", params: { limit: 2, offset: 0 } },
{ action: "users.find", params: { limit: 2, sort: "username" } },
{ action: "service.notfound", params: { notfound: 1 } },
],
{ settled: true },
);
console.log(res);
The res will be something similar to
[
{
status: "fulfilled",
value: [
/*... response of `posts.find`...*/
],
},
{
status: "fulfilled",
value: [
/*... response of `users.find`...*/
],
},
{
status: "rejected",
reason: {
/*... Rejected response/Error`...*/
},
},
];