addVisit = async function (profileId, {url, title}) {
  // validate parameters
  if (!url || typeof url !== 'string') {
    throw new BadParamError('url must be a string')
  }
  if (!title || typeof title !== 'string') {
    throw new BadParamError('title must be a string')
  }

  var release = await lock('history-db')
  try {
    await db.run('BEGIN TRANSACTION;')

    var ts = Date.now()
    if (!url.startsWith('beaker://')) { // dont log stats on internal sites, keep them out of the search
      // get current stats
      var stats = await db.get('SELECT * FROM visit_stats WHERE url = ?;', [url])

      // create or update stats
      if (!stats) {
        await db.run('INSERT INTO visit_stats (url, num_visits, last_visit_ts) VALUES (?, ?, ?);', [url, 1, ts])
        await db.run('INSERT INTO visit_fts (url, title) VALUES (?, ?);', [url, title])
      } else {
        let num_visits = (+stats.num_visits || 1) + 1
        await db.run('UPDATE visit_stats SET num_visits = ?, last_visit_ts = ? WHERE url = ?;', [num_visits, ts, url])
      }
    }

    // visited within 1 hour?
    var visit = await db.get('SELECT rowid, * from visits WHERE profileId = ? AND url = ? AND ts > ? ORDER BY ts DESC LIMIT 1', [profileId, url, ts - 1000 * 60 * 60])
    if (visit) {
      // update visit ts and title
      await db.run('UPDATE visits SET ts = ?, title = ? WHERE rowid = ?', [ts, title, visit.rowid])
    } else {
      // log visit
      await db.run('INSERT INTO visits (profileId, url, title, tabClose, ts) VALUES (?, ?, ?, ?, ?);', [profileId, url, title, 0, ts])
    }

    await db.run('COMMIT;')
  } catch (e) {
    await db.run('ROLLBACK;')
    throw e
  } finally {
    release()
  }
}
@param {number} profileId
@param {Object} values
@param {string} values.url
@param {string} values.title
@returns {Promise<void>}