Skip to content

Commit

Permalink
feat: introducing async middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Nov 4, 2024
1 parent 35a4d1b commit 68e2687
Show file tree
Hide file tree
Showing 14 changed files with 489 additions and 38 deletions.
21 changes: 21 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ const github = 'https://github.com/missive-js/missive.js';
const githubURL = new URL(github);
const githubPathParts = githubURL.pathname.split('/');
const title = 'Missive.js';

const stamps = [
'IdentityStamp',
'HandledStamp',
'ReprocessedStamp',
'AsyncStamp',
'FromCacheStamp',
'FeatureFlagFallbackStamp',
'TimingsStamp',
'RetriedStamp',
'WebhookCalledStamp',
];

export default defineConfig({
site: `https://${githubPathParts[1]}.github.io/${githubPathParts[2]}`,
base: `${githubPathParts[2]}`,
Expand Down Expand Up @@ -59,6 +72,14 @@ export default defineConfig({
label: 'Built-in Middlewares',
autogenerate: { directory: 'built-in-middlewares' },
},
{
label: 'Built-in Stamps',
collapsed: true,
items: stamps.map((stamp) => ({
label: stamp,
link: `built-in-stamps#${stamp.toLowerCase()}`,
})),
},
{
label: 'Contributing',
slug: 'contributing',
Expand Down
107 changes: 107 additions & 0 deletions docs/src/content/docs/built-in-middlewares/async.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: Async Middleware
description: Built-in middleware to defer handling to a consumer.
---

import { Icon, Aside, Steps } from '@astrojs/starlight/components';

The Async Middleware is built-in middleware that gives you capability to defer the handling to an consumer to achieve real asynchronousity.
It's only available for _CommandBus_ and _EventBus_

## How to use it

As for any Middleware, you can use it by adding it to the `bus` instance.

```typescript
const commandBus = createCommandBus<CommandHandlerRegistry>();
commandBus.useAsyncMiddleware({
consume: false, // KEY POINT
produce: async (envelope) => {
// use your favorite queue system here
console.log('Generic Push to Queue', envelope);
},
async: true,// default is true
intents: {
createUser: {
async: true,
produce: async (envelope) => {
// use your favorite queue system here
console.log('createUser Push to Queue', envelope);
},
},
},
});
```

> Remember built-in middlewares are _intent_ aware, therefore you can customize the behavior per intent using the key `intents`.
Next, you need to have a consumer that will consume it. The way to do that with Missive.js is to create another bus with this middlware with `consume: true`.

```typescript
commandBus.useAsyncMiddleware({
consume: true, // KEY POINT
});
```

The worker script that consumes the queue can dispatch the message it receives directly to the dispatch method:

```typescript
// Consumer script
onMessage: async (message) => {
const envelope = JSON.parse(message);
await commandBus.dispatch(intent);
}
```
<Aside title="Gotchas" type="note">
Generally, the `dispatch` receives an `intent` but it can also receive an `envelope`. In this case, the `envelope` is the message received from the queue.
</Aside>

### Explanation

The flow is the following:

<Steps>

1. Your application (web node for instance) will have a bus on which this middleware is added with `consume: false`.

2. When you dispatch an intent, the middleware will push the intent to the queue system (via the `produce` method that you provide) instead of handling it.

3. You have another application (worker node for instance) that will have a bus on which this middleware is added with `consume: true`.

4. This worker will consume the intent from the queue system and handle it.

</Steps>

<Aside title="Important things to remember" type="caution">
- `intent` will pass through the bus twice, once for the `produce` and once for the `consume`, so make sure your middleware have no side effects.
- Async Middleware breaks sthe chain of middlewares and for this reason, it is usually register the last.
</Aside>

<Aside title="Gotchas" type="tip">
When you dispatch an `envelope` to the `bus`, the bus will save the `envelope` original `stamps` in the `ReprocessedStamp` stamp.
This way, the bus that consumes the `envelope` can have access to the original `stamps`.
</Aside>


## Added Stamps

The Async Middleware is going to add:

-
```typescript
type AsyncStamp = Stamp<undefined, 'missive:async'>;
```
> When the intent is pushed to the queue.

-
```typescript
type ReprocessedStamp = Stamp<{ stamps: Stamp[] }, 'missive:reprocessed'>;
```
> When the envelope is dispatched.

## Going further

<div class='flex flex-row'>
<span className='pr-2'>Look at the code of the </span>
<a href="https://github.com/Missive-js/missive.js/tree/main/libs/missive.js/src/middlewares/async-middleware.ts" class="contents" target="_blank"><Icon name="github" class="mr-2"/>Async Middleware</a>
</div>
106 changes: 106 additions & 0 deletions docs/src/content/docs/built-in-stamps.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
title: Built-in Stamps
description: All the built-in Stamps that Missive.js provides.
---

import { Aside } from '@astrojs/starlight/components';


This page lists all the built-in Stamps that Missive.js provides.
Stamps are a way to handle cross-cutting concerns in your application. They are key to keeping your code clean and maintainable.
Most of all, they are easy to write and use, and they can be generic!

## Added by the Bus

### IdentityStamp

```typescript
type AsyncStamp = Stamp<undefined, 'missive:async'>;
```
Added on `bus.dispatch(intent|envelope)`.

<Aside title="Dispatching an envelope" type="note">
If an `envelope` is dispatched, the original `IdentityStamp` will be preserved.
</Aside>

### HandledStamp

```typescript
type HandledStamp<R> = Stamp<R, 'missive:handled'>;
```
Added when the intent is handled by the handler.

<Aside title="More than one handler" type="tip">
The `HandledStamp` will be added by each handler.
</Aside>

<Aside title="Conditional handling" type="caution">
If a Middleware adds this stamp, the bus will not run the handler.
</Aside>


### ReprocessedStamp

```typescript
type ReprocessedStamp = Stamp<{ stamps: Stamp[] }, 'missive:reprocessed'>;
```

Added when the `envelope` is dispatched through the bus instead of an `intent`. When this happens,
the bus will save the original `stamps` in the `ReprocessedStamp` stamp.


## Added by the Middlewares

### AsyncStamp

```typescript
type AsyncStamp = Stamp<undefined, 'missive:async'>;
```

Added when the envelope is sent to a queue via the [Async middleware](/missive.js/built-in-middlewares/async).

### FromCacheStamp

```typescript
type FromCacheStamp = Stamp<undefined, 'missive:cache:hit'>;
```

Added when the [Cacher middleware](/missive.js/built-in-middlewares/cacher) finds the result in the cache.

### FeatureFlagFallbackStamp

```typescript
type FeatureFlagFallbackStamp = Stamp<undefined, 'missive:feature-flag-fallback'>;
```

When the [Feature Flag Middleware](/missive.js/built-in-middlewares/feature-flag) uses a fallbackHandler.

### TimingsStamp

```typescript
type TimingsStamp = Stamp<{ total: number }, 'missive:timings'>
```
Add by the [Logger middleware](/missive.js/built-in-middlewares/logger) when the message is handled or errored with the total time elapsed in nanoseconds.
### RetriedStamp
```typescript
type RetriedStamp = Stamp<{ attempt: number; errorMessage: string }, 'missive:retried'>;
```

Added by the [Retryer middleware](/missive.js/built-in-middlewares/retryer) when the middleware retries the handling of the intent.
<Aside title="More than one retry" type="tip">
You will get more than one Stamp!
</Aside>


### WebhookCalledStamp

```typescript
type WebhookCalledStamp = Stamp<{ attempt: number; text?: string, status?: number }, 'missive:webhook-called'>;
```

Add by the [Webhook middleware](/missive.js/built-in-middlewares/webhook) when the middleware succeed to call the webhook(s) or ultimately at the end of the retries.


3 changes: 2 additions & 1 deletion docs/src/content/docs/guides/middlewares.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,5 @@ For this reason the bus will never handle an intent that has been handled alread

Within the built-in middlewares, here is the list of the ones where you have the option to break the chain. _(default: yes)_:
- [**CacherMiddleware**](/missive.js/built-in-middlewares/cacher): if the intent has been cached.
- [**FeatureFlagiddleware**](/missive.js/built-in-middlewares/feature-flag): if the intent has been handled by a _fallbackHandler_.
- [**FeatureFlagMiddleware**](/missive.js/built-in-middlewares/feature-flag): if the intent has been handled by a _fallbackHandler_.
- [**AsyncMiddleware**](/missive.js/built-in-middlewares/async): when the envelope is sent to a queue.
6 changes: 3 additions & 3 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ We love clean architecture and CQRS, we didn't find any simple Service Bus that
[Lock](/missive.js/built-in-middlewares/lock),
[FeatureFlag](/missive.js/built-in-middlewares/feature-flag),
[Mock](/missive.js/built-in-middlewares/mocker),
[Async](/missive.js/built-in-middlewares/async),
or [Webhook](/missive.js/built-in-middlewares/webhook).
</Card>
<Card title="Envelopes and Stamps" icon="email">
Expand Down Expand Up @@ -147,9 +148,8 @@ That's the power of Missive.js! Here are the built-in middlewares:
</Card>

<Card title="Async Middleware" icon="seti:nunjucks">
<p>You can already do async if you don't _await_ in your handler, but this is next level. Push the intent to a queue and handle it asynchronously.</p>
<p>Missive.js will provide the consumer which is going to be smart enough to handle the async processing.</p>
<LinkButton href={'/missive.js/contributing'} variant="secondary" class="float-right" ><Badge text='coming soon' variant='caution'/></LinkButton>
<p>You can already do async if you don't _await_ in your handler, but this is next level to defer handling to a consumer. Push the intent to a queue and handle it asynchronously.</p>
<LinkButton href={'/missive.js/built-in-middlewares/async'} variant="secondary" class="float-right" title={"Read the doc about the Async Middleware"}>Read the doc</LinkButton>
</Card>

</CardGrid>
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/why.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ And on top of what you can do on your own, Missive.js provides a set of built-in
- [**Logger**](/missive.js/built-in-middlewares/logger): Log the messages and the full Envelope (with the Stamps) before and once handled (or errored) in the backend of your choice.
- [**FeatureFlag**](/missive.js/built-in-middlewares/feature-flag): Control the activation of specific features and manage dynamic feature management, safer rollouts, and efficient A/B testing.
- [**Mocker**](/missive.js/built-in-middlewares/mocker): Mock the result of a specific intent to bypass the handler.
- [**Async**](/missive.js/built-in-middlewares/async): Defer handling to a consumer via a queue.


This middleware architecture is familiar if you've used libraries like Express, Fastify, Koa, etc. making it intuitive for both backend and frontend developers.
Expand Down
44 changes: 30 additions & 14 deletions examples/shared/src/core/buses.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,20 @@ const commandBus: CommandBus = createCommandBus<CommandHandlerRegistry>({
{ messageName: 'removeUser', schema: removeUserCommandSchema, handler: createRemoveUserHandler({}) },
],
});
commandBus.useMockerMiddleware({
intents: {
createUser: async (envelope) => ({
success: true,
userId: '1234',
}),
removeUser: async (envelope) => {
return {
success: true,
removeCount: 42,
};
},
},
});
// commandBus.useMockerMiddleware({
// intents: {
// createUser: async (envelope) => ({
// success: true,
// userId: '1234',
// }),
// removeUser: async (envelope) => {
// return {
// success: true,
// removeCount: 42,
// };
// },
// },
// });
commandBus.useLockMiddleware({
adapter: {
acquire: async () => true,
Expand All @@ -114,6 +114,22 @@ commandBus.useLockMiddleware({
},
});

commandBus.useAsyncMiddleware({
consume: false,
produce: async (envelope) => {
console.log('Generic Push to Queue', envelope);
},
async: false,
intents: {
createUser: {
async: true,
produce: async (envelope) => {
console.log('createUser Push to Queue', envelope);
},
},
},
});

commandBus.useWebhookMiddleware({
async: true,
parallel: true,
Expand Down
1 change: 0 additions & 1 deletion examples/shared/src/domain/use-cases/get-orders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { stat } from 'fs';
import { Envelope, QueryHandlerDefinition } from 'missive.js';
import { CacheableStamp } from 'missive.js';
import { z } from 'zod';
Expand Down
2 changes: 1 addition & 1 deletion libs/missive.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"Sébastien Morel <[email protected]>",
"Anaël Chardan"
],
"version": "0.2.0",
"version": "0.3.0",
"type": "module",
"main": "./build/index.cjs",
"module": "./build/index.js",
Expand Down
Loading

0 comments on commit 68e2687

Please sign in to comment.