npm package: @startier/ohrid
Description: A library for managing distributed nodes
Author: Mihail Mojsoski
Latest version: 1.1.0

# @startier/ohrid

> A library for managing distributed nodes.

## How does it work?

- You may define **Services**, **Methods** and **Entrypoints**.

- **Services** may contain **Methods** and **Entrypoints**.

- **Services** can be started, creating a **Node**.

- **Methods** can be invoked by **Nodes**.

- **Nodes** automatically execute **Entrypoints** if any are present.

- When a **Node** invokes a **Method**:

  - If the **Node** and the **Method** are in the same **Service**, the **Method** gets run on the same **Node** that invoked it.
  - If the **Node** and the **Method** are in a different **Service**, a **Remote call** is made.

- **Remote calls** are handled by _communication_ **Drivers**.

> [!NOTE]
> Features like `network connections`, `server / client handling`, `load balancing`, etc... are not defined by the library and their handling is entierly up to the driver, if you need a fully-featured driver you can use `@startier/ohrid-jsonrpc-driver`.

## Usage

- Install the package using `npm` (or anything compatible):

```sh
npm install @startier/ohrid
```

- Read the output of the help command:

```sh
npx ohrid help
```

- Install a communication driver (for example: `@startier/ohrid-local-driver`) and initialize the project:

```sh
npm install @startier/ohrid-local-driver
npx ohrid init --driver=@startier/ohrid-local-driver
```

- _Optional_ Edit the base path in `services.json`:

> [!NOTE]  
> This is **required** when you use TypeScript and the path should be set to where `tsc` outputs the `.js` files.

```json
{
  "path": "dist"
}
```

- _Optional_ Create a service (or a couple of them):

> [!NOTE]  
> This is recommended when you are using a non-trivial driver that needs configuration.

```sh
npx ohrid new service my-service --entrypoint=my-service-entrypoint
```

- Create a method (or a couple of them):

```sh
npx ohrid new method my-method --service=my-service
```

- Modify your methods and services (see the API reference section for more info)

- Run a service (you can run a service without creating a config for it, it will inherit the default config)

> [!NOTE]  
> A service without an entrypoint will run forever (or until the driver decides to exit).

```sh
npx ohrid start my-service
```

# File Structure

- `./services.json`: the configuration file
- `./package.json`: npm/node package file
- `./rpc/`: RPC method directory
  - `./*/index.[ts|js]`: RPC method
- `./src/`: entrypoint directory
  - `./**/*/*.[ts|js]`: entrypoint
- `./drivers/`: communication driver directory
  - `./*/index.[ts|js]`: communication driver

# Configuration

> [!WARNING]
> Any flag that isn't explicitly defined by a command overrides this configuration (ex: `--services.test.settings.doFlagsWork=` would override the `doFlagsWork` setting on the service `test`.)

The configuration follows the following schema:

```ts
{
    path?: string;
    typescript?: boolean;
    driver?: ServiceConfig["driver"];
    settings?: ServiceConfig["settings"];
    docker?: DockerConfig;
    exports?: "named" | "default";
    services?: Record<string, ServiceConfig>;
    import?: string[];
    export?: string[];
}

ServiceConfig = {
  entrypoint?: string;
  amount?: number;
  settings?: Record<string, object | string | number | boolean>;
  driver?: string;
}

DockerConfig = {
  from?: string;
  script?: string;
  image?: string;
}


```

- `path`: location of compiled JavaScript files, defaults to `"."`
- `typescript`: should the `new` command generate `.ts` files, defaults to `true` if `"typescript"` is a `dependency` or `devDependency` in `package.json`
- `driver`: the default communication driver that will be used if no driver is specified for a service
- `settings`: the default service settings that get merged with the service settings (defined by external libraries, drivers, etc..)
- `docker`: configuration used by `npx ohrid generate docker`
  - `docker.from`: the source image used for the generated `Dockerfile`
  - `docker.script`: the `npm run` script used in the build step in the `Dockerfile`
  - `docker.image`: the image used by `docker-compose.yml`, will generate a script called `./build-image.sh` that runs `docker` with the correct arguments if set, otherwise the correct arguments will be passed to the `docker-compose.yml` directly
