<commit_before>/* jshint ignore: start */
// FIXME UPDATE cherrypick

function format2str (format) {
}

function htmlesc (text) {
    return text;
}

/// range/ids -???  first, all   then...

function rtf2txt (rtf) {
    var txt = rtf.filter(function(o){ return o && o.constructor===String });
    return txt.join('');
}

function meta2attstr (meta) {
    var ret = [], styles={}, classes={}, atts={}, structs={};
    var kv = {':':styles,'_':classes,'=':atts,'s':structs};
    var re_meta = /^(\w[\w_\-]*)([:_=]?)$/, m;
    for(key in meta)
        if (m=re_meta.exec(key))
            kv [m[2]||'s'] [m[1]] = meta[key];

    ret.push(' class="');
    for (var c in classes)
        ret.push(c, '_', classes[c], ' ');
    ret.pop()===' ' && ret.push('"');

    ret.push(' style="');
    for (var s in styles)
        ret.push(s, ':', styles[s], ';');
    ret.pop() === ';' && ret.push('"');

    for (var a in atts)
        ret.push(' ', a, '="', atts[a], '"'); // TODO esc "

    return ret.join('');
};

function rtf2html (origRTF) { // TODO meta_in, indent
    !origRTF && (origRTF='');
    origRTF.constructor===String && (origRTF=[origRTF]);
    var html = [], openTag = null, rtf=origRTF.slice(0).reverse();
    var frg, format={};
    while (frg=rtf.pop()) {
        if (frg.constructor===Object) {
            format = frg;
            continue;
        } else if (frg.constructor!==String)
            throw new Error('format violation: ',frg);
        var head = frg.charAt(0);
        if (head==='\n') {
            // plant open/close // use struct markers
            if (openTag) {
                html.push('</',openTag,'>\n');
                openTag = null;
            }
            // TODO check structf for indent/nesting
            if (format.head)
                openTag = 'h'+format.head;
            else
                openTag = 'p';
            html.push('<',openTag,'>'); // TODO pid
            frg = frg.substr(1);
        }
        //find next struct (lookahead; had \n => check for \t)
        var n = frg.indexOf('\n',1);
        if (n!==-1) { // repair
            rtf.push(frg.substr(n));
            frg = frg.substr(0,n);
        }
        var atts = meta2attstr(format);
        html.push('<span',atts,'>',htmlesc(frg),'</span>');
    }
    openTag && html.push('</',openTag,'>\n');
    return html.join('');
}

function DOMEditor (root) {
}

DOMEditor.prototype.setRTF = function (range, rtf) {
    var html = rtf2html(rtf);
    // staging element
    body.innerHTML = html;
    /*
    var from=range.get('#'), till=range.get('#',from);
    // clean DOM range
    for(var pid=from; pid && pid!==till; pid=???)
        body.removeChild(pid);
    // install
    while ()
        body.insertBefore(newp,till); // TODO batch?
    // recover selection
    // */
};

//DOMEditor.prototype.getRTF = function (from, till, format, rtf) {
function dom2rtf (from, till, format, rtf) {
    rtf = rtf || [];
    format = format || {};
    if (till && from.parentNode!==till.parentNode) throw 'not siblings';
    for(var i=from; i && i!==till; i=i.nextSibling)
        if (i.nodeType===Document.ELEMENT_NODE) {
            var child_format = {};
            for(var key in format)
                child_format[key] = format[key];
            switch (i.nodeName.toUpperCase()) {
                case 'B':  child_format['font-weight:'] = 'bold'; break;
                case 'BR': // rtf.push(struct);
                           rtf.push('\n');
                           break;
                case 'UL': child_format['list'] = 'o'; break;
                case 'LI': rtf.push('\n'); break;
                case 'P':  rtf.push(child_format,'\n'); break;
                case 'H1': child_format['head'] = 1;
                           rtf.push(child_format,'\n');
                           break;
            }
            dom2rtf(i.firstChild,null,child_format,rtf);
        } else if (i.nodeType===Document.TEXT_NODE) {
            rtf.push(format);
            rtf.push(i.nodeValue.replace(/\n/g,'')); // TODO \n spaces
        } else {
        }
    return rtf;
}

function normalizeRTF (rtf) {
    var ret = [], format = {}, prev = undefined;
    for(var i=0; i<rtf.length; i++) {
        var n = rtf[i];
        if (!n)
            continue;
        if (n.constructor===String) {
            if (prev && prev.constructor===String)
                ret.push(ret.pop()+n);
            else
                ret.push(n);
        } else if (n.constructor===Object) {
            var match = true;
            for(var key in n)
                if (n[key]!==format[key]) {
                    match = false;
                    break;
                }
            if (match)
                for(var key in format)
                    if (n[key]!==format[key]) {
                        match = false;
                        break;
                    }
            format = n;
            if (match)
                continue;
            if (prev && prev.constructor===Object)
                ret.pop();
            ret.push(n);
        } else
            continue;
        prev = n;
    }
    return ret;
}

function Weave (chain) {
    this.rtf = []; // TODO this.weave :)
}

Weave.prototype.setRTF = function (range, rtf) {
    // SHORTCUT: equalize
    var preex = this.getRTF(range);
    // diff
    var basic = dmp.diff_main(preexText,givenText);
    // make ops
    // pass to compare formattings
    // (SEE NOTEBOOK 22feb Kiev)
    // just effective formatting on new/remaining parts
    this.text = '';
    // apply
};

Weave.prototype.getRTF = function (range,spec) {
    // find paragraph

    // render according to the mode, base, version
    // cumul formattings

    return this.rtf;
};
/* jshint ignore: end  */
<commit_msg>clean-up<commit_after><|endoftext|>


<commit_before>import {
  DAY_DEBATE,
  DAY_DEBATE_POLL,
  DAY_TRIAL,
  DAY_TRIAL_POLL,
  LANG
}
from './settings/gameSettings'
import Poll from './Poll'
import { sleep } from './utils'
import DayCycleStrings from './strings/dayCycle'
import miscStrings from './strings/misc'
import _ from 'lodash'
import async from 'async'

const str = new DayCycleStrings(LANG)
const misc = miscStrings[LANG]

export default class DayCycle {
  constructor(game) {
    this.game = game
    this.lastNightKills = _.last(game.gameState.cycles)
      .kills
    this.maxTrial = 2
    this.kills = []
    this.state
  }

  // Announce day cycle
  // Reset attributes
  // Announce dead players during previous night
  // Show graveyard and remaining players
  // Check for win
  // If win then end game
  // Else start debate poll
  start() {
    const chan = this.game.getTownRoom()
    this.game.postMessage(chan, str.start('start'))
      .then(() => {
        this.game.resetPoll()
        this.game.resetImmunity()
        this.game.resetProtections()
        this.game.resetRoleBlock()
        this.game.resetFramed()
        this.makeAnnouncements()
          .then(() => this.game.showGraveyard(chan))
          .then(() => this.game.resetCleaned())
          .then(() => this.game.showAlive(chan))
          .then(() => {
            const hasWon = this.game.checkVictory()
            if (hasWon) {
              this.game.resolveVictory(hasWon)
            } else {
              this.game.updateMafiaRoles()
                .then(() => this.game.postMessage(chan, str.start('debate')))
                .then(() => sleep(DAY_DEBATE))
                .then(() => this.startDebatePoll())
            }
          })
      })
  }

  // Check for win
  // Else emit new night cycle
  end() {
    const hasWon = this.game.checkVictory()
    if (hasWon) {
      this.game.resolveVictory(hasWon)
    } else {
      this.game.updateMafiaRoles()
        .then(() =>
          this.game.postMessage(this.game.getTownRoom(), str.end()))
        .then(() => sleep(10))
        .then(() => this.game.gameEmitter.emit('newCycle', 'night'))
    }
  }

  // Players choose their suspects
  startDebatePoll() {
    const chan = this.game.getTownRoom()
    const text = str.startDebate()
    const choices = this.game.getPlayers()
    const poll = new Poll(this.game, chan, text, choices, true)
    poll.start()
    sleep(DAY_DEBATE_POLL)
      .then(() => poll.end())
      .then(() => this.endDebatePoll(poll))
  }

  // Get the most suspected player then put him to trial and mute rest of town.
  // Unmute town after a while then start lynch poll
  // If no vote call end()
  endDebatePoll(poll) {
    const chan = this.game.getTownRoom()
    const resPoll = poll.getMaxVoted()
    if (resPoll.maxVote > 0) {
      // if multiple suspects, take randomly
      let suspect = _.sample(resPoll.targets)
      suspect = _.find(this.game.players, {
        name: suspect
      })
      const text = str.endDebate('trial', { name: suspect.name, time: String(DAY_TRIAL) })
      this.game.postMessage(chan, text)
        .then(() => {
          const muted = _.filter(this.game.getPlayers(), (o) => o.id != suspect.id)
          this.game.mute(muted)
          sleep(DAY_TRIAL)
            .then(() => {
              this.game.unmute(muted)
              this.startTrialPoll(suspect.id)
            })
        })
    } else {
      const text = str.endDebate('noVote')
      this.game.postMessage(chan, text)
        .then(() => sleep(5))
        .then(() => this.end())
    }
  }

  // Yes/No poll to lynch player
  startTrialPoll(playerId) {
    const chan = this.game.getTownRoom()
    const text = str.startTrial()
    const choices = [
      {
        name: misc.yes
      },
      {
        name: misc.no
      }]
    const poll = new Poll(this.game, chan, text, choices, true)
    poll.start()
    sleep(DAY_TRIAL_POLL)
      .then(() => poll.end())
      .then(() => this.endTrialPoll(poll, playerId))
  }

