/*global assert*/
class CancellationError extends Error {}
CancellationError.prototype.name = 'CancellationError'

function deferred () {
  let _resolve, _reject
  const promise = new Promise((resolve, reject) => {
    _resolve = resolve
    _reject = reject
  })
  return {promise, resolve: _resolve, reject: _reject}
}

function cancellable (fn) {
  let cancel
  let cancelled = false
  let d = deferred()
  const cancellation = new Promise((resolve, reject) => {
    d.promise.then(() => resolve(), () => resolve())
    cancel = (reason) => {
      cancelled = true
      if (reason) assert.equal(typeof reason, 'string')
      reject(new CancellationError(reason || 'cancelled'))
    }
  })
  fn(cancellation, () => cancelled).then(d.resolve, d.reject)
  return Object.assign(d.promise, {cancel})
}

function somethingAsync () {
  return cancellable((cancellation, cancelled) => new Promise((resolve, reject) => {
    const start = Date.now()
    let timeout
    cancellation.catch((err) => {
      clearTimeout(timeout)
      console.log(`Cancelled after ${Date.now() - start}ms`)
      reject(err)
    })
    timeout = setTimeout(() => {
      console.log(`Resolving after ${Date.now() - start}ms`)
      resolve('two seconds have passed')
    }, 2000)
  }))
}

let p = somethingAsync()
p.then(console.log, console.error)
p.cancel()