<div style="text-align:center">
    <img src="https://gitee.com/nnxr/lakutata-module-service/raw/master/assets/lakutata-banner.png" />
</div>

[![NPM Version](https://img.shields.io/npm/v/@lakutata-module/service?color=informational&style=flat-square)](https://npmjs.org/package/@lakutata-module/service)
[![NODE Version](https://img.shields.io/node/v/@lakutata-module/service?color=informational&style=flat-square)](https://npmjs.org/package/@lakutata-module/service)
[![Known Vulnerabilities](https://snyk.io/test/npm/@lakutata-module/service/badge.svg?style=flat-square)](https://snyk.io/test/npm/@lakutata-module/service)
[![NPM Download](https://img.shields.io/npm/dm/@lakutata-module/service?style=flat-square)](https://npmjs.org/package/@lakutata-module/service)

## Features

- Micro-service
- Distributed Events
- Cross nodes service invoking (directly|chain|parallel)
- Scalable

## Quickstart

### Service Examples

#### Service Registry Example

```typescript
const serviceRegistryApp = await createApp({
    id: 'registry.service.test.app',
    name: 'test-service-registry',
    modules: {
        registry: {
            class: ServiceRegistry,
            logger: true,
            redisOptions: {
                host: '192.168.0.132'
            },
            token: '123456'
        }
    }
})
serviceRegistryApp.Logger.info(serviceRegistryApp.getID(), 'is running')
```

#### Service Provider Example

```typescript
 const serviceProviderApp = await createApp({
    id: 'provider.service.test.app',
    name: 'test-service-provider',
    modules: {
        provider: {
            class: Service,
            registry: '127.0.0.1',
            token: '123456',
            controllers: [
                `${path.resolve(__dirname, '../controllers')}/**/*`
            ],
            logger: true
        }
    }
})
serviceProviderApp.Logger.info(serviceProviderApp.getID(), 'is running')
```

#### Service Consumer Example

```typescript
const serviceConsumerApp = await createApp({
    id: 'consumer.service.test.app',
    name: 'test-service-consumer',
    modules: {
        consumer: {
            class: Service,
            registry: '127.0.0.1',
            token: '123456',
            services: [
                `${path.resolve(__dirname, '../services')}/**/*`
            ],
            logger: true
        }
    }
})
serviceConsumerApp.Logger.info(serviceConsumerApp.getID(), 'is running')
setTimeout(async () => {
    serviceConsumerApp.Logger.info('Service consumer test begin:')
    const consumer = serviceConsumerApp.Modules.get<Service>('consumer')
    consumer.channel('test').on('test', (x, y, z) => {
        serviceConsumerApp.Logger.debug('Received channel event [test] data:', x, y, z)
    })
    consumer.channel('test').triggerOne('test', 'a', 'b', 'c')
    try {
        serviceConsumerApp.Logger.info('Start test manually service invoking:')
        serviceConsumerApp.Logger.debug('Service chain invoke result:', await consumer.invoke({
                serviceId: 'provider.service.test.app',
                data: {test: true},
                transform: (response) => {
                    return {name: response}
                }
            },
            {
                serviceId: 'provider.service.test.app',
                data: {
                    test: true,
                    ccc: true
                },
                transform: (response) => {
                    return `this is response:${response}`
                }
            }))
        serviceConsumerApp.Logger.debug('Service parallel invoke result:', await consumer.parallel({
                serviceId: 'provider.service.test.app',
                data: {test: true},
                transform: (response) => {
                    return `this is response:${response}`
                }
            }, [
                {
                    serviceId: 'provider.service.test.app',
                    data: {test: true},
                    transform: (response) => {
                        return {test: true}
                    }
                },
                {
                    serviceId: 'provider.service.test.app',
                    data: {
                        test: true,
                        name: 'manually service invoking',
                        ccc: true
                    },
                    transform: (response) => {
                        return `this is response:${response}`
                    }
                }
            ]
        ))
    } catch (e) {
        serviceConsumerApp.Logger.error(e)
    }
}, 3000)
```

### Service Controller Examples (For Service Provider)

#### TestController1

```typescript
@Controller()
export class TestController1 extends BaseController {
    @Accept(Validator.Object({
        name: Validator.String.required()
    }))
    @PATTERN({test: true, ccc: true})
    public async serviceTest(a: any) {
        return a.name
    }

    @PATTERN({test: true, abd: true})
    public async serviceTest2() {
        return [true, true]
    }
}
```

#### TestController2

```typescript
@Controller()
export class TestController2 extends BaseController {
    @PATTERN({test: true})
    public async tc2Test() {
        return 'this is tc2 test'
    }
}
```

> @lakutata/core required.

## Documentations

### Configure Service Module (Registry|Provider|Consumer)

#### Service Registry Options

| Name         | DataType       | Default | Required | Description                                                                                                 |
|--------------|----------------|---------|----------|-------------------------------------------------------------------------------------------------------------|
| port         | Number         | 6911    | No       | Service registry listen port                                                                                |
| token        | String         |         | No       | Service registry access toke for clients                                                                    |
| logger       | Boolean/Logger | false   | No       | Log service registry activities                                                                             |
| redisOptions | RedisOptions   |         | No       | If not set, the service registry is running in single node mode, else it will build a cluster automatically |

#### Service Options

| Name        | DataType        | Default   | Required | Description                                                                                                                              |
|-------------|-----------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------|
| registry    | String          |           | Yes      | Service registry address url,if port not specified, it will be 6911                                                                      |
| controllers | String\String[] |           | No       | The service controllers load path/paths                                                                                                  |
| timeout     | Number          | 60000(ms) | No       | Service consumer invoke remote service timeout                                                                                           |
| retry       | Number          | 0         | No       | If service consumer invoke remote service return a retryable error, it will retry to invoke again until the retry chances decreases to 0 |
| token       | String          |           | No       | Service registry access token                                                                                                            |
| logger      | Boolean\Logger  | false     | No       | Log service consumer activities                                                                                                          |

### Service Controller

A valid service controller MUST extend BaseController, and it MUST decorate by @Controller(), the @Controller()
decorator has an optional parameter:

    @Controller(basicPattern: TPattern = {})

if basicPattern set, all patterns declared in the controller will contain it.

In the service controller, @PATTERN() decorator is for decorate method, the parameter is the invoking pattern.

    @PATTERN(pattern: TPattern)

### Classes

- ServiceRegistry (Module)
    - fetchServices(appId?: string | RegExp): { [appId: string]: IServiceInfo[] }

- Service (Module)
    - async invoke(...params: TInvokeObject[]): Promise<any>
    - async parallel(...params: (TInvokeObject | TInvokeObject[])[]): Promise<any[]>
    - channel(name: string): Channel
        - trigger(event: string, ...args: any[]): boolean
        - triggerAll(event: string, ...args: any[]): boolean
        - triggerOne(event: string, ...args: any[]): boolean
        - directTrigger(serviceId: string, event: string, ...args: any[]): boolean
        - directTriggerAll(serviceId: string, event: string, ...args: any[]): boolean
        - directTriggerOne(serviceId: string, event: string, ...args: any[]): boolean
        - privateTrigger(event: string, ...args: any[]): boolean
        - privateTriggerAll(event: string, ...args: any[]): boolean
        - privateTriggerOne(event: string, ...args: any[]): boolean
        - on(event: string, listener: (...args: any[]) => void): Channel
        - once(event: string, listener: (...args: any[]) => void): Channel
        - off(event?: string, listener?: (...args: any[]) => void): Channel
    - channels(): string[]
    - async fetchServices(appId?: string | RegExp): Promise<{ [appId: string]: IServiceInfo[] }>
- BaseController

### Decorators

- @Controller
- @PATTERN

### Exceptions

- BrokenServiceInvokeException
- IncorrectTokenException
- InvalidRegistryProtocolException
- PatternNotFoundException
- RequestException
- RequestTimeoutException
- ServiceClientInitializationException
- ServiceInvokeTimeoutException
- ServiceProviderNotFoundException
- ServiceRegistryInitializationException

## Examples

Examples can be found in project [src/tests](https://gitee.com/nnxr/lakutata-module-service/tree/master/src/tests).

## How to Contribute

Please let us know how can we help. Do check out [issues](https://gitee.com/nnxr/lakutata-core/issues) for bug reports
or suggestions first.

## License

[MIT](LICENSE)
