<commit_before>
var $ = document.querySelector.bind(document)

var state = {
  theme,
  raw,
  themes,
  content,
  compiler,
  html: '',
  markdown: '',
  toc: '',
  interval: null,
  ms: 1000,
}

chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
  if (req.message === 'reload') {
    location.reload(true)
  }
  else if (req.message === 'theme') {
    state.theme = req.theme
    m.redraw()
  }
  else if (req.message === 'themes') {
    state.themes = req.themes
    m.redraw()
  }
  else if (req.message === 'raw') {
    state.raw = req.raw
    m.redraw()
  }
  else if (req.message === 'autoreload') {
    clearInterval(state.interval)
  }
})

var oncreate = {
  markdown: () => {
    scroll.body()
  },
  html: () => {
    scroll.body()

    if (state.content.toc && !state.toc) {
      state.toc = toc()
      m.redraw()
    }

    setTimeout(() => Prism.highlightAll(), 20)

    anchors()
  },
  toc: () => {
    scroll.toc()
  }
}

function mount () {
  $('pre').style.display = 'none'
  var md = $('pre').innerText

  m.mount($('body'), {
    oninit: () => {
      state.markdown = md
      chrome.runtime.sendMessage({
        message: 'markdown',
        compiler: state.compiler,
        markdown: state.markdown
      }, (res) => {
        state.html = state.content.emoji ? emojinator(res.html) : res.html
        m.redraw()
      })
    },
    view: () => {
      var dom = []

      if (state.raw) {
        dom.push(m('pre#_markdown', {oncreate: oncreate.markdown}, state.markdown))
        $('body').classList.remove('_toc-left', '_toc-right')
      }
      else {
        if (state.theme) {
          dom.push(m('link#_theme', {
            rel: 'stylesheet', type: 'text/css',
            href: chrome.runtime.getURL(`/themes/${state.theme}.css`),
          }))
        }
        if (state.html) {
          dom.push(m('#_html', {oncreate: oncreate.html,
            class: (/github(-dark)?/.test(state.theme) ? 'markdown-body' : 'markdown-theme') +
            (state.themes.wide ? ' wide-theme' : '')
          },
            m.trust(state.html)
          ))
          if (state.content.toc && state.toc) {
            dom.push(m('#_toc', {oncreate: oncreate.toc},
              m.trust(state.toc)
            ))
            $('body').classList.add('_toc-left')
          }
          if (state.content.mathjax) {
            dom.push(m('script', {type: 'text/x-mathjax-config'}, mathjax))
            dom.push(m('script', {
              src: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js'
            }))
          }
          if (state.content.mermaid) {
            dom.push(m('script', {
              src: 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.8.4/mermaid.min.js'
            }))
            dom.push(m('script', {type: 'text/javascript'}, `
              ;(() => {
                var timeout = setInterval(() => {
                  if (!!(window.mermaid && mermaid.init)) {
                    clearInterval(timeout)
                    mermaid.init({}, 'code.language-mmd, code.language-mermaid')
                  }
                }, 50)
              })()
            `))
          }
        }
      }

      return (dom.length ? dom : m('div'))
    }
  })
}

var scroll = (() => {
  function race (done) {
    Promise.race([
      Promise.all([
        new Promise((resolve) => {
          var diagrams = Array.from(document.querySelectorAll('code.language-mmd, code.language-mermaid'))
          if (!state.content.mermaid || !diagrams.length) {
            resolve()
          }
          else {
            var timeout = setInterval(() => {
              var svg = Array.from(document.querySelectorAll('code.language-mmd svg, code.language-mermaid svg'))
              if (diagrams.length === svg.length) {
                clearInterval(timeout)
                resolve()
              }
            }, 50)
          }
        }),
        new Promise((resolve) => {
          var images = Array.from(document.querySelectorAll('img'))
          if (!images.length) {
            resolve()
          }
          else {
            var loaded = 0
            images.forEach((img) => {
              img.addEventListener('load', () => {
                if (++loaded === images.length) {
                  resolve()
                }
              }, {once: true})
            })
          }
        }),
      ]),
      new Promise((resolve) => setTimeout(resolve, 500))
    ])
    .then(done)
  }
  function debounce (container, done) {
    var listener = /html/i.test(container.nodeName) ? window : container
    var timeout = null
    listener.addEventListener('scroll', () => {
      clearTimeout(timeout)
      timeout = setTimeout(done, 100)
    })
  }
  function listen (container, prefix) {
    var key = prefix + location.origin + location.pathname
    try {
      container.scrollTop = parseInt(localStorage.getItem(key))
      debounce(container, () => {
        localStorage.setItem(key, container.scrollTop)
      })
    }
    catch (err) {
      chrome.storage.local.get(key, (res) => {
        container.scrollTop = parseInt(res[key])
      })
      debounce(container, () => {
        chrome.storage.local.set({[key]: container.scrollTop})
      })
    }
  }
  return {
    body: () => {
      var loaded
      race(() => {
        if (!loaded) {
          loaded = true
          if (state.content.scroll) {
            listen($('html'), 'md-')
          }
          else if (location.hash && $(location.hash)) {
            $('html').scrollTop = $(location.hash).offsetTop
          }
        }
      })
    },
    toc: () => {
      listen($('#_toc'), 'md-toc-')
    }
  }
})()

function anchors () {
  Array.from($('#_html').childNodes)
  .filter((node) => /h[1-6]/i.test(node.tagName))
  .forEach((node) => {
    var a = document.createElement('a')
    a.className = 'anchor'
    a.name = node.id
    a.href = '#' + node.id
    a.innerHTML = ''
    node.prepend(a)
  })
}

var toc = (
  link = (header) => '' + header.title + '') =>
  Array.from($('#_html').childNodes)
  .filter((node) => /h[1-6]/i.test(node.tagName))
  .map((node) => ({
    id: node.getAttribute('id'),
    level: parseInt(node.tagName.replace('H', '')),
    title: node.innerText.replace(/</g, '<').replace(/>/g, '>')
  }))
  .reduce((html, header) => {
    html += '
'.repeat(header.level)
    html += link(header)
    html += '
'.repeat(header.level)
    return html
  }, '')

if (document.readyState === 'complete') {
  mount()
}
else {
  var timeout = setInterval(() => {
    if (document.readyState === 'complete') {
      clearInterval(timeout)
      mount()
    }
  }, 0)
}

if (state.content.autoreload) {
  ;(() => {
    var initial = ''

    var response = (body) => {
      if (!initial) {
        initial = body
      }
      else if (initial !== body) {
        location.reload(true)
      }
    }

    var xhr = new XMLHttpRequest()
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        response(xhr.responseText)
      }
    }

    var get = () => {
      if (location.protocol === 'file:') {
        chrome.runtime.sendMessage({
          message: 'autoreload',
          location: location.href
        }, (res) => {
          if (res.err) {
            console.error(res.err)
            clearInterval(state.interval)
          }
          else {
            response(res.body)
          }
        })
      }
      else {
        xhr.open('GET', location.href + '?preventCache=' + Date.now(), true)
        try {
          xhr.send()
        }
        catch (err) {
          console.error(err)
          clearInterval(state.interval)
        }
      }
    }

    get()
    state.interval = setInterval(get, state.ms)
  })()
}

var mathjax = `
  // TeX-AMS_HTML
  MathJax.Hub.Config({
    jax: [
      'input/TeX',
      'output/HTML-CSS',
      'output/PreviewHTML',
    ],
    extensions: [
      'tex2jax.js',
      'AssistiveMML.js',
      'a11y/accessibility-menu.js',
    ],
    TeX: {
      extensions: [
        'AMSmath.js',
        'AMSsymbols.js',
        'noErrors.js',
        'noUndefined.js',
      ]
    },
    tex2jax: {
      inlineMath: [
        ['$', '$'],
        ['\\\\(', '\\\\)'],
      ],
      displayMath: [
        ['$$', '$$'],
        ['\\\\[', '\\\\]'],
      ],
      processEscapes: true
    },
    showMathMenu: false,
    showProcessingMessages: false,
    messageStyle: 'none',
    skipStartupTypeset: true, // disable initial rendering
    positionToHash: false
  })
  // set specific container to render, can be delayed too
  MathJax.Hub.Queue(
    ['Typeset', MathJax.Hub, '_html']
  )
`
<commit_msg>Choose main scroll container<commit_after>
var $ = document.querySelector.bind(document)

