composie
v1.1.0
Published
Compose middleware with router and run it like a charm. Also serves as an advanced event bus with on, off, and emit methods.
Downloads
174
Maintainers
Readme
Features
- Middleware Composition: Supports chaining and complex middleware compositions.
- Routing: Based on path routing for middleware.
- Event Bus: Easily handle and trigger events using
on
,off
, andemit
.
Installation
Install via npm:
npm install composie
Or via yarn:
yarn add composie
Usage
Basic Usage
import Composie from 'composie'; // or const Composie = require('composie');
const composie = new Composie();
// Add global middleware
composie.use((ctx, next) => {
console.log('Global middleware');
return next();
});
// Add route middleware
composie.route('api/user-info', (ctx, next) => {
ctx.response = 'User info';
return next();
});
// Run middleware
composie.run('api/user-info').then(response => {
console.log(response); // 'User info'
});
// remove route
composie.remove('api/user-info');
Using as an Event Bus
const Composie = require('composie');
const composie = new Composie();
// add general middleware for all events
composie.use(async (ctx, next) => {
// simulate async operation
await Promise(resolve => setTimeout(resolve, 1000));
console.log(`Handling event: ${ctx.channel}`);
return next();
});
// Listen for events
composie.on('user-registered', (ctx, next) => {
console.log(`Handling event for user: ${ctx.request.user}`);
// to continue to the next callback
return next();
});
// chain another callback
composie.on('user-registered', (ctx, next) => {
if (ctx.request.user === 'Alice') {
ctx.response = 'Alice is registered';
} else {
// to continue to the next callback
return next()
}
});
// even chain more callbacks
composie.on('user-registered', (ctx, next) => {
ctx.response = `User ${ctx.request.user} is not registered`;
});
// Emit events
composie.emit('user-registered', { user: 'Alice' }).then((response) => {
console.log(response); // 'Alice is registered'
});
composie.emit('user-registered', { user: 'Bob' }).then((response) => {
console.log(response); // 'User Bob is not registered'
});
// Remove event listener
composie.off('user-registered');
API Documentation
Before you get started, there are some basic concepts of this library.
- Every middleware(callback) is a function that receive two parameters
ctx
andnext
:
function (ctx, next) {
// your logic here
}
ctx
is an object contains request info:
{
// channel name, required
channel: 'channel name',
// data you passed by run or emit, optional
request: 'any request data'
// response data, you should save your result in it
response: 'any response data'
// you can add your own property to ctx to share information between middlewares
...
}
- You should save your result in
ctx.response
, you will get it in.run
(or.emit
) promise resolve - and you can add your own property to
ctx
to share information between middlewares - use
return next()
orawait next()
(in async function) if you wantctx
be processed by the next middleware, or justreturn
to terminate it - use
throw
to throw an error if an error occurred, you will catch it in.run
(or.emit
) promise reject
Composie
Create an instance
import Composie from 'composie'
// no arguments is needed
const composie = new Composie()
This libaray is writen in class
, you should create an instance with new
before use it
composie.use(middleware)
Add global middleware, all invoking will proccessed by global middleware
// basic usage
composie.use(function(ctx, next) {
// your logic here
return next();
});
// advanced usage
composie.use(async function(ctx, next) {
try {
// your logic here
await next();
} catch (err) {
// handle error, you can throw it again or just log it
console.error(err);
// set a default response, if you want
ctx.response = '404 Not Found';
// or just throw it again, then you can catch it in `.run`(or `.emit`) promise reject
// throw err;
}
})
// chain more than one middleware
.use(function(ctx, next) {
// your logic here
return next();
});
You can add more than more than one global middleware
, they will run by the order you adding.
composie.use(channelPrefix, middleware)
Add a middleware for channel has specific prefix, when run the middleware, if the channel match the prefix, then will be processed by the that middleware.
composie.use('api', function(ctx, next) {
// your logic here
return next()
});
You can also add as many middlewares as you want for one prefix, they will be called in the order they added.
composie.route(channel, middleware, [middleware...])
Add middlewares for a specific channel, you can add more than one at a time.
composie.on
is an alias of composie.route
, use it as you like.
compose.route(
"api/user",
function(ctx, next) {
// your logic here
},
function(ctx, next) {
// another middleware
}
);
composie.route({channel: [middleware...]})
Add middlewares for more than one channel
composie.on
is an alias of composie.route
, use it as you like.
compose.route({
"api/user": function(ctx, next) {
// your logic here
},
"api/detail": [
function(ctx, next) {
// your logic here
},
function(ctx, next) {
// another middleware
}
]
});
chain use
, route
together
You can chain use
and route
together
compose
.use(function(ctx, next) {
// your logic here
// ...
return next();
})
.use('api', function(ctx, nexdt) {
// your logic here
// ...
return next();
})
.route('api/user', function(ctx, next) {
// your logic here
})
// `on` is an alias of `route`
.on('api/detail', function(ctx, next) {
// your logic here
});
composie.alias(existingChannel, aliasName)
add an alias for an existing channel, when you run the alias, it will be processed by the middleware of the existing channel.
composie.route('api/user', function(ctx, next) {
ctx.response = 'User info';
});
composie.alias('api/user', 'user');
composie.run('user').then(response => {
console.log(response); // 'User info'
});
composie.removeRoute(channel, middleware?)
Remove a middleware for a specific channel, if middleware
is not provided, then all middlewares for that channel will be removed.
composie.off
is an alias of composie.removeRoute
, use it as you like.
composie.removeRoute('api/user');
compose.off('api/user', middleware);
When you remove a alias, the alias will be removed, but the existing channel will be kept.
composie.run(channel, request?)
run middleware for channel
, it will return a promise
composie.emit
and composie.call
are aliases of composie.run
, use it as you like.
compose.run("api/user", { id: "xxx" }).then(
resp => {
console.log("response ", resp);
},
err => {
console.log("error", err);
}
);
createEventBus(options)
Utilize Composie as an Event Bus for efficient event handling in your applications.
import { createEventBus } from 'composie';
const eventBus = createEventBus(...)
interface IEventBusOptions<IContext extends IBaseContext> {
/**
* convert a normal callback to a route handler
*/
convertCallback2Middleware?: (fn: Function) => ((ctx: IContext, next: Function): any)
/**
* create context function
*/
createContext?: (channel: string, request: any) => IContext
/**
* throw when no route found
*/
throwWhenNoRoute?: boolean
}
for detail usage check the example below
Use Composite as a Event Bus
import { createEventBus } from 'composie';
const eventBus = createEventBus({
// this is default the default converter
convertCallback2Middleware: (fn) => {
return async (ctx, next) => {
try {
const response = await fn(ctx.request);
if (typeof response !== 'undefined') {
ctx.response = response
}
return next();
} catch (err) {
throw err;
}
}
}
});
eventBus.use(...)
eventBus.on('userChanged', (userInfo) => {
console.log(userInfo)
})
eventBus.emit('userChanged', { name: 'xxx', email: 'xxxx'})
Advanced Example
Following example shows how to use Composie as an advanced fetch module for a real world application.
import Composie, { ComposieError } from 'composie';
const grab = new Composie({
createContext: (channel, request) => ({
// lower case the channel name
channel: channel.toLowerCase(),
request,
},
throwWhenNoRoute: true
));
grab.use(async (ctx, next) => {
try {
// append query string to the url
if (ctx.request.qs) {
const qs = new URLSearchParams(ctx.request.qs);
const url = new URL(ctx.request.url);
// append query string to the url, concat with the existing query string
url.search = new URLSearchParams([...url.searchParams, ...qs]);
ctx.request.url = url.toString();
}
await next();
} catch (err) {
if (err instanceof ComposieError) {
if (err.code === ComposieError.CODES.ROUTE_NOT_FOUND) {
console.error('Route not found');
} else if (err.code === '') {
console.error('Method not allowed');
}
console.error(err.message);
} else {
if (err.code === '401') {
location.path = '/login';
}
}
}
});
grab.use('post/', (ctx, next) => {
ctx.request.method = ctx.request.method || 'POST';
return next();
});
grab.on('post/json', (ctx, next) => {
if (ctx.request.data) {
ctx.request.headers = {
...ctx.request.headers,
'Content-Type': 'application/json'
};
ctx.request.body = JSON.stringify(ctx.request.data);
}
ctx.response = await fetch(ctx.request).then(res => res.json());
});
grab.on('post/form', async (ctx, next) => {
if (ctx.request.data) {
ctx.request.headers = {
...ctx.request.headers,
'Content-Type': 'application/x-www-form-urlencoded'
};
ctx.request.body = new URLSearchParams(ctx.request.data);
}
ctx.response = await fetch(ctx.request).then(res => res.json());
})
grab.use('get/', (ctx, next) => {
ctx.request.method = ctx.request.method || 'GET';
return next();
});
grab.on('post/json', async (ctx, next) => {
ctx.response = await fetch(ctx.request).then(res => res.json());
return next();
});
grab.emit('post/json', {
url: 'https://jsonplaceholder.typicode.com/users/1'
}).then((response) => {
console.log(response);
});
Contribution Guide
We welcome contributions! Please read the following guide to understand how to contribute to the project.
Submitting Issues
If you find a bug or have a feature request, please submit an issue detailing the problem or suggestion.
Submitting Pull Requests
- Fork the repository
- Create a new branch (git checkout -b feature-branch)
- Commit your changes (`git commit -am '