# 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 ...; }, }, }; }
Copied!
# Base properties
The Service has some base properties in the schema.
this.name = "bookService"; this.version = 1;
Copied!
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") { ... } }, }, }; }
Copied!
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' } } }
Copied!
# Actions
# Connecting broker to service
ts-node maker make-service MyNewService -broker
You 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); }, }, }; }
Copied!
# 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 { BaseBroker } from "Elucidate/Broker"; π Broker Handler import { BookService } from "App/Service/BookService"; import { BookBroker } from "App/Service/BookService/BookBroker"; export class AppServiceProvider 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 BaseBroker.registerBroker(this.use(BookBroker)); π register book broker to service broker } }
Copied!
registerBroker
method should be used in the booted
method.
# Calling our broker
We can invoke our broker either within our project's controller/service or within another controller/service class in ExpressWebJs.
# Calling in controller
import { BookService } from "App/Service/BookService"; import { Request, Response } from "Config/Http"; import { BaseController } from "./BaseController"; import { BaseBroker } from "Elucidate/Broker"; 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 BaseBroker.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 BaseBroker.callBrokerService("v1.bookService.getUserBooksByUserId", { user_id }); return this.response.OK(res, books); }; }
Copied!
The v1
is the broker version, bookService
is the broker name and getAllBooks
is the action
# Calling in service
import { IBookService } from "App/Service/BookService/IBookService"; import { BaseService } from "./BaseService"; import { BaseBroker } from "Elucidate/Broker"; export class UserService extends BaseService { constructor(private bookService: IBookService) { super(); } public async getAllBooks() { return await BaseBroker.callBrokerService("v1.bookService.getAllBooks"); } public async getUserBooks(user_id: string) { return await BaseBroker.callBrokerService("v1.bookService.getUserBooksByUserId", { user_id }); } }
Copied!
The v1
is the broker version, bookService
is the broker name and getAllBooks
is the action
# Usages
# Call without params
return await BaseBroker.callBrokerService("v1.user.allUsers");
Copied!
# Call with params
return await BaseBroker.callBrokerService("v1.user.getUserById", { id: 19 });
Copied!
# Call with options
return await this.callBrokerService( "v1.user.recommendation", { limit: 5 }, { timeout: 500, retries: 3, fallbackResponse: defaultRecommendation, } );
Copied!
# Call with promise error handling
return await BaseBroker.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));
Copied!
# Direct call to a node
return await BaseBroker.callBrokerService("v1.user.allUsers", null, { nodeID: "node-21" });
Copied!
# 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 BaseBroker.callBrokerService("v1.bookService.getAllBooks", null, { meta: { a: "Alex", b: "Igbokwe", }, });
Copied!
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" } }, }, }; }
Copied!
# 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) => { ... }, }, }; }
Copied!
# Multiple calls
Calling multiple actions at the same time is also possible. To do it use mCallBrokerService
method.
return await BaseBroker.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" }, } );
Copied!
# 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 BaseBroker.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);
Copied!
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`...*/ }, }, ];
Copied!