NAV Navbar
info example

Introduction

Scalecube is a toolkit for creating microservices based systems.

as part of the solution, Scalecube provides the follow modules:

all modules are pluggable in a microservice container and can be customized and replaced.

Installation

scalecube available in three templates:

package default configuration
@scalecube/browser provide default configuration for running in browser
@scalecube/node provide default configuration for running in server
@scalecube/scalecube-microservice does not provide any default configuration
package yarn npm
@scalecube/browser yarn add @scalecube/browser npm i @scalecube/browser
@scalecube/node yarn add @scalecube/node npm i @scalecube/node
@scalecube/scalecube-microservice yarn add @scalecube/scalecube-microservice npm i @scalecube/scalecube-microservice

Motivation

Scalecube provide solution for microservice's architecture.
It is based on the principle of Decouple by interface.
it is event-base system that allow to create loosely coupled services in a distributed environment.

Environment

scalecube can be used on browser or node

Reactive programing.

support Observable pattern.

Isolation

RunTime

Different services communicate via events, this solution isolates each service.
if one of the services throw exception, it won't break the whole js application.

Development

Each feature/service can be developed in isolation from other features/services.
services will be able to integrate together base on the interface of each service.

private methods/property

side effect of using Scalecube allow you to design a system in which the developer of the service can determine which methods/property are public.
only methods/property that are in the service definition can be access from out-side the service.

Scalability

RunTime

Easy to bootstrap in every environment/process.

Browser A feature can be located on the main thread or in a web-worker, Scalecube will manage the communication between the services.
Allow you to split your services between multiple process and scale your runtime processing.

NodeJS A feature can be located on different servers.
Scalecube will manage the communication between the services.

Development

Scalecube provide easy way to integrate services base on their definition.

Old browser support

currently @scalecube/browser transpile the code to es5.
but it is still require to add proxy-polyfill to browsers that does not have proxy support.

browser version
Chrome 37+
IE 11
Edge 15
FF 41
Safari 7.1

Core-concepts

Member

member is an entity in the distributed environment.
in the eco-system of scalecube, member will be a microservice container.

Distributed environment

Distributed environment is collection of members that share services between them.
Each member have access to all the services that are shared in the distributed environment.

possible topologies:

Registry

Registry store all endpoints of the available services it can request.
the registry reflect the state in the distributed environment.
if the state has changed, then the registry is notified and update accordingly.

ServiceCall

ServiceCall is the process of requesting a service to execute.

Scenario: microservice use its own service (LocalCall)
Given     a service (definition + reference)
And       bootstraping a microservice container with the service
When      using the service definition to create a proxy from the microservice container
And       calling a method from the service
Then      the microservice container will perform localCall inorder to execute the method


Scenario: microservice use other microservice service (RemoteCall)
Given     a service (definition + reference)
And       bootstraping a microservice container A with the service
And       bootstraping a microservice container B without the service
When      using the service definition to create a proxy from the microservice container B
And       calling a method from the service
Then      the microservice container B will perform remoteCall to microservice container A inorder to request excution of the method
And       the microservice container A will perform localCall inorder to execute the method
// main.js
import { createMicroservice } from '@scalecube/scalecube-microservice';

const msA = createMicroservice({ address: 'msA', services:[{ /* add your services */ }]});
const msB = createMicroservice({ address: 'msB', seedAddress : 'msA', services:[{ /* add your services */ }]});

const proxyA = msA.createProxy({ /* create proxy to the services on the microservice instance */ });
const proxyB = msB.createProxy({ /* create proxy to the services on the other microservice instance */ });

LocalCall

When a microservice use its own services.

RemoteCall

When a microservice use another microservice's services.

Seed

the seed is a microservice container that used as an entry-point to the distributed environment. each microservice can act as a seed for other microservice containers.

seedAddress is the address of the seed.

API

Address

interface Address {
  host: string;
  port: number;
  protocol: string;
  path: string;
}

Address is the URI for the service.

AsyncModel


type RequestStreamAsyncModel = 'requestStream';

type RequestResponseAsyncModel = 'requestResponse';

type AsyncModel = RequestStreamAsyncModel | RequestResponseAsyncModel;

AsyncModel is the way a service can be resolved.
It can be a stream and use requestStream or can be a promise and use requestResponse

Cluster

type JoinCluster = (options: ClusterOptions) => Cluster;

interface ClusterOptions {
  address: Address;
  seedAddress?: Address;
  itemsToPublish: any;
  retry?: {
    timeout: number;
  };
  debug?: boolean;
}

