argosy
A modular, pipable, micro-service framework.
Table of Contents
motivation
Why a framework? After building micro-services a wide variety of ways over a number of years, in small organizations and large, I wanted to standardize the approach, and bring together all the lessons learned. Argosy draws inspiration from many sources including a smorgasbord of systems (I've used in other micro-service projects) such as RabbitMQ and Zookeeper, as well as other node libraries including but not limited to dnode, rpc-stream, and seneca.
Like the micro-service model, the Argosy ecosystem consists of many small modules. These components are streams, designed to be connected together via pipes. Extending Argosy is a matter of manipulating the stream.
example
es5
var http = require('http'),
query = require('querystring'),
argosy = require('argosy')()
var weatherRequest = argosy.accept({
get: 'weather',
location: argosy.pattern.match.defined
})
weatherRequest.process(function (msg, cb) {
var qs = query.stringify({ q: msg.location, units: msg.units || 'imperial' })
http.get('http://api.openweathermap.org/data/2.5/weather?' + qs, function (res) {
var body = ''
res.on('data', function (data) {
body += data
}).on('end', function () {
cb(null, JSON.parse(body).main)
})
})
})
argosy.invoke({ get: 'weather', location: 'Boston,MA' }, function (err, weather) {
console.log(weather.temp + ' degrees (F) in Boston.')
})
var getWeather = argosy.invoke.partial({ get: 'weather', units: 'metric' })
getWeather({ location: 'Dublin,IE' }, function (err, weather) {
console.log(weather.temp + ' degrees (C) in Dublin.')
})
getWeather({ location: 'London,UK' }).then(function (weather) {
console.log(weather.temp + ' degrees (C) in London.')
})
es6+
var http = require('http'),
query = require('querystring').stringify,
request = require('request-promise'),
co = require('co'),
argosy = require('..')()
var weatherRequest = argosy.accept({
get: 'weather',
location: argosy.pattern.match.defined
})
var weatherUrl = 'http://api.openweathermap.org/data/2.5/weather?'
weatherRequest.process(co.wrap(function* ({ location: q, units = 'imperial' }) {
var weather = yield request.get(weatherUrl + query({ q, units }))
return JSON.parse(weather).main
}))
var getWeather = argosy.invoke.partial({ get: 'weather', units: 'metric' })
co(function* () {
var boston = yield argosy.invoke({ get: 'weather', location: 'Boston,MA' })
var dublin = yield getWeather({ location: 'Dublin,IE' })
var london = yield getWeather({ location: 'London,UK' })
console.log(boston.temp + ' degrees (F) in Boston.')
console.log(dublin.temp + ' degrees (C) in Dublin.')
console.log(london.temp + ' degrees (C) in London.\n')
})
Note: If you're runtime doesn't offer generators or promises, you can still run the above example from the example directory via babel. Just do: npm i -g babel && babel-node example/es6.js
api
var argosy = require('argosy')()
queue = argosy.accept(pattern)
Create a concurrent-queue that will be pushed messages that match the pattern
object provided (see argosy-pattern for details on defining patterns). These messages should be processed and responded to using the process
function of the queue
. Responses will be sent to the requesting Argosy endpoint.
It is advised not to match the key argosy
as this is reserved for internal use.
queue.process([opts,] func)
Process messages. See concurrent-queue for more information. The processor function func
has a signature of msg [, cb]
. The callback cb
if provided should be executed with any applicable return value or error object (as 1st argument) for the invoking client, once the request has been completed. Alternatively, a promise may be returned from the processor function func
, and it's resolved value or rejected error will be returned to the invoking client.
argosy.invoke(msg [, cb])
Invoke a service which implements the msg
pattern. Upon completion, the callback cb
, if supplied, will be called with the result or error. The argosy.invoke
function also returns a promise which will resolve or reject appropriately. If the msg
matches one oft he patterns implemtned by the argosy
endpoint performing the invoke
, then the invoke
request will be taken care of locally by the the Argosy endpoint invoke
was called from, otherwise the invoke
request will be written to the stream's output, and the stream's input will be monitored for a response.
func = argosy.invoke.partial(partialMsg)
Return a function that represents a partial invocation. The function returned has the same signature as argosy.invoke
, but when called, the msg
parameter will be merged with the partialMsg
parameter provided at the time the function was created. Otherwise, the generated function behaves identically to argosy.invoke
.
pattern = argosy.pattern(object)
See also argosy-pattern.
Create an Argosy pattern, given an object containing rules. Each key in the object represents a key that is to be validated in compared message objects. These keys will be tested to have the same literal value, matching regular expression, or to be of a given type using the matching system described below. Nested keys may be matched using the dot notation. For example, {'message.count':1}
equates to {message: {count: 1}}
.
pattern.matches(object)
Returns true of the given object matches the pattern, or false otherwise.
argosy.pattern.match
Argosy patterns support more than literal values. The values of the pattern keys may be any of the following in addition to literal values:
- A regular expression - values will be tested against the regular expression to determine a match
argosy.pattern.match.number
- matches any numberargosy.pattern.match.string
- matches any stringargosy.pattern.match.bool
- matches true
or false
argosy.pattern.match.array
- matches any arrayargosy.pattern.match.object
- matches any truthy objectargosy.pattern.match.defined
- matches anything other than undefined
argosy.pattern.match.undefined
- matches undefined
or missing key
connecting endpoints
One Argosy endpoint may be connected to another via pipes.
var argosy = require('argosy'),
service1 = argosy(),
service2 = argosy()
service1.pipe(service2).pipe(service)
This will create a duplex connection between the two Argosy endpoints, and allow both to invoke implemented servcies via the other. For example:
service1.accept({ get: 'random number' }).process(function (msg, cb) {
})
service2.accept({ get: 'random letter' }).process(function (msg, cb) {
})
service1.invoke({ get: 'random letter' }, function (err, letter) {
})
service2.invoke({ get: 'random number' }, function (err, number) {
})
testing
npm test [--dot | --spec] [--grep=pattern]
Specifying --dot
or --spec
will change the output from the default TAP style.
Specifying --grep
will only run the test files that match the given pattern.
coverage
npm run coverage [--html]
This will output a textual coverage report. Including --html
will also open
an HTML coverage report in the default browser.