<commit_before>/*
Copyright (c) 2011, Claus Augusti <claus@formatvorlage.de>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

"use strict";

/**
 *
 */

module.exports = function (tagmgr, resmgr, modcontainer) {
    if (!tagmgr || !resmgr || !modcontainer) { throw new Error('dependencies missing'); }
    var mod_sys                 = require('sys')
        , mod_path              = require('path')
        , mod_fs                = require('fs')
        , mod_promise           = require('promised-io/lib/promise')
        , mod_url               = require('url')
        , mod_querystring       = require('querystring')
        , Resource              = require('./resources.js').Resource
        , mod_resourcemanager   = resmgr
        , tagmanager            = tagmgr
        , logger                = require('./logger.js').getLogger(mod_path.basename(module.filename))
        , Renderer              = require('.//renderer.js')(tagmanager, modcontainer).Renderer
        , parser                = require('./parser.js').parseHtmlView
        , modulecontainer       = modcontainer
        , RAIN_INSTANCE_PARAM   = 'rain.instanceid'
        , MAX_RESPONSE_TIME     = 3000
        , ins = mod_sys.inspect
        , c = console.log;

    function handleControllerRequest (req, res, next) {
        logger.info('handleControllerRequest ' + req.url);

        (function (res) {
            var complete = false;
            modulecontainer.handleControllerRequest(req, res);
            setTimeout(function () {
                if (!complete) {
                    res.writeHead(404, { 'Content-Type' : 'text/plain'} );
                    res.end('timeout');
                }
            }, MAX_RESPONSE_TIME);
        })(res);
    }

    function handleViewRequest (req, res, next) {
        console.time('render');
        var parse         = mod_url.parse(req.url),
            query         = mod_querystring.parse(parse.query),
            outputMode    = (query['rain.output'] || 'html').toLowerCase(),
            modulename    = req.params[0],
            viewname      = req.params[1],
            module,                       // component config object associated to request
            resource,                     // view template resource of requested view
            data          = {},
            key;

        logger.info('handleViewRequest, module: ' + req.params[0] + ', view: ' + req.params[1]);
        for (key in query) {
            data['req_' + key] = query[key];
        }
        module = modulecontainer.resolveFromRequestPath(parse.pathname);
        var renderer = new Renderer(parse.pathname, outputMode, null, data);
        renderer.once('stateChanged', function (renderer) {
            if (renderer.state == Renderer.STATES.RENDERED) {
                if (renderer.mode == 'html') {
                    res.setHeader('Content-Type', 'text/html');
                    res.end(renderer.renderresult.content);
                } else {
                    res.setHeader('Content-Type', 'application/json');
                    res.end(JSON.stringify(renderer.renderresult));
                }
                console.timeEnd('render');
                //Renderer.showTree(render1);
            }
        });

        /*
        if (query[RAIN_INSTANCE_PARAM]) {
            logger.debug('requiring instance per request parameter');
            if (query[RAIN_INSTANCE_PARAM] instanceof Array) {
                query[RAIN_INSTANCE_PARAM].forEach(function (iid) { addInstance(iid, resource, renderer); });
            } else {
                addInstance(query[RAIN_INSTANCE_PARAM], resource, renderer);
            }
        }

        function addInstance (id, resource, renderer) {
            var dataurl = 'file://' + mod_path.join(__dirname, '..', 'instances', id);
            var instanceresource = mod_resourcemanager.loadResourceByUrl(dataurl);
            instanceresource.load();
            resource.addDependency(instanceresource);
            renderer.instanceResources[id] = instanceresource;
        }
        */
    }

    return {
        'handleControllerRequest'   : handleControllerRequest
        , 'handleViewRequest'       : handleViewRequest
    }
}
<commit_msg>better style<commit_after>/*
Copyright (c) 2011, Claus Augusti <claus@formatvorlage.de>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

"use strict";

/**
 *
 */

module.exports = function (tagmgr, resmgr, modcontainer) {
    if (!tagmgr || !resmgr || !modcontainer) { throw new Error('dependencies missing'); }
    var mod_sys                 = require('sys')
        , mod_path              = require('path')
        , mod_fs                = require('fs')
        , mod_promise           = require('promised-io/lib/promise')
        , mod_url               = require('url')
        , mod_querystring       = require('querystring')
        , Resource              = require('./resources.js').Resource
        , mod_resourcemanager   = resmgr
        , tagmanager            = tagmgr
        , logger                = require('./logger.js').getLogger(mod_path.basename(module.filename))
        , Renderer              = require('.//renderer.js')(tagmanager, modcontainer).Renderer
        , parser                = require('./parser.js').parseHtmlView
        , modulecontainer       = modcontainer
        , RAIN_INSTANCE_PARAM   = 'rain.instanceid'
        , MAX_RESPONSE_TIME     = 3000
        , ins = mod_sys.inspect
        , c = console.log;

    function handleControllerRequest (req, res, next) {
        logger.info('handleControllerRequest ' + req.url);
        (function (res) {
            modulecontainer.handleControllerRequest(req, res);
            setTimeout(function () {
                if (!res.finished) {
                    res.writeHead(404, { 'Content-Type' : 'text/plain'} );
                    res.end('timeout');
                }
            }, MAX_RESPONSE_TIME);
        })(res);
    }

    function handleViewRequest (req, res, next) {
        console.time('render');
        var parse         = mod_url.parse(req.url),
            query         = mod_querystring.parse(parse.query),
            outputMode    = (query['rain.output'] || 'html').toLowerCase(),
            modulename    = req.params[0],
            viewname      = req.params[1],
            module,                       // component config object associated to request
            resource,                     // view template resource of requested view
            data          = {},
            key;

        logger.info('handleViewRequest, module: ' + req.params[0] + ', view: ' + req.params[1]);
        for (key in query) {
            data['req_' + key] = query[key];
        }
        module = modulecontainer.resolveFromRequestPath(parse.pathname);
        var renderer = new Renderer(parse.pathname, outputMode, null, data);
        renderer.once('stateChanged', function (renderer) {
            if (renderer.state == Renderer.STATES.RENDERED) {
                if (renderer.mode == 'html') {
                    res.setHeader('Content-Type', 'text/html');
                    res.end(renderer.renderresult.content);
                } else {
                    res.setHeader('Content-Type', 'application/json');
                    res.end(JSON.stringify(renderer.renderresult));
                }
                console.timeEnd('render');
                //Renderer.showTree(render1);
            }
        });

        /*
        if (query[RAIN_INSTANCE_PARAM]) {
            logger.debug('requiring instance per request parameter');
            if (query[RAIN_INSTANCE_PARAM] instanceof Array) {
                query[RAIN_INSTANCE_PARAM].forEach(function (iid) { addInstance(iid, resource, renderer); });
            } else {
                addInstance(query[RAIN_INSTANCE_PARAM], resource, renderer);
            }
        }

        function addInstance (id, resource, renderer) {
            var dataurl = 'file://' + mod_path.join(__dirname, '..', 'instances', id);
            var instanceresource = mod_resourcemanager.loadResourceByUrl(dataurl);
            instanceresource.load();
            resource.addDependency(instanceresource);
            renderer.instanceResources[id] = instanceresource;
        }
        */
    }

    return {
        'handleControllerRequest'   : handleControllerRequest
        , 'handleViewRequest'       : handleViewRequest
    }
}
<|endoftext|>


