Skip to content

Commit

Permalink
feat: introducing the feature flag middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Nov 2, 2024
1 parent 9ee3745 commit b819473
Show file tree
Hide file tree
Showing 13 changed files with 359 additions and 27 deletions.
4 changes: 2 additions & 2 deletions docs/src/content/docs/built-in-middlewares/cacher.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ The Cacher Middleware is going to add:

-
```typescript
export type HandledStamp<R> = Stamp<R, 'missive:handled'>;
type HandledStamp<R> = Stamp<R, 'missive:handled'>;
```
> With the result from the cache.
> With the result from the cache. (this stamp will always be there, added by this middleware or the handler)


## Shortcircuiting the processing
Expand Down
114 changes: 114 additions & 0 deletions docs/src/content/docs/built-in-middlewares/feature-flag.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: Feature Flag Middleware
description: Built-in middleware to control the activation of specific features.
---

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

The FeatureFlag Middleware in Missive.js provides developers with the ability to control the activation of specific features when handling commands
or queries in their applications. This middleware ensures that requests are conditionally processed based on the state of feature flags,
enabling dynamic feature management, safer rollouts, and efficient A/B testing.

## How to use it

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

```typescript
const queryBus = createQueryBus<QueryHandlerRegistry>();
queryBus.useFeatureFlagMiddleware({
// this is most likely be a service that you inject here
// for the sake of a clear documentation we are using a simple function to randomly return true or false
featureFlagChecker: async (intent) => {
if (intent === 'getUser') {
return Math.random() > 0.5;
}
return true;
},
intents: {
getUser: {
// same here, your handler exist already and can be injected here
// for the sake of a clear documentation we are returning a simple object
fallbackHandler: async (envelope) => {
return {
success: false,
nickname: '1234',
user: {
id: '1234',
email: 'asd',
}
}
},
shortCircuit: false, // default is true
}
}
})
```

### Explanation

With the Feature Flag Middleware you can control the activation of specific features based on the result of the `featureFlagChecker` function.
But what if the feature is not activated? You can provide a `fallbackHandler` that will be called instead of the handler.
From there, you can decide to return a default value, throw an error, or do whatever you want.

The `shortCircuit` flag is also available to control the behavior of the middleware. By default, it is set to `true`, which means that the middleware
will shortcircuit the processing if the feature is not activated (fallback used). If you set it to `false`, the middleware will continue the processing
and call the next middlewares but the final handler will not be called.

<Aside title="Result consistency" type="tip">
For consistency, you will notice that the _fallbackHandler_ must return the same type as the handler .
</Aside>

## Added Stamps

The Feature Flag Middleware is going to add:

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

```
> When a fallbackHandler is used.

-
```typescript
type HandledStamp<R> = Stamp<R, 'missive:handled'>;
```
> When a fallbackHandler is used. (this stamp will always be there, added by this middleware or the handler)


## Shortcircuiting the processing

As explain the [Middleware guide](/missive.js/guides/middlewares#breaking-the-chain-of-middlewares), you can shortcircuit the processing by _NOT_ calling `next()` in a middleware.

This is exactly what the CacherMiddleware does. It does not call `next()` which means that all the middlewares after it will be skipped.

You can change this behavior by providing the `shortCircuit` flag to `false`.

```typescript
queryBus.useCacherMiddleware({
adapter: memoryStorage,
shortCircuit: false, // default is true
defaultTtl: 20,
intents: {
ListAllCharacters: { shortCircuit: true }, // this intent will skip all the middlewares after Cacher
},
});
```

<Aside title="Handling won't happen twice!" type="note">
Shortcircuiting has no impact on the "handling". As CacherMiddleware is adding the `HandledStamp` the bus won't call the handler anyway when the result is coming from the cache.
The only impact of this flag is wether or not the next middlewares are called or not.
</Aside>

> **With great power comes great responsibility.** - Missive.js is not opinionated, it's up to you to decide what is best for your application.

<Aside title="Why do we default to `shortCircuit: true`" type="tip">
We believe this is the expected behavior, and as subsequent middlewares might have side-effects, we prefer to skip them in those scenarios. (but you do you!)
</Aside>

## 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/cacher-middleware.ts" class="contents" target="_blank"><Icon name="github" class="mr-2"/>Cacher Middleware</a>
</div>
1 change: 0 additions & 1 deletion docs/src/content/docs/built-in-middlewares/lock.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ Then as a second argument, you can provide the following parameters (and per int

No stamps are added by this middleware.


## Going further

<div class='flex flex-row'>
Expand Down
29 changes: 16 additions & 13 deletions docs/src/content/docs/built-in-middlewares/webhook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,27 @@ commandBus.useWebhookMiddleware({
multiplier: 1.5,
waitingAlgorithm: 'exponential',
fetcher: fetch,
mapping: {
createUser: [
{
url: 'https://webhook.site/c351ab7a-c4cc-4270-9872-48a2d4f67ea4',
method: 'POST',
headers: {
'Content-Type': 'application/json',
intents: {
createUser: {
async: false,
parallel: false,
jitter: 0.25,
endpoints: [
{
url: 'https://webhook.site/c351ab7a-c4cc-4270-9872-48a2d4f67ea4',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
signatureHeader: 'X-Plop-Signature',
signature: (payload) => 'signature',
},
signatureHeader: 'X-Plop-Signature',
signature: (payload) => 'signature',
}
],
],
},
},
});
```

