├── .gitignore ├── .travis.yml ├── README.md ├── browser.js ├── command.js ├── connect.js ├── createClient.js ├── createServer.js ├── example ├── browser.js ├── cli.js ├── config.js ├── index.html ├── server.js └── services │ ├── index.js │ ├── static.js │ └── things.js ├── index.js ├── listen.js ├── package.json ├── serialize.js ├── test ├── basic.js ├── index.js ├── multipleServices.js └── nestedServices.js └── walk.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | /example/bundle.js 4 | /coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "5" 3 | - "6" 4 | sudo: false 5 | language: node_js 6 | script: "npm run test:coverage" 7 | after_script: "npm i -g codecov.io && cat ./coverage/lcov.info | codecov" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | human vasculatory system 7 |
8 | vas 9 |

10 | 11 |

12 | :seedling: composable client/server data services using pull streams 13 |

14 | 15 |
16 | 17 | 18 | stability 19 | 20 | 21 | 22 | npm version 23 | 24 | 25 | 26 | build status 27 | 28 | 29 | 30 | test coverage 31 | 32 | 33 | 34 | downloads 35 | 36 | 37 | 38 | standard style 39 | 40 |
41 | 42 |
43 | table of contents 44 |
  • features
  • 45 |
  • demos
  • 46 |
  • example
  • 47 |
  • concepts
  • 48 |
  • usage
  • 49 |
  • install
  • 50 |
  • inspiration
  • 51 |
    52 | 53 | 54 | ## features 55 | 56 | - **API is a data structure**: easy to understand and simple to extend 57 | - **functional**: methods, permissions, handlers are just functions, no magic 58 | - **fractal**: compose one API from many smaller APIs 59 | - **database-agnostic**: create API services on top of anything 60 | - **authentication**: identify who the current user is 61 | - **authorization**: permit what a user can do 62 | - **http stack**: same paradigm for http request handlers like front-end bundlers, blob stores, etc 63 | - [**omakse**](https://youtu.be/E99FnoYqoII): consistent flavoring with [pull streams](https://pull-streams.github.io) all the way down 64 | 65 | for a user interface complement, see [`inu`](https://github.com/ahdinosaur/inu) 66 | 67 | ## demos 68 | 69 | - [holodex/app](https://github.com/holodex/app): full-stack user directory app using [`inu`](https://github.com/ahdinosaur/inu), [`inux`](https://github.com/ahdinosaur/inux), and [`vas`](https://github.com/ahdinosaur/vas) 70 | 71 | *if you want to share anything using `vas`, add your thing here!* 72 | 73 | ## example 74 | 75 | ```js 76 | var vas = require('vas') 77 | var pull = vas.pull 78 | var values = require('object-values') 79 | 80 | var service = { 81 | name: 'things', 82 | manifest: { 83 | all: 'source', 84 | get: 'async' 85 | }, 86 | methods: function (server, config) { 87 | return { all, get } 88 | 89 | function all () { 90 | const things = values(config.data) 91 | return pull.values(things) 92 | } 93 | 94 | function get (id, cb) { 95 | cb(null, config.data[id]) 96 | } 97 | }, 98 | permissions: function (server, config) { 99 | return { get } 100 | 101 | function get (id) { 102 | if (id === 'nobody') { 103 | return new Error('nobody is not allowed.') 104 | } 105 | } 106 | }, 107 | handlers: function (server, config) { 108 | return [ 109 | function (req, res, next) { 110 | console.log('cookie:', req.headers.cookie) 111 | next() 112 | } 113 | ] 114 | } 115 | } 116 | 117 | // could also attach db connection, file descriptors, etc. 118 | var config = { 119 | data: { 120 | 1: 'human', 121 | 2: 'computer', 122 | 3: 'JavaScript' 123 | } 124 | } 125 | 126 | var port = 6000 127 | var url = `ws://localhost:${port}` 128 | var server = vas.listen(service, config, { port }) 129 | var client = vas.connect(service, config, { url }) 130 | 131 | client.things.get(1, (err, value) => { 132 | if(err) throw err 133 | console.log('get', value) 134 | // get human 135 | }) 136 | 137 | pull( 138 | client.things.all(), 139 | pull.drain(v => console.log('all', v)) 140 | ) 141 | // all human 142 | // all computer 143 | // all JavaScript 144 | 145 | setTimeout(function () { 146 | server.close() 147 | client.close() 148 | }, 1000) 149 | ``` 150 | 151 | for a more complete example, see [./example](./example), which you can run with `npm run example` and query using command-line using `npm run example:cli -- things.find`. 152 | 153 | ## concepts 154 | 155 | let's say we're writing a todo app (so lame right). 156 | 157 | we want to be able to get all the todo items, update a todo item, and add another one. 158 | 159 | if we think of these _methods_ as functions, it might look like this (using [knex](http://knexjs.org/)): 160 | 161 | ```js 162 | const toPull = require('stream-to-pull-stream') 163 | const Db = require('knex') 164 | 165 | const db = Db({ 166 | client: 'sqlite3', 167 | connection: { 168 | filename: './mydb.sqlite' 169 | } 170 | }) 171 | 172 | const methods = { 173 | getAll, 174 | update, 175 | add 176 | } 177 | 178 | function getAll () { 179 | return toPull(db('todos').select().stream()) 180 | } 181 | 182 | function update (nextTodo, cb) { 183 | db('todos') 184 | .where('id', nextTodo.id) 185 | .update(nextTodo) 186 | .asCallback(cb) 187 | } 188 | 189 | function add (todo, cb) { 190 | db('todos').insert(todo).asCallback(cb) 191 | } 192 | ``` 193 | 194 | what if we could call these functions directly from the front-end? 195 | 196 | to do so, we need to specify which functions are available and of what type they are, which is called a _manifest_. 197 | 198 | ```js 199 | const manifest = { 200 | getAll: 'source', 201 | update: 'async', 202 | add: 'async' 203 | } 204 | ``` 205 | 206 | where 'source' corresponds to a [source pull stream](https://github.com/pull-stream/pull-stream) and 'async' corresponds to a function that receives an error-first callback. 207 | 208 | this manifest provides us with enough information to construct a mirrored function on the client: 209 | 210 | ```js 211 | pull( 212 | getAll(), 213 | pull.log() 214 | ) 215 | ``` 216 | 217 | together, this could become a _service_, complete with a name and version: 218 | 219 | ```js 220 | const service = { 221 | name: 'todos', 222 | version: '1.0.0', 223 | manfest, 224 | methods 225 | } 226 | ``` 227 | 228 | what if we had multiple services that need to share some configuration, such as a single database connection? 229 | 230 | to do so, we want to pass a _config_ object to the service methods, in particular a function that receives the config and returns the method functions. 231 | 232 | combine these concepts together and welcome to `vas`. :) 233 | 234 | ## usage 235 | 236 | a `vas` service is a definition for a duplex stream that responds to requests. 237 | 238 | a `vas` service is defined by an object with the following keys: 239 | 240 | - `name`: a string name 241 | - `version` (optional): a string semantic version 242 | - `manifest`: an object [muxrpc manifest](https://github.com/ssbc/muxrpc#manifest) 243 | - `methods`: a `methods(server, config)` pure function that returns an object of method functions to pass into [`muxrpc`](https://github.com/ssbc/muxrpc) 244 | - `permissions`: a `permissions(server, config)` pure function that returns an object of permission functions which correspond to methods. each permission function accepts the same arguments as the method and can return an optional `new Error(...)` if the method should not be called. 245 | - `handlers` a `handlers(server, config)` pure function that returns an array of http request handler functions, each of shape `(req, res, next) => { next() }`. 246 | - `authenticate`: a `authenticate(server, config)` pure function that returns an authentication function, of shape `(req, cb) => cb(err, id)`. only the first `authenticate` function will be used for a given set of services. the `id` returned by `authenticate` will be available as `this.id` in method or permission functions and `req.id` in handler functions. 247 | - `services`: any recursive sub-services 248 | 249 | many `vas` services can refer to a single service or an `Array` of services 250 | 251 | ### `vas = require('vas')` 252 | 253 | the top-level `vas` module is a grab bag of all `vas/*` modules. 254 | 255 | you can also require each module separately like `require('vas/listen')`. 256 | 257 | ### `vas.listen(services, config, options)` 258 | 259 | creates a server with `createServer(services, config)`, then 260 | 261 | listens to a port and begins to handle requests from clients using [`pull-ws-server`](https://github.com/pull-stream/pull-ws-server) 262 | 263 | `options` is an object with the following (optional) keys: 264 | 265 | - `port`: port to open WebSocket server 266 | - `onListen`: function to call once server is listening, receives `(err, httpServer, wsServer)`. 267 | - `createHttpServer`: function to create http server, of shape `(handlers) => server`. default is `(handlers) => http.createServer(Stack(...handlers))` 268 | - `serialize`: a duplex pull stream to stringify and parse json objects being sent to and from methods 269 | 270 | ### `vas.connect(client, config, options)` 271 | 272 | creates a client with `createClient(services, config)`, then 273 | 274 | connects the client to a server over websockets using [`pull-ws-server`](https://github.com/pull-stream/pull-ws-server) 275 | 276 | `options` is an object with the following (optional) keys: 277 | 278 | - `url`: string or [object](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) to refer to WebSocket server 279 | - `onConnect`: function to call once client is connected 280 | - `serialize`: a duplex pull stream to stringify and parse json objects being sent to and from methods 281 | 282 | ### `vas.command(services, config, options, argv)` 283 | 284 | run a command on a server as a command-line interface using [`muxrpcli`](https://github.com/ssbc/muxrpcli) 285 | 286 | `options` are either those passed to `vas.listen` or `vas.connect`, depending on if `argv[0] === 'server'` 287 | 288 | `argv` is expected to be `process.argv`. 289 | 290 | --- 291 | 292 | ### `server = vas.createServer(services, config, options)` 293 | 294 | a `vas` server is an instantiation of a service that responds to requests. 295 | 296 | `createServer` returns an object that corresponds to the (recursive) services and respective methods returned by `methods`. 297 | 298 | `options` is an object with the following (optional) keys: 299 | 300 | - `serialize`: a duplex pull stream to stringify and parse json objects being sent to and from methods 301 | 302 | ### `client = vas.createClient(services, config, options)` 303 | 304 | a `vas` client is a composition of manifests to makes requests. 305 | 306 | `createClient` returns an object that corresponds to the (recursive) services and respective methods in `manifest`. 307 | 308 | `options` is an object with the following (optional) keys: 309 | 310 | - `serialize`: a duplex pull stream to stringify and parse json objects being sent to and from methods 311 | 312 | ### `server.createStream(id)` 313 | ### `client.createStream(id)` 314 | 315 | returns a [duplex pull stream](https://github.com/dominictarr/pull-stream-examples/blob/master/duplex.js) using [`muxrpc`](https://github.com/ssbc/muxrpc) 316 | 317 | for a server, if `id` is passed in, will bind each method or permission function with `id` as `this.id`. 318 | 319 | ## frequently asked questions (FAQ) 320 | 321 | ### how to reduce browser bundles 322 | 323 | by design, service definitions are re-used between client and server creations. 324 | 325 | this leads to all the server code being included in the browser, when really we only need the service names and manifests to create the client. 326 | 327 | to reduce our bundles to only this information (eliminating any `require` calls or other bloat in our service files), use the [`evalify`](https://github.com/ahdinosaur/evalify) browserify transform. 328 | 329 | to [`evalify`](https://github.com/ahdinosaur/evalify) only service files, where service files are always named `service.js`, install `evalify` and add the following to your `package.json` 330 | 331 | ```json 332 | { 333 | "browserify": { 334 | "transform": [ 335 | ["evalify", { "files": ["**/service.js"] } ] 336 | ] 337 | } 338 | } 339 | ``` 340 | 341 | ### how to do authentication 342 | 343 | authentication is answers the question of _who you are_. 344 | 345 | here's an example of how to do this in `vas`, stolen stolen from [`holodex/app/dex/user/service`](https://github.com/holodex/app/blob/master/dex/user/service.js): 346 | 347 | (where `config.tickets` corresponds to an instance of [`ticket-auth`](https://github.com/dominictarr/ticket-auth)) 348 | 349 | ```js 350 | const Route = require('http-routes') 351 | 352 | const service = { 353 | name: 'user', 354 | manifest: { 355 | whoami: 'sync' 356 | }, 357 | authenticate: function (server, config) { 358 | return (req, cb) => { 359 | config.tickets.check(req.headers.cookie, cb) 360 | } 361 | }, 362 | methods: function (server, config) { 363 | return { whoami } 364 | 365 | function whoami () { 366 | return this.id 367 | } 368 | }, 369 | handlers: (server, config) => { 370 | return [ 371 | Route([ 372 | // redeem a user ticket at /login/ and set cookie. 373 | ['login/:ticket', function (req, res, next) { 374 | config.tickets.redeem(req.params.ticket, function (err, cookie) { 375 | if(err) return next(err) 376 | // ticket is redeemed! set it as a cookie, 377 | res.setHeader('Set-Cookie', cookie) 378 | res.setHeader('Location', '/') // redirect to the login page. 379 | res.statusCode = 303 380 | res.end() 381 | }) 382 | }], 383 | // clear cookie. 384 | ['logout', function (req, res, next) { 385 | res.setHeader('Set-Cookie', 'cookie=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;') 386 | res.setHeader('Location', '/') // redirect to the login page. 387 | res.statusCode = 303 388 | res.end() 389 | }], 390 | // return current user. (for debugging) 391 | ['whoami', function (req, res, next) { 392 | res.end(JSON.stringify(req.id) + '\n') 393 | }] 394 | ]) 395 | ] 396 | } 397 | } 398 | ``` 399 | 400 | ## install 401 | 402 | ```shell 403 | npm install --save vas 404 | ``` 405 | 406 | ## inspiration 407 | 408 | - [`big`](https://jfhbrook.github.io/2013/05/28/the-case-for-a-nodejs-framework.html) 409 | - [`feathers`](http://feathersjs.com/) 410 | - [`secret-stack`](https://github.com/ssbc/secret-stack) 411 | 412 | ## license 413 | 414 | The Apache License 415 | 416 | Copyright © 2016 Michael Williams 417 | 418 | Licensed under the Apache License, Version 2.0 (the "License"); 419 | you may not use this file except in compliance with the License. 420 | You may obtain a copy of the License at 421 | 422 | http://www.apache.org/licenses/LICENSE-2.0 423 | 424 | Unless required by applicable law or agreed to in writing, software 425 | distributed under the License is distributed on an "AS IS" BASIS, 426 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 427 | See the License for the specific language governing permissions and 428 | limitations under the License. 429 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createClient: require('./createClient'), 3 | connect: require('./connect'), 4 | pull: require('pull-stream') 5 | } 6 | -------------------------------------------------------------------------------- /command.js: -------------------------------------------------------------------------------- 1 | const Url = require('url') 2 | const muxrpcli = require('muxrpcli') 3 | const defined = require('defined') 4 | 5 | const listen = require('./listen') 6 | const connect = require('./connect') 7 | 8 | module.exports = command 9 | 10 | function command (services, config, options, argv) { 11 | options = defined(options, {}) 12 | argv = defined(argv, process.argv) 13 | 14 | const args = argv.slice(2) 15 | if (args[0] === 'server') { 16 | // special server command: 17 | // start the server 18 | options.onListen = onListen 19 | return listen(services, config, options) 20 | } else { 21 | // normal command: 22 | // create a client connection to the server 23 | options.onConnect = onConnect 24 | var client = connect(services, config, options) 25 | return client 26 | } 27 | 28 | function onListen (err) { 29 | if (err) throw err 30 | const url = (typeof options.url === 'string' 31 | ? options.url 32 | : Url.format(options.url) 33 | ) || `ws://localhost:${options.port}` 34 | 35 | console.log(`server listening at ${url}`) 36 | } 37 | 38 | function onConnect (err, ws) { 39 | if (err) { 40 | if (err.code === 'ECONNREFUSED') { 41 | console.log(`Error: Could not connect to the server at ${err.target.url}.`) 42 | console.log('Use the "server" command to start it.') 43 | if (options.verbose) throw err 44 | process.exit(1) 45 | } 46 | throw err 47 | } 48 | 49 | // run commandline flow 50 | muxrpcli(args, client.manifest, client, options.verbose) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /connect.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var Ws = require('pull-ws/client') 3 | var Url = require('url') 4 | var defined = require('defined') 5 | 6 | var createClient = require('./createClient') 7 | 8 | module.exports = connect 9 | 10 | function connect (services, config, options) { 11 | options = defined(options, {}) 12 | 13 | var url = defined(options.url, '/') 14 | var onConnect = options.onConnect 15 | 16 | var client = createClient(services, config, options) 17 | var stream = Ws.connect( 18 | getUrl(url), 19 | onConnect 20 | ) 21 | 22 | pull( 23 | stream, 24 | client.createStream(), 25 | stream 26 | ) 27 | 28 | return client 29 | } 30 | 31 | function getUrl (url) { 32 | return typeof url === 'string' 33 | ? url : Url.format(url) 34 | } 35 | -------------------------------------------------------------------------------- /createClient.js: -------------------------------------------------------------------------------- 1 | var muxrpc = require('muxrpc') 2 | var setIn = require('set-in') 3 | var defined = require('defined') 4 | 5 | var defaultSerialize = require('./serialize') 6 | var walk = require('./walk') 7 | 8 | module.exports = createClient 9 | 10 | function createClient (services, config, options) { 11 | services = defined(services, []) 12 | config = defined(config, {}) 13 | options = defined(options, {}) 14 | 15 | var serialize = defined(options.serialize, defaultSerialize) 16 | 17 | var manifest = {} 18 | 19 | walk(services, function (service, path) { 20 | // merge manifest 21 | setIn(manifest, path, service.manifest) 22 | }) 23 | 24 | var client = muxrpc(manifest, null, serialize)() 25 | 26 | client.manifest = manifest 27 | 28 | return client 29 | } 30 | -------------------------------------------------------------------------------- /createServer.js: -------------------------------------------------------------------------------- 1 | const muxrpc = require('muxrpc') 2 | const setIn = require('set-in') 3 | const getIn = require('get-in') 4 | const defined = require('defined') 5 | 6 | const defaultSerialize = require('./serialize') 7 | const walk = require('./walk') 8 | 9 | module.exports = createServer 10 | 11 | function createServer (services, config, options) { 12 | services = defined(services, []) 13 | config = defined(config, {}) 14 | options = defined(options, {}) 15 | 16 | const serialize = defined(options.serialize, defaultSerialize) 17 | 18 | var server = { 19 | manifest: {}, 20 | permissions: {}, 21 | methods: {}, 22 | handlers: [], 23 | createStream 24 | } 25 | 26 | walk(services, function (service, path) { 27 | // merge manifest 28 | setIn(server.manifest, path, service.manifest) 29 | // merge methods by calling service.init(service, config) 30 | setIn(server.methods, path, service.methods && service.methods(server, config)) 31 | // merge permissions 32 | setIn(server.permissions, path, service.permissions && service.permissions(server, config)) 33 | // merge http handlers 34 | if (service.handlers) server.handlers = server.handlers.concat(service.handlers(server, config)) 35 | if (!server.authenticate && service.authenticate) server.authenticate = service.authenticate(server, config) 36 | }) 37 | 38 | if (!server.authenticate) server.authenticate = defaultAuthenticate 39 | 40 | return server 41 | 42 | function createStream (id) { 43 | return createRpc(id).stream 44 | } 45 | 46 | function createRpc (id) { 47 | return muxrpc(server.manifest, server.manifest, server.methods, id, permission, serialize) 48 | } 49 | 50 | function permission (name, args) { 51 | const perm = getIn(server.permissions, name) 52 | return perm != null ? perm.apply(this, args) : null 53 | } 54 | } 55 | 56 | function defaultAuthenticate (req, cb) { 57 | cb(null, null) 58 | } 59 | -------------------------------------------------------------------------------- /example/browser.js: -------------------------------------------------------------------------------- 1 | const vas = require('../') 2 | const pull = vas.pull 3 | 4 | const services = require('./services') 5 | const config = require('./config') 6 | 7 | const url = config.url 8 | const client = vas.connect(services, config, { url }) 9 | 10 | console.log('client', client) 11 | 12 | pull( 13 | client.things.find(), 14 | pull.drain(function (thing) { 15 | console.log('thing', thing) 16 | }) 17 | ) 18 | -------------------------------------------------------------------------------- /example/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const vas = require('../') 4 | 5 | const services = require('./services') 6 | const config = require('./config') 7 | 8 | const options = { 9 | port: config.port, 10 | url: config.url 11 | } 12 | vas.command(services, config, options, process.argv) 13 | -------------------------------------------------------------------------------- /example/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: __dirname, 3 | data: { 4 | things: { 5 | 1: { 6 | id: 1, 7 | name: 'desk', 8 | description: 'clean and tidy, wait just kidding.' 9 | }, 10 | 2: { 11 | id: 1, 12 | name: 'vas', 13 | description: 'want continuous improvement, but need help.' 14 | } 15 | } 16 | }, 17 | port: 5000, 18 | url: { 19 | protocol: 'ws', 20 | hostname: 'localhost', 21 | port: 5000 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vas 6 | 7 | 8 | 9 |
    10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const vas = require('../') 2 | 3 | const services = require('./services') 4 | const config = require('./config') 5 | 6 | const port = config.port 7 | vas.listen(services, config, { port }) 8 | 9 | console.log(`server listening on port ${port}.`) 10 | -------------------------------------------------------------------------------- /example/services/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./things'), 3 | require('./static') 4 | ] 5 | -------------------------------------------------------------------------------- /example/services/static.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { join } = require('path') 3 | const browserify = require('browserify') 4 | 5 | module.exports = { 6 | name: 'static', 7 | version: '1.0.0', 8 | handlers: function (server, config) { 9 | return [ 10 | function (req, res, next) { 11 | if (req.url === '/bundle.js') { 12 | const entry = join(config.root, 'browser.js') 13 | browserify(entry) 14 | .transform('evalify', { files: '**/services/*.js' }) 15 | .bundle() 16 | .pipe(res) 17 | } else next() 18 | }, 19 | function (req, res, next) { 20 | if (req.url === '/') { 21 | const index = join(config.root, 'index.html') 22 | fs.createReadStream(index) 23 | .pipe(res) 24 | } 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/services/things.js: -------------------------------------------------------------------------------- 1 | const pull = require('../../').pull 2 | 3 | module.exports = { 4 | name: 'things', 5 | version: '1.0.0', 6 | manifest: { 7 | find: 'source' 8 | }, 9 | methods: function (server, config) { 10 | return { find: find } 11 | 12 | function find () { 13 | const things = values(config.data.things) 14 | return pull.values(things) 15 | } 16 | } 17 | } 18 | 19 | function values (obj) { 20 | return Object.keys(obj) 21 | .reduce((sofar, key) => { 22 | return sofar.concat([obj[key]]) 23 | }, []) 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createServer: require('./createServer'), 3 | createClient: require('./createClient'), 4 | listen: require('./listen'), 5 | connect: require('./connect'), 6 | command: require('./command'), 7 | pull: require('pull-stream') 8 | } 9 | -------------------------------------------------------------------------------- /listen.js: -------------------------------------------------------------------------------- 1 | const pull = require('pull-stream') 2 | const Ws = require('pull-ws') 3 | const defined = require('defined') 4 | const http = require('http') 5 | const Stack = require('stack') 6 | 7 | const createServer = require('./createServer') 8 | 9 | const DEFAULT_PORT = 5000 10 | 11 | module.exports = listen 12 | 13 | function listen (api, config, options) { 14 | options = defined(options, {}) 15 | 16 | const port = defined(options.port, DEFAULT_PORT) 17 | const createHttpServer = defined(options.createHttpServer, defaultCreateHttpServer) 18 | const onListen = options.onListen 19 | 20 | const server = createServer(api, config, options) 21 | 22 | const handlers = [ 23 | (req, res, next) => { 24 | server.authenticate(req, (err, id) => { 25 | if (err) console.error(err) // should we handle this error? 26 | req.id = id; next() 27 | }) 28 | } 29 | ].concat(server.handlers) 30 | 31 | const httpServer = createHttpServer(handlers, config) 32 | 33 | const ws = Ws.createServer( 34 | Object.assign({ server: httpServer }, options), 35 | function onConnect (ws) { 36 | server.authenticate(ws, (err, id) => { 37 | if (err) console.error(err) // should we handle this error? 38 | pull(ws, server.createStream(id), ws) 39 | }) 40 | } 41 | ) 42 | 43 | return ws.listen(port, function (err) { 44 | onListen(err, httpServer, ws) 45 | }) 46 | } 47 | 48 | function defaultCreateHttpServer (handlers, config) { 49 | return http.createServer(Stack.apply(null, handlers)) 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vas", 3 | "version": "2.1.0", 4 | "description": "simple composable data services using muxrpc", 5 | "main": "index.js", 6 | "browser": "browser.js", 7 | "scripts": { 8 | "test:deps": "dependency-check . && dependency-check . --extra --no-dev", 9 | "test:lint": "standard", 10 | "test:node": "NODE_ENV=test tape test/*.js", 11 | "test:coverage": "NODE_ENV=test istanbul cover test", 12 | "test": "npm-run-all -s test:deps test:lint test:node", 13 | "example:server": "node-dev example/server.js", 14 | "example:cli": "node-dev example/cli", 15 | "example": "npm-run-all -p example:server", 16 | "bundle": "browserify example -g envify -g uglifyify", 17 | "build": "npm run --silent bundle -- -o example/bundle.js", 18 | "disc": "npm run --silent bundle -- --full-paths | discify --open" 19 | }, 20 | "browserify": { 21 | "transform": [] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ahdinosaur/vas.git" 26 | }, 27 | "keywords": [ 28 | "simple", 29 | "composable", 30 | "data", 31 | "service", 32 | "micro", 33 | "muxrpc", 34 | "stream", 35 | "pull", 36 | "pull-stream" 37 | ], 38 | "author": "Mikey (http://dinosaur.is)", 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/ahdinosaur/vas/issues" 42 | }, 43 | "homepage": "https://github.com/ahdinosaur/vas#readme", 44 | "devDependencies": { 45 | "browserify": "^13.0.0", 46 | "dependency-check": "^2.5.1", 47 | "disc": "^1.3.2", 48 | "envify": "^3.4.0", 49 | "evalify": "^2.0.0", 50 | "gh-pages": "^0.11.0", 51 | "istanbul": "^0.4.4", 52 | "node-dev": "^3.1.3", 53 | "npm-run-all": "^1.6.0", 54 | "standard": "^7.1.2", 55 | "tape": "^4.6.0", 56 | "uglifyify": "^3.0.1", 57 | "watchify": "^3.7.0" 58 | }, 59 | "dependencies": { 60 | "defined": "^1.0.0", 61 | "get-in": "^2.0.0", 62 | "muxrpc": "^6.3.3", 63 | "muxrpcli": "^1.0.5", 64 | "pull-serializer": "^0.3.2", 65 | "pull-stream": "^3.4.3", 66 | "pull-ws": "^3.2.3", 67 | "set-in": "^2.0.0", 68 | "stack": "^0.1.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /serialize.js: -------------------------------------------------------------------------------- 1 | var Serializer = require('pull-serializer') 2 | 3 | module.exports = serialize 4 | 5 | function serialize (stream) { 6 | return Serializer(stream, JSON, { split: '\n\n' }) 7 | } 8 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | var vas = require('../') 4 | var pull = vas.pull 5 | 6 | test('can create a client and server streams', function (t) { 7 | var expected = ['Timmy', 'Bob'] 8 | var service = { 9 | name: 'people', 10 | version: '0.0.0', 11 | permissions: function (path, args) {}, 12 | manifest: { 13 | find: 'source' 14 | }, 15 | methods: function (server, config) { 16 | return { find } 17 | 18 | function find () { 19 | return pull.values(expected) 20 | } 21 | } 22 | } 23 | 24 | var client = vas.createClient(service, {}) 25 | var server = vas.createServer(service, {}) 26 | 27 | var clientStream = client.createStream() 28 | var serverStream = server.createStream() 29 | 30 | pull( 31 | clientStream, 32 | serverStream, 33 | clientStream 34 | ) 35 | 36 | pull( 37 | client.people.find(), 38 | pull.collect(function (err, arr) { 39 | t.error(err) 40 | t.deepEqual(arr, expected) 41 | t.end() 42 | }) 43 | ) 44 | }) 45 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./basic') 2 | require('./multipleServices') 3 | require('./nestedServices') 4 | -------------------------------------------------------------------------------- /test/multipleServices.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | var vas = require('../') 4 | var pull = vas.pull 5 | 6 | test('can create client and server streams with multiple services', function (t) { 7 | t.plan(4) 8 | var expectedPeople = ['Timmy', 'Bob'] 9 | var expectedCats = ['Fluffy', 'Meow'] 10 | var services = [ 11 | { 12 | name: 'cats', 13 | version: '0.0.0', 14 | permissions: function (path, args) {}, 15 | manifest: { 16 | find: 'source' 17 | }, 18 | methods: function (server, config) { 19 | return { find } 20 | 21 | function find () { 22 | return pull.values(expectedCats) 23 | } 24 | } 25 | }, 26 | { 27 | name: 'people', 28 | version: '0.0.0', 29 | permissions: function (path, args) {}, 30 | manifest: { 31 | find: 'source' 32 | }, 33 | methods: function (server, config) { 34 | return { find } 35 | 36 | function find () { 37 | return pull.values(expectedPeople) 38 | } 39 | } 40 | } 41 | ] 42 | 43 | var client = vas.createClient(services, {}) 44 | var server = vas.createServer(services, {}) 45 | 46 | var clientStream = client.createStream() 47 | var serverStream = server.createStream() 48 | 49 | pull( 50 | clientStream, 51 | serverStream, 52 | clientStream 53 | ) 54 | 55 | pull( 56 | client.people.find(), 57 | pull.collect(function (err, arr) { 58 | t.error(err) 59 | t.deepEqual(arr, expectedPeople) 60 | }) 61 | ) 62 | pull( 63 | client.cats.find(), 64 | pull.collect(function (err, arr) { 65 | t.error(err) 66 | t.deepEqual(arr, expectedCats) 67 | }) 68 | ) 69 | }) 70 | -------------------------------------------------------------------------------- /test/nestedServices.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | var vas = require('../') 4 | var pull = vas.pull 5 | 6 | test('can create client and server streams with nested services', function (t) { 7 | t.plan(4) 8 | var expectedPeople = ['Timmy', 'Bob'] 9 | var expectedCats = ['Fluffy', 'Meow'] 10 | var service = { 11 | name: 'cats', 12 | version: '0.0.0', 13 | permissions: function (path, args) {}, 14 | manifest: { 15 | find: 'source' 16 | }, 17 | methods: function (server, config) { 18 | return { find } 19 | 20 | function find () { 21 | return pull.values(expectedCats) 22 | } 23 | }, 24 | services: [{ 25 | name: 'people', 26 | version: '0.0.0', 27 | permissions: function (path, args) {}, 28 | manifest: { 29 | find: 'source' 30 | }, 31 | methods: function (server, config) { 32 | return { find } 33 | 34 | function find () { 35 | return pull.values(expectedPeople) 36 | } 37 | } 38 | }] 39 | } 40 | 41 | var client = vas.createClient(service, {}) 42 | var server = vas.createServer(service, {}) 43 | var clientStream = client.createStream() 44 | var serverStream = server.createStream() 45 | 46 | pull( 47 | clientStream, 48 | serverStream, 49 | clientStream 50 | ) 51 | 52 | pull( 53 | client.cats.find(), 54 | pull.collect(function (err, arr) { 55 | t.error(err) 56 | t.deepEqual(arr, expectedCats) 57 | }) 58 | ) 59 | pull( 60 | client.cats.people.find(), 61 | pull.collect(function (err, arr) { 62 | t.error(err) 63 | t.deepEqual(arr, expectedPeople) 64 | }) 65 | ) 66 | }) 67 | -------------------------------------------------------------------------------- /walk.js: -------------------------------------------------------------------------------- 1 | module.exports = walk 2 | 3 | function walk (services, cb, path) { 4 | path = path || [] 5 | 6 | if (!Array.isArray(services)) { 7 | services = [services] 8 | } 9 | 10 | services.forEach(function (service) { 11 | var name = service.name 12 | var servicePath = path.concat([name]) 13 | 14 | cb(service, servicePath) 15 | 16 | if (service.services) { 17 | walk(service.services, cb, servicePath) 18 | } 19 | }) 20 | } 21 | --------------------------------------------------------------------------------