├── 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 | --------------------------------------------------------------------------------