Skip to content

Commit

Permalink
feat(core): provide message abstractions (#12)
Browse files Browse the repository at this point in the history
closes #11
  • Loading branch information
RomanReznichenko authored Apr 5, 2022
1 parent 2026f20 commit 6e44b1f
Show file tree
Hide file tree
Showing 26 changed files with 603 additions and 10 deletions.
87 changes: 78 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"dependencies": {
"reflect-metadata": "^0.1.13",
"tslib": "~2.3.1",
"tsyringe": "^4.6.0"
"tsyringe": "^4.6.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@commitlint/cli": "^16.2.1",
Expand All @@ -93,6 +94,7 @@
"@semantic-release/git": "^10.0.1",
"@types/jest": "^27.4.0",
"@types/node": "~16.11.25",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2",
"eslint": "8.7.0",
Expand Down
119 changes: 119 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,125 @@ const config = new Configuration({
});
```

### Messages

Message is used for syncing state between SDK, application and/or external services.
This functionality is done by sending messages outside using a concrete implementation of `Dispatcher`.

Depending on the type of derived class from the `Message`, it might be addressed to only one consumer or have typically multiple consumers as well.
When a message is sent to multiple consumers, the appropriate event handler in each consumer handles the message.

The `Message` is a data-holding class, but it implements a [Visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern#:~:text=In%20object%2Doriented%20programming%20and,structures%20without%20modifying%20the%20structures.)
to allow clients to perform operations on it using a visitor class (see `Dispatcher`) without modifying the source.

For instance, you can dispatch a message in a way that is more approach you or convenient from the client's perspective.

```ts
import { CommandDispatcher } from '@secbox/core';

const dispatcher = container.resolve(CommandDispatcher);

interface Payload {
status: 'connected' | 'disconnected';
}

class Ping extends Command<Payload> {
constructor(payload: Payload) {
super(payload);
}
}

// using a visitor pattern
await new Ping({ status: 'connected' }).execute(dispatcher);

// or directly
await dispatcher.execute(new Ping({ status: 'disconnected' }));
```

The same is applicable for the `Event`. You just need to use the `EventDispatcher` instead of `CommandDispatcher`.

Each message have a correlation ID to ensure atomicity. The regular UUID is used, but you might also want to consider other options.

### Request-response

The request-response message (aka `Command`) style is useful when you need to exchange messages between various external services.
Using `Command` you can easily ensure that the service has actually received the message and sent a response back.

To create an instance of `Command` use the abstract class as follows:

```ts
interface RequestOptions {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: string;
}

class Request<R = unknown> extends Command<RequestOptions, R> {
constructor(options: RequestOptions) {
super(options);
}
}
```

To adjust its behavior you can use next options:

| Option | Description |
| :------------- | -------------------------------------------------------------------------------------------- |
| `payload` | Message that we want to transmit to the remote service. |
| `expectReply` | Indicates whether to wait for a reply. By default `true`. |
| `ttl` | Period of time that command should be handled before being discarded. By default `10000` ms. |
| `type` | The name of a command. By default, it is the name of specific class. |
| `corelationId` | Used to ensure atomicity while working with EventBus. By default, random UUID. |
| `createdAt` | The exact date and time the command was created. |

### Publish-subscribe

When you just want to publish events without waiting for a response, it is better to use the `Event`.
The ideal use case for the publish-subscribe model is when you want to simply notify another service that a certain condition has occurred.

To create an instance of `Event` use the abstract class as follows:

```ts
interface Issue {
name: string;
details: string;
type: string;
cvss?: string;
cwe?: string;
}

class IssueDetected extends Event<Issue> {
constructor(payload: Issue) {
super(payload);
}
}
```

To adjust its behavior you can use next options:

| Option | Description |
| :------------- | ------------------------------------------------------------------------------ |
| `payload` | Message that we want to transmit to the remote service. |
| `type` | The name of a command. By default, it is the name of specific class. |
| `corelationId` | Used to ensure atomicity while working with EventBus. By default, random UUID. |
| `createdAt` | The exact date and time the event was created. |

To create an event handler, you should implement the `Handler` interface and use the `@bind()` decorator to subscribe a handler to an event:

```ts
@bind(IssueDetected)
class IssueDetectedHandler implements EventHandler<Issue> {
public handle(payload: Issue): Promise<void> {
// implementation
}
}
```

You can register multiple event handlers for a single event pattern and all of them will be automatically triggered in parallel.

As soon as the `IssueDetected` event appears, the event handler takes a single argument, the data passed from the client (in this case, an event payload which has been sent over the network).

## License

Copyright © 2022 [NeuraLegion](https://github.com/NeuraLegion).
Expand Down
Loading

0 comments on commit 6e44b1f

Please sign in to comment.