Skip to main content

Transporter

There is no class named Transporter at all, it is simply a name for a group of heper functions that assist you in making requests.

1. Send#

send is a promise based function, it help you send internal message via virtual network.

import { send, IRQ } from '@cellularjs/net';
const downloadFileIrq = new IRQ(  { to: 'Media:DownloadFile' },  { file: 'cellularjs.svg' },);
const irs = await send(downloadFileIrq);

2. Using specific driver#

CellularJS does not decide what driver to use to handle request, you need to decide by yourself. You can easily specify driver like below example:

import { send, IRQ } from '@cellularjs/net';
const irq = new IRQ({ to: 'User:MyInfo', userClaims: { roles: [], userId: '***' } });
send(irq); // default `driver` is 'local'send(irq, { driver: 'remote' });send(irq, { driver: 'ssh' });// ...
tip

When sending request between cells, it is better to wrap send function inside a class/function. Currently, CellularJS has no built-in support for this stuff, you can create your own "Transporter".

Example:

import { Injectable } from '@cellularjs/di';import { send, getResolvedCell, IRQ, LOCAL_DRIVER } from '@cellularjs/net';
const CORR_ID_HEADER = 'id';
@Injectable()export class Transporter {  constructor(    private inComingIrq?: IRQ,  ) { }
  async send(irq: IRQ) {    const modifiedIRQ = this.modifyIRQ(irq);    const driver = this.specifyDriver(irq);
    try {      return await send(modifiedIRQ, {        driver,      });
    } catch (errIrs) {      if (!errIrs.header) {        throw errIrs;      }
      if (errIrs.header[CORR_ID_HEADER]) {        throw errIrs;      }
      throw errIrs.withHeaderItem(        CORR_ID_HEADER,        modifiedIRQ.header[CORR_ID_HEADER],      );    }  }
  private modifyIRQ(irq: IRQ) {    if (irq.header[CORR_ID_HEADER]) {      return irq.withHeaderItem('referer', this.inComingIrq?.header.to);    }
    // Of cource, `inComingIrq` must have its own ID. This ID can be set     // when transforming external request into internal request at gateway.    return irq      .withHeaderItem('referer', this.inComingIrq?.header.to)      .withHeaderItem(        CORR_ID_HEADER,        this.inComingIrq.header[CORR_ID_HEADER],      );  }
  private specifyDriver(irq: IRQ) {    if (!this.inComingIrq) {      return LOCAL_DRIVER;    }
    const fromCell = this.inComingIrq.header.to.split(':')[0];    const toCell = irq.header.to.split(':')[0];
    const fromSpace = getResolvedCell(fromCell).spaceId;    const toSpace = getResolvedCell(toCell).spaceId;
    if (fromSpace !== toSpace) {      return 'remote'; // 'ssh', '***'    }
    return LOCAL_DRIVER;  }}
// task-x.ts
@Service({ scope: 'publish' })class TaskX {  constructor(    private transporter: Transporter,  ) { }
  async handle() {    // In this sample, for simplicity, we make a request here.    const { send } = this.transporter;    const sendMailIrq = new IRQ({ to: 'Mailer:SendMail' });    await send(sendMailIrq);  }}
tip

Because cell name is not specified by cell itself but network, so for private request(eg: request from cell A to cell A itself), you shouldn't specify cell name in to header, it may be changed due to different network can use different name for the cell, ...

To prevent this issue, you can create self method like below example to convert to header (Eg: :GetInfo => User:GetInfo).

@Injectable()export class Transporter {  constructor(    private inComingIrq?: IRQ,  ) { }
  async self(irq: IRQ) {    if (!this.inComingIrq) {      throw new Error('Transporter: can not get incoming IRQ. Please make sure:' +        '\n- `self` method is invoked from a Cell.');    }
    const targetCell = this.inComingIrq.header.to.split(':')[0];    const targetAction = irq.header.to.split(':')[1];    const newIRQ = irq.withHeaderItem('to', `${targetCell}:${targetAction}`);
    return this.send(newIRQ);    // const irq = new IRQ({ to: ':GetInfo' }, { userId: 'x' });    // await this.transporter.self(irq);  }
  async send(irq: IRQ) {    // ...  }}

3. Error handling#

3.1. Throw Error#

To simulate real isolated environment, it is not allowed to throw exception directly from one cell to another. So if an Error is throwed, CellularJS will create unexpected error response and throw it to caller.

// Unexpected error response will look like this:new IRS({ status: 500 });

If you want to get original error, you can add a listener to fail event with transportListener.

3.2. Throw IRS#

Unexpected error without any information seem obscure to client, you can throw internal response as an error response instead.

Example: RegisterAccount call User:CreateProfile without try/catch so error response will simply go to gateway directly.

import { Inject } from '@cellularjs/di';import { send, Service, ServiceHandler, IRQ, IRS } from '@cellularjs/net';
class BadRequest extends IRS {  constructor(errs) {    super(      { status: 400 },      {        err: 'BAD_REQUEST',        errs,      },    );  }}
// user cell@Service({ scope: 'publish' })export class CreateProfile {  handle() {    throw new BadRequest([      { src: 'name', err: 'REQUIRED' },    ];  }}
// auth cell@Service({ scope: 'publish' })export class RegisterAccount implements ServiceHandler {  async handle() {    const createUserProfileIrq = new IRQ({ to: 'User:CreateProfile' });    await send(createUserProfileIrq);
    // ...  }}

4. Request lifecyle#

Whenever you make an internal request, CellularJS can emit these events: start, success/fail. CellularJS provide transportListener and requestContext to help you add listener and modify data.

caution

transportListener is a GLOBAL object. That mean listener will be able to listen and change data of every requests from different cells. So it is only suitable for "global" tasks such as logging request,... and code should be used at common place(Eg: $share folder).

If you want to modify/add behaviour to specific service, have a look at service proxy.

note

You can add any key to requestContext object, but there are some reserverd key with specific purpose you should know:

  • irq: current internal request.
  • irs: current internal response.
  • originalError: the original error throwed by callee.
  • reqOpts: request options.

Example: measure internal request execution time.

import { transportListener } from '@cellularjs/net';
transportListener.on('start', ctx => {  const { irq } = ctx;  const targetService = irq.header.to;  ctx.startTime = process.hrtime();
  console.info(`Receive request to "${targetService}"`);});
transportListener.on('success', ctx => {  const { startTime, irq } = ctx;  const targetService = irq.header.to;  const hrend = process.hrtime(startTime);
  console.info(`Execution time of "${targetService}": %ds %dms`, hrend[0], hrend[1] / 1000000);});
transportListener.on('fail', ctx => {  const { irq } = ctx;  const targetService = irq.header.to;
  console.info(`Failed to send request to "${targetService}"`);});