interface Cluster {
  getCurrentMembersData: () => Promise<MembersData>;
  listen$: () => Observable<ClusterEvent>;
  destroy: () => Promise<string>;
}
// browser: 
import { joinCluster } from '@scalecube/cluster-browser';
// server:
import { joinCluster } from '@scalecube/cluster-nodejs';

const cluster = joinCluster({
  address: { /* my address */ },
  seedAddress: {/* address to the distributed environment */},
  itemsToPublish: [/* items to publish in the distributed environment */]
});

cluster.listen$().subscribe((response)=>{console.log(response)});

create a member from the data it receive from the discovery,
and use it in-order to share data in the distributed environment.

scalecube provide two cluster implementations: * @scalecube/cluster-browser * @scalecube/cluster-nodejs

Discovery

type CreateDiscovery = (options: DiscoveryOptions) => Discovery;

interface DiscoveryOptions {
  address: Address;
  itemsToPublish: Item[];
  seedAddress?: Address;
  cluster?: (opt: ClusterOptions) => Cluster;
  debug?: boolean;
}

interface Discovery {
  discoveredItems$: () => Observable<ServiceDiscoveryEvent>;
  destroy(): Promise<string>;
}

type Item = any;

Discovery is a tool that connect the microservice instance to the distributed environment.

It convert events received from the distributed environment to events that the registry can understand.

Endpoint

interface Endpoint {
  qualifier: string;
  serviceName: string;
  methodName: string;
  asyncModel: AsyncModel;
  address: Address;
}

Endpoint is the metadata of a service.
Contain information of how to access the service.

LookUp

type LookUp = (options: LookupOptions) => Endpoint[] | [];

interface LookupOptions {
  qualifier: string;
}

Search for endPoints in the registry that match the qualifier.

Message

interface Message {
  qualifier: string;
  data: any[];
}
const message = {
  qualifier : 'Service/someMethod',
  data: ['value']
}

structure of the data in scalecube.

Microservice

type CreateMicroservice = (options: MicroserviceOptions) => Microservice;

export interface Microservice {
  destroy: () => Promise<any>;
  createProxy: CreateProxy;
  createServiceCall: CreateServiceCall;
}

export interface MicroserviceOptions {
  defaultRouter?: Router;
  services?: Service[];
  seedAddress?: Address | string;
  address?: Address | string;
  transport?: Transport;
  cluster?: (opt: ClusterOptions) => Cluster;
  debug?: boolean;
}
import { createMicroservice } from '@scalecube/scalecube-microservice';
import { TransportNodeJS } from '@scalecube/transport-nodejs';
import { joinCluster } from '@scalecube/cluster-nodejs';

const microserviceInstance = createMicroservice({
  services: [/* array of services */],
  seedAddress : 'pm://myOrganization:8080/ServiceA',
  address : {
    protocol : 'pm',
    host: 'myOrganization',
    port : 8080,
    path: 'ServiceB'
  },
  transport: TransportNodeJS, // scalecube provide a default transport configuration when running on browser,
  cluster: joinCluster, // scalecube provide a default cluster configuration when running on browser,
  defaultRouter: retryRouter({period:10}),
  debug: true // default is false
})

Router

import { roundRobin, retryRouter } from '@scalecube/routers';

const proxyA = ms.createProxy({serviceDefinition, router: roundRobin});

const proxyB = ms.createProxy({serviceDefinition, router: retryRouter({period: 10})});

type Router = (options: RouterOptions) => Promise<Endpoint>;

interface RouterOptions {
  lookUp: LookUp;
  message: Message;
}


type RetryRouter = (options: RetryRouterOptions) => Router

interface RetryRouterOptions { 
  period: number; 
  maxRetry?: number 
}

router is a tool for picking the best service base on given criteria.

default

pick the first available item.

RoundRobin

pick the next item from a list of available items.

retryRouter

ping the registry every @ms and checking if there are any available items. pick the first available item.

Service

interface Service {
  definition: ServiceDefinition;
  reference: ServiceReference;
}

interface ServiceDefinition {
  serviceName: string;
  methods: {
    [methodName: string]: {
      asyncModel: AsyncModel;
    };
  };
}

type ServiceReference = ServiceFactory | ServiceObject;

type ServiceFactory = ({ createProxy, createServiceCall }: ServiceFactoryOptions) => ServiceObject;

interface ServiceObject {
  constructor?: any;

  [methodName: string]: any;
}

interface ServiceFactoryOptions {
  createProxy: CreateProxy;
  createServiceCall: CreateServiceCall;
}

Service is combination of definition and the reference that uphold the contract.

ServiceDefinition

Its the metadata that describe the service.