<commit_before>(function () {
  'use strict';

  var Module = require('module');
  var expect = require('chai').expect;
  var sinon = require('sinon');

  function unloadToobusyMiddleware() {
    delete require.cache[require.resolve('../')];
    delete require.cache.toobusy;
  }

  describe('toobusy-middleware', function () {
    var req;
    var res;
    var next;

    before(function () {
      res = {
        send : sinon.stub()
      };
    });

    before(function () {
      req = {};
    });

    before(function () {
      next = sinon.spy();
    });

    afterEach(function () {
      res.send.reset();
    });

    afterEach(function () {
      next.reset();
    });

    describe('with toobusy', function () {
      var toobusyMock;
      var toobusyMiddleware;

      before(unloadToobusyMiddleware);

      before(function () {
        toobusyMock = sinon.stub();
        toobusyMock.maxLag = sinon.stub();
        toobusyMock.shutdown = sinon.stub();
        toobusyMock.returns(false);
      });

      before(function () {
        var load = Module._load;

        sinon
          .stub(Module, '_load', function (request, parent, isMain) {
            if (request === 'toobusy') {
              return toobusyMock;
            }

            return load.call(Module, request, parent, isMain);
          });
      });

      after(function () {
        Module._load.restore();
      });

      before(function () {
        toobusyMiddleware = require('../');
      });

      afterEach(function () {
        toobusyMock.reset();
        toobusyMock.maxLag.reset();
        toobusyMock.shutdown.reset();
      });

      it('exposes a function', function () {
        expect(toobusyMiddleware).to.be.a('function');
      });

      it('returns a middleware', function () {
        var middleware = toobusyMiddleware();
        expect(middleware).to.be.a('function');

        // Accepts a req, res and next params
        expect(middleware).to.have.length(3);
      });

      describe('middleware', function () {
        var middleware;

        before(function () {
          middleware = toobusyMiddleware();
        });

        it('sends a response with status 503 (Service Unavailable) if too busy',
          function () {
            toobusyMock.returns(true);
            middleware(req, res, next);
            expect(res.send).to.have.been.calledWith(503);
          });

        it('does not call the next() callback if too busy', function () {
          toobusyMock.returns(true);
          middleware(req, res, next);
          expect(next).not.to.have.been.called;
        });

        it('sends the message set in the options if too busy', function () {
          var opts = {
            message : 'Test message'
          };

          var middleware = toobusyMiddleware(opts);

          middleware(req, res, next);
          expect(res.send).to.have.been.calledWith(503, opts.message);
        });

        it('sets toobusy\'s maximum lag option if set in the options',
          function () {
            var opts = {
              maxLag : 100
            };

            toobusyMiddleware(opts);
            expect(toobusyMock.maxLag).to.have.been.calledWith(opts.maxLag);
          });

        it('calls the `next()` callback if not too busy', function () {
          toobusyMock.returns(false);

          middleware(req, res, next);
          expect(next).to.have.been.called;
        });

      }); // with toobusy middleware

      describe('.shutdown()', function () {
        it('is accessible', function () {
          expect(toobusyMiddleware.shutdown).to.be.defined;
        });

        it('is a function', function () {
          expect(toobusyMiddleware.shutdown).to.be.a('function');
        });

        it('calls the toobusy\'s shutdown function', function () {
          toobusyMiddleware.shutdown();
          expect(toobusyMock.shutdown).to.have.been.called;
        });
      }); // with toobusy .shutdown()
    }); // with toobusy

    describe('without toobusy', function () {
      var toobusyMiddleware;

      before(unloadToobusyMiddleware);

      before(function () {
        var load = Module._load;

        sinon
          .stub(Module, '_load', function (request, parent, isMain) {
            var err;

            if (request === 'toobusy') {
              err = new Error();
              err.code = 'MODULE_NOT_FOUND';
              throw(err);
            }

            return load.call(Module, request, parent, isMain);
          });
      });

      before(function () {
        toobusyMiddleware = require('../');
      });

      after(function () {
        Module._load.restore();
      });

      it('exposes a function', function () {
        expect(toobusyMiddleware).to.be.a('function');
      });

      it('returns a middleware', function () {
        var middleware = toobusyMiddleware();
        expect(middleware).to.be.a('function');
        // Accepts a req, res and next params
        expect(middleware).to.have.length(3);
      });

      describe('middleware', function () {
        var middleware;

        before(function () {
          middleware = toobusyMiddleware();
        });

        it('does not throw any errors', function () {
          function callMiddleware() {
            middleware(req, res, next);
          }

          expect(callMiddleware).not.to.throw;
        });

        it('calls the `next()` callback', function () {
          middleware(req, res, next);
          expect(next).to.have.been.called;
        });
      }); // without toobusy middleware

      describe('.shutdown()', function () {
        it('is accessible', function () {
          expect(toobusyMiddleware.shutdown).to.be.defined;
        });

        it('is a function', function () {
          expect(toobusyMiddleware.shutdown).to.be.a('function');
        });

        it('does not throw any errors', function () {
          function shutdown() {
            toobusyMiddleware.shutdown();
          }

          expect(shutdown).not.to.throw;
        });
      }); // without toobusy .shutdown()

    }); // without toobusy

  }); // toobusy-middleware

})();
<commit_msg>Added a test case for unexpected errors when requiring toobusy<commit_after>(function () {
  'use strict';

  var Module = require('module');
  var expect = require('chai').expect;
  var sinon = require('sinon');

  function unloadToobusyMiddleware() {
    delete require.cache[require.resolve('../')];
    delete require.cache.toobusy;
  }

  describe('toobusy-middleware', function () {
    var req;
    var res;
    var next;

    before(function () {
      res = {
        send : sinon.stub()
      };
    });

    before(function () {
      req = {};
    });

    before(function () {
      next = sinon.spy();
    });

    afterEach(function () {
      res.send.reset();
    });

    afterEach(function () {
      next.reset();
    });

    describe('with toobusy', function () {
      var toobusyMock;
      var toobusyMiddleware;

      before(unloadToobusyMiddleware);

      before(function () {
        toobusyMock = sinon.stub();
        toobusyMock.maxLag = sinon.stub();
        toobusyMock.shutdown = sinon.stub();
        toobusyMock.returns(false);
      });

      before(function () {
        var load = Module._load;

        sinon
          .stub(Module, '_load', function (request, parent, isMain) {
            if (request === 'toobusy') {
              return toobusyMock;
            }

            return load.call(Module, request, parent, isMain);
          });
      });

      after(function () {
        Module._load.restore();
      });

      before(function () {
        toobusyMiddleware = require('../');
      });

      afterEach(function () {
        toobusyMock.reset();
        toobusyMock.maxLag.reset();
        toobusyMock.shutdown.reset();
      });

      it('exposes a function', function () {
        expect(toobusyMiddleware).to.be.a('function');
      });

      it('returns a middleware', function () {
        var middleware = toobusyMiddleware();
        expect(middleware).to.be.a('function');

        // Accepts a req, res and next params
        expect(middleware).to.have.length(3);
      });

      describe('middleware', function () {
        var middleware;

        before(function () {
          middleware = toobusyMiddleware();
        });

        it('sends a response with status 503 (Service Unavailable) if too busy',
          function () {
            toobusyMock.returns(true);
            middleware(req, res, next);
            expect(res.send).to.have.been.calledWith(503);
          });

        it('does not call the next() callback if too busy', function () {
          toobusyMock.returns(true);
          middleware(req, res, next);
          expect(next).not.to.have.been.called;
        });

        it('sends the message set in the options if too busy', function () {
          var opts = {
            message : 'Test message'
          };

          var middleware = toobusyMiddleware(opts);

          middleware(req, res, next);
          expect(res.send).to.have.been.calledWith(503, opts.message);
        });

        it('sets toobusy\'s maximum lag option if set in the options',
          function () {
            var opts = {
              maxLag : 100
            };

            toobusyMiddleware(opts);
            expect(toobusyMock.maxLag).to.have.been.calledWith(opts.maxLag);
          });

        it('calls the `next()` callback if not too busy', function () {
          toobusyMock.returns(false);

          middleware(req, res, next);
          expect(next).to.have.been.called;
        });

      }); // with toobusy middleware

      describe('.shutdown()', function () {
        it('is accessible', function () {
          expect(toobusyMiddleware.shutdown).to.be.defined;
        });

        it('is a function', function () {
          expect(toobusyMiddleware.shutdown).to.be.a('function');
        });

        it('calls the toobusy\'s shutdown function', function () {
          toobusyMiddleware.shutdown();
          expect(toobusyMock.shutdown).to.have.been.called;
        });
      }); // with toobusy .shutdown()
    }); // with toobusy

    describe('without toobusy', function () {
      var toobusyMiddleware;

      before(unloadToobusyMiddleware);

      before(function () {
        var load = Module._load;

        sinon
          .stub(Module, '_load', function (request, parent, isMain) {
            var err;

            if (request === 'toobusy') {
              err = new Error();
              err.code = 'MODULE_NOT_FOUND';
              throw(err);
            }

            return load.call(Module, request, parent, isMain);
          });
      });

      before(function () {
        toobusyMiddleware = require('../');
      });

      after(function () {
        Module._load.restore();
      });

      it('exposes a function', function () {
        expect(toobusyMiddleware).to.be.a('function');
      });

      it('returns a middleware', function () {
        var middleware = toobusyMiddleware();
        expect(middleware).to.be.a('function');
        // Accepts a req, res and next params
        expect(middleware).to.have.length(3);
      });

      describe('middleware', function () {
        var middleware;

        before(function () {
          middleware = toobusyMiddleware();
        });

        it('does not throw any errors', function () {
          function callMiddleware() {
            middleware(req, res, next);
          }

          expect(callMiddleware).not.to.throw;
        });

        it('calls the `next()` callback', function () {
          middleware(req, res, next);
          expect(next).to.have.been.called;
        });
      }); // without toobusy middleware

      describe('.shutdown()', function () {
        it('is accessible', function () {
          expect(toobusyMiddleware.shutdown).to.be.defined;
        });

        it('is a function', function () {
          expect(toobusyMiddleware.shutdown).to.be.a('function');
        });

        it('does not throw any errors', function () {
          function shutdown() {
            toobusyMiddleware.shutdown();
          }

          expect(shutdown).not.to.throw;
        });
      }); // without toobusy .shutdown()

    }); // without toobusy

    it('re-throws unexpected errors when the module is require()\'d',
      function () {
        unloadToobusyMiddleware();

        var load = Module._load;

        sinon
          .stub(Module, '_load', function (request, parent, isMain) {
            var err;

            if (request === 'toobusy') {
              err = new Error('Unexpected error');
              throw(err);
            }

            return load.call(Module, request, parent, isMain);
          });

        function requireToobusyMiddleware() {
          require('../');
        }

        expect(requireToobusyMiddleware).to.throw('Unexpected error');

        Module._load.restore();
      });

  }); // toobusy-middleware

})();
<|endoftext|>


<commit_before>var Promise = require('bluebird')
  , models = require('./models')
  , async = require('async')
  , redis = require('redis').createClient

