Goodly is a simple microservice framework that uses RabbitMQ. It has a few design goals to make working events pleasant:
- Provide a simple programming interface for handling and emitting events
- Allow extensibility through middleware
- Perform common eventing patterns with no fuss:
- Services can to subscribe to any events (pub/sub)
- Services scale out by starting another instance (work queues)
- Direct connection between services (rpc)
Using Node.js 7.6+, you can easily create a service with a few lines of code.
npm install goodly
// import goodly
const goodly = require('goodly');
// create the service
const service = goodly('ping');
// listen for an event and attach a handler
service.on('pong', handlePong);
async function handlePong({ data, emit }) {
console.log(`received a pong event with message: ${data.message}`);
await emit('ping', { message: 'ping' });
}
// start the service by attaching to a RabbitMQ instance
service.start('127.0.0.1');Goodly is simple but powerful. Here are some of the features.
Note: for a detailed overview of how Goodly services leverage AMQP and RabbitMQ refer to AMQP and the Goodly Spec.
Goodly makes heavy use of async/await. This was a design descision to make writing handlers simpler.
Working with async function is simple. For example, defining a handler can be done with three techniques:
service.on('some-event', someEventHandler);
async function someEventHandler(event) {
// do something
}service.on('some-event', async function(event) {
// do something
});async lambda function
service.on('some-event', async (event) => {
// do something
});Because async functions always return a Promise you can swap between async/await and callback based code by using Promise syntax.
function delay(next) {
setTimeout(function() {
service.emit('delayed').then(next);
}, 1000);
}That said, all asynchronous operations return Promises.
Events can be emitted by the service or from inside an event handlers. Emit takes the form emit(path, payload).
Service emits are defined as events generated by calling the emit method directly on the service.
Emitting events from the service is a great way to start an event chain. The only requirement for emitting events from the service is that the service needs to be started.
When an event is emitted by the service, it will generate a new correlation Id that is attached to the event. This is a useful way to track a series events through all of the handlers.
A few practical examples of where emitting events directly from the service can be useful...
This example demonstrates how Express or another HTTP framework can form an API-Gateway. Requests are made to the gateway over HTTP and Goodly emits events to perform various actions.
const express = require('express');
const goodly = require('goodly');
// create and start the service
let service = goodly('api-gateway');
service.start('127.0.0.1');
// define the express application
let app = express();
app.post('/login', (req, res, next) => postLogin(req, res).catch(next));
// defined an async HTTP handler
async function postLogin(req, res) {
// do some stuff
// let user = doSomething(req);
// emit that the user has logged in
await service.emit('user.login.complete', user);
res.send(user);
}This example demonstrates how a timer can be used to emit events periodically.
const goodly = require('goodly');
const service = goodly('timer');
service.start('127.0.0.1');
setInternal(function() {
service
.emit('timer.ticked', Date.now)
.catch(console.error);
}, 60000);Handler emitted events are the other type of event emission. These events are triggered by the emit method on the event object.
This is a special emit method that automatically includes the correlation Id from the handled event. This provides a handy way to chain events together and track them via the correlation Id.
For example:
const goodly = require('goodly');
const service = goodly('hello-world');
service.on('ping', async ({ emit }) => {
await emit('pong'); // handler emit retains original correlation Id
});
service.start('127.0.0.1');The above code creates a handler that is executed whenever there is a ping event. The event passed into the handler includes the emit method that is called to emit the pong event. The pong event will include the same correlation Id that was in the event event!
For example, the above method could be modifed to output the correlation Id
const goodly = require('goodly');
const service = goodly('hello-world');
// handler with handler emit
service.on('ping', async ({ emit, correlationId }) => {
console.log('ping', correlationId);
await emit('pong');
});
// handler with handler emit
service.on('pong', async ({ emit, correlationId }) => {
console.log('pong', correlationId);
await emit('ping');
});
// start the service and emit a ping to start it off!
service
.start('127.0.0.1')
.then(() => service.emit('ping'));
// start it off with a service emit
service.emit('ping');With an event-based system you should be able to listen to events. Doing so is quite easy. Events are emitted by other services. You can easily
const goodly = require('goodly');
// import required code for email handler
const emailProcessor = require('./email-processor');
const emailService = goodly({ name: 'email-service' });
emailService.on('order.created', async ({ data: order, emit }) => {
await emailProcessor.sendEmail(order);
await emit('order.emailsent');
});
const inventoryService = goodly({ name: 'inventory-service' });
inventoryService.on('order.created', async({ data, emit }) => {
console.log('placeholder
});-
Promise start({ brokerPath, concurrent)
Starts the service and creates all exchange and queues owned by the service. Handlers must be attached prior to starting the service. Once start is complete, the service will immeidately begin pulling work from the service queue.
Parameters:
- string brokerPath - host of RabbitMQ
- int concurrent - the number of open messages that will be concurrent consumed
-
Promise stop()
stops the service and disconnects from RabbitMQ
-
Promise emit(path, data, options)
Emits a message with the specified path and data. Emit does not expect a response and is equivalent to a publish actions. The service needs to be started prior to emitting a request.
Paramters:
- string path - name of the event to send
- any data - the data that will be send that could be a buffer, string, int, float, bool, array, object, or error
-
Promise request(path, data)
Makes a request with the specified path and data. Request will block until a response is received. The service must be started prior to emitting a request.
Paramters:
- string path - name of the event to send
- any data - the data that will be send that could be a buffer, string, int, float, bool, array, object, or error
-
void on(path, fn1, fn2, ...)
Adds a handler to the service for supplied path. The supplied functions will be executed in order of attachment.
Parameters:
- string path - name of the event to listen for
- fn handler - a handler for the event
-
void use(path, args)
Adds middleware function for modifying in-bound or out-bound events. The path is optional, and if not supplied will attach the middleware to all event.
Parameters:
- string path - optional path to attach middleware to
- object|function args - function for inbound middleware or an object with properties
inandoutfor attaching a function to in-bound and out-bound events.
Handlers are called with an Event object as the first argument and next function as the second argument. Using next allows middleware to pass control to the next layer of middleware and await it's completion.
For example:
service.use(async function (event, next) {
console.log('recieved an event');
await next();
console.log('processed event successfully';
}
The Event object contains several methods and pieces of data:
Properties:
- any data - the data that will be send that could be a buffer, string, int, float, bool, array, object, or error
- correlationId - the UUID mapping to a unique origin event
- msg - the raw RabbitMQ message
Methods:
- emit - calls the emit method on the service but uses the supplied correlationId for the incoming event
- reply - replys to a message that is a Request/Response message