ServiceReference

Its the code of the service.

Transport

interface Transport {
  clientTransport: ClientTransport;
  serverTransport: ServerTransport;
}

interface ClientTransport {
   start: (options: ClientTransportOptions) => Promise<RequestHandler>;
   destroy: TDestroy;
}

type ServerTransport = (options: ServerTransportOptions) => ServerStop;

interface ClientTransportOptions {
  remoteAddress: Address;
  logger: TLogger;
}

interface ServerTransportOptions {
  localAddress: Address;
  serviceCall: RequestHandler;
  logger: TLogger;
}

interface RequestHandler {
  requestResponse: (message: Message) => Promise<any>;
  requestStream: (message: Message) => Observable<any>;
}

type ServerStop = () => void;
type TLogger = (msg: any, type: 'warn' | 'log') => void;
type TDestroy = ({ logger }: TDestroyOptions) => void;

interface TDestroyOptions {
  address: string;
  logger: TLogger;
}

Opinionated communication layer.
It is used when requesting a service from another microservice instance.

When bootstrapping microservice, it is possible to pass transport in the options.

transport is your custom implementation to RSocket Transport Providers.

Bootstrap

bootstrap the microservice can be done in 3 steps:

1. define the service

interface ServiceDefinition {
  serviceName: string;
  methods: {
    [methodName: string]: {
      asyncModel: AsyncModel;
    };
  };
}
import { ASYNC_MODEL_TYPES } from '@scalecube/scalecube-microservice';

const serviceDefinition = {
  serviceName: 'Service',
  methods:{
  someMethod : {
    asyncModel : ASYNC_MODEL_TYPES.RequestResponse
    }
  } 
}

service definition is the contract between the provider to the consumer of the service.
in other words, it is the contract that the service must uphold.

service definition is used when we are bootstrapping a service and when we are creating a proxy to a service.

when we are boostraping the service we are binding the serviceReference with the serviceDefinition. when we are creating a proxy with a serviceDefinition then scalecube search for the serviceReference that is bound to the serviceDefinition.

2. create the serviceReference

// class example
class Service {
  constructor(){

  }

  someMethod(){
    return Promise.resolve('some method been resolved');
  }
}
// module example
const someMethod = () => Promise.resolve('some method been resolved');

type ServiceReference = ServiceFactory | ServiceObject;

type ServiceFactory = ({ createProxy, createServiceCall }: ServiceFactoryOptions) => ServiceObject;

interface ServiceObject {
  constructor?: any;

  [methodName: string]: any;
}

interface ServiceFactoryOptions {
  createProxy: CreateProxy;
  createServiceCall: CreateServiceCall;
}

ServiceReference is the implementation of the contract.

It can be a class instance, module or a callback function.

passing callback function in the ServiceReference call depedency hook and it can be used to inject proxy/service call to the service.

it is possible that the service will contain more functionality then what you define in the contract,
but only the functions that are in the definition will be public (accessible) in the distributed environment.

3. creating microservice

import { createMicroservice } from '@scalecube/scalecube-microservice';

const ms = createMicroservice({
             services : [{
               reference : {someMethod},
               definition: serviceDefinition
             },
             {
                reference : new Service(),
                definition: serviceDefinition
              }]
           });
type CreateMicroservice = (options: MicroserviceOptions) => Microservice;

interface MicroserviceOptions {
  services?: Service[];
  seedAddress?: Address | string;
  address?: Address | string;
  transport?: Transport;
  cluster?: (opt: ClusterOptions) => Cluster;
  debug?: boolean;
 }

interface Service {
  definition: ServiceDefinition;
  reference: ServiceReference;
}

After creating a service and a service-definition we can now bootstrap our microservice.

Basic usage

After bootstrapping the microservice, we can use it to request services from the distributed environment

createProxy

type CreateProxy = <T = any>(options: ProxyOptions) => T;

interface ProxyOptions {
  router?: Router;
  serviceDefinition: ServiceDefinition;
}

const serviceProxy = ms.createProxy({serviceDefinition});
serviceProxy.someMethod().then(console.log) // resolve with `someMethod` response

It is possible to createProxy from the microservice instance we have just created.

This is a proxy to a different service that is shared in the distributed environment.

createServiceCall

type CreateServiceCall = (options: CreateServiceCallOptions) => ServiceCall;

interface CreateServiceCallOptions {
  router?: Router;
}

interface ServiceCall {
  requestStream: (message: Message) => Observable<Message>;
  requestResponse: (message: Message) => Promise<Message>;
}
const message = {
  qualifier: 'Service/someMethod',
  data: ['value']
};