exports.init = function(database) {
  "use strict";

  var pubSub = function() {
  }

  pubSub.newPost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:new',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.destroyPost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:destroy',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.updatePost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:update',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.newComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return post.getCommentsFriendOfFriendTimelines(this.comment.userId)
        })
        .then(function(timelines) {
          return Promise.map(timelines, function(timeline) {
            database.publishAsync('comment:new',
                                  JSON.stringify({
                                    timelineId: timeline.id,
                                    commentId: commentId
                                  }))
          })
        })
        .then(function() {
          return database.publishAsync('comment:new',
                                       JSON.stringify({
                                         postId: this.post.id,
                                         commentId: commentId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.destroyComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return database.publishAsync('comment:destroy',
                                JSON.stringify({
                                  postId: post.id,
                                  commentId: this.comment.id
                                }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.updateComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return database.publishAsync('comment:update',
                                       JSON.stringify({
                                         postId: post.id,
                                         commentId: this.comment.id
                                       }))
        })
        .then(function() { return this.post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('comment:update',
                                  JSON.stringify({
                                    timelineId: timelineId,
                                    commentId: commentId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.newLike = function(postId, userId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId).bind({})
        .then(function(post) {
          this.post = post
          return post.getLikesFriendOfFriendTimelines(userId)
        })
        .then(function(timelines) {
          return Promise.map(timelines, function(timeline) {
            return database.publishAsync('like:new',
                                  JSON.stringify({
                                    timelineId: timeline.id,
                                    userId: userId,
                                    postId: postId
                                  }))

          })
        })
        .then(function() {
          return database.publishAsync('like:new',
                                       JSON.stringify({
                                         userId: userId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.removeLike = function(postId, userId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId).bind({})
        .then(function(post) {
          this.post = post
          return post.getSubscribedTimelineIds()
        })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            return database.publishAsync('like:remove',
                                  JSON.stringify({
                                    timelineId: timelineId,
                                    userId: userId,
                                    postId: postId
                                  }))
          })
        })
        .then(function() {
          return database.publishAsync('like:remove',
                                       JSON.stringify({
                                         userId: userId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.hidePost = function(userId, postId) {
    return new Promise(function(resolve, reject) {
      models.User.findById(userId).bind({})
        .then(function(user) { return user.getRiverOfNewsTimelineId() })
        .then(function(riverOfNewsId) {
          return database.publishAsync('post:hide',
                                       JSON.stringify({
                                         timelineId: riverOfNewsId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.unhidePost = function(userId, postId) {
    return new Promise(function(resolve, reject) {
      models.User.findById(userId).bind({})
        .then(function(user) { return user.getRiverOfNewsTimelineId() })
        .then(function(riverOfNewsId) {
          return database.publishAsync('post:unhide',
                                       JSON.stringify({
                                         timelineId: riverOfNewsId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.listen = function(server, app) {
    var io = require('socket.io')(server)

    var adapter = require('socket.io-redis')
     , redisPub = redis()
     , redisSub = redis({ detect_buffers: true })

    io.adapter(adapter({
      pubClient: redisPub,
      subClient: redisSub
    }))

    io.sockets.on('connection', function(socket) {
      socket.on('subscribe', function(data) {
        for(var channel in data) {
          if (data[channel]) {
            data[channel].forEach(function(id) {
              if (id) {
                app.logger.info('User has subscribed to ' + id + ' ' + channel)

                socket.join(channel + ':' + id)
              }
            })
          }
        }
      })

      socket.on('unsubscribe', function(data) {
        for(var channel in data) {
          if (data[channel]) {
            data[channel].forEach(function(id) {
              if (id) {
                app.logger.info('User has disconnected from ' + id + ' ' + channel)

                socket.leave(channel + ':' + id)
              }
            })
          }
        }
      })
    })

    var channels = redis()
    channels.subscribe('post:new', 'post:destroy', 'post:update',
                       'comment:new', 'comment:destroy', 'comment:update',
                       'like:new', 'like:remove', 'post:hide', 'post:unhide' )

    // TODO: extract to separate functions
    channels.on('message', function(channel, msg) {
      switch(channel) {
      case 'post:destroy':
        var data = JSON.parse(msg)
        var event = { postId: data.postId }

        io.sockets.in('timeline:' + data.timelineId).emit('post:destroy', event)
        io.sockets.in('post:' + data.postId).emit('post:destroy', event)

        break

      case 'post:new':
      var data = JSON.parse(msg)

        models.Post.findById(data.postId)
          .then(function(post) {
            new models.PostSerializer(post).toJSON(function(err, json) {
              io.sockets.in('timeline:' + data.timelineId).emit('post:new', json)
            })
          })

        break

      case 'post:update':
        var data = JSON.parse(msg)

        models.Post.findById(data.postId)
          .then(function(post) {
            new models.PostSerializer(post).toJSON(function(err, json) {
              io.sockets.in('timeline:' + data.timelineId).emit('post:update', json)
              io.sockets.in('post:' + data.postId).emit('post:update', json)
            })
          })

        break

      case 'comment:new':
        var data = JSON.parse(msg)

        models.Comment.findById(data.commentId)
          .then(function(comment) {
            new models.PubsubCommentSerializer(comment).toJSON(function(err, json) {
              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('comment:new', json)
              } else {
                io.sockets.in('post:' + data.postId).emit('comment:new', json)
              }
            })
          })

        break

      case 'comment:update':
        var data = JSON.parse(msg)

        models.Comment.findById(data.commentId)
          .then(function(comment) {
            new models.PubsubCommentSerializer(comment).toJSON(function(err, json) {
              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('comment:update', json)
              } else {
                io.sockets.in('post:' + data.postId).emit('comment:update', json)
              }
            })
          })

        break

      case 'comment:destroy':
        var data = JSON.parse(msg)
        var event = { postId: data.postId, commentId: data.commentId }

        io.sockets.in('post:' + data.postId).emit('comment:destroy', event)

        models.Post.findById(data.postId)
          .then(function(post) {
            return post.getTimelineIds()
          })
          .then(function(timelineIds) {
            return Promise.map(timelineIds, function(timelineId) {
              return io.sockets.in('timeline:' + timelineId).emit('comment:destroy', event)
            })
          })

        break

      case 'like:new':
        var data = JSON.parse(msg)

        models.User.findById(data.userId)
          .then(function(user) {
            new models.LikeSerializer(user).toJSON(function(err, json) {
              var event = json
              event.meta = { postId: data.postId }

              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('like:new', event)
              } else {
                io.sockets.in('post:' + data.postId).emit('like:new', event)
              }
            })
          })

        break

      case 'like:remove':
        var data = JSON.parse(msg)
        var event = { meta: { userId: data.userId, postId: data.postId } }

        if (data.timelineId)
          io.sockets.in('timeline:' + data.timelineId).emit('like:remove', event)
        else
          io.sockets.in('post:' + data.postId).emit('like:remove', event)

        break

      case 'post:hide':
        // NOTE: posts are hidden only on RiverOfNews timeline so this
        // event won't leak any personal information
        var data = JSON.parse(msg)
        var event = { meta: { postId: data.postId } }
        io.sockets.in('timeline:' + data.timelineId).emit('post:hide', event)

        break

      case 'post:unhide':
        // NOTE: posts are hidden only on RiverOfNews timeline so this
        // event won't leak any personal information
        var data = JSON.parse(msg)
        var event = { meta: { postId: data.postId } }
        io.sockets.in('timeline:' + data.timelineId).emit('post:unhide', event)

        break
      }
    })
  }

  return pubSub
}
<commit_msg>Uses meta attrtibute to pass postId to post:destroy pubsub event.<commit_after>var Promise = require('bluebird')
  , models = require('./models')
  , async = require('async')
  , redis = require('redis').createClient

exports.init = function(database) {
  "use strict";

  var pubSub = function() {
  }

  pubSub.newPost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:new',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.destroyPost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:destroy',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.updatePost = function(postId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId)
        .then(function(post) { return post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('post:update',
                                  JSON.stringify({
                                    postId: postId,
                                    timelineId: timelineId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.newComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return post.getCommentsFriendOfFriendTimelines(this.comment.userId)
        })
        .then(function(timelines) {
          return Promise.map(timelines, function(timeline) {
            database.publishAsync('comment:new',
                                  JSON.stringify({
                                    timelineId: timeline.id,
                                    commentId: commentId
                                  }))
          })
        })
        .then(function() {
          return database.publishAsync('comment:new',
                                       JSON.stringify({
                                         postId: this.post.id,
                                         commentId: commentId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.destroyComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return database.publishAsync('comment:destroy',
                                JSON.stringify({
                                  postId: post.id,
                                  commentId: this.comment.id
                                }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.updateComment = function(commentId) {
    return new Promise(function(resolve, reject) {
      models.Comment.findById(commentId).bind({})
        .then(function(comment) {
          this.comment = comment
          return comment.getPost()
        })
        .then(function(post) {
          this.post = post
          return database.publishAsync('comment:update',
                                       JSON.stringify({
                                         postId: post.id,
                                         commentId: this.comment.id
                                       }))
        })
        .then(function() { return this.post.getSubscribedTimelineIds() })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            database.publishAsync('comment:update',
                                  JSON.stringify({
                                    timelineId: timelineId,
                                    commentId: commentId
                                  }))
          })
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.newLike = function(postId, userId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId).bind({})
        .then(function(post) {
          this.post = post
          return post.getLikesFriendOfFriendTimelines(userId)
        })
        .then(function(timelines) {
          return Promise.map(timelines, function(timeline) {
            return database.publishAsync('like:new',
                                  JSON.stringify({
                                    timelineId: timeline.id,
                                    userId: userId,
                                    postId: postId
                                  }))

          })
        })
        .then(function() {
          return database.publishAsync('like:new',
                                       JSON.stringify({
                                         userId: userId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.removeLike = function(postId, userId) {
    return new Promise(function(resolve, reject) {
      models.Post.findById(postId).bind({})
        .then(function(post) {
          this.post = post
          return post.getSubscribedTimelineIds()
        })
        .then(function(timelineIds) {
          return Promise.map(timelineIds, function(timelineId) {
            return database.publishAsync('like:remove',
                                  JSON.stringify({
                                    timelineId: timelineId,
                                    userId: userId,
                                    postId: postId
                                  }))
          })
        })
        .then(function() {
          return database.publishAsync('like:remove',
                                       JSON.stringify({
                                         userId: userId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.hidePost = function(userId, postId) {
    return new Promise(function(resolve, reject) {
      models.User.findById(userId).bind({})
        .then(function(user) { return user.getRiverOfNewsTimelineId() })
        .then(function(riverOfNewsId) {
          return database.publishAsync('post:hide',
                                       JSON.stringify({
                                         timelineId: riverOfNewsId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.unhidePost = function(userId, postId) {
    return new Promise(function(resolve, reject) {
      models.User.findById(userId).bind({})
        .then(function(user) { return user.getRiverOfNewsTimelineId() })
        .then(function(riverOfNewsId) {
          return database.publishAsync('post:unhide',
                                       JSON.stringify({
                                         timelineId: riverOfNewsId,
                                         postId: postId
                                       }))
        })
        .then(function(res) { resolve(res) })
    })
  }

  pubSub.listen = function(server, app) {
    var io = require('socket.io')(server)

    var adapter = require('socket.io-redis')
     , redisPub = redis()
     , redisSub = redis({ detect_buffers: true })

    io.adapter(adapter({
      pubClient: redisPub,
      subClient: redisSub
    }))

    io.sockets.on('connection', function(socket) {
      socket.on('subscribe', function(data) {
        for(var channel in data) {
          if (data[channel]) {
            data[channel].forEach(function(id) {
              if (id) {
                app.logger.info('User has subscribed to ' + id + ' ' + channel)

                socket.join(channel + ':' + id)
              }
            })
          }
        }
      })

      socket.on('unsubscribe', function(data) {
        for(var channel in data) {
          if (data[channel]) {
            data[channel].forEach(function(id) {
              if (id) {
                app.logger.info('User has disconnected from ' + id + ' ' + channel)

                socket.leave(channel + ':' + id)
              }
            })
          }
        }
      })
    })

    var channels = redis()
    channels.subscribe('post:new', 'post:destroy', 'post:update',
                       'comment:new', 'comment:destroy', 'comment:update',
                       'like:new', 'like:remove', 'post:hide', 'post:unhide' )

    // TODO: extract to separate functions
    channels.on('message', function(channel, msg) {
      switch(channel) {
      case 'post:destroy':
        var data = JSON.parse(msg)
        var event = { meta: { postId: data.postId } }

        io.sockets.in('timeline:' + data.timelineId).emit('post:destroy', event)
        io.sockets.in('post:' + data.postId).emit('post:destroy', event)

        break

      case 'post:new':
      var data = JSON.parse(msg)

        models.Post.findById(data.postId)
          .then(function(post) {
            new models.PostSerializer(post).toJSON(function(err, json) {
              io.sockets.in('timeline:' + data.timelineId).emit('post:new', json)
            })
          })

        break

      case 'post:update':
        var data = JSON.parse(msg)

        models.Post.findById(data.postId)
          .then(function(post) {
            new models.PostSerializer(post).toJSON(function(err, json) {
              io.sockets.in('timeline:' + data.timelineId).emit('post:update', json)
              io.sockets.in('post:' + data.postId).emit('post:update', json)
            })
          })

        break

      case 'comment:new':
        var data = JSON.parse(msg)

        models.Comment.findById(data.commentId)
          .then(function(comment) {
            new models.PubsubCommentSerializer(comment).toJSON(function(err, json) {
              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('comment:new', json)
              } else {
                io.sockets.in('post:' + data.postId).emit('comment:new', json)
              }
            })
          })

        break

      case 'comment:update':
        var data = JSON.parse(msg)

        models.Comment.findById(data.commentId)
          .then(function(comment) {
            new models.PubsubCommentSerializer(comment).toJSON(function(err, json) {
              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('comment:update', json)
              } else {
                io.sockets.in('post:' + data.postId).emit('comment:update', json)
              }
            })
          })

        break

      case 'comment:destroy':
        var data = JSON.parse(msg)
        var event = { postId: data.postId, commentId: data.commentId }

        io.sockets.in('post:' + data.postId).emit('comment:destroy', event)

        models.Post.findById(data.postId)
          .then(function(post) {
            return post.getTimelineIds()
          })
          .then(function(timelineIds) {
            return Promise.map(timelineIds, function(timelineId) {
              return io.sockets.in('timeline:' + timelineId).emit('comment:destroy', event)
            })
          })

        break

      case 'like:new':
        var data = JSON.parse(msg)

        models.User.findById(data.userId)
          .then(function(user) {
            new models.LikeSerializer(user).toJSON(function(err, json) {
              var event = json
              event.meta = { postId: data.postId }

              if (data.timelineId) {
                io.sockets.in('timeline:' + data.timelineId).emit('like:new', event)
              } else {
                io.sockets.in('post:' + data.postId).emit('like:new', event)
              }
            })
          })

        break

      case 'like:remove':
        var data = JSON.parse(msg)
        var event = { meta: { userId: data.userId, postId: data.postId } }

        if (data.timelineId)
          io.sockets.in('timeline:' + data.timelineId).emit('like:remove', event)
        else
          io.sockets.in('post:' + data.postId).emit('like:remove', event)

        break

      case 'post:hide':
        // NOTE: posts are hidden only on RiverOfNews timeline so this
        // event won't leak any personal information
        var data = JSON.parse(msg)
        var event = { meta: { postId: data.postId } }
        io.sockets.in('timeline:' + data.timelineId).emit('post:hide', event)

        break

      case 'post:unhide':
        // NOTE: posts are hidden only on RiverOfNews timeline so this
        // event won't leak any personal information
        var data = JSON.parse(msg)
        var event = { meta: { postId: data.postId } }
        io.sockets.in('timeline:' + data.timelineId).emit('post:unhide', event)

        break
      }
    })
  }

  return pubSub
}
<|endoftext|>


<commit_before>'use strict'

function addMultipleEventListener (element, events, handler) {
	events.forEach(e => element.addEventListener(e, handler))
}

document.addEventListener('DOMContentLoaded', () => {
	let points = 0
	const pointContainers = document.querySelectorAll('.points')
	let keypressed = 'key-'
	const container = document.querySelector('.keys-container')
	const square = document.querySelector('.key-selector')
	// array of objects: {score, speed, message, points}
	const speeds = [
		{
			score: 0,
			speed: 800,
			message: '',
			points: 1,
			keys: 1,
		},
		{
			score: 3,
			speed: 750,
			message: "You've got it!",
			points: 2,
			keys: 2,
		},
		{
			score: 20,
			speed: 670,
			message: 'Keep going!',
			points: 5,
			keys: 3,
		},
		{
			score: 70,
			speed: 620,
			message: "You're doing great!",
			points: 7,
			keys: 3,
		},
		{
			score: 150,
			speed: 560,
			message: 'You rock!',
			points: 10,
			keys: 3,
		},
		{
			score: 300,
			speed: 510,
			message: "Don't stop!",
			points: 12,
			keys: 4,
		},
		{
			score: 500,
			speed: 490,
			message: 'Tricky!',
			points: 15,
			keys: 4,
		},
		{
			score: 760,
			speed: 465,
			message: 'Great!',
			points: 17,
			keys: 4,
		},
		{
			score: 1100,
			speed: 440,
			message: 'I like your style!',
			points: 20,
			keys: 4,
		},
		{
			score: 1500,
			speed: 390,
			message: 'Awesome!',
			points: 22,
			keys: 4,
		},
		{
			score: 2000,
			speed: 360,
			message: 'Yeah!!',
			points: 25,
			keys: 4,
		},
		{
			score: 2700,
			speed: 330,
			message: 'How do you do that?',
			points: 27,
			keys: 4,
		},
		{
			score: 3500,
			speed: 310,
			message: '...how?',
			points: 30,
			keys: 4,
		},
		{
			score: 4300,
			speed: 290,
			message: 'Don\'t ever stop!!',
			points: 32,
			keys: 4,
		},
		{
			score: 5500,
			speed: 280,
			message: 'I\'m really impressed.',
			points: 35,
			keys: 4,
		},
		{
			score: 7000,
			speed: 270,
			message: 'Arrow hero!',
			points: 40,
			keys: 4,
		},
		{
			score: 10000,
			speed: 260,
			message: 'You\'re really still here?',
			points: 40,
			keys: 4,
		},
		{
			score: 10500,
			speed: 250,
			message: "That's incredible!",
			points: 40,
			keys: 4,
		},
	]
	let current = speeds[0]
	let gameState = false
	let maxLife = 5000
	let currentLife = maxLife
	// array of objects: {delay, started, interval}
	let scheduledSpawns = []
	let bestScore = localStorage.getItem('bestScore')
	const mobileControls = document.querySelector('.mobile-controls')
	const body = document.querySelector('body')

	function updatePoints (pts) {
		if (pts < 1)
			pts = 1

		pts = Math.floor(pts)
		points += pts
		square.classList.add('bump')
		square.onanimationend = () => {
			square.classList.remove('bump')
		}

		pointContainers.forEach(pointContainer => {
			pointContainer.textContent = points
			pointContainer.classList.add('bump')
			pointContainer.onanimationend = () => {
				pointContainer.classList.remove('bump')
			}
		})

		const ding = document.createElement('div')
		ding.classList.add('ding')
		ding.textContent = `+${ pts }`
		ding.onanimationend = () => {
			ding.remove()
		}

		square.appendChild(ding)
	}

	const levelMessage = document.querySelector('.level-message')

	function updateSpeed () {
		const oldSpeed = current
		for (const i in speeds) {
			const _speed = speeds[i]
			if (points >= _speed.score)
				current = _speed
			else if (points < _speed.score)
				break

		}

		if (current.speed !== oldSpeed.speed) {
			// Speed changed !
			levelMessage.textContent = current.message
			levelMessage.classList.add('show')
			levelMessage.onanimationend = () => {
				levelMessage.classList.remove('show')
			}
		}

	}

	function spawnRandomKey (obj) {
		if (gameState !== 'paused') // running or ended
			removeScheduledSpawn(obj)

		if (gameState === 'end' || gameState === 'paused' || gameState === 'restart')
			return

		const arr = [ 'key-right', 'key-left', 'key-down', 'key-up' ]
		const direction = arr[Math.floor(Math.random() * current.keys)]
		let nextKey = container.querySelector('.idle')
		if (nextKey === null) {
			nextKey = document.createElement('div')
			nextKey.classList.add('key', direction)
			nextKey.onanimationend = () => {

				if (gameState === 'end' || gameState === 'restart' || nextKey.classList.contains('idle'))
					return

				if (nextKey.classList.contains(keypressed)) {
					currentLife = Math.min(currentLife + 200, maxLife)

					updatePoints(current.points)
				} else {
					currentLife -= 1000
					square.classList.add('bad')
					square.onanimationend = () => {
						square.classList.remove('bad')
					}
				}

				updateSpeed()

				const percent = currentLife * 100 / maxLife
				const percentElem = document.querySelector('.percent')
				percentElem.style.width = percent + '%'

				if (percent < 20)
					percentElem.classList.add('low')
				else if (percent < 60) {
					percentElem.classList.add('medium')
					percentElem.classList.remove('low')
				} else
					percentElem.classList.remove('low', 'medium')

				if (currentLife <= 0 && gameState === 'running')
					endGame()

				nextKey.classList.add('idle')
				nextKey.classList.remove('key-up', 'key-down', 'key-left', 'key-right')
			}
			container.appendChild(nextKey)
		} else {
			nextKey.classList.remove('idle')
			nextKey.classList.add(direction)
		}

		// Spawn next key
		scheduleSpawn(current.speed)
	}

	function scheduleSpawn (delay) {
		const now = new Date()
		const obj = {
			delay,
			started: now.getTime(),
		}
		obj.interval = setTimeout(spawnRandomKey, delay, obj)
		scheduledSpawns.push(obj)
	}

	function removeScheduledSpawn (obj) {
		const index = scheduledSpawns.indexOf(obj)
		if (index > -1)
			scheduledSpawns.splice(index, 1)

	}

	function pauseScheduledSpawns () {
		const now = new Date()
		for (const i in scheduledSpawns) {
			const obj = scheduledSpawns[i]
			obj.delay -= now.getTime() - obj.started
			obj.started = null
			clearInterval(obj.interval)
		}
	}

	function resumeScheduledSpawns () {
		const now = new Date()
		for (const i in scheduledSpawns) {
			const obj = scheduledSpawns[i]
			obj.started = now.getTime()
			obj.interval = setTimeout(spawnRandomKey, obj.delay, obj)
		}
	}

	function endGame () {
		gameState = 'end'
		document.querySelectorAll('.key').forEach(k => k.classList.add('hide'))

		const keySelectorContainer = document.querySelector('.key-selector-container')
		keySelectorContainer.classList.add('hide')
		keySelectorContainer.classList.remove('show')

		const results = document.querySelector('.results')
		results.classList.add('show')
		results.classList.remove('hide')

		const pointsContainer = document.querySelector('.points-container')
		pointsContainer.classList.add('hide')
		pointsContainer.classList.remove('show')

		const percent = document.querySelector('.percent')
		percent.style.width = '0%'

		document.querySelector('.pause-btn').textContent = 'Restart'

		if (points > bestScore) {
			// update best score
			bestScore = points
			sessionStorage.setItem('bestScore', bestScore)
			document.querySelector('.best-points .value').textContent = bestScore
			document.querySelector('.best').style.display = 'block'
		}
	}

	function restartGame () {
		gameState = 'restart'
		points = 0
		maxLife = 5000
		currentLife = maxLife
		current = speeds[0]

		if (keypressed !== '')
			square.classList.remove('s-' + keypressed)

		keypressed = ''

		container.querySelectorAll('.key').forEach(key => {
			key.classList.remove('key-up', 'key-down', 'key-left', 'key-right', 'hide')
			key.classList.add('idle')
		})

		const keySelectorContainer = document.querySelector('.key-selector-container')
		keySelectorContainer.classList.add('show')
		keySelectorContainer.classList.remove('hide')

		const results = document.querySelector('.results')
		results.classList.add('hide')
		results.classList.remove('show')

		const pointsContainer = document.querySelector('.points-container')
		pointsContainer.classList.add('show')
		pointsContainer.classList.remove('hide')

		setTimeout(() => {
			for (const i in scheduledSpawns) {
				const obj = scheduledSpawns[i]
				clearInterval(obj.interval)
			}
			scheduledSpawns = []
			pointContainers.forEach(pointContainer => pointContainer.textContent = points)

			const percent = document.querySelector('.percent')
			percent.style.width = '100%'
			percent.classList.remove('low', 'medium')

			setTimeout(() => {
				gameState = 'running'
				document.querySelectorAll('.key').forEach(k => k.classList.add('remove'))
				document.querySelector('.pause-btn').textContent = 'Pause'
				scheduleSpawn(1)
			}, 950)
		}, 1000)
	}

	document.onkeydown = e => {

		if (e.keyCode === 32) {
			e.preventDefault()
			if (gameState === 'running' || gameState === 'paused') {
				// space bar pressed

				gameState = gameState === 'running' ? 'paused' : 'running'
				document.querySelectorAll('.key').forEach(k => k.classList.toggle('paused', gameState === 'paused'))
				document.querySelector('.pause').classList.toggle('show', gameState === 'paused')

				if (gameState === 'paused')
					pauseScheduledSpawns()
				else if (gameState === 'running')
					resumeScheduledSpawns()

			} else if (gameState === 'end')
				restartGame()

		}

		if ((e.keyCode >= 37 && e.keyCode <= 40 || e.keyCode >= 72 && e.keyCode <= 76)
            && gameState !== 'paused' && gameState !== 'restart') {
			// arrow keys pressed

			e.preventDefault()
			if (gameState === false) {
				startGame()
				return false
			}

			if (keypressed !== '')
				square.classList.remove('s-' + keypressed)

			switch (e.keyCode) {
				case 37: // left
				case 72: // h
					keypressed = 'key-left'
					break

				case 38: // up
				case 75: // j
					keypressed = 'key-up'
					break

				case 39: // right
				case 76: // l
					keypressed = 'key-right'
					break

				case 40: // down
				case 74: // j
					keypressed = 'key-down'
					break
			}
			square.classList.add('s-' + keypressed)
		}

	}

	function startGame () {
		gameState = 'running'

		document.querySelector('.points-container').classList.add('show')
		document.querySelector('.helper-container').classList.add('hide')
		setTimeout(() => {
			const keySelector = document.querySelector('.key-selector')
			keySelector.classList.add('show', 'fade')
			keySelector.onanimationend = () => keySelector.classList.remove('fade')
		}, 500)
		scheduleSpawn(1000)

		document.querySelector('.percent').style.width = '100%'
	}

	if (bestScore) {
		document.querySelector('.best-points .value').textContent = bestScore
		document.querySelector('.best').style.display = 'block'
	}

	function addMobileListener (selector, /* @deprecated */ keyCode) {
		const keyElem = mobileControls.querySelector(selector)
		addMultipleEventListener(keyElem, [ 'touchstart', 'click' ], () => {
			document.dispatchEvent(new KeyboardEvent('keydown', { keyCode }))
		})
	}

	addMobileListener('.key-left', 37)
	addMobileListener('.key-up', 38)
	addMobileListener('.key-right', 39)
	addMobileListener('.key-down', 40)
	mobileControls.querySelector('.pause-btn').ontouchstart = () => {
		document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 32 /* space */ }))
	}

	body.classList.toggle('colorblind', localStorage.getItem('colorblind') === 'yes')
	addMultipleEventListener(document.querySelector('.colorblind-btn'), [ 'click', 'touchstart' ], () => {
		body.classList.toggle('colorblind', !body.classList.contains('colorblind'))
		localStorage.setItem('colorblind', body.classList.contains('colorblind') ? 'yes' : 'no')
	})
})
<commit_msg>Fix game restart delay<commit_after>'use strict'

function addMultipleEventListener (element, events, handler) {
	events.forEach(e => element.addEventListener(e, handler))
}

document.addEventListener('DOMContentLoaded', () => {
	let points = 0
	const pointContainers = document.querySelectorAll('.points')
	let keypressed = 'key-'
	const container = document.querySelector('.keys-container')
	const square = document.querySelector('.key-selector')
	// array of objects: {score, speed, message, points}
	const speeds = [
		{
			score: 0,
			speed: 800,
			message: '',
			points: 1,
			keys: 1,
		},
		{
			score: 3,
			speed: 750,
			message: "You've got it!",
			points: 2,
			keys: 2,
		},
		{
			score: 20,
			speed: 670,
			message: 'Keep going!',
			points: 5,
			keys: 3,
		},
		{
			score: 70,
			speed: 620,
			message: "You're doing great!",
			points: 7,
			keys: 3,
		},
		{
			score: 150,
			speed: 560,
			message: 'You rock!',
			points: 10,
			keys: 3,
		},
		{
			score: 300,
			speed: 510,
			message: "Don't stop!",
			points: 12,
			keys: 4,
		},
		{
			score: 500,
			speed: 490,
			message: 'Tricky!',
			points: 15,
			keys: 4,
		},
		{
			score: 760,
			speed: 465,
			message: 'Great!',
			points: 17,
			keys: 4,
		},
		{
			score: 1100,
			speed: 440,
			message: 'I like your style!',
			points: 20,
			keys: 4,
		},
		{
			score: 1500,
			speed: 390,
			message: 'Awesome!',
			points: 22,
			keys: 4,
		},
		{
			score: 2000,
			speed: 360,
			message: 'Yeah!!',
			points: 25,
			keys: 4,
		},
		{
			score: 2700,
			speed: 330,
			message: 'How do you do that?',
			points: 27,
			keys: 4,
		},
		{
			score: 3500,
			speed: 310,
			message: '...how?',
			points: 30,
			keys: 4,
		},
		{
			score: 4300,
			speed: 290,
			message: 'Don\'t ever stop!!',
			points: 32,
			keys: 4,
		},
		{
			score: 5500,
			speed: 280,
			message: 'I\'m really impressed.',
			points: 35,
			keys: 4,
		},
		{
			score: 7000,
			speed: 270,
			message: 'Arrow hero!',
			points: 40,
			keys: 4,
		},
		{
			score: 10000,
			speed: 260,
			message: 'You\'re really still here?',
			points: 40,
			keys: 4,
		},
		{
			score: 10500,
			speed: 250,
			message: "That's incredible!",
			points: 40,
			keys: 4,
		},
	]
	let current = speeds[0]
	let gameState = false
	let maxLife = 5000
	let currentLife = maxLife
	// array of objects: {delay, started, interval}
	let scheduledSpawns = []
	let bestScore = localStorage.getItem('bestScore')
	const mobileControls = document.querySelector('.mobile-controls')
	const body = document.querySelector('body')

	function updatePoints (pts) {
		if (pts < 1)
			pts = 1

		pts = Math.floor(pts)
		points += pts
		square.classList.add('bump')
		square.onanimationend = () => {
			square.classList.remove('bump')
		}

		pointContainers.forEach(pointContainer => {
			pointContainer.textContent = points
			pointContainer.classList.add('bump')
			pointContainer.onanimationend = () => {
				pointContainer.classList.remove('bump')
			}
		})

		const ding = document.createElement('div')
		ding.classList.add('ding')
		ding.textContent = `+${ pts }`
		ding.onanimationend = () => {
			ding.remove()
		}

		square.appendChild(ding)
	}

	const levelMessage = document.querySelector('.level-message')

	function updateSpeed () {
		const oldSpeed = current
		for (const i in speeds) {
			const _speed = speeds[i]
			if (points >= _speed.score)
				current = _speed
			else if (points < _speed.score)
				break

		}

		if (current.speed !== oldSpeed.speed) {
			// Speed changed !
			levelMessage.textContent = current.message
			levelMessage.classList.add('show')
			levelMessage.onanimationend = () => {
				levelMessage.classList.remove('show')
			}
		}

	}

	function spawnRandomKey (obj) {
		if (gameState !== 'paused') // running or ended
			removeScheduledSpawn(obj)

		if (gameState === 'end' || gameState === 'paused' || gameState === 'restart')
			return

		const arr = [ 'key-right', 'key-left', 'key-down', 'key-up' ]
		const direction = arr[Math.floor(Math.random() * current.keys)]
		let nextKey = container.querySelector('.idle')
		if (nextKey === null) {
			nextKey = document.createElement('div')
			nextKey.classList.add('key', direction)
			nextKey.onanimationend = () => {

				if (gameState === 'end' || gameState === 'restart' || nextKey.classList.contains('idle'))
					return

				if (nextKey.classList.contains(keypressed)) {
					currentLife = Math.min(currentLife + 200, maxLife)

					updatePoints(current.points)
				} else {
					currentLife -= 1000
					square.classList.add('bad')
					square.onanimationend = () => {
						square.classList.remove('bad')
					}
				}

				updateSpeed()

				const percent = currentLife * 100 / maxLife
				const percentElem = document.querySelector('.percent')
				percentElem.style.width = percent + '%'

				if (percent < 20)
					percentElem.classList.add('low')
				else if (percent < 60) {
					percentElem.classList.add('medium')
					percentElem.classList.remove('low')
				} else
					percentElem.classList.remove('low', 'medium')

				if (currentLife <= 0 && gameState === 'running')
					endGame()

				nextKey.classList.add('idle')
				nextKey.classList.remove('key-up', 'key-down', 'key-left', 'key-right')
			}
			container.appendChild(nextKey)
		} else {
			nextKey.classList.remove('idle')
			nextKey.classList.add(direction)
		}

		// Spawn next key
		scheduleSpawn(current.speed)
	}

	function scheduleSpawn (delay) {
		const now = new Date()
		const obj = {
			delay,
			started: now.getTime(),
		}
		obj.interval = setTimeout(spawnRandomKey, delay, obj)
		scheduledSpawns.push(obj)
	}

	function removeScheduledSpawn (obj) {
		const index = scheduledSpawns.indexOf(obj)
		if (index > -1)
			scheduledSpawns.splice(index, 1)

	}

	function pauseScheduledSpawns () {
		const now = new Date()
		for (const i in scheduledSpawns) {
			const obj = scheduledSpawns[i]
			obj.delay -= now.getTime() - obj.started
			obj.started = null
			clearInterval(obj.interval)
		}
	}

	function resumeScheduledSpawns () {
		const now = new Date()
		for (const i in scheduledSpawns) {
			const obj = scheduledSpawns[i]
			obj.started = now.getTime()
			obj.interval = setTimeout(spawnRandomKey, obj.delay, obj)
		}
	}

	function endGame () {
		gameState = 'end'
		document.querySelectorAll('.key').forEach(k => k.classList.add('hide'))

		const keySelectorContainer = document.querySelector('.key-selector-container')
		keySelectorContainer.classList.add('hide')
		keySelectorContainer.classList.remove('show')

		const results = document.querySelector('.results')
		results.classList.add('show')
		results.classList.remove('hide')

		const pointsContainer = document.querySelector('.points-container')
		pointsContainer.classList.add('hide')
		pointsContainer.classList.remove('show')

		const percent = document.querySelector('.percent')
		percent.style.width = '0%'

		document.querySelector('.pause-btn').textContent = 'Restart'

		if (points > bestScore) {
			// update best score
			bestScore = points
			sessionStorage.setItem('bestScore', bestScore)
			document.querySelector('.best-points .value').textContent = bestScore
			document.querySelector('.best').style.display = 'block'
		}
	}

	function restartGame () {
		gameState = 'restart'
		points = 0
		maxLife = 5000
		currentLife = maxLife
		current = speeds[0]

		if (keypressed !== '')
			square.classList.remove('s-' + keypressed)

		keypressed = ''

		container.querySelectorAll('.key').forEach(key => {
			key.classList.remove('key-up', 'key-down', 'key-left', 'key-right', 'hide')
			key.classList.add('idle')
		})

		const keySelectorContainer = document.querySelector('.key-selector-container')
		keySelectorContainer.classList.add('show')
		keySelectorContainer.classList.remove('hide')

		const results = document.querySelector('.results')
		results.classList.add('hide')
		results.classList.remove('show')

		const pointsContainer = document.querySelector('.points-container')
		pointsContainer.classList.add('show')
		pointsContainer.classList.remove('hide')

		setTimeout(() => {
			for (const i in scheduledSpawns) {
				const obj = scheduledSpawns[i]
				clearInterval(obj.interval)
			}
			scheduledSpawns = []
			pointContainers.forEach(pointContainer => pointContainer.textContent = points)

			const percent = document.querySelector('.percent')
			percent.style.width = '100%'
			percent.classList.remove('low', 'medium')

			setTimeout(() => {
				gameState = 'running'
				document.querySelector('.pause-btn').textContent = 'Pause'
				scheduleSpawn(1)
			}, 950)
		}, 1000)
	}

	document.onkeydown = e => {

		if (e.keyCode === 32) {
			e.preventDefault()
			if (gameState === 'running' || gameState === 'paused') {
				// space bar pressed

				gameState = gameState === 'running' ? 'paused' : 'running'
				document.querySelectorAll('.key').forEach(k => k.classList.toggle('paused', gameState === 'paused'))
				document.querySelector('.pause').classList.toggle('show', gameState === 'paused')

				if (gameState === 'paused')
					pauseScheduledSpawns()
				else if (gameState === 'running')
					resumeScheduledSpawns()

			} else if (gameState === 'end')
				restartGame()

		}

		if ((e.keyCode >= 37 && e.keyCode <= 40 || e.keyCode >= 72 && e.keyCode <= 76)
            && gameState !== 'paused' && gameState !== 'restart') {
			// arrow keys pressed

			e.preventDefault()
			if (gameState === false) {
				startGame()
				return false
			}

			if (keypressed !== '')
				square.classList.remove('s-' + keypressed)

			switch (e.keyCode) {
				case 37: // left
				case 72: // h
					keypressed = 'key-left'
					break

				case 38: // up
				case 75: // j
					keypressed = 'key-up'
					break

				case 39: // right
				case 76: // l
					keypressed = 'key-right'
					break

				case 40: // down
				case 74: // j
					keypressed = 'key-down'
					break
			}
			square.classList.add('s-' + keypressed)
		}

	}

	function startGame () {
		gameState = 'running'

		document.querySelector('.points-container').classList.add('show')
		document.querySelector('.helper-container').classList.add('hide')
		setTimeout(() => {
			const keySelector = document.querySelector('.key-selector')
			keySelector.classList.add('show', 'fade')
			keySelector.onanimationend = () => keySelector.classList.remove('fade')
		}, 500)
		scheduleSpawn(1000)

		document.querySelector('.percent').style.width = '100%'
	}

	if (bestScore) {
		document.querySelector('.best-points .value').textContent = bestScore
		document.querySelector('.best').style.display = 'block'
	}

	function addMobileListener (selector, /* @deprecated */ keyCode) {
		const keyElem = mobileControls.querySelector(selector)
		addMultipleEventListener(keyElem, [ 'touchstart', 'click' ], () => {
			document.dispatchEvent(new KeyboardEvent('keydown', { keyCode }))
		})
	}

	addMobileListener('.key-left', 37)
	addMobileListener('.key-up', 38)
	addMobileListener('.key-right', 39)
	addMobileListener('.key-down', 40)
	mobileControls.querySelector('.pause-btn').ontouchstart = () => {
		document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 32 /* space */ }))
	}

	body.classList.toggle('colorblind', localStorage.getItem('colorblind') === 'yes')
	addMultipleEventListener(document.querySelector('.colorblind-btn'), [ 'click', 'touchstart' ], () => {
		body.classList.toggle('colorblind', !body.classList.contains('colorblind'))
		localStorage.setItem('colorblind', body.classList.contains('colorblind') ? 'yes' : 'no')
	})
})
<|endoftext|>


<commit_before>import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { t } from '../util/locale';
import { actionAddMidpoint, actionMoveNode, actionNoop } from '../actions';
import { behaviorDraw } from './draw';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';
import { utilKeybinding } from '../util';

export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselineGraph) {

    var origWay = context.entity(wayID);

    var annotation = t((origWay.isDegenerate() ?
        'operations.start.annotation.' :
        'operations.continue.annotation.') + context.geometry(wayID)
    );

    var behavior = behaviorDraw(context);
    behavior.hover().initialNodeId(index ? origWay.nodes[index] : origWay.nodes[origWay.nodes.length - 1]);

    var _tempEdits = 0;

    var end = osmNode({ loc: context.map().mouseCoordinates() });

    // Push an annotated state for undo to return back to.
    // We must make sure to remove this edit later.
    context.perform(actionNoop(), annotation);
    _tempEdits++;

    // Add the drawing node to the graph.
    // We must make sure to remove this edit later.
    context.perform(_actionAddDrawNode());
    _tempEdits++;

    function keydown() {
        if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            if (context.surface().classed('nope')) {
                context.surface()
                    .classed('nope-suppressed', true);
            }
            context.surface()
                .classed('nope', false)
                .classed('nope-disabled', true);
        }
    }

    function keyup() {
        if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            if (context.surface().classed('nope-suppressed')) {
                context.surface()
                    .classed('nope', true);
            }
            context.surface()
                .classed('nope-suppressed', false)
                .classed('nope-disabled', false);
        }
    }

    function allowsVertex(d) {
        return context.presets().allowsVertex(d, context.graph());
    }

    // related code
    // - `mode/drag_node.js`     `doMode()`
    // - `behavior/draw.js`      `click()`
    // - `behavior/draw_way.js`  `move()`
    function move(datum) {
        context.surface().classed('nope-disabled', d3_event.altKey);

        var targetLoc = datum && datum.properties && datum.properties.entity && allowsVertex(datum.properties.entity) && datum.properties.entity.loc;
        var targetNodes = datum && datum.properties && datum.properties.nodes;
        var loc = context.map().mouseCoordinates();

        if (targetLoc) {   // snap to node/vertex - a point target with `.loc`
            loc = targetLoc;

        } else if (targetNodes) {   // snap to way - a line target with `.nodes`
            var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id);
            if (choice) {
                loc = choice.loc;
            }
        }

        context.replace(actionMoveNode(end.id, loc));
        end = context.entity(end.id);
        checkGeometry(false);
    }

    // Check whether this edit causes the geometry to break.
    // If so, class the surface with a nope cursor.
    // `finishDraw` - Only checks the relevant line segments if finishing drawing
    function checkGeometry(finishDraw) {
        var nopeDisabled = context.surface().classed('nope-disabled');
        var isInvalid = isInvalidGeometry(end, context.graph(), finishDraw);

        if (nopeDisabled) {
            context.surface()
                .classed('nope', false)
                .classed('nope-suppressed', isInvalid);
        } else {
            context.surface()
                .classed('nope', isInvalid)
                .classed('nope-suppressed', false);
        }
    }

    function isInvalidGeometry(entity, graph, finishDraw) {
        var parents = graph.parentWays(entity);

        for (var i = 0; i < parents.length; i++) {
            var parent = parents[i];

            var nodes = graph.childNodes(parent);

            if (origWay.isClosed()) { // Check if Area
                if (finishDraw) {
                    nodes.splice(-2, 1);
                    entity = nodes[nodes.length-2];
                } else {
                    nodes.pop();
                }
            } else { // Line
                if (finishDraw) {
                    nodes.pop();
                }
            }

            if (geoHasSelfIntersections(nodes, entity.id)) {
                return true;
            }
        }

        return false;
    }

    function undone() {
        // Undo popped the history back to the initial annotated no-op edit.
        _tempEdits = 0;     // We will deal with the temp edits here
        context.pop(1);     // Remove initial no-op edit

        if (context.graph() === baselineGraph) {    // We've undone back to the beginning
            // baselineGraph may be behind startGraph if this way was added rather than continued
            while (context.graph() !== startGraph) {
                context.pop();
            }
            context.enter(modeSelect(context, [wayID]));
        } else {
            // Remove whatever segment was drawn previously and continue drawing
            context.pop(1);
            context.enter(mode);
        }
    }

    function setActiveElements() {
        context.surface().selectAll('.' + end.id)
            .classed('active', true);
    }

    var drawWay = function(surface) {
        behavior
            .on('move', move)
            .on('click', drawWay.add)
            .on('clickWay', drawWay.addWay)
            .on('clickNode', drawWay.addNode)
            .on('undo', context.undo)
            .on('cancel', drawWay.cancel)
            .on('finish', drawWay.finish);

        d3_select(window)
            .on('keydown.drawWay', keydown)
            .on('keyup.drawWay', keyup);

        context.map()
            .dblclickEnable(false)
            .on('drawn.draw', setActiveElements);

        setActiveElements();

        surface.call(behavior);

        context.history()
            .on('undone.draw', undone);
    };

    drawWay.off = function(surface) {
        // Drawing was interrupted unexpectedly.
        // This can happen if the user changes modes,
        // clicks geolocate button, a hashchange event occurs, etc.
        if (_tempEdits) {
            context.history().reset('drawWay-initial');
        }

        context.map()
            .on('drawn.draw', null);

        surface.call(behavior.off)
            .selectAll('.active')
            .classed('active', false);

        surface
            .classed('nope', false)
            .classed('nope-suppressed', false)
            .classed('nope-disabled', false);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);

        context.history()
            .on('undone.draw', null);
    };

    function _actionAddDrawNode() {
        return function(graph) {
            return graph
                .replace(end)
                .replace(origWay.addNode(end.id, index));
        };
    }

    function _actionReplaceDrawNode(newNode) {
        return function(graph) {
            return graph
                .replace(origWay.addNode(newNode.id, index))
                .remove(end);
        };
    }

    // Accept the current position of the drawing node and continue drawing.
    drawWay.add = function(loc, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing way.
    drawWay.addWay = function(loc, edge, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            actionAddMidpoint({ loc: loc, edge: edge }, end),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing node and continue drawing.
    drawWay.addNode = function(node, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionReplaceDrawNode(node),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Finish the draw operation, removing the temporary edits.
    // If the way has enough nodes to be valid, it's selected.
    // Otherwise, delete everything and return to browse mode.
    drawWay.finish = function() {
        checkGeometry(true);   // finishDraw = true
        if (context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        var way = context.hasEntity(wayID);
        if (!way || way.isDegenerate()) {
            drawWay.cancel();
            return;
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);
        var isNewFeature = !mode.isContinuing;
        context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));
    };

    // Cancel the draw operation, delete everything, and return to browse mode.
    drawWay.cancel = function() {
        context.pop(_tempEdits);
        _tempEdits = 0;

        while (context.graph() !== startGraph) {
            context.pop();
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.surface()
            .classed('nope', false)
            .classed('nope-disabled', false)
            .classed('nope-suppressed', false);

        context.enter(modeBrowse(context));
    };

    drawWay.activeID = function() {
        if (!arguments.length) return end.id;
        // no assign
        return drawWay;
    };

    drawWay.tail = function(text) {
        behavior.tail(text);
        return drawWay;
    };

    return drawWay;
}
<commit_msg>Fix a bug where all data would disappear when switching modes while drawing a way (close #5917)<commit_after>import {
    event as d3_event,
    select as d3_select
} from 'd3-selection';

import { t } from '../util/locale';
import { actionAddMidpoint, actionMoveNode, actionNoop } from '../actions';
import { behaviorDraw } from './draw';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';
import { utilKeybinding } from '../util';

export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselineGraph) {

    var origWay = context.entity(wayID);

    var annotation = t((origWay.isDegenerate() ?
        'operations.start.annotation.' :
        'operations.continue.annotation.') + context.geometry(wayID)
    );

    var behavior = behaviorDraw(context);
    behavior.hover().initialNodeId(index ? origWay.nodes[index] : origWay.nodes[origWay.nodes.length - 1]);

    var _tempEdits = 0;

    var end = osmNode({ loc: context.map().mouseCoordinates() });

    // Push an annotated state for undo to return back to.
    // We must make sure to remove this edit later.
    context.perform(actionNoop(), annotation);
    _tempEdits++;

    // Add the drawing node to the graph.
    // We must make sure to remove this edit later.
    context.perform(_actionAddDrawNode());
    _tempEdits++;

    function keydown() {
        if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            if (context.surface().classed('nope')) {
                context.surface()
                    .classed('nope-suppressed', true);
            }
            context.surface()
                .classed('nope', false)
                .classed('nope-disabled', true);
        }
    }

    function keyup() {
        if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            if (context.surface().classed('nope-suppressed')) {
                context.surface()
                    .classed('nope', true);
            }
            context.surface()
                .classed('nope-suppressed', false)
                .classed('nope-disabled', false);
        }
    }

    function allowsVertex(d) {
        return context.presets().allowsVertex(d, context.graph());
    }

    // related code
    // - `mode/drag_node.js`     `doMode()`
    // - `behavior/draw.js`      `click()`
    // - `behavior/draw_way.js`  `move()`
    function move(datum) {
        context.surface().classed('nope-disabled', d3_event.altKey);

        var targetLoc = datum && datum.properties && datum.properties.entity && allowsVertex(datum.properties.entity) && datum.properties.entity.loc;
        var targetNodes = datum && datum.properties && datum.properties.nodes;
        var loc = context.map().mouseCoordinates();

        if (targetLoc) {   // snap to node/vertex - a point target with `.loc`
            loc = targetLoc;

        } else if (targetNodes) {   // snap to way - a line target with `.nodes`
            var choice = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id);
            if (choice) {
                loc = choice.loc;
            }
        }

        context.replace(actionMoveNode(end.id, loc));
        end = context.entity(end.id);
        checkGeometry(false);
    }

    // Check whether this edit causes the geometry to break.
    // If so, class the surface with a nope cursor.
    // `finishDraw` - Only checks the relevant line segments if finishing drawing
    function checkGeometry(finishDraw) {
        var nopeDisabled = context.surface().classed('nope-disabled');
        var isInvalid = isInvalidGeometry(end, context.graph(), finishDraw);

        if (nopeDisabled) {
            context.surface()
                .classed('nope', false)
                .classed('nope-suppressed', isInvalid);
        } else {
            context.surface()
                .classed('nope', isInvalid)
                .classed('nope-suppressed', false);
        }
    }

    function isInvalidGeometry(entity, graph, finishDraw) {
        var parents = graph.parentWays(entity);

        for (var i = 0; i < parents.length; i++) {
            var parent = parents[i];

            var nodes = graph.childNodes(parent);

            if (origWay.isClosed()) { // Check if Area
                if (finishDraw) {
                    nodes.splice(-2, 1);
                    entity = nodes[nodes.length-2];
                } else {
                    nodes.pop();
                }
            } else { // Line
                if (finishDraw) {
                    nodes.pop();
                }
            }

            if (geoHasSelfIntersections(nodes, entity.id)) {
                return true;
            }
        }

        return false;
    }

    function undone() {
        // Undo popped the history back to the initial annotated no-op edit.
        _tempEdits = 0;     // We will deal with the temp edits here
        context.pop(1);     // Remove initial no-op edit

        if (context.graph() === baselineGraph) {    // We've undone back to the beginning
            // baselineGraph may be behind startGraph if this way was added rather than continued
            resetContextGraphToStartGraph();
            context.enter(modeSelect(context, [wayID]));
        } else {
            // Remove whatever segment was drawn previously and continue drawing
            context.pop(1);
            context.enter(mode);
        }
    }

    function setActiveElements() {
        context.surface().selectAll('.' + end.id)
            .classed('active', true);
    }

    var drawWay = function(surface) {
        behavior
            .on('move', move)
            .on('click', drawWay.add)
            .on('clickWay', drawWay.addWay)
            .on('clickNode', drawWay.addNode)
            .on('undo', context.undo)
            .on('cancel', drawWay.cancel)
            .on('finish', drawWay.finish);

        d3_select(window)
            .on('keydown.drawWay', keydown)
            .on('keyup.drawWay', keyup);

        context.map()
            .dblclickEnable(false)
            .on('drawn.draw', setActiveElements);

        setActiveElements();

        surface.call(behavior);

        context.history()
            .on('undone.draw', undone);
    };

    function resetContextGraphToStartGraph() {
        while (context.graph() !== startGraph) {
            context.pop();
        }
    }

    drawWay.off = function(surface) {
        // Drawing was interrupted unexpectedly.
        // This can happen if the user changes modes,
        // clicks geolocate button, a hashchange event occurs, etc.
        if (_tempEdits) {
            context.pop(_tempEdits);
            resetContextGraphToStartGraph();
        }

        context.map()
            .on('drawn.draw', null);

        surface.call(behavior.off)
            .selectAll('.active')
            .classed('active', false);

        surface
            .classed('nope', false)
            .classed('nope-suppressed', false)
            .classed('nope-disabled', false);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);

        context.history()
            .on('undone.draw', null);
    };

    function _actionAddDrawNode() {
        return function(graph) {
            return graph
                .replace(end)
                .replace(origWay.addNode(end.id, index));
        };
    }

    function _actionReplaceDrawNode(newNode) {
        return function(graph) {
            return graph
                .replace(origWay.addNode(newNode.id, index))
                .remove(end);
        };
    }

    // Accept the current position of the drawing node and continue drawing.
    drawWay.add = function(loc, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing way.
    drawWay.addWay = function(loc, edge, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionAddDrawNode(),
            actionAddMidpoint({ loc: loc, edge: edge }, end),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Connect the way to an existing node and continue drawing.
    drawWay.addNode = function(node, d) {
        if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        context.perform(
            _actionReplaceDrawNode(node),
            annotation
        );

        checkGeometry(false);   // finishDraw = false
        context.enter(mode);
    };

    // Finish the draw operation, removing the temporary edits.
    // If the way has enough nodes to be valid, it's selected.
    // Otherwise, delete everything and return to browse mode.
    drawWay.finish = function() {
        checkGeometry(true);   // finishDraw = true
        if (context.surface().classed('nope')) {
            return;   // can't click here
        }

        context.pop(_tempEdits);
        _tempEdits = 0;

        var way = context.hasEntity(wayID);
        if (!way || way.isDegenerate()) {
            drawWay.cancel();
            return;
        }

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);
        var isNewFeature = !mode.isContinuing;
        context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));
    };

    // Cancel the draw operation, delete everything, and return to browse mode.
    drawWay.cancel = function() {
        context.pop(_tempEdits);
        _tempEdits = 0;

        resetContextGraphToStartGraph();

        window.setTimeout(function() {
            context.map().dblclickEnable(true);
        }, 1000);

        context.surface()
            .classed('nope', false)
            .classed('nope-disabled', false)
            .classed('nope-suppressed', false);

        context.enter(modeBrowse(context));
    };

    drawWay.activeID = function() {
        if (!arguments.length) return end.id;
        // no assign
        return drawWay;
    };

    drawWay.tail = function(text) {
        behavior.tail(text);
        return drawWay;
    };

    return drawWay;
}
<|endoftext|>