var state = {
  theme,
  raw,
  themes,
  content,
  compiler,
  html: '',
  markdown: '',
  toc: '',
  interval: null,
  ms: 1000,
}

chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
  if (req.message === 'reload') {
    location.reload(true)
  }
  else if (req.message === 'theme') {
    state.theme = req.theme
    m.redraw()
  }
  else if (req.message === 'themes') {
    state.themes = req.themes
    m.redraw()
  }
  else if (req.message === 'raw') {
    state.raw = req.raw
    m.redraw()
  }
  else if (req.message === 'autoreload') {
    clearInterval(state.interval)
  }
})

var oncreate = {
  markdown: () => {
    scroll.body()
  },
  html: () => {
    scroll.body()

    if (state.content.toc && !state.toc) {
      state.toc = toc()
      m.redraw()
    }

    setTimeout(() => Prism.highlightAll(), 20)

    anchors()
  },
  toc: () => {
    scroll.toc()
  }
}

function mount () {
  $('pre').style.display = 'none'
  var md = $('pre').innerText

  m.mount($('body'), {
    oninit: () => {
      state.markdown = md
      chrome.runtime.sendMessage({
        message: 'markdown',
        compiler: state.compiler,
        markdown: state.markdown
      }, (res) => {
        state.html = state.content.emoji ? emojinator(res.html) : res.html
        m.redraw()
      })
    },
    view: () => {
      var dom = []

      if (state.raw) {
        dom.push(m('pre#_markdown', {oncreate: oncreate.markdown}, state.markdown))
        $('body').classList.remove('_toc-left', '_toc-right')
      }
      else {
        if (state.theme) {
          dom.push(m('link#_theme', {
            rel: 'stylesheet', type: 'text/css',
            href: chrome.runtime.getURL(`/themes/${state.theme}.css`),
          }))
        }
        if (state.html) {
          dom.push(m('#_html', {oncreate: oncreate.html,
            class: (/github(-dark)?/.test(state.theme) ? 'markdown-body' : 'markdown-theme') +
            (state.themes.wide ? ' wide-theme' : '')
          },
            m.trust(state.html)
          ))
          if (state.content.toc && state.toc) {
            dom.push(m('#_toc', {oncreate: oncreate.toc},
              m.trust(state.toc)
            ))
            $('body').classList.add('_toc-left')
          }
          if (state.content.mathjax) {
            dom.push(m('script', {type: 'text/x-mathjax-config'}, mathjax))
            dom.push(m('script', {
              src: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js'
            }))
          }
          if (state.content.mermaid) {
            dom.push(m('script', {
              src: 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.8.4/mermaid.min.js'
            }))
            dom.push(m('script', {type: 'text/javascript'}, `
              ;(() => {
                var timeout = setInterval(() => {
                  if (!!(window.mermaid && mermaid.init)) {
                    clearInterval(timeout)
                    mermaid.init({}, 'code.language-mmd, code.language-mermaid')
                  }
                }, 50)
              })()
            `))
          }
        }
      }

      return (dom.length ? dom : m('div'))
    }
  })
}

var scroll = (() => {
  function race (done) {
    Promise.race([
      Promise.all([
        new Promise((resolve) => {
          var diagrams = Array.from(document.querySelectorAll('code.language-mmd, code.language-mermaid'))
          if (!state.content.mermaid || !diagrams.length) {
            resolve()
          }
          else {
            var timeout = setInterval(() => {
              var svg = Array.from(document.querySelectorAll('code.language-mmd svg, code.language-mermaid svg'))
              if (diagrams.length === svg.length) {
                clearInterval(timeout)
                resolve()
              }
            }, 50)
          }
        }),
        new Promise((resolve) => {
          var images = Array.from(document.querySelectorAll('img'))
          if (!images.length) {
            resolve()
          }
          else {
            var loaded = 0
            images.forEach((img) => {
              img.addEventListener('load', () => {
                if (++loaded === images.length) {
                  resolve()
                }
              }, {once: true})
            })
          }
        }),
      ]),
      new Promise((resolve) => setTimeout(resolve, 500))
    ])
    .then(done)
  }
  function debounce (container, done) {
    var listener = /html|body/i.test(container.nodeName) ? window : container
    var timeout = null
    listener.addEventListener('scroll', () => {
      clearTimeout(timeout)
      timeout = setTimeout(done, 100)
    })
  }
  function listen (container, prefix) {
    var key = prefix + location.origin + location.pathname
    try {
      container.scrollTop = parseInt(localStorage.getItem(key))
      debounce(container, () => {
        localStorage.setItem(key, container.scrollTop)
      })
    }
    catch (err) {
      chrome.storage.local.get(key, (res) => {
        container.scrollTop = parseInt(res[key])
      })
      debounce(container, () => {
        chrome.storage.local.set({[key]: container.scrollTop})
      })
    }
  }
  return {
    body: () => {
      var loaded
      race(() => {
        if (!loaded) {
          loaded = true
          var container = ((html = $('html')) => (
            html.scrollTop = 1,
            html.scrollTop ? (html.scrollTop = 0, html) : $('body')
          ))()
          if (state.content.scroll) {
            listen(container, 'md-')
          }
          else if (location.hash && $(location.hash)) {
            container.scrollTop = $(location.hash).offsetTop
          }
        }
      })
    },
    toc: () => {
      listen($('#_toc'), 'md-toc-')
    }
  }
})()

function anchors () {
  Array.from($('#_html').childNodes)
  .filter((node) => /h[1-6]/i.test(node.tagName))
  .forEach((node) => {
    var a = document.createElement('a')
    a.className = 'anchor'
    a.name = node.id
    a.href = '#' + node.id
    a.innerHTML = ''
    node.prepend(a)
  })
}

var toc = (
  link = (header) => '' + header.title + '') =>
  Array.from($('#_html').childNodes)
  .filter((node) => /h[1-6]/i.test(node.tagName))
  .map((node) => ({
    id: node.getAttribute('id'),
    level: parseInt(node.tagName.replace('H', '')),
    title: node.innerText.replace(/</g, '<').replace(/>/g, '>')
  }))
  .reduce((html, header) => {
    html += '
'.repeat(header.level)
    html += link(header)
    html += '
'.repeat(header.level)
    return html
  }, '')

if (document.readyState === 'complete') {
  mount()
}
else {
  var timeout = setInterval(() => {
    if (document.readyState === 'complete') {
      clearInterval(timeout)
      mount()
    }
  }, 0)
}

if (state.content.autoreload) {
  ;(() => {
    var initial = ''

    var response = (body) => {
      if (!initial) {
        initial = body
      }
      else if (initial !== body) {
        location.reload(true)
      }
    }

    var xhr = new XMLHttpRequest()
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        response(xhr.responseText)
      }
    }

    var get = () => {
      if (location.protocol === 'file:') {
        chrome.runtime.sendMessage({
          message: 'autoreload',
          location: location.href
        }, (res) => {
          if (res.err) {
            console.error(res.err)
            clearInterval(state.interval)
          }
          else {
            response(res.body)
          }
        })
      }
      else {
        xhr.open('GET', location.href + '?preventCache=' + Date.now(), true)
        try {
          xhr.send()
        }
        catch (err) {
          console.error(err)
          clearInterval(state.interval)
        }
      }
    }

    get()
    state.interval = setInterval(get, state.ms)
  })()
}

