# 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!
πŸ’‘ The 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

πŸ’‘ You can create your service broker together with your services with this Maker service command 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!
⚠️ If you are injecting a class into your broker constructor, you will need to register your broker in the register method in AppServiceProvider class in App/Providers/AppServiceProvider

# 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!
⚠️ The 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!