ms.createServiceCall().requestResponse(message).then(console.log);
ms.createServiceCall().requestStream(message).subscribe(console.log);

serviceCall is another way to request a service.
it is more low level approach and the user must define the message properly in-order for it to work.

good example for preferring serviceCall over Proxy is in the Gateway,
in the Gateway we want to receive a request from outside of our distributed environment and then pass the request to the correct service inside ot it.

destroy

interface Microservice {
  destroy: () => Promise<any>;
}
ms.destroy().then(console.log);

destroy the microservice instance.

  1. stop listen to the address.
  2. notify in the distributed environment that the services from this microservice are not available anymore.
  3. remove it self from the distributed environment.

Advance usage

remoteCall

createMicroservice({
  services: [{
    definition: serviceDefinition,
    reference : new Service()
  }],
  address: 'A'
});

const localMs = createMicroservice({
  seedAddress: 'A'
});

const proxyA = localMs.createProxy({serviceDefinition});

proxyA.someMethod().then(console.log);

remoteCall is a request for a service that located in different microservice container in our distributed environment,

the other microservice can be located on a different process and it will still be accessible with scalecube.

dependency hook

type ServiceFactory = ({ createProxy, createServiceCall }: ServiceFactoryOptions) => ServiceObject;

interface ServiceFactoryOptions {
  createProxy: CreateProxy;
  createServiceCall: CreateServiceCall;
}

interface ServiceObject {
  constructor?: any;

  [methodName: string]: any;
}
import { createMicroservice, ASYNC_MODEL_TYPES } from '@scalecube/scalecube@scalecube/scalecube-microservice';
  const definitionA = {
    serviceName: 'serviceA',
    methods: {
      someMethodA: {
        asyncModel: ASYNC_MODEL_TYPES.REQUEST_RESPONSE,
      },
    },
  };

  const definitionB = {
    serviceName: 'serviceB',
    methods: {
      someMethodB: {
        asyncModel: ASYNC_MODEL_TYPES.REQUEST_RESPONSE,
      },
    },
  };

  class ServiceB {
    constructor(proxyA) {
      // work with proxy to serviceA
      proxyA.someMethodA().then(console.log);
    }
  }

createMicroservice({
  services: [
    {
      definition: definitionA,
      reference : { someMethodA}
    },
    {
      definition: definitionB,
      reference: ({ createProxy, createServiceCall }) => {
        const proxyA = createProxy({serviceDefinition: definitionA });

        return new ServiceB(proxyA);
      }
    }    
  ]
})

dependency hook is used if your service deepened on another service,
or if you want to add life cycle to your bootstrap process.

injectProxy example:

In the example we have two services with dependency between them,
ServiceB deepened on ServiceA.

instead of passing the class instance of serviceB in the reference, we are passing a callback.

the callback is called from scalecube as part of the bootstrapping process.
the callback will be called with createProxy or createServiceCall methods which allow us to create proxy to serviceA and inject it to ServiceB constructor.

this technique can be apply to modules by passing the proxy to a factory.

gateway

type RequestHandler = (serviceCall: ServiceCall, data: any, subscriber: any) => void;

interface GatewayOptions {
  port: number;
  requestResponse?: RequestHandler;
  requestStream?: RequestHandler;
}

interface GatewayStartOptions {
  serviceCall: ServiceCall;
}

interface Gateway {
  start: (options: GatewayStartOptions) => void;
  stop: () => void;
}
import {Gateway} from '@scalecube/rsocket-ws-gateway';
import {createMicroservice} from '@scalecube/scalecube-microservice';

const gateway = new Gateway({port : 3000});
const serviceCall = createMicroservice({
  services: [{
    reference: new Service(),
    definition: serviceDefinition
  }]
}).createServiceCall({});

gateway.start({serviceCall});

Its a technique to centralize all the requests that are coming from outside of the distributed environment.

scalecube provide gateway implementation base on RSocketWebsocket.

Web workers (browser)

Scalecube provide an easy way to work in the browser different process (aka webworkers)
it provide a workers tool to attach webworkers to scalecube ecosystem.
can be import from sclecube/browser or scalecube/utils.

Debug

Mocks

import utils = require('@scalecube/utils');

// polyfill for messageChannel
utils.mockMessageChannel();

When trying to test app as if running on browser and testing microservices in node environment (jest) and the test involve multiple microservice instances and performing remoteCall then it is require to add the code snippet before the test is running.

the snippet will mock scalecube check if running in node and will add polyfill messageChannel which required by browser implementation of scalecube.

Errors

