├── example
├── home
│ ├── public
│ │ ├── home
│ │ ├── main.css
│ │ ├── style.css
│ │ ├── model.js
│ │ ├── main.js
│ │ ├── index.html
│ │ └── config.js
│ ├── .gitignore
│ ├── view
│ │ ├── page.jade
│ │ └── index.jade
│ ├── package.json
│ └── index.js
├── io
│ ├── public
│ │ ├── io
│ │ ├── main.js
│ │ └── config.js
│ ├── view
│ │ └── index.jade
│ ├── package.json
│ └── index.js
├── balancer
│ ├── public
│ │ ├── main.js
│ │ └── style.css
│ ├── views
│ │ └── layout.jade
│ ├── server.js
│ └── package.json
├── .istanbul.yml
├── Dockerfile
├── coveralls.sh
├── account
│ ├── view
│ │ ├── hello.jade
│ │ └── login.jade
│ ├── passport.js
│ └── index.js
├── channel
│ ├── view
│ │ └── index.jade
│ ├── package.json
│ ├── index.js
│ ├── channel-b.js
│ ├── channel-a.js
│ └── public
│ │ ├── main.js
│ │ └── config.js
├── package.json
├── test
│ └── e2e
│ │ ├── nightwatch.json
│ │ └── index.js
├── LICENSE
├── rpc-adapters
│ ├── socket.io.js
│ ├── axon.js
│ └── zmq.js
├── Makefile
├── README.md
└── docker-compose.yml
├── .npmignore
├── doc
├── README.md
├── images
│ ├── 2-mixed.png
│ ├── 5-rpc.png
│ ├── 6-asset.png
│ ├── 1-components.png
│ ├── 3-middleware.png
│ └── 4-render-page.png
└── micromono-logo.png
├── .jshintignore
├── SUMMARY.md
├── lib
├── entrance
│ ├── index.js
│ ├── balancer.js
│ └── service.js
├── web
│ ├── asset
│ │ ├── index.js
│ │ ├── pipeline.js
│ │ ├── jspm.js
│ │ ├── bundle.js
│ │ └── pjson.js
│ ├── middleware
│ │ └── layout.js
│ ├── router.js
│ └── framework
│ │ └── express.js
├── config
│ ├── index.js
│ └── settings.js
├── service
│ ├── index.js
│ ├── announcement.js
│ ├── local.js
│ └── remote.js
├── pipeline
│ ├── channel.js
│ ├── balancer.js
│ └── service.js
├── logger.js
├── discovery
│ ├── pipe.js
│ ├── prober.js
│ ├── scheduler.js
│ ├── udp.js
│ ├── nats.js
│ └── index.js
├── server
│ ├── health.js
│ └── pipe.js
├── channel
│ ├── gateway.js
│ └── backend.js
├── api
│ ├── socketmq.js
│ └── rpc.js
└── index.js
├── .eslintignore
├── book.json
├── bin
├── micromono
└── micromono-bundle
├── .jsbeautifyrc
├── .jshintrc
├── .gitignore
├── .travis.yml
├── .eslintrc
├── .esformatter
├── LICENSE
├── package.json
└── HISTORY.md
/example/home/public/home:
--------------------------------------------------------------------------------
1 | ../public
--------------------------------------------------------------------------------
/example/io/public/io:
--------------------------------------------------------------------------------
1 | ../public
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | book.json
3 | example
4 | doc
5 |
--------------------------------------------------------------------------------
/example/balancer/public/main.js:
--------------------------------------------------------------------------------
1 | require('style.css!css')
2 |
--------------------------------------------------------------------------------
/example/home/public/main.css:
--------------------------------------------------------------------------------
1 | h1 {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/doc/README.md:
--------------------------------------------------------------------------------
1 | ## Table of Contents
2 |
3 | * [Read Me](/README.md)
--------------------------------------------------------------------------------
/example/balancer/public/style.css:
--------------------------------------------------------------------------------
1 | h1 {
2 | color: #6399fa;
3 | }
4 |
--------------------------------------------------------------------------------
/example/home/.gitignore:
--------------------------------------------------------------------------------
1 | public/*-bundle.*
2 | public/jspm_packages
3 |
--------------------------------------------------------------------------------
/example/home/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #999;
3 | }
4 |
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | example/server/public/config.js
4 |
--------------------------------------------------------------------------------
/doc/images/2-mixed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/2-mixed.png
--------------------------------------------------------------------------------
/doc/images/5-rpc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/5-rpc.png
--------------------------------------------------------------------------------
/doc/images/6-asset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/6-asset.png
--------------------------------------------------------------------------------
/doc/micromono-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/micromono-logo.png
--------------------------------------------------------------------------------
/doc/images/1-components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/1-components.png
--------------------------------------------------------------------------------
/doc/images/3-middleware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/3-middleware.png
--------------------------------------------------------------------------------
/doc/images/4-render-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lsm/micromono/HEAD/doc/images/4-render-page.png
--------------------------------------------------------------------------------
/example/.istanbul.yml:
--------------------------------------------------------------------------------
1 | instrumentation:
2 | root: /opt/micromono
3 | excludes: ['example/**', 'node_modules/**']
4 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | * [MicroMono](README.md)
4 | * [example](README.md#two_components_service_define_a_service)
5 |
6 |
--------------------------------------------------------------------------------
/example/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:4
2 |
3 |
4 | RUN npm --loglevel silent i -g istanbul jspm coveralls
5 |
6 | WORKDIR /opt
7 |
8 | EXPOSE 3000
9 |
--------------------------------------------------------------------------------
/lib/entrance/index.js:
--------------------------------------------------------------------------------
1 | exports.startBalancer = require('./balancer').startBalancer
2 | exports.startService = require('./service').startService
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 | coverage
3 | example/io/public/config.js
4 | example/home/public/config.js
5 | example/balancer/public/config.js
6 |
--------------------------------------------------------------------------------
/example/coveralls.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "repo_token: ${COVERALLS_REPO_TOKEN}" > ./.coveralls.yml
4 | # docker-compose run --rm micromono make docker-coveralls
5 | cat ./coverage/lcov.info | coveralls
6 |
--------------------------------------------------------------------------------
/lib/web/asset/index.js:
--------------------------------------------------------------------------------
1 | var jspm = require('./jspm')
2 | var pjson = require('./pjson')
3 | var bundle = require('./bundle')
4 | var assign = require('lodash.assign')
5 |
6 |
7 | module.exports = assign({}, jspm, pjson, bundle)
8 |
--------------------------------------------------------------------------------
/example/account/view/hello.jade:
--------------------------------------------------------------------------------
1 | h2 /account/protected
2 | h3 Hello #{name}, you can not see this page unless you have logged in successfully.
3 | h4 Click to
4 | form(action="/logout", method="POST")
5 | button(type="submit") Logout
6 |
--------------------------------------------------------------------------------
/example/home/public/model.js:
--------------------------------------------------------------------------------
1 | // var sync = require('ampersand-state');
2 | module.exports = function Model(name) {
3 | console.log('Hello %s', name);
4 | };
5 |
6 |
7 | // export default function Model() {
8 | // console.log(`Hello ${name}`);
9 | // }
10 |
--------------------------------------------------------------------------------
/example/home/view/page.jade:
--------------------------------------------------------------------------------
1 | h2 #{title}
2 | h3 Hello #{name}
3 | h4 User ID: #{id}
4 | h4 User password: #{password}
5 |
6 | This page rendered by #{method} request.
7 |
8 | form(action="/home/private-form" method="POST")
9 | label(for="key") Key
10 | input(name="key")
11 | input(type="submit" name="Submit")
12 |
--------------------------------------------------------------------------------
/example/home/public/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // import Model from 'model';
4 | var Model = require('home/model');
5 | require('home/style.css!');
6 | require('home/main.css!');
7 |
8 | new Model('john');
9 |
10 | var name = 'Bob';
11 | var time = 'today';
12 |
13 | console.log('Hello %s, how are you %s?', name, time);
14 |
--------------------------------------------------------------------------------
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitbook": "2.4.3",
3 | "structure": {
4 | "summary": "doc/README.md"
5 | },
6 | "plugins": ["edit-link", "prism", "-highlight"],
7 | "pluginsConfig": {
8 | "edit-link": {
9 | "base": "https://github.com/lsm/micromono/tree/master",
10 | "label": "Edit This Page"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/io/view/index.jade:
--------------------------------------------------------------------------------
1 | div
2 | h1 MicroMono Socket.IO Example
3 |
4 | h3 Message from server (emits every 2 seconds):
5 | div#message
6 |
7 | if asset && asset.bundleJs
8 | script(type='text/javascript', src="#{asset.bundleJs}")
9 |
10 | if asset && asset.main
11 | script(type='text/javascript').
12 | System.import('#{asset.main}');
13 |
--------------------------------------------------------------------------------
/example/account/view/login.jade:
--------------------------------------------------------------------------------
1 | h2 /account/login
2 |
3 | form(action="/account/login", method="POST")
4 | div
5 | label(for="username") Username (micromono) :
6 | input(type="text", name="username", value="micromono")
7 | div
8 | label Password (123456) :
9 | input(type="password", name="password" value="123456")
10 | div
11 | input(type="submit", value="Log In")
12 |
--------------------------------------------------------------------------------
/example/channel/view/index.jade:
--------------------------------------------------------------------------------
1 | div
2 | h1#sub-title MicroMono Channel Example
3 | h4 (open this page in multiple tabs/browsers to see how it works)
4 | h3 Message from server:
5 | div#message
6 |
7 | if asset && asset.bundleJs
8 | script(type='text/javascript', src="#{asset.bundleJs}")
9 |
10 | if asset && asset.main
11 | script(type='text/javascript').
12 | System.import('#{asset.main}');
13 |
--------------------------------------------------------------------------------
/bin/micromono:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var program = require('cmdenv')('micromono')
4 |
5 |
6 | program
7 | .version('0.2.0')
8 | .usage('[OPTIONS] COMMAND [ARG...]')
9 | .command('bundle [ARG...] [path]', 'Bundle asset files for given service path')
10 | // .command('balancer [options] path', 'Start a balancer from path')
11 | // .command('service [options] path', 'Start a service from path')
12 | .parse(process.argv)
13 |
--------------------------------------------------------------------------------
/.jsbeautifyrc:
--------------------------------------------------------------------------------
1 | {
2 | "indent_with_tabs": false,
3 | "max_preserve_newlines": 4,
4 | "preserve_newlines": true,
5 | "space_in_paren": false,
6 | "jslint_happy": false,
7 | "brace_style": "collapse",
8 | "keep_array_indentation": false,
9 | "keep_function_indentation": false,
10 | "eval_code": false,
11 | "unescape_strings": false,
12 | "break_chained_methods": false,
13 | "e4x": false,
14 | "wrap_line_length": 0
15 | }
16 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micromono-example",
3 | "dependencies": {
4 | "body-parser": "1.17.2",
5 | "connect": "3.6.2",
6 | "cookie-parser": "1.4.3",
7 | "engine.io": "3.1.0",
8 | "express": "4.15.3",
9 | "express-session": "1.15.3",
10 | "jade": "1.11.0",
11 | "jspm": "0.16.53",
12 | "nats": "0.7.20",
13 | "passport": "0.3.2",
14 | "passport-local": "1.0.0",
15 | "socket.io": "2.0.3"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/config/index.js:
--------------------------------------------------------------------------------
1 | var cmdenv = require('cmdenv')
2 | var settings = require('./settings')
3 |
4 | module.exports = function(configs) {
5 | var config = cmdenv('micromono').allowUnknownOption()
6 | configs.forEach(function(name) {
7 | var setting = settings[name]
8 | if (name) {
9 | setting.forEach(function(option) {
10 | config.option(option[0], option[1], option[2])
11 | })
12 | }
13 | })
14 | return config.parse(process.argv)
15 | }
16 |
--------------------------------------------------------------------------------
/example/home/view/index.jade:
--------------------------------------------------------------------------------
1 | h3 Index Page of Home Service
2 |
3 | ul
4 | li
5 | a(href="/home/private") Home Private Page
6 | li
7 | a(href="/public") Home Public Page
8 | li
9 | a(href="/account/protected") Account Protected Page
10 | li
11 | a(href="/account/login") Account Login Page
12 | li
13 | a(href="/project/param1/param2") Page with parameters
14 | li
15 | a(href="/io") Socket.IO exmaple
16 | li
17 | a(href="/channel") Channel exmaple
18 |
--------------------------------------------------------------------------------
/example/home/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 | hello world!
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example/io/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "io",
3 | "version": "1.0.0",
4 | "micromono": {
5 | "name": "io",
6 | "publicURL": "/public/io"
7 | },
8 | "jspm": {
9 | "main": "io/main.js",
10 | "directories": {
11 | "baseURL": "public"
12 | },
13 | "dependencies": {
14 | "socket.io-client": "npm:socket.io-client@1.4.8"
15 | },
16 | "devDependencies": {
17 | "babel": "npm:babel-core@5.8.34",
18 | "babel-runtime": "npm:babel-runtime@5.8.34",
19 | "core-js": "npm:core-js@1.2.6"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/example/test/e2e/nightwatch.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders" : ["."],
3 | "test_settings" : {
4 | "default" : {
5 | "launch_url": "https://balancer:3000",
6 | "selenium_host" : "hub",
7 | "desiredCapabilities": {
8 | "browserName": "chrome"
9 | },
10 | "screenshots" : {
11 | "enabled" : false,
12 | "on_failure" : false,
13 | "path" : "tests_output"
14 | }
15 | },
16 | "firefox" : {
17 | "desiredCapabilities": {
18 | "browserName": "firefox"
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/example/io/public/main.js:
--------------------------------------------------------------------------------
1 | var socket = require('socket.io-client')({
2 | path: '/io/example-socket',
3 | 'force new connection': true
4 | });
5 | socket.emit('message', 'first message');
6 |
7 | setInterval(function() {
8 | socket.emit('message', 'hello');
9 | }, 2000);
10 |
11 | socket.on('message', function(msg) {
12 | msg = new Date() + '
' + 'message from server: ' + msg;
13 | var el = document.getElementById('message');
14 | el.innerHTML += msg + '
';
15 | });
16 |
17 | socket.on('connect', function() {
18 | console.log('client connected');
19 | });
20 |
--------------------------------------------------------------------------------
/example/balancer/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 |
5 | if mainBundleCss
6 | link(type="text/css", rel="stylesheet", href="#{mainBundleCss}")
7 |
8 | script(src="/public/jspm_packages/system.js")
9 | script(src="/public/config.js")
10 |
11 | if (asset && !asset.bundleDeps) && mainBundleJs
12 | script(type="text/javascript", src="#{mainBundleJs}")
13 |
14 | body
15 | if mainEntryJs
16 | script(type='text/javascript').
17 | System.import('#{mainEntryJs}');
18 | h1
19 | a#title(href="/") MicroMono Example
20 |
21 | h2 #{name}
22 |
23 | div !{yield}
24 |
25 |
--------------------------------------------------------------------------------
/lib/service/index.js:
--------------------------------------------------------------------------------
1 | var util = require('util')
2 | var assign = require('lodash.assign')
3 |
4 |
5 | /**
6 | * Dummy constructor for defining service.
7 | */
8 | exports.Service = function MicroMonoService() {}
9 |
10 | /**
11 | * Create a service class from an object as its prototype.
12 | * @param {Object} serviceObj Prototype object of the service class.
13 | * @return {Service} Subclass of Service class.
14 | */
15 | exports.createService = function(serviceObj) {
16 | var Service = function() {}
17 | util.inherits(Service, exports.Service)
18 | assign(Service.prototype, serviceObj)
19 |
20 | return Service
21 | }
22 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "asi": true,
3 | "boss": false,
4 | "laxcomma": false,
5 | "loopfunc": false,
6 | "eqeqeq": true,
7 | "forin": true,
8 | "freeze": true,
9 | "immed": true,
10 | "latedef": "nofunc",
11 | "newcap": true,
12 | "nonew": false,
13 | "quotmark": "single",
14 | "noempty": true,
15 | "curly": false,
16 | "expr": true,
17 | "trailing": true,
18 | "undef": true,
19 | "unused": "vars",
20 | "white": true,
21 | "funcscope": false,
22 | "evil": false,
23 | "maxparams": 5,
24 | "maxdepth": 3,
25 | "strict": false,
26 | "browser": true,
27 | "node": true,
28 | "indent": 2,
29 | "esnext": true
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 | tests_output
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
28 | node_modules
29 | jspm_packages
30 | bundle-*
31 |
--------------------------------------------------------------------------------
/example/home/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "home",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "MicroMono",
10 | "license": "MIT",
11 | "micromono": {
12 | "name": "home",
13 | "publicURL": "/public/home"
14 | },
15 | "jspm": {
16 | "main": "home/main.js",
17 | "directories": {
18 | "baseURL": "public"
19 | },
20 | "dependencies": {
21 | "lodash.assign": "npm:lodash.assign@4.0.0"
22 | },
23 | "devDependencies": {
24 | "babel": "npm:babel-core@5.8.34",
25 | "babel-runtime": "npm:babel-runtime@5.8.34",
26 | "core-js": "npm:core-js@1.2.6"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/test/e2e/index.js:
--------------------------------------------------------------------------------
1 | var APP_URL = process.env.APP_URL
2 |
3 | module.exports = {
4 | ' Test home page': function(browser) {
5 | browser
6 | .url(APP_URL + '/')
7 |
8 | browser.expect.element('#title').text.to.equal('MicroMono Example').before(10000)
9 |
10 | browser
11 | .url(APP_URL + '/channel')
12 |
13 | browser.expect.element('#sub-title').text.to.equal('MicroMono Channel Example').before(10000)
14 | browser.pause(8000)
15 |
16 | // Teardown services so istanbul can generate reports
17 | browser.url(APP_URL + '/io/exit')
18 | browser.url(APP_URL + '/home/exit')
19 | browser.url(APP_URL + '/account/exit')
20 | browser.url(APP_URL + '/channel/exit')
21 | browser.url(APP_URL + '/balancer/exit')
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | services:
3 | - docker
4 |
5 | language: node_js
6 | node_js:
7 | - "6"
8 |
9 | install: npm i -g coveralls
10 |
11 | fast_finish: true
12 |
13 | # cache:
14 | # directories:
15 | # - node_modules
16 |
17 | env:
18 | DOCKER_COMPOSE_VERSION: 1.13.0
19 |
20 | before_install:
21 | - sudo rm /usr/local/bin/docker-compose
22 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
23 | - chmod +x docker-compose
24 | - sudo mv docker-compose /usr/local/bin
25 |
26 | script:
27 | - cd ./example
28 | - docker-compose pull
29 | - docker-compose build
30 | - make install
31 | - make e2e-ci
32 | # - make report
33 | # - 'sudo mv ../../micromono /opt/'
34 | # - make coveralls
35 |
--------------------------------------------------------------------------------
/example/channel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "channel",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "MIT",
11 | "micromono": {
12 | "name": "channel",
13 | "publicURL": "/public/channel"
14 | },
15 | "jspm": {
16 | "main": "channel/main.js",
17 | "directories": {
18 | "baseURL": "public"
19 | },
20 | "dependencies": {
21 | "engine.io-client": "npm:engine.io-client@1.6.11",
22 | "socketmq": "npm:socketmq@0.7.1"
23 | },
24 | "devDependencies": {
25 | "babel": "npm:babel-core@5.8.34",
26 | "babel-runtime": "npm:babel-runtime@5.8.34",
27 | "core-js": "npm:core-js@1.2.6"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/pipeline/channel.js:
--------------------------------------------------------------------------------
1 | var Superpipe = require('superpipe')
2 |
3 | exports.setupChannel = Superpipe.pipeline()
4 | .pipe('normalizeChannels?', 'channel', 'channels')
5 | .pipe('checkChannelPropertyName?', ['channels', 'service'])
6 | .pipe('createChannelAdapters?',
7 | ['channels', 'service'],
8 | ['chnBackend', 'chnAdapters'])
9 | .pipe('setupChannels?',
10 | ['channels', 'chnAdapters', 'initChannel', 'service', 'next'])
11 |
12 | exports.initChannel = Superpipe.pipeline()
13 | .pipe('setDefaultChannelHandlers?', 'channel')
14 | .pipe('bindChannelMethods?', ['channel', 'chnAdapter', 'service'])
15 | .pipe('buildJoinHook?', 'channel', 'chnJoinHook')
16 | .pipe('buildAllowHook?', 'channel', 'chnAllowHook')
17 | .pipe('attachChannelHooks?', ['chnAdapter', 'channel', 'chnJoinHook', 'chnAllowHook'])
18 | .pipe('attachEventHandlers?', ['chnAdapter', 'channel'], 'chnRepEvents')
19 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": [
4 | 2,
5 | 2,
6 | {"SwitchCase": 1}
7 | ],
8 | "quotes": [
9 | 2,
10 | "single"
11 | ],
12 | "linebreak-style": [
13 | 2,
14 | "unix"
15 | ],
16 | "semi": [
17 | 2,
18 | "never"
19 | ],
20 | "yoda": [2, "always", { "onlyEquality": true }],
21 | "curly": [
22 | 2,
23 | "multi-or-nest",
24 | "consistent"
25 | ]
26 | },
27 | "env": {
28 | "es6": true,
29 | "node": true,
30 | "browser": true
31 | },
32 | "extends": "eslint:recommended",
33 | "ecmaFeatures": {
34 | "jsx": true,
35 | "experimentalObjectRestSpread": true
36 | },
37 | "plugins": [
38 | "react"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/.esformatter:
--------------------------------------------------------------------------------
1 | {
2 | "indent": {
3 | "value": " ",
4 | "FunctionExpression": 1,
5 | "ArrayExpression": 1,
6 | "ObjectExpression": 1
7 | },
8 |
9 | "lineBreak": {
10 | "before": {
11 | "ElseStatement": 0,
12 | "ElseIfStatement": 0,
13 | "FunctionDeclaration": ">=1",
14 | "FunctionDeclarationOpeningBrace": 0,
15 | "FunctionDeclarationClosingBrace": ">=1",
16 | "FunctionExpressionOpeningBrace": 0
17 | },
18 | "after": {
19 | "FunctionDeclaration": ">=1",
20 | "FunctionDeclarationOpeningBrace": 1
21 | }
22 | },
23 |
24 | "whiteSpace": {
25 | "before": {
26 | "FunctionDeclaration": 1,
27 | "FunctionExpressionOpeningBrace": 1,
28 | "FunctionExpressionClosingBrace": 1
29 | },
30 | "after": {
31 | "FunctionName": 0,
32 | "FunctionExpressionOpeningBrace": 0,
33 | "FunctionExpressionClosingBrace": 0
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/balancer/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example of micromono server.
3 | */
4 |
5 | // setup express app
6 | var app = require('express')()
7 | app.set('views', __dirname + '/views')
8 | app.set('view engine', 'jade')
9 |
10 | app.get('/balancer/exit', function(req, res) {
11 | res.send('ok')
12 | setTimeout(function() {
13 | process.exit(0)
14 | }, 1000)
15 | })
16 |
17 | // Get a micromono instance.
18 | var micromono = require('/opt/micromono')
19 | micromono.set('MICROMONO_BUNDLE_DEV', undefined)
20 |
21 | // Boot the service(s) with an express app
22 | // do stuff in the callback.
23 | micromono.startBalancer(app, function(balancerAsset) {
24 | console.log('server booted')
25 |
26 | var assetInfo = balancerAsset.assetInfo
27 |
28 | if (assetInfo) {
29 | if (assetInfo.bundleJs)
30 | app.locals.mainBundleJs = assetInfo.bundleJs
31 |
32 | if (assetInfo.bundleCss)
33 | app.locals.mainBundleCss = assetInfo.bundleCss
34 |
35 | if (assetInfo.main)
36 | app.locals.mainEntryJs = assetInfo.main
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/example/account/passport.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module for setup passport with fake authentication
3 | */
4 |
5 | var passport = require('passport');
6 | var LocalStrategy = require('passport-local').Strategy;
7 |
8 | // User is hard coded for demostration purpose
9 | var user = {
10 | id: 1,
11 | username: 'micromono',
12 | password: '123456'
13 | };
14 |
15 | passport.serializeUser(function(user, done) {
16 | done(null, user.id);
17 | });
18 |
19 | passport.deserializeUser(function(id, done) {
20 | if (user.id === id) {
21 | done(null, user);
22 | } else {
23 | done('Wrong id');
24 | }
25 | });
26 |
27 | passport.use(new LocalStrategy({
28 | usernameField: 'username',
29 | passwordField: 'password'
30 | },
31 |
32 | function(username, password, done) {
33 | // check fake data for example
34 | if (username === user.username && password === user.password) {
35 | return done(null, user);
36 | } else {
37 | return done(null, false, {
38 | message: 'Username and passport do not match.'
39 | });
40 | }
41 | }));
42 |
43 | module.exports = passport;
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 lsm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 lsm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/example/balancer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "balancer",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "jspm": "jspm install"
9 | },
10 | "author": "",
11 | "license": "MIT",
12 | "micromono": {
13 | "publicURL": "/public",
14 | "commonBundles": {
15 | "common30": [
16 | "engine.io-client",
17 | "socketmq",
18 | "lodash.assign",
19 | "socket.io-client"
20 | ]
21 | }
22 | },
23 | "jspm": {
24 | "main": "main.js",
25 | "directories": {
26 | "baseURL": "public"
27 | },
28 | "dependencies": {
29 | "engine.io-client": "npm:engine.io-client@1.6.8",
30 | "lodash.assign": "npm:lodash.assign@4.0.0",
31 | "socket.io-client": "npm:socket.io-client@1.4.5",
32 | "socketmq": "npm:socketmq@0.7.1"
33 | },
34 | "devDependencies": {
35 | "babel": "npm:babel-core@5.8.34",
36 | "babel-runtime": "npm:babel-runtime@5.8.34",
37 | "core-js": "npm:core-js@1.2.6",
38 | "css": "github:systemjs/plugin-css@0.1.12"
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/example/channel/index.js:
--------------------------------------------------------------------------------
1 | var micromono = require('/opt/micromono')
2 | var channelA = require('./channel-a')
3 | var channelB = require('./channel-b')
4 |
5 | var IO = module.exports = {
6 | // Multiple channels
7 | channel: {
8 | '/channel/a': channelA,
9 | '/channel/b': channelB
10 | },
11 |
12 | // Single channel
13 | // channel: channelA,
14 |
15 | use: {
16 | // Tell micromono to use `layout` middleware at the balancer side
17 | // for request url matching `/channel$`.
18 | 'layout': '/channel$'
19 | },
20 |
21 | route: {
22 | '/channel': function(req, res) {
23 | res.render('index')
24 | },
25 | '/channel/exit': function(req, res) {
26 | res.send('ok')
27 | setTimeout(function() {
28 | process.exit(0)
29 | }, 1000)
30 | }
31 | },
32 |
33 | init: [function(app) {
34 | // setup express app
35 | app.set('views', __dirname + '/view')
36 | app.set('view engine', 'jade')
37 | }, ['app']]
38 | }
39 |
40 |
41 | // Start the service if this is the main file
42 | if (require.main === module) {
43 | micromono.startService(IO, function(httpPort) {
44 | console.log('local http port: %s', httpPort)
45 | }, ['httpPort'])
46 | }
47 |
--------------------------------------------------------------------------------
/lib/web/asset/pipeline.js:
--------------------------------------------------------------------------------
1 | var Superpipe = require('superpipe')
2 |
3 | exports.bundleAsset = Superpipe.pipeline()
4 | .pipe('getPackageJSON', 'packagePath', 'packageJSON')
5 | .pipe('getServiceInfo',
6 | ['packageJSON', 'service'],
7 | ['hasAsset', 'serviceName', 'serviceInfo', 'serviceVersion'])
8 | .pipe('getAssetInfo',
9 | ['packagePath', 'packageJSON', 'serviceName'],
10 | ['assetInfo', 'publicURL', 'publicPath'])
11 | .pipe('getJSPMBinPath', 'packagePath', 'jspmBinPath')
12 | .pipe('getJSPMConfig',
13 | ['assetInfo', 'publicPath', 'next'],
14 | ['jspmConfig', 'jspmConfigPath'])
15 | .pipe('prepareBundleInfo',
16 | ['assetInfo', 'publicPath', 'bundleOptions'],
17 | ['bundleCmd', 'bundleOptions'])
18 | .pipe('updateJSPMConfig', ['jspmConfigPath', 'jspmConfig', 'bundleOptions', 'next'])
19 | .pipe('bundle',
20 | ['assetInfo', 'packagePath', 'jspmBinPath', 'bundleCmd', 'bundleOptions', 'set'],
21 | ['bundleJs', 'bundleCss'])
22 | .pipe('getJSPMConfig',
23 | ['assetInfo', 'publicPath', 'next'],
24 | ['jspmConfig', 'jspmConfigPath'])
25 | .pipe('updateJSPMConfig', ['jspmConfigPath', 'jspmConfig', 'bundleOptions', 'next'])
26 | .pipe('updatePackageJSON', ['assetInfo', 'packagePath', 'packageJSON', 'next'])
27 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | var debug = require('debug')
2 | var LOG_LEVEL = process.env.MICROMONO_LOG_LEVEL || 'info'
3 | var DEFAULT_LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
4 |
5 | /**
6 | * Export constructor with default settings.
7 | * @type {Function}
8 | */
9 | exports = module.exports = createLogger(LOG_LEVEL, DEFAULT_LEVELS)
10 |
11 |
12 | /**
13 | * Export constructor creator for customized usage.
14 | * @type {Function}
15 | */
16 | exports.createLogger = createLogger
17 |
18 |
19 | /**
20 | * The constructor creator.
21 | * @param {String} DEFAULT_LEVEL Default level of logging if not supplied.
22 | * @param {Array} LEVELS Levels of logging.
23 | * @return {Logger} The Logger constructor.
24 | */
25 | function createLogger(DEFAULT_LEVEL, LEVELS) {
26 | LEVELS = LEVELS || DEFAULT_LEVELS
27 | return function Logger(namespace, level) {
28 | level = level || DEFAULT_LEVEL
29 | var levelIdx = LEVELS.indexOf(level)
30 | var logger = {}
31 |
32 | LEVELS.forEach(function(lv, idx) {
33 | if (levelIdx >= idx) {
34 | var ns = namespace ? (namespace + ':' + lv) : lv
35 | var log = debug(ns)
36 | logger[lv] = function() {
37 | log.apply(null, arguments)
38 | return logger
39 | }
40 | } else {
41 | logger[lv] = function() {
42 | return logger
43 | }
44 | }
45 | })
46 |
47 | return logger
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/discovery/pipe.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:discovery:pipe')
2 | var config = require('../config')
3 |
4 |
5 | exports.prepareDiscovery = function(discoveryOptions) {
6 | if (!discoveryOptions)
7 | discoveryOptions = config(['default', 'discovery'])
8 |
9 | var options = {}
10 | Object.keys(discoveryOptions).forEach(function(key) {
11 | if (/^MICROMONO_DISCOVERY/.test(key))
12 | options[key] = discoveryOptions[key]
13 | })
14 |
15 | var discovery = require('./' + options.MICROMONO_DISCOVERY_BACKEND)
16 |
17 | return {
18 | discoveryListen: discovery.listen,
19 | discoveryAnnounce: discovery.announce,
20 | discoveryOptions: options
21 | }
22 | }
23 |
24 | exports.listenProviders = function(services, discoveryListen, discoveryOptions, addProvider) {
25 | var remoteServices = Object.keys(services).filter(function(serviceName) {
26 | return true === services[serviceName].isRemote
27 | })
28 |
29 | if (0 < remoteServices.length) {
30 | logger.info('start listening remote service providers', {
31 | remoteServices: remoteServices
32 | })
33 | discoveryListen(discoveryOptions, function(err, ann) {
34 | if (err) {
35 | logger.error('Service discovery error', {
36 | error: err && err.stack || err,
37 | provider: ann
38 | })
39 | } else if (ann && -1 < remoteServices.indexOf(ann.name)) {
40 | addProvider(services[ann.name].scheduler, ann)
41 | }
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/example/io/index.js:
--------------------------------------------------------------------------------
1 | var micromono = require('/opt/micromono')
2 |
3 |
4 | var IO = module.exports = {
5 | upgradeUrl: '/io/example-socket',
6 |
7 | use: {
8 | // Tell micromono to use `layout` middleware at the balancer side
9 | // for request url matching `/io$`.
10 | 'layout': '/io$'
11 | },
12 |
13 | route: {
14 | '/io': function(req, res) {
15 | res.render('index')
16 | },
17 | '/io/exit': function(req, res) {
18 | res.send('ok')
19 | setTimeout(function() {
20 | process.exit(0)
21 | }, 1000)
22 | }
23 | },
24 |
25 | init: function(app, httpServer) {
26 | var socketPath = IO.upgradeUrl
27 | console.log('socket.io path', socketPath)
28 |
29 | // listen to the `server` event
30 | console.log('Please open http://127.0.0.1:3000/io in your browser (no trailing slash).')
31 | // setup socket.io with server
32 | var io = require('socket.io')(httpServer, {
33 | path: socketPath
34 | })
35 |
36 | io.on('connection', function(socket) {
37 | socket.on('message', function(msg) {
38 | console.log(new Date())
39 | console.log('client message: ', msg)
40 | socket.emit('message', msg)
41 | })
42 | })
43 |
44 | // setup express app
45 | app.set('views', __dirname + '/view')
46 | app.set('view engine', 'jade')
47 | }
48 | }
49 |
50 |
51 | // Start the service if this is the main file
52 | if (require.main === module) {
53 | micromono.startService(IO, function(httpPort) {
54 | console.log('local http port: %s', httpPort)
55 | }, ['httpPort'])
56 | }
57 |
--------------------------------------------------------------------------------
/example/channel/channel-b.js:
--------------------------------------------------------------------------------
1 | var util = require('util')
2 |
3 | module.exports = {
4 | auth: function(meta, next) {
5 | var cookie = meta.cookie
6 | var session = meta.session
7 | if (session && 'string' === typeof session) {
8 | session = JSON.parse(session)
9 | // Dencrypt session
10 | next(null, 'session', session)
11 | } else if (cookie) {
12 | // Auth client
13 | session = {
14 | uid: 1,
15 | sid: meta.sid
16 | }
17 | // Encrypt
18 | next(null, {
19 | ssn: JSON.stringify(session),
20 | session: session
21 | })
22 | }
23 | },
24 |
25 | join: function(session, channel, next) {
26 | console.log('join b', session, channel)
27 | next(null, {
28 | repEvents: ['hello:message', 'hello:reply'],
29 | subEvents: ['server:message']
30 | })
31 | },
32 |
33 | allow: function(session, channel, event, next) {
34 | console.log('allow /channel/b', channel, event);
35 | next()
36 | },
37 |
38 | 'hello:message': function(session, channel, msg) {
39 | this
40 | .getChannel('/channel/b')
41 | .pubChn(channel,
42 | 'server:message',
43 | 'message for everyone in namespace "/channel/b" channel ' + channel)
44 |
45 | var message = util.format('sid: %s
namespace: %s
channel:%s
',
46 | session.sid, '/channel/b', channel)
47 | this.chnBackend.channel('/channel/b', channel).pubSid(session.sid, 'server:message', message)
48 | },
49 |
50 | 'readFile': function(session, channel, filename, reply) {
51 | throw new Error('No one should be able to reach here.')
52 | },
53 | 'hello:reply': function(session, channel, msg, reply) {
54 | reply(null, 'Hi, how are you user ' + session.uid)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/discovery/prober.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Serivce discovery script. Used as proxy for probing network services
3 | * synchronously upon service startup. E.g. micromono.require('some-service')
4 | */
5 |
6 | /**
7 | * Module dependencies
8 | */
9 | var logger = require('../logger')('micromono:discovery:prober')
10 | var prepareDiscovery = require('./pipe').prepareDiscovery
11 |
12 | // Get service name and discovery options
13 | var discovery = prepareDiscovery()
14 | var options = discovery.discoveryOptions
15 | var backend = options.MICROMONO_DISCOVERY_BACKEND
16 | var timeout = options.MICROMONO_DISCOVERY_TIMEOUT || 90000
17 | var serviceName = options.MICROMONO_DISCOVERY_TARGET
18 |
19 | info()
20 |
21 | var Discovery = require('./' + backend)
22 |
23 | setInterval(info, 5000)
24 |
25 | Discovery.listen(options, function(err, data) {
26 | if (err) {
27 | logger.error('Discovering error', {
28 | data: data,
29 | error: err,
30 | service: serviceName
31 | }).debug(options)
32 | return
33 | }
34 | if (data && serviceName === data.name) {
35 | timer && clearTimeout(timer)
36 | process.stdout.write(JSON.stringify(data), function() {
37 | process.exit(0)
38 | })
39 | }
40 | })
41 |
42 | var timer = setTimeout(function() {
43 | process.stdout.write('Probing service [' + serviceName + '] timeout after ' + timeout / 1000 + ' seconds', function() {
44 | process.exit(1)
45 | })
46 | }, timeout)
47 |
48 | process.on('SIGINT', function() {
49 | timer && clearTimeout(timer)
50 | logger.info('Stop probing service', {
51 | backend: backend,
52 | service: serviceName
53 | }).debug(options)
54 | process.exit(255)
55 | })
56 |
57 | function info() {
58 | logger.info('Discovering service', {
59 | backend: backend,
60 | service: serviceName
61 | }).debug(options)
62 | }
63 |
--------------------------------------------------------------------------------
/example/channel/channel-a.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = {
4 | namespace: '/channel/a',
5 | auth: function(meta, next) {
6 | // console.log('auth', meta)
7 | var cookie = meta.cookie
8 | var session = meta.session
9 | if (session && 'string' === typeof session) {
10 | session = JSON.parse(session)
11 | // Dencrypt session
12 | next(null, 'session', session)
13 | } else if (cookie) {
14 | // Auth client
15 | session = {
16 | uid: 1,
17 | sid: meta.sid
18 | }
19 | // Encrypt
20 | next(null, {
21 | ssn: JSON.stringify(session),
22 | session: session
23 | })
24 | }
25 | },
26 |
27 | join: function(session, channel, next) {
28 | console.log('join a', session, channel)
29 | next(null, {
30 | repEvents: ['hello:message', 'hello:reply'],
31 | subEvents: ['server:message']
32 | })
33 | },
34 |
35 | allow: function(session, channel, event, next) {
36 | // console.log('allow', session, channel, event)
37 | next()
38 | },
39 |
40 | 'hello:message': function(session, channel, msg) {
41 | console.log('/channel/a hello:message', session, channel, msg)
42 | this.pub('/channel/a', channel, 'server:message', 'message for everyone in /channel/a ' + channel)
43 |
44 | this.chnBackend
45 | .channel('/channel/a', channel)
46 | .pubSid(session.sid, 'server:message',
47 | 'This message is only for sid: ' + session.sid + ' /channel/a ' + channel)
48 | },
49 |
50 | 'readFile': function(session, channel, filename, reply) {
51 | throw new Error('No one should be able to reach here.')
52 | },
53 | 'hello:reply': function(session, channel, msg, reply) {
54 | // console.log('hello:reply', session, channel, msg);
55 | reply(null, 'Hi, how are you user ' + session.uid)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/example/channel/public/main.js:
--------------------------------------------------------------------------------
1 | var socketmq = require('socketmq')
2 | var smq = socketmq()
3 |
4 | smq.on('message', function(msg) {
5 | console.log('onmessage', msg.toString())
6 | })
7 |
8 | var firstChannel = smq.channel('/channel/a', 'room x')
9 |
10 | firstChannel.sub('server:message', function(msg) {
11 | msg = '/channel/a: ' + new Date() + ' message from server:
' + msg
12 | var el = document.getElementById('message')
13 | el.innerHTML += msg + '
'
14 | })
15 |
16 | firstChannel.on('join', function() {
17 | console.log('"/channel/a" "room x" Joined')
18 | })
19 |
20 | firstChannel.req('hello:reply', 'Hi server', function(err, msg) {
21 | msg = '/channel/a: ' + new Date() + ' reply from server:
' + msg
22 | var el = document.getElementById('message')
23 | el.innerHTML += msg + '
'
24 | })
25 |
26 | setTimeout(function() {
27 | firstChannel.req('hello:message', 'message from client')
28 | }, 2000)
29 |
30 |
31 | var secondChannel = smq.channel('/channel/b', 'room y')
32 |
33 | secondChannel.sub('server:message', function(msg) {
34 | msg = '/channel/b: ' + new Date() + ' message from server:
' + msg
35 | var el = document.getElementById('message')
36 | el.innerHTML += msg + '
'
37 | })
38 |
39 | secondChannel.on('join', function() {
40 | console.log('"/channel/b" "room y" Joined')
41 | })
42 |
43 |
44 | secondChannel.req('hello:reply', 'Hi server', function(err, msg) {
45 | msg = '/channel/b: ' + new Date() + ' reply from server:
' + msg
46 | var el = document.getElementById('message')
47 | el.innerHTML += msg + '
'
48 | })
49 |
50 |
51 | setTimeout(function() {
52 | secondChannel.req('hello:message', 'message from client')
53 | }, 1000)
54 |
55 |
56 | smq.connect('eio://', function(stream) {
57 | console.log(stream);
58 | })
59 |
--------------------------------------------------------------------------------
/example/rpc-adapters/socket.io.js:
--------------------------------------------------------------------------------
1 | var debug = require('debug')('micromono:rpc:socketio')
2 |
3 |
4 | module.exports = {
5 | client: {
6 | send: function(data) {
7 | var self = this
8 | this.scheduleProvider(function(provider) {
9 | var socket = provider.socket
10 | var args = data.args
11 | var fn
12 |
13 | if (typeof args[args.length - 1] === 'function') {
14 | fn = args.pop()
15 | data.cid = true
16 | }
17 |
18 | var msg = self.encodeData(data)
19 | fn ? socket.emit('message', msg, fn) : socket.emit('message', msg)
20 | })
21 | },
22 |
23 | connect: function(provider) {
24 | var endpoint = 'http://' + provider.host + ':' + provider.rpc.port
25 | var socket = require('socket.io-client')(endpoint)
26 |
27 | var self = this
28 | socket.on('disconnect', function() {
29 | debug('socket.io provider %s disconnected', endpoint)
30 | self.onProviderDisconnect(provider)
31 | })
32 |
33 | provider.socket = socket
34 | }
35 | },
36 |
37 | server: {
38 |
39 | dispatch: function(msg, socket, callback) {
40 | var data = this.decodeData(msg)
41 | var args = data.args || []
42 | var handler = this.getHandler(data.name)
43 |
44 | if (data.cid === true) {
45 | args.push(callback)
46 | }
47 |
48 | handler.apply(this, args)
49 | },
50 |
51 | startRPCServer: function(port) {
52 | var ioServer = require('socket.io')()
53 | ioServer.serveClient(false)
54 | ioServer.listen(port)
55 |
56 | this.announcement.rpcPort = port
57 | this.announcement.rpcType = 'socket.io'
58 |
59 | var self = this
60 | ioServer.on('connection', function(socket) {
61 | socket.on('message', function(data, callback) {
62 | self.dispatch(data, socket, callback)
63 | })
64 | })
65 |
66 | return Promise.resolve()
67 | }
68 | }
69 |
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/example/Makefile:
--------------------------------------------------------------------------------
1 | NPM := npm --loglevel warn
2 | JSPM_INSTALL := /opt/node_modules/.bin/jspm i -y
3 | SERVICES = account balancer home io test
4 |
5 |
6 | sink:
7 | - docker rm -f `docker ps -qa`
8 |
9 | clean:
10 | @for file in * .* ; do\
11 | if [ -d "./$$file/node_modules" ]; then\
12 | dir=./$$file/node_modules;\
13 | echo "remove $$dir";\
14 | rm -rf $$dir; \
15 | fi;\
16 | if [ -d "./$$file/public/jspm_packages" ]; then\
17 | dir=./$$file/public/jspm_packages;\
18 | echo "remove $$dir";\
19 | rm -rf $$dir; \
20 | fi;\
21 | if [ -f "./$$file/public/config.js" ]; then\
22 | dir=./$$file/public/config.js;\
23 | echo "remove $$dir";\
24 | rm -rf $$dir; \
25 | fi;\
26 | done
27 |
28 | install:
29 | docker-compose run --rm installation make docker-install
30 |
31 | docker-install:
32 | cd /opt && $(NPM) i
33 | cd /opt/micromono && $(NPM) i
34 | cd /opt/home/ && $(JSPM_INSTALL)
35 | cd /opt/io/ && $(JSPM_INSTALL)
36 | cd /opt/balancer/ && $(JSPM_INSTALL)
37 | cd /opt/channel/ && $(JSPM_INSTALL)
38 |
39 | mono:
40 | DEBUG=micromono* node server/server.js --service-dir ./ --service account,home
41 |
42 | mono-io:
43 | DEBUG=micromono* node server/server.js --service-dir ./ --service io
44 |
45 | cluster:
46 | docker-compose run --publish 3000:3000 --rm balancer
47 |
48 | nats-cluster:
49 | docker-compose run --publish 3000:3000 --rm balancer-nats
50 |
51 |
52 | e2e:
53 | docker-compose up -d chromedebug
54 | sleep 60
55 | open vnc://test:secret@`docker-machine ip default`:5900
56 | docker-compose run --rm nightwatch --config ./nightwatch.json -e default
57 |
58 | e2e-ci:
59 | docker-compose up -d chrome
60 | sleep 60
61 | docker-compose logs &
62 | docker-compose run --rm nightwatch --config ./nightwatch.json -e default
63 |
64 | report:
65 | docker-compose run --rm micromono istanbul report
66 |
67 | coveralls:
68 | ls -al /opt/micromono
69 | chmod +x ./coveralls.sh
70 | ./coveralls.sh
71 |
72 | docker-coveralls:
73 | cat ./coverage/lcov.info | coveralls
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micromono",
3 | "version": "0.10.1",
4 | "description": "Monolithic micro-services framework",
5 | "main": "lib/index.js",
6 | "bin": {
7 | "micromono": "./bin/micromono"
8 | },
9 | "directories": {
10 | "example": "example",
11 | "lib": "lib"
12 | },
13 | "scripts": {
14 | "test": "npm test",
15 | "release": "npm run release-patch",
16 | "release-patch": "git checkout master && mversion patch -m \"%s\" -t \"%s\" && git push origin master --tags && npm publish",
17 | "release-minor": "git checkout master && mversion minor -m \"%s\" -t \"%s\" && git push origin master --tags && npm publish",
18 | "release-major": "git checkout master && mversion major -m \"%s\" -t \"%s\" && git push origin master --tags && npm publish"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/lsm/micromono.git"
23 | },
24 | "keywords": [
25 | "microservices",
26 | "service",
27 | "framework",
28 | "micro-services",
29 | "monolithic",
30 | "web",
31 | "rpc",
32 | "message",
33 | "websockets",
34 | "rest",
35 | "restful",
36 | "router",
37 | "app",
38 | "api"
39 | ],
40 | "author": "lsm ",
41 | "license": "MIT",
42 | "bugs": {
43 | "url": "https://github.com/lsm/micromono/issues"
44 | },
45 | "homepage": "https://github.com/lsm/micromono",
46 | "dependencies": {
47 | "callsite": "1.0.0",
48 | "cmdenv": "0.5.0",
49 | "debug": "2.6.8",
50 | "engine.io": "3.1.0",
51 | "express": "4.15.3",
52 | "http-proxy": "1.16.2",
53 | "ip": "1.1.5",
54 | "jdad": "0.2.0",
55 | "js-args-names": "0.0.2",
56 | "jspm": "0.16.53",
57 | "lodash.assign": "4.2.0",
58 | "lodash.difference": "4.5.0",
59 | "lodash.isplainobject": "4.0.6",
60 | "lodash.merge": "4.6.0",
61 | "lodash.toarray": "4.4.0",
62 | "lodash.union": "4.6.0",
63 | "msgpack-lite": "0.1.26",
64 | "socketmq": "0.10.1",
65 | "superpipe": "0.14.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/discovery/scheduler.js:
--------------------------------------------------------------------------------
1 | var assign = require('lodash.assign')
2 | var EventEmitter = require('events').EventEmitter
3 |
4 | /**
5 | * A simple roundrobin scheduler.
6 | *
7 | * @constructor
8 | * @return {Scheduler} Instance of scheduler.
9 | */
10 | var Scheduler = module.exports = function MicroMonoScheduler() {
11 | this._items = []
12 | this.n = 0
13 | }
14 |
15 | assign(Scheduler.prototype, EventEmitter.prototype)
16 |
17 | /**
18 | * Get number of items in this scheduler.
19 | *
20 | * @return {Number} Number of items available.
21 | */
22 | Scheduler.prototype.len = function() {
23 | return this._items.length
24 | }
25 |
26 | /**
27 | * Add an item to the scheduler.
28 | *
29 | * @param {Object} item An object represents some kind of resources.
30 | */
31 | Scheduler.prototype.add = function(item) {
32 | if (item) {
33 | this._items.push(item)
34 | this.emit('add', item)
35 | }
36 | }
37 |
38 | /**
39 | * Get an item from the scheduler.
40 | *
41 | * @return {Object} An object represents some kind of resources.
42 | */
43 | Scheduler.prototype.get = function() {
44 | var items = this._items
45 | return items[this.n++ % items.length]
46 | }
47 |
48 | /**
49 | * Remove an item from the scheduler.
50 | *
51 | * @param {Object} item An object represents some kind of resources.
52 | */
53 | Scheduler.prototype.remove = function(item) {
54 | this._items = this._items.filter(function(i) {
55 | return item !== i
56 | })
57 | this.emit('remove', item)
58 | }
59 |
60 | /**
61 | * Find out if the item is existed in the scheduler pool.
62 | *
63 | * @param {Any} item The item to compare.
64 | * @param {Function} compare The comparation function.
65 | * @return {Boolean} True if the comparation function returns true.
66 | */
67 | Scheduler.prototype.hasItem = function(item, compFn) {
68 | var oldItem = false
69 |
70 | this._items.some(function(_item) {
71 | if (compFn(_item, item))
72 | oldItem = _item
73 | return oldItem
74 | })
75 |
76 | return oldItem
77 | }
78 |
79 | Scheduler.prototype.each = function(fn) {
80 | this._items.forEach(function(item) {
81 | fn(item)
82 | })
83 | }
84 |
--------------------------------------------------------------------------------
/lib/discovery/udp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * UDP multicast backend for service discovery
3 | */
4 |
5 | /**
6 | * Module dependencies
7 | */
8 |
9 | var dgram = require('dgram')
10 | var logger = require('../logger')('micromono:discovery:udp')
11 | var ERR_PORT_INUSE = 'UDP port in use, please make sure you don\'t have other instances of micromono running as consumer with the same network settings.'
12 |
13 | // Defaults
14 | var PORT = 11628
15 | var ADDRESS = '224.0.0.116'
16 |
17 |
18 | exports.announce = function(data, options, interval) {
19 | interval = interval || options.MICROMONO_DISCOVERY_ANNOUNCE_INTERVAL || 3000
20 | var port = Number(options.MICROMONO_DISCOVERY_UDP_PORT || PORT)
21 | var address = options.MICROMONO_DISCOVERY_UDP_ADDRESS || ADDRESS
22 |
23 | logger.info('Announcing service using udp multicast', {
24 | port: port,
25 | address: address,
26 | service: data.name,
27 | interval: interval
28 | }).debug(options).trace(data)
29 |
30 | var buf = JSON.stringify(data)
31 | var len = Buffer.byteLength(buf)
32 | var socket = dgram.createSocket('udp4')
33 | var send = function() {
34 | socket.send(buf, 0, len, port, address)
35 | }
36 |
37 | // Start announcing
38 | send()
39 | setInterval(send, interval)
40 | }
41 |
42 | exports.listen = function(options, callback) {
43 | var port = options.MICROMONO_DISCOVERY_UDP_PORT || PORT
44 | var address = options.MICROMONO_DISCOVERY_UDP_ADDRESS || ADDRESS
45 |
46 | logger.info('Listening service annoucements using udp multicast', {
47 | port: port,
48 | address: address
49 | }).debug(options)
50 |
51 | var socket = dgram.createSocket({
52 | type: 'udp4',
53 | reuseAddr: true
54 | })
55 |
56 | socket.bind(port, function() {
57 | socket.addMembership(address)
58 | })
59 |
60 | socket.on('error', function(err) {
61 | if (err) {
62 | logger.fatal('EADDRINUSE' === err.errno ? ERR_PORT_INUSE : 'UDP socket error', {
63 | port: port,
64 | address: address
65 | })
66 | }
67 | callback(err)
68 | })
69 |
70 | socket.on('message', function(data, rinfo) {
71 | try {
72 | data = JSON.parse(data)
73 | if (!data.host)
74 | data.host = rinfo.address
75 | callback(null, data, rinfo)
76 | } catch (e) {
77 | callback(e, data)
78 | }
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/lib/server/health.js:
--------------------------------------------------------------------------------
1 | var http = require('http')
2 | var logger = require('../logger')('micromono:server:health')
3 |
4 | exports.prepareHealthAliveHandler = function(healthAliveHandler) {
5 | logger.debug('prepareHealthAliveHandler')
6 |
7 | if (!healthAliveHandler) {
8 | healthAliveHandler = function(req, res) {
9 | res.statusCode = 200
10 | res.end('ok')
11 | }
12 | }
13 |
14 | return {
15 | healthAliveHandler: healthAliveHandler
16 | }
17 | }
18 |
19 | exports.prepareHealthFunctionalHandler = function(services, healthFunctionalHandler) {
20 | logger.debug('prepareHealthFunctionalHandler')
21 |
22 | if (!healthFunctionalHandler) {
23 | healthFunctionalHandler = function(req, res) {
24 | var allDependenciesAvailable = true
25 |
26 | if (services) {
27 | allDependenciesAvailable = Object.keys(services)
28 | .filter(function(serviceName) {
29 | return true === services[serviceName].isRemote
30 | })
31 | .every(function(serviceName) {
32 | return services[serviceName].scheduler.len() > 0
33 | })
34 | }
35 |
36 | if (allDependenciesAvailable) {
37 | res.statusCode = 200
38 | res.end('ok')
39 | } else {
40 | res.statusCode = 503
41 | res.end('Service error')
42 | }
43 | }
44 | }
45 |
46 | return {
47 | healthFunctionalHandler: healthFunctionalHandler
48 | }
49 | }
50 |
51 |
52 | exports.startHealthinessServer = function(host, healthPort, healthinessHandlers) {
53 | logger.debug('startHealthinessServer', {
54 | host: host,
55 | healthPort: healthPort
56 | })
57 |
58 | var alivePath = healthinessHandlers.alivePath
59 | var aliveHandler = healthinessHandlers.aliveHandler
60 | var functionalPath = healthinessHandlers.functionalPath
61 | var functionalHandler = healthinessHandlers.functionalHandler
62 |
63 | var server = http.createServer(function(req, res) {
64 | res.setHeader('Content-Type', 'text/plain')
65 | switch (req.url) {
66 | case alivePath:
67 | aliveHandler(req, res)
68 | break
69 | case functionalPath:
70 | functionalHandler(req, res)
71 | break
72 | default:
73 | res.statusCode = 404
74 | res.end('Not found')
75 | }
76 | })
77 |
78 | server.listen(healthPort, host)
79 | }
80 |
--------------------------------------------------------------------------------
/lib/service/announcement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Announcement module.
3 | * The main purpose of this module is to define a spec for services.
4 | */
5 |
6 | var Announcement = module.exports = function(service) {
7 | this.name = service.name
8 | this.version = service.version
9 | delete this.web
10 | }
11 |
12 | /**
13 | * Name of the service
14 | *
15 | * @type {String}
16 | * @required
17 | */
18 | Announcement.prototype.name = undefined
19 |
20 | /**
21 | * Version of the service
22 | *
23 | * @type {String}
24 | * @required
25 | */
26 | Announcement.prototype.version = undefined
27 |
28 | /**
29 | * Info of client side resources.
30 | *
31 | * @type {Object}
32 | * @optional
33 | */
34 | Announcement.prototype.asset = undefined
35 |
36 | /**
37 | * Web related information. The value assigned is for documentation only.
38 | *
39 | * @type {Object}
40 | * @optional
41 | */
42 | Announcement.prototype.web = {
43 |
44 | /**
45 | * Port of the http(s) server binds to.
46 | * @type {Number}
47 | * @required
48 | */
49 | port: undefined,
50 |
51 | /**
52 | * Host of the http(s) server binds to.
53 | * @type {Number}
54 | * @required
55 | */
56 | host: undefined,
57 |
58 | /**
59 | * Middleware used on the balancer side.
60 | *
61 | * @type {Object}
62 | * @optional
63 | */
64 | use: undefined,
65 |
66 | /**
67 | * The route definition object.
68 | *
69 | * @type {Object}
70 | * @optional
71 | */
72 | route: undefined,
73 |
74 | /**
75 | * Name of the framework.
76 | *
77 | * @type {String}
78 | * @optional
79 | */
80 | framework: undefined,
81 |
82 | /**
83 | * Defines what middleware this service provides.
84 | *
85 | * @type {Object}
86 | * @optional
87 | */
88 | middleware: undefined,
89 |
90 | /**
91 | * Define endpoint which accepts upgrade request (websockets).
92 | *
93 | * @type {String}
94 | * @optional
95 | */
96 | upgradeUrl: undefined
97 | }
98 |
99 | /**
100 | * The API definition object.
101 | *
102 | * @type {Object}
103 | * @optional
104 | */
105 | Announcement.prototype.api = undefined
106 |
107 | /**
108 | * The channel definition object.
109 | *
110 | * @type {Object}
111 | * @optional
112 | */
113 | Announcement.prototype.channel = undefined
114 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | MicroMono express + passport example
2 | ====================================
3 |
4 |
5 | This is an exmaple shows how you can use expressjs and passportjs together with micromono to create a simple web app with password protected page. The most important feature micromono provides is the ability to run services either in one process or run them individually in different processes or machines, aka. remote services.
6 |
7 |
8 | ## Quick Start
9 |
10 | This example contains 3 services: `account`, `home` and `io`:
11 |
12 | - **[Account](/example/account)** is the service/app which provides login/logout features, auth middleware and a api for getting user by id.
13 | - **[Home](/example/home)** is the example shows how to use the features provided by the `account` service.
14 | - **[IO](/example/io)** is a websocket example using socket.io.
15 |
16 | We also have a **[server](/example/server)** sub-folder which demostrates how to run the above services together with a existing express application.
17 |
18 | Please make sure you have nodejs and npm installed before you run following commands in terminal.
19 |
20 | ### Installation
21 |
22 | Clone repository:
23 |
24 | git clone https://github.com/lsm/micromono
25 |
26 | Go to the example folder and install dependencies:
27 |
28 | cd micromono/example
29 |
30 | make install
31 |
32 | The script will install dependencies for all sub-folders: `account`, `home` and `server`.
33 |
34 | ### Run it in monolithic mode
35 |
36 | In this mode, we use `account` and `home` as normal npm package and everything runs in the same process (server and services).
37 |
38 | make mono
39 |
40 | Then open [http://127.0.0.1:3000](http://127.0.0.1:3000)
41 |
42 | Or try the `IO` example by running:
43 |
44 | make mono-io
45 |
46 | And open [http://127.0.0.1:3000/io](http://127.0.0.1:3000/io)
47 |
48 | ### Run it in microservice mode
49 |
50 | Also, you can choose to run `account`, `home` and `server` in separate processes. This requires us to run them in three separate terminals:
51 |
52 | First, run `account` service by:
53 |
54 | DEBUG=micromono* node account/index.js
55 |
56 | Then, do the same thing for `home` service in second terminal:
57 |
58 | DEBUG=micromono* node home/index.js
59 |
60 | Finally, run our server to start serving requests in the thrid terminal:
61 |
62 | DEBUG=micromono* node server/server.js --service account,home
63 |
64 | Then open [http://127.0.0.1:3000](http://127.0.0.1:3000)
65 |
--------------------------------------------------------------------------------
/lib/pipeline/balancer.js:
--------------------------------------------------------------------------------
1 | var Superpipe = require('superpipe')
2 |
3 | exports.initBalancer = Superpipe.pipeline()
4 | .pipe('getPackageJSON', 'balancerPackagePath', 'packageJSON:balancerPackageJSON')
5 | .pipe('getBalancerAsset',
6 | ['balancerPackagePath', 'balancerPackageJSON'],
7 | ['balancerAsset', 'balancerAssetInfo', 'balancerPublicPath'])
8 | .pipe('getServiceNames', 'MICROMONO_SERVICES', 'serviceNames')
9 | .pipe('prepareFrameworkForBalancer',
10 | ['mainFramework', 'mainApp'],
11 | ['attachHttpServer', 'serveBalancerAsset'])
12 | .pipe('prepareDiscovery', ['defaultDiscoveryOptions'],
13 | ['discoveryListen', 'discoveryAnnounce', 'discoveryOptions'])
14 | .pipe('getJSPMConfig?', ['balancerAssetInfo', 'balancerPublicPath', 'next'],
15 | ['jspmConfig:balancerJSPMConfig', 'jspmConfigPath:balancerJSPMConfigPath'])
16 | .pipe('getJSPMBinPath', 'balancerPackagePath', 'jspmBinPath')
17 | .pipe('createHttpServer', ['setGlobal'], ['httpServer', 'setHttpRequestHandler'])
18 | .pipe('requireAllServices',
19 | ['serviceNames', 'MICROMONO_SERVICE_DIR', 'require'],
20 | 'services')
21 | // Channel
22 | .pipe('ensureChannelGateway', ['services', 'setGlobal'], 'chnGateway')
23 | .pipe('runServices', ['micromono', 'services', 'runService', 'next'])
24 | // Get common bundles
25 | .pipe('filterServicesWithAsset', 'services', 'servicesWithAsset')
26 | .pipe('getCommonAssetDependencies?', 'servicesWithAsset', 'assetDependenciesMap')
27 | .pipe('getCommonBundles?',
28 | ['balancerAssetInfo', 'servicesWithAsset', 'assetDependenciesMap'],
29 | ['commonBundles'])
30 | // Update changes to package json
31 | .pipe('updatePackageJSON?',
32 | ['balancerAssetInfo', 'balancerPackagePath', 'balancerPackageJSON', 'next'])
33 | .pipe('bundleDevDependencies?',
34 | ['balancerAssetInfo', 'balancerPublicPath', 'balancerPackagePath', 'jspmBinPath', 'set', 'MICROMONO_BUNDLE_DEV'],
35 | ['bundleJs', 'bundleCss'])
36 | // .pipe('getJSPMConfig?', ['balancerAssetInfo', 'balancerPublicPath', 'next'],
37 | // ['jspmConfig:balancerJSPMConfig', 'jspmConfigPath:balancerJSPMConfigPath'])
38 | // .pipe('updateJSPMConfig?', ['balancerJSPMConfigPath', 'balancerJSPMConfig', 'balancerAssetInfo', 'next'])
39 | .pipe('serveBalancerAsset?', 'balancerAsset')
40 | // Start the Http server
41 | .pipe('attachHttpServer', ['httpServer', 'setHttpRequestHandler'])
42 | .pipe('startWebServer',
43 | ['httpServer', 'MICROMONO_PORT', 'MICROMONO_HOST', 'set'],
44 | ['httpPort', 'httpHost'])
45 | // Start channel gateway server if necessary
46 | .pipe('attachChnGatewayServer?', ['chnGateway', 'httpServer'])
47 |
--------------------------------------------------------------------------------
/lib/entrance/balancer.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:entrance:balancer')
2 | var Router = require('../web/router')
3 | var argsNames = require('js-args-names')
4 | var discovery = require('../discovery')
5 | var AssetPipe = require('../web/asset')
6 | var LocalPipe = require('../service/local')
7 | var HealthPipe = require('../server/health')
8 | var ServerPipe = require('../server/pipe')
9 | var RemotePipe = require('../service/remote')
10 | var initBalancer = require('../pipeline/balancer').initBalancer
11 | var DiscoveryPipe = require('../discovery/pipe')
12 | var ServicePipeline = require('../pipeline/service')
13 | var ChannelGatewayPipe = require('../channel/gateway')
14 |
15 |
16 | exports.startBalancer = function(micromono, app, callback) {
17 | var packagePath = ServerPipe.getCallerPath()
18 | logger.info('Start balancer pipeline').debug({
19 | packagePath: packagePath
20 | })
21 |
22 | // Use 3000 as default port for balancer
23 | if (!micromono.get('MICROMONO_PORT'))
24 | micromono.set('MICROMONO_PORT', 3000)
25 |
26 | // Use express as default web framework
27 | if (!micromono.get('mainFramework'))
28 | micromono.set('mainFramework', ServerPipe.initFramework('express').framework)
29 |
30 | // Set global dependencies for executing pipeline.
31 | micromono
32 | .set(Router, '*^')
33 | .set(AssetPipe, '*^')
34 | .set(LocalPipe, '*^')
35 | .set(HealthPipe, '*^')
36 | .set(RemotePipe, '*^')
37 | .set(ServerPipe, '*^')
38 | .set(DiscoveryPipe, '*^')
39 | .set(ChannelGatewayPipe, '*^')
40 | .set('mainApp', app || undefined)
41 | .set('micromono', micromono)
42 | .set('balancerPackagePath', packagePath)
43 | .set('defaultDiscoveryOptions', discovery.getDiscoveryOptions(micromono))
44 | .set('errorHandler', function(err, errPipeName) {
45 | logger.fatal('StartBalancer pipeline error', {
46 | error: err && err.stack || err,
47 | errPipeName: errPipeName
48 | })
49 | process.exit(1)
50 | })
51 |
52 | // Create the `startBalancer` pipeline.
53 | var balancer = micromono.superpipe('startBalancer')
54 | // Concat pipelines required for starting the balancer server.
55 | .concat(initBalancer)
56 | .concat(ServicePipeline.listenRemoteProviders)
57 | .concat(ServicePipeline.startHealthinessServer)
58 |
59 | // Set error and debugging handlers.
60 | balancer
61 | .error('errorHandler', [null, 'errPipeName'])
62 | .debug(micromono.get('MICROMONO_DEBUG_PIPELINE') && logger.debug)
63 |
64 | // Add the callback function as the last pipe.
65 | if ('function' === typeof callback)
66 | balancer.pipe(callback, argsNames(callback))
67 |
68 | balancer.pipe(function() {
69 | logger.info('Balancer pipeline started')
70 | })
71 |
72 | // Execute the pipeline.
73 | balancer()
74 | }
75 |
--------------------------------------------------------------------------------
/lib/channel/gateway.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:channel:gateway')
2 | var socketmq = require('socketmq')
3 |
4 | exports.ensureChannelGateway = function(services, set) {
5 | logger.debug('ensureChannelGateway')
6 |
7 | var chnGateway
8 | var hasServiceWithChannel = Object.keys(services).some(function(name) {
9 | return !!services[name].announcement.channel
10 | })
11 |
12 | if (hasServiceWithChannel) {
13 | logger.info('Create new Channel gateway instance')
14 |
15 | chnGateway = socketmq.gateway()
16 | chnGateway.isUntrusted = function(stream) {
17 | return 'tcp' !== stream.__smq__.protocol
18 | }
19 | chnGateway.on('disconnect', function(stream) {
20 | if (stream.provider && stream.scheduler) {
21 | var provider = stream.provider
22 |
23 | logger.info('Channel provider disconnected', {
24 | service: provider.name + '@' + provider.version,
25 | host: provider.host
26 | }).trace(provider)
27 |
28 | stream.scheduler.remove(provider)
29 | }
30 | })
31 | chnGateway.on('error', function(err) {
32 | var provider = err.stream && err.stream.provider
33 | var name
34 | var host
35 | if (provider) {
36 | name = provider.name
37 | host = provider.host
38 | }
39 | logger.error('Channel provider error', {
40 | service: name,
41 | host: host
42 | })
43 | })
44 | }
45 |
46 | // `set` would be `setGlobal` for balancer.
47 | set('chnGateway', chnGateway)
48 | // There's a bug which `setGlobal` won't trigger the next pipe.
49 | // Return true to trigger next for now.
50 | return true
51 | }
52 |
53 | exports.attachChnGatewayServer = function(chnGateway, httpServer) {
54 | logger.debug('attachChnGatewayServer')
55 | chnGateway.bind('eio://', {
56 | httpServer: httpServer
57 | })
58 | }
59 |
60 | exports.connectToChannel = function(channel, chnGateway, announcement, scheduler, next) {
61 | logger.debug('connectToChannel')
62 | chnGateway.connect(channel.endpoint, function(stream) {
63 | logger.info('New channel provider connected', {
64 | service: announcement.name + '@' + announcement.version,
65 | endpoint: channel.endpoint,
66 | namespaces: channel.namespaces
67 | }).trace(announcement)
68 |
69 | stream.provider = announcement
70 | stream.scheduler = scheduler
71 | next && next()
72 | })
73 | }
74 |
75 | exports.channelOnNewProvider = function(chnGateway, scheduler) {
76 | logger.debug('channelOnNewProvider')
77 | scheduler.on('add', function(provider) {
78 | if (provider.channel) {
79 | logger.info('Found new channel provider', {
80 | service: provider.name + '@' + provider.version,
81 | host: provider.host
82 | }).trace(provider)
83 |
84 | exports.connectToChannel(provider.channel, chnGateway, provider, scheduler)
85 | }
86 | })
87 | }
88 |
--------------------------------------------------------------------------------
/example/home/public/config.js:
--------------------------------------------------------------------------------
1 | System.config({
2 | defaultJSExtensions: true,
3 | transpiler: "babel",
4 | babelOptions: {
5 | "optional": [
6 | "runtime",
7 | "optimisation.modules.system"
8 | ]
9 | },
10 | paths: {
11 | "github:*": "jspm_packages/github/*",
12 | "npm:*": "jspm_packages/npm/*"
13 | },
14 |
15 | map: {
16 | "babel": "npm:babel-core@5.8.34",
17 | "babel-runtime": "npm:babel-runtime@5.8.34",
18 | "core-js": "npm:core-js@1.2.6",
19 | "lodash.assign": "npm:lodash.assign@4.0.0",
20 | "github:jspm/nodelibs-assert@0.1.0": {
21 | "assert": "npm:assert@1.4.1"
22 | },
23 | "github:jspm/nodelibs-buffer@0.1.0": {
24 | "buffer": "npm:buffer@3.6.0"
25 | },
26 | "github:jspm/nodelibs-path@0.1.0": {
27 | "path-browserify": "npm:path-browserify@0.0.0"
28 | },
29 | "github:jspm/nodelibs-process@0.1.2": {
30 | "process": "npm:process@0.11.8"
31 | },
32 | "github:jspm/nodelibs-util@0.1.0": {
33 | "util": "npm:util@0.10.3"
34 | },
35 | "github:jspm/nodelibs-vm@0.1.0": {
36 | "vm-browserify": "npm:vm-browserify@0.0.4"
37 | },
38 | "npm:assert@1.4.1": {
39 | "assert": "github:jspm/nodelibs-assert@0.1.0",
40 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
41 | "process": "github:jspm/nodelibs-process@0.1.2",
42 | "util": "npm:util@0.10.3"
43 | },
44 | "npm:babel-runtime@5.8.34": {
45 | "process": "github:jspm/nodelibs-process@0.1.2"
46 | },
47 | "npm:buffer@3.6.0": {
48 | "base64-js": "npm:base64-js@0.0.8",
49 | "child_process": "github:jspm/nodelibs-child_process@0.1.0",
50 | "fs": "github:jspm/nodelibs-fs@0.1.2",
51 | "ieee754": "npm:ieee754@1.1.6",
52 | "isarray": "npm:isarray@1.0.0",
53 | "process": "github:jspm/nodelibs-process@0.1.2"
54 | },
55 | "npm:core-js@1.2.6": {
56 | "fs": "github:jspm/nodelibs-fs@0.1.2",
57 | "path": "github:jspm/nodelibs-path@0.1.0",
58 | "process": "github:jspm/nodelibs-process@0.1.2",
59 | "systemjs-json": "github:systemjs/plugin-json@0.1.2"
60 | },
61 | "npm:inherits@2.0.1": {
62 | "util": "github:jspm/nodelibs-util@0.1.0"
63 | },
64 | "npm:lodash.assign@4.0.0": {
65 | "lodash.keys": "npm:lodash.keys@4.0.8",
66 | "lodash.rest": "npm:lodash.rest@4.0.4"
67 | },
68 | "npm:lodash.rest@4.0.4": {
69 | "process": "github:jspm/nodelibs-process@0.1.2"
70 | },
71 | "npm:path-browserify@0.0.0": {
72 | "process": "github:jspm/nodelibs-process@0.1.2"
73 | },
74 | "npm:process@0.11.8": {
75 | "assert": "github:jspm/nodelibs-assert@0.1.0",
76 | "fs": "github:jspm/nodelibs-fs@0.1.2",
77 | "vm": "github:jspm/nodelibs-vm@0.1.0"
78 | },
79 | "npm:util@0.10.3": {
80 | "inherits": "npm:inherits@2.0.1",
81 | "process": "github:jspm/nodelibs-process@0.1.2"
82 | },
83 | "npm:vm-browserify@0.0.4": {
84 | "indexof": "npm:indexof@0.0.1"
85 | }
86 | }
87 | });
88 |
--------------------------------------------------------------------------------
/lib/config/settings.js:
--------------------------------------------------------------------------------
1 |
2 | exports.default = [
3 | ['-d --service-dir [dir]', 'Directory of locally available services. Env name: MICROMONO_SERVICE_DIR'],
4 | ['-p --port [port]', 'The http port which balancer/service binds to. MICROMONO_PORT'],
5 | ['-H --host [host]', 'The host which balancer/service binds to. MICROMONO_HOST']
6 | ]
7 |
8 | exports.discovery = [
9 | // Default options for discovery
10 | ['--discovery-target [service]', 'The target service to be discovered. MICROMONO_DISCOVERY_TARGET'],
11 | ['--discovery-backend [backend]',
12 | 'The backend of service discovery. Default `udp`. MICROMONO_DISCOVERY_BACKEND', 'udp'],
13 | ['--discovery-timeout [timeout]',
14 | 'The discoverying process will exit out after this time. Default 90 seconds. MICROMONO_DISCOVERY_TIMEOUT', '90000'
15 | ],
16 | ['--discovery-announce-interval [interval]',
17 | 'The interval between sending out announcements. MICROMONO_DISCOVERY_ANNOUNCE_INTERVAL', '3000'],
18 | // Agent settings
19 | ['--discovery-agent',
20 | 'Run micromono service/server in discovery agent mode. MICROMONO_DISCOVERY_AGENT'],
21 | ['--discovery-agent-path [/path/to/agent]',
22 | 'Use the indicated executable as discovery prober instead of the default one. MICROMONO_DISCOVERY_AGENT_PATH'],
23 | // UDP address
24 | ['--discovery-udp-address [address]',
25 | 'Multicast address of udp network. MICROMONO_DISCOVERY_UDP_ADDRESS', '224.0.0.116'],
26 | ['--discovery-udp-port [port]',
27 | 'Port for udp socket to bind to. MICROMONO_DISCOVERY_UDP_PORT', '11628'],
28 | // NATS
29 | ['--discovery-nats-servers [servers]',
30 | 'Comma separated list of nats server adresses. MICROMONO_DISCOVERY_NATS_SERVERS']
31 | ]
32 |
33 | exports.health = [
34 | ['--health-port [port]', 'Port of the healthiness http server listens. MICROMONO_HEALTH_PORT. Setting this option will enable the default healthiness handlers.'],
35 | ['--health-alive-path [path]', 'Relative url path of the liveness http request handler. MICROMONO_HEALTH_ALIVE_PATH', '/__health/alive'],
36 | ['--health-functional-path [path]', 'Relative url path of the functional (readiness) http request handler. MICROMONO_HEALTH_FUNCTIONAL_PATH', '/__health/functional']
37 | ]
38 |
39 | exports.server = [
40 | ['-s --services [services]',
41 | 'Names of services to require. Use comma to separate multiple services. (e.g. --services account,cache) Env name: MICROMONO_SERVICES'],
42 | ['--local-services [services]', 'List of local services required.'],
43 | ['--remote-services [services]', 'List of remote services required.']
44 | ]
45 |
46 | exports.service = [
47 | ['-r --rpc [type]', 'Type of rpc to use. Default `socketmq`. MICROMONO_RPC', 'socketmq'],
48 | ['--rpc-port [port]', 'The port which service binds the rpc server to. MICROMONO_RPC_PORT'],
49 | ['--rpc-host [host]', 'The host which service binds the rpc server to. MICROMONO_RPC_HOST'],
50 | ['--chn-endpoint', 'The endpoint [protocol]://[address]:[port] which channel server binds to. MICROMONO_CHN_ENDPOINT']
51 | ]
52 |
--------------------------------------------------------------------------------
/lib/api/socketmq.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:rpc:socketmq')
2 | var msgpack = require('msgpack-lite')
3 | var socketmq = require('socketmq')
4 |
5 | var REQ_NAME = 'micromono/api/rpc'
6 |
7 | var SocketMQAdapter = module.exports = function() {}
8 |
9 | SocketMQAdapter.prototype.type = 'socketmq'
10 |
11 | SocketMQAdapter.prototype.getSocketMQ = function() {
12 | var smq = this.smq
13 | if (!smq) {
14 | smq = socketmq()
15 | smq.setMsgEncoder(msgpack, Buffer('m'))
16 | this.smq = smq
17 | }
18 | return smq
19 | }
20 |
21 | SocketMQAdapter.prototype.send = function(data) {
22 | logger.trace('Send message', {
23 | data: data
24 | })
25 |
26 | var fn
27 | var args = data.args
28 |
29 | if ('function' === typeof args[args.length - 1]) {
30 | fn = args.pop()
31 | data.cid = true
32 | }
33 |
34 | this.adapter.smq.req(REQ_NAME, data, fn || function() {})
35 | }
36 |
37 | SocketMQAdapter.prototype.connect = function(provider) {
38 | var self = this
39 | var adapter = this.adapter
40 | var name = provider.name
41 | var endpoint = 'tcp://' + provider.host + ':' + provider.api.port
42 |
43 | logger.info('Connecting to API provider', {
44 | service: provider.name + '@' + provider.version,
45 | endpoint: endpoint
46 | })
47 |
48 | var smq = adapter.smq
49 | if (!smq) {
50 | // Create socketmq instance if not exists.
51 | smq = this.adapter.getSocketMQ()
52 | smq.on('disconnect', function(socket) {
53 | self.onProviderDisconnect(socket.provider)
54 | logger.info('API provider disconnected', {
55 | name: name,
56 | host: socket.provider.host
57 | })
58 | })
59 | smq.on('error', function(err) {
60 | var provider = err.stream && err.stream.provider
61 | logger.warn('API provider error', {
62 | name: name,
63 | host: provider ? provider.host : 'unknown host',
64 | error: err
65 | })
66 | })
67 | }
68 |
69 | var socket = smq.connect(endpoint)
70 | socket.provider = provider
71 | }
72 |
73 | SocketMQAdapter.prototype.startServer = function(port, host, callback) {
74 | var self = this
75 | var smq = this.adapter.getSocketMQ()
76 | var uri = 'tcp://' + host + ':' + port
77 |
78 | logger.info('Start API server', {
79 | endpoint: uri
80 | })
81 |
82 | var server = smq.bind(uri)
83 |
84 | smq.on('bind', function() {
85 | callback(null, server)
86 | })
87 | smq.on('connect', function(stream) {
88 | logger.info('Remote client connected', {
89 | remoteAddress: stream.remoteAddress
90 | })
91 | })
92 | smq.on('disconnect', function(stream) {
93 | logger.info('Remote client disconnected', {
94 | remoteAddress: stream.remoteAddress
95 | })
96 | })
97 |
98 | // Response the rpc request.
99 | smq.rep(REQ_NAME, self.dispatch.bind(self))
100 | }
101 |
102 | // SocketMQ has built-in serializer, override.
103 |
104 | SocketMQAdapter.prototype.serialize = function(msg) {
105 | return msg
106 | }
107 |
108 | SocketMQAdapter.prototype.deserialize = function(data) {
109 | return data
110 | }
111 |
--------------------------------------------------------------------------------
/lib/entrance/service.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:entrance:service')
2 | var discovery = require('../discovery')
3 | var AssetPipe = require('../web/asset')
4 | var LocalPipe = require('../service/local')
5 | var HealthPipe = require('../server/health')
6 | var ServerPipe = require('../server/pipe')
7 | var RemotePipe = require('../service/remote')
8 | var ChnPipeline = require('../pipeline/channel')
9 | var DiscoveryPipe = require('../discovery/pipe')
10 | var ServicePipeline = require('../pipeline/service')
11 | var ChannelBackendPipe = require('../channel/backend')
12 |
13 |
14 | /**
15 | * Start a service with standalone internal servers (web, rpc and healthiness etc.).
16 | *
17 | * @param {Micromono} micromono Micromono instance.
18 | * @param {Function|Object} Service Service instance or constructor.
19 | * @param {Function} [callback] Optional callback for getting called when the service is started.
20 | * @param {Array} [cbDependencies] Array of dependencies names for the callback.
21 | */
22 | exports.startService = function(micromono, Service, callback, cbDependencies) {
23 | logger.info('Start service pipeline')
24 | // Get instance of service.
25 | var service = 'function' === typeof Service ? new Service() : Service
26 | // Prepare global service dependencies
27 | micromono
28 | .set(AssetPipe, '*^')
29 | .set(LocalPipe, '*^')
30 | .set(HealthPipe, '*^')
31 | .set(RemotePipe, '*^')
32 | .set(DiscoveryPipe, '*^')
33 | .set(ChannelBackendPipe, '*^')
34 | .set('service', service)
35 | .set('initChannel', ChnPipeline.initChannel)
36 | // Guess package path based on the caller of this function if not present.
37 | .set('packagePath', service.packagePath || ServerPipe.getCallerPath())
38 | .set('initFramework', ServerPipe.initFramework)
39 | .set('defaultDiscoveryOptions', discovery.getDiscoveryOptions(micromono))
40 | // Dependencies for listening new providers for remote services
41 | .set('errorHandler', function(err, serviceName, errPipeName) {
42 | logger.fatal('StartService pipeline error', {
43 | error: err && err.stack || err,
44 | service: serviceName,
45 | errPipeName: errPipeName
46 | })
47 | process.exit(1)
48 | })
49 |
50 | // Build the `startService` pipeline.
51 | var servicePipeline = micromono.superpipe('startService')
52 | .concat(ServicePipeline.initLocalService)
53 | .concat(ChnPipeline.setupChannel)
54 | .concat(ServicePipeline.startServers)
55 | // Insert service.init as a pipe.
56 | .concat(LocalPipe.getServiceInitPipeline(service))
57 | .concat(ServicePipeline.runLocalService)
58 | .concat(ServicePipeline.listenRemoteProviders)
59 | .concat(ServicePipeline.startHealthinessServer)
60 | .concat(ServicePipeline.announceLocalService)
61 |
62 | // Set error and debugging handlers.
63 | servicePipeline
64 | .error('errorHandler', [null, 'serviceName', 'errPipeName'])
65 | .debug(micromono.get('MICROMONO_DEBUG_PIPELINE') && logger.debug)
66 |
67 | if (callback)
68 | servicePipeline.pipe(callback, cbDependencies)
69 |
70 | servicePipeline.pipe(function() {
71 | logger.info('Service pipeline started')
72 | })
73 |
74 | // Execute the pipeline.
75 | servicePipeline()
76 | }
77 |
--------------------------------------------------------------------------------
/example/rpc-adapters/axon.js:
--------------------------------------------------------------------------------
1 | var axon = require('axon')
2 | var debug = require('debug')('micromono:rpc:axon')
3 |
4 |
5 | var AxonAdapter = module.exports = {}
6 |
7 | AxonAdapter.type = 'axon'
8 |
9 | AxonAdapter.send = function(data) {
10 | var provider = this.scheduler.get()
11 | var socket = provider.socket
12 | var args = data.args
13 | var fn
14 |
15 | if ('function' === typeof args[args.length - 1]) {
16 | fn = args.pop()
17 | data.cid = true
18 | }
19 |
20 | var msg = this.serialize(data)
21 | socket.send(msg, fn || function() {})
22 | }
23 |
24 | AxonAdapter.connect = function(provider) {
25 | var self = this
26 | var name = provider.name
27 | var endpoint = 'tcp://' + provider.host + ':' + provider.rpc.port
28 | debug('[%s] connecting to endpoint "%s"', name, endpoint)
29 |
30 | var socket = axon.socket('req')
31 | var closing = false
32 |
33 | function closeSocket() {
34 | if (!closing) {
35 | closing = true
36 | socket.close()
37 | self.onProviderDisconnect(provider)
38 | debug('[%s] provider "%s" closed', name, endpoint)
39 | }
40 | }
41 |
42 | socket.on('close', function() {
43 | debug('[%s] socket on close, provider "%s"', name, endpoint)
44 | closeSocket()
45 | })
46 |
47 | socket.on('socket error', function(err) {
48 | debug('[%s] socket on error [%s], provider "%s"', name, err.code, endpoint)
49 | closeSocket()
50 | })
51 |
52 | socket.on('connect', function(sock) {
53 | var closeListeners = sock.listeners('close')
54 | sock.removeAllListeners('close')
55 |
56 | closeListeners.unshift(function() {
57 | debug('[%s] sock on close, provider "%s"', name, endpoint)
58 | closeSocket()
59 | })
60 |
61 | closeListeners.forEach(function(listener) {
62 | sock.on('close', listener)
63 | })
64 | })
65 |
66 | socket.connect(endpoint, function() {
67 | debug('[%s] connected to endpoint "%s"', name, endpoint)
68 |
69 | // heartbeat
70 | var hid
71 | var lastPong = Date.now()
72 |
73 | function heartbeat() {
74 | var now = Date.now()
75 | if (now - lastPong > 3000) {
76 | // timeout, disconnect socket
77 | debug('[%s] heartbeat timeout, close socket provider "%s"', name, endpoint)
78 | clearInterval(hid)
79 | closeSocket()
80 | } else {
81 | socket.send('ping', function(msg) {
82 | // debug('[%s] client got ' + msg, name)
83 | if (msg === 'pong') {
84 | lastPong = Date.now()
85 | }
86 | })
87 | }
88 | }
89 |
90 | debug('[%s] start heartbeating', name)
91 | heartbeat()
92 | hid = setInterval(heartbeat, 1000)
93 | })
94 |
95 | provider.socket = socket
96 | }
97 |
98 | AxonAdapter.startServer = function(port, host) {
99 | var self = this
100 | var socket = axon.socket('rep')
101 |
102 | var promise = new Promise(function(resolve, reject) {
103 | socket.bind(port, host, function() {
104 | resolve(socket.server)
105 | })
106 |
107 | socket.on('message', function(msg, callback) {
108 | if (msg === 'ping') {
109 | // debug('server got ping')
110 | callback('pong')
111 | } else {
112 | self.dispatch(msg, callback)
113 | }
114 | })
115 |
116 | socket.on('connect', function(sock) {
117 | debug('client connected from', sock._peername)
118 | })
119 | })
120 |
121 | return promise
122 | }
123 |
--------------------------------------------------------------------------------
/lib/web/middleware/layout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | var assign = require('lodash.assign')
6 |
7 | /**
8 | * The remote partial composing middleware
9 | */
10 | module.exports = function(app) {
11 | var layoutName = app.get('micromono layout name') || 'layout'
12 | app.set('micromono layout name', layoutName)
13 |
14 | return function renderLayout(req, res, next) {
15 | if (req.__micromono_layout_attached__) {
16 | next()
17 | return
18 | }
19 | req.__micromono_layout_attached__ = true
20 |
21 | var _end = res.end
22 | var _write = res.write
23 | var _writeHead = res.writeHead
24 |
25 | var _headers
26 | res.writeHead = function(code, message, headers) {
27 | if (code)
28 | res.statusCode = code
29 |
30 | switch (typeof message) {
31 | case 'string':
32 | res.statusMessage = message
33 | break
34 | default:
35 | case 'object':
36 | _headers = headers
37 | }
38 |
39 | return res
40 | }
41 |
42 | function end(data, encoding, callback) {
43 | res.set('Content-Length', Buffer.byteLength(data, encoding))
44 | if (!res._header)
45 | res._implicitHeader()
46 | _writeHead.call(res, res.statusCode)
47 | if (data)
48 | _write.call(res, data, encoding)
49 | _end.call(res, callback)
50 | }
51 |
52 | var buf = ''
53 |
54 | res.write = function(body) {
55 | buf += body
56 | return true
57 | }
58 |
59 | res.end = function(data, encoding, callback) {
60 | if (data) {
61 | if ('function' === typeof data)
62 | callback = data
63 | else
64 | buf += data
65 | }
66 |
67 | if (_headers)
68 | res.set(_headers)
69 |
70 | var locals
71 | var JSONStr
72 | var contentType = res.getHeader('content-type')
73 |
74 | if (/json/.test(contentType)) {
75 | JSONStr = buf.toString('utf8')
76 | } else if (/text/.test(contentType)) {
77 | locals = {
78 | yield: buf
79 | }
80 | } else {
81 | // No content type, just return what we get
82 | end(buf, encoding, callback)
83 | return
84 | }
85 |
86 | var accept = req.accepts(['html', 'json'])
87 |
88 | if (JSONStr) {
89 | try {
90 | locals = JSON.parse(JSONStr)
91 | } catch (e) {
92 | res.status(500)
93 | end('Service error.', 'utf8')
94 | return
95 | }
96 | }
97 |
98 | // merge local context
99 | res.locals = assign(res.locals, locals)
100 |
101 | if (req.xhr && 'json' === accept) {
102 | // send json if this is a xhr request which accepts json
103 | res.type('json')
104 | var jsonStr = JSON.stringify(locals)
105 | end(jsonStr, encoding, callback)
106 | } else if ('html' === accept) {
107 | // render html page with data
108 | res.type('html')
109 | app.render(layoutName, res.locals, function(err, html) {
110 | if (err) {
111 | var data = err.toString()
112 | res.status(500)
113 | res.statusMessage = 'Server error.'
114 | end(data, 'utf8')
115 | return
116 | }
117 | end(html, encoding, callback)
118 | })
119 | } else {
120 | res.status(406)
121 | end('Not Acceptable.', 'utf8')
122 | }
123 | }
124 |
125 | next()
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib/discovery/nats.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NATS backend for service discovery
3 | */
4 |
5 | /**
6 | * Module dependencies
7 | */
8 |
9 | var nats = require('nats')
10 | var logger = require('../logger')('micromono:discovery:nats')
11 | var assign = require('lodash.assign')
12 | var NATS_DEFAULT_OPTIONS = {
13 | reconnect: true,
14 | reconnectTimeWait: 3000,
15 | waitOnFirstConnect: true,
16 | maxReconnectAttempts: 60
17 | }
18 |
19 |
20 | /**
21 | * Announce service.
22 | *
23 | * @param {Object} data Data to announce.
24 | * @param {Object} options Discovery options.
25 | * @param {Number} [interval] Optional interval in milliseconds.
26 | */
27 | exports.announce = function(data, options, interval) {
28 | options = assign({}, NATS_DEFAULT_OPTIONS, options)
29 | interval = interval || options.MICROMONO_DISCOVERY_ANNOUNCE_INTERVAL || 3000
30 |
31 | logger.info('Announcing service using nats pubsub', {
32 | service: data.name,
33 | interval: interval
34 | }).debug(options).trace(data)
35 |
36 | var ann = JSON.stringify(data)
37 | var natsClient = connect(options)
38 | var send = function() {
39 | natsClient.publish('micromono/service/announcement', ann)
40 | }
41 |
42 | // Wait for first connect.
43 | natsClient.once('connect', function() {
44 | logger.debug('Nats connected, start announcing service.')
45 | send()
46 | setInterval(send, interval)
47 | })
48 |
49 | natsClient.on('error', function(err) {
50 | logger.fatal('Failed to connect nats', {
51 | error: err,
52 | service: data.name
53 | }).debug(options).trace(data)
54 | throw err
55 | })
56 |
57 | natsClient.on('close', function() {
58 | logger.fatal('All connections to nats have been lost', {
59 | service: data.name,
60 | natsServers: options.servers
61 | }).debug(options).trace(data)
62 | throw new Error('All connections to nats have been lost.')
63 | })
64 | }
65 |
66 | /**
67 | * Listen service announcements.
68 | *
69 | * @param {Object} options Discovery options
70 | * @param {Function(Error|null, String|Object)} callback Returns result of
71 | * discovery through callback. It returns `null` & `Object` on successful
72 | * discovery or `Error` and `String` on failure.
73 | */
74 | exports.listen = function(options, callback) {
75 | logger.info('Listening service annoucements using nats pubsub.')
76 | .debug(options)
77 |
78 | var natsClient = connect(options)
79 |
80 | natsClient.on('error', function(err) {
81 | logger.fatal('Failed to connect nats', {
82 | error: err
83 | }).debug(options)
84 | throw err
85 | })
86 |
87 | natsClient.on('close', function() {
88 | logger.fatal('All connections to nats have been lost', {
89 | natsServers: options.servers
90 | }).debug(options)
91 | throw new Error('All connections to nats have been lost.')
92 | })
93 |
94 | natsClient.subscribe('micromono/service/announcement', function(data) {
95 | try {
96 | data = JSON.parse(data)
97 | callback(null, data)
98 | } catch (e) {
99 | callback(e, data)
100 | }
101 | })
102 | }
103 |
104 |
105 | /**
106 | * Private for connecting to nats.
107 | */
108 | function connect(options) {
109 | var servers = options.MICROMONO_DISCOVERY_NATS_SERVERS.split(',')
110 | options = assign({}, NATS_DEFAULT_OPTIONS, {
111 | 'servers': servers
112 | })
113 | logger.info('Connecting to nats servers', {
114 | servers: servers
115 | }).debug(options)
116 | return nats.connect(options)
117 | }
118 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 | var ip = require('ip')
5 | var logger = require('./logger')('micromono')
6 | var config = require('./config')
7 | var service = require('./service')
8 | var entrance = require('./entrance')
9 | var discovery = require('./discovery')
10 | var Superpipe = require('superpipe')
11 |
12 |
13 | /**
14 | * Micromono constructor
15 | */
16 | var Micromono = function() {
17 | var micromono = this
18 |
19 | // Store instances of services
20 | micromono.services = {}
21 | // Make instance of superpipe
22 | micromono.superpipe = Superpipe()
23 | micromono.superpipe.autoBind(true)
24 | // Assign submodules to the main object
25 | micromono.register = discovery.register.bind(micromono)
26 | micromono.Service = service.Service
27 | micromono.createService = service.createService
28 |
29 | // Expose constructor
30 | micromono.Micromono = Micromono
31 |
32 | // Apply default configurations
33 | micromono.defaultConfig()
34 |
35 | if (micromono.get('MICROMONO_DISCOVERY_AGENT')) {
36 | // Running in discovery agent mode. No service or server will be started.
37 | // It's also not possible to require services.
38 | micromono.startService = function() {}
39 | micromono.startBalancer = function() {}
40 | micromono.require = function() {}
41 | // Run prober
42 | require('./discovery/prober')
43 | // Ignore uncaught exception for this case.
44 | process.removeAllListeners('uncaughtException')
45 | process.on('uncaughtException', function(err) {
46 | logger.warn('\n\tCaught "uncaughtException" in agent mode. The error might be okay to be ignored:\n\t', err, '\n');
47 | })
48 | } else {
49 | micromono.startService = entrance.startService.bind(null, micromono)
50 | micromono.startBalancer = entrance.startBalancer.bind(null, micromono)
51 | micromono.require = discovery.require.bind(micromono)
52 | micromono.set('require', micromono.require)
53 | }
54 |
55 | return micromono
56 | }
57 |
58 | // Add configurable api using superpipe
59 | Micromono.prototype.set = function(name, deps, props) {
60 | this.superpipe.set(name, deps, props)
61 | return this
62 | }
63 |
64 | Micromono.prototype.get = function(name) {
65 | return this.superpipe.get(name)
66 | }
67 |
68 | Micromono.prototype.config = function(options) {
69 | var micromono = this
70 | Object.keys(options).forEach(function(key) {
71 | if (/^MICROMONO_/.test(key))
72 | micromono.set(key, options[key])
73 | })
74 | return this
75 | }
76 |
77 | Micromono.prototype.defaultConfig = function() {
78 | // Bundle asset for dev?
79 | if ('development' === process.env.NODE_ENV
80 | && undefined === this.get('MICROMONO_BUNDLE_DEV'))
81 | this.set('MICROMONO_BUNDLE_DEV', true)
82 |
83 | this.set('services', this.services)
84 | // Default configurations
85 | var host = process.env.HOST || ip.address() || '0.0.0.0'
86 | this.set({
87 | MICROMONO_PORT: process.env.PORT || 0,
88 | MICROMONO_HOST: host,
89 | MICROMONO_RPC_PORT: 0,
90 | MICROMONO_RPC_HOST: host,
91 | MICROMONO_CHN_ENDPOINT: 'tcp://' + host
92 | })
93 |
94 | // Load configurations from command line and env.
95 | var options = config(['default', 'discovery', 'health', 'service', 'server'])
96 | this.serviceDir = options.serviceDir
97 | this.config(options)
98 | }
99 |
100 | /**
101 | * Add logger constructor as instance function.
102 | */
103 | Micromono.prototype.logger = require('./logger')
104 |
105 |
106 | /**
107 | * Exports the main MicroMono instance object.
108 | *
109 | * @type {Object}
110 | */
111 | module.exports = new Micromono()
112 |
113 |
114 | /**
115 | * Capture exit signal
116 | */
117 | process.on('SIGINT', function() {
118 | logger.warn('\n\tShutting down micromono...\n')
119 | process.exit(0)
120 | })
121 |
--------------------------------------------------------------------------------
/example/home/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies
5 | */
6 |
7 | var path = require('path')
8 | var assert = require('assert')
9 | var bodyParser = require('body-parser')
10 |
11 | // setup micromono
12 | var micromono = require('/opt/micromono')
13 |
14 | // require instance of account service
15 | var account = micromono.require('account')
16 |
17 | /**
18 | * Example service which render pages and use other service as dependency.
19 | */
20 | var Home = module.exports = micromono.createService({
21 |
22 | channel: {
23 | namespace: 'home',
24 | 'home/default': function() {}
25 | },
26 |
27 | use: {
28 | // tell micromono to use `layout` middleware at the server side
29 | // for request urls in the array.
30 | 'layout': ['get::/home/private$', '/public$', '/$', '/project/:project/:id']
31 | },
32 |
33 | route: {
34 | // a password protected page
35 | 'get::/home/private': [account.middleware.auth(), function privatePage(req, res) {
36 | // var user = req.user
37 | account.api.getUserById(req.user.id, function(user) {
38 | account.api.getMultiArgs(1, {
39 | key: 'value'
40 | }, function(err, result) {
41 | assert.equal(err, null)
42 | assert.equal(result[0], 1)
43 | assert.equal(result[1], '2')
44 | assert.equal(Buffer.isBuffer(result[2]), true)
45 | assert.equal(result[2].toString(), 'hello')
46 |
47 | res.render('page', {
48 | title: 'Home Private Page',
49 | name: user.username + ', you can not see this page unless you have logged in successfully.',
50 | id: user.id,
51 | password: user.password,
52 | method: 'GET'
53 | })
54 | })
55 | })
56 | }],
57 |
58 | 'post::/home/private-form': [account.middleware.auth(), function privatePage(req, res) {
59 | // var user = req.user
60 | account.api.getUserById(req.user.id, function(user) {
61 | res.render('page', {
62 | title: 'Home Private Page',
63 | name: user.username + ', you can not see this page unless you have logged in successfully.',
64 | id: user.id,
65 | password: user.password,
66 | method: 'POST'
67 | })
68 | })
69 | }],
70 |
71 | 'get::/public': function publicPage(req, res) {
72 | res.render('page', {
73 | title: 'Home Public Page',
74 | name: 'anonymouse'
75 | })
76 | },
77 |
78 | 'get::/': function index(req, res) {
79 | res.render('index')
80 | },
81 |
82 | 'get::/project/:project/:id': function(req, res) {
83 | res.send(['project', req.params.project, req.params.id].join('/'))
84 | },
85 |
86 | 'get::/user/:name': function(req, res) {
87 | var context = {
88 | name: req.params.name
89 | }
90 | var accept = req.accepts(['html', 'json'])
91 | if ('json' === accept) {
92 | // We don't need the rendered content here as
93 | // this is probably a api xhr request. (Client side rendering)
94 | res.json(context)
95 | } else {
96 | // A html page request, send the rendered data for first page load.
97 | // (Server side rendering)
98 | context.yield = 'this is data rendered for page'
99 | res.json(context)
100 | }
101 | },
102 |
103 | '/home/exit': function(req, res) {
104 | res.send('ok')
105 | setTimeout(function() {
106 | process.exit(0)
107 | }, 1000)
108 | }
109 |
110 | },
111 |
112 | /**
113 | * Initialize function, do setup for your service here.
114 | * Resolve a promise when the initialization is done.
115 | *
116 | * @return {Promise}
117 | */
118 | init: function(app, next) {
119 | app.use(bodyParser.urlencoded({
120 | extended: false
121 | }))
122 |
123 | app.set('views', path.join(__dirname, './view'))
124 | app.set('view engine', 'jade')
125 |
126 | next()
127 | }
128 | })
129 |
130 | // Start the service if this is the main file
131 | if (require.main === module) {
132 | micromono.startService(Home)
133 | }
134 |
--------------------------------------------------------------------------------
/example/account/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies
3 | */
4 |
5 | var path = require('path')
6 | var assert = require('assert')
7 | var bodyParser = require('body-parser')
8 | var cookieParser = require('cookie-parser')
9 | var session = require('express-session')
10 | var connect = require('connect')
11 |
12 | // get micromono instance
13 | var micromono = require('/opt/micromono')
14 |
15 | // get passport
16 | var passport = require('./passport')
17 |
18 | // generate passport authentication function
19 | var passportAuth = passport.authenticate('local', {
20 | successRedirect: '/account/protected',
21 | failureRedirect: '/account/login',
22 | failureFlash: false
23 | })
24 |
25 | function isAuthenticated(req, res) {
26 | if (req.isAuthenticated()) {
27 | return true
28 | } else {
29 | res.redirect('/account/login')
30 | return false
31 | }
32 | }
33 |
34 | // setup a dedicated connect middleware for parsing data and session,
35 | // so we can reuse it in the `auth` middleware and the express app.
36 | var connectAuth = connect()
37 |
38 | connectAuth.use(bodyParser.json())
39 | connectAuth.use(bodyParser.urlencoded({
40 | extended: false
41 | }))
42 |
43 | connectAuth.use(cookieParser())
44 | connectAuth.use(session({
45 | secret: 'micromono',
46 | resave: true,
47 | saveUninitialized: true
48 | }))
49 |
50 | connectAuth.use(passport.initialize())
51 | connectAuth.use(passport.session())
52 |
53 | /**
54 | * Account service
55 | */
56 | var Account = module.exports = {
57 | middleware: {
58 | auth: function() {
59 | return function(req, res, next) {
60 | if (req.isAuthenticated()) {
61 | next()
62 | } else {
63 | connectAuth(req, res, function() {
64 | if (isAuthenticated(req, res)) {
65 | next()
66 | }
67 | })
68 | }
69 | }
70 | }
71 | },
72 |
73 | use: {
74 | // tell micromono to use `layout` middleware at the server side
75 | // for request url matching `/account/:page`.
76 | 'layout': '/account/:page'
77 | },
78 |
79 | /**
80 | * Route definition property
81 | * @type {Object}
82 | */
83 | route: {
84 | /**
85 | * Example protected page
86 | */
87 | 'get::/account/protected': function protectedPage(req, res) {
88 | if (isAuthenticated(req, res)) {
89 | res.render('hello', {
90 | name: req.user.username
91 | })
92 | }
93 | },
94 |
95 | 'post::/logout': function logout(req, res) {
96 | req.logout()
97 | res.redirect('/account/login')
98 | },
99 |
100 | 'get::/account/login': function login(req, res) {
101 | res.render('login')
102 | },
103 |
104 | /**
105 | * Login form handler
106 | */
107 | 'post::/account/login': [passportAuth, function loginOkay(req, res) {
108 | res.redirect('/account/protected')
109 | }],
110 |
111 | '/account/exit': function(req, res) {
112 | res.send('ok')
113 | setTimeout(function() {
114 | process.exit(0)
115 | }, 1000)
116 | }
117 | },
118 |
119 | init: function(app) {
120 | // attach the connect auth middleware to our local express app
121 | app.use(connectAuth)
122 |
123 | // setup template engine
124 | app.set('views', path.join(__dirname, './view'))
125 | app.set('view engine', 'jade')
126 |
127 | return true
128 | },
129 |
130 | getUserById: function(id, callback) {
131 | if (id === 1) {
132 | callback({
133 | id: 1,
134 | username: 'micromono',
135 | password: '123456'
136 | })
137 | } else {
138 | callback(null)
139 | }
140 | },
141 |
142 |
143 |
144 | api: {
145 | getUserById: function(id, callback) {
146 | this.getUserById(id, callback)
147 | },
148 | getMultiArgs: function (arg1, arg2, callback) {
149 | assert.equal(arg1, 1)
150 | assert.equal(arg2.key, 'value')
151 | callback(null, [1, '2', Buffer('hello')])
152 | }
153 | }
154 | }
155 |
156 | // Start the service if this is the main file
157 | if (require.main === module) {
158 | micromono.startService(Account)
159 | }
160 |
--------------------------------------------------------------------------------
/example/rpc-adapters/zmq.js:
--------------------------------------------------------------------------------
1 | var zmq = require('zmq')
2 | var debug = require('debug')('micromono:rpc:zmq')
3 | var shortid = require('shortid')
4 | var toArray = require('lodash.toarray')
5 |
6 |
7 | module.exports = {
8 |
9 | client: {
10 |
11 | send: function(data) {
12 | var args = data.args
13 |
14 | if (typeof args[args.length - 1] === 'function') {
15 | // last argument is a callback function, add callback identity to data
16 | var cid = this.generateID()
17 | data.cid = cid
18 | this.callbacks[cid] = args.pop()
19 | }
20 |
21 | var msg = this.encodeData(data)
22 | this.socket.send(msg)
23 | },
24 |
25 | generateID: function() {
26 | var id = shortid.generate()
27 |
28 | if (!this.callbacks[id]) {
29 | this.callbacks[id] = null
30 | return id
31 | } else {
32 | return this.generateID()
33 | }
34 | },
35 |
36 | dispatch: function(msg) {
37 | var data = this.decodeData(msg)
38 |
39 | if (data.cid) {
40 | var args = data.args
41 | var callback = this.callbacks[data.cid]
42 | if (typeof callback === 'function') {
43 | callback.apply(this, args)
44 | }
45 | }
46 | },
47 |
48 | connect: function(provider) {
49 | var endpoint = 'tcp://' + provider.host + ':' + provider.rpc.port
50 |
51 | if (!this.socket) {
52 | var socket = zmq.socket('dealer')
53 | var self = this
54 | socket.identity = shortid.generate()
55 | socket.monitor(100, 0)
56 | socket.on('disconnect', function(fd, ep) {
57 | debug('zmq provider %s disconnected', ep)
58 | self.onProviderDisconnect(provider)
59 | })
60 | socket.on('data', function(msg) {
61 | self.dispatch(msg)
62 | })
63 | this.socket = socket
64 | }
65 |
66 | this.socket.connect(endpoint)
67 | }
68 | },
69 |
70 | server: {
71 |
72 | /**
73 | * Dispatch message to local route/api handler
74 | *
75 | * @param {String} msg A JSON string with following properties:
76 | * {
77 | * // when there's callback in the function signature
78 | * cid: 'Vk7HgAGv',
79 | * // name of the RPC api
80 | * name: 'createPost'
81 | * // input arguments for the api
82 | * // A callback will be generated and
83 | * // pushed to the end of `args` if `cid` exists
84 | * args: ['this', 'is', 'data'],
85 | *
86 | * }
87 | * @param {String} envelope String identity of the sending client
88 | */
89 | dispatch: function(msg, envelope) {
90 | var data = this.decodeData(msg)
91 | var args = data.args || []
92 | var handler = this.getHandler(data.name)
93 | var self = this
94 |
95 | if (data.cid) {
96 | var callback = function() {
97 | var _args = toArray(arguments)
98 | var _data = {
99 | cid: data.cid,
100 | args: _args
101 | }
102 | self.socket.send([envelope, self.encodeData(_data)])
103 | }
104 | args.push(callback)
105 | }
106 |
107 | handler.apply(this, args)
108 | },
109 |
110 | startRPCServer: function(port) {
111 | var self = this
112 |
113 | var _port = 'tcp://0.0.0.0:' + port
114 | var socket = zmq.socket('router')
115 | var ann = this.announcement
116 | ann.rpcPort = port
117 | ann.rpcType = 'zmq'
118 | socket.identity = ann.name + '::' + _port
119 | self.socket = socket
120 |
121 | return new Promise(function(resolve, reject) {
122 | socket.bind(_port, function(err) {
123 | if (err) {
124 | return reject(err)
125 | }
126 |
127 | socket.on('message', function(envelope, msg) {
128 | self.dispatch(msg, envelope)
129 | })
130 |
131 | resolve()
132 | })
133 | })
134 | }
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/lib/web/asset/jspm.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var vm = require('vm')
3 | var path = require('path')
4 | var logger = require('../../logger')('micromono:asset:jspm')
5 | var spawn = require('child_process').spawn
6 | var assign = require('lodash.assign')
7 |
8 |
9 | exports.getJSPMConfig = function(assetInfo, publicPath, next) {
10 | var configPath = path.join(publicPath, 'config.js')
11 |
12 | logger.debug('getJSPMConfig', {
13 | configPath: configPath
14 | })
15 |
16 | fs.readFile(configPath, 'utf8', function(err, configCode) {
17 | if (err || !configCode) {
18 | next(err || 'config.js has no content', {
19 | jspmConfig: null,
20 | jspmConfigPath: configPath
21 | })
22 | return
23 | }
24 |
25 | var sandbox = {
26 | System: {
27 | config: function(cfg) {
28 | return cfg
29 | }
30 | },
31 | config: null
32 | }
33 | var script = new vm.Script('config = ' + configCode)
34 | script.runInNewContext(sandbox)
35 | var jspmConfig = sandbox.config
36 | if (jspmConfig)
37 | assetInfo.bundles = jspmConfig && jspmConfig.bundles
38 | next(null, {
39 | jspmConfig: jspmConfig,
40 | jspmConfigPath: configPath
41 | })
42 | })
43 | }
44 |
45 | exports.prefixJSPMBundles = function(assetInfo) {
46 | var bundles = assetInfo.bundles
47 |
48 | if (bundles) {
49 | logger.debug('prefixJSPMBundles')
50 |
51 | var publicURL = assetInfo.publicURL[0]
52 | if (publicURL) {
53 | Object.keys(bundles).forEach(function(name) {
54 | logger.debug(publicURL, name)
55 | var b = bundles[name]
56 | delete bundles[name]
57 | name = path.join(publicURL, name)
58 | bundles[name] = b
59 | })
60 | }
61 |
62 | logger.debug('Prefixed bundles', bundles)
63 | }
64 |
65 | return {
66 | assetBundles: bundles
67 | }
68 | }
69 |
70 | exports.updateJSPMConfig = function(jspmConfigPath, jspmConfig, updateOpts, next) {
71 | logger.debug('updateJSPMConfig', {
72 | jspmConfigPath: jspmConfigPath
73 | })
74 |
75 | // In case we just need to rewrite the config.js with consistent double quotes.
76 | updateOpts = updateOpts || {}
77 |
78 | // Note: the systemjs loader need `CSS` upper case.
79 | if (updateOpts.hasOwnProperty('buildCss'))
80 | jspmConfig.buildCSS = updateOpts.buildCss || false
81 |
82 | if (updateOpts.hasOwnProperty('separateCss'))
83 | jspmConfig.separateCSS = updateOpts.separateCss || false
84 |
85 | if (updateOpts.bundles)
86 | jspmConfig.bundles = assign(jspmConfig.bundles, updateOpts.bundles)
87 |
88 | fs.writeFile(jspmConfigPath, 'System.config(' + JSON.stringify(jspmConfig, null, 2) + ');', next)
89 | }
90 |
91 | exports.getJSPMBinPath = function(packagePath) {
92 | var locations = [
93 | // From service/node_modules
94 | path.join(packagePath, '/node_modules/.bin/jspm'),
95 | // From micromono/node_modules
96 | path.join(__dirname, '../../../', '/node_modules/.bin/jspm'),
97 | // From ../node_modules
98 | path.join(__dirname, '../../../../', '/.bin/jspm')
99 | ]
100 |
101 | var jspmBinPath
102 |
103 | locations.some(function(p) {
104 | if (isExecutable(p)) {
105 | jspmBinPath = p
106 | return true
107 | }
108 | })
109 |
110 | if (!jspmBinPath)
111 | logger.debug('Using global `jspm`. Can not be found locally:', locations)
112 |
113 | return {
114 | jspmBinPath: jspmBinPath || 'jspm'
115 | }
116 | }
117 |
118 | exports.runJSPM = function(packagePath, jspmBinPath, spwanArgs, next) {
119 | logger.debug('runJSPM', {
120 | jspmBinPath: jspmBinPath,
121 | spwanArgs: spwanArgs,
122 | packagePath: packagePath
123 | })
124 |
125 | var childOpts = {
126 | cwd: packagePath,
127 | stdio: [process.stdin, process.stdout, process.stderr]
128 | }
129 | var child = spawn(jspmBinPath, spwanArgs, childOpts)
130 | child.on('exit', function(code) {
131 | next(0 === code ? undefined : code)
132 | }).on('error', next)
133 | }
134 |
135 | exports.jspmInstall = function(packagePath, jspmBinPath, next) {
136 | exports.runJSPM(packagePath, jspmBinPath, ['install', '-y', '--lock'], next)
137 | }
138 |
139 | function isExecutable(filePath) {
140 | try {
141 | fs.accessSync(filePath)
142 | return true
143 | } catch (e) {
144 | return false
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/example/docker-compose.yml:
--------------------------------------------------------------------------------
1 | installation:
2 | image: node:6
3 | command: node /opt/index.js
4 | working_dir: /opt
5 | volumes:
6 | - ./:/opt
7 | - ../:/opt/micromono
8 | environment:
9 | - DEBUG=micromono*
10 | - MICROMONO_LOG_LEVEL=debug
11 | - NODE_ENV=development
12 | - NPM_CONFIG_LOGLEVEL=warn
13 |
14 | micromono:
15 | extends: installation
16 | volumes:
17 | - ../:/opt/node_modules/micromono
18 |
19 | #
20 | # UDP discovery backend
21 | #
22 |
23 | balancer:
24 | extends: micromono
25 | command: node /opt/balancer/server.js
26 | ports:
27 | - "3000:3000"
28 | links:
29 | - account
30 | - channel
31 | - home
32 | - io
33 | # volumes:
34 | # - ../../socketmq:/opt/balancer/public/jspm_packages/npm/socketmq@0.7.1
35 | environment:
36 | - MICROMONO_PORT=3000
37 | - MICROMONO_SERVICES=account,channel,home,io
38 |
39 | account:
40 | extends: micromono
41 | ports:
42 | - "4545:4545"
43 | command: node /opt/account/index.js --health-port 4545
44 |
45 | channel:
46 | extends: micromono
47 | command: node /opt/channel/index.js
48 |
49 | home:
50 | extends: micromono
51 | ports:
52 | - "4646:4646"
53 | command: node /opt/home/index.js --health-port 4646
54 |
55 | io:
56 | extends: micromono
57 | command: node /opt/io/index.js
58 |
59 | #
60 | # nightwatch + selenium
61 | #
62 |
63 | balancer-istanbul:
64 | extends: micromono
65 | command: istanbul cover --config /opt/.istanbul.yml --dir /opt/balancer/coverage /opt/balancer/server.js
66 | ports:
67 | - "3000:3000"
68 | links:
69 | - account
70 | - channel
71 | - home
72 | - io
73 | environment:
74 | - MICROMONO_SERVICES=account,channel,home,io
75 |
76 | account-istanbul:
77 | extends: micromono
78 | command: istanbul cover --config /opt/.istanbul.yml --dir /opt/account/coverage /opt/account/index.js
79 |
80 | channel-istanbul:
81 | extends: micromono
82 | command: istanbul cover --config /opt/.istanbul.yml --dir /opt/channel/coverage /opt/channel/index.js
83 |
84 | home-istanbul:
85 | extends: micromono
86 | command: istanbul cover --config /opt/.istanbul.yml --dir /opt/home/coverage /opt/home/index.js
87 |
88 | io-istanbul:
89 | extends: micromono
90 | command: istanbul cover --config /opt/.istanbul.yml --dir /opt/io/coverage /opt/io/index.js
91 |
92 | hub:
93 | image: selenium/hub:3
94 | chrome:
95 | image: selenium/node-chrome:3
96 | links:
97 | - hub
98 | - balancer
99 | chromedebug:
100 | image: selenium/node-chrome-debug:3
101 | links:
102 | - hub
103 | - balancer
104 | ports:
105 | - "5900:5900"
106 | firefox:
107 | image: selenium/node-firefox:3
108 | links:
109 | - hub
110 | - balancer-istanbul
111 | firefoxdebug:
112 | image: selenium/node-firefox-debug:3
113 | links:
114 | - hub
115 | - balancer
116 | ports:
117 | - 5901:5900
118 | nightwatch:
119 | image: blueimp/nightwatch:0.9
120 | links:
121 | - hub
122 | volumes:
123 | - ./test/e2e:/home/node
124 | environment:
125 | - APP_URL=http://balancer:3000
126 |
127 |
128 | #
129 | # NATS as service discovery backend
130 | #
131 |
132 | balancer-nats:
133 | extends: micromono
134 | command: node /opt/balancer/server.js --services account,channel,home,io
135 | links:
136 | - account-nats
137 | - channel-nats
138 | - home-nats
139 | - io-nats
140 | - nats:nats.dev
141 | environment:
142 | - MICROMONO_DISCOVERY_BACKEND=nats
143 | - MICROMONO_DISCOVERY_NATS_SERVERS=nats://nats.dev:4222
144 |
145 | nats:
146 | image: nats:0.9.6
147 |
148 | account-nats:
149 | extends: account
150 | links:
151 | - nats:nats.dev
152 | environment:
153 | - MICROMONO_DISCOVERY_BACKEND=nats
154 | - MICROMONO_DISCOVERY_NATS_SERVERS=nats://nats.dev:4222
155 |
156 | home-nats:
157 | extends: home
158 | links:
159 | - nats:nats.dev
160 | environment:
161 | - MICROMONO_DISCOVERY_BACKEND=nats
162 | - MICROMONO_DISCOVERY_NATS_SERVERS=nats://nats.dev:4222
163 |
164 | io-nats:
165 | extends: io
166 | links:
167 | - nats:nats.dev
168 | environment:
169 | - MICROMONO_DISCOVERY_BACKEND=nats
170 | - MICROMONO_DISCOVERY_NATS_SERVERS=nats://nats.dev:4222
171 |
172 | channel-nats:
173 | extends: channel
174 | links:
175 | - nats:nats.dev
176 | environment:
177 | - MICROMONO_DISCOVERY_BACKEND=nats
178 | - MICROMONO_DISCOVERY_NATS_SERVERS=nats://nats.dev:4222
179 |
--------------------------------------------------------------------------------
/lib/discovery/index.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var logger = require('../logger')('micromono:discovery')
3 | var assign = require('lodash.assign')
4 | var Router = require('../web/router')
5 | var spawnSync = require('child_process').spawnSync
6 | var RemotePipe = require('../service/remote')
7 |
8 |
9 | exports.require = function(serviceName, serviceDir) {
10 | var service
11 | var servicePath = serviceName
12 | serviceDir = serviceDir || this.serviceDir
13 |
14 | var ServiceClass = exports.localRequire(serviceName, serviceDir, this.services)
15 |
16 | if (false === ServiceClass) {
17 | logger.info('Failed to locate service locally, try to discover from network.', {
18 | service: serviceName
19 | })
20 | ServiceClass = exports.remoteRequire(this, serviceName, serviceDir)
21 | }
22 |
23 | if ('function' === typeof ServiceClass)
24 | service = new ServiceClass()
25 | else
26 | service = ServiceClass
27 |
28 | service.name = serviceName
29 | if (servicePath)
30 | service.packagePath = servicePath
31 | this.register(service, serviceName)
32 |
33 | return service
34 | }
35 |
36 | exports.localRequire = function(serviceName, serviceDir, services) {
37 | var ServiceClass
38 | var servicePath = serviceName
39 |
40 | try {
41 | if (serviceDir) {
42 | servicePath = path.resolve(serviceDir, serviceName)
43 | logger.debug('Resolved service path', {
44 | path: servicePath,
45 | service: serviceName
46 | })
47 | }
48 | if (services[serviceName]) {
49 | logger.debug('Service already required', {
50 | service: serviceName
51 | })
52 | return services[serviceName]
53 | } else {
54 | logger.info('Require service locally', {
55 | path: servicePath,
56 | service: serviceName
57 | })
58 | ServiceClass = require(servicePath)
59 | }
60 | } catch (e) {
61 | var expectedMessage = new RegExp('Cannot find module \'' + servicePath + '\'')
62 | if ('MODULE_NOT_FOUND' !== e.code || !expectedMessage.test(e.message))
63 | // throw error if we found the module which contains error.
64 | throw e
65 | else
66 | return false
67 | }
68 |
69 | return ServiceClass
70 | }
71 |
72 | exports.remoteRequire = function(micromono, serviceName, serviceDir) {
73 | var proberPath
74 | var ServiceClass
75 | var proberCommand
76 | var discoveryOptions = exports.getDiscoveryOptions(micromono)
77 |
78 | var args = ['--discovery-target', serviceName]
79 | args = args.concat(process.argv.slice(2))
80 |
81 | if (discoveryOptions.MICROMONO_DISCOVERY_AGENT_PATH) {
82 | // Customized discovery agent.
83 | proberCommand = discoveryOptions.MICROMONO_DISCOVERY_AGENT_PATH
84 | // Tell the child process this suppose to be a discovery agent.
85 | args.push('--discovery-agent')
86 | } else {
87 | // Use default discovery agent.
88 | proberPath = require.resolve('./prober')
89 | proberCommand = 'node'
90 | args.unshift(proberPath)
91 | }
92 |
93 | logger.debug('Probing remote service', {
94 | service: serviceName,
95 | proberCommand: proberCommand,
96 | args: args.join
97 | })
98 |
99 | var probedResult = spawnSync(proberCommand, args, {
100 | env: assign({}, process.env, discoveryOptions),
101 | stdio: ['inherit', 'pipe', 'inherit']
102 | })
103 |
104 | if (255 === probedResult.status) {
105 | logger.fatal('Stopped discovering service\n', {
106 | service: serviceName
107 | })
108 | process.exit(probedResult.status)
109 | } else if (0 !== probedResult.status) {
110 | logger.fatal('Service probing error', {
111 | service: serviceName,
112 | status: probedResult.status,
113 | stdout: probedResult.stdout.toString()
114 | })
115 | process.exit(probedResult.status)
116 | } else {
117 | try {
118 | var announcement = JSON.parse(probedResult.stdout)
119 | logger.info('Service probed from network', {
120 | service: announcement.name,
121 | version: announcement.version
122 | })
123 | ServiceClass = RemotePipe.buildServiceFromAnnouncement(announcement)
124 | if (ServiceClass.middleware)
125 | Router.rebuildRemoteMiddlewares(ServiceClass.middleware, ServiceClass)
126 | } catch (e) {
127 | logger.error('Invalid announcement data', {
128 | service: serviceName,
129 | stdout: probedResult.stdout.toString(),
130 | error: e
131 | })
132 | return exports.remoteRequire(micromono, serviceName, serviceDir)
133 | }
134 | }
135 |
136 | return ServiceClass
137 | }
138 |
139 | exports.register = function(serviceInstance, name) {
140 | logger.debug('Register service instance', {
141 | service: name
142 | })
143 | this.services[name] = serviceInstance
144 | return serviceInstance
145 | }
146 |
147 | exports.getDiscoveryOptions = function(micromono) {
148 | var keys = [
149 | 'MICROMONO_DISCOVERY_BACKEND',
150 | 'MICROMONO_DISCOVERY_TIMEOUT',
151 | 'MICROMONO_DISCOVERY_ANNOUNCE_INTERVAL',
152 | 'MICROMONO_DISCOVERY_AGENT_PATH',
153 | 'MICROMONO_DISCOVERY_UDP_MULTICAST',
154 | 'MICROMONO_DISCOVERY_UDP_PORT',
155 | 'MICROMONO_DISCOVERY_NATS_SERVERS'
156 | ]
157 | var options = {}
158 |
159 | keys.forEach(function(key) {
160 | var value = micromono.get(key)
161 | if (value)
162 | options[key] = value
163 | })
164 |
165 | return options
166 | }
167 |
--------------------------------------------------------------------------------
/bin/micromono-bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path')
4 | var program = require('cmdenv')('micromono')
5 | var SuperPipe = require('superpipe')
6 | var AssetPipe = require('../lib/web/asset')
7 | var bundleAsset = require('../lib/web/asset/pipeline').bundleAsset
8 | var LocalServicePipe = require('../lib/service/local')
9 |
10 |
11 | program
12 | .usage('[OPTIONS] path')
13 | .description('Bundle asset files for a service')
14 | .option('-d --bundle-deps', 'Include dependencies when bundling. Default false.')
15 | .option('-o --out-file [path]', 'Set the path of the output file.')
16 | .option('--source-maps', 'Enable source maps. Default false.')
17 | .option('-m --source-map-contents', 'Enable source maps. Default false.')
18 | .option('--low-res-source-maps', 'Generate low resolution source maps. Default false.')
19 | .option('-i --inject', 'Inject bundle info into `config.js`. Default false.')
20 | .option('-z --minify', 'Minify the output files. Default false.')
21 | .option('-g --mangle', 'Default false.')
22 | .option('-c --bundle-css', 'Bundle CSS files. Default false.')
23 | .option('-s --separate-css', 'Bundle CSS into a separate file. Default false.')
24 | .option('--production', 'Enable all options suitable for production.')
25 | .option('--common-bundles', 'Bundle dependencies based on the value of the '
26 | + 'commonBundles property defined in package.json. Default false.')
27 | .parse(process.argv)
28 |
29 | var servicePath = program.args[0]
30 | if (servicePath) {
31 | servicePath = path.resolve(servicePath)
32 |
33 | var superpipe = new SuperPipe()
34 | superpipe
35 | .setDep(AssetPipe, '*^')
36 | .setDep(LocalServicePipe, '*^')
37 | .setDep('packagePath', servicePath)
38 | .setDep('service', {})
39 | .setDep('errorHandler', function(err, errPipeName) {
40 | console.error('[%s] `bundle` error', errPipeName, err && err.stack || err)
41 | process.exit(-1)
42 | })
43 |
44 | var options = {
45 | bundleDeps: program.bundleDeps || false,
46 | sourceMaps: program.sourceMaps || false,
47 | sourceMapContents: program.sourceMapContents || false,
48 | lowResSourceMaps: program.lowResSourceMaps || false,
49 | inject: program.inject || false,
50 | minify: program.minify || false,
51 | mangle: program.mangle || false,
52 | buildCss: program.buildCss || false,
53 | separateCss: program.separateCss || false,
54 | commonBundles: program.commonBundles || false
55 | }
56 |
57 | if (program.production) {
58 | options = {
59 | bundleDeps: program.bundleDeps || false,
60 | sourceMaps: program.sourceMaps || false,
61 | sourceMapContents: program.sourceMapContents || false,
62 | lowResSourceMaps: program.lowResSourceMaps || true,
63 | inject: program.inject || true,
64 | minify: program.minify || true,
65 | mangle: program.mangle || true,
66 | buildCss: program.buildCss || true,
67 | separateCss: program.separateCss || true,
68 | commonBundles: program.commonBundles || false
69 | }
70 | }
71 |
72 | if (program.outFile)
73 | options.outFile = program.outFile
74 |
75 | console.log('bundleOptions:', options)
76 |
77 | superpipe.setDep('bundleOptions', options)
78 |
79 | var bundlePipeline
80 | if (options.commonBundles) {
81 | var pubPath
82 | superpipe.setDep('bundleCommon', function(bundle, deps, outFile, assetInfo, packagePath, bundleOptions, jspmBinPath, setDep) {
83 | if (!deps || !Array.isArray(deps) || 0 === deps.length) {
84 | console.log('Nothing to bundle for %s', outFile)
85 | return true
86 | }
87 | var bundleCmd = 'bundle ' + deps.join(' + ')
88 | bundleOptions.outFile = path.join(pubPath, outFile)
89 | bundleCmd += AssetPipe.convertBundleOptionsToStr(bundleOptions)
90 | bundle(assetInfo, packagePath, jspmBinPath, bundleCmd, bundleOptions, setDep)
91 | })
92 |
93 | superpipe.setDep('cleanAssetInfo', function(assetInfo) {
94 | // Common bundle files should not be set as main bundle files
95 | assetInfo.bundleJs = ''
96 | assetInfo.bundleCss = ''
97 | })
98 |
99 | bundlePipeline = bundleAsset.slice(0, 4).connect(superpipe)
100 | .pipe(function(assetInfo, publicPath, setDep) {
101 | pubPath = publicPath
102 | setDep({
103 | 'outCommon70': 'bundle-common70.js',
104 | 'outCommon50': 'bundle-common50.js',
105 | 'outCommon30': 'bundle-common30.js',
106 | 'outCommon0': 'bundle-common0.js'
107 | })
108 | setDep(assetInfo.commonBundles)
109 | }, ['assetInfo', 'publicPath', 'setDep'])
110 | .pipe('bundleCommon', ['bundle', 'common0', 'outCommon0',
111 | 'assetInfo', 'packagePath', 'bundleOptions', 'jspmBinPath', 'setDep'])
112 | .pipe('bundleCommon', ['bundle', 'common30', 'outCommon30',
113 | 'assetInfo', 'packagePath', 'bundleOptions', 'jspmBinPath', 'setDep'])
114 | .pipe('bundleCommon', ['bundle', 'common50', 'outCommon50',
115 | 'assetInfo', 'packagePath', 'bundleOptions', 'jspmBinPath', 'setDep'])
116 | .pipe('bundleCommon', ['bundle', 'common70', 'outCommon70',
117 | 'assetInfo', 'packagePath', 'bundleOptions', 'jspmBinPath', 'setDep'])
118 | .pipe('cleanAssetInfo', 'assetInfo')
119 | .pipe('updatePackageJSON', ['assetInfo', 'packagePath', 'packageJSON', 'next'])
120 | } else {
121 | bundlePipeline = bundleAsset.clone(superpipe)
122 | .pipe(function(bundleCmd) {
123 | console.log('jspm ' + bundleCmd)
124 | console.log('Bundled successfully')
125 | process.exit(0)
126 | }, ['bundleCmd'])
127 | }
128 | bundlePipeline.error('errorHandler', ['error', 'errPipeName'])()
129 | } else {
130 | program.outputHelp()
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/example/io/public/config.js:
--------------------------------------------------------------------------------
1 | System.config({
2 | defaultJSExtensions: true,
3 | transpiler: "babel",
4 | babelOptions: {
5 | "optional": [
6 | "runtime",
7 | "optimisation.modules.system"
8 | ]
9 | },
10 | paths: {
11 | "github:*": "jspm_packages/github/*",
12 | "npm:*": "jspm_packages/npm/*"
13 | },
14 |
15 | map: {
16 | "babel": "npm:babel-core@5.8.34",
17 | "babel-runtime": "npm:babel-runtime@5.8.34",
18 | "core-js": "npm:core-js@1.2.6",
19 | "socket.io-client": "npm:socket.io-client@1.4.8",
20 | "github:jspm/nodelibs-assert@0.1.0": {
21 | "assert": "npm:assert@1.4.1"
22 | },
23 | "github:jspm/nodelibs-buffer@0.1.0": {
24 | "buffer": "npm:buffer@3.6.0"
25 | },
26 | "github:jspm/nodelibs-path@0.1.0": {
27 | "path-browserify": "npm:path-browserify@0.0.0"
28 | },
29 | "github:jspm/nodelibs-process@0.1.2": {
30 | "process": "npm:process@0.11.8"
31 | },
32 | "github:jspm/nodelibs-util@0.1.0": {
33 | "util": "npm:util@0.10.3"
34 | },
35 | "github:jspm/nodelibs-vm@0.1.0": {
36 | "vm-browserify": "npm:vm-browserify@0.0.4"
37 | },
38 | "npm:assert@1.4.1": {
39 | "assert": "github:jspm/nodelibs-assert@0.1.0",
40 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
41 | "process": "github:jspm/nodelibs-process@0.1.2",
42 | "util": "npm:util@0.10.3"
43 | },
44 | "npm:babel-runtime@5.8.34": {
45 | "process": "github:jspm/nodelibs-process@0.1.2"
46 | },
47 | "npm:benchmark@1.0.0": {
48 | "process": "github:jspm/nodelibs-process@0.1.2"
49 | },
50 | "npm:better-assert@1.0.2": {
51 | "assert": "github:jspm/nodelibs-assert@0.1.0",
52 | "callsite": "npm:callsite@1.0.0",
53 | "fs": "github:jspm/nodelibs-fs@0.1.2",
54 | "process": "github:jspm/nodelibs-process@0.1.2"
55 | },
56 | "npm:buffer@3.6.0": {
57 | "base64-js": "npm:base64-js@0.0.8",
58 | "child_process": "github:jspm/nodelibs-child_process@0.1.0",
59 | "fs": "github:jspm/nodelibs-fs@0.1.2",
60 | "ieee754": "npm:ieee754@1.1.6",
61 | "isarray": "npm:isarray@1.0.0",
62 | "process": "github:jspm/nodelibs-process@0.1.2"
63 | },
64 | "npm:core-js@1.2.6": {
65 | "fs": "github:jspm/nodelibs-fs@0.1.2",
66 | "path": "github:jspm/nodelibs-path@0.1.0",
67 | "process": "github:jspm/nodelibs-process@0.1.2",
68 | "systemjs-json": "github:systemjs/plugin-json@0.1.2"
69 | },
70 | "npm:debug@2.2.0": {
71 | "ms": "npm:ms@0.7.1"
72 | },
73 | "npm:engine.io-client@1.6.11": {
74 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
75 | "component-emitter": "npm:component-emitter@1.1.2",
76 | "component-inherit": "npm:component-inherit@0.0.3",
77 | "debug": "npm:debug@2.2.0",
78 | "engine.io-parser": "npm:engine.io-parser@1.2.4",
79 | "has-cors": "npm:has-cors@1.1.0",
80 | "indexof": "npm:indexof@0.0.1",
81 | "parsejson": "npm:parsejson@0.0.1",
82 | "parseqs": "npm:parseqs@0.0.2",
83 | "parseuri": "npm:parseuri@0.0.4",
84 | "yeast": "npm:yeast@0.1.2"
85 | },
86 | "npm:engine.io-parser@1.2.4": {
87 | "after": "npm:after@0.8.1",
88 | "arraybuffer.slice": "npm:arraybuffer.slice@0.0.6",
89 | "base64-arraybuffer": "npm:base64-arraybuffer@0.1.2",
90 | "blob": "npm:blob@0.0.4",
91 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
92 | "has-binary": "npm:has-binary@0.1.6",
93 | "utf8": "npm:utf8@2.1.0"
94 | },
95 | "npm:has-binary@0.1.6": {
96 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
97 | "fs": "github:jspm/nodelibs-fs@0.1.2",
98 | "isarray": "npm:isarray@0.0.1"
99 | },
100 | "npm:has-binary@0.1.7": {
101 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
102 | "fs": "github:jspm/nodelibs-fs@0.1.2",
103 | "isarray": "npm:isarray@0.0.1"
104 | },
105 | "npm:inherits@2.0.1": {
106 | "util": "github:jspm/nodelibs-util@0.1.0"
107 | },
108 | "npm:parsejson@0.0.1": {
109 | "better-assert": "npm:better-assert@1.0.2"
110 | },
111 | "npm:parseqs@0.0.2": {
112 | "better-assert": "npm:better-assert@1.0.2"
113 | },
114 | "npm:parseuri@0.0.4": {
115 | "better-assert": "npm:better-assert@1.0.2"
116 | },
117 | "npm:path-browserify@0.0.0": {
118 | "process": "github:jspm/nodelibs-process@0.1.2"
119 | },
120 | "npm:process@0.11.8": {
121 | "assert": "github:jspm/nodelibs-assert@0.1.0",
122 | "fs": "github:jspm/nodelibs-fs@0.1.2",
123 | "vm": "github:jspm/nodelibs-vm@0.1.0"
124 | },
125 | "npm:socket.io-client@1.4.8": {
126 | "backo2": "npm:backo2@1.0.2",
127 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
128 | "component-bind": "npm:component-bind@1.0.0",
129 | "component-emitter": "npm:component-emitter@1.2.0",
130 | "debug": "npm:debug@2.2.0",
131 | "engine.io-client": "npm:engine.io-client@1.6.11",
132 | "has-binary": "npm:has-binary@0.1.7",
133 | "indexof": "npm:indexof@0.0.1",
134 | "object-component": "npm:object-component@0.0.3",
135 | "parseuri": "npm:parseuri@0.0.4",
136 | "socket.io-parser": "npm:socket.io-parser@2.2.6",
137 | "to-array": "npm:to-array@0.1.4"
138 | },
139 | "npm:socket.io-parser@2.2.6": {
140 | "benchmark": "npm:benchmark@1.0.0",
141 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
142 | "component-emitter": "npm:component-emitter@1.1.2",
143 | "debug": "npm:debug@2.2.0",
144 | "isarray": "npm:isarray@0.0.1",
145 | "json3": "npm:json3@3.3.2"
146 | },
147 | "npm:utf8@2.1.0": {
148 | "systemjs-json": "github:systemjs/plugin-json@0.1.2"
149 | },
150 | "npm:util@0.10.3": {
151 | "inherits": "npm:inherits@2.0.1",
152 | "process": "github:jspm/nodelibs-process@0.1.2"
153 | },
154 | "npm:vm-browserify@0.0.4": {
155 | "indexof": "npm:indexof@0.0.1"
156 | }
157 | }
158 | });
159 |
--------------------------------------------------------------------------------
/lib/service/local.js:
--------------------------------------------------------------------------------
1 | var RPC = require('../api/rpc')
2 | var path = require('path')
3 | var util = require('util')
4 | var logger = require('../logger')('micromono:service:local')
5 | var crypto = require('crypto')
6 | var Router = require('../web/router')
7 | var Superpipe = require('superpipe')
8 | var argsNames = require('js-args-names')
9 | var Announcement = require('./announcement')
10 |
11 |
12 | exports.getPackageJSON = function(packagePath) {
13 | var pjsonPath = path.join(packagePath, 'package.json')
14 | var packageJSON
15 |
16 | try {
17 | packageJSON = require(pjsonPath)
18 | } catch (e) {
19 | // Set default settings when failed to load package.json
20 | packageJSON = {
21 | name: path.basename(packagePath),
22 | version: '0.0.0'
23 | }
24 | logger.info('Failed to load package.json. Use default settings', {
25 | packageJSONPath: pjsonPath,
26 | service: packageJSON
27 | })
28 | }
29 |
30 | return {
31 | packageJSON: packageJSON
32 | }
33 | }
34 |
35 | exports.getServiceInfo = function(packageJSON, service) {
36 | var serviceName = packageJSON.name
37 | if (packageJSON.micromono && packageJSON.micromono.name)
38 | serviceName = packageJSON.micromono.name
39 |
40 | service.name = serviceName
41 | service.version = packageJSON.version
42 |
43 | return {
44 | hasAsset: packageJSON.jspm,
45 | serviceName: serviceName,
46 | serviceInfo: packageJSON.micromono,
47 | serviceVersion: packageJSON.version
48 | }
49 | }
50 |
51 | exports.prepareService = function(hasAsset, service) {
52 | var hasWebFeature = !!(hasAsset || service.route || service.middleware)
53 | return {
54 | api: service.api,
55 | use: service.use,
56 | init: service.init,
57 | page: service.page || {},
58 | route: service.route,
59 | channel: service.channel,
60 | frameworkType: hasWebFeature && (service.framework || 'express'),
61 | middleware: service.middleware,
62 | upgradeUrl: service.upgradeUrl,
63 | pageApiBaseUrl: service.pageApiBaseUrl || path.join('/_api', service.name),
64 | middlewareBaseUrl: service.middlewareBaseUrl || path.join('/_middleware', service.name)
65 | }
66 | }
67 |
68 | exports.prepareFrameworkForLocal = function(framework, set) {
69 | logger.debug('prepareFrameworkForLocal', {
70 | framework: framework.type
71 | })
72 |
73 | // App might be a function, set it directly to avoid autoBind.
74 | set('app', framework.app)
75 | set(framework, ['attachRoutes', 'attachLocalMiddlewares',
76 | 'startHttpServer', 'serveLocalAsset', 'injectAssetInfo'])
77 | }
78 |
79 | exports.setupRoute = function(route, page, pageApiBaseUrl) {
80 | return {
81 | routes: Router.normalizeRoutes(route, page, pageApiBaseUrl)
82 | }
83 | }
84 |
85 | exports.setupUse = function(use) {
86 | return {
87 | uses: Router.normalizeUses(use)
88 | }
89 | }
90 |
91 | exports.setupMiddleware = function(middleware, middlewareBaseUrl) {
92 | return {
93 | middlewares: Router.normalizeMiddlewares(middleware, middlewareBaseUrl)
94 | }
95 | }
96 |
97 | exports.setupRPC = function(api, rpcType, service) {
98 | // Bind api functions with service instance
99 | Object.keys(api).forEach(function(apiName) {
100 | var handler = api[apiName]
101 | var args = argsNames(handler)
102 | handler = handler.bind(service)
103 | handler.args = args
104 | api[apiName] = handler
105 | })
106 |
107 | var rpcOptions = {
108 | api: api,
109 | type: rpcType,
110 | isRemote: false
111 | }
112 | var rpc = new RPC(rpcOptions)
113 |
114 | return {
115 | rpc: rpc,
116 | rpcApi: rpc.getAPIs()
117 | }
118 | }
119 |
120 | exports.startRPCServer = function(rpc, rpcPort, rpcHost, next) {
121 | rpc.startServer(rpcPort, rpcHost, function(err, server) {
122 | if (!err && server)
123 | rpcPort = server.address().port
124 | next(err, {
125 | rpcPort: rpcPort
126 | })
127 | })
128 | }
129 |
130 | exports.getServiceInitPipeline = function(service) {
131 | var initPipeline = Superpipe.pipeline()
132 | if (service.init) {
133 | var srvInit = service.init
134 | if ('function' === typeof srvInit) {
135 | var initArgs = argsNames(srvInit)
136 | initPipeline.pipe(srvInit.bind(service), initArgs)
137 | } else if (Array.isArray(srvInit)) {
138 | initPipeline.pipe(srvInit[0].bind(service), srvInit[1], srvInit[2])
139 | }
140 | }
141 | return initPipeline
142 | }
143 |
144 | exports.generateAnnouncement = function(service, serviceInfo, host, web, api, channel) {
145 | var ann = new Announcement(service)
146 | ann.host = host
147 | if (web.asset)
148 | ann.asset = web.asset
149 | ann.timeout = 10000
150 |
151 | if (serviceInfo)
152 | ann.timeout = serviceInfo.timeout || ann.timeout
153 |
154 | if (web.use || web.route || web.middleware || web.asset) {
155 | var framework = web.framework
156 | web.framework = framework && framework.type || framework
157 | web.upgradeUrl = service.upgradeUrl
158 | delete web.asset
159 | ann.web = web
160 | }
161 |
162 | if (api.handlers && api.port && api.type)
163 | ann.api = api
164 |
165 | if (channel && Object.keys(channel).length > 0)
166 | ann.channel = channel
167 |
168 | ann.id = crypto.createHash('sha256')
169 | .update(Date.now() + '')
170 | .update(JSON.stringify(ann))
171 | .update(crypto.randomBytes(128))
172 | .digest('hex')
173 |
174 | var annStr = util.inspect(ann, {
175 | colors: true,
176 | depth: 4
177 | })
178 |
179 | logger.info('Local service started', {
180 | service: service.name,
181 | version: service.version
182 | }).debug(annStr)
183 |
184 | return {
185 | announcement: ann
186 | }
187 | }
188 |
189 | exports.announceService = function(announcement, discoveryAnnounce, discoveryOptions) {
190 | logger.info('Service starts announcing', {
191 | service: announcement.name,
192 | version: announcement.version,
193 | backend: discoveryOptions.MICROMONO_DISCOVERY_BACKEND
194 | })
195 | discoveryAnnounce(announcement, discoveryOptions)
196 | }
197 |
--------------------------------------------------------------------------------
/lib/web/asset/bundle.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var path = require('path')
3 | var jspm = require('./jspm')
4 | var logger = require('../../logger')('micromono:asset:bundle')
5 | var assign = require('lodash.assign')
6 |
7 |
8 | exports.prepareBundleInfo = function(assetInfo, publicPath, bundleOptions) {
9 | // Prepare bundleOptions for jspm/systemjs builder
10 | bundleOptions.name = assetInfo.name
11 | bundleOptions = getDefaultBundleOptions(bundleOptions)
12 |
13 | var entryFile = assetInfo.main
14 | var bundleCmd = ''
15 |
16 | // ignore or include dependencies
17 | var operator = true === bundleOptions.bundleDeps ? ' + ' : ' - '
18 | var deps = assetInfo.dependencies && Object.keys(assetInfo.dependencies)
19 | deps = deps || []
20 |
21 | // First we filter all dependencies found in `bundleDeps` and `ignoreDeps` in
22 | // assetInfo. Then add them back with correct operator accordingly.
23 | if (assetInfo.bundleDeps) {
24 | deps = deps.filter(function(dep) {
25 | return -1 === assetInfo.bundleDeps.indexOf(dep)
26 | })
27 | }
28 |
29 | if (assetInfo.ignoreDeps) {
30 | // Ignore service bundled deps.
31 | deps = deps.filter(function(dep) {
32 | return -1 === assetInfo.ignoreDeps.indexOf(dep)
33 | })
34 | }
35 |
36 | if (deps.length > 0)
37 | bundleCmd += deps.join(operator)
38 |
39 | if (assetInfo.bundleDeps && 0 < assetInfo.bundleDeps.length)
40 | // We should include the dependencies regardless the value of `bundleOptions.bundleDeps`
41 | bundleCmd += ' + ' + assetInfo.bundleDeps.join(' + ')
42 |
43 | if (assetInfo.ignoreDeps && 0 < assetInfo.ignoreDeps.length)
44 | // We should exclude the dependencies regardless the value of `bundleOptions.ignoreDeps`
45 | bundleCmd += ' - ' + assetInfo.ignoreDeps.join(' - ')
46 |
47 | bundleCmd = 'bundle ' + (entryFile ? [entryFile, deps.length > 0 ? operator : ''].join('') : '') + bundleCmd
48 |
49 | var outFile = bundleOptions.outFile
50 | if ('/' !== outFile[0])
51 | outFile = path.join(publicPath, outFile)
52 |
53 | // override to make sure systemjs use the correct `outFile` path
54 | bundleOptions.outFile = outFile
55 | bundleCmd += exports.convertBundleOptionsToStr(bundleOptions)
56 |
57 | return {
58 | bundleCmd: bundleCmd,
59 | bundleOptions: bundleOptions
60 | }
61 | }
62 |
63 | exports.bundle = function(assetInfo, packagePath, jspmBinPath, bundleCmd, bundleOptions, set) {
64 | var publicURL = assetInfo.publicURL[0]
65 | jspm.runJSPM(packagePath, jspmBinPath, bundleCmd.split(' '), function(err) {
66 | if (err)
67 | return set('error', err)
68 |
69 | var outFileJs = bundleOptions.outFile
70 | logger.debug('Check js output file', {
71 | outFile: outFileJs
72 | })
73 | fs.stat(outFileJs, function(err, stats) {
74 | if (stats && stats.isFile()) {
75 | logger.debug('JSPM js file bundled', {
76 | outFile: outFileJs
77 | })
78 | assetInfo.bundleJs = path.join(publicURL, path.basename(outFileJs))
79 | } else {
80 | assetInfo.bundleJs = false
81 | }
82 | set('bundleJs', assetInfo.bundleJs)
83 | })
84 |
85 | var outFileCss = outFileJs.replace(/\.js$/, '.css')
86 | logger.debug('check css output file "%s"', {
87 | outFile: outFileCss
88 | })
89 | fs.stat(outFileCss, function(err, stats) {
90 | if (stats && stats.isFile()) {
91 | logger.debug('JSPM css file bundled', {
92 | outFile: outFileCss
93 | })
94 | assetInfo.bundleCss = path.join(publicURL, path.basename(outFileCss))
95 | } else {
96 | assetInfo.bundleCss = false
97 | }
98 | set('bundleCss', assetInfo.bundleCss)
99 | })
100 | })
101 | }
102 |
103 | exports.bundleDevDependencies = function(assetInfo, publicPath, packagePath, jspmBinPath, set) {
104 | logger.info('Bundle development dependencies', {
105 | publicPath: publicPath,
106 | }).debug({
107 | jspmBinPath: jspmBinPath,
108 | packagePath: packagePath
109 | }).trace(assetInfo)
110 |
111 | if (assetInfo.dependencies) {
112 | var main = assetInfo.main
113 | assetInfo.main = ''
114 | var bundleInfo = exports.prepareBundleInfo(assetInfo, publicPath, {
115 | bundleDeps: true,
116 | sourceMaps: false,
117 | sourceMapContents: false,
118 | minify: false,
119 | inject: false,
120 | buildCss: true,
121 | separateCss: true,
122 | outFile: 'bundle-' + assetInfo.name + '-dev-deps'
123 | })
124 | assetInfo.main = main
125 | bundleInfo.bundleCmd = bundleInfo.bundleCmd.replace(new RegExp(assetInfo.main + '\s\+'), '')
126 | logger.debug('bundleInfo', bundleInfo)
127 | exports.bundle(assetInfo, packagePath, jspmBinPath, bundleInfo.bundleCmd, bundleInfo.bundleOptions, set)
128 | }
129 | }
130 |
131 | exports.convertBundleOptionsToStr = function convertBundleOptionsToStr(options) {
132 | var str = ' ' + options.outFile
133 |
134 | if (!options.sourceMaps)
135 | str += ' --skip-source-maps'
136 | if (options.sourceMapContents)
137 | str += ' --source-map-contents'
138 | if (options.inject)
139 | str += ' --inject'
140 | if (options.minify)
141 | str += ' --minify'
142 | if (!options.mangle)
143 | str += ' --no-mangle'
144 |
145 | return str
146 | }
147 |
148 | /**
149 | * Private functions
150 | */
151 |
152 | function getDefaultBundleOptions(opts, env) {
153 | opts = opts || {}
154 | var _opts = {
155 | bundleDeps: false,
156 | outFile: 'bundle' + (opts.name ? '-' + opts.name : '') + '.js',
157 | sourceMaps: 'inline',
158 | sourceMapContents: true,
159 | lowResSourceMaps: true,
160 | inject: false,
161 | minify: false,
162 | mangle: false,
163 | buildCss: true,
164 | separateCss: false
165 | }
166 |
167 | env = env || process.env.NODE_ENV
168 | // Set default options for production.
169 | if ('production' === env) {
170 | _opts.bundleDeps = true
171 | _opts.sourceMaps = false
172 | _opts.sourceMapContents = false
173 | _opts.lowResSourceMaps = false
174 | _opts.inject = true
175 | _opts.minify = true
176 | _opts.mangle = true
177 | _opts.buildCss = true
178 | _opts.separateCss = true
179 | }
180 |
181 | _opts = assign(_opts, opts)
182 | // make sure we have the `.js` suffix for outFile
183 | var ext = path.extname(_opts.outFile)
184 | if (!ext) {
185 | _opts.outFile += '.js'
186 | } else if ('.js' !== ext) {
187 | // Replace it with the .js suffix. e.g. `.jsx`.
188 | _opts.outFile = _opts.outFile.slice(0, -ext.length) + '.js'
189 | }
190 | return _opts
191 | }
192 |
--------------------------------------------------------------------------------
/lib/api/rpc.js:
--------------------------------------------------------------------------------
1 | var logger = require('../logger')('micromono:rpc')
2 | var toArray = require('lodash.toarray')
3 |
4 |
5 | /**
6 | * The RPC class for managing different transport adapters.
7 | *
8 | * @param {Object} options Options for RPC with following format:
9 | *
10 | * ```javascript
11 | * {
12 | * api: {
13 | * fn: function(){}
14 | * }, // an object contains handler functions
15 | * type: 'axon', // type of adapter or adapter it self
16 | * isRemote: true, // whether this is client side or server side
17 | * scheduler: obj, // the scheduler for distributing requests, client side only
18 | * }
19 | * ```
20 | *
21 | * @return {RPC} Instance of RPC.
22 | */
23 | var RPC = module.exports = function MicromonoRPC(options) {
24 | logger.info('Initialize MicromonoRPC', {
25 | type: options.type,
26 | isRemote: options.isRemote
27 | }).trace(options)
28 |
29 | var rpcAdapter
30 |
31 | // figure out adapter
32 | if ('string' === typeof options.type) {
33 | this.type = options.type
34 | rpcAdapter = require('./' + this.type)
35 | } else if ('object' === typeof options.type) {
36 | rpcAdapter = options.type
37 | this.type = rpcAdapter.type
38 | }
39 |
40 | if ('function' === typeof rpcAdapter) {
41 | rpcAdapter = new rpcAdapter()
42 | // Keep a reference of the adpater so if adpater support multiple connections
43 | // it can refer to itself handle that internally.
44 | this.adapter = rpcAdapter
45 | }
46 |
47 | if ('object' !== typeof rpcAdapter)
48 | throw new Error('options.type should be either type of adapter or the adapter itself, got ' + typeof options.type)
49 |
50 | // internal object holds all the api handlers
51 | this._handlers = {}
52 |
53 | // Override serializer if found in adapter.
54 | if (rpcAdapter.serialize)
55 | this.serialize = rpcAdapter.serialize
56 |
57 | if (rpcAdapter.deserialize)
58 | this.deserialize = rpcAdapter.deserialize
59 |
60 | // add client or server features
61 | if (options.isRemote)
62 | this.prepareClient(rpcAdapter, options)
63 | else
64 | this.prepareServer(rpcAdapter, options)
65 | }
66 |
67 | RPC.prototype.prepareClient = function(rpcAdapter, options) {
68 | logger.debug('Prepare RPC client', {
69 | type: options.type,
70 | service: options.ann.name + '@' + options.ann.version
71 | }).trace(options)
72 |
73 | var self = this
74 | this.ann = options.ann
75 | this.send = rpcAdapter.send
76 | this.connect = rpcAdapter.connect
77 | this.scheduler = options.scheduler
78 | this.scheduler.on('add', function(provider) {
79 | logger.info('Found new RPC provider', {
80 | host: provider.host,
81 | service: provider.name + '@' + provider.version
82 | }).trace(provider)
83 | self.connect(provider)
84 | })
85 |
86 | if (options.api)
87 | Object.keys(options.api).forEach(this.addRemoteAPI.bind(this))
88 | }
89 |
90 | RPC.prototype.prepareServer = function(rpcAdapter, options) {
91 | logger.debug('Prepare RPC server', {
92 | type: options.type
93 | }).trace(options)
94 |
95 | this.startServer = rpcAdapter.startServer
96 | if (options.api) {
97 | var api = options.api
98 | var self = this
99 | Object.keys(api).forEach(function(apiName) {
100 | var handler = api[apiName]
101 | // Add local api handler
102 | self.addAPI(apiName, handler)
103 | })
104 | }
105 | }
106 |
107 | /**
108 | * Handler for disconnect event of provider
109 | *
110 | * @param {Object} provider The annoucement data of disconnected provider.
111 | */
112 | RPC.prototype.onProviderDisconnect = function(provider) {
113 | logger.info('RPC provider disconnected', {
114 | id: provider.id.slice(0, 8),
115 | host: provider.host,
116 | service: provider.name + '@' + provider.version
117 | }).trace(provider)
118 |
119 | this.scheduler.remove(provider)
120 | }
121 |
122 | /**
123 | * Add an API handler.
124 | *
125 | * @param {String} name Name of the api.
126 | * @param {Function} handler Handler of the api.
127 | */
128 | RPC.prototype.addAPI = function(name, handler) {
129 | if ('function' === typeof handler) {
130 | logger.debug('Add server api', {
131 | name: name,
132 | args: handler.args
133 | })
134 |
135 | this._handlers[name] = {
136 | name: name,
137 | args: handler.args,
138 | handler: handler
139 | }
140 | }
141 | }
142 |
143 | /**
144 | * Generate a remote api handler based on name.
145 | *
146 | * @param {String} name Name of the remote api.
147 | */
148 | RPC.prototype.addRemoteAPI = function(name) {
149 | logger.debug('Generate local interface of remote api', {
150 | name: name,
151 | service: this.ann.name + '@' + this.ann.version
152 | })
153 |
154 | var self = this
155 | this._handlers[name] = function() {
156 | var args = toArray(arguments)
157 | var data = {
158 | name: name,
159 | args: args
160 | }
161 | self.send(data)
162 | }
163 | }
164 |
165 | /**
166 | * Get an api handler by name.
167 | *
168 | * @param {String} name Name of the api.
169 | * @param {Function} The handler function.
170 | */
171 | RPC.prototype.getHandler = function(name) {
172 | var handler = this._handlers[name]
173 | if (handler && handler.handler)
174 | handler = handler.handler
175 |
176 | return handler
177 | }
178 |
179 | /**
180 | * Get all api handlers.
181 | *
182 | * @param {Object} The api handlers object.
183 | */
184 | RPC.prototype.getAPIs = function() {
185 | return this._handlers
186 | }
187 |
188 | /**
189 | * Dispatch message received to corresponding api handler.
190 | *
191 | * @param {String|Buffer} msg The message data.
192 | * @param {Function} reply A callback function for replying the result to client.
193 | */
194 | RPC.prototype.dispatch = function(msg, reply) {
195 | var data = this.deserialize(msg)
196 | var handler = this.getHandler(data.name)
197 |
198 | if (handler) {
199 | var args = data.args || []
200 | if (true === data.cid)
201 | args.push(reply)
202 |
203 | handler.apply(null, args)
204 | }
205 | }
206 |
207 | /**
208 | * Serialize data.
209 | *
210 | * @param {Any} data Data to serialize.
211 | * @return {String} Serialized data.
212 | */
213 | RPC.prototype.serialize = function(data) {
214 | return JSON.stringify(data)
215 | }
216 |
217 | /**
218 | * Deserialize message to data.
219 | *
220 | * @param {String} msg Message data to deserialize.
221 | * @return {Any} Deserialized data.
222 | */
223 | RPC.prototype.deserialize = function(msg) {
224 | return JSON.parse(msg)
225 | }
226 |
--------------------------------------------------------------------------------
/lib/pipeline/service.js:
--------------------------------------------------------------------------------
1 | var Superpipe = require('superpipe')
2 |
3 |
4 | exports.initLocalService = Superpipe.pipeline()
5 | // Gether service information
6 | .pipe('getPackageJSON', 'packagePath', 'packageJSON')
7 | .pipe('getServiceInfo',
8 | ['packageJSON', 'service'],
9 | ['hasAsset', 'serviceName', 'serviceInfo', 'serviceVersion'])
10 | .pipe('prepareService', ['hasAsset', 'service'])
11 | .pipe('prepareDiscovery', ['defaultDiscoveryOptions'],
12 | ['discoveryListen', 'discoveryAnnounce', 'discoveryOptions'])
13 | // Setup web features
14 | .pipe('initFramework', ['frameworkType', 'framework'], 'framework')
15 | .pipe('prepareFrameworkForLocal?',
16 | ['framework', 'set'],
17 | ['app', 'attachRoutes', 'attachLocalMiddlewares', 'startHttpServer', 'serveLocalAsset', 'injectAssetInfo'])
18 |
19 | .pipe('getAssetInfo',
20 | ['packagePath', 'packageJSON', 'serviceName'],
21 | ['assetInfo', 'publicURL', 'publicPath'])
22 | .pipe('getJSPMConfig?', ['assetInfo', 'publicPath', 'next'],
23 | ['jspmConfig', 'jspmConfigPath'])
24 | .pipe('injectAssetInfo?', ['assetInfo'])
25 | .pipe('setupRoute?', ['route', 'page', 'pageApiBaseUrl'], 'routes')
26 | .pipe('setupUse?', 'use', 'uses')
27 | .pipe('setupMiddleware?', ['middleware', 'middlewareBaseUrl'], 'middlewares')
28 | // Setup RPC
29 | .pipe('setupRPC?', ['api', 'MICROMONO_RPC', 'service'], ['rpc', 'rpcApi'])
30 |
31 |
32 | exports.startServers = Superpipe.pipeline()
33 | // Start web server
34 | .pipe('startHttpServer?',
35 | ['MICROMONO_PORT', 'MICROMONO_HOST', 'serviceName', 'set'],
36 | ['httpServer', 'httpPort', 'httpHost'])
37 | // Start RPC server
38 | .pipe('startRPCServer?',
39 | ['rpc', 'MICROMONO_RPC_PORT', 'MICROMONO_RPC_HOST', 'next'],
40 | ['rpcPort'])
41 | // Start channel server
42 | .pipe('startChnBackendServer?',
43 | ['channels', 'chnBackend', 'MICROMONO_CHN_ENDPOINT', 'next'],
44 | ['chnAnn'])
45 |
46 | exports.runLocalService = Superpipe.pipeline()
47 | // Attach web request handlers
48 | .pipe('serveLocalAsset?', ['publicURL', 'publicPath', 'serviceName'])
49 | .pipe('useMiddlewares?',
50 | ['uses', 'routes', 'service', 'loadMiddleware', 'mainFramework'])
51 | .pipe('attachRoutes?', ['routes', 'service'])
52 | .pipe('attachLocalMiddlewares?', ['middlewares', 'service'])
53 | .pipe('mergeAssetDependencies?', ['balancerAssetInfo', 'assetInfo'],
54 | ['assetInfo:balancerAssetInfo', 'assetDependenciesChanged'])
55 | .pipe('prefixJSPMBundles?', ['assetInfo'], ['assetBundles'])
56 | // .pipe('bundleDevDependencies?',
57 | // ['assetInfo', 'publicPath', 'packagePath', 'set', 'MICROMONO_BUNDLE_DEV'])
58 |
59 |
60 | exports.listenRemoteProviders = Superpipe.pipeline()
61 | .pipe('addRemoteServicesProvider', ['services', 'addProvider'])
62 | .pipe('listenProviders',
63 | ['services', 'discoveryListen', 'discoveryOptions', 'addProvider'])
64 | .pipe('checkRemoteServicesAvailability', ['services', 'discoveryOptions'])
65 |
66 | exports.startHealthinessServer = Superpipe.pipeline()
67 | // Start healthiness server
68 | .pipe('prepareHealthAliveHandler', ['healthAliveHandler'], 'healthAliveHandler')
69 | .pipe('prepareHealthFunctionalHandler', ['services', 'healthFunctionalHandler'], 'healthFunctionalHandler')
70 | .pipe('startHealthinessServer?', ['MICROMONO_HOST', 'MICROMONO_HEALTH_PORT', {
71 | alivePath: 'MICROMONO_HEALTH_ALIVE_PATH',
72 | aliveHandler: 'healthAliveHandler',
73 | functionalPath: 'MICROMONO_HEALTH_FUNCTIONAL_PATH',
74 | functionalHandler: 'healthFunctionalHandler'
75 | }])
76 |
77 | // Announcement
78 | exports.announceLocalService = Superpipe.pipeline()
79 | .pipe('generateAnnouncement',
80 | ['service', 'serviceInfo', 'MICROMONO_HOST',
81 | {
82 | asset: 'assetInfo',
83 | port: 'httpPort',
84 | host: 'httpHost',
85 | route: 'routes',
86 | use: 'uses',
87 | middleware: 'middlewares',
88 | framework: 'framework'
89 | },
90 | {
91 | handlers: 'rpcApi',
92 | port: 'rpcPort',
93 | host: 'MICROMONO_RPC_HOST',
94 | type: 'MICROMONO_RPC'
95 | },
96 | 'chnAnn'
97 | ], 'announcement')
98 | .pipe('announceService',
99 | ['announcement', 'discoveryAnnounce', 'discoveryOptions'])
100 |
101 |
102 | exports.initRemoteService = Superpipe.pipeline()
103 | .pipe('prepareRemoteService',
104 | ['service', 'announcement'],
105 | ['uses', 'channel', 'routes', 'scheduler', 'middlewares', 'upgradeUrl',
106 | 'assetInfo', 'serviceName'])
107 | .pipe('handleProviderRemoval', 'scheduler')
108 |
109 | // Web
110 | .pipe('rebuildRemoteMiddlewares?', ['middlewares', 'service'])
111 | .pipe('initFramework', ['frameworkType', 'framework'], 'framework')
112 | .pipe('prepareFrameworkForRemote?',
113 | ['framework', 'set'],
114 | ['injectAssetInfo', 'proxyAsset', 'attachRoutes', 'proxyWebsocket'])
115 | .pipe('makeProxyHandlers',
116 | ['getProxyHandler', 'scheduler', 'httpServer', 'upgradeUrl'],
117 | ['proxyHandler', 'wsProxyHandler'])
118 | .pipe('injectAssetInfo?', ['assetInfo'])
119 | .pipe('addProxyHandlerToRoutes?', ['routes', 'proxyHandler'])
120 | .pipe('proxyAsset?', ['assetInfo', 'proxyHandler', 'serviceName'])
121 | .pipe('useMiddlewares?',
122 | ['uses', 'routes', 'service', 'loadMiddleware', 'mainFramework'])
123 | .pipe('attachRoutes?', ['routes', 'service'])
124 | .pipe('proxyWebsocket?', ['upgradeUrl', 'wsProxyHandler'])
125 | .pipe('addProvider?', ['scheduler', 'announcement'])
126 | // Channel
127 | .pipe('connectToChannel?',
128 | ['channel', 'chnGateway', 'announcement', 'scheduler', 'next'])
129 | .pipe('channelOnNewProvider?', ['chnGateway', 'scheduler', 'channel'])
130 |
131 |
132 | exports.mergeAssetDependencies = Superpipe.pipeline()
133 | .pipe('getJSPMBinPath', 'balancerPackagePath', 'jspmBinPath')
134 | .pipe('mergeAssetDependencies?', ['balancerAssetInfo', 'assetInfo'],
135 | ['assetInfo:balancerAssetInfo', 'assetDependenciesChanged'])
136 | .pipe('updatePackageJSON?',
137 | ['balancerAssetInfo', 'balancerPackagePath', 'balancerPackageJSON', 'next', 'assetDependenciesChanged'])
138 | .pipe('jspmInstall?', ['balancerPackagePath', 'jspmBinPath', 'next', 'assetDependenciesChanged'])
139 | .pipe('getJSPMConfig?', ['balancerAssetInfo', 'balancerPublicPath', 'next', 'assetDependenciesChanged'],
140 | ['jspmConfig:balancerJSPMConfig', 'jspmConfigPath:balancerJSPMConfigPath'])
141 | .pipe('updateJSPMConfig?',
142 | ['balancerJSPMConfigPath', 'balancerJSPMConfig', {
143 | bundles: 'assetBundles'
144 | }, 'next', 'assetDependenciesChanged'])
145 |
--------------------------------------------------------------------------------
/lib/service/remote.js:
--------------------------------------------------------------------------------
1 | var RPC = require('../api/rpc')
2 | var logger = require('../logger')('micromono:service:remote')
3 | var Scheduler = require('../discovery/scheduler')
4 |
5 |
6 | exports.buildServiceFromAnnouncement = function(ann) {
7 | logger.info('Build remote service from announcement', {
8 | service: ann.name,
9 | version: ann.version
10 | }).trace(ann)
11 |
12 | var service = {
13 | isRemote: true,
14 | announcement: ann
15 | }
16 |
17 | service.name = ann.name
18 | service.version = ann.version
19 | service.timeout = ann.timeout
20 | service.scheduler = new Scheduler()
21 |
22 | service.scheduler.serviceName = ann.name
23 |
24 | if (ann.web) {
25 | service.use = ann.web.use
26 | service.route = ann.web.route
27 | service.middleware = ann.web.middleware
28 | service.upgradeUrl = ann.web.upgradeUrl
29 | }
30 |
31 | if (ann.api) {
32 | var rpcOptions = {
33 | ann: ann,
34 | api: ann.api.handlers,
35 | type: ann.api.type,
36 | isRemote: true,
37 | scheduler: service.scheduler
38 | }
39 | var rpc = new RPC(rpcOptions)
40 | service.api = rpc.getAPIs()
41 | service.rpcType = ann.api.type
42 | }
43 |
44 | return service
45 | }
46 |
47 | exports.handleProviderRemoval = function(scheduler) {
48 | scheduler.on('remove', function(provider) {
49 | if (provider.proxy) {
50 | logger.debug('Remove service provider (proxy)', {
51 | target: provider.proxy.options.target,
52 | service: provider.name,
53 | version: provider.version
54 | })
55 | provider.proxy.close()
56 | }
57 | })
58 | }
59 |
60 | exports.prepareRemoteService = function(service, announcement) {
61 | return {
62 | uses: service.use,
63 | channel: announcement.channel,
64 | routes: service.route,
65 | scheduler: service.scheduler,
66 | middlewares: service.middleware,
67 | upgradeUrl: service.upgradeUrl,
68 | assetInfo: announcement.asset,
69 | serviceName: service.name
70 | }
71 | }
72 |
73 | exports.prepareFrameworkForRemote = function(framework, set) {
74 | set(framework, ['injectAssetInfo', 'proxyAsset', 'attachRoutes', 'proxyWebsocket'])
75 | }
76 |
77 | exports.makeProxyHandlers = function(getProxyHandler, scheduler, httpServer, upgradeUrl) {
78 | var proxyHandler = getProxyHandler(scheduler)
79 | var wsProxyHandler
80 |
81 | if (upgradeUrl)
82 | wsProxyHandler = getProxyHandler(scheduler, httpServer, upgradeUrl)
83 |
84 | return {
85 | proxyHandler: proxyHandler,
86 | wsProxyHandler: wsProxyHandler
87 | }
88 | }
89 |
90 | exports.addProxyHandlerToRoutes = function(routes, proxyHandler) {
91 | Object.keys(routes).forEach(function(routePath) {
92 | var route = routes[routePath]
93 | route.handler = proxyHandler
94 | })
95 | }
96 |
97 | exports.loadMiddleware = function(name, service, framework) {
98 | try {
99 | logger.debug('Load internal middleware for service', {
100 | service: service.name,
101 | version: service.version,
102 | middleware: name
103 | })
104 | return require('../web/middleware/' + name)
105 | } catch (e) {
106 | logger.info('Can not find middleware. Try require globally.', {
107 | middleware: name
108 | })
109 | return require(name)
110 | }
111 | }
112 |
113 | exports.useMiddlewares = function(uses, routes, service, loadMiddleware, framework) {
114 | Object.keys(uses).forEach(function(middlewareName) {
115 | logger.debug('Use middleware for service', {
116 | service: service.name,
117 | version: service.version,
118 | middleware: middlewareName
119 | })
120 | var url = uses[middlewareName]
121 | // Load middleware module
122 | var middleware = loadMiddleware(middlewareName, service, framework)
123 | framework.useMiddleware(url, middleware, routes, service)
124 | })
125 | }
126 |
127 | exports.addProvider = function(scheduler, ann) {
128 | ann.lastSeen = Date.now()
129 | var oldAnn = exports.hasProvider(scheduler, ann)
130 |
131 | if (oldAnn) {
132 | oldAnn.lastSeen = Date.now()
133 | return
134 | }
135 |
136 | logger.info('Found new provider for service', {
137 | service: ann.name,
138 | version: ann.version,
139 | host: ann.host
140 | })
141 |
142 | scheduler.add(ann)
143 | }
144 |
145 | exports.hasProvider = function(scheduler, ann) {
146 | return scheduler.hasItem(ann, function(old, ann) {
147 | return old.id === ann.id
148 | })
149 | }
150 |
151 | exports.addRemoteServicesProvider = function(services, addProvider) {
152 | Object.keys(services).forEach(function(serviceName) {
153 | var service = services[serviceName]
154 | if (service.isRemote)
155 | addProvider(service.scheduler, service.announcement)
156 | })
157 | }
158 |
159 | exports.checkRemoteServicesAvailability = function(services, discoveryOptions) {
160 | var minInterval = 3000
161 | var remoteServices = []
162 |
163 | Object.keys(services).forEach(function(serviceName) {
164 | var service = services[serviceName]
165 | if (service.isRemote) {
166 | var ann = service.announcement
167 | if (minInterval > ann.timeout)
168 | minInterval = ann.timeout > 1000 ? ann.timeout : 1000
169 | remoteServices.push(services[serviceName])
170 | }
171 | })
172 |
173 | function checkAvailability() {
174 | var now = Date.now()
175 | remoteServices.forEach(function(service) {
176 | var scheduler = service.scheduler
177 | scheduler.each(function(ann) {
178 | if (now - ann.lastSeen > ann.timeout) {
179 | logger.info('Service provider timeout', {
180 | service: ann.name,
181 | version: ann.version,
182 | host: ann.host
183 | })
184 | scheduler.remove(ann)
185 | }
186 | })
187 | if (0 === scheduler.len()) {
188 | logger.info('\n\tNo available providers for service. Waiting for new providers...', {
189 | service: service.name,
190 | version: service.version
191 | })
192 | if (!service.timer) {
193 | service.timer = setTimeout(function() {
194 | if (0 === scheduler.len()) {
195 | logger.fatal('\n\tLost all providers of service. Exiting micromono...\n', {
196 | service: service.name,
197 | version: service.version
198 | })
199 | process.exit(1)
200 | }
201 | service.timer = undefined
202 | }, discoveryOptions.MICROMONO_DISCOVERY_TIMEOUT)
203 | }
204 | }
205 | })
206 | }
207 |
208 | setInterval(checkAvailability, minInterval)
209 | }
210 |
--------------------------------------------------------------------------------
/example/channel/public/config.js:
--------------------------------------------------------------------------------
1 | System.config({
2 | defaultJSExtensions: true,
3 | transpiler: "babel",
4 | babelOptions: {
5 | "optional": [
6 | "runtime",
7 | "optimisation.modules.system"
8 | ]
9 | },
10 | paths: {
11 | "github:*": "jspm_packages/github/*",
12 | "npm:*": "jspm_packages/npm/*"
13 | },
14 |
15 | map: {
16 | "babel": "npm:babel-core@5.8.34",
17 | "babel-runtime": "npm:babel-runtime@5.8.34",
18 | "core-js": "npm:core-js@1.2.6",
19 | "engine.io-client": "npm:engine.io-client@1.6.11",
20 | "socketmq": "npm:socketmq@0.7.1",
21 | "github:jspm/nodelibs-assert@0.1.0": {
22 | "assert": "npm:assert@1.4.1"
23 | },
24 | "github:jspm/nodelibs-buffer@0.1.0": {
25 | "buffer": "npm:buffer@3.6.0"
26 | },
27 | "github:jspm/nodelibs-events@0.1.1": {
28 | "events": "npm:events@1.0.2"
29 | },
30 | "github:jspm/nodelibs-path@0.1.0": {
31 | "path-browserify": "npm:path-browserify@0.0.0"
32 | },
33 | "github:jspm/nodelibs-process@0.1.2": {
34 | "process": "npm:process@0.11.8"
35 | },
36 | "github:jspm/nodelibs-stream@0.1.0": {
37 | "stream-browserify": "npm:stream-browserify@1.0.0"
38 | },
39 | "github:jspm/nodelibs-url@0.1.0": {
40 | "url": "npm:url@0.10.3"
41 | },
42 | "github:jspm/nodelibs-util@0.1.0": {
43 | "util": "npm:util@0.10.3"
44 | },
45 | "github:jspm/nodelibs-vm@0.1.0": {
46 | "vm-browserify": "npm:vm-browserify@0.0.4"
47 | },
48 | "npm:amp@0.3.1": {
49 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
50 | "stream": "github:jspm/nodelibs-stream@0.1.0"
51 | },
52 | "npm:assert@1.4.1": {
53 | "assert": "github:jspm/nodelibs-assert@0.1.0",
54 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
55 | "process": "github:jspm/nodelibs-process@0.1.2",
56 | "util": "npm:util@0.10.3"
57 | },
58 | "npm:babel-runtime@5.8.34": {
59 | "process": "github:jspm/nodelibs-process@0.1.2"
60 | },
61 | "npm:better-assert@1.0.2": {
62 | "assert": "github:jspm/nodelibs-assert@0.1.0",
63 | "callsite": "npm:callsite@1.0.0",
64 | "fs": "github:jspm/nodelibs-fs@0.1.2",
65 | "process": "github:jspm/nodelibs-process@0.1.2"
66 | },
67 | "npm:buffer@3.6.0": {
68 | "base64-js": "npm:base64-js@0.0.8",
69 | "child_process": "github:jspm/nodelibs-child_process@0.1.0",
70 | "fs": "github:jspm/nodelibs-fs@0.1.2",
71 | "ieee754": "npm:ieee754@1.1.6",
72 | "isarray": "npm:isarray@1.0.0",
73 | "process": "github:jspm/nodelibs-process@0.1.2"
74 | },
75 | "npm:core-js@1.2.6": {
76 | "fs": "github:jspm/nodelibs-fs@0.1.2",
77 | "path": "github:jspm/nodelibs-path@0.1.0",
78 | "process": "github:jspm/nodelibs-process@0.1.2",
79 | "systemjs-json": "github:systemjs/plugin-json@0.1.2"
80 | },
81 | "npm:core-util-is@1.0.2": {
82 | "buffer": "github:jspm/nodelibs-buffer@0.1.0"
83 | },
84 | "npm:debug@2.2.0": {
85 | "ms": "npm:ms@0.7.1"
86 | },
87 | "npm:engine.io-client@1.6.11": {
88 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
89 | "component-emitter": "npm:component-emitter@1.1.2",
90 | "component-inherit": "npm:component-inherit@0.0.3",
91 | "debug": "npm:debug@2.2.0",
92 | "engine.io-parser": "npm:engine.io-parser@1.2.4",
93 | "has-cors": "npm:has-cors@1.1.0",
94 | "indexof": "npm:indexof@0.0.1",
95 | "parsejson": "npm:parsejson@0.0.1",
96 | "parseqs": "npm:parseqs@0.0.2",
97 | "parseuri": "npm:parseuri@0.0.4",
98 | "yeast": "npm:yeast@0.1.2"
99 | },
100 | "npm:engine.io-parser@1.2.4": {
101 | "after": "npm:after@0.8.1",
102 | "arraybuffer.slice": "npm:arraybuffer.slice@0.0.6",
103 | "base64-arraybuffer": "npm:base64-arraybuffer@0.1.2",
104 | "blob": "npm:blob@0.0.4",
105 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
106 | "has-binary": "npm:has-binary@0.1.6",
107 | "utf8": "npm:utf8@2.1.0"
108 | },
109 | "npm:has-binary@0.1.6": {
110 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
111 | "fs": "github:jspm/nodelibs-fs@0.1.2",
112 | "isarray": "npm:isarray@0.0.1"
113 | },
114 | "npm:inherits@2.0.1": {
115 | "util": "github:jspm/nodelibs-util@0.1.0"
116 | },
117 | "npm:parsejson@0.0.1": {
118 | "better-assert": "npm:better-assert@1.0.2"
119 | },
120 | "npm:parseqs@0.0.2": {
121 | "better-assert": "npm:better-assert@1.0.2"
122 | },
123 | "npm:parseuri@0.0.4": {
124 | "better-assert": "npm:better-assert@1.0.2"
125 | },
126 | "npm:path-browserify@0.0.0": {
127 | "process": "github:jspm/nodelibs-process@0.1.2"
128 | },
129 | "npm:process@0.11.8": {
130 | "assert": "github:jspm/nodelibs-assert@0.1.0",
131 | "fs": "github:jspm/nodelibs-fs@0.1.2",
132 | "vm": "github:jspm/nodelibs-vm@0.1.0"
133 | },
134 | "npm:punycode@1.3.2": {
135 | "process": "github:jspm/nodelibs-process@0.1.2"
136 | },
137 | "npm:readable-stream@1.1.14": {
138 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
139 | "core-util-is": "npm:core-util-is@1.0.2",
140 | "events": "github:jspm/nodelibs-events@0.1.1",
141 | "inherits": "npm:inherits@2.0.1",
142 | "isarray": "npm:isarray@0.0.1",
143 | "process": "github:jspm/nodelibs-process@0.1.2",
144 | "stream-browserify": "npm:stream-browserify@1.0.0",
145 | "string_decoder": "npm:string_decoder@0.10.31"
146 | },
147 | "npm:socketmq@0.7.1": {
148 | "amp": "npm:amp@0.3.1",
149 | "buffer": "github:jspm/nodelibs-buffer@0.1.0",
150 | "events": "github:jspm/nodelibs-events@0.1.1",
151 | "inherits": "npm:inherits@2.0.1",
152 | "stream": "github:jspm/nodelibs-stream@0.1.0",
153 | "url": "github:jspm/nodelibs-url@0.1.0"
154 | },
155 | "npm:stream-browserify@1.0.0": {
156 | "events": "github:jspm/nodelibs-events@0.1.1",
157 | "inherits": "npm:inherits@2.0.1",
158 | "readable-stream": "npm:readable-stream@1.1.14"
159 | },
160 | "npm:string_decoder@0.10.31": {
161 | "buffer": "github:jspm/nodelibs-buffer@0.1.0"
162 | },
163 | "npm:url@0.10.3": {
164 | "assert": "github:jspm/nodelibs-assert@0.1.0",
165 | "punycode": "npm:punycode@1.3.2",
166 | "querystring": "npm:querystring@0.2.0",
167 | "util": "github:jspm/nodelibs-util@0.1.0"
168 | },
169 | "npm:utf8@2.1.0": {
170 | "systemjs-json": "github:systemjs/plugin-json@0.1.2"
171 | },
172 | "npm:util@0.10.3": {
173 | "inherits": "npm:inherits@2.0.1",
174 | "process": "github:jspm/nodelibs-process@0.1.2"
175 | },
176 | "npm:vm-browserify@0.0.4": {
177 | "indexof": "npm:indexof@0.0.1"
178 | }
179 | }
180 | });
181 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | 0.8.0 (2016/12/07)
2 | ==================
3 | - [Bug] Fix a potential crash for router.
4 | - [Bug] Fix a potential crash for discovery.
5 | - Add healthiness module:
6 | - Add liveness http handler default path `__health/alive`.
7 | - Add readiness http handler default path `__health/functional`.
8 | - Re-organize files and directories.
9 |
10 | 0.2.0 (2016/02/28)
11 | ==================
12 | - Allow using `this` to reference service instance in api handlers.
13 | - Simplify `Scheduler` remove external dependencies.
14 | - One proxy instance per proxy handler.
15 | - Rewrite asset implementation using pipelines.
16 | - Cleanup bundle command and its options.
17 |
18 | 0.1.111 (2016/02/19)
19 | ===================
20 | - Use socketmq as default rpc adapter.
21 |
22 | 0.1.110 (2016/01/23)
23 | ===================
24 | - Bug fix for wrong proxy url.
25 |
26 | 0.1.109 (2016/01/23)
27 | ===================
28 | - Close proxy after provider is removed.
29 | - #15 Bug fix for proxying websocket requests.
30 |
31 | 0.1.37 (2015/12/02)
32 | ===================
33 | - Expose page info to route.
34 |
35 | 0.1.36 (2015/12/01)
36 | ===================
37 | - Quick fix: routes should be optional.
38 |
39 | 0.1.35 (2015/11/30)
40 | ===================
41 | - Bug fix for route handler can't get `next`.
42 | - Escape unsafe characters when render template with layout middleware `web/middleware/express-layout.js`.
43 | - Accept using middleware name in route definition.
44 | ```javascript
45 | route: {
46 | '/hello': ['layout', handlerFn]
47 | }
48 | ```
49 | - Fix incorrect parameter for express `router.param`.
50 |
51 |
52 | 0.1.34 (2015/11/25)
53 | ===================
54 | - [Breaking] Rename sub command `micromono asset` to `micromono bundle`.
55 | - [Breaking] `Service#use` now accepts http method prefix same as in `route`. e.g. `post::/user/update`.
56 | - [Breaking] Rename `-a` to `-b` for `--bundle-asset`.
57 | - Make main export stateless and export `MicroMonoServer` to support multipe micromono instances in one process.
58 |
59 |
60 | 0.1.33 (2015/11/18)
61 | ===================
62 | - Make the layout middleware more friendly for isomorphic rendering.
63 |
64 |
65 | 0.1.32 (2015/11/13)
66 | ===================
67 | - Expose more asset info in service announcement.
68 | - Fix bugs for bundling asset.
69 |
70 |
71 | 0.1.31 (2015/11/09)
72 | ===================
73 | - Support using `^` to override `baseUrl`.
74 |
75 |
76 | 0.1.30 (2015/10/30)
77 | ===================
78 | - Bundle static asset on the fly with option `-a` or `--bundle-asset`.
79 |
80 |
81 | 0.1.29 (2015/10/30)
82 | ===================
83 | - Add command `micromono asset` for building asset files.
84 |
85 |
86 | 0.1.28 (2015/10/28)
87 | ===================
88 | - Bug fix for setting/getting upgrade url.
89 | - Bug fix for proxying websockets request.
90 | - Bug fix for `setHttpServer`
91 |
92 |
93 | 0.1.27 (2015/10/26)
94 | ===================
95 | - Bug fix for setting http server for services.
96 | - Bug fix for merge and install jspm dependencies.
97 | - Get micromono specific settings from property `micromono` of package.json.
98 |
99 |
100 | 0.1.26 (2015/10/23)
101 | ===================
102 | - [Breaking] Functions will be treated as rpc only when they are defined under
103 | property `api` when you extend a Service.
104 |
105 | ```javascript
106 | var MyService = Service.extend({
107 |
108 | // functions defined under `api` property will be exposed through rpc.
109 | api: {
110 | // this function could be called remotely like this:
111 | // myService.api.rpcMethod()
112 | rpcMethod: function() {
113 | // body...
114 | }
115 | }
116 |
117 | // this will not be exposed as a rpc endpoint
118 | myServiceFunc: function() {
119 | // body...
120 | }
121 | })
122 | ```
123 |
124 | - Rewrite and reorganize code to an adaptive style to support different web
125 | frameworks and rpc transporters through adapters.
126 | - Add standalone service manager class.
127 | - Add standalone scheduler class.
128 | - `micromono()` now returns an instance of `MicroMonoServer` class.
129 | - Use `axon` as default rpc transporter.
130 | - Use `cmdenv` to unify settings from environment and command line options.
131 | - Change to no semicolon coding style.
132 | - Add lots of debugging info.
133 |
134 |
135 | 0.1.25 (2015/09/18)
136 | ===================
137 | - Support mounting multiple `publicURL` to the same local asset directory.
138 |
139 |
140 | 0.1.24 (2015/09/12)
141 | ===================
142 | - Upgrade jspm to version 0.16.2
143 | - Use `jspm.directories.baseURL` instead of `jspm.directories.lib` as directory
144 | of local static asset.
145 |
146 |
147 | 0.1.23 (2015/09/10)
148 | ===================
149 | - Allow setting upgrade url in service definition.
150 | - Allow setting service name by using `Service#name`.
151 |
152 | 0.1.22 (2015/08/26)
153 | ===================
154 | - Generate public path from `jspm.directories.lib` if possible. Otherwise fall back to use `jspm.directories.publicURL`.
155 | - [Breaking change] New format for defining server middleware in `Service.use`.
156 | - [Breaking change] Rename built-in middleware `partial-render` to `layout`.
157 |
158 | 0.1.21 (2015/08/11)
159 | ===================
160 | - Bug fix for asset/jspm.
161 |
162 | 0.1.20 (2015/08/10)
163 | ===================
164 | - Expose http `server` instance to service.
165 | - Add WebSocket support (handle upgrade request).
166 | - Add socket.io service example.
167 | - Add server-side middleware support.
168 |
169 | 0.1.19 (2015/08/07)
170 | ===================
171 | - [Breaking change] Use `startService` and `runServer` instead of `boot` to run service/server.
172 | - Add `Makefile` for example.
173 |
174 |
175 | 0.1.18 (2015/08/05)
176 | ===================
177 | - Use socket.io as the default transporter for RPC.
178 | - Only one micromono instance per process.
179 | - Fully functional express+passport example.
180 | - Use a separate connect instance for middleware.
181 | - Some bug fixes.
182 |
183 | 0.1.17 (2015/07/30)
184 | ===================
185 | - Load services with command line option `--service`.
186 | - Allow waiting for services with command line option `--allow-pending`.
187 |
188 |
189 | 0.1.16 (2015/07/30)
190 | ===================
191 | - [Breaking Changes] Use `route` instead of `routes` when define a service.
192 | - Bug fix for serving asset files.
193 |
194 |
195 | 0.1.15 (2015/07/23)
196 | ===================
197 | - Bug fix for incorrect path of middleware router.
198 |
199 |
200 | 0.1.14 (2015/07/22)
201 | ===================
202 | - Add remote middleware support.
203 |
204 |
205 | 0.1.13 (2015/07/14)
206 | ===================
207 | The first usable version with following features:
208 | - Load services locally/remotely with service discovery.
209 | - Proxy remote asset requests and merge client side dependencies.
210 | - Proxy page/rest routing requests.
211 | - Compose partial html with local template on the fly.
212 | - Simple RPC system
213 |
--------------------------------------------------------------------------------
/lib/web/asset/pjson.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var path = require('path')
3 | var union = require('lodash.union')
4 | var logger = require('../../logger')('micromono:asset:pjson')
5 | var assign = require('lodash.assign')
6 |
7 |
8 | exports.getAssetInfo = function(packagePath, packageJSON, serviceName) {
9 | serviceName = serviceName || ''
10 |
11 | var assetInfo
12 | var publicURL
13 | var publicPath
14 | var micromono = packageJSON.micromono || {}
15 |
16 | if (packageJSON.jspm) {
17 | assetInfo = assign({}, packageJSON.jspm)
18 | assetInfo.name = serviceName
19 | var directories = assetInfo.directories = assetInfo.directories || {}
20 |
21 | // Public url for generating other urls (e.g. config.js, system.js etc.)
22 | publicURL = micromono.publicURL || path.join(directories.baseURL || '/', serviceName)
23 | if (!Array.isArray(publicURL))
24 | publicURL = [publicURL]
25 |
26 | publicURL = publicURL.map(function(url) {
27 | if ('/' !== url[0])
28 | url = '/' + url
29 | return url
30 | })
31 | assetInfo.publicURL = publicURL
32 |
33 | // Local path for asset files.
34 | publicPath = path.join(packagePath, directories.baseURL || '/')
35 |
36 | // Entry script.
37 | assetInfo.main = assetInfo.main || 'index.js'
38 |
39 | // Bundles configurations.
40 | assetInfo.bundleDeps = micromono.bundleDeps
41 | assetInfo.ignoreDeps = micromono.ignoreDeps
42 | assetInfo.commonBundles = micromono.commonBundles
43 | // Relative urls of assets.
44 | assetInfo.entryJs = path.join(publicURL[0], assetInfo.main + (/\.js$/.test(assetInfo.main) ? '' : '.js'))
45 | assetInfo.bundleJs = micromono.bundleJs
46 | assetInfo.bundleCss = micromono.bundleCss
47 | }
48 |
49 | return {
50 | assetInfo: assetInfo,
51 | publicURL: publicURL,
52 | publicPath: publicPath
53 | }
54 | }
55 |
56 | exports.mergeAssetDependencies = function(dstAssetInfo, srcAssetInfo) {
57 | var assetDependenciesChanged = undefined
58 |
59 | if (srcAssetInfo.dependencies) {
60 | var srcDeps = srcAssetInfo.dependencies
61 | var dstDeps = dstAssetInfo.dependencies
62 | logger.debug('Merging asset dependencies', {
63 | name: srcAssetInfo.name,
64 | srcDeps: srcDeps
65 | })
66 |
67 | // Ignore deps bundled by services.
68 | var srcBundleDeps = srcAssetInfo.srcBundleDeps || []
69 | var dstIgnoreDeps = dstAssetInfo.ignoreDeps || []
70 | dstAssetInfo.ignoreDeps = union(dstIgnoreDeps, srcBundleDeps)
71 |
72 | // Merge
73 | Object.keys(srcDeps).forEach(function(depName) {
74 | var oldDep = dstDeps[depName]
75 | var newDep = srcDeps[depName]
76 | if (!oldDep) {
77 | dstDeps[depName] = newDep
78 | assetDependenciesChanged = true
79 | } else if (oldDep !== newDep) {
80 | logger.info('Conflicting package version', {
81 | old: oldDep,
82 | new: newDep
83 | })
84 | }
85 | })
86 |
87 | dstAssetInfo.dependencies = dstDeps
88 | }
89 |
90 | return {
91 | assetInfo: dstAssetInfo,
92 | assetDependenciesChanged: assetDependenciesChanged
93 | }
94 | }
95 |
96 | exports.filterServicesWithAsset = function(services) {
97 | var servicesWithAsset = []
98 | Object.keys(services).forEach(function(name) {
99 | var service = services[name]
100 | if (service.announcement.asset)
101 | servicesWithAsset.push(service)
102 | })
103 |
104 | return {
105 | servicesWithAsset: 0 < servicesWithAsset.length ? servicesWithAsset : undefined
106 | }
107 | }
108 |
109 | exports.getCommonAssetDependencies = function(servicesWithAsset) {
110 | var depsMap = {}
111 | servicesWithAsset.forEach(function(service) {
112 | var asset = service.announcement.asset
113 | var dependencies = asset.dependencies
114 | dependencies && Object.keys(dependencies).forEach(function(depName) {
115 | if (!depsMap[depName])
116 | depsMap[depName] = []
117 | depsMap[depName].push(service.name)
118 | })
119 | })
120 |
121 | return {
122 | assetDependenciesMap: depsMap
123 | }
124 | }
125 |
126 | exports.getCommonBundles = function(assetInfo, servicesWithAsset, assetDependenciesMap) {
127 | var numServices = servicesWithAsset.length
128 | // Any dependency required by 70% or more of the services
129 | // Minus any dependencies in assetInfo.micromono.bundleDeps
130 | // (which will be bundled with the main bundle.)
131 | // Minus any dependencies in assetInfo.micromono.ignoreDeps
132 | var common70 = []
133 | // Any dependency required by 50% or more but less than 70% of the services
134 | // Minus any dependencies in assetInfo.micromono.ignoreDeps
135 | var common50 = []
136 | // Any dependency required by 30% or more but less than 50% of the services
137 | // Minus any dependencies in assetInfo.micromono.ignoreDeps
138 | var common30 = []
139 | // All other dependencies
140 | // Minus any dependencies in assetInfo.micromono.ignoreDeps
141 | var common0 = []
142 | var commonBundles = {}
143 |
144 | var bundleDeps = assetInfo.bundleDeps || []
145 | var ignoreDeps = assetInfo.ignoreDeps || []
146 |
147 | function filter(percentage, requiredBy, depName) {
148 | return percentage <= requiredBy / numServices
149 | && -1 === ignoreDeps.indexOf(depName)
150 | && -1 === bundleDeps.indexOf(depName)
151 | }
152 |
153 | Object.keys(assetDependenciesMap).forEach(function(depName) {
154 | var requiredBy = assetDependenciesMap[depName].length
155 | if (filter(.7, requiredBy, depName))
156 | common70.push(depName)
157 | else if (filter(.5, requiredBy, depName))
158 | common50.push(depName)
159 | else if (filter(.3, requiredBy, depName))
160 | common30.push(depName)
161 | else if (filter(0, requiredBy, depName))
162 | common0.push(depName)
163 | })
164 |
165 | if (0 < common70.length)
166 | commonBundles.common70 = common70
167 | if (0 < common50.length)
168 | commonBundles.common50 = common50
169 | if (0 < common30.length)
170 | commonBundles.common30 = common30
171 | if (0 < common0.length)
172 | commonBundles.common0 = common0
173 |
174 | assetInfo.commonBundles = commonBundles
175 | return {
176 | commonBundles: commonBundles
177 | }
178 | }
179 |
180 | exports.updatePackageJSON = function(assetInfo, packagePath, packageJSON, next) {
181 | logger.debug('updatePackageJSON')
182 |
183 | var jspmInfo = packageJSON.jspm || {}
184 | jspmInfo.directories = assetInfo.directories
185 | jspmInfo.dependencies = assetInfo.dependencies
186 | packageJSON.jspm = jspmInfo
187 |
188 | packageJSON.micromono = packageJSON.micromono || {}
189 |
190 | if (assetInfo.bundleJs)
191 | packageJSON.micromono.bundleJs = assetInfo.bundleJs
192 |
193 | if (assetInfo.bundleCss)
194 | packageJSON.micromono.bundleCss = assetInfo.bundleCss
195 |
196 | if (assetInfo.commonBundles)
197 | packageJSON.micromono.commonBundles = assetInfo.commonBundles
198 |
199 | var pkgJSONStr = JSON.stringify(packageJSON, null, 2)
200 | fs.writeFile(path.join(packagePath, 'package.json'), pkgJSONStr, next)
201 | }
202 |
--------------------------------------------------------------------------------
/lib/server/pipe.js:
--------------------------------------------------------------------------------
1 | var http = require('http')
2 | var path = require('path')
3 | var logger = require('../logger')('micromono:server:pipe')
4 | var callsite = require('callsite')
5 | var AssetPipe = require('../web/asset')
6 | var argsNames = require('js-args-names')
7 | var ServicePipeline = require('../pipeline/service')
8 |
9 | exports.getCallerPath = function(num) {
10 | var stack = callsite()
11 | var callerFilename = stack[num || 2].getFileName()
12 | return path.dirname(callerFilename)
13 | }
14 |
15 | exports.getBalancerAsset = function(packagePath, balancerPackageJSON) {
16 | var balancerAsset = AssetPipe.getAssetInfo(packagePath, balancerPackageJSON, 'MicroMonoBalancer')
17 | return {
18 | balancerAsset: balancerAsset,
19 | balancerAssetInfo: balancerAsset.assetInfo,
20 | balancerPublicPath: balancerAsset.publicPath
21 | }
22 | }
23 |
24 | exports.getServiceNames = function(services) {
25 | var serviceNames
26 | if ('string' === typeof services) {
27 | serviceNames = services.split(',').map(function(srv) {
28 | return srv.trim()
29 | })
30 | if (0 === serviceNames.length)
31 | serviceNames = undefined
32 | }
33 | return {
34 | serviceNames: serviceNames
35 | }
36 | }
37 |
38 | exports.requireAllServices = function(serviceNames, serviceDir, require) {
39 | logger.info('Requiring all services', {
40 | services: serviceNames
41 | })
42 |
43 | var services = {}
44 | serviceNames.forEach(function(name) {
45 | services[name] = require(name, serviceDir)
46 | })
47 |
48 | return {
49 | services: services
50 | }
51 | }
52 |
53 | exports.initFramework = function(frameworkType, framework) {
54 | if (!framework && 'string' === typeof frameworkType) {
55 | logger.info('Initialize web framework adapter', {
56 | framework: frameworkType
57 | })
58 | var FrameworkAdapter = require('../web/framework/' + frameworkType)
59 | framework = new FrameworkAdapter()
60 | }
61 |
62 | return {
63 | framework: framework
64 | }
65 | }
66 |
67 | exports.prepareFrameworkForBalancer = function(framework, app) {
68 | framework.app = app
69 | return {
70 | serveBalancerAsset: function(balancerAsset) {
71 | if (balancerAsset.publicURL && balancerAsset.publicPath)
72 | framework.serveLocalAsset(balancerAsset.publicURL, balancerAsset.publicPath, 'MicroMonoBalancer')
73 | },
74 | attachHttpServer: framework.attachHttpServer.bind(framework)
75 | }
76 | }
77 |
78 | exports.createHttpServer = function(set) {
79 | var requestHandler
80 |
81 | function setHttpRequestHandler(fn) {
82 | requestHandler = fn
83 | }
84 |
85 | function serverHandler(req, res) {
86 | requestHandler(req, res)
87 | }
88 |
89 | var httpServer = http.createServer(serverHandler)
90 | // Set to global superpipe so the children pipelines can use it.
91 | set('httpServer', httpServer)
92 |
93 | return {
94 | httpServer: httpServer,
95 | setHttpRequestHandler: setHttpRequestHandler
96 | }
97 | }
98 |
99 | exports.runServices = function(micromono, services, runService, next) {
100 | var pipeline = micromono.superpipe()
101 |
102 | Object.keys(services).forEach(function(serviceName) {
103 | var service = services[serviceName]
104 | var serviceDepName = 'service:' + serviceName
105 | pipeline.pipe(function setServiceDepName() {
106 | var srv = {}
107 | srv[serviceDepName] = service
108 | return srv
109 | })
110 | pipeline.pipe(runService, [serviceDepName, 'micromono', 'next'])
111 | })
112 |
113 | pipeline.error('errorHandler')
114 | pipeline.pipe(next).toPipe(null, 'runServices')()
115 | }
116 |
117 | exports.runService = function(service, micromono, next) {
118 | logger.info('Run service', {
119 | service: service.name + '@' + service.version,
120 | isRemote: service.isRemote
121 | })
122 |
123 | var pipelineName = service.name
124 | var pipeline = micromono.superpipe()
125 |
126 | if (service.isRemote) {
127 | pipeline = buildRemoteServicePipeline(service, pipeline)
128 | pipelineName += ':runRemote'
129 | } else {
130 | pipeline = buildLocalServicePipeline(service, pipeline)
131 | pipelineName += ':runLocal'
132 | }
133 |
134 | pipeline = pipeline.concat(ServicePipeline.mergeAssetDependencies)
135 |
136 | pipeline
137 | .pipe(next)
138 | .error('errorHandler', [null, 'serviceName'])
139 | .debug(micromono.get('MICROMONO_DEBUG_PIPELINE') && logger.debug)
140 |
141 | // Execute the pipeline.
142 | pipeline.toPipe(null, pipelineName)()
143 | }
144 |
145 | function buildRemoteServicePipeline(service, pipeline) {
146 | pipeline.pipe(function prepareRemotePipeline(mainFramework) {
147 | return {
148 | 'service': service,
149 | 'framework': mainFramework,
150 | 'announcement': service.announcement
151 | }
152 | }, ['mainFramework'], ['service', 'framework', 'announcement'])
153 |
154 | // The solution below is not optimal. Setup channel gateway should be happened
155 | // in the balancer pipeline level not individual services here (hence the move).
156 | // Left the following comments for referencing:
157 | //
158 | // `setGlobal` here won't work immediately since this part runs in the middle
159 | // of the balancer pipeline not a separate one. At the time we call `setGlobal`
160 | // the balancer pipeline already cloned the DI container so any changes made
161 | // to the global container will be isolated and has no impact for the on going
162 | // balancer pipeline. Although, it does avoid the recreation of the
163 | // `chnGateway` object for subsequential services.
164 | //
165 | // if (service.announcement.channel)
166 | // pipeline.pipe('ensureChannelGateway', ['chnGateway', 'setGlobal'], 'chnGateway')
167 |
168 | return pipeline.concat(ServicePipeline.initRemoteService)
169 | }
170 |
171 | function buildLocalServicePipeline(service, pipeline) {
172 | // Initialize service
173 | pipeline = pipeline
174 | .pipe(function setService() {
175 | return {
176 | service: service,
177 | packagePath: service.packagePath
178 | }
179 | })
180 | .concat(ServicePipeline.initLocalService)
181 |
182 | // Add service.init to pipeline if exists
183 | if (service.init) {
184 | var initArgs = argsNames(service.init)
185 | pipeline.pipe(service.init, initArgs)
186 | }
187 |
188 | // Run service and prepare announcement.
189 | return pipeline
190 | .concat(ServicePipeline.runLocalService)
191 | .pipe('attachToMainFramework?', ['mainFramework', 'framework'])
192 | .pipe('generateAnnouncement',
193 | ['assetInfo', 'routes', 'uses', 'middlewares',
194 | 'service', 'httpPort', 'framework', 'rpcApi',
195 | 'rpcPort', 'rpcType', 'host', 'rpcHost'
196 | ], 'announcement')
197 | }
198 |
199 | exports.attachToMainFramework = function(mainFramework, framework) {
200 | if (mainFramework !== framework)
201 | mainFramework.app.use(framework.app)
202 | }
203 |
204 | exports.startWebServer = function(httpServer, port, host, set) {
205 | logger.debug('Start web server', {
206 | host: host,
207 | port: port
208 | })
209 |
210 | httpServer.listen(port, host, function() {
211 | var address = httpServer.address()
212 |
213 | logger.info('Web server started', address)
214 |
215 | set({
216 | httpPort: address.port,
217 | httpHost: address.address
218 | })
219 | })
220 | }
221 |
--------------------------------------------------------------------------------
/lib/channel/backend.js:
--------------------------------------------------------------------------------
1 | var Url = require('url')
2 | var type = require('socketmq/lib/message/type')
3 | var logger = require('../logger')('micromono:channel:backend')
4 | var toArray = require('lodash.toarray')
5 | var socketmq = require('socketmq')
6 | var Superpipe = require('superpipe')
7 |
8 |
9 | var INF = type.INF
10 | var ACK = type.ACK
11 | var LVE = type.LVE
12 | var REQ = type.REQ
13 | var REP = type.REP
14 | var SUB = type.SUB
15 | var MCH = type.MCH
16 | var CKE = type.CKE
17 | var SSN = type.SSN
18 | var SID = type.SID
19 | var reservedNames = ['pub', 'pubChn', 'sub', 'req',
20 | 'reqChn', 'rep', 'chnAdapter', 'chnRepEvents']
21 |
22 |
23 | exports.normalizeChannels = function(channel) {
24 | logger.debug('normalizeChannels')
25 |
26 | var chn = {}
27 | // This service only contains one channel definition
28 | if (channel.namespace) {
29 | chn[channel.namespace] = channel
30 | } else {
31 | // Multiple namespaced channels, set namespace value to individual channel
32 | // definition object
33 | Object.keys(channel).forEach(function(namespace) {
34 | channel[namespace].namespace = namespace
35 | })
36 | chn = channel
37 | }
38 |
39 | logger.trace(chn)
40 |
41 | return {
42 | channels: chn
43 | }
44 | }
45 |
46 | exports.checkChannelPropertyName = function(channels, service) {
47 | logger.debug('checkChannelPropertyName', {
48 | service: service.name
49 | }).trace(channels)
50 |
51 | function check(obj) {
52 | Object.keys(obj).forEach(function(name) {
53 | if (reservedNames.indexOf(name) > -1) {
54 | var e = new Error('Event name "' + name + '" is reserved for channel.')
55 | logger.fatal(e.stack)
56 | process.exit(1)
57 | }
58 | })
59 | }
60 | Object.keys(channels).forEach(function(namespace) {
61 | check(channels[namespace])
62 | })
63 | check(service)
64 | }
65 |
66 | exports.createChannelAdapters = function(channels, service) {
67 | logger.debug('createChannelAdapters', {
68 | service: service.name
69 | }).trace(channels)
70 |
71 | var chnBackend = socketmq()
72 | var chnAdapters = {}
73 |
74 | Object.keys(channels).forEach(function(namespace) {
75 | chnAdapters[namespace] = chnBackend.channel(namespace)
76 | })
77 |
78 | // Add channel methods and chnBackend to service instance
79 | service.getChannel = function(namespace) {
80 | var chn = chnAdapters[namespace]
81 | if (!chn) {
82 | logger.fatal('Service channel has no such namespace', {
83 | service: service.name,
84 | namespace: namespace
85 | })
86 | process.exit(1)
87 | }
88 | return chn
89 | }
90 |
91 | service.pub = function(namespace) {
92 | var args = toArray(arguments)
93 | var adapter = this.getChannel(namespace)
94 | adapter.pubChn.apply(adapter, args.slice(1))
95 | return this
96 | }
97 |
98 | service.chnBackend = chnBackend
99 |
100 | return {
101 | chnBackend: chnBackend,
102 | chnAdapters: chnAdapters
103 | }
104 | }
105 |
106 | exports.setupChannels = function(channels, chnAdapters, initChannel, service, next) {
107 | var namespaces = Object.keys(channels)
108 |
109 | logger.debug('setupChannels', {
110 | service: service.name,
111 | namespaces: namespaces
112 | }).trace(channels)
113 |
114 | var pipeline = Superpipe()
115 | .set(exports)
116 | .set('service', service)()
117 |
118 | namespaces.forEach(function(namespace) {
119 | var channel = channels[namespace]
120 | var chnAdapter = chnAdapters[namespace]
121 | pipeline = pipeline
122 | .pipe(function() {
123 | return {
124 | channel: channel,
125 | chnAdapter: chnAdapter
126 | }
127 | }, null, ['channel', 'chnAdapter'])
128 | .concat(initChannel)
129 | .pipe(function(chnRepEvents) {
130 | channel.chnRepEvents = chnRepEvents
131 | }, 'chnRepEvents')
132 | })
133 |
134 | pipeline.pipe(next)()
135 | }
136 |
137 | exports.setDefaultChannelHandlers = function(channel) {
138 | if (!channel.auth) {
139 | logger.warn('Please define `auth` property in channel to set your own auth handler function. All requests will be allowed by default.')
140 | channel.auth = function(session, next) {
141 | next()
142 | }
143 | }
144 | if (!channel.join) {
145 | logger.warn('Please define `join` property in channel to set your own join handler function. All requests will be allowed by default.')
146 | channel.join = function(session, chn, next) {
147 | next()
148 | }
149 | }
150 | if (!channel.left) {
151 | logger.debug('Please define `left` property in channel to set your own leave handler function.')
152 | channel.left = function(session, chn, reason) {}
153 | }
154 | if (!channel.allow) {
155 | logger.debug('Please define `allow` property in channel to set your own allow handler function. All requests will be allowed by default.')
156 | channel.allow = function(session, chn, event, next) {
157 | next()
158 | }
159 | }
160 | if (!channel.error) {
161 | logger.debug('Please define `error` property in channel to set your own error handler function.')
162 | channel.error = function(error) {
163 | logger.error('Channel error', {
164 | error: error
165 | })
166 | }
167 | }
168 | }
169 |
170 | exports.bindChannelMethods = function(channel, chnAdapter, service) {
171 | Object.keys(channel).forEach(function(name) {
172 | var fn = channel[name]
173 | if ('function' === typeof fn)
174 | channel[name] = fn.bind(service)
175 | })
176 | }
177 |
178 | exports.attachEventHandlers = function(chnAdapter, channel) {
179 | logger.debug('attachEventHandlers').trace(channel)
180 |
181 | var excluded = ['auth', 'join', 'left', 'allow', 'error']
182 | var repEvents = []
183 | Object.keys(channel).forEach(function(name) {
184 | var handler = channel[name]
185 | if ('function' === typeof handler && -1 === excluded.indexOf(name)) {
186 | repEvents.push(name)
187 | chnAdapter.rep(name, handler)
188 | }
189 | })
190 | return {
191 | chnRepEvents: repEvents
192 | }
193 | }
194 |
195 | function prepareChnPipeline(pack, stream, next) {
196 | var meta = pack.meta
197 |
198 | var _meta = {
199 | sid: meta[SID],
200 | cookie: meta[CKE],
201 | session: meta[SSN]
202 | }
203 |
204 | return {
205 | chn: meta[MCH],
206 | meta: _meta,
207 | event: pack.event,
208 | parentNext: next
209 | }
210 | }
211 |
212 | function requireSession(session, next) {
213 | if (session)
214 | next()
215 | }
216 |
217 | exports.buildJoinHook = function(channel) {
218 | logger.debug('buildJoinHook').trace(channel)
219 |
220 | var chnJoinHook = Superpipe.pipeline()
221 | .pipe(prepareChnPipeline, 3, ['chn', 'meta', 'event', 'parentNext'])
222 | .pipe(channel.auth, ['meta', 'next'], ['session', 'ssn'])
223 | .pipe(requireSession, ['session', 'next'])
224 | .pipe(channel.join, ['session', 'chn', 'next'], ['repEvents', 'subEvents'])
225 | .pipe('parentNext', ['session', 'ssn', {
226 | REP: 'repEvents',
227 | SUB: 'subEvents'
228 | }])
229 | .error(channel.error)
230 | .toPipe()
231 | return {
232 | chnJoinHook: chnJoinHook
233 | }
234 | }
235 |
236 | exports.buildAllowHook = function(channel) {
237 | logger.debug('buildAllowHook').trace(channel)
238 |
239 | var chnAllowHook = Superpipe.pipeline()
240 | .pipe(prepareChnPipeline, 3, ['chn', 'meta', 'event', 'parentNext'])
241 | .pipe(channel.auth, ['meta', 'next'], ['session', 'ssn'])
242 | .pipe(requireSession, ['session', 'next'])
243 | .pipe(channel.allow, ['session', 'chn', 'event', 'next'])
244 | .pipe('parentNext?', 'session')
245 | .error(channel.error)
246 | .toPipe()
247 | return {
248 | chnAllowHook: chnAllowHook
249 | }
250 | }
251 |
252 | exports.attachChannelHooks = function(chnAdapter, channel, chnJoinHook, chnAllowHook) {
253 | logger.debug('attachChannelHooks').trace(channel)
254 |
255 | var allowHook = function(pack, stream, dispatch) {
256 | if (REQ === pack.type) {
257 | chnAllowHook(pack, stream, function(session) {
258 | pack.msg.unshift(session)
259 | dispatch(pack, stream)
260 | })
261 | } else if (INF === pack.type) {
262 | chnJoinHook(pack, stream, function(session, ssn, allowedEvents) {
263 | if (ACK === pack.event && (ssn || allowedEvents[REP] || allowedEvents[SUB])) {
264 | var meta = pack.meta
265 | // session value is set, ack gateway.
266 | if (ssn)
267 | meta[SSN] = ssn
268 | chnAdapter.queue.one([stream], {
269 | type: type.INF,
270 | event: type.ACK,
271 | msg: allowedEvents,
272 | meta: meta
273 | })
274 | } else if (LVE === pack.event && Array.isArray(pack.msg)) {
275 | channel.left(session, pack.meta[MCH], pack.msg[0])
276 | }
277 | })
278 | }
279 | }
280 | chnAdapter.allow(allowHook)
281 | }
282 |
283 | exports.startChnBackendServer = function(channels, chnBackend, chnEndpoint, next) {
284 | logger.debug('startChnBackendServer').trace({
285 | chnEndpoint: chnEndpoint,
286 | channels
287 | })
288 |
289 | var target = Url.parse(chnEndpoint)
290 | var server = chnBackend.bind(chnEndpoint)
291 | chnBackend.on('bind', function() {
292 | var address = server.address()
293 | var endpoint = target.protocol + '//' + target.hostname + ':' + address.port
294 |
295 | logger.info('Channel backend bound')
296 |
297 | var namespaces = {}
298 | Object.keys(channels).forEach(function(namespace) {
299 | namespaces[namespace] = {
300 | REP: channels[namespace].chnRepEvents
301 | }
302 | })
303 |
304 | logger.debug({
305 | address: address,
306 | endpoint: endpoint,
307 | namespaces: namespaces
308 | })
309 |
310 | next(null, 'chnAnn', {
311 | endpoint: endpoint,
312 | namespaces: namespaces
313 | })
314 | })
315 | }
316 |
--------------------------------------------------------------------------------
/lib/web/router.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var http = require('http')
3 | var jDad = require('jdad')
4 | var merge = require('lodash.merge')
5 | var logger = require('../logger')('micromono:web:router')
6 | var assign = require('lodash.assign')
7 | var argsNames = require('js-args-names')
8 | var httpProxy = require('http-proxy')
9 |
10 |
11 | /**
12 | * Create route definition object from route name.
13 | *
14 | * @param {String} routeName Path of the route
15 | * @return {Object} Route definition object
16 | */
17 | exports.generateRouteByName = function(routeName, defaultMethod) {
18 | if ('string' === typeof routeName) {
19 | var _path = routeName.split('::')
20 | var method = defaultMethod || 'get'
21 | var route = {
22 | name: routeName
23 | }
24 |
25 | if (2 === _path.length) {
26 | method = _path[0]
27 | _path = _path[1]
28 | } else if (3 === _path.length) {
29 | // This is GET route with page definition in format of:
30 | // `[method]::[routeUrl]::[templatePath]`
31 | // `get::/abc::public/abc.jsx!`
32 | method = _path[0]
33 | route.page = _path[2]
34 | _path = _path[1]
35 | } else {
36 | _path = routeName
37 | }
38 |
39 | route.path = _path
40 | route.method = method
41 |
42 | return route
43 | } else {
44 | logger.fatal('Route name must be a string.', {
45 | routeName: routeName
46 | })
47 | process.exit(1)
48 | }
49 | }
50 |
51 | /**
52 | * Normalize route definition to a portable format which could be easily used
53 | * by different web frameworks.
54 | *
55 | * ```javascript
56 | * route: {
57 | * 'get::/user/:name': function(req, res) {...}
58 | * }
59 | * ```
60 | *
61 | * will be formatted into:
62 | *
63 | * ```javascript
64 | * {
65 | * name: 'get::/user/:name',
66 | * method: 'get',
67 | * path: '/user/:name',
68 | * handler: [Function],
69 | * args: ['req', 'res'],
70 | * middleware: null
71 | * }
72 | * ```
73 | *
74 | * Example with route middleware:
75 | *
76 | * ```javascript
77 | * route: {
78 | * 'get::/user/:name': [function(req, res, next) {...}, function(req, res) {...}]
79 | * }
80 | * ```
81 | *
82 | * will be formatted into:
83 | *
84 | * ```javascript
85 | * {
86 | * name: 'get::/user/:name',
87 | * method: 'get',
88 | * path: '/user/:name',
89 | * handler: Function,
90 | * args: ['req', 'res'],
91 | * middleware: [Function]
92 | * }
93 | * ```
94 | *
95 | * @param {Object} route Route definition object.
96 | * @param {Service} service Instance of service.
97 | * @return {Object} Formatted routes object.
98 | */
99 | exports.normalizeRoutes = function(route, page, pageApiBaseUrl) {
100 | var _routes = {}
101 |
102 | Object.keys(route).forEach(function(routePath) {
103 | var middleware
104 | var routeHandler = route[routePath]
105 | var _route = exports.generateRouteByName(routePath)
106 |
107 | // Page contains mapping between url and template for client side routing
108 | // and rendering. e.g.:
109 | //
110 | // {`/abc`: 'public/abc.mustache'}
111 | //
112 | // The above tells url `/abc` uses template `public/abc.mustache` for client
113 | // side rendering.
114 | //
115 | if (page && page.hasOwnProperty(routePath))
116 | _route.page = page[routePath]
117 | if (Array.isArray(routeHandler)) {
118 | middleware = routeHandler
119 | routeHandler = middleware.pop()
120 | }
121 |
122 | _route.args = argsNames(routeHandler)
123 | _route.handler = routeHandler
124 | _route.middleware = middleware || null
125 |
126 | if (_route.page) {
127 | // Add the page api route
128 | var apiRoutePath = path.join(pageApiBaseUrl, _route.path)
129 |
130 | if (apiRoutePath.length > 1 && '/' === apiRoutePath[apiRoutePath.length - 1])
131 | apiRoutePath = apiRoutePath.slice(0, -1)
132 |
133 | if (!_routes[apiRoutePath]) {
134 | _route.api = apiRoutePath
135 | _routes[apiRoutePath] = {
136 | name: apiRoutePath,
137 | path: apiRoutePath,
138 | args: _route.args,
139 | method: _route.method,
140 | handler: _route.handler,
141 | middleware: _route.middleware
142 | }
143 | }
144 | }
145 |
146 | _routes[routePath] = _route
147 | })
148 |
149 | return _routes
150 | }
151 |
152 | exports.normalizeUses = function(use) {
153 | var _uses = {}
154 |
155 | Object.keys(use).forEach(function(name) {
156 | var _use = use[name]
157 | if (!Array.isArray(_use))
158 | _use = [_use]
159 |
160 | _use = _use.map(function(url) {
161 | if ('string' === typeof url)
162 | url = exports.generateRouteByName(url, 'default')
163 | return url
164 | })
165 | _uses[name] = _use
166 | })
167 |
168 | return _uses
169 | }
170 |
171 | exports.normalizeMiddlewares = function(middleware, middlewareBaseUrl) {
172 | var _middlewares = {}
173 |
174 | Object.keys(middleware).forEach(function(name) {
175 | _middlewares[name] = {
176 | name: name,
177 | path: path.join(middlewareBaseUrl, name),
178 | handler: middleware[name]
179 | }
180 | })
181 |
182 | return _middlewares
183 | }
184 |
185 | // Static function for processing remote middleware. It rebuilds the remote
186 | // middleware handler so it could be used locally.
187 | exports.rebuildRemoteMiddlewares = function(middlewares, service) {
188 | Object.keys(middlewares).forEach(function(mName) {
189 | var middleware = middlewares[mName]
190 | var _path = middleware.path
191 |
192 | logger.debug('Rebuild remote middleware handler', {
193 | path: _path,
194 | service: service.name,
195 | middleware: middleware.name
196 | })
197 |
198 | var handler = function() {
199 | return function(req, res, next) {
200 | var provider = service.scheduler.get()
201 | if (!provider) {
202 | exports.noProviderAvailable(req, res, next)
203 | return
204 | }
205 |
206 | var headers = assign({}, req.headers)
207 | delete headers['content-length']
208 | delete headers['x-micromono-req']
209 | var proxyReq = http.request({
210 | host: provider.host,
211 | port: provider.web.port,
212 | path: _path,
213 | method: req.method,
214 | headers: headers
215 | }, function(proxyRes) {
216 | if (103 === proxyRes.statusCode) {
217 | var reqMerge = proxyRes.headers['x-micromono-req']
218 | if (reqMerge) {
219 | try {
220 | reqMerge = jDad.parse(reqMerge, {
221 | decycle: true
222 | })
223 | merge(req, reqMerge)
224 | } catch (e) {
225 | logger.warn('Failed to merge request from remote middleware', {
226 | error: e,
227 | service: service.name
228 | })
229 | }
230 | }
231 | next()
232 | } else {
233 | if (res.set) {
234 | res.set(proxyRes.headers)
235 | } else {
236 | res.headers = res.headers || {}
237 | assign(res.headers, proxyRes.headers)
238 | }
239 | res.statusCode = proxyRes.statusCode
240 | proxyRes.pipe(res)
241 | }
242 | })
243 |
244 | proxyReq.on('error', function(err, req, res) {
245 | if (res) {
246 | res.writeHead && res.writeHead(500, {
247 | 'Content-Type': 'text/plain'
248 | })
249 | res.end && res.end('Service error')
250 | }
251 | logger.debug('Middleware "%s" proxy error', {
252 | error: err.stack,
253 | service: service.name,
254 | middleware: mName
255 | })
256 | })
257 |
258 | proxyReq.end()
259 | }
260 | }
261 |
262 | service.middleware[middleware.name] = handler
263 | })
264 | }
265 |
266 | /**
267 | * Get a function which proxy the requests to the real services.
268 | *
269 | * @param {String} baseUrl The base url for the target endpoint of the service.
270 | * @param {String} upgradeUrl The url for upgrade request (websockets).
271 | * @param {Object} httpServer
272 | * @return {Function} The proxy handler function.
273 | */
274 | exports.getProxyHandler = function(scheduler, httpServer, upgradeUrl) {
275 | var proxy = httpProxy.createProxyServer({})
276 |
277 | if (httpServer && upgradeUrl) {
278 | var re = new RegExp('^' + upgradeUrl)
279 | httpServer.on('upgrade', function(req, socket, head) {
280 | if (re.test(req.url)) {
281 | var provider = scheduler.get()
282 | if (!provider) {
283 | exports.noProviderAvailable(req, socket)
284 | } else {
285 | var target = 'http://' + provider.host + ':' + provider.web.port
286 | proxy.ws(req, socket, head, {
287 | target: target
288 | })
289 | }
290 | }
291 | })
292 | }
293 |
294 | proxy.on('error', function(err, req, res) {
295 | logger.debug('Proxy error', {
296 | error: err.stack,
297 | method: req.method,
298 | remoteAddress: res && res.remoteAddress || req.hostname
299 | })
300 |
301 | if (/^Error: socket hang up/.test(err.stack))
302 | // Ignore socket hang up error.
303 | return
304 |
305 | if (res && res.writeHead) {
306 | res.writeHead(500, {
307 | 'Content-Type': 'text/plain'
308 | })
309 | res.end('Service error')
310 | }
311 | })
312 |
313 | return function(req, res) {
314 | var provider = scheduler.get()
315 | if (!provider) {
316 | exports.noProviderAvailable(req, res)
317 | } else {
318 | var target = 'http://' + provider.host + ':' + provider.web.port
319 | if (upgradeUrl)
320 | target += '/' === upgradeUrl[0] ? upgradeUrl : '/' + upgradeUrl
321 |
322 | proxy.web(req, res, {
323 | target: target
324 | })
325 | }
326 | }
327 | }
328 |
329 | exports.noProviderAvailable = function(req, res) {
330 | res.writeHead && res.writeHead(503)
331 | res.end && res.end('Service Unavailable')
332 | }
333 |
--------------------------------------------------------------------------------
/lib/web/framework/express.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var jDad = require('jdad')
3 | var logger = require('../../logger')('micromono:web:express')
4 | var express = require('express')
5 | var difference = require('lodash.difference')
6 | var isPlainObject = require('lodash.isplainobject')
7 |
8 | /**
9 | * ExpressAdapter constructor
10 | */
11 | var ExpressAdapter = module.exports = function() {
12 | this.app = express()
13 | this.mapp = express()
14 | this.aapp = express()
15 | }
16 |
17 | ExpressAdapter.prototype.type = 'express'
18 |
19 | ExpressAdapter.prototype.startHttpServer = function(port, host, serviceName, callback) {
20 | logger.debug('Starting http server', {
21 | host: host,
22 | port: port,
23 | service: serviceName
24 | })
25 |
26 | // Attach internal asset app
27 | this.app.use(this.aapp)
28 | // Attach internal middleware app
29 | this.app.use(this.mapp)
30 | // Create and listen http requests
31 | var server = this.app.listen(port, host, function() {
32 | var address = server.address()
33 | logger.info('Http server started', {
34 | service: serviceName,
35 | address: address
36 | })
37 | callback({
38 | httpHost: address.address,
39 | httpPort: address.port,
40 | httpServer: server
41 | })
42 | })
43 | }
44 |
45 | ExpressAdapter.prototype.attachHttpServer = function(httpServer, setHttpRequestHandler) {
46 | // Attach internal asset app
47 | this.app.use(this.aapp)
48 | // Attach internal middleware app
49 | this.app.use(this.mapp)
50 | setHttpRequestHandler(this.app)
51 | }
52 |
53 | ExpressAdapter.prototype.proxyWebsocket = function(upgradeUrl, wsProxyHandler) {
54 | upgradeUrl = path.join('/', upgradeUrl, '*')
55 | this.app.get(upgradeUrl, wsProxyHandler)
56 | this.app.post(upgradeUrl, wsProxyHandler)
57 | }
58 |
59 | /**
60 | * Attach a single route to express app.
61 | *
62 | * @param {Object} route The route definition object which has following format:
63 | *
64 | * ```javascript
65 | * {
66 | * name: 'get::/user/:name',
67 | * method: 'get',
68 | * path: '/user/:name',
69 | * handler: [Function],
70 | * args: ['req', 'res'],
71 | * middleware: null
72 | * }
73 | * ```
74 | *
75 | * @param {Router} router The instance of Router.
76 | * @param {Service} service The service instance.
77 | * @return {ExpressAdapter} The instance of this adpater.
78 | */
79 | ExpressAdapter.prototype.attachRoutes = function(routes, service) {
80 | var app = this.app
81 |
82 | Object.keys(routes).forEach(function(routeName) {
83 | var route = routes[routeName]
84 | var method = route.method.toLowerCase()
85 | var routePath = route.path
86 |
87 | if ('param' === method)
88 | routePath = route.name.split('::')[1]
89 | logger.debug('Attach route handler', {
90 | service: service.name,
91 | method: method,
92 | routePath: routePath
93 | })
94 |
95 | var handler = route.handler.bind(service)
96 |
97 | var middlewares = []
98 | if (Array.isArray(route.middleware)) {
99 | // Only allow functions
100 | route.middleware.forEach(function(m) {
101 | if ('function' === typeof m)
102 | middlewares.push(m.bind(service))
103 | })
104 | }
105 |
106 | if (middlewares.length > 0)
107 | app[method](routePath, middlewares, handler)
108 | else
109 | app[method](routePath, handler)
110 | })
111 |
112 | return this
113 | }
114 |
115 | /**
116 | * Serve the static asset files accroding to service settings.
117 | *
118 | * @param {Asset} asset The instance of asset.
119 | * @param {Router} [router] The instance of Router.
120 | * @param {Service} [service] The service instance.
121 | * @return {ExpressAdapter} The instance of this adpater.
122 | */
123 | ExpressAdapter.prototype.serveLocalAsset = function(publicURL, publicPath, serviceName) {
124 | var assetApp = this.aapp
125 |
126 | if (!publicURL)
127 | throw new Error('Asset has no publicURL configured.')
128 |
129 | if (!publicPath)
130 | throw new Error('Asset has no publicPath configured.')
131 |
132 | publicURL.forEach(function(url) {
133 | logger.debug('Serve local static asset', {
134 | service: serviceName,
135 | publicPath: publicPath,
136 | url: url
137 | })
138 | assetApp.use(url, express.static(publicPath))
139 | })
140 |
141 | return this
142 | }
143 |
144 | ExpressAdapter.prototype.proxyAsset = function(assetInfo, proxyHandler, serviceName) {
145 | var assetApp = this.aapp
146 | var publicURL = assetInfo.publicURL
147 |
148 | publicURL.forEach(function(url) {
149 | logger.debug('Proxy static asset to remote', {
150 | service: serviceName,
151 | url: url
152 | })
153 | var assetUrl = path.join(url, '*')
154 | assetApp.get(assetUrl, proxyHandler)
155 | })
156 | }
157 |
158 | ExpressAdapter.prototype.injectAssetInfo = function(assetInfo) {
159 | this.app.use(function(req, res, next) {
160 | res.locals.asset = assetInfo
161 | next()
162 | })
163 | }
164 |
165 | /**
166 | * Use a middleware directly with framework without any modifications.
167 | *
168 | * @param {String} url The url which the middleware will be applied to.
169 | * @param {Any} middleware The middleware object accepts by the framework.
170 | * @return {ExpressAdapter} The instance of this adpater.
171 | */
172 | ExpressAdapter.prototype.useMiddleware = function(url, middleware, routes, service) {
173 | if (!Array.isArray(url))
174 | url = [url]
175 |
176 | var app = this.app
177 | var _middleware = middleware(app)
178 |
179 | url.forEach(function(link) {
180 | var method = link.method
181 | var mounted = false
182 |
183 | if ('default' === method && routes) {
184 | Object.keys(routes).forEach(function(routeName) {
185 | var _route = routes[routeName]
186 | // It's a router based middleware if we have exactly same url defined in route
187 | if (_route.path === link.path) {
188 | logger.debug('Attach router level middleware directly', {
189 | path: link.path,
190 | method: _route.method,
191 | service: service.name
192 | })
193 | app[_route.method](link.path, _middleware)
194 | mounted = true
195 | }
196 | })
197 | }
198 |
199 | if (false === mounted) {
200 | method = 'default' === method ? 'use' : method
201 | logger.debug('Use app level middleware directly', {
202 | path: link.path,
203 | method: method,
204 | service: service.name
205 | })
206 | app[method](link.path, _middleware)
207 | }
208 | })
209 | }
210 |
211 | /**
212 | * Attach a single middleware to express app.
213 | *
214 | * @param {Object} middleware The middleware definition object which has following format:
215 | *
216 | * ```javascript
217 | * {
218 | * // the name of the middleware
219 | * name: 'auth',
220 | * // relative path to the middleware
221 | * path: '/account/middleware/auth',
222 | * // the function for generating handler function
223 | * handler: function() {
224 | * ...
225 | * return function(req, res, next) {...}
226 | * }
227 | * }
228 | *
229 | * ```
230 | *
231 | * @param {Router} router The instance of Router.
232 | * @param {Service} service The service instance.
233 | * @return {ExpressAdapter} The instance of this adpater.
234 | */
235 |
236 |
237 | ExpressAdapter.prototype.attachLocalMiddlewares = function(middlewares, service) {
238 | var self = this
239 | Object.keys(middlewares).forEach(function(mName) {
240 | var middleware = middlewares[mName]
241 | //
242 | self._attachLocalMiddleware(middleware, service)
243 | })
244 | }
245 |
246 | // Private function for attaching local middlewares
247 | ExpressAdapter.prototype._attachLocalMiddleware = function(middleware, service) {
248 | var app = this.mapp
249 |
250 | middleware.handler = middleware.handler.bind(service)
251 | var handlerFn = middleware.handler()
252 |
253 | logger.debug('Attach local middleware', {
254 | path: middleware.path,
255 | service: service.name,
256 | middleware: middleware.name
257 | })
258 |
259 | app.use(middleware.path, function(req, res, next) {
260 | var semi = true
261 |
262 | // find out if the middleware wants to alter response
263 | var _writeHead = req.writeHead
264 | var _write = req.write
265 | var _end = req.end
266 |
267 | // record changes of `req` and `req.headers`
268 | var reqKeys = Object.keys(req)
269 | var headerKeys = Object.keys(req.headers)
270 |
271 | handlerFn(req, res, function(err) {
272 | semi = _writeHead === req.writeHead && _write === req.write && _end === req.end
273 |
274 | if (err) {
275 | logger.warn('Middleware error', {
276 | error: err
277 | })
278 | res.writeHead && res.writeHead(500, 'MicroMono middleware error.')
279 | res.end && res.end()
280 | return
281 | }
282 |
283 | if (semi) {
284 | // using a non-exists status code to indicate that the middleware
285 | // does not need to change the response
286 | res.statusCode = 103
287 |
288 | // we only care about properties which have been added to the `req`
289 | // object
290 | var changedReqKeys = difference(Object.keys(req), reqKeys)
291 | var changedHeaderKeys = difference(Object.keys(req.headers), headerKeys)
292 |
293 | var _req = {}
294 | var _headers = {}
295 |
296 | // Only allow value type `string`, `array` and `plain object`.
297 | // But, properties or members of object and array are not checked.
298 | // This should be able to handle most of the cases.
299 | changedReqKeys.forEach(function(key) {
300 | var value = req[key]
301 | if ('string' === typeof value || Array.isArray(value) || isPlainObject(value))
302 | _req[key] = value
303 | })
304 |
305 | changedHeaderKeys.forEach(function(key) {
306 | _headers[key] = req.headers[key]
307 | })
308 |
309 | if (Object.keys(_headers).length > 0)
310 | _req.headers = _headers
311 |
312 | if (Object.keys(_req).length > 0) {
313 | res.setHeader('X-MicroMono-Req', jDad.stringify(_req, {
314 | cycle: true
315 | }))
316 | }
317 |
318 | res.end()
319 | } else {
320 | // let the request go if this is a fully-remote middleware
321 | next()
322 | }
323 | })
324 | })
325 | }
326 |
--------------------------------------------------------------------------------