- `exports`: sets the export style for code generated by the `new` command
- `services`: service configuration
  - `services[name].entrypoint`: the file that gets run when the service starts, otherwise the service will run forever and only execute methods when requested
  - `services[name].amount`: how many copies of the service should get added to the `docker-compose.yml` file
  - `services[name].settings`: service settings for external libraries or drivers
  - `services[name].driver`: the communication driver used by the service
- `import`: sub-directories and packages where there are other `services.json` files to import services and methods from
- `export`: the methods that should be exported for importing using `import` in other `services.json` files

# API Reference

`@startier/ohrid`

### `Log`

A function type that is used to print information to the console.

**Signature**:

```ts
function Log(
  type: "info" | "output" | "error" | "debug" | "warning",
  message: string
): void;
```

### `Context`

An object that is used for communication between nodes.

This is used by communication drivers to provide a transport layer to `ohrid`.

**Properties**:

- `log: Log`: the default logger passed down by the creator of the driver
- `exit: boolean`: an observed value that will stop the node when set to `true`
- `remoteCall: function`: a function that gets called when the node invokes a method from an other service
  - **Signature**:

```ts
function remoteCall<TService extends string, TParams extends any[], TReturn>(
  method: Method<TService, TParams, TReturn>,
  ...params: TParams
): Promise<Awaited<TReturn>>;
```

### `Driver`

A communication driver for handling remote calls.

**Properties**

- `createNode: function`: a function that gets called when a context for a node needs to be created
  - **Signature**:

```ts
function createNode(
  name: string,
  config: ServiceConfig,
  rpcMethods: Record<string, Method>,
  log: Log
): Context;
```

- `handleDockerCompose: function`: a function that gets called when a service uses this driver and needs to generate a `docker-compose.yml`
  - **Signature**:

```ts
function handleDockerCompose(item: {
  service: ServiceConfig;
  dockerServiceName: string;
  appendLine: (text: string): void;
  block: (cb: (): Promise<void>): Promise<void>;
  store: Record<string, string | undefined>;
}): Promise<void>;
```

- `getDockerfileExtensions: function`: a function that gets called when a service uses this driver and needs to generate a `Dockerfile`
  - **Signature**:

```ts
function getDockerfileExtensions(
  place:
    | "beforeDeps"
    | "afterDeps"
    | "beforeBuild"
    | "afterBuild"
    | "beforeRunner"
    | "afterRunner",
  serviceConfigs: Record<string, ServiceConfig> | undefined
): string;
```

### `Method<TService extends string, TParams extends any[], TReturn>`

An invocable method returned by `declareMethod()`.

**Properties**

- `createCaller: function`: a function that gets called when a `Client` needs to invoke a method locally.
  - **Signature**:

```ts
function createCaller(
  context: Context
): (...params: TParams): Promise<Awaited<TReturn>>;
```

- `service: TService`: the service's name that is allowed to run this method
- `name: string`: the method's unique name

### `Client`

An abstraction over `Context` that is used by methods and entrypoints.

**Properties**

- `invoke: function`: this function should be called when a `Method` needs to be invoked
  - **Signature**:

```ts
function invoke<TService extends string, TParams extends any[], TReturn>(
  method:
    | Method<TService, TParams, TReturn>
    | Promise<{ default: Method<TService, TParams, TReturn> }>,
  ...params: TParams
): Promise<Awaited<TReturn>>;
```

- `waitForExit: function`: this function returns a promise that resolves when the node finishes with operation (or due to `Context.exit` being set to `true`)
  - **Signature**:

```ts
function waitForExit(): Promise<void>;
```

- `log: Log`: the default logger passed by the driver

### `client`

A function that can wrap a `Context` into a `Client`.

**Signature**:

```ts
function client(context: Context): Client;
```

### `declareMethod`

Wrap a function and service name into a `Method` to be used as a default export of a RPC method.

Makes the function be remote-callable by nodes that aren't the correct service when invoked using `Client.invoke()`.

**Signature**:

```ts
function declareMethod<TService extends string, TParams extends any[], TReturn>(
  service: TService,
  method: (context: Client, ...params: TParams): TReturn
): Method<TService, TParams, TReturn>;
```

`@startier/ohrid/service`

### `runService`

Runs a service `Context` with an optional entrypoint.

This command is equivalent to how the command `npx ohrid start <service>`, but the driver and the context must be created manually.

**Signature**:

```ts
function runService(
  context: Context,
  entrypoint?: (client: Client): Promise<boolean | void>
): Promise<void>
```