var mathjax = `
  // TeX-AMS_HTML
  MathJax.Hub.Config({
    jax: [
      'input/TeX',
      'output/HTML-CSS',
      'output/PreviewHTML',
    ],
    extensions: [
      'tex2jax.js',
      'AssistiveMML.js',
      'a11y/accessibility-menu.js',
    ],
    TeX: {
      extensions: [
        'AMSmath.js',
        'AMSsymbols.js',
        'noErrors.js',
        'noUndefined.js',
      ]
    },
    tex2jax: {
      inlineMath: [
        ['$', '$'],
        ['\\\\(', '\\\\)'],
      ],
      displayMath: [
        ['$$', '$$'],
        ['\\\\[', '\\\\]'],
      ],
      processEscapes: true
    },
    showMathMenu: false,
    showProcessingMessages: false,
    messageStyle: 'none',
    skipStartupTypeset: true, // disable initial rendering
    positionToHash: false
  })
  // set specific container to render, can be delayed too
  MathJax.Hub.Queue(
    ['Typeset', MathJax.Hub, '_html']
  )
`
<|endoftext|>


<commit_before>// Libraries
var https = require('https');
var url = require('url');
var uuid = require('node-uuid');
var db = require('redis').createClient();

/******************************************************************************
* Handle DB errors                                                            *
******************************************************************************/
db.on('error', function (err) {
  console.error('DB Error: ' + err);
});

/******************************************************************************
* Helper Functions                                                            *
******************************************************************************/
function success(response, ob) {
  response.writeHead(200, {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*'
  });
  response.write(JSON.stringify(ob));
  response.end();
}

function error(response, errno, errtext) {
  console.error('===ERROR===', errno, errtext);
  response.writeHead(errno, {'Content-Type': 'application/json'});
  response.write(JSON.stringify({error:errtext}));
  response.end();
}

function isUndefined(x) {
  return x === undefined;
}

function isString(x) {
  return (typeof x) === 'string';
}

function isBlank(str) {
  return str.trim() === '';
}

function isValidString(x) {
  return !isUndefined(x) && isString(x) && !isBlank(x);
}

function isValidPositiveIntegerString(x) {
  return isValidString(x) && x.trim().match(/^\d+$/) !== null;
}

/******************************************************************************
* Getters                                                                     *
******************************************************************************/
function codeByID(request, response) {
  var query = url.parse(request.url, true).query;
  var id = query.id;
  if (id === undefined) {
    return error(response, 400, 'Invalid code id');
  }
  db.get('code:' + id, function(err, reply) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (reply === null) {
      return error(response, 404, 'Code not found');
    }
    return success(response, JSON.parse(reply));
  });
};

function commentsOnLine(request, response) {
  var query = url.parse(request.url, true).query;
  var id = query.code_id;
  var line = query.line;
  if (id === undefined) {
    return error(response, 400, 'Invalid comment id');
  }
  if (line === undefined) {
    return error(response, 400, 'Invalid line number');
  }
  db.lrange('comment:' + id + ':' + line, 0, -1, function(err, reply) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (reply === null) {
      return error(response, 404, 'Comment not found');
    }
    var out = [];
    reply.forEach(function(value) {
      out.push(JSON.parse(value));
    });
    return success(response, out);
  });
};

function commentCount(request, response) {
  var query = url.parse(request.url, true).query;
  var code_id = query.code_id;
  db.smembers('comment:' + code_id + ':indices', function(err, indices) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (indices === null) {
      return error(response, 404, 'Comments not found.');
    }
    var multi = db.multi();
    indices.forEach(function(index) {
      multi.llen('comment:' + code_id + ':' + index);
    });
    multi.exec(function(err, lengths) {
      var out = {};
      lengths.forEach(function(length, i) {
        out[indices[i]] = length;
      });
      return success(response, out);
    });
  });
};

/******************************************************************************
* Setters                                                                     *
******************************************************************************/
function newcode(request, response) {
  // do some basic validation
  var fields = request.body;
  if (fields === null || !isValidString(fields.text)) {
    return error(response, 400, 'Invalid code text.');
  }
  var id=uuid.v4();
  // whitelist the data fields
  var data = {
    text: fields.text,
    lang: fields.lang
  };
  db.set('code:' + id, JSON.stringify(data), function(err) {
    if (err !== null) {
      return error(response, 500, 'Error while writing to database.');
    }
    return success(response, {id:id});
  });
};

function newcomment(request, response) {
  // reject if no referer
  if (request === null ||
     request.headers === undefined ||
     request.headers.referer === undefined) {
    return error(response, 400, 'Invalid referer');
  }
  // do some basic validation
  var fields = request.body;
  if (fields === null ||
     !isValidString(fields.text) ||
     !isValidPositiveIntegerString(fields.line_start) ||
     !isValidPositiveIntegerString(fields.line_end)) {
    return error(response, 400, 'Invalid field');
  }
  // make sure the line numbers are sane
  if (Number(fields.line_start) > Number(fields.line_end)) {
    return error(response, 400, 'Invalid line numbers');
  }
  // whitelist the data fields
  var data = {
    user: 'Anonymous',
    code_id: fields.code_id,
    text: fields.text,
    line_start: fields.line_start,
    line_end: fields.line_end,
    diffs: fields.diffs
  };
  if(request.session.email) {
    data.user = request.session.email;
  }
  db.multi()
    .rpush('comment:' + data.code_id + ':' + data.line_start,
           JSON.stringify(data))
    .sadd('comment:' + data.code_id + ':indices', data.line_start)
    .exec(function(err) {
      if (err !== null) {
        return error(response, 500, 'Error while writing to database.');
      }
      return success(response, data);
    });
};

/******************************************************************************
* Authentication                                                              *
*                                                                             *
* In the event of implementing more than just persona for authentication, you *
* should replace most of the following code with a library like               *
*                                                                             *
* passport           http://passportjs.org/                                   *
* passport-browserid https://github.com/jaredhanson/passport-browserid        *
*                                                                             *
******************************************************************************/

module.exports = function(host, clientPort) {
  var exports = {};

  exports.login = function login(request, response) {
    if(request === null ||
       request.body === null ||
       request.body.assertion === null) {
      return error(response, 400, 'Invalid assertion');
    }
    var assertion = request.body.assertion;
    var content = 'assertion=' + assertion + '&audience=http://';
    content += host + ':' + clientPort;
    var auth_request = https.request({
      host: 'verifier.login.persona.org',
      port: 443,
      path: '/verify',
      method: 'POST',
      headers: {'Content-Length': content.length,
                'Content-Type': 'application/x-www-form-urlencoded'}
    },function(auth_response) {
      var data_ob = null;
      // assume uft8
      auth_response.setEncoding('utf8');
      auth_response.on('data',function(data) {
        data_ob = JSON.parse(data);
      });
      auth_response.on('end',function() {
        if (auth_response.statusCode === 200 &&
            data_ob !== null &&
            data_ob.status === 'okay') {
          request.session.email = data_ob.email;
          return success(response, {email: data_ob.email});
        } else {
          return error(response,
                       auth_response.statusCode,
                       'Error validating assertion');
        }
      });
    });

    auth_request.write(content);
    auth_request.end();
  };

  exports.logout = function logout(request, response) {
    delete request.session.email;
    return success(response, {});
  };

  exports.codeByID = codeByID;
  exports.commentsOnLine = commentsOnLine;
  exports.commentCount = commentCount;
  exports.newcode = newcode;
  exports.newcomment = newcomment;

  return exports;
};

/* vim: set softtabstop=2 shiftwidth=2 tabstop=8 expandtab textwidth=80: */
<commit_msg>Cleaned up requestHandlers exports<commit_after>// Libraries
var https = require('https');
var url = require('url');
var uuid = require('node-uuid');
var db = require('redis').createClient();

/******************************************************************************
* The methods on this object will be exported as the public api.              *
******************************************************************************/
var public_api = {};