  // If equality in yes/no then take random
  // If yes then lynch player
  // Else restart a poll to find a new suspect
  // If no vote then call end()
  endTrialPoll(poll, playerId) {
    const chan = this.game.getTownRoom()
    const resPoll = poll.getMaxVoted()
    const suspect = _.find(this.game.getPlayers(), { id: playerId })
    const result = _.sample(resPoll.targets)
    let text = ''

    if (resPoll.maxVote > 0) {
      if (resPoll.targets.length > 1) {
        text = str.endTrial('draw')
        this.game.postMessage(chan, text)
          .then(() => sleep(3))
          .then(() => {
            if (Math.random() > 0.5) {
              this.game.postMessage(chan, str.endTrial('die'))
                .then(() => {
                  this.kills.push({ type: 'kill', player: 'Town', target: suspect.name, killType: 'lynch' })
                  this.game.newVictim(suspect, 'lynch')
                    .then(() => this.end())
                })
            } else {
              text = str.endTrial('live')
              this.game.postMessage(chan, text)
                .then(() => this.restartDebate())
            }
          })
      } else if (result == misc.yes) {
        this.game.postMessage(chan, str.endTrial('lynch', suspect.name))
          .then(() => {
            this.kills.push({ type: 'kill', player: 'Town', target: suspect.name, killType: 'lynch' })
            this.game.newVictim(suspect, 'lynch')
              .then(() => this.end())
          })

      } else {
        text = str.endTrial.innocent + ' ' + suspect.name + ' :innocent:'
        this.game.postMessage(chan, text)
          .then(() => this.restartDebate())
      }
    } else {
      text = str.endTrial('noVote')
      this.game.postMessage(chan, text)
        .then(() => this.end())

    }
  }

  // If it's possible, restart a poll to find a suspect
  restartDebate() {
    const chan = this.game.getTownRoom()
    if (--this.maxTrial > 0) {
      this.game.postMessage(chan, str.restartDebate(true))
        .then(() => this.startDebatePoll())
    } else {
      this.game.postMessage(chan, str.restartDebate(false))
        .then(() => this.end())
    }
  }

  // Start of day announcement's function
  // Show all players dead during the previous night
  makeAnnouncements() {
    return new Promise((resolve, reject) => {
      const nKills = this.lastNightKills.length < 14 ? this.lastNightKills.length : 14
      const chan = this.game.getTownRoom()
      this.game.postMessage(chan, str.announcements(nKills))
        .then(() => {
          async.forEachSeries(this.lastNightKills, (event, callback) => {
            const victim = _.find(this.game.players, {
              name: event.target
            })
            this.game.newVictim(victim, event.killType)
              .then(() => sleep(2))
              .then(() => callback())
          }, () => resolve(true))
        })
    })
  }
}
<commit_msg>feat: add little delay before announcing victims<commit_after>import {
  DAY_DEBATE,
  DAY_DEBATE_POLL,
  DAY_TRIAL,
  DAY_TRIAL_POLL,
  LANG
}
from './settings/gameSettings'
import Poll from './Poll'
import { sleep } from './utils'
import DayCycleStrings from './strings/dayCycle'
import miscStrings from './strings/misc'
import _ from 'lodash'
import async from 'async'

const str = new DayCycleStrings(LANG)
const misc = miscStrings[LANG]

export default class DayCycle {
  constructor(game) {
    this.game = game
    this.lastNightKills = _.last(game.gameState.cycles)
      .kills
    this.maxTrial = 2
    this.kills = []
    this.state
  }

  // Announce day cycle
  // Reset attributes
  // Announce dead players during previous night
  // Show graveyard and remaining players
  // Check for win
  // If win then end game
  // Else start debate poll
  start() {
    const chan = this.game.getTownRoom()
    this.game.postMessage(chan, str.start('start'))
      .then(() => {
        this.game.resetPoll()
        this.game.resetImmunity()
        this.game.resetProtections()
        this.game.resetRoleBlock()
        this.game.resetFramed()
        this.makeAnnouncements()
          .then(() => this.game.showGraveyard(chan))
          .then(() => this.game.resetCleaned())
          .then(() => this.game.showAlive(chan))
          .then(() => {
            const hasWon = this.game.checkVictory()
            if (hasWon) {
              this.game.resolveVictory(hasWon)
            } else {
              this.game.updateMafiaRoles()
                .then(() => this.game.postMessage(chan, str.start('debate')))
                .then(() => sleep(DAY_DEBATE))
                .then(() => this.startDebatePoll())
            }
          })
      })
  }

  // Check for win
  // Else emit new night cycle
  end() {
    const hasWon = this.game.checkVictory()
    if (hasWon) {
      this.game.resolveVictory(hasWon)
    } else {
      this.game.updateMafiaRoles()
        .then(() =>
          this.game.postMessage(this.game.getTownRoom(), str.end()))
        .then(() => sleep(10))
        .then(() => this.game.gameEmitter.emit('newCycle', 'night'))
    }
  }

  // Players choose their suspects
  startDebatePoll() {
    const chan = this.game.getTownRoom()
    const text = str.startDebate()
    const choices = this.game.getPlayers()
    const poll = new Poll(this.game, chan, text, choices, true)
    poll.start()
    sleep(DAY_DEBATE_POLL)
      .then(() => poll.end())
      .then(() => this.endDebatePoll(poll))
  }

  // Get the most suspected player then put him to trial and mute rest of town.
  // Unmute town after a while then start lynch poll
  // If no vote call end()
  endDebatePoll(poll) {
    const chan = this.game.getTownRoom()
    const resPoll = poll.getMaxVoted()
    if (resPoll.maxVote > 0) {
      // if multiple suspects, take randomly
      let suspect = _.sample(resPoll.targets)
      suspect = _.find(this.game.players, {
        name: suspect
      })
      const text = str.endDebate('trial', { name: suspect.name, time: String(DAY_TRIAL) })
      this.game.postMessage(chan, text)
        .then(() => {
          const muted = _.filter(this.game.getPlayers(), (o) => o.id != suspect.id)
          this.game.mute(muted)
          sleep(DAY_TRIAL)
            .then(() => {
              this.game.unmute(muted)
              this.startTrialPoll(suspect.id)
            })
        })
    } else {
      const text = str.endDebate('noVote')
      this.game.postMessage(chan, text)
        .then(() => sleep(5))
        .then(() => this.end())
    }
  }

  // Yes/No poll to lynch player
  startTrialPoll(playerId) {
    const chan = this.game.getTownRoom()
    const text = str.startTrial()
    const choices = [
      {
        name: misc.yes
      },
      {
        name: misc.no
      }]
    const poll = new Poll(this.game, chan, text, choices, true)
    poll.start()
    sleep(DAY_TRIAL_POLL)
      .then(() => poll.end())
      .then(() => this.endTrialPoll(poll, playerId))
  }

  // If equality in yes/no then take random
  // If yes then lynch player
  // Else restart a poll to find a new suspect
  // If no vote then call end()
  endTrialPoll(poll, playerId) {
    const chan = this.game.getTownRoom()
    const resPoll = poll.getMaxVoted()
    const suspect = _.find(this.game.getPlayers(), { id: playerId })
    const result = _.sample(resPoll.targets)
    let text = ''

    if (resPoll.maxVote > 0) {
      if (resPoll.targets.length > 1) {
        text = str.endTrial('draw')
        this.game.postMessage(chan, text)
          .then(() => sleep(3))
          .then(() => {
            if (Math.random() > 0.5) {
              this.game.postMessage(chan, str.endTrial('die'))
                .then(() => {
                  this.kills.push({ type: 'kill', player: 'Town', target: suspect.name, killType: 'lynch' })
                  this.game.newVictim(suspect, 'lynch')
                    .then(() => this.end())
                })
            } else {
              text = str.endTrial('live')
              this.game.postMessage(chan, text)
                .then(() => this.restartDebate())
            }
          })
      } else if (result == misc.yes) {
        this.game.postMessage(chan, str.endTrial('lynch', suspect.name))
          .then(() => {
            this.kills.push({ type: 'kill', player: 'Town', target: suspect.name, killType: 'lynch' })
            this.game.newVictim(suspect, 'lynch')
              .then(() => this.end())
          })

      } else {
        text = str.endTrial.innocent + ' ' + suspect.name + ' :innocent:'
        this.game.postMessage(chan, text)
          .then(() => this.restartDebate())
      }
    } else {
      text = str.endTrial('noVote')
      this.game.postMessage(chan, text)
        .then(() => this.end())

    }
  }

  // If it's possible, restart a poll to find a suspect
  restartDebate() {
    const chan = this.game.getTownRoom()
    if (--this.maxTrial > 0) {
      this.game.postMessage(chan, str.restartDebate(true))
        .then(() => this.startDebatePoll())
    } else {
      this.game.postMessage(chan, str.restartDebate(false))
        .then(() => this.end())
    }
  }

  // Start of day announcement's function
  // Show all players dead during the previous night
  makeAnnouncements() {
    return new Promise((resolve, reject) => {
      const nKills = this.lastNightKills.length < 14 ? this.lastNightKills.length : 14
      const chan = this.game.getTownRoom()
      this.game.postMessage(chan, str.announcements(nKills))
        .then(() => sleep(2))
        .then(() => {
          async.forEachSeries(this.lastNightKills, (event, callback) => {
            const victim = _.find(this.game.players, {
              name: event.target
            })
            this.game.newVictim(victim, event.killType)
              .then(() => sleep(2))
              .then(() => callback())
          }, () => resolve(true))
        })
    })
  }
}
<|endoftext|>


<commit_before>import cookie from 'react-cookie'
import fetch from 'isomorphic-unfetch'

let baseUrl

if (!process.browser) {
  baseUrl =
    process.env.NODE_ENV === 'development'
      ? 'https://api.linklet.ml/api'
      : 'http://172.17.0.8:5000/api'
} else {
  baseUrl = 'https://api.linklet.ml/api'
}