> Remember built-in middlewares are _intent_ aware, therefore you can customize the behavior per intent using the key `intents`.
### Explanation

The Webooh middleware is simply taking the envelope and send it to the confifured Webhooks.
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/guides/middlewares.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@ the chain middleware will be broken, and the intent will not be processed furthe
This is really convenient for performance for instance, but it is not always what you want to do.
For this reason the bus will never handle an intent that has been handled already. It does that information thanks to the `missive:handled` stamp.

Within the built-in middlewares, here is the list of the ones that can break the chain:
- [**CacherMiddleware**](/missive.js/built-in-middlewares/cacher): if the intent has been cached you have the option to break the chain.

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_.
25 changes: 18 additions & 7 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ We love clean architecture and CQRS, we didn't find any simple Service Bus that
<Card title="Middlewares" icon="open-book">
Can we call it a Service Bus without Middlewares? _(we don't)_. Register middlewares to leverage the full power
of the service bus.
We got you covered with built-in middlewares like [Logger](/missive.js/built-in-middlewares/logger), [Caching](/missive.js/built-in-middlewares/cacher), [Retry](/missive.js/built-in-middlewares/retryer), [Lock](/missive.js/built-in-middlewares/lock) or [Webhook](/missive.js/built-in-middlewares/webhook).
We got you covered with built-in middlewares like
[Logger](/missive.js/built-in-middlewares/logger),
[Caching](/missive.js/built-in-middlewares/cacher),
[Retry](/missive.js/built-in-middlewares/retryer),
[Lock](/missive.js/built-in-middlewares/lock),
[FeatureFlag](/missive.js/built-in-middlewares/feature-flag),
or [Webhook](/missive.js/built-in-middlewares/webhook).
</Card>
<Card title="Envelopes and Stamps" icon="email">
Handle cross-cutting concerns with Envelopes and Stamps.
Expand Down Expand Up @@ -124,17 +130,22 @@ That's the power of Missive.js! Here are the built-in middlewares:
<LinkButton href={'/missive.js/built-in-middlewares/webhook'} variant="secondary" class="float-right" title={"Read the doc about the Webhook Middleware"}>Read the doc</LinkButton>
</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>
</Card>

<Card title="Lock Middleware" icon="seti:lock">
<p>It provide a way to lock the processing of a message based on something in the intent itself. For instance your could lock the `command` that touches a Cart.</p>
<LinkButton href={'/missive.js/built-in-middlewares/lock'} variant="secondary" class="float-right" title={"Read the doc about the Lock Middleware"}>Read the doc</LinkButton>
</Card>

<Card title="Feature Flag Middleware" icon="seti:yml">
<p>This middleware ensures that requests are conditionally processed based on the state of feature flags, enabling dynamic feature management, safer rollouts, and efficient A/B testing.</p>
<LinkButton href={'/missive.js/built-in-middlewares/feature-flag'} variant="secondary" class="float-right" title={"Read the doc about the Feature Flag Middleware"}>Read the doc</LinkButton>
</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>
</Card>

</CardGrid>

## Framework Agnostic, see the examples
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 @@ -53,6 +53,7 @@ And on top of what you can do on your own, Missive.js provides a set of built-in
- [**Retryer**](/missive.js/built-in-middlewares/retryer): Retry handling if an error occurs.
- [**Webhook**](/missive.js/built-in-middlewares/webhook): Send the envelope to webhook(s).
- [**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.


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
36 changes: 36 additions & 0 deletions examples/shared/src/core/buses.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,42 @@ const queryBus: QueryBus = createQueryBus<QueryHandlerRegistry>();
queryBus.useLoggerMiddleware();
queryBus.useCacherMiddleware();
queryBus.use(loggerMiddleware);

queryBus.useFeatureFlagMiddleware({
featureFlagChecker: async (intent) => {
if (intent === 'getUser') {
return Math.random() > 0.5;
}
return true;
},
intents: {
getUser: {
fallbackHandler: async (envelope) => {
return {
success: false,
nickname: '1234',
user: {
id: '1234',
email: 'asd',
},
};
},
shortCircuit: false,
},
getOrders: {
fallbackHandler: async (envelope) => {
return {
success: false,
orders: [],
user: {
id: '1234',
email: 'asd',
},
};
},
},
},
});
queryBus.register('getUser', getUserQuerySchema, createGetUserHandler({}));
queryBus.register('getOrders', getOrdersQuerySchema, createGetOrdersHandler({}));

Expand Down
1 change: 1 addition & 0 deletions examples/shared/src/domain/use-cases/get-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const handler = async (envelope: Envelope<Query>, deps: Deps) => {
envelope.addStamp<CacheableStamp>('missive:cacheable', { ttl: 1800 });
return {
success: true,
nickname: 'plopix',
user: {
id: '1234',
email: '[email protected]',
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.1.0",
"version": "0.1.1",
"type": "module",
"main": "./build/index.cjs",
"module": "./build/index.js",
Expand Down
25 changes: 25 additions & 0 deletions libs/missive.js/src/core/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createCacherMiddleware } from '../middlewares/cacher-middleware.js';
import { createRetryerMiddleware } from '../middlewares/retryer-middleware.js';
import { createWebhookMiddleware } from '../middlewares/webhook-middleware.js';
import { createLockMiddleware } from '../middlewares/lock-middleware.js';
import { createFeatureFlagMiddleware } from '../middlewares/feature-flag-middleware.js';

export type BusKinds = 'query' | 'command' | 'event';
export type MessageRegistryType<BusKind extends BusKinds> = Record<string, HandlerDefinition<BusKind>>;
Expand Down Expand Up @@ -81,6 +82,9 @@ type MissiveCommandBus<HandlerDefinitions extends CommandMessageRegistryType> =
useRetryerMiddleware: (...props: Parameters<typeof createRetryerMiddleware<'command', HandlerDefinitions>>) => void;
useWebhookMiddleware: (...props: Parameters<typeof createWebhookMiddleware<'command', HandlerDefinitions>>) => void;
useLockMiddleware: (...props: Parameters<typeof createLockMiddleware<'command', HandlerDefinitions>>) => void;
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'command', HandlerDefinitions>>
) => void;
};

export type CommandBus<HandlerDefinitions extends CommandMessageRegistryType> = Prettify<
Expand All @@ -96,6 +100,9 @@ type MissiveQueryBus<HandlerDefinitions extends QueryMessageRegistryType> = Repl
useWebhookMiddleware: (...props: Parameters<typeof createWebhookMiddleware<'query', HandlerDefinitions>>) => void;
useLockMiddleware: (...props: Parameters<typeof createLockMiddleware<'query', HandlerDefinitions>>) => void;
useCacherMiddleware: (...props: Parameters<typeof createCacherMiddleware<HandlerDefinitions>>) => void;
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'query', HandlerDefinitions>>
) => void;
};
export type QueryBus<HandlerDefinitions extends QueryMessageRegistryType> = Prettify<
MissiveQueryBus<HandlerDefinitions>
Expand All @@ -109,6 +116,9 @@ type MissiveEventBus<HandlerDefinitions extends EventMessageRegistryType> = Repl
useRetryerMiddleware: (...props: Parameters<typeof createRetryerMiddleware<'event', HandlerDefinitions>>) => void;
useWebhookMiddleware: (...props: Parameters<typeof createWebhookMiddleware<'event', HandlerDefinitions>>) => void;
useLockMiddleware: (...props: Parameters<typeof createLockMiddleware<'event', HandlerDefinitions>>) => void;
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'event', HandlerDefinitions>>
) => void;
};
export type EventBus<HandlerDefinitions extends EventMessageRegistryType> = Prettify<
MissiveEventBus<HandlerDefinitions>
Expand Down Expand Up @@ -273,6 +283,11 @@ export const createCommandBus = <HandlerDefinitions extends CommandMessageRegist
useWebhookMiddleware: (...props: Parameters<typeof createWebhookMiddleware<'command', HandlerDefinitions>>) => {
commandBus.use(createWebhookMiddleware(...props));
},
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'command', HandlerDefinitions>>
) => {
commandBus.use(createFeatureFlagMiddleware(...props));
},
register: commandBus.register,
dispatch: commandBus.dispatch,
createCommand: commandBus.createIntent,
Expand Down Expand Up @@ -302,6 +317,11 @@ export const createQueryBus = <HandlerDefinitions extends QueryMessageRegistryTy
useCacherMiddleware: (...props: Parameters<typeof createCacherMiddleware<HandlerDefinitions>>) => {
queryBus.use(createCacherMiddleware(...props));
},
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'query', HandlerDefinitions>>
) => {
queryBus.use(createFeatureFlagMiddleware(...props));
},
register: queryBus.register,
dispatch: queryBus.dispatch,
createQuery: queryBus.createIntent,
Expand All @@ -327,6 +347,11 @@ export const createEventBus = <HandlerDefinitions extends EventMessageRegistryTy
useWebhookMiddleware: (...props: Parameters<typeof createWebhookMiddleware<'event', HandlerDefinitions>>) => {
eventBus.use(createWebhookMiddleware(...props));
},
useFeatureFlagMiddleware: (
...props: Parameters<typeof createFeatureFlagMiddleware<'event', HandlerDefinitions>>
) => {
eventBus.use(createFeatureFlagMiddleware(...props));
},
register: eventBus.register,
dispatch: eventBus.dispatch,
createEvent: eventBus.createIntent,
Expand Down
Loading

0 comments on commit b819473

Please sign in to comment.