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. Sendsend
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 driverCellularJS 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 ErrorTo 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 IRSUnexpected 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 lifecyleWhenever 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}"`);});