/******************************************************************************
* Handle DB errors                                                            *
******************************************************************************/
db.on('error', function (err) {
  console.error('DB Error: ' + err);
});

/******************************************************************************
* Helper Functions                                                            *
******************************************************************************/
function success(response, ob) {
  response.writeHead(200, {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*'
  });
  response.write(JSON.stringify(ob));
  response.end();
}

function error(response, errno, errtext) {
  console.error('===ERROR===', errno, errtext);
  response.writeHead(errno, {'Content-Type': 'application/json'});
  response.write(JSON.stringify({error:errtext}));
  response.end();
}

function isUndefined(x) {
  return x === undefined;
}

function isString(x) {
  return (typeof x) === 'string';
}

function isBlank(str) {
  return str.trim() === '';
}

function isValidString(x) {
  return !isUndefined(x) && isString(x) && !isBlank(x);
}

function isValidPositiveIntegerString(x) {
  return isValidString(x) && x.trim().match(/^\d+$/) !== null;
}

/******************************************************************************
* Getters                                                                     *
******************************************************************************/
public_api.codeByID = function codeByID(request, response) {
  var query = url.parse(request.url, true).query;
  var id = query.id;
  if (id === undefined) {
    return error(response, 400, 'Invalid code id');
  }
  db.get('code:' + id, function(err, reply) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (reply === null) {
      return error(response, 404, 'Code not found');
    }
    return success(response, JSON.parse(reply));
  });
};

public_api.commentsOnLine = function commentsOnLine(request, response) {
  var query = url.parse(request.url, true).query;
  var id = query.code_id;
  var line = query.line;
  if (id === undefined) {
    return error(response, 400, 'Invalid comment id');
  }
  if (line === undefined) {
    return error(response, 400, 'Invalid line number');
  }
  db.lrange('comment:' + id + ':' + line, 0, -1, function(err, reply) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (reply === null) {
      return error(response, 404, 'Comment not found');
    }
    var out = [];
    reply.forEach(function(value) {
      out.push(JSON.parse(value));
    });
    return success(response, out);
  });
};

public_api.commentCount = function commentCount(request, response) {
  var query = url.parse(request.url, true).query;
  var code_id = query.code_id;
  db.smembers('comment:' + code_id + ':indices', function(err, indices) {
    if (err !== null) {
      return error(response, 500, 'Error while reading from database.');
    }
    if (indices === null) {
      return error(response, 404, 'Comments not found.');
    }
    var multi = db.multi();
    indices.forEach(function(index) {
      multi.llen('comment:' + code_id + ':' + index);
    });
    multi.exec(function(err, lengths) {
      var out = {};
      lengths.forEach(function(length, i) {
        out[indices[i]] = length;
      });
      return success(response, out);
    });
  });
};

/******************************************************************************
* Setters                                                                     *
******************************************************************************/
public_api.newcode = function newcode(request, response) {
  // do some basic validation
  var fields = request.body;
  if (fields === null || !isValidString(fields.text)) {
    return error(response, 400, 'Invalid code text.');
  }
  var id=uuid.v4();
  // whitelist the data fields
  var data = {
    text: fields.text,
    lang: fields.lang
  };
  db.set('code:' + id, JSON.stringify(data), function(err) {
    if (err !== null) {
      return error(response, 500, 'Error while writing to database.');
    }
    return success(response, {id:id});
  });
};

public_api.newcomment = function newcomment(request, response) {
  // reject if no referer
  if (request === null ||
     request.headers === undefined ||
     request.headers.referer === undefined) {
    return error(response, 400, 'Invalid referer');
  }
  // do some basic validation
  var fields = request.body;
  if (fields === null ||
     !isValidString(fields.text) ||
     !isValidPositiveIntegerString(fields.line_start) ||
     !isValidPositiveIntegerString(fields.line_end)) {
    return error(response, 400, 'Invalid field');
  }
  // make sure the line numbers are sane
  if (Number(fields.line_start) > Number(fields.line_end)) {
    return error(response, 400, 'Invalid line numbers');
  }
  // whitelist the data fields
  var data = {
    user: 'Anonymous',
    code_id: fields.code_id,
    text: fields.text,
    line_start: fields.line_start,
    line_end: fields.line_end,
    diffs: fields.diffs
  };
  if(request.session.email) {
    data.user = request.session.email;
  }
  db.multi()
    .rpush('comment:' + data.code_id + ':' + data.line_start,
           JSON.stringify(data))
    .sadd('comment:' + data.code_id + ':indices', data.line_start)
    .exec(function(err) {
      if (err !== null) {
        return error(response, 500, 'Error while writing to database.');
      }
      return success(response, data);
    });
};

/******************************************************************************
* Authentication                                                              *
*                                                                             *
* In the event of implementing more than just persona for authentication, you *
* should replace most of the following code with a library like               *
*                                                                             *
* passport           http://passportjs.org/                                   *
* passport-browserid https://github.com/jaredhanson/passport-browserid        *
*                                                                             *
******************************************************************************/

module.exports = function(host, clientPort) {
  public_api.login = function login(request, response) {
    if(request === null ||
       request.body === null ||
       request.body.assertion === null) {
      return error(response, 400, 'Invalid assertion');
    }
    var assertion = request.body.assertion;
    var content = 'assertion=' + assertion + '&audience=http://';
    content += host + ':' + clientPort;
    var auth_request = https.request({
      host: 'verifier.login.persona.org',
      port: 443,
      path: '/verify',
      method: 'POST',
      headers: {'Content-Length': content.length,
                'Content-Type': 'application/x-www-form-urlencoded'}
    },function(auth_response) {
      var data_ob = null;
      // assume uft8
      auth_response.setEncoding('utf8');
      auth_response.on('data',function(data) {
        data_ob = JSON.parse(data);
      });
      auth_response.on('end',function() {
        if (auth_response.statusCode === 200 &&
            data_ob !== null &&
            data_ob.status === 'okay') {
          request.session.email = data_ob.email;
          return success(response, {email: data_ob.email});
        } else {
          return error(response,
                       auth_response.statusCode,
                       'Error validating assertion');
        }
      });
    });

    auth_request.write(content);
    auth_request.end();
  };

  public_api.logout = function logout(request, response) {
    delete request.session.email;
    return success(response, {});
  };

  return public_api;
};

/* vim: set softtabstop=2 shiftwidth=2 tabstop=8 expandtab textwidth=80: */
<|endoftext|>


