[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git\nnode_modules\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\n*~\n\nnode_modules*"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: required\ndist: trusty\nlanguage: node_js\n\nnode_js:\n  - \"6\"\n  - \"node\""
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) Richard Rodger and other contributors 2015-2016.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ramanujan\n\nThis project is an implementation of a microblogging system (similar\nto the basic functionality of [Twitter](http://twitter.com)) using the\n[microservice architecture](http://www.richardrodger.com/seneca-microservices-nodejs#.VyCjoWQrL-k)\nand [Node.js](https://nodejs.org). It is the example system discussed\nin Chapter 1 of [The Tao of Microservices](http://bit.ly/rmtaomicro)\nbook.\n\n\nThis purpose of this code base to help you learn how to design and\nbuild microservice systems. You can follow the construction through\nthe following steps:\n\n  * [Informal Requirements](#informal-requirements)\n  * [Message specification](#message-specification)\n  * [Service specification](#service-specification)\n\nThe system uses the\n[Seneca microservice framework](http://senecajs.org) to provide\ninter-service communication, and the\n[fuge microservice development tool](https://github.com/apparatus/fuge) to manage\nservices on a local development machine.\n\nThe system is also a demonstration of the\n[SWIM protocol](https://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf)\nfor peer-to-peer service discovery. A service registry is not needed\nas the network automatically reconfigures as microservices are added\nand removed.\n\n## Scope of the system\n\nThe system shows implementations of some of the essential features of\na microblogging system, but not all. Of particular focus is the use of\nuse of separate microservices for separate content pages, the use of\nmessages for data manipulation, and the use of a reactive message flow\nfor scaling.\n\nThe system does not provide for full accounts, or user\nauthentication. This could be added relatively easily using the\nseneca-auth and seneca-user plugins. Avoiding the need to login makes\nit easier to experiment as you can check multiple user experiences in\nthe browser.\n\nThe system exposes a (RESTish) JSON API over HTTP. However, the user\ninterface does _not_ use any client-side JavaScript, and entirely\ndelivered by server-side templates. This is an old school POST and\nredirect architecture to keep things simple and focused on the\nserver-side.\n\nThe system does not use persistent storage. You can easily make the\ndata persistent by using a Seneca data storage plugin. Keeping\neverything in memory makes for faster development, easier\nexperimentation, and lets you reboot the system if you end up with\ncorrupted data during development.\n\nThis system also provides an example of message tracing, using <a\nhref=\"http://zipkin.io/\">Zipkin</a>.\n\nThis example codebase does not provide a production deployment\nconfiguration. It does however provide a Docker Swarm example that you\ncan start building from.\n\n\n## Unit test examples\n\nThe system also includes example code for unit testing microservices.\nThe unit test code for each service is in the `test` subfolder of each\nmicroservice folder.\n\nTo run all the tests, use:\n\n``js\nnpm test\n``\n\nThe microservices can be unit tested independently and offline. Mock\nmessages are used to isolate each microservice from its network\ndependencies.\n\n\n## Running the system\n\nThe system is implemented in Node.js. You will need to have Node.js\nversion 4.0 or greater installed.\n\nYou can run the system directly from the command line by running the\n`start.sh` script:\n\n\n```sh\n$ ./start.sh\n```\n\nThis starts all the microservices in the background. While this is a\nquick way to get started, and verify that everything works, it is not\nthe most convenient option.\n\nTo have more control, you can use\n[fuge](https://github.com/apparatus/fuge) to run the microservice\nprocesses. Detailed instructions are provided next.\n\nYou can also use Docker to run the services. Example Dockerfiles are\nprovided in the\n[docker folder](https://github.com/senecajs/ramanujan/tree/master/docker). See\nbelow for more details.\n\n\n\n## Running with fuge\n\n\n#### Step 0: Install fuge\n\nFollow the instructions at [fuge repository](https://github.com/apparatus/fuge).\n\n_fuge_ is a development tool that lets you manage and control a\nmicroservice system for local development. The ramanjun repository is\npreconfigured for fuge (see the fuge folder), so you don't have to set\nanything up. The ramanujan system has 14 microservices (at last\ncount), so you really do need a local tool to help run the system.\n\nThis is trade-off that you make when you choose the microservice\narchitecture. You can move faster because you have very low coupling,\nand thus lower technical debt, but you will need more automation to\nmanage the higher number of moving parts.\n\n#### Step 1: Clone the repository\n\nUse git to clone the repository to a local development folder of your choice\n\n```sh\n$ git clone https://github.com/senecajs/ramanujan.git\n```\n\n#### Step 2: Download dependencies\n\nThe system needs a number of Node.js modules from npmjs.org to\nfunction correctly. These are the only external dependencies.\n\n```sh\n$ npm install\n```\n\nWait until the downloads complete. Some modules will require local\ncompilation. If you run into problems due to your operating system,\nusing a [Linux virtual machine](https://www.virtualbox.org/) is\nprobably your fastest solution. If you are using Windows,\n[configuring msbuild](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules)\nfirst is a good place to start.\n\n\nThe Zipkin message tracing is optional, and the system will work fine\nif there is no Zipkin installation. However, it is pretty easy to set\none up using <a href=\"http://docker.com\">Docker</a>:\n\n```sh\n$ docker run -d -p 9411:9411 openzipkin/zipkin\n```\n\nOnce you've run through some of the use cases, open <a\nhref=\"http://localhost:9411/\">http://localhost:9411/</a> to see the\nmessage traces. Note that this is a demonstration system, so all\ntraces are captured. In production you'll want to use a much lower\nsampling rate - see the Zipkin documentation for details.\n\n\n\n\n#### Step 3: Run fuge\n\nFrom within the repository folder, run the fuge shell.\n\n```sh\n$ fuge shell fuge/fuge.yml\n```\n\nThis will start fuge, output some logging messages about the ramanujan services, and then place you in an interactive repl:\n\n```sh\n...\nstarting shell..\n? fuge>\n```\n\nEnter the command `help` to see a list of commands. Useful commands\nare `ps` to list the status of the services (try it!), and `exit` to\nshutdown all services and exit. If your system state becomes corrupted\nin some way (this often happens during development due to bugs in\nmicroservices), exit fuge completely and restart the fuge shell.\n\n\n#### Step 4: Start up the system\n\nTo start the system, use the fuge command:\n\n```sh\n...\n? fuge> start all\n```\n\nYou see a list of startup logs from each service. _fuge_ prefixes the\nlogs for each service with the service names, and gives them different\ncolors so they are easy to tell apart. This also makes is easy to\nreview message flows. The system takes about a few seconds to start\nall microservices.\n\nNow use the `ps` command to list the state of the services. They\nshould all be running.\n\n### Using the system\n\nOpen your web browser to interact with the system. The steps below\ndefine a \"happy path\" to validate the basic functionality of the\nsystem.\n\n#### Step 1: Post microblogs entries for user _foo_\n\nOpen `http://localhost:8000/foo`.\n\nThis is the homepage for the user _foo_, and shows their timeline. The\ntimeline is a list of recent microblog entries from all users that the\nuser _foo_ follows, and also entries from _foo_ themselves.\n\n\nAt first there are no entries, so go ahead and post an entry, say:\n\n> _three colors: blue_\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm01.png\" width=\"440\">\n\nClick the _post_ button or hit return. You should see the new entry.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm02.png\" width=\"440\">\n\nPost another entry, say:\n\n> _three colors: white_\n\nYou should see both entries listed, with the most recent one at the\ntop. This is the timeline for user _foo_.\n\n\n#### Step 2: Review microblogs for user _foo_\n\nOpen `http://localhost:8000/mine/foo` (Or click on the _Mine_ navigation tab).\n\nThis shows only the entries for user _foo_, omitting entries for followers.\n\nYou can use this page to verify the entry list for a given user.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm03.png\" width=\"440\">\n\n\n#### Step 3: Load search page of user _bar_, and follow user _foo_\n\nOpen `http://localhost:8000/search/bar`.\n\nYou are now acting as user _bar_. Use the text _blue_ as a search query:\n\nClick on the _follow_ button. Now user _bar_ is following user _foo_.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm04.png\" width=\"440\">\n\n\n#### Step 4: Review timeline for user _bar_\n\nOpen `http://localhost:8000/bar` (Or click on the _Home_ navigation tab).\n\nYou should see the entries from user _foo_, as user _bar_ is now a follower.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm05.png\" width=\"440\">\n\n\n#### Step 5: Post microblog entries for user _bar_\n\nEnter and post the text:\n\n> _the sound of music_\n\nThe timeline for user _bar_ now includes entries from both users _foo_\nand _bar_.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm06.png\" width=\"440\">\n\n\n#### Step 5: Post microblog entries for user _foo_\n\nReturn to user _foo_. Open `http://localhost:8000/foo`.\n\nPost a new entry:\n\n> _three colors: red_\n\nYou should see entries only for user _foo_, as _foo_ does **not** follow _bar_.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm07.png\" width=\"440\">\n\n\n#### Step 6: Load microblog timeline of user _bar_\n\nGo back to user _bar_. Open `http://localhost:8000/bar`.\n\nYou should see an updated list of entries, included all the entries\nfor user _foo_, as _bar_ **does** follow _foo_.\n\n<img src=\"https://github.com/senecajs/ramanujan/blob/master/img/rm08.png\" width=\"440\">\n\n\n### Starting and stopping services\n\nOne of the main benefits of a microservice system is that you can\ndeploy services independently. In a local development setting this\nmeans you should be able to start and stop services independently,\nwithout stopping and starting the entire system. This has a huge\nproductivity benefit as you don't have to wait for the entire system\nto ready itself.\n\nTo work on a particular service, update the code for that service, and\nthen stop and restart the service to see the new functionality. The\nrest of the system keeps working. To really get the maximum benefit\nfrom this technique, you need to avoid the use of schema validation\nfor your messages, and you must avoid creating hard couplings\n(services should not know about each other). That is why the Seneca\nframework provides pattern matching and transport independence as key\nfeatures - they enable rapid development.\n\nThe payoff for more deployment complexity is that you can change parts\nof the system dynamically - don't lose that ability!\n\n_fuge_ allows you to start and stop services using the 'start' and\n'stop' commands.\n\nTo stop a service (say, _search_), use the command:\n\n```sh\n? fuge> stop search\n```\n\nIf you now try to use the search feature, it will fail, but other\npages will still work. Another important benefit of microservices is that they can isolate errors in this way.\n\nTo restart the _search_ service, use:\n\n```sh\n? fuge> start search\n```\n\nAnd the search functionality works again. Notice that you did not have\nto do any manual configuration to let the other services know about\nthe new instance of the _search_ service. Notice also that the other\nservices knew almost instantaneously about the the new instance of the\n_search_ service. That's becuase the SWIM algorithm propogated that\ninformation quickly and efficiently throughout the network. No need\nfor 30 second timeouts to detect errors - SWIM works much more quickly\nas it has many observers (the other services) so can detect failure,\nand new services, very quickly with a high degree of confidence.\n\nYou can also run multiple instances of the same service. This lets you\nscale to handle load. The underlying seneca-mesh network will\nautomatically round-robin messages between all available services for\na given message. Just start the service again:\n\n```sh\n? fuge> start search\n```\n\nAnd is you now run the `ps` command in fuge, you'll see the count is 2\ninstances.\n\n\n### Accessing the network REPL\n\nThe system comes with a REPL service that lets you submit messages to the network manually. This is very useful for debugging. Access the REPL by telnetting into it:\n\n```sh\n$ telnet localhost 10001\n```\n\nUse the following message to see the user _foo's_ timeline:\n\n```sh\nseneca 2.0.1 7k/repl> timeline:list,user:foo\nIN  000000: { timeline: 'list', user: 'foo' } # t7/39 timeline:* (6ln6zlc2qaer) transport_client\nOUT 000000: { '0':\n   { user: 'foo',\n     text: 'three colors: red',\n     when: 1461759716373,\n     can_follow: false },\n  '1':\n   { user: 'foo',\n     text: 'three colors: white',\n     when: 1461759467135,\n     can_follow: false },\n  '2':\n   { user: 'foo',\n     text: 'three colors: blue',\n     when: 1461759353996,\n     can_follow: false } }\n```\n\nYou can enter messages directly into the terminal, in JSON format (the\nformat is lenient, see\n[jsonic](https://github.com/rjrodger/jsonic)). The output will show\nthe message data `IN` and `OUT` of the network.\n\nThe REPL is a JavaScript console environment. There is a `seneca`\nobject that you can use directly, calling any methods of the seneca\nAPI.\n\n```sh\nseneca 2.0.1 7k/repl> seneca.id\n'7k/repl'\n```\n\nTo get a list of all services on the network, and which messages they\nlisten for, try:\n\n```sh\nseneca 2.0.1 7k/repl> role:mesh,get:members\nIN  000001: { role: 'mesh', get: 'members' } # aa/ie get:members,role:mesh (9mxp6qx6zyox) get_members\nOUT 000001: {\n  ...\n  '4':\n   { pin: 'timeline:*',\n     port: 54932,\n     host: '0.0.0.0',\n     type: 'web',\n     model: 'consume',\n     instance: 'gt/timeline-shard' },\n  ...\n  }\n```\n\nThis message is so useful, that the repl service defines an alias for it: `m`.\n\nThe default configuration of the system uses shortened identifers to\nmake debugging easier.\n\n\n### Using the monitor\n\nYou can monitor the state of each service, and the message patterns\nthat it responds to, by running the `monitor` service separately in\nit's own terminal window. The `monitor` service prints a table of\nshowing each service, and dynamically updates the table as services\ncome and go. See\n[seneca-mesh](https://github.com/senecajs/seneca-mesh) for details.\n\n```sh\n$ node monitor/monitor.js \n```\n\n\n## Using Docker\n\nYou'll need to have the latest version of\n[Docker](https://www.docker.com/) installed.\n\nThe [docker](https://github.com/senecajs/ramanujan/tree/master/docker)\nfolder contains Docker image setup Makefiles and Dockerfiles. Run the\ntop level `Makefile` to build all the images:\n\n```\n$ cd docker\n$ make\n```\n\nThen deploy all the images using Docker Stack:\n\n```\n$ docker stack deploy -c ramanujan.yml ramanujan\n```\n\nThis will start up everything. The containers run in their own overlay\nnetwork, but you will be able to access the website and repl on\nlocalhost as with fuge.\n\nIf things go funny (hey, it's Docker), delete the stack, restart\nDocker, and try again:\n\n```\n$ docker stack rm ramanujan\n```\n\nYou can see some information about the containers with these commands:\n\n```\n$ docker stats\n$ docker services ls\n$ docker ps\n```\n\nTo view the monitor, run the it on the `repl` container:\n\n```\n$ docker exec -it `docker ps | grep repl | cut -f 1 -d ' '` /bin/sh\n# node monitor.js\n```\n\n\n## Informal Requirements\n\n> TODO\n\n## Message Specification\n\n> TODO\n\n## Service Specification\n\n> TODO\n\n## Help and Questions\n\n[github issue]: https://github.com/senecajs/ramanujan/issues\n[gitter-url]: https://gitter.im/senecajs/ramanujan\n\n\n## License\nCopyright (c) Richard Rodger and other contributors 2015-2016, Licensed under [MIT](/LICENSE).\n"
  },
  {
    "path": "api/api-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar Hapi   = require('hapi')\nvar Chairo = require('chairo')\nvar Seneca = require('seneca')\nvar Rif    = require('rif')\n\n\nvar tag = 'api'\n\nvar server = new Hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register({\n  register: Chairo,\n  options:{\n    seneca: Seneca({\n      tag: tag,\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n    //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/api/ping'},\n        {path: '/api/post/{user}', method: 'post'},\n        {path: '/api/follow/{user}', method: 'post'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.route({\n  method: 'GET', path: '/api/ping',\n  handler: function( req, reply ){\n    server.seneca.act(\n      'role:api,cmd:ping',\n      function(err,out) {\n        reply(err||out)\n      }\n  )}\n})\n\nserver.route({\n  method: 'POST', path: '/api/post/{user}',\n  handler: function( req, reply ){\n\n      console.log('/api/post A', req.params, req.payload)\n      \n    server.seneca.act(\n      'post:entry',\n      {user:req.params.user, text:req.payload.text},\n      function(err,out) {\n\t  console.log('/api/post B', err, out)\n\n\t  if( err ) return reply.redirect('/error')\n\n        reply.redirect(req.payload.from)\n      }\n    )}\n})\n\nserver.route({\n  method: 'POST', path: '/api/follow/{user}',\n  handler: function( req, reply ){\n    server.seneca.act(\n      'follow:user',\n      {user:req.params.user, target:req.payload.user},\n      function(err,out) {\n        if( err ) return reply.redirect('/error')\n\n        reply.redirect(req.payload.from)\n      }\n    )}\n})\n\n\nserver.seneca\n  .add('role:api,cmd:ping', function(msg,done){\n    done( null, {pong:true,api:true,time:Date.now()})\n  })\n    .use('mesh',{\n\thost: host,\n\tbases: BASES,\n\tsneeze: {\n          silent: JSON.parse(SILENT),\n          swim: {interval: 1111}\n        }\n    })\n\n\nserver.start(function(){\n  console.log(tag,server.info.host,server.info.port)\n})\n\n"
  },
  {
    "path": "base/base.js",
    "content": "// node base.js base0 39000 127.0.0.1 127.0.0.1:39000,127.0.0.1:39001\n// node base.js base1 39001 127.0.0.1 127.0.0.1:39000,127.0.0.1:39001\n\nvar TAG = process.env.TAG || process.argv[2] || 'base'\nvar PORT = process.env.PORT || process.argv[3] || 39999\nvar HOST = process.env.HOST || process.argv[4] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[5] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[6] || 'true'\n\n\nrequire('seneca')({\n  tag: TAG,\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n  //.test(console.log,'print')\n  //.use('zipkin-tracer', {sampling:1})\n  .use('mesh',{\n    isbase: true,\n    port: PORT,\n    host: HOST,\n    bases: BASES,\n    pin:'role:mesh',\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/Makefile",
    "content": "containers :\n\t$(MAKE) -C shared container\n\t$(MAKE) -C api container\n\t$(MAKE) -C base container\n\t$(MAKE) -C entry-cache container\n\t$(MAKE) -C entry-store container\n\t$(MAKE) -C fanout container\n\t$(MAKE) -C follow container\n\t$(MAKE) -C front container\n\t$(MAKE) -C home container\n\t$(MAKE) -C index container\n\t$(MAKE) -C mine container\n\t$(MAKE) -C post container\n\t$(MAKE) -C repl container\n\t$(MAKE) -C reserve container\n\t$(MAKE) -C search container\n\t$(MAKE) -C timeline container\n\t$(MAKE) -C timeline-shard container\n\t$(MAKE) -C monitor container\n\n.PHONY : containers\n"
  },
  {
    "path": "docker/api/Dockerfile",
    "content": "FROM shared\n\nADD api-service.js .\n\nCMD [\"node\", \"api-service.js\"]\n\n"
  },
  {
    "path": "docker/api/Makefile",
    "content": "container :\n\tcp ../../api/api-service.js .\n\tdocker build -t api .\n\tdocker images | grep api\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan  --name api -e HOST=eth0 -e BASES=base0:39000,base1:39000 api\n\nrm-single :\n\tdocker service rm api\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/api/api-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar Hapi   = require('hapi')\nvar Chairo = require('chairo')\nvar Seneca = require('seneca')\nvar Rif    = require('rif')\n\n\nvar tag = 'api'\n\nvar server = new Hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register({\n  register: Chairo,\n  options:{\n    seneca: Seneca({\n      tag: tag,\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n    //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/api/ping'},\n        {path: '/api/post/{user}', method: 'post'},\n        {path: '/api/follow/{user}', method: 'post'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.route({\n  method: 'GET', path: '/api/ping',\n  handler: function( req, reply ){\n    server.seneca.act(\n      'role:api,cmd:ping',\n      function(err,out) {\n        reply(err||out)\n      }\n  )}\n})\n\nserver.route({\n  method: 'POST', path: '/api/post/{user}',\n  handler: function( req, reply ){\n\n      console.log('/api/post A', req.params, req.payload)\n      \n    server.seneca.act(\n      'post:entry',\n      {user:req.params.user, text:req.payload.text},\n      function(err,out) {\n\t  console.log('/api/post B', err, out)\n\n\t  if( err ) return reply.redirect('/error')\n\n        reply.redirect(req.payload.from)\n      }\n    )}\n})\n\nserver.route({\n  method: 'POST', path: '/api/follow/{user}',\n  handler: function( req, reply ){\n    server.seneca.act(\n      'follow:user',\n      {user:req.params.user, target:req.payload.user},\n      function(err,out) {\n        if( err ) return reply.redirect('/error')\n\n        reply.redirect(req.payload.from)\n      }\n    )}\n})\n\n\nserver.seneca\n  .add('role:api,cmd:ping', function(msg,done){\n    done( null, {pong:true,api:true,time:Date.now()})\n  })\n    .use('mesh',{\n\thost: host,\n\tbases: BASES,\n\tsneeze: {\n          silent: JSON.parse(SILENT),\n          swim: {interval: 1111}\n        }\n    })\n\n\nserver.start(function(){\n  console.log(tag,server.info.host,server.info.port)\n})\n\n"
  },
  {
    "path": "docker/base/Dockerfile",
    "content": "FROM shared\n\nADD base.js .\n\nCMD [\"node\", \"base.js\"]\n\n"
  },
  {
    "path": "docker/base/Makefile",
    "content": "container :\n\tcp ../../base/base.js .\n\tdocker build -t base .\n\tdocker images | grep base\n\nrun-single-base0:\n\tdocker service create --replicas 1 --network ramanujan --name base0 -e TAG=base0 -e PORT=39000 -e HOST=base0 -e BASES=base0:39000,base1:39000 base\n\nrun-single-base1:\n\tdocker service create --replicas 1 --network ramanujan --name base1 -e TAG=base1 -e PORT=39000 -e HOST=base1 -e BASES=base0:39000,base1:39000 base\n\nrm-single-base0:\n\tdocker service rm base0\n\nrm-single-base1:\n\tdocker service rm base1\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/base/base.js",
    "content": "// node base.js base0 39000 127.0.0.1 127.0.0.1:39000,127.0.0.1:39001\n// node base.js base1 39001 127.0.0.1 127.0.0.1:39000,127.0.0.1:39001\n\nvar TAG = process.env.TAG || process.argv[2] || 'base'\nvar PORT = process.env.PORT || process.argv[3] || 39999\nvar HOST = process.env.HOST || process.argv[4] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[5] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[6] || 'true'\n\n\nrequire('seneca')({\n  tag: TAG,\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n  //.test(console.log,'print')\n  //.use('zipkin-tracer', {sampling:1})\n  .use('mesh',{\n    isbase: true,\n    port: PORT,\n    host: HOST,\n    bases: BASES,\n    pin:'role:mesh',\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/docker.txt",
    "content": "\n# These are development notes NOT instructions!\n\n\n# on host \n\n\ndocker-machine ls\n\ndocker-machine create --driver virtualbox manager1\ndocker-machine create --driver virtualbox worker1\ndocker-machine create --driver virtualbox worker2\n\ndocker-machine ls\n\n# MANAGER-IP\ndocker-machine ip manager1\n\ndocker-machine ssh manager1\n\n# inside manager1\n\ndocker swarm init --advertise-addr <MANAGER-IP>\n\ndocker swarm join-token worker\n\nexit\n\n\n\ndocker-machine ssh worker1\n\n# inside worker1\n\ndocker swarm join --token <TOKEN> <MANAGER-IP>:2377\n\nexit\n\n\n\ndocker-machine ssh worker2\n\n# inside worker2\n\ndocker swarm join --token <TOKEN> <MANAGER-IP>:2377\n\nexit\n\n\n\ndocker-machine ssh manager1 -A\n\n# inside manager1\n\ndocker node ls\n\ndocker network create --driver overlay ramanujan\n\ndocker network ls\n\n\n# install emacs\ntce\n# emacs.tcz\n# libXrandr.tcz\n# make.tcz\n\nTERM=vt100 emacs -nw  \n\n\n# IMPORTANT: use the same host name everywhere otherwise swim-js mappings will fail\n\n\n# TODO: order properly\n\ndocker service create --replicas 1 --network ramanujan --name base1 -e TAG=base1 -e PORT=39000 -e HOST=base\n1 -e BASES=base0:39000,base1:39000 base\n\n\ndocker service create --replicas 1 --network ramanujan -p 10001:10001 --name repl -e TAG=repl -e REPL_HOST=0.0.0.0  -e HOST=@eth2 -e BASES=base0:3900\n0,base1:39000 repl\n\n\n# eth2 as publish\ndocker service create --replicas 1 --network ramanujan --publish 8000:8000 --name front -e HOST=eth2\n -e BASES=base0:39000,base1:39000 front\n\n\n# eth0 as no publish\ndocker service create --replicas 1 --network ramanujan  --name home -e HOST=eth0 -e BASES=base0:39000,ba\nse1:39000 home\n\n\n\n# stack\n\ndocker stack deploy -c ramanujan.yml ramanujan\ndocker stack rm ramanujan\n\n\n# monitor\n\ndocker exec -it `docker ps | grep repl | cut -f 1 -d ' '` /bin/sh\n$ node monitor.js\n\n"
  },
  {
    "path": "docker/entry-cache/Dockerfile",
    "content": "FROM shared\n\nADD entry-cache-logic.js .\nADD entry-cache-service.js .\n\nCMD [\"node\", \"entry-cache-service.js\"]\n\n"
  },
  {
    "path": "docker/entry-cache/Makefile",
    "content": "container :\n\tcp ../../entry-cache/entry-cache-*.js .\n\tdocker build -t entry-cache .\n\tdocker images | grep entry-cache\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name entry-cache -e HOST=@eth0 -e BASES=base0:39000,base1:39000 entry-cache\n\nrm-single :\n\tdocker service rm entry-cache\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/entry-cache/entry-cache-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function entry_cache (options) {\n  var seneca = this\n\n  var cache = {}\n\n  seneca.add('store:save,kind:entry', function(msg, done) {\n    delete cache[msg.user]\n    msg.cache = true\n    this.act(msg, done)\n  })\n\n\n  seneca.add('store:list,kind:entry', function(msg, done) {\n    if( cache[msg.user] ) {\n      return done( null, cache[msg.user] )\n    }\n\n    msg.cache = true\n\n    this.act(msg, function(err,list){\n      if(err) return done(err)\n      cache[msg.user] = list\n      done(null,list)\n    })\n  })\n}\n"
  },
  {
    "path": "docker/entry-cache/entry-cache-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag:'entry-cache',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('basic')\n  .use('entity')\n  .use('entry-cache-logic')\n  .use('mesh',{\n    pin: 'store:*,kind:entry',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/entry-store/Dockerfile",
    "content": "FROM shared\n\nADD entry-store-logic.js .\nADD entry-store-service.js .\n\nCMD [\"node\", \"entry-store-service.js\"]\n\n"
  },
  {
    "path": "docker/entry-store/Makefile",
    "content": "container :\n\tcp ../../entry-store/entry-store-*.js .\n\tdocker build -t entry-store .\n\tdocker images | grep entry-store\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name entry-store -e HOST=@eth0 -e BASES=base0:39000,base1:39000 entry-store\n\nrm-single :\n\tdocker service rm entry-store\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/entry-store/entry-store-logic.js",
    "content": "module.exports = function entry_store (options) {\n  var seneca = this\n\n  seneca.add('store:save,kind:entry', function(msg, done) {\n    this\n      .make('entry', {\n        when: msg.when,\n        user: msg.user,\n        text: msg.text\n      })\n      .save$(function(err, entry) {\n        if(err) return done(err)\n\n        this.act(\n          {\n            timeline: 'insert',\n            users: [msg.user],\n          }, \n          entry, \n          function(err) {\n            return done(err, entry)\n          })\n      })\n  })\n\n  seneca.add('store:list,kind:entry', function(msg, done) {\n    this\n      .make('entry')\n      .list$( {user: msg.user}, function(err, list) {\n        if(err) return done(err)\n\n        list.reverse( function(a, b) {\n          return a.when - b.when\n        })\n\n        done( null, list )\n      })\n  })\n}\n"
  },
  {
    "path": "docker/entry-store/entry-store-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'entry-store',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('basic')\n  .use('entity')\n  .use('entry-store-logic')\n  .use('mesh',{\n    pin: 'store:*,kind:entry,cache:true',\n    bases: BASES,\n    host: HOST,\n    sneeze:{\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/fanout/Dockerfile",
    "content": "FROM shared\n\nADD fanout-logic.js .\nADD fanout-service.js .\n\nCMD [\"node\", \"fanout-service.js\"]\n\n"
  },
  {
    "path": "docker/fanout/Makefile",
    "content": "container :\n\tcp ../../fanout/fanout-*.js .\n\tdocker build -t fanout .\n\tdocker images | grep fanout\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name fanout -e HOST=@eth0 -e BASES=base0:39000,base1:39000 fanout\n\nrm-single :\n\tdocker service rm fanout\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/fanout/fanout-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function fanout (options) {\n  var seneca = this\n\n  seneca.add('fanout:entry', function(msg, done) {\n    done()\n\n    var entry = this.util.clean(msg)\n    delete entry.fanout\n\n    this.act('follow:list,kind:followers',{user:entry.user},function(err,userlist){\n      if(err) return\n\n      if( userlist && 0 < userlist.length ) {\n        this.act({\n          timeline: 'insert',\n          users: userlist,\n        }, entry)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "docker/fanout/fanout-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'fanout',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('fanout-logic')\n\n  .add('info:entry', function(msg,done){\n    delete msg.info\n    this.act('fanout:entry',msg,done)\n  })\n\n  .use('mesh',{\n    listen:[\n      {pin: 'fanout:*'},\n      {pin: 'info:entry', model:'observe'}\n    ],\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/follow/Dockerfile",
    "content": "FROM shared\n\nADD follow-logic.js .\nADD follow-service.js .\n\nCMD [\"node\", \"follow-service.js\"]\n\n"
  },
  {
    "path": "docker/follow/Makefile",
    "content": "container :\n\tcp ../../follow/follow-*.js .\n\tdocker build -t follow .\n\tdocker images | grep follow\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name follow -e HOST=@eth0 -e BASES=base0:39000,base1:39000 follow\n\nrm-single :\n\tdocker service rm follow\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/follow/follow-logic.js",
    "content": "var _ = require('lodash')\n\nmodule.exports = function follow (options) {\n  var seneca = this\n\n  seneca.add('follow:user', function(msg, done) {\n    var seneca = this\n\n    relate( seneca, 'followers', msg.target, msg.user, true, function(err) {\n      if( err ) return done(err)\n\n      relate( seneca, 'following', msg.user, msg.target, true, function(err) {\n        if( err ) return done(err)\n\n        seneca.act('store:list,kind:entry',{user:msg.target}, function(err,list) {\n          if( err ) return done(err)\n\n          _.each(list,function(entry){\n            seneca.act({\n              timeline: 'insert',\n              users: [msg.user],\n            }, entry.data$())\n          })\n\n          done()\n        })\n      })\n    })\n  })\n\n\n  seneca.add('follow:list', function(msg,done){\n    this\n      .make('follow')\n      .load$(msg.user, function(err,follow){\n        var list = (follow && follow[msg.kind]) || []\n        done(err, list)\n      })\n  })\n\n\n  function relate(seneca,relation,from,to,create,done) {\n    seneca\n      .make('follow')\n      .load$(from, function(err, follow) {\n        if( err ) return done(err)\n        \n        if (follow) {\n          add_follower( null, follow, done )\n        }\n        else if (create) {\n          this.act('reserve:create', {key: 'follow/'+from}, function (err, status) {\n            if( err ) return done(err)\n            \n            if( !status.ok ) {\n              return relate(this,relation,from,to,false,done)\n            }\n\n            var follow = this.make('follow',{id$:from})\n            follow[relation] = []\n            add_follower(err, follow, function (err) {\n              if( err ) return done(err)\n              \n              this.act('reserve:remove', {key: 'follow/'+from})\n              done()\n            })\n          })\n        }\n\n        function add_follower( err, follow, done ) {\n          if( err ) return done(err)\n\n          follow[relation] = (follow[relation] || [])\n          follow[relation].push(to)\n          follow[relation] = _.uniq(follow[relation])\n\n          follow.save$(done)\n        }\n      })\n  }\n}\n \n"
  },
  {
    "path": "docker/follow/follow-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'follow',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('follow-logic')\n  .use('mesh',{\n    pin: 'follow:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/front/Dockerfile",
    "content": "FROM shared\n\nADD front.js .\nADD www www\n\nCMD [\"node\", \"front.js\"]\n\n"
  },
  {
    "path": "docker/front/Makefile",
    "content": "container :\n\tcp ../../front/front.js .\n\tcp -r ../../front/www .\n\tdocker build -t front .\n\tdocker images | grep front\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --publish 8000:8000 --name front -e HOST=eth2 -e BASES=base0:39000,base1:39000 front\n\nrm-single :\n\tdocker service rm front\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/front/front.js",
    "content": "\"use strict\"\nvar HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nvar Hapi = require('hapi')\nvar Rif = require('rif')\n\nvar server = new Hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({ \n  port: 8000 // test with http://localhost:8000/api/ping\n})\n\nserver.register(require('inert'))\n\nserver.register({\n  register: require('wo'),\n  options: {\n    bases: BASES,\n      sneeze: {\n\thost: host,\n\tsilent: JSON.parse(SILENT),\n        swim: {interval: 1111}\n      }\n  }\n})\n\nserver.route({ \n  method: 'GET', path: '/api/ping', \n  handler: {\n    wo: {}\n  }\n})\n\nserver.route({\n  method: 'POST', path: '/api/post/{user}', \n  handler: {\n    wo: {\n      passThrough: true\n    }\n  }\n})\n\nserver.route({\n  method: 'POST', path: '/api/follow/{user}', \n  handler: {\n    wo: {\n      passThrough: true\n    }\n  }\n})\n\n\nserver.route({ \n  method: 'GET', path: '/mine/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\n\nserver.route({ \n  method: ['GET','POST'], path: '/search/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\n\nserver.route({ \n  method: 'GET', path: '/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\nserver.route({\n  path: '/favicon.ico',\n  method: 'get',\n  config: {\n    cache: {\n      expiresIn: 1000*60*60*24*21\n    }\n  },\n  handler: function(request, reply) {\n    reply().code(200).type('image/x-icon')\n  }\n})\n\nserver.route({\n  method: 'GET',\n  path: '/res/{path*}',\n  handler: {\n    directory: {\n      path: __dirname + '/www/res',\n    }\n  }\n})\n\n\nserver.start(function(){\n  console.log('front',server.info.uri)\n})\n"
  },
  {
    "path": "docker/front/www/res/site.css",
    "content": "* {\n  font-family: arial;\n  padding: 0px;\n  margin: 0px;\n  text-decoration: none;\n}\n\nbody {\n  background-color: #eef;\n}\n\ninput {\n  border: 1px solid #222;\n  font-size: 14pt;\n  padding: 2px;\n}\n\ninput[type='text'] {\n  width: 25%;\n}\n\ndiv.header {\n  background-color: #222;\n  margin: 0px 0px 10px 0px;\n  padding: 4px;\n}\n\ndiv.header a {\n  display: inline-block;\n  padding: 4px;\n  color: #eee;\n}\n\ndiv.header .nav_active {\n  background-color: #eef;\n  color: #222;\n}\n\ndiv.container {\n  padding: 4px;\n}\n\ndiv.entry {\n  padding: 2px;\n  margin: 4px 2px;\n  border: 1px solid #ccc;\n  width: 33%;\n  color: #666;\n}\n\ndiv.text {\n  color: #222;\n  padding: 2px;\n  margin-top: 2px;\n}\n"
  },
  {
    "path": "docker/home/Dockerfile",
    "content": "FROM shared\n\nADD home-service.js .\nADD www www\n\nCMD [\"node\", \"home-service.js\"]\n\n"
  },
  {
    "path": "docker/home/Makefile",
    "content": "container :\n\tcp ../../home/home-service.js .\n\tcp -r ../../home/www .\n\tdocker build -t home .\n\tdocker images | grep home\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan  --name home -e HOST=eth0 -e BASES=base0:39000,base1:39000 home\n\nrm-single :\n\tdocker service rm home\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/home/home-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar tag = 'home'\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: tag,\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n      //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: 'GET', path: '/{user}',\n  handler: function( req, reply )\n  {\n    server.seneca.act(\n      'timeline:list',\n      {user:req.params.user},\n      function( err, entrylist ) {\n        if(err) {\n          entrylist = []\n        }\n\n        reply.view('home',{\n          user: req.params.user,\n          entrylist: _.map(entrylist,function(entry){\n            entry.when = moment(entry.when).fromNow()\n            return entry\n          })\n        })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n    host:host,\n    bases:BASES,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n})\n\n\nserver.start(function(){\n  console.log(tag,server.info.host,server.info.port)\n})\n\n"
  },
  {
    "path": "docker/home/www/home.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/{{../user}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "docker/home/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\" class=\"nav_active\">Home</a>\n    <a href=\"/mine/{{user}}\">Mine</a> \n    <a href=\"/search/{{user}}\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "docker/index/Dockerfile",
    "content": "FROM shared\n\nADD index-logic.js .\nADD index-service.js .\n\nCMD [\"node\", \"index-service.js\"]\n\n"
  },
  {
    "path": "docker/index/Makefile",
    "content": "container :\n\tcp ../../index/index-*.js .\n\tdocker build -t index .\n\tdocker images | grep index\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name index -e HOST=@eth0 -e BASES=base0:39000,base1:39000 index\n\nrm-single :\n\tdocker service rm index\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/index/index-logic.js",
    "content": "// Business logic for the index microservice.\n// Provides a full text search index for microblog entries.\n\n\n// Modules providing a simple in-memory full text search index.\n// In production you could replace these with API calls to elasticsearch\n// or similar.\nvar Levelup = require('levelup')\nvar Memdown = require('memdown')\nvar Search  = require('search-index')\n\n\n// This is the standard way to define a Seneca plugin.\nmodule.exports = function index (options) {\n\n  // The plugin Seneca instance is provided by `this`.\n  // This Seneca instance tracks patterns against this plugin\n  // as an aid to debugging.\n  var seneca = this\n\n\n  // The search index. This is the internal state of the service. In general.\n  // services should *not* have internal state, as it has to be synchronized\n  // between multiple instances. This service is purely for demonstration purposes,\n  // and only a single instance should be run.\n  var index\n\n\n  // The Seneca patterns that this plugin defines.\n  // This is the `interface` for this plugin - matching messages will end up here.\n  seneca.add('search:query', search_query)\n  seneca.add('search:insert', search_insert)\n  seneca.add('init:index', init)\n\n\n  // Query the search index.\n  // The implementation logic consists of calls to the search index API.\n  function search_query (msg, done) {\n    console.log(terms)\n\n    var terms = msg.query.split(/ +/)\n\n    var query = {\n      query: {\n        AND: {text:terms}\n      }\n    }\n\n    index.search(query, function (err, out) {\n      var hits = (out && out.hits) || []\n\n      hits = hits.map(function (hit) {\n        return hit.document\n      })\n\n      done(null, hits)\n    })\n  }\n\n\n  // Insert a document into the search index.\n  function search_insert (msg, done) {\n    index.add([{\n      id: msg.id,\n      text: msg.text,\n      user: msg.user,\n      when: msg.when\n    }], {}, done)\n  }\n\n\n  // Initialize the plugin. This is the standard mechanism to initialize a Seneca\n  // plugin - by defining a special pattern of the form init:<plugin-name>.\n  function init (msg, done) {\n    Search({\n      indexes: Levelup('si', {\n        db: Memdown, \n        valueEncoding: 'json'\n      })\n    }, function(err, si) {\n      if (err) return done(err)\n      index = si\n      done()\n    })\n  }\n}\n"
  },
  {
    "path": "docker/index/index-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'index',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n\n  .use('index-logic')\n\n  .add('info:entry', function(msg,done){\n    delete msg.info\n    this.act('search:insert',msg,done)\n  })\n\n  .use('mesh',{\n    listen:[\n      {pin: 'search:*'},\n      {pin: 'info:entry', model:'observe'}\n    ],\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/mine/Dockerfile",
    "content": "FROM shared\n\nADD mine-service.js .\nADD www www\n\nCMD [\"node\", \"mine-service.js\"]\n\n"
  },
  {
    "path": "docker/mine/Makefile",
    "content": "container :\n\tcp ../../mine/mine-service.js .\n\tcp -r ../../mine/www .\n\tdocker build -t mine .\n\tdocker images | grep mine\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan  --name mine -e HOST=eth0 -e BASES=base0:39000,base1:39000 mine\n\nrm-single :\n\tdocker service rm mine\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/mine/mine-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || 0\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: 'mine',\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n      //.use('zipkin-tracer', {sampling:1})\n      .use('entity')\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/mine/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: 'GET', path: '/mine/{user}',\n  handler: function( req, reply )\n  {\n    server.seneca.act(\n      'store:list,kind:entry',\n      {user:req.params.user},\n      function( err, entrylist ) {\n        if(err) {\n          entrylist = []\n        }\n\n        reply.view('mine',{\n          user: req.params.user,\n          entrylist: _.map(entrylist,function(entry){\n            entry.when = moment(entry.when).fromNow()\n            return entry\n          })\n        })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n    bases:BASES,\n    host:host\n})\n\nserver.start(function(){\n  console.log('mine',server.info.host,server.info.port)\n})\n\n\n"
  },
  {
    "path": "docker/mine/www/home.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/{{../user}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "docker/mine/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\">Home</a>\n    <a href=\"/mine/{{user}}\" class=\"nav_active\">Mine</a> \n    <a href=\"/search/{{user}}\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "docker/mine/www/mine.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/mine/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<small>{{this.when}}</small>\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "docker/post/Dockerfile",
    "content": "FROM shared\n\nADD post-logic.js .\nADD post-service.js .\n\nCMD [\"node\", \"post-service.js\"]\n\n"
  },
  {
    "path": "docker/post/Makefile",
    "content": "container :\n\tcp ../../post/post-*.js .\n\tdocker build -t post .\n\tdocker images | grep post\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name post -e HOST=@eth0 -e BASES=base0:39000,base1:39000 post\n\nrm-single :\n\tdocker service rm post\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/post/post-logic.js",
    "content": "module.exports = function post (options) {\n  var seneca = this\n\n  seneca.add('post:entry', function(msg, done) {\n    var entry = this.util.clean(msg)\n    delete entry.post\n\n    entry.when = Date.now()\n\n    this.act('store:save,kind:entry', entry, function(err,entry) {\n\tdone(err)\n\n      if( !err ) {\n        this.act('info:entry',entry.data$())\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "docker/post/post-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'post',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: { short_logs: true }\n})\n  //.use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('post-logic')\n\n  .use('mesh',{\n    pin: 'post:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/ramanujan.yml",
    "content": "\nversion: '3'\n\nnetworks:\n  ramanujan:\n    driver: overlay\n\nservices:\n  base0:\n    image: base\n    environment:\n      TAG: base0\n      PORT: 39000\n      HOST: base0\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      ramanujan:\n        aliases: \n          - base0\n    deploy:\n      replicas: 1\n\n  base1:\n    image: base\n    environment:\n      TAG: base1\n      PORT: 39001\n      HOST: base1\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      ramanujan:\n        aliases: \n          - base1\n    deploy:\n      replicas: 1\n\n  repl:\n    image: repl\n    environment:\n      REPL_HOST: 0.0.0.0\n      HOST: '@eth2'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    ports:\n      - '10001:10001'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  front:\n    image: front\n    environment:\n      HOST: 'eth2'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    ports:\n      - '8000:8000'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  home:\n    image: home\n    environment:\n      HOST: 'eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  mine:\n    image: mine\n    environment:\n      HOST: 'eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  search:\n    image: search\n    environment:\n      HOST: 'eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  api:\n    image: api\n    environment:\n      HOST: 'eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  entry-store:\n    image: entry-store\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  entry-cache:\n    image: entry-cache\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  index:\n    image: index\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  reserve:\n    image: reserve\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  fanout:\n    image: fanout\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  follow:\n    image: follow\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  post:\n    image: post\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  timeline:\n    image: timeline\n    environment:\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  timeline-shard-0:\n    image: timeline-shard\n    environment:\n      SHARD: 0,\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n\n\n  timeline-shard-1:\n    image: timeline-shard\n    environment:\n      SHARD: 1,\n      HOST: '@eth0'\n      BASES: base0:39000,base1:39001\n      SILENT: 'false'\n    networks:\n      - ramanujan\n    deploy:\n      replicas: 1\n"
  },
  {
    "path": "docker/repl/Dockerfile",
    "content": "FROM shared\n\nADD repl-service.js .\nADD monitor.js .\n\nEXPOSE 10001\n\nCMD [\"node\", \"repl-service.js\"]\n\n"
  },
  {
    "path": "docker/repl/Makefile",
    "content": "container :\n\tcp ../../repl/repl-service.js .\n\tcp ../../monitor/monitor.js .\n\tdocker build -t repl .\n\tdocker images | grep repl\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan -p 10001:10001 --name repl -e REPL_HOST=0.0.0.0  -e HOST=@eth2 -e BASES=base0:39000,base1:39000 repl\n\n\nrm-single :\n\tdocker service rm repl\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/repl/monitor.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '127.0.0.1:39000,127.0.0.1:39001').split(',')\n\nrequire('seneca')()//({log: 'silent'})\n  .use('mesh',{\n    bases: BASES,\n    host: HOST,\n    monitor: true,\n    tag: null\n  })\n"
  },
  {
    "path": "docker/repl/repl-service.js",
    "content": "var REPL_PORT = parseInt(process.env.REPL_PORT || process.argv[2] || 10001)\nvar REPL_HOST = process.env.REPL_HOST || process.argv[3] || '127.0.0.1'\nvar HOST = process.env.HOST || process.argv[4] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[5] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[6] || 'true'\n\n\nvar repl = require('seneca-repl');\n\n\nvar seneca = require('seneca')({\n  tag: 'repl',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n//.use('zipkin-tracer', {sampling:1})\n.use('mesh',{\n  tag: null, // ensures membership of all tagged meshes\n  bases: BASES,\n  host: HOST,\n  make_entry: function( entry ) {\n    if( 'wo' === entry.tag$ ) {\n      return {\n        route: JSON.stringify(entry.route),\n        host: entry.host,\n        port: entry.port,\n        identifier: entry.identifier$\n      }\n    }\n  },\n  sneeze:{\n    silent: JSON.parse(SILENT),\n    swim: {interval: 1111}\n  }\n})\n.use(repl)\n.ready(function () {\n  seneca.repl({\n    port: REPL_PORT,\n    host: REPL_HOST,\n      alias: {\n      m: 'role:mesh,get:members'\n    }\n  })\n})\n"
  },
  {
    "path": "docker/reserve/Dockerfile",
    "content": "FROM shared\n\nADD reserve-logic.js .\nADD reserve-service.js .\n\nCMD [\"node\", \"reserve-service.js\"]\n\n"
  },
  {
    "path": "docker/reserve/Makefile",
    "content": "container :\n\tcp ../../reserve/reserve-*.js .\n\tdocker build -t reserve .\n\tdocker images | grep reserve\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name reserve -e HOST=@eth0 -e BASES=base0:39000,base1:39000 reserve\n\nrm-single :\n\tdocker service rm reserve\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/reserve/reserve-logic.js",
    "content": "var _ = require('lodash')\n\nmodule.exports = function follow (options) {\n  var seneca = this\n\n  var interval = options.interval || 22\n  var expires  = options.expires || 1111\n\n\n  seneca.add('reserve:create', reserve_create)\n  seneca.add('reserve:read', reserve_read)\n  seneca.add('reserve:remove', reserve_remove)\n  seneca.add('reserve:state', reserve_state)\n\n\n  var reservations = {}\n  \n  \n  setInterval(function () {\n    var now = Date.now()\n    Object.keys(reservations).forEach(function (key) {\n      var when = reservations[key]\n\n      if (expires < now - when) {\n        delete reservations[key]\n      }\n    })\n  }, interval)\n\n\n  function reserve_create(msg, reply) {\n    var key = msg.key\n    \n    if (reservations[key]) {\n      return reply(null, {ok:false})\n    }\n\n    reservations[key] = Date.now()\n    return reply(null, {ok:true})\n  }\n\n\n  function reserve_read(msg, reply) {\n    return reply(null, {ok: !!reservations[msg.key]})\n  }\n\n\n  function reserve_remove(msg, reply) {\n    var found = !!reservations[msg.key]\n    delete reservations[msg.key]\n    return reply(null, {ok:found})\n  }\n\n\n  function reserve_state(msg, reply) {\n    return reply(null, reservations)\n  }\n}\n \n"
  },
  {
    "path": "docker/reserve/reserve-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'reserve',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n  //.use('zipkin-tracer', {sampling:1})\n  .use('reserve-logic')\n\n  .use('mesh',{\n    pin: 'reserve:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent:JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/search/Dockerfile",
    "content": "FROM shared\n\nADD search-service.js .\nADD www www\n\nCMD [\"node\", \"search-service.js\"]\n\n"
  },
  {
    "path": "docker/search/Makefile",
    "content": "container :\n\tcp ../../search/search-service.js .\n\tcp -r ../../search/www .\n\tdocker build -t search .\n\tdocker images | grep search\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan  --name search -e HOST=eth0 -e BASES=base0:39000,base1:39000 search\n\nrm-single :\n\tdocker service rm search\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/search/search-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || 0\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: 'search',\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n\t  //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {method: ['GET','POST'], path: '/search/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT)\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: ['GET','POST'],\n  path: '/search/{user}',\n  handler: function( req, reply )\n  {\n    var query\n      = (req.query ? (null == req.query.query ? '' : ' '+req.query.query) : '')\n      + (req.payload ? (null == req.payload.query ? '' : ' '+req.payload.query) : '')\n\n    query = query.replace(/^ +/,'')\n    query = query.replace(/ +$/,'')\n\n    server.seneca.act(\n      'follow:list,kind:following',\n      {user:req.params.user},\n      function(err,following){\n        if( err ) {\n          following = []\n        }\n\n        this.act(\n          'search:query',\n          {query: query },\n          function( err, entrylist ) {\n            if(err) {\n              this.log.warn(err)\n              entrylist = []\n            }\n\n            reply.view('search',{\n              query: encodeURIComponent(query),\n              user: req.params.user,\n              entrylist: _.map(entrylist,function(entry){\n                entry.when = moment(entry.when).fromNow()\n                entry.can_follow =\n                  req.params.user != entry.user &&\n                  !_.includes(following,entry.user)\n                return entry\n              })\n            })\n          })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n  bases:BASES,\n  host:host,\n  sneeze:{\n    silent: JSON.parse(SILENT),\n    swim: {interval: 1111}\n  }\n})\n\nserver.start(function(){\n  console.log('search',server.info.uri)\n})\n\n"
  },
  {
    "path": "docker/search/www/home.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/{{../user}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "docker/search/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\">Home</a>\n    <a href=\"/mine/{{user}}\">Mine</a> \n    <a href=\"/search/{{user}}\" class=\"nav_active\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "docker/search/www/search.html",
    "content": "<form action=\"/search/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"query\">\n<input type=\"submit\" value=\"search\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/search/{{../user}}?query={{{../query}}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "docker/shared/Dockerfile",
    "content": "\nFROM mhart/alpine-node:4\n\nRUN apk add --no-cache make gcc g++ python git\n\nADD package.json /\n\nRUN npm install\n\n\n\n\n"
  },
  {
    "path": "docker/shared/Makefile",
    "content": "\ncontainer :\n\tcp ../../package.json .\n\tdocker build -t shared .\n\tdocker images | grep shared\n\nclean :\n\trm -f *~\n\trm -f *.json\n\n.PHONY : container clean\n\n"
  },
  {
    "path": "docker/shared/package.json",
    "content": "{\n  \"name\": \"ramanujan\",\n  \"version\": \"0.0.1\",\n  \"description\": \"ramanujan\",\n  \"main\": \"ramanujan.js\",\n  \"scripts\": {\n    \"test\": \"lab -v */test -I nil\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/senecajs/ramanujan.git\"\n  },\n  \"keywords\": [\n    \"seneca\",\n    \"demo\",\n    \"ramanujan\"\n  ],\n  \"author\": \"Richard Rodger richardrodger.com\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/senecajs/ramanujan/issues\"\n  },\n  \"homepage\": \"https://github.com/senecajs/ramanujan\",\n  \"dependencies\": {\n    \"chairo\": \"2.2.1\",\n    \"handlebars\": \"4.0.5\",\n    \"hapi\": \"15.0.3\",\n    \"inert\": \"4.0.2\",\n    \"levelup\": \"1.3.3\",\n    \"lodash\": \"4.15.0\",\n    \"memdown\": \"1.2.0\",\n    \"moment\": \"2.14.1\",\n    \"search-index\": \"0.8.15\",\n    \"seneca\": \"3.3.0\",\n    \"seneca-balance-client\": \"0.6.0\",\n    \"seneca-basic\": \"0.5.0\",\n    \"seneca-demo-logger\": \"0.2.0\",\n    \"seneca-entity\": \"1.3.0\",\n    \"seneca-mesh\": \"0.10.0\",\n    \"seneca-repl\": \"0.3.0\",\n    \"seneca-zipkin-tracer\": \"0.1.0\",\n    \"sneeze\": \"https://github.com/rjrodger/sneeze#diagnostic\",\n    \"vision\": \"4.1.0\",\n    \"wo\": \"0.8.0\",\n    \"rif\": \"0.3.0\"\n  },\n  \"devDependencies\": {\n    \"code\": \"3.0.2\",\n    \"lab\": \"11.0.1\"\n  }\n}\n"
  },
  {
    "path": "docker/timeline/Dockerfile",
    "content": "FROM shared\n\nADD timeline-shard-service.js .\n\nCMD [\"node\", \"timeline-shard-service.js\"]\n\n"
  },
  {
    "path": "docker/timeline/Makefile",
    "content": "container :\n\tcp ../../timeline/timeline-shard-service.js .\n\tdocker build -t timeline .\n\tdocker images | grep timeline\n\nrun-single :\n\tdocker service create --replicas 1 --network ramanujan --name timeline -e HOST=@eth0 -e BASES=base0:39000,base1:39000 timeline\n\nrm-single :\n\tdocker service rm timeline\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/timeline/timeline-shard-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar _ = require('lodash')\n\nfunction resolve_shard(user) {\n  return user.charCodeAt(0) % 2\n}\n\nrequire('seneca')({\n  tag: 'timeline-shard',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n\n  .add('timeline:list',function(msg,done){\n    var shard = resolve_shard(msg.user)\n    this.act({shard:shard},msg,done)\n  })\n\n  .add('timeline:insert',function(msg,done){\n    var seneca = this\n    done()\n\n    var shards = [[],[]]\n\n    _.each(msg.users,function(user){\n      shards[resolve_shard(user)].push(user)\n    })\n\n    _.each(shards,function(users,shard){\n      if( 0 < users.length ) {\n        seneca.act({\n          shard: shard,\n          users: users,\n        }, msg)\n      }\n    })\n  })\n\n  .use('mesh',{\n    pin: 'timeline:*',\n      bases: BASES,\n      host: HOST,\n      sneeze:{\n        silent: JSON.parse(SILENT),\n        swim: {interval: 1111}\n      }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "docker/timeline-shard/Dockerfile",
    "content": "FROM shared\n\nADD timeline-service.js .\nADD timeline-logic.js .\n\nCMD [\"node\", \"timeline-service.js\"]\n\n"
  },
  {
    "path": "docker/timeline-shard/Makefile",
    "content": "container :\n\tcp ../../timeline/timeline-logic.js .\n\tcp ../../timeline/timeline-service.js .\n\tdocker build -t timeline-shard .\n\tdocker images | grep timeline-shard\n\nrun-single-0 :\n\tdocker service create --replicas 1 --network ramanujan --name timeline-shard-0 -e SHARD=0 -e HOST=@eth0 -e BASES=base0:39000,base1:39000 timeline-shard\n\nrun-single-1 :\n\tdocker service create --replicas 1 --network ramanujan --name timeline-shard-1 -e SHARD=1 -e HOST=@eth0 -e BASES=base0:39000,base1:39000 timeline-shard\n\nrm-single-0 :\n\tdocker service rm timeline-shard-0\n\nrm-single-1 :\n\tdocker service rm timeline-shard-1\n\n\nclean :\n\trm -f *~\n\trm -f *.js\n\trm -f *.json\n\n.PHONY : container clean\n"
  },
  {
    "path": "docker/timeline-shard/timeline-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function timeline (options) {\n  var seneca = this\n\n\n  seneca.add('timeline:insert', insert)\n  seneca.add('timeline:list', list)\n\n\n  function insert (msg, done) {\n    var seneca = this\n    done()\n\n    var entry = {\n      user: msg.user,\n      text: msg.text,\n      when: msg.when,\n    }\n\n    var users = _.clone(msg.users)\n    var index = -1\n\n    do_user()\n\n    function do_user(err) {\n      // try to complete the entire list, despite individual errors\n      if( err ) {\n        seneca.log.error(err)\n      }\n\n      ++index\n      if( users.length <= index ) return\n\n      insert_entry( users[index], true, do_user )\n    }\n\n\n    function insert_entry( user, create, next ) {\n      seneca\n        .make('timeline')\n        .load$(user,function(err,timeline){\n          if(err) return next(err)\n\n          if (timeline) {\n            do_insert(timeline, next)\n          }\n          else if (create) {\n            this.act(\n              'reserve:create', \n              {key: 'timeline/'+user}, \n              function (err, status) {\n                if( err ) return next(err)\n            \n                if( !status.ok ) {\n                  return insert_entry(user, false, next)\n                }\n\n                this\n                  .make('timeline',{id$:user, entrylist:[]})\n                  .save$( function(err,timeline) {\n                    if( err ) return next(err)\n\n                    do_insert(timeline, function (err) {\n                      if( err ) return next(err)\n\n                      this.act('reserve:remove', {key: 'timeline/'+user})\n                    })\n                  })\n              })\n          }\n\n          function do_insert (timeline, next) {\n            timeline.entrylist.push(entry)\n            timeline.entrylist.sort(function(a,b){\n              return b.when - a.when\n            })\n\n            timeline.save$(next)\n          }\n        })\n    }\n  }\n\n\n  function list (msg, done) {\n      this.act('follow:list,kind:following',{user:msg.user,default$:{}},function(err,following){\n      if( err ) return done(err)\n\n      this\n        .make('timeline')\n        .load$(msg.user,function(err,timeline){\n          var entrylist = (timeline ? timeline.entrylist : [])\n          _.each(entrylist,function(entry){\n            entry.can_follow = \n              entry.user !== msg.user && \n              !_.includes(following,entry.user)\n          })\n\n          done(err,entrylist)\n        })\n    })\n  }\n}\n"
  },
  {
    "path": "docker/timeline-shard/timeline-service.js",
    "content": "var SHARD = process.env.SHARD || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\nrequire('seneca')({\n  tag: 'timeline'+SHARD,\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n  .use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('timeline-logic')\n  .use('mesh',{\n    pin: 'timeline:*,shard:'+SHARD,\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent:JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "entry-cache/entry-cache-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function entry_cache (options) {\n  var seneca = this\n\n  var cache = {}\n\n  seneca.add('store:save,kind:entry', function(msg, done) {\n    delete cache[msg.user]\n    msg.cache = true\n    this.act(msg, done)\n  })\n\n\n  seneca.add('store:list,kind:entry', function(msg, done) {\n    if( cache[msg.user] ) {\n      return done( null, cache[msg.user] )\n    }\n\n    msg.cache = true\n\n    this.act(msg, function(err,list){\n      if(err) return done(err)\n      cache[msg.user] = list\n      done(null,list)\n    })\n  })\n}\n"
  },
  {
    "path": "entry-cache/entry-cache-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag:'entry-cache',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('basic')\n  .use('entity')\n  .use('entry-cache-logic')\n  .use('mesh',{\n    pin: 'store:*,kind:entry',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "entry-cache/test/entry-cache-test.js",
    "content": "// Unit test for the entry-cache microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('entry-cache', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('save-list', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    var data = {}\n    var miss = {}\n\n    seneca\n    .add('store:save,kind:entry,cache:true', function (msg, reply) {\n      var entry = this.make('entry', {\n        when: msg.when,\n        user: msg.user,\n        text: msg.text\n      })\n\n      data[msg.user] = data[msg.user] || []\n      data[msg.user].push(entry)\n      reply(null, entry)\n    })\n\n    .add('store:list,kind:entry,cache:true', function (msg, reply) {\n      miss[msg.user] = miss[msg.user] || 0\n      miss[msg.user]++\n\n      reply(null, data[msg.user])\n    })\n\n\n    // Gate the execution of actions for this instance. Gated actions are executed\n    // in sequence and each action waits for the previous one to complete. Gating\n    // is not required, but avoids excessive callbacks in the unit test code.\n    seneca\n      .gate()\n\n      .act({\n        store: 'save',\n        kind: 'entry',\n        when: Date.now(),\n        user: 'u0',\n        text: 't0'\n\n        // Because test mode is active, it is not necessary to handle\n        // callback errors. These are passed directly to the 'fin' callback.\n      }, function (ignore, entry) {\n        expect(entry.user).to.equal('u0')\n        expect(entry.text).to.equal('t0')\n\n        expect(data[entry.user].length).to.equal(1)\n        expect(data[entry.user][0].text).to.equal('t0')\n      })\n\n      .act({\n        store: 'list',\n        kind: 'entry',\n        user: 'u0'\n      }, function (ignore, list) {\n        expect(list.length).to.equal(1)\n        expect(list[0].text).to.equal('t0')\n\n        expect(miss['u0']).to.equal(1)\n      })\n\n      .act({\n        store: 'list',\n        kind: 'entry',\n        user: 'u0'\n      }, function (ignore, list) {\n        expect(list.length).to.equal(1)\n        expect(list[0].text).to.equal('t0')\n\n        expect(miss['u0']).to.equal(1)\n      })\n\n\n    // Second save invalidates cache and miss count confirms this.\n      .act({\n        store: 'save',\n        kind: 'entry',\n        when: Date.now(),\n        user: 'u0',\n        text: 't1'\n\n        // Because test mode is active, it is not necessary to handle\n        // callback errors. These are passed directly to the 'fin' callback.\n      }, function (ignore, entry) {\n        expect(entry.user).to.equal('u0')\n        expect(entry.text).to.equal('t1')\n\n        expect(data[entry.user].length).to.equal(2)\n        expect(data[entry.user][0].text).to.equal('t0')\n        expect(data[entry.user][1].text).to.equal('t1')\n      })\n\n      .act({\n        store: 'list',\n        kind: 'entry',\n        user: 'u0'\n      }, function (ignore, list) {\n        expect(list.length).to.equal(2)\n        expect(list[0].text).to.equal('t0')\n        expect(list[1].text).to.equal('t1')\n\n        expect(miss['u0']).to.equal(2)\n      })\n\n      .act({\n        store: 'list',\n        kind: 'entry',\n        user: 'u0'\n      }, function (ignore, list) {\n        expect(list.length).to.equal(2)\n        expect(list[0].text).to.equal('t0')\n        expect(list[1].text).to.equal('t1')\n\n        expect(miss['u0']).to.equal(2)\n      })\n\n    // Once all the tests are complete, invoke the test callback\n      .ready(fin)\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n\n\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when a error occurs anywhere.\n    .test(fin)\n  \n  // Load the plugin dependencies of the microservice\n    .use('basic')\n    .use('entity')\n\n  // Load the microservice business logic\n    .use(require('../entry-cache-logic'))\n}\n"
  },
  {
    "path": "entry-store/entry-store-logic.js",
    "content": "module.exports = function entry_store (options) {\n  var seneca = this\n\n  seneca.add('store:save,kind:entry', function(msg, done) {\n    this\n      .make('entry', {\n        when: msg.when,\n        user: msg.user,\n        text: msg.text\n      })\n      .save$(function(err, entry) {\n        if(err) return done(err)\n\n        this.act(\n          {\n            timeline: 'insert',\n            users: [msg.user],\n          }, \n          entry, \n          function(err) {\n            return done(err, entry)\n          })\n      })\n  })\n\n  seneca.add('store:list,kind:entry', function(msg, done) {\n    this\n      .make('entry')\n      .list$( {user: msg.user}, function(err, list) {\n        if(err) return done(err)\n\n        list.reverse( function(a, b) {\n          return a.when - b.when\n        })\n\n        done( null, list )\n      })\n  })\n}\n"
  },
  {
    "path": "entry-store/entry-store-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'entry-store',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('basic')\n  .use('entity')\n  .use('entry-store-logic')\n  .use('mesh',{\n    pin: 'store:*,kind:entry,cache:true',\n    bases: BASES,\n    host: HOST,\n    sneeze:{\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "entry-store/test/entry-store-test.js",
    "content": "// Unit test for the entry-store microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('entry-store', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('add-entry', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Gate the execution of actions for this instance. Gated actions are executed\n    // in sequence and each action waits for the previous one to complete. Gating\n    // is not required, but avoids excessive callbacks in the unit test code.\n    seneca\n      .gate()\n\n    // Send an action, and validate the response.\n      .act({\n        store: 'save',\n        kind: 'entry',\n        when: Date.now(),\n        user: 'u0',\n        text: 't0'\n\n        // Because test mode is active, it is not necessary to handle\n        // callback errors. These are passed directly to the 'fin' callback.\n      }, function (ignore, entry) {\n        expect(entry.user).to.equal('u0')\n        expect(entry.text).to.equal('t0')\n      })\n\n      .act({\n        store: 'save',\n        kind: 'entry',\n        when: Date.now(),\n        user: 'u0',\n        text: 't1'\n      }, function (ignore, entry) {\n        expect(entry.user).to.equal('u0')\n        expect(entry.text).to.equal('t1')\n      })\n\n      .act({\n        store: 'list',\n        kind: 'entry',\n        user: 'u0'\n      }, function (ignore, list) {\n        expect(list.length).to.equal(2)\n        expect(list[0].text).to.equal('t1')\n        expect(list[1].text).to.equal('t0')\n      })\n\n    // Once all the tests are complete, invoke the test callback\n      .ready(fin)\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when a error occurs anywhere.\n    .test(fin)\n\n  // Load the plugin dependencies of the microservice\n    .use('basic')\n    .use('entity')\n\n  // Load the microservice business logic\n    .use(require('../entry-store-logic'))\n  \n  // IMPORTANT! Provide mocks for any message patterns that the microservice\n  // depends on. In production these are provided by other microservices.\n  // To define a mock message, just add an action for the message pattern.\n    .add('timeline:insert', function (msg, reply) {\n      reply()\n    })\n}\n"
  },
  {
    "path": "fanout/fanout-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function fanout (options) {\n  var seneca = this\n\n  seneca.add('fanout:entry', function(msg, done) {\n    done()\n\n    var entry = this.util.clean(msg)\n    delete entry.fanout\n\n    this.act('follow:list,kind:followers',{user:entry.user},function(err,userlist){\n      if(err) return\n\n      if( userlist && 0 < userlist.length ) {\n        this.act({\n          timeline: 'insert',\n          users: userlist,\n        }, entry)\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "fanout/fanout-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'fanout',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('fanout-logic')\n\n  .add('info:entry', function(msg,done){\n    delete msg.info\n    this.act('fanout:entry',msg,done)\n  })\n\n  .use('mesh',{\n    listen:[\n      {pin: 'fanout:*'},\n      {pin: 'info:entry', model:'observe'}\n    ],\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "fanout/test/fanout-test.js",
    "content": "// Unit test for the fanout microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('fanout', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('entry', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Add dynamic mock messages, just for this test.\n    seneca\n      .add('follow:list,kind:followers', function (msg, reply) {\n        reply(null, ['red', 'green', 'blue'])\n      })\n\n    // The final verification step of this test.\n      .add('timeline:insert', function (msg, reply) {\n        expect(msg.users).to.equal(['red', 'green', 'blue'])\n        reply()\n        fin()\n      })\n\n    // No need to gate this test, as just one message sent.\n    seneca\n\n      .act({\n        fanout: 'entry',\n        user: 'foo',\n        text: 't0',\n        when: Date.now()\n      })\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when an error occurs anywhere.\n    .test(fin)\n\n  // Load the microservice business logic\n    .use(require('../fanout-logic'))\n}\n"
  },
  {
    "path": "follow/follow-logic.js",
    "content": "var _ = require('lodash')\n\nmodule.exports = function follow (options) {\n  var seneca = this\n\n  seneca.add('follow:user', function(msg, done) {\n    var seneca = this\n\n    relate( seneca, 'followers', msg.target, msg.user, true, function(err) {\n      if( err ) return done(err)\n\n      relate( seneca, 'following', msg.user, msg.target, true, function(err) {\n        if( err ) return done(err)\n\n        seneca.act('store:list,kind:entry',{user:msg.target}, function(err,list) {\n          if( err ) return done(err)\n\n          _.each(list,function(entry){\n            seneca.act({\n              timeline: 'insert',\n              users: [msg.user],\n            }, entry.data$())\n          })\n\n          done()\n        })\n      })\n    })\n  })\n\n\n  seneca.add('follow:list', function(msg,done){\n    this\n      .make('follow')\n      .load$(msg.user, function(err,follow){\n        var list = (follow && follow[msg.kind]) || []\n        done(err, list)\n      })\n  })\n\n\n  function relate(seneca,relation,from,to,create,done) {\n    seneca\n      .make('follow')\n      .load$(from, function(err, follow) {\n        if( err ) return done(err)\n        \n        if (follow) {\n          add_follower( null, follow, done )\n        }\n        else if (create) {\n          this.act('reserve:create', {key: 'follow/'+from}, function (err, status) {\n            if( err ) return done(err)\n            \n            if( !status.ok ) {\n              return relate(this,relation,from,to,false,done)\n            }\n\n            var follow = this.make('follow',{id$:from})\n            follow[relation] = []\n            add_follower(err, follow, function (err) {\n              if( err ) return done(err)\n              \n              this.act('reserve:remove', {key: 'follow/'+from})\n              done()\n            })\n          })\n        }\n\n        function add_follower( err, follow, done ) {\n          if( err ) return done(err)\n\n          follow[relation] = (follow[relation] || [])\n          follow[relation].push(to)\n          follow[relation] = _.uniq(follow[relation])\n\n          follow.save$(done)\n        }\n      })\n  }\n}\n \n"
  },
  {
    "path": "follow/follow-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'follow',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('follow-logic')\n  .use('mesh',{\n    pin: 'follow:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "follow/test/follow-test.js",
    "content": "// Unit test for the follow microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('follow', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('add-followers', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    seneca.act(\n      {\n        follow: 'user',\n        user: 'f0',\n        target: 'u0'\n      }, \n      function () {\n\n        seneca\n          .act(\n            {\n              follow: 'user',\n              user: 'f1',\n              target: 'u0'\n            },\n            function () {\n\n              seneca\n                .act({\n                  follow: 'list',\n                  user: 'u0',\n                  kind: 'followers'\n                }, function (ignore, list) {\n                  expect(list.length).to.equal(2)\n                  expect(list).to.equal(['f0', 'f1'])\n                  fin()\n                })\n            })\n      })\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  // In production, reservations will expire\n  var reservations = {}\n\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when a error occurs anywhere.\n    .test(fin)\n\n  // Load the plugin dependencies of the microservice\n    .use('entity')\n\n  // Load the microservice business logic\n    .use(require('../follow-logic'))\n  \n  // IMPORTANT! Provide mocks for any message patterns that the microservice\n  // depends on. In production these are provided by other microservices.\n  // To define a mock message, just add an action for the message pattern.\n\n    .add('timeline:insert', function (msg, reply) {\n      reply()\n    })\n\n    .add('store:list,kind:entry', function (msg, reply) {\n      reply(null, [])\n    })\n\n    .add('reserve:create', function (msg, reply) {\n      if (reservations[msg.key]) {\n        return reply(null, {ok: false})\n      }\n      else {\n        reservations[msg.key] = true\n        return reply(null, {ok: true})\n      }\n    })\n\n    .add('reserve:remove', function (msg, reply) {\n      reservations[msg.key] = false\n    })\n}\n"
  },
  {
    "path": "front/front.js",
    "content": "\"use strict\"\nvar HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nvar Hapi = require('hapi')\nvar Rif = require('rif')\n\nvar server = new Hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({ \n  port: 8000 // test with http://localhost:8000/api/ping\n})\n\nserver.register(require('inert'))\n\nserver.register({\n  register: require('wo'),\n  options: {\n    bases: BASES,\n      sneeze: {\n\thost: host,\n\tsilent: JSON.parse(SILENT),\n        swim: {interval: 1111}\n      }\n  }\n})\n\nserver.route({ \n  method: 'GET', path: '/api/ping', \n  handler: {\n    wo: {}\n  }\n})\n\nserver.route({\n  method: 'POST', path: '/api/post/{user}', \n  handler: {\n    wo: {\n      passThrough: true\n    }\n  }\n})\n\nserver.route({\n  method: 'POST', path: '/api/follow/{user}', \n  handler: {\n    wo: {\n      passThrough: true\n    }\n  }\n})\n\n\nserver.route({ \n  method: 'GET', path: '/mine/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\n\nserver.route({ \n  method: ['GET','POST'], path: '/search/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\n\nserver.route({ \n  method: 'GET', path: '/{user}', \n  handler: {\n    wo: {}\n  }\n})\n\nserver.route({\n  path: '/favicon.ico',\n  method: 'get',\n  config: {\n    cache: {\n      expiresIn: 1000*60*60*24*21\n    }\n  },\n  handler: function(request, reply) {\n    reply().code(200).type('image/x-icon')\n  }\n})\n\nserver.route({\n  method: 'GET',\n  path: '/res/{path*}',\n  handler: {\n    directory: {\n      path: __dirname + '/www/res',\n    }\n  }\n})\n\n\nserver.start(function(){\n  console.log('front',server.info.uri)\n})\n"
  },
  {
    "path": "front/www/res/site.css",
    "content": "* {\n  font-family: arial;\n  padding: 0px;\n  margin: 0px;\n  text-decoration: none;\n}\n\nbody {\n  background-color: #eef;\n}\n\ninput {\n  border: 1px solid #222;\n  font-size: 14pt;\n  padding: 2px;\n}\n\ninput[type='text'] {\n  width: 25%;\n}\n\ndiv.header {\n  background-color: #222;\n  margin: 0px 0px 10px 0px;\n  padding: 4px;\n}\n\ndiv.header a {\n  display: inline-block;\n  padding: 4px;\n  color: #eee;\n}\n\ndiv.header .nav_active {\n  background-color: #eef;\n  color: #222;\n}\n\ndiv.container {\n  padding: 4px;\n}\n\ndiv.entry {\n  padding: 2px;\n  margin: 4px 2px;\n  border: 1px solid #ccc;\n  width: 33%;\n  color: #666;\n}\n\ndiv.text {\n  color: #222;\n  padding: 2px;\n  margin-top: 2px;\n}\n"
  },
  {
    "path": "fuge/fuge.yml",
    "content": "fuge_global:\n  tail: true\n  monitor: false\n  auto_generate_environment: false\n  monitor_excludes:\n    - '**/node_modules/**'\n    - '**/.git/**'\n    - '**/*.log'\n  environment:\n    - BASES=127.0.0.1:39000,127.0.0.1:39001\n    - HOST=127.0.0.1\n    - SILENT=true\nbase0:\n  type: node\n  path: ../base\n  run: node base.js\n  ports:\n    - main=39000\n  environment:\n    - TAG=base0\n    - PORT=39000\nbase1:\n  type: node\n  path: ../base\n  run: node base.js\n  ports:\n    - main=39001\n  environment:\n    - TAG=base1\n    - PORT=39001\nfront:\n  type: node\n  path: ../front\n  run: node front.js\n  ports:\n    - main=8000\n  environment:\n    - PORT=8000\napi:\n  type: node\n  path: ../api\n  run: node api-service.js\n  ports:\n    - main=8001\n  environment:\n    - PORT=0\npost:\n  type: node\n  path: ../post\n  run: node post-service.js\nentry_store:\n  type: node\n  path: ../entry-store\n  run: node entry-store-service.js\nentry_cache:\n  type: node\n  path: ../entry-cache\n  run: node entry-cache-service.js\nrepl:\n  type: node\n  path: ../repl\n  run: node repl-service.js \n  ports:\n    - main=10001\n  environment:\n    - REPL_HOST=127.0.0.1\n    - REPL_PORT=10001\nmine:\n  type: node\n  path: ../mine\n  run: node mine-service.js\n  environment:\n    - PORT=0\nhome:\n  type: node\n  path: ../home\n  run: node home-service.js\n  environment:\n    - PORT=0\nindex:\n  type: node\n  path: ../index\n  run: node index-service.js\nsearch:\n  type: node\n  path: ../search\n  run: node search-service.js\n  environment:\n    - PORT=0\nfollow:\n  type: node\n  path: ../follow\n  run: node follow-service.js\nfanout:\n  type: node\n  path: ../fanout\n  run: node fanout-service.js\ntimeline0:\n  type: node\n  path: ../timeline\n  run: node timeline-service.js\n  environment:\n    - SHARD=0\ntimeline1:\n  type: node\n  path: ../timeline\n  run: node timeline-service.js\n  environment:\n    - SHARD=1\ntimeline_shard:\n  type: node\n  path: ../timeline\n  run: node timeline-shard-service.js\nreserve:\n  type: node\n  path: ../reserve\n  run: node reserve-service.js\n\n"
  },
  {
    "path": "home/home-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar tag = 'home'\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: tag,\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n      //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: 'GET', path: '/{user}',\n  handler: function( req, reply )\n  {\n    server.seneca.act(\n      'timeline:list',\n      {user:req.params.user},\n      function( err, entrylist ) {\n        if(err) {\n          entrylist = []\n        }\n\n        reply.view('home',{\n          user: req.params.user,\n          entrylist: _.map(entrylist,function(entry){\n            entry.when = moment(entry.when).fromNow()\n            return entry\n          })\n        })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n    host:host,\n    bases:BASES,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n})\n\n\nserver.start(function(){\n  console.log(tag,server.info.host,server.info.port)\n})\n\n"
  },
  {
    "path": "home/www/home.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/{{../user}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "home/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\" class=\"nav_active\">Home</a>\n    <a href=\"/mine/{{user}}\">Mine</a> \n    <a href=\"/search/{{user}}\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "index/index-logic.js",
    "content": "// Business logic for the index microservice.\n// Provides a full text search index for microblog entries.\n\n\n// Modules providing a simple in-memory full text search index.\n// In production you could replace these with API calls to elasticsearch\n// or similar.\nvar Levelup = require('levelup')\nvar Memdown = require('memdown')\nvar Search  = require('search-index')\n\n\n// This is the standard way to define a Seneca plugin.\nmodule.exports = function index (options) {\n\n  // The plugin Seneca instance is provided by `this`.\n  // This Seneca instance tracks patterns against this plugin\n  // as an aid to debugging.\n  var seneca = this\n\n\n  // The search index. This is the internal state of the service. In general.\n  // services should *not* have internal state, as it has to be synchronized\n  // between multiple instances. This service is purely for demonstration purposes,\n  // and only a single instance should be run.\n  var index\n\n\n  // The Seneca patterns that this plugin defines.\n  // This is the `interface` for this plugin - matching messages will end up here.\n  seneca.add('search:query', search_query)\n  seneca.add('search:insert', search_insert)\n  seneca.add('init:index', init)\n\n\n  // Query the search index.\n  // The implementation logic consists of calls to the search index API.\n  function search_query (msg, done) {\n    console.log(terms)\n\n    var terms = msg.query.split(/ +/)\n\n    var query = {\n      query: {\n        AND: {text:terms}\n      }\n    }\n\n    index.search(query, function (err, out) {\n      var hits = (out && out.hits) || []\n\n      hits = hits.map(function (hit) {\n        return hit.document\n      })\n\n      done(null, hits)\n    })\n  }\n\n\n  // Insert a document into the search index.\n  function search_insert (msg, done) {\n    index.add([{\n      id: msg.id,\n      text: msg.text,\n      user: msg.user,\n      when: msg.when\n    }], {}, done)\n  }\n\n\n  // Initialize the plugin. This is the standard mechanism to initialize a Seneca\n  // plugin - by defining a special pattern of the form init:<plugin-name>.\n  function init (msg, done) {\n    Search({\n      indexes: Levelup('si', {\n        db: Memdown, \n        valueEncoding: 'json'\n      })\n    }, function(err, si) {\n      if (err) return done(err)\n      index = si\n      done()\n    })\n  }\n}\n"
  },
  {
    "path": "index/index-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'index',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n\n  .use('index-logic')\n\n  .add('info:entry', function(msg,done){\n    delete msg.info\n    this.act('search:insert',msg,done)\n  })\n\n  .use('mesh',{\n    listen:[\n      {pin: 'search:*'},\n      {pin: 'info:entry', model:'observe'}\n    ],\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "index/test/index-test.js",
    "content": "// Unit test for the index microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('index', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('insert-query', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Gate the execution of actions for this instance. Gated actions are executed\n    // in sequence and each action waits for the previous one to complete. Gating\n    // is not required, but avoids excessive callbacks in the unit test code.\n    seneca\n      .gate()\n\n    // Send an action, and validate the response.\n      .act({\n        search: 'insert',\n        id: ''+Math.random(),\n        when: Date.now(),\n        user: 'u0',\n        text: 'lorem ipsum dolor sit amet'\n      }, function (ignore) {})\n\n      .act({\n        search: 'query',\n        query: 'ipsum',\n\n        // Because test mode is active, it is not necessary to handle\n        // callback errors. These are passed directly to the 'fin' callback.\n      }, function (ignore, list) {\n        expect(list.length).to.equal(1)\n        expect(list[0].text).to.equal('lorem ipsum dolor sit amet')\n      })\n\n    // Once all the tests are complete, invoke the test callback\n      .ready(fin)\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when an error occurs anywhere.\n    .test(fin)\n\n  // Load the microservice business logic.\n    .use(require('../index-logic'))\n}\n"
  },
  {
    "path": "mine/mine-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || 0\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: 'mine',\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n      //.use('zipkin-tracer', {sampling:1})\n      .use('entity')\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {path: '/mine/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: 'GET', path: '/mine/{user}',\n  handler: function( req, reply )\n  {\n    server.seneca.act(\n      'store:list,kind:entry',\n      {user:req.params.user},\n      function( err, entrylist ) {\n        if(err) {\n          entrylist = []\n        }\n\n        reply.view('mine',{\n          user: req.params.user,\n          entrylist: _.map(entrylist,function(entry){\n            entry.when = moment(entry.when).fromNow()\n            return entry\n          })\n        })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n    bases:BASES,\n    host:host\n})\n\nserver.start(function(){\n  console.log('mine',server.info.host,server.info.port)\n})\n\n\n"
  },
  {
    "path": "mine/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\">Home</a>\n    <a href=\"/mine/{{user}}\" class=\"nav_active\">Mine</a> \n    <a href=\"/search/{{user}}\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "mine/www/mine.html",
    "content": "<form action=\"/api/post/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"text\">\n<input type=\"hidden\" name=\"from\" value=\"/mine/{{user}}\">\n<input type=\"submit\" value=\"post\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<small>{{this.when}}</small>\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "monitor/monitor.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '127.0.0.1:39000,127.0.0.1:39001').split(',')\n\nrequire('seneca')()//({log: 'silent'})\n  .use('mesh',{\n    bases: BASES,\n    host: HOST,\n    monitor: true,\n    tag: null\n  })\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"ramanujan\",\n  \"version\": \"0.0.1\",\n  \"description\": \"ramanujan\",\n  \"main\": \"ramanujan.js\",\n  \"scripts\": {\n    \"test\": \"lab -v */test -I nil\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/senecajs/ramanujan.git\"\n  },\n  \"keywords\": [\n    \"seneca\",\n    \"demo\",\n    \"ramanujan\"\n  ],\n  \"author\": \"Richard Rodger richardrodger.com\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/senecajs/ramanujan/issues\"\n  },\n  \"homepage\": \"https://github.com/senecajs/ramanujan\",\n  \"dependencies\": {\n    \"chairo\": \"2.2.1\",\n    \"handlebars\": \"4.0.5\",\n    \"hapi\": \"15.0.3\",\n    \"inert\": \"4.0.2\",\n    \"levelup\": \"1.3.3\",\n    \"lodash\": \"4.15.0\",\n    \"memdown\": \"1.2.0\",\n    \"moment\": \"2.14.1\",\n    \"search-index\": \"0.8.15\",\n    \"seneca\": \"3.3.0\",\n    \"seneca-balance-client\": \"0.6.0\",\n    \"seneca-basic\": \"0.5.0\",\n    \"seneca-demo-logger\": \"0.2.0\",\n    \"seneca-entity\": \"1.3.0\",\n    \"seneca-mesh\": \"0.10.0\",\n    \"seneca-repl\": \"0.3.0\",\n    \"seneca-zipkin-tracer\": \"0.1.0\",\n    \"sneeze\": \"https://github.com/rjrodger/sneeze#diagnostic\",\n    \"vision\": \"4.1.0\",\n    \"wo\": \"0.8.0\",\n    \"rif\": \"0.3.0\"\n  },\n  \"devDependencies\": {\n    \"code\": \"3.0.2\",\n    \"lab\": \"11.0.1\"\n  }\n}\n"
  },
  {
    "path": "post/post-logic.js",
    "content": "module.exports = function post (options) {\n  var seneca = this\n\n  seneca.add('post:entry', function(msg, done) {\n    var entry = this.util.clean(msg)\n    delete entry.post\n\n    entry.when = Date.now()\n\n    this.act('store:save,kind:entry', entry, function(err,entry) {\n\tdone(err)\n\n      if( !err ) {\n        this.act('info:entry',entry.data$())\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "post/post-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\n\nrequire('seneca')({\n  tag: 'post',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: { short_logs: true }\n})\n  //.use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('post-logic')\n\n  .use('mesh',{\n    pin: 'post:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent: JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "post/test/post-test.js",
    "content": "// Unit test for the post microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('post', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('entry', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Add dynamic mock messages, just for this test.\n    seneca\n      .add('store:save,kind:entry', function (msg, reply) {\n        reply(null, this.make('entry', {\n          when: msg.when,\n          user: msg.user,\n          text: msg.text\n        }))\n      })\n\n    // The final verification step of this test.\n      .add('info:entry', function (msg, reply) {\n        expect(msg.user).to.equal('u0')\n        expect(msg.text).to.equal('t0')\n        reply()\n        fin()\n      })\n\n    // No need to gate this test, as just one message sent.\n    seneca\n\n      .act({\n        post: 'entry',\n        user: 'u0',\n        text: 't0',\n        when: Date.now()\n      })\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when an error occurs anywhere.\n    .test(fin)\n\n  // The test needs to construct entities\n    .use('entity')\n\n  // Load the microservice business logic\n    .use(require('../post-logic'))\n}\n"
  },
  {
    "path": "repl/repl-service.js",
    "content": "var REPL_PORT = parseInt(process.env.REPL_PORT || process.argv[2] || 10001)\nvar REPL_HOST = process.env.REPL_HOST || process.argv[3] || '127.0.0.1'\nvar HOST = process.env.HOST || process.argv[4] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[5] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[6] || 'true'\n\n\nvar repl = require('seneca-repl');\n\n\nvar seneca = require('seneca')({\n  tag: 'repl',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n//.use('zipkin-tracer', {sampling:1})\n.use('mesh',{\n  tag: null, // ensures membership of all tagged meshes\n  bases: BASES,\n  host: HOST,\n  make_entry: function( entry ) {\n    if( 'wo' === entry.tag$ ) {\n      return {\n        route: JSON.stringify(entry.route),\n        host: entry.host,\n        port: entry.port,\n        identifier: entry.identifier$\n      }\n    }\n  },\n  sneeze:{\n    silent: JSON.parse(SILENT),\n    swim: {interval: 1111}\n  }\n})\n.use(repl)\n.ready(function () {\n  seneca.repl({\n    port: REPL_PORT,\n    host: REPL_HOST,\n      alias: {\n      m: 'role:mesh,get:members'\n    }\n  })\n})\n"
  },
  {
    "path": "reserve/reserve-logic.js",
    "content": "var _ = require('lodash')\n\nmodule.exports = function follow (options) {\n  var seneca = this\n\n  var interval = options.interval || 22\n  var expires  = options.expires || 1111\n\n\n  seneca.add('reserve:create', reserve_create)\n  seneca.add('reserve:read', reserve_read)\n  seneca.add('reserve:remove', reserve_remove)\n  seneca.add('reserve:state', reserve_state)\n\n\n  var reservations = {}\n  \n  \n  setInterval(function () {\n    var now = Date.now()\n    Object.keys(reservations).forEach(function (key) {\n      var when = reservations[key]\n\n      if (expires < now - when) {\n        delete reservations[key]\n      }\n    })\n  }, interval)\n\n\n  function reserve_create(msg, reply) {\n    var key = msg.key\n    \n    if (reservations[key]) {\n      return reply(null, {ok:false})\n    }\n\n    reservations[key] = Date.now()\n    return reply(null, {ok:true})\n  }\n\n\n  function reserve_read(msg, reply) {\n    return reply(null, {ok: !!reservations[msg.key]})\n  }\n\n\n  function reserve_remove(msg, reply) {\n    var found = !!reservations[msg.key]\n    delete reservations[msg.key]\n    return reply(null, {ok:found})\n  }\n\n\n  function reserve_state(msg, reply) {\n    return reply(null, reservations)\n  }\n}\n \n"
  },
  {
    "path": "reserve/reserve-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[4] || 'true'\n\nrequire('seneca')({\n  tag: 'reserve',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs: true}\n})\n  //.use('zipkin-tracer', {sampling:1})\n  .use('reserve-logic')\n\n  .use('mesh',{\n    pin: 'reserve:*',\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent:JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "reserve/test/reserve-test.js",
    "content": "// Unit test for the reserve microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('reserve', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('create-remove', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Gate the execution of actions for this instance. Gated actions are executed\n    // in sequence and each action waits for the previous one to complete. Gating\n    // is not required, but avoids excessive callbacks in the unit test code.\n    seneca\n      .gate()\n\n    // Send an action, and validate the response.\n      .act({\n        reserve: 'create',\n        key: 'k0'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(true)\n      })\n\n      .act({\n        reserve: 'create',\n        key: 'k1'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(true)\n      })\n\n\n      .act({\n        reserve: 'create',\n        key: 'k0'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(false)\n      })\n\n      .act({\n        reserve: 'create',\n        key: 'k1'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(false)\n      })\n\n\n      .act({\n        reserve: 'remove',\n        key: 'k1'\n      })\n\n\n      .act({\n        reserve: 'create',\n        key: 'k0'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(false)\n      })\n\n      .act({\n        reserve: 'create',\n        key: 'k1'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(true)\n      })\n\n\n      .act({\n        reserve: 'remove',\n        key: 'k0'\n      })\n\n      .act({\n        reserve: 'remove',\n        key: 'k1'\n      })\n\n\n      .act({\n        reserve: 'create',\n        key: 'k0'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(true)\n      })\n\n      .act({\n        reserve: 'create',\n        key: 'k1'\n      }, function (ignore, status) {\n        expect(status.ok).to.equal(true)\n      })\n\n    setTimeout(function() {\n      seneca\n\n        .act({\n          reserve: 'read',\n          key: 'k0'\n        }, function (ignore, status) {\n          expect(status.ok).to.equal(false)\n        })\n\n        .act({\n          reserve: 'read',\n          key: 'k1'\n        }, function (ignore, status) {\n          expect(status.ok).to.equal(false)\n        })\n\n      fin()\n    }, 222)\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when a error occurs anywhere.\n    .test(fin)\n\n  // Load the microservice business logic\n    .use(require('../reserve-logic'), {interval: 11, expires: 111})\n}\n"
  },
  {
    "path": "search/search-service.js",
    "content": "\"use strict\"\n\nvar PORT = process.env.PORT || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || 0\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar hapi       = require('hapi')\nvar chairo     = require('chairo')\nvar vision     = require('vision')\nvar inert      = require('inert')\nvar handlebars = require('handlebars')\nvar _          = require('lodash')\nvar moment     = require('moment')\nvar Seneca     = require('seneca')\nvar Rif        = require('rif')\n\n\nvar server = new hapi.Server()\nvar rif = Rif()\n\n\nvar host = rif(HOST) || HOST\n\n\nserver.connection({\n    port: PORT,\n    host: host\n})\n\n\nserver.register( vision )\nserver.register( inert )\n\nserver.register({\n  register:chairo,\n  options:{\n    seneca: Seneca({\n      tag: 'search',\n      internal: {logger: require('seneca-demo-logger')},\n      debug: {short_logs:true}\n    })\n\t  //.use('zipkin-tracer', {sampling:1})\n  }\n})\n\nserver.register({\n  register: require('wo'),\n  options:{\n    bases: BASES,\n    route: [\n        {method: ['GET','POST'], path: '/search/{user}'},\n    ],\n    sneeze: {\n      host: host,\n      silent: JSON.parse(SILENT)\n    }\n  }\n})\n\n\nserver.views({\n  engines: { html: handlebars },\n  path: __dirname + '/www',\n  layout: true\n})\n\n\nserver.route({\n  method: ['GET','POST'],\n  path: '/search/{user}',\n  handler: function( req, reply )\n  {\n    var query\n      = (req.query ? (null == req.query.query ? '' : ' '+req.query.query) : '')\n      + (req.payload ? (null == req.payload.query ? '' : ' '+req.payload.query) : '')\n\n    query = query.replace(/^ +/,'')\n    query = query.replace(/ +$/,'')\n\n    server.seneca.act(\n      'follow:list,kind:following',\n      {user:req.params.user},\n      function(err,following){\n        if( err ) {\n          following = []\n        }\n\n        this.act(\n          'search:query',\n          {query: query },\n          function( err, entrylist ) {\n            if(err) {\n              this.log.warn(err)\n              entrylist = []\n            }\n\n            reply.view('search',{\n              query: encodeURIComponent(query),\n              user: req.params.user,\n              entrylist: _.map(entrylist,function(entry){\n                entry.when = moment(entry.when).fromNow()\n                entry.can_follow =\n                  req.params.user != entry.user &&\n                  !_.includes(following,entry.user)\n                return entry\n              })\n            })\n          })\n      })\n  }\n})\n\n\nserver.seneca.use('mesh',{\n  bases:BASES,\n  host:host,\n  sneeze:{\n    silent: JSON.parse(SILENT),\n    swim: {interval: 1111}\n  }\n})\n\nserver.start(function(){\n  console.log('search',server.info.uri)\n})\n\n"
  },
  {
    "path": "search/www/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Microblog</title>\n  <link rel=\"stylesheet\" href=\"/res/site.css\">\n</head>\n<body>\n\n  <div class=\"header\">\n    <a href=\"/{{user}}\">Home</a>\n    <a href=\"/mine/{{user}}\">Mine</a> \n    <a href=\"/search/{{user}}\" class=\"nav_active\">Search</a>\n  </div>\n\n  <div class=\"container\">\n    {{{ content }}}\n  </div>\n\n</body>\n</html>\n\n"
  },
  {
    "path": "search/www/search.html",
    "content": "<form action=\"/search/{{user}}\" method=\"post\">\n<input type=\"text\" name=\"query\">\n<input type=\"submit\" value=\"search\">\n</form>\n\n<br>\n\n{{#each entrylist}}\n<div class=\"entry\">\n<form action=\"/api/follow/{{../user}}\" method=\"post\">\n<b><a href=\"/{{this.user}}\">@{{this.user}}</a></b> <small>{{this.when}}</small>\n<input type=\"hidden\" name=\"from\" value=\"/search/{{../user}}?query={{{../query}}}\">\n<input type=\"hidden\" name=\"user\" value=\"{{this.user}}\">\n{{#if can_follow}}\n<input type=\"submit\" value=\"follow\">\n{{/if}}\n<br>\n<div class=\"text\">\n{{this.text}}\n</div>\n</form>\n</div>\n{{/each}}\n\n\n"
  },
  {
    "path": "start.sh",
    "content": "HOST=\"127.0.0.1\"\nBASES=\"127.0.0.1:39000,127.0.0.1:39001\"\nOPTS=\"\"\n\n# for demos use OPTS = '--seneca.options.debug.undead=true --seneca.options.plugin.mesh.sneeze.silent=1'\n\n\nnode base/base.js base0 39000 $HOST $BASES $OPTS &\nsleep 1\nnode base/base.js base1 39001 $HOST $BASES $OPTS &\nsleep 1\nnode front/front.js $HOST $BASES $OPTS &\nsleep 1\nnode api/api-service.js 0 $HOST $BASES $OPTS &\nsleep 1\nnode post/post-service.js $HOST $BASES $OPTS &\nsleep 1\nnode entry-store/entry-store-service.js $HOST $BASES $OPTS &\nsleep 1\nnode entry-cache/entry-cache-service.js $HOST $BASES $OPTS &\nsleep 1\nnode repl/repl-service.js 10001 $HOST $HOST $BASES $OPTS &\nsleep 1\nnode mine/mine-service.js 0 $HOST $BASES $OPTS &\nsleep 1\nnode home/home-service.js 0 $HOST $BASES $OPTS &\nsleep 1\nnode search/search-service.js 0 $HOST $BASES $OPTS &\nsleep 1\nnode index/index-service.js $HOST $BASES $OPTS &\nsleep 1\nnode follow/follow-service.js $HOST $BASES $OPTS &\nsleep 1\nnode fanout/fanout-service.js $HOST $BASES $OPTS &\nsleep 1\nnode timeline/timeline-service.js 0 $HOST $BASES $OPTS &\nsleep 1\nnode timeline/timeline-service.js 1 $HOST $BASES $OPTS &\nsleep 1\nnode timeline/timeline-shard-service.js $HOST $BASES $OPTS &\nsleep 1\nnode reserve/reserve-service.js $HOST $BASES $OPTS &\n\n\n\n\n\n"
  },
  {
    "path": "timeline/test/timeline-test.js",
    "content": "// Unit test for the timeline microservice.\n// Uses https://github.com/hapijs/lab but easy to refactor for other unit testers.\n\n// The utility function test_seneca constructs an instance of Seneca\n// suitable for test execution, using the seneca.test() method.\n\nvar Lab = require('lab')\nvar Code = require('code')\nvar Seneca = require('seneca')\n\nvar lab = exports.lab = Lab.script()\nvar describe = lab.describe\nvar it = lab.it\nvar expect = Code.expect\n\n// A suite of unit tests for this microservice.\ndescribe('timeline', function () {\n\n  // A unit test (the test callback is named 'fin' to distinguish it from others).\n  it('insert-list', function (fin) {\n\n    // Create a Seneca instance for testing.\n    var seneca = test_seneca(fin)\n\n    // Gate the execution of actions for this instance. Gated actions are executed\n    // in sequence and each action waits for the previous one to complete. Gating\n    // is not required, but avoids excessive callbacks in the unit test code.\n    seneca\n      .gate()\n\n    // Send an action; there's no response expected for this message.\n    // User aaa has posted entry t0 which is inserted into the timelines of\n    // users bbb and ccc.\n      .act({\n        timeline: 'insert',\n        user: 'aaa',\n        users: ['bbb', 'ccc'],\n        text: 't0',\n        when: Date.now()\n      })\n\n      .act({\n        timeline: 'insert',\n        user: 'bbb',\n        users: ['aaa', 'ccc'],\n        text: 't1',\n        when: Date.now()\n      })\n\n    // aaa has the t1 entry by bbb\n      .act({timeline: 'list', user: 'aaa'},\n           function (ignore, list) {\n             expect(list.length).to.equal(1)\n             expect(list[0].text).to.equal('t1')\n           })\n\n    // bbb has the to entry by aaa\n      .act({timeline: 'list', user: 'bbb'},\n           function (ignore, list) {\n             expect(list.length).to.equal(1)\n             expect(list[0].text).to.equal('t0')\n           })\n    \n    // ccc has nothing\n      .act({timeline: 'list', user: 'ccc'},\n           function (ignore, list) {\n             expect(list.length).to.equal(0)\n           })\n    \n    // Once all the tests are complete, invoke the test callback\n      .ready(fin)\n  })\n})\n\n\n// Construct a Seneca instance suitable for unit testing\nfunction test_seneca (fin) {\n  // In production, reservations will expire\n  var reservations = {}\n\n  return Seneca({log: 'test'})\n\n  // activate unit test mode. Errors provide additional stack tracing context.\n  // The fin callback is called when a error occurs anywhere.\n    .test(fin)\n\n  // Load the plugin dependencies of the microservice\n    .use('basic')\n    .use('entity')\n\n  // Load the microservice business logic\n    .use(require('../timeline-logic'))\n  \n  // IMPORTANT! Provide mocks for any message patterns that the microservice\n  // depends on. In production these are provided by other microservices.\n  // To define a mock message, just add an action for the message pattern.\n\n    .add('follow:list,kind:following', function (msg, reply) {\n      reply(null, ['bbb'])\n    })\n\n    .add('reserve:create', function (msg, reply) {\n      if (reservations[msg.key]) {\n        return reply(null, {ok: false})\n      }\n      else {\n        reservations[msg.key] = true\n        return reply(null, {ok: true})\n      }\n    })\n\n    .add('reserve:remove', function (msg, reply) {\n      reservations[msg.key] = false\n    })\n}\n"
  },
  {
    "path": "timeline/timeline-logic.js",
    "content": "'use strict'\n\nvar _ = require('lodash')\n\nmodule.exports = function timeline (options) {\n  var seneca = this\n\n\n  seneca.add('timeline:insert', insert)\n  seneca.add('timeline:list', list)\n\n\n  function insert (msg, done) {\n    var seneca = this\n    done()\n\n    var entry = {\n      user: msg.user,\n      text: msg.text,\n      when: msg.when,\n    }\n\n    var users = _.clone(msg.users)\n    var index = -1\n\n    do_user()\n\n    function do_user(err) {\n      // try to complete the entire list, despite individual errors\n      if( err ) {\n        seneca.log.error(err)\n      }\n\n      ++index\n      if( users.length <= index ) return\n\n      insert_entry( users[index], true, do_user )\n    }\n\n\n    function insert_entry( user, create, next ) {\n      seneca\n        .make('timeline')\n        .load$(user,function(err,timeline){\n          if(err) return next(err)\n\n          if (timeline) {\n            do_insert(timeline, next)\n          }\n          else if (create) {\n            this.act(\n              'reserve:create', \n              {key: 'timeline/'+user}, \n              function (err, status) {\n                if( err ) return next(err)\n            \n                if( !status.ok ) {\n                  return insert_entry(user, false, next)\n                }\n\n                this\n                  .make('timeline',{id$:user, entrylist:[]})\n                  .save$( function(err,timeline) {\n                    if( err ) return next(err)\n\n                    do_insert(timeline, function (err) {\n                      if( err ) return next(err)\n\n                      this.act('reserve:remove', {key: 'timeline/'+user})\n                    })\n                  })\n              })\n          }\n\n          function do_insert (timeline, next) {\n            timeline.entrylist.push(entry)\n            timeline.entrylist.sort(function(a,b){\n              return b.when - a.when\n            })\n\n            timeline.save$(next)\n          }\n        })\n    }\n  }\n\n\n  function list (msg, done) {\n      this.act('follow:list,kind:following',{user:msg.user,default$:{}},function(err,following){\n      if( err ) return done(err)\n\n      this\n        .make('timeline')\n        .load$(msg.user,function(err,timeline){\n          var entrylist = (timeline ? timeline.entrylist : [])\n          _.each(entrylist,function(entry){\n            entry.can_follow = \n              entry.user !== msg.user && \n              !_.includes(following,entry.user)\n          })\n\n          done(err,entrylist)\n        })\n    })\n  }\n}\n"
  },
  {
    "path": "timeline/timeline-service.js",
    "content": "var SHARD = process.env.SHARD || process.argv[2] || 0\nvar HOST = process.env.HOST || process.argv[3] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[4] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\nrequire('seneca')({\n  tag: 'timeline'+SHARD,\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n  .use('zipkin-tracer', {sampling:1})\n  .use('entity')\n  .use('timeline-logic')\n  .use('mesh',{\n    pin: 'timeline:*,shard:'+SHARD,\n    bases: BASES,\n    host: HOST,\n    sneeze: {\n      silent:JSON.parse(SILENT),\n      swim: {interval: 1111}\n    }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  },
  {
    "path": "timeline/timeline-shard-service.js",
    "content": "var HOST = process.env.HOST || process.argv[2] || '127.0.0.1'\nvar BASES = (process.env.BASES || process.argv[3] || '').split(',')\nvar SILENT = process.env.SILENT || process.argv[5] || 'true'\n\n\nvar _ = require('lodash')\n\nfunction resolve_shard(user) {\n  return user.charCodeAt(0) % 2\n}\n\nrequire('seneca')({\n  tag: 'timeline-shard',\n  internal: {logger: require('seneca-demo-logger')},\n  debug: {short_logs:true}\n})\n    //.use('zipkin-tracer', {sampling:1})\n\n  .add('timeline:list',function(msg,done){\n    var shard = resolve_shard(msg.user)\n    this.act({shard:shard},msg,done)\n  })\n\n  .add('timeline:insert',function(msg,done){\n    var seneca = this\n    done()\n\n    var shards = [[],[]]\n\n    _.each(msg.users,function(user){\n      shards[resolve_shard(user)].push(user)\n    })\n\n    _.each(shards,function(users,shard){\n      if( 0 < users.length ) {\n        seneca.act({\n          shard: shard,\n          users: users,\n        }, msg)\n      }\n    })\n  })\n\n  .use('mesh',{\n    pin: 'timeline:*',\n      bases: BASES,\n      host: HOST,\n      sneeze:{\n        silent: JSON.parse(SILENT),\n        swim: {interval: 1111}\n      }\n  })\n\n  .ready(function(){\n    console.log(this.id)\n  })\n"
  }
]