Error Code Message Possible solution
MS0000 microservice does not exists microservice instance have been destroyed
MS0001 Message has not been provided calling requestResponse or requestStream must contain a Message
MS0002 Message data has not been provided Message must contain data property
MS0003 Message qualifier has not been provided Message must contain qualifier property
MS0004 Message should not to be empty object can not pass empty object as Message
MS0005 qualifier expected to be service/method format qualifier is not a string divided by '/'
MS0006 Service missing definition when bootstrap microservice you provide service without a contract
MS0007 Message format error: data must be Array Message data property must be array, how can i pass array in message
MS0008 Not valid format, services must be Array when bootstrap microservice, services must be array of service
MS0009 Not valid format, service must be Object service must be an object
MS0010 Not valid format, Microservice configuration must be an Object missing configuration when bootstrapping a microservice instance
MS0011 qualifier should not be empty string qualifier must be valid string in the format 'part1/part2'
MS0013 Transport provider is not define when running on nodejs, Transport must be provided
MS0014 service method missing in the serviceDefinition try to call a method from a proxy that does not in the definition
MS0015 can't find services that match the give criteria: <qualifier> requesting a service that is not in the registry
MS0016 <asyncModel does not match, expect asyncModel in the proxy definition>, but received asyncModel in the request does not match the asyncModel define in the registry for the service
MS0017 service (<qualifier>) has valid definition but reference is not a function. ServiceObject method is not a function
MS0018 service does not uphold the contract, is not provided definition has a method that is not provided in the reference
MS0019 Not valid format, reference must be an Object ServiceReference must return object
MS0020 Invalid format, definition must contain valid serviceName serviceDefinition must contain property serviceName
MS0021 Invalid format, definition must contain valid methods of type object methods must be of type object
MS0022 Invalid format, definition must contain valid methods methods must be none empty object
MS0023 Invalid format, serviceName must be not empty string but received type fix the serviceDefinition, serviceName must be a string
MS0024 ROUTER_NOT_PROVIDED pass router plugin in the createProxy
MS0025 (service provider)'s service 'serviceName/methodName' define as Promise but service return not Promise incorrect implementation of the service
MS0026 (service provider)'s service 'serviceName/methodName' define as Observable but service return not Observable incorrect implementation of the service
MS0027 (service provider)'s 'serviceName/methodName' has no valid response, expect Promise or Observable incorrect implementation of the service
MS0028 invalid async model asyncModel in definition is invalid

Release Notes

0.1.5

0.1.4

0.1.4-next.18

0.1.4-next.17

0.1.4-next.15

0.1.4-next.14

0.1.4-next.13

0.1.4-next.12

0.1.4-next.11

0.1.4-next.10

0.1.4-next.9

0.1.4-next.8

0.1.4-next.7

FAQ

How can I pass array in message?


const arrayToPassInArgs = [1,2,3,4];
const valueToPassInArgs = 5;
const objectToPassInArgs = {'6':6};

const message = {
  qualifier : 'Service/someMethod',
  data: [arrayToPassInArgs, valueToPassInArgs, objectToPassInArgs]
}

It is possbile to pass any data structure in the message.
just need to make sure it wrap in array.

please look at the example

message

Invoking a method from proxy throw error code MS0015 - can't find services ...

createProxy does not guarantee that the service is available.

it just provide a proxy to the service.

possible solutions:

using retryRouter when creating the proxy. retryRouter will ping the registry before the remoteCall till the service is registered.

remoteCall, retryRouter

Does the order of services is important when using the dependency hook?

The order of the services you provide is the order in which they are registered in the registry.

but, you don't need the service in the registry in order to create a proxy to it.

farther more, it is possible to use the dependency hook for services that are located in other microservice instances.

possible solution:

it is common to apply retry logic in order to make sure the service will run.

remoteCall

Does seedAddress and address are mandatory for scalecube?

No, they are both optional.

Why to provide seedAddress?

seedAddress is used to connected to a distributed environment.

you must provide the address to one of the microservice instance in the distributed environment you want to joined.

why to provide address?

It is important to set your microservice instance address if you want that other microservice instance will use it as seedAddress.

farther more, microservice without address won't be able to share it services in the distributed environment.

if no address is provided, then microservice bootstrap will generate address for you. auto generate address stop you from sharing your services in the distributed environment.

remoteCall, distributed environment, seed, address

Does scalecube support in old browsers?

@scalecube/browser transpile to es5,

but you must add:

 <script nomodule src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.6.0/polyfill.min.js"></script>
 <script nomodule src="https://cdn.jsdelivr.net/npm/proxy-polyfill@0.3.0/proxy.min.js"></script>

old browser support