<commit_before>/* Copyright 2018 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  AbortException,
  assert,
  createPromiseCapability,
  MissingPDFException,
  PasswordException,
  UnexpectedResponseException,
  UnknownErrorException,
  warn,
} from "./util.js";

const CallbackKind = {
  UNKNOWN: 0,
  DATA: 1,
  ERROR: 2,
};

const StreamKind = {
  UNKNOWN: 0,
  CANCEL: 1,
  CANCEL_COMPLETE: 2,
  CLOSE: 3,
  ENQUEUE: 4,
  ERROR: 5,
  PULL: 6,
  PULL_COMPLETE: 7,
  START_COMPLETE: 8,
};

function wrapReason(reason) {
  if (
    !(
      reason instanceof Error ||
      (typeof reason === "object" && reason !== null)
    )
  ) {
    if (
      typeof PDFJSDev === "undefined" ||
      PDFJSDev.test("!PRODUCTION || TESTING")
    ) {
      throw new Error(
        'wrapReason: Expected "reason" to be a (possibly cloned) Error.'
      );
    }
    warn('wrapReason: Expected "reason" to be a (possibly cloned) Error.');
    return reason;
  }
  switch (reason.name) {
    case "AbortException":
      return new AbortException(reason.message);
    case "MissingPDFException":
      return new MissingPDFException(reason.message);
    case "PasswordException":
      return new PasswordException(reason.message, reason.code);
    case "UnexpectedResponseException":
      return new UnexpectedResponseException(reason.message, reason.status);
    case "UnknownErrorException":
      return new UnknownErrorException(reason.message, reason.details);
    default:
      return new UnknownErrorException(reason.message, reason.toString());
  }
}

class MessageHandler {
  constructor(sourceName, targetName, comObj) {
    this.sourceName = sourceName;
    this.targetName = targetName;
    this.comObj = comObj;
    this.callbackId = 1;
    this.streamId = 1;
    this.streamSinks = Object.create(null);
    this.streamControllers = Object.create(null);
    this.callbackCapabilities = Object.create(null);
    this.actionHandler = Object.create(null);

    this._onComObjOnMessage = event => {
      const data = event.data;
      if (data.targetName !== this.sourceName) {
        return;
      }
      if (data.stream) {
        this._processStreamMessage(data);
        return;
      }
      if (data.callback) {
        const callbackId = data.callbackId;
        const capability = this.callbackCapabilities[callbackId];
        if (!capability) {
          throw new Error(`Cannot resolve callback ${callbackId}`);
        }
        delete this.callbackCapabilities[callbackId];

        if (data.callback === CallbackKind.DATA) {
          capability.resolve(data.data);
        } else if (data.callback === CallbackKind.ERROR) {
          capability.reject(wrapReason(data.reason));
        } else {
          throw new Error("Unexpected callback case");
        }
        return;
      }
      const action = this.actionHandler[data.action];
      if (!action) {
        throw new Error(`Unknown action from worker: ${data.action}`);
      }
      if (data.callbackId) {
        const cbSourceName = this.sourceName;
        const cbTargetName = data.sourceName;

        new Promise(function (resolve) {
          resolve(action(data.data));
        }).then(
          function (result) {
            comObj.postMessage({
              sourceName: cbSourceName,
              targetName: cbTargetName,
              callback: CallbackKind.DATA,
              callbackId: data.callbackId,
              data: result,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName: cbSourceName,
              targetName: cbTargetName,
              callback: CallbackKind.ERROR,
              callbackId: data.callbackId,
              reason: wrapReason(reason),
            });
          }
        );
        return;
      }
      if (data.streamId) {
        this._createStreamSink(data);
        return;
      }
      action(data.data);
    };
    comObj.addEventListener("message", this._onComObjOnMessage);
  }

  on(actionName, handler) {
    if (
      typeof PDFJSDev === "undefined" ||
      PDFJSDev.test("!PRODUCTION || TESTING")
    ) {
      assert(
        typeof handler === "function",
        'MessageHandler.on: Expected "handler" to be a function.'
      );
    }
    const ah = this.actionHandler;
    if (ah[actionName]) {
      throw new Error(`There is already an actionName called "${actionName}"`);
    }
    ah[actionName] = handler;
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   */
  send(actionName, data, transfers) {
    this.comObj.postMessage(
      {
        sourceName: this.sourceName,
        targetName: this.targetName,
        action: actionName,
        data,
      },
      transfers
    );
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * Expects that the other side will callback with the response.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   * @returns {Promise} Promise to be resolved with response data.
   */
  sendWithPromise(actionName, data, transfers) {
    const callbackId = this.callbackId++;
    const capability = createPromiseCapability();
    this.callbackCapabilities[callbackId] = capability;
    try {
      this.comObj.postMessage(
        {
          sourceName: this.sourceName,
          targetName: this.targetName,
          action: actionName,
          callbackId,
          data,
        },
        transfers
      );
    } catch (ex) {
      capability.reject(ex);
    }
    return capability.promise;
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * Expect that the other side will callback to signal 'start_complete'.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Object} queueingStrategy - Strategy to signal backpressure based on
   *                 internal queue.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   * @returns {ReadableStream} ReadableStream to read data in chunks.
   */
  sendWithStream(actionName, data, queueingStrategy, transfers) {
    const streamId = this.streamId++,
      sourceName = this.sourceName,
      targetName = this.targetName,
      comObj = this.comObj;

    return new ReadableStream(
      {
        start: controller => {
          const startCapability = createPromiseCapability();
          this.streamControllers[streamId] = {
            controller,
            startCall: startCapability,
            pullCall: null,
            cancelCall: null,
            isClosed: false,
          };
          comObj.postMessage(
            {
              sourceName,
              targetName,
              action: actionName,
              streamId,
              data,
              desiredSize: controller.desiredSize,
            },
            transfers
          );
          // Return Promise for Async process, to signal success/failure.
          return startCapability.promise;
        },

        pull: controller => {
          const pullCapability = createPromiseCapability();
          this.streamControllers[streamId].pullCall = pullCapability;
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.PULL,
            streamId,
            desiredSize: controller.desiredSize,
          });
          // Returning Promise will not call "pull"
          // again until current pull is resolved.
          return pullCapability.promise;
        },

        cancel: reason => {
          assert(reason instanceof Error, "cancel must have a valid reason");
          const cancelCapability = createPromiseCapability();
          this.streamControllers[streamId].cancelCall = cancelCapability;
          this.streamControllers[streamId].isClosed = true;
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.CANCEL,
            streamId,
            reason: wrapReason(reason),
          });
          // Return Promise to signal success or failure.
          return cancelCapability.promise;
        },
      },
      queueingStrategy
    );
  }

  /**
   * @private
   */
  _createStreamSink(data) {
    const streamId = data.streamId,
      sourceName = this.sourceName,
      targetName = data.sourceName,
      comObj = this.comObj;
    const self = this,
      action = this.actionHandler[data.action];

    const streamSink = {
      enqueue(chunk, size = 1, transfers) {
        if (this.isCancelled) {
          return;
        }
        const lastDesiredSize = this.desiredSize;
        this.desiredSize -= size;
        // Enqueue decreases the desiredSize property of sink,
        // so when it changes from positive to negative,
        // set ready as unresolved promise.
        if (lastDesiredSize > 0 && this.desiredSize <= 0) {
          this.sinkCapability = createPromiseCapability();
          this.ready = this.sinkCapability.promise;
        }
        comObj.postMessage(
          {
            sourceName,
            targetName,
            stream: StreamKind.ENQUEUE,
            streamId,
            chunk,
          },
          transfers
        );
      },

      close() {
        if (this.isCancelled) {
          return;
        }
        this.isCancelled = true;
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.CLOSE,
          streamId,
        });
        delete self.streamSinks[streamId];
      },

      error(reason) {
        assert(reason instanceof Error, "error must have a valid reason");
        if (this.isCancelled) {
          return;
        }
        this.isCancelled = true;
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.ERROR,
          streamId,
          reason: wrapReason(reason),
        });
      },

      sinkCapability: createPromiseCapability(),
      onPull: null,
      onCancel: null,
      isCancelled: false,
      desiredSize: data.desiredSize,
      ready: null,
    };

    streamSink.sinkCapability.resolve();
    streamSink.ready = streamSink.sinkCapability.promise;
    this.streamSinks[streamId] = streamSink;

    new Promise(function (resolve) {
      resolve(action(data.data, streamSink));
    }).then(
      function () {
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.START_COMPLETE,
          streamId,
          success: true,
        });
      },
      function (reason) {
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.START_COMPLETE,
          streamId,
          reason: wrapReason(reason),
        });
      }
    );
  }

  /**
   * @private
   */
  _processStreamMessage(data) {
    const streamId = data.streamId,
      sourceName = this.sourceName,
      targetName = data.sourceName,
      comObj = this.comObj;
    const streamController = this.streamControllers[streamId],
      streamSink = this.streamSinks[streamId];

    switch (data.stream) {
      case StreamKind.START_COMPLETE:
        if (data.success) {
          streamController.startCall.resolve();
        } else {
          streamController.startCall.reject(wrapReason(data.reason));
        }
        break;
      case StreamKind.PULL_COMPLETE:
        if (data.success) {
          streamController.pullCall.resolve();
        } else {
          streamController.pullCall.reject(wrapReason(data.reason));
        }
        break;
      case StreamKind.PULL:
        // Ignore any pull after close is called.
        if (!streamSink) {
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.PULL_COMPLETE,
            streamId,
            success: true,
          });
          break;
        }
        // Pull increases the desiredSize property of sink, so when it changes
        // from negative to positive, set ready property as resolved promise.
        if (streamSink.desiredSize <= 0 && data.desiredSize > 0) {
          streamSink.sinkCapability.resolve();
        }
        // Reset desiredSize property of sink on every pull.
        streamSink.desiredSize = data.desiredSize;

        new Promise(function (resolve) {
          resolve(streamSink.onPull && streamSink.onPull());
        }).then(
          function () {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.PULL_COMPLETE,
              streamId,
              success: true,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.PULL_COMPLETE,
              streamId,
              reason: wrapReason(reason),
            });
          }
        );
        break;
      case StreamKind.ENQUEUE:
        assert(streamController, "enqueue should have stream controller");
        if (streamController.isClosed) {
          break;
        }
        streamController.controller.enqueue(data.chunk);
        break;
      case StreamKind.CLOSE:
        assert(streamController, "close should have stream controller");
        if (streamController.isClosed) {
          break;
        }
        streamController.isClosed = true;
        streamController.controller.close();
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.ERROR:
        assert(streamController, "error should have stream controller");
        streamController.controller.error(wrapReason(data.reason));
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.CANCEL_COMPLETE:
        if (data.success) {
          streamController.cancelCall.resolve();
        } else {
          streamController.cancelCall.reject(wrapReason(data.reason));
        }
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.CANCEL:
        if (!streamSink) {
          break;
        }

        new Promise(function (resolve) {
          resolve(
            streamSink.onCancel && streamSink.onCancel(wrapReason(data.reason))
          );
        }).then(
          function () {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.CANCEL_COMPLETE,
              streamId,
              success: true,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.CANCEL_COMPLETE,
              streamId,
              reason: wrapReason(reason),
            });
          }
        );
        streamSink.sinkCapability.reject(wrapReason(data.reason));
        streamSink.isCancelled = true;
        delete this.streamSinks[streamId];
        break;
      default:
        throw new Error("Unexpected stream case");
    }
  }

  /**
   * @private
   */
  async _deleteStreamController(streamController, streamId) {
    // Delete the `streamController` only when the start, pull, and cancel
    // capabilities have settled, to prevent `TypeError`s.
    await Promise.allSettled([
      streamController.startCall && streamController.startCall.promise,
      streamController.pullCall && streamController.pullCall.promise,
      streamController.cancelCall && streamController.cancelCall.promise,
    ]);
    delete this.streamControllers[streamId];
  }

  destroy() {
    this.comObj.removeEventListener("message", this._onComObjOnMessage);
  }
}

export { MessageHandler };
<commit_msg>Simplify the `wrapReason` helper function<commit_after>/* Copyright 2018 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  AbortException,
  assert,
  createPromiseCapability,
  MissingPDFException,
  PasswordException,
  UnexpectedResponseException,
  UnknownErrorException,
  unreachable,
} from "./util.js";

const CallbackKind = {
  UNKNOWN: 0,
  DATA: 1,
  ERROR: 2,
};

const StreamKind = {
  UNKNOWN: 0,
  CANCEL: 1,
  CANCEL_COMPLETE: 2,
  CLOSE: 3,
  ENQUEUE: 4,
  ERROR: 5,
  PULL: 6,
  PULL_COMPLETE: 7,
  START_COMPLETE: 8,
};

function wrapReason(reason) {
  if (
    !(
      reason instanceof Error ||
      (typeof reason === "object" && reason !== null)
    )
  ) {
    unreachable(
      'wrapReason: Expected "reason" to be a (possibly cloned) Error.'
    );
  }
  switch (reason.name) {
    case "AbortException":
      return new AbortException(reason.message);
    case "MissingPDFException":
      return new MissingPDFException(reason.message);
    case "PasswordException":
      return new PasswordException(reason.message, reason.code);
    case "UnexpectedResponseException":
      return new UnexpectedResponseException(reason.message, reason.status);
    case "UnknownErrorException":
      return new UnknownErrorException(reason.message, reason.details);
    default:
      return new UnknownErrorException(reason.message, reason.toString());
  }
}

class MessageHandler {
  constructor(sourceName, targetName, comObj) {
    this.sourceName = sourceName;
    this.targetName = targetName;
    this.comObj = comObj;
    this.callbackId = 1;
    this.streamId = 1;
    this.streamSinks = Object.create(null);
    this.streamControllers = Object.create(null);
    this.callbackCapabilities = Object.create(null);
    this.actionHandler = Object.create(null);

    this._onComObjOnMessage = event => {
      const data = event.data;
      if (data.targetName !== this.sourceName) {
        return;
      }
      if (data.stream) {
        this._processStreamMessage(data);
        return;
      }
      if (data.callback) {
        const callbackId = data.callbackId;
        const capability = this.callbackCapabilities[callbackId];
        if (!capability) {
          throw new Error(`Cannot resolve callback ${callbackId}`);
        }
        delete this.callbackCapabilities[callbackId];

        if (data.callback === CallbackKind.DATA) {
          capability.resolve(data.data);
        } else if (data.callback === CallbackKind.ERROR) {
          capability.reject(wrapReason(data.reason));
        } else {
          throw new Error("Unexpected callback case");
        }
        return;
      }
      const action = this.actionHandler[data.action];
      if (!action) {
        throw new Error(`Unknown action from worker: ${data.action}`);
      }
      if (data.callbackId) {
        const cbSourceName = this.sourceName;
        const cbTargetName = data.sourceName;

        new Promise(function (resolve) {
          resolve(action(data.data));
        }).then(
          function (result) {
            comObj.postMessage({
              sourceName: cbSourceName,
              targetName: cbTargetName,
              callback: CallbackKind.DATA,
              callbackId: data.callbackId,
              data: result,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName: cbSourceName,
              targetName: cbTargetName,
              callback: CallbackKind.ERROR,
              callbackId: data.callbackId,
              reason: wrapReason(reason),
            });
          }
        );
        return;
      }
      if (data.streamId) {
        this._createStreamSink(data);
        return;
      }
      action(data.data);
    };
    comObj.addEventListener("message", this._onComObjOnMessage);
  }

  on(actionName, handler) {
    if (
      typeof PDFJSDev === "undefined" ||
      PDFJSDev.test("!PRODUCTION || TESTING")
    ) {
      assert(
        typeof handler === "function",
        'MessageHandler.on: Expected "handler" to be a function.'
      );
    }
    const ah = this.actionHandler;
    if (ah[actionName]) {
      throw new Error(`There is already an actionName called "${actionName}"`);
    }
    ah[actionName] = handler;
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   */
  send(actionName, data, transfers) {
    this.comObj.postMessage(
      {
        sourceName: this.sourceName,
        targetName: this.targetName,
        action: actionName,
        data,
      },
      transfers
    );
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * Expects that the other side will callback with the response.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   * @returns {Promise} Promise to be resolved with response data.
   */
  sendWithPromise(actionName, data, transfers) {
    const callbackId = this.callbackId++;
    const capability = createPromiseCapability();
    this.callbackCapabilities[callbackId] = capability;
    try {
      this.comObj.postMessage(
        {
          sourceName: this.sourceName,
          targetName: this.targetName,
          action: actionName,
          callbackId,
          data,
        },
        transfers
      );
    } catch (ex) {
      capability.reject(ex);
    }
    return capability.promise;
  }

  /**
   * Sends a message to the comObj to invoke the action with the supplied data.
   * Expect that the other side will callback to signal 'start_complete'.
   * @param {string} actionName - Action to call.
   * @param {JSON} data - JSON data to send.
   * @param {Object} queueingStrategy - Strategy to signal backpressure based on
   *                 internal queue.
   * @param {Array} [transfers] - List of transfers/ArrayBuffers.
   * @returns {ReadableStream} ReadableStream to read data in chunks.
   */
  sendWithStream(actionName, data, queueingStrategy, transfers) {
    const streamId = this.streamId++,
      sourceName = this.sourceName,
      targetName = this.targetName,
      comObj = this.comObj;

    return new ReadableStream(
      {
        start: controller => {
          const startCapability = createPromiseCapability();
          this.streamControllers[streamId] = {
            controller,
            startCall: startCapability,
            pullCall: null,
            cancelCall: null,
            isClosed: false,
          };
          comObj.postMessage(
            {
              sourceName,
              targetName,
              action: actionName,
              streamId,
              data,
              desiredSize: controller.desiredSize,
            },
            transfers
          );
          // Return Promise for Async process, to signal success/failure.
          return startCapability.promise;
        },

        pull: controller => {
          const pullCapability = createPromiseCapability();
          this.streamControllers[streamId].pullCall = pullCapability;
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.PULL,
            streamId,
            desiredSize: controller.desiredSize,
          });
          // Returning Promise will not call "pull"
          // again until current pull is resolved.
          return pullCapability.promise;
        },

        cancel: reason => {
          assert(reason instanceof Error, "cancel must have a valid reason");
          const cancelCapability = createPromiseCapability();
          this.streamControllers[streamId].cancelCall = cancelCapability;
          this.streamControllers[streamId].isClosed = true;
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.CANCEL,
            streamId,
            reason: wrapReason(reason),
          });
          // Return Promise to signal success or failure.
          return cancelCapability.promise;
        },
      },
      queueingStrategy
    );
  }

  /**
   * @private
   */
  _createStreamSink(data) {
    const streamId = data.streamId,
      sourceName = this.sourceName,
      targetName = data.sourceName,
      comObj = this.comObj;
    const self = this,
      action = this.actionHandler[data.action];

    const streamSink = {
      enqueue(chunk, size = 1, transfers) {
        if (this.isCancelled) {
          return;
        }
        const lastDesiredSize = this.desiredSize;
        this.desiredSize -= size;
        // Enqueue decreases the desiredSize property of sink,
        // so when it changes from positive to negative,
        // set ready as unresolved promise.
        if (lastDesiredSize > 0 && this.desiredSize <= 0) {
          this.sinkCapability = createPromiseCapability();
          this.ready = this.sinkCapability.promise;
        }
        comObj.postMessage(
          {
            sourceName,
            targetName,
            stream: StreamKind.ENQUEUE,
            streamId,
            chunk,
          },
          transfers
        );
      },

      close() {
        if (this.isCancelled) {
          return;
        }
        this.isCancelled = true;
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.CLOSE,
          streamId,
        });
        delete self.streamSinks[streamId];
      },

      error(reason) {
        assert(reason instanceof Error, "error must have a valid reason");
        if (this.isCancelled) {
          return;
        }
        this.isCancelled = true;
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.ERROR,
          streamId,
          reason: wrapReason(reason),
        });
      },

      sinkCapability: createPromiseCapability(),
      onPull: null,
      onCancel: null,
      isCancelled: false,
      desiredSize: data.desiredSize,
      ready: null,
    };

    streamSink.sinkCapability.resolve();
    streamSink.ready = streamSink.sinkCapability.promise;
    this.streamSinks[streamId] = streamSink;

    new Promise(function (resolve) {
      resolve(action(data.data, streamSink));
    }).then(
      function () {
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.START_COMPLETE,
          streamId,
          success: true,
        });
      },
      function (reason) {
        comObj.postMessage({
          sourceName,
          targetName,
          stream: StreamKind.START_COMPLETE,
          streamId,
          reason: wrapReason(reason),
        });
      }
    );
  }

  /**
   * @private
   */
  _processStreamMessage(data) {
    const streamId = data.streamId,
      sourceName = this.sourceName,
      targetName = data.sourceName,
      comObj = this.comObj;
    const streamController = this.streamControllers[streamId],
      streamSink = this.streamSinks[streamId];

    switch (data.stream) {
      case StreamKind.START_COMPLETE:
        if (data.success) {
          streamController.startCall.resolve();
        } else {
          streamController.startCall.reject(wrapReason(data.reason));
        }
        break;
      case StreamKind.PULL_COMPLETE:
        if (data.success) {
          streamController.pullCall.resolve();
        } else {
          streamController.pullCall.reject(wrapReason(data.reason));
        }
        break;
      case StreamKind.PULL:
        // Ignore any pull after close is called.
        if (!streamSink) {
          comObj.postMessage({
            sourceName,
            targetName,
            stream: StreamKind.PULL_COMPLETE,
            streamId,
            success: true,
          });
          break;
        }
        // Pull increases the desiredSize property of sink, so when it changes
        // from negative to positive, set ready property as resolved promise.
        if (streamSink.desiredSize <= 0 && data.desiredSize > 0) {
          streamSink.sinkCapability.resolve();
        }
        // Reset desiredSize property of sink on every pull.
        streamSink.desiredSize = data.desiredSize;

        new Promise(function (resolve) {
          resolve(streamSink.onPull && streamSink.onPull());
        }).then(
          function () {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.PULL_COMPLETE,
              streamId,
              success: true,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.PULL_COMPLETE,
              streamId,
              reason: wrapReason(reason),
            });
          }
        );
        break;
      case StreamKind.ENQUEUE:
        assert(streamController, "enqueue should have stream controller");
        if (streamController.isClosed) {
          break;
        }
        streamController.controller.enqueue(data.chunk);
        break;
      case StreamKind.CLOSE:
        assert(streamController, "close should have stream controller");
        if (streamController.isClosed) {
          break;
        }
        streamController.isClosed = true;
        streamController.controller.close();
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.ERROR:
        assert(streamController, "error should have stream controller");
        streamController.controller.error(wrapReason(data.reason));
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.CANCEL_COMPLETE:
        if (data.success) {
          streamController.cancelCall.resolve();
        } else {
          streamController.cancelCall.reject(wrapReason(data.reason));
        }
        this._deleteStreamController(streamController, streamId);
        break;
      case StreamKind.CANCEL:
        if (!streamSink) {
          break;
        }

        new Promise(function (resolve) {
          resolve(
            streamSink.onCancel && streamSink.onCancel(wrapReason(data.reason))
          );
        }).then(
          function () {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.CANCEL_COMPLETE,
              streamId,
              success: true,
            });
          },
          function (reason) {
            comObj.postMessage({
              sourceName,
              targetName,
              stream: StreamKind.CANCEL_COMPLETE,
              streamId,
              reason: wrapReason(reason),
            });
          }
        );
        streamSink.sinkCapability.reject(wrapReason(data.reason));
        streamSink.isCancelled = true;
        delete this.streamSinks[streamId];
        break;
      default:
        throw new Error("Unexpected stream case");
    }
  }

  /**
   * @private
   */
  async _deleteStreamController(streamController, streamId) {
    // Delete the `streamController` only when the start, pull, and cancel
    // capabilities have settled, to prevent `TypeError`s.
    await Promise.allSettled([
      streamController.startCall && streamController.startCall.promise,
      streamController.pullCall && streamController.pullCall.promise,
      streamController.cancelCall && streamController.cancelCall.promise,
    ]);
    delete this.streamControllers[streamId];
  }

  destroy() {
    this.comObj.removeEventListener("message", this._onComObjOnMessage);
  }
}

export { MessageHandler };
<|endoftext|>


<commit_before>/**
 * Pops up a dialog to edit the configuration of a single track.
 */
define([
           'dojo/_base/declare',
           'dojo/aspect',
           'dojo/json',
           'dojo/on',
           'dojo/dom-construct',
           'dijit/Dialog',
           'dijit/form/Button'
       ],
       function(
           declare,
           aspect,
           JSON,
           on,
           dom,
           Dialog,
           Button
       ) {

return declare( null, {

    constructor: function( trackConfig ) {
        this.trackConfig = trackConfig;
    },

     _makeActionBar: function( editCallback, cancelCallback ) {
        var actionBar = dom.create(
            'div', {
                className: 'dijitDialogPaneActionBar'
            });

        new Button({ iconClass: 'dijitIconDelete', label: 'Cancel',
                     onClick: dojo.hitch( this, function() {
                                              cancelCallback && cancelCallback();
                                              this.dialog.hide();
                                          })
                   })
            .placeAt( actionBar );
        this.applyButton = new Button({
            iconClass: 'dijitIconEdit',
            label: 'Apply',
            onClick: dojo.hitch( this, function() {
                if( this.newConfig ) {
                    editCallback && editCallback({
                        conf: this.newConfig
                    });
                } else {
                    cancelCallback && cancelCallback();
                }
                this.dialog.hide();
            })
        });
        this.applyButton.placeAt( actionBar );

        return { domNode: actionBar };
    },

    show: function( editCallback, cancelCallback ) {
        var dialog = this.dialog = new Dialog(
            { title: "Edit track configuration", className: 'trackConfigEditor' }
            );

        var content = [
            this._makeEditControls().domNode,
            this._makeActionBar( editCallback, cancelCallback ).domNode
        ];
        dialog.set( 'content', content );
        dialog.show();

        aspect.after( dialog, 'hide', dojo.hitch( this, function() {
                              dialog.destroyRecursive();
                      }));
    },

    _makeEditControls: function() {
        var realChange = dojo.hitch( this, function() {
            this.newConfig = this._parseNewConfig( textArea.value );
        });

        var container = dom.create( 'div', { className: 'editControls'} );

        var confString = this._stringifyConfig( this.trackConfig );
        var textArea = dom.create(
            'textarea',{
                rows: Math.min( (confString||'').match(/\n/g).length+4, 20 ),
                cols: 70,
                value: confString,
                spellcheck: false,
                onchange: realChange
            }, container );
        // watch the input text for changes.  just do it every 700ms
        // because there are many ways that text can get changed (like
        // pasting), not all of which fire the same events.  not using
        // the onchange event, because that doesn't fire until the
        // textarea loses focus.
        var previousText = '';
        var checkFrequency = 700;
        var checkForChange = function() {
            if( textArea.value != previousText ) {
                realChange();
                previousText = textArea.value;
            }
            window.setTimeout( checkForChange, checkFrequency );
        };
        window.setTimeout( checkForChange, checkFrequency );

        var errorArea = dom.create( 'div', { className: 'errors' }, container );
        this.errorReportArea = errorArea;

        return { domNode: container };
    },

    _stringifyConfig: function( config ) {

        // don't let people edit the store configuration, just the
        // track configuration.  make a shallow copy and delete the
        // store conf.  will add back in later.
        var c = dojo.mixin( {}, config ); // shallow copy
        delete c.store;

        // put a style in there if there isn't already one, for convenience
        if( ! c.style ) c.style = {};

        return JSON.stringify( c, undefined, 2 );
    },

    _reportError: function( error ) {
        this.errorReportArea.innerHTML = '
'+error+'
';
        this.applyButton.set('disabled',true);
    },
    _clearErrors: function() {
        dom.empty( this.errorReportArea );
        this.applyButton.set('disabled',false);
    },

    _parseNewConfig: function( conf ) {
        var newconf;
        try {
            newconf = JSON.parse( conf, true );
            this._clearErrors();
        } catch(e) {
            this._reportError( e );
        }
        if( newconf )
            newconf.store = this.trackConfig.store;
        return newconf;
    }

});
});<commit_msg>in the track config editor, make the change watcher only active when the dialog is open<commit_after>/**
 * Pops up a dialog to edit the configuration of a single track.
 */
define([
           'dojo/_base/declare',
           'dojo/aspect',
           'dojo/json',
           'dojo/on',
           'dojo/dom-construct',
           'dijit/Dialog',
           'dijit/form/Button'
       ],
       function(
           declare,
           aspect,
           JSON,
           on,
           dom,
           Dialog,
           Button
       ) {

return declare( null, {

    constructor: function( trackConfig ) {
        this.trackConfig = trackConfig;
    },

     _makeActionBar: function( editCallback, cancelCallback ) {
        var actionBar = dom.create(
            'div', {
                className: 'dijitDialogPaneActionBar'
            });

        new Button({ iconClass: 'dijitIconDelete', label: 'Cancel',
                     onClick: dojo.hitch( this, function() {
                                              cancelCallback && cancelCallback();
                                              this.dialog.hide();
                                          })
                   })
            .placeAt( actionBar );
        this.applyButton = new Button({
            iconClass: 'dijitIconEdit',
            label: 'Apply',
            onClick: dojo.hitch( this, function() {
                if( this.newConfig ) {
                    editCallback && editCallback({
                        conf: this.newConfig
                    });
                } else {
                    cancelCallback && cancelCallback();
                }
                this.dialog.hide();
            })
        });
        this.applyButton.placeAt( actionBar );

        return { domNode: actionBar };
    },

    show: function( editCallback, cancelCallback ) {
        var dialog = this.dialog = new Dialog(
            { title: "Edit track configuration", className: 'trackConfigEditor' }
            );

        var content = [
            this._makeEditControls().domNode,
            this._makeActionBar( editCallback, cancelCallback ).domNode
        ];
        dialog.set( 'content', content );
        dialog.show();

        aspect.after( dialog, 'hide', dojo.hitch( this, function() {
                              dialog.destroyRecursive();
                      }));
    },

    _makeEditControls: function() {
        var realChange = dojo.hitch( this, function() {
            this.newConfig = this._parseNewConfig( textArea.value );
        });

        var container = dom.create( 'div', { className: 'editControls'} );

        var confString = this._stringifyConfig( this.trackConfig );
        var textArea = dom.create(
            'textarea',{
                rows: Math.min( (confString||'').match(/\n/g).length+4, 20 ),
                cols: 70,
                value: confString,
                spellcheck: false,
                onchange: realChange
            }, container );
        // watch the input text for changes.  just do it every 700ms
        // because there are many ways that text can get changed (like
        // pasting), not all of which fire the same events.  not using
        // the onchange event, because that doesn't fire until the
        // textarea loses focus.
        var previousText = '';
        var checkFrequency = 700;
        var that = this;
        var checkForChange = function() {
            if( that.dialog.get('open') ) {
                if( textArea.value != previousText ) {
                    realChange();
                    previousText = textArea.value;
                }
                window.setTimeout( checkForChange, checkFrequency );
            }
        };
        window.setTimeout( checkForChange, checkFrequency );

        var errorArea = dom.create( 'div', { className: 'errors' }, container );
        this.errorReportArea = errorArea;

        return { domNode: container };
    },

    _stringifyConfig: function( config ) {

        // don't let people edit the store configuration, just the
        // track configuration.  make a shallow copy and delete the
        // store conf.  will add back in later.
        var c = dojo.mixin( {}, config ); // shallow copy
        delete c.store;

        // put a style in there if there isn't already one, for convenience
        if( ! c.style ) c.style = {};

        return JSON.stringify( c, undefined, 2 );
    },

    _reportError: function( error ) {
        this.errorReportArea.innerHTML = '
'+error+'
';
        this.applyButton.set('disabled',true);
    },
    _clearErrors: function() {
        dom.empty( this.errorReportArea );
        this.applyButton.set('disabled',false);
    },

    _parseNewConfig: function( conf ) {
        var newconf;
        try {
            newconf = JSON.parse( conf, true );
            this._clearErrors();
        } catch(e) {
            this._reportError( e );
        }
        if( newconf )
            newconf.store = this.trackConfig.store;
        return newconf;
    }

});
});<|endoftext|>