const getAll = ({ page = 1, search, myLinks, req, bookmarks, sort } = {}) => {
  let loginToken = cookie.load('loginToken')
  if (!loginToken) {
    loginToken = req && req.cookies && req.cookies.loginToken
  }
  console.log('loginToken', loginToken)
  console.log('myLinks', myLinks)
  let url
  if (myLinks) {
    url = search
      ? `${baseUrl}/links/me/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/links/me/all/?page=${page}&sort=${sort}`
  } else if (bookmarks) {
    url = search
      ? `${baseUrl}/bookmarks/me/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/bookmarks/me/all/?page=${page}&sort=${sort}`
  } else {
    url = search
      ? `${baseUrl}/links/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/links/all/?page=${page}&sort=${sort}`
  }
  console.log(url)
  return fetch(url, {
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const getByFilter = (
  { start, end, page = 1, search, myLinks, req, bookmarks, sort } = {}
) => {
  let loginToken = cookie.load('loginToken')
  if (!loginToken) {
    loginToken = req && req.cookies && req.cookies.loginToken
  }
  let url
  if (myLinks) {
    url =
      start && end && search
        ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/links/me/filter/?page=${page}&sort=${sort}`
  } else if (bookmarks) {
    url =
      start && end && search
        ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/bookmarks/me/filter/?page=${page}&sort=${sort}`
  } else {
    url =
      start && end && search
        ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/links/filter/?page=${page}&sort=${sort}`
  }

  return fetch(url, {
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const getMetaData = link => {
  const url = `${baseUrl}/metadata?url=${link}`
  return fetch(url).then(r => r.json()).then(res => ({ data: res }))
}

const saveLink = data => {
  const loginToken = cookie.load('loginToken')
  const url = `${baseUrl}/links`
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(data),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const incrementView = id => {
  const loginToken = cookie.load('loginToken')
  console.log(loginToken)
  const url = `${baseUrl}/links/${id}/views`
  return fetch(url, {
    method: 'PATCH',
    body: JSON.stringify({}),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const likeLink = id => {
  const loginToken = cookie.load('loginToken')
  console.log(loginToken)
  const url = `${baseUrl}/links/${id}/bookmark`
  return fetch(url, {
    method: 'PATCH',
    body: JSON.stringify({}),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

export default {
  getAll,
  getByFilter,
  getMetaData,
  saveLink,
  baseUrl,
  incrementView,
  likeLink
}
<commit_msg>implemenet cache for get requests<commit_after>import cookie from 'react-cookie'
import fetch from 'isomorphic-unfetch'

let baseUrl

if (!process.browser) {
  baseUrl =
    process.env.NODE_ENV === 'development'
      ? 'https://api.linklet.ml/api'
      : 'http://172.17.0.2:5000/api'
} else {
  baseUrl = 'https://api.linklet.ml/api'
}

const getAll = ({ page = 1, search, myLinks, req, bookmarks, sort } = {}) => {
  let loginToken = cookie.load('loginToken')
  if (!loginToken) {
    loginToken = req && req.cookies && req.cookies.loginToken
  }
  console.log('loginToken', loginToken)
  console.log('myLinks', myLinks)
  let url
  if (myLinks) {
    url = search
      ? `${baseUrl}/links/me/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/links/me/all/?page=${page}&sort=${sort}`
  } else if (bookmarks) {
    url = search
      ? `${baseUrl}/bookmarks/me/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/bookmarks/me/all/?page=${page}&sort=${sort}`
  } else {
    url = search
      ? `${baseUrl}/links/all/?page=${page}&search=${search}&sort=${sort}`
      : `${baseUrl}/links/all/?page=${page}&sort=${sort}`
  }
  console.log(url)
  return fetch(url, {
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const getByFilter = (
  { start, end, page = 1, search, myLinks, req, bookmarks, sort } = {}
) => {
  let loginToken = cookie.load('loginToken')
  if (!loginToken) {
    loginToken = req && req.cookies && req.cookies.loginToken
  }
  let url
  if (myLinks) {
    url =
      start && end && search
        ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/links/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/links/me/filter/?page=${page}&sort=${sort}`
  } else if (bookmarks) {
    url =
      start && end && search
        ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/bookmarks/me/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/bookmarks/me/filter/?page=${page}&sort=${sort}`
  } else {
    url =
      start && end && search
        ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&search=${search}&sort=${sort}`
        : start && end
          ? `${baseUrl}/links/filter/?page=${page}&start=${start}&end=${end}&sort=${sort}`
          : `${baseUrl}/links/filter/?page=${page}&sort=${sort}`
  }

  return fetch(url, {
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const getMetaData = link => {
  const url = `${baseUrl}/metadata?url=${link}`
  return fetch(url).then(r => r.json()).then(res => ({ data: res }))
}

const saveLink = data => {
  const loginToken = cookie.load('loginToken')
  const url = `${baseUrl}/links`
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(data),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const incrementView = id => {
  const loginToken = cookie.load('loginToken')
  console.log(loginToken)
  const url = `${baseUrl}/links/${id}/views`
  return fetch(url, {
    method: 'PATCH',
    body: JSON.stringify({}),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

const likeLink = id => {
  const loginToken = cookie.load('loginToken')
  console.log(loginToken)
  const url = `${baseUrl}/links/${id}/bookmark`
  return fetch(url, {
    method: 'PATCH',
    body: JSON.stringify({}),
    headers: {
      'x-auth': loginToken || ''
    }
  })
    .then(r => r.json())
    .then(res => ({ data: res }))
}

export default {
  getAll,
  getByFilter,
  getMetaData,
  saveLink,
  baseUrl,
  incrementView,
  likeLink
}
<|endoftext|>


<commit_before>var loki = require('lokijs'),
q = require('q'),
fs = require('fs'),
_ = require('lodash'),
common = require('@screeps/common'),
config = common.configManager.config;

/**
 * @type Loki
 */
var db;

Object.assign(config.storage, {
    dbOptions: {autosave: true, autosaveInterval: 10000},
    getDb() {
        try {
            fs.statSync(process.env.DB_PATH);
        }
        catch (e) {
            fs.writeFileSync(process.env.DB_PATH, '');
        }
        return new loki(process.env.DB_PATH, config.storage.dbOptions);
    },
    loadDb() {
        db = config.storage.getDb();
        return q.ninvoke(db, 'loadDatabase', {});
    }
});

function updateDocument(doc, update) {
    if (update.$set) {
        _.extend(doc, update.$set);
    }
    if (update.$merge) {
        _.merge(doc, update.$merge);
    }
    if (update.$inc) {
        _.forEach(update.$inc, (val, key) => doc[key] = (doc[key] || 0) + val);
    }
    if (update.$unset) {
        for (var j in update.$unset) {
            delete doc[j];
        }
    }
}

setInterval(function envCleanExpired() {
    var env = db.getCollection('env');
    var values = env.get(1);
    var expiration = env.get(2);
    var dirty = false;
    if (expiration) {
        for (var name in expiration.data) {
            if (Date.now() > expiration.data[name]) {
                dirty = true;
                if (values.data[name]) {
                    delete values.data[name];
                }
                delete expiration.data[name];
            }
        }
    }
    if (dirty) {
        env.update(values);
        env.update(expiration);
    }
}, 10000);

function getRandomString() {
    for (var val = Math.floor(Math.random() * 0x10000).toString(16); val.length < 4; val = '0' + val);
    return val;
}

function genId(obj) {
    var id = getRandomString() + Date.now().toString(16).slice(4) + getRandomString();
    if (obj && !obj._id) {
        obj._id = id;
    }
    return id;
}

function getOrAddCollection(collectionName) {
    var collection = db.getCollection(collectionName);
    if (!collection) {
        collection = db.addCollection(collectionName);
    }
    collection.ensureUniqueIndex('_id');
    switch(collectionName) {
        case 'rooms.objects': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.intents': {
            collection.ensureIndex('room');
            break;
        }
        case 'users': {
            collection.ensureIndex('username');
            break;
        }
        case 'rooms.flags': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.terrain': {
            collection.ensureIndex('room');
            break;
        }
        case 'transactions': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.code': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.console': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.money': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.notifications': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.resources': {
            collection.ensureIndex('user');
            break;
        }
    }
    return collection;
}

function recursReplaceNeNull(val) {
    if(!_.isObject(val)) {
        return;
    }

    for(var i in val) {
        if (_.isEqual(val[i], {$ne: null}) && !val.$and) {
            val.$and = [{[i]: {$ne: null}}, {[i]: {$ne: undefined}}];
            delete val[i];
        }
        if (_.isEqual(val[i], {$eq: null}) && !val.$or) {
            val.$or = [{[i]: {$eq: null}}, {[i]: {$eq: undefined}}];
            delete val[i];
        }
        recursReplaceNeNull(val[i]);
    }
}

module.exports = {

    dbRequest(collectionName, method, argsArray, cb) {
        try {
            var collection = getOrAddCollection(collectionName);
            if (method == 'insert') {
                if (_.isArray(argsArray[0])) {
                    argsArray[0].forEach(genId);
                }
                else {
                    genId(argsArray[0]);
                }
            }

            if(method == 'find' || method == 'findOne' || method == 'count' || method == 'removeWhere') {
                recursReplaceNeNull(argsArray[0]);
            }

            var result = collection[method].apply(collection, argsArray);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbUpdate(collectionName, query, update, params, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var result = [];
            if (Object.keys(query).length == 1 && query._id && _.isString(query._id)) {
                var found = collection.by('_id', query._id);
                if(found) {
                    result = [found];
                }
            }
            else {
                result = collection.find(query);
            }
            if (result.length) {
                result.forEach(doc => {
                    updateDocument(doc, update);
                    collection.update(doc);
                });
                cb(null, {modified: result.length});
            }
            else if (params && params.upsert) {
                var item = {};
                if (query.$and) {
                    query.$and.forEach(i => _.extend(item, i));
                }
                else {
                    _.extend(item, query);
                }
                updateDocument(item, update);
                genId(item);
                collection.insert(item);
                cb(null, {inserted: 1});
            }
            else {
                cb(null, {});
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbBulk(collectionName, bulk, cb) {
        try {
            var collection = getOrAddCollection(collectionName), result;
            bulk.forEach(i => {
                switch (i.op) {
                    case 'update': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            updateDocument(result, {$set: i.$set});
                            collection.update(result);
                        }
                        break;
                    }
                    case 'insert': {
                        genId(i.data);
                        collection.insert(i.data);
                        break;
                    }
                    case 'remove': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            collection.remove(result);
                        }
                        break;
                    }
                    case 'inc': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            result[i.key] += i.amount;
                            collection.update(result);
                        }
                        break;
                    }
                }
            });
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbFindEx(collectionName, query, opts, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var chain = collection.chain().find(query);
            if (opts.sort) {
                for (var field in opts.sort) {
                    chain = chain.simplesort(field, opts.sort[field] == -1);
                }
            }
            if (opts.offset) {
                chain = chain.offset(opts.offset);
            }
            if (opts.limit) {
                chain = chain.limit(opts.limit);
            }
            cb(null, chain.data());
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvGet(name, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            cb(null, item.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvMget(names, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            var result = names.map(name => item.data[name]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSet(name, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: value}};
                env.insert(values);
            }
            cb && cb(null, value);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvExpire(name, seconds, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (expiration) {
                expiration.data[name] = Date.now() + seconds * 1000;
                env.update(expiration);
            }
            else {
                expiration = {data: {[name]: Date.now() + seconds * 1000}};
                env.insert(expiration);
            }
            cb && cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSetex(name, seconds, value, cb) {
        try {
            module.exports.dbEnvSet(name, value);
            module.exports.dbEnvExpire(name, seconds);
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvTtl(name, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (!expiration || !expiration.data[name] || expiration.data[name] < Date.now()) {
                cb(null, -1);
                return;
            }
            cb(null, (expiration.data[name] - Date.now()) / 1000);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvDel(name, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data[name]) {
                delete values.data[name];
                cb(null, 1);
            }
            else {
                cb(null, 0);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHget(name, field, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data && values.data[name]) {
                cb(null, values.data[name][field]);
            }
            else {
                cb(null);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHset(name, field, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                values.data[name][field] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: {[field]: value}}};
                env.insert(values);
            }
            cb(null, values.data[name][field]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmget(name, fields, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1) || {data: {}};
            values.data[name] = values.data[name] || {};
            var result = fields.map(i => values.data[name][i]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmset(name, data, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                _.extend(values.data[name], data);
                env.update(values);
            }
            else {
                values = {data: {[name]: data}};
                env.insert(values);
            }
            cb(null, values.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbResetAllData(cb) {
        try {
            db.loadJSON(JSON.stringify(require('../db.original')));
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    }
};<commit_msg>BREAKING CHANGE: removed `inc` bulk command, and added `$addToSet` and `$pull` update operators<commit_after>var loki = require('lokijs'),
q = require('q'),
fs = require('fs'),
_ = require('lodash'),
common = require('@screeps/common'),
config = common.configManager.config;

/**
 * @type Loki
 */
var db;

Object.assign(config.storage, {
    dbOptions: {autosave: true, autosaveInterval: 10000},
    getDb() {
        try {
            fs.statSync(process.env.DB_PATH);
        }
        catch (e) {
            fs.writeFileSync(process.env.DB_PATH, '');
        }
        return new loki(process.env.DB_PATH, config.storage.dbOptions);
    },
    loadDb() {
        db = config.storage.getDb();
        return q.ninvoke(db, 'loadDatabase', {});
    }
});

function updateDocument(doc, update) {
    if (update.$set) {
        _.extend(doc, update.$set);
    }
    if (update.$merge) {
        _.merge(doc, update.$merge);
    }
    if (update.$inc) {
        _.forEach(update.$inc, (val, key) => doc[key] = (doc[key] || 0) + val);
    }
    if (update.$unset) {
        for (var j in update.$unset) {
            delete doc[j];
        }
    }
    if(update.$addToSet) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                doc[i] = [];
            }
            if(doc[i].indexOf(update.$addToSet[i]) == -1) {
                doc[i].push(update.$addToSet[i]);
            }
        }
    }
    if(update.$pull) {
        for(let i in update.$addToSet) {
            if(!doc[i]) {
                continue;
            }
            var idx = doc[i].indexOf(update.$pull[i]);
            if(idx != -1) {
                doc[i].splice(idx, 1);
            }
        }
    }
}

setInterval(function envCleanExpired() {
    var env = db.getCollection('env');
    var values = env.get(1);
    var expiration = env.get(2);
    var dirty = false;
    if (expiration) {
        for (var name in expiration.data) {
            if (Date.now() > expiration.data[name]) {
                dirty = true;
                if (values.data[name]) {
                    delete values.data[name];
                }
                delete expiration.data[name];
            }
        }
    }
    if (dirty) {
        env.update(values);
        env.update(expiration);
    }
}, 10000);

function getRandomString() {
    for (var val = Math.floor(Math.random() * 0x10000).toString(16); val.length < 4; val = '0' + val);
    return val;
}

function genId(obj) {
    var id = getRandomString() + Date.now().toString(16).slice(4) + getRandomString();
    if (obj && !obj._id) {
        obj._id = id;
    }
    return id;
}

function getOrAddCollection(collectionName) {
    var collection = db.getCollection(collectionName);
    if (!collection) {
        collection = db.addCollection(collectionName);
    }
    collection.ensureUniqueIndex('_id');
    switch(collectionName) {
        case 'rooms.objects': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.intents': {
            collection.ensureIndex('room');
            break;
        }
        case 'users': {
            collection.ensureIndex('username');
            break;
        }
        case 'rooms.flags': {
            collection.ensureIndex('room');
            collection.ensureIndex('user');
            break;
        }
        case 'rooms.terrain': {
            collection.ensureIndex('room');
            break;
        }
        case 'transactions': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.code': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.console': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.money': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.notifications': {
            collection.ensureIndex('user');
            break;
        }
        case 'users.resources': {
            collection.ensureIndex('user');
            break;
        }
    }
    return collection;
}

function recursReplaceNeNull(val) {
    if(!_.isObject(val)) {
        return;
    }

    for(var i in val) {
        if (_.isEqual(val[i], {$ne: null}) && !val.$and) {
            val.$and = [{[i]: {$ne: null}}, {[i]: {$ne: undefined}}];
            delete val[i];
        }
        if (_.isEqual(val[i], {$eq: null}) && !val.$or) {
            val.$or = [{[i]: {$eq: null}}, {[i]: {$eq: undefined}}];
            delete val[i];
        }
        recursReplaceNeNull(val[i]);
    }
}

module.exports = {

    dbRequest(collectionName, method, argsArray, cb) {
        try {
            var collection = getOrAddCollection(collectionName);
            if (method == 'insert') {
                if (_.isArray(argsArray[0])) {
                    argsArray[0].forEach(genId);
                }
                else {
                    genId(argsArray[0]);
                }
            }

            if(method == 'find' || method == 'findOne' || method == 'count' || method == 'removeWhere') {
                recursReplaceNeNull(argsArray[0]);
            }

            var result = collection[method].apply(collection, argsArray);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbUpdate(collectionName, query, update, params, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var result = [];
            if (Object.keys(query).length == 1 && query._id && _.isString(query._id)) {
                var found = collection.by('_id', query._id);
                if(found) {
                    result = [found];
                }
            }
            else {
                result = collection.find(query);
            }
            if (result.length) {
                result.forEach(doc => {
                    updateDocument(doc, update);
                    collection.update(doc);
                });
                cb(null, {modified: result.length});
            }
            else if (params && params.upsert) {
                var item = {};
                if (query.$and) {
                    query.$and.forEach(i => _.extend(item, i));
                }
                else {
                    _.extend(item, query);
                }
                updateDocument(item, update);
                genId(item);
                collection.insert(item);
                cb(null, {inserted: 1});
            }
            else {
                cb(null, {});
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbBulk(collectionName, bulk, cb) {
        try {
            var collection = getOrAddCollection(collectionName), result;
            bulk.forEach(i => {
                switch (i.op) {
                    case 'update': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            updateDocument(result, i.update);
                            collection.update(result);
                        }
                        break;
                    }
                    case 'insert': {
                        genId(i.data);
                        collection.insert(i.data);
                        break;
                    }
                    case 'remove': {
                        result = collection.by('_id', i.id);
                        if (result) {
                            collection.remove(result);
                        }
                        break;
                    }
                }
            });
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbFindEx(collectionName, query, opts, cb) {
        try {
            recursReplaceNeNull(query);
            var collection = getOrAddCollection(collectionName);
            var chain = collection.chain().find(query);
            if (opts.sort) {
                for (var field in opts.sort) {
                    chain = chain.simplesort(field, opts.sort[field] == -1);
                }
            }
            if (opts.offset) {
                chain = chain.offset(opts.offset);
            }
            if (opts.limit) {
                chain = chain.limit(opts.limit);
            }
            cb(null, chain.data());
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvGet(name, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            cb(null, item.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvMget(names, cb) {
        try {
            var item = db.getCollection('env').get(1) || {data: {}};
            var result = names.map(name => item.data[name]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSet(name, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: value}};
                env.insert(values);
            }
            cb && cb(null, value);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvExpire(name, seconds, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (expiration) {
                expiration.data[name] = Date.now() + seconds * 1000;
                env.update(expiration);
            }
            else {
                expiration = {data: {[name]: Date.now() + seconds * 1000}};
                env.insert(expiration);
            }
            cb && cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvSetex(name, seconds, value, cb) {
        try {
            module.exports.dbEnvSet(name, value);
            module.exports.dbEnvExpire(name, seconds);
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvTtl(name, cb) {
        try {
            var env = db.getCollection('env');
            var expiration = env.get(2);
            if (!expiration || !expiration.data[name] || expiration.data[name] < Date.now()) {
                cb(null, -1);
                return;
            }
            cb(null, (expiration.data[name] - Date.now()) / 1000);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvDel(name, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data[name]) {
                delete values.data[name];
                cb(null, 1);
            }
            else {
                cb(null, 0);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHget(name, field, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values && values.data && values.data[name]) {
                cb(null, values.data[name][field]);
            }
            else {
                cb(null);
            }
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHset(name, field, value, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                values.data[name][field] = value;
                env.update(values);
            }
            else {
                values = {data: {[name]: {[field]: value}}};
                env.insert(values);
            }
            cb(null, values.data[name][field]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmget(name, fields, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1) || {data: {}};
            values.data[name] = values.data[name] || {};
            var result = fields.map(i => values.data[name][i]);
            cb(null, result);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbEnvHmset(name, data, cb) {
        try {
            var env = db.getCollection('env');
            var values = env.get(1);
            if (values) {
                values.data[name] = values.data[name] || {};
                _.extend(values.data[name], data);
                env.update(values);
            }
            else {
                values = {data: {[name]: data}};
                env.insert(values);
            }
            cb(null, values.data[name]);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    },

    dbResetAllData(cb) {
        try {
            db.loadJSON(JSON.stringify(require('../db.original')));
            cb(null);
        }
        catch (e) {
            cb(e.message);
            console.error(e);
        }
    }
};<|endoftext|>


<commit_before>var Buffer = require('buffer').Buffer;
var mysql = require('mysql');
var Q = require('q');
var zephyr = require('zephyr');

var conf = require('./config.js');

var MAX_MESSAGES_RETURNED = 100;

// Per vasilvv, use VARBINARY everywhere instead of VARCHAR. MySQL
// collations are sad.

// TODO: We probably can get away with significantly less isolated
// transactions here if it becomes a problem.

var schemas = [
  'CREATE TABLE users (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    'username VARBINARY(255) UNIQUE NOT NULL' +
    ') ENGINE=InnoDB;',

  'CREATE TABLE subs (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    'user_id BIGINT NOT NULL,' +
    'FOREIGN KEY user_fkey (user_id) REFERENCES users(id),' +
    // Preserve the original versions of the strings for display.
    'class VARBINARY(255) NOT NULL,' +
    'instance VARBINARY(255) NULL,' +
    'recipient VARBINARY(255) NULL,' +
    // Downcased versions for querying.
    'class_key VARBINARY(255) NOT NULL,' +
    'instance_key VARBINARY(255) NULL,' +
    'UNIQUE query_triple (user_id, class_key, instance_key, recipient)' +
    ') ENGINE=InnoDB;',

  'CREATE TABLE messages (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    // Message fields. Store both downcased and display versions of
    // class and instance.
    'class VARBINARY(255) NOT NULL,' +
    'instance VARBINARY(255) NOT NULL,' +
    'class_key VARBINARY(255) NOT NULL,' +
    'instance_key VARBINARY(255) NOT NULL,' +
    // Stores the number of milliseconds past the Unix epoch,
    // i.e. what Date.prototype.getTime spits out. Technically zephyr
    // gives you microsecond resolution, but I don't think V8 gives
    // you more than millisecond resolution anyway.
    'time BIGINT NOT NULL,' +
    // Distinguish time in the message from the time we received it;
    // if the time is bogus, we can display in the UI. Also we should
    // use our time when jumping the cursor to a date. At least it can
    // be sorted and stuff.
    'receive_time BIGINT NOT NULL,' +
    'auth TINYINT NOT NULL,' +
    'sender VARBINARY(255) NOT NULL,' +
    'recipient VARBINARY(255) NOT NULL,' +
    // TODO(davidben): Don't really need to store this one...
    'realm VARBINARY(255) NOT NULL,' +
    'opcode VARBINARY(255) NOT NULL,' +
    'signature VARBINARY(255) NOT NULL,' +
    'message BLOB NOT NULL' +
    ') ENGINE=InnoDB;',

  // Note: look in git history for other scheme where adding a new
  // message inserted O(1) rows. Unfortunately, MySQL doesn't seem to
  // perform that query efficiently. This schema, on the other hand,
  // is trivial to use indices with.
  'CREATE TABLE user_messages (' +
    'user_id BIGINT NOT NULL REFERENCES users, ' +
    'message_id BIGINT NOT NULL REFERENCES messages, ' +
    'PRIMARY KEY(user_id, message_id) ' +
    ') ENGINE=InnoDB;',

  // HACK: Until we actually get users and stuff.
  'INSERT INTO users (username) VALUES ("davidben@ATHENA.MIT.EDU")',
];

var pool = mysql.createPool(conf.get('db'));

exports.initTables = function() {
  var connection = mysql.createConnection(conf.get('db'));
  return schemas.reduce(function(soFar, schema) {
    return soFar.then(function() {
      console.log(schema);
      return Q.ninvoke(connection, 'query', schema);
    });
  }, Q()).finally(function() {
    connection.end();
  });
};

function toBufferOrNull(arg) {
  if (arg === null)
    return null;
  return new Buffer(arg);
}

function Connection(connection) {
  this.connection = connection;
};

Connection.prototype.end = function() {
  this.connection.end();
  this.connection = null;
};

Connection.prototype.query = function(query, values) {
  // Q.ninvoke would do this, but it's annoying. Returns an array
  // because the callback has two result arguments.
  var deferred = Q.defer();
  var queryObj = this.connection.query(query, values, function(err, result) {
    if (err) {
      deferred.reject(err);
    } else {
      deferred.fulfill(result);
    }
  });
  console.log(queryObj.sql);
  return deferred.promise;
};

Connection.prototype.withTransaction = function(operation) {
  return this.query('START TRANSACTION').then(function() {
    return operation();
  }).then(function(ret) {
    return this.query('COMMIT').then(function() {
      return ret;
    }.bind(this));
  }.bind(this), function(err) {
    return this.query('ROLLBACK').then(function() {
      throw err;
    });
  }.bind(this));
};

Connection.prototype.addUserSubscription = function(user, klass, inst, recip) {
  var klassKey = zephyr.downcase(klass);
  var instKey = (inst === null) ? null : zephyr.downcase(inst);

  // Would be nice to use ON DUPLICATE KEY, but MySQL's UNIQUE
  // constraints and NULL values are dumb.
  return this.withTransaction(function() {
    var query = 'SELECT COUNT(*) AS count FROM subs WHERE user_id = ? AND ' +
      'class_key = ? AND recipient = ?';
    var values = [user, new Buffer(klassKey), new Buffer(recip)];
    if (instKey === null) {
      query += ' AND instance_key IS NULL';
    } else {
      query += ' AND instance_key = ?';
      values.push(new Buffer(instKey));
    }
    return this.query(query, values).then(function(result) {
      // Already subscribed.
      if (result[0].count > 0)
        return;

      var sub = {
        user_id: user,
        class: new Buffer(klass),
        instance: toBufferOrNull(inst),
        recipient: new Buffer(recip),
        class_key: new Buffer(klassKey),
        instance_key: toBufferOrNull(instKey)
      };
      return this.query('INSERT INTO subs SET ?', [sub]);
    }.bind(this));
  }.bind(this));
};

Connection.prototype.removeUserSubscription = function(user, klass, inst, recip) {
  var klassKey = zephyr.downcase(klass);
  var instKey = (inst === null) ? null : zephyr.downcase(inst);

  var query = 'DELETE FROM subs WHERE user_id = ? AND ' +
    'class_key = ? AND recipient = ?';
  var values = [user, new Buffer(klassKey), new Buffer(recip)];
  if (instKey === null) {
    query += ' AND instance_key IS NULL';
  } else {
    query += ' AND instance_key = ?';
    values.push(new Buffer(instKey));
  }
  return this.query(query, values);
};

Connection.prototype.getUserSubscriptions = function(user) {
  return this.query(
    'SELECT class, instance, recipient FROM subs WHERE user_id = ?', [user]
  ).then(function(rows) {
    return rows.map(function(row) {
      return [
        row.class.toString('utf8'),
        row.instance == null ? null : row.instance.toString('utf8'),
        row.recipient.toString('utf8')
      ];
    });
  });
};

Connection.prototype.loadActiveSubs = function() {
  return this.query(
    'SELECT DISTINCT subs.class_key, subs.instance_key, subs.recipient ' +
      'FROM subs WHERE instance_key IS NOT NULL'
  ).then(function(result) {
    // Query wildcard subscriptions separately; otherwise SQL doesn't
    // SELECT DISTINCT the NULLs right.
    return this.query(
      'SELECT DISTINCT subs.class_key, subs.recipient ' +
        'FROM subs WHERE instance_key IS NULL'
    ).then(function(wildcardResult) {
      return wildcardResult.concat(result).map(function(row) {
        return [row.class_key.toString('utf8'),
                row.instance_key ? row.instance_key.toString('utf8') : null,
                row.recipient.toString('utf8')];
      });
    });
  }.bind(this));
};

Connection.prototype.saveMessage = function(msg) {
  var klassKey = zephyr.downcase(msg.class);
  var instKey = (msg.instance === null) ? null : zephyr.downcase(msg.instance);

  // TODO(davidben): Ideally this would be done with an
  // INSERT..SELECT, but I want to return the user ids that see it, so
  // we resolve races between sub/unsub and receiving messages
  // consistently.
  return this.withTransaction(function() {
    // Get the users that see this message.
    return this.query(
      'SELECT DISTINCT user_id FROM subs WHERE class_key = ? '+
        'AND recipient = ? ' +
        'AND (instance_key IS NULL OR instance_key = ?)',
      [new Buffer(klassKey), new Buffer(msg.recipient), new Buffer(instKey)]
    ).then(function(rows) {
      var userIds = rows.map(function(row) { return row.user_id; });
      // We didn't save the message.
      if (userIds.length == 0)
        return null;

      // Insert the message into the database.
      return this.query(
        'INSERT INTO messages SET ?',
        {
          class: new Buffer(msg.class),
          instance: new Buffer(msg.instance),
          class_key: new Buffer(klassKey),
          instance_key: new Buffer(instKey),
          time: msg.time,
          receive_time: msg.receiveTime,
          auth: msg.auth,
          sender: new Buffer(msg.sender),
          recipient: new Buffer(msg.recipient),
          realm: new Buffer(msg.realm),
          opcode: new Buffer(msg.opcode),
          signature: new Buffer(msg.signature),
          message: new Buffer(msg.message),
        }
      ).then(function(result) {
        var id = result.insertId;
        var query = 'INSERT INTO user_messages (user_id, message_id) VALUES';
        var values = [];
        // Bleegh.
        for (var i = 0; i < userIds.length; i++) {
          if (i > 0)
            query += ',';
          query += ' (?, ?)';
          values.push(userIds[i], result.insertId);
        }
        return this.query(query, values).then(function() {
          return {
            id: id,
            userIds: userIds,
          };
        });
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

// TODO: Might want to support other queries? Yeah, I dunno.
Connection.prototype.getMessages = function(user, msgId, opts) {
  // For sanity, pick a random limit.
  var limit = Math.min(opts.limit|0, MAX_MESSAGES_RETURNED);
  var compare = (opts.reverse ? '<' : '>') +
    (opts.inclusive ? '=' : '');
  var sortOrder = opts.reverse ? 'DESC' : 'ASC';

  // Build a query.
  var queryStr =
    'SELECT messages.* ' +
    'FROM user_messages JOIN messages ON ' +
      'user_messages.message_id = messages.id ' +
    'WHERE user_messages.user_id = ? ';
  var values = [user];
  if (msgId != null) {
    queryStr +=
      'AND user_messages.message_id ' + compare + ' ? ';
    values.push(msgId);
  }
  queryStr +=
    'ORDER BY user_messages.message_id ' + sortOrder + ' ' +
    'LIMIT ?';
  values.push(limit);

  return this.query(queryStr, values).then(function(rows) {
    var messages = rows.map(function(row) {
      return {
        id: row.id,
        time: row.time,
        receiveTime: row.receive_time,
        class: row.class.toString('utf8'),
        instance: row.instance.toString('utf8'),
        sender: row.sender.toString('utf8'),
        recipient: row.recipient.toString('utf8'),
        realm: row.realm.toString('utf8'),
        auth: row.auth,
        opcode: row.opcode.toString('utf8'),
        signature: row.signature.toString('utf8'),
        message: row.message.toString('utf8')
      };
    });
    return {
      messages: messages,
      isDone: messages.length < limit
    };
  });
};

var getPoolConnection = function() {
  return Q.ninvoke(pool, 'getConnection').then(function(conn) {
    return new Connection(conn);
  });
};

exports.createConnection = function() {
  return new Connection(mysql.createConnection(conf.get('db')));
};

exports.addUserSubscription = function(user, klass, inst, recip) {
  return getPoolConnection().then(function(conn) {
    return conn.addUserSubscription(user, klass, inst, recip).finally(
      conn.end.bind(conn));
  });
};

exports.removeUserSubscription = function(user, klass, inst, recip) {
  return getPoolConnection().then(function(conn) {
    return conn.removeUserSubscription(user, klass, inst, recip).finally(
      conn.end.bind(conn));
  });
};

exports.getUserSubscriptions = function(user) {
  return getPoolConnection().then(function(conn) {
    return conn.getUserSubscriptions(user).finally(conn.end.bind(conn));
  });
};

exports.loadActiveSubs = function() {
  return getPoolConnection().then(function(conn) {
    return conn.loadActiveSubs().finally(conn.end.bind(conn));
  });
};

exports.saveMessage = function(msg) {
  return getPoolConnection().then(function(conn) {
    return conn.saveMessage(msg).finally(conn.end.bind(conn));
  });
};

exports.getMessages = function(user, msgId, opts) {
  return getPoolConnection().then(function(conn) {
    return conn.getMessages(user, msgId, opts).finally(conn.end.bind(conn));
  });
};
<commit_msg>Rename the username column to principal<commit_after>var Buffer = require('buffer').Buffer;
var mysql = require('mysql');
var Q = require('q');
var zephyr = require('zephyr');

var conf = require('./config.js');

var MAX_MESSAGES_RETURNED = 100;

// Per vasilvv, use VARBINARY everywhere instead of VARCHAR. MySQL
// collations are sad.

// TODO: We probably can get away with significantly less isolated
// transactions here if it becomes a problem.

var schemas = [
  'CREATE TABLE users (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    'principal VARBINARY(255) UNIQUE NOT NULL' +
    ') ENGINE=InnoDB;',

  'CREATE TABLE subs (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    'user_id BIGINT NOT NULL,' +
    'FOREIGN KEY user_fkey (user_id) REFERENCES users(id),' +
    // Preserve the original versions of the strings for display.
    'class VARBINARY(255) NOT NULL,' +
    'instance VARBINARY(255) NULL,' +
    'recipient VARBINARY(255) NULL,' +
    // Downcased versions for querying.
    'class_key VARBINARY(255) NOT NULL,' +
    'instance_key VARBINARY(255) NULL,' +
    'UNIQUE query_triple (user_id, class_key, instance_key, recipient)' +
    ') ENGINE=InnoDB;',

  'CREATE TABLE messages (' +
    'id BIGINT AUTO_INCREMENT PRIMARY KEY,' +
    // Message fields. Store both downcased and display versions of
    // class and instance.
    'class VARBINARY(255) NOT NULL,' +
    'instance VARBINARY(255) NOT NULL,' +
    'class_key VARBINARY(255) NOT NULL,' +
    'instance_key VARBINARY(255) NOT NULL,' +
    // Stores the number of milliseconds past the Unix epoch,
    // i.e. what Date.prototype.getTime spits out. Technically zephyr
    // gives you microsecond resolution, but I don't think V8 gives
    // you more than millisecond resolution anyway.
    'time BIGINT NOT NULL,' +
    // Distinguish time in the message from the time we received it;
    // if the time is bogus, we can display in the UI. Also we should
    // use our time when jumping the cursor to a date. At least it can
    // be sorted and stuff.
    'receive_time BIGINT NOT NULL,' +
    'auth TINYINT NOT NULL,' +
    'sender VARBINARY(255) NOT NULL,' +
    'recipient VARBINARY(255) NOT NULL,' +
    // TODO(davidben): Don't really need to store this one...
    'realm VARBINARY(255) NOT NULL,' +
    'opcode VARBINARY(255) NOT NULL,' +
    'signature VARBINARY(255) NOT NULL,' +
    'message BLOB NOT NULL' +
    ') ENGINE=InnoDB;',

  // Note: look in git history for other scheme where adding a new
  // message inserted O(1) rows. Unfortunately, MySQL doesn't seem to
  // perform that query efficiently. This schema, on the other hand,
  // is trivial to use indices with.
  'CREATE TABLE user_messages (' +
    'user_id BIGINT NOT NULL REFERENCES users, ' +
    'message_id BIGINT NOT NULL REFERENCES messages, ' +
    'PRIMARY KEY(user_id, message_id) ' +
    ') ENGINE=InnoDB;',

  // HACK: Until we actually get users and stuff.
  'INSERT INTO users (principal) VALUES ("davidben@ATHENA.MIT.EDU")',
];

var pool = mysql.createPool(conf.get('db'));

exports.initTables = function() {
  var connection = mysql.createConnection(conf.get('db'));
  return schemas.reduce(function(soFar, schema) {
    return soFar.then(function() {
      console.log(schema);
      return Q.ninvoke(connection, 'query', schema);
    });
  }, Q()).finally(function() {
    connection.end();
  });
};

function toBufferOrNull(arg) {
  if (arg === null)
    return null;
  return new Buffer(arg);
}

function Connection(connection) {
  this.connection = connection;
};

Connection.prototype.end = function() {
  this.connection.end();
  this.connection = null;
};

Connection.prototype.query = function(query, values) {
  // Q.ninvoke would do this, but it's annoying. Returns an array
  // because the callback has two result arguments.
  var deferred = Q.defer();
  var queryObj = this.connection.query(query, values, function(err, result) {
    if (err) {
      deferred.reject(err);
    } else {
      deferred.fulfill(result);
    }
  });
  console.log(queryObj.sql);
  return deferred.promise;
};

Connection.prototype.withTransaction = function(operation) {
  return this.query('START TRANSACTION').then(function() {
    return operation();
  }).then(function(ret) {
    return this.query('COMMIT').then(function() {
      return ret;
    }.bind(this));
  }.bind(this), function(err) {
    return this.query('ROLLBACK').then(function() {
      throw err;
    });
  }.bind(this));
};

Connection.prototype.addUserSubscription = function(user, klass, inst, recip) {
  var klassKey = zephyr.downcase(klass);
  var instKey = (inst === null) ? null : zephyr.downcase(inst);

  // Would be nice to use ON DUPLICATE KEY, but MySQL's UNIQUE
  // constraints and NULL values are dumb.
  return this.withTransaction(function() {
    var query = 'SELECT COUNT(*) AS count FROM subs WHERE user_id = ? AND ' +
      'class_key = ? AND recipient = ?';
    var values = [user, new Buffer(klassKey), new Buffer(recip)];
    if (instKey === null) {
      query += ' AND instance_key IS NULL';
    } else {
      query += ' AND instance_key = ?';
      values.push(new Buffer(instKey));
    }
    return this.query(query, values).then(function(result) {
      // Already subscribed.
      if (result[0].count > 0)
        return;

      var sub = {
        user_id: user,
        class: new Buffer(klass),
        instance: toBufferOrNull(inst),
        recipient: new Buffer(recip),
        class_key: new Buffer(klassKey),
        instance_key: toBufferOrNull(instKey)
      };
      return this.query('INSERT INTO subs SET ?', [sub]);
    }.bind(this));
  }.bind(this));
};

Connection.prototype.removeUserSubscription = function(user, klass, inst, recip) {
  var klassKey = zephyr.downcase(klass);
  var instKey = (inst === null) ? null : zephyr.downcase(inst);

  var query = 'DELETE FROM subs WHERE user_id = ? AND ' +
    'class_key = ? AND recipient = ?';
  var values = [user, new Buffer(klassKey), new Buffer(recip)];
  if (instKey === null) {
    query += ' AND instance_key IS NULL';
  } else {
    query += ' AND instance_key = ?';
    values.push(new Buffer(instKey));
  }
  return this.query(query, values);
};

Connection.prototype.getUserSubscriptions = function(user) {
  return this.query(
    'SELECT class, instance, recipient FROM subs WHERE user_id = ?', [user]
  ).then(function(rows) {
    return rows.map(function(row) {
      return [
        row.class.toString('utf8'),
        row.instance == null ? null : row.instance.toString('utf8'),
        row.recipient.toString('utf8')
      ];
    });
  });
};

Connection.prototype.loadActiveSubs = function() {
  return this.query(
    'SELECT DISTINCT subs.class_key, subs.instance_key, subs.recipient ' +
      'FROM subs WHERE instance_key IS NOT NULL'
  ).then(function(result) {
    // Query wildcard subscriptions separately; otherwise SQL doesn't
    // SELECT DISTINCT the NULLs right.
    return this.query(
      'SELECT DISTINCT subs.class_key, subs.recipient ' +
        'FROM subs WHERE instance_key IS NULL'
    ).then(function(wildcardResult) {
      return wildcardResult.concat(result).map(function(row) {
        return [row.class_key.toString('utf8'),
                row.instance_key ? row.instance_key.toString('utf8') : null,
                row.recipient.toString('utf8')];
      });
    });
  }.bind(this));
};

Connection.prototype.saveMessage = function(msg) {
  var klassKey = zephyr.downcase(msg.class);
  var instKey = (msg.instance === null) ? null : zephyr.downcase(msg.instance);

  // TODO(davidben): Ideally this would be done with an
  // INSERT..SELECT, but I want to return the user ids that see it, so
  // we resolve races between sub/unsub and receiving messages
  // consistently.
  return this.withTransaction(function() {
    // Get the users that see this message.
    return this.query(
      'SELECT DISTINCT user_id FROM subs WHERE class_key = ? '+
        'AND recipient = ? ' +
        'AND (instance_key IS NULL OR instance_key = ?)',
      [new Buffer(klassKey), new Buffer(msg.recipient), new Buffer(instKey)]
    ).then(function(rows) {
      var userIds = rows.map(function(row) { return row.user_id; });
      // We didn't save the message.
      if (userIds.length == 0)
        return null;

      // Insert the message into the database.
      return this.query(
        'INSERT INTO messages SET ?',
        {
          class: new Buffer(msg.class),
          instance: new Buffer(msg.instance),
          class_key: new Buffer(klassKey),
          instance_key: new Buffer(instKey),
          time: msg.time,
          receive_time: msg.receiveTime,
          auth: msg.auth,
          sender: new Buffer(msg.sender),
          recipient: new Buffer(msg.recipient),
          realm: new Buffer(msg.realm),
          opcode: new Buffer(msg.opcode),
          signature: new Buffer(msg.signature),
          message: new Buffer(msg.message),
        }
      ).then(function(result) {
        var id = result.insertId;
        var query = 'INSERT INTO user_messages (user_id, message_id) VALUES';
        var values = [];
        // Bleegh.
        for (var i = 0; i < userIds.length; i++) {
          if (i > 0)
            query += ',';
          query += ' (?, ?)';
          values.push(userIds[i], result.insertId);
        }
        return this.query(query, values).then(function() {
          return {
            id: id,
            userIds: userIds,
          };
        });
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

// TODO: Might want to support other queries? Yeah, I dunno.
Connection.prototype.getMessages = function(user, msgId, opts) {
  // For sanity, pick a random limit.
  var limit = Math.min(opts.limit|0, MAX_MESSAGES_RETURNED);
  var compare = (opts.reverse ? '<' : '>') +
    (opts.inclusive ? '=' : '');
  var sortOrder = opts.reverse ? 'DESC' : 'ASC';

  // Build a query.
  var queryStr =
    'SELECT messages.* ' +
    'FROM user_messages JOIN messages ON ' +
      'user_messages.message_id = messages.id ' +
    'WHERE user_messages.user_id = ? ';
  var values = [user];
  if (msgId != null) {
    queryStr +=
      'AND user_messages.message_id ' + compare + ' ? ';
    values.push(msgId);
  }
  queryStr +=
    'ORDER BY user_messages.message_id ' + sortOrder + ' ' +
    'LIMIT ?';
  values.push(limit);

  return this.query(queryStr, values).then(function(rows) {
    var messages = rows.map(function(row) {
      return {
        id: row.id,
        time: row.time,
        receiveTime: row.receive_time,
        class: row.class.toString('utf8'),
        instance: row.instance.toString('utf8'),
        sender: row.sender.toString('utf8'),
        recipient: row.recipient.toString('utf8'),
        realm: row.realm.toString('utf8'),
        auth: row.auth,
        opcode: row.opcode.toString('utf8'),
        signature: row.signature.toString('utf8'),
        message: row.message.toString('utf8')
      };
    });
    return {
      messages: messages,
      isDone: messages.length < limit
    };
  });
};

var getPoolConnection = function() {
  return Q.ninvoke(pool, 'getConnection').then(function(conn) {
    return new Connection(conn);
  });
};

exports.createConnection = function() {
  return new Connection(mysql.createConnection(conf.get('db')));
};

exports.addUserSubscription = function(user, klass, inst, recip) {
  return getPoolConnection().then(function(conn) {
    return conn.addUserSubscription(user, klass, inst, recip).finally(
      conn.end.bind(conn));
  });
};

exports.removeUserSubscription = function(user, klass, inst, recip) {
  return getPoolConnection().then(function(conn) {
    return conn.removeUserSubscription(user, klass, inst, recip).finally(
      conn.end.bind(conn));
  });
};

exports.getUserSubscriptions = function(user) {
  return getPoolConnection().then(function(conn) {
    return conn.getUserSubscriptions(user).finally(conn.end.bind(conn));
  });
};

exports.loadActiveSubs = function() {
  return getPoolConnection().then(function(conn) {
    return conn.loadActiveSubs().finally(conn.end.bind(conn));
  });
};

exports.saveMessage = function(msg) {
  return getPoolConnection().then(function(conn) {
    return conn.saveMessage(msg).finally(conn.end.bind(conn));
  });
};

exports.getMessages = function(user, msgId, opts) {
  return getPoolConnection().then(function(conn) {
    return conn.getMessages(user, msgId, opts).finally(conn.end.bind(conn));
  });
};
<|endoftext|>


<commit_before>var mongo = require("mongodb");
var async = require("async");

var db = null;

/*
 * Test and create db catalogs if not exists based on config object.
 */
exports.prepare = function( db_config, end_handler ){

	checkdb();

	var catalogs = db_config.system_catalogs.concat(db_config.user_catalogs);

	async.forEach(catalogs,function(catalog_name,next){

		db.collection( catalog_name, function(err, collection){

			if( !collection ){

				db.createCollection( catalog_name, function(err, collection){

					if(err)
						throw err;
					else
						next();
				});
			}else
				next();
		});

	},function(err){

		end_handler(err, catalogs);
	});

}

/*
 * Close connection to db
 */
exports.close = function(){

	db && db.close();
	db = null;
}

/*
 * Connect to db before any other db operation.
 */
exports.connect = function( params, ret_handler ){

	if( db ){

		db.close();
	}

	params = params || {};
	params.dbhost = params.dbhost || "localhost";
	params.dbport = params.dbport || 27017;
	params.dbname = params.dbname || "jsa";

	db = new mongo.Db(params.dbname, new mongo.Server(params.dbhost,params.dbport,{auto_reconnect:true, socketOptions:{timeout:5000}}),{strict:true}); //,poolSize:5,socketOptions:{timeout:1}

	db.open( function(err, client){

		if(client){
			if( params.dbuser !== undefined && params.dbpass !== undefined ){

				db.authenticate(params.dbuser,params.dbpass,function(err,client){

					if(client){
						console.log('**MongoDb driver auth-connected');
						if(ret_handler)
							ret_handler(null,1);
					}else{

						db = null;
						if(ret_handler)
							ret_handler(err,0);
					}
				});
			}else{
				if(ret_handler)
					ret_handler(null,1);
			}
		}else{

			db = null;
			if(ret_handler)
				ret_handler(err,0);
		}

	});

}

/*
 * Check db availability before any db operation.
 */
function checkdb(){

	if( !db )
		throw new Error("Driver not connected, should call connect first.");
}

/*
 * Save object to db.
 *
 */
var save = exports.save = function( col_name, obj, ret_handler ){

    checkdb();

	db.collection(col_name, function(err, collection){

	    collection.save( obj, {safe:true}, function(err, val){

			if( !err )
			    	ret_handler( null, val);
			else
				ret_handler(err,null);
	    });
	});

}

/*
 * Retrieve object from db by ID.
 *
 */
var select = exports.select = function( col_name, id_str, ret_handler ){

    checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    var ObjectID = require('mongodb').ObjectID;
	    collection.findOne({_id:new ObjectID(id_str)},function(err, object){

			if( !err )
			    ret_handler( null,object );
			else
				ret_handler(err,null);
	    });
	});

}

/*
 * Funcion para seleccionar documentos de la bbdd.
 */
var criteria = exports.criteria = function( col_name, criteria, order_field, ret_handler ){

  	checkdb();

	db.collection( col_name, function(err, collection){

    	collection.find(criteria,{}).sort(order_field).toArray(function(err, objects){

			if(!err)
				ret_handler(null,objects);
			else
				ret_handler( err, null );
		});

	});
}

/*
 * Remove object from db by ID.
 */
var removeById = exports.removeById = function( col_name, id_str, ret_handler){

	checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    var ObjectID = require('mongodb').ObjectID;
	    collection.remove( {_id:new ObjectID(id_str)} ,{safe:true},function(err, n){

			if( ret_handler )
				if( !err )
				    ret_handler( null, n );
			    else
			    	ret_handler(err,null);
	    });
	});

}

/*
 * Remove object from db by criteria.
 */
var remove = exports.remove = function( col_name, criteria, ret_handler){

	checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    collection.remove( criteria ,{safe:true},function(err, n){//n: number of rows/docs removed

			if( ret_handler )
				if( !err )
				    ret_handler( null, n );
			    else
			    	ret_handler(err,null);
	    });
	});

}

/*
 * Remove expired document from db, its events and timer.
 */
var remove_global = exports.remove_global = function( col_name, id_str, ret_handler ){

	async.parallel([
		function( end_handler ){//remove doc by id

			removeById(col_name, id_str, function(err){

				end_handler(err);
			});
		},
		function( end_handler ){//remove  events related to doc id

			remove("events",{"ev_msg.ev_data.wid":id_str}, function(err,n){

				end_handler(err);
			});
		}
	], function(err,results){

		if(err)
			throw err;
		else
			ret_handler(null,1);
	});

}

<commit_msg>safe:true init parameter when creating db to avoid annoying driver mssage.<commit_after>var mongo = require("mongodb");
var async = require("async");

var db = null;

/*
 * Test and create db catalogs if not exists based on config object.
 */
exports.prepare = function( db_config, end_handler ){

	checkdb();

	var catalogs = db_config.system_catalogs.concat(db_config.user_catalogs);

	async.forEach(catalogs,function(catalog_name,next){

		db.collection( catalog_name, function(err, collection){

			if( !collection ){

				db.createCollection( catalog_name, function(err, collection){

					if(err)
						throw err;
					else
						next();
				});
			}else
				next();
		});

	},function(err){

		end_handler(err, catalogs);
	});

}

/*
 * Close connection to db
 */
exports.close = function(){

	db && db.close();
	db = null;
}

/*
 * Connect to db before any other db operation.
 */
exports.connect = function( params, ret_handler ){

	if( db ){

		db.close();
	}

	params = params || {};
	params.dbhost = params.dbhost || "localhost";
	params.dbport = params.dbport || 27017;
	params.dbname = params.dbname || "jsa";

	db = new mongo.Db(params.dbname, new mongo.Server(params.dbhost,params.dbport,{auto_reconnect:true, socketOptions:{timeout:5000}}),{strict:true, safe:true}); //,poolSize:5,socketOptions:{timeout:1}

	db.open( function(err, client){

		if(client){
			if( params.dbuser !== undefined && params.dbpass !== undefined ){

				db.authenticate(params.dbuser,params.dbpass,function(err,client){

					if(client){
						console.log('**MongoDb driver auth-connected');
						if(ret_handler)
							ret_handler(null,1);
					}else{

						db = null;
						if(ret_handler)
							ret_handler(err,0);
					}
				});
			}else{
				if(ret_handler)
					ret_handler(null,1);
			}
		}else{

			db = null;
			if(ret_handler)
				ret_handler(err,0);
		}

	});

}

/*
 * Check db availability before any db operation.
 */
function checkdb(){

	if( !db )
		throw new Error("Driver not connected, should call connect first.");
}

/*
 * Save object to db.
 *
 */
var save = exports.save = function( col_name, obj, ret_handler ){

    checkdb();

	db.collection(col_name, function(err, collection){

	    collection.save( obj, {safe:true}, function(err, val){

			if( !err )
			    	ret_handler( null, val);
			else
				ret_handler(err,null);
	    });
	});

}

/*
 * Retrieve object from db by ID.
 *
 */
var select = exports.select = function( col_name, id_str, ret_handler ){

    checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    var ObjectID = require('mongodb').ObjectID;
	    collection.findOne({_id:new ObjectID(id_str)},function(err, object){

			if( !err )
			    ret_handler( null,object );
			else
				ret_handler(err,null);
	    });
	});

}

/*
 * Funcion para seleccionar documentos de la bbdd.
 */
var criteria = exports.criteria = function( col_name, criteria, order_field, ret_handler ){

  	checkdb();

	db.collection( col_name, function(err, collection){

    	collection.find(criteria,{}).sort(order_field).toArray(function(err, objects){

			if(!err)
				ret_handler(null,objects);
			else
				ret_handler( err, null );
		});

	});
}

/*
 * Remove object from db by ID.
 */
var removeById = exports.removeById = function( col_name, id_str, ret_handler){

	checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    var ObjectID = require('mongodb').ObjectID;
	    collection.remove( {_id:new ObjectID(id_str)} ,{safe:true},function(err, n){

			if( ret_handler )
				if( !err )
				    ret_handler( null, n );
			    else
			    	ret_handler(err,null);
	    });
	});

}

/*
 * Remove object from db by criteria.
 */
var remove = exports.remove = function( col_name, criteria, ret_handler){

	checkdb();

	db.collection( col_name, function(err, collection){

		if(err){

			ret_handler(err,null);
			return;
		}

	    collection.remove( criteria ,{safe:true},function(err, n){//n: number of rows/docs removed

			if( ret_handler )
				if( !err )
				    ret_handler( null, n );
			    else
			    	ret_handler(err,null);
	    });
	});

}

/*
 * Remove expired document from db, its events and timer.
 */
var remove_global = exports.remove_global = function( col_name, id_str, ret_handler ){

	async.parallel([
		function( end_handler ){//remove doc by id

			removeById(col_name, id_str, function(err){

				end_handler(err);
			});
		},
		function( end_handler ){//remove  events related to doc id

			remove("events",{"ev_msg.ev_data.wid":id_str}, function(err,n){

				end_handler(err);
			});
		}
	], function(err,results){

		if(err)
			throw err;
		else
			ret_handler(null,1);
	});

}

<|endoftext|>


<commit_before>
var mmm = require("multi-master-merge");
var level = require("level"); // LevelUP + LevelDOWN
var path = require("path");
var url = require("url");
var mkdirp = require("mkdirp");
var sublevel = require("level-sublevel");
var net = require("net");
var DHT = require("bittorrent-dht");
var SSDP = require("node-ssdp");
var ip = require("ip");

// hole punch for our replication interface
var entry = require("entry");
var natPmp = require("nat-pmp");

var log = require("./log");

var dht = new DHT();
var ssdp = new SSDP.Client();

var dbPath = path.join(module.parent.dbPath);
mkdirp.sync(dbPath);
var db = mmm(level(dbPath), { encoding: "json" });

var server;
db.listenReplications = function(id) {
	server = net.createServer(function(c) {
		log.important("DB replication connection established from "+c.remoteAddress+":"+c.remotePort);
		c.pipe(db.sync()).pipe(c);
	});
	server.listen(function() {
		var port = server.address().port;

		log.important("DB replication server listening at "+port);
		dht.announce(id, port);

		// We can play around with public / private - maybe we want a consistent public port?
		try {
			// Both throw err in their async code which we cannot catch
			entry.map({ external: port, internal: port, name: require("../package").name }, function(e) { e && console.error(e) });
			//natPmp.connect("10.0.1.1").portMapping({ public: port, private: port, ttl: 1000, description: require("../package").name }, function(e) { e && console.error(e) });
		} catch(e) { console.error(e) }
	});

	// Announce as an SSDP server
	var ssdpServer = new SSDP.Server({ location: ip.address() +":" + server.address().port });
	ssdpServer.addUSN("upnp:rootdevice");
	ssdpServer.addUSN("urn:schemas-upnp-org:service:MultiPassTorrent:"+id);
	ssdpServer.start("0.0.0.0");
};

db.findReplications = function(id) {
	log.important(id+": finding other instances to replicate with");

	// WARNING: what if it emits beforehand?
	dht.on("ready", function() {
		dht.lookup(id);
	});

	ssdp.search("urn:schemas-upnp-org:service:MultiPassTorrent:"+id);
};

var peers = { };
function onpeer(addr)
{
	if (ip.address()+":"+server.address().port == addr) return; // Do not connect to ourselves

	if (peers[addr]) return;
	peers[addr] = true;

	var spl = addr.split(":");
	var c = net.connect(spl[1], spl[0]);
	c.on("connect", function() {
		log.message("connected to peer "+addr); // TODO: handle errs
		c.pipe(db.sync()).pipe(c);
	}).on("error", function(e) {
		c.destroy()
	}).on("end", function() {
		c.destroy();
		delete peers[addr];
	}).on("close", function() {
		// cleanup sync pipe?
	});
};
dht.on("peer", onpeer);
ssdp.on("response", function(meta, status, matchine) { onpeer(meta.LOCATION) });

// TODO: hole punch

module.exports = db;<commit_msg>DB: enable mmm garbage collection<commit_after>
var mmm = require("multi-master-merge");
var level = require("level"); // LevelUP + LevelDOWN
var path = require("path");
var url = require("url");
var mkdirp = require("mkdirp");
var sublevel = require("level-sublevel");
var net = require("net");
var DHT = require("bittorrent-dht");
var SSDP = require("node-ssdp");
var ip = require("ip");

// hole punch for our replication interface
var entry = require("entry");
var natPmp = require("nat-pmp");

var log = require("./log");

var dht = new DHT();
var ssdp = new SSDP.Client();

var dbPath = path.join(module.parent.dbPath);
mkdirp.sync(dbPath);
var db = mmm(level(dbPath), { encoding: "json", gc: true });

var server;
db.listenReplications = function(id) {
	server = net.createServer(function(c) {
		log.important("DB replication connection established from "+c.remoteAddress+":"+c.remotePort);
		c.pipe(db.sync()).pipe(c);
	});
	server.listen(function() {
		var port = server.address().port;

		log.important("DB replication server listening at "+port);
		dht.announce(id, port);

		// We can play around with public / private - maybe we want a consistent public port?
		try {
			// Both throw err in their async code which we cannot catch
			entry.map({ external: port, internal: port, name: require("../package").name }, function(e) { e && console.error(e) });
			//natPmp.connect("10.0.1.1").portMapping({ public: port, private: port, ttl: 1000, description: require("../package").name }, function(e) { e && console.error(e) });
		} catch(e) { console.error(e) }
	});

	// Announce as an SSDP server
	var ssdpServer = new SSDP.Server({ location: ip.address() +":" + server.address().port });
	ssdpServer.addUSN("upnp:rootdevice");
	ssdpServer.addUSN("urn:schemas-upnp-org:service:MultiPassTorrent:"+id);
	ssdpServer.start("0.0.0.0");
};

db.findReplications = function(id) {
	log.important(id+": finding other instances to replicate with");

	// WARNING: what if it emits beforehand?
	dht.on("ready", function() {
		dht.lookup(id);
	});

	ssdp.search("urn:schemas-upnp-org:service:MultiPassTorrent:"+id);
};

var peers = { };
function onpeer(addr)
{
	if (ip.address()+":"+server.address().port == addr) return; // Do not connect to ourselves

	if (peers[addr]) return;
	peers[addr] = true;

	var spl = addr.split(":");
	var c = net.connect(spl[1], spl[0]);
	c.on("connect", function() {
		log.message("connected to peer "+addr); // TODO: handle errs
		c.pipe(db.sync()).pipe(c);
	}).on("error", function(e) {
		c.destroy()
	}).on("end", function() {
		c.destroy();
		delete peers[addr];
	}).on("close", function() {
		// cleanup sync pipe?
	});
};
dht.on("peer", onpeer);
ssdp.on("response", function(meta, status, matchine) { onpeer(meta.LOCATION) });

// TODO: hole punch

module.exports = db;<|endoftext|>