import EventEmitter from 'events';
import onFinished from 'on-finished';

/*
 * RequestQueue to ensure that only a single request is executing at a time.
 *
 * This middleware intercepts requests as they come in by delaying executing of
 * next() until previous requests finish processing. This complements external
 * server configuration via haproxy or similar that restricts concurrent
 * requests. This per-process queue allows an application level guarantee of
 * mutual exclusion of requests.  This allows that behavior to be depended
 * upon, allowing for safe (but careful) use of global state. Additionally,
 * this allows for lifecycle hooks to be added for the periods when no request
 * is currently executing, before or after the request has been run. These are
 * ideal points to install behavior to reset global state or perform actions
 * against the server at a "clean state" point in time.
 */
export default class RequestQueue extends EventEmitter {
  constructor() {
    super();
    this.queue = [];
    this.current = null;

    this.outerMiddleware = this.outerMiddleware.bind(this);
    this.innerMiddleware = this.innerMiddleware.bind(this);
    this.finishCurrent = this.finishCurrent.bind(this);
  }

  process() {
    if (!this.current) {
      this.current = this.queue.shift();
      this.emit('queueLength', this.queue.length);

      if (this.current) {
        this.emit('beforeRequest');
        this.current.start();
      }
    } else {
      this.emit('queueLength', this.queue.length);
    }
  }

  /*
   * Outer middleware must be the very first middleware installed on the app.
   * This intercepts and begins queueing the request.
   */
  outerMiddleware(req, res, next) {
    const job = { req, res, start: next };

    this.push(job);
  }

  /*
   * Inner middleware must be last middleware installed before endpoints.  This
   * is only necessary because on-finished executes its callbacks in the order
   * in which they were installed.  We need this to be innermost so that we
   * advance the queue only after the request and all other on-finished
   * callbacks complete.
   *
   * Not adding this middleware will result in the queue never being drained.
   */
  innerMiddleware(req, res, next) {
    onFinished(res, this.finishCurrent);
    next();
  }


  push(job) {
    this.queue.push(job);
    this.process();
  }

  finishCurrent() {
    this.current = null;
    this.emit('afterRequest');
    this.process();
  }
}