Seneca

Now it's time for a completely different approach. Let's look at another framework called Seneca. This framework was designed to help you develop message-based microservices. It has two distinct characteristics:

  • Transport agnostic: Communication and message transport is separated from your service logic and it's easy to swap transports
  • Pattern matching: Messages are JSON objects and each function exposes what sort of messages they can handle based on object properties

Being able to change transports is not a big deal; many tools allow you to do so. What is really interesting about this framework is its ability to expose functions based on object patterns. Let's start by installing Seneca:

npm install seneca

For now, let's forget the transport and create a producer and consumer in the same file. Let's look at an example:

const seneca  = require("seneca");
const service = seneca();

service.add({ math: "sum" }, (msg, next) => {
next(null, {
sum : msg.values.reduce((total, value) => (total + value), 0)
});
});

service.act({ math: "sum", values: [ 1, 2, 3 ] }, (err, msg) => {
if (err) return console.error(err);

console.log("sum = %s", msg.sum);
});

There's a lot to absorb. The easy part comes first as we include the seneca module and create a new service.

We then expose a producer function that matches an object that has math equal to sum. This means that any request object to the service that has the property math and that is equal to sum will be passed to this function. This function accepts two arguments. The first, which we called msg, is the request object (the one with the math property and anything else the object might have). The second argument, next, is the callback that the function should invoke when finished or in case of an error. In this particular case, we're expecting an object that also has a values list and we're returning the sum of all values by using the reduce method that's available in arrays.

Finally, we invoke act, expecting it to consume our producer. We pass an object with the math equal to sum and a list of values. Our producer should be invoked and should return the sum.

Assuming you have this code in app.js, if you run this in the command line, you should see something like this:

$ node app
sum = 6

Let's try and replicate our previous stack example. This time, instead of having the consumer and producer in the code, we'll use curl as the consumer, just like we did previously.

First, we need to create our service. We do that, as we've seen before, by loading Seneca and creating an instance:

const seneca  = require("seneca");
const service = seneca({ log: "silent" });

We explicitly tell it that we don't care about logging for now. Now, let's create a variable to hold our stack:

const stack = [];

We then create our producers. We'll create three of them: one for adding an element to the stack, called push; one to remove the last element from the stack, called pop; and one to see the stack, called get. Both push and pop will return the final stack result. The third producer is just a helper function so that we can see the stack without performing any operations.

To add elements to the stack, we define:

service.add("stack:push,value:*", (msg, next) => {
stack.push(msg.value);

next(null, stack);
});

There are a few new things to see here:

  • We defined our pattern as a string instead of an object. This action string is a shortcut to the extended object definition.
  • We explicitly indicate that we need a value.
  • We also indicate that we don't care what the value is (remember, this is pattern matching).

We now define a simpler function to remove the last element of the stack:

service.add("stack:pop", (msg, next) => {
stack.pop();

next(null, stack);
});

This one is simpler as we don't need a value, we're just removing the last one. We're not addressing the case where the stack is empty already. An empty array won't throw an exception, but perhaps, in a real scenario, you would want another response.

Our third function is even simpler as we just return the stack:

service.add("stack:get", (msg, next) => {
next(null, stack);
});

Finally, we need to tell our service to listen for messages. The default transport is HTTP and we just indicate port 3000 as we did in our previous examples:

service.listen(3000);

Wrap all this code in a file and try it out. You can use curl or just try it in your browser. Seneca won't differentiate between HTTP verbs in this case. Let's begin by checking our stack. The URL describes an action (/act) we want to perform and the query parameter gets converted to our pattern:

We can then try adding the value one to our stack and see the final stack:

We can continue and add the value two and see how the stack grows:

If we then try to remove the last element, we'll see the stack shrinking:

As in Express, Seneca also has middleware that you can install and use. In this case, the middleware is called plugins. By default, Seneca includes a number of core plugins for transport, and both HTTP and TCP transports are supported. There are more transports available, such as Advanced Message Queuing Protocol (AMQP) and Redis.

There are also storage plugins for persistent data and there's support for several database servers, both relational and non-relational. Seneca exposes an object-relational mapping (ORM)-like interface to manage data entities. You can manipulate entities and use a simple storage in development and then move to production storage later on. Let's see a more complex example of this:

const async   = require("async");
const seneca = require("seneca");
const service = seneca();

service.use("basic");
service.use("entity");
service.use("jsonfile-store", { folder : "data" });

const stack = service.make$("stack");

stack.load$((err) => {
if (err) throw err;

service.add("stack:push,value:*", (msg, next) => {
stack.make$().save$({ value: msg.value }, (err) => {
return next(err, { value: msg.value });
});
});

service.add("stack:pop,value:*", (msg, next) => {
stack.list$({ value: msg.value }, (err, items) => {
async.each(items, (item, next) => {
item.remove$(next);
}, (err) => {
if (err) return next(err);

return next(err, { remove: items.length });
});
});
});

service.add("stack:get", (msg, next) => {
stack.list$((err, items) => {
if (err) return next(err);

return next(null, items.map((item) => (item.value)));
});
});

service.listen(3000);
});

Just run this new code and we'll see how this code behaves by making some requests to test it. First, let's see how our stack is by requesting it:

Nothing different. Now, let's add the value one to the stack:

Well, we haven't received the final stack. We could, but instead we changed the service to return the exact item that was added. It's actually a good way to confirm what we just did. Let's add another one:

Again, it returns the value we just added. Now, let's see how our stack is:

Our stack now has our two values. Now comes one big difference compared with the previous code. We're using entities, an API exposed by Seneca, which helps you store and manipulate data objects using a simple abstraction layer similar to an ORM, or to people who are familiar with Ruby, an ActiveRecord.

Our new code, instead of just popping out the last value, removes a value we indicate. So, let's remove the value one instead of two:

Success! We removed exactly one item. Our code will remove all items from the stack that match the value (it has no duplication check so you can have repeated items). Let's try to remove the same item again:

No more items match one, so it didn't remove anything. We can now check our stack and confirm that we still have the value two:

Correct! And, as a bonus, you can stop and restart the code and your stack will still have the value two. That's because we're using the JSON file store plugin.

When testing using Chrome or any other browser, be aware that sometimes, browsers make requests in advance while you're typing. Because we already tested our first code, which had the same URL addresses, the browser might duplicate requests and you might get a stack with duplicated values without knowing why. This is why.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset