├── .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 |
7 |
8 | vas
9 |
10 |
11 |
12 | :seedling: composable client/server data services using pull streams
13 |
14 |
15 |
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 |
--------------------